当前位置:   article > 正文

WebSocket 通信流程,注解和Spring实现WebSocket ,实战多人聊天室系统_spring websocket

spring websocket

一、前言

实现即时通信常见的有四种方式-分别是:轮询、长轮询(comet)、长连接(SSE)、WebSocket

①短轮询

很多网站为了实现推送技术,所用的技术都是轮询。轮询是在特定的的时间间隔(如每1秒),由客户端浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。

优点:后端编码比较简单

缺点:这种传统的模式带来很明显的缺点, 由于HTTP请求是单向的,是只能由客户端发起请求,由服务端响应的【请求-响应模式】,即客户端的浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。

短轮询

短轮询

②长轮询

客户端向发起一个到服务端的请求,然后服务端一直保持连接打开,直到数据发送到客户端为止

优点:避免了服务端在没有信息更新时的频繁请求,节省流量

缺点:服务器一直保持连接会消耗资源,需要同时维护多个线程,而服务器所能承载的 TCP 连接是有上限的,所以这种轮询很容易导致连接上限

长轮询

长轮询

长连接

客户端和服务端建立连接后不进行断开,之后客户端再次访问这个服务端上的内容时,继续使用这一条连接通道(长轮询是一次发送完后就断开了,)

优点:消息即时到达,不发无用请求

缺点:与长轮询一样,服务器一直保持连接是会消耗资源的,如果有大量的长连接的话,对于服务器的消耗是巨大的,而且服务器承受能力是有上限的,不可能维持无限个长连接。

HTTP长连接

HTTP长连接

④WebSocket

客户端向服务器发送一个携带特殊信息的请求头(Upgrade:WebSocket )建立连接,建立连接后双方即可实现自由的实时双向通信。

优点:

  • 较少的控制开销。在连接创建后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小。
  • 更强的实时性。由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于HTTP请求需要等待客户端发起请求服务端才能响应,延迟明显更少;即使是和Comet等类似的长轮询比较,其也能在短时间内更多次地传递数据。
  • 保持连接状态。与HTTP不同的是,Websocket需要先创建连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息。而HTTP请求可能需要在每个请求都携带状态信息(如身份认证等)。

缺点:相对来说,开发成本和难度更高

WebSocket

WebSocket

⑤对比总结

轮询(Polling)

长轮询(Long-Polling)

Websocket

长连接(SSE)

通信协议

http

http

tcp

http

触发方式

client(客户端)

client(客户端)

client、server(客户端、服务端)

client、server(客户端、服务端)

优点

兼容性好容错性强,实现简单

比短轮询节约资源

全双工通讯协议,性能开销小、安全性高,可扩展性强

实现简便,开发成本低

缺点

安全性差,占较多的内存资源与请求数

安全性差,占较多的内存资源与请求数

传输数据需要进行二次解析,增加开发成本及难度

只适用高级浏览器

延迟

非实时,延迟取决于请求间隔

非实时,延迟取决于请求间隔

实时

非实时,默认3秒延迟,延迟可自定义

长连接和长轮询的区别: 长轮询的含义就是客户端发起请求,如果服务端的数据没有发生变更,那么就hold住请求,直到服务端的数据发生了变更,或者达到了一定的时间就会返回。这样就减少了客户端和服务端不断频繁连接和传递数据的过程,并且不会消耗服务端太多资源。长连接指的是TCP链接长久保持复用。

二、WebSocket概述

WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。很多网站为了实现推送技术,所用的技术都是轮询。轮询是在特定的时间间隔(如每1秒),由浏览器对服务器发出,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。

而比较新的技术去做轮询的效果是长轮询(comet)。这种技术虽然可以双向通信,但依然需要反复发出请求。而且在Comet中,普遍采用的长链接,也会消耗服务器资源。

在这种情况下,HTML5定义了WebSocket协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。

1.Java接入WebSocket的两种方式

基于JAVA注解实现

服务端

  1. package com.mc.wsdemo.java;
  2. import lombok.extern.slf4j.Slf4j;
  3. import org.springframework.scheduling.annotation.Scheduled;
  4. import org.springframework.stereotype.Component;
  5. import javax.websocket.OnClose;
  6. import javax.websocket.OnMessage;
  7. import javax.websocket.OnOpen;
  8. import javax.websocket.Session;
  9. import javax.websocket.server.ServerEndpoint;
  10. import java.io.IOException;
  11. import java.util.Map;
  12. import java.util.concurrent.ConcurrentHashMap;
  13. /**
  14. * 监听websocket地址/myWs
  15. */
  16. @ServerEndpoint("/myWs")
  17. @Component
  18. @Slf4j
  19. public class WsServerEndpont {
  20. static Map<String,Session> sessionMap = new ConcurrentHashMap<>();
  21. //连接建立时执行的操作
  22. @OnOpen
  23. public void onOpen(Session session){
  24. sessionMap.put(session.getId(),session);
  25. log.info("websocket is open");
  26. }
  27. //收到了客户端消息执行的操作
  28. @OnMessage
  29. public String onMessage(String text){
  30. log.info("收到了一条消息:"+text);
  31. return "已收到你的消息";
  32. }
  33. //连接关闭的时候执行的操作
  34. @OnClose
  35. public void onClose(Session session){
  36. sessionMap.remove(session.getId());
  37. log.info("websocket is close");
  38. }
  39. //每2s发送给客户端心跳消息,设置一个定时任务
  40. @Scheduled(fixedRate = 2000)
  41. public void sendMsg() throws IOException {
  42. for(String key:sessionMap.keySet()){
  43. sessionMap.get(key).getBasicRemote().sendText("心跳");
  44. }
  45. }
  46. }

 配置类

  1. package com.mc.wsdemo.java;
  2. import org.springframework.context.annotation.Bean;
  3. import org.springframework.context.annotation.Configuration;
  4. import org.springframework.web.socket.server.standard.ServerEndpointExporter;
  5. @Configuration 注解表明这是一个Spring的配置类,其中包含了一些用于生成bean的方法。
  6. ServerEndpointExporter 是Spring对WebSocket的支持类,它负责扫描并自动注册所有通过
  7. @ServerEndpoint注解标记的WebSocket端点类。
  8. @Bean 注解方法 serverEndpointExporter()告诉Spring容器去实例化一个ServerEndpointExporter
  9. bean,并将其添加到IoC容器中。这样,在Spring应用启动时,ServerEndpointExporter就会自动发现
  10. 并注册那些使用了@ServerEndpoint注解的WebSocket端点,无需手动配置或初始化。
  11. @Configuration
  12. public class WebSocketConfig {
  13. @Bean
  14. public ServerEndpointExporter serverEndpointExporter(){
  15. return new ServerEndpointExporter();
  16. }
  17. }

客户端

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>ws client</title>
  6. </head>
  7. <body>
  8. <p style="border:1px solid black;width: 600px;height: 500px" id="talkMsg"></p>
  9. <input id="message" /><button id="sendBtn" onclick="sendMsg()">发送</button>
  10. </body>
  11. <script>
  12. let ws = new WebSocket("ws://localhost:8080/myWs1")
  13. // ws.onopen=function () {
  14. // }
  15. ws.onmessage=function (message) {
  16. document.getElementById("talkMsg").innerHTML = message.data
  17. }
  18. function sendMsg() {
  19. ws.send(document.getElementById("message").value)
  20. document.getElementById("message").value=""
  21. }
  22. </script>
  23. </html>

效果

img

②Spring框架实现WebSocket服务器端:多人聊天室

  1. package com.mc.wsdemo.spring;
  2. import lombok.extern.slf4j.Slf4j;
  3. import org.springframework.http.server.ServerHttpRequest;
  4. import org.springframework.http.server.ServerHttpResponse;
  5. import org.springframework.stereotype.Component;
  6. import org.springframework.web.socket.WebSocketHandler;
  7. import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;
  8. import java.util.Map;
  9. /**
  10. * 握手拦截器
  11. *HttpSessionHandshakelnterceptor (抽象类):握手拦截器,在握手前后添加操作
  12. */
  13. @Component
  14. @Slf4j
  15. public class MyWsInterceptor extends HttpSessionHandshakeInterceptor {
  16. @Override
  17. public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
  18. //在这里实现自己逻辑,最后都要调用父类的方法
  19. log.info(request.getRemoteAddress().toString()+"开始握手");
  20. return super.beforeHandshake(request, response, wsHandler, attributes);
  21. }
  22. @Override
  23. public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception ex) {
  24. log.info(request.getRemoteAddress().toString()+"完成握手");
  25. super.afterHandshake(request, response, wsHandler, ex);
  26. }
  27. }
  1. /**
  2. * web socket 主处理程序
  3. * AbstractWebSocketHandler (抽象类) :WebSocket处理程序,监听连接前,连接中,连接后
  4. */
  5. @Slf4j
  6. @Component
  7. public class MyWsHandler extends AbstractWebSocketHandler {
  8. private static Map<String,SessionBean> sessionBeanMap ;
  9. //这里定义为一个map,这个Map<String,Session>这样写不能封装自己的信息,所以我们封装了一个sessionBeanMap
  10. private static AtomicInteger clientIdMaker;//定义一个整型
  11. private static StringBuffer stringBuffer;//定义一个字符串缓冲区
  12. static {
  13. sessionBeanMap = new ConcurrentHashMap<>();//在静态代码快里初始化map.
  14. clientIdMaker = new AtomicInteger(0);//线程安全初始化整型
  15. stringBuffer = new StringBuffer();
  16. }
  17. //连接建立
  18. @Override
  19. public void afterConnectionEstablished(WebSocketSession session) throws Exception {
  20. 在 MyWsHandler 中,自定义了在连接建立之后存储 SessionBean 并发送通知消息的操作,但同时通过调
  21. super.afterConnectionEstablished(session) 确保没有遗漏掉 Spring WebSocket 框架内部预设的
  22. 连接管理逻辑。如果不调用 super 方法,可能会错过某些重要的框架内置功能,影响整个 WebSocket 服务
  23. 的正确运行。
  24. super.afterConnectionEstablished(session);
  25. SessionBean sessionBean = new SessionBean(session,clientIdMaker.getAndIncrement());//调用我们封装类的构造方法。
  26. sessionBeanMap.put(session.getId(),sessionBean);
  27. log.info(sessionBeanMap.get(session.getId()).getClientId()+"建立了连接");
  28. stringBuffer.append(sessionBeanMap.get(session.getId()).getClientId()+"进入了群聊<br/>");
  29. sendMessage(sessionBeanMap);
  30. }
  31. //收到消息
  32. @Override
  33. protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
  34. super.handleTextMessage(session, message);
  35. log.info(sessionBeanMap.get(session.getId()).getClientId()+":"+message.getPayload());
  36. stringBuffer.append(sessionBeanMap.get(session.getId()).getClientId()+":"+message.getPayload()+"<br/>");
  37. sendMessage(sessionBeanMap);
  38. }
  39. //传输异常
  40. @Override
  41. public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
  42. super.handleTransportError(session, exception);
  43. if(session.isOpen()){
  44. session.close();
  45. }
  46. sessionBeanMap.remove(session.getId());
  47. }
  48. //连接关闭
  49. @Override
  50. public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
  51. super.afterConnectionClosed(session, status);
  52. int clientId = sessionBeanMap.get(session.getId()).getClientId();
  53. sessionBeanMap.remove(session);
  54. log.info(clientId+"关闭了连接");
  55. stringBuffer.append(clientId+"退出了群聊<br/>");
  56. sendMessage(sessionBeanMap);
  57. }
  58. // //每2s发送给客户端心跳消息
  59. // @Scheduled(fixedRate = 2000)
  60. // public void sendMsg() throws IOException {
  61. // for(String key:sessionBeanMap.keySet()){
  62. // sessionBeanMap.get(key).getWebSocketSession().sendMessage(new TextMessage("心跳"));
  63. // }
  64. // }
  65. public void sendMessage(Map<String,SessionBean> sessionBeanMap){
  66. for(String key:sessionBeanMap.keySet()){
  67. try {
  68. sessionBeanMap.get(key).getWebSocketSession().sendMessage(new TextMessage(stringBuffer.toString()));
  69. } catch (IOException e) {
  70. // e.printStackTrace();
  71. log.error(e.getMessage());
  72. }
  73. }
  74. }
  75. }
  1. package com.mc.wsdemo.spring;
  2. import org.springframework.context.annotation.Configuration;
  3. import org.springframework.web.socket.config.annotation.EnableWebSocket;
  4. import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
  5. import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
  6. import javax.annotation.Resource;
  7. /**
  8. *WebSocketConfigurer (接口):配置程序,比如配置监听哪个端口
  9. *上面的握手拦截器,处理程序的使用
  10. */
  11. @Configuration
  12. @EnableWebSocket
  13. public class MyWsConfig implements WebSocketConfigurer {
  14. @Resource
  15. MyWsHandler myWsHandler;
  16. @Resource
  17. MyWsInterceptor myWsInterceptor;
  18. @Override
  19. public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
  20. registry.addHandler(myWsHandler,"/myWs1").addInterceptors(myWsInterceptor).setAllowedOrigins("*");
  21. }
  22. }

  1. package com.mc.wsdemo.spring;
  2. import lombok.AllArgsConstructor;
  3. import lombok.Data;
  4. import org.springframework.web.socket.WebSocketSession;
  5. @AllArgsConstructor
  6. @Data
  7. public class SessionBean {
  8. 这个是对原Session类的二次封装,加入了一些我们想要的信息,封装曾一个新的类,园session作为属性
  9. private WebSocketSession webSocketSession;
  10. private Integer clientId;
  11. }

本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号