當初做爲編程小白的我,剛剛從過後臺工做,以爲http是個很牛逼的東西,然然後面隨着本身深刻學習並實踐以後,以爲原來和我所想的天壤之別,沒你們想象的那麼複雜,僅僅是個協議嘛!。後面學習的東西多了,慢慢的就淡定了。今天這裏之因此要講websocket,而不是其它的協議,從某種意義上來講(請容許我裝個逼),更能說明問題,若是你把websocket都搞懂了,那麼http對於你來講,簡直就是雕蟲小技啊,關於websocket的代碼,之前我使用C和C++寫的,可是爲了PHP的coder(PHP是世界上最好的語言)能明白,我用PHP從新寫了一遍,可是個精簡版,對於咱們完全搞懂websocket,理解它的精華所在,已經足夠了。代碼我已經上傳到了碼雲(php-websocket-base-implemention),請你們必定必定要下載下來,並親自運行,實踐纔是檢驗真理的惟一標準啊,代碼是徹底能夠運行的,若是運行的時候有障礙,請聯繫我。該博文差很少修修改改了3天(幸好公司裏面事很少),儘量的給你們講清楚。忽然感受,寫文章好累啊,這都不重要,但願你們可以看懂,否則我寫的就沒啥用了。更但願你們遇到不懂的,提出疑問。寫完以後,我再次審查了當前博文的內容,修改了一些拼寫錯誤,可能還會有一些漏網之魚,但願你們多多指正。php
在閱讀這篇博文以前,須要你們有必定的基礎知識儲備,下面我會給你們列出來,先裝一下逼html
)基本的socket編程技能,若是你不知道,也不要慌,以防萬一,我已經爲你們準備好了,請參考PHP 編寫基本的 Socket 程序git
由於在通常的php編程當中,不多遇到會有位操做的狀況,因此遺忘和不熟悉就理所固然了,咱們能夠參考php官方文檔,可是我仍是要講一點,異或(^)操做,請看下面,這個結論很重要,請你們必定要記住,切記切記,重要的事情講三遍。web
a ^ b = c 能夠推導出 c ^ b = a
複製代碼
是否是有的時候打開一個文件顯示亂碼,就像下面這樣算法
由於你打開的是二進制數據, 二進制數據和文本數據的最根本的區別就是在數字的存儲,舉個例子,假設數字 int a=100,咱們假設它會佔用4個字節的空間,可是注意了,若是將它做爲字符串存儲,結果只須要三個字節(每一位佔用一個字節),文本軟件無論這些啊,都當作文本,顯示的內容就成了亂碼了。所以若是某個二進制文件不是你寫入的,想要解析它的內容,不太現實。之因此存在這種說法,是由於不一樣的CPU架構下,多字節數據在內容中的存儲格式有所不一樣,這裏咱們以int(假設爲4字節)數據m(數據採用16進制格式)爲例,m=0x12345678,來進行說明,請仔細體會a,b,c,d的內存地址依次增大。編程
從上面的分析能夠知道,當咱們從網絡數據中解析多字節數據時,是必定要考慮字節的順序的,這就是我這裏着重強調的緣由。瀏覽器
Websocket協議現在應用很是普遍,,形成這一現象的很大緣由,在於http協議的短暫性,客戶端和服務器之間每一次的請求應答都須要創建TCP三次握手,這對於流量很大的服務器來講是很是恐怖的(系統級資源),因此這個時候websocket誕生了,具體的誕生日期是哪一年已經不得而知了,可是真正的標準化時間是在2011年,由IETF正式完成,具體請參考RFC6455。bash
下面有一張圖,能夠說明這一點,該圖片來自Google,服務器
websocket協議和http協議都屬於應用層協議(在TCP/IP之上),可是websocket協議相對於http協議多了一個握手(這個握手不是平時所說的tcp三次握手啊,注意了)的過程,從上面的圖能夠很清晰的看出來,http是是一個文本協議,可是websocket有所不一樣,它有本身嚴格的字節格式,稍後會講到。websocket
該協議由2部分組成,握手和數據傳輸,握手部分並不複雜,而且握手是創建在HTTP協議之上的,下面咱們先來看一下協議的握手過程。
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
複製代碼
服務器響應以下:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
複製代碼
不管是請求或者是響應包,頭部字段的順序是沒有要求的,這其中有些字段相信你們都很是熟悉了,就算不熟悉,百度一下,仍是很容易搞清楚的,咱們來仔細的討論一下Websocket所特有的一些字段:
這個字段表示須要升級到的協議,這個字段是必須的,而且它的值必須是websocket。
這個字段表示須要升級協議,也是必須的,它的值必須是Upgrade。
這個是用來客戶端和服務器握手使用的,必須傳遞,由於服務器會使用這個值進行必定的轉換而後回傳給客戶端,客戶端再檢查這個值, 是否和本身計算的值同樣,若是不同,那麼客戶端會認爲,服務端是有問題的,那麼結果只能是鏈接失敗了。在介紹具體的操做以前,咱們還須要介紹一個常量GUID,它的值爲258EAFA5-E914-47DA-95CA-C5AB0DC85B11,這個值是固定的,任何的Websocket服務器和客戶端(包括瀏覽器)必須定義這個值。如今咱們重點來看一下這個字段,假如客戶端傳遞的值爲 dGhlIHNhbXBsZSBub25jZQ==,那麼用PHP代碼來表示的話,就會是下面這樣:
$GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
$sec_websocket_key = "dGhlIHNhbXBsZSBub25jZQ==";
$result = base64_encode(sha1($sec_websocket_key . $GUID));
複製代碼
這個計算出的$result值最終會被回傳給客戶端的http響應頭Sec-WebSocket-Accept,客戶端會驗證這個值,這個就是客戶端的事了。
websocket協議的版本號,根據RFC6455的文檔,咱們知道,這個值必須是13,其它的任何值都不行,下面是它的描述:
The request MUST include a header field with the name |Sec-WebSocket-Version|. The value of this header field MUST be 13. NOTE: Although draft versions of this document (-09, -10, -11,and -12) were posted (they were mostly comprised of editorial changes and clarifications and not changes to the wire protocol), values 9, 10, 11, and 12 were not used as valid values for Sec-WebSocket-Version. These values were reserved in the IANA registry but were not and will not be used.
選擇websocket所使用的子協議,這個字段不是必須的,取決於具體的實現,若是你使用的是Google瀏覽器的話,那麼這個值是不會傳遞的。
在講解完了Websocket主要的http頭部字段以後,咱們來看一下服務端的檢查代碼,這裏我把實例程序中的代碼貼出來,給你們分析一哈
/**
* @param $client_socket_handle
* @throws Exception
*/
private function shakehand($client_socket_handle)
{
if (socket_recv($client_socket_handle, $buffer, 1000, 0) < 0) {
throw new Exception(socket_strerror(socket_last_error($this->socket_handle)));
}
while (1) {
if (preg_match("/([^\r]+)\r\n/", $buffer, $match) > 0) {
$content = $match[1];
if (strncmp($content, "Sec-WebSocket-Key", strlen("Sec-WebSocket-Key")) == 0) {
$this->websocket_key = trim(substr($content, strlen("Sec-WebSocket-Key:")), " \r\n");
}
$buffer = substr($buffer, strlen($content) + 2);
} else {
break;
}
}
//響應客戶端
$this->writeToSocket($client_socket_handle, "HTTP/1.1 101 Switching Protocol\r\n");
$this->writeToSocket($client_socket_handle, "Upgrade: websocket\r\n");
$this->writeToSocket($client_socket_handle, "Connection: upgrade\r\n");
$this->writeToSocket($client_socket_handle, "Sec-WebSocket-Accept:" . $this->calculateResponseKey() . "\r\n");
$this->writeToSocket($client_socket_handle, "Sec-WebSocket-Version: 13\r\n\r\n");
}
複製代碼
首先咱們從客戶端socket中讀取1000字節的內容,這1000的字節足以讀出全部的頭部了(可是在企業級代碼中,咱們不能這麼寫,咱們永遠不能假設整個http頭部有多大,在這片博文中,咱們爲了突出問題的重點,簡化了不少代碼,可是你放心,對咱們來講,絲毫沒有影響,socket_recv請參考我上面所說的),接下來的while循環遍歷咱們讀取到的內容,要看懂循環裏面的代碼,咱們有必要提下http協議的格式了,看下圖
我以爲上面的圖片,已經足以描述http協議的格式了,若是你還不懂,不要緊,給你們推薦一篇來自簡書的博文( HTTP協議格式詳解),如今對於咱們來講,最關心的是當前請求的Sec-WebSocket-Key頭部,由於這個值須要返回給客戶端,獲取到這個值以後,咱們把它存儲在當前對象中。緊接着咱們須要迴應客戶端吧,若是你不知道它的格式,我稍微講一下:對於websocket握手來講,若是服務端贊成客戶端的鏈接的話,那麼返回的狀態碼必須是101 ,至於後面的文本,不必定得是 Switching Protocol,只是別人都這麼傳,那就這麼傳了。其次,Upgrade: websocket,Connection: upgrade還有Sec-WebSocket-Version: 13,必須傳遞給客戶端,這個是固定的,應該沒有啥難度吧,另外的,Sec-WebSocket-Accept咱們前面已經說了,它的計算代碼,我上面已經貼出來了,這個計算方式也是固定的,千萬不要忘記每一行後面得有\r\n啊,最後一行後面得有兩個\r\n。
看了上面握手的代碼以後,是否是以爲本身要上天了,感受真是太簡單了??騷年,醒醒,醒醒。哈哈,真實太年輕了,年輕就是好
看到我上面貼出來的websocket數據包格式了麼,是時候解開它面紗的時候了,這部分可能有點兒難度,不要怕,有我在。下面我來來個原子級別的分析。
FIN位,也是整個片斷的第一個字節的最高位,他只能是0或者是1,這個位的做用只有一個,若是它爲1,表示這個片斷是整個消息的最後一個片斷,若是是0,表示這個片斷以後,還有其它的片斷。是否是聽着直接懵逼了,啥是 片斷?啥是 消息?很是好,看來我裝逼的時候已經來臨了,廢話很少說。爲了搞清楚這幾個概念,代碼爲敬
(new WebSocket()).send("我是奧巴馬");
複製代碼
這是一段JAVASCRIPT代碼,send函數的參數就是一條消息,很是短,可是注意了,咱們不能假設任什麼時候間,任何地點,都這麼短,當它變得很長的時候,客戶端就有可能對它進行切割,好比,我有一個字符串,大小爲4M,我把它分爲4個1M的字符串,那麼每個1M的字符串,就只能成爲一個片斷,每一個片斷獨立發送,四個片斷組合在一塊兒造成了一條消息,每個片斷的格式都是固定的,格式和上面的貼圖是同樣的,按照剛纔說的,前面的三個片斷,FIN都是0,第四個纔是1,清楚了麼?So easy!!
這三位是保留給擴展使用的,基本不會用到,反正我沒用到,因此咱們能夠把它們當作空氣就行,永遠設置爲0,就是這麼果斷。
opcode顧名思義就是操做碼,佔用第一個字節的低四位,因此opcode能夠表明16種不一樣的值。你是否是想問,opcode是用來幹嗎的? opcode是用 來解析當前片斷的載荷(攜帶的數據)的,具體的後面會再次說明。
表示當前片斷所攜帶的數據是否通過加密,位置爲第二個字節的最高位,總共1位,它的值不是你想設置就設置的啊,RFC6455 明確規定,全部從客戶端發送給服務器的數據必須加密,因此mask的值必須是1。還有,全部從服務器發往客戶端的數據,必定不能加密,因此呢,mask必須爲0,就是這麼簡單粗暴。
這部分是用來定義負載數據的長度的,總共7位,因此最大值爲127,就這麼簡單?哼哼,不會的。
它的位置緊接着數據長度的後面,大小爲0或者是4個字節。前面分析了mask的做用,若是mask爲1的話,數據須要加密,此時mask key佔用4個字節,不然長度爲0,至於mask key如何用來解密數據的,後面會再次講到。
這裏就是咱們從客戶端接收到的數據,不過它是通過加密的,「我是奧巴馬」,以前payload_length的長度,就是通過加密以後的數據的長度,而不是原始數據的長度。
講解完上面的內容以後,咱們能夠開始分析如何用php來解析Websocket消息片斷了。
這篇博文的開頭我就說過了,當前的websocket實現會專一於websocket最爲精華,最困難的部分,因此會忽略掉一些內容,若是你理解了下面講的內容,其他的一些細枝末節都不是問題。
//等待客戶端新傳輸的數據
if (!socket_recv($client_socket_handle, $buffer, 1000, 0)) {
throw new Exception(socket_strerror(socket_last_error($client_socket_handle)));
}
//解析消息的長度
$payload_length = ord($buffer[1]) & 0x7f;//第二個字符的低7位
if ($payload_length >= 0 && $payload_length < 125) {
$this->current_message_length = $payload_length;
$payload_type = 1;
echo $payload_length . "\n";
} else if ($payload_length = 126) {
$payload_type = 2;
$this->current_message_length = ((ord($buffer[2]) & 0xff) << 8) | (ord($buffer[3]) & 0xff);
echo $this->current_message_length;
} else {
$payload_type = 3;
$this->current_message_length =
(ord($buffer[2]) << 56)
| (ord($buffer[3]) << 48)
| (ord($buffer[4]) << 40)
| (ord($buffer[5]) << 32)
| (ord($buffer[6]) << 24)
| (ord($buffer[7]) << 16)
| (ord($buffer[8]) << 8)
| (ord($buffer[7]) << 0);
}
複製代碼
對於上面的代碼,下面進行逐行解析
$payload_length = ord($buffer[1]) & 0x7f;//第二個字符的低7位
複製代碼
讀取第二個字節的低7位,也就是以前討論的payload_length,0x7f轉換爲二進制就是01111111,ord($buffer[1]) 就是把第二個字符轉換爲對應的ASCII數值,兩個進行與運算,就能夠獲得第二個字節的低7位對應的數值(與運算不熟悉的朋友,請先查看我在這篇博文前面給你們指定的連接),
if ($payload_length >= 0 && $payload_length < 125) {
$this->current_message_length = $payload_length;
$payload_type = 1;
echo $payload_length . "\n";
}
複製代碼
當payload_length的長度小於125的話,數據長度就等於片斷長度。
if ($payload_length = 126) {
$payload_type = 2;
$this->current_message_length = ((ord($buffer[2]) & 0xff) << 8) | (ord($buffer[3]) & 0xff);
echo $this->current_message_length;
}
複製代碼
當payload_length的長度等於126的時候,就有些麻煩了,此時第3和第4個字節組合爲一個無符號16位整數,還記得咱們以前說的,網絡字節序嗎?高位字節在前,低位字節在後面,因此當咱們讀的時候,第3個字節就是高8位,第4個字節就是低8位,因此咱們首先將高8位左移8位再和低8位作或運算。
$payload_type = 3;
$this->current_message_length =
(ord($buffer[2]) << 56)
| (ord($buffer[3]) << 48)
| (ord($buffer[4]) << 40)
| (ord($buffer[5]) << 32)
| (ord($buffer[6]) << 24)
| (ord($buffer[7]) << 16)
| (ord($buffer[8]) << 8)
| (ord($buffer[9]) << 0);
複製代碼
當payload_length的長度等於127的時候,此時的第3到第10位組合爲一個無符號64位整數,因此最高的8位須要左移56位,後面的依次類推,低8位保持不動。
//解析掩碼,這個必須有的,掩碼總共4個字節
$mask_key_offset = ($payload_type == 1 ? 0 : ($payload_type == 2 ? 2 : 8)) + 2;
$this->mask_key = substr($buffer, $mask_key_offset, 4);
複製代碼
要找到maskey,首先必須找到它在當前片斷的偏移,若是payload_length<=125,那麼偏移就是2,若是payload_length==126,那麼偏移就是(2+2)=4,若是payload_length>126,那麼偏移就是(2+8)=10,同時mask key的大小爲4個字節,因此找到了偏移和長度,mask key就能夠獲取到了。
//獲取加密的內容
$real_message = substr($buffer, $mask_key_offset + 4);
$i = 0;
$parsed_ret = '';
//解析加密的數據
while ($i < strlen($real_message)) {
$parsed_ret .= chr((ord($real_message[$i]) ^ ord(($this->mask_key[$i % 4]))));
$i++;
}
複製代碼
解密數據的第一步就是要找到加密數據在當前片斷中的偏移,很簡單,這個值等於maskkey的偏移(上面已經求過了)+maskkey自己的長度4,那麼怎麼來解密數據呢?看上面的代碼,就能夠看出來,解密的過程其實就是遍歷加密數據的每個字符的ASCII值和數據(當前遍歷的位置對4取模,得出的數據一定是0,1,2,3,將得出的數據找到maskkey對應位置的ASCII值)進行異或運算求得,這個算法是RFC6455規定的,全世界都是這樣。
從客戶端發送到服務器和服務器傳遞給客戶端的數據格式都遵循着一樣的數據包格式,因此在個人實現中,代碼以下:
function echoContentToClient($client_socket, $content)
{
$len = strlen($content);
//第一個字節
$char_seq = chr(0x80 | 1);
$b_2 = 0;
//fill length
if ($len > 0 && $len <= 125) {
$char_seq .= chr(($b_2 | $len));
} else if ($len <= 65535) {
$char_seq .= chr(($b_2 | 126));
$char_seq .= (chr($len >> 8) . chr($len & 0xff));
} else {
$char_seq .= chr(($b_2 | 127));
$char_seq .=
(chr($len >> 56)
. chr($len >> 48)
. chr($len >> 40)
. chr($len >> 32)
. chr($len >> 24)
. chr($len >> 16)
. chr($len >> 8)
. chr($len >> 0));
}
$char_seq .= $content;
$this->writeToSocket($client_socket, $char_seq);
}
複製代碼
爲了簡便起見,第一個字節中FIN=1,opcode設置爲1,接下來檢查數據的長度,這部份內容和解析數據長度的步驟恰好相反,就再也不分析了,若是你把以前的都看懂了,這裏也應該沒有問題,可是特別注意了,以前咱們就已經提到過,服務器返回給客戶端的數據,不能加密,因此mask必須設置爲0,mask key的長度爲0。
就和本篇博文開篇所提到的,我寫了一個簡單的websocket實現,請必定要下載本身運行起來,光看是沒有用的:php-websocket-base-implemention
如何運行websocket服務器爲了你能夠看到實際運行的結果,請打開websocket.html文件,頁面上出現這個就表示運行成功了。
運行以前,請檢查端口8080是否被佔用,固然你能夠修改websocket.html,改成其餘的均可以,確保不被佔用就能夠了,若是你仍然沒法運行,請聯繫我,若是你想看到其餘的內容,也請修改websocket.html文件,而後重啓服務器。
本篇博文的目的僅僅是爲了向你們簡要的介紹websocket最爲核心的內容,還有一些內容沒有講到(剩下的不難,感興趣的本身能夠去實現),出於讓你們更爲直觀的看清楚websocket的目的,代碼中去掉了錯誤檢查等內容,所以並不嚴謹,祝你學習愉快。
若是你有什麼問題,請聯繫我,歡迎你們加入QQ羣:971572229