這篇文章主要介紹了PHP+Socket之如何實現(xiàn)websocket聊天室的相關(guān)知識,內(nèi)容詳細易懂,操作簡單快捷,具有一定借鑒價值,相信大家閱讀完這篇PHP+Socket之如何實現(xiàn)websocket聊天室文章都會有所收獲,下面我們一起來看看吧。
為固鎮(zhèn)等地區(qū)用戶提供了全套網(wǎng)頁設(shè)計制作服務(wù),及固鎮(zhèn)網(wǎng)站建設(shè)行業(yè)解決方案。主營業(yè)務(wù)為網(wǎng)站制作、做網(wǎng)站、固鎮(zhèn)網(wǎng)站設(shè)計,以傳統(tǒng)方式定制建設(shè)網(wǎng)站,并提供域名空間備案等一條龍服務(wù),秉承以專業(yè)、用心的態(tài)度為用戶提供真誠的服務(wù)。我們深信只要達到每一位用戶的要求,就會得到認可,從而選擇與我們長期合作。這樣,我們也可以走得更遠!
為什么需要websocket
HTTP 協(xié)議是一種無狀態(tài)的、無連接的、單向的應用層協(xié)議。它采用了 請求 => 響應
模型,通信請求僅能由客戶端發(fā)起,服務(wù)端對請求做出應答處理,這種通信模型有一個弊端:無法實現(xiàn)服務(wù)端主動向客戶端發(fā)起消息。傳統(tǒng)的 HTTP 請求,其并發(fā)能力都是依賴同時發(fā)起多個 TCP 連接訪問服務(wù)器實現(xiàn)的而 websocket 則允許我們在一條 ws 連接上同時并發(fā)多個請求,即在 A 請求發(fā)出后 A 響應還未到達,就可以繼續(xù)發(fā)出 B 請求。由于 TCP 的慢啟動特性,以及連接本身的握手損耗,都使得 websocket 協(xié)議的這一特性有很大的效率提升。
建立在 TCP 協(xié)議之上,服務(wù)端的實現(xiàn)相對比較容易
與 HTTP 協(xié)議有良好的兼容性,默認端口也是 80 和 443,并且握手階段采用 HTTP 協(xié)議,因此握手時不容易被屏蔽,能通過各種 HTTP 代理服務(wù)器。
數(shù)據(jù)格式比較輕量,性能開銷小,通信高效。
可以發(fā)送文本,也可以發(fā)送二進制數(shù)據(jù)。
沒有同源限制,客戶端可以與任意服務(wù)器進行通信。
協(xié)議標識符是 ws(如果加密則為 wss),服務(wù)地址就是 URL。
客戶端與服務(wù)端握手
websocket 協(xié)議在連接前需要握手[^2],通常握手方式有以下幾種方式
基于 flash 的握手協(xié)議(不建議)
基于 md5 加密方式的握手協(xié)議
較早的握手方法,有兩個 key,使用 md5 加密
基于 sha1 加密方式的握手協(xié)議
當前主要的握手協(xié)議,本文將以此協(xié)議為主
獲取客戶端上報的 Sec-WebSocket-key
拼接 key
+ 258EAFA5-E914-47DA-95CA-C5AB0DC85B11
對字符串做 SHA1
計算,再把得到的結(jié)果通過 base64
加密,最后再返回給客戶端
客戶端請求信息如下:
GET /chat HTTP/1.1Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
客戶端需返回如下數(shù)據(jù):
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Sec-WebSocket-Version: 13Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
我們根據(jù)此協(xié)議通過 PHP 方式實現(xiàn):
使用前端測試,打開我們的任意瀏覽器控制臺(console)輸入以下內(nèi)容,返回的 websocket 對象的 readyState 為 1 即為握手成功:
console.log(new WebSocket('ws://192.162.2.166:8888'));
// 運行后返回:
WebSocket {
binaryType: "blob"
bufferedAmount: 0
extensions: ""
onclose: null
onerror: null
onmessage: null
onopen: null
protocol: ""
readyState: 1
url: "ws://192.162.2.166:8888/"}
發(fā)送數(shù)據(jù)與接收數(shù)據(jù)
使用 websocket 協(xié)議傳輸協(xié)議需要遵循特定的格式規(guī)范
為了方便,這里直接貼出加解密代碼,以下代碼借鑒與 workerman 的 src/Protocols/Websocket.php
文件:
// 解碼客戶端發(fā)送的消息
function decode($buffer)
{
$len = \ord($buffer[1]) & 127;
if ($len === 126) {
$masks = \substr($buffer, 4, 4);
$data = \substr($buffer, 8);
} else {
if ($len === 127) {
$masks = \substr($buffer, 10, 4);
$data = \substr($buffer, 14);
} else {
$masks = \substr($buffer, 2, 4);
$data = \substr($buffer, 6);
}
}
$dataLength = \strlen($data);
$masks = \str_repeat($masks, \floor($dataLength / 4)) . \substr($masks, 0, $dataLength % 4);
return $data ^ $masks;
}
// 編碼發(fā)送給客戶端的消息
function encode($buffer)
{
if (!is_scalar($buffer)) {
throw new \Exception("You can't send(" . \gettype($buffer) . ") to client, you need to convert it to a string. ");
}
$len = \strlen($buffer);
$first_byte = "\x81";
if ($len <= 125) {
$encode_buffer = $first_byte . \chr($len) . $buffer;
} else {
if ($len <= 65535) {
$encode_buffer = $first_byte . \chr(126) . \pack("n", $len) . $buffer;
} else {
$encode_buffer = $first_byte . \chr(127) . \pack("xxxxN", $len) . $buffer;
}
}
return $encode_buffer;
}
我們修改剛才 客戶端與服務(wù)端握手 階段的代碼,修改后全代碼全文如下,該段代碼實現(xiàn)了將客戶端發(fā)送的消息轉(zhuǎn)為大寫后返回給客戶端(當然只是為了演示):
我們緊接著上文的代碼繼續(xù)優(yōu)化,以實現(xiàn)簡易的web聊天室
多路復用
其實就是加一下 socket_select()
函數(shù) ,以下代碼修改自前文 發(fā)送數(shù)據(jù)與接收數(shù)據(jù)
...
socket_listen($socket);
+$sockets[] = $socket;
+$user = [];
while (true) {
+ $tmp_sockets = $sockets;
+ socket_select($tmp_sockets, $write, $except, null);
+ foreach ($tmp_sockets as $sock) {
+ if ($sock == $socket) {
+ $sockets[] = socket_accept($socket);
+ $user[] = ['socket' => $socket, 'handshake' => false];
+ } else {
+ $curr_user = $user[array_search($sock, $user)];
+ if ($curr_user['handshake']) { // 已握手
+ $msg = socket_read($sock, 102400);
+ echo '客戶端發(fā)來消息' . decode($msg);
+ socket_write($sock, encode('這是來自服務(wù)端的消息'));
+ } else {
+ // 握手
+ }
+ }
+ }
- $conn_sock = socket_accept($socket);
- $request = socket_read($conn_sock, 102400);
...
我們將上述代碼改造成類,并在類變量儲存用戶信息,添加消息處理等邏輯,最后貼出代碼,建議保存下來自己嘗試一下,也許會有全新的認知,后端代碼:
socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_option($this->socket, SOL_SOCKET, SO_REUSEADDR, true);
socket_bind($this->socket, 0, 8888);
socket_listen($this->socket);
// 將 socket 資源放入 socket_list
$this->socket_list[] = $this->socket;
while (true) {
$tmp_sockets = $this->socket_list;
socket_select($tmp_sockets, $write, $except, null);
foreach ($tmp_sockets as $sock) {
if ($sock == $this->socket) {
$conn_sock = socket_accept($sock);
$this->socket_list[] = $conn_sock;
$this->user[] = ['socket' => $conn_sock, 'handshake' => false, 'name' => '無名氏'];
} else {
$request = socket_read($sock, 102400);
$k = $this->getUserIndex($sock);
if (!$request) {
continue;
}
// 用戶端斷開連接
if ((\ord($request[0]) & 0xf) == 0x8) {
$this->close($k);
continue;
}
if (!$this->user[$k]['handshake']) {
// 握手
$this->handshake($k, $request);
} else {
// 已握手
$this->send($k, $request);
}
}
}
}
}
/**
* 關(guān)閉連接
*
* @param $k
*/
protected function close($k)
{
$u_name = $this->user[$k]['name'] ?? '無名氏';
socket_close($this->user[$k]['socket']);
$socket_key = array_search($this->user[$k]['socket'], $this->socket_list);
unset($this->socket_list[$socket_key]);
unset($this->user[$k]);
$user = [];
foreach ($this->user as $v) {
$user[] = $v['name'];
}
$res = [
'type' => 'close',
'users' => $user,
'msg' => $u_name . '已退出',
'time' => date('Y-m-d H:i:s')
];
$this->sendAllUser($res);
}
/**
* 獲取用戶索引
*
* @param $socket
* @return int|string
*/
protected function getUserIndex($socket)
{
foreach ($this->user as $k => $v) {
if ($v['socket'] == $socket) {
return $k;
}
}
}
/**
* 握手
* @param $k
* @param $request
*/
protected function handshake($k, $request)
{
preg_match("/Sec-WebSocket-Key: (.*)\r\n/", $request, $match);
$key = base64_encode(sha1($match[1] . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true));
$response = "HTTP/1.1 101 Switching Protocols\r\n";
$response .= "Upgrade: websocket\r\n";
$response .= "Connection: Upgrade\r\n";
$response .= "Sec-WebSocket-Accept: {$key}\r\n\r\n";
socket_write($this->user[$k]['socket'], $response);
$this->user[$k]['handshake'] = true;
}
/**
* 接收并處理消息
*
* @param $k
* @param $msg
*/
public function send($k, $msg)
{
$msg = $this->decode($msg);
$msg = json_decode($msg, true);
if (!isset($msg['type'])) {
return;
}
switch ($msg['type']) {
case 'login': // 登錄
$this->user[$k]['name'] = $msg['name'] ?? '無名氏';
$users = [];
foreach ($this->user as $v) {
$users[] = $v['name'];
}
$res = [
'type' => 'login',
'name' => $this->user[$k]['name'],
'msg' => $this->user[$k]['name'] . ': login success',
'users' => $users,
];
$this->sendAllUser($res);
break;
case 'message': // 接收并發(fā)送消息
$res = [
'type' => 'message',
'name' => $this->user[$k]['name'] ?? '無名氏',
'msg' => $msg['msg'],
'time' => date('H:i:s'),
];
$this->sendAllUser($res);
break;
}
}
/**
* 發(fā)送給所有人
*
*/
protected function sendAllUser($msg)
{
if (is_array($msg)) {
$msg = json_encode($msg);
}
$msg = $this->encode($msg);
foreach ($this->user as $k => $v) {
socket_write($v['socket'], $msg, strlen($msg));
}
}
/**
* 解碼
*
* @param $buffer
* @return string
*/
protected function decode($buffer)
{
$len = \ord($buffer[1]) & 127;
if ($len === 126) {
$masks = \substr($buffer, 4, 4);
$data = \substr($buffer, 8);
} else {
if ($len === 127) {
$masks = \substr($buffer, 10, 4);
$data = \substr($buffer, 14);
} else {
$masks = \substr($buffer, 2, 4);
$data = \substr($buffer, 6);
}
}
$dataLength = \strlen($data);
$masks = \str_repeat($masks, \floor($dataLength / 4)) . \substr($masks, 0, $dataLength % 4);
return $data ^ $masks;
}
protected function encode($buffer)
{
if (!is_scalar($buffer)) {
throw new \Exception("You can't send(" . \gettype($buffer) . ") to client, you need to convert it to a string. ");
}
$len = \strlen($buffer);
$first_byte = "\x81";
if ($len <= 125) {
$encode_buffer = $first_byte . \chr($len) . $buffer;
} else {
if ($len <= 65535) {
$encode_buffer = $first_byte . \chr(126) . \pack("n", $len) . $buffer;
} else {
$encode_buffer = $first_byte . \chr(127) . \pack("xxxxN", $len) . $buffer;
}
}
return $encode_buffer;
}
}
前端代碼如下(前端內(nèi)容不在本文討論范圍之內(nèi),具體可參考 菜鳥教程):
這是一個php socket實現(xiàn)的web聊天室
[^1]:是通訊傳輸?shù)囊粋€術(shù)語。 通信允許數(shù)據(jù)在兩個方向上同時傳輸,它在能力上相當于兩個單工通信方式的結(jié)合
[^2]: 為了建立 websocket 連接,需要通過瀏覽器發(fā)出請求,之后服務(wù)器進行回應,這個過程通常稱為“握手”
關(guān)于“PHP+Socket之如何實現(xiàn)websocket聊天室”這篇文章的內(nèi)容就介紹到這里,感謝各位的閱讀!相信大家對“PHP+Socket之如何實現(xiàn)websocket聊天室”知識都有一定的了解,大家如果還想學習更多知識,歡迎關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道。