一、技术对比
-
EventSource
优势:
服务器推送:基于HTTP协议,服务器主动向客户端推送数据,客户端只能接收服务器发送的数据。
适用场景:
实时股票报价、天气预报、社交媒体通知等。 -
WebSocket:
优势:
双向通信:基于TCP协议,客户端和服务器可以互相发送数据。
实时性:更低的延迟和更快的数据传输速度,适用于实时性要求较高的应用场景。
适用场景:
在线聊天室、多人游戏和股票市场等需要快速实时响应的应用程序。
二、WebSocket
1. 服务端
- pom.xml
<dependency>
<groupId>com.corundumstudio.socketio</groupId>
<artifactId>netty-socketio</artifactId>
<version>2.0.9</version>
</dependency>
- NettySocketRunner.java
package com.example.socket;
import com.corundumstudio.socketio.SocketIOServer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
/**
* Socket启动
*/
@Component
class NettySocketRunner implements CommandLineRunner, DisposableBean {
private static Log logger = LogFactory.getLog(NettySocketRunner.class);
@Autowired
private SocketIOServer socketIOServer;
@Override
public void run(String... args) {
socketIOServer.start();
logger.info("========== SocketIOServer启动成功");
}
@Override
public void destroy() {
socketIOServer.stop();
logger.info("========== SocketIOServer关闭成功");
}
}
- NettySocketConfig.java
package com.example.socket;
import com.corundumstudio.socketio.SocketConfig;
import com.corundumstudio.socketio.SocketIOServer;
import com.corundumstudio.socketio.annotation.SpringAnnotationScanner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Socket配置
*/
@Configuration
public class NettySocketConfig {
/**
* 创建SocketIO服务
*/
@Bean
public SocketIOServer socketIOServer() {
SocketConfig socketConfig = new SocketConfig();
socketConfig.setReuseAddress(true); // 端口复用
socketConfig.setTcpNoDelay(true); // 确保包无论大小, 会尽可能发送
com.corundumstudio.socketio.Configuration config = new com.corundumstudio.socketio.Configuration();
config.setSocketConfig(socketConfig);
config.setPort(8116); // 端口
config.setWorkerThreads(1000); // 连接数
config.setAllowCustomRequests(true); // 允许客户请求
config.setUpgradeTimeout(30000); // HTTP升级为ws协议超时时间 30秒
config.setPingTimeout(90000); // Ping消息超时时间 90秒
config.setPingInterval(30000); // Ping消息间隔 30秒
config.setMaxHttpContentLength(1048576); // http交互最大内容长度 1MB
config.setMaxFramePayloadLength(1048576); // 每帧最大数据长度 1MB
final SocketIOServer server = new SocketIOServer(config);
return server;
}
/**
* 扫描SocketIO注解
*/
@Bean
public SpringAnnotationScanner springAnnotationScanner() {
return new SpringAnnotationScanner(socketIOServer());
}
}
- NettyEventHandler.java
package com.example.socket;
import com.alibaba.fastjson2.JSONObject;
import com.corundumstudio.socketio.SocketIOClient;
import com.corundumstudio.socketio.annotation.OnConnect;
import com.corundumstudio.socketio.annotation.OnDisconnect;
import com.corundumstudio.socketio.annotation.OnEvent;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Socket消息事件处理
*/
@Component
public class NettyEventHandler {
private static final Log logger = LogFactory.getLog(NettyEventHandler.class);
// 用于保存和客户端的会话
public static ConcurrentMap<String, SocketIOClient> socketClientMap = new ConcurrentHashMap<>();
/**
* 客户端连接
*/
@OnConnect
public void onConnect(SocketIOClient client) {
String id = client.getHandshakeData().getSingleUrlParam("id");
String token = client.getHandshakeData().getSingleUrlParam("token");
if (StringUtils.isEmpty(id) || !"flgn920320erg8erg9".equals(token)) {
return;
}
socketClientMap.put(id, client);
logger.info("客户端 " + client.getSessionId() + " 已连接, id: " + id);
}
/**
* 客户端关闭连接
*/
@OnDisconnect
public void onDisconnect(SocketIOClient client) {
SocketIOClient socketIOClient = socketClientMap.get(client.getHandshakeData().getSingleUrlParam("id"));
String token = client.getHandshakeData().getSingleUrlParam("token");
if (!"flgn920320erg8erg9".equals(token)) {
return;
}
if (null!=socketIOClient){
socketClientMap.remove(client.getHandshakeData().getSingleUrlParam("id"));
}
logger.info("客户端 " + client.getSessionId() + " 断开连接");
}
/**
* 监听客户端事件
*/
@OnEvent(value = "client_message")
public void clientMessagelListener(SocketIOClient client, JSONObject data) {
logger.info("客户端消息: " + data);
}
}
- NettySocketRest.java
package com.example.web.rest.comm;
import com.alibaba.fastjson2.JSONObject;
import com.corundumstudio.socketio.SocketIOClient;
import com.example.socket.NettyEventHandler;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* @description 测试NettySocket
*/
@RestController
@RequestMapping("/api/comm/nettysocket")
public class NettySocketRest {
private static Log logger = LogFactory.getLog(NettySocketRest.class);
@Resource
private NettyEventHandler nettyEventHandler;
/**
* 获取客户端连接数
*/
@PostMapping(value="getNum",consumes="application/json")
public String getNum(@RequestBody JSONObject json) {
int num = nettyEventHandler.socketClientMap.size();
JSONObject result = new JSONObject();
result.put("msg", num);
return result.toString();
}
/**
* 服务端接收消息, 然后发给客户端
*/
@PostMapping(value="sendMsg",consumes="application/json")
public String sendMsg(@RequestBody JSONObject json) {
SocketIOClient client = nettyEventHandler.socketClientMap.get(json.getString("id"));
JSONObject result = new JSONObject();
if (client == null) {
logger.error("消息未处理: " + json);
result.put("msg", "client is null");
} else {
client.sendEvent("server_message", json.getString("content"));
result.put("msg", "ok");
}
return result.toString();
}
}
2. 客户端
- socket.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<script type="text/javascript" src="https://lib.baomitu.com/socket.io/4.7.2/socket.io.js"></script>
</head>
<body font-size="14px">
<strong>服务端当前连接Socket客户端数量</strong>
<p>
<span>地址:</span><input id="ip-num" type="text" size="32" value="http://127.0.0.1:8081" />
<button type="button" onClick="postNum()">获取数量</button>
</p>
<textarea id="receive-num" type="text" rows="1" cols="73" readonly></textarea>
<br><br><br>
<strong>发送消息到服务端,服务端推送给Socket客户端</strong>
<p>
<span>地址:</span><input id="ip-msg" type="text" size="32" value="http://127.0.0.1:8081" />
<span>连接ID:</span><input id="id-msg" type="text" size="20" value="123456" />
<button type="button" onClick="postMsg()">发送消息</button>
</p>
<p>
<span>发送内容:</span><br>
</p>
<textarea id="send-msg" type="text" rows="6" cols="73">{"info":"HelloWord"}</textarea>
<br><br><br>
<strong>Socket客户端连接服务端</strong>
<p>
<span>地址:</span><input id="ip-socket" type="text" size="32" value="http://127.0.0.1:8116" />
<span>连接ID:</span><input id="id-socket" type="text" size="20" value="123456" />
</p>
<p>
<span>认证token:</span><input id="token-socket" type="text" size="32" value="flgn920320erg8erg9" />
<button type="button" onClick="startSocket()"> 连接 </button>
<button type="button" onClick="stopSocket()"> 断开 </button>
</p>
<p><strong>Socket客户端接收服务端消息</strong></p>
<textarea id="receive-content" type="text" rows="5" cols="73" readonly></textarea>
<script>
let socket;
// 启动socket
function startSocket() {
const ip = document.getElementById('ip-socket').value;
const id = document.getElementById('id-socket').value;
const token = document.getElementById('token-socket').value;
const wsUrl = `${ip}/?id=${id}&token=${token}`;
socket = io.connect(wsUrl);
if (socket) {
socket.on('connect', function (data) {
socket.emit('client_message', {msg:'client msg'});
document.getElementById("receive-content").innerHTML = `连接\n` + document.getElementById("receive-content").innerHTML;
});
socket.on('server_message', function (data) {
document.getElementById("receive-content").innerHTML = `${data}\n` + document.getElementById("receive-content").innerHTML;
});
socket.on('disconnect', function (data) {
document.getElementById("receive-content").innerHTML = `断开\n` + document.getElementById("receive-content").innerHTML;
});
}
}
// 结束socket
function stopSocket() {
if (socket) {
socket.disconnect();
}
}
// 发送信息
function postNum() {
const ip = document.getElementById('ip-num').value;
const url = `${ip}/test/api/comm/nettysocket/getNum`;
sendPostRequest(url, {
}).then((res) => {
const r = JSON.parse(res);
document.getElementById("receive-num").innerHTML = `${r.msg}`;
})
}
// 发送信息
function postMsg() {
const message = document.getElementById('send-msg').value;
const id = document.getElementById('id-msg').value;
const ip = document.getElementById('ip-msg').value;
const url = `${ip}/test/api/comm/nettysocket/sendMsg`;
sendPostRequest(url, {
content: message,
id: id
})
}
// 发起Post请求
function sendPostRequest(url, data) {
return new Promise(function (resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.open('post', url, true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = function () {
var status = xhr.status;
if (status == 200) {
resolve(xhr.response);
} else {
reject(status);
}
};
xhr.send(JSON.stringify(data));
});
};
</script>
</body>
</html>
- 效果
3. Nginx中配置
- server 中
location /socket.io/ {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_pass http://127.0.0.1:8116/socket.io/;
}
三、EventSource
1. 服务端
- EventStreamRest.java
package com.example.web.rest.comm;
import com.alibaba.fastjson2.JSONObject;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* @description 测试EventStream
*/
@RestController
@RequestMapping("/api/comm/eventstream")
public class EventStreamRest {
private static Log logger = LogFactory.getLog(EventStreamRest.class);
private static ConcurrentMap<String, SseEmitter> sseEmitterMap = new ConcurrentHashMap<>();
/**
* 连接服务端
*/
@GetMapping(path = "connect", produces = {MediaType.TEXT_EVENT_STREAM_VALUE})
public SseEmitter connect(String id, String token) {
if (!"flgn920320erg8erg9".equals(token)) {
return null;
}
SseEmitter sseEmitter = new SseEmitter(0L);
sseEmitterMap.put(id,sseEmitter);
sseEmitter.onTimeout(() -> sseEmitterMap.remove(id));
return sseEmitter;
}
/**
* 关闭连接
*/
@PostMapping(value="close",consumes="application/json")
public String close(@RequestBody JSONObject json) {
if (!"flgn920320erg8erg9".equals(json.getString("token"))) {
return null;
}
SseEmitter sseEmitter = sseEmitterMap.get(json.getString("id"));
if (sseEmitter != null) {
logger.info("sseEmitter complete");
sseEmitter.complete();
sseEmitterMap.remove(json.getString("id"));
}
return "ok";
}
/**
* 服务端接收消息, 然后发给客户端
*/
@PostMapping(value="sendMsg",consumes="application/json")
public String send(@RequestBody JSONObject json) {
try {
SseEmitter sseEmitter = sseEmitterMap.get(json.getString("id"));
if (sseEmitter != null) {
sseEmitter.send(json.get("content"));
} else {
logger.error("消息未处理: " + json);
}
} catch (Exception e) {
e.printStackTrace();
return "error";
}
return "ok";
}
/**
* 获取客户端连接数
*/
@PostMapping(value="getNum",consumes="application/json")
public String getNum(@RequestBody JSONObject json) {
int num = sseEmitterMap.size();
JSONObject result = new JSONObject();
result.put("msg", num);
return result.toString();
}
}
2. 客户端
- eventstream.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<script type="text/javascript" src="https://lib.baomitu.com/socket.io/4.7.2/socket.io.js"></script>
</head>
<body font-size="14px">
<strong>服务端当前连接SSE客户端数量</strong>
<p>
<span>地址:</span><input id="ip-num" type="text" size="32" value="http://127.0.0.1:8081" />
<button type="button" onClick="postNum()">获取数量</button>
</p>
<textarea id="receive-num" type="text" rows="1" cols="73" readonly></textarea>
<br><br><br>
<strong>发送消息到服务端,服务端推送给SSE客户端</strong>
<p>
<span>地址:</span><input id="ip-msg" type="text" size="32" value="http://127.0.0.1:8081" />
<span>连接ID:</span><input id="id-msg" type="text" size="20" value="123456" />
<button type="button" onClick="postMsg()">发送消息</button>
</p>
<p>
<span>发送内容:</span><br>
</p>
<textarea id="send-msg" type="text" rows="6" cols="73">{"info":"HelloWord"}</textarea>
<br><br><br>
<strong>SSE客户端连接服务端</strong>
<p>
<span>地址:</span><input id="ip-sse" type="text" size="32" value="http://127.0.0.1:8081" />
<span>连接ID:</span><input id="id-sse" type="text" size="20" value="123456" />
</p>
<p>
<span>认证token:</span><input id="token-sse" type="text" size="32" value="flgn920320erg8erg9" />
<button type="button" onClick="connectSse()"> 连接 </button>
<button type="button" onClick="closeSse()"> 断开 </button>
</p>
<p><strong>SSE客户端接收服务端消息</strong></p>
<textarea id="receive-content" type="text" rows="5" cols="73" readonly></textarea>
<script>
let eventSource;
// 连接服务端
function connectSse() {
const ip = document.getElementById('ip-sse').value;
const id = document.getElementById('id-sse').value;
const token = document.getElementById('token-sse').value;
const sseUrl = `${ip}/test/api/comm/eventstream/connect?id=${id}&token=${token}`;
eventSource = new EventSource(sseUrl);
eventSource.onmessage = function (event) {
document.getElementById("receive-content").innerHTML = `${event.data}\n` + document.getElementById("receive-content").innerHTML;
};
document.getElementById("receive-content").innerHTML = `连接\n` + document.getElementById("receive-content").innerHTML;
}
// 关闭连接
function closeSse() {
if (eventSource) {
eventSource.close()
}
const ip = document.getElementById('ip-sse').value;
const id = document.getElementById('id-sse').value;
const token = document.getElementById('token-sse').value;
const url = `${ip}/test/api/comm/eventstream/close`;
sendPostRequest(url, {
token: token,
id: id
}).then((res) => {
document.getElementById("receive-content").innerHTML = `断开\n` + document.getElementById("receive-content").innerHTML;
})
}
// 发送信息
function postNum() {
const ip = document.getElementById('ip-num').value;
const url = `${ip}/test/api/comm/eventstream/getNum`;
sendPostRequest(url, {
}).then((res) => {
const r = JSON.parse(res);
document.getElementById("receive-num").innerHTML = `${r.msg}`;
})
}
// 发送信息
function postMsg() {
const message = document.getElementById('send-msg').value;
const id = document.getElementById('id-msg').value;
const ip = document.getElementById('ip-msg').value;
const url = `${ip}/test/api/comm/eventstream/sendMsg`;
sendPostRequest(url, {
content: message,
id: id
})
}
// 发起Post请求
function sendPostRequest(url, data) {
return new Promise(function (resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.open('post', url, true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = function () {
var status = xhr.status;
if (status == 200) {
resolve(xhr.response);
} else {
reject(status);
}
};
xhr.send(JSON.stringify(data));
});
};
</script>
</body>
</html>
- 效果