SpringBoot2+WebSocket之聊天应用实战
什么是WebSocket?
WebSocket协议是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工(full-duplex)通信——允许服务器主动发送信息给客户端
为什么需要 WebSocket?
初次接触 WebSocket 的人,都会问同样的问题:我们已经有了 HTTP 协议,为什么还需要另一个协议?它能带来什么好处?
答案很简单,因为 HTTP 协议有一个缺陷:通信只能由客户端发起,HTTP 协议做不到服务器主动向客户端推送信息。
话不多说,马上进入干货时刻。
maven依赖
SpringBoot2.0对WebSocket的支持简直太棒了,直接就有包可以引入
1 2 3 4 | <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> |
WebSocketConfig
启用WebSocket的支持也是很简单,几句代码搞定
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.server.standard.ServerEndpointExporter; /** * 开启WebSocket支持 */ @Configuration public class WebSocketConfig { @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } } |
WebSocketServer
这就是重点了,核心都在这里。
1.因为WebSocket是类似客户端服务端的形式(采用ws协议),那么这里的WebSocketServer其实就相当于一个ws协议的Controller
2.直接@ServerEndpoint("/imserver/{userId}") 、@Component启用即可,然后在里面实现@OnOpen开启连接,@onClose关闭连接,@onMessage接收消息等方法。
3.新建一个ConcurrentHashMap webSocketMap 用于接收当前userId的WebSocket,方便IM之间对userId进行推送消息。单机版实现到这里就可以。
4.集群版(多个ws节点)还需要借助mysql或者redis等进行处理,改造对应的sendMessage方法即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 | import java.io.IOException; import java.util.concurrent.ConcurrentHashMap; import javax.websocket.OnClose; import javax.websocket.OnError; import javax.websocket.OnMessage; import javax.websocket.OnOpen; import javax.websocket.Session; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import org.apache.commons.lang.StringUtils; import org.springframework.stereotype.Component; import cn.hutool.log.Log; import cn.hutool.log.LogFactory; @ServerEndpoint ( "/imserver/{userId}" ) @Component public class WebSocketServer { static Log log=LogFactory.get(WebSocketServer. class ); /**静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。*/ private static int onlineCount = 0 ; /**concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。*/ private static ConcurrentHashMap<String,WebSocketServer> webSocketMap = new ConcurrentHashMap<>(); /**与某个客户端的连接会话,需要通过它来给客户端发送数据*/ private Session session; /**接收userId*/ private String userId= "" ; /** * 连接建立成功调用的方法*/ @OnOpen public void onOpen(Session session, @PathParam ( "userId" ) String userId) { this .session = session; this .userId=userId; if (webSocketMap.containsKey(userId)){ webSocketMap.remove(userId); webSocketMap.put(userId, this ); //加入set中 } else { webSocketMap.put(userId, this ); //加入set中 addOnlineCount(); //在线数加1 } log.info( "用户连接:" +userId+ ",当前在线人数为:" + getOnlineCount()); try { sendMessage( "连接成功" ); } catch (IOException e) { log.error( "用户:" +userId+ ",网络异常!!!!!!" ); } } /** * 连接关闭调用的方法 */ @OnClose public void onClose() { if (webSocketMap.containsKey(userId)){ webSocketMap.remove(userId); //从set中删除 subOnlineCount(); } log.info( "用户退出:" +userId+ ",当前在线人数为:" + getOnlineCount()); } /** * 收到客户端消息后调用的方法 * * @param message 客户端发送过来的消息*/ @OnMessage public void onMessage(String message, Session session) { log.info( "用户消息:" +userId+ ",报文:" +message); //可以群发消息 //消息保存到数据库、redis if (StringUtils.isNotBlank(message)){ try { //解析发送的报文 JSONObject jsonObject = JSON.parseObject(message); //追加发送人(防止串改) jsonObject.put( "fromUserId" , this .userId); String toUserId=jsonObject.getString( "toUserId" ); //传送给对应toUserId用户的websocket if (StringUtils.isNotBlank(toUserId)&&webSocketMap.containsKey(toUserId)){ webSocketMap.get(toUserId).sendMessage(jsonObject.toJSONString()); } else { log.error( "请求的userId:" +toUserId+ "不在该服务器上" ); //否则不在这个服务器上,发送到mysql或者redis } } catch (Exception e){ e.printStackTrace(); } } } /** * * @param session * @param error */ @OnError public void onError(Session session, Throwable error) { log.error( "用户错误:" + this .userId+ ",原因:" +error.getMessage()); error.printStackTrace(); } /** * 实现服务器主动推送 */ public void sendMessage(String message) throws IOException { this .session.getBasicRemote().sendText(message); } /** * 发送自定义消息 * */ public static void sendInfo(String message, @PathParam ( "userId" ) String userId) throws IOException { log.info( "发送消息到:" +userId+ ",报文:" +message); if (StringUtils.isNotBlank(userId)&&webSocketMap.containsKey(userId)){ webSocketMap.get(userId).sendMessage(message); } else { log.error( "用户" +userId+ ",不在线!" ); } } public static synchronized int getOnlineCount() { return onlineCount; } public static synchronized void addOnlineCount() { WebSocketServer.onlineCount++; } public static synchronized void subOnlineCount() { WebSocketServer.onlineCount--; } } |
消息推送
至于推送新信息,可以再自己的Controller写个方法调用WebSocketServer.sendInfo();即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | import com.softdev.system.demo.config.WebSocketServer; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.ModelAndView; import java.io.IOException; /** * WebSocketController * @author zhengkai.blog.csdn.net */ @RestController public class DemoController { @GetMapping ( "index" ) public ResponseEntity<String> index(){ return ResponseEntity.ok( "请求成功" ); } @GetMapping ( "page" ) public ModelAndView page(){ return new ModelAndView( "websocket" ); } @RequestMapping ( "/push/{toUserId}" ) public ResponseEntity<String> pushToWeb(String message, @PathVariable String toUserId) throws IOException { WebSocketServer.sendInfo(message,toUserId); return ResponseEntity.ok( "MSG SEND SUCCESS" ); } } |
页面发起
页面用js代码调用websocket
,当然,太古老的浏览器是不行的,一般新的浏览器或者谷歌浏览器是没问题的。还有一点,记得协议是ws
的,如果使用了一些路径类,可以replace(“http”,“ws”)来替换协议。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | <!DOCTYPE html> <html> <head> <meta charset= "utf-8" > <title>websocket通讯</title> </head> <script src= "https://cdn.bootcss.com/jquery/3.3.1/jquery.js" ></script> <script> var socket; function openSocket() { if (typeof(WebSocket) == "undefined" ) { console.log( "您的浏览器不支持WebSocket" ); } else { console.log( "您的浏览器支持WebSocket" ); //实现化WebSocket对象,指定要连接的服务器地址与端口 建立连接 //等同于socket = new WebSocket("ws://localhost:8888/xxxx/im/25"); //var socketUrl="${request.contextPath}/im/"+$("#userId").val(); var socketUrl= "http://localhost:9999/demo/imserver/" +$( "#userId" ).val(); socketUrl=socketUrl.replace( "https" , "ws" ).replace( "http" , "ws" ); console.log(socketUrl); if (socket!= null ){ socket.close(); socket= null ; } socket = new WebSocket(socketUrl); //打开事件 socket.onopen = function() { console.log( "websocket已打开" ); //socket.send("这是来自客户端的消息" + location.href + new Date()); }; //获得消息事件 socket.onmessage = function(msg) { console.log(msg.data); //发现消息进入 开始处理前端触发逻辑 }; //关闭事件 socket.onclose = function() { console.log( "websocket已关闭" ); }; //发生了错误事件 socket.onerror = function() { console.log( "websocket发生了错误" ); } } } function sendMessage() { if (typeof(WebSocket) == "undefined" ) { console.log( "您的浏览器不支持WebSocket" ); } else { console.log( "您的浏览器支持WebSocket" ); console.log( '{"toUserId":"' +$("#toUserId ").val()+'" , "contentText" : "'+$(" #contentText ").val()+'" }'); socket.send( '{"toUserId":"' +$("#toUserId ").val()+'" , "contentText" : "'+$(" #contentText ").val()+'" }'); } } </script> <body> <p>【userId】:<div><input id= "userId" name= "userId" type= "text" value= "10" ></div> <p>【toUserId】:<div><input id= "toUserId" name= "toUserId" type= "text" value= "20" ></div> <p>【toUserId】:<div><input id= "contentText" name= "contentText" type= "text" value= "hello websocket" ></div> <p>【操作】:<div><a onclick= "openSocket()" >开启socket</a></div> <p>【操作】:<div><a onclick= "sendMessage()" >发送消息</a></div> </body> </html> |
运行效果
先打开两个页面,按F12调出控控制台查看测试效果:
再分别开启socket
,再发送消息
图二:
2. 向前端推送数据:
http://localhost:9999/demo/push/10?message=123123
通过调用push api
,可以向指定的userId推送信息
,当然报文这里乱写,建议规定好格式。
ServerEndpointExporter错误
1 | org.springframework.beans.factory.BeanCreationException: Error creating bean with name ‘serverEndpointExporter’ defined in class path resource [com/xxx/WebSocketConfig. class ]: Invocation of init method failed; nested exception is java.lang.IllegalStateException: javax.websocket.server.ServerContainer not available |
如果tomcat部署一直报这个错,请移除 WebSocketConfig 中@Bean ServerEndpointExporter 的注入 。
ServerEndpointExporter 是由Spring官方提供的标准实现,用于扫描ServerEndpointConfig配置类和@ServerEndpoint注解实例。使用规则也很简单:
如果使用默认的嵌入式容器 比如Tomcat 则必须手工在上下文提供ServerEndpointExporter。
如果使用外部容器部署war包,则不需要提供提供ServerEndpointExporter,因为此时SpringBoot默认将扫描服务端的行为交给外部容器处理,所以线上部署的时候要把WebSocketConfig中这段注入bean的代码注掉。
Vue版本的websocket连接
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | <script> export default { data() { return { socket: null , userId:localStorage.getItem( "ms_uuid" ), toUserId: '2' , content: '3' } }, methods: { openSocket() { if (typeof WebSocket == "undefined" ) { console.log( "您的浏览器不支持WebSocket" ); } else { console.log( "您的浏览器支持WebSocket" ); //实现化WebSocket对象,指定要连接的服务器地址与端口 建立连接 //等同于socket = new WebSocket("ws://localhost:8888/xxxx/im/25"); //var socketUrl="${request.contextPath}/im/"+$("#userId").val(); var socketUrl = "http://localhost:8081/imserver/" + this .userId; socketUrl = socketUrl.replace( "https" , "ws" ).replace( "http" , "ws" ); console.log(socketUrl); if ( this .socket != null ) { this .socket.close(); this .socket = null ; } this .socket = new WebSocket(socketUrl); //打开事件 this .socket = new WebSocket(socketUrl); //打开事件 this .socket.onopen = function() { console.log( "websocket已打开" ); //socket.send("这是来自客户端的消息" + location.href + new Date()); }; //获得消息事件 this .socket.onmessage = function(msg) { console.log(msg.data); //发现消息进入 开始处理前端触发逻辑 }; //关闭事件 this .socket.onclose = function() { console.log( "websocket已关闭" ); }; //发生了错误事件 this .socket.onerror = function() { console.log( "websocket发生了错误" ); }; } }, sendMessage() { if (typeof WebSocket == "undefined" ) { console.log( "您的浏览器不支持WebSocket" ); } else { console.log( "您的浏览器支持WebSocket" ); console.log( '{"toUserId":"' + this .toUserId + '","contentText":"' + this .content + '"}' ); this .socket.send( '{"toUserId":"' + this .toUserId + '","contentText":"' + this .content + '"}' ); } } |
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步