socket数据读写
学习《Linux高性能服务器编程》第五章Linux网络编程基础API,为了印象深刻一些,多动手多实践,所以记下这个笔记。这一篇主要记录Linux中socket数据读写的部分,包括TCP数据读写、UDP数据读写和通用数据读写。
TCP数据读写
对文件的读写操作read
和write
同样适用于socket。但是socket编程接口提供了几个专门用于socket数据读写的系统调用,它们增加了对数据的读写的控制。在TCP中流数据读写的系统调用是:
1 2 3 4 5
| #include <sys/types.h> #include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags); ssize_t send(int sockfd, const void *buf, size_t len, int flags);
|
recv
读取sockfd
上的数据,buf
和len
参数分别指定读缓冲区的位置和大小。
recv
成功读取时返回实际读取到的数据长度,它可能小于我们期望的长度len
。因此需要多次调用recv
才能读取到完整的数据。recv
返回0,意味着对方已经关闭连接。recv
出错时返回-1并设置errno
。
send
发送sockfd
上的数据。buf
和len
参数分别指定写缓冲区的位置和大小。
send
成功读取时返回实际读取到的数据长度,出错时返回-1并设置errno
。
flags
用于控制数据的接收和发送,一般来说设置为0,也可以进行设置,从而进行控制。
控制参数可以通过man手册进行查看,这里直接截取书上的表格
UDP数据读写
1 2 3 4 5
| #include <sys/types.h> #include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
|
针对UDP系统提供的是读写函数是recvfrom
和sendto
,其中函数recvfrom
和sendto
前4个参数和recv
、send
意义相同,最后两个是发送端/接收端的地址。因为UDP是没有连接的概念,所以调用这两个函数的时候都要指定地址。recvfrom
和sendto
的返回值和recv
、send
也相同,所以不用过多介绍。
除此之外,recvfrom
和sendto
也可以用于TCP使用,只需要把最后两个参数设置为NULL即可。
通用数据读写函数
1 2 3 4 5
| #include <sys/types.h> #include <sys/socket.h>
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags); ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
|
recvmsg
和sendmsg
的参数中sockfd
和flags
比较简单,复杂一些的参数就是msg
。msg
的结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| #include <sys/socket.h>:
struct iovec { void *iov_base; size_t iov_len; };
struct msghdr { void *msg_name; socklen_t msg_namelen; struct iovec *msg_iov; size_t msg_iovlen; void *msg_control; size_t msg_controllen; int msg_flags; };
|
msg_name
指向socket地址,对于TCP协议无意义,所以在TCP协议中设置为NULL,而对于UDP等其他协议就说明了发送或者接收的地址。msg_namelen
指定socket地址的长度。
msg_iov
是iovec
类型的指针,根据注释来判断应该是个数组。iovec
结构体封装了一块内存的起始位置和长度。msg_iovlen
指定这样的iovec
结构对象有多少个。
对于recvmsg
而言,数据将被读取并存放在msg_iovlen
块分散的内存中,这些内存的位置和长度则由msg_iov
指向的数组指定,这称为分散读( scatter read);对于sendmsg
而言,msg_iovlen
块分散内存中的数据将被一并发送,这称为集中写( gather write)。
为什么要有分散读和集中写呢,这其实是一个非常方便的使用,方便传输结构不同的数据。比如:发送http应答时,我们可以把前面的请求头和请求的文件分为两个buffer,但是最终一起进行写入,减少了拼接带来的麻烦。同理我接收的时候也是想请求头和请求的文件分开,所以使用分散读。
msg_flags
成员无须设定,它会复制recvmsg/sendmsg
的flags
参数的内容以影响数据读写过程。recvmsg
还会在调用结束前,将某些更新后的标志设置到msg_flags
中。
recvmsg/sendmsg
的 flags
参数以及返回值的含义均与sendrecv
的 flags
参数及返回值相同。
msg_control
和 msg_controllen
成员用于辅助数据的传送。目前书中并未进行讲解,后续再补充。
recvmsg
和sendmsg
的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
| #include <arpa/inet.h> #include <cstdio> #include <string.h> #include <cstdlib> #include <assert.h> #include <errno.h> #include <unistd.h>
#define BUFFER_SIZE 256 int main(int argc, char const *argv[]) { if (argc <= 2) { printf("usage: %s ip_address port_number\n", basename(argv[0])); return 1; } const char *ip = argv[1]; int port = atoi(argv[2]);
sockaddr_in address; bzero(&address, sizeof(address)); address.sin_family = AF_INET; inet_pton(AF_INET, ip, &address.sin_addr); address.sin_port = htons(port);
int sock = socket(AF_INET, SOCK_STREAM, 0); assert(sock >= 0);
int ret = bind(sock, (sockaddr *)&address, sizeof(address)); assert(ret != -1);
ret = listen(sock, 5); assert(ret != -1);
struct sockaddr_in client; socklen_t client_addrlength = sizeof(client); int connfd = accept(sock, (struct sockaddr *)&client, &client_addrlength);
if (connfd < 0) { printf("errno is: %d\n", errno); } else { char remote[INET_ADDRSTRLEN]; printf("connected with ip: %s and port: %d\n", inet_ntop(AF_INET, &client.sin_addr, remote, INET_ADDRSTRLEN), ntohs(client.sin_port)); char buffer1[6]; char buffer2[BUFFER_SIZE]; struct msghdr msg; bzero(&msg, sizeof(msg)); struct iovec iovec_arry[2]; iovec_arry[0].iov_base = (void *)buffer1; iovec_arry[0].iov_len = sizeof(buffer1); iovec_arry[1].iov_base = (void *)buffer2; iovec_arry[1].iov_len = sizeof(buffer2);
msg.msg_iov = iovec_arry; msg.msg_iovlen = 2;
int n = recvmsg(connfd, &msg, 0); assert(n != -1); printf(" have recv %d byte msg1 %s and msg2 %s \n", n, buffer1, buffer2); close(connfd); } close(sock); return 0; }
|
sendmsg
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| #include <arpa/inet.h> #include <cstdio> #include <string.h> #include <cstdlib> #include <assert.h> #include <errno.h> #include <unistd.h>
int main(int argc, char const *argv[]) { if (argc <= 2) { printf("usage: %s ip_address port_number\n", basename(argv[0])); return 1; } const char *ip = argv[1]; int port = atoi(argv[2]);
sockaddr_in address; bzero(&address, sizeof(address)); address.sin_family = AF_INET; inet_pton(AF_INET, ip, &address.sin_addr); address.sin_port = htons(port);
int sock = socket(AF_INET, SOCK_STREAM, 0); assert(sock >= 0);
int ret = connect(sock, (struct sockaddr *)&address, sizeof(address)); assert(ret == 0);
char buffer1[] = "hello"; char buffer2[] = "world";
struct msghdr msg; bzero(&msg, sizeof(msg)); msg.msg_name = NULL; msg.msg_namelen = 0;
struct iovec iovec_arry[2]; iovec_arry[0].iov_base = (void *)buffer1; iovec_arry[0].iov_len = sizeof(buffer1); iovec_arry[1].iov_base = (void *)buffer2; iovec_arry[1].iov_len = sizeof(buffer2);
msg.msg_iov = iovec_arry; msg.msg_iovlen = 2;
int n = sendmsg(sock, &msg, 0); assert(n != -1); printf(" have send %d byte msg1 %s and msg2 %s \n", n, buffer1, buffer2); close(sock); return 0; }
|
运行结果:
需要注意的是recvmsg
只有在前面的buffer使用完之后,才会使用后面的buffer。这也是为啥把buffer1
的大小设置为6