TCP/IP网络编程(10) IO函数
在Linux下,一般使用read & write函数完成数据IO,因为Linux下的套接字,可视为文件,其操作方式与文件类似,当套接字分配之后,会为其分配对应的文件描述符。在Windows下,则需要使用recv & send函数完成数据IO
1. Linux下的recv & send 函数
Linux下的recv 和send函数与windows下的其实并无差别,其参数顺序,含义,使用方法完全相同,与实际区别不大:
#include <sys/socket.h> ssize_t send(int sockfd, const void* buf, size_t nbytes, int flags); /* sockfd: 与数据传输对象连接的套接字文件描述符 buf: 待传输数据的缓冲区 nbytes: 待传输的字节数 flags: 传输数据时指定的可选项信息 */
#include <sys/socket.h> ssize_t recv(int sockfd, const void* buf, size_t nbytes, int flags); /* sockfd: 与数据接收对象连接的套接字文件描述符 buf: 待接收数据的缓冲区 nbytes: 待接收的字节数 flags: 接收数据时指定的可选项信息 */
send()和recv()函数的最后一个参数表示数据收发的可选项,该选项可使用位或运算同时传递多个信息:
可选项 | 含义 | send | recv |
MSG_OOB | 用于传输带外数据(out-of-band data) | 是 | 是 |
MSG_PEEK | 验证输入缓冲中是否存在接收的数据 | 否 | 是 |
MSG_DONTROUTE | 数据传输过程中不参照路由表,在本地网络中寻找目的地 | 是 | 否 |
MSG_DONTWAIT | 调用IO函数时不阻塞,用于使用非阻塞IO | 是 | 是 |
MSG_WAITALL | 防止函数返回,直到接收全部请求的字节数 | 否 | 是 |
1.1 MSG_OOB发送紧急消息:
MSG_OOB可用于创建特殊发送方法和特殊发送通道,发送带外数据紧急消息。使用MSG_OOB收发消息例程如下所示:
客户端client.cpp代码:
/* send the msg oob data */ #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <arpa/inet.h> #define BUFF_SIZE 30 #define RECV_ADDRESS "127.0.0.1" #define RECV_PORT 54100 void error_handler(char* message); int main(int argc, char* argv[]) { int sock; struct sockaddr_in recvAddr; sock = socket(PF_INET, SOCK_STREAM, 0); memset(&recvAddr, 0, sizeof(recvAddr)); recvAddr.sin_family = AF_INET; recvAddr.sin_addr.s_addr = inet_addr(RECV_ADDRESS); // 地址 recvAddr.sin_port = htons(RECV_PORT); // 端口 printf("Connecting to server......\n"); if (connect(sock, (struct sockaddr*)&recvAddr, sizeof(recvAddr)) == -1) { char message[50]; sprintf(message, "Failed to connect to address: %s %d", RECV_ADDRESS, RECV_PORT); error_handler(message); } printf("Successfully connectiing server.\n"); write(sock, "1234", strlen("1234")); send(sock, "1131", strlen("1131"), MSG_OOB); // 紧急发送消息,带外数据 out of band (OOB) write(sock, "333", strlen("333")); send(sock, "991", strlen("991"), MSG_OOB); // 紧急发送消息,带外数据 out of band (OOB) close(sock); return 0; } void error_handler(char* message) { fputs(message, stderr); fputc('\n', stderr); exit(1); }
服务端server.cpp代码:
/* server receive data */ #include <stdio.h> #include <unistd.h> #include <string.h> #include <stdlib.h> #include <signal.h> #include <sys/socket.h> #include <arpa/inet.h> #include <fcntl.h> #define BUFF_SZIE 30 #define SERVER_ADDRESS "127.0.0.1" #define SERVER_PORT 54100 void error_handler(char* message); void urge_handler(int signo); int server_sock; int client_sock; int main(int argc, char* argv[]) { struct sockaddr_in serverAddr, clientAddr; int strLen, state; socklen_t addrSize = sizeof(serverAddr); struct sigaction act; char buf[BUFF_SZIE]; memset(buf, 0, BUFF_SZIE); // 信号初始化 act.sa_handler = urge_handler; // 信号处理函数 sigemptyset(&act.sa_mask); act.sa_flags = 0; server_sock = socket(PF_INET, SOCK_STREAM, 0); memset(&serverAddr, 0, sizeof(serverAddr)); serverAddr.sin_family = AF_INET; serverAddr.sin_addr.s_addr = inet_addr(SERVER_ADDRESS); serverAddr.sin_port = htons(SERVER_PORT); if (bind(server_sock, (struct sockaddr*)&serverAddr, addrSize) == -1) { error_handler("Failed to bind()\n"); } listen(server_sock, 5); client_sock = accept(server_sock, (struct sockaddr*)&serverAddr, &addrSize); if (client_sock == -1) { error_handler("Failed to accept new connection from client.\n"); } fcntl(client_sock, F_SETOWN, getpid()); state = sigaction(SIGURG, &act, 0); // 注册信号 while ((strLen = recv(client_sock, buf, BUFF_SZIE - 1, 0)) != 0) { /* code */ if (strLen == -1) continue; char content[2*BUFF_SZIE]; memset(content, 0, 2*BUFF_SZIE); sprintf(content, "Receive buffer is : %s\n", buf); puts(content); memset(buf, 0, BUFF_SZIE); } return 0; } void error_handler(char* message) { fputs(message, stderr); fputc('\n', stderr); exit(1); } /* 如果客户端调用了紧急发送带外数据的函数,则系统会产生SIGURGE信号,并调用注册的信号处理函数进行处理 */ void urge_handler(int signo) { char buff[BUFF_SZIE]; memset(buff, 0, BUFF_SZIE); int recvLen = recv(client_sock, buff, BUFF_SZIE - 1, MSG_OOB); printf("Urge Message: %s\n", buff); }
输出:
需要介绍一下fcntl函数的作用:
fcntl函数可以用来对已打开的文件描述符进行各种控制操作以改变已打开文件的的各种属性;
介绍:fcntl 函数 - 天池渔隐 - 博客园 (cnblogs.com)
fcntl(client_sock, F_SETOWN, getpid());
上述语句含义表示:文件描述符client_sock指向的套接字引发的SIGURG信号处理进程将变为getpid函数返回值用作ID的进程。
上述描述中的处理SIGURG信号表示的是“调用SIGURG信号处理函数”。在多进程服务器中,多个进程可以共同拥有一个套接字文件描述符。例如,通过fork函数创建了子进程,并同时复制了文件描述符,此时如果发生了SIGURG信号,应该调用哪个进程的信号处理函数就成为了一个问题。因此,在处理信号的时候,必须指定哪一个进程来处理信号。在上述代码中,处理SIGURG信号的时候必须指定处理信号的进程,而getpid函数返回调用此函数的进程。则fctl语句指定的是当前进程作为处理SIGURG信号的主体。
*通过观察程序的输出结果,发现通过MSG_OOB可选项传递数据时,只会返回一个字节;通过MSG_OOB可选项传输数据时并不会加快数据的传输速度,而且通过信号处理函数读取传输过来的数据时,也只能读取到一个字节的数据,剩余的数据只能通未设置MSG_OOB可选项的普通输入函数进行读取。因为TCP不存在真正意义上的带外数据(Out of b band),真正意义上的带外数据需要通过单独的通信路径高速传输数据,TCP不提供这样的传输路径,仅是利用TCP的紧急模式进行传输。
检查输入缓冲区
同时设置MSG_PEEK和MSG_DONTWAIT选项,可以验证输入缓冲中是否存在接收的数据。设置MSG_PEEK选项并调用recv函数时,即使读取了输入缓冲中的数据,也不会将缓冲中的数据进行删除。此选项通常与MSG_DONTWAIT合作,用于调用以非阻塞方式验证待读取数据是否存在。示例代码如下所示:
客户端代码:
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <arpa/inet.h> #define ADDRESS "127.0.0.1" #define PORT 38400 int main(int argc, char** argv) { int sock; struct sockaddr_in address; sock = socket(PF_INET, SOCK_STREAM, 0); memset(&address, 0, sizeof(address)); address.sin_family = AF_INET; address.sin_addr.s_addr = inet_addr(ADDRESS); address.sin_port = htons(PORT); if (connect(sock, (struct sockaddr*)&address, sizeof(address))) { printf("Failed to connect to server.\n"); return -1; } write(sock, "3456", strlen("3456")); close(sock); return 0; }
服务端代码:
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <arpa/inet.h> #define ADDRESS "127.0.0.1" #define PORT 38400 #define BUFF_SIZE 30 int main(int argc, char** argv) { int serverSock, clientSock; struct sockaddr_in servAddr, clientAddr; socklen_t addrSize = sizeof(servAddr); char buffer[BUFF_SIZE]; memset(buffer, 0, BUFF_SIZE); memset(&servAddr, 0, sizeof(servAddr)); memset(&clientAddr,0 , sizeof(clientAddr)); serverSock = socket(PF_INET, SOCK_STREAM, 0); servAddr.sin_family = AF_INET; servAddr.sin_addr.s_addr = inet_addr(ADDRESS); servAddr.sin_port = htons(PORT); int bindRes = bind(serverSock, (struct sockaddr*)&servAddr, sizeof(servAddr)); if (bindRes == -1) { printf("Failed to bind the socket."); return -1; } // 开始监听 listen(serverSock, 5); clientSock = accept(serverSock, (struct sockaddr*)&servAddr, &addrSize); while (true) { /* code */ int recvLen = recv(clientSock, buffer, BUFF_SIZE-1, MSG_PEEK | MSG_DONTWAIT); if (recvLen > 0) break; } printf("Buffer content: %s\n", buffer); printf("Clearing the buffer.\n"); memset(buffer, 0, BUFF_SIZE); recv(clientSock, buffer, BUFF_SIZE-1, 0); printf("Read again: %s\n", buffer); close(clientSock); close(serverSock); return 0; }
运行结果:
可以看到在第一次读取完缓冲区中的数据之后,并未将数据进行清除,还可再次读取数据
readv函数和writev函数 (矢量IO操作)
”这两个函数主要用于对数据进行整合传输以及发送“
即writev函数可以将分散保存在多个缓冲区中的数据一并发送,通过readv函数可以由多个缓冲区分别接收,因此适当使用这个两个函数可减少IO函数的调用次数。
#include <sys/uio.h> ssize_t writev(int filedes, const struct iovec* iov, int iovcnt);
filedes: 表示数据传输对象的套接字文件描述符,但是该函数并不只限于套接字,因此可以向read函数一样向其传输文件或标准输出描述符。
返回值:失败时返回-1,成功时返回发送的字节数
iov: iovec结构体数组的地址值,结构体iovec中包含待发送数据的位置和大小信息
iovcnt: iovec结构体数组的长度
iovec结构体的声明如下所示:
struct iovec { void* iov_base; // 缓冲地址 size_t iovlen; // 缓冲大小 }
示例代码如下:
#include <stdio.h> #include <sys/uio.h> int main(int argc, char* argv[]) { struct iovec vec[2]; char buf1[] = "sdkjfcdc"; char buf2[] = "4613241356"; vec[0].iov_base = buf1; vec[0].iov_len = 3; vec[1].iov_base = buf2; vec[1].iov_len = 5; int lengtn = writev(1, vec, 2); puts(""); // 输出一个换行 printf("write length is: %d", lengtn); return 0; }
输出结果:
readv函数的定义:
#include <sys/uio.h> ssize_t readv(int filedes, const struct iovec *iov, int iovcnt); // 返回值:失败时返回-1,成功时返回接受到的总字节数 filedes:传递接收数据的文件(套接字)描述符
示例代码:
#include <stdio.h> #include <sys/uio.h> #define BUFF_SIZE 100 int main(int argc, char* argv[]) { struct iovec vec[2]; char buf1[BUFF_SIZE] = {0}; char buf2[BUFF_SIZE] = {0}; vec[0].iov_base = buf1; vec[0].iov_len = 5; vec[1].iov_base = buf2; vec[1].iov_len = BUFF_SIZE; int lengtn = readv(0, vec, 2); puts(""); // 输出一个换行 printf("read length is: %d\n", lengtn); printf("First message is : %s\n", vec[0].iov_base); printf("Second message is : %s\n", vec[1].iov_base); printf(""); return 0; }
输出结果:
使用场景:需要传输的数据位于不同的缓冲区时,可以通过1次writev函数调用替代多次write函数调用,提高效率。同样,需要将输入缓冲区中的数据读入不同的位置时,也可以利用1次readv函数进行读取,从而提高效率。仅从C语言的角度来讲,减少函数的调用次数,也能相应的提高性能,但其更大的意义在于减少了数据包的个数,因此writev函数在不在Nagle算法时更有价值。
如果将不同位置数据按照发送的顺序,移动到一个大数组,再调用write一次发送,达到的效果与writev函数相同,但是很明显writev函数更为方便~
---------------------------------------end-------------------------------------------------
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET10 - 预览版1新功能体验(一)