《TCP/IP网络编程》学习笔记 | Chapter 13:多种 I/O 函数
《TCP/IP网络编程》学习笔记 | Chapter 13:多种 I/O 函数
- 《TCP/IP网络编程》学习笔记 | Chapter 13:多种 I/O 函数
- send & recv 函数
- Linux 平台下的 send 和 recv 函数
- MSG_OOB:发送紧急消息
- 紧急模式的工作原理
- 检查输入缓冲
《TCP/IP网络编程》学习笔记 | Chapter 13:多种 I/O 函数
send & recv 函数
Linux 平台下的 send 和 recv 函数
#include <sys/socket.h>
ssize_t recv(int sockfd, const void *buf, size_t nbytes, int flags);
成功时返回发送的字节数,失败时返回 -1。
参数:
- sockfd:表示与数据传输对象的连接的套接字文件描述符
- buf:保存待传输数据的缓冲地址值
- nbytes:待传输字节数
- flags:传输数据时指定的可选项信息
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t nbytes, int flags);
成功时返回接收的字节数(收到 EOF 返回 0),失败时返回 -1。
参数:
- sockfd:表示与数据接收对象的连接的套接字文件描述符
- buf:保存接收数据的缓冲地址值
- nbytes:可接收最大字节数
- flags:接收数据时指定的可选项信息
send函数和recv函数的最后一个参数是收发数据时的可选项,该可选项可利用位(bit)或运算符同时传递多个信息,通过表下表整理可选项的种类及含义。
可选项(Option) | 含义 | send | recv |
---|---|---|---|
MSG_OOB | 用于传输带外数据(Out-of-band data) | O | O |
MSG_PEEK | 验证输入缓冲中是否存在接受的数据 | X | O |
MSG_DONTROUTE | 数据传输过程中不参照本地路由(Routing)表,在本地(Local)网络中寻找目的地 | O | X |
MSG_DONTWAIT | 调用 I/O 函数时不阻塞,用于使用非阻塞(Non-blocking)I/O | O | O |
MSG_WAITALL | 防止函数返回,直到接收到全部请求的字节数 | X | O |
不同操作系统对上述可选项的支持也不同。因此,为了使用不同可选项,需对实际开发采用的操作系统有一定了解。
MSG_OOB:发送紧急消息
MSG_OOB是特定于TCP的一个选项,它用于在TCP连接中发送紧急或带外数据(out-of-band data),通常用于中断正常的数据流,它允许发送端发送一些紧急信息,这些信息会绕过正常的数据队列,直接发送给接收端,而接收端也可以优先处理这些带外数据。
下面示例将通过MSG_OOB可选项收发数据。
oob_send.c:
在这里插入代码片
从上面示例可以看出,紧急消息的传输比即将介绍的接收过程要简单,只需在调用send函数时指定MSG_OOB可选项,接收紧急消息的过程要相对复杂一点
oob_recv.c:
在这里插入代码片
上述示例中插入了未曾讲解的fcntl函数调用语句,关于此函数只讲解必要部分,后面还会再讲解此函数
fcntl(recv_sock, F_SETOWN, getpid());
fntcl函数用于控制文件描述符,但上述调用语句的含义为:将文件描述符recv_sock指向的套接字拥有者(F_SETOWN)改为把getpid函数返回值用作ID进程。
可能大家对“套接字拥有者”的概念有些生疏,操作系统实际上创建管理套接字,所以从严格意义上说,操作系统才是套接字的拥有者,只是此处所谓的“拥有者”是指套接字所有事物的主体,
上述描述可简要概括为:文件描述符recv_sock指向的套接字引发的SIGURG信号处理进程变为将getpid函数返回值用作ID的进程。
当然,上述描述中的“处理SIGURG信号”指的是“调用SIGURG信号处理函数”。但之前讲过,多进程可以共同拥有一个套接字描述符。例如,通过调用fork函数创建子进程并同时复制文件描述符。此时如果发生SIGURG信号,应该调用哪个进程的信号处理函数呢?可以肯定的是,不会调用所有进程的信号处理函数。因此,处理SIGURG信号时必须指定处理信号的进程,而getpid函数返回调用此函数的进程ID。上述调用语句指定当前进程为处理SIGURG信号的主体。该程序只创建一个进程,因此,理应由该进程处理SIGURG信号。
编译oob_recv.c并运行:
# gcc oob_recv.c -o oob_recv
# ./oob_recv 8500
123
Urgent message: 4
567
Urgent message: 0
89
编译oob_send.c并运行:
# gcc oob_send.c -o oob_send
# ./oob_send 127.0.0.1 8500
从运行结果可以看出,send 是客户端,recv 是服务端,客户端给服务端发送消息,服务端接收完消息之后显示出来。
注意:每次运行的效果,并不是一样的。
输出结果出乎意料:通过 MSG_OOB 可选项传递数据时只返回 1 个字节,而且也不快。
的确,通过 MSG_OOB 并不会加快传输速度,而通过信号处理函数 urg_handler 也只能读取一个字节。剩余数据只能通过未设置 MSG_OOB 可选项的普通输入函数读取。这是因为 TCP 不存在真正意义上的「外带数据」。实际上,MSG_OOB 中的 OOB 指的是 Out-of-band ,而「外带数据」的含义是:通过完全不同的通信路径传输的数据。
真正意义上的 Out-of-band 需要通过单独的通信路径高速传输数据,但是 TCP 不另外提供,只利用 TCP 的紧急模式(Urgent mode)进行传输。
紧急模式的工作原理
MSG_OOB 的真正意义在于督促数据接收对象尽快处理数据。这是紧急模式的全部内容,而 TCP 「保持传输顺序」的传输特性依然成立。TCP 的紧急消息无法保证及时到达,但是可以要求急救。下面是 MSG_OOB 可选项状态下的数据传输过程:
上图给出的是示例oob_send.c的第32行中调用如下函数后的输出缓冲状态,此处假设已传输之前的数据。
send(sock, "890", strlen("890"), MSG_OOB);
如果将缓冲最左端的位置视作偏移量为0,字符0保存于偏移量为2的位置。另外,字符0右侧偏移量为3的位置存有紧急指针。紧急指针指向紧急消息的下一个位置(偏移量加1),同时向对方主机传递消息:紧急指针指向的偏移量为3之前的部分就是紧急消息。
也就是说,实际只用一个字节表示紧急消息。这一点可以通过下图中用于传输数据的TCP数据包(段)的结构看的更清楚。
TCP数据包实际包含很多信息,图1-2只标注了与我们主题相关的内容,TCP头含有如下两种信息:
- URG=1:载有紧急消息的数据包
- URG指针:紧急指针位于偏移量为3的位置
指定MSG_OOB选项的数据包本身就是紧急数据包,并通过紧急指针表示紧急消息所在位置,但无法得知紧急消息是字符串890?还是90?亦或是单个0?但这并不重要,如前所述,除紧急指针的前面一个字节外,数据接收方将通过调用常用输入函数读取剩余部分。换言之,紧急消息的意义在于督促消息处理,而非紧急传输形式受限的消息。
检查输入缓冲
同时设置MSG_PEEK选项和MSG_DONTWAIT选项,以验证输入缓冲中是否存在接收的数据。设置MSG_PEEK选项并调用recv函数时,即使读取了输入缓冲的数据也不会删除。因此,该选项通常与MSG_DONTWAIT合作,用于调用以非阻塞方式验证待读数据存在与否的函数。
peek_send.c:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
void error_handling(char *message);
int main(int argc, char *argv[])
{
int sock;
struct sockaddr_in send_adr;
if (argc != 3)
{
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
sock = socket(PF_INET, SOCK_STREAM, 0);
memset(&send_adr, 0, sizeof(send_adr));
send_adr.sin_family = AF_INET;
send_adr.sin_addr.s_addr = inet_addr(argv[1]);
send_adr.sin_port = htons(atoi(argv[2]));
if (connect(sock, (struct sockaddr *)&send_adr, sizeof(send_adr)) == -1)
error_handling("connect() error!");
write(sock, "123", strlen("123"));
close(sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
发起连接请求,然后发送字符串“123”。
peek_recv.c:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define BUF_SIZE 30
void error_handling(char *message);
int main(int argc, char *argv[])
{
int acpt_sock, recv_sock;
struct sockaddr_in acpt_adr, recv_adr;
int str_len, state;
socklen_t recv_adr_sz;
char buf[BUF_SIZE];
if (argc != 2)
{
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
acpt_sock = socket(PF_INET, SOCK_STREAM, 0);
memset(&acpt_adr, 0, sizeof(acpt_adr));
acpt_adr.sin_family = AF_INET;
acpt_adr.sin_addr.s_addr = htonl(INADDR_ANY);
acpt_adr.sin_port = htons(atoi(argv[1]));
if (bind(acpt_sock, (struct sockaddr *)&acpt_adr, sizeof(acpt_adr)) == -1)
error_handling("bind() error");
listen(acpt_sock, 5);
recv_adr_sz = sizeof(recv_adr);
recv_sock = accept(acpt_sock, (struct sockaddr *)&recv_adr, &recv_adr_sz);
while (1)
{
str_len = recv(recv_sock, buf, sizeof(buf) - 1, MSG_PEEK | MSG_DONTWAIT);
if (str_len > 0)
break;
}
buf[str_len] = 0;
printf("Buffering %d bytes: %s \n", str_len, buf);
str_len = recv(recv_sock, buf, sizeof(buf) - 1, 0);
buf[str_len] = 0;
printf("Read again: %s \n", buf);
close(acpt_sock);
close(recv_sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
程序调用recv函数的同时传递MSG_PEEK可选项,这是为了保证即使不存在待读取数据也不会进入阻塞状态。再次调用recv函数,这次并未设置任何可选项。因此,本次读取的数据将从输入缓冲中删除。
编译peek_recv.c并运行:
# gcc peek_recv.c -o peek_recv
# ./peek_recv 8500
Buffering 3 bytes: 123
Read again: 123
编译peek_send.c并运行:
# gcc peek_send.c -o peek_send
# ./peek_send 127.0.0.1 8500
通过运行结果可以验证,仅发送一次的数据被读取两次,因为第一次调用recv函数时设置了MSG_PEEK选项,以上就是MSG_PEEK可选项的功能。