Unix下可用的I/O模型一共有五种:阻塞I/O 、非阻塞I/O 、I/O复用 、信号驱动I/O 、异步I/O。此处我们主要介绍第三种I/O符复用。
I/O复用的功能:如果一个或多个I/O条件满足(输入已准备好读,或者描述字可以承接更多输出)时,我们就被通知到。这就是有select、poll、epoll实现。
I/O复用应用场合:
1、当客户处理多个描述字时(一般是交互式输入和网络套接口),必须使用I/O复用。在这前一段中已做描述。
2、一个客户同时处理多个套接口是可能的,但很少出现。
3、如果一个TCP服务器机要处理监听套接口,有要处理已连接套接口,一般也要用到I/O复用。
4、如果一个服务器机要处理TCP,有要处理UDP,一般也要使用I/O复用。
5、如果一个服务器要处理多个服务或者多个协议,一般要使用I/O复用。
I/O复用原理图:
select:
使用Select就可以完成非阻塞(所谓非阻塞方式non-block,就是进程或线程执行此函数时不必非要等待事件的发生,一旦执行肯定返回,以返回值的不同来反映函数的执行情况,如果事件发生则与阻塞方式相 同,若事件没有发生则返回一个代码来告知事件未发生,而进程或线程继续执行,所以效率较高)方式工作的程序,它能够监视我们需要监视的文件描述符的变化情 况——读写或是异常。
所要用到的结构体:
struct timeval{
long tv_sec; //等待的秒数
long tv_usec; //等待的微秒数
}
select()函数:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
作用:用来检测描述符中是否有准备好读、写、或异常的描述符
参数1(nfds):被测试的描述字个数;它的值为要被测试的最大描述符个 数+1,而描述字0,1,2,……..,nfds-1;
参数2—4 (readfds)、(writefds)、(exceptfds):这三个参数指定我们要让内核测试读、写、异常条件所需的描述字。当我们在调用该函数,指定好我们所要检测的描述字集后,如果检测三种情况下任何一中情况准备好,则将相应的状态变为可用状态。如果到达函数返回时没有可读可写则返回失败。如果我们不关心其中哪个状态,可将其设为NULL。
参数5(timeout):指定等待时间,有三种情况:
(1)、永远等待下去(参数timeout设置为空指针):仅在有一个描述字准备好I/O时才返回。
(2)、等待固定时间(指定timeval中的秒数和微秒数):在不超过timeval结构体中所指定的秒数和微秒数内检测到有一个描述字准备好I/O时返回
(3)、根本不等待(timeval中秒数和微秒数均设置为0):检查描述字后立即返回。
select工作原理:
select就是巧妙的利用等待队列机制让用户进程适当在没有资源可读/写时睡眠,有资源可读/写时唤醒。下面我们看看select睡眠的详细过程。
select会循环遍历它所监测的fd_set(一组文件描述符(fd)的集合)内的所有文件描述符对应的驱动程序的poll函数。驱动程序提供的poll函数首先会将调用select的用户进程插入到该设备驱动对应资源的等待队列(如读/写等待队列),然后返回一个bitmask告诉select当前资源哪些可用。当select循环遍历完所有fd_set内指定的文件描述符对应的poll函数后,如果没有一个资源可用(即没有一个文件可供操作),则select让该进程睡眠,一直等到有资源可用为止,进程被唤醒(或者timeout)继续往下执行。
select调用过程:
头文件:下面poll、epoll的头文件与该文件相同
#include<unistd.h>
#include<stdio.h>
#include<string.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/select.h>
#include<poll.h>
#include<sys/epoll.h>
#include<sys/types.h>
#define IPADDR "192.168.3.169"
#define PORT 8787
#define MAXLINE 1024
#define LISTENQ 5
//select
#define SIZE 10
//poll
#define OPEN_SIZE 10
//epoll
#define FDSIZE 100
服务器端:
#include"../unp.h"
#include<malloc.h>
typedef struct server_context_st //服务器描述表
{
int cli_cnt; //客户端连接个数
int clifds[SIZE]; //描述字集合
fd_set allfds; //设置所有的描述字
int maxfd; //最大描述字个数
}server_context_st;
static server_context_st *s_srv_ctx = NULL;
int server_init() //服务器初始化函数
{
s_srv_ctx = (server_context_st*)malloc(sizeof(server_context_st)); //申请一个服务器描述表
if(s_srv_ctx == NULL)
return -1;
memset(s_srv_ctx, 0, sizeof(server_context_st)); //将该描述表清0
for(int i=0; i<SIZE; ++i) //将该表中的每一位设为-1
{
s_srv_ctx->clifds[i] = -1;
}
return 0;
}
void server_uninit() //服务器去初始化函数
{
if(s_srv_ctx) //如果服务器描述表不为0,即该表申请成功存在
{
free(s_srv_ctx); //释放该表的内存
s_srv_ctx = NULL; //将指针值为NULL。
}
}
int create_server_proc(const char *ip, short port) //创建服务器进程
{
int fd;
fd = socket(AF_INET, SOCK_STREAM, 0); //建立一个套接字,记录返回的描述字,
if(fd == -1) //检测是否创建成功
{
perror("socket");
return -1;
}
//初始化服务器信息结构体
struct sockaddr_in addrSer;
addrSer.sin_family = AF_INET; //指定用到的协议族
addrSer.sin_port = htons(port); //指定服务器端口号
addrSer.sin_addr.s_addr = inet_addr(ip); //指定服务器ip地址
socklen_t addrlen = sizeof(struct sockaddr);
int res = bind(fd, (struct sockaddr*)&addrSer, addrlen); //将创建的描述字与刚才所设置的服务器信息绑定
if(res == -1) //检测是否绑定成功嗯
{
perror("bind");
return -1;
}
listen(fd, LISTENQ); //监听是否有客户端请求连接,如果有则将该套接字设为可用
return fd;
}
int accept_client_proc(int srvfd) //结束客户端连接请求
{
struct sockaddr_in addrCli;
socklen_t addrlen = sizeof(struct sockaddr);
int clifd;
ACCEPT:
clifd = accept(srvfd, (struct sockaddr*)&addrCli, &addrlen); //结束客户端的连接请求
if(clifd == -1) //判断是否连接成功
{
goto ACCEPT; //如果没有连接成功,则跳转至ACCEPT处继续连接
}
printf("accept a new client: %s:%d\n",inet_ntoa(addrCli.sin_addr),addrCli.sin_port);
int i;
for(i=0; i<SIZE; ++i) //循环遍历描述字
{
if(s_srv_ctx->clifds[i] == -1) //如果描述字为-1,表明只连接了i个客户端(0 —— i-1)
{
s_srv_ctx->clifds[i] = clifd; //则将连接描述字赋给服务器描述表中第i个描述字
s_srv_ctx->cli_cnt++; //已连接的客户端数量加一
break;
}
}
if(i == SIZE) //如果i等于SIZE,说明描述字集合已满
{
printf("Server Over Load.\n");
return -1;
}
}
void handle_client_msg(