要理解socket就要先理解http和tcp的區別,簡單說就是一個是短鏈,一個是長鏈,一個是去服務器拉數據,一個是服務器可以主動推數據。
而socket就是應用層與TCP/IP協議族通信的中間軟件抽象層,它是一組接口。在設計模式中,Socket其實就是一個門面模式,它把復雜的TCP/IP協議族隱藏在Socket接口后面,對用戶來說,一組簡單的接口就是全部,讓Socket去組織數據,以符合指定的協議。-來自網絡。
那么如何用php+js做到服務器推呢?
客戶端
客戶端非常簡單,利用現代瀏覽器的WebSocket API,這里介紹的很詳細:http://msdn.microsoft.com/zh-cn/library/ie/hh673567
核心代碼:
1 2 3 4 5 |
var wsServer = 'ws://127.0.0.1:8080'; var ws = new WebSocket(wsServer); ws.onmessage = function (evt) { do sth }; |
前兩行會向指定服務器發送一個握手請求,如果服務器返回合法的http頭,則握手成功,之后可通過監聽onmessage事件來處理服務器發來的消息。還有很多其他事件可監聽,見前面的url。
服務器
思路
難點是服務器,沒有了apache和nginx這些http服務器在前面頂着,只用php該怎么寫?
這里有個教程講的很深入http://blog.csdn.net/shagoo/article/details/6396089
寫之前捋一捋思路:
1 監聽:首先要掛起一個進程來監聽來自客戶端的請求
2 握手:對於第一次合法的請求,發送合法的header回去
3 保持連接:有新消息到了就廣播出去。直到客戶端斷開
4 接受另一個請求,重復2和3
關鍵代碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
public function start_server() { $this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); //允許使用本地地址 socket_set_option($this->socket, SOL_SOCKET, SO_REUSEADDR, TRUE); socket_bind($this->socket, $this->host, $this->port); //最多10個人連接,超過的客戶端連接會返回WSAECONNREFUSED錯誤 socket_listen($this->socket, $this->maxuser); while(TRUE) { $this->cycle = $this->accept; $this->cycle[] = $this->socket; //阻塞用,有新連接時才會結束 socket_select($this->cycle, $write, $except, null); foreach ($this->cycle as $k => $v) { if($v === $this->socket) { if (($accept = socket_accept($v)) < 0) { continue; } //如果請求來自監聽端口那個套接字,則創建一個新的套接字用於通信 $this->add_accept($accept); continue; } $index = array_search($v, $this->accept); if ($index === NULL) { continue; } if (!@socket_recv($v, $data, 1024, 0) || !$data) {//沒消息的socket就跳過 $this->close($v); continue; } if (!$this->isHand[$index]) { $this->upgrade($v, $data, $index); if(!empty($this->function['add'])) { call_user_func_array($this->function['add'], array($this)); } continue; } $data = $this->decode($data); if(!empty($this->function['send'])) { call_user_func_array($this->function['send'], array($data, $index, $this)); } } sleep(1); } } //增加一個初次連接的用戶 private function add_accept($accept) { $this->accept[] = $accept; $index = array_keys($this->accept); $index = end($index); $this->isHand[$index] = FALSE; } //關閉一個連接 private function close($accept) { $index = array_search($accept, $this->accept); socket_close($accept); unset($this->accept[$index]); unset($this->isHand[$index]); if(!empty($this->function['close'])) { call_user_func_array($this->function['close'], array($this)); } } //響應升級協議 private function upgrade($accept, $data, $index) { if (preg_match("/Sec-WebSocket-Key: (.*)\r\n/",$data,$match)) { $key = base64_encode(sha1($match[1] . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true)); $upgrade = "HTTP/1.1 101 Switching Protocol\r\n" . "Upgrade: websocket\r\n" . "Connection: Upgrade\r\n" . "Sec-WebSocket-Accept: " . $key . "\r\n\r\n"; //必須以兩個回車結尾 socket_write($accept, $upgrade, strlen($upgrade)); $this->isHand[$index] = TRUE; } } |
關鍵地方有那么幾個,一是while(true)掛起進程,不然執行一次后進程就退出了。二是socket_select和socket_accept函數的使用。三是客戶端第一次請求時握手。
socket_select
這個函數是同時接受多個連接的關鍵,我的理解它是為了阻塞程序繼續往下執行和自動選擇當前有活動的連接。
socket_select ($sockets, $write = NULL, $except = NULL, NULL);
$sockets可以理解為一個數組,這個數組中存放的是文件描述符。當它有變化(就是有新消息到或者有客戶端連接/斷開)時,socket_select函數才會返回,繼續往下執行。
$write是監聽是否有客戶端寫數據,傳入NULL是不關心是否有寫變化。
$except是$sockets里面要被排除的元素,傳入NULL是”監聽”全部。
最后一個參數是超時時間
如果為0:則立即結束
如果為n>1: 則最多在n秒后結束,如遇某一個連接有新動態,則提前返回
如果為null:如遇某一個連接有新動態,則返回
為了理解,dump測試一下:
1 2 3 4 5 |
$this->cycle = $this->accept; $this->cycle[] = $this->socket; var_dump($this->cycle);//array(n),n>=1 socket_select($this->cycle, $write, $except, null);//有活動后繼續往下 var_dump($this->cycle);//array(0),n==0 |
這一測就完全明白了,socket_select之前把所有的socket連接都丟進去給它,其中一個有活動時它就把那個連接拋出來給我們用。表達能力有限,大概就是這么個意思。。。
socket_accept
此函數接受唯一參數,即前面socket_create創建的socket文件(句柄)。返回一個新的資源,或者FALSE。本函數將會通知socket_listen(),將會傳入一個連接的socket資源。一旦成功建立socket連接,將會返回一個新的socket資源,用於通信。如果有多個socket在隊列中,那么將會先處理第一個。關鍵就是這里:如果沒有socket連接,那么本函數將會等待,直到有新socket進來。
如果前面不用socket_select在沒有socket的時候阻塞住程序,那么就卡在這里永遠無法結束了。
后面的流程就很清晰了,當有一個新的客戶端請求到達,用socket_accept創建一個資源,並加入到$this->accept連接池里面。並將它的標示isHand設為false,那么下次循環(因為$this->cycle[] = $this->socket;$this->cycle有變化,所以socket_select會返回)的時候就會執行upgrade握手。然后等待它的新消息即可。
程序經調試可以成功運行,php5.3+websocket13。