1. 서문
회사 게임에 간이채팅방이 있는데 알고보니 node+websocket으로 만든줄 알았더니 PHP를 이용해서 간이채팅방을 만들어볼까 하는 생각이 들었습니다. 그래서 다양한 정보를 수집하고, 문서를 읽고, 사례를 찾아보고, 간단한 채팅방을 직접 작성했습니다.
HTTP 연결은 짧은 연결과 긴 연결로 구분됩니다. 짧은 연결은 일반적으로 ajax를 사용하여 구현할 수 있으며 긴 연결은 웹소켓입니다. 짧은 연결은 비교적 구현이 간단하지만 너무 많은 리소스를 소비합니다. Websocket은 효율적이지만 호환성에 몇 가지 문제가 있습니다. websocket은 html5의 리소스입니다
웹소켓 긴 연결의 원리에 대해 더 알고 싶다면 https://www.zhihu.com/question/20215561을 참조하세요.
이 글에서는 웹소켓 간이채팅방의 구현 단계를 주로 소개합니다. 특정 부분에 대한 깊이 있는 지식 포인트를 링크나 고민 독자들이 스스로 수집할 수 있도록 하겠습니다.
2. 프런트엔드
프런트 엔드에서 웹소켓을 구현하는 것은 매우 간단하고 간단합니다
//웹소켓 연결
var ws = new WebSocket("ws://127.0.0.1:8000");
//websoc에 성공적으로 접속한 경우
ws.onopen = function(){}
//서버에서 출력된 메시지를 성공적으로 가져왔습니다
ws.onmessage = 함수(e){}
//연결 오류가 있는 경우
ws.onerror = function(){}
//서버에 데이터 보내기
ws.send();
3. 백스테이지
웹소켓의 어려움은 주로 배경에 있습니다
3.1websocket 연결 과정
웹소켓 통신 다이어그램 이것은 클라이언트와 서버 간의 간단한 통신 다이어그램입니다. PHP가 하는 주요 작업은 암호화 키를 수락하고 이를 반환하여 소켓 생성 및 핸드셰이크 작업을 완료하는 것입니다.
아래 사진은 웹소켓을 처리하는 서버의 세부 흐름도입니다
3.2 코드 연습
서버에서 수행되는 프로세스는 대략 다음과 같습니다.
①. 연결을 기다리는 소켓 프로세스를 일시 중지합니다
② 소켓 연결이 발생한 후 소켓 배열을 탐색합니다
3. 핸드셰이크가 없으면 데이터를 파싱하여 버퍼에 기록하여 출력합니다
다음은 샘플 코드입니다(클래스를 작성했기 때문에 기능별로 코드가 나누어져 있습니다). 텍스트 하단에는 github 주소와 제가 겪은 몇 가지 함정이 나와 있습니다.
1. 먼저 소켓을 생성합니다
<span style="color: #008000;">//</span><span style="color: #008000;">建立套接字</span> <span style="color: #0000ff;">public</span> <span style="color: #0000ff;">function</span> createSocket(<span style="color: #800080;">$address</span>,<span style="color: #800080;">$port</span><span style="color: #000000;">) { </span><span style="color: #008000;">//</span><span style="color: #008000;">创建一个套接字</span> <span style="color: #800080;">$socket</span>= socket_create(AF_INET, SOCK_STREAM,<span style="color: #000000;"> SOL_TCP); </span><span style="color: #008000;">//</span><span style="color: #008000;">设置套接字选项</span> socket_set_option(<span style="color: #800080;">$socket</span>, SOL_SOCKET, SO_REUSEADDR, 1<span style="color: #000000;">); </span><span style="color: #008000;">//</span><span style="color: #008000;">绑定IP地址和端口</span> socket_bind(<span style="color: #800080;">$socket</span>,<span style="color: #800080;">$address</span>,<span style="color: #800080;">$port</span><span style="color: #000000;">); </span><span style="color: #008000;">//</span><span style="color: #008000;">监听套接字</span> socket_listen(<span style="color: #800080;">$socket</span><span style="color: #000000;">); </span><span style="color: #0000ff;">return</span> <span style="color: #800080;">$socket</span><span style="color: #000000;">; }</span>
2. 소켓을 배열에 넣습니다
<span style="color: #0000ff;">public</span> <span style="color: #0000ff;">function</span> __construct(<span style="color: #800080;">$address</span>,<span style="color: #800080;">$port</span><span style="color: #000000;">) { </span><span style="color: #008000;">//</span><span style="color: #008000;">建立套接字</span> <span style="color: #800080;">$this</span>->soc=<span style="color: #800080;">$this</span>->createSocket(<span style="color: #800080;">$address</span>,<span style="color: #800080;">$port</span><span style="color: #000000;">); </span><span style="color: #800080;">$this</span>->socs=<span style="color: #0000ff;">array</span>(<span style="color: #800080;">$this</span>-><span style="color: #000000;">soc); }</span>
3. 정지된 프로세스가 소켓 배열을 순회하며 여기서 주요 작업이 완료됩니다
<span style="color: #0000ff;">public</span> <span style="color: #0000ff;">function</span><span style="color: #000000;"> run(){ </span><span style="color: #008000;">//</span><span style="color: #008000;">挂起进程</span> <span style="color: #0000ff;">while</span>(<span style="color: #0000ff;">true</span><span style="color: #000000;">){ </span><span style="color: #800080;">$arr</span>=<span style="color: #800080;">$this</span>-><span style="color: #000000;">socs; </span><span style="color: #800080;">$write</span>=<span style="color: #800080;">$except</span>=<span style="color: #0000ff;">NULL</span><span style="color: #000000;">; </span><span style="color: #008000;">//</span><span style="color: #008000;">接收套接字数字 监听他们的状态</span> socket_select(<span style="color: #800080;">$arr</span>,<span style="color: #800080;">$write</span>,<span style="color: #800080;">$except</span>, <span style="color: #0000ff;">NULL</span><span style="color: #000000;">); </span><span style="color: #008000;">//</span><span style="color: #008000;">遍历套接字数组</span> <span style="color: #0000ff;">foreach</span>(<span style="color: #800080;">$arr</span> <span style="color: #0000ff;">as</span> <span style="color: #800080;">$k</span>=><span style="color: #800080;">$v</span><span style="color: #000000;">){ </span><span style="color: #008000;">//</span><span style="color: #008000;">如果是新建立的套接字返回一个有效的 套接字资源</span> <span style="color: #0000ff;">if</span>(<span style="color: #800080;">$this</span>->soc == <span style="color: #800080;">$v</span><span style="color: #000000;">){ </span><span style="color: #800080;">$client</span>=socket_accept(<span style="color: #800080;">$this</span>-><span style="color: #000000;">soc); </span><span style="color: #0000ff;">if</span>(<span style="color: #800080;">$client</span> <0<span style="color: #000000;">){ </span><span style="color: #0000ff;">echo</span> "socket_accept() failed"<span style="color: #000000;">; }</span><span style="color: #0000ff;">else</span><span style="color: #000000;">{ </span><span style="color: #008000;">//</span><span style="color: #008000;"> array_push($this->socs,$client); // unset($this[]); //将有效的套接字资源放到套接字数组 <span style="color: #800080;">$this</span>->socs[]=<span style="color: #800080;">$client</span><span style="color: #000000;">; } }</span><span style="color: #0000ff;">else</span><span style="color: #000000;">{ </span><span style="color: #008000;">//</span><span style="color: #008000;">从已连接的socket接收数据 返回的是从socket中接收的字节数</span> <span style="color: #800080;">$byte</span>=socket_recv(<span style="color: #800080;">$v</span>, <span style="color: #800080;">$buff</span>,20480, 0<span style="color: #000000;">); </span><span style="color: #008000;">//</span><span style="color: #008000;">如果接收的字节是0</span> <span style="color: #0000ff;">if</span>(<span style="color: #800080;">$byte</span><7<span style="color: #000000;">) </span><span style="color: #0000ff;">continue</span><span style="color: #000000;">; </span><span style="color: #008000;">//</span><span style="color: #008000;">判断有没有握手没有握手则进行握手,如果握手了 则进行处理</span> <span style="color: #0000ff;">if</span>(!<span style="color: #800080;">$this</span>->hand[(int)<span style="color: #800080;">$client</span><span style="color: #000000;">]){ </span><span style="color: #008000;">//</span><span style="color: #008000;">进行握手操作</span> <span style="color: #800080;">$this</span>->hands(<span style="color: #800080;">$client</span>,<span style="color: #800080;">$buff</span>,<span style="color: #800080;">$v</span><span style="color: #000000;">); }</span><span style="color: #0000ff;">else</span><span style="color: #000000;">{ </span><span style="color: #008000;">//</span><span style="color: #008000;">处理数据操作</span> <span style="color: #800080;">$mess</span>=<span style="color: #800080;">$this</span>->decodeData(<span style="color: #800080;">$buff</span><span style="color: #000000;">); </span><span style="color: #008000;">//</span><span style="color: #008000;">发送数据</span> <span style="color: #800080;">$this</span>->send(<span style="color: #800080;">$mess</span>,<span style="color: #800080;">$v</span><span style="color: #000000;">); } } } } }</span>
4. 핸드셰이크 프로세스는 웹소켓 콘텐츠를 수신하고 Sec-WebSocket-Key에서 키를 얻은 다음 암호화 알고리즘을 통해 이를 버퍼에 쓰는 것입니다(자동 확인에는 처리가 필요하지 않습니다. )
<span style="color: #0000ff;">public</span> <span style="color: #0000ff;">function</span> hands(<span style="color: #800080;">$client</span>,<span style="color: #800080;">$buff</span>,<span style="color: #800080;">$v</span><span style="color: #000000;">) { </span><span style="color: #008000;">//</span><span style="color: #008000;">提取websocket传的key并进行加密 (这是固定的握手机制获取Sec-WebSocket-Key:里面的key)</span> <span style="color: #800080;">$buf</span> = <span style="color: #008080;">substr</span>(<span style="color: #800080;">$buff</span>,<span style="color: #008080;">strpos</span>(<span style="color: #800080;">$buff</span>,'Sec-WebSocket-Key:')+18<span style="color: #000000;">); </span><span style="color: #008000;">//</span><span style="color: #008000;">去除换行空格字符</span> <span style="color: #800080;">$key</span> = <span style="color: #008080;">trim</span>(<span style="color: #008080;">substr</span>(<span style="color: #800080;">$buf</span>,0,<span style="color: #008080;">strpos</span>(<span style="color: #800080;">$buf</span>,"\r\n"<span style="color: #000000;">))); </span><span style="color: #008000;">//</span><span style="color: #008000;">固定的加密算法</span> <span style="color: #800080;">$new_key</span> = <span style="color: #008080;">base64_encode</span>(<span style="color: #008080;">sha1</span>(<span style="color: #800080;">$key</span>."258EAFA5-E914-47DA-95CA-C5AB0DC85B11",<span style="color: #0000ff;">true</span><span style="color: #000000;">)); </span><span style="color: #800080;">$new_message</span> = "HTTP/1.1 101 Switching Protocols\r\n"<span style="color: #000000;">; </span><span style="color: #800080;">$new_message</span> .= "Upgrade: websocket\r\n"<span style="color: #000000;">; </span><span style="color: #800080;">$new_message</span> .= "Sec-WebSocket-Version: 13\r\n"<span style="color: #000000;">; </span><span style="color: #800080;">$new_message</span> .= "Connection: Upgrade\r\n"<span style="color: #000000;">; </span><span style="color: #800080;">$new_message</span> .= "Sec-WebSocket-Accept: " . <span style="color: #800080;">$new_key</span> . "\r\n\r\n"<span style="color: #000000;">; </span><span style="color: #008000;">//</span><span style="color: #008000;">将套接字写入缓冲区</span> socket_write(<span style="color: #800080;">$v</span>,<span style="color: #800080;">$new_message</span>,<span style="color: #008080;">strlen</span>(<span style="color: #800080;">$new_message</span><span style="color: #000000;">)); </span><span style="color: #008000;">//</span><span style="color: #008000;"> socket_write(socket,$upgrade.chr(0), strlen($upgrade.chr(0))); //标记此套接字握手成功</span> <span style="color: #800080;">$this</span>->hand[(int)<span style="color: #800080;">$client</span>]=<span style="color: #0000ff;">true</span><span style="color: #000000;">; }</span>
5. 클라이언트 데이터를 구문 분석합니다(여기에서는 암호화하지 않았습니다. 필요한 경우 직접 암호화할 수 있습니다)
<span style="color: #008000;">//</span><span style="color: #008000;">解析数据</span> <span style="color: #0000ff;">public</span> <span style="color: #0000ff;">function</span> decodeData(<span style="color: #800080;">$buff</span><span style="color: #000000;">) { </span><span style="color: #008000;">//</span><span style="color: #008000;">$buff 解析数据帧</span> <span style="color: #800080;">$mask</span> = <span style="color: #0000ff;">array</span><span style="color: #000000;">(); </span><span style="color: #800080;">$data</span> = ''<span style="color: #000000;">; </span><span style="color: #800080;">$msg</span> = <span style="color: #008080;">unpack</span>('H*',<span style="color: #800080;">$buff</span>); <span style="color: #008000;">//</span><span style="color: #008000;">用unpack函数从二进制将数据解码</span> <span style="color: #800080;">$head</span> = <span style="color: #008080;">substr</span>(<span style="color: #800080;">$msg</span>[1],0,2<span style="color: #000000;">); </span><span style="color: #0000ff;">if</span> (<span style="color: #008080;">hexdec</span>(<span style="color: #800080;">$head</span>{1}) === 8<span style="color: #000000;">) { </span><span style="color: #800080;">$data</span> = <span style="color: #0000ff;">false</span><span style="color: #000000;">; }</span><span style="color: #0000ff;">else</span> <span style="color: #0000ff;">if</span> (<span style="color: #008080;">hexdec</span>(<span style="color: #800080;">$head</span>{1}) === 1<span style="color: #000000;">){ </span><span style="color: #800080;">$mask</span>[] = <span style="color: #008080;">hexdec</span>(<span style="color: #008080;">substr</span>(<span style="color: #800080;">$msg</span>[1],4,2<span style="color: #000000;">)); </span><span style="color: #800080;">$mask</span>[] = <span style="color: #008080;">hexdec</span>(<span style="color: #008080;">substr</span>(<span style="color: #800080;">$msg</span>[1],6,2<span style="color: #000000;">)); </span><span style="color: #800080;">$mask</span>[] = <span style="color: #008080;">hexdec</span>(<span style="color: #008080;">substr</span>(<span style="color: #800080;">$msg</span>[1],8,2<span style="color: #000000;">)); </span><span style="color: #800080;">$mask</span>[] = <span style="color: #008080;">hexdec</span>(<span style="color: #008080;">substr</span>(<span style="color: #800080;">$msg</span>[1],10,2<span style="color: #000000;">)); </span><span style="color: #008000;">//</span><span style="color: #008000;">遇到的问题 刚连接的时候就发送数据 显示 state connecting</span> <span style="color: #800080;">$s</span> = 12<span style="color: #000000;">; </span><span style="color: #800080;">$e</span> = <span style="color: #008080;">strlen</span>(<span style="color: #800080;">$msg</span>[1])-2<span style="color: #000000;">; </span><span style="color: #800080;">$n</span> = 0<span style="color: #000000;">; </span><span style="color: #0000ff;">for</span> (<span style="color: #800080;">$i</span>=<span style="color: #800080;">$s</span>; <span style="color: #800080;">$i</span><= <span style="color: #800080;">$e</span>; <span style="color: #800080;">$i</span>+= 2<span style="color: #000000;">) { </span><span style="color: #800080;">$data</span> .= <span style="color: #008080;">chr</span>(<span style="color: #800080;">$mask</span>[<span style="color: #800080;">$n</span>%4]^<span style="color: #008080;">hexdec</span>(<span style="color: #008080;">substr</span>(<span style="color: #800080;">$msg</span>[1],<span style="color: #800080;">$i</span>,2<span style="color: #000000;">))); </span><span style="color: #800080;">$n</span>++<span style="color: #000000;">; } </span><span style="color: #008000;">//</span><span style="color: #008000;">发送数据到客户端 //如果长度大于125 将数据分块</span> <span style="color: #800080;">$block</span>=<span style="color: #008080;">str_split</span>(<span style="color: #800080;">$data</span>,125<span style="color: #000000;">); </span><span style="color: #800080;">$mess</span>=<span style="color: #0000ff;">array</span><span style="color: #000000;">( </span>'mess'=><span style="color: #800080;">$block</span>[0],<span style="color: #000000;"> ); </span><span style="color: #0000ff;">return</span> <span style="color: #800080;">$mess</span><span style="color: #000000;">; }</span>
6. 버퍼에 소켓 쓰기
<span style="color: #008000;">//</span><span style="color: #008000;">发送数据</span> <span style="color: #0000ff;">public</span> <span style="color: #0000ff;">function</span> send(<span style="color: #800080;">$mess</span>,<span style="color: #800080;">$v</span><span style="color: #000000;">) { </span><span style="color: #008000;">//</span><span style="color: #008000;">遍历套接字数组 成功握手的 进行数据群发</span> <span style="color: #0000ff;">foreach</span> (<span style="color: #800080;">$this</span>->socs <span style="color: #0000ff;">as</span> <span style="color: #800080;">$keys</span> => <span style="color: #800080;">$values</span><span style="color: #000000;">) { </span><span style="color: #008000;">//</span><span style="color: #008000;">用系统分配的套接字资源id作为用户昵称</span> <span style="color: #800080;">$mess</span>['name']="Tourist's socket:{<span style="color: #800080;">$v</span>}"<span style="color: #000000;">; </span><span style="color: #800080;">$str</span>=json_encode(<span style="color: #800080;">$mess</span><span style="color: #000000;">); </span><span style="color: #800080;">$writes</span> ="\x81".<span style="color: #008080;">chr</span>(<span style="color: #008080;">strlen</span>(<span style="color: #800080;">$str</span>)).<span style="color: #800080;">$str</span><span style="color: #000000;">; </span><span style="color: #008000;">//</span><span style="color: #008000;"> ob_flush(); // flush(); // sleep(3);</span> <span style="color: #0000ff;">if</span>(<span style="color: #800080;">$this</span>->hand[(int)<span style="color: #800080;">$values</span><span style="color: #000000;">]) socket_write(</span><span style="color: #800080;">$values</span>,<span style="color: #800080;">$writes</span>,<span style="color: #008080;">strlen</span>(<span style="color: #800080;">$writes</span><span style="color: #000000;">)); } }</span>
7、运行方法
github地址git@github.com:rsaLive/websocket.git
①最好在控制台运行server.php
转到server.php脚本目录(可以先php -v 看下有没有配置php如果没有Linux配置下bash windows 配置下path)
php -f server.php
如果有错误会提示
②通过服务器访问html文件
8、踩过的坑,打开调试工作方便查看错误
①server.php 挂起的进程中可以打印输出的,如果出现问题可以在代码中加入打印来调试
可以在各个判断里面做标记在控制台查看代码运行在哪个区间
不过每次修改完代码之后需要重新运行脚本 php server.php
②
如果出现这种错误可能是
1、在与服务器初始套接字的时候发送数据 (在第一次与服务器验证握手的时候不能发送内容)
2、如果已经验证过了但是客户端没有发送或者发送的消息为空也会出现这样的情况
所以要检验已连接的套接字的数据
③可能浏览器不支持或者服务端没有开启socket开始之前最好验证下
<span style="color: #000000;">if (window.WebSocket){ console.log("This browser supports WebSocket!"); } else { console.log("This browser does not support WebSocket."); }</span>
如有不正欢迎指出