专栏首页可能是东半球最正规的API社区再次和老李一起憋山寨Workerman(九)

再次和老李一起憋山寨Workerman(九)

大家好,我是嗓音你从未听过、相貌你们尚未见过、随手一编就能瞎编文章一坨的老李。

今天接着昨天的socket_recv()继续编,上来就得先尝试解决一个问题:客户端每次发来的数据长度都是不固定的,怎么办?

你问:“ 啊?我哪儿知道怎么办?老李,难不成你今天这篇文章要说这个? ”

我答:“ 是啊,是啊 ”

你问:“ 啊,这都被我猜到了! ”

我答:“ 是啊是啊,不然这TM文章我都不知道该这么继续编下去了 ”

你问:“ 哎呀呀呀,哪儿你这样写文章的,编不下去强行编 ”

我答:

其实每次数据长度不固定就是原本的网络世界,每次发送数据长度都一样才叫诡异。一般说来,这个问题有两种解决方案:

  • header+body:其中header里只有一个长度,这个长度就是后面body体的长度。当读取了header里存储的body长度后,就可以指定socket_recv()里的length了。
  • EOF:这种比较粗暴,约定数据分界线的分界标识符,比如\r\n,也就是说服务器收到数据后一旦遇到\r\n就算到头;这里处理上的细节就是使用while循环调用socket_recv(),一直读到有\r\n

我琢磨了一下,决定采用第一种,主要是第二种写demo代码比较恶心而我又比较懒。在正式开始前,我要介绍一对函数,这对函数可能有人接触过,而且还对着文档研究过,但是研究后又发现似乎完全看不懂文档在说什么;然后我还要再介绍一对概念:

  • pack()/unpack()
  • 网络字节序/主机字节序

先说下网络字节序和主机字节序,这两个词英文分别叫做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至少提升了两点:

  • 预先fork,避免系统在遭遇突发访问时不断fork导致系统资源爆炸
  • fork后的进程不会在处理完成后退出,而是接着服务下一个客户端,避免了反复fork

你们是不是以为这就完了...太年轻了,有时候还显得颇为幼稚,我编得正在兴头上,你们竟然想结束?好了,请看这里:

也就是代码里的:

// 这个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歇着?这种好事儿有的话,麻烦后台告诉我一声,五块包邮给我来一打...

怎么办呢?


本文中引用的相关资料信息关键字:

  • 影流之主
  • UNP(UNIX网络编程)
  • 《疯狂的赛车》以及《楚汉争霸》

本文分享自微信公众号 - 高性能API社区(high-performance-api),作者:老李秀

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-01-05

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 携手老李一起整山寨Workerman(八)

    大家好,我还是那个文风浮夸词藻华丽、内容正规内涵犀利、写出一篇文章往那里一放,就能吸引极少数泥腿子的老李。

    老李秀
  • PHP中on回调的实现(十六节)

    各位好,我是老李。和老李一同完成《PHP网络编程》,虽然我知道实际上从头到尾可能只有我一个人在搞。我告诉你们一定要好好在家好好学习、远程工作,不要折腾地自己最后...

    老李秀
  • PHP网络编程之深入Libevent(十五节)

    大家周末好,这里有趣有用广告少的公众号高性能API社区,我是老李,本文属于《PHP网络编程》系列中的一个章节。

    老李秀
  • php 纯socket编程核心的东西!socket_read阻塞的问题!

    最大视角-从Unix底层 理解 python的io模型、python异步IO、python的select、Unix的select、epoll fileno 的...

    waki
  • python Socket模块

    send_data = struct.pack('!H8sb5sb',1,'test.jpg',0,'octet',0) ========>利用pack可以规定...

    py3study
  • [python网络编程]socket

    1.在建立socket对象的时候,需要告诉系统两件事情 1.1 通信的类型是什么(IPv4/IPv6等) 1.2 使用的协议是什么?(TCP/UDP等)

    py3study
  • Python 中的 socket 模块

    import socket help(socket)     Functions:     socket() -- create a new socket o...

    py3study
  • python之socket

    socket 是网络连接端点,每个socket都被绑定到一个特定的IP地址和端口。IP地址是一个由4个数组成的序列,这4个数均是范围 0~255中的值(例如,...

    py3study
  • [javaSE] 网络编程(浏览器客户端-自定义服务端)

    获取PrintWriter对象,new出来,构造参数:OutputSream对象,true自动刷新

    陶士涵
  • Python中socket的UDP学习(1)

    TCP是建立可靠连接,并且通信双方都可以以流的形式发送数据。相对TCP,UDP则是面向无连接的协议。

    萌海无涯

扫码关注云+社区

领取腾讯云代金券