php官方文档对于socket编程的介绍少的可怜,好在不同的编程语言底层都是调用了相同的系统方法,参考了其他语言的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 | 提供一个可靠的数据层,但不保证到达顺序。一般的操作系统都未实现此功能。 |
支持的运输层协议
支持的协议有很多,但是官方文档并未说明,只给出了常见协议:
协议 | 描述 |
---|---|
icmp | Internet Control Message Protocol 主要用于网关和主机报告错误的数据通信。例如“ping”命令(在目前大部分的操作系统中)就是使用 ICMP 协议实现的。 |
udp | User Datagram Protocol 是一个无连接的、不可靠的、具有固定最大长度的报文协议。由于这些特性,UDP 协议拥有最小的协议开销。 |
tcp | Transmission Control Protocol 是一个可靠的、基于连接的、面向数据流的全双工协议。TCP 能够保障所有的数据包是按照其发送顺序而接收的。如果任意数据包在通讯时丢失,TCP 将自动重发数据包直到目标主机应答已接收。因为可靠性和性能的原因,TCP 在数据传输层使用 8bit 字节边界。因此,TCP 应用程序必须允许传送部分报文的可能。 |
创建套接字
socket_create ( int $domain , int $type , int $protocol ) : resource
socket_create() 正确时返回一个套接字,失败时返回 FALSE
domain:
值 | 描述 |
---|---|
AF_INET | IPv4 网络协议。TCP 和 UDP 都可使用此协议。 |
AF_INET6 | IPv6 网络协议。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_STREAM和 SOCK_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 字节的数据将被接收。 |
flags | flags 的值可以为下列任意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()操作的心路历程
- 检查输出缓冲区可用空间大小,如果小于 len, 则 socket_write()/socket_send() 会阻塞程序,直到缓冲区中的数据被发送到目标机器,腾出足够的空间,才唤醒 socket_write()/socket_send() 函数继续写入数据。
- 如果系统正在从输出缓冲区取数据发送到目标机器,则输出缓冲区会被加锁,socket_write()/socket_send() 会阻塞,直到数据发送完毕,缓冲区解锁。
- 如果要 len 大于缓冲区的最大长度,那么将分批写入。
- 直到所有数据被写入缓冲区 socket_write()/socket_send() 才能返回。
这里有个疑问,len 的长度如果大于 buf 中待发送数据的长度,那缓冲区还需要预留出 len 的空间吗?还是实际 buf 中的数据长度。
我猜测如果len 大于 buf 的长度的话,程序会用实际 buf 的长度去跟缓冲区作比较。
socket_read()/socket_recv() 时
- 检查输入缓冲区中是否有待取数据,如果没有则阻塞,直到有数据进来。
- 如果缓冲区正在接收数据,则阻塞,直到数据接收完毕缓冲区释放
- 如果缓冲区的待取数据长度小于 len ,则取出全部数据并返回实际取到子数据长度
- 如果缓冲区的待取数据长度大于 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协议族》