问题
write() 和 send() 都可以发送数据,有什么区别?
read() 和 recv() 都可以接收数据,有什么区别?
- send() 和 recv() 比 write() 和 read() 多了一个 flags 参数,用于描述收发网络数据时的选项
数据收发选项
flags - 收发数据时指定的可选项信息
当调用 send()、recv() 函数,将 flags 参数指定为 0 时:
调用 send() 函数时,首先查看发送缓冲区的数据是否被发送出去了,等发送缓冲区的数据都被发送完成后,再把网络数据拷贝到发送缓冲区中,然后就返回。这和调用 write() 函数的效果是一样的。
调用 recv() 函数时,首先去查看接收缓冲区是否有数据,有数据则将数据拷贝到用户空间中用户定义的缓冲区中,无数据则阻塞等待数据的到来。 这和调用 read() 函数的效果是一样的。
flags 选项信息
注意:
不同的操作系统对上述可选项的支持不同,实际工程开发时,需要事先对目标系统中支持的可选项进行调研。
MSG_OOB (带外数据,紧急数据)
原生定义
- 使用与普通数据不同的通道独立传输的数据
- 带外数据优先级比普通数据高 (优先传输,对端优先接收)
TCP 中的带外数据
- 由于原生设计的限制,TCP 无法提供真正意义上的带外数据
- TCP 中仅能通过传输协议消息头中的标记,传输紧急数据,且长度仅 1 字节
TCP 带外数据实现原理
URG 指针指向紧急消息的下一个位置。即:URG 指针指向位置的前一个字节存储了紧急消息。
TCP 带外数据处理策略
由于 TCP 设计为流式数据,因此,无法做到真正的带外数据
被标记的紧急数据可被提前接收,进入特殊缓冲区 (仅 1 字节)
- 每个 TCP 包最多只有一个紧急数据
- 特殊缓冲区仅存放最近的紧急数据 (不及时接收将丢失)
用下面的方式收发数据会发生什么?
发送普通数据,普通方式接收
- 有数据到来则接收,无数据到来则阻塞等待
发送普通数据,紧急方式接收
- 会去特殊缓冲区去取数据,由于发送的是普通数据,特殊缓冲区并无数据,所以会出错返回
发送紧急数据,普通方式接收
- 会去接收缓冲区去取数据,由于发送的是紧急数据,紧急数据存放在接收缓冲区,所以会阻塞等待接收缓冲区的数据到来
发送紧急数据,紧急方式接收
- 发送的数据将存放到特殊缓冲区,接收方去特殊缓冲区中取数据
TCP 紧急数据的发送与接收
client.c
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
int main()
{
int sock = -1;
struct sockaddr_in addr = {0};
char* test = "D.T.Software";
sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock == -1)
{
printf("socker error\n");
return -1;
}
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
addr.sin_port = htons(8888);
if(connect(sock, (struct sockaddr*)&addr, sizeof(addr)) == -1)
{
printf("connect error\n");
return -1;
}
printf("connect succeed\n");
send(sock, test, strlen(test), MSG_OOB);
getchar();
close(sock);
return 0;
}
server.c
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <string.h>
int main()
{
int server = 0;
struct sockaddr_in saddr = {0};
int client = 0;
struct sockaddr_in caddr = {0};
socklen_t csize = 0;
char buf[64] = {0};
int r = 0;
server = socket(AF_INET, SOCK_STREAM, 0);
if(server == -1)
{
printf("server socket error\n");
return -1;
}
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = htonl(INADDR_ANY);
saddr.sin_port = htons(8888);
if(bind(server, (struct sockaddr*)&saddr, sizeof(saddr)) == -1)
{
printf("server bind error\n");
return -1;
}
if(listen(server, 1) == -1)
{
printf("server listen error\n");
return -1;
}
printf("start to accept\n");
while(1)
{
csize = sizeof(caddr);
client = accept(server, (struct sockaddr*)&caddr, &csize);
if(client == -1)
{
printf("server accept error\n");
return -1;
}
printf("client = %d\n", client);
do
{
r = recv(client, buf, sizeof(buf), MSG_OOB);
if(r > 0)
{
buf[r] = '\0';
printf("OOB: %s\n", buf);
}
r = recv(client, buf, sizeof(buf), 0);
if(r > 0)
{
buf[r] = '\0';
printf("Receive: %s\n", buf);
}
}while(r > 0);
printf("\n");
close(client);
}
close(server);
return 0;
}
客户端把 D.T.Software 作为紧急数据发送给服务端;服务端接收紧急数据和普通数据,并打印出来。
程序运行结果如下所示:
我们想把多个字节以紧急数据的方式发送出去,由于一个数据包只能携带一个字节的紧急数据,所以 D.T.Software 最后一个字节 e 会作为紧急数据发送出去,其他数据以普通数据的方式被发送出去。
我们一共测试了 5 次。4 次先打印紧急数据,1 次先打印普通数据。
先收到紧急数据是因为:当接收方调用 recv 接收紧急数据时,此时接收方的特殊缓冲区已经有数据了,所以把接收缓冲区的一字节数据拷贝到用户缓冲区,然后打印出来,随后处理普通数据。
先打印普通数据是因为:当接收方调用 recv 接收紧急数据时,此时接收方的特殊缓冲区并没有数据,随后返回 -1,去接收普通数据,由于接收普通数据是阻塞接收,所以一定会接收到,接收到普通数据后,紧急数据也已经到达接收缓冲区了,这时调用 recv 就能成功收到紧急数据了。
小问题
实际开发中,如何高效的接收 TCP 紧急数据?
使用 select 接收紧急数据
socket 收到普通数据和紧急数据都会使得 select 立即返回
- 普通数据:socket 处于数据可读状态 (可读取普通数据)
- 紧急数据:socket 处于异常状态 (可读取紧急数据)
紧急数据接收示例
使用 select 接收紧急数据
client.c
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
int main()
{
int sock = -1;
struct sockaddr_in addr = {0};
char* test = "D.T.Software";
sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock == -1)
{
printf("socker error\n");
return -1;
}
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
addr.sin_port = htons(8888);
if(connect(sock, (struct sockaddr*)&addr, sizeof(addr)) == -1)
{
printf("connect error\n");
return -1;
}
printf("connect succeed\n");
send(sock, test, strlen(test), MSG_OOB);
getchar();
close(sock);
return 0;
}
select-server.c
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <stdio.h>
#include <string.h>
int server_handler(int server)
{
struct sockaddr_in addr = {0};
int asize = sizeof(addr);
return accept(server, (struct sockaddr*)&addr, &asize);
}
int client_handler(int client)
{
int ret = -1;
char buf[32] = {0};
ret = recv(client, buf, sizeof(buf) - 1, 0);
if(ret > 0)
{
buf[ret] = '\0';
printf("Receive: %s\n", buf);
if(strcmp(buf, "quit") != 0)
{
ret = write(client, buf, strlen(buf));
}
else
{
ret = -1;
}
}
return ret;
}
int main()
{
int server = 0;
struct sockaddr_in saddr = {0};
int max = 0;
int num = 0;
fd_set reads = {0};
fd_set temps = {0};
fd_set except = {0};
struct timeval timeout = {0};
server = socket(PF_INET, SOCK_STREAM, 0);
if(server == -1)
{
printf("server socket error\n");
return -1;
}
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = htonl(INADDR_ANY);
saddr.sin_port = htons(8888);
if(bind(server, (struct sockaddr*)&saddr, sizeof(saddr)) == -1)
{
printf("server bind error\n");
return -1;
}
if(listen(server, 1) == -1)
{
printf("server listen error\n");
return -1;
}
FD_ZERO(&reads);
FD_SET(server, &reads);
max = server;
printf("start to accept\n");
while(1)
{
timeout.tv_sec = 0;
timeout.tv_usec = 10000;
temps = reads;
except = reads;
num = select(max + 1, &temps, NULL, &except, &timeout);
if(num > 0)
{
for(int i = 0; i <= max; i++)
{
if(FD_ISSET(i, &except))
{
if(i != server)
{
char buf[2] = {0};
int r = recv(i, buf, sizeof(buf) - 1, MSG_OOB);
if(r > 0)
{
printf("OOB: %s\n", buf);
}
}
}
if(FD_ISSET(i, &temps))
{
if(i == server)
{
int client = server_handler(server);
if(client >= 0)
{
FD_SET(client, &reads);
max = (max > client) ? max : client;
printf("accept client: %d\n", client);
}
}
else
{
if(client_handler(i) == -1)
{
FD_CLR(i, &reads);
close(i);
}
}
}
}
}
}
close(server);
return 0;
}
当有紧急数据到来时,与客户端通信 socket 会产生一个异常事件;我们通过 select 来监听对应描述符上的异常事件,当 select 监听到异常事件后,如果不是服务端上的异常事件,我们就通过 recv 来读取紧急数据。这样就实现了紧急数据的高效读取。
程序运行结果如下所示:
当一个数据包同时存在普通数据和紧急数据时,客户端的紧急数据会优先发送出去,所以服务端首先会收到紧急数据,接着会收到普通数据。
小结论
read() / write() 可用于收发普通数据 (不具备拓展功能)
send() / recv() 可通过选项信息拓展更多功能
TCP 紧急数据可标识 256 种紧急事件 (异常事件)
通过 select 能够及时处理紧急数据,并区分普通数据