阻塞和非阻塞套接字
套接字的默认状态是阻塞的,可能阻塞的套接字调用可分为以下四类:
- 输入操作,包括read、readv、recv、recvfrom和recvmsg共5个函数。
- 阻塞的TCP套接字:如果该套接字接收缓冲区中没有数据可读,进程将被投入睡眠,直到有一些数据(单个字节或一个TCP分节等)到达。
- 阻塞的UDP套接字:如果该套接字接收缓冲区为空,对它调用输入函数的进程将被投入睡眠,直到有UDP数据报到达。
- 非阻塞套接字:上述TCP和UDP输入操作不会被投入睡眠,而是立即返回一个
EWOULDBLOCK
错误。
- 输出操作,包括write、writev、send、sendto和sendmsg共5个函数。
- 阻塞的TCP套接字:如果其发送缓冲区中没有空间,进程将被投入睡眠,直到发送缓冲区有一定空间并且进程请求的数据已全部复制到发送缓冲区后才返回。
- 非阻塞TCP套接字:如果其发送缓冲区中没有空间,输出函数调用立即返回一个
EWOULDBLOCK
错误。如果其发送缓冲区中有一些空间,返回值将是内核能够复制到该缓冲区的字节数。 - 阻塞的UDP套接字:由于UDP不存在真正的发送缓冲区,输出操作不会因TCP一样的原因而阻塞,但仍可能因其他原因阻塞。
- 接收外来连接,即accept函数。
- 阻塞的TCP套接字:如果尚无新的连接到达,调用进程将被投入睡眠。
- 非阻塞TCP套接字:如果尚无新的连接到达,accept调用将立即返回一个
EWOULDBLOCK
错误。
- 发起外出连接,即connect函数。
- 阻塞的TCP套接字:调用进程至少花费一个到服务器的RTT时间。
- 非阻塞TCP套接字:如果连接不能立即建立,会返回一个
EINPROGRESS
错误,但是能正常发起TCP的三路握手过程。
read和write
#include <unistd.h>
ssize_t read(int fildes, void *buff, size_t nbyte);
ssize_t write(int fildes, const void *buff, size_t nbyte);
字节流套接字(TCP套接字)上调用read或write输入或输出的字节数可能比请求的数量少,这个现象的原因在于内核中用于套接字的缓冲区可能已达到了极限。这个现象在read一个字节流套接字时很常见,但是在write一个字节流套接字时只能在该套接字为非阻塞的前提下才出现。
recv和send
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buff, size_t nbyte, int flags);
ssize_t send(int sockfd, const void *buff, size_t nbyte, int flags);
recv和send的前三个参数等同于read和write。flag参数的值或为0,或为下图:
另外注意,flags参数是按值传递的,因此它只能用于从进程向内核传递标志,而内核无法向进程传回标志。
readv和writev
#include <sys/uio.h>
ssize_t readv(int filedes, const struct iovec *iov, int iovcnt);
ssize_t writev(int filedes, const struct iovec *iov, int iovcnt);
struct iovec { /* Scatter/gather array items */
void *iov_base; /* Starting address */
size_t iov_len; /* Number of bytes to transfer */
};
这两个函数类似read和write,不过readv和writev允许单个系统调用读入到或写出自一个或多个缓冲区。iov参数是指向某个iovec结构数组的指针,iovec结构数组中元素的数目存在某个限制,POSIX要求在头文件<sys/uio.h>
中定义IOV_MAX
常值,而且其值至少为16。另外,writev是一个原子操作,由于一个4字节的write跟一个396字节的write可能触发Nagle算法,此时首选办法之一是针对这两个缓冲区调用writev。
recvfrom和sendto
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buff, size_t nbytes, int flags, struct sockaddr *from, socklen_t *addrlen);
ssize_t sendto(int sockfd, const void *buff, size_t nbytes, int flags, const struct sockaddr *to, socklen_t *addrlen);
前三个参数:等同于read和write函数,而且sendto长度为0的数据报或者recvfrom返回0值都是可行的。
flag
参数:与recv/send类型函数相关。
sendto
的后两个参数:指向一个含有数据报接收者的协议地址(类似于connect)。
recvfrom
的后两个参数:指向一个数据报发送者的协议地址(类似于accept)。
返回值:所接收数据报中的用户数据量大小。
recvmsg和sendmsg
#include <sys/socket.h>
ssize_t recvmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
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 */
};
struct cmsghdr {
socklen_t cmsg_len; /* data byte count, including hdr */
int cmsg_level; /* originating protocol */
int cmsg_type; /* protocol-specific type */
/* followed by
unsigned char cmsg_data[]; */
};
这两个函数是最通用的I/O函数,实际上可把所有read、readv、recv和recvfrom调用替换成recvmsg调用。类似地,替换为sendmsg调用。这两个函数参数说明如下:
- msg参数
msg_name
和msg_namelen
用于套接字未连接的场合(如未连接UDP),对于sendmsg调用,msg_name
存放接收者的协议地址,msg_namelen
是一个值参数;对于recvmsg调用,msg_name
存放发送者的协议地址,msg_namelen
是一个值-结果参数。如果无需指明协议地址,msg_name
应置为空指针。msg_iov
和msg_iovlen
类似于readv或writev的第二个和第三个参数。msg_control
和msg_controllen
指定辅助数据的位置和大小。msg_control
成员指向的缓冲区被填以一个cmsghdr
结构,其中msg_controllen
对于recvmsg是一个值-结果参数。msg_flags
成员只有recvmsg才能使用,而sendmsg则忽略msg_flags
成员。recvmsg被调用时,flags
参数被复制到msg_flags
成员,内核还依据recvmsg的结果更新msg_flags
成员的值。
- flags参数可设标志如下: