send
& recv
函数
(一)send
函数
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
-
sockfd
:用于标识套接字的文件描述符。 -
buf
:指向包含待发送数据的缓冲区的指针。 -
len
:要发送的字节数。 -
flags
:发送数据时的标志,通常为 0。可选值有:MSG_DONTROUTE
:数据不通过路由,而直接发送到本地网络。MSG_OOB
:发送带外数据(适用于 TCP)。MSG_DONTWAIT
:非阻塞发送。
(二)recv
函数
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
-
sockfd
:用于标识套接字的文件描述符。 -
buf
:指向用于存放接收数据的缓冲区的指针。 -
len
:可接收的最大字节数,即缓冲区的大小。 -
flags
:接收数据时的标志,通常为 0。可选值有:MSG_PEEK
:读取数据但不从系统缓冲区移除。MSG_OOB
:接收带外数据。MSG_WAITALL
:等待接收全部数据,除非遇到超时或出错。MSG_DONTWAIT
:调用 I/O 函数时不阻塞,如果数据在缓冲区中不可用,函数会立即返回而不是等待数据到达。
(三)可选项 MSG_OOB
:发送紧急消息(带外数据)
-
发送紧急数据:当你希望通过 TCP 连接发送高优先级数据(如中断信号或控制命令)时,可以使用
send
函数并设置MSG_OOB
标志。 -
接收紧急数据:接收端可以使用
recv
函数并设置MSG_OOB
标志来接收这些紧急数据。
下面给出示例:
(1)oob_send.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 sock;
struct sockaddr_in recv_adr;
if(argc!=3) {
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
sock=socket(PF_INET, SOCK_STREAM, 0);
memset(&recv_adr, 0, sizeof(recv_adr));
recv_adr.sin_family=AF_INET;
recv_adr.sin_addr.s_addr=inet_addr(argv[1]);
recv_adr.sin_port=htons(atoi(argv[2]));
if(connect(sock, (struct sockaddr*)&recv_adr, sizeof(recv_adr))==-1)
error_handling("connect() error!");
write(sock, "123", strlen("123"));
send(sock, "4", strlen("4"), MSG_OOB);
write(sock, "567", strlen("567"));
send(sock, "890", strlen("890"), MSG_OOB);
close(sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
- 第31、33行紧急传输数据。正常顺序应该是123、4、567、890,但紧急传输了4和890,所以接收数据也将改变。
(2)oob_recv.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h>
#define BUF_SIZE 30
void error_handling(char *message);
void urg_handler(int signo);
int acpt_sock;
int recv_sock;
int main(int argc, char *argv[])
{
struct sockaddr_in recv_adr, serv_adr;
int str_len, state;
socklen_t serv_adr_sz;
struct sigaction act;
char buf[BUF_SIZE];
if(argc!=2) {
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
act.sa_handler=urg_handler;
sigemptyset(&act.sa_mask);
act.sa_flags=0;
acpt_sock=socket(PF_INET, SOCK_STREAM, 0);
memset(&recv_adr, 0, sizeof(recv_adr));
recv_adr.sin_family=AF_INET;
recv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
recv_adr.sin_port=htons(atoi(argv[1]));
if(bind(acpt_sock, (struct sockaddr*)&recv_adr, sizeof(recv_adr))==-1)
error_handling("bind() error");
listen(acpt_sock, 5);
serv_adr_sz=sizeof(serv_adr);
recv_sock=accept(acpt_sock, (struct sockaddr*)&serv_adr, &serv_adr_sz);
fcntl(recv_sock, F_SETOWN, getpid());
state=sigaction(SIGURG, &act, 0);
while((str_len=recv(recv_sock, buf, sizeof(buf), 0))!= 0)
{
if(str_len==-1)
continue;
buf[str_len]=0;
puts(buf);
}
close(recv_sock);
close(acpt_sock);
return 0;
}
void urg_handler(int signo)
{
int str_len;
char buf[BUF_SIZE];
str_len=recv(recv_sock, buf, sizeof(buf)-1, MSG_OOB);
buf[str_len]=0;
printf("Urgent message: %s \n", buf);
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
-
设置信号处理器
-
struct sigaction act; act.sa_handler = urg_handler; sigemptyset(&act.sa_mask); act.sa_flags = 0;
sigaction
结构体用于设置信号处理器。sa_handler
被设置为自定义的处理函数urg_handler
,它会在接收到SIGURG
信号时被调用。sigemptyset
初始化sa_mask
,使其不阻塞任何信号。sa_flags
设置为 0,表示使用默认行为。
-
-
设置接收带外数据的进程
-
fcntl(recv_sock, F_SETOWN, getpid()); state = sigaction(SIGURG, &act, 0);
fcntl()
函数设置recv_sock
的拥有者为当前进程,使得当前进程能够接收到该套接字的SIGURG
信号(表示带外数据到达)。F_SETOWN
是fcntl()
函数的一个选项,用于设置某个套接字文件描述符的“所有者进程”或“所有者进程组”。getpid()
返回当前进程的 PID(进程 ID),将这个 PID 设置为recv_sock
的所有者。sigaction()
函数将SIGURG
信号与之前设置的urg_handler
处理函数关联。当带外数据到达时,系统会发送SIGURG
信号。
-
(四)基于 Windows 的 MSG_OOB
在 Windows 中无法完成针对 MSG_OOB
的事件处理,需要考虑其他方法。可以通过 select
函数解决这一问题,因为之前提到 select
函数可以监视异常套接字,而 Out-of-band 数据也属于异常。
(1)oob_send_win.c
#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>
#define BUF_SIZE 30
void ErrorHandling(char *message);
int main(int argc, char *argv[])
{
WSADATA wsaData;
SOCKET hSocket;
SOCKADDR_IN sendAdr;
if(argc!=3) {
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
if(WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
ErrorHandling("WSAStartup() error!");
hSocket=socket(PF_INET, SOCK_STREAM, 0);
memset(&sendAdr, 0, sizeof(sendAdr));
sendAdr.sin_family=AF_INET;
sendAdr.sin_addr.s_addr=inet_addr(argv[1]);
sendAdr.sin_port=htons(atoi(argv[2]));
if(connect(hSocket, (SOCKADDR*)&sendAdr, sizeof(sendAdr))==SOCKET_ERROR)
ErrorHandling("connect() error!");
send(hSocket, "123", 3, 0);
send(hSocket, "4", 1, MSG_OOB);
send(hSocket, "567", 3, 0);
send(hSocket, "890", 3, MSG_OOB);
closesocket(hSocket);
WSACleanup();
return 0;
}
void ErrorHandling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
(2)oob_recv_win.c
#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>
#define BUF_SIZE 30
void ErrorHandling(char *message);
int main(int argc, char *argv[])
{
WSADATA wsaData;
SOCKET hAcptSock, hRecvSock;
SOCKADDR_IN recvAdr;
SOCKADDR_IN sendAdr;
int sendAdrSize, strLen;
char buf[BUF_SIZE];
int result;
fd_set read, except, readCopy, exceptCopy;
struct timeval timeout;
if(argc!=2) {
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
if(WSAStartup(MAKEWORD(2, 2), &wsaData)!=0)
ErrorHandling("WSAStartup() error!");
hAcptSock=socket(PF_INET, SOCK_STREAM, 0);
memset(&recvAdr, 0, sizeof(recvAdr));
recvAdr.sin_family=AF_INET;
recvAdr.sin_addr.s_addr=htonl(INADDR_ANY);
recvAdr.sin_port=htons(atoi(argv[1]));
if(bind(hAcptSock, (SOCKADDR*)&recvAdr, sizeof(recvAdr))==SOCKET_ERROR)
ErrorHandling("bind() error");
if(listen(hAcptSock, 5)==SOCKET_ERROR)
ErrorHandling("listen() error");
sendAdrSize=sizeof(sendAdr);
hRecvSock=accept(hAcptSock, (SOCKADDR*)&sendAdr, &sendAdrSize);
FD_ZERO(&read);
FD_ZERO(&except);
FD_SET(hRecvSock, &read);
FD_SET(hRecvSock, &except);
while(1)
{
readCopy=read;
exceptCopy=except;
timeout.tv_sec=5;
timeout.tv_usec=0;
result=select(0, &readCopy, 0, &exceptCopy, &timeout);
if(result>0)
{
if(FD_ISSET(hRecvSock, &exceptCopy))
{
strLen=recv(hRecvSock, buf, BUF_SIZE-1, MSG_OOB);
buf[strLen]=0;
printf("Urgent message: %s \n", buf);
}
if(FD_ISSET(hRecvSock, &readCopy))
{
strLen=recv(hRecvSock, buf, BUF_SIZE-1, 0);
if(strLen==0)
{
break;
closesocket(hRecvSock);
}
else
{
buf[strLen]=0;
puts(buf);
}
}
}
}
closesocket(hAcptSock);
WSACleanup();
return 0;
}
void ErrorHandling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
(四)检查输入缓冲
同时设置 MSG_PEEK
和 MSG_DONTWAIT
选项,以验证输入缓冲中是否存在接收的数据。设置 MSG_PEEK
选项并调用 recv
函数时,即使读取了输入缓冲中的数据也不会删除。 因此该选项通常与 MSG_DONTWAIT
合作,用于调用以非阻塞方式验证待读数据存在与否的函数。
(1)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);
}
(2)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(recv_sock, buf, sizeof(buf)-1, MSG_PEEK|MSG_DONTWAIT)
:使用MSG_PEEK
标志查看接收缓冲区中的数据而不从缓冲区中移除数据。MSG_DONTWAIT
表示在没有数据可用时立即返回。这个调用会阻塞直到有数据可用。str_len
记录读取到的数据长度。buf[str_len] = 0
:将缓冲区末尾添加 null 字符,形成有效的 C 字符串。printf("Buffering %d bytes: %s \n", str_len, buf)
:打印缓冲区中数据的内容和长度。
-
再次接收数据:
recv(recv_sock, buf, sizeof(buf)-1, 0)
:使用普通模式接收数据,接收到的数据将会从缓冲区中移除。str_len
记录读取到的数据长度。buf[str_len] = 0
:将缓冲区末尾添加 null 字符,形成有效的 C 字符串。printf("Read again: %s \n", buf)
:打印第二次接收的数据的内容。
readv
& writev
函数
readv
& writev
函数的功能可概括如下:
“对数据进行整合传输及发送的函数。”
也就是说,通过 writev
函数可以将分散保存在多个缓冲中的数据一并发送;通过 readv
函数可以由多个缓冲分别接收。
(一)writev
函数
#include <sys/uio.h>
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
-
fd
:要读取的文件描述符。 -
iov
:指向iovec
结构体数组的指针。iovec
结构体用于描述每个缓冲区的起始地址和长度。 -
iovcnt
:iovec
结构体数组的元素数量。
-
iovec
结构体:struct iovec { void *iov_base; // 指向数据缓冲区的指针 size_t iov_len; // 缓冲区的长度 };
函数使用方法:
(1)writev.c
#include <stdio.h>
#include <sys/uio.h>
int main(int argc, char *argv[])
{
struct iovec vec[2];
char buf1[] = "ABCDEFG";
char buf2[] = "1234567";
int str_len;
// 设置 vec[0] 指向 buf1,并指定 buf1 的长度
vec[0].iov_base = buf1;
vec[0].iov_len = 3; // 只写入 buf1 的前 3 个字节
// 设置 vec[1] 指向 buf2,并指定 buf2 的长度
vec[1].iov_base = buf2;
vec[1].iov_len = 4; // 只写入 buf2 的前 4 个字节
// 使用 writev 函数将数据写入标准输出(文件描述符 1)
str_len = writev(1, vec, 2); // 写入 2 个 iovec 结构描述的缓冲区
puts(""); // 输出一个换行符
printf("Write bytes: %d \n", str_len); // 打印实际写入的字节数
return 0;
}
(二)readv
函数
#include <sys/uio.h>
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
参数顺序和含义与 writev
的一致。
(1)readv.c
#include <stdio.h>
#include <sys/uio.h>
#define BUF_SIZE 100
int main(int argc, char *argv[])
{
struct iovec vec[2];
char buf1[BUF_SIZE] = {0,}; // 初始化 buf1 为全零
char buf2[BUF_SIZE] = {0,}; // 初始化 buf2 为全零
int str_len;
// 配置 vec[0] 以读取到 buf1
vec[0].iov_base = buf1;
vec[0].iov_len = 5; // 读取 5 个字节到 buf1
// 配置 vec[1] 以读取到 buf2
vec[1].iov_base = buf2;
vec[1].iov_len = BUF_SIZE; // 读取 BUF_SIZE 个字节到 buf2
// 从标准输入读取数据到两个缓冲区
str_len = readv(0, vec, 2);
// 打印读取的字节数
printf("Read bytes: %d \n", str_len);
// 打印两个缓冲区的内容
printf("First message: %s \n", buf1);
printf("Second message: %s \n", buf2);
return 0;
}
(三)合理使用 readv
& writev
函数
writev
函数在不采用Nagle
算法时更有价值。
上述示例中, Nagle
算法关闭。待发送的数据分别存在三个不同的地方,此时如果使用 write
函数则需要3次函数调用,但若为提高速度而关闭了 Nagle
算法,则极有可能通过3个数据包传递数据。反之,若使用 write
函数将所有数据一次性写入输出缓冲,则很有可能仅通过一个数据包传送数据。
- 再考虑一种情况,将不同位置的数据按照发送顺序移动(复制)到1个大数组,并通过一次
write
函数调用进行传输。这种方式与调用writev
函数的效果相同.
问答
(一)TCP 中设置 MSG_OOB
选项的数据先到达对方主机吗?
-
带外数据的发送:使用
MSG_OOB
标志标记数据为带外数据。 -
接收顺序:带外数据的到达顺序不一定优于其他数据,TCP 的传输机制并不保证带外数据在网络上优先到达。
-
应用层处理:应用程序需要专门的处理逻辑来处理带外数据,并可能需要用到如信号或特定的 API 函数来处理。
-
MSG_OOB
无法脱离 TCP 默认数据传输方式,即使设置了MSG_OOB
,也会保持原有传输顺序。该选项只用于要求接收方紧急处理。