最近在做的一个项目中需要使用到HTML5中引入的WebSocket技术,本来以为应该很容易就能搞定,谁知道在真正上手开发了以后才发现有很多麻烦的地方,虽然我们是一个以前端开发和设计见长的团队,而且作为一个二手程序猿又长期不被待见,但是为了让有同样需求的朋友少走些弯路,我还是决定把实现方法贴在这个地方。
关于WebSocket的基本概念,维基百科上解释的很清楚,而且网上也能搜出来一大把,这里就略过不表,直接进入正题。
这次的问题首先有一个前提,就是得用Python来实现这个服务器,如果对具体语言没有限制的话,推荐大家首选Node.js的一个第三方库:Socket.IO,非常好用,10分钟不打针不吃药搞定WebSocket Server,而且用JS来写后端,相信也能对上很多文艺开发者的胃口。
但是如果选择用Python,google搜索的结果几乎都不能用,最要命的问题是,WebSocket协议本身还是一个草案,所以不同浏览器支持的协议版本有所不同,Safari 5.1支持的是老版本协议Hybi-02,Chrome 15以及Firefox 8.0支持的是新版本协议Hybi-10,老版本协议和新版本协议在建立通信的握手方法还有数据传输的格式要求上都有所不同,导致网上大多数实现方式只能适用于Safari浏览器,并且Safari和C&F浏览器之间无法互相通信。
首先第一步需要解释的是新、旧版本WebSocket协议的握手方式。我们先来看看三个不同浏览器发送的握手数据的结构:
Chrome:
可以看出,Chrome和Firefox实现的是新版协议,因此只传输了一个”Sec-WebSocket-Key”头以供服务端生成握手Token,但是遵循老版本的Safari的数据中有两个Key:”Sec-WebSocket-Key1″和”Sec-WebSocket-Key2″,因此服务端在生成握手Token的时候,需要做一次判断。先来看使用老版本协议的Safari,Token生成算法如下:
取出Sec-WebSocket-Key1中的所有数字字符形成一个数值,这里是1427964708,然后除以Key1中的空格数目,这里好像是6个空格,得到一个数值,保留该数值整数位,得到数值N1;对Sec-WebSocket-Key2如法炮制,得到第二个整数N2;把N1和N2按照Big-Endian字符序列连接起来,然后再与另外一个Key3连接,得到一个原始序列ser_key。那么Key3是什么呢?大家可以看到在Safari发送过来的握手请求最后,有一个8字节的奇怪的字符串“;”######”,这个就是Key3。回到ser_key,对这个原始序列做md5算出一个16字节长的digest,这就是老版本协议需要的token,然后将这个token附在握手消息的最后发送回Client,即可完成握手。
新版协议生成Token的方法比较简单:首先把Sec-WebSocket-Key和一串固定的UUID “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”做拼接,然后对这个拼接后的字符串做SHA1加密,得到digest以后,做一次base64编码,即可获得Token。
新しいバージョンと古いバージョンのハンドシェイク プロトコルによってクライアントに返されるデータ構造が異なることにも注意してください。それは添付ファイルのサーバーのソース コードに明確に記載されています。それで。
ハンドシェイクの完了は、WebSocket サーバーの機能の半分にすぎません。ただし、Chrome から Safari にメッセージを送信しようとすると、このサーバーが両方のバージョンのブラウザとのリンクを確立できることだけが保証されます。 Safariでは受信できないことがわかりました。この結果の理由は、プロトコルの 2 つのバージョンのデータ フレーミング構造が異なるためです。つまり、ハンドシェイクによって接続が確立された後、クライアントによって送受信されるデータ構造が異なります。
まず、最初のステップは、さまざまなバージョンのプロトコルでクライアントによって送信された元のデータを取得することです。古いバージョンのプロトコルは、実際には元のデータの前に「x00」を追加し、最後に「xFF」を追加します。そのため、Safari クライアントが文字列「test」を送信すると、WebSocket サーバーはそれを実際に受信します。データは「x00testxFF」なので、最初と最後の 2 文字を削除するだけで済みます。
さらに厄介なのは、新バージョンのプロトコルのデータです。 新バージョンのドラフトの説明によると、Chrome と Firefox が送信するデータ パケットは次の部分で構成されます。 まず、固定バイトです。 (1000 0001 または 1000 0002) 、このバイトは無視できます。問題は 2 番目のバイトです。ここでは 2 番目のバイトが 1011 1100 であると仮定します。まず、このバイトの最初のビットは 1 でなければなりません。これは、これが「マスクされた」ビットであることを示し、残りの 7 ビット 0/1 は A でなければなりません。たとえば、ここでの残りの値は 011 1100 で、計算された値は 60 です。この値には次の判断が必要です。
この値が 0000 0000 と 0111 1101 (0 ~ 125) の間の場合、この値は実際のデータの長さを表し、この値が 0111 1110 (126) と正確に等しい場合は、次の 2 ワードが表されます。実際のデータ長を表します。この値が 0111 1111 (127) と正確に等しい場合、次の 8 バイトがデータ長を表します。
この判定の後、データ長を表すバイトがどこで終了するかを知ることができます。たとえば、例 60 では、この値は 0 ~ 125 であるため、2 番目のバイト自体は元のデータの長さが 60 バイトであることを表します。したがって、3 バイト目から始めて 4 バイトをキャプチャできます。このバイト列は「マスク」と呼ばれます (マスク?)。データは実際のデータの兄弟です。このデータは実際のデータにマスクに従ってビット演算を行った後に得られるため、兄弟であると言われます。元のデータを得る方法は、兄弟データの各ビット x と i%4 番目の間で xor を実行することです。マスクのビット。ここで、i は兄弟データ内の x のインデックスです。次のコード スニペットを見ていただければわかると思います:
この方法で生成された back_str は、新しいバージョンのプロトコルを使用する Chrome または Firefox に送信できます。
これで、新旧プロトコルのSocket接続や、異なるバージョン間のデータ転送に対応したシンプルなWebSocketサーバーが完成しました。サーバーのソース コードをダウンロードするには、ここをクリックしてください。TCP サービスの実行にはツイスト フレームワークが使用されているため、コードは参照のみを目的としています。