現(xiàn)在用springboot集成websocket變的很方便快捷了家坎,下面簡單寫個小demo疮茄,實現(xiàn)群發(fā)消息,單對單的聊天煌张。主要是功能呐赡,界面將就一下。
參考:
http://tech.lede.com/2017/03/08/qa/websocket+spring/
https://blog.csdn.net/mr_zhuqiang/article/details/46618197
開發(fā)工具是IDEA骏融,2018.2.3
依照上圖的流程链嘀,新建一個簡單的工程。
然后在pom文件中添加一些額外用到的插件档玻。
完整的pom.xml文件:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>websocketdemo1</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<name>websocketdemo1</name>
<description>springboot2.0+websocket的集成,實現(xiàn)群發(fā)消息+單對單消息推送.</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- json工具 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>LATEST</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<!-- 包含本地的 jar -->
<includeSystemScope>true</includeSystemScope>
<!--創(chuàng)建成系統(tǒng)服務(wù)在后臺運行-->
<executable>true</executable>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<!--打包的時候, 略過test, 不運行-->
<skipTests>true</skipTests>
</configuration>
</plugin>
</plugins>
</build>
</project>
1. 新建MyHandshake實現(xiàn)HandshakeInterceptor接口
從名字大致上就可以猜測出這個接口的含義怀泊,handshake,Interceptor误趴。
這里主要是實現(xiàn)2個方法霹琼。
//握手之前干啥,常用來注冊用戶信息凉当,綁定 WebSocketSession
beforeHandshake
//握手之后干啥
afterHandshake
完整的實現(xiàn)
package com.example.websocketdemo1.websocket;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.stereotype.Service;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
/**
* @author linyun
* @date 2018/9/13 下午3:12
*/
@Slf4j
@Service
public class MyHandshake implements HandshakeInterceptor {
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler webSocketHandler, Map<String, Object> attributes) throws Exception {
if (request instanceof ServletServerHttpRequest) {
HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest();
// 從session中獲取到當(dāng)前登錄的用戶信息. 作為socket的賬號信息. session的的WEBSOCKET_USERNAME信息,在用戶打開頁面的時候設(shè)置.
String userName = (String) servletRequest.getSession().getAttribute("WEBSOCKET_USERNAME");
attributes.put("WEBSOCKET_USERNAME", userName);
}
return true;
}
@Override
public void afterHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Exception e) {
}
}
2. 新建MyHandler實現(xiàn)WebSocketHandler接口
主要是負(fù)責(zé)消息的分發(fā)枣申,用戶統(tǒng)計等。
package com.example.websocketdemo1.websocket;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.socket.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* @author linyun
* @date 2018/9/13 下午3:26
*/
@Slf4j
@Service
public class MyHandler implements WebSocketHandler {
/**
* 為了保存在線用戶信息看杭,在方法中新建一個list存儲一下【實際項目依據(jù)復(fù)雜度忠藤,可以存儲到數(shù)據(jù)庫或者緩存】
*/
private final static List<WebSocketSession> SESSIONS = Collections.synchronizedList(new ArrayList<>());
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
log.info("鏈接成功......");
SESSIONS.add(session);
String userName = (String) session.getAttributes().get("WEBSOCKET_USERNAME");
if (userName != null) {
JSONObject obj = new JSONObject();
// 統(tǒng)計一下當(dāng)前登錄系統(tǒng)的用戶有多少個
obj.put("count", SESSIONS.size());
users(obj);
session.sendMessage(new TextMessage(obj.toJSONString()));
}
}
@Override
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
log.info("處理要發(fā)送的消息");
JSONObject msg = JSON.parseObject(message.getPayload().toString());
JSONObject obj = new JSONObject();
if (msg.getInteger("type") == 1) {
//給所有人
obj.put("msg", msg.getString("msg"));
sendMessageToUsers(new TextMessage(obj.toJSONString()));
} else {
//給個人
String to = msg.getString("to");
obj.put("msg", msg.getString("msg"));
sendMessageToUser(to, new TextMessage(obj.toJSONString()));
}
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
if (session.isOpen()) {
session.close();
}
log.info("鏈接出錯,關(guān)閉鏈接......");
SESSIONS.remove(session);
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
log.info("鏈接關(guān)閉......" + closeStatus.toString());
SESSIONS.remove(session);
}
@Override
public boolean supportsPartialMessages() {
return false;
}
/**
* 給所有在線用戶發(fā)送消息
*
* @param message
*/
public void sendMessageToUsers(TextMessage message) {
for (WebSocketSession user : SESSIONS) {
try {
if (user.isOpen()) {
user.sendMessage(message);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 給某個用戶發(fā)送消息
*
* @param userName
* @param message
*/
public void sendMessageToUser(String userName, TextMessage message) {
for (WebSocketSession user : SESSIONS) {
if (user.getAttributes().get("WEBSOCKET_USERNAME").equals(userName)) {
try {
if (user.isOpen()) {
user.sendMessage(message);
}
} catch (IOException e) {
e.printStackTrace();
}
break;
}
}
}
/**
* 將系統(tǒng)中的用戶傳送到前端
*
* @param obj
*/
private void users(JSONObject obj) {
List<String> userNames = new ArrayList<>();
for (WebSocketSession webSocketSession : SESSIONS) {
userNames.add((String) webSocketSession.getAttributes().get("WEBSOCKET_USERNAME"));
}
obj.put("users", userNames);
}
}
3. 實現(xiàn)WebSocketConfigurer接口
主要的配置文件楼雹,很簡單
package com.example.websocketdemo1.websocket;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
/**
* @author linyun
* @date 2018/9/13 下午3:41
*/
@Slf4j
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Autowired
private MyHandshake handshake;
@Autowired
private MyHandler handler;
/**
* 實現(xiàn) WebSocketConfigurer 接口模孩,重寫 registerWebSocketHandlers 方法尖阔,這是一個核心實現(xiàn)方法,配置 websocket 入口榨咐,允許訪問的域介却、注冊 Handler、SockJs 支持和攔截器块茁。
* <p>
* registry.addHandler()注冊和路由的功能齿坷,當(dāng)客戶端發(fā)起 websocket 連接,把 /path 交給對應(yīng)的 handler 處理龟劲,而不實現(xiàn)具體的業(yè)務(wù)邏輯胃夏,可以理解為收集和任務(wù)分發(fā)中心。
* <p>
* addInterceptors昌跌,顧名思義就是為 handler 添加攔截器仰禀,可以在調(diào)用 handler 前后加入我們自己的邏輯代碼。
* <p>
* setAllowedOrigins(String[] domains),允許指定的域名或 IP (含端口號)建立長連接蚕愤,如果只允許自家域名訪問答恶,這里輕松設(shè)置。如果不限時使用”*”號萍诱,如果指定了域名悬嗓,則必須要以 http 或 https 開頭。
*
* @param registry
*/
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
//部分 支持websocket 的訪問鏈接,允許跨域
registry.addHandler(handler, "/echo").addInterceptors(handshake).setAllowedOrigins("*");
//部分 不支持websocket的訪問鏈接,允許跨域
registry.addHandler(handler, "/sockjs/echo").addInterceptors(handshake).setAllowedOrigins("*").withSockJS();
}
}
4. 頁面模擬聊天
模擬用戶登錄裕坊,存儲用戶信息到session中包竹。
package com.example.websocketdemo1;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpServletRequest;
/**
* @author linyun
* @date 2018/9/13 下午3:45
*/
@Controller
@RequestMapping("/socket")
public class WebSocketController {
/**
* 第一個用戶
*
* @param request
* @return
*/
@RequestMapping("/chat1")
public String chat1(HttpServletRequest request) {
// 假設(shè)用戶tom登錄,存儲到session中
request.getSession().setAttribute("WEBSOCKET_USERNAME", "tom");
return "chat1";
}
/**
* 第二個用戶登錄
*
* @param request
* @return
*/
@RequestMapping("/chat2")
public String chat2(HttpServletRequest request) {
// 假設(shè)用戶jerry登錄,存儲到session中
request.getSession().setAttribute("WEBSOCKET_USERNAME", "jerry");
return "chat2";
}
/**
* 第三個用戶登錄
*
* @param request
* @return
*/
@RequestMapping("/chat3")
public String chat3(HttpServletRequest request) {
// 假設(shè)用戶jack登錄,存儲到session中
request.getSession().setAttribute("WEBSOCKET_USERNAME", "jack");
return "chat3";
}
}
頁面都是一樣的代碼,分別新建3份籍凝,chat1.html周瞎,chat2.html,chat3.html饵蒂。
<!DOCTYPE html>
<html lang="en">
<head>
<title>測試websocket</title>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
<link rel="stylesheet" >
<link rel="stylesheet" >
</head>
<body>
<div class="container">
<div class="input-group mb-3">
<div class="input-group-prepend">
<label class="input-group-text" for="inputGroupSelect01">用戶</label>
</div>
<select class="custom-select" id="inputGroupSelect01">
<option selected>選擇一個...</option>
</select>
</div>
<div class="input-group mb-3">
<input type="text" class="form-control">
<div class="input-group-append">
<span class="input-group-text" id="btn1">發(fā)送給所有人</span>
</div>
</div>
<div class="input-group mb-3">
<input type="text" class="form-control">
<div class="input-group-append">
<span class="input-group-text" id="btn2">發(fā)送給單人</span>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.4/jquery.min.js" type="text/javascript"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.1.2/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-toast-plugin/1.3.2/jquery.toast.min.js"></script>
<script language=javascript>
$(function () {
var websocket;
if ('WebSocket' in window) {
console.log("WebSocket");
websocket = new WebSocket("ws://localhost:8080/echo");
} else if ('MozWebSocket' in window) {
console.log("MozWebSocket");
websocket = new MozWebSocket("ws://echo");
} else {
console.log("SockJS");
websocket = new SockJS("http://127.0.0.1:8080/sockjs/echo");
}
websocket.onopen = function (evnt) {
console.log("鏈接服務(wù)器成功!", evnt.data);
};
websocket.onmessage = function (evnt) {
console.log('收到消息:', evnt.data);
var json = JSON.parse(evnt.data);
if (json.hasOwnProperty('users')) {
var users = json.users;
for (var i = 0; i < users.length; i++) {
$("#inputGroupSelect01").append('<option value="' + users[i] + '">' + users[i] + '</option>');
}
} else {
//打印消息
toast(json.msg, 'info')
}
};
websocket.onerror = function (evnt) {
};
websocket.onclose = function (evnt) {
console.log("與服務(wù)器斷開了鏈接!")
}
$('#btn2').bind('click', function () {
if (websocket != null) {
//根據(jù)勾選的人數(shù)確定是群聊還是單聊
var value = $(this).parent().parent().find('input').val();
//得到選擇的用戶
var name = $("#inputGroupSelect01").find("option:selected").val();
console.log('選中的用戶', name);
if (name === '選擇一個...') {
toast('請選擇一個用戶', 'warning')
} else {
var object = {
to: name,
msg: value,
type: 2
};
//將object轉(zhuǎn)成json字符串發(fā)送給服務(wù)端
var json = JSON.stringify(object);
websocket.send(json);
}
} else {
console.log('未與服務(wù)器鏈接.');
}
});
$('#btn1').bind('click', function () {
if (websocket != null) {
//根據(jù)勾選的人數(shù)確定是群聊還是單聊
var value = $(this).parent().parent().find('input').val();
var object = {
msg: value,
type: 1
};
//將object轉(zhuǎn)成json字符串發(fā)送給服務(wù)端
var json = JSON.stringify(object);
websocket.send(json);
} else {
console.log('未與服務(wù)器鏈接.');
}
});
})
function toast(text, icon) {
$.toast({
text: text,
heading: '新消息',
icon: icon,
showHideTransition: 'slide',
allowToastClose: true,
hideAfter: 3000,
stack: 5,
position: 'top-right',
bgColor: '#444444',
textColor: '#eeeeee',
textAlign: 'left',
loader: true,
loaderBg: '#006eff'
});
}
</script>
</body>
</html>
5. 頁面的支持
修改配置文件声诸,我比較喜歡yml的。所以重命名配置文件為yml格式
application.yml
spring:
####配置 頁面模板的參數(shù)退盯,
freemarker:
charset: utf-8
suffix: .html
content-type: text/html
settings:
##格式化這個項目中彼乌,頁面數(shù)字的顯示,小數(shù)位數(shù)最多顯示10位
number_format: 0.##########
http:
encoding:
charset: UTF-8
#### 配置靜態(tài)資源的地址渊迁。html文件中慰照,可以直接使用/static/目錄
resources:
static-locations: "classpath:/"
6.測試
服務(wù)跑起來。
然后瀏覽器打開3個頁面琉朽,分別訪問:
http://localhost:8080/socket/chat1
http://localhost:8080/socket/chat2
http://localhost:8080/socket/chat3
效果如下:
可以看到最先打開的頁面返回的用戶信息只有tom毒租,后面的用戶加入就出現(xiàn)了jerry和jack。
測試發(fā)送一條群消息
下面查看單人消息漓骚,先刷新3個頁面蝌衔,保證都加載了3個用戶信息。發(fā)送的時候選擇另外用戶推送消息蝌蹂,另外2個人是收不到消息的噩斟。
完整項目 git:git@gitlab.com:tulongx/websocketdemo1.git
以上。