一、背景知识
1.消息传输

为什么张三不能直接发消息给李四,而需要通过服务器中转?
①NAT背景下,不在同一局域网的两个内网的设备无法直接通信。IPv4地址不够用,无法给每个设备分配一个唯一的IP。张三,李四都是普通客户端,设备大概率没有外网IP,只有内网IP,若没有外网IP则不能被直接访问到,除非张三李四在同一局域网中。
②通过服务器中转,容易在服务器端记录历史消息,方便查询历史记录
IPv4地址不够用解决方法:
(1)动态分配IP
(2)NAT网络地址转换:一个设备在进行上网时,IP数据报中的IP地址就会被NAT设备(通常是路由器)进行自动修改。缺点是网络环境复杂。
(3)IPv6
2、基于http实现消息的实时传输

**张三和李四不能直接通信(NAT)**必须是张三发消息给服务器,服务器转发给李四(服务器有外网IP,张三和李四都能访问到)张三(客户端)发消息(请求)给服务器,服务器主动发送响应给李四(客户端)
一般http系列的程序,第一种情况:客户端发起请求,服务器返回响应,客户端不发请求,服务器就不返回响应;第二种:服务器推送数据给客户端(不太支持)
第二种情况可以通过轮询实现:李四周期性地给服务器发送请求,如果有消息就获取,没有就sleep。但此种方式也会产生问题:消耗更多的系统资源,接收方在等待的过程中需要频繁地给服务器发送请求,这些请求中大部分是空请求;获取消息不够及时,需要等到下个请求才能拿到数据。提高轮询的频率,获取消息及时,但系统资源消耗更多;降低轮询的频率,系统资源消耗降低,但获取消息就不及时。
引入WebSocket解决消息推送问题。
3、基于WebSocket实现消息的实时传输
3.1简介
WebSocket是一个应用层协议,和http的地位是对等的,都是基于传输层的TCP实现的一个广泛被使用的应用层协议。WebSocket协议可以实现服务器给客户端主动推送数据(本身TCP就能让服务器给客户端主动推送数据,三次握手结束后客户端/服务器主动发消息都可以,业务角度上客户端主动发消息更常见)
实现【张三发消息,李四收到】的思路:服务器统一管理所有在线客户端的session,收到消息后遍历转发。先用集合存储所有的会话;然后约定格式,由服务器解析后只转发给指定客户端。
3.2报文格式

(1)FIN:是否要关闭WebSocket(此处的FIN不是TCP的FIN,只是需要通过FIN触发应用协议断开连接的操作)
(2)RSV:reserve保留位
(3)opcode:操作码,描述当前WebSocket数据帧的作用(取值为0x1表示为文本数据,0x2表示为二进制数据)
(4)MASK:是否开启掩码操作,避免缓冲区溢出
(5)payload length:载荷长度。2^7(0~127),最多能保存127字节。共三种模式,7bit,16bit,64bit。
(6)payload data:载荷数据
3.3握手过程

接下来浏览器和服务器之间相当于建立好了websocket的连接,可以使用websocket进行数据传输
!websocket基于http协议实现(❌️)
二、简单实现
1、实现方法
1、直接使用Tomcat提供的原生websocket API
2、使用Spring提供的websocket API(此处用这种)
2、举例:基于Spring的websocket API编写简单的hello world

Spring Web(提供 HTTP 基础能力,必须选)
WebSocket(核心依赖,直接提供 TextWebSocketHandler 类)
2.1服务器代码
(1)创建一个类,继承自TextWebSocketHandler (Spring内置的类)重写父类方法,处理websocket中的各个通信流程,同时加上**@Component**注解,注册到Spring中。


java
package com.gitee.zhouzhou.testwebsocketapi.api;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
@Component
public class TestWebSocketAPI extends TextWebSocketHandler {
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
//websocket连接成功建立后自动调用
System.out.println("TestAPI 连接成功!");
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
//websocket收到消息后自动调用
System.out.println("TestAPI 收到消息!"+message.toString());
//session会话记录通信双方是谁,session中持有websocket的通信连接
session.sendMessage(message);
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
//websocket连接异常后自动调用
System.out.println("TestAPI 连接异常!");
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
//websocket连接正常关闭后自动调用
System.out.println("TestAPI 连接断开!");
}
}
(2)把上述实例注册到Spring配置中,配置路由(哪个路径对应到上述的handler)创建另外一个类WebSocketConfig,实现websocket的配置interface

java
package com.gitee.zhouzhou.testwebsocketapi.config;
import com.gitee.zhouzhou.testwebsocketapi.api.TestWebSocketAPI;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Autowired
private TestWebSocketAPI testWebSocketAPI;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
//把刚才创建好的handler类注册到具体的路径上
//浏览器 websocket的请求路径是"/test"时,调用TestWebSocketAPI类中的方法
registry.addHandler(testWebSocketAPI,"/test");
}
}
2.2客户端代码
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<input type="text" id="message">
<button id="send-button">发送</button>
<script>
//创建websocket实例
let websocket=new WebSocket("ws://127.0.0.1:8080/test");//ws:websocket协议缩写
//注册回调函数
websocket.onopen=function(){
//连接成功,自动执行
console.log("websocket 连接成功!");
}
websocket.onclose=function(){
//连接断开,自动执行
console.log("websocket 连接断开!");
}
websocket.onerror=function(){
//连接异常,自动执行
console.log("websocket 连接异常!");
}
websocket.onmessage=function(e){
//收到消息,自动执行
console.log("websocket 收到消息!"+e.data);
}
//注册按钮点击事件
let messageInput=document.querySelector('#message');
let sendButton=document.querySelector('#send-button');
sendButton.onclick=function(){
console.log('发送消息:'+messageInput.value);
websocket.send(messageInput.value);
}
</script>
</body>
</html>
2.3测试

