標籤:int 引用 位元運算 erro version 資料轉換 accept 方法 toc
一.簡介
http請求只能由用戶端主動發起,伺服器響應的模式, 伺服器無法主動向用戶端推資料,websocket的出現完美的解決了這一問題。
websocket和http處於同一層,都是基於TCP協議的,用戶端和伺服器使用websocket通訊的時候需要握手和傳輸資料兩步,
握手藉助http狀態代碼101 switch protocol從http協議轉換到websocket協議,之後便和http協議無關了。
二.握手
websocket首先由瀏覽器主動發起一個http請求,主要要求標頭內容如下:
Connection: 告知伺服器當前請求串連是升級的
Upgrade: websocket Upgrade 告訴伺服器這個http連結是升級的websocket連結
Sec-WebSocket-Version: 13 協議版本
Sec-WebSocket-Key: IYiYjdXLDgHybP4teMOnsQ== 驗證key
伺服器回應標頭如下
HTTP/1.1 101 Switching Protocols 表示變換協議
Upgrade: websocket
Connection:Upgrade 伺服器返回的告知用戶端同意使用升級並使用websocket協議,用來完善HTTP升級響應
Sec-WebSocket-Accept:Ev/nT3aIpWH9deAfyYMPbBwkQWo= 用戶端 Sec-WebSocket-Key經過加密後的字串演算法 base64_encode(sha1(Sec-WebSocket-Key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11));
三.資料幀構造和解析
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 ... | +---------------------------------------------------------------+
構造協議文本幀的演算法(PHP)
其中的opcode為1代表一個文本幀
private function encode($data){ $len = strlen($data); $encode = ‘‘; if($len < 126){ $encode = chr(0x81) . chr($len) . $data; }else if($len >= 126 && $len <= 65535){ $low = $len & 0x00FF; $high = ($len & 0xFF00) >> 8; $encode = chr(0x81) . chr(0x7F) . chr($high) . chr($low) . $data; }else{ encode = chr(0x81) . } return $encode;}
如果playload len < 126,則playloadlen 是資料的真實長度
如果playload len = 126,資料的長度等於playload len後面2個位元組對應的不帶正負號的整數就是資料的真實長度
如果playload len = 127,資料的長度等於playload len後面8個位元組對應的不帶正負號的整數就是資料的真實長度
之前對位元運算並不熟悉,這裡也寫下構建資料幀詳細的步驟
php使用chr將資料轉換為標準ascii所指定的單個字元
長度 < 126 FIN + RSV1 + RSV2 + RSV3 + opcode = 0x81 = 10000001, 再加上資料長度和資料
長度 >=126 <= 65535 FIN + RSV1 + RSV2 + RSV3 + opcode = 0x81 = 10000001 加上 Payload len = 0x7E = 126 由於ASCII範圍為 0-127即1個位元組,所以必須將2個位元組拆分成單個位元組即高位$high和低位$low來表示
$low = $len & (11111111 = 0x00FF)這樣就取得了$len第二個位元組的值。 因為$len是兩個位元組 取第一個位元組的值需要 $len & (1111111100000000 = 0xFF00) 然後向右移8個位
解析文本幀的演算法
private function decode($data){ if(!$data) return array(); //第一個位元組和00001111按位與運算取得的後4位元據就是opcode $opcode = ord(substr($data, 0, 1)) & 0x0f; //第二個位元組和10000000按位與運算,保留第一位的值,然後右移7位取得的就是ismask $ismask = (ord(substr($data, 1, 1)) & 0x80) >> 7; //第二個位元組和01000000按位與運算取得後7位的值就是playloadlen $playloadlen = ord(substr($data, 1, 1)) & 0x7f; $cdata = $maskkey = $decode = ‘‘; if($playloadlen < 126){ $maskkey = substr($data, 2, 4); $cdata = substr($data, 6); }else if($playloadlen == 126){ $maskkey = substr($data, 4, 4); $cdata = substr($data, 8); }else if($playloadlen == 127){ $maskkey = substr($data, 10, 4); $cdata = substr($data, 14); } if($cdata && $maskkey){ for($i = 0; $i < strlen($cdata); $i++){ $decode{$i} = $cdata{$i} ^ $maskkey[$i % 4]; } $decode = join(‘‘, $decode); } return array($opcode, $ismask, $decode);}
websocket規定用戶端發送給服務端的資料必須經過掩碼處理,伺服器端發送給用戶端的資料無需掩碼處理,
解碼演算法: 將playload的未經處理資料的每個字元下標與4模數,然後將這個原始字元與前面模數後相應位置的掩碼字元進行異或運算即可
data[i] = source[i] ^ maskkey[i / 4];
四.PHP服務端
之前對於socket的select方法也不是很瞭解,
function socket_select (array &$read, array &$write, array &$except, $tv_sec, [, int $tv_usec = 0 ])
1.新串連到來時,被監聽的連接埠是活躍的,如果是新資料到來或者用戶端關閉連結時,活躍的是對應的用戶端socket而不是伺服器上被監聽的連接埠
2.如果用戶端發來資料沒有被讀走,則socket_select將會始終顯示用戶端是活躍狀態並將其儲存在read數組中
3.如果用戶端先關閉了,則必須手動關閉伺服器上相對應的用戶端socket,否則socket_select也始終顯示該用戶端活躍(這個道理跟"有新串連到來然後沒有用socket_access把它讀出來,導致監聽的連接埠一直活躍"是一樣的)
$read是一個引用變數,每次執行的時候傳入我們需要監聽的socket資源,執行過後,返回活躍的socket資源,核心虛擬碼如下
$socket = socketcreate();
$socket_select = array($socket);
while(true){
$read = $socket_select;
socket_select($read, $write, $except, null);
foreach($read as $sock){
if($sock == $socket){//新串連到來時
}else{//用戶端發送資料或者用戶端關閉的時候
}
}
}
五.用戶端
用戶端websocket api就很簡單了
// 建立一個 websocket 串連var ws = new WebSocket("ws:XXXXX");// websocket 建立成功事件ws.onopen = function () {};// websocket 接收到訊息事件ws.onmessage = function (e) { var msg = JSON.parse(e.data);}// websocket 錯誤事件ws.onerror = function () {};//websocket 關閉事件ws.close = function () {};
完整代碼在我的github
參考文章
http://www.cnblogs.com/zhenbianshu/p/6111257.html
http://blog.csdn.net/u010487568/article/details/20908427?utm_source=tuicool&utm_medium=referral
PHP webSocket實現網頁聊天室