1. 程式人生 > >PHP簡單實現WebSocket(聊天室)

PHP簡單實現WebSocket(聊天室)

在PHP中,開發者需要考慮的東西比較多,從socket的連線、建立、繫結、監聽等都需要開發者自己去操作完成,對於初學者來說,難度方面也挺大的,所以本文的思路如下:

1、socket協議的簡介

2、介紹client與server之間的連線原理

3、PHP中建立socket的過程講解

4、用一個聊天室作為例項詳細講解在PHP中如何使用socket

一、socket協議的簡介

  WebSocket是什麼,有什麼優點

  WebSocket是一個持久化的協議,這是相對於http非持久化來說的。

  舉個簡單的例子,http1.0的生命週期是以request作為界定的,也就是一個request,一個response,對於http來說,本次client與server的會話到此結束;而在http1.1中,稍微有所改進,即添加了keep-alive,也就是在一個http連線中可以進行多個request請求和多個response接受操作。然而在實時通訊中,並沒有多大的作用,http只能由client發起請求,server才能返回資訊,即server不能主動向client推送資訊,無法滿足實時通訊的要求。而WebSocket可以進行持久化連線,即client只需進行一次握手,成功後即可持續進行資料通訊,值得關注的是WebSocket實現client與server之間全雙工通訊,即server端有資料更新時可以主動推送給client端。

二、介紹client與server之間的socket連線原理

1、下面是一個演示client和server之間建立WebSocket連線時握手部分

  

 

2、client與server建立socket時握手的會話內容,即request與response

  a、client建立WebSocket時向伺服器端請求的資訊

  GET /chat HTTP/1.1 
  Host: server.example.com 
  Upgrade: websocket //告訴伺服器現在傳送的是WebSocket協議
  Connection: Upgrade 
  Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw== //是一個Base64 encode的值,這個是瀏覽器隨機生成的,用於驗證伺服器端返回資料是否是WebSocket助理
  Sec-WebSocket-Protocol: chat, superchat 
  Sec-WebSocket-Version: 13 
  Origin: http://example.com

  b、伺服器獲取到client請求的資訊後,根據WebSocket協議對資料進行處理並返回,其中要對Sec-WebSocket-Key進行加密等操作

  HTTP/1.1 101 Switching Protocols 
  Upgrade: websocket //依然是固定的,告訴客戶端即將升級的是Websocket協議,而不是mozillasocket,lurnarsocket或者shitsocket
  Connection: Upgrade 
  Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk= //這個則是經過伺服器確認,並且加密過後的 Sec-WebSocket-Key,也就是client要求建立WebSocket驗證的憑證
  Sec-WebSocket-Protocol: chat

 

3、socket建立連線原理圖:

  

 

 

三、PHP中建立socket的過程講解

1、在PHP中,client與server之間建立socket通訊,首先在PHP中建立socket並監聽埠資訊,程式碼如下:

1

2

3

4

5

6

7

8

//傳相應的IP與埠進行建立socket操作

function WebSocket($address,$port){

    $server = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);

    socket_set_option($server, SOL_SOCKET, SO_REUSEADDR, 1);//1表示接受所有的資料包

    socket_bind($server$address$port);

    socket_listen($server);

    return $server;

}

2、設計一個迴圈掛起WebSocket通道,進行資料的接收、處理和傳送

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

74

75

//對建立的socket迴圈進行監聽,處理資料

function run(){

    //死迴圈,直到socket斷開

    while(true){

        $changes=$this->sockets;

        $write=NULL;

        $except=NULL;

         

        /*

        //這個函式是同時接受多個連線的關鍵,我的理解它是為了阻塞程式繼續往下執行。

        socket_select ($sockets, $write = NULL, $except = NULL, NULL);

 

        $sockets可以理解為一個數組,這個陣列中存放的是檔案描述符。當它有變化(就是有新訊息到或者有客戶端連線/斷開)時,socket_select函式才會返回,繼續往下執行。

        $write是監聽是否有客戶端寫資料,傳入NULL是不關心是否有寫變化。

        $except是$sockets裡面要被排除的元素,傳入NULL是”監聽”全部。

        最後一個引數是超時時間

        如果為0:則立即結束

        如果為n>1: 則最多在n秒後結束,如遇某一個連線有新動態,則提前返回

        如果為null:如遇某一個連線有新動態,則返回

        */

        socket_select($changes,$write,$except,NULL);

        foreach($changes as $sock){

             

            //如果有新的client連線進來,則

            if($sock==$this->master){

 

                //接受一個socket連線

                $client=socket_accept($this->master);

 

                //給新連線進來的socket一個唯一的ID

                $key=uniqid();

                $this->sockets[]=$client;  //將新連線進來的socket存進連線池

                $this->users[$key]=array(

                    'socket'=>$client,  //記錄新連線進來client的socket資訊

                    'shou'=>false       //標誌該socket資源沒有完成握手

                );

            //否則1.為client斷開socket連線,2.client傳送資訊

            }else{

                $len=0;

                $buffer='';

                //讀取該socket的資訊,注意:第二個引數是引用傳參即接收資料,第三個引數是接收資料的長度

                do{

                    $l=socket_recv($sock,$buf,1000,0);

                    $len+=$l;

                    $buffer.=$buf;

                }while($l==1000);

 

                //根據socket在user池裡面查詢相應的$k,即健ID

                $k=$this->search($sock);

 

                //如果接收的資訊長度小於7,則該client的socket為斷開連線

                if($len<7){

                    //給該client的socket進行斷開操作,並在$this->sockets和$this->users裡面進行刪除

                    $this->send2($k);

                    continue;

                }

                //判斷該socket是否已經握手

                if(!$this->users[$k]['shou']){

                    //如果沒有握手,則進行握手處理

                    $this->woshou($k,$buffer);

                }else{

                    //走到這裡就是該client傳送資訊了,對接受到的資訊進行uncode處理

                    $buffer $this->uncode($buffer,$k);

                    if($buffer==false){

                        continue;

                    }

                    //如果不為空,則進行訊息推送操作

                    $this->send($k,$buffer);

                }

            }

        }

         

    }

     

}

3、以上伺服器端完成的WebSocket的前期工作後,就等著client連線進行,client建立WebSocket很簡單,程式碼如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

var ws = new WebSocket("ws://IP:埠");

//握手監聽函式

ws.onopen=function(){

     //狀態為1證明握手成功,然後把client自定義的名字傳送過去

    if(so.readyState==1){

         //握手成功後對伺服器傳送資訊

     so.send('type=add&ming='+n);

    }

}

//錯誤返回資訊函式

ws.onerror = function(){

    console.log("error");

};

//監聽伺服器端推送的訊息

ws.onmessage = function (msg){

    console.log(msg);

}

 

//斷開WebSocket連線

ws.onclose = function(){

    ws = false;

}

四、聊天室例項程式碼

1、PHP部分

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

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

276

277

278

279

280

281

282

283

284

285

286

287

288

289

290

291

292

293

294

295

296

297

298

299

300

301

302

303

304

305

306

307

308

309

310

311

312

313

314

315

316

317

318

319

320

321

322

323

324

325

326

327

328

329

330

<?php

error_reporting(E_ALL ^ E_NOTICE);

ob_implicit_flush();

 

//地址與介面,即建立socket時需要伺服器的IP和埠

$sk=new Sock('127.0.0.1',8000);

 

//對建立的socket迴圈進行監聽,處理資料

$sk->run();

 

//下面是sock類

class Sock{

    public $sockets//socket的連線池,即client連線進來的socket標誌

    public $users;   //所有client連線進來的資訊,包括socket、client名字等

    public $master;  //socket的resource,即前期初始化socket時返回的socket資源

     

    private $sda=array();   //已接收的資料

    private $slen=array();  //資料總長度

    private $sjen=array();  //接收資料的長度

    private $ar=array();    //加密key

    private $n=array();

     

    public function __construct($address$port){

 

        //建立socket並把儲存socket資源在$this->master

        $this->master=$this->WebSocket($address$port);

 

        //建立socket連線池

        $this->sockets=array($this->master);

    }

     

    //對建立的socket迴圈進行監聽,處理資料

    function run(){

        //死迴圈,直到socket斷開

        while(true){

            $changes=$this->sockets;

            $write=NULL;

            $except=NULL;

             

            /*

            //這個函式是同時接受多個連線的關鍵,我的理解它是為了阻塞程式繼續往下執行。

            socket_select ($sockets, $write = NULL, $except = NULL, NULL);

 

            $sockets可以理解為一個數組,這個陣列中存放的是檔案描述符。當它有變化(就是有新訊息到或者有客戶端連線/斷開)時,socket_select函式才會返回,繼續往下執行。

            $write是監聽是否有客戶端寫資料,傳入NULL是不關心是否有寫變化。

            $except是$sockets裡面要被排除的元素,傳入NULL是”監聽”全部。

            最後一個引數是超時時間

            如果為0:則立即結束

            如果為n>1: 則最多在n秒後結束,如遇某一個連線有新動態,則提前返回

            如果為null:如遇某一個連線有新動態,則返回

            */

            socket_select($changes,$write,$except,NULL);

            foreach($changes as $sock){

                 

                //如果有新的client連線進來,則

                if($sock==$this->master){

 

                    //接受一個socket連線

                    $client=socket_accept($this->master);

 

                    //給新連線進來的socket一個唯一的ID

                    $key=uniqid();

                    $this->sockets[]=$client;  //將新連線進來的socket存進連線池

                    $this->users[$key]=array(

                        'socket'=>$client,  //記錄新連線進來client的socket資訊

                        'shou'=>false       //標誌該socket資源沒有完成握手

                    );

                //否則1.為client斷開socket連線,2.client傳送資訊

                }else{

                    $len=0;

                    $buffer='';

                    //讀取該socket的資訊,注意:第二個引數是引用傳參即接收資料,第三個引數是接收資料的長度

                    do{

                        $l=socket_recv($sock,$buf,1000,0);

                        $len+=$l;

                        $buffer.=$buf;

                    }while($l==1000);

 

                    //根據socket在user池裡面查詢相應的$k,即健ID

                    $k=$this->search($sock);

 

                    //如果接收的資訊長度小於7,則該client的socket為斷開連線

                    if($len<7){

                        //給該client的socket進行斷開操作,並在$this->sockets和$this->users裡面進行刪除

                        $this->send2($k);

                        continue;

                    }

                    //判斷該socket是否已經握手

                    if(!$this->users[$k]['shou']){

                        //如果沒有握手,則進行握手處理

                        $this->woshou($k,$buffer);

                    }else{

                        //走到這裡就是該client傳送資訊了,對接受到的資訊進行uncode處理

                        $buffer $this->uncode($buffer,$k);

                        if($buffer==false){

                            continue;

                        }

                        //如果不為空,則進行訊息推送操作

                        $this->send($k,$buffer);

                    }

                }

            }

             

        }

         

    }

     

    //指定關閉$k對應的socket

    function close($k){

        //斷開相應socket

        socket_close($this->users[$k]['socket']);

        //刪除相應的user資訊

        unset($this->users[$k]);

        //重新定義sockets連線池

        $this->sockets=array($this->master);

        foreach($this->users as $v){

            $this->sockets[]=$v['socket'];

        }

        //輸出日誌

        $this->e("key:$k close");

    }

     

    //根據sock在users裡面查詢相應的$k

    function search($sock){

        foreach ($this->users as $k=>$v){

            if($sock==$v['socket'])

            return $k;

        }

        return false;

    }

     

    //傳相應的IP與埠進行建立socket操作

    function WebSocket($address,$port){

        $server = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);

        socket_set_option($server, SOL_SOCKET, SO_REUSEADDR, 1);//1表示接受所有的資料包

        socket_bind($server$address$port);

        socket_listen($server);

        $this->e('Server Started : '.date('Y-m-d H:i:s'));

        $this->e('Listening on   : '.$address.' port '.$port);

        return $server;

    }

     

     

    /*

    * 函式說明:對client的請求進行迴應,即握手操作

    * @$k clien的socket對應的健,即每個使用者有唯一$k並對應socket

    * @$buffer 接收client請求的所有資訊

    */

    function woshou($k,$buffer){

 

        //擷取Sec-WebSocket-Key的值並加密,其中$key後面的一部分258EAFA5-E914-47DA-95CA-C5AB0DC85B11字串應該是固定的

        $buf  substr($buffer,strpos($buffer,'Sec-WebSocket-Key:')+18);

        $key  = trim(substr($buf,0,strpos($buf,"\r\n")));

        $new_key base64_encode(sha1($key."258EAFA5-E914-47DA-95CA-C5AB0DC85B11",true));

         

        //按照協議組合資訊進行返回

        $new_message "HTTP/1.1 101 Switching Protocols\r\n";

        $new_message .= "Upgrade: websocket\r\n";

        $new_message .= "Sec-WebSocket-Version: 13\r\n";

        $new_message .= "Connection: Upgrade\r\n";

        $new_message .= "Sec-WebSocket-Accept: " $new_key "\r\n\r\n";

        socket_write($this->users[$k]['socket'],$new_message,strlen($new_message));

 

        //對已經握手的client做標誌

        $this->users[$k]['shou']=true;

        return true;

         

    }

     

    //解碼函式

    function uncode($str,$key){

        $mask array(); 

        $data ''

        $msg = unpack('H*',$str);

        $head substr($msg[1],0,2); 

        if ($head == '81' && !isset($this->slen[$key])) { 

            $len=substr($msg[1],2,2);

            $len=hexdec($len);//把十六進位制的轉換為十進位制

            if(substr($msg[1],2,2)=='fe'){

                $len=substr($msg[1],4,4);

                $len=hexdec($len);

                $msg[1]=substr($msg[1],4);

            }else if(substr($msg[1],2,2)=='ff'){

                $len=substr($msg[1],4,16);

                $len=hexdec($len);

                $msg[1]=substr($msg[1],16);

            }

            $mask[] = hexdec(substr($msg[1],4,2)); 

            $mask[] = hexdec(substr($msg[1],6,2)); 

            $mask[] = hexdec(substr($msg[1],8,2)); 

            $mask[] = hexdec(substr($msg[1],10,2));

            $s = 12;

            $n=0;

        }else if($this->slen[$key] > 0){

            $len=$this->slen[$key];

            $mask=$this->ar[$key];

            $n=$this->n[$key];

            $s = 0;

        }

         

        $e strlen($msg[1])-2;

        for ($i=$s$i<= $e$i+= 2) { 

            $data .= chr($mask[$n%4]^hexdec(substr($msg[1],$i,2))); 

            $n++; 

        

        $dlen=strlen($data);

         

        if($len > 255 && $len $dlen+intval($this->sjen[$key])){

            $this->ar[$key]=$mask;

            $this->slen[$key]=$len;

            $this->sjen[$key]=$dlen+intval($this->sjen[$key]);

            $this->sda[$key]=$this->sda[$key].$data;

            $this->n[$key]=$n;

            return false;

        }else{

            unset($this->ar[$key],$this->slen[$key],$this->sjen[$key],$this->n[$key]);

            $data=$this->sda[$key].$data;

            unset($this->sda[$key]);

            return $data;

        }

         

    }

     

    //與uncode相對

    function code($msg){

        $frame array(); 

        $frame[0] = '81'

        $len strlen($msg);

        if($len < 126){

            $frame[1] = $len<16?'0'.dechex($len):dechex($len);

        }else if($len < 65025){

            $s=dechex($len);

            $frame[1]='7e'.str_repeat('0',4-strlen($s)).$s;

        }else{

            $s=dechex($len);

            $frame[1]='7f'.str_repeat('0',16-strlen($s)).$s;

        }

        $frame[2] = $this->ord_hex($msg);

        $data = implode('',$frame); 

        return pack("H*"$data); 

    }

     

    function ord_hex($data)  { 

        $msg ''

        $l strlen($data); 

        for ($i= 0; $i<$l$i++) { 

            $msg .= dechex(ord($data{$i})); 

        

        return $msg

    }

     

    //使用者加入或client傳送資訊

    function send($k,$msg){

        //將查詢字串解析到第二個引數變數中,以陣列的形式儲存如:parse_str("name=Bill&age=60",$arr)

        parse_str($msg,$g);

        $ar=array();

 

        if($g['type']=='add'){

            //第一次進入新增聊天名字,把姓名儲存在相應的users裡面

            $this->users[$k]['name']=$g['ming'];

            $ar['type']='add';

            $ar['name']=$g['ming'];

            $key='all';

        }else{

            //傳送資訊行為,其中$g['key']表示面對大家還是個人,是前段傳過來的資訊

 &nb