這兩天花時間看了一下websocket,自己也跟着動手做了一個畢竟簡單的實現 記錄一下:
websocket分為 客戶端 和 服務端 兩部分
客戶端代碼
var ws = new WebSocket('ws://127.0.0.1:7777'); // 訪問的是本地的服務器
function sendMsg() {
let _file = document.querySelector('input[type=file]').files[0];
let text = document.getElementById('txtinput').value;
if (ws.readyState == 1) {
// 處理圖片為base64格式
if (_file) {
let reader = new FileReader();
reader.readAsDataURL(_file); // 轉化為二進制流
reader.onload = () => {
ws.send(reader.result);
}
} else {
ws.send(text);
}
}
}
// 連接服務器
ws.onopen = function (e) {
console.log('connect');
}
// 接收服務器的消息
ws.onmessage = function(e) {
if (e.data.indexOf('base64,') > 0) {
let img = new Image();
img.src = e.data;
document.getElementById('area').appendChild(img);
} else {
let text = document.createElement('div');
text.innerHTML = e.data;
document.getElementById('area').appendChild(text);
}
}
ws.onclose = e => {
console.log('lost connect');
ws = 0;
}
ws.onerror = e => {
console.log('error');
console.log(e);
}
客戶端第一次連接服務端的時候會有一個握手的步驟 客服端發送WebSocket-Key給服務端,服務端拿到之后 又加密返給客戶端 雙方都通過后 websocket連接建立成功

服務端代碼(PHP實現)
class SocketController
{
public $master;
public $sockets;
private $len = 1024;
public function __construct($ip, $port)
{
// 創建一個IPV4的套接字
$this->master = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_option($this->master, SOL_SOCKET, SO_REUSEADDR, 1); // 設置接收所有數據
// 綁定IP 端口
socket_bind($this->master, $ip, $port);
//開啟監聽
socket_listen($this->master);
// socket_set_nonblock($this->master);
$this->sockets['master'] = $this->master;
echo 'socket create' . PHP_EOL;
}
// 接收消息
public function run()
{
while (true)
{
$changes = $this->sockets;
$write = $exception = [];
// 沒處理異常的情況 關於第四個參數 null 表示等待客戶端發消息或有新連接再往下走 會阻塞在這里 非null的情況沒做嘗試
socket_select($changes, $write, $exception, null);
foreach ($changes as $k => $master)
{
// 說明有新的客戶端進來
if ($master == $this->master)
{
$client = socket_accept($master);
//獲取客戶端發送內容
$buffer = trim(socket_read($client,1024));
$this->hand($client, $buffer); // 握手
$key = uniqid();
echo 'new connect ' . $key . PHP_EOL;
$this->sockets[$key] = $client;
}
else // 說明客戶端發了信息過來
{
$content = '';
$contentLength = 0;
echo 'begin get content ' . $k . PHP_EOL;
// 用do while 是因為socket_recv 會阻塞程序 消息接收完了也卡在這不往下走
do
{
$len = socket_recv($master, $subContent, $this->len, 0);
$content .= $subContent;
$contentLength += $len;
}while($len == $this->len);
if ($contentLength > 0)
{
// 把消息 給到其他連接
$this->sendMsg($content);
} else {
$this->unlinkSocket($k); // 關閉socket連接
}
}
}
}
}
// 發送消息
public function sendMsg($content)
{
$fixedContent = self::encode($content);
foreach ($this->sockets as $k => $socket)
{
if ($k == 'master') continue;
socket_write($socket, $fixedContent, strlen($fixedContent));
}
echo 'send msg' . PHP_EOL;
}
// 關閉套接字
public function unlinkSocket($connect)
{
echo 'lost connect' . $connect . PHP_EOL;
@socket_shutdown($this->sockets[$connect]);
socket_close($this->sockets[$connect]);
unset($this->sockets[$connect]);
}
// 握手
public function hand($client, $buffer)
{
$buf = substr($buffer, strpos($buffer,'Sec-WebSocket-Key:') + 18);
$key = trim(substr($buf, 0, strpos($buf, "\r\n")));
$newKey = base64_encode(sha1($key . "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", true));
//按照協議組合信息進行返回
$newMessage = "HTTP/1.1 101 Switching Protocols\r\n";
$newMessage .= "Upgrade: websocket\r\n";
$newMessage .= "Sec-WebSocket-Version: 13\r\n";
$newMessage .= "Connection: Upgrade\r\n";
$newMessage .= "Sec-WebSocket-Accept: " . $newKey . "\r\n\r\n";
socket_write($client, $newMessage, strlen($newMessage));
}
/**
* https://www.cnblogs.com/zhangmingda/p/12678630.html 很好的解釋了$masks $data為什么這樣取值
* @param $buffer
* @return string
*/
public function encode($buffer)
{
// 解析成文案
$len = $masks = $data = $decoded = null;
$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);
}
for ($index = 0; $index < strlen($data); $index++)
{
$decoded .= $data[$index] ^ $masks[$index % 4];
}
// end
// 這段的意思是 把數據分割成125個字節的長度大小 大於125個字節的 就分段返回
// $a = str_split($decoded, 125);
// if (count($a) == 1)
// {
// return "\x81" . chr(strlen($a[0])) . $a[0];
// }
// $ns = "";
// foreach ($a as $o)
// {
// $ns .= "\x81" . chr(strlen($o)) . $o;
// }
// return $ns;
// end
// 這個就不分段了
$frame = array();
$frame[0] = '81';
$len = strlen($decoded);
if ($len < 126)
{
$frame[1] = $len < 16 ? '0' . dechex($len) : dechex($len);
}
elseif ($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;
}
$msg = '';
for ($i = 0; $i < $len; $i++)
{
$msg .= dechex(ord($decoded{$i}));
}
$frame[2] = $msg;
$data = implode('', $frame);
return pack("H*", $data);
}
}
$sc = new SocketController('127.0.0.1', 7777);
$sc->run();
剛寫完服務端代碼的時候並沒有發送過來的數據做處理,以為是發送過來一個 ‘hello world’ 服務端拿到的就應該是一個明文的字符串 一眼就能看到懂那種,后來打印了之后才發現事情並沒有這么簡單,看了 https://www.cnblogs.com/zhangmingda/p/12678630.html 這個老哥的文章才知道數據在這個過程中是怎么傳遞的 牽扯到位運算及字符操作函數 對這方面也了解不深就不贅述了
上面已經基本上算是實現了聊天室的功能
