一、使用Tomcat提供的WebSocket库
Java可以使用Tomcat提供的WebSocket库接口实现WebSocket服务,代码编写也非常的简单。现在的H5联网游戏基本上都是使用WebSocket协议,基于长连接,服务器可以主动推送消息,而不是传统的网页采用客户端轮询的方式获取服务器的消息。下面给出简单使用Tomcat的WebSocket服务的基本代码结构。
1 @ServerEndpoint("/webSocket") 2 public class WebSocket { 3 @OnOpen 4 public void onOpen(Session session) throws IOException{ 5 logger.debug("新连接"); 6 } 7 @OnClose 8 public void onClose(){ 9 logger.debug("连接关闭"); 10 } 11 @OnMessage 12 public void onMessage(String message, Session session) throws IOException { 13 logger.debug("收到消息"); 14 } 15 @OnError 16 public void onError(Session session, Throwable error){ 17 error.printStackTrace(); 18 } 19 }
二、WebSocket协议的整个流程
1. 基于TCP协议
WebSocket本质是基于TCP协议的,采用Java编写WebSocket服务时可以使用NIO或者AIO实现高并发的服务。
2. 握手过程
客户端采用TCP协议连接服务器指定端口后,首先需要发送一条HTTP的握手协议
GET /web HTTP/1.1 Upgrade: websocket Connection: Upgrade Host: 127.0.0.1:8001 Origin: http://127.0.0.1:8001 Sec-WebSocket-Key: hj0eNqbhE/A0GkBXDRrYYw== Sec-WebSocket-Version: 13
请求的头里面必须包含以下内容:
1. Connection 其值为Upgrade,表示升级协议
2. Upgrade 其值为websocket,表示升级为WebSocket协议
3. Sec-WebSocket-Key 客户端发送给服务器的密钥,用于标识每个客户端,其值是16位的随机base64编码。
4. Sec-WebSocket-Version WebSocket的协议版本
服务器收到这条协议验证成功后进行协议升级,并且不会关闭Socket连接,并发送给客户端响应升级握手成功的HTTP协议包。
HTTP/1.1 101 Switching Protocols Content-Length: 0 Upgrade: websocket Sec-Websocket-Accept: ZEs+c+VBk8Aj01+wJGN7Y15796g= Connection: Upgrade Date: Wed, 21 Jun 2017 03:29:14 GMT
响应的协议包里面,首先是101的状态码,更换协议;其中最重要的就是Sec-WebSocket-Accept字段。其值是通过客户端的Key加上固定的"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"密钥,通过采用16位的base64编码后发送给客户端验证,如果客户端也验证成功就表示握手完成。
1 String acc = secKey + WEBSOCK_MAGIC_TAG; 2 MessageDigest sh1 = MessageDigest.getInstance("SHA1"); 3 String key = Base64.getEncoder().encodeToString(sh1.digest(acc.getBytes()));
3. 数据的读写
握手成功后就可以进行数据发送和读取,WebSocket的数据可以是二进制或者纯文本。每次读取和发送数据需要打包成帧数据,需要按照其标准的格式进行发送或读取才能够正常的进行数据通信。
上图就是帧数据的结构图,解析帧数据的代码如下,由于是摘录的部分代码,所以只能作为理解和参考,不可直接使用。
1 protected WebSocketFrameData ParseFrame(NetPacketBuffer bytes){ 2 bytes.mark(); 3 WebSocketFrameData frame = new WebSocketFrameData(); 4 int opData = bytes.readByte(); 5 frame.UnPackOpCodeHeader(opData); // 第一步 6 int length = frame.UnPackMaskHeader(bytes.readByte()); // 第二步 7 // 读取长度 8 if (length == 126) { 9 length = bytes.readShort(); 10 } else if (length == 127){ 11 length = (int) bytes.readInt64(); 12 } 13 // 数据不足,进来的是半包 14 if(length + 4 > bytes.remaining()){ 15 bytes.reset(); // 16 return null; 17 } 18 // 读取mask if frame.mMasked 19 byte[] masks = new byte[4]; // 第三步 20 for (int i = 0; i < 4; i++) { 21 masks[i] = (byte) bytes.readByte(); 22 } 23 frame.mLength = length; 24 frame.mData = bytes.readMulitBytes(length); 25 frame.MaskData(masks); // 第四步 26 return frame; 27 }
上面代码中第一步是解析出当前帧是否是最后帧mFin标记、操作码mOpCode,采用位处理,具体的实现如下。
1 public void UnPackOpCodeHeader(int opData){ 2 mRsv1 = (opData & 64) == 64; 3 mRsv2 = (opData & 32) == 32; 4 mRsv3 = (opData & 16) == 16; 5 6 mFin = (opData & 128) == 128; 7 mOpCode = (opData & 15); 8 }
第二步在读取长度前,先解析当前帧是否有采用Mask掩码加密处理,并且里面有可能包含整个帧的长度信息,具体看上面的判断代码。
1 public int UnPackMaskHeader(int mkData){ 2 mMasked = (mkData & 128) == 128; 3 return (mkData & 127); // 这里返回的是长度信息 4 }
接下来就是读取Mask内容,注意只有客户端发送给服务端时需要采用Mask对数据做处理,服务端发送给客户端时不需要做处理。最后通过Mask掩码解析出真实数据。
1 public void MaskData(byte[] masks){ 2 if (!mMasked or masks.length == 0) return ; 3 for (int i = 0; i < mLength; i++) { 4 mData[i] = (byte) (mData[i] ^ masks[i % 4]); 5 } 6 }
以上就解析出单帧的数据,帧数据可以分为消息数据(细分为文本数据和二进制数据)、PING包、PONG包、CLOSE包、CONTINUATION包(数据未发送完成包)。而且帧数据又有mFin标记数据是否完整,否则需要将多个帧数据合成一个完整的消息数据。
1 // 读取帧数据,可能存在多帧数据,因此需要手动拆分 2 WebSocketFrameData frame = ParseFrame(mCachePacket); 3 if(frame == null){ 4 break; // 说明数据不完整,暂不处理。 5 } 6 // 不完整的帧的时候,只有第一帧会标记帧的类型 7 opCode = opCode == -1? frame.mOpCode: opCode; 8 mCacheFrame.append(frame.mData, 0, frame.mLength); 9 if(!frame.mFin) // 非完整的数据不处理。 10 { 11 continue; 12 } 13 // 处理完整的数据 14 switch(opCode) 15 { 16 case WebSocketFrameData.OP_TEXT: 17 case WebSocketFrameData.OP_BINARY: 18 mCacheFrame.flip(); 19 this.OnMessage(mCacheFrame, opCode); 20 break; 21 case WebSocketFrameData.OP_PING: 22 this.OnPing(mCacheFrame); 23 break; 24 case WebSocketFrameData.OP_PONG: 25 this.OnPong(mCacheFrame); 26 break; 27 case WebSocketFrameData.OP_CLOSE: 28 this.OnClosed(); 29 break; 30 case WebSocketFrameData.OP_CONTINUATION: 31 this.Close(); 32 break; 33 } 34 opCode = -1; 35 mCacheFrame.clear();
读取整个客户端的协议数据流程就已经完成了,服务端发送回去的数据就只需要注意两点:
1. 大的数据包需要分帧数据发送。
2. 不需要采用Mask掩码加密,因此Mask位置设置为0,并且不写入掩码数据。
三、最后
WebSocket协议已经在H5的游戏中使用了,学习有助于以后工作中的使用.文章来自我的公众号,大家如果有兴趣可以关注,具体扫描关注下图。