Socket 是网络协议栈暴露给编程人员的 API,相比复杂的计算机网络协议,API 对关键操作和配置数据进行了抽象,简化了程序编程。
本文讲述的 socket 内容源自 Linux man。本文主要对各 API 进行详细介绍,从而更好的理解 socket 编程。
poll
poll() 遵循 POSIX.1 - 2008
ppoll() 遵循 Linux
1.库
标准 c 库,libc, -lc
2.头文件
<poll.h>
3.接口定义
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
int ppoll(struct pollfd *fds, nfds_t nfds,
const struct timespec *_Nullable tmo_p,
const sigset_t *_Nullable sigmask);
4.接口描述
poll() 和 select() 做的事情差不多,它等待一个文件描述符集 I/O 就绪。Linux 的 epoll() 也是类似的,只是比 poll() 提供多了一些特性。
fds 参数是要监控的文件描述符集,是下面结构体的一个数组:
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
由调用者指定 fds 的项数。
结构体中 fd 包含了一个打开的文件描述符,如果它是负值,那么 events 参数将被忽略,revents 返回 0。(也就是说 可以将 fd 设置为其补码就可以忽略它)。
events 参数是一个输入参数,通过按位掩码来标识应用感兴趣的文件描述符上的事件。参数可以设置为 0,那么就只能返回 POLLHUP/POLLERR/POLLNVAL 事件。
revents 是一个输出参数,由内核填充实际发生的事件。这些事件可以是 events 中指定的事件,也可以是 POLLHUP/POLLERR/POLLNVAL 中的一个。(events 中这三个事件对应的位并没有什么意义,只要对应的条件发生,revents 就会返回该事件。)
如果没有请求的事件(包括错误)发生,那么 poll() 会一直阻塞,直到有事件发生。
timeout 参数指定了 poll() 等待文件描述符就绪的毫秒数,该调用会一直阻塞直到:
- 文件描述符就绪
- 调用被信号打断
- 发生超时
同样,timeout 值也会向上近似到系统时钟粒度,由于内核调度延迟阻塞的事件可能会稍微多一点。如果 timeout 是负值,表示超时时间是无限长。如果 timeout 设置为 0,那么 poll() 会马上返回,即使没有任何文件描述符就绪。
events 和 revents 中各个位在 poll.h 中定义:
POLLIN
有数据可以读。
POLLPRI
文件描述符上有异常发生,可能是(1)TCP socket 上有带外数据(2)处于报文模式的伪终端主机发现了从机状态变化(3)cgroup.events 文件被修改了。
POLLOUT
当前可写,但是写大于 socket 或 pipe 中可用空间的数据仍然会导致阻塞(除非设置了 O_NONBLOCK)。
POLLRDHUP
流 socket 对端关闭了连接或者在写半连接时关机。这个定义依赖于 _GNU_SOURCE 宏定。
POLLERR
发生错误。如果文件描述符指向了 pipe 的写端,而读端关闭了,那么也会返回这个错误。
POLLHUP
挂断。在读取 pipe 或者流 socket 时,这个事件只表示对端关闭了其通道,后面的数据读取时,在通道中数据读尽后再继续读会返回 0(EOF)。
POLLNVAL
请求不合法:fd 没有打开。
在使用 _XOPEN_SOURCE 宏编译时,还会有以下一些事件,不过也没有提供太多信息:
POLLRDNORM
等同于 POLLIN。
POLLRDBAND
优先带宽数据可以读(通常在 Linux 上用)
POLLWRNORM
等同于 POLLOUT
POLLWRBAND
可能写了优先数据
ppoll()
ppoll() 和 poll() 的关系就像 select() 和 pselect() 的关系一样,ppoll() 为应用提供了等待信号或者就绪事件的安全方法。
除了 timeout 时间精度上的差异,以下两段代码几乎等效
ready = ppoll(&fds, nfds, tmo_p, &sigmask);
sigset_t origmask;
int timeout;
timeout = (tmo_p == NULL) ? -1 :
(tmo_p->tv_sec * 1000 + tmo_p->tv_nsec / 1000000);
pthread_sigmask(SIG_SETMASK, &sigmask, &origmask);
ready = poll(&fds, nfds, timeout);
pthread_sigmask(SIG_SETMASK, &origmask, NULL);
上面代码说成几乎等效而不是等效主要是因为负值的 timeout 会被 poll() 解释为一直等待,而 ppoll() 中负值的 *tmo_p 会报错。
可以参考 pselect(2) 来看为什么 ppoll 是必要的。
如果 sigmask 参数为 NULL,那么就不会有任何信号屏蔽操作,这时这两个接口唯一的区别就是时间精度。
tmo_p 指定了 ppoll() 会阻塞的时间上限,它是指向 timespec 结构体的指针,指针为空时,ppoll() 会一直阻塞。
5.返回值
成功时,poll() 返回一个非负数表示 pollfds 中有多少个文件描述符上有事件发生,即对应的 revents 有被更新为非 0 值。返回 0 表示没有任何文件描述符就绪并超时。
发生错误时,返回 -1,并设置errno 来指示错误类型。
错误值定义如下:
EFAULT | fds 指向了进程外的地址空间 |
EINTR | 请求事件发生前,发生了信号,具体参见 signal(7) |
EIVAL | nfds 值超出了 RLIMIT_NOFILE 限制 |
EINVAL | ppoll() 中的 *tmo_P 是一个非法值(负数) |
ENOMEM | 没有足够内存来分配内核数据结构 |
一些其他 UNIX 系统上,如果内核无法发分配内核资源,poll() 可能会产生 EAGAIN 类的错误,而不像 Linux 上的 ENOMEM。POSIX 允许这种行为。所以,一个可移植的程序需要检测该错误,并重试,就像处理 EINTR 一样。
一些实现定义了非标准常量 INFTIM(-1),用作 poll() 的 timeout,但是这个常量并没有被被 glibc 提供。
6.注意
poll() 和 ppoll() 的行为不受 O_NONBLOCK 标志影响。
对于一个文件描述符正在被 poll() 监听却被另一个线程关闭了这种情况的讨论,可以参考 select(2)。
7.BUGS
可以参考 select(2) 中关于虚假就绪通知的讨论。
8.代码实例
该程序会打开命令行参数传进来的文件名并监听其 POLLIN 事件,程序会循环调用 poll() 来监听文件描述符,打印已经就绪的文件描述符数。对于每个就绪的文件描述符,程序会:
- 以可读的格式显示返回的 revents
- 如果文件描述符就绪,那么就从中读一些数据出来并打印
- 如果文件描述符不可读,但是发生了一些其他事件(比如 POLLHUP),就关闭文件描述符
假定我们在一个终端运行程序,让他打开一个 FIFO:
$ mkfifo myfifo
$ ./poll_input myfifo
在另一个终端打开 FIFO,并写入一些数据,然后关闭 FIFO:
$ echo aaaaabbbbbccccc > myfifo
我们将在运行程序的终端上看到如下信息:
Opened "myfifo" on fd 3
About to poll()
Ready: 1
fd=3; events: POLLIN POLLHUP
read 10 bytes: aaaaabbbbb
About to poll()
Ready: 1
fd=3; events: POLLIN POLLHUP
read 6 bytes: ccccc
About to poll()
Ready: 1
fd=3; events: POLLHUP
closing fd 3
All file descriptors closed; bye
从上面我们可以看到 poll() 返回了三次:文章来源:https://www.toymoban.com/news/detail-705963.html
- 第一次返回是 POLLIN,表示文件描述符可读,另一个是 POLLHUP 表示文件描述符的另一个端关闭了。程序接着读取了一些可用的输入数据
- 第二次返回同样是这两个事件,依然消费了一些可用数据
- 最后一次返回,poll() 只有 POLLHUP 事件,然后关闭文件描述符并结束了程序。
/* poll_input.c
Licensed under GNU General Public License v2 or later.
*/
#include <fcntl.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); \
} while (0)
int
main(int argc, char *argv[])
{
int ready;
char buf[10];
nfds_t num_open_fds, nfds;
ssize_t s;
struct pollfd *pfds;
if (argc < 2) {
fprintf(stderr, "Usage: %s file...\n", argv[0]);
exit(EXIT_FAILURE);
}
num_open_fds = nfds = argc - 1;
pfds = calloc(nfds, sizeof(struct pollfd));
if (pfds == NULL)
errExit("malloc");
/* Open each file on command line, and add it to 'pfds' array. */
for (nfds_t j = 0; j < nfds; j++) {
pfds[j].fd = open(argv[j + 1], O_RDONLY);
if (pfds[j].fd == -1)
errExit("open");
printf("Opened \"%s\" on fd %d\n", argv[j + 1], pfds[j].fd);
pfds[j].events = POLLIN;
}
/* Keep calling poll() as long as at least one file descriptor is
open. */
while (num_open_fds > 0) {
printf("About to poll()\n");
ready = poll(pfds, nfds, -1);
if (ready == -1)
errExit("poll");
printf("Ready: %d\n", ready);
/* Deal with array returned by poll(). */
for (nfds_t j = 0; j < nfds; j++) {
if (pfds[j].revents != 0) {
printf(" fd=%d; events: %s%s%s\n", pfds[j].fd,
(pfds[j].revents & POLLIN) ? "POLLIN " : "",
(pfds[j].revents & POLLHUP) ? "POLLHUP " : "",
(pfds[j].revents & POLLERR) ? "POLLERR " : "");
if (pfds[j].revents & POLLIN) {
s = read(pfds[j].fd, buf, sizeof(buf));
if (s == -1)
errExit("read");
printf(" read %zd bytes: %.*s\n",
s, (int) s, buf);
} else { /* POLLERR | POLLHUP */
printf(" closing fd %d\n", pfds[j].fd);
if (close(pfds[j].fd) == -1)
errExit("close");
num_open_fds--;
}
}
}
}
printf("All file descriptors closed; bye\n");
exit(EXIT_SUCCESS);
}
下一篇 【计算机网络】网络编程接口 Socket API 解读(4)文章来源地址https://www.toymoban.com/news/detail-705963.html
到了这里,关于【计算机网络】网络编程接口 Socket API 解读(3)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!