1 前言
1.1 什么是 SocketIO ?
Socket.IO 是一个可以在浏览器与服务器之间实现实时、双向、基于事件的通信的工具库。 Socket.IO 能够在任何平台、浏览器或设备上运行,可靠性和速度同样出色。其本质上是将 webSocket、Ajax 和其他通信方式再封装了一层,更强大,适应性和兼容性更好。
(这句话怎么理解呢?简单的来说,就是客户端可以给服务端发消息,服务端也可以给客户端发消息,而链接它们之间的消息纽带,就是“事件监听”。)
1.2 webSocket 的优点
1.2 webSocket 和 socket.io 区别?
- webSocket a:一种让客户端和服务器之间能进行双向实时通信的技术 b:使用时,虽然主流浏览器都已经支持,但仍然可能有不兼容的情况 c:适合用于client和基于node搭建的服务端使用
- socket.io a:将 webSocket、Ajax 和其它的通信方式全部封装成了统一的通信接口 b:使用时,不用担心兼容问题,底层会自动选用最佳的通信方式 c:适合进行服务端和客户端双向数据通信 d:Socket.IO中文网地址:https://socket.nodejs.cn/docs/v4/
1.3 应用及版本
- spring-boot:2.5.14
- socketio:2.0.3
- jdk:java8
- 本文是基于《若依前后端分离》版本的基础上进行代码编写和演示的
2 物料准备(均为后端代码)
2.1 添加 Socket 依赖包
<dependency><groupId>com.corundumstudio.socketio</groupId><artifactId>netty-socketio</artifactId><version>2.0.3</version></dependency>
2.2 创建频道常量类:SocketEventContants
我这个常量类是为了统一频道所建,你们不一定需要这个类
packagecom.mss.common.constant;/**
* @Description: Socket 自定义事件名称
* @Author: zhanleai
*/publicclassSocketEventContants{/**
* 用户频道
**/publicstaticfinalStringCHANNEL_USER="channel_user";/**
* 系统频道
**/publicstaticfinalStringCHANNEL_SYSTEM="channel_system";}
2.3 创建 Socket 连接类:SocketHandler
用来监听 socket 客户端上下线,以及服务端自动关闭;
有些博主把这个类的内容跟工具类里监听事件方法放在一起,个人认为需要解耦,特别是在分布式的项目中;
packagecom.mss.framework.handle;importcom.corundumstudio.socketio.SocketIOServer;importcom.mss.common.utils.socket.SocketUtil;importlombok.extern.slf4j.Slf4j;importorg.springframework.beans.factory.annotation.Autowired;importjavax.annotation.PostConstruct;importjavax.annotation.PreDestroy;importorg.springframework.stereotype.Component;/**
* @Author: zhanleai
* @Description: 客户端自动连接和断开、服务端关闭
*/@Component@Slf4jpublicclassSocketHandler{@AutowiredprivateSocketIOServer socketIoServer;/**
* 容器销毁前,自动调用此方法,关闭 socketIo 服务端
*
* @Param []
* @return
**/@PreDestroyprivatevoiddestroy(){try{
log.debug("关闭 socket 服务端");
socketIoServer.stop();}catch(Exception e){
e.printStackTrace();}}@PostConstructpublicvoidinit(){
log.debug("SocketEventListener initialized");//添加监听,客户端自动连接到 socket 服务端
socketIoServer.addConnectListener(client ->{String userId = client.getHandshakeData().getSingleUrlParam("userId");SocketUtil.connectMap.put(userId, client);
log.debug("客户端userId: "+ userId+"已连接,客户端ID为:"+ client.getSessionId());});//添加监听,客户端跟 socket 服务端自动断开
socketIoServer.addDisconnectListener(client ->{String userId = client.getHandshakeData().getSingleUrlParam("userId");SocketUtil.connectMap.remove(userId, client);
log.debug("客户端userId:"+ userId +"断开连接,客户端ID为:"+ client.getSessionId());});}// // 注释说明:以下 onConnect和 onDisconnect 方法在某些场景下会失效,不建议使用,所以注释掉// /**// * 客户端自动连接到 socket 服务端// *// * @Param [client]// * @return// **/// @OnConnect// public void onConnect(SocketIOClient client) {// String userId = client.getHandshakeData().getSingleUrlParam("userId");// SocketUtil.connectMap.put(userId, client);// log.debug("客户端userId: "+ userId+ "已连接,客户端ID为:" + client.getSessionId());// }//// /**// * 客户端跟 socket 服务端自动断开// *// * @Param [client]// * @return// **/// @OnDisconnect// public void onDisconnect(SocketIOClient client) {// String userId = client.getHandshakeData().getSingleUrlParam("userId");// log.debug("客户端userId:" + userId + "断开连接,客户端ID为:" + client.getSessionId());// SocketUtil.connectMap.remove(userId, client);// }}
2.4 Socket 配置文件和配置类
用来定义 socket 的一些配置
2.4.1 yml 配置
socketio:host: 127.0.0.1 //主机名,默认是 0.0.0.0 (这个设不设置无所谓,因为后面的 SocketConfig 类一般不用设置这个)
port: 33000 //监听端口
maxFramePayloadLength:1048576maxHttpContentLength:1048576bossCount:1workCount:100allowCustomRequests:trueupgradeTimeout: 1000000 //协议升级超时时间(毫秒),默认10000。HTTP握手升级为ws协议超时时间
pingTimeout: 6000000 //Ping消息超时时间(毫秒),默认60000,这个时间间隔内没有接收到心跳消息就会发送超时事件
pingInterval: 25000 //Ping消息间隔(毫秒),默认25000。客户端向服务器发送一条心跳消息间隔
2.4.2 配置类:SocketConfig
packagecom.mss.framework.config;importcom.corundumstudio.socketio.SocketIOServer;importorg.springframework.beans.factory.annotation.Value;importorg.springframework.context.annotation.Bean;importorg.springframework.stereotype.Component;@ComponentpublicclassSocketConfig{@Value("${socketio.host}")privateString host;@Value("${socketio.port}")privateInteger port;@Value("${socketio.bossCount}")privateint bossCount;@Value("${socketio.workCount}")privateint workCount;@Value("${socketio.allowCustomRequests}")privateboolean allowCustomRequests;@Value("${socketio.upgradeTimeout}")privateint upgradeTimeout;@Value("${socketio.pingTimeout}")privateint pingTimeout;@Value("${socketio.pingInterval}")privateint pingInterval;@BeanpublicSocketIOServersocketIOServer(){com.corundumstudio.socketio.Configuration configuration =newcom.corundumstudio.socketio.Configuration();
configuration.setPort(port);com.corundumstudio.socketio.SocketConfig socketConfig=newcom.corundumstudio.socketio.SocketConfig();
socketConfig.setReuseAddress(true);
configuration.setSocketConfig(socketConfig);
configuration.setOrigin(null);
configuration.setBossThreads(bossCount);
configuration.setWorkerThreads(workCount);
configuration.setAllowCustomRequests(allowCustomRequests);
configuration.setUpgradeTimeout(upgradeTimeout);
configuration.setPingTimeout(pingTimeout);
configuration.setPingInterval(pingInterval);//设置 sessionId 随机
configuration.setRandomSession(true);// configuration.setKeyStorePassword("pi0yo93pqgrs");// configuration.setKeyStore(this.getClass().getResourceAsStream("www.ibms.club.jks"));// configuration.setAuthorizationListener(data -> {// String token = data.getSingleUrlParam("token");// return StrUtil.isNotBlank(token);// });//初始化 Socket 服务端配置returnnewSocketIOServer(configuration);}/**
* Spring加载 SocketIOServer
*
* @Param [server]
* @return
**/@BeanpublicSpringAnnotationScannerspringAnnotationScanner(SocketIOServer socketIOServer ){returnnewSpringAnnotationScanner(socketIOServer );}}
2.5 Socket 服务启动类:ServerRunner
实现 CommandLineRunner 接口类,项目启动时自动执行 socketIOServer.start() 方法
packagecom.mss.framework.run;importcom.corundumstudio.socketio.SocketIOServer;importlombok.AllArgsConstructor;importlombok.extern.slf4j.Slf4j;importorg.springframework.boot.CommandLineRunner;importorg.springframework.stereotype.Component;@Slf4j@Component@AllArgsConstructorpublicclassServerRunnerimplementsCommandLineRunner{privatefinalSocketIOServer socketIOServer;/**
* 项目启动时,自动启动 socket 服务,服务端开始工作
*
* @Param [args]
* @return
**/@Overridepublicvoidrun(String... args){
socketIOServer.start();
log.info("socket.io server started !");}}
2.6 Socket 工具类:SocketUtil
下列实例代码中,是使用 userId 来当做客户端唯一标识,这个每个人可以根据自己项目里自行设置;
下列实例代码的应用场景,只有服务端向客户端发送消息的需求,所以实际这个工具类只有 sendToOne() 方法是实际起作用的,其余的代码都是为了本文额外写的方法;
packagecom.mss.common.utils.socket;importcom.corundumstudio.socketio.SocketIOClient;importcom.corundumstudio.socketio.annotation.OnEvent;importcom.mss.common.constant.SocketEventContants;importlombok.extern.slf4j.Slf4j;importorg.springframework.stereotype.Component;importorg.springframework.util.StringUtils;importjava.util.Map;importjava.util.Objects;importjava.util.concurrent.ConcurrentHashMap;importjava.util.concurrent.ConcurrentMap;/**
* @Author: zhanleai
* @Description:
*/@Component@Slf4jpublicclassSocketUtil{//暂且把用户&客户端信息存在缓存publicstaticConcurrentMap<String,SocketIOClient> connectMap =newConcurrentHashMap<>();/**
* 单发消息(以 userId 为标识符,给用户发送消息)
*
* @Param [userId, message]
* @return
**/publicstaticvoidsendToOne(String userId,Object message){//拿出某个客户端信息SocketIOClient socketClient =getSocketClient(userId);if(Objects.nonNull(socketClient)){//单独给他发消息
socketClient.sendEvent(SocketEventContants.CHANNEL_USER,message);}else{
log.info(userId +"已下线,暂不发送消息。");}}/**
* 群发消息
*
* @Param
* @return
**/publicstaticvoidsendToAll(Object message){if(connectMap.isEmpty()){return;}//给在这个频道的每个客户端发消息for(Map.Entry<String,SocketIOClient> entry : connectMap.entrySet()){
entry.getValue().sendEvent(SocketEventContants.CHANNEL_SYSTEM, message);}}/**
* 根据 userId 识别出 socket 客户端
* @param userId
* @return
*/publicstaticSocketIOClientgetSocketClient(String userId){SocketIOClient client =null;if(StringUtils.hasLength(userId)&&!connectMap.isEmpty()){for(String key : connectMap.keySet()){if(userId.equals(key)){
client = connectMap.get(key);}}}return client;}/**
* 1)使用事件注解,服务端监听获取客户端消息;
* 2)拿到客户端发过来的消息之后,可以再根据业务逻辑发送给想要得到这个消息的人;
* 3)channel_system 之所以会向全体客户端发消息,是因为我跟前端约定好了,你们也可以自定定义;
*
* @Param message
* @return
**/@OnEvent(value =SocketEventContants.CHANNEL_SYSTEM)publicvoidchannelSystemListener(String message){if(!StringUtils.hasLength(message)){return;}this.sendToAll(message);}}
3 Socket 调用
3.1 实际项目的应用场景:在需要发送消息通知的业务代码中调用
这个方法里有几个类:Message、DateUtils、IMessageService、MessageMapper,均为根据自身业务场景自定义的类,你们自己建吧。有需要再私信我要;
后端代码写到这里,实际上已经写完了。从 3.2 开始均为测试代码;
packagecom.mss.message.service.impl;importcom.mss.common.utils.DateUtils;importcom.mss.common.utils.socket.SocketUtil;importlombok.extern.slf4j.Slf4j;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.stereotype.Service;importcom.mss.message.mapper.MessageMapper;importcom.mss.message.domain.entity.Message;importcom.mss.message.service.IMessageService;/**
* 消息Service业务层处理
*
* @author zhanleai
*/@Service@Slf4jpublicclassMessageServiceImplimplementsIMessageService{@AutowiredprivateMessageMapper messageMapper;/**
* 新增消息
*
* @param message 消息
* @return 结果
*/@OverridepublicintinsertMessage(Message message){
message.setSendTime(DateUtils.getNowDate());// 消息入库,消息持久化int i = messageMapper.insertMessage(message);if(i >0){// 新增消息之后,再向前端推送 Socket 消息SocketUtil.sendToOne(message.getSendUserId().toString(),message);}return i;}}
3.2 测试Controller
下文均为测试的代码
packagecom.mss.message.controller;importcom.mss.common.utils.socket.SocketUtil;importio.swagger.annotations.Api;importorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.RestController;importcom.mss.common.core.controller.BaseController;importcom.mss.common.core.domain.AjaxResult;/**
* 消息Controller
*
* @author zhanleai
*/@RestController@Api(tags="消息")@RequestMapping("/message")publicclassMessageControllerextendsBaseController{/**
* 给指定客户端发送消息
*
* @Param [userId, message]
* @return
**/@GetMapping("/sendToOne")publicAjaxResultsendToOne(String userId ,String message){SocketUtil.sendToOne(userId,message);returnAjaxResult.success("单独发送消息成功。");}}
4 前端调用代码
前端代码监听了 channel_user 和 channel_system 两个频道,一个做了三个动作:
1)连接上服务端; 2)监听并接收 channel_user 频道的消息; 3)给服务端发送一条消息,并广播到所有客户端;postman 只做了一个动作,给后端指定的 userId 发送一条 channel_user 频道的消息,并被指定客户端捕获;
4.1 html 测试代码以及说明
此处不做赘述,详细的 html 测试代码见我另一篇文章《SocketIO 的 html 代码示例》:
链接跳转
4.2 浏览器打开 html 文件,然后查看后端服务日志
(socket 服务端启动,端口号为 33000,客户端 zhanleai 连接上来了)
- 浏览器截图
- 后端服务日志截图
4.3 postman 工具测试
- postman 截图
- 浏览器收到消息截图
本文转载自: https://blog.csdn.net/qq_23845083/article/details/141032116
版权归原作者 我只会发热 所有, 如有侵权,请联系我们删除。
版权归原作者 我只会发热 所有, 如有侵权,请联系我们删除。