1.功能
I/O多路复用允许我们同时检测多个文件描述符,看其中任意一个是否可以执行I/O操作。我们可以在普通文件、终端、伪终端、管道、FIFO、套接字以及一些其他类型的字符型设备上使用select()和poll()来检查文件描述符。
本文以select()函数操作socket来讲述。
select 机制会监听它所负责的所有 socket,当其中一个 socket 或者多个 socket 可读或者可写的时候,它就会返回,而如果所有的 socket 都是不可读或者不可写的时候,这个进程就会被阻塞,直到超时,当 select 函数返回后,可以通过遍历 fdset,来找到就绪的描述符。
简单点说就是,假如想在一个线程处理两个阻塞的socket时,就得用select()函数。
2.函数原型
#include <sys/time.h>
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set exceptfds, struct timeval *timeout)
nfds
:需要监听的所有描述符集合中的最大值+1,例如监听socket_a = 5、socket_b = 9,那么此时这里应该输入10。这时select函数会轮询描述符为0-9的所有I/O,系统开销比较大,这也是select()函数的缺点。
readfds
: 是用来检测输入是否就绪的描述符集合;
writefds
: 是用来检测输出是否就绪的描述符集合;
exceptfds
: 是用来检测异常情况是否发生的描述符集合;
timeout
: 阻塞超时时间。
数据类型fd_set以位掩码的形式来实现,我们不需要知道这些细节,因为所有的操作都是通过下面这四个函数来完成的:
#include <sys/select.h>
// 将fdset指向的集合初始化为空
void FD_ZERO(fd_set *fdset);
// 将文件描述符fd添加到fdset指向的集合
void FD_SET(int fd, fd_set *fdset);
// 将文件描述符fd从fdset指向的集合中移除
void FD_CLR(int fd, fd_set *fdset);
// 判断文件描述符fd是否在fdset指向的集合中,如果是返回True
int FD_ISSET(int fd, fd_set *fdset);
3.demo
在一个线程中同时创建一个tcp_socket和一个udp_socket,利用select()来让两个socket都能随时收到消息。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <netinet/in.h>
#include <sys/select.h>
#include <arpa/inet.h>
#include <netdb.h>
#define TCP_HOST "192.168.123.121" // 设置服务器IP
#define UDP_HOST "192.168.123.195" // 绑定本机IP
#define PORT 6666 // 端口号
#define BUF_SIZE_MAX (4 * 1024) // 4k 的数据区域
int main(void)
{
int tcp_socket_fd, udp_socket_fd, ret;
struct sockaddr_in tcp_addr, udp_addr;
char buf[BUF_SIZE_MAX];
struct timeval tv;
fd_set readfds;
// 创建TCP套接字描述符
tcp_socket_fd = socket(AF_INET, SOCK_STREAM, 0);
if (tcp_socket_fd == -1)
{
printf("Fail to creat tcp_socket.\r\n");
exit(EXIT_FAILURE);
}
// 设置TCP链接的服务器IP与端口
memset(&tcp_addr, 0,sizeof(tcp_addr));
tcp_addr.sin_family = AF_INET; // IPV4
tcp_addr.sin_port = htons(PORT);
tcp_addr.sin_addr.s_addr = inet_addr(TCP_HOST);
// 建立TCP连接
ret = connect(tcp_socket_fd, (struct sockaddr *)&tcp_addr, sizeof(struct sockaddr));
if (ret == -1)
{
close(tcp_socket_fd);
printf("Fail to connect server.\r\n");
exit(EXIT_FAILURE);
}
// 创建UDP套接字描述符
udp_socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (udp_socket_fd == -1)
{
close(tcp_socket_fd);
printf("Fail to creat udp_socket.\r\n");
exit(EXIT_FAILURE);
}
// 设置UDP链接需要绑定的本地IP与端口
memset(&udp_addr, 0,sizeof(udp_addr));
udp_addr.sin_family = AF_INET;
udp_addr.sin_port = htons(PORT);
udp_addr.sin_addr.s_addr = inet_addr(UDP_HOST);
// 绑定IP与端口
ret = bind(udp_socket_fd, (struct sockaddr*)&udp_addr, sizeof(struct sockaddr));
if(ret == -1)
{
close(tcp_socket_fd);
close(udp_socket_fd);
printf("Fail to bind udp_addr.\r\n");
exit(EXIT_FAILURE);
}
// 设置select阻塞时间
tv.tv_sec = tv.tv_usec = 0;
while (1)
{
memset(buf, 0, BUF_SIZE_MAX);
FD_ZERO(&readfds);
FD_SET(tcp_socket_fd, &readfds);
FD_SET(udp_socket_fd, &readfds);
ret = select(udp_socket_fd + 1, &readfds, NULL, NULL, &tv);
if(ret < 0)
{
printf("ret = %d.\r\n", ret);
break;
}
else
{
// TCP数据
if(FD_ISSET(tcp_socket_fd, &readfds))
{
ret = read(tcp_socket_fd, buf, BUF_SIZE_MAX);
printf("[TCP recv]: %s\r\n", buf);
}
// UDP数据
else if(FD_ISSET(udp_socket_fd, &readfds))
{
ret = read(udp_socket_fd, buf, BUF_SIZE_MAX);
printf("[UDP recv]: %s\r\n", buf);
}
}
usleep(100000);
}
close(tcp_socket_fd);
close(udp_socket_fd);
exit(0);
}