0%

socket数据读写

socket数据读写

学习《Linux高性能服务器编程》第五章Linux网络编程基础API,为了印象深刻一些,多动手多实践,所以记下这个笔记。这一篇主要记录Linux中socket数据读写的部分,包括TCP数据读写、UDP数据读写和通用数据读写。

TCP数据读写

对文件的读写操作readwrite同样适用于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上的数据,buflen参数分别指定读缓冲区的位置和大小。

recv成功读取时返回实际读取到的数据长度,它可能小于我们期望的长度len。因此需要多次调用recv才能读取到完整的数据。recv返回0,意味着对方已经关闭连接。recv出错时返回-1并设置errno

send发送sockfd上的数据。buflen参数分别指定写缓冲区的位置和大小。

send成功读取时返回实际读取到的数据长度,出错时返回-1并设置errno

flags用于控制数据的接收和发送,一般来说设置为0,也可以进行设置,从而进行控制。

控制参数可以通过man手册进行查看,这里直接截取书上的表格

image-20220812171929139

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系统提供的是读写函数是recvfromsendto,其中函数recvfromsendto前4个参数和recvsend意义相同,最后两个是发送端/接收端的地址。因为UDP是没有连接的概念,所以调用这两个函数的时候都要指定地址。recvfromsendto的返回值和recvsend也相同,所以不用过多介绍。

除此之外,recvfromsendto也可以用于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);

recvmsgsendmsg的参数中sockfdflags比较简单,复杂一些的参数就是msgmsg的结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <sys/socket.h>:

struct iovec { /* Scatter/gather array items */
void *iov_base; /* Starting address */
size_t iov_len; /* Number of bytes to transfer */
};

struct msghdr {
void *msg_name; /* Optional address */
socklen_t msg_namelen; /* Size of address */
struct iovec *msg_iov; /* Scatter/gather array */
size_t msg_iovlen; /* # elements in msg_iov */
void *msg_control; /* Ancillary data, see below */
size_t msg_controllen; /* Ancillary data buffer len */
int msg_flags; /* Flags on received message */
};

msg_name指向socket地址,对于TCP协议无意义,所以在TCP协议中设置为NULL,而对于UDP等其他协议就说明了发送或者接收的地址。msg_namelen指定socket地址的长度。

msg_ioviovec类型的指针,根据注释来判断应该是个数组。iovec结构体封装了一块内存的起始位置和长度。msg_iovlen指定这样的iovec结构对象有多少个。

对于recvmsg而言,数据将被读取并存放在msg_iovlen块分散的内存中,这些内存的位置和长度则由msg_iov指向的数组指定,这称为分散读( scatter read);对于sendmsg而言,msg_iovlen块分散内存中的数据将被一并发送,这称为集中写( gather write)。

为什么要有分散读和集中写呢,这其实是一个非常方便的使用,方便传输结构不同的数据。比如:发送http应答时,我们可以把前面的请求头请求的文件分为两个buffer,但是最终一起进行写入,减少了拼接带来的麻烦。同理我接收的时候也是想请求头请求的文件分开,所以使用分散读。

msg_flags成员无须设定,它会复制recvmsg/sendmsgflags参数的内容以影响数据读写过程。recvmsg还会在调用结束前,将某些更新后的标志设置到msg_flags中。

recvmsg/sendmsgflags参数以及返回值的含义均与sendrecvflags参数及返回值相同。

msg_controlmsg_controllen成员用于辅助数据的传送。目前书中并未进行讲解,后续再补充。

recvmsgsendmsg的例子:

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));
// 因为是针对TCP,所以msg_name无意义
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;
}

运行结果:

image-20220813123219330

需要注意的是recvmsg只有在前面的buffer使用完之后,才会使用后面的buffer。这也是为啥把buffer1的大小设置为6