WebSocket

<!DOCTYPE html>
<html>
<head>
<title>查看源</title>
<link rel="canonical" href="/pages/viewpage.action?pageId=$action.page.id" />
<script>
window.WRM=window.WRM||{};window.WRM._unparsedData=window.WRM._unparsedData||{};window.WRM._unparsedErrors=window.WRM._unparsedErrors||{};
WRM._unparsedData["com.atlassian.plugins.atlassian-plugins-webresource-plugin:context-path.context-path"]="\u0022\u0022";
if(window.WRM._dataArrived)window.WRM.dataArrived();</script>
<link type="text/css" rel="stylesheet" href="/s/2b107a7e58b6a6cc647e0c6632cd043c-CDN/zh_CN/6441/3dfb154e647c41466ff19113f0197feded41ba10/ae872ac6c9ab3e34265d6fd71347d61f/
/download/contextbatch/css/_super/batch.css?atlassian.aui.raphael.disabled=true" data-wrm-key="_super" data-wrm-batch-type="context" media="all">

<link type="text/css" rel="stylesheet" href="/s/d41d8cd98f00b204e9800998ecf8427e-CDN/zh_CN/6441/3dfb154e647c41466ff19113f0197feded41ba10/665c8bb8944fcb9bdcc0d6508200760e/_/download/contextbatch/css/plugin.viewsource,-_super/batch.css?atlassian.aui.raphael.disabled=true" data-wrm-key="plugin.viewsource,-super" data-wrm-batch-type="context" media="all">
<link type="text/css" rel="stylesheet" href="/s/3c1de4a43daa0fe17b5080559cff640e-CDN/zh_CN/6441/3dfb154e647c41466ff19113f0197feded41ba10/0ca31745b1ca288788fbf9e3bb602c3e/
/download/contextbatch/css/page,-_super/batch.css?atlassian.aui.raphael.disabled=true&build-number=6441" data-wrm-key="page,-super" data-wrm-batch-type="context" media="all">
<link type="text/css" rel="stylesheet" href="/s/3c1de4a43daa0fe17b5080559cff640e-CDN/zh_CN/6441/3dfb154e647c41466ff19113f0197feded41ba10/0ca31745b1ca288788fbf9e3bb602c3e/
/download/contextbatch/css/page,-_super/batch.css?atlassian.aui.raphael.disabled=true&build-number=6441&media=print" media="print" data-wrm-key="page,-super" data-wrm-batch-type="context">
<link type="text/css" rel="stylesheet" href="/s/504290b912ad9ed1e8dbba11f193d045-CDN/zh_CN/6441/3dfb154e647c41466ff19113f0197feded41ba10/b8812ebb1e08c662c8a081352dabf379/
/download/contextbatch/css/editor-content,-_super/batch.css?atlassian.aui.raphael.disabled=true&confluence.table.resizable=true&confluence.view.edit.transition=true" data-wrm-key="editor-content,-_super" data-wrm-batch-type="context" media="all">

</head>

<body class="mceContentBody aui-theme-default wiki-content fullsize">
    <p> </p>         <div class="contentLayout2">

<div class="columnLayout single" data-layout="single">
<div class="cell normal" data-type="normal">
<div class="innerCell">

<p>
</p><h1>一、WebSocket定义</h1><p><span>WebSocket协议是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工通信——允许服务器主动推送消息到客户端。</span></p><p><span>WebSocket通信协议于2011年被</span><a href="https://baike.baidu.com/item/IETF">IETF</a><span>定为标准RFC 6455,并被RFC7936所补充规范。</span></p><p>对于以下应用的开发,WebSocket是一种非常自然的选择:</p><ol style="margin-left: 10.0px;"><li>需要玩家之间实时协作的游戏,<a href="https://www.qcloud.com/community/article/585387001484793629?fromSource=gwzcw.93407.93407.93407">你猜我画</a></li><li>实时监控系统</li><li>需要用户进行协作的系统,例如聊天、共享文档的编辑等等。</li></ol><p> </p><p> </p><p> </p><p>看完这段话,大家对WebSocket哪方面比较好奇、想了解的吗?</p><h1>二、服务端推送消息技术方案汇总</h1><p>
分享WebSocket探秘 > 屏幕快照 2017-08-02 下午8.10.26.png" data-location="赵许星 > 分享WebSocket探秘 > 屏幕快照 2017-08-02 下午8.10.26.png" data-image-height="828" data-image-width="810"> </p><ul><li>大象扫码登录</li><li>服务端通知商家有新的外卖订单</li><li>炎热夏天,到家前,远程打开家里的空调</li></ul><p>这些场景,具有一个共同的特点:需要服务器主动推送消息,通知客户端,完成某个流程。</p><p>大家都知道HTTP协议不支持该功能:服务端 推送消息到 客户端!但现实如何<strong>变相</strong>支持该功能的呢?</p><p> </p></div>
</div>

</div>
<div class="columnLayout single" data-layout="single">
<div class="cell normal" data-type="normal">
<div class="innerCell">
<h2>1、技术方案</h2></div>
</div>
</div>
<div class="columnLayout two-equal" data-layout="two-equal">
<div class="cell normal" data-type="normal">
<div class="innerCell">

<h3>a、轮询 Polling</h3><p>
分享WebSocket探秘 > image2017-8-1 13:16:46.png" data-location="赵许星 > 分享WebSocket探秘 > image2017-8-1 13:16:46.png" data-image-height="259" data-image-width="396"></p><p>缺点:(注:设置connection: keep-Alive,一次请求结束后,连接<span style="color: rgb(68,68,68);">保持,等待后续请求</span>)</p><ol><li>时效性差:轮询间隔过长,无法及时获取服务端变更。</li><li>耗用网络带宽:每次请求和应答都带有完整的Http头。</li><li>移动端耗电:存在大量无效请求,通信效率低。</li></ol></div>
</div>
<div class="cell normal" data-type="normal">
<div class="innerCell">
<h3>b、长轮询 Long-Polling</h3><p>
分享WebSocket探秘 > image2017-8-1 13:24:59.png" data-location="赵许星 > 分享WebSocket探秘 > image2017-8-1 13:24:59.png" data-image-height="309" data-image-width="396"></p><p>缺点:(注:设置connection: keep-Alive,一次请求结束后,连接<span style="color: rgb(68,68,68);">保持,等待后续请求</span>)</p><ol><li>服务端有较高的消息量时,和传统轮询在第2、3两点无区别</li></ol></div>
</div>
</div>

<div class="columnLayout two-equal" data-layout="two-equal">
<div class="cell normal" data-type="normal">
<div class="innerCell">

<h3>c、SSE(Server-sent Events)流技术 Streaming</h3><p>
分享WebSocket探秘 > image2017-8-1 13:25:33.png" data-location="赵许星 > 分享WebSocket探秘 > image2017-8-1 13:25:33.png" data-image-height="309" data-image-width="403"></p><p>缺点:</p><ol><li>单向,只支持服务端推动到客户端</li><li>不支持所有的IE浏览器</li></ol><p> </p></div>
</div>
<div class="cell normal" data-type="normal">
<div class="innerCell">
<h3>d、Flash开放自己的Socket接口</h3><p>缺点:</p><ol><li>移动设备覆盖度差,<span style="color: rgb(50,50,50);">2012 年 Adobe 官方宣布不再支持 Android4.1+系统。</span></li></ol></div>
</div>
</div>
<div class="columnLayout single" data-layout="single">
<div class="cell normal" data-type="normal">
<div class="innerCell">
<h2>2、案例</h2><p>给大家找了几个真实案例,大家辨别下各自属于哪种方案。</p><p> </p></div>
</div>
</div>
<div class="columnLayout single" data-layout="single">
<div class="cell normal" data-type="normal">
<div class="innerCell">
<h3><span style="font-size: 16.0px;font-weight: bold;">案例1 </span><a style="font-size: 16.0px;font-weight: bold;" href="http://stock.finance.sina.com.cn/usstock/quotes/.DJI.html">Web版新浪股票</a><span style="font-size: 16.0px;font-weight: bold;">普通版 </span></h3><p>
分享WebSocket探秘 > 屏幕快照 2017-08-01 下午6.54.07.png" data-location="赵许星 > 分享WebSocket探秘 > 屏幕快照 2017-08-01 下午6.54.07.png" data-image-height="886" data-image-width="1300">
分享WebSocket探秘 > 屏幕快照 2017-08-01 00.10.57.png" data-location="赵许星 > 分享WebSocket探秘 > 屏幕快照 2017-08-01 00.10.57.png" data-image-height="170" data-image-width="622"></p><h3>案例2 <a href="https://wx2.qq.com/">微信网页版</a>聊天页</h3><p>
分享WebSocket探秘 > 屏幕快照 2017-08-01 01.59.14.png" data-location="赵许星 > 分享WebSocket探秘 > 屏幕快照 2017-08-01 01.59.14.png" data-image-height="1116" data-image-width="1926">
分享WebSocket探秘 > 屏幕快照 2017-08-01 01.42.04.png" data-location="赵许星 > 分享WebSocket探秘 > 屏幕快照 2017-08-01 01.42.04.png" data-image-height="988" data-image-width="1022"></p><h3>案例3 大象扫描登录</h3><p>
分享WebSocket探秘 > 屏幕快照 2017-08-02 下午8.10.26.png" data-location="赵许星 > 分享WebSocket探秘 > 屏幕快照 2017-08-02 下午8.10.26.png" data-image-height="828" data-image-width="810"></p><h3>案例4 <a href="http://stock.finance.sina.com.cn/usstock/quotes/.DJI.html">Web版新浪股票</a>急速版</h3><p> </p><p>
分享WebSocket探秘 > 屏幕快照 2017-08-01 03.02.06.png" data-location="赵许星 > 分享WebSocket探秘 > 屏幕快照 2017-08-01 03.02.06.png" data-image-height="318" data-image-width="484">
分享WebSocket探秘 > 屏幕快照 2017-08-01 02.42.11.png" data-location="赵许星 > 分享WebSocket探秘 > 屏幕快照 2017-08-01 02.42.11.png" data-image-height="956" data-image-width="2448"></p><p>
分享WebSocket探秘 > 屏幕快照 2017-08-01 00.13.01.png" data-location="赵许星 > 分享WebSocket探秘 > 屏幕快照 2017-08-01 00.13.01.png" data-image-height="1086" data-image-width="1818"></p><p> </p><p> </p><p> </p><p> </p><p>WebSocket是个真汉子,真实的支持服务端主动推动消息到客户端。</p><p><span style="color: rgb(62,62,62);">1千、1万、10万个客户端每秒请求一次服务端,每次HTTP需要<span style="color: rgb(71,71,71);">871字节、WebSocket需要2字节</span>,WebSocket和长轮询之间的带宽消耗差异:<span style="color: rgb(71,71,71);"> </span></span></p><p>
分享WebSocket探秘 > image2017-8-1 0:33:4.png" data-location="赵许星 > 分享WebSocket探秘 > image2017-8-1 0:33:4.png" data-image-height="360" data-image-width="503"></p><p> </p></div>
</div>
</div>

<div class="columnLayout single" data-layout="single">
<div class="cell normal" data-type="normal">
<div class="innerCell">
<h1>三、基于Okhttp3.8版本的WebSocket小Demo</h1><table class="wysiwyg-macro" data-macro-name="code" data-macro-id="2ad41927-cfdf-4e10-a53d-ee322e6bc71a" data-macro-parameters="collapse=true|language=java|linenumbers=true|title=WebSocket客户端代码" data-macro-schema-version="1" style="background-image: url(/plugins/servlet/confluence/placeholder/macro-heading?definition=e2NvZGU6bGFuZ3VhZ2U9amF2YXx0aXRsZT1XZWJTb2NrZXTlrqLmiLfnq6_ku6PnoIF8bGluZW51bWJlcnM9dHJ1ZXxjb2xsYXBzZT10cnVlfQ&locale=zh_CN&version=2); background-repeat: no-repeat;" data-macro-body-type="PLAIN_TEXT"><tr><td class="wysiwyg-macro-body"><pre>//初始化 WebSocket通道
private void initWebSocket() {
mOkHttpClient = new OkHttpClient.Builder()
.build();
mRequest = new Request.Builder()
.url("ws://10.4.227.205:8418")
.build();
mOkHttpClient.newWebSocket(mRequest, mWebSocketListener);
}

// WebSocket通道状态变化监听器
private WebSocketListener mWebSocketListener = new WebSocketListener() {
//通道已建立
public void onOpen(WebSocket webSocket, Response response) {
//向服务端发送消息
webSocket.send("服务端,服务端,收到请回答,收到请回答");
}

//接受来自服务端的String消息
public void onMessage(WebSocket webSocket, String text) {
}
//接受来自服务端的二进制消息
public void onMessage(WebSocket webSocket, ByteString bytes) {
}

//通道关闭中
public void onClosing(WebSocket webSocket, int code, String reason) {
}
//通道已关闭,由于客户端或服务端主动关闭
public void onClosed(WebSocket webSocket, int code, String reason) {
}

//通道初始化失败、发送失败、读取失败
public void onFailure(WebSocket webSocket, Throwable t, Response response) {
    //失败重连
    tryReconnect();
}

};</pre></td></tr></table><p> </p><p> </p></div>
</div>
</div>
<div class="columnLayout single" data-layout="single">
<div class="cell normal" data-type="normal">
<div class="innerCell">
<h1>四、WebSocket协议</h1><p>学习别人的网络协议好处就是,在自己定义协议时有很好的借鉴作用,更利于业务协议完善性,比如鉴权、扩展性、稳定性。WebSocket协议比较简单,建议每个人get到协议中每个字段的作用。</p><p>看完协议之后,我来问大家两个问题:</p><ol><li>WebSocket和Socket有什么区别?</li><li>WebSocket和Http有什么区别?</li></ol><h2>1、建立WebSocket通道过程交互图</h2><p>

</p></div>
</div>
</div>
<div class="columnLayout two-equal" data-layout="two-equal">
<div class="cell normal" data-type="normal">
<div class="innerCell">
<h2>2、Http请求切换到WebSocket协议</h2><p>
分享WebSocket探秘 > 屏幕快照 2017-08-01 下午9.52.51.png" data-location="赵许星 > 分享WebSocket探秘 > 屏幕快照 2017-08-01 下午9.52.51.png" data-image-height="1076" data-image-width="1312"></p></div>
</div>
<div class="cell normal" data-type="normal">
<div class="innerCell">
<p> </p><p>
分享WebSocket探秘 > 屏幕快照 2017-08-02 下午12.55.16.png" data-location="赵许星 > 分享WebSocket探秘 > 屏幕快照 2017-08-02 下午12.55.16.png" data-image-height="1312" data-image-width="1646"></p></div>
</div>
</div>
<div class="columnLayout single" data-layout="single">
<div class="cell normal" data-type="normal">
<div class="innerCell">
<table class="wysiwyg-macro" data-macro-name="code" data-macro-id="b3e6e602-b007-4796-9510-047bdc0ddada" data-macro-parameters="collapse=true|linenumbers=true|title=Http请求切换WebSocket协议解释" data-macro-schema-version="1" style="background-image: url(/plugins/servlet/confluence/placeholder/macro-heading?definition=e2NvZGU6dGl0bGU9SHR0cOivt-axguWIh-aNoldlYlNvY2tldOWNj-iuruino-mHinxsaW5lbnVtYmVycz10cnVlfGNvbGxhcHNlPXRydWV9&locale=zh_CN&version=2); background-repeat: no-repeat;" data-macro-body-type="PLAIN_TEXT"><tr><td class="wysiwyg-macro-body"><pre>/General/
Request URL: wss://dx.neixin.cn:8610/ //wss, ws代表WebSocket请求
Request Method: GET //必须是GET请求
Status Code: 101 Switching Protocols //101 代表服务端已成功完成协议切换

/Request Headers/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.8,en;q=0.6
Cache-Control: no-cache
Host: dx.neixin.cn:8610
Origin: https://x.sankuai.com
Pragma: no-cache
Upgrade: websocket //请求升级到WebSocket 协议
Connection: Upgrade //通道类型,keep-alive:通道长连,close:请求完毕后通道断开,Upgrade:升级协议
Sec-WebSocket-Key: DXra0t8WFqGcEhUUDXhWgg== //客户端随机生成的Key,校验服务器合法性,生成方式:随机16字节再被base64编码
Sec-WebSocket-Version: 13 //版本号
Sec-WebSocket-Extensions:x-webkit-deflate-frame //可选,希望采用的扩展协议
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.104 Safari/537.36

/Response Headers/
Upgrade: websocket //服务端协议已切换到WebSocket
Connection: upgrade
Sec-WebSocket-Accept: +vYpVaR7s3RSl9YgaL3U2EYRz5o= //用于校验WebSocket服务端是否合法,生成方式:客户端请求参数中的 Sec-WebSocket-Key值+258EAFA5-E914-47DA-95CA-C5AB0DC85B11,再进行base64
Date: Tue, 01 Aug 2017 16:19:37 GMT
Server: Tengine</pre></td></tr></table></div>
</div>
</div>
<div class="columnLayout two-equal" data-layout="two-equal">
<div class="cell normal" data-type="normal">
<div class="innerCell">
<h2>3、WebSocket底层通道协议</h2><p>如何向服务端发送一帧消息帧,消息内容是String文本</p><p>如何向服务端发送一帧控制帧,结束WebSocket通道</p><table class="wysiwyg-macro" data-macro-name="code" data-macro-id="19d67453-16a4-4efe-990e-58ca83e71295" data-macro-parameters="language=java|linenumbers=true|theme=Eclipse|title=WebSocket底层通道协议" data-macro-schema-version="1" style="background-image: url(/plugins/servlet/confluence/placeholder/macro-heading?definition=e2NvZGU6bGFuZ3VhZ2U9amF2YXx0aGVtZT1FY2xpcHNlfHRpdGxlPVdlYlNvY2tldOW6leWxgumAmumBk-WNj-iurnxsaW5lbnVtYmVycz10cnVlfQ&locale=zh_CN&version=2); background-repeat: no-repeat;" data-macro-body-type="PLAIN_TEXT"><tr><td class="wysiwyg-macro-body"><pre>WebSocket Frame Format:
+---------------------------------------------------------------+
0 1 2 3
0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+-+-+-+-+-------+-+-------------+-------------------------------+
|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 ... |
                      +---------------------------------------------------------------+</pre></td></tr></table></div>
                      </div>
                      <div class="cell normal" data-type="normal">
                      <div class="innerCell">
                      <p> </p><p> </p><p> </p><p> </p><ol><li><p>FIN: 1 bit 。表示此帧是否是消息的最后帧,第一帧也可能是最后帧。</p></li><li><p>RSV1,RSV2,RSV3: 各1 bit 。必须是0,除非协商了扩展定义了非0的意义。</p></li><li><p>opcode:4 bit。表示被传输帧的类型:</p><ol><li><p>x0 表示一个后续帧;</p></li><li><p>x1 表示一个文本帧;</p></li><li><p>x2 表示一个二进制帧;</p></li><li><p>x3-7 为以后的非控制帧保留;</p></li><li><p>x8 表示一个连接关闭close;</p></li><li><p>x9 表示一个ping;</p></li><li><p>xA 表示一个pong;</p></li><li><p>xB-F 为以后的控制帧保留。</p></li></ol></li><li><p>Mask: 1 bit。表示净荷是否有掩码(只适用于客户端发送给服务器的消息)。</p></li><li><p>Payload length: 7 bit, 7 + 16 bit, 7 + 64 bit。 净荷长度由可变长度字段表示: 如果是 0~125,就是净荷长度;如果是 126,则接下来 2 字节表示的 16 位无符号整数才是这一帧的长度; 如果是 127,则接下来 8 字节表示的 64 位无符号整数才是这一帧的长度。</p></li><li><p>Masking-key:0或4 Byte。 用于给净荷加掩护,客户端到服务器标记。</p></li><li><p>Extension data: x Byte。默认为0 Byte,除非通过字段Sec-WebSocket-Extensions协商了扩展协议。</p></li><li><p>Application data: y Byte。 要发送的消息,在”Extension data”之后,占据了帧的剩余部分。</p></li><li><p>Payload data: (x + y) Byte。”extension data” + “application data”。</p></li></ol></div>
                      </div>
                      </div>
                      <div class="columnLayout two-equal" data-layout="two-equal">
                      <div class="cell normal" data-type="normal">
                      <div class="innerCell">
                      <h2>4、WebSocket应用层协议</h2><p>由业务方自定义</p><table class="relative-table wrapped confluenceTable" style="width: 47.0301%;"><colgroup><col style="width: 30.9343%;" /><col style="width: 26.1995%;" /><col style="width: 42.9293%;" /></colgroup><tbody><tr><th class="confluenceTh"><pre>protocolType协议类型</pre></th><th class="confluenceTh">说明</th><th colspan="1" class="confluenceTh"><pre>operationType操作码</pre></th></tr><tr><td colspan="1" class="confluenceTd">3</td><td colspan="1" class="confluenceTd">PushOrderOperation 推送订单相关操作</td><td colspan="1" class="confluenceTd"><table class="wrapped confluenceTable"><tbody><tr><th class="confluenceTh"><pre>operationType</pre></th><th class="confluenceTh">说明</th></tr><tr><td class="confluenceTd"><p>7</p></td><td class="confluenceTd">接单</td></tr><tr><td class="confluenceTd">2</td><td class="confluenceTd">获取新订单信息</td></tr></tbody></table></td></tr><tr><td class="confluenceTd">11</td><td class="confluenceTd">.BindPoiRequest 门店绑定</td><td colspan="1" class="confluenceTd">无</td></tr><tr><td class="confluenceTd">12</td><td class="confluenceTd">.BindPoiResponse 门店绑定结果</td><td colspan="1" class="confluenceTd">无</td></tr><tr><td class="confluenceTd">13</td><td class="confluenceTd">.Error 主动推送的错误</td><td colspan="1" class="confluenceTd">无</td></tr></tbody></table><p> </p><p> </p></div>
                      </div>
                      <div class="cell normal" data-type="normal">
                      <div class="innerCell">
                      <p> </p><p> </p><table class="wysiwyg-macro" data-macro-name="code" data-macro-id="6e419c71-ff6c-4997-9d8a-0fb3220ff8f3" data-macro-parameters="title=云端下发接单命令" data-macro-schema-version="1" style="background-image: url(/plugins/servlet/confluence/placeholder/macro-heading?definition=e2NvZGU6dGl0bGU95LqR56uv5LiL5Y-R5o6l5Y2V5ZG95LukfQ&locale=zh_CN&version=2); background-repeat: no-repeat;" data-macro-body-type="PLAIN_TEXT"><tr><td class="wysiwyg-macro-body"><pre>{
                      "protocolId":"61874bc5-d84e-4b8b-a778-87a3d10e4aae",
                      "protocolType":3, //协议类型
                      "protocolName":"PushOrderOperation",
                      "timestamp":11111,
                      "data":
                      {
                      "platformId":1,
                      "platformName":"elm",
                      "operationType":7, //操作码
                      "operationName":"confirmOrder",
                      "pushContent":[
                      {
                      "name":"orderId_placeholder",
                      "value":"123456789"
                      }
                      ]
                      },
                      "error":null
                      }</pre></td></tr></table></div>
                      </div>
                      </div>
                      <div class="columnLayout single" data-layout="single">
                      <div class="cell normal" data-type="normal">
                      <div class="innerCell">
                      <h1>五、OkHttp3.8源码实现核心流程</h1><p>
                      </p><table class="wysiwyg-macro" data-macro-name="code" data-macro-id="5f555ea6-4054-4626-b7a7-daad38f1e326" data-macro-parameters="collapse=true|linenumbers=true|title=Client端WebSocket简版源码" data-macro-schema-version="1" style="background-image: url(/plugins/servlet/confluence/placeholder/macro-heading?definition=e2NvZGU6dGl0bGU9Q2xpZW5056uvV2ViU29ja2V0566A54mI5rqQ56CBfGxpbmVudW1iZXJzPXRydWV8Y29sbGFwc2U9dHJ1ZX0&locale=zh_CN&version=2); background-repeat: no-repeat;" data-macro-body-type="PLAIN_TEXT"><tr><td class="wysiwyg-macro-body"><pre>简版

@Override public WebSocket newWebSocket(Request request, WebSocketListener listener) {
//1
RealWebSocket webSocket = new RealWebSocket(request, listener, new Random());
webSocket.connect(this);
return webSocket;
}

public void connect(OkHttpClient client) {
client = client.newBuilder()
.protocols(ONLY_HTTP1)
.build();
//2
final Request request = originalRequest.newBuilder()
.header("Upgrade", "websocket")
.header("Connection", "Upgrade")
.header("Sec-WebSocket-Key", key)
.header("Sec-WebSocket-Version", "13")
.build();
call = Internal.instance.newWebSocketCall(client, request);
call.enqueue(new Callback() {
@Override public void onResponse(Call call, Response response) {
try {
//3
checkResponse(response);
} catch (ProtocolException e) {
failWebSocket(e, response);
closeQuietly(response);
return;
}

  try {
    listener.onOpen(RealWebSocket.this, response);
    //4
    loopReader();
  } catch (Exception e) {
    failWebSocket(e, null);
  }
}

});
}

void checkResponse(Response response) throws ProtocolException {
if (response.code() != 101) {
throw new ProtocolException("Expected HTTP 101 response but was '"
+ response.code() + " " + response.message() + "'");
}

String headerConnection = response.header("Connection");
if (!"Upgrade".equalsIgnoreCase(headerConnection)) {
throw new ProtocolException("Expected 'Connection' header value 'Upgrade' but was '"
+ headerConnection + "'");
}

String headerUpgrade = response.header("Upgrade");
if (!"websocket".equalsIgnoreCase(headerUpgrade)) {
throw new ProtocolException(
"Expected 'Upgrade' header value 'websocket' but was '" + headerUpgrade + "'");
}

String headerAccept = response.header("Sec-WebSocket-Accept");
String acceptExpected = ByteString.encodeUtf8(key + WebSocketProtocol.ACCEPT_MAGIC)
.sha1().base64();
if (!acceptExpected.equals(headerAccept)) {
throw new ProtocolException("Expected 'Sec-WebSocket-Accept' header value '"
+ acceptExpected + "' but was '" + headerAccept + "'");
}
}

/读取控制帧、消息帧/
public void loopReader() throws IOException {
while (receivedCloseCode == -1) {
reader.processNextFrame();
}
}

void processNextFrame() throws IOException {
readHeader();
if (isControlFrame) {
readControlFrame();
} else {
readMessageFrame();
}
}

private void readHeader() throws IOException {
if (closed) throw new IOException("closed");

int b0;
long timeoutBefore = source.timeout().timeoutNanos();
source.timeout().clearTimeout();
try {
b0 = source.readByte() & 0xff;
} finally {
source.timeout().timeout(timeoutBefore, TimeUnit.NANOSECONDS);
}

opcode = b0 & B0_MASK_OPCODE;
isFinalFrame = (b0 & B0_FLAG_FIN) != 0;
isControlFrame = (b0 & OPCODE_FLAG_CONTROL) != 0;

boolean reservedFlag1 = (b0 & B0_FLAG_RSV1) != 0;
boolean reservedFlag2 = (b0 & B0_FLAG_RSV2) != 0;
boolean reservedFlag3 = (b0 & B0_FLAG_RSV3) != 0;

if (reservedFlag1 || reservedFlag2 || reservedFlag3) {
throw new ProtocolException("Reserved flags are unsupported.");
}

int b1 = source.readByte() & 0xff;

isMasked = (b1 & B1_FLAG_MASK) != 0;
if (isMasked == isClient) {
// Masked payloads must be read on the server. Unmasked payloads must be read on the client.
throw new ProtocolException(isClient
? "Server-sent frames must not be masked."
: "Client-sent frames must be masked.");
}

// Get frame length, optionally reading from follow-up bytes if indicated by special values.
frameLength = b1 & B1_MASK_LENGTH;
if (frameLength == PAYLOAD_SHORT) {
frameLength = source.readShort() & 0xffffL; // Value is unsigned.
} else if (frameLength == PAYLOAD_LONG) {
frameLength = source.readLong();
if (frameLength < 0) {
throw new ProtocolException(
"Frame length 0x" + Long.toHexString(frameLength) + " > 0x7FFFFFFFFFFFFFFF");
}
}

frameBytesRead = 0;
}

private void readMessageFrame() throws IOException {
int opcode = this.opcode;
if (opcode != OPCODE_TEXT && opcode != OPCODE_BINARY) {
throw new ProtocolException("Unknown opcode: " + toHexString(opcode));
}

Buffer message = new Buffer();
readMessage(message);

//不要在onReadMessage做耗时操作,因为当前是读线程,影响服务端Push消息时效性
if (opcode == OPCODE_TEXT) {
frameCallback.onReadMessage(message.readUtf8());
} else {
frameCallback.onReadMessage(message.readByteString());
}
}

/发送消息/
private synchronized boolean send(ByteString data, int formatOpcode) {
//写缓存队列容量有限制 最大16M
if (queueSize + data.size() > MAX_QUEUE_SIZE) {
close(CLOSE_CLIENT_GOING_AWAY, null);
return false;
}

// Enqueue the message frame.
queueSize += data.size();
messageAndCloseQueue.add(new Message(formatOpcode, data));
//5
runWriter();
return true;
}

private void runWriter() {
assert (Thread.holdsLock(this));

if (executor != null) {
executor.execute(writerRunnable);
}
}

this.writerRunnable = new Runnable() {
@Override public void run() {
try {
while (writeOneFrame()) {
}
} catch (IOException e) {
failWebSocket(e, null);
}
}
};
}

//发送到服务端的消息通过掩码处理进行加密
public static void toggleMask() {
//原始数据
byte[] data = new String("我是demo,我要4字节掩码加密").getBytes();
//四个字节的掩码
byte[] mask = {0x24, 0x48, 0x23, 0x54};

for (int i = 0; i < data.length; i++) {
    data[i] = (byte) (data[i] ^ mask[i % 4]);
}
//data即可变成 加密或解密后的数据

}</pre></td></tr></table><p>除了Okhttp3.5+支持WebSocket,还有<a href="https://github.com/koush/AndroidAsync">AndroidAsync</a> 、<a href="https://github.com/socketio/socket.io-client-java">socket.io-client-java</a></p><p> </p></div>
</div>
</div>
<div class="columnLayout single" data-layout="single">
<div class="cell normal" data-type="normal">
<div class="innerCell">
<h1>六、全家桶项目实践</h1><ol><li>需要建立断开后自动重连机制。<ol><li>检测断开,由协议底层定时ping和pong检测,当写失败代表断开,可能是网络等问题造成</li><li>重连时机,通路状态WebSocketListener回调onFailure()</li><li>重连策略,无限次重连,首次断开立刻重连,重连间隔步进值10s,最大重连间隔120s</li></ol></li><li>通过回传ACK统计关键消息到达率,以及服务端消息重传机制。</li><li>一帧的最大传输量是多少? 协议上,最大传输 2^63 ,实际上,取决于两端的消息缓存区容量,Okhttp定义的发送缓存区为16M。</li><li>在客户端onMessage回调中,不要做耗时操作,影响Push消息时效性。</li><li>在Android中运行WebSocketServer ,Android4.4+版本WebView中的H5可以通过WebSocket与本地Server直连通信。</li><li>WebSocket如何把异步调用 转化为 同步调用?实现同步的HTTP请求-响应模型?比如通过WebSocket完成通过OrderId同步查询OrderInfo的过程。<br /><ol><li>查询OrderInfo的消息体里加一个唯一消息ID,另外有一个与此ID对应的CallBack,并由Map维护关系</li><li>服务器端回复的消息里也加上同样的ID,客户端收到后找到ID对应的CallBack回调即可。</li><li>查询线程在未收到结果前一直阻塞,直到CallBack回调setResult</li></ol></li></ol><p> </p></div>
</div>
</div>
<div class="columnLayout single" data-layout="single">
<div class="cell normal" data-type="normal">
<div class="innerCell">
<h1>七、总结</h1><ol><li>服务端推送消息功能技术方案汇总</li><li>WebSocket协议和源码实现</li><li>WebSocket实践经验</li></ol><p>服务端与客户端之间,存在频繁的双向通信、要求高时效性,用于交换数据 或 相互控制的场景,可选用WebSocket。</p><h1 class="t">八、留给大家一个问题</h1><p>不管HTTP协议还是WebSocket协议都是人制订的。</p><p><a href="https://www.baidu.com/link?url=atV73bKs5neq5ErUDNBQ0VkmiHe663I4hVDXgGePGLFSKSuoUVN4-4rLtLSK3jrk-WP_Vo_jx5hGm2DjN5Oe0a&wd=&eqid=821f036400034d5e00000003596b866c">为什么HTTP协议要求完成一次请求后,WebServer要主动断开Tcp连接呢 ? 为什么HttpWebServer不能主动推送消息呢?</a></p><p> </p><p> </p><p> </p><p> </p><p> </p><p> </p><p> </p><p> </p><p> </p><p> </p><p> </p><p>备注</p><ol><li>WebSocket协议地址 <a href="https://tools.ietf.org/html/rfc6455">https://tools.ietf.org/html/rfc6455</a></li><li>各方案的实现 <a href="http://blog.zhangruipeng.me/2015/10/22/Web-Connectivity/">http://blog.zhangruipeng.me/2015/10/22/Web-Connectivity/</a></li><li>后端均衡负载<a href="http://www.cnblogs.com/549294286/p/5067865.html">http://www.cnblogs.com/549294286/p/5067865.html</a></li><li>各方案对比</li></ol><table class="relative-table wrapped confluenceTable" style="width: 99.7559%;"><colgroup><col style="width: 12.6263%;" /><col style="width: 15.2258%;" /><col style="width: 14.1117%;" /><col style="width: 19.7564%;" /><col style="width: 38.0273%;" /></colgroup><thead><tr><th class="confluenceTh">对比项</th><th class="confluenceTh">传统轮询</th><th class="confluenceTh">长轮询</th><th class="confluenceTh">服务器发送事件</th><th class="confluenceTh">WebSocket</th></tr></thead><tbody><tr><td class="confluenceTd">浏览器支持</td><td class="confluenceTd">几乎所有现代浏览器</td><td class="confluenceTd">几乎所有现代浏览器</td><td class="confluenceTd">Firefox 6+ Chrome 6+ Safari 5+ Opera 10.1+</td><td class="confluenceTd">IE 10+ Edge Firefox 4+ Chrome 4+ Safari 5+ Opera 11.5+</td></tr><tr><td class="confluenceTd">服务器负载</td><td class="confluenceTd">较少的CPU资源,较多的内存资源和带宽资源</td><td class="confluenceTd">与传统轮询相似,但是占用带宽较少</td><td class="confluenceTd">与长轮询相似,除非每次发送请求后服务器不需要断开连接</td><td class="confluenceTd">无需循环等待(长轮询),CPU和内存资源不以客户端数量衡量,而是以客户端事件数衡量。四种方式里性能最佳。</td></tr><tr><td class="confluenceTd">客户端负载</td><td class="confluenceTd">占用较多的内存资源与请求数。</td><td class="confluenceTd">与传统轮询相似。</td><td class="confluenceTd">浏览器中原生实现,占用资源很小。</td><td class="confluenceTd">同Server-Sent Event。</td></tr><tr><td class="confluenceTd">延迟</td><td class="confluenceTd">非实时,延迟取决于请求间隔。</td><td class="confluenceTd">同传统轮询。</td><td class="confluenceTd">非实时,默认3秒延迟,延迟可自定义。</td><td class="confluenceTd">实时。</td></tr><tr><td class="confluenceTd">实现复杂度</td><td class="confluenceTd">非常简单。</td><td class="confluenceTd">需要服务器配合,客户端实现非常简单。</td><td class="confluenceTd">需要服务器配合,而客户端实现甚至比前两种更简单。</td><td class="confluenceTd"><p>需要Socket程序实现和额外端口,客户端实现简单。</p><p> </p></td></tr></tbody></table></div>
</div>
</div>
</div>
<p> </p>
</body>
</html>

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

推荐阅读更多精彩内容