多进程与I/O复用
之前为了构建并发服务器,采用多进程的方式,一旦有客户端连接就会创建新进场,但由于创建新进场需要耗费大量的CPU资源与内存资源,且进程之间拥有完全独立的内存空间,还需要使用IPC;I/O复用则不需要创建多个进程,采用事件监听的方式,一旦事件发生了才去处理
I/O复用https://www.zhihu.com/question/32163005/answer/55772739
步骤、设置文件描述符、设置超时 2、调用select函数 3、查看调用结果
设置文件描述符:
利用select函数监视多个文件描述符之前,需要将要监视的文件描述符按照读(接收)、写(发送)、异常分类,相同的类型集中到一起
使用fd_set变量可以完成某种类型的集中,fd_set变量的每一位bit代表一个文件描述符的状态。fd_set变量的最左端的位表示文件描述符0,由于linux下的文件描述符是从0开始往上递增的,所以fd_set中从左向右第二位是文件描述符1,第三位………fd_set变量中哪一位是1,则表示对应的文件描述符是监视对象
对fd_set变量的操作由宏完成
(1) FD_ZERO(fd_set *fdset) 将fdset所指变量所有位初始化为0
(2) FD_SET(int fd ,fd_set * fdset) 将文件描述符fd注册到fdset所指变量中
(3) FD_CLR(int fd,fd_set* fdset) 将文件描述符fd从fdset所指变量中删除
(4) FD_ISSET(int fd,fd_set* fdset) 检查fdset所指变量中是否包含文件描述符fd的信息,有返回真
其中,宏FD_ISSET(int fd,fd_set* fdset)是用来查看调用结果的,宏的功能如图:
调用select函数
int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout) //成功返回发生事件的文件描述符数目,超时返回0,出错返回-1
(1)第一个参数maxfdp1指定被监视的描述符个数,它的值是被监视的最大描述符加1 (linux下文件描述符是从0开始递增的),描述符0、1、2...maxfdp1-1均将被监视。
(2)中间的三个参数readset、writeset和exceptset指定我们要让内核监视读(接收)、写(发送)和异常条件的描述符。如果对某一个的条件不感兴趣,就可以把它设为空指针NULL。
(3)最后一个参数告知内核等待所指定描述符中的任何一个发生事件可花多少时间。其timeval结构用于指定这段时间的秒数和微秒数。
struct timeval
{
long tv_sec; //seconds
long tv_usec; //microseconds
};
这个参数有三种可能:
(1)永远等待下去(阻塞):仅在有一个描述符发生事件时才返回。为此,把该参数设置为空指针NULL。
(2)等待一段固定时间:在有一个描述符发生事件时返回,但是不超过由该参数所指向的timeval结构中指定的秒数和微秒数。
(3)根本不等待(阻塞):检查描述符后立即返回,这称为轮询。为此,该参数必须指向一个timeval结构,而且其中的定时器值必须为0。
查看调用结果
select函数在调用后,如果返回值大于0,即有相应数目的文件描述符发生事件。相应的,向其传递fd_set变量中也会发生变化:除发生事件的文件描述符对应位,原来为1的所有位均变为0,也就是说,调用select函数后fd_set变量中值仍为1的位对应的文件描述符发生了事件
基于I/O复用的服务器端:
#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);
int main(int argc, const char * argv[])
{
int serv_sock = -1, clnt_sock = -1;
struct sockaddr_in serv_adr, clnt_adr;
struct timeval timeout;
fd_set reads, cpy_reads;
socklen_t adr_sz;
int fd_max = -1, str_len = -1, fd_num = -1, i = -1;
char buf[BUF_SIZE] = { 0 };
if( 2 != argc )
{
printf("Usage: %s <port> \n", argv[0]);
exit(1);
}
serv_sock = socket(PF_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( -1 == bind(serv_sock, (struct sockaddr *) &serv_adr, sizeof(serv_adr)) )
error_handling("bind() error");
if(-1 == listen(serv_sock, 5) )
error_handling("listen() error");
FD_ZERO(&reads); //初始化所有位
FD_SET(serv_sock, &reads); //将文件描述符serv_sock注册进fd_set变量,即监控serv_sock的事件
fd_max = serv_sock;
while (1)
{
cpy_reads = reads; //因为发生事件后select函数会改变其参数fd_set所指变量,故备份
timeout.tv_sec = 5; //指定超时时间,发生事件后,select函数会将其参数timeout所指结构体成员值设为超市前剩余时间
timeout.tv_usec = 5000; //故将fd_set类型变量与结构体timeout设置在循环内
if( -1 == (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++) //遍历fd_set变量所有被监视位
{
if( FD_ISSET(i, &cpy_reads) )//检查变量中是否有文件描述符i的信息
{
if( i == serv_sock ) //serv_sock发生的读事件(请求连接也是通过传输数据发生的,套接字缓冲中接收了数据)
{
adr_sz = sizeof(clnt_adr);
clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz);
FD_SET(clnt_sock, &reads); //注册文件描述符clnt_sock,即监控clnt_sock事件
if(fd_max < clnt_sock)
fd_max = clnt_sock; //更新最大描述符
printf("connected client: %d \n", clnt_sock);
}
else //某个客户端套接字发生读事件
{ //此处为i不为clnt_sock!描述符(套接字)i发生读事件,服务器需要服务多个客户端
str_len = read(i, buf, BUF_SIZE);
if( 0 == str_len )//客服端发送EOF close或shutdown
{
FD_CLR(i, &reads); //清除clnt_sock的注册信息
close(i); //关闭套接字
printf("closed client: %d \n", i);
}
else
{
write(i, buf, str_len); //回传
}
}
}
}
}
close(serv_sock); //关闭服务器套接字
return 0;
}
void error_handling(const char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
客户端:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
void error_handling(const char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int main(int argc, const char * argv[])
{
int sock;
char message[BUF_SIZE];
int str_len, recv_len, recv_cnt;
struct sockaddr_in serv_adr;
if( 3 != argc )
{
printf("Usage: %s <IP> <port> \n", argv[0]);
exit(1);
}
sock = socket(PF_INET, SOCK_STREAM, 0);
if( -1 == sock )
error_handling("socket() error");
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
serv_adr.sin_port = htons(atoi(argv[2]));
if( -1 == connect(sock, (struct sockaddr *) &serv_adr, sizeof(serv_adr)) )
error_handling("connect() error");
else
puts("Connected ...............");
while (1)
{
fputs("Input message(Q to quit): ", stdout);
fgets(message, BUF_SIZE, stdin);
if (!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
break;
str_len = write(sock, message, strlen(message));
recv_len = 0;
while (recv_len < str_len)
{
recv_cnt = read(sock, &message[recv_len], BUF_SIZE - 1);
if(recv_cnt == -1)
error_handling("read() error");
recv_len += recv_cnt;
}
message[recv_len] = 0;
printf("Message from server: %s", message);
}
close(sock);
return 0;
}
void error_handling(const char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
windows下的select函数
函数原型与返回值与linux下完全相同,只是参数1无意义,填0即可
timeval结构使用了typedef
typedef struct timeval
{
long tv_sec; //seconds
long tv_usec; //microseconds
}TIMEVAL;
fd_set变量为结构体
typedef struct fd_set
{
u_int fd_count;
SOCKET fd_array[FD_SETSIZE];
}fd_set;
Linux下文件描述符从0递增,而windows下的句柄并非从0开始,而且句柄之间无规律可循。因此需要保存句柄数量与句柄
fd_set reads, cpy_reads;
……..
cpy_reads = reads;
……
for(i=0; i< reads.fd_count; i++) //注意使用reads而不是cpy_reads
{
if( FD_ISSET(reads.fd_array[i], &cpy_reads) );
…..
}
细谈select函数(C语言):http://blog.csdn.net/piaojun_pj/article/details/5991968/
IO多路复用之select总结:http://www.cnblogs.com/Anker/archive/2013/08/14/3258674.html
select函数详解及实例分析:http://blog.csdn.net/leo115/article/details/8097143
Linux五种IO模型性能分析:http://blog.csdn.net/jay900323/article/details/18141217/