목차
웹소켓
소개
http와의 관계
악수
服务器逻辑
客户端
创建客户端
页面功能
用户名异步处理
小结
聊天室扩展方向
总结
php教程 php手册 웹소켓 구현을 위해 PHP를 사용한 웹 실시간 채팅

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

Nov 30, 2016 pm 11:59 PM

머리말

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이라는 고유한 데이터 전송 형식이 있습니다. 다음 그림은 단위가 비트인 데이터 프레임의 구조입니다.

  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 ...                |
 +---------------------------------------------------------------+
로그인 후 복사
각 필드의 구체적인 의미는 무엇입니까? 관심이 있는 경우 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로 변환할 수 있으므로 '클래스 인덱스 배열'을 사용하여 소켓 리소스와 해당 사용자 정보를 저장할 수 있습니다.

결과는 비슷합니다.

$connected_sockets = array(
    (int)$socket => array(
        'resource' => $socket,
        'name' => $name,
        'ip' => $ip,
        'port' => $port,
        ...
    )
)
로그인 후 복사
서버 소켓 생성

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

// 创建一个 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);
로그인 후 복사

这样,我们就得到一个服务器 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 返回。

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

$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);
        }
    }
}
로그인 후 복사

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

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

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

客户端

创建客户端

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

// 创建一个 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 () {
};
로그인 후 복사

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

页面功能

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

function confirm(event) {
    var key_num = event.keyCode;
    if (13 == key_num) {
        send();
    } else {
        return false;
    }
}
로그인 후 복사

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

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

用户名异步处理

这里不得不提一下用户登陆时确定用户名时的一个小问题,我原来是想在客户端创建一个连接后直接发送用户名到服务器,可是控制台里报出了 “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协议—从顶层到底层的实现原理(修订版)

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

본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.

핫 AI 도구

Undresser.AI Undress

Undresser.AI Undress

사실적인 누드 사진을 만들기 위한 AI 기반 앱

AI Clothes Remover

AI Clothes Remover

사진에서 옷을 제거하는 온라인 AI 도구입니다.

Undress AI Tool

Undress AI Tool

무료로 이미지를 벗다

Clothoff.io

Clothoff.io

AI 옷 제거제

AI Hentai Generator

AI Hentai Generator

AI Hentai를 무료로 생성하십시오.

인기 기사

R.E.P.O. 에너지 결정과 그들이하는 일 (노란색 크리스탈)
3 몇 주 전 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O. 최고의 그래픽 설정
3 몇 주 전 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O. 아무도들을 수없는 경우 오디오를 수정하는 방법
3 몇 주 전 By 尊渡假赌尊渡假赌尊渡假赌
WWE 2K25 : Myrise에서 모든 것을 잠금 해제하는 방법
4 몇 주 전 By 尊渡假赌尊渡假赌尊渡假赌

뜨거운 도구

메모장++7.3.1

메모장++7.3.1

사용하기 쉬운 무료 코드 편집기

SublimeText3 중국어 버전

SublimeText3 중국어 버전

중국어 버전, 사용하기 매우 쉽습니다.

스튜디오 13.0.1 보내기

스튜디오 13.0.1 보내기

강력한 PHP 통합 개발 환경

드림위버 CS6

드림위버 CS6

시각적 웹 개발 도구

SublimeText3 Mac 버전

SublimeText3 Mac 버전

신 수준의 코드 편집 소프트웨어(SublimeText3)