13.1 send& recv 函数
13.1 send& recv 函数
send
NAME
send, sendto, sendmsg - send a message on a socket
SYNOPSIS
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);| 参数 | 值 | 用途 | 说明 |
|---|---|---|---|
| sockfd | The argument sockfd is the file descriptor of the sending socket. | ||
| buf | the message is found in buf | 存储发送msg 的buf | |
| len | msg的长度 | ||
| RETURN | On success, these calls return the number of bytes sent. On error, -1 is returned, and errno is set appropri ately. |
相关信息
The send() call may be used only when the socket is in a connected state (so that the intended recipient is known). The only difference between send() and write(2) is the presence of flags.
| flag值 | 含义 | 说明 |
|---|---|---|
| MSG_OOB | Sends out-of-band data on sockets that support this notion (e.g., of type SOCK_STREAM); the underlying protocol must also support out-of-band data. | OOB是个淘汰的东西,不应该使用(使用标准都没有确定),不是书上有我也不会看 |
| MSG_DONTWAIT | Enables nonblocking operation; if the operation would block, EAGAIN or EWOULDBLOCK is returned. This provides similar behavior to setting the O_NONBLOCK flag (via the fcntl(2) F_SETFL operation), but differs in that MSG_DONTWAIT is a per-call option, whereas O_NONBLOCK is a setting on the open file description (see open(2)), which will affect all threads in the calling process and as well as other processes that hold file descriptors referring to the same open file description. | 在调用send 时设置该标志,send不会阻塞,仅针对当前send有效,file description 仍然是阻塞的。使用fcntl 设置 O_NONBLOCK 会使file description 变成非阻塞的 |
| MSG_DONTROUTE | Don't use a gateway to send out the packet, send to hosts only on directly connected networks. This is usually used only by diagnostic or routing programs. This is defined only for protocol families that route; packet sockets don't. |
MSG_OOB: 发送紧急消息
out-of-band 带外数据
发送紧急消息
客户端发紧急消息给服务端
write(sock, "123", strlen("123"));
send(sock, "4", strlen("4"), MSG_OOB);
write(sock, "567", strlen("567"));
send(sock, "890", strlen("890"), MSG_OOB);使用wireshark 抓包,查看发送的消息:
使用带有 MSG_OOB 选项发送的msg具有Urg值
查看两个Urg msg:
两个msg 的Urgent 标志位均为1
第一个Urg的 Pointer为1
第二个Urg的Pointer为3
接收紧急消息
实现:
int recv_sock;
void urg_handler(int signo)
{
int str_len = 0;
char buf[BUF_SIZE];
str_len = recv(recv_sock, buf, BUF_SIZE, MSG_OOB);
if(str_len > 0) {
buf[str_len] = '\0';
printf("Urgent data received: %s\n", buf);
}
}
int main()
{
struct sigaction act;
act.sa_handler = urg_handler;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
fcntl(recv_sock, F_SETOWN, getpid()); // Set the owner of the socket to the current process
if(sigaction(SIGURG, &act, 0) == -1)
error_handling("sigaction() error");
while(true) {
str_len = recv(recv_sock, message, BUF_SIZE-1, 0);
if(str_len == -1) {
if(errno == EINTR) continue; // Interrupted by signal
error_handling("recv() error");
} else if(str_len == 0) {
break; // Connection closed
}
message[str_len] = '\0';
printf("Message from client: %s\n", message);
}
close(recv_sock);
close(sock);
return 0;
}| 行号 | 功能 | 说明 |
|---|---|---|
| 1 | 连接的客户端的socket | 因为要在信号处理函数中读取数据,将客户端socket fd定义为全局变量,以便处理函数访问 |
| 3-12 | 问:读取时可不可以不用MSG_OOB 选项? 问:这里进入信号处理,一定产生了信号,是否一定有数据可读(recv是否一定返回大于0?)从而不需要判断出错(返回值小于0)的情况? | |
| 16-23 | 设置SIGURG 信号处理进程 以及 对应的函数 | |
| 21 | 设置处理的进程 | ”多个进程可以共同拥有1个套接字的文件描述符“,发生信号时需要指定处理信号的进程,通过fcntl 可以指定socket 的拥有者,拥有者负责处理信号。getpid()返回调用进程的pid。21行语句的含义:设置当前进程为recv_sock 的拥有者(通过F_SETOWN指定设置参数)。socket 原本属于操作系统,而操作系统对于 SIGURG 信号的Action 为Ign-忽略。 |
| 25-35 | 正常接收并打印数据 |
效果:
在虚拟机中运行服务端以及客户端:
- 启动服务端
ming@ubuntu:/media/sf_share/Network/build$ ./Network 192.168.56.101 10086- 启动客户端
ming@ubuntu:/media/sf_share/Network/build$ ./MSG_OOB 192.168.56.101 10086- 服务端打印信息:
Message from client: 123
Urgent data received: 4
Message from client: 567
Urgent data received: 0
Message from client: 89注意到信号处理中 recv 接收的数据长度为 BUF_SIZE 远远大于发生的msg 的长度,但是为什么在发送 890 时,处理函数中仅recv 到1字节数据?
相关信息
带外数据的含义是:
通过完全不同的通信路径传输的数据
真正意义上的 Out-of-band 需要通过单独的通信路径高速传输数据,但TCP不另外提供,只利用TCP的紧急模式(Urgent mode) 进行传输
- TCP没有单独的路径传输带外数据:换句话说recv 带有 MSG_OOB 的带外数据 和 recv 非MSG_OOB 数据的传输方式相同
- 紧急模式:会产生SIGURG信号
紧急模式
相关信息
MSG_OOB真正意义在于督促数据接收对象尽快处理数据
- TCP 保持传输顺序:带外数据按照发送顺序传输,并不会比非带外数据更快到达
- 如何尽快处理:通过信号处理-可以中断其他操作
识别带外数据
书中的解释没看懂
示例为传输890,假设已传输之前的数据,字符8在输出缓冲中的偏移量为0,紧急指针的偏移量为3,”紧急指针指向的偏移量为3之前的部分就是紧急消息“
几个问题:
- 非紧急消息数据会和紧急消息数据混杂在一起吗?
- 如果不会混杂,那么带有URG 标志的msg 的所有用户数据不都是紧急数据?紧急指针有什么用?
- 如果会混杂,那非紧急消息在紧急消息前面(偏移量小于紧急指针),单个紧急指针也没办法区分紧急和非紧急吧?
在 SIGURG 信号处理函数中,如果不指定接收MSG_OOB
实现:
str_len = recv(recv_sock, buf, BUF_SIZE-1, 0);效果:
Message from client: 123
str_len: 3
Urgent data received: 567
str_len: 0
Message from client: 89- 收不到4的紧急消息
- 567不是紧急消息,但是在信号处理中读取
- 0也没有读到
参考AI解答:
- recv 时若未指定MSG_OOB 则只会读取普通数据
- 紧急消息只包含一个字节数据
- 对于第4行str_len 为0,是因为主循环recv从接收缓冲中读取完数据,client 发送了FIN,所以recv 返回0
偏移量的理解
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Urgent data |
|---|---|---|---|---|---|---|---|---|---|---|
| 4 | Urgent Pointer = 1 | 4 | ||||||||
| 8 | 9 | 0 | Urgent Pointer = 3 | 0 | ||||||
| 4 | 5 | 6 | 7 | 8 | 9 | 0 | Urgent Pointer = 7 | 0 |
BUG?还是丢失数据?
增加str_len的打印,第一次运行时没有打印4
ming@ubuntu:/media/sf_share/Network/build$ ./Network 192.168.56.101 10086
Message from client: 123
Message from client: 567
str_len: 1
Urgent data received: 0
Message from client: 89
ming@ubuntu:/media/sf_share/Network/build$ ./Network 192.168.56.101 10086
Message from client: 123
str_len: 1
Urgent data received: 4
Message from client: 567
str_len: 1
Urgent data received: 0
Message from client: 89看第二行 和 第三行,在123 和 567 之间没有收到4???
参考AI:
- OOB 缓冲只有1个字节
- 客户端发送数据过快,在4还没有被读取前,接收缓冲已经收到 567 和 890
- ”Unix 信号是不排队的(Standard Signals are not queued),如果两次 SIGURG 发生得太快,它们会被合并成一次信号通知。“
问:这里就有疑问了,根据之前讲的信号不能嵌套,在处理一个信号时如果再次接收到信号,不会处理新的信号,那是不是和这里的 ”不排队“、”合并“冲突了???
与其说是信号合并,我更接收OOB 缓冲的1字节数据被更新了。
- OOB不是队列;OOB 是指针;OOB 是状态:只能知道OOB发生了变化,并获取最新的OOB值,无法获知 OOB变化了几次,如果有多次变化,中间的变化过程。也就是只保留最新结果,不缓存中间状态。
在wsl中启动客户端
在wsl中运行客户端,连接到虚拟机中的linux 服务端
效果:
ming@ubuntu:/media/sf_share/Network/build$ ./Network 192.168.56.101 10086
Message from client: 123456789- 没有调用信号处理
抓到的msg:
第一次发送了012,3bytes数据,第二次发送了 4567890. 第二包数据带有URG ,Urg pointer 值为7
和理论正确的包相比,多了 TSval 和 TSecr,AI 告知是时间戳,应该不影响 OOB
问:为什么使用WSL 和 在 虚拟机中 运行的结果不一样?为什么WSL 连接的结果是这个样子???
相关信息
来自ChatGPT
TCP OOB 的定义本身就是模糊的,不同 OS 对 urgent pointer 的解释并不一致。WSL 中你看
到的,其实是 Windows TCP 的 urgent 语义,而不是 Linux 的
MSG_DONTWAIT:调用send时不阻塞
- 什么情况下send会阻塞?
接收(读取)数据的阻塞很好理解、测试,没有数据时(数据为空)时阻塞。但是send呢?对于socket,那默认情况有一定的缓冲,要把缓冲填满然后等待有空余?只要接收方不从接收缓冲中读取数据,那么发送缓冲就可以被填满,然后阻塞send - 查看缓冲区大小
参考 9.1 套接字的多种可选项
读取发送缓冲区 以及 接收缓冲区大小
结果:
Input buffer size: 16384
Output buffer size: 131072那么填充满发送以及接收缓冲的字节数:16384 + 131072 = 147456
有点多,改小一点,将客户端的接收缓冲设置为5000,服务端的发送缓冲设置为5000
注意虽然接收缓冲大小设置的是5000,但实际大小并不是5000(而是10000)
发送缓冲大小变化为设定值
- 测试
实现:
char send_buf[atoi(argv[3])] = {0};
printf("buf size: %d\n", atoi(argv[3]));
while(true) {
printf("send data\n");
str_len = send(recv_sock, send_buf, sizeof(send_buf), 0);
printf("str_len: %d\n", str_len);
}注意
注意这里send之前的打印信息需要换行(刷新stdout 缓冲,立即显示打印信息),否则可能因为send阻塞,导致写入stdout缓冲中的数据没有被输出到屏幕
效果:
在发送13000字节长度数据时
ming@ubuntu:/media/sf_share/Network/build$ ./Network 192.168.56.101 10086 13000
TCP sock sendbuffer size : 16384
after set bufsize, TCP sock sendbuffer size : 5000
send datasend 阻塞住了
在发送12000字节长度数据时:
ming@ubuntu:/media/sf_share/Network/build$ ./Network 192.168.56.101 10086 12000
TCP sock sendbuffer size : 16384
after set bufsize, TCP sock sendbuffer size : 5000
buf size: 12000
send data
str_len: 12000
send data第一次成功发送了12000字节长度数据,第二次写入时阻塞
两个结论:
- send在缓冲大小不足时被阻塞
- send不被阻塞的数据长度小于 发送缓冲大小 + 接收缓冲大小
设置MSG_DONTWAIT
str_len = send(recv_sock, send_buf, sizeof(send_buf), MSG_DONTWAIT);
if(str_len == -1)
unix_error("send error");结果:
11 send error: Resource temporarily unavailable错误信息提示:资源暂时不可用
EAGAIN 和 EWOULDBLOCK 是一个值
#define EAGAIN 0x11
#define EWOULDBLOCK EAGAINrecv
NAME
recv, recvfrom, recvmsg - receive a message from a socket
SYNOPSIS
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);| 参数 | 值 | 用途 | 说明 |
|---|---|---|---|
| RETURN VALUE | These calls return the number of bytes received, or -1 if an error occurred. |
| flag值 | 含义 | 说明 |
|---|---|---|
| MSG_PEEK | This flag causes the receive operation to return data from the beginning of the receive queue without removing that data from the queue. Thus, a subsequent receive call will return the same data. | |
| MSG_DONTWAIT | 和send中的功能相同 |
检查输入缓冲
MSG_PEEK 读取输入缓冲中的数据,但是不会删除数据。配合 MSG_DONTWAIT 用来“验证待读取数据存在与否”
实现:
while(true) {
str_len = recv(recv_sock, buf, sizeof(buf)-1, MSG_DONTWAIT | MSG_PEEK);
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 %d bytes: %s\n", str_len, buf);| 行号 | 功能 | 说明 |
|---|---|---|
| 2 | 非阻塞、PEEK 方式读取输入缓冲中的数据 | |
| 9 | 正常的recv读取缓冲中的数据 |
效果:
Buffering 3 bytes: abc
Read 3 bytes: abcreadv & writev
NAME
readv, writev, preadv, pwritev - read or write data into multiple buffers
SYNOPSIS
#include <sys/uio.h>
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);| 行号 | 功能 | 说明 |
|---|---|---|
| fd | writev: 要写入的fd; readv: 从fd中读数据 | |
| iov | The pointer iov points to an array of iovec structures | |
| iovcnt | iov的数量 |
iovec
struct iovec {
void *iov_base; /* Starting address */
size_t iov_len; /* Number of bytes to transfer */
};相关信息
writev函数可以将分散保存在多个缓冲中的数据一并发送,通过readv函数可以由多个缓冲分别接收
writev:将保存在iov指向的缓冲中的数据写入到fd指向的文件
readv:将fd指向的文件中的数据写入到iov指向的缓冲(按iov的顺序写入)
writev
在iov中存放字符串,输出到标准输出
#include <sys/uio.h>
#include <string.h>
int main()
{
struct iovec iov[2];
iov[0].iov_base = (void *)"Hello, ";
iov[0].iov_len = strlen((char *)iov[0].iov_base);
iov[1].iov_base = (void *)"World!\n";
iov[1].iov_len = strlen((char *)iov[1].iov_base);
writev(1, iov, 2); // Write to standard output (file descriptor 1)
return 0;
}效果:
Hello, World!readv
从标准输入中读取数据,分别存放到不同的ivo中
实现:
#include <sys/uio.h>
#include <string.h>
#include <stdio.h>
#define BUF_SIZE 3
int main()
{
char buffer0[BUF_SIZE] = {0};
char buffer1[BUF_SIZE] = {0};
struct iovec iov[2];
iov[0].iov_base = buffer0;
iov[0].iov_len = BUF_SIZE;
iov[1].iov_base = buffer1;
iov[1].iov_len = BUF_SIZE;
printf("readv returned: %d\n", readv(0, iov, 2));
printf("Buffer 0: %s\n", buffer0);
printf("Buffer 1: %s\n", buffer1);
return 0;
}效果:
ming@ubuntu:/media/sf_share/Network/build$ ./Network
readv returned: 1
Buffer 0:
Buffer 1:1
readv returned: 2
Buffer 0: 1
Buffer 1:12
readv returned: 4
Buffer 0: 12
Buffer 1:1234
readv returned: 5
Buffer 0: 123
Buffer 1: 4123456
readv returned: 6
Buffer 0: 123
Buffer 1: 456▒123456789
readv returned: 6
Buffer 0: 123
Buffer 1: 456b- 标准输入中回车会被当作一字节输入
- 在第一个(前面一个)iov指向的接收缓冲满了之后填写第二个(后面的)接收缓冲
- 一旦有数据,readv就会写入到iov中并返回,即使数据大小小于可写入的总的数据大小(所以如果要等待写入指定大小的数据的话,需要通过readv的返回值进行判断?)
问:为什么输入12回车回车时,第一个回车没有结束输入,而是要再输入一个回车?而其他的输入第一个回车后就结束输入?
优点
避免多次调用read、write,这样在关闭了Nagle 算法时,使用write多包发送的数据可以通过单包发送。
或者数据类型、内容的来源、去向不同时更容易区分