大家好,我是嗓音你从未听过、相貌你们尚未见过、随手一编就能瞎编文章一坨的老李。
今天接着昨天的socket_recv()继续编,上来就得先尝试解决一个问题:客户端每次发来的数据长度都是不固定的,怎么办?
你问:“ 啊?我哪儿知道怎么办?老李,难不成你今天这篇文章要说这个? ”
我答:“ 是啊,是啊 ”
你问:“ 啊,这都被我猜到了! ”
我答:“ 是啊是啊,不然这TM文章我都不知道该这么继续编下去了 ”
你问:“ 哎呀呀呀,哪儿你这样写文章的,编不下去强行编 ”
我答:
其实每次数据长度不固定就是原本的网络世界,每次发送数据长度都一样才叫诡异。一般说来,这个问题有两种解决方案:
我琢磨了一下,决定采用第一种,主要是第二种写demo代码比较恶心而我又比较懒。在正式开始前,我要介绍一对函数,这对函数可能有人接触过,而且还对着文档研究过,但是研究后又发现似乎完全看不懂文档在说什么;然后我还要再介绍一对概念:
先说下网络字节序和主机字节序,这两个词英文分别叫做big-endian/little-endian,所以有些地方翻译过来又分别叫大端字节序和小端字节序。据说啊,从这计算机出生的时候,用的就是主机字节序(小端字节序)大概意思就是电路优先从低位开始读取数据效率贼高,谁知道后来有了网,这网上的数据都是网络字节序。这两个玩意的历史大概就是这样的。
比如说0x12345678这个数字(十六进制),高位其实就是1,最低位是8,其实就是百分位,比如说[ 178元 ]中1所在位置是百位、7是十位、8是个位。假如说内存地址从0x0001开始一直到0x0004(从低内存到高内存),那么我们下面分别按照网络字节序和主机字节序在内存中存储应该是什么样的。
这个数字按照网络字节序在内存里排列,就是这样的,也就是高位字节放在了低内存处:
0x0004 78
0x0003 56
0x0002 34
0x0001 12
按照主机字节序在内存排列,那就是这样的,也就是说高位字节放在高内存处:
0x0004 12
0x0003 34
0x0002 56
0x0001 78
到这里事情就清楚了,如果说从网上飞来一坨数据钻到主机的内存里后,CPU如果要读出来就一定会存在字节序的问题了,也就是字节序是需要转换的。可惜在PHP里对字节序转换可能并不多,如果看UNP或者APUE的话,里面有大量函数提供主机/网络字节序转换功能,我举几个例子:
htons()/ntohs()
分别是将主机字节序转为网络字节序、将网络字节序转为主机字节序
这个可以满足short类型数值
htonl()/ntohl()
分别是将主机字节序转为网络字节序、将网络字节序转为主机字节序
这个可以满足long类型数值
inet_ntoa()/inet_aton()
这个是将IP地址按照规则进行转换的函数
在PHP里,如果你想要按照某种字节序飞数据,那么pack()函数就可以帮你满足愿望,当然了负责接受数据的地方自然要用unpack()来处理应对咯。
UNP和APUE这么恶心,所以你们是不是感觉用PHP很幸福呢?大声告诉我用PHP香不香?
好了,代码我写好了,你们复制粘贴一下跑跑看看能不能用。
服务器
<?php
$host = '0.0.0.0';
$port = 9999;
// 创建一个tcp socket
$listen_socket = socket_create( AF_INET, SOCK_STREAM, SOL_TCP );
// 将socket bind到IP:port上
socket_bind( $listen_socket, $host, $port );
// 开始监听socket
socket_listen( $listen_socket );
while( true ){
// 所以你不用担心while循环会将机器拖垮,不会的
$connection_socket = socket_accept( $listen_socket );
// 从客户端读取信息
// 数据包是由 header+body 组成的
// 先读取header,header中包含剩余body体的具体长度
// 这里为什么用4呢?因为pack()里的N就是32位无符号整数
socket_recv( $connection_socket, $recv_content, 4, MSG_WAITALL );
// Nlen是啥意思?N是固定的,但是len可以随意换别的,比如Nval
// 然后我建议你打印一下$body_len_pack就知道了
$body_len_pack = unpack( "Nlen", $recv_content );
$body_len = $body_len_pack['len'];
// 有了body的长度,再使用socket_recv()指定长度读取即可
socket_recv( $connection_socket, $recv_content, $body_len, MSG_WAITALL );
echo "从客户端收到:{$recv_content}".PHP_EOL;
// 向客户端发送一个helloworld
$msg = "helloworld\r\n";
socket_write( $connection_socket, $msg, strlen( $msg ) );
socket_close( $connection_socket );
}
socket_close( $listen_socket );
客户端
<?php
$host = "127.0.0.1";
$port = 9999;
$content = "12345678123456781234567812345678";
$socket = socket_create( AF_INET, SOCK_STREAM, SOL_TCP );
$conn = socket_connect( $socket, $host, $port );
// payload = header + body
$body = pack( "a*", $content );
$body_len = strlen( $body );
echo $body.' | '.$body_len.PHP_EOL;
// pack()的N参数就表示按照网络字节序打包
$header = pack( "N", $body_len );
$payload = $header.$body;
$send_ret = socket_write( $socket, $payload, strlen( $payload ) );
var_dump( $send_ret );
感受下:
好了,这个问题我们算解决了,下面我们接着聊一聊服务器模型,在上一节里那个服务器,实际上在同一时刻只能供一个客户端使用,不信你可以在socket_accpet()后用sleep()模拟业务逻辑处理,然后同时用两个客户端连接试试,第二个连接上去上的客户端一定会在连接那里那里等待一会儿。
所以为了能伺候更多客户端,我先出一个馊主意,比如说当第二个客户端来的时候我们用fork()搞出一个子进程来处理当前客户端的,第三个客户端连接时候再fork()出第三个子进程来处理... ...
我改下代码,改动非常简单,你们感受一下:
<?php
$host = '0.0.0.0';
$port = 9999;
$listen_socket = socket_create( AF_INET, SOCK_STREAM, SOL_TCP );
socket_bind( $listen_socket, $host, $port );
socket_listen( $listen_socket );
while( true ){
$connection_socket = socket_accept( $listen_socket );
$pid = pcntl_fork();
// 在子进程里处理业务逻辑
if ( 0 == $pid ) {
socket_recv( $connection_socket, $recv_content, 4, MSG_WAITALL );
$body_len_pack = unpack( "Nlen", $recv_content );
$body_len = $body_len_pack['len'];
socket_recv( $connection_socket, $recv_content, $body_len, MSG_WAITALL );
echo "从客户端收到:{$recv_content}".PHP_EOL;
// 模拟5秒钟的业务逻辑处理
sleep( 5 );
$msg = "helloworld\r\n";
socket_write( $connection_socket, $msg, strlen( $msg ) );
socket_close( $connection_socket );
}
}
socket_close( $listen_socket );
这么做最大的问题是什么:如果有一千个客户端突然同时涌过来,就要fork()一千次。是的,fork()一千次,先不说这一千次fork()多费劲,就说fork()出来一千个进程... ...你要不要看看你们服务器一共有多少进程?我说这个也并不是绝对的,如果服务器CPU足够好,使用php-fpm没准真能开一千个进程。只是进程很明显,是很浪费资源的,所以这就是大家都采用线程的原因。
所以上述的代码我们还可以再进一步优化一下,就是根据平时业务繁忙程度预估一下访问量,优先fork()出固定数量的进程来(同样方法适用于线程),这种做法有一个专业地说法叫做pre-fork,我们再次改下上面的代码(实际上只有我改),你们感受一下:
<?php
$host = '0.0.0.0';
$port = 9999;
$listen_socket = socket_create( AF_INET, SOCK_STREAM, SOL_TCP );
socket_bind( $listen_socket, $host, $port );
socket_listen( $listen_socket );
cli_set_process_title( "Yaw Master Process" );
// fork出10个子进程
// 子进程会继承父进程中$listen_socket
// 因此每个子进程都以accept客户端了
for( $i = 1; $i <= 10; $i++ ) {
$pid = pcntl_fork();
if ( 0 == $pid ) {
cli_set_process_title( "Yaw Worker Process" );
while ( true ) {
$connection_socket = socket_accept( $listen_socket );
socket_recv( $connection_socket, $recv_content, 4, MSG_WAITALL );
$body_len_pack = unpack( "Nlen", $recv_content );
$body_len = $body_len_pack['len'];
socket_recv( $connection_socket, $recv_content, $body_len, MSG_WAITALL );
echo posix_getpid().PHP_EOL;
socket_close( $connection_socket );
}
}
}
while( true ) {
sleep( 1 );
}
这么做,就能提前保护好系统,反正一共就这么点儿进程;其次是这个pre-fork的数量是根据业务繁忙程度预估的,因此响应用户也不会有大问题。相比原来那个服务器,TA至少提升了两点:
你们是不是以为这就完了...太年轻了,有时候还显得颇为幼稚,我编得正在兴头上,你们竟然想结束?好了,请看这里:
也就是代码里的:
// 这个socket_accpet()我曾经说过是阻塞的,也正式得益于这个
// 所以我们的while循环才不会不断地打空炮
// 什么叫阻塞,就是当服务器在accpet客户端连接的时候,服务器代码
// 别的什么不干,只干这一件事,只等这一个客户端彻底完成连接
// 这就叫阻塞,此处请暂时临时以我为准
$connection_socket = socket_accept( $listen_socket );
那么我提一个问题,如果说一个客户端发起连接后服务器收到连接并把该请求放到了请求队列(就是请求队列,不理解先跳过)里,但是此时服务器还尚未执行accept,与此同时客户端却又断开了连接,然后很快服务器拿出队列里这个请求开始accept,由于这个$listen_socket是阻塞的,那么... ...你们能想象一下吗?服务器就会阻塞在accept这里一个劲等,但是客户端实际上已经断开连接跑路了。
所以怎么办?请使用非阻塞。
一旦这个$listen_socket变成了非阻塞IO,那么socket_accept()就不会卡着不动弹了。这也满足非阻塞的概念:也就是在服务器在accept的时候不会一直等着,如果没有结果TA会返回false然后立马开始跑别的请求accept,不断不断不断不断...比while循环还DIAO还密集。这样的话,是不是可以避免刚才我提出的问题了?代码修改起来非常简单,你们感受一下:
<?php
$host = '0.0.0.0';
$port = 9999;
$listen_socket = socket_create( AF_INET, SOCK_STREAM, SOL_TCP );
socket_bind( $listen_socket, $host, $port );
socket_listen( $listen_socket );
// 将listen_socket设置为非阻塞
socket_set_nonblock( $listen_socket );
cli_set_process_title( "Yaw Master Process" );
for( $i = 1; $i <= 50; $i++ ) {
$pid = pcntl_fork();
if ( 0 == $pid ) {
cli_set_process_title( "Yaw Worker Process" );
while ( true ) {
// 此时accept就会不断不断不断不断进行...
// 由于没有连接,accept会返回false
// 所以此处需要我们判断一下
// 如果你想参观一下非阻塞的不断不断不断不断
// 那么你可以人为echo输出一些东西。。感受下???
$connection_socket = socket_accept( $listen_socket );
if ( !$connection_socket ) {
continue;
}
socket_recv( $connection_socket, $recv_content, 4, MSG_WAITALL );
$body_len_pack = unpack( "Nlen", $recv_content );
$body_len = $body_len_pack['len'];
socket_recv( $connection_socket, $recv_content, $body_len, MSG_WAITALL );
echo posix_getpid().PHP_EOL;
socket_close( $connection_socket );
}
}
}
while( true ) {
sleep( 1 );
}
事情到这里你们是不是以为总算可以松一口气了?
还是那么的年轻可爱同时又不失幼稚...
上面那个非阻塞版本的服务器跑起来后,你们看CPU占用率了吗?没有的话赶紧去看看,是不是这样shai儿的?
既然TA会不断不断不断不断查询其他是否有其他accept,你还想让CPU歇着?这种好事儿有的话,麻烦后台告诉我一声,五块包邮给我来一打...
怎么办呢?
本文中引用的相关资料信息关键字: