在Linux之socket编程:网络编程基础中介绍了socket编程中的一些框架性函数。可以保证网络数据能够正常地到达用户,这篇博客主要讲解网络通信数据的交互,即网络数据的收发以及IO模型。
网络通信中使用的IO函数
网络通信中使用的IO函数主要有:recv()/send()、readv()/writev()、recvmsg()/sendmsg()、read()/write()、recvfrom()/sendto()。其中read/write是所有文件的通用操作接口这里就不讲解了,recvfrom/sendto在上一篇中讲解了,这里也不过多的介绍。
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
#include <sys/uio.h>
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
其中接收和发送的流程都是一致的,他们的区别主要在于一次发送数据的数量以及是否可以选择对方的信息(地址结构信息,如IP、port等)。区别如下:
- read/write和readv/writev可以对所有的文件描述符使用,recvfrom/sendto、recv/send、recvmsg/sendmsg只能操作套接字描述符。
- readv/writev和recvmsg/sendmsg可以操作多个缓冲区,read/write、recv/send、recvfrom/sendto只能操作单个缓冲区
- recvfrom/sendto、recvmsg/sendmsg可以选择对方的IP地址
- recvmsg/sendmsg有可选择的控制信息,能进行高级操作
这些函数中flags的值有如下,可以是单独的一个值也可以是按位或生成的复合值
- MSG_DONTWAIT: 非阻塞IO操作,立刻返回,不等待。针对单次操作,并不修改文件的flag值。
- MSG_ERRQUEUE: 错误消息从套接字错误队列接收
- MSG_OOB: 接收带外数据
- MSG_PEEK: 查看数据,并不清除缓冲区的数据
- MSG_TRUNC: 返回所有数据,如缓冲区过小会导致数据丢失
- MSG_WAITALL: 等待所有消息,即缓冲区中数据长度达到指定的长度后才会返回。
其中接收操作返回值是成功接收到的字节数,返回-1是表示发生错误。当另外一方使用正常方式关闭连接的时候返回值为0。
接收函数和内核交互流程:当内核缓冲区中的数据比指定缓冲区小时,在没有设置MSG_WAITALL时会复制缓冲区中的所有数据到用户缓冲区,并返回数据的长度。当内核接收缓冲区中的数据比用户指定的多时,会将用户指定长度len的接收缓冲区中的数据复制到用户指定地址,其余的数据需要在下次调用接收函数的时候再复制。内核在复制用户指定的数据之后,会销毁已经复制完毕的数据,并进行调整。
发送数据数据时,返回-1表示发生错误,当另一方使用正常方式关闭连接的时候返回值为0,其余是返回成功发送的字节数。由于用户缓冲区buf中的数据在通过send函数发送的时候,并不一定能够全部发送出去,所以需要检查send()函数的返回值,来进行下一步操作(是否需要重发之前的数据)。当发送缓冲区已满时,调用发送函数会返回ENOBUFS错误类型。套接字关闭的时候会返回EPIPE错误类型
readv和writev 以及recvmsg和sendmsg的区别
这几个函数都可用于接收多个缓冲区数据。其中readv和writev直接在参数中指定缓冲区向量vector的个数,其中vector是一组vector向量的数组。其数据结构定义如下:
struct iovec {
void *iov_base; /* Starting address */
size_t iov_len; /* Number of bytes to transfer */
};
在使用中需要指定iovec的iov_base的长度,值存放在iov_len中。
而在recvmsg和sendmsg中的参数更为复杂,struct 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 (unused) */
};
其中msg_name表示源地址,即为指向一个struct sockaddr的指针,msg_iov和readv中的结构一样。msg_iovlen表示msg_iov缓存区的个数。
这里只是简单地介绍了其使用方法,如有更为详细的区分欢迎留言,readv/writev和recvmsg/sendmsg使用的场合各是什么?
IO模型
IO的方式有阻塞IO、非阻塞IO、IO复用、信号驱动、异步IO等。其中阻塞IO是最通用的IO类型,使用这个模型进行数据接收的时候,在数据没有到达之前程序会一直等待。而非阻塞IO,则对每次请求,内核都不会阻塞,无论是否有数据到达都会立即返回。
IO复用
使用IO复用模型可以在等待的时候加入超时时间,当超时时间没有到达的时候与阻塞的情况一致,而当超市时间到达仍然没有数据接收到,会返回不再等待。流程如下:
其中函数有select和poll函数。
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);
其中readfds、writefds、exceptfds分别为我们关心的是否可读、可写、异常的文件描述符集,不关心的可以设置为NULL。在Linux中fd_set是一个很大的字节数组,其中每个文件描述符都只占用一位(描述符0占用第0位…)。可以使用FD_CLR把文件描述符fd相应的位清零,使用FD_SET把文件描述符fd相对应的位置位,而使用FD_ZERO把文件描述符集中的所有位都清0,FD_ISSET用来检测文件描述符fd在文件描述符集中是否置位。nfds参数是这三个文件描述集中最大文件描述符的值加1,这个数其实是告诉内核我们所关心的描述符的范围,可以使用FD_SETSIZE(1024),但是这会浪费资源。最后一个参数便是超时时间的设置。
select函数的返回值有三种:返回-1,表示发生了错误,在监听的时候发生捕捉到了信号;返回0,表示超时;返回一个正数,表示有文件已经就绪了,这时需要使用FD_ISSET来判断是哪个文件准备好了。select函数返回时会把没有满足条件的文件描述符fd对应的位清零,所以再次使用时需要重新添加。
io复用中还有一个poll函数也可以实现相同的功能,其中poll不是为每个条件构造一个描述符集,而是构造一个pollfd结构的数组,每个成员指定一个描述符编号以及我们对该描述符感兴趣的条件。超时时间是以毫秒为单位。返回值的情况和select一致。第二个参数是pollfd的个数。
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
由于select和poll函数会受到信号的影响,所以可以使用pselect和ppoll这两个函数来屏蔽信号的影响,从而避免函数执行过程中受信号的干扰。
异步IO模型
前几种io模式我们过于被动,复用io也是需要等待一段时间(实质是轮询查看)。这里可以使用异步IO,在我们所关心的条件满足后,通过信号来通知。这样就不需要进行过多的等待了。原理如图:
在Linux中异步io信号是SIGIO和SIGURG的组合,其中SIGIO是通用异步IO信号,而SIGURG则只用来通知进程网络连接上的带外数据已经到达。
为了接收到异步IO信号,需要执行以下3步:
- 调用signal或sigaction为SIGIO信号建立信号处理程序。
- 以命令F_SETOWN掉用fcntl来设置进程ID或进程组ID,用于接收对于该信号。
- 以命令F_SETFL调用fcntl设置O_ASYNC文件状态标志
第3步仅能对指向终端或网络描述符执行。异步io有很大的限制:它们并不能用在所有的文件类型上,而且只能使用一个信号。如果要对多个描述符进行异步IO,那么在进程接收到该信号时并不知道这一信号对应于哪一个描述符。