WebSocket是一种在TCP连接上进行全双工通信的协议,建立客户端和服务器之间的通信渠道。浏览器和服务器仅需一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

WebSocket的实现方式有很多所以网上的文章都有些不一样,推荐使用第一种和第二种
方式一:原生jdk注解
1.pom先导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2.创建WebSocket配置类,通过这个配置才能去扫描WebSocket的注解
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* @Description WebSocket配置类
*/
@Configuration
public class WebSocketConfig {
/**
* 注入ServerEndpointExporter,
* 这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
3.创建WebSocket服务
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import org.springframework.stereotype.Component;
/**
* @Description WebSocket服务
*/
@Component
@ServerEndpoint("/websocket/{userId}") // 接口路径 ws://localhost:8080/websocket/1
public class WebSocket {
private static final Logger log = LoggerFactory.getLogger(WebSocket.class);
//与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
/**
* 用户ID
*/
private String userId;
//concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
//虽然@Component默认是单例模式的,但springboot还是会为每个websocket连接初始化一个bean,所以可以用一个静态set保存起来。
// 注:底下WebSocket是当前类名
private static final CopyOnWriteArraySet<WebSocket> webSockets = new CopyOnWriteArraySet<>();
// 用来存在线连接用户信息
private static final ConcurrentHashMap<String, Session> sessionPool = new ConcurrentHashMap<String, Session>();
/**
* 链接成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam(value = "userId") String userId) throws InterruptedException {
try {
this.session = session;
this.userId = userId;
webSockets.add(this);
sessionPool.put(userId, session);
log.info(session.getId());
log.info("【websocket消息】有新的连接,总数为:" + webSockets.size());
} catch (Exception e) {
e.printStackTrace();
}
//响应消息
session.getAsyncRemote().sendText("连接成功");
}
}
/**
* 链接关闭调用的方法
*/
@OnClose
public void onClose() {
try {
webSockets.remove(this);
sessionPool.remove(this.userId);
log.info("【websocket消息】连接断开,总数为:" + webSockets.size());
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 收到客户端消息后调用的方法
*
* @param message
*/
@OnMessage
public void onMessage(String message) {
log.info("【websocket消息】收到客户端消息:" + message);
}
/**
* 发送错误时的处理
*
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
log.error("用户错误,原因:" + error.getMessage());
error.printStackTrace();
}
// 此为广播消息
public void sendAllMessage(String message) {
log.info("【websocket消息】广播消息:" + message);
for (WebSocket webSocket : webSockets) {
try {
if (webSocket.session.isOpen()) {
webSocket.session.getAsyncRemote().sendText(message);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 此为单点消息
public void sendOneMessage(String userId, String message) {
Session session = sessionPool.get(userId);
if (session != null && session.isOpen()) {
try {
log.info("【websocket消息】 单点消息:" + message);
session.getAsyncRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 此为单点消息(多人)
public void sendMoreMessage(String[] userIds, String message) {
for (String userId : userIds) {
Session session = sessionPool.get(userId);
if (session != null && session.isOpen()) {
try {
log.info("【websocket消息】 单点消息:" + message);
session.getAsyncRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
// 调用 :
//@Resource
//private WebSocket webSocket;
public void sendTest() {
//创建业务消息信息
JSONObject obj = new JSONObject();
obj.put("cmd", "topic");//业务类型
obj.put("msgId", "messageid");//消息id
obj.put("msgTxt", "内容");//消息内容
//全体发送
sendAllMessage(obj.toJSONString());
//单个用户发送 (userId为用户id)
sendOneMessage(userId, obj.toJSONString());
//多个用户发送 (userIds为多个用户id,逗号‘,’分隔)
sendMoreMessage(new String[]{"1"}, obj.toJSONString());
}
}
前端调用接口路径 ws://localhost:8080/websocket/1
方式二:SpringBoot封装
1.在项目的pom.xml文件中,添加以下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2.创建一个WebSocket配置类,用于配置WebSocket相关的拦截路径参数和处理器。
@Configuration
@EnableWebSocket //启用WebSocket支持
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
//auth用于鉴权处理 openid用户的id
registry.addHandler(webSocketHandler(),"/netgate/{auth}/{openid}") //注册Handler
.addInterceptors(new WebSocketHandshakeInterceptor()) //握手过滤器注册Interceptor
.setAllowedOrigins("*"); //关闭跨域校验
//注册SockJS,提供SockJS支持(主要是兼容ie8)
registry.addHandler(myHandler(), "/my-websocket-url") //注册Handler
.withSockJS(); //支持sockjs协议
}
@Bean
public ServletServerContainerFactoryBean createWebSocketContainer() {
ServletServerContainerFactoryBean container = new
ServletServerContainerFactoryBean();
container.setMaxTextMessageBufferSize(2*1024*1024);//8192*1024 1024*1024*1024
container.setMaxBinaryMessageBufferSize(2*1024*1024);
container.setAsyncSendTimeout(55000l);
container.setMaxSessionIdleTimeout(55000l);//心跳
return container;
}
@Bean
public TextWebSocketHandler webSocketHandler() {
return new NetgateHandler();
}
@Bean
public WebSocketHandler myHandler() {
return new MyWebSocketHandler();
}
}
3.Websocket握手过滤器
public class WebSocketHandshakeInterceptor implements HandshakeInterceptor {
private final static Logger LOGGER = LoggerFactory.getLogger(WebSocketHandshakeInterceptor.class);
/**
* 握手前
*/
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Map<String, Object> attributes) throws Exception {
if (request instanceof ServletServerHttpRequest) {
String path = request.getURI().getPath();
if(requestIsValid(path)){
String[] params = getParams(path);
attributes.put("WEBSOCKET_AUTH", params[0]);
attributes.put("WEBSOCKET_OPENID", params[1]);
attributes.put("WEBSOCKET_FIRSTONE","yes");
}
}
System.out.println("================Before Handshake================");
return true;
}
/**
* 握手后
*/
public void afterHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Exception e) {
System.out.println("================After Handshake================");
if(e!=null) e.printStackTrace();
System.out.println("================After Handshake================");
}
private boolean requestIsValid(String url){
//在这里可以写上具体的鉴权逻辑
boolean isvalid = false;
if(StringUtils.isNotEmpty(url)
&& url.startsWith("/netgate/")
&& url.split("/").length==6){
isvalid = true;
}
return isvalid;
}
private String[] getParams(String url){
url = url.replace("/netgate/","");
return url.split("/");
}
}
4.创建一个WebSocket处理器类,用于处理WebSocket的连接、消息和事件,有两种接口实现方式。
TextWebSocketHandler:文本内容
BinaryWebSocketHandler:二进制内容
/**
* Websocket处理器
*/
@Component
public class NetgateHandler extends TextWebSocketHandler {
@Autowired
private MqttGateway mqttGateway;
/*
* 网关连接集合
* 第一级:设备序列号 sn
* 第二级:用户唯一标识 openid
*/
private static ConcurrentHashMap<String,ConcurrentHashMap<String,WebSocketSession>> netgates = new ConcurrentHashMap<String,ConcurrentHashMap<String,WebSocketSession>>();
/**
* 处理前端发送的文本信息
* js调用websocket.send时候,会调用该方法
* WebSocketSession代表每个客户端会话
*/
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
if(!session.isOpen()) {
System.out.println("连接已关闭,不再处理该连接的消息!");
return;
}
String mes = ObjectUtils.toString(message.getPayload(),"");
String pid = session.getAttributes().get("WEBSOCKET_PID").toString();
String sn = session.getAttributes().get("WEBSOCKET_SN").toString();
if(message==null || "".equals(mes)){
System.out.println(getSysDate()+"============接收到空消息,不予处理。");
}else if(mes.length()==1){
//心跳消息过滤掉
return;
}else {
//转发成mqtt消息
String topic = "pay/"+pid+"/server/"+sn;
System.out.println(topic);
System.out.println(getSysDate()+"============消息处理完成:"+mes);
mqttGateway.sendToMqtt(topic,mes);
}
}
/**
* 当新连接建立的时候,被调用
* 连接成功时候,会触发页面上onOpen方法
*
* @param session
* @throws Exception
*/
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
System.out.println(getSysDate()+"============正在初始化连接:"+session.getId());
try {
//初始化连接,把session存储起来
this.initUsers(session);
} catch (Exception e) {
System.out.println(getSysDate()+"============初始化连接异常-开始:"+e.getMessage());
e.printStackTrace();
System.out.println(getSysDate()+"============初始化连接异常-结束:"+e.getMessage());
}
System.out.println(getSysDate()+"============初始化连接完成:"+session.getId());
}
/**
* 当连接关闭时被调用
*
* @param session
* @param status
* @throws Exception
*/
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
System.out.println(getSysDate()+"============正在关闭连接:"+session.getId()+",isOpen:"+session.isOpen()+";code:"+status.getCode());
try {
System.out.println("断开连接状态值"+status.getCode());
this.removeSession(session);
} catch (Exception e) {
System.out.println(getSysDate()+"============关闭连接异常-开始:"+e.getMessage());
e.printStackTrace();
System.out.println(getSysDate()+"============关闭连接异常-结束:"+e.getMessage());
}
System.out.println(getSysDate()+"============正在关闭完成:"+session.getId()+",isOpen:"+session.isOpen()+";code:"+status.getCode());
}
/**
* 传输错误时调用
*
* @param session
* @param exception
* @throws Exception
*/
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
System.out.println(getSysDate()+"============发生传输错误:"+session.getId()+";session.isOpen():"+session.isOpen()+";exception:"+exception.getMessage());
exception.printStackTrace();
if (session.isOpen()) {
//try { session.close(); } catch (Exception e) {e.printStackTrace();}
}else {
try {
this.removeSession(session);
} catch (Exception e) {
System.out.println(getSysDate()+"============传输错误处理异常-开始:"+e.getMessage());
e.printStackTrace();
System.out.println(getSysDate()+"============传输错误处理异常-结束:"+e.getMessage());
}
}
System.out.println(getSysDate()+"============错误处理结束:"+session.getId()+";session.isOpen():"+session.isOpen()+";exception:"+exception.getMessage());
}
public synchronized void sendMsgToNetgateSn(String sn, String msg) {
if(netgates.size()>0 && netgates.containsKey(sn) && !netgates.get(sn).isEmpty()){
//获取EID对应的后台管理连接 多个
for (WebSocketSession ws: netgates.get(sn).values()){
System.out.println("对网关指令开始发送啦:sn="+sn+"消息内容"+msg);
try {ws.sendMessage(new TextMessage(msg));} catch (IOException e) {System.out.println(getSysDate()+"发生了异常:"+e.getMessage());e.printStackTrace();continue;}
}
}
}
//连接接入的处理方法
private synchronized void initUsers(WebSocketSession session){
String pid = (String) session.getAttributes().get("WEBSOCKET_PID");
String sn = (String) session.getAttributes().get("WEBSOCKET_SN");
String openid = (String) session.getAttributes().get("WEBSOCKET_OPENID");
if(StringUtils.isNotEmpty(pid) && StringUtils.isNotEmpty(sn) && StringUtils.isNotEmpty(openid)){
ConcurrentHashMap<String,WebSocketSession> netgate = netgates.get(sn);
if(netgate == null){
netgate = new ConcurrentHashMap<String,WebSocketSession>();
}
WebSocketSession session_exist = netgate.get(sn);
if(session_exist!=null) {
System.out.println("检测到相同SN重复连接,SN:"+sn+",连接ID:"+session_exist.getId()+",准备清理失效的连接。。。");
try {session_exist.close();} catch (IOException e) {e.printStackTrace();}
}
netgate.putIfAbsent(openid, session);
netgates.put(sn,netgate);
}
}
//连接被关闭时处理集合
private synchronized void removeSession(WebSocketSession session){
String sn = (String) session.getAttributes().get("WEBSOCKET_SN");
String openid = (String) session.getAttributes().get("WEBSOCKET_OPENID");
if(netgates.get(sn).containsKey(openid)) {
WebSocketSession exist_session = netgates.get(sn).get(openid);
//确保是同一个session 不是同一个session则不应该进行下一步的处理
if(exist_session.getId()!=null && exist_session.getId().equals(session.getId())) {
netgates.get(sn).remove(openid);
System.out.println("有一网关连接关闭!SN:"+sn+",当前在线数量为"+netgates.get(sn).keySet().size());
}else {
System.out.println("检测到关闭session异常,程序中断处理,关闭sessionId:"+session.getId()+",当前实际sessionId:"+exist_session.getId());
}
}else {
System.out.println("检测到关闭session异常,程序中断处理,系统中未找到对应的session,Sn="+sn+"openid="+openid);
}
}
private String getSysDate() {
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//设置日期格式
return df.format(new Date());
}
}
public class MyWebSocketHandler extends TextWebSocketHandler {
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
// 在WebSocket连接建立时执行的逻辑
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
// 处理接收到的文本消息
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
// 处理传输错误事件
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
// 在WebSocket连接关闭时执行的逻辑
}
}
5.Controller中可以通过@Autowired注入WebSocketHandler,并在方法中调用处理器的方法来与WebSocket进行交互。
方式三:TIO
1.引入pom依赖配置application
<dependency>
<groupId>org.t-io</groupId>
<artifactId>tio-websocket-spring-boot-starter</artifactId>
<version>3.5.5.v20191010-RELEASE</version>
</dependency>
tio:
websocket:
server:
port: 8989
2.实现handler
@Component
public class MyHandler implements IWsMsgHandler {
/**
* 握手
*
* @param httpRequest
* @param httpResponse
* @param channelContext
* @return
* @throws Exception
*/
@Override
public HttpResponse handshake(HttpRequest httpRequest, HttpResponse httpResponse, ChannelContext channelContext) throws Exception {
return httpResponse;
}
/**
* 握手成功
*
* @param httpRequest
* @param httpResponse
* @param channelContext
* @throws Exception
*/
@Override
public void onAfterHandshaked(HttpRequest httpRequest, HttpResponse httpResponse, ChannelContext channelContext) throws Exception {
System.out.println("握手成功");
}
/**
* 接收二进制文件
*
* @param wsRequest
* @param bytes
* @param channelContext
* @return
* @throws Exception
*/
@Override
public Object onBytes(WsRequest wsRequest, byte[] bytes, ChannelContext channelContext) throws Exception {
return null;
}
/**
* 断开连接
*
* @param wsRequest
* @param bytes
* @param channelContext
* @return
* @throws Exception
*/
@Override
public Object onClose(WsRequest wsRequest, byte[] bytes, ChannelContext channelContext) throws Exception {
System.out.println("关闭连接");
return null;
}
/**
* 接收消息
*
* @param wsRequest
* @param s
* @param channelContext
* @return
* @throws Exception
*/
@Override
public Object onText(WsRequest wsRequest, String s, ChannelContext channelContext) throws Exception {
System.out.println("接收文本消息:" + s);
return "success";
}
}
3.主启动类加上开启注解
@EnableTioWebSocketServer
方式四:STOMP
1.引入maven坐标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2.加入配置类
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// 配置客户端尝试连接地址
registry.addEndpoint("/ws").setAllowedOrigins("*").withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 设置广播节点
registry.enableSimpleBroker("/topic", "/user");
// 客户端向服务端发送消息需有/app 前缀
registry.setApplicationDestinationPrefixes("/app");
// 指定用户发送(一对一)的前缀 /user/
registry.setUserDestinationPrefix("/user/");
}
}
3.添加处理器
@Controller
public class WSController {
@Autowired
private SimpMessagingTemplate simpMessagingTemplate;
@MessageMapping("/hello")
@SendTo("/topic/hello")
public ResponseMessage hello(RequestMessage requestMessage) {
System.out.println("接收消息:" + requestMessage);
return new ResponseMessage("服务端接收到你发的:" + requestMessage);
}
@GetMapping("/sendMsgByUser")
public @ResponseBody
Object sendMsgByUser(String token, String msg) {
simpMessagingTemplate.convertAndSendToUser(token, "/msg", msg);
return "success";
}
@GetMapping("/sendMsgByAll")
public @ResponseBody
Object sendMsgByAll(String msg) {
simpMessagingTemplate.convertAndSend("/topic", msg);
return "success";
}
@GetMapping("/test")
public String test() {
return "test-stomp.html";
}
}
在线接口测试:
websocket在线测试 (websocket-test.com)
在线websocket测试-在线工具-postjson (coolaf.com)
下面是两个前端的示例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>本地websocket测试</title>
<meta name="robots" content="all" />
<meta name="keywords" content="本地,websocket,测试工具" />
<meta name="description" content="本地,websocket,测试工具" />
<style>
.btn-group{
display: inline-block;
}
</style>
</head>
<body>
<input type='text' value='通信地址, ws://开头..' class="form-control" style='width:390px;display:inline'
id='wsaddr' />
<div class="btn-group" >
<button type="button" class="btn btn-default" onclick='addsocket();'>连接</button>
<button type="button" class="btn btn-default" onclick='closesocket();'>断开</button>
<button type="button" class="btn btn-default" onclick='$("#wsaddr").val("")'>清空</button>
</div>
<div class="row">
<div id="output" style="border:1px solid #ccc;height:365px;overflow: auto;margin: 20px 0;"></div>
<input type="text" id='message' class="form-control" style='width:810px' placeholder="待发信息" onkeydown="en(event);">
<span class="input-group-btn">
<button class="btn btn-default" type="button" onclick="doSend();">发送</button>
</span>
</div>
</div>
</body>
<script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
<script language="javascript" type="text/javascript">
function formatDate(now) {
var year = now.getFullYear();
var month = now.getMonth() + 1;
var date = now.getDate();
var hour = now.getHours();
var minute = now.getMinutes();
var second = now.getSeconds();
return year + "-" + (month = month < 10 ? ("0" + month) : month) + "-" + (date = date < 10 ? ("0" + date) : date) +
" " + (hour = hour < 10 ? ("0" + hour) : hour) + ":" + (minute = minute < 10 ? ("0" + minute) : minute) + ":" + (
second = second < 10 ? ("0" + second) : second);
}
var output;
var websocket;
function init() {
output = document.getElementById("output");
testWebSocket();
}
function addsocket() {
var wsaddr = $("#wsaddr").val();
if (wsaddr == '') {
alert("请填写websocket的地址");
return false;
}
StartWebSocket(wsaddr);
}
function closesocket() {
websocket.close();
}
function StartWebSocket(wsUri) {
websocket = new WebSocket(wsUri);
//监听是否连接成功
websocket.onopen = function(evt) {
onOpen(evt)
};
// 监听连接关闭事件
websocket.onclose = function(evt) {
onClose(evt)
};
// 接听服务器发回的信息并处理展示
websocket.onmessage = function(evt) {
onMessage(evt)
};
// 监听并处理error事件
websocket.onerror = function(evt) {
onError(evt)
};
}
function onOpen(evt) {
writeToScreen("<span style='color:red'>连接成功,现在你可以发送信息啦!!!</span>");
}
function onClose(evt) {
writeToScreen("<span style='color:red'>websocket连接已断开!!!</span>");
websocket.close();
}
function onMessage(evt) {
writeToScreen('<span style="color:blue">服务端回应 ' + formatDate(new Date()) + '</span><br/><span class="bubble">' +
evt.data + '</span>');
}
function onError(evt) {
writeToScreen('<span style="color: red;">发生错误:</span> ' + evt.data);
}
function doSend() {
var message = $("#message").val();
if (message == '') {
alert("请先填写发送信息");
$("#message").focus();
return false;
}
if (typeof websocket === "undefined") {
alert("websocket还没有连接,或者连接失败,请检测");
return false;
}
if (websocket.readyState == 3) {
alert("websocket已经关闭,请重新连接");
return false;
}
console.log(websocket);
$("#message").val('');
writeToScreen('<span style="color:green">你发送的信息 ' + formatDate(new Date()) + '</span><br/>' + message);
websocket.send(message);
}
function writeToScreen(message) {
var div = "<div class='newmessage'>" + message + "</div>";
var d = $("#output");
var d = d[0];
var doScroll = d.scrollTop == d.scrollHeight - d.clientHeight;
$("#output").append(div);
if (doScroll) {
d.scrollTop = d.scrollHeight - d.clientHeight;
}
}
function en(event) {
var evt = evt ? evt : (window.event ? window.event : null);
if (evt.keyCode == 13) {
doSend()
}
}
</script>
</html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta content="text/html;charset=UTF-8"/>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>WebSocket Examples: Reverse</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script th:src="@{/layui/layui.js}"></script>
<link th:href="@{/layui/css/layui.css}" rel="stylesheet">
<style type="text/css">
#connect-container {
margin: 0 auto;
width: 400px;
}
#connect-container div {
padding: 5px;
margin: 0 7px 10px 0;
}
.layui-btn {
display: inline-block;
}
</style>
<script type="text/javascript">
var ws = null;
$(function () {
var target = $("#target");
if (window.location.protocol === 'http:') {
target.val('ws://' + window.location.host + target.val());
} else {
target.val('wss://' + window.location.host + target.val());
}
});
function setConnected(connected) {
var connect = $("#connect");
var disconnect = $("#disconnect");
var echo = $("#echo");
if (connected) {
connect.addClass("layui-btn-disabled");
disconnect.removeClass("layui-btn-disabled");
echo.removeClass("layui-btn-disabled");
} else {
connect.removeClass("layui-btn-disabled");
disconnect.addClass("layui-btn-disabled");
echo.addClass("layui-btn-disabled");
}
connect.attr("disabled", connected);
disconnect.attr("disabled", !connected);
echo.attr("disabled", !connected);
}
//连接
function connect() {
var target = $("#target").val();
ws = new WebSocket(target);
//监听是否连接成功
ws.onopen = function () {
setConnected(true);
log('Info: WebSocket connection opened.');
};
// 接听服务器发回的信息并处理展示
ws.onmessage = function (event) {
log('Received: ' + event.data);
};
// 监听连接关闭事件
ws.onclose = function () {
setConnected(false);
log('Info: WebSocket connection closed.');
};
}
//断开连接
function disconnect() {
if (ws != null) {
ws.close();
ws = null;
}
setConnected(false);
}
//Echo
function echo() {
if (ws != null) {
var message = $("#message").val();
log('Sent: ' + message);
ws.send(message);
} else {
alert('WebSocket connection not established, please connect.');
}
}
//日志输出
function log(message) {
console.debug(message);
}
</script>
</head>
<body>
<noscript><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websockets rely on Javascript being
enabled. Please enable
Javascript and reload this page!</h2></noscript>
<div>
<div id="connect-container" class="layui-elem-field">
<legend>Echo</legend>
<div>
<input id="target" type="text" class="layui-input" size="40" style="width: 350px" value="/echoMessage"/>
</div>
<div>
<button id="connect" class="layui-btn layui-btn-normal" onclick="connect();">Connect</button>
<button id="disconnect" class="layui-btn layui-btn-normal layui-btn-disabled" disabled="disabled"
onclick="disconnect();">Disconnect
</button>
</div>
<div>
<textarea id="message" class="layui-textarea" placeholder="请输入请求的内容" style="width: 350px"></textarea>
</div>
<div>
<button id="echo" class="layui-btn layui-btn-normal layui-btn-disabled" disabled="disabled"
onclick="echo();">Echo message
</button>
</div>
</div>
</div>
</body>
</html>
单工消息推送 SSE
SSE 是基于 HTTP 协议的;单向通信,只能由服务端向客户端单向通信;默认支持断线重连;只能传送文本消息;不支持IE;

sse 规范
在 html5 的定义中,服务端 sse,一般需要遵循以下要求
请求头
开启长连接 + 流方式传递
Content-Type: text/event-stream;charset=UTF-8
Cache-Control: no-cache
Connection: keep-alive
数据格式
服务端发送的消息,由 message 组成,其格式: field:value\n\n
其中 field 有五种可能
空: 即以:开头,表示注释,可以理解为服务端向客户端发送的心跳,确保连接不中断
data:数据。订阅后,服务端在消息可用时立即发送给客户端。事件是采用 UTF-8 编码的文本消息。事件之间由两个换行符分隔\n\n。每个事件由一个或多个名称:值字段组成,由单个换行符\n 分隔。
event: 事件,默认值
id: 数据标识符 id 字段表示,相当于每一条数据的编号 。服务器可以发送唯一的事件标识符(id字段)。如果连接中断,客户端会自动重新连接并发送最后接收到的带有header的 Last-Event-ID 的事件 ID。
retry: 重连时间 ,在重试字段中,服务器可以发送超时(以毫秒为单位),之后客户端应在连接中断时自动重新连接。如果未指定此字段,则标准应为 3000 毫秒。
SSE工具类
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
@Slf4j
public class SSEServer {
/**
* 当前连接数
*/
private static AtomicInteger count = new AtomicInteger(0);
private static Map<String, SseEmitter> sseEmitterMap = new ConcurrentHashMap<>();
public static SseEmitter connect(String userId){
//设置超时时间,0表示不过期,默认是30秒,超过时间未完成会抛出异常
SseEmitter sseEmitter = new SseEmitter(0L);
//注册回调
sseEmitter.onCompletion(completionCallBack(userId));
sseEmitter.onError(errorCallBack(userId));
sseEmitter.onTimeout(timeOutCallBack(userId));
sseEmitterMap.put(userId,sseEmitter);
//数量+1
count.getAndIncrement();
log.info("create new sse connect ,current user:{}",userId);
return sseEmitter;
}
/**
* 给指定用户发消息
*/
public static void sendMessage(String userId, String message){
if(sseEmitterMap.containsKey(userId)){
try{
sseEmitterMap.get(userId).send(message);
}catch (IOException e){
log.error("user id:{}, send message error:{}",userId,e.getMessage());
e.printStackTrace();
}
}
}
/**
* 想多人发送消息,组播
*/
public static void groupSendMessage(String groupId, String message){
if(sseEmitterMap!=null&&!sseEmitterMap.isEmpty()){
sseEmitterMap.forEach((k,v) -> {
try{
if(k.startsWith(groupId)){
v.send(message, MediaType.APPLICATION_JSON);
}
}catch (IOException e){
log.error("user id:{}, send message error:{}",groupId,message);
removeUser(k);
}
});
}
}
public static void batchSendMessage(String message) {
sseEmitterMap.forEach((k,v)->{
try{
v.send(message,MediaType.APPLICATION_JSON);
}catch (IOException e){
log.error("user id:{}, send message error:{}",k,e.getMessage());
removeUser(k);
}
});
}
/**
* 群发消息
*/
public static void batchSendMessage(String message, Set<String> userIds){
userIds.forEach(userId->sendMessage(userId,message));
}
public static void removeUser(String userId){
sseEmitterMap.remove(userId);
//数量-1
count.getAndDecrement();
log.info("remove user id:{}",userId);
}
public static List<String> getIds(){
return new ArrayList<>(sseEmitterMap.keySet());
}
public static int getUserCount(){
return count.intValue();
}
private static Runnable completionCallBack(String userId) {
return () -> {
log.info("结束连接,{}",userId);
removeUser(userId);
};
}
private static Runnable timeOutCallBack(String userId){
return ()->{
log.info("连接超时,{}",userId);
removeUser(userId);
};
}
private static Consumer<Throwable> errorCallBack(String userId){
return throwable -> {
log.error("连接异常,{}",userId);
removeUser(userId);
};
}
}
Controller层
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import vip.huhailong.catchat.sse.SSEServer;
@Slf4j
@RestController
@CrossOrigin
@RequestMapping("/sse")
public class SSEController {
@GetMapping(value = "/connect/{userId}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter connect(@PathVariable String userId){
return SSEServer.connect(userId);
}
@GetMapping("/process")
public void sendMessage() throws InterruptedException {
for(int i=0; i<=100; i++){
if(i>50&&i<70){
Thread.sleep(500L);
}else{
Thread.sleep(100L);
}
SSEServer.batchSendMessage(String.valueOf(i));
}
}
}
前端
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Home</title>
<script>
if (window.EventSource) {
let data = new EventSource("/cat-chat/sse/connect/huhailong")
data.onmessage = function(event){
console.log("test=>",event)
document.getElementById("result").innerText = event.data+'%';
document.getElementById("my-progress").value = event.data;
}
} else {
console.log("你的浏览器不支持SSE");
}
</script>
</head>
<body>
<div id="result"></div>
<progress style="width: 300px" id="my-progress" value="0" max="100"></progress>
</body>
</html>
nginx 配置 proxy_buffering off
不配置proxy_buffering off的话,会出现请求发出后,接口收到直接返回,无法保持长连接。
参考网上说明:proxy_buffering这个参数用来控制是否打开后端响应内容的缓冲区,如果这个设置为off,那么proxy_buffers和proxy_busy_buffers_size这两个指令将会失效。
本文介绍了WebSocket协议,它能建立客户端与服务器的全双工通信。详细阐述了WebSocket的四种实现方式,包括原生jdk注解、SpringBoot封装、TIO和STOMP,还提供了在线接口测试工具。此外,讲解了基于HTTP协议的单工消息推送SSE,涉及请求头、数据格式、工具类等内容,以及nginx配置要点。
5万+

被折叠的 条评论
为什么被折叠?



