socketio 安装配置
Socket.IO是一个完全由JavaScript实现、基于Node.js、支持WebSocket的协议用于实时通信、跨平台的开源框架,它包括了客户端的JavaScript和服务器端的Node.js。
Socket.IO除了支持WebSocket通讯协议外,还支持许多种轮询(Polling)机制以及其它实时通信方式,并封装成了通用的接口,并且在服务端实现了这些实时机制的相应代码。Socket.IO实现的Polling通信机制包括Adobe Flash Socket、AJAX长轮询、AJAX multipart streaming、持久Iframe、JSONP轮询等。Socket.IO能够根据浏览器对通讯机制的支持情况自动地选择最佳的方式来实现网络实时应用。
GitHub:https://github.com/mrniko/netty-socketio
安装 netty-socketio 依赖
<dependency>
<groupId>com.corundumstudio.socketio</groupId>
<artifactId>netty-socketio</artifactId>
<version>1.7.23</version>
</dependency>
这个配置写在服务端,客户端不用写,主要是一些 socket.io 的配置信息。
#socket.io 配置
socketio.host=127.0.0.1(别写 localhost,写服务器的 ip)
socketio.port=9999
# 设置最大每帧处理数据的长度,防止他人利用大数据来攻击服务器
socketio.maxFramePayloadLength=1048576
# 设置 http 交互最大内容长度
socketio.maxHttpContentLength=1048576
# socket连接数大小(如只监听一个端口 boss 线程组为 1 即可)
socketio.bossCount=1
socketio.workCount=100
socketio.allowCustomRequests=true
# 协议升级超时时间(毫秒),默认 10 秒。HTTP握手升级为 ws 协议超时时间
socketio.upgradeTimeout=1000000
# Ping 消息超时时间(毫秒),默认 60 秒,这个时间间隔内没有接收到心跳消息就会发送超时事件
socketio.pingTimeout=6000000
# Ping 消息间隔(毫秒),默认 25 秒。客户端向服务器发送一条心跳消息间隔
socketio.pingInterval=25000
创建 Socketio 配置类
package com.mslmsxp.mathscat.config;
import com.corundumstudio.socketio.SocketConfig;
import com.corundumstudio.socketio.SocketIOServer;
import com.corundumstudio.socketio.annotation.SpringAnnotationScanner;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SocketioConfig {
@Value("${socketio.host}")
private String host;
@Value("${socketio.port}")
private Integer port;
@Value("${socketio.bossCount}")
private int bossCount;
@Value("${socketio.workCount}")
private int workCount;
@Value("${socketio.allowCustomRequests}")
private boolean allowCustomRequests;
@Value("${socketio.upgradeTimeout}")
private int upgradeTimeout;
@Value("${socketio.pingTimeout}")
private int pingTimeout;
@Value("${socketio.pingInterval}")
private int pingInterval;
@Bean
public SocketIOServer socketIOServer() {
SocketConfig socketConfig = new SocketConfig();
socketConfig.setTcpNoDelay(true);
socketConfig.setSoLinger(0);
com.corundumstudio.socketio.Configuration config = new com.corundumstudio.socketio.Configuration();
config.setSocketConfig(socketConfig);
config.setHostname(host);
config.setPort(port);
config.setBossThreads(bossCount);
config.setWorkerThreads(workCount);
config.setAllowCustomRequests(allowCustomRequests);
config.setUpgradeTimeout(upgradeTimeout);
config.setPingTimeout(pingTimeout);
config.setPingInterval(pingInterval);
return new SocketIOServer(config);
}
@Bean
public SpringAnnotationScanner springAnnotationScanner() {
return new SpringAnnotationScanner(socketIOServer());
}
}
public SpringAnnotationScanner springAnnotationScanner()
用于扫描netty-socketio的注解,比如 @OnConnect、@OnEvent
注意:如果想要SocketIO 的注解生效,必须注入SpringAnnotationScanner 这个类。
说明:@OnDisconnect,@OnConnect,@OnEvent都属于SocketIO的注解,想要注解生效,则必须在配置配配置SpringAnnotationScanner;
@OnConnect: 监听客户端连接
@OnDisconnect: 监听客户端断开连接
@OnEvent (value=“text”): 用于监听客户端发送的消息,value的值就是客户端请求的唯一标识,如:socket.emit(‘text’,‘要发送的消息’);
注意:SocketIOMessageEventHandler 继承了Observable,Observable是JDK自带的观察者模式中的类,继承这个类的类说明是被观察者。
ConcurrentHashMap
ConcurrentHashMap
和 HashMap
一样,是一个存放键值对的容器。使用 hash 算法
来获取值的地址,因此时间复杂度是 O(1)
。查询非常快。
同时,ConcurrentHashMap
是线程安全的 HashMap
。专门用于多线程环境。
HashMap
HashMap
是线程不安全的,因为 HashMap
中操作都没有加锁,因此在多线程环境下会导致数据覆盖之类的问题,所以,在多线程中使用 HashMap
是会抛出异常的。
HashTable
HashTable
是线程安全的,但是 HashTable
只是单纯的在 put()
方法上加上 synchronized
。保证插入时阻塞其他线程的插入操作。虽然安全,但因为设计简单,所以性能低下。
ConcurrentHashMap
ConcurrentHashMap
是线程安全的,ConcurrentHashMap
并非锁住整个方法,而是通过原子操作和局部加锁的方法保证了多线程的线程安全,且尽可能减少了性能损耗。
java.lang.IllegalStateException: Failed to execute CommandLineRunner
······
Caused by: java.nio.channels.UnresolvedAddressException: null
socketio 业务层
package com.mslmsxp.mathscat.socketio;
import com.corundumstudio.socketio.SocketIOClient;
import com.corundumstudio.socketio.SocketIOServer;
import com.corundumstudio.socketio.annotation.OnConnect;
import com.corundumstudio.socketio.annotation.OnDisconnect;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Component
public class SocketService{
@Autowired
private SocketIOServer socketIoServer;
@OnConnect
public void onConnect(SocketIOClient client) {
String username = client.getHandshakeData().getSingleUrlParam("username");
log.info("客户端:" + client.getRemoteAddress() + " sessionId:" + client.getSessionId() + " username: " + username + "已连接");
}
@OnDisconnect
public void onDisconnect(SocketIOClient client) {
log.info("客户端:" + client.getSessionId() + "断开连接");
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("type", "disconnect");
paramMap.put("sessionId", client.getSessionId().toString());
log.error(paramMap.toString());
}
}
socket 会话本地存储
package com.mslmsxp.mathscat.socketio;
import com.corundumstudio.socketio.SocketIOClient;
import io.micrometer.common.util.StringUtils;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
@Component
public class ClientCache {
private static Map<String, HashMap<UUID, SocketIOClient>> concurrentHashMap = new ConcurrentHashMap<>();
public void saveClient(String userId, UUID sessionId, SocketIOClient socketIOClient) {
if (StringUtils.isNotBlank(userId)) {
HashMap<UUID, SocketIOClient> sessionIdClientCache = concurrentHashMap.get(userId);
if (sessionIdClientCache == null) {
sessionIdClientCache = new HashMap<>();
}
sessionIdClientCache.put(sessionId, socketIOClient);
concurrentHashMap.put(userId, sessionIdClientCache);
}
}
public HashMap<UUID, SocketIOClient> getUserClient(String userId) {
return concurrentHashMap.get(userId);
}
public void deleteSessionClient(String userId, UUID sessionId) {
concurrentHashMap.get(userId).remove(sessionId);
}
}
启动 socketio server
方法一:@PostConstruct 注解 注入完成 启动
package com.mslmsxp.mathscat.socketio;
import com.corundumstudio.socketio.SocketIOServer;
import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
public class SocketioServer {
@Autowired
private SocketIOServer server;
@PostConstruct
void init() {
server.start();
System.out.println("socketio launch");
}
}
该注解是 Java jdk 提供的注解,而不是 Spring 框架提供的, JavaEE5 引入了 @PostConstruct 和 @PreDestroy 两个作用于 Servlet 生命周期的注解,实现 Bean 初始化之前和销毁之前的自定义操作。
该注解的方法在整个 Bean 初始化中的执行顺序
Constructor(构造方法)
> @Autowired(依赖注入)
> @PostConstruct(注释的初始化方法)
官方文档:https://docs.oracle.com/javase/8/docs/api/javax/annotation/PostConstruct.html
@PostConstruct 注解的功能:当依赖注入完成后用于执行初始化的方法,并且只会被执行一次。
2023-02-20T18:33:27.120+08:00 INFO 16508 --- [ restartedMain] c.c.socketio.SocketIOServer : Session store / pubsub factory used: MemoryStoreFactory (local session store only)
2023-02-20T18:33:27.300+08:00 INFO 16508 --- [ntLoopGroup-2-1] c.c.socketio.SocketIOServer : SocketIO server started at port: 9999
socketio launch
2023-02-20T18:33:27.490+08:00 INFO 16508 --- [ restartedMain] o.s.b.d.a.OptionalLiveReloadServer : LiveReload server is running on port 35729
2023-02-20T18:33:27.509+08:00 INFO 16508 --- [ restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 5000 (http) with context path ''
2023-02-20T18:33:27.515+08:00 INFO 16508 --- [ restartedMain] c.mslmsxp.mathscat.MathscatApplication : Started MathscatApplication in 1.594 seconds (process running for 2.074)
方法二:使用 CommandLineRunner 启动
package com.mslmsxp.mathscat.socketio;
import com.corundumstudio.socketio.SocketIOServer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
public class SocketioServer implements CommandLineRunner {
@Autowired
private SocketIOServer server;
@Override
public void run(String... args) throws Exception {
this.server.start();
System.out.println("Command Line Start Socketio Server");
}
}
最后等 http Tomcat 启动完成之后 进行启动。
2023-02-20T18:36:58.703+08:00 INFO 31684 --- [ restartedMain] o.s.b.d.a.OptionalLiveReloadServer : LiveReload server is running on port 35729
2023-02-20T18:36:58.725+08:00 INFO 31684 --- [ restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 5000 (http) with context path ''
2023-02-20T18:36:58.734+08:00 INFO 31684 --- [ restartedMain] c.mslmsxp.mathscat.MathscatApplication : Started MathscatApplication in 1.907 seconds (process running for 2.505)
2023-02-20T18:36:58.736+08:00 INFO 31684 --- [ restartedMain] c.c.socketio.SocketIOServer : Session store / pubsub factory used: MemoryStoreFactory (local session store only)
Command Line Start Socketio Server
2023-02-20T18:36:58.947+08:00 INFO 31684 --- [ntLoopGroup-2-1] c.c.socketio.SocketIOServer : SocketIO server started at port: 9999
客户端运行测试
SocketIO 官方网站:https://socket.io/zh-CN/
使用 <script>
引入
<script src="/socket.io/socket.io.js"></script>
使用 ESM 引入
<script type="module">
import {
io } from "https://cdn.socket.io/4.3.2/socket.io.esm.min.js";
</script>
前端代码案例
<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.0">
<title>Socket Client First</title>
</head>
<body>
<div>Socket Client</div>
<script type="module">
import {
io } from "https://cdn.socket.io/4.3.2/socket.io.esm.min.js";
const socket = io.connect("ws://localhost:3000?username=helloworld")
socket.on("connection", (socket) => {
console.log(socket.id); // x8WIv7-mJelg7on_ALbx
});
socket.on("connect", () => {
console.log(socket.id); // x8WIv7-mJelg7on_ALbx
});
socket.on("disconnect", () => {
console.log(socket.id); // undefined
});
</script>
</body>
</html>
服务器端运行结果:
2023-02-20T19:53:59.651+08:00 INFO 14856 --- [tLoopGroup-3-40] c.m.mathscat.socketio.SocketService : 客户端:/127.0.0.1:63182 sessionId:64971bce-996e-4d2f-a235-800d3ac1d4a6 username: helloworld已连接