问题
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 能够及时处理紧急数据,并区分普通数据
MSG_PEEK (数据窥探)
使用 MSG_PEEK 选项能够获取接收缓冲区数据的拷贝
- recv() 专用选项,可用于数据预接收
- 指定 MSG_PEEK 选项时,不会清空缓冲区
- 可用于获取接收缓冲区中的数据量 (字节数)
MSG_PEEK 数据窥探示例
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");
sleep(2);
send(sock, test, strlen(test), 0);
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_PEEK);
if(r > 0)
{
buf[r] = '\0';
printf("len = %d\n", r);
printf("data: %s\n", buf);
r = recv(client, buf, sizeof(buf), 0);
buf[r] = '\0';
printf("Receive: %s\n", buf);
}
else
{
printf("no data in receive buf\n");
}
}while(r > 0);
printf("\n");
close(client);
}
close(server);
return 0;
}
当调用 recv() 函数指定 flags 为 MSG_PEEK 时,当接收缓冲区中没有数据时就阻塞等待,接收缓冲区有数据时就返回,将接收缓冲区的数据拷贝到我们指定的 buf 中,但接收缓冲区的数据并没有清空,只是进行了数据窥探,这样我们就可以知道数据的长度和内容了,之后再调用 recv(),进行数据的接收。
程序运行结果如下所示:
服务端首先窥探出接收缓冲区的数据的长度和内容,随后进行再进行读取;当客户端退出后,reccv() 返回 0。
MSG_DONTWAIT (立即收发模式)
数据收发时不阻塞,立即返回
- send() - 如果无法将数据送入发送缓冲区,那么直接错误返回
- recv() - 如果接收缓冲区没有数据,那么直接错误返回
MSG_DONTWAIT 示例
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");
sleep(1);
send(sock, test, strlen(test), 0);
sleep(2);
send(sock, "quit", 5, 0);
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_DONTWAIT);
if(r > 0)
{
buf[r] = '\0';
printf("len = %d\n", r);
printf("Receive: %s\n", buf);
if(strcmp(buf, "quit") == 0)
{
break;
}
}
else
{
printf("no data receive\n");
sleep(1);
}
}while(1);
printf("\n");
close(client);
}
close(server);
return 0;
}
服务端在接收数据的时候使用的是不阻塞接收,无论接收缓冲区是否有数据都立即返回,如果接收缓冲区没有数据就返回 -1。
程序运行结果如下所示:
recv() 返回 -1 时,表示接收缓冲区并没有数据,我们休眠 1s 后,再进行读取。
再论阻塞发送模式 (flags => 0)
send()
再论阻塞接收模式 (flags => 0)
recv()
通信框架的迭代增强
MSG_WAITALL (等待数据)
接收专用,等待需要的数据量完全满足时,recv() 才返回
MSG_MORE (更多数据)
发送专用,指示内核不着急将发送缓冲区中的数据进行传输
数据收发拓展用法
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";
int i = 0;
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");
for(i = 0; i < strlen(test); i++)
{
send(sock, test + i, 1, 0);
if(i % 2)
{
sleep(1);
}
}
test = "quit";
for(i = 0; i < strlen(test) - 1; i++)
{
send(sock, test + i, 1, MSG_MORE);
sleep(1);
}
send(sock, test + i, 1, 0);
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);
static int len[2] = {12, 4};
do
{
for(int i = 0; i < 2; i++)
{
r = recv(client, buf, len[i], MSG_WAITALL);
if(r > 0)
{
buf[r] = '\0';
printf("data: %s\n", buf);
if(strcmp(buf, "quit") == 0)
{
break;
}
}
}
}while(r > 0);
printf("\n");
close(client);
}
close(server);
return 0;
}
服务端在接收数据时指定 flags 为 MSG_WAITALL,当接收缓冲区中有我们指定多的数据长度后才返回。
客户端在发送 quit 时,前三个字节发送时指定了 flags 为 MSG_MORE,这会告诉内核先不要将发送缓冲区的数据发送出去,等第四个字节 t 写入到发送缓冲区后再进行发送。
程序运行结果如下所示:
服务端接收到数据的字节数一定等于想要获取到的数据字节数。