专栏首页可能是东半球最正规的API社区携手老李一起整山寨Workerman(八)

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

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

在我们历尽铅华去伪存真孜孜不倦连续七个章节连拼带凑说完关于PHP与进程操控的基础章节后,我们终于可以进入到网络socket阶段了。

正当我准备开始憋正文如何开篇的时候,住在我隔壁的中年男人又开始陷入无限痛苦之中并在失控中开始暴躁吼叫。第一次听到他吼的时候还是上次老赵来我这里的时候,当时就听到嗷的一嗓子老赵筷子上夹着的猪肉吓得一哆嗦自己就掉进盘子里了。TA们是一对被辅导小孩儿写作业而逼疯的夫妇。一般流程是这样shai儿的,他媳妇先上阵辅导一波儿,但是一般在5-10分钟内情绪就会立马失控并冲出跑道,然后他媳妇退下后他立马替补上去,大约在10-20分钟后这老哥也开始逐渐失控,此时小孩儿也会失控并发出类似于大马猴子那种“ 细长而又高频 ”的哭声,到这会儿如果不出意外的话,这老哥就会打开窗子抽烟(我们都是朝南大主卧,即便是冬季,阳光足的时候不开窗子通风的话,整个屋子热气袭人)。这才刚过才完年第三天,你这就又开始吼孩子了,辅导个作业整的全家鬼哭狼嚎,小孩子委屈地坐在桌子旁不断啜泣,暴躁老哥一脸怒气然后站在窗子这儿“ baba ”地抽闷烟,有一有二无再三再四,这次实在是连我都看不过眼了,于是走到我这儿窗户旁边朝隔壁方向说:“ 先生您好,请您不要在室内抽烟,吸烟有害健康 ”。

在被回复了一声“ 玩蛋去 ”后,我将不得不在隔壁的狂风暴雨声中完成我们新征程的第一个辉煌篇章。

如果不出意外,这个周日或下周一的早上,还会有一波儿怒吼,下篇文章时候我录音让你们感受一下。

众所周知(依然是大概不超过十个人),我是一个相对来说比较务实的人,这个[ 相对 ]是比永强那种[ 能坐公交就一定不会坐地铁 ]的务实还是稍微有些不一样的。比如说我们写了七个章节的PHP与进程以及WM进程源码分析,那么我就得一定得山寨模仿一个出来,所以我已经写好了一个已经实现了start stop以及reload的Core文件,github地址在下面:

https://github.com/elarity/Yaw

其实这里说到务实,我还是忍不住想多聊聊务实,比如柱子,也很务实,凡事都是[ 不见兔子不撒鹰、见了兔子也尽量不撒鹰 ]的那种。老赵在微信群里发红包,如果柱子看到了这个红包他一定会去拼一波儿手气;如果大家接着让柱子发红包,他就会变成一个类似于[ 你们下次发的红包我不抢了,就当我给你们发红包了 ]这样的逻辑鬼才。我认为这也是务实的一种友好体现。

都已经快一千字了,我们还在鬼扯,我自己都看不过眼了。从这行开始我们聊聊关于PHP操控socket的内容,socket翻译过来叫做套接字,这里先简单说下[ socket与TCP、UDP ]的关系。首先要意识到一点,那就是无论是活在理论中的七层网络模型还是现实中一统浆糊的四层模型,socket都不是其中的一层,socket与网络模型没啥关系。

在现实中的四层网络模型中,从下往上数到第三层便是传输层

在理论中的七层网络模型中,从下往上数到第四层便是传输层

传输层中其实就是TCP和UDP两个协议所在地,而socket按我个人理解,就是对TCP、UDP的上层“ 操作封装 ”,但socket不是独立的一层。

在PHP里,操作socket有两大系函数,一系是stream前缀的系列,另一系是socket前缀的系列,我截个图你们感受一下:

stream系其实就是PHP中流的概念,因为一般说来诸位腿子们用PHP都在搞Web堆网页所以对流接触的并不多,流对各种协议都做了一层抽象封装,比如[ http:// ]、[ file:// ]、[ ftp:// ]、[ php://input ]等等,也就说流系列函数提供了统一的函数来处理各种各样的花式协议。在Workerman里,就是使用stream系函数实现的tcp、udp等服务器。

socket系应该是就PHP直接对底层socket API的一层粗暴封装,说白了就是把C语言版本的socket()、accpet()、listen()拿过来很粗暴的处理一下就算能用了,这里要补充的是之前我在写advance-php系列的时候是妄加猜测的主观认为socket系函数对直接对socket API的封装,这次我决定验证一下,主要是怕被打脸:

我们这儿呢,为了保证自主产权独立性,就和Workerman劈开一下吧:我们将会选用socket系函数进行基础解析并实现。

So, Let‘s ROCK~

其实socket用我同事的话说就是[ 非常简单 ]的(用TA话说就是天天socket socket,就那点儿东西),你就记住了一个流程四个步骤:

create -> bind -> listen -> accept

先摆一个非常粗暴的案例,你们先感受下,请留意代码里的注释:

<?php
$host = '0.0.0.0';
$port = 9999;
// 创建一个tcp socket,底层就是对socket() API的封装
// 第一个参数有AF_INET、AF_INET6、AF_UNIX三种,其实分别就是IPv4、IPv6、文件sock的意思
// 第二个参数有SOCK_STREAM、SOCK_DGRAM、SOCK_RAW等五种,这三种比较常见
// SOCK_STREAM就是流式面向连接的可靠协议,TCP就是基于SOCK_STREAM
// SOCK_DGRAM就是数据包、无连接的不可靠协议,UDP基于SOCK_DGRAM
// SOCK_RAW就是最粗暴原始的那种,你要完全手工来控制,你可以做成面向连接,
// 也可以做成无连接,由你掌控,这种用的比较多的是基于SOCK_RAW实现ping
// 第三个参数共有两个值SOL_TCP、SOL_UDP
// 这里提醒一下就是,后两个参数的选择是有关联性的,比如第二个参你用了
// SOCK_STREAM,那么第三个参数记得用SOL_TCP
// 这里值得注意是:$listen_socket实际上就是一个文件描述符了,也就是fd
$listen_socket = socket_create( AF_INET, SOCK_STREAM, SOL_TCP );
// 将socket bind到IP:port上
// 这里实际上还是有一些说法的,我这里结合UNP按照自己理解这么来说:
// 就是将$listen_socket协议捆绑到以$host&$port指定的socket上
// 实际上$listen_socket已经实现了一个本地协议
// bind就是要把这个本地协议绑定到指定的网络socket上
socket_bind( $listen_socket, $host, $port );
// 开始监听socket
socket_listen( $listen_socket );
// 进入while循环,不用担心死循环死机,因为程序将会阻塞在下面的socket_accept()函数上
while( true ){
  // 此处将会阻塞住,一直到有客户端来连接服务器。阻塞状态的进程是不会占据CPU的
  // 所以你不用担心while循环会将机器拖垮,不会的 
  // 此处请你记住阻塞这个词
  $connection_socket = socket_accept( $listen_socket );
  // 从客户端读取信息
  $content = socket_read( $connection_socket, 4096 );
  echo "从客户端获取:{$content}"; 
  // 向客户端发送一个helloworld
  $msg = "helloworld\r\n";
  socket_write( $connection_socket, $msg, strlen( $msg ) );
  socket_close( $connection_socket );
}
// 出于礼貌,记得用完了关闭掉...
socket_close( $listen_socket );

这里我认为有一个两个地方值得提醒一下:

  • 一是while( true ),可能有腿子会好奇就用while( true )保持运行,是不是有点儿low?然而一点儿都不low。很多服务器服务器程序要保证在后台运行,就是用的类似于while的循环保证的,比如event-loop
  • 二是这个while不断运行,CPU不会爆炸吗?不会的,因为会阻塞在socket_accept()上,所谓阻塞就是传说中[ 同步阻塞 ]中的阻塞。既然阻塞了,while就不会一个劲空打炮了

这个我们用telnet客串一把客户端测试一下,你们可以感受一下:

好了,代码你们都复制粘贴了吗?如果已经好了完事了的话,我们需要关注一下关于接受数据的地方。在上述的代码里我们使用的是socket_read()函数来接受的数据,但是如果你翻一波儿PHP文档你会有意外发现:socket_recv()... ...我把socket_read()的原型说明复制粘贴过来,你们感受下:

socket_read ( resource $socket , int $length [, int $type = PHP_BINARY_READ ] ) : string

// 第一个参数,好理解
socket
A valid socket resource created with socket_create() or socket_accept().

// 第二个参数,好理解吧?
length
The maximum number of bytes read is specified by the length parameter. Otherwise you can use \r, \n, or \0 to end reading (depending on the type parameter, see below).

// 第三个参数,我们重点关注下
type
Optional type parameter is a named constant:
// 这个是默认的,这种情况下,实际上就相当于调用了系统的recv() API
// 这个函数是二进制安全的
PHP_BINARY_READ (Default) - use the system recv() function. Safe for reading binary data.
// 当遇到\r或\n就会停止
PHP_NORMAL_READ - reading stops at \n or \r.

// 这个函数返回值就是收到内容

然后我在把socket_recv()的原型复制粘贴过来,你们再仔细感受下:

socket_recv ( resource $socket , string &$buf , int $len , int $flags ) : int

// 这个参数好说
socket
参数 socket 必须是一个由 socket_create() 创建的socket资源。

// 接受的数据将会保存到buf里来
// 可能有同学没太见过用法,这种方式叫做“ 值-结果 ”参数
buf
从socket中获取的数据将被保存在由 buf 制定的变量中。如果有错误发生,如链接被重置,数据不可用等等, buf 将被设为 NULL。

// 这个我要解释了,似乎就是在侮辱你们
len
长度最多为 len 字节的数据将被接收。

// 很好,这个参数需要重点照顾了
flags
flags 的值可以为下列任意flag的组合。使用按位或运算符(|)来 组合不同的flag。
MSG_OOB       处理超出边界的数据
MSG_PEEK      从接受队列的起始位置接收数据,但不将他们从接受队列中移除。
MSG_WAITALL   在接收到至少 len 字节的数据之前,造成一个阻塞,并暂停脚本运行(block)。但是, 如果接收到中断信号,或远程服务器断开连接,该函数将返回少于 len 字节的数据。
MSG_DONTWAIT  如果制定了该flag,函数将不会造成阻塞,即使在全局设置中指定了阻塞设置。

那么到底选用哪个呢?选用socket_recv(),理由很简单:flags参数给了很多可控制的选项参数。我写个demo,你们琢磨下:

<?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 );
  // 从客户端读取信息
  // MSG_WAITALL的意思就是“阻塞读取客户端消息”,一只要等足够6个字节长度
  $recv_len = socket_recv( $connection_socket, $recv_content, 6, MSG_WAITALL );
  echo "从客户端获取:{$recv_content},长度是{$recv_len}".PHP_EOL; 
  // 向客户端发送一个helloworld
  $msg = "helloworld\r\n";
  socket_write( $connection_socket, $msg, strlen( $msg ) );
  socket_close( $connection_socket );
}
socket_close( $listen_socket );

运行一下:

我们还是用telnet客串客户端发送数据,第一次发送的数据是[ ab+换行符+cdef ],这次服务器收到的是[ ab+换行符+cd ],我估计这里的telnet里的换行符是\r\n两个,这样加起来一共就是6个字节咯;第二次发送的是[ 123456789 ],服务器收到的是[ 123456 ],符合预期。这里我们用的最后一个参数是MSG_WAITALL,这个参数的含义我已经在上面demo代码注释里说过了,还有一个常用的参数是MSG_DONTWAIT,这个参数的含义就是[ 非阻塞 ]读取,我们改下上面demo你们感受下:

<?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 );
  // 从客户端读取信息
  $total_len = 8;
  $recv_len  = 0;
  $recv_content = ''; 
  // 程序不会阻塞在socket_recv()这里,如果没有收到客户端数据
  // 因为用了MSG_DONTWAIT所以会立马往下执行
  $len = socket_recv( $connection_socket, $content, $total_len, MSG_DONTWAIT );
  // 到了while后,一旦客户端连接上来,就会不断循环
  while ( $recv_len < $total_len ) { 
    $len = socket_recv( $connection_socket, $content, ( $total_len - $recv_len ), MSG_DONTWAIT );
    $recv_len = $recv_len + $len;
    // ⚠️⚠️⚠️⚠️⚠️⚠️⚠️ 如果你想观察 ⚠️⚠️⚠️⚠️⚠️⚠️
    // 你可以人为添加一行sleep(1)来~~~
    echo $recv_len.':'.$total_len.PHP_EOL;
    if ( $recv_len > 0 ) { 
      $recv_content = $recv_content.$content;
    }   
  }
  echo "从客户端获取:{$recv_content}"; 
  // 向客户端发送一个helloworld
  $msg = "helloworld\r\n";
  socket_write( $connection_socket, $msg, strlen( $msg ) );
  socket_close( $connection_socket );
}
socket_close( $listen_socket );

那么MSG_OOB和MSG_PEEK是什么意思?MSG_OOB其实叫做外带数据,有时候有些地方叫做紧急数据。我举个例子,由于TCP数据有些时候会被分拆成好几个数据坨(假如说3个),然后服务器方收到数据后需要按照序号去排列好手这坨数据坨,如果此时你有个紧急的数据期望能够送到服务器,那么发送方只需要在send数据数据时加上一个MSG_OOB的标记,服务器就会优先接受这个数据,不用等候服务器按顺序收好那3个普通数据坨坨。不过,我是没怎么用过,只知道TA的用法和存在含义。MSG_PEEK则有这样一个作用,假设你有一坨数据" lalala-password ",如果服务器在recv()接受数据时发现数据包中带有MSG_PEEK标记,那么TA会先读取完毕lalala遇到-停止(这个-是你规定自定义的),虽然你读取数据了,但是这坨数据不会从TCP接受缓冲区被清除掉,TA还会留在那里,等你下次再次使用recv()接受,TA就会接着从-位置读取剩下的“ password ”。这个功能主要用于预探测功能,我意思是先读取一次数据,可以从第一次读取的数据(本次数据中可以包含关于剩余那坨数据的主要信息),然后根据本次数据的信息来让程序做决定下次recv()是执行还是不执行,如果执行了,是否需要走什么其他特殊逻辑。

已经快五千字了,我知道你们快顶不住了,然而我要告诉你们:这些还是经过了PHP处理过后的socket API,我给你们看下UNP中最原始API的。

PHP的socket_recv()选项有如下四个项,且每项之间均可以使用|(或运算)来搭配使用同时获得多个特性:

  • MSG_OOB
  • MSG_PEEK
  • MSG_WAITALL
  • MSG_DONTWAIT

而recv()则拥有五个配置项:

  • MSG_OOB
  • MSG_PEEK
  • MSG_WAITALL
  • MSG_DONTWAIT
  • MSG_DONTROUTE

而当你使用recvmsg()的时候,配置项则达到了十一项...如果再结合上TCP/IP,如果没有毅力一般人怕是坚持不下来的,而且觉得不就一个[ socket ]不就一个[ create-bind-listen-accpet ]的佬们,似乎应该好好正视一下。

所以,知足吧,今天先说到这里,明天接着继续。

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

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

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

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

我来说两句

0 条评论
登录 后参与评论

相关文章

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

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

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

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

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

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

    老李秀
  • udp发送广播消息

    skylark
  • python之socket

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

    py3study
  • Python黑帽编程2.8 套接字编程

    套接字编程在本系列教程中地位并不是很突出,但是我们观察网络应用,绝大多数都是基于Socket来做的,哪怕是绝大多数的木马程序也是如此。官方关于socket编程的...

    用户1631416
  • 一分钟了解PythonSocket

    Socket中文译作:套接字,但是大家一般约定俗称的都用:socket。我想在解释socket是什么之前,先说它是用来干嘛的:socket是来建立‘通信’的基础...

    小小科
  • socket 编程初探

    一 简介 socket是两个应用程序进行通信的管道,这两个应用程序可以在同一台机器上,也可以位于两台不同的机器上,相同的网络或者不同网络之间的。Pyth...

    用户1278550
  • python实现socket通讯(UDP)

    import socket address = ('127.0.0.1', 31500) s = socket.socket(socket.AF_INET, ...

    阳光岛主
  • php socket用法你知道吗?

    本篇文章分享一个简单的socket示例,用php。实现一个接收输入字符串,处理并返回这个字符串到客户端的TCP服务。 产生一个 socket 服务端 <?php...

    wangxl

扫码关注云+社区

领取腾讯云代金券