> php教程 > php手册 > 웹소켓 구현을 위해 PHP를 사용한 웹 실시간 채팅

웹소켓 구현을 위해 PHP를 사용한 웹 실시간 채팅

WBOY
풀어 주다: 2016-11-30 23:59:33
원래의
1602명이 탐색했습니다.

머리말

HTML5의 새로운 기능인 websocket은 http의 "요청-응답"에 대한 기존의 생각을 깨고 서버가 클라이언트에 메시지를 적극적으로 푸시할 수 있다는 점에서 항상 많은 관심을 받아왔습니다. 이를 사용하기 위해 PHP와 JS는 websocket을 적용하여 웹 실시간 채팅방을 구현합니다.

이전에 Ajax 롱 폴링을 사용하여 실시간 웹 채팅을 구현하는 방법에 대한 기사를 작성했습니다. 링크를 참조하세요. js 및 jQuery를 사용하여 ajax 롱 폴링을 구현하지만 폴링 및 서버 보류는 모두 불필요합니다. 소비 웹소켓이 새로운 트렌드입니다.

최근에는 어렵게 "압축"하고, 오래전에 만들었던 웹소켓 "요청-반환" 서버를 완성하고, 클라이언트 기능을 js로 개선한 과정과 아이디어를 공유하겠습니다. 물론 웹소켓에 관한 지식을 살펴보겠습니다. 물론 지금은 몇 가지 이론적인 내용은 건너뛰고 모든 사람이 선택하여 읽을 수 있도록 참고 자료를 제공하겠습니다.

텍스트를 시작하기 전에 채팅방 렌더링을 게시하겠습니다(CSS 쓰레기 페이지에 주의하지 마세요).

물론 소스 코드는 다음과 같습니다.

내가 바로 소스 코드 링크 - github - Pillow Book


웹소켓

소개

WebSocket은 기술이 아니라 완전히 새로운 프로토콜입니다. 이는 TCP의 소켓(소켓)을 사용하고 네트워크 애플리케이션을 위한 새로운 중요한 기능인 전이중 전송 및 클라이언트와 서버 간의 양방향 통신을 정의합니다. Java 애플릿, XMLHttpRequest, Adobe Flash, ActiveXObject 및 다양한 Comet 기술 이후에 서버가 클라이언트 메시지를 푸시하는 것이 새로운 추세입니다.

http와의 관계

네트워크 계층 측면에서 websocket과 http 프로토콜은 모두 애플리케이션 계층 프로토콜입니다. 둘 다 tcp 전송 계층을 기반으로 합니다. 그러나 websocket은 연결을 설정할 때 프로토콜 변환(업그레이드)을 달성하기 위해 http의 101 스위치 프로토콜을 차용합니다. 예, HTTP 프로토콜에서 WebSocket 통신 프로토콜로 전환합니다.

핸드셰이크가 성공한 후 websocket은 자체 프로토콜에 지정된 방법을 사용하여 통신하며 http와는 아무 관련이 없습니다.

악수

다음은 내 브라우저에서 보낸 일반적인 핸드셰이크 http 헤더입니다.

서버는 핸드셰이크 요청을 받은 후 요청 헤더에서 "Sec-WebSocket-Key" 필드를 추출하고 고정 문자열 '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'을 복구한 후 sha1 암호화를 수행하고 최종적으로 base64 인코딩으로 변환하고 "Sec-WebSocket-Accept" 필드를 키로 사용하여 클라이언트에 반환하면 연결이 설정되고 핸드셰이크가 완료됩니다.

데이터 전송

Websocket에는 Frame이라는 고유한 데이터 전송 형식이 있습니다. 다음 그림은 단위가 비트인 데이터 프레임의 구조입니다.

<code class="language-none">  0                   1                   2                   3
  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
 +-+-+-+-+-------+-+-------------+-------------------------------+
 |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
 |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
 |N|V|V|V|       |S|             |   (if payload len==126/127)   |
 | |1|2|3|       |K|             |                               |
 +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
 |     Extended payload length continued, if payload len == 127  |
 + - - - - - - - - - - - - - - - +-------------------------------+
 |                               |Masking-key, if MASK set to 1  |
 +-------------------------------+-------------------------------+
 | Masking-key (continued)       |          Payload Data         |
 +-------------------------------- - - - - - - - - - - - - - - - +
 :                     Payload Data continued ...                :
 + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
 |                     Payload Data continued ...                |
 +---------------------------------------------------------------+</code>
로그인 후 복사
각 필드의 구체적인 의미는 무엇입니까? 관심이 있는 경우 The WebSocket Protocol 기사를 읽어보세요. 5. 데이터 프레임은 이진 연산에서 그다지 유연하지 않다고 느껴지므로 알고리즘을 작성하는 데 어려움이 없습니다. 데이터를 구문 분석하려면 다음 데이터 프레임 구문 분석 및 캡슐화는 온라인 알고리즘을 사용합니다.

그러나 내 작업에서는 결제 게이트웨이를 작성할 때 데이터 16진수 연산이 자주 사용됩니다. 이를 주의 깊게 연구하고 요약해야 합니다.


PHP는 웹소켓 서버를 구현합니다

PHP에서 웹소켓을 구현하는 경우 주로 PHP의 소켓 함수 라이브러리를 사용합니다.

PHP의 소켓 함수 라이브러리는 C 언어의 소켓 함수와 매우 유사합니다. 이전에 APUE를 한 번 읽어본 적이 있어서 꽤 이해하기 쉬운 것 같습니다. PHP 매뉴얼의 소켓 기능을 읽고 나면 모든 사람이 PHP의 소켓 프로그래밍에 대해 어느 정도 이해할 수 있을 것이라고 생각합니다.

사용된 기능은 아래 코드에 간략하게 설명하겠습니다.

파일 설명자

갑자기 '파일 설명자'가 언급되어 조금 놀라실 수도 있습니다.

단, 서버로서 연결된 소켓을 저장하고 식별하는 것이 필요합니다. 각 소켓은 사용자를 나타냅니다. 사용자 정보와 소켓 간의 대응을 연결하고 쿼리하는 방법은 파일 설명자에 대한 약간의 트릭이 적용됩니다.

리눅스는 '모든 것이 파일'이고 C 언어의 소켓 구현은 '파일 설명자'라는 것을 알고 있습니다. 이 파일 설명자는 일반적으로 파일이 열리는 순서대로 증가하는 int 값입니다. 0부터. (물론 시스템에는 한계가 있습니다). 각 소켓은 파일에 해당하며 해당 파일에 대해 읽기 및 쓰기 소켓이 작동하므로 읽기 및 쓰기 기능도 파일 시스템처럼 적용할 수 있습니다.

팁: Linux에서 표준 입력은 파일 설명자 0에 해당하고, 표준 오류는 파일 설명자 2에 해당하므로 0 1 2를 사용하여 입력 및 출력을 리디렉션할 수 있습니다.

그러면 C 소켓과 유사한 PHP 소켓이 자연스럽게 이를 상속하고, 그것이 생성하는 소켓도 int 값 4 5와 같은 리소스 유형 유형입니다. (int) 또는 intval() 함수를 사용하여 소켓을 고유 ID로 변환할 수 있으므로 '클래스 인덱스 배열'을 사용하여 소켓 리소스와 해당 사용자 정보를 저장할 수 있습니다.

결과는 비슷합니다.

<code class="language-none">$connected_sockets = array(
    (int)$socket => array(
        'resource' => $socket,
        'name' => $name,
        'ip' => $ip,
        'port' => $port,
        ...
    )
)</code>
로그인 후 복사
서버 소켓 생성

다음은 서버 소켓을 생성하는 코드입니다.

<code class="language-none">// 创建一个 TCP socket, 此函数的可选值在官方文档中写得十分详细,这里不再提了
$this->master = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
// 设置IP和端口重用,在重启服务器后能重新使用此端口;
socket_set_option($this->master, SOL_SOCKET, SO_REUSEADDR, 1);
// 将IP和端口绑定在服务器socket上;
socket_bind($this->master, $host, $port);
// listen函数使主动连接套接口变为被连接套接口,使得此 socket 能被其他 socket 访问,从而实现服务器功能。后面的参数则是自定义的待处理socket的最大数目,并发高的情况下,这个值可以设置大一点,虽然它也受系统环境的约束。
socket_listen($this->master, self::LISTEN_SOCKET_NUM);</code>
로그인 후 복사

这样,我们就得到一个服务器 socket,当有客户端连接到此 socket 上时,它将改变状态为可读,那就看接下来服务器的处理逻辑了。

服务器逻辑

这里着重讲一下 socket_select($read, $write, $except, $tv_sec [, $tv_usec]):

select 函数使用传统的 select 模型,可读、写、异常的 socket 会被分别放入 $socket, $write, $except 数组中,然后返回 状态改变的 socket 的数目,如果发生了错误,函数将会返回 false.

需要注意的是最后两个时间参数,它们只有单位不同,可以搭配使用,用来表示 socket_select 阻塞的时长,为0时此函数立即返回,可以用于轮询机制。 为 NULL 时,函数会一直阻塞下去, 这里我们置 $tv_sec 参数为null,让它一直阻塞,直到有可操作的 socket 返回。

下面是服务器的主要逻辑:

<code class="language-none">$write = $except = NULL;
$sockets = array_column($this->sockets, 'resource'); // 获取到全部的 socket 资源
$read_num = socket_select($sockets, $write, $except, NULL);

foreach ($sockets as $socket) {
        // 如果可读的是服务器 socket, 则处理连接逻辑;            
        if ($socket == $this->master) {
            socket_accept($this->master);
            // socket_accept() 接受 请求 “正在 listen 的 socket(像我们的服务器 socket )” 的连接, 并一个客户端 socket, 错误时返回 false;
             self::connect($client);
             continue;
            }
        // 如果可读的是其他已连接 socket ,则读取其数据,并处理应答逻辑
        } else {
            // 函数 socket_recv() 从 socket 中接受长度为 len 字节的数据,并保存在 $buffer 中。
            $bytes = @socket_recv($socket, $buffer, 2048, 0);

            if ($bytes < 9) {
                // 当客户端忽然中断时,服务器会接收到一个 8 字节长度的消息(由于其数据帧机制,8字节的消息我们认为它是客户端异常中断消息),服务器处理下线逻辑,并将其封装为消息广播出去
                $recv_msg = $this->disconnect($socket);
            } else {
                // 如果此客户端还未握手,执行握手逻辑
                if (!$this->sockets[(int)$socket]['handshake']) {
                    self::handShake($socket, $buffer);
                    continue;
                } else {
                    $recv_msg = self::parse($buffer);
                }
            }

            // 广播消息
            $this->broadcast($msg);
        }
    }
}</code>
로그인 후 복사

这里只是服务器处理消息的基础代码,日志记录和异常处理都略过了,而且还有些数据帧解析和封装的方法,各位也不一定看爱,有兴趣的可以去 github 上支持一下我的源码~~

此外,为了便于服务器与客户端的交互,我自己定义了 json 类型的消息格式,形似:

<code class="language-none">$msg = [
    'type' => $msg_type, // 有普通消息,上下线消息,服务器消息
    'from' => $msg_resource, // 消息来源
    'content' => $msg_content, // 消息内容
    'user_list' => $uname_list, // 便于同步当前在线人数与姓名
    ];</code>
로그인 후 복사

客户端

创建客户端

前端我们使用 js 调用 Websocket 方法很简单就能创建一个 websocket 连接,服务器会为帮我们完成连接、握手的操作,js 使用事件机制来处理浏览器与服务器的交互:

<code class="language-none">// 创建一个 websocket 连接
var ws = new WebSocket("ws://127.0.0.1:8080");

// websocket 创建成功事件
ws.onopen = function () {
};

// websocket 接收到消息事件
ws.onmessage = function (e) {
    var msg = JSON.parse(e.data);
}

// websocket 错误事件
ws.onerror = function () {
};</code>
로그인 후 복사

发送消息也很简单,直接调用 ws.send(msg) 方法就行了。

页面功能

页面部分主要是让用户使用起来方便,这里给消息框 textarea 添加了一个键盘监控事件,当用户按下回车键时直接发送消息;

<code class="language-none">function confirm(event) {
    var key_num = event.keyCode;
    if (13 == key_num) {
        send();
    } else {
        return false;
    }
}</code>
로그인 후 복사

还有用户打开客户端时生成一个默认唯一用户名;

然后是一些对数据的解析构造,对客户端页面的更新,这里就不再啰嗦了,感兴趣的可以看源码。

用户名异步处理

这里不得不提一下用户登陆时确定用户名时的一个小问题,我原来是想在客户端创建一个连接后直接发送用户名到服务器,可是控制台里报出了 “websocket 仍在连接中或已关闭” 的错误信息。

Uncaught DOMException: Failed to execute 'send' on 'WebSocket': Still in CONNECTING state.

考虑到连接可能还没处理好,我就实现了 sleep 方法等了一秒再发送用户名,可是错误仍然存在。

后来忽然想到 js 的单线程阻塞机制,才明白使用 sleep 一直阻塞也是没有用的,利用好 js 的事件机制才是正道:于是在服务器端添加逻辑,在握手成功后,向客户端发送握手已成功的消息;客户端先将用户名存入一个全局变量,接收到服务器的握手成功的提醒消息后再发送用户名,于是成功在第一时间更新用户名。


小结

聊天室扩展方向

简易聊天室已经完成,当然还要给它带有希望的美好未来,希望有人去实现:

  • 页面美化(信息添加颜色等)
  • 服务器识别 '@' 字符而只向某一个 socket 写数据实现聊天室的私聊;
  • 多进程(使用 redis 等缓存数据库来实现资源的共享),可参考我以前的一篇文章: 初探PHP多进程
  • 消息记录数据库持久化(log 日志还是不方便分析)
  • ...

总结

多读些经典书籍还是很有用的,有些东西真的是触类旁通,APUE/UNP 还是要再多翻几遍。此外互联网技术日新月异,挑一些自己喜欢的学习一下,跟大家分享一下也是挺舒服的(虽然程序和博客加一块用了至少10个小时...)。

参考:

websocket协议翻译

刨根问底 HTTP 和 WebSocket 协议(下)

学习WebSocket协议—从顶层到底层的实现原理(修订版)

嗯,持续更新。喜欢的可以点个推荐或关注,有错漏之处,请指正,谢谢。

원천:php.cn
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
인기 추천
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿