php socket实践

php官方文档对于socket编程的介绍少的可怜,好在不同的编程语言底层都是调用了相同的系统方法,参考了其他语言的socket文档,简单整理一下,方便以后查阅。

什么是socket

socket的原意是插座,在计算机通信领域 socket 被翻译成 套接字,是计算机通信的一种方式。

那我们怎么才能使用socket实现计算机之间的通信呢?
这个时候就引入了 socket层,socket层是位于运输层和应用层之间的一个抽象层。
为什么说是抽象层,因为在标准的 TCP/IP协议族五层模型里是没有 socket 层的。它把TCP/IP层复杂的操作抽象为几个简单的接口,供应用层调用实现进程在网络中的通信。
在这里插入图片描述

socket 协议类型

前文提到过,socket 是介于运输层与应用层之间的抽象层。
不同的协议类型代表了运输层在可靠性和系统开销方面的取舍。比如TCP协议和UDP协议,虽然在数据的可靠性方面UDP比TCP差,但是系统开销更低,传输效率更快。不同的协议类型有着适合自己的场景。

php 官方文档提出支持5种协议类型,我们主要讲前两种,也就是TCP和UDP的协议类型。

协议类型描述
SOCK_STREAM提供一个顺序化的、可靠的、全双工的、基于连接的字节流。支持数据传送流量控制机制。TCP 协议即基于这种流式套接字。
SOCK_DGRAM提供数据报文的支持。(无连接,不可靠、固定最大长度).UDP协议即基于这种数据报文套接字。
SOCK_SEQPACKET提供一个顺序化的、可靠的、全双工的、面向连接的、固定最大长度的数据通信;数据端通过接收每一个数据段来读取整个数据包。
SOCK_RAW提供读取原始的网络协议。这种特殊的套接字可用于手工构建任意类型的协议。一般使用这个套接字来实现 ICMP 请求(例如 ping)。
SOCK_RDM提供一个可靠的数据层,但不保证到达顺序。一般的操作系统都未实现此功能。

支持的运输层协议

支持的协议有很多,但是官方文档并未说明,只给出了常见协议:

协议描述
icmpInternet Control Message Protocol 主要用于网关和主机报告错误的数据通信。例如“ping”命令(在目前大部分的操作系统中)就是使用 ICMP 协议实现的。
udpUser Datagram Protocol 是一个无连接的、不可靠的、具有固定最大长度的报文协议。由于这些特性,UDP 协议拥有最小的协议开销。
tcpTransmission Control Protocol 是一个可靠的、基于连接的、面向数据流的全双工协议。TCP 能够保障所有的数据包是按照其发送顺序而接收的。如果任意数据包在通讯时丢失,TCP 将自动重发数据包直到目标主机应答已接收。因为可靠性和性能的原因,TCP 在数据传输层使用 8bit 字节边界。因此,TCP 应用程序必须允许传送部分报文的可能。

创建套接字

socket_create ( int $domain , int $type , int $protocol ) : resource

socket_create() 正确时返回一个套接字,失败时返回 FALSE
domain:

描述
AF_INETIPv4 网络协议。TCP 和 UDP 都可使用此协议。
AF_INET6IPv6 网络协议。TCP 和 UDP 都可使用此协议。
AF_UNIX本地通讯协议。具有高性能和低成本的 IPC(进程间通讯)。

type:
type 为数据传输方式/套接字类型,常用的有 SOCK_STREAM(流格式套接字/面向连接的套接字) 和 SOCK_DGRAM(数据报套接字/无连接的套接字),请参照第2小节。
protocol:
protocol 表示传输协议,常用的有 SOL_TCP 和 SOL_UDP,分别表示 TCP 传输协议和 UDP 传输协议。
在这里需要传入的是数字,只不过TCP协议和UDP比较常用,有常量代替,其他的协议可以自己通过 getprotobyname 查询。
比如我们想查看 ICMP的值:

echo getprotobyname('icmp');

有了地址类型(domain)和数据传输方式(type),还不足以决定采用哪种协议吗?为什么还需要第三个参数呢?

正如大家所想,一般情况下有了 domain 和 type 两个参数就可以创建套接字了,操作系统会自动推演出协议类型,除非遇到这样的情况:有两种不同的协议支持同一种地址类型和数据传输类型。如果我们不指明使用哪种协议,操作系统是没办法自动推演的。

本教程使用 IPv4 地址,参数 domain 的值为 AF_INET。如果使用 SOCK_STREAM 传输数据,那么满足这两个条件的协议只有 TCP,因此可以这样来调用 socket_create() 函数:

$sock = socket_create(AF_INET,SOCK_STREAM,SOL_TCP);
var_dump($sock);
die();

输出如下:

/Users/csw/Workspace/weblistenner/app/testClient.php:11:
resource(4) of type (Socket)

如果使用 SOCK_DGRAM 传输方式,那么满足这两个条件的协议只有 UDP

$sock = socket_create(AF_INET,SOCK_DGRAM,SOL_UDP);
var_dump($sock);
die();

输出如下:

/Users/csw/Workspace/weblistenner/app/testClient.php:11:
resource(4) of type (Socket)

上面两种情况都只有一种协议满足条件,可以将 protocol 的值设为 0,系统会自动推演出应该使用什么协议,如下所示:

$sockTcp = socket_create(AF_INET,SOCK_STREAM,0);
$sockUDp = socket_create(AF_INET,SOCK_DGRAM,0);
var_dump($sockTcp);
var_dump($sockUDp);
die();

输出如下:

/Users/csw/Workspace/weblistenner/app/testClient.php:12:
resource(4) of type (Socket)
/Users/csw/Workspace/weblistenner/app/testClient.php:13:
resource(5) of type (Socket)

socket_bind() 和 socket_connect()

socket_bind()
socket_create() 函数用来创建套接字,确定套接字的各种属性,然后服务器端要用 socket_bind() 函数将套接字与特定的 IP 地址和端口绑定起来,只有这样,流经该 IP 地址和端口的数据才能交给套接字处理。

socket_bind ( resource $socket , string $address [, int $port = 0 ] ) : bool
参数描述
socket用 socket_create() 创建的一个有效的套接字资源。
address如果套接字是 AF_INET 族,那么 address 必须是一个四点分法的 IP 地址(例如 127.0.0.1 ),如果套接字是 AF_UNIX 族,那么 address 是 Unix 套接字一部分(例如 /tmp/my.sock )。
port参数 port 仅仅用于 AF_INET 套接字连接的时候,并且指定连接中需要监听的端口号。

成功时返回 TRUE, 或者在失败时返回 FALSE。

$sockTcp = socket_create(AF_INET,SOCK_STREAM,SOL_TCP);
$sockBind = socket_bind($sockTcp,'0.0.0.0',9876);
var_dump($sockBind);

输出:

/Users/csw/Workspace/weblistenner/app/testServer.php:11:
bool(true)

socket_connect()
客户端通过改方法与服务器建立连接

socket_connect ( resource $socket , string $address [, int $port = 0 ] ) : bool

socket_listen() 和 socket_accept()

对于服务器端程序,使用 socket_bind() 绑定套接字后,还需要使用 listen() 函数让套接字进入被动监听状态,再调用 socket_accept() 函数,就可以随时响应客户端的请求了。

socket_listen

socket_listen ( resource $socket [, int $backlog = 0 ] ) : bool

对于面向连接的协议,比如TCP协议,在通信之前需要先建立连接,也就是大家熟知的三次握手。需要用 socket_listen() 来监听客户端的连接请求。
第2小节中提到的两种协议类型 SOCK_STREAMSOCK_SEQPACKET 都是基于连接的,所以服务端要使用 socket_listen() 建立连接。

参数描述
socket通过socket_create() 创建的有效的socket资源
backlog请求队列的长度

请求队列
当套接字正在处理客户端请求时,如果有新的请求进来,套接字是没法处理的,只能把它放进缓冲区,待当前请求处理完毕后,再从缓冲区中读取出来处理。如果不断有新的请求进来,它们就按照先后顺序在缓冲区中排队,直到缓冲区满。这个缓冲区,就称为请求队列(Request Queue)。

请求队列的长度可以通过 backlog 参数来指定,你也可以通过使用 SOMAXCONN 来制定为最大长度,这个长度是有系统决定的。

如果连接请求在队列已满的情况下到达,客户端可能会收到带有ECONNREFUSED指示的错误,或者,如果基础协议支持重传,则可以忽略该请求,以便重试成功

socket_accept()

socket_accept ( resource $socket ) : resource

当套接字处于监听状态时,可以通过 socket_accept() 函数来接收客户端请求.

它将接受该套接字上的传入连接。成功建立连接后,将返回一个新的套接字资源,该资源可用于通信。

如果有多个连接在请求队列中,则返回第一个。如果没有待处理的连接,程序会阻塞(其实也可以设置未非阻塞模式,默认是阻塞),知道有新的连接请求进来。

socket_listen() 只是让套接字进入监听状态,并没有真正接收客户端请求,socket_listen() 后面的代码会继续执行,直到遇到 socket_accept()。socket_accept() 会阻塞程序执行(后面代码不能被执行),直到有新的请求到来。

这里需要着重强调的是,用于和客户端通信的是 socket_accept() 返回的新的套接字,而非 socket_create() 创建的原始套接字,原始套接字在没有关闭的情况下还可以产生新的连接

我们用代码实现TCP协议,看看效果,
server端

$sockTcp = socket_create(AF_INET,SOCK_STREAM,SOL_TCP);
socket_bind($sockTcp,'0.0.0.0',9876);
socket_listen($sockTcp,1);
while (true)
{
    $connection = socket_accept($sockTcp);
    echo "新连接来了\n";
    var_dump($connection);
}

client端

$max = 3;
for($i=1;$i<=$max;$i++){
    $sockTcp = socket_create(AF_INET,SOCK_STREAM,0);
    $sockBind = socket_connect($sockTcp,'127.0.0.1',9876);
    var_dump($sockBind);
}

我们先启动 sever 开始监听
server端

XYBJM01617:app csw$ php testServer.php


再打开新的窗口,让 client 发起连接请求
client 端

XYBJM01617:app csw$ php testClient.php
/Users/csw/Workspace/weblistenner/app/testClient.php:12:
bool(true)
/Users/csw/Workspace/weblistenner/app/testClient.php:12:
bool(true)
/Users/csw/Workspace/weblistenner/app/testClient.php:12:
bool(true)

server端输出如下

新连接来了
/Users/csw/Workspace/weblistenner/app/testServer.php:16:
resource(5) of type (Socket)
新连接来了
/Users/csw/Workspace/weblistenner/app/testServer.php:16:
resource(6) of type (Socket)
新连接来了
/Users/csw/Workspace/weblistenner/app/testServer.php:16:
resource(7) of type (Socket)

可以看到成功建立了三个连接。


这里大家有没有疑问,到底是 socket_listen() 开始监听之后,客户端就可以实现三次握手,还是 socket_accept() 的时候实现的三次握手。

我们来验证一下
server 端

$sockTcp = socket_create(AF_INET,SOCK_STREAM,SOL_TCP);
socket_bind($sockTcp,'0.0.0.0',9876);
socket_listen($sockTcp,1);
sleep(10);    //保持程序不退出,不然客户端是无法连接的
die();

client 端

$max = 3;
for($i=1;$i<=$max;$i++){
    $sockTcp = socket_create(AF_INET,SOCK_STREAM,0);
    $sockBind = socket_connect($sockTcp,'127.0.0.1',9876);
    var_dump($sockBind);
}

同样,我们先执行 php testServer.php 建立监听,然后在让客户端去请求连接,客户端一共发送了三次连接请求。
client 输出

XYBJM01617:app csw$ php testClient.php
/Users/csw/Workspace/weblistenner/app/testClient.php:21:
bool(true)
PHP Warning:  socket_connect(): unable to connect [61]: Connection refused in /Users/csw/Workspace/weblistenner/app/testClient.php on line 20
PHP Stack trace:
PHP   1. {main}() /Users/csw/Workspace/weblistenner/app/testClient.php:0
PHP   2. socket_connect() /Users/csw/Workspace/weblistenner/app/testClient.php:20
/Users/csw/Workspace/weblistenner/app/testClient.php:21:
bool(false)
PHP Warning:  socket_connect(): unable to connect [61]: Connection refused in /Users/csw/Workspace/weblistenner/app/testClient.php on line 20
PHP Stack trace:
PHP   1. {main}() /Users/csw/Workspace/weblistenner/app/testClient.php:0
PHP   2. socket_connect() /Users/csw/Workspace/weblistenner/app/testClient.php:20
/Users/csw/Workspace/weblistenner/app/testClient.php:21:
bool(false)

可以看到,客户端马上会有一个连接成功,然后阻塞,等服务端脚本结束之后,客户端会报连接拒绝。我们可以通过抓包的方式来分析下。
在这里插入图片描述
这样就很清晰了,其实在socket_listen()开始监听之后,就可以成功建立连接,建立好的连接会放入请求队列,socket_accept() 只负责从请求队列取连接

我们的请求队列长度是1,客户端共发起了3次请求,只有第一个请求顺利建立连接并进入请求队列。其他两次请求被拒绝后,会不停的重试,直到服务端使用 socket_accepted() 消耗了请求队列或者服务器停止监听,客户端报连接 connect refused。有兴趣的朋友可以把请求队列改大一点,在观察一下。

阻塞模式和非阻塞模式
前面有提到如果 请求队列 没有待处理的连接,socket_accepte() 会默认阻塞程序,直到有新的请求进来。
我们来聊一聊 阻塞 和 非阻塞 的区别,以及使用方法。

在阻塞模式下,程序将会一直等到从资源流里面获取到数据才能返回
在非阻塞模式下,程序会立即返回;

我们可以使用 socket_set_blocking()socket_set_nonblocking() 设置程序为阻塞模式和非阻塞模式

上文的例子里已经演示过,阻塞模式,我们来试一下非阻塞模式
server 端

$sockTcp = socket_create(AF_INET,SOCK_STREAM,SOL_TCP);
socket_bind($sockTcp,'0.0.0.0',9876);
socket_listen($sockTcp,2);
socket_set_nonblock($sockTcp);
while (true)
{
    $connection = socket_accept($sockTcp);
    if($connection){
        echo "新连接来了\n";
    }else{
        echo "没有新连接,休息3秒\n";
        sleep(3);
    }
}

client 端

$max = 3;
for($i=1;$i<=$max;$i++){
    $sockTcp = socket_create(AF_INET,SOCK_STREAM,0);
    $sockBind = socket_connect($sockTcp,'127.0.0.1',9876);
    var_dump($sockBind);
}

server 端输出

XYBJM01617:app csw$ php testServer.php
没有新连接,休息3秒
新连接来了
新连接来了
没有新连接,休息3秒
新连接来了
没有新连接,休息3秒
没有新连接,休息3秒
没有新连接,休息3秒

非阻塞模式下,如果请求队列没有连接,socket_accept() 会直接返回false,如果有待处理连接则消耗请求队列,并返回套接字。

socket_write()/socket_send() 和 socket_read()/socket_recv()

顺利建立连接之后,客户端和服务器就可以进行通信了。因为TCP协议是全双工的,client 和 server 都可以使用 socket_write() 和 socket_read() 进行数据的发送和接收。

socket_write()

socket_write ( resource $socket , string $buffer [, int $length = 0 ] ) : int
参数描述
socket有效的socket资源
buf要写入的缓冲区
length可选参数length可以指定写入套接字的备用字节长度。如果此长度大于缓冲区长度,则将其无提示地截断为缓冲区的长度。

返回值
返回成功写入套接字(这里用输出缓冲区应该更合适)的字节数,或者在失败时返回FALSE。
需要注意的是返回0表示写到输出缓冲区的字节数是0,要判断是否成功写入,要使用 === 运算符检查false。

大家可能好奇为什么 buf 参数的释义是要写入的缓冲区。文档上也没有详细解释,后面我们会有解释,本小节就先简单的理解为要发送的数据好了。

socket_read()

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

函数socket_read()从socket_create()或socket_accept()函数创建的套接字资源套接字中读取

参数描述
socket有效的socket 资源
length读取的最大字节数由length参数指定。否则,您可以使用\ r,\ n或\ 0结束读取(取决于type参数,请参见下文)
type可选的type参数是一个命名常量: PHP_BINARY_READ(默认)-使用系统recv()函数,安全读取二进制数据。PHP_NORMAL_READ-读取在\ n或\ r停止。

官方文档对该函数的解释过于敷衍,文档下最近的评论也是5年前的。可能是由于php的官方定位不同吧,对网络编程的支持并不是很友好,有兴趣的同学可以取学习下swoole,或者直接换一门更香的语言也不错。
至于实际情况下 socket_write() 和 socket_read() 方法的不同表现,因为要涉及到缓冲区的知识,我们放到后面讲解。
先来实现简单的数据发送和接收:
server 端

$sockTcp = socket_create(AF_INET,SOCK_STREAM,SOL_TCP);
socket_bind($sockTcp,'0.0.0.0',9877);
socket_listen($sockTcp,2);
while (true)
{
   $connection = socket_accept($sockTcp);
   echo "新连接来了\n";
   $buffer = "welcome new connection";
   $resLength = socket_write($connection,$buffer);
   echo "服务器发送数据[{$buffer}],数据长度为[{$resLength}]\n";
   $resRead = socket_read($connection,100);
   echo "服务器接收数据[{$resRead}]\n";
}

client 端

$max = 1;
for($i=1;$i<=$max;$i++) {
    $sockTcp = socket_create(AF_INET, SOCK_STREAM, 0);
    socket_connect($sockTcp, '127.0.0.1', 9877);
    $resRead = socket_read($sockTcp,100);
    echo "客户端读取数据[{$resRead}]\n";
    $buffer = "yes i am conection1";
    $resSend = socket_write($sockTcp,$buffer);
    echo "客户端发送数据[{$buffer}],长度为[{$resSend}]\n";
}

server 端输出

XYBJM01617:app csw$ php testServer.php
新连接来了
服务器发送数据[welcome new connection],数据长度为[22]
服务器接收数据[yes i am conection1]

client 端输出

XYBJM01617:app csw$ php testClient.php
客户端读取数据[welcome new connection]
客户端发送数据[yes i am conection1],长度为[19]

这只是简单交互,实际应用中的逻辑比这些要复杂的多。
我们再来看看另外两个跟数据交互有关的函数

socket_send()

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

函数socket_send()将len个字节从buf发送到套接字(输出缓冲区)。

参数描述
socket一个有效的socket资源
buf将发送到远程主机的数据的缓冲区
len从buf发送到远程主机的字节数
flags标志的值可以是以下标志的任何组合,并与二进制OR(|)运算符结合在一起。

flags

描述
MSG_OOB发送带外数据,有兴趣的朋友可以去了解一下,简单理解就是打破了TCP顺序达到壁垒,可以优先传输的紧急数据。
MSG_EOR表示记录标记。发送的数据完成记录。
MSG_EOF关闭套接字的发送方,并在发送的数据末尾包含对此的适当通知。发送的数据完成交易。
MSG_DONTROUTE绕过路由,使用本地接口。本地测试的时候,加上这个参数,系统就不会去扫描本地路由表了。

socket_recv

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

函数 socket_recv() 从 socket 中接受长度为 len 字节的数据,并保存在 buf 中。 socket_recv() 用于从已连接的socket中接收数据。除此之外,可以设定一个或多个 flags 来控制函数的具体行为。

buf 以引用形式传递,因此必须是一个以声明的有效的变量。从 socket 中接收到的数据将会保存在 buf 中。

参数描述
socket参数 socket 必须是一个由 socket_create() 创建的socket资源。
buf从socket中获取的数据将被保存在由 buf 制定的变量中。 如果有错误发生,如链接被重置,数据不可用等等, buf 将被设为 NULL。
len长度最多为 len 字节的数据将被接收。
flagsflags 的值可以为下列任意flag的组合。使用按位或运算符(

flags

描述
MSG_OOB处理带外数据
MSG_PEEK从接受队列的起始位置接收数据,但不将他们从接受队列中移除
MSG_WAITALL在接收到至少 len 字节的数据之前,造成一个阻塞,并暂停脚本运行(block)。但是, 如果接收到中断信号,或远程服务器断开连接,该函数将返回少于 len 字节的数据。
MSG_DONTWAIT如果制定了该flag,函数将不会造成阻塞,即使在全局设置中指定了阻塞设置。

很显然 socket_send()/socket_recv() 两个方法要比socket_write()/socket_read() 方法支持的场景更多。那这两组方法有什么区别呢?

我的理解是,在linux 操作系统中,一切皆文件,在系统底层可以用 write() 和 read() 操作文件的方式操作套接字。而windows系统则不然,文件和套接字的操作方式是不一样的。所以有了 send() 和 recv()专用于套接字的函数。但是linux同样支持使用 send() 和 recv() 方法的。

注意这里虽然把函数分成的两组,但并不意味着必须对应使用,比如说我用socket_send()发送数据,仍然可以用socket_read()进行数据接收的。

那我们用 socket_send() 和 socket_recv() 来实现简单的数据收发
server端

$sockTcp = socket_create(AF_INET,SOCK_STREAM,SOL_TCP);
socket_bind($sockTcp,'0.0.0.0',9877);
socket_listen($sockTcp,2);
while (true)
{
    $connection = socket_accept($sockTcp);
    echo "新连接来了\n";
    $buffer = "welcome new connection";
    $resLength = socket_send($connection,$buffer,strlen($buffer),0);
    echo "服务器发送数据[{$buffer}],数据长度为[{$resLength}]\n";
    socket_recv($connection,$resRead,100,0);
    echo "服务器接收数据[{$resRead}]\n";
}

client 端

$max = 1;
for($i=1;$i<=$max;$i++) {
    $sockTcp = socket_create(AF_INET, SOCK_STREAM, 0);
    socket_connect($sockTcp, '127.0.0.1', 9877);
    socket_recv($sockTcp,$resRead,100,0);
    echo "客户端读取数据[{$resRead}]\n";
    $buffer = "yes i am conection1";
    $resSend = socket_send($sockTcp,$buffer,strlen($buffer),0);
    echo "客户端发送数据[{$buffer}],长度为[{$resSend}]\n";
}

server端输出

XYBJM01617:app csw$ php testServer.php
新连接来了
服务器发送数据[welcome new connection],数据长度为[22]
服务器接收数据[yes i am conection1]

client端输出

XYBJM01617:app csw$ php testClient.php
客户端读取数据[welcome new connection]
客户端发送数据[yes i am conection1],长度为[19]

有趣的是文档并没有明确表明 flags 的值可以传 0 ,而且传 0 的话,效果是等同于 socket_write() 和 socket_read() 的。 你说尴尬不,所以大家想了解socket 编程的话,还是看看其他语言的文档吧,比如说C语言的,就很不错。
尴尬。。。。。

socket_send() 和 socket_recv() flags选项的值,我就不一一赘述,因为就我目前的知识储备还整不太明白。先立个 flag 吧,等到后续来补充。

缓冲区和阻塞模式

终于要讲缓冲区了,对于socket编程,缓冲区很重要。只有理解了缓冲区才能更好的运用socket系列函数进行开发。

每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区。熟悉运输层协议的朋友,肯定知道个缓冲区就是滑动窗口。本文不详述缓冲区的实现原理,有兴趣的朋友可以去了解下运输层协议。

socket_write()/socket_send() 并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区,函数就可以成功返回,不管它们有没有到达目标机器,也不管它们何时被发送到网络,这些都是TCP协议负责的事情。

TCP协议独立于 socket_write()/socket_send() 函数,数据有可能刚被写入缓冲区就发送到网络,也可能在缓冲区中不断积压,多次写入的数据被一次性发送到网络,这取决于当时的网络情况、当前线程是否空闲等诸多因素,不由程序员控制。

socekt_read()/socket_recv() 函数也是如此,也从输入缓冲区中读取数据,而不是直接从网络中读取。

在这里插入图片描述
这也就解释了,为什么 socket_write()/socket_send() 的第二个参数 buf 被称作缓冲区,我的理解是应用层缓冲区,因为 buf 里的数据并不一定是马上在同一批次放到输出缓冲区的。这取决于当时输出缓冲区的可用空间大小。在阻塞模式下,只有buf里的数据全部写入到输出缓冲区才会返回实际写入的字节数。

通常的用法是,我们根据输出缓冲区的大小,指定len的值,这个值固定且需要小于输出缓冲区的总长度,每次从buf中取出定长 len 的数据放到输出缓冲区。

同理 socket_read()/socket_recv() 也可能分批次从缓冲区读取定长 len 的数据放到 buf 里。

输入/输出缓冲区的特性

  • 缓冲区在每个TCP套接字中单独存在
  • 缓冲区在创建套接字的时候自动生成
  • 即使关闭套接字也会继续传送输出缓冲区中遗留的数据
  • 关闭套接字将丢失输入缓冲区中的数据

我们可以通过系统函数获取缓冲区的大小

输出缓冲区 :socket_get_option($sockTcp,SOL_SOCKET,SO_SNDBUF);
输入缓冲区:socket_get_option($sockTcp,SOL_SOCKET,SO_RCVBUF);

同样我们也可以用 socket_set_option() 来改变缓冲区大小的值。
but,系统实际使用的值并不一定是你指定的,首先不同的系统会拿你指定的值跟系统配置的值做对比,然后做选择。在握手阶段,系统还会根据当时的网络状况,对方的缓冲区配置等确定一个最终的窗口大小。所以还是抓包吧,这样能看到实际窗口大小。

阻塞模式下,一次 socket_write()/socket_send()操作的心路历程

  1. 检查输出缓冲区可用空间大小,如果小于 len, 则 socket_write()/socket_send() 会阻塞程序,直到缓冲区中的数据被发送到目标机器,腾出足够的空间,才唤醒 socket_write()/socket_send() 函数继续写入数据。
  2. 如果系统正在从输出缓冲区取数据发送到目标机器,则输出缓冲区会被加锁,socket_write()/socket_send() 会阻塞,直到数据发送完毕,缓冲区解锁。
  3. 如果要 len 大于缓冲区的最大长度,那么将分批写入。
  4. 直到所有数据被写入缓冲区 socket_write()/socket_send() 才能返回。

这里有个疑问,len 的长度如果大于 buf 中待发送数据的长度,那缓冲区还需要预留出 len 的空间吗?还是实际 buf 中的数据长度。
我猜测如果len 大于 buf 的长度的话,程序会用实际 buf 的长度去跟缓冲区作比较。

socket_read()/socket_recv() 时

  1. 检查输入缓冲区中是否有待取数据,如果没有则阻塞,直到有数据进来。
  2. 如果缓冲区正在接收数据,则阻塞,直到数据接收完毕缓冲区释放
  3. 如果缓冲区的待取数据长度小于 len ,则取出全部数据并返回实际取到子数据长度
  4. 如果缓冲区的待取数据长度大于 len,则取出 len 长度的数据,并返回数据长度,指的注意的是没有取的数据还在输入缓冲区积压,需要程序重复去取,才能取完。

实例

说了这么多,总得来点实际的吧。既然socket层是运输层和应用层之间的抽象接口,那我们就可以用它做很多好玩的事情,这也是我对socket编程感兴趣的原因。平时工作都是web开发,学习一下网络编程,可以帮助我们更好的理解网络协议。
闲暇时间自己用php写了个网络框架,实现了http协议和websocket协议。当中也有本博文的测试代码,有兴趣的朋友可以拿去参考。仅仅用于自己学习,想应用到生产的朋友还请三思。
传送门》》.

鸣谢

第一次写这么长的博文,前前后后大概耗费了一周的时间。当然有很多时间是自己在学习。能力有限,有问题的地方欢迎大家批评指正。
感谢网络上大神的分享
参考博文
https://www.cnblogs.com/alantu2018/p/8472718.html
http://c.biancheng.net/view/2358.html
https://juejin.im/entry/58ae4636b123db0052b1caf8
https://blog.csdn.net/yu_yuan_1314/article/details/9766137
https://blog.csdn.net/c359719435/article/details/8815499/
https://www.cnblogs.com/lisuyun/articles/5803352.html
http://www.macfreek.nl/memory/Kernel_Configuration#Other_Buffer_Size
参考书籍
《TCP/IP协议族》

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

csw_coder

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值