1. WebSocket是什么?
websocket是一种网络协议,其于2011年成为了国际标准(rfc6455)。
websocket解决了HTTP只能单向通信的问题。
比如我们需要服务器能定时向客户端传输一份数据,因为HTTP协议做不到服务器主动向客户端推送消息,所以需要client定时到服务器轮询,服务器收到请求后,将客户端需要的数据返回给客户端。这样轮询效率是低下的,并且浪费了资源。
websocket可以解决这样的问题,服务器可以主动向客户端推送消息,客户端也可以向服务器发送消息,实现了全双工。
2. WebSocket如何工作?
websocket是建立于http之上的,当客户端与服务器端建立了websocket连接后,这个连接将一直存在。
websocket工作可以分为两部分:建立连接和发送数据
2.1 建立连接
2.1.1 建立过程
当建立websocket连接的时候,client会先发送一个upgrade http报文到server,然后server再对此进行回复,从而完成一次握手过程。
client发送的数据包是类似这个样子的:
GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Origin: http://example.com Sec-WebSocket-Protocol: chat, superchat Sec-WebSocket-Version: 13 |
server返回的数据包大概是这个样子:
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= Sec-WebSocket-Protocol: chat |
1)第一行为遵循Request-Line(rfc2616)格式,首先是method,为“GET”,紧跟着Request-URI为“/chat”,“/chat”的是websocket的服务端点(endpoint),最后是http版本号。
2)Host:host是http协议要求必须带的,host指明了client要访问的具体资源,比如一个server(一个IP),部署了多个域名,那么当client访问其中一个域名的时候,就可以通过host进行区分。如果server收到一个client请求,但是没有host,server会返回错误(400)。
3)Upgrade:当client想通过其他协议与server通信的话,就需要带upgrade,服务器统一协议升级后会回复101(switching protocol),并且在response中也会带上upgrade
4)connection: 表名client和server对于长连接如何处理,如果是keep-alive,那么就会保持,如果是close,那么在本次通信完成之后要将连接断开。
5)Sec-WebSocket-Key:这个是为了client和server之间相互验证用的。当server收到Sec-WebSocket-Key之后,它会将这个key链接加上Globally Unique Identifier(GUID)形成一个新的字符串(base64),发送回client(Sec-WebSocket-Accept)。client回进行检查,如果发现收到的不是期望的值,那么websocket的连接就建立不成功。
6)Origin:origin头只包含协议和域名。一般用于CORS跨域请求中
7)Sec-WebSocket-Protocol:指明客户端可以接受那些子协议,可以填写多个,服务器会从中选择一个。
8)Sec-WebSocket-Version:websocket版本
9)Sec-WebSocket-Extensions:websocket扩展头,用于协商本次连接要使用的 WebSocket 扩展:客户端发送支持的扩展,服务器通过返回相同的首部确认自己支持一个或多个扩展,具体扩展内容和含义可以参考(rfc7692)
10)Sec-WebSocket-Accept:服务端的回复,意思见Sec-WebSocket-Key
根据协议,在handshake过程中,客户端和服务器都会有很多的检查,比如服务器会检查http协议版本是否大于1.1并且是GET请求等,这些一般对于普通开发者来说,都是不需要关注的。
2.1.2 wireshark抓包
wireshark抓包看了下,第一个包是客户端发送的:
可以看到,我实际抓的包中还有cookie。
下一个是服务端返回的报文:
相对简单好多。
2.2 发送数据
handshake成功之后,客户端和服务器端就可以互相发送报文了。
2.2.1 报文结构
报文结构如下:
0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-------+-+-------------+-------------------------------+ |F|R|R|R| opcode|M| Payload len | Extended payload length | |I|S|S|S| (4) |A| (7) | (16/64) | |N|V|V|V| |S| | (if payload len==126/127) | | |1|2|3| |K| | | +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + | Extended payload length continued, if payload len == 127 | + - - - - - - - - - - - - - - - +-------------------------------+ | |Masking-key, if MASK set to 1 | +-------------------------------+-------------------------------+ | Masking-key (continued) | Payload Data | +-------------------------------- - - - - - - - - - - - - - - - + : Payload Data continued ... : + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | Payload Data continued ... | +---------------------------------------------------------------+
可以看下各个字段都是什么含义:
- FIN: 1bit,用来指示这个数据包是不是一条消息的最后一个包
- RSV1,RSV2,RSV3:3个保留bit
- opcode:操作码,用于表示“payload data”字段的含义:
- 0x0:说明这是一个continuous frame,
- 0x1:说明是文本数据
- 0x2:说明是二进制数据
- 0x3-7:保留
- 0x8:连接断开
- 0x9:表明是一个ping包
- 0xA:表明是一个pong包
- 0xB-F:保留
- Mask:表明payload data是否是mask的,如果设置为1,那么“Masking-kay”字段要有值
- payload len:“payload data”的长度,8bit。又分成了3种情况:
- 如果值为 0-125,那么就表示负载数据的长度;
- 如果是 126,那么接下来的 2 个字节以16 位的无符号整形作为负载数据的长度(无符号整型);
- 如果是 127,那么接下来的 8 个字节作为负载数据的长度(无符号整型)。
- Masking-key:如果“mask”值为0,那么masking-key的长度也是0,否则为4字节。从客户端发送到服务器的数据需要通过masking-key计算掩码后发送。mask算法是公开的,但是我没有细究。
- payload data:数据包中的实际数据。
2.2.2 发送报文
发送报文的时候,需要有以下条件:
- websocket连接处于open状态
- 将要发送的数据封装与websocket数据包中,一个放不下就放在多个包中
- 如果是客户端发包,payload data必须是mask的
- 如果协商的时候有扩展,需要应用这些扩展
2.2.2 关闭连接
一般来说,连接的关闭应该是服务器端发起。
服务器先发送一个关闭的控制包,客户端收到后会返回一个关闭的控制包。当两端都发送和收到 关闭连接的控制包之后,websocket连接就被关闭了。
本来我也想wireshark抓包,看下data报文的样子,但是没有抓到,或者是抓到了我不认识?我wireshark用http过滤没有发现websocket报文,后来用websocket直接过滤也不行。不知道哪个地方弄的不对,以后有时间了再研究。
3. WebSocket实例1
环境是idea+jdk8+springboot2.3.12
功能很简单,客户端启动后和服务端建立websocket连接,客户端给服务端发送消息,服务端收到后,通过websocket给客户端回复一个消息。
pom.xml文件如下:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.12.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.example.jpademo</groupId> <artifactId>demo</artifactId> <version>0.0.1-SNAPSHOT</version> <name>demo</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project> |
3.1 服务端编写
先看服务端。服务端websocket编写有几种方式,我选择是通过config方式,也可以通过注解方式
1. 先编写websocket handler
package com.example.jpademo.demo.handler; import lombok.extern.slf4j.Slf4j; 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; import java.io.IOException; import java.time.LocalDateTime; import java.util.concurrent.ConcurrentHashMap; /** * ws消息处理类 */ @Component @Slf4j public class MyWsHandler extends TextWebSocketHandler { //Logger log = LoggerFactory.getLogger(MyWsHandler.class); public static ConcurrentHashMap<String, WebSocketSession> SESSION_POOL = new ConcurrentHashMap<>(); @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { log.info("建立ws连接"); System.out.println("after web socker connection is established"); SESSION_POOL.put(session.getId(),session); } @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { log.info("发送文本消息"); // 获得客户端传来的消息 String payload = message.getPayload(); payload = payload + " : all well"; log.info("server 接收到消息 " + payload); session.sendMessage(new TextMessage("server 发送给的消息 " + payload + ",发送时间:" + LocalDateTime.now().toString())); } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { log.info("关闭ws连接"); if (session != null) { try { // 关闭连接 session.close(); } catch (IOException e) { // todo: 关闭出现异常处理 log.info(e.getMessage()); } } SESSION_POOL.remove(session.getId()); } @Override public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception { log.error("异常处理"); WsSessionManager.removeAndClose(session.getId()); } } |
websocket handler要继承TextWebSocketHandler类,当然也可以继承AbstractWebSocketHandler类型,然后override几个方法,通过方法的名字,比较容易看出这些方法都是做什么的:
- afterConnectionEstablished:建立websocket成功后被触发
- handleTextMessage:处理对端发送过来的websocket消息
- afterConnectionClosed:websocket连接关闭后触发
- handleTransportError:传输出问题触发
代码中的SESSION_POOL用于存放建立的session。当一个新websocket session建立后,就将这个session放到pool中,当一个websocket session被关闭后,就从这个pool删除。在例子中,这个pool其实作用不大,因为服务端在收到客户端消息后,直接就回复了消息。但是对于其他情况,保存session就显得有必要了。
2. 注册websocket
package com.example.jpademo.demo.config; import com.example.jpademo.demo.handler.MyWsHandler; 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 MyWsHandler myWsHandler; @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { System.out.println("hello web socket"); registry.addHandler(myWsHandler, "/wsok").setAllowedOrigins("*"); } } |
注册的关键是registry的addHandler方法,这个方法会将"/wsok"(暴露给客户端的请求路径)和第一步创建的MyWsHandler进行绑定。
3.2 客户端编写
这个是前端代码,网上找的:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title></title> </head> <body> websocket Demo---- user000 <br /> <input id="text" type="text" /> <button οnclick="send()"> Send </button> <button οnclick="closeWebSocket()"> Close </button> <div id="message"> </div> <script type="text/javascript"> //判断当前浏览器是否支持WebSocket if('WebSocket' in window){ //websocket = new WebSocket("ws://localhost:8080/Demo/websocketTest/user000"); websocket = new WebSocket("ws://localhost:8080//wsok"); console.log("link success") }else{ alert('Not support websocket') } //连接发生错误的回调方法 websocket.onerror = function(){ setMessageInnerHTML("error"); }; //连接成功建立的回调方法 websocket.onopen = function(event){ setMessageInnerHTML("open"); } console.log("-----") //接收到消息的回调方法 websocket.onmessage = function(event){ setMessageInnerHTML(event.data); } //连接关闭的回调方法 websocket.onclose = function(){ setMessageInnerHTML("close"); } //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。 window.onbeforeunload = function(){ websocket.close(); } //将消息显示在网页上 function setMessageInnerHTML(innerHTML){ document.getElementById('message').innerHTML += innerHTML + '<br/>'; } //关闭连接 function closeWebSocket(){ websocket.close(); } //发送消息 function send(){ var message = document.getElementById('text').value; websocket.send(message); } </script> </body> </html> |
发送效果如下:
4. WebSocket实例2
除了上述方法,也可以用原生注解的方法实现websocket
pom.xml文件不变
4.1 服务端编写
1. bean注册
package com.example.jpademo.demo.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.config.annotation.EnableWebSocket; import org.springframework.web.socket.server.standard.ServerEndpointExporter; @Configuration @EnableWebSocket public class WebSocketOriginConfig { @Bean public ServerEndpointExporter serverEndpoint() { return new ServerEndpointExporter(); } } |
2. 编写handler
package com.example.jpademo.demo.handler; import org.springframework.stereotype.Component; import org.springframework.web.socket.WebSocketSession; import javax.websocket.OnClose; import javax.websocket.OnMessage; import javax.websocket.OnOpen; import javax.websocket.Session; import javax.websocket.server.ServerEndpoint; import java.io.IOException; import java.util.concurrent.ConcurrentHashMap; @Component @ServerEndpoint("/hellows") public class WsServerEndpoint { public static ConcurrentHashMap<String, Session> SESSION_POOL = new ConcurrentHashMap<>(); /** * 连接成功 * * @param session */ @OnOpen public void onOpen(Session session) { System.out.println("连接成功"); SESSION_POOL.put(session.getId(), session); } /** * 连接关闭 * * @param session */ @OnClose public void onClose(Session session) { System.out.println("连接关闭"); } /** * 接收到消息 * * @param text */ @OnMessage public String onMsg(Session session, String text) throws IOException { for(Session session1 : SESSION_POOL.values()){ session1.getBasicRemote().sendText("good morning"); } //SESSION_POOL.get(session.getId()).getBasicRemote().sendText("good morning"); return "servet 发送:" + text; } } |
与例子1不同的是,此处用注解实现:
- @ServerEndpoint: 这个注解可以让 spring boot 知道你暴露出去的 ws 应用的路径,与registry.addHandler(myWsHandler, "/wsok").setAllowedOrigins("*");类似
- @OnOpen:当 websocket 建立连接成功后会触发这个注解方法,它有一个 Session 参数,可以在方法中将这个session保存,以供后续使用
- @OnClose:当 websocket 连接断开后会触发这个注解修方法,注意它有一个 Session 参数,可以将这个session从pool中删除
- @OnMessage:当客户端发送消息到服务端时,会触发这个注解方法, String 参数是客户端传入的值
- @OnError:当 websocket 建立连接时出现异常会触发这个注解修饰的方法,它有一个 Session 参数,这个我在例子中没有写
4.2 客户端编写
客户端与例子1一样,只是改下websocket的url为:
websocket = new WebSocket("ws://localhost:8080//hellows"); |
发送效果如下:
5 参考文档
参考了网上的文章如下,他们都写的比我好,但是自己做一遍能加深印象
【websocket】spring boot 集成 websocket 的四种方式 - 云+社区 - 腾讯云这个配置类很简单,通过这个配置 spring boot 才能去扫描后面的关于 websocket 的注解https://cloud.tencent.com/developer/article/1530872
万字长文,一篇吃透WebSocket:概念、原理、易错常识、动手实践 - 码农教程本文章向大家介绍万字长文,一篇吃透WebSocket:概念、原理、易错常识、动手实践,主要包括万字长文,一篇吃透WebSocket:概念、原理、易错常识、动手实践使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。http://www.manongjc.com/detail/25-awrtfwjcoejxpjh.html
https://segmentfault.com/a/1190000018214719https://segmentfault.com/a/1190000018214719WebSocket协议分析 - 功夫Panda - 博客园内容不断更新,目前包括协议中握手和数据帧的分析 1.1 背景 1.2 协议概览 协议包含两部分:握手,数据传输。 客户端的握手如下:GET /chat HTTP/1.1Host: server.exa
https://www.cnblogs.com/caosiyang/archive/2012/08/14/2637721.html还有几篇文章,找不到了,也表示感谢!