spring-boot之webSocket · 上

前言
昨天我們已經(jīng)分享完了security的相關(guān)知識(shí)點(diǎn),所以從今天開始我們要開始學(xué)習(xí)spring-boot另一個(gè)組件——webSocket。
websocket也算是spring-boot的一個(gè)核心組件,目前我能想到的應(yīng)用場(chǎng)景就是群聊,所以我們今天的內(nèi)容核心就是搭建一個(gè)簡(jiǎn)易版的網(wǎng)絡(luò)聊天室。
webSocket
websocket是什么
在開始正文之前,我們先看下什么是webSocket,下面是我在一本springboot書籍上找到的解釋:
WebSocket協(xié)議是基于TCP的一種新的網(wǎng)絡(luò)協(xié)議 。它實(shí)現(xiàn)了瀏覽器與服務(wù)器全雙工(full-duplex)通信一一允許服務(wù)器主動(dòng)發(fā)送信息給客戶端,這樣就可以實(shí)現(xiàn)從客戶端發(fā)送消息到服務(wù)器 ,而服務(wù)器又可以轉(zhuǎn)發(fā)消息到客戶端,這樣就能夠?qū)崿F(xiàn)客戶端之間的交互。對(duì)于WebSocket的 開發(fā) ,Spring也提供了 良好 的支持 。目前很多瀏覽器己經(jīng)實(shí)現(xiàn)了Web Socket協(xié)議 ,但是依舊存在著很多瀏覽器沒有實(shí)現(xiàn)該協(xié)議,為了 兼容那 些沒有實(shí)現(xiàn)該協(xié)議的瀏覽器 , 往往還需要通過 STOMP 協(xié)議來完成這些兼容。
簡(jiǎn)單來說,webSocket就是一種新的網(wǎng)絡(luò)協(xié)議,在這種協(xié)議的加持下,運(yùn)行服務(wù)端給客戶端直接發(fā)送消息,而且服務(wù)器也可以把消息轉(zhuǎn)發(fā)給客戶端。
在以前的網(wǎng)絡(luò)協(xié)議中,服務(wù)端只能被動(dòng)接受客戶端的請(qǐng)求,然后才能給客戶端發(fā)送數(shù)據(jù),但是有了webSocket協(xié)議,我們就可以實(shí)現(xiàn)類似于打電話這樣的雙工通信,確實(shí)方便了很多。


簡(jiǎn)易聊天室
下面我們通過webSocket來搭建一個(gè)簡(jiǎn)易的網(wǎng)絡(luò)聊天室。
項(xiàng)目依賴
首先創(chuàng)建一個(gè)spring-boot項(xiàng)目,然后引入websocket的依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
同時(shí)我還加入了security、thymeleaf等附屬依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
這兩個(gè)依賴就不過多說明了,security昨天才分享完,還是熱乎的。
websocket配置類
websocket的配置比較簡(jiǎn)單,主要就是創(chuàng)建一個(gè)服務(wù)端實(shí)例,就相當(dāng)于往容器中注入了一個(gè)ServerEndpointExporter實(shí)例對(duì)象。
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
websokcet服務(wù)實(shí)現(xiàn)
這里就是websocket服務(wù)的關(guān)鍵,也就是服務(wù)提供者。
@ServerEndpoint("/ws")
@Service
public class WebSocketService {
private final Logger logger = LoggerFactory.getLogger(WebSocketService.class);
private Map<String, String> nameMap = Maps.newHashMap();
{
nameMap.put("nezha", "哪吒");
nameMap.put("pangu", "盤古");
nameMap.put("zhongkui", "鐘馗");
nameMap.put("fuxi", "伏羲");
nameMap.put("shennongshi", "神農(nóng)氏");
nameMap.put("kuafu", "夸父");
nameMap.put("nvwa", "女媧");
nameMap.put("jiangziya", "姜子牙");
nameMap.put("jingwei", "精衛(wèi)");
}
// 在線數(shù)量
private static AtomicInteger onlineCount = new AtomicInteger(0);
// 保存已建立連接的客戶端(在線)
private static CopyOnWriteArraySet<WebSocketService> webSocketServiceSet = Sets.newCopyOnWriteArraySet();
private Session session;
public Session getSession() {
return session;
}
public void setSession(Session session) {
this.session = session;
}
@OnOpen
public void onOpen(Session session) {
String name = nameMap.get(session.getUserPrincipal().getName());
this.session = session;
webSocketServiceSet.add(this);
addOnlineCount();
logger.info("有新連接加入!當(dāng)前在線人數(shù)為: {}", onlineCount.get());
webSocketServiceSet.parallelStream().forEach(item -> {
try {
sendMessage(item.getSession(), String.format("%s加入群聊!", name));
} catch (Exception e) {
logger.error("發(fā)送消息異常:", e);
}
});
}
@OnMessage
public void onMessage(String message, Session session) {
logger.info("來自客戶端的消息:{}", message);
webSocketServiceSet.parallelStream().forEach(item -> {
String name = nameMap.get(session.getUserPrincipal().getName());
logger.info("{}發(fā)送了一條消息:{}", name, message);
try {
item.sendMessage(item.getSession(), String.format("%s:%s", name, message));
} catch (IOException e) {
e.printStackTrace();
}
});
}
@OnClose
public void onClose() {
webSocketServiceSet.remove(this);
subOnlineCount();
}
@OnError
public void onError(Session session, Throwable t) {
logger.error("發(fā)生錯(cuò)誤:", t);
}
/**
* 在線人數(shù)加一
*/
private void addOnlineCount() {
onlineCount.incrementAndGet();
}
/**
* 在線人數(shù)減一
*/
private void subOnlineCount() {
onlineCount.decrementAndGet();
}
private void sendMessage(Session session, String message) throws IOException {
session.getBasicRemote().sendText(message);
}
}
@ServerEndpoint注解制定了我們服務(wù)的節(jié)點(diǎn)路徑,這樣也確定了我們wesocket服務(wù)的訪問地址:
ws://localhost:8080/ws
地址中的ws表示協(xié)議類別,也就是websocket的縮寫,緊跟著的是我們springboot服務(wù)的地址(主機(jī)、端口等),然后就是我們的websocket的節(jié)點(diǎn)地址。
@service注解也就是我們最常用的服務(wù)注解,就是把他標(biāo)記成springboot可以管理的組件,沒有這個(gè)注解,websocket是訪問不到的:

緊接著,我們寫了四個(gè)監(jiān)聽方法,方法上都有對(duì)應(yīng)的注解標(biāo)注:
OnOpen:客戶端首次連接服務(wù)端時(shí)會(huì)調(diào)用該方法OnMessage:客戶端發(fā)送消息時(shí)會(huì)調(diào)用該方法OnClose:客戶端斷開連接時(shí),會(huì)調(diào)用該方法OnError:發(fā)生錯(cuò)誤時(shí)會(huì)調(diào)用該方法
用戶登錄配置
為了更好的演示,我加入security組件,這樣用戶登錄之后,session中就保留了用戶的用戶信息,方便前端對(duì)數(shù)據(jù)進(jìn)行展示:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("zhongkui").password(passwordEncoder.encode("123456")).roles("user")
.and().withUser("fuxi").password(passwordEncoder.encode("123456")).roles("user")
.and().withUser("pangu").password(passwordEncoder.encode("123456")).roles("user")
.and().withUser("nezha").password(passwordEncoder.encode("123456")).roles("user")
.and().withUser("nvwa").password(passwordEncoder.encode("123456")).roles("user")
.and().withUser("jiangziya").password(passwordEncoder.encode("123456")).roles("user")
.and().passwordEncoder(passwordEncoder);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin().and()
.httpBasic()
.and().logout().logoutUrl("/logout");
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
在websocket服務(wù)中,我還構(gòu)建了用戶名和用戶姓名的映射,這樣在用戶建立連接的時(shí)候或者發(fā)送消息的時(shí)候,我就可以根據(jù)session的用戶名拿到用戶的姓名了。
前端頁(yè)面實(shí)現(xiàn)
這里最核心的就是websocket連接的那段js了:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>test page</title>
</head>
<body>
websocket測(cè)試<br>
<input id = "message" type="text">
<button onclick="sendMessage()">發(fā)送消息</button>
<button onclick="closeWebSocket()">關(guān)閉websocket連接</button>
<button onclick="logout()">退出登錄</button>
<div id="context"></div>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<script type="application/javascript">
function logout() {
closeWebSocket();
$.ajax({
url: "/logout",
type: "POST",
success: function (rsp) {
console.log("退出登錄成功")
console.log(rsp)
}
})
}
var websocket = null;
// 判斷當(dāng)前瀏覽榕是否支持 WebSocket
if ('WebSocket' in window) {
// 創(chuàng)建 WebSocket 對(duì)象,連接服務(wù)器端點(diǎn)
websocket = new WebSocket("ws://localhost:8080/ws");
} else {
alert('Not support websocket')
}
// 連接發(fā)生錯(cuò)誤的 回調(diào)方法
websocket.onerror = function () {
appendMessage("error");
}
// 連接成功建立的回調(diào)方法
websocket.onopen = function (event) {
appendMessage("open ");
}
// 接收到消息的回調(diào)方法
websocket.onmessage = function (event) {
appendMessage(event.data);
}
// 連接關(guān)閉的回調(diào)方法
websocket.onclose = function () {
appendMessage(" close ");
}
// 監(jiān)聽窗口關(guān)閉事件,當(dāng)窗口關(guān)閉時(shí),主動(dòng)關(guān)閉 websocket 連接
// 防止連接還沒斷開就關(guān)閉窗口,server 端會(huì)拋異常
window.onbeforeunload = function () {
websocket.close();
}
// 將消息顯示在網(wǎng)頁(yè)上
function appendMessage(message) {
var context = $("#context").html() + "<br/>" + message;
$("#context").html(context);
}
// 關(guān)閉連接
function closeWebSocket() {
websocket.close();
logout();
}
// 發(fā)送消息
function sendMessage() {
var message = $("#message").val();
websocket.send(message);
}
</script>
</body>
</html>
首先我們判斷瀏覽器是否支持WebSocket,如果支持會(huì)建立websocket連接,然后設(shè)定WebSocket的一些回調(diào)函數(shù),和服務(wù)器端對(duì)應(yīng),而且頁(yè)面還是比較簡(jiǎn)單的。
測(cè)試
下面我們簡(jiǎn)單測(cè)試下,我們分別登錄三個(gè)賬號(hào):nezha,nvwa、伏羲,然后用三個(gè)賬號(hào)分別發(fā)送消息:

效果還是可以的,首先是哪吒三太子加入群聊,然后時(shí)女媧加入群聊,然后他們分別發(fā)送消息,接著伏羲加入群聊,發(fā)送消息。第一個(gè)進(jìn)群的人,會(huì)收到后面進(jìn)群的所有人的消息,是不是和我們的微信差不多呢?
總結(jié)
websocket還是蠻有意思的,而且很容易上手。如果你有做一款自己的聊天工具,那么websocket應(yīng)該是最佳選擇,相比于socket,它更輕量,也更靈活,相比于傳統(tǒng)的http通信,它支持雙工通信。
總之,用websocket做一款聊天工具,真的是太簡(jiǎn)單了。后面有時(shí)間的話,用它做一個(gè)簡(jiǎn)易版的微信。好了,今天就先到這里吧!
最后,附上今天項(xiàng)目的源碼地址,有興趣的小伙伴可以自己動(dòng)手練練,還挺有意思的:
https://github.com/Syske/learning-dome-code/tree/dev/sping-boot-websocket-demo
- END -