WebSocket在SpringBoot中的实现:原理与案例

10 阅读6分钟

什么是websocket?

WebSocket 是一种全双工的一种网络通信协议,这种协议可以支持主动向客户端推送消息,客户端也可以主动向服务器发送信息,属于服务器推送技术的一种 WebSocket协议是建立在TCP协议之上的,同时WebSocket协议和HTTP协议兼容性比较好默认端口80和443,在握手阶段采用的是Http协议。 WebSocket的数据格式比较轻量,开销也比较小,可以发送文本也可以发送二进制的数据(blob对象或ArraryBuffer对象) 没有同源策略的限制,协议的标识符是ws,如果是加密后的协议标识符则是wss

为什么要有websocket?

实时通信:传统的 HTTP 是请求-响应模型,每次客户端想获取数据时,都必须发起请求并等待服务器响应。而 WebSocket 协议在建立连接后,客户端和服务器可以互相发送数据,适合实时聊天、股票行情、在线游戏等应用场景。

减少延迟:由于 WebSocket 连接是持久的,数据可以在连接建立后即时双向传输,无需每次请求时重新建立连接,降低了通信的延迟。

减少网络开销:WebSocket 连接在建立后,不需要每次都发送 HTTP 请求的头部信息,这比传统的 HTTP 请求更加节省带宽和资源

为什么使用WebSocket?

Http是你问我答,服务器不能主动找到客户端,http的流程是 ,客户端发起请求 -> 服务器响应 -> 断开连接;服务器不可以主动的推送消息给客户端。如果你想知道有没有新的消息就只能进行轮询或者长轮询;这两种方式再比较简单的的场景下完全可以使用,而且这两种方式实现起来也是比较简单。但如果是网页游戏呢,游戏一般会有大量的数据需要从服务器主动推送到客户端,也就是高频访问的场景下,这种轮询的实现成本就极高了,这时就需要用的websocket了。 websocket可以主动往客户端推送消息,不需要客户端来询问,而且websocket建立连接以后是可以长期使用的,避免频繁的建立连接销毁连接,节省网络资源上的开销。

什么时候用Websocket?

服务器要主动通知客户端例如:IM聊天、好友申请/红点提醒 消息发送的频率非常高:实时日志、页游、实时监控面板 在线状态 / 会话存在感很重要 : 在线用户列表 , 是否在线等场景

Websocket的连接是如何建立?

平时我们刷网页这个时候我们用的是Http协议,访问页游的时候http协议就得切换成Websocket。 为了要兼容这些场景,浏览器再TCP三次握手以后建立连接以后,都统一使用http协议先进行一次通信,如果这个时候是普通的http请求那就继续用http请求,如果这时想要建立Websocket连接,就会再http的请求头上加上一些特殊的header头

Connection: Upgrade
Upgrade: WebSocket
Sec-WebSocket-Key: T2a6wZ1AwhgQNqruZ2YUyg==

Connection: Upgrade 是浏览器想要升级协议

Upgrade: WebSocket 表示想要升级的协议类型,证明浏览器想要升级成WebSocket

Sec-WebSocket-Key 一段随机Base64码

如果服务器支持升级成WebSocket协议的话,就会走WebSocket的握手流程,同时根据客户端生成的Base64码,用某个公开的算法变成另外一段字符串,放在Http的响应头里面,同时状态携带上101状态码,发回给浏览器

HTTP/1.1 101 Switching Protocols
Sec-WebSocket-Accept: iBJkv/ALIW2DobfoA4dmr3JHBCY=
Upgrade: WebSocket
Connection: Upgrade

服务器端 base64 -》 某个函数/算法 -》服务器新字符串。

之后客户端也会把 base64 -》 某个函数/算法 -》客户端新字符串。

若是服务器新字符串和客户端新字符串相等,证明连接已经完成了,后续就可以使用websocket的数据格式来进行通信了。

后端实现WebSocket

新建立websocket服务

pom 依赖包

<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
  </dependency>

  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>

  <dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.24</version>
  </dependency>
</dependencies>

yml配置

#服务器配置
server:
  port: 8081
  servlet:
    context-path: /api

websocket配置类

@Configuration
public class WebsocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

WebSocket注册点

@ServerEndpoint(value = "/websocket/{userId}")
@Component
public class WebSocket {
    private static ConcurrentHashMap<String, WebSocket> webSocketMap = new ConcurrentHashMap<>();
    //实例一个session,这个session是websocket的session
    private Session session;

    //新增一个方法用于主动向客户端发送消息
    public static void sendMessage(Object message, String userId) {
        WebSocket webSocket = webSocketMap.get(userId);
        if (webSocket != null) {
            try {
                webSocket.session.getBasicRemote().sendText(JSONUtil.toJsonStr(message));
                System.out.println("【websocket消息】发送消息成功,用户="+userId+",消息内容"+message.toString());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static ConcurrentHashMap<String, WebSocket> getWebSocketMap() {
        return webSocketMap;
    }

    public static void setWebSocketMap(ConcurrentHashMap<String, WebSocket> webSocketMap) {
        WebSocket.webSocketMap = webSocketMap;
    }

    //前端请求时一个websocket时
    @OnOpen
    public void onOpen(Session session, @PathParam("userId") String userId) {
        this.session = session;
        webSocketMap.put(userId, this);
        sendMessage("CONNECT_SUCCESS", userId);
        System.out.println("【websocket消息】有新的连接,连接id"+userId);
    }

    //前端关闭时一个websocket时
    @OnClose
    public void onClose(@PathParam("userId") String userId) {
        webSocketMap.remove(userId);
        System.out.println("【websocket消息】连接断开,总数:"+ webSocketMap.size());
    }

    //前端向后端发送消息
    @OnMessage
    public void onMessage(String message) throws IOException {
        if (!message.equals("ping")) {
            System.out.println("【websocket消息】收到客户端发来的消息:"+message);
            session.getBasicRemote().sendText("收到客户端发来的消息了,消息内容为:"+message);
        }
    }
}

@ServerEndpoint 声明一个Websocket的服务点

@OnOpen 前端连接到后台的WebSocket

@OnClose 前端关闭一个连接

@OnMessage 前端向后端发送消息

启动类

@SpringBootApplication
public class WebSocketMain {
    public static void main(String[] args) {
        SpringApplication.run(WebSocketMain.class, args);
    }
}

前端测试页面代码

<!DOCTYPE html>
<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">
    <title>本地websocket测试</title>
    <meta name="robots" content="all" />
    <meta name="keywords" content="本地,websocket,测试工具" />
    <meta name="description" content="本地,websocket,测试工具" />
    <style>
      .btn-group{
        display: inline-block;
      }
    </style>
  </head>
  <body>
    <input type='text' value='通信地址, ws://开头..' class="form-control" style='width:390px;display:inline'
      id='wsaddr' />
    <div class="btn-group" >
      <button type="button" class="btn btn-default" onclick='addsocket();'>连接</button>
      <button type="button" class="btn btn-default" onclick='closesocket();'>断开</button>
      <button type="button" class="btn btn-default" onclick='$("#wsaddr").val("")'>清空</button>
    </div>
    <div class="row">
      <div id="output" style="border:1px solid #ccc;height:365px;overflow: auto;margin: 20px 0;"></div>
      <input type="text" id='message' class="form-control" style='width:810px' placeholder="待发信息" onkeydown="en(event);">
      <span class="input-group-btn">
        <button class="btn btn-default" type="button" onclick="doSend();">发送</button>
      </span>
    </div>
  </div>
  </body>		

  <script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
  <script language="javascript" type="text/javascript">
    function formatDate(now) {
      var year = now.getFullYear();
      var month = now.getMonth() + 1;
      var date = now.getDate();
      var hour = now.getHours();
      var minute = now.getMinutes();
      var second = now.getSeconds();
      return year + "-" + (month = month < 10 ? ("0" + month) : month) + "-" + (date = date < 10 ? ("0" + date) : date) +
        " " + (hour = hour < 10 ? ("0" + hour) : hour) + ":" + (minute = minute < 10 ? ("0" + minute) : minute) + ":" + (
          second = second < 10 ? ("0" + second) : second);
    }
    var output;
    var websocket;

    function init() {
      output = document.getElementById("output");
      testWebSocket();
    }

    function addsocket() {
      var wsaddr = $("#wsaddr").val();
      if (wsaddr == '') {
        alert("请填写websocket的地址");
        return false;
      }
      StartWebSocket(wsaddr);
    }

    function closesocket() {
      websocket.close();
    }

    function StartWebSocket(wsUri) {
      websocket = new WebSocket(wsUri);
      websocket.onopen = function(evt) {
        onOpen(evt)
      };
      websocket.onclose = function(evt) {
        onClose(evt)
      };
      websocket.onmessage = function(evt) {
        onMessage(evt)
      };
      websocket.onerror = function(evt) {
        onError(evt)
      };
    }

    function onOpen(evt) {
      writeToScreen("<span style='color:red'>连接成功,现在你可以发送信息啦!!!</span>");
    }

    function onClose(evt) {
      writeToScreen("<span style='color:red'>websocket连接已断开!!!</span>");
      websocket.close();
    }

    function onMessage(evt) {
      writeToScreen('<span style="color:blue">服务端回应&nbsp;' + formatDate(new Date()) + '</span><br/><span class="bubble">' +
                    evt.data + '</span>');
			}
 
			function onError(evt) {
				writeToScreen('<span style="color: red;">发生错误:</span> ' + evt.data);
			}
 
			function doSend() {
				var message = $("#message").val();
				if (message == '') {
					alert("请先填写发送信息");
					$("#message").focus();
					return false;
				}
				if (typeof websocket === "undefined") {
					alert("websocket还没有连接,或者连接失败,请检测");
					return false;
				}
				if (websocket.readyState == 3) {
					alert("websocket已经关闭,请重新连接");
					return false;
				}
				console.log(websocket);
				$("#message").val('');
				writeToScreen('<span style="color:green">你发送的信息&nbsp;' + formatDate(new Date()) + '</span><br/>' + message);
				websocket.send(message);
			}
 
			function writeToScreen(message) {
				var div = "<div class='newmessage'>" + message + "</div>";
				var d = $("#output");
				var d = d[0];
				var doScroll = d.scrollTop == d.scrollHeight - d.clientHeight;
				$("#output").append(div);
				if (doScroll) {
					d.scrollTop = d.scrollHeight - d.clientHeight;
				}
			}
 
 
			function en(event) {
				var evt = evt ? evt : (window.event ? window.event : null);
				if (evt.keyCode == 13) {
					doSend()
				}
			}
		</script>
 
</html>

前端测试网站 在线websocket测试-在线工具-postjson