简介
在HTTP请求中,服务器往往处于被动的一方,通常都是客户端向服务器发送请求时,服务器才会做出响应,服务器并不会主动向客户端推送消息。因此WebSocket API就为此诞生。WebSocket API是HTML5中的一大特色,能够使得建立连接的双方在任意时刻相互推送消息,这意味着不同于HTTP,服务器服务器也可以主动向客户端推送消息了。
关于WebSocket的介绍,可以参考下一篇博文http://blog.csdn.net/zwto1/article/details/52493119#websocket%E5%8E%9F%E7%90%86
WebSocket协议的格式
为了实现一个能与H5的WebSocket API通信的服务器,我们需要先熟悉WebSocket数据包的格式。定的格式。
握手数据包
在一个连接建立以后,建立连接的双方才可以互相推送消息。双方通过握手即可建立一个连接。握手数据包的格式如下:
客户端向服务器发起请求
可以见到,客户端请求连接建立的数据包是一个字符串,而且第一行表明这实际上是一个HTTP报文。其中Connection: Upgrade以及Upgrade: websocket两字段就是用来告知服务器这是一个WebSocket握手请求。
服务器还要关心的一个字段是Sec-WebSocket-Key(倒数第二行),其值是一个随机base64字符串,服务器怎么处理该字符串请往下看。
服务器回应请求
可以看到HTTP状态码为101,同样,服务端也带有Connection和Upgrade字段来表明这是一个WebSocket数据包。
Sec-WebSocket-Accept字段是对请求报文中Sec-WebSocket-Key字段进行摘要运算的结果。其运算过程如下
1、将Sec-WebSocket-Key字段的值与字符串258EAFA5-E914-47DA-95CA-C5AB0DC85B11拼接。
2、对拼接后的字符串进行sha1运算,得到160位摘要(二进制)。
3、以base64的形式表示得到的摘要。
客户端会进行同样的运算,并且与服务器返回来的字段作对比,如果发现二者不相同,连接就无法建立了。
通信数据帧
通信数据帧的格式如下(参考官方文档https://www.rfc-editor.org/rfc/rfc6455.txt)
其中各个字段的含义如下
FIN: 1bit,表示这是否为分片的最后一个数据帧。这是考虑到发送的数据有可能被分片的情况,如果存在分片,将此字段置1就表明这是最后一个分片。如果不存在分片,此字段恒为1。因为只有一个分片就一定是最后一个分片。
RSV1, RSV2, RSV3: 各1bit,全0。现在暂时用不上,为了将来可能用于功能拓展保留的字段。
Opcode: 4bits
指出数据的类型,值的解释如下
值 | 含义 |
---|---|
0x0 | 附加数据帧 |
0x1 | 文本数据帧 |
0x2 | 二进制数据帧 |
0x3-0x7 | 暂无定义 |
0x8 | 关闭连接 |
0x9 | 表示ping |
0xA | 表示pong |
0aB-0xF | 暂无定义 |
MASK: 1bit
表明是否对数据进行掩码运算,置1表示使用掩码。从客户端向服务器发送的数据必须使用掩码。
Payload length: 7 bits, 7+16 bits, or 7+64 bits
表明数据的长度。
如果长度在0-125内,这7bits就表示数据的长度;
如果值为126,紧接着后面2字节(16bits)才表示数据的长度;
如果值为127,后面8字节(64bits)表示数据的长度。
Masking-key: 无 或 4 字节
如果掩码字段(MASK)置0,就不需要Masking-key。如果掩码字段为1,这4字节就是Masking-key,用它与数据部分进行异或运算。
Payload Data: 数据部分,长度可变。
关于其他详细说明可以参考官方文档,例如消息分片规则等。
实现一个WebSocket服务器(群聊天室例子)
为了更加深刻的理解这样一个协议,这里没有使用Java已经封装好操作的类库。
基于NIO监听端口
基于NIO中的ServerSocketChannel,实现一个接收并读取Socket内容的服务端套路如下。
public class WebSocketServer {
private Selector serverSelector;
private WebSocketListener socketListener;
private boolean isRunning = true;
public WebSocketServer(int serverPort, WebSocketListener socketListener) throws IOException {
//初始化ServerSocketChannel
ServerSocketChannel serverSocketChannel =
ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(serverPort));
serverSocketChannel.configureBlocking(false);
//创建选择器
serverSelector = Selector.open();
//注册ServerSocketChannel的ACCEPT事件至选择器
serverSocketChannel.register(serverSelector, SelectionKey.OP_ACCEPT);
this.socketListener = socketListener;
}
public void run() throws IOException {
while (isRunning) {
int selectCount = serverSelector.select();
if (selectCount == 0)
continue;
Iterator<SelectionKey> iterator = serverSelector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey selectKey = iterator.next();
if (selectKey.isAcceptable()) {
//ACCEPT就绪,此时调用ServerSocketChannel的accept()方法可获得连接的SocketChannel对象,将其READ事件注册到选择器,就可以读取内容了。