1. 程式人生 > >websocket+php socket實現聊天室

websocket+php socket實現聊天室

原文地址:http://www.cnblogs.com/nickbai/articles/6169745.html

 這兩天用了點時間,研究了一下,用php socket+ websocket實現了一個小型的聊天室。我採用的是 select/poll 的同步模型,雖然扛不住很大的併發,但是理論上 維持 幾百人線上還是可以的。

目前完成了第一版。這一版的由於採用的是 select/poll 和單程序,所以在win下面就可以執行。不需要額外的其他擴充套件支援。

 

我最近在 看雲 發表了 ThinkPHP5+workerman+layIM打造聊天系統 教程,感興趣的可以去看看。傳送門:

ThinkPHP5+workerman+layIM打造聊天系統

ichat v3.0 版本正在和 layim 合作中,詳情可以參看 layim.layui.com。現開通了 ichat 線上預覽功能。地址 :ichat v3預覽 

 

  專案的依賴

  php版本大於 5.4,瀏覽器支援 websocket和localstorage。

  看一下核心的服務端程式碼吧:

複製程式碼

  1 <?php
  2 /**
  3  * author: NickBai
  4  * createTime: 2016/12/9 0009 下午 4:17
  5  */
  6 namespace NickBai;
  7 
  8 class SocketChat
  9 {
 10     private $timeout = 60;  //超時時間
 11     private $handShake = False; //預設未牽手
 12     private $master = 1;  //主程序
 13     private $port = 2000;  //監聽埠
 14     private static $connectPool = [];  //連線池
 15     private static $maxConnectNum = 1024; //最大連線數
 16     private static $chatUser = [];  //參與聊天的使用者
 17 
 18 
 19     public function __construct( $port = 0 )
 20     {
 21         !empty( $port ) && $this->port = $port;
 22         $this->startServer();
 23     }
 24 
 25     //開始伺服器
 26     public function startServer()
 27     {
 28         $this->master = socket_create_listen( $this->port );
 29         if( !$this->master ) throw new \Exception('listen $this->port fail !');
 30 
 31         $this->runLog("Server Started : ".date('Y-m-d H:i:s'));
 32         $this->runLog("Listening on   : 127.0.0.1 port " . $this->port);
 33         $this->runLog("Master socket  : ".$this->master."\n");
 34 
 35         self::$connectPool[] = $this->master;
 36 
 37         while( true ){
 38             $readFds = self::$connectPool;
 39             //阻塞接收客戶端連結
 40             @socket_select( $readFds, $writeFds, $e = null, $this->timeout );
 41 
 42             foreach( $readFds as $socket ){
 43                 //當前連結 是主程序
 44                 if( $this->master == $socket ){
 45 
 46                     $client = socket_accept( $this->master );  //接收新的連結
 47                     $this->handShake = False;
 48 
 49                     if ($client < 0){
 50                         $this->log('clinet connect false!');
 51                         continue;
 52                     } else{
 53                         //超過最大連線數
 54                         if( count( self::$connectPool ) > self::$maxConnectNum )
 55                             continue;
 56 
 57                         //加入連線池
 58                         $this->connect( $client );
 59                     }
 60 
 61                 }else{
 62                     //不是主程序,開始接收資料
 63                     $bytes = @socket_recv($socket, $buffer, 2048, 0);
 64                     //未讀取到資料
 65                     if( $bytes == 0 ){
 66                         $this->disConnect( $socket );
 67                     }else{
 68                         //未握手 先握手
 69                         if( !$this->handShake ){
 70 
 71                             $this->doHandShake( $socket, $buffer );
 72                         }else{
 73 
 74                             //如果是已經握完手的資料,廣播其傳送的訊息
 75                             $buffer = $this->decode( $buffer );
 76                             $this->parseMessage( $buffer, $socket );
 77                         }
 78                     }
 79 
 80                 }
 81             }
 82 
 83         }
 84     }
 85 
 86     //解析傳送的資料
 87     public function parseMessage( $message, $socket )
 88     {
 89         //msg type  1 初始化  2 通知  3 一般聊天  4 斷開連結  5 獲取線上使用者 6 通知下線
 90         $message = json_decode( $message, true );
 91         switch( $message['type'] ){
 92 
 93             case 1:
 94                 $this->bind( $socket, $message );
 95                 //通知其他客戶端,當前使用者上線
 96                 $msg = [
 97                     'type' => "2",
 98                     'msg' => 'online',
 99                     'avar' => $message['avar']
100                 ];
101                 $this->sendToAll( $socket,  $msg );
102                 //更新線上使用者
103                 $this->freshOnlineUser();
104 
105                 break;
106             case 3:
107                 $this->sendToAll( $socket, $message );
108                 break;
109             case 4:
110                 //通知使用者離線
111                 $msgOutline = [
112                     'type' => '6',
113                     'user' => self::$chatUser[(int)$socket]['user']
114                 ];
115                 $this->tellOnlineInfo( $msgOutline );
116                 //斷開 要離線的使用者
117                 $this->disConnect( $socket );
118                 //更新線上使用者
119                 $this->freshOnlineUser();
120 
121                 break;
122             default:
123                 break;
124         }
125     }
126 
127     //使用者--連結 繫結
128     public function bind( $socket, $user )
129     {
130         self::$chatUser[(int) $socket] = [
131             'user' => $user['user'],
132             'avar' => $user['avar']
133         ];
134     }
135 
136     //使用者--連結 解綁
137     public function unBind( $socket )
138     {
139         unset( self::$chatUser[(int) $socket] );
140     }
141 
142     //獲取線上使用者
143     public function getOnlineUser()
144     {
145         return self::$chatUser;
146     }
147 
148     //更新線上使用者
149     public function freshOnlineUser()
150     {
151         $msgOnlie = [
152             'type' => "5",
153             'msg' => 'online user',
154             'info' => self::$chatUser
155         ];
156         $this->tellOnlineInfo( $msgOnlie );
157     }
158 
159     //廣播所有的客戶端(排除自己和master)
160     public function sendToAll( $client, $mess )
161     {
162         //拼裝傳送者的名稱
163         $mess['user'] = self::$chatUser[(int) $client]['user'];
164         $mess['stime'] = date('Y-m-d H:i:s');
165 
166         foreach( self::$connectPool as $socket ){
167             if( $socket != $this->master && $socket != $client  ){
168                 $this->send( $socket, $mess );
169             }
170         }
171     }
172 
173     //廣播客戶端線上使用者資訊
174     public function tellOnlineInfo( $mess )
175     {
176         foreach( self::$connectPool as $socket ){
177             if( $socket != $this->master ){
178                 $this->send( $socket, $mess );
179             }
180         }
181     }
182 
183     //處理髮送資訊
184    public function send( $client, $msg )
185     {
186         $msg = $this->frame( json_encode( $msg ) );
187         socket_write( $client, $msg, strlen($msg) );
188     }
189 
190     //握手協議
191     function doHandShake($socket, $buffer)
192     {
193         list($resource, $host, $origin, $key) = $this->getHeaders($buffer);
194         $upgrade  = "HTTP/1.1 101 Switching Protocol\r\n" .
195             "Upgrade: websocket\r\n" .
196             "Connection: Upgrade\r\n" .
197             "Sec-WebSocket-Accept: " . $this->calcKey($key) . "\r\n\r\n";  //必須以兩個回車結尾
198 
199         socket_write($socket, $upgrade, strlen($upgrade));
200         $this->handShake = true;
201         return true;
202     }
203 
204     //獲取請求頭
205     function getHeaders( $req )
206     {
207         $r = $h = $o = $key = null;
208         if (preg_match("/GET (.*) HTTP/"              , $req, $match)) { $r = $match[1]; }
209         if (preg_match("/Host: (.*)\r\n/"             , $req, $match)) { $h = $match[1]; }
210         if (preg_match("/Origin: (.*)\r\n/"           , $req, $match)) { $o = $match[1]; }
211         if (preg_match("/Sec-WebSocket-Key: (.*)\r\n/", $req, $match)) { $key = $match[1]; }
212         return [$r, $h, $o, $key];
213     }
214 
215     //驗證socket
216     function calcKey( $key )
217     {
218         //基於websocket version 13
219         $accept = base64_encode(sha1($key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true));
220         return $accept;
221     }
222 
223 
224     //打包函式 返回幀處理
225     public function frame( $buffer )
226     {
227         $len = strlen($buffer);
228         if ($len <= 125) {
229 
230             return "\x81" . chr($len) . $buffer;
231         } else if ($len <= 65535) {
232 
233             return "\x81" . chr(126) . pack("n", $len) . $buffer;
234         } else {
235 
236             return "\x81" . char(127) . pack("xxxxN", $len) . $buffer;
237         }
238     }
239 
240     //解碼 解析資料幀
241     function decode( $buffer )
242     {
243         $len = $masks = $data = $decoded = null;
244         $len = ord($buffer[1]) & 127;
245 
246         if ($len === 126) {
247             $masks = substr($buffer, 4, 4);
248             $data = substr($buffer, 8);
249         }
250         else if ($len === 127) {
251             $masks = substr($buffer, 10, 4);
252             $data = substr($buffer, 14);
253         }
254         else {
255             $masks = substr($buffer, 2, 4);
256             $data = substr($buffer, 6);
257         }
258         for ($index = 0; $index < strlen($data); $index++) {
259             $decoded .= $data[$index] ^ $masks[$index % 4];
260         }
261         return $decoded;
262     }
263 
264     //客戶端連結處理函式
265     function connect( $socket )
266     {
267         array_push( self::$connectPool, $socket );
268         $this->runLog("\n" . $socket . " CONNECTED!");
269         $this->runLog(date("Y-n-d H:i:s"));
270     }
271 
272     //客戶端斷開連結函式
273     function disConnect( $socket )
274     {
275         $index = array_search( $socket, self::$connectPool );
276         socket_close( $socket );
277 
278         $this->unBind( $socket );
279         $this->runLog( $socket . " DISCONNECTED!" );
280         if ($index >= 0){
281             array_splice( self::$connectPool, $index, 1 );
282         }
283     }
284 
285     //列印執行資訊
286     public function runLog( $mess = '' )
287     {
288         echo $mess . PHP_EOL;
289     }
290 
291     //系統日誌
292     public function log( $mess = '' )
293     {
294         @file_put_contents( './' . date("Y-m-d") . ".log", date('Y-m-d H:i:s') . "  " . $mess . PHP_EOL, FILE_APPEND );
295     }
296 }

 

  客戶端的程式碼,篇幅有限,我就不放出了。專案已經放入 本人github,需要了解的請 關注 :https://github.com/nick-bai/HappyChat

  看一下頁面效果吧:

 

ab併發測試:

 

參考文章:

http://blog.csdn.net/shagoo/article/details/6396089

http://www.cnblogs.com/hustskyking/p/websocket-with-php.html

https://www.web-tinker.com/article/20306.html

宣告:本文內容僅是本人學習的記錄,不保證在專案中可用,若引用此程式碼導致了嚴重後果,本人不承擔任何法律責任。