Websocket协议原理与实现(一)

最近时间空闲,稍微研究了一下聊天系统的搭建,深入了解了它的实现原理,那就顺便整理一下成文章好了。我主要是写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.数据的封装与传输

....待续

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容

  • 我的地址 :http://blog.csdn.net/jinglijun/article/details/9365...
    傻傻小萝卜阅读 1,336评论 0 1
  • WebSocket简介 谈到Web实时推送,就不得不说WebSocket。在WebSocket出现之前,很多网站为...
    吧啦啦小汤圆阅读 8,129评论 15 75
  • 儿子现在小学五年级,从小没有像当下其他孩子那样,被各种兴趣班“包围”。幼儿园里,我也一直是老师口中:另类家...
    巧克力0310阅读 186评论 0 2
  • 同学新婚,很好的朋友,也算个小型的同学聚会,大家其乐融融。本来是应该欢乐的日子,可我又开始伤悲了。 我一见油油的菜...
    青灯古阅读 303评论 0 0
  • 1.思维笔的空性 本来我想在店里好好写点东西,但是旁边的人一直在看电视,很噪杂,让我没法安静下来。 思维笔的空性,...
    柔光宝宝阅读 132评论 0 0