我们之前说,实现并发服务器的方式有三种:
- 多进程服务器(通过创建多个进程提供服务)
- 多路复用服务器(通过捆绑并统一管理I/O对象提供服务)
- 多线程服务器(通过生成与客户端等量的线程提供服务)
现在我们来介绍多路复用实现并发服务器。
基于I/O复用的服务器
之前介绍的多进程服务器,只要有客户端请求连接,就会创建与之通信的子进程,而操作系统创建进程的成本是比较大的,而服务器与客户端又不是时时刻刻都在通信,所以采用这种方式实现并发服务器非常浪费计算资源与内存。此处所提出的I/O复用实现并发服务器就可以解决这个问题,其实也就是服务器在一个进程中与多个客户端进行通信。
select函数
通过select
函数可以实现多路复用并发服务器,它可以将多个文件描述符集中到一起统一监控。在使用它之前,我们需要进行一些与它相关的工作:
- 设置文件描述符
select
要统一监视文件描述符,那么首先就要将文件描述符集中到一起。根据所要监视的事件,将文件描述符集中到三个集合中。集合为fd_set
,它是一个只有0和1的位数组,其相关操作如下:- FD_ZERO(fd_set *fdset):将fd_set变量的所有初始位设置为0。
- FD_SET(int fd, fd_set *fdset):在参数fdset指向的数组中,注册文件描述符fd的信息。
- FD_CLR(int fd, fd_set *fdset):在参数fdset指向的数组中,清除文件描述符fd的信息。
- FD_ISSET(int fd, fd_set *fdset):若参数fdset指向的数组中,包含文件描述符fd的信息,返回真。
- 设置监视范围及时间
通过设置监视时间,就可以防止在读写数据或者处理其他信息的时候陷入阻塞状态,导致程序无法继续向下执行。- 监视范围通过传入
select
的参数位置来确定 - 监视时间通过设置
timeval
结构体来确定
- 监视范围通过传入
struct timeval
{
long tv_sec; // 保存监视时间的秒数
long tv_usec; // 保存监视时间的微秒数,这两个是相加关系,不是换算关系
}
做完上述准备,就可以使用select
函数了:
#include <sys/select.h>
#include <sys/time.h>
int select(int maxfd, // 要监视的文件描述符的总数
fd_set *readset, // 监视"是否有待读取数据"的文件描述符集合的地址值
fd_set *writeset, // 监视"是否有可传输无阻塞数据"的文件描述符集合的地址值
fd_set *exceptset, // 监听"是否发生异常"的文件描述符集合的地址值
const struct timeval *timeous); // 为防止程序陷入无限阻塞状态设置的时间限制
实例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/select.h>
#define BUF_SIZE 100
void error_handling(const char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int main(int argc, char *argv[])
{
int serv_sock, clnt_sock;
struct sockaddr_in serv_adr, clnt_adr;
struct timeval timeout;
fd_set reads, cpy_reads;
socklen_t adr_sz;
int fd_max, str_len, fd_num, i;
char buf[BUF_SIZE];
if(argc != 2)
{
printf("Usage: %s <port>\n", argv[0]);
exit(1);
}
serv_sock = socket(AF_INET, SOCK_STREAM, 0);
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_adr.sin_port = htons(atoi(argv[1]));
if(bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
error_handling("bind error");
if(listen(serv_sock, 5) == -1)
error_handling("listen error");
FD_ZERO(&reads);
FD_SET(serv_sock, &reads);
fd_max = serv_sock;
while(1)
{
// cpy_reads是本次监视的文件描述符,中间如果有要加入新的文件描述符,加入reads,然后下次再拷贝进来监视
cpy_reads = reads;
timeout.tv_sec = 5;
timeout.tv_usec = 5000;
if((fd_num = select(fd_max + 1, &cpy_reads, 0, 0, &timeout)) == -1)
break;
if(fd_num == 0)
continue;
for(i = 0; i < fd_max + 1; i++)
{
if(FD_ISSET(i, &cpy_reads))
{
if(i == serv_sock) // 服务器套接字则接收连接,并将创建的通信套接字加入监听
{
adr_sz = sizeof(clnt_adr);
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
FD_SET(clnt_sock, &reads);
if(fd_max < clnt_sock)
fd_max = clnt_sock;
printf("connect client: %d \n", clnt_sock);
}
else // 不是服务器套接字则是通信套接字,那么就可以从中读取信息
{
str_len = read(i, buf, BUF_SIZE);
if(str_len == 0)
{
FD_CLR(i, &reads);
close(i);
printf("close client: %d \n", i);
}
else
{
write(i, buf, str_len);
}
}
}
}
}
close(serv_sock);
return 0;
}
客户端用之前章节中的,然后运行结果如下:
服务器:
客户端:
可以看到服务器先监听了serv_soick
文件描述符,然后客户端发送连接请求后,服务器接受了,然后客户端发送数据给服务器,服务器的clnt_sock
也接收并返回了。
以上就是通过select来实现基于I/O复用的并发服务器