unix网络编程2.3——高并发服务器(三)多路IO复用之select
目录
前置知识
阅读本文需要先阅读下面的文章:
unix网络编程1.1——TCP协议详解(一)
unix网络编程2.1——高并发服务器(一)基础——io与文件描述符、socket编程与单进程服务端客户端实现
unix网络编程2.2——高并发服务器(二)多进程与多线程实现
前言
在unix网络编程2.2——高并发服务器(二)多进程与多线程实现中实现了多进程和多线程的服务器模型,但是实际开发中往往使用IO复用的方式,避免开多进程/线程的资源浪费,并且可以避免多进程/线程由于资源受限带来的并发量不足的情况,为此在本文中学习多路IO复用的入门接口select,一种基于轮询来借助内核帮助实现高并发服务器的思想。
- 多路IO转接服务器也叫做多任务IO服务器。该类服务器实现的主旨思想是,不再由应用程序自己监视客户端连接,取而代之由内核替应用程序监视文件。
select函数参数与返回值
man select查看函数:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
参数:
nfds: 监控的文件描述符集里最大文件描述符加1,因为此参数会告诉内核检测前多少个文件描述符的状态
readfds: 监控有读数据到达文件描述符集合,传入传出参数
writefds: 监控写数据到达文件描述符集合,传入传出参数
exceptfds: 监控异常发生达文件描述符集合,如带外数据到达异常,传入传出参数
fd_set类型: 位图
timeout: 定时阻塞监控时间,3种情况
1.NULL,永远等下去
2.设置timeval,等待固定时间
3.设置timeval里时间均为0,检查描述字后立即返回,轮询
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
返回值:
成功:所监听的所有 监听集合(readfds + writefds + exceptfds)中,满足条件的总数
失败:-1
配套使用的接口:
void FD_CLR(int fd, fd_set *set); // 把文件描述符集合里fd清0
int FD_ISSET(int fd, fd_set *set); // 测试文件描述符集合里fd是否置1
void FD_SET(int fd, fd_set *set); // 把文件描述符集合里fd位置1
void FD_ZERO(fd_set *set); // 把文件描述符集合里所有位清0
select模型图与单进程服务端对比
单进程服务端模型图
- 单进程服务端基于阻塞IO的设计思想如下:
select模型图
- IO多路复用会使用非阻塞IO
与单进程服务端模型比较
- accept和read对应IO不会阻塞,全部交由select统一管理,select阻塞等待IO读写事件就绪
- 在select监听的事件集合中,有n个IO读写事件就绪,则返回n+1的整型数据,记为n_ready,后续的建立连接、读写数据依赖n_ready和FD_ISSET等接口进行判断后处理,这个过程需要循环遍历(轮询方式),效率较低,因此select不适用大规模客户端的场景,但比起单进程服务端的效率与资源占用率已经好很多
select参数分析 r_fds + w_fds + e_fds 三个集合
- r_fds + w_fds + e_fds 三个集合对应三个事件,分别是:读事件、写事件、异常事件,发生这三个事件的前提是三次握手完成,server和client已经建立了连接,但在此之前呢?
请看建立请求——accept不会再阻塞**
建立请求——accept不会再阻塞
- 还有一个在server的事件需要关注:listen_fd的读事件,如果listen_fd发送了读事件,说明由client来发送SYN请求建立连接, 此时select会将listen_fd在r_fds中的bit位置1,标记listen_fd读事件就绪,此时再调用accept
读事件——read不会再阻塞
- 与listen_fd读事件之于accept不会阻塞同理,conn_fd的读写事件被select阻塞监听后,服务端read也不会阻塞了
基于select实现server.c
- 版本1
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/select.h>
#include <string.h>
#define PORT 9999
#define START_FD 3
#define BUFFER_LENGTH 128
int main(void)
{
int listen_fd, conn_fd;
// block
listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd == -1) return -1;
// listen_fd
struct sockaddr_in serv_addr, client_addr;
socklen_t client_len;
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(PORT);
if (-1 == bind(listen_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr))) {
return -2;
}
#if 1 // nonblock
int flag = fcntl(listen_fd, F_GETFL, 0);
flag |= O_NONBLOCK;
fcntl(listen_fd, F_SETFL, flag);
#endif
listen(listen_fd, 128);
// 由于每次有读写事件会调用FD_SET改变r_fds, w_fds,因此需要r_set, w_set作备份
fd_set r_fds, w_fds;
fd_set r_set, w_set;
FD_ZERO(&r_fds);
FD_SET(listen_fd, &r_fds); // 监听listen_fd的读事件
FD_ZERO(&w_fds);
int max_fd = listen_fd;
unsigned char buf[BUFFER_LENGTH] = {0};
int n_ready, ret = 0;
while (1) {
r_set = r_fds;
w_set = w_fds;
n_ready = select(max_fd + 1, &r_set, &w_set, NULL, NULL); // 阻塞等待IO读写事件就绪
// 走到这里说明有IO事件就绪
if (FD_ISSET(listen_fd, &r_set)) {
printf("listen_fd --> \n");
client_len = sizeof(client_addr);
conn_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len); // 不会阻塞
FD_SET(conn_fd, &r_fds); // 将conn_fd添加到r_fds中监听
if (conn_fd > max_fd) {
max_fd = conn_fd;
}
}
for (int i = START_FD; i <= max_fd; i++) {
if (FD_ISSET(i, &r_set)) {
ret = recv(i, buf, sizeof(buf), 0);
// 说明客户端断开了连接
if (ret == 0) {
close(i);
FD_CLR(i, &r_fds);
} else if (ret > 0) {
printf("buf : %s, ret : %d\n", buf, ret);
FD_SET(i, &w_fds); // 将fd添加到写集合中
} else {
close(i);
FD_CLR(i, &r_fds);
}
} else if (FD_ISSET(i, &w_set)) {
ret = send(i, buf, ret, 0);
FD_CLR(i, &w_fds); // 将fd从写集合中删除
FD_SET(i, &r_fds);
}
}
}
return 0;
}
- 版本2
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <ctype.h>
#define SERVER_PORT 8888
/*
统称:
文件描述符: fd
下标(索引): id
*/
int main(void)
{
/* init */
int listen_fd, max_fd, conn_fd, sock_fd;
char buf[BUFSIZ], ip_str[INET_ADDRSTRLEN];
int i, j, max_i, n;
fd_set r_set, all_set; /* 位图,r_set:监听读事件fd集合,all_set存r_set上一次的状态 */
int n_ready, client[FD_SETSIZE]; /* 自定义数组client[],防止每次都遍历1024个fd,FD_SETSIZE:1024 */
struct sockaddr_in server_addr, client_addr;
socklen_t client_addr_len;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
listen_fd = socket(AF_INET, SOCK_STREAM, 0); /* 创建socket,return listen_fd */
bind(listen_fd, (struct sockaddr *)&server_addr, /* bind listen_fd */
sizeof(server_addr));
listen(listen_fd, 128); /* start listen */
/************************************************ begin select /************************************************/
max_fd = listen_fd; /* 初始化时,listen_fd作为最大fd */
max_i = -1; /* max_i作为client[]的id,初始值指向第0个元素之前的id */
memset(client, -1, sizeof(client)); /* client[]所有id初始化为-1 */
FD_ZERO(&all_set);
FD_SET(listen_fd, &all_set); /* 构造select监控的fd信号集 */
while (1) {
r_set = all_set;
n_ready = select(max_fd + 1, &r_set, NULL, NULL, NULL); /* n_ready: 所监听的所有 监听集合(readfds + writefds + exceptfds)中,满足条件的总数 */
if (n_ready < 0) {
perror("select error");
exit(-1);
}
if (FD_ISSET(listen_fd, &r_set)) { /* 说明由新的客户端连接请求(监听到了listen_fd的读事件) */
client_addr_len = sizeof(client_addr);
conn_fd = accept(listen_fd, (struct sockaddr *)&client_addr, /* accept不会再阻塞(listen已经ok) */
&client_addr_len);
printf("accept done, client ip:%s, port:%d\n",
inet_ntop(AF_INET, &client_addr.sin_addr, ip_str, sizeof(ip_str)),
ntohs(client_addr.sin_port));
for (i = 0; i < FD_SETSIZE; i++) {
if (client[i] == -1) { /* 找到client[]中没有使用的id */
client[i] = conn_fd; /* 保存新连接的conn_fd到client[]中 */
break;
}
}
if (i == FD_SETSIZE) { /* 达到select能监控的fd最大上限数 1024 */
fputs("too many clients\n", stderr);
exit(-1);
}
FD_SET(conn_fd, &all_set); /* 更新conn_fd到select监控的fd信号集中 */
if (conn_fd > max_fd) {
max_fd = conn_fd; /* 更新max_fd */
}
if (i > max_i) {
max_i = i; /* 保证max_i总是指向client[]最后一个元素下标 */
}
if (--n_ready == 0) { /* 其实这里的n_ready,是为了处理多个client可能同时发数据并且有新client连接的情况 */
continue; /* 如果没有更多的就绪fd继续回到上面select阻塞监听 */
}
}
for (i = 0; i <= max_i; i++) { /* 轮询检测有哪个clients 有数据就绪(max_i指向的是client[]中最后一个有数据的fd) */
if ((sock_fd = client[i]) < 0) {
continue;
}
if (FD_ISSET(sock_fd, &r_set)) {
if ((n = read(sock_fd, buf, sizeof(buf))) == 0) { /* 当client关闭连接时,server也应关闭对应连接 */
close(sock_fd);
FD_CLR(sock_fd, &all_set); /* 清除select对此fd的监听 */
client[i] = -1;
} else if (n > 0) {
for (j = 0; j < n; j++) {
buf[j] = toupper(buf[j]);
}
write(sock_fd, buf, n);
}
if (--n_ready == 0) {
break; /* 跳出for,但还在while中 */
}
}
}
}
return 0;
}
总结
- 1、select能监听的文件描述符个数受限于FD_SETSIZE,一般为1024,单纯改变进程打开的文件描述符个数并不能改变select监听文件个数
- 2、解决1024以下客户端时使用select是很合适的,但如果链接客户端过多,select采用的是轮询模型,会大大降低服务器响应效率,不应在select上投入更多精力