WebSocket是一种在Web应用程序中提供双向通信的协议。它允许服务器主动向客户端推送数据,而不是像传统的HTTP请求-响应模式一样,客户端必须发送请求才能获取数据。WebSocket 最早是在HTML5中引入的,建立在HTTP协议之上,使用握手阶段来升级连接,然后通过保持连接的状态来实现实时通信。
与传统的HTTP协议相比,WebSocket具有以下优势:
⑴ 增强实时性
服务器可以随时主动给客户端下发数据,相对于HTTP请求需要等待客户端发起请求服务端才能响应,延迟明显更少。和传统的轮询比较,WebSocket也可以在短时间内更有效率地传递数据;
⑵ 维持连接状态
在一些需要身份认证的场景下,HTTP请求可能需要在每个请求都携带状态信息(服务器不记录每次的请求和响应信息),而WebSocket一次连接建立后就会保持住会话状态,这就使其成为一种有状态的协议,后续通信时就可以省略部分状态信息;
⑶ 更灵活的扩展支持
开发者可以对WebSocket自定义二进制帧,相对HTTP,可以更轻松地处理二进制内容,此外开发者也自行扩展协议、实现部分自定义的子协议;
⑷ 更好的压缩效果
WebSocket在适当的扩展支持下,可以沿用之前内容的上下文,在传递类似的数据时,可以显著地提高压缩率。
- 第0个字节
0位(FIN):0表示报文没有结束,1表示报文结束。
1-3位(RSV1、RSV2、RSV3):保留字段,一般全部为0。也可用于扩展自己的协议。
4-7位(opcode):报文类型。0 代表一个继续帧,1代表文本帧,2代表二进制帧,8 代表连接关闭,9 代表ping,10代表pong。
- 第1个字节
8位(MASK):1表示需要掩码操作,0表示不需要。如果为1,数据帧的masking-key属性会存在一个值,接收方会利用这个值来进行解掩码操作,所有从客户端传输到服务器的数据帧的Mask都被设置为1。
9-15位(Payload len):表示Payload data的长度。如果值是0~125,则真实长度就是前7位表示;如果值是126,则真实长度就是后16位(Extended payload length 126);如果值是127,则真实长度就是后64位(Extended payload length 127)
- ping帧,Opcode=9,Mask=1,Payload len=0,Masking-Key有内容
图片
- pong帧,Opcode=10,Mask=0,Payload len=0,Masking-Key为空
- 文本帧,Opcode=1,Mask=0,Payload len=84,Masking-Key为空
接下来,一起动手编写WebSocket服务端和客户端,这里提供一个java版的服务端demo和一个html版的客户端demo。
- 引用spring-boot-starter-websocket依赖
<!--WebSocket--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId></dependency>
- session管理
publicclassWsSessionManager{publicstaticConcurrentHashMap<String,WebSocketSession>SESSION_POOL=newConcurrentHashMap<>();publicstaticvoidadd(Stringkey,WebSocketSessionsession){SESSION_POOL.put(key,session);}publicstaticWebSocketSessionremove(Stringkey){returnSESSION_POOL.remove(key);}publicstaticvoidremoveAndClose(Stringkey){WebSocketSessionsession=remove(key);if(session!=null){try{session.close();}catch(IOExceptione){e.printStackTrace();}}}publicstaticWebSocketSessionget(Stringkey){returnSESSION_POOL.get(key);}}
- 消息处理
@Component@Slf4jpublicclassMyWsHandlerextendsAbstractWebSocketHandler{@OverridepublicvoidafterConnectionEstablished(WebSocketSessionsession)throwsException{log.info("建立ws连接,sessionId:{}",session.getId());WsSessionManager.add(session.getId(),session);}@OverrideprotectedvoidhandleTextMessage(WebSocketSessionsession,TextMessagemessage)throwsException{// 获得客户端传来的消息Stringpayload=message.getPayload();log.info("server 接收到消息, sessionId "+session.getId()+", payload: "+payload);session.sendMessage(newTextMessage("server 发送给的消息 "+payload+",发送时间:"+LocalDateTime.now().toString()));}@OverrideprotectedvoidhandleBinaryMessage(WebSocketSessionsession,BinaryMessagemessage){log.info("发送二进制消息, sessionId "+session.getId());}@OverridepublicvoidhandleTransportError(WebSocketSessionsession,Throwableexception){log.error("异常处理, sessionId "+session.getId());
- WebSocket配置
@Configuration@EnableWebSocketpublicclassWsServerConfigimplementsWebSocketConfigurer{@AutowiredprivateMyWsHandlermyWsHandler;@OverridepublicvoidregisterWebSocketHandlers(WebSocketHandlerRegistryregistry){registry.addHandler(myWsHandler,"myWs")//允许跨域.setAllowedOrigins("*");}}
<!doctype html><form name="publish"><input type="text"name="message"maxlength="50"/><input type="submit"value="Send"/></form><div id="messages"></div><script>let url='ws://127.0.0.1:8889/myWs';let socket=newWebSocket(url);// send message from the formdocument.forms.publish.onsubmit=function(){let outgoingMessage=this.message.value;socket.send(outgoingMessage);returnfalse;};// handle incoming messagessocket.onmessage=function(event){let incomingMessage=event.data;showMessage(incomingMessage);};socket.onclose=event=>console.log(`Closed${event.code}`);// show message in div#messagesfunctionshowMessage(message){let messageElem=document.createElement('div');messageElem.textContent=message;document.getElementById('messages').prepend(messageElem);
WebSocket适用于服务器端需要快速向浏览器发送消息的场景,例如网页游戏、视频网站、在线文档、运维工具等。浏览器可以看做是一个瘦客户端,它不提供直接操作tcp长连接的编程接口,也无法简单地集成消息组件客户端。在使用浏览器作为客户端的场景下,WebSocket是最常用的服务器端主动推送方案。
当然,有很多种技术可以实现服务器端向客户端主动推送消息,WebSocket只是其中一种,其他常见的方案还有tcp长连接、消息组件(如mqtt、kafka)等,不过各有优缺点,这个可以在日后进一步学习。