Websocket协议原理与实现(一)

作者: 黑岛様 | 来源:发表于2015-04-28 18:45 被阅读5054次

最近时间空闲,稍微研究了一下聊天系统的搭建,深入了解了它的实现原理,那就顺便整理一下成文章好了。我主要是写Android的,所以具体的分析会以移动端的聊天系统搭建为主。

一个聊天系统说复杂也不复杂,但是要实现一个稳健的系统,考虑的事情是非常多的。最基本的就是聊天协议的处理。常见的即时通讯协议有XMPP,Websocket,大公司一般会自己定义协议,如腾讯、网易之类的,他们用的都是自己的协议。我看的源码是Leancloud的即时通讯组件,他们的聊天是基于Websocket的,所以这篇博文的主题是Websocket。Leancloud的Android即时通讯组件里,Websocket的封装用的是Github上的一个开源项目,Nathan RajlichJava-Websocket,这是一个“100%Java写的极简Websocket客户端和服务端实现”。

文章的一个大致框架

  • Websocket 协议的简单介绍
  • 协议的封装与传输
  • Websocket 客户端的实现

Websocket协议的简单介绍

Websocket是一种在单个TCP连接上进行全双工通讯的协议,双工(duplex)是指两台通讯设备之间,允许有双向的资料传输。全双工的是指,允许两台设备间同时进行双向资料传输。这是相对于半双工来说的,半双工不能同时进行双向传输,这期间的区别相当于手机和对讲机的区别,手机在讲话的同时也能听到对方说话,对讲机只能一个说完另一个才能说。

长话短说,在Websocket协议中,客户端和服务端只需要做一个握手的动作,就能形成一条通道,两者之间可以进行数据互相传送。

所以WebSocket协议分为两部分:

  1. 握手
  2. 数据传输

握手

客户端发送一个请求

GET / HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: example.com
Origin: null
Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==
Sec-WebSocket-Version: 13

服务器回应

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: fFBooB7FAkLlXgRSz0BT3v4hq5s=
Sec-WebSocket-Origin: null
Sec-WebSocket-Location: ws://example.com/

握手时,客户端发送一个随机的Sec-WebSocket-Key,服务端根据这个key做一些处理,返回一个Sec-WebSocket-Accept的值给客户端,具体的原理在后面的文章中再具体说。

数据传输

这是Websocket的数据传输协议,聊天信息一般会按照这个协议的规则来传输,下图中的一整个东西称为一个数据帧,数据帧的成帧和解析是处理这个协议时最麻烦的一部分了。具体这个表怎么看可以参照

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 ...                |
+---------------------------------------------------------------+

具体每一bit的意思
FIN      1bit 表示信息的最后一帧
RSV 1-3  1bit each 以后备用的 默认都为 0
Opcode   4bit 帧类型,稍后细说
Mask     1bit 掩码,是否加密数据,默认必须置为1 (这里很蛋疼)
Payload  7bit 数据的长度
Masking-key      1 or 4 bit 掩码
Payload data     (x + y) bytes 数据
Extension data   x bytes  扩展数据
Application data y bytes  程序数据

协议的封装与传输

1.握手协议的封装与传输

Handshake类是根据请求头

GET / HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: example.com
Origin: null
Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==
Sec-WebSocket-Version: 13

来封装的,由于这个请求头的字段顺序是随便的,我们可以用一个map来存储,发送消息时再写入Socket的输出流

下面是Handshakedata类,为了文章易读,简化了代码

public class Handshakedata
{
    private byte[] content;                 //请求体,在握手协议中一般为空
    private TreeMap<String, String> map;  //用于存储请求头

}

初始化握手请求头,为了代码容易理解,稍微对Java-Websocket中的代码作了修改。

public Handshakedata postProcessHandshakeRequestAsClient(Handshakedata request)
{
    request.put("Upgrade", "websocket");
    request.put("Connection", "Upgrade");
    request.put("Sec-WebSocket-Version", "8");    

    byte[] random = new byte[16];
    this.reuseableRandom.nextBytes(random);     //生成一个随机的Sec-WebSocket-Key
    request.put("Sec-WebSocket-Key", Base64.encodeBytes(random));

    return request;
}

生成数据帧,由于是通过Socket传输消息,最终传输的内容要写入到Socket的OutputStream中,需要一个把握手消息转换成bytebuffer的方法,再通过这个bytebuffer写入流中

public ByteBuffer createHandshake(Handshakedata handshakedata) {
    StringBuilder bui = new StringBuilder(100);
    bui.append("GET ");
    bui.append(handshakedata.getResourceDescriptor());
    bui.append(" HTTP/1.1");
    bui.append("\r\n");
    Iterator it = handshakedata.iterateHttpFields();
    while (it.hasNext()) {
        String fieldname = (String)it.next();
        String fieldvalue = handshakedata.getFieldValue(fieldname);
        bui.append(fieldname);
        bui.append(": ");
        bui.append(fieldvalue);
        bui.append("\r\n");
    }
    bui.append("\r\n");
    byte[] httpheader = Charsetfunctions.asciiBytes(bui.toString());

    byte[] content = withcontent ? handshakedata.getContent() : null;
    ByteBuffer bytebuffer = ByteBuffer.allocate((content == null ? 0 : content.length) + httpheader.length);
    bytebuffer.put(httpheader);
    bytebuffer.flip();
    return bytebuffer;
}

最后写入Socket的流中

ByteBuffer buffer = (ByteBuffer)WebSocketClient.this.engine.outQueue.take();   //从消息队列中取出刚才转换好的bytebuffer
WebSocketClient.this.ostream.write(buffer.array(), 0, buffer.limit());    //this.ostream = this.socket.getOutputStream() Socket的输出流
WebSocketClient.this.ostream.flush();   //刷新,发送消息

以上是客户端发送握手协议的过程。

客户端接收服务端回应

服务端接收到客户端的握手请求后,需要返回响应

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: fFBooB7FAkLlXgRSz0BT3v4hq5s=
Sec-WebSocket-Origin: null
Sec-WebSocket-Location: ws://example.com/

收到这一段响应后,客户端需要比对Sec-WebSocket-Accept值,这个值表示服务器同意握手建立连接,是客户端传输过来的Sec-WebSocket-Key跟“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”拼接后,用SHA-1加密,并进行BASE-64编码得来的。

客户端收到Sec-WebSocket-Accept后,将本地的Sec-WebSocket-Key进行同样的编码,然后比对。

public Draft.HandshakeState acceptHandshakeAsClient(ClientHandshake request, ServerHandshake response)
throws InvalidHandshakeException
  {
    if ((!request.hasFieldValue("Sec-WebSocket-Key")) || (!response.hasFieldValue("Sec-WebSocket-Accept"))) {
      return Draft.HandshakeState.NOT_MATCHED;
    }

//Sec-WebSocket-Key和Sec-WebSocket-Accept进行比对
    String seckey_answere = response.getFieldValue("Sec-WebSocket-Accept");
    String seckey_challenge = request.getFieldValue("Sec-WebSocket-Key");
    seckey_challenge = generateFinalKey(seckey_challenge);

    if (seckey_challenge.equals(seckey_answere))
      return Draft.HandshakeState.MATCHED;
    return Draft.HandshakeState.NOT_MATCHED;
  }

 //产生Sec-WebSocket-Accept的方法
private String generateFinalKey(String in) { 
    String seckey = in.trim();
    String acc = seckey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
    MessageDigest sh1;
    try {
      sh1 = MessageDigest.getInstance("SHA1");
    } catch (NoSuchAlgorithmException e) {
      throw new RuntimeException(e);
    }
    return Base64.encodeBytes(sh1.digest(acc.getBytes()));
  }

2.数据的封装与传输

....待续

相关文章

网友评论

本文标题:Websocket协议原理与实现(一)

本文链接:https://www.haomeiwen.com/subject/nojvfttx.html