Socket 是网络协议栈暴露给编程人员的 API,相比复杂的计算机网络协议,API 对关键操作和配置数据进行了抽象,简化了程序编程。
本文讲述的 socket 内容源自 Linux man。本文主要对各 API 进行详细介绍,从而更好的理解 socket 编程。
recv() 遵循 POSIX.1 - 2008
标准 c 库,libc, -lc
- ssize_t recv(int sockfd, void buf[.len], size_t len,
- int flags);
-
- ssize_t recvfrom(int sockfd, void buf[restrict .len], size_t len,
- int flags,
- struct sockaddr *_Nullable restrict src_addr,
- socklen_t *_Nullable restrict addrlen);
-
- ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
recv()、recvfrom()、recvmsg() 调用用来从套接字接收消息,它们都可以用在连接或非连接套接字上。我们首先描述几个系统调用共同的特性,然后在介绍它们的差别。
recv() 和 read(2) 的唯一区别就是是否有 flags 标记,当 recv() 的标记为 0 时,它基本上等同于 read(2)(具体参见注意部分)。同样的,下面调用:
recv(sockfd, buf, len, flags);
等同于
recvfrom(sockfd, buf, len, flags, NULL, NULL);
三个调用都在成功时返回消息的长度,如果提供的 buffer 盛不下消息,那么超出的消息可能会被遗弃,这要取决于接收套接字的类型。
如果套接字上没有消息,那么接收调用会一直等到有消息到来,除非套接字是非阻塞的(参考 fcntl(2) ),这时会返回 -1 并将 errno 设置为 EAGAIN 或者 EWOULDBLOCK。接收调用正常情况只要有可用数据就会返回,接近请求的数据量,而不是一直等到接收到所有的请求数据量。
应用程序可以使用 select(2)、poll(2)、epoll(7) 来决定套接字上有更多数据发生的时机。
flags 参数
flags 是下面值的位或值:
MSG_CMSG_CLOEXEC (只有 recvmsg() 可用)
设置接收文件描述符的异常关闭标记,通过 UNIX 域文件描述符的 SCM_RIGHTS 操作实现。这个标记的用途和 O_CLOEXEC 类似。
MSG_DONTWAIT
使能非阻塞操作,如果操作要阻塞,那么调用会报出 EAGAIN、EWOULDBLOCK 错误。这个和设置 O_NONBLOCK 标记类似(通过 fcntl(2) F_SETFL 操作),不过 MSG_DONTWAIT 只对本次调用管用,而 O_NONBLOCK 是设置到了文件描述符上,这样就会影响所有调用进程的所有线程以及其他持有该套接字句柄的进程。
MSG_ERRQUEQUE
这个标记指定了排队错误应该被套接字错误队列接收,错误信息会以依赖具体协议的类型来传递(对于 IPv4 是 IP_RECVERR)。用户应该提供足够的 buffer 大小。导致错误的原始报文的载荷大小以正常数据的 msg_iovec 格式传递,导致错误的原始报文的地址以 msg_name 形式提供。
错误以 sock_extended_err 结构提供:
- #define SO_EE_ORIGIN_NONE 0
- #define SO_EE_ORIGIN_LOCAL 1
- #define SO_EE_ORIGIN_ICMP 2
- #define SO_EE_ORIGIN_ICMP6 3
-
- struct sock_extended_err
- {
- uint32_t ee_errno; /* Error number */
- uint8_t ee_origin; /* Where the error originated */
- uint8_t ee_type; /* Type */
- uint8_t ee_code; /* Code */
- uint8_t ee_pad; /* Padding */
- uint32_t ee_info; /* Additional information */
- uint32_t ee_data; /* Other data */
- /* More data may follow */
- };
-
- struct sockaddr *SO_EE_OFFENDER(struct sock_extended_err *);
ee_errno 包含了排队错误的 errno 值,ee_origin 是错误发源地的代码,其他域都是协议相关的。宏 SO_EE_OFFENDER 作为辅助信息返回错误发生点的网络对象地址。如果地址未知,那么 sockaddr 中的 sa_family 会包含 AF_UNSPEC,其他域为未知值。导致错误的报文的载荷以正常数据传递。
对于本地错误,不传递地址(可以通过 cmsghdr 的 cmsg_len 值来确认)。 收到错误时,msghdr 会设置 MSG_ERRQUEQUE 标记。错误传递后,套接字错误码会根据下一个队列错误重新生成,在下一个套接字操作发生时传递。
MSG_OOB
这个标记请求接收通常不会在正常数据量中接到的带外数据。一些协议会将加速数据放在正常数据队列的前面,这就会导致这个标记没办法在这些协议中使用。
MSG_PEEK
这个标记指定从接收队列的头部接收数据,并且不会将数据从队列中移除。因此,下一次接收调用会返回相同的值。
MSG_TRUNC
对于原始协议(AF_PACKET),Internet datagram、netlink、UNIX datagram、sequenced-packet 套接字会返回实际的分组或报文长度,即使它比提供的 buffer 大。
对于网络流套接字,参考 tcp(7)。
MSG_WAITALL
这个标记请求操作一直等到请求大小完全满足为止。然后,当信号、错误、连接断开、后面接收数据和之前的数据类型不同等发生时,调用仍然可能返回少于请求大小的数据。
recvfrom()
recvfrom() 将收到的消息放到缓冲区 buf 中,调用者必须通过 len 参数指定 buf 的大小。
如果 src_addr 不是 NULL,底层协议提供了消息的原地址,那么原地址会填到 src_addr 中,这种情况下 addrlen 是一个输入输出参数。调用前,它应该被初始化为 src_addr 缓冲区的大小,返回时会有原地址的实际大小更新。如果提供的缓冲器太小,那么返回地址就会被截断,这种情况下,addrlen 的值就会比提供的值大。
如果调用者对原地址不感兴趣,那么 src_addr 和 addrlen 都应该被设置为 NULL。
recv()
recv() 通常只能用于连接的套接字(参考 connect(2)),它相当于下面的调用:
recvfrom(fd, buf, len, flags, NULL, 0);
recvmsg()
recvmsg() 调用使用 msghdr 结构来减少需要传递参数的个数,结构体在
- 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 域指向用户分配的缓冲区,用来存放非连接套接字的源地址,调用者应该通过 msg_namelen 来设置缓冲器的大小,一旦成功返回,msg_namelen 会被设置为源地址的实际大小。如果应用不关心源地址,那么 msg_name 可以设置为 NULL。
msg_iov 和 msg_iovlen 描述 scatter-gather 方式的区域(就是一些类分散的缓冲区列表),在 readv(2) 中有讨论。
msg_control 域具有 msg_controllen 长度,是一个为其他协议控制消息或者各种辅助数据准备的缓冲区。当 recvmsg() 调用时,msg_controllen 应指定 msg_control 缓冲区可用大小,一旦成功返回,它将包含控制消息序列的大小。消息格式如下:
- struct cmsghdr {
- size_t cmsg_len; /* Data byte count, including header
- (type is socklen_t in POSIX) */
- int cmsg_level; /* Originating protocol */
- int cmsg_type; /* Protocol-specific type */
- /* followed by
- unsigned char cmsg_data[]; */
- };
辅助数据应该只能被 cmsg(3) 中定义的宏来访问。
作为例子,Linux 使用这个辅助数据机制在 UNIX 域套接字上传递扩展错误、IP 选项、文件描述符,参考 unix(7) 和 ip(7)。
msghdr 中的 msg_fags 域会在 recvmsg() 返回时更新,它可能包含以下一些标记:
MSG_EOR
指示记录结束,记录中所有数据都已返回(通常用在 SOCK_SEQPACKET 中)。
MSG_TRUNC
指示数据报文的结尾部分因为大于提供的缓冲区大小而被丢弃。
MSG_OOB
指示有带外或者加速数据到达
调用会返回接收到数据的字节数。
发生错误时,返回 -1,并设置errno 来指示错误类型。
当流套接字对端自己关闭了,那么将返回 0(传统意义的 EOF 返回)。
各个域中的数据报文套接字允许 0 长度报文,当这样的报文收到时,返回的值就是 0。
在流套接字请求接收 0 个字节时,返回值也可能是 0。
错误值定义如下:
EAGAIN/EWOULDBLOCK | 如果套接字被标记为非阻塞并且接收操作打算阻塞,或者设置了超时值,在数据到达前发生了超时。POSIX.1 允许使用两个错误值的任何一个,也不假设两个值相等,这就需要应用检查对两个错误都进行检查。 |
EBADF | sockfd 参数是一个非法的文件描述符 |
ECONNREFUSED | 远程主机拒绝网络连接(通常是没有运行请求的服务) |
EFAULT | 接收缓冲区指针指向进程外地址 |
EINTR | 接收操作在数据来临前被传递来的信号打断 |
EINVAL | 参数不合法 |
ENOMEM | 无法申请 recvmsg() 的内存 |
ENOTCONN | 套接字是一个面向连接的套接字,但是没有连接(参考 connect(2) 和 accept(2)) |
ENOTSOCK | 文件描述符不是一个套接字 |
如果有 0 长度报文处于等待,那么 read(2) 和 标记为 0 的recv() 的处理行为是不同的。read(2) 没有任何影响(报文还在等待中),而 recv() 会消耗掉报文。
参考 recvmmsg(2) 来看 Linux 系统特定的系统调用来在一次调用中处理多个报文。
- Server program
-
- #include
- #include
- #include
- #include
- #include
- #include
- #include
-
- #define BUF_SIZE 500
-
- int
- main(int argc, char *argv[])
- {
- int sfd, s;
- char buf[BUF_SIZE];
- ssize_t nread;
- socklen_t peer_addrlen;
- struct addrinfo hints;
- struct addrinfo *result, *rp;
- struct sockaddr_storage peer_addr;
-
- if (argc != 2) {
- fprintf(stderr, "Usage: %s port\n", argv[0]);
- exit(EXIT_FAILURE);
- }
-
- memset(&hints, 0, sizeof(hints));
- hints.ai_family = AF_UNSPEC; /* Allow IPv4 or IPv6 */
- hints.ai_socktype = SOCK_DGRAM; /* Datagram socket */
- hints.ai_flags = AI_PASSIVE; /* For wildcard IP address */
- hints.ai_protocol = 0; /* Any protocol */
- hints.ai_canonname = NULL;
- hints.ai_addr = NULL;
- hints.ai_next = NULL;
-
- s = getaddrinfo(NULL, argv[1], &hints, &result);
- if (s != 0) {
- fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(s));
- exit(EXIT_FAILURE);
- }
-
- /* getaddrinfo() returns a list of address structures.
- Try each address until we successfully bind(2).
- If socket(2) (or bind(2)) fails, we (close the socket
- and) try the next address. */
-
- for (rp = result; rp != NULL; rp = rp->ai_next) {
- sfd = socket(rp->ai_family, rp->ai_socktype,
- rp->ai_protocol);
- if (sfd == -1)
- continue;
-
- if (bind(sfd, rp->ai_addr, rp->ai_addrlen) == 0)
- break; /* Success */
-
- close(sfd);
- }
-
- freeaddrinfo(result); /* No longer needed */
-
- if (rp == NULL) { /* No address succeeded */
- fprintf(stderr, "Could not bind\n");
- exit(EXIT_FAILURE);
- }
-
- /* Read datagrams and echo them back to sender. */
-
- for (;;) {
- char host[NI_MAXHOST], service[NI_MAXSERV];
-
- peer_addrlen = sizeof(peer_addr);
- nread = recvfrom(sfd, buf, BUF_SIZE, 0,
- (struct sockaddr *) &peer_addr, &peer_addrlen);
- if (nread == -1)
- continue; /* Ignore failed request */
-
- s = getnameinfo((struct sockaddr *) &peer_addr,
- peer_addrlen, host, NI_MAXHOST,
- service, NI_MAXSERV, NI_NUMERICSERV);
- if (s == 0)
- printf("Received %zd bytes from %s:%s\n",
- nread, host, service);
- else
- fprintf(stderr, "getnameinfo: %s\n", gai_strerror(s));
-
- if (sendto(sfd, buf, nread, 0, (struct sockaddr *) &peer_addr,
- peer_addrlen) != nread)
- {
- fprintf(stderr, "Error sending response\n");
- }
- }
- }
- Client program
-
- #include
- #include
- #include
- #include
- #include
- #include
- #include
-
- #define BUF_SIZE 500
-
- int
- main(int argc, char *argv[])
- {
- int sfd, s;
- char buf[BUF_SIZE];
- size_t len;
- ssize_t nread;
- struct addrinfo hints;
- struct addrinfo *result, *rp;
-
- if (argc < 3) {
- fprintf(stderr, "Usage: %s host port msg...\n", argv[0]);
- exit(EXIT_FAILURE);
- }
-
- /* Obtain address(es) matching host/port. */
-
- memset(&hints, 0, sizeof(hints));
- hints.ai_family = AF_UNSPEC; /* Allow IPv4 or IPv6 */
- hints.ai_socktype = SOCK_DGRAM; /* Datagram socket */
- hints.ai_flags = 0;
- hints.ai_protocol = 0; /* Any protocol */
-
- s = getaddrinfo(argv[1], argv[2], &hints, &result);
- if (s != 0) {
- fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(s));
- exit(EXIT_FAILURE);
- }
-
- /* getaddrinfo() returns a list of address structures.
- Try each address until we successfully connect(2).
- If socket(2) (or connect(2)) fails, we (close the socket
- and) try the next address. */
-
- for (rp = result; rp != NULL; rp = rp->ai_next) {
- sfd = socket(rp->ai_family, rp->ai_socktype,
- rp->ai_protocol);
- if (sfd == -1)
- continue;
-
- if (connect(sfd, rp->ai_addr, rp->ai_addrlen) != -1)
- break; /* Success */
-
- close(sfd);
- }
-
- freeaddrinfo(result); /* No longer needed */
-
- if (rp == NULL) { /* No address succeeded */
- fprintf(stderr, "Could not connect\n");
- exit(EXIT_FAILURE);
- }
-
- /* Send remaining command-line arguments as separate
- datagrams, and read responses from server. */
-
- for (size_t j = 3; j < argc; j++) {
- len = strlen(argv[j]) + 1;
- /* +1 for terminating null byte */
-
- if (len > BUF_SIZE) {
- fprintf(stderr,
- "Ignoring long message in argument %zu\n", j);
- continue;
- }
-
- if (write(sfd, argv[j], len) != len) {
- fprintf(stderr, "partial/failed write\n");
- exit(EXIT_FAILURE);
- }
-
- nread = read(sfd, buf, BUF_SIZE);
- if (nread == -1) {
- perror("read");
- exit(EXIT_FAILURE);
- }
-
- printf("Received %zd bytes: %s\n", nread, buf);
- }
-
- exit(EXIT_SUCCESS);
- }