文章目录
- 介绍常用的IO函数recv()/send()、read()/write()、recvmsg()/sendmsg(),并讲解函数的主要应用的场合;
- 用几个简单的例子,说明如何使用上述函数进行程序的设计;
- 介绍常用的几种IO模型,以图形式的方法形象地进行说明;
- 介绍select()和pselect()函数,如何使用这两个函数进行文件描述符读写条件的监视;
- 简单介绍函数poll()和ppoll()的含义,使用和区别;
- 以简单的例子介绍非阻塞编程的方法。
1. IO 函数
Linux操作系统中的IO函数主要有read()、write()、recv()、send()、recvmsg()、sendmsg()、readv()、writev()。本节将对上述的主要函数进行介绍,其中read()和write()函数在前面已经介绍过。
1.1 使用recv()函数接收数据
recv()函数用于接收数据,函数原型如下。recv()函数从套接字s中接收数据放到缓冲区buf中,buf的长度为len,操作的方式由flags指定。第一个参数s是套接口文件描述符,它是由系统调用socket()返回的。第2个参数buf是一个指针,指向接收网络数据的缓冲区。第3个参数len表示接收缓冲区的大小,以字节为单位。
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int s, void* buf, size_t len, int flags);
recv()函数的参数flags用于设置接收数据的方式,可选择的值及含义在表9.1中列出(flags的值可以是表中值的按位或生成的复合值)。例如,经常使用的MSG_DONTWAIT进行接收数据的时候,不进行等待,即使没有数据也立刻返回,即此刻的套接字是非阻塞操作。
recv()函数flags的值及含义
值 | 含义 |
---|---|
MSG_DONTWAIT | 非阻塞操作,立刻返回,不等待 |
MSG_ERRQUEUE | 错误消息从套接字错误队列接收 |
MSG__OOB | 接收外数据数据 |
MSG_PEEK | 查看数据,不进行数据缓冲区的清空 |
MSG_TRUNC | 返回所有的数据,即使指定的缓冲区过小 |
MSG_WAITALL | 等待所有消息 |
recv()的返回值是成功接收大的字节数,但返回值为-1时错误发生,可以查看errno获取错误码。recv()函数通常用与TCP类型的套接字,UDP使用recvfrom()函数接收数据,当然在数据报套接字绑定地址和端口后,也可以使用recv()函数接收数据。
1.2 使用send()函数发送数据
send()函数用于发送数据,函数原型如下:
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int s, const void* buf, size_t len, int flags);
send()函数将缓冲区buf中大小为len的数据,通过套接字文件描述符按照flags指定的方式发送出去。其中的参数含义与recv中的含义一致,它的返回值是成功发送的字节数。由于用户缓冲区buf中的数据在通过send()函数进行发送的时候,并不一定能够全部发送出去,所以要检查send()函数的返回值,按照与计划发送的字节长度len是否相等来判断如何进行下一步操作。
当send()函数的返回值小于len的时候,表明缓冲区中仍然有部分数据没有成功发送,这时需要重新发送剩余部分的数据。通常的剩余数据发送方法是对原来buf中的数据位置进行偏移,偏移的大小为已发送成功的字节数。
函数send()只能用于套接字处于连接状态的描述符,之前必须用connect()函数或者其他函数进行连接。对于send()函数和write()函数之间的差别是表示发送方式的flag,当flag为0时,send()函数和write()函数完全一致。而且send(s,buf,len,flags)与sendto(s,buf,len,flags,NULL,0)是等价的。
1.3 使用readv()函数接收数据
readv()函数可用于接收多个缓冲区数据,函数原型如下所示。readv()函数从套接字描述符s中读取count块数据放到缓冲区向量vector中。
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
其中,fd是文件描述符,iov是一个iovec结构体数组,iovcnt是数组中元素的个数。iovec结构体定义如下:
struct iovec {
void *iov_base; /* Starting address */
size_t iov_len; /* Number of bytes to transfer */
};
eadv()函数将从fd中读取iovec数组中所有缓冲区的数据,并将它们合并到一个缓冲区中。
如下是一个从多个缓冲区读取数据的例子:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/uio.h>
#include <unistd.h>
int main() {
char buf1[10], buf2[20], buf3[30];
struct iovec iov[3];
ssize_t nread;
iov[0].iov_base = buf1;
iov[0].iov_len = sizeof(buf1);
iov[1].iov_base = buf2;
iov[1].iov_len = sizeof(buf2);
iov[2].iov_base = buf3;
iov[2].iov_len = sizeof(buf3);
nread = readv(STDIN_FILENO, iov, 3);
if (nread == -1) {
perror("readv");
exit(EXIT_FAILURE);
}
printf("Read %ld bytes\n", (long) nread);
printf("buf1: %s\n", buf1);
printf("buf2: %s\n", buf2);
printf("buf3: %s\n", buf3);
exit(EXIT_SUCCESS);
}
1.4 使用writev()函数发送数据
writev()函数是用于发送数据的函数,它可以一次性发送多个缓冲区的数据。writev()函数的参数如下:
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
其中,fd是文件描述符,iov是一个iovec结构体数组,iovcnt是数组中元素的个数。iovec结构体定义如下:
struct iovec {
void *iov_base; /* Starting address */
size_t iov_len; /* Number of bytes to transfer */
};
writev()函数将把iovec数组中所有缓冲区的数据合并到一个缓冲区中,并将它们一次性发送出去。
在调用writev()函数的时候必须指定iovec的iov_base的长度,将值放到成员iov_len中。参数vector指向一块结构vector的内存,大小由count指定。
当我们需要向多个缓冲区中写入数据时,可以使用writev()函数。下面是一个使用writev()函数的例子:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/uio.h>
#include <unistd.h>
int main() {
char buf1[10] = "hello", buf2[20] = "world", buf3[30] = "!";
struct iovec iov[3];
ssize_t nwritten;
iov[0].iov_base = buf1;
iov[0].iov_len = strlen(buf1);
iov[1].iov_base = buf2;
iov[1].iov_len = strlen(buf2);
iov[2].iov_base = buf3;
iov[2].iov_len = strlen(buf3);
nwritten = writev(STDOUT_FILENO, iov, 3);
if (nwritten == -1) {
perror("writev");
exit(EXIT_FAILURE);
}
printf("Wrote %ld bytes\n", (long) nwritten);
exit(EXIT_SUCCESS);
}
1.5 使用recvmsg()函数接收数据
recvmsg()函数是用于接收数据的函数,它可以一次性接收多个缓冲区的数据。recvmsg()函数的参数如下:
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
其中,sockfd是文件描述符,msg是一个msghdr结构体,flags是标志位。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 */
};
recvmsg()函数将把多个缓冲区中的数据合并到一个缓冲区中,并将它们一次性接收出去。
- 成员msg_name表示源地址,即为一个指向struct sockaddr的指针,当套接字还没有连接的时候有效。
- 成员msg_namelen表示msg_name指向结构的长度。
- 成员msg_iov与函数readv()中的含义一致。
- 成员msg_iovlen表示msg_iov缓冲区的字节数。
- 成员msg_control指向缓冲区,根据msg_flags的值,会放入不同的值。
- 成员msg_controllen为msg_control指向缓冲区的大小。
- 成员msg_flags为操作的方式。
1.6 使用sendmsg()函数发送数据
函数sendmsg()可用于向多个缓冲区发送数据,函数原型如下所示。函数sendmsg()向套接字描述符s中按照结构msg的设定写入数据,其中操作方式由flags指定。
#include <sys/uio.h>
ssize_t sendmsg(int s, const struct msghdr* msg, int flags);
2. select()函数和pselect()函数
2.1 select()函数
select()函数是Linux中的一个系统调用,用于监视文件描述符的变化情况,包括读、写、异常等。它可以等待多个文件描述符中的任何一个变为“准备好”的状态,从而完成 I/O 操作。select()函数的语法如下:
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
其中,nfds 是需要监视的文件描述符数量,readfds、writefds 和 exceptfds 分别是需要监视的读、写和异常事件的文件描述符集合。timeout 是超时时间,如果在超时时间内没有任何事件发生,则 select() 函数返回 0。
select()函数返回值:
负值:select错误
正值:表示某些文件可读或可写
通常会和如下几个函数一起使用:
void FD_ZERO(fd_set *set);//清空一个文件描述符的集合
void FD_SET(int fd, fd_set *set);//将一个文件描述符添加到一个指定的文件描述符集合中
void FD_CLR(int fd, fd_set *set);//将一个指定的文件描述符从集合中清除;
int FD_ISSET(int fd, fd_set *set);//检查集合中指定的文件描述符是否可以读写
举个例子,下面的代码使用 select() 函数实现了一个简单的 TCP 服务器:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
int main(int argc, char *argv[]) {
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(8080);
if (bind(listen_fd, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
perror("bind");
exit(EXIT_FAILURE);
}
if (listen(listen_fd, 10) == -1) {
perror("listen");
exit(EXIT_FAILURE);
}
fd_set read_fds;
FD_ZERO(&read_fds);
FD_SET(listen_fd, &read_fds);
while (1) {
fd_set tmp_fds = read_fds;
if (select(listen_fd + 1, &tmp_fds, NULL, NULL, NULL) == -1) {
perror("select");
exit(EXIT_FAILURE);
}
for (int i = 0; i <= listen_fd; i++) {
if (FD_ISSET(i, &tmp_fds)) {
if (i == listen_fd) {
int conn_fd = accept(listen_fd, NULL, NULL);
if (conn_fd == -1) {
perror("accept");
exit(EXIT_FAILURE);
}
printf("accept connection %d\n", conn_fd);
FD_SET(conn_fd, &read_fds);
} else {
char buf[1024];
ssize_t n = recv(i, buf, sizeof(buf), 0);
if (n <= 0) {
printf("close connection %d\n", i);
close(i);
FD_CLR(i, &read_fds);
} else {
buf[n] = '\0';
printf("recv from connection %d: %s", i, buf);
}
}
}
}
}
return 0;
}
这个服务器使用 select() 函数监听了一个 TCP 端口,并在有新连接到来时接受连接。当有数据到来时,它会打印出数据内容。这个服务器可以同时处理多个连接,并且不会阻塞在任何一个连接上。
2.2 pselect()函数
pselect() 函数是 select() 函数的一个变种,它可以在等待期间阻塞指定的信号。pselect() 函数的语法如下:
#include <sys/select.h>
int pselect(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, const struct timespec *timeout, const sigset_t *sigmask);
其中,sigmask 是一个指向信号集的指针,用于阻塞指定的信号。timeout 是超时时间,如果在超时时间内没有任何事件发生,则 pselect() 函数返回 0。
pselect() 函数与 select() 函数的区别在于,它可以在等待期间阻塞指定的信号。这个特性可以用于实现更加复杂的 I/O 操作,例如同时等待多个文件描述符和多个信号。
举个例子,下面的代码使用 pselect() 函数实现了一个简单的 TCP 服务器:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
int main(int argc, char *argv[]) {
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(8080);
if (bind(listen_fd, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
perror("bind");
exit(EXIT_FAILURE);
}
if (listen(listen_fd, 10) == -1) {
perror("listen");
exit(EXIT_FAILURE);
}
fd_set read_fds;
FD_ZERO(&read_fds);
FD_SET(listen_fd, &read_fds);
sigset_t sigmask;
sigemptyset(&sigmask);
sigaddset(&sigmask, SIGINT);
while (1) {
fd_set tmp_fds = read_fds;
struct timespec timeout = { .tv_sec = 1 };
if (pselect(listen_fd + 1, &tmp_fds, NULL, NULL, &timeout, &sigmask) == -1) {
perror("pselect");
exit(EXIT_FAILURE);
}
for (int i = 0; i <= listen_fd; i++) {
if (FD_ISSET(i, &tmp_fds)) {
if (i == listen_fd) {
int conn_fd = accept(listen_fd, NULL, NULL);
if (conn_fd == -1) {
perror("accept");
exit(EXIT_FAILURE);
}
printf("accept connection %d\n", conn_fd);
FD_SET(conn_fd, &read_fds);
} else {
char buf[1024];
ssize_t n = recv(i, buf, sizeof(buf), 0);
if (n <= 0) {
printf("close connection %d\n", i);
close(i);
FD_CLR(i, &read_fds);
} else {
buf[n] = '\0';
printf("recv from connection %d: %s", i, buf);
}
}
}
}
}
return 0;
}
这个服务器使用 pselect() 函数监听了一个 TCP 端口,并在有新连接到来时接受连接。当有数据到来时,它会打印出数据内容。这个服务器可以同时处理多个连接,并且不会阻塞在任何一个连接上。
pselect() 函数和 select() 函数基本上是一致的,但是有三个区别:
- select 函数用的 timeout 参数,是一个 timeval 的结构体(包含秒和微秒),而 pselect 用的是一个 timespec 结构体(包含秒和纳秒)。
- select 函数可能会为了指示还剩多长时间而更新 timeout 参数,而 pselect 不会改变 timeout 参数。
- select 函数没有 sigmask 参数,当 pselect 的 sigmask 参数为 null 时,两者行为时一致的。
因此,当需要阻塞指定的信号时,可以使用 pselect() 函数。
以下是一个需要阻塞信号的 pselect() 函数的例子:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/select.h>
#include <sys/time.h>
#include <unistd.h>
static void sig_handler(int signo)
{
printf("Caught signal %d\n", signo);
}
int main(void)
{
fd_set rfds;
struct timeval tv;
int retval;
/* Watch stdin (fd 0) to see when it has input. */
FD_ZERO(&rfds);
FD_SET(0, &rfds);
/* Wait up to five seconds. */
tv.tv_sec = 5;
tv.tv_usec = 0;
/* Set up the signal handler. */
struct sigaction sa;
sa.sa_handler = sig_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
if (sigaction(SIGINT, &sa, NULL) == -1) {
perror("sigaction");
exit(EXIT_FAILURE);
}
/* Block SIGINT and wait for input. */
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGINT);
if (sigprocmask(SIG_BLOCK, &mask, NULL) == -1) {
perror("sigprocmask");
exit(EXIT_FAILURE);
}
retval = pselect(1, &rfds, NULL, NULL, &tv, &mask);
if (retval == -1)
perror("pselect()");
else if (retval)
printf("Data is available now.\n");
else
printf("No data within five seconds.\n");
exit(EXIT_SUCCESS);
}
3. poll()函数和ppoll()函数
3.1 poll()函数
poll()函数是Linux系统上的一个用于执行I/O多路复用的函数,与select()函数类似。poll()函数的机制与select()类似,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是poll()没有最大文件描述符数量的限制(但是数量过大后性能也是会下降)。poll()函数需要一个pollfd结构类型的数组,用于存放需要检测其状态的Socket描述符。每当有一个Socket描述符状态发生变化时,poll()函数就会在这个数组中找到对应的元素,然后修改这个元素中revents成员的值,表明这个Socket描述符上发生了什么事件。
poll()函数的声明如下:
#include <poll.h>
int poll(struct pollfd fds[], nfds_t nfds, int timeout);
其中,fds是一个struct pollfd结构类型的数组,用于存放需要检测其状态的Socket描述符;nfds表示fds数组中元素的数量;timeout表示poll()函数等待的时间(单位为毫秒),如果timeout为0,则表示立即返回;如果timeout为-1,则表示无限等待。
以下是一个简单的poll()函数的例子,用于监测按键按下的事件,如果按下了就将键值打印出来;如果超过5S,还没有按键按下,就打印出超时信息:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <poll.h>
int main(void)
{
struct pollfd fds;
int ret;
char c;
fds.fd = STDIN_FILENO;
fds.events = POLLIN;
ret = poll(&fds, 1, 5000);
if (ret == -1) {
perror("poll");
return 1;
}
if (!ret) {
printf("timeout\n");
return 0;
}
if (fds.revents & POLLIN) {
scanf("%c", &c);
printf("input = %c\n", c);
}
return 0;
}
POLLIN是poll()函数的一个参数,用于指定需要监测的事件类型。POLLIN表示需要监测的是输入事件,即文件描述符是否可读。其他常见的事件类型还有POLLOUT(文件描述符是否可写)和POLLERR(文件描述符是否出错)等。
3.2 ppoll()函数
ppoll()函数是Linux系统下的一个系统调用,用于监测多个文件描述符的状态,类似于poll()函数。与poll()函数不同的是,ppoll()函数可以指定一个sigmask参数,用于指定需要屏蔽的信号集合。ppoll()函数的函数原型如下:
#include <poll.h>
int ppoll(struct pollfd *fds, nfds_t nfds,
const struct timespec *timeout_ts,
const sigset_t *sigmask);
其中,fds参数是一个指向pollfd结构体数组的指针,用于指定需要监测的文件描述符;nfds参数表示fds数组中元素的个数;timeout_ts参数表示超时时间;sigmask参数表示需要屏蔽的信号集合。ppoll()函数返回值与poll()函数相同,表示就绪文件描述符的个数。
其区别同函数select()和pselect()的区别相同,主要有两点:
- 超时时间timeout,采用了纳秒级的变量。
- 可以在ppoll()函数的处理过程中挂接临时的信号掩码。
4. 非阻塞编程
非阻塞方式的操作与阻塞方式的操作最大的不同点是函数的调用立刻返回,不管数据是否成功读取或者成功写入。使用fcntl()将套接字文件描述符按照如下的代码进行设置后,可以进行非阻塞的编程;
fcntl(s, F_SETFL, O_NONBLOCK);
其中的s是套接字文件描述符,使用F_SETFL命令将套接字s设置为非阻塞方式后,再进行读写操作就可以马上返回了.