实现一个简易chat聊天室(4种方法)

  • Post author:
  • Post category:其他




提高服务器性能的方法有



I/O模型



阻塞I/O

程序阻塞于读写函数(当没有数据可读时,程序一直阻塞到读取数据完成)
阻塞的过程:数据从无到有的时间段



非阻塞I/O

当文件描述符不可读或者写时,立即返回
一般采用轮询的方式进行 过一段时间再来读写试一试



I/O复用

程序阻塞与I/O复用系统调用,可以同时监听多个I/O事件
对I/O本身的读写操作是非阻塞的
一个线程可以监听多个文件描述符,通过一定的方式返回有数据可读的



SIGO信号

信号触发读写就绪事件,用户程序执行读写操作,程序没有阻塞阶段



异步I/O

内核执行读写操作并触发读写完成事件,程序没有阻塞阶段
当内核中有数据可读:,
	同步读的过程,需要把内核中数据拷贝到用户空间中才返回
read(fd,buf,buf_len);
	异步读的过程,aio_read() 调用该函数之后,立即返回
当内核中的数据全部拷贝到用户空间时,会发送信号(提醒用)



池 进程池 线程池

客户端要连接上来,立即创建进程或者线程去服务
假设客户端连接上来,只发送一个请求就结束了
	频繁的创建和销毁进程or线程(这个就是短连接)
	短连接:客户端连接时间很短  HTTP服务器中很重要,网页都是短连接
	CPU占比基本上都在创建和销毁线程 业务逻辑占比很少

池的概念就是:先创建好若干个进程或者线程
	当有客户端连接上来时,让线程池中的线程去跟这个客户端服务
	当客户端断开连接后,线程不销毁,放回到池中

当遇到长连接时,池的概念就不太重要了(因为业务比重高),所以不需要



零拷贝读写

减少了数据复制
读写操作,频繁的进行 用户态和内核态的数据拷贝
读:从内核态拷贝到用户态
写:从用户态拷贝到内核态
	下载文件的步骤:
		打开文件 读取文件中的数据(内核->用户态)
		把读到的内容发送到客户端 send(用户态->内核态)
	这个过程如果直接用sendfile()   就是零拷贝



高级I/O函数

	pipe			 管道(无名管道)
	dup/dup2    复制文件描述符
	readv/writev  分散读写
	sendfile    零拷贝
	mmap munmap  内存映射
	splice 零拷贝
	tee	  零拷贝
	aio_read/aio_write 异步读写



上下文切换和锁

上下文件切换 进程or线程切换需要系统开销
密集型的I/O服务器 线程会大量频繁的切换 占比CPU时间比例大
	需要减少线程的数量    有半同步/半异步作为解决方案

锁:因为程序并发需要考虑的另外一个问题是
	共享资源的同步问题(加锁保护),所以需要加锁

锁通常被认为是导致服务器效率低下的一个很重要的因素
	不用锁,用其他模式替换
	一定要用锁,减小锁的粒度
	
如果是读大于写的频率的频率,我们可以用读写锁替换互斥锁来提高性能



pthread创建法

chatsvrpthread.c

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <errno.h>
#include <assert.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>


#define NAME_LEN 48
#define MAX_CLIENTS 100
#define MSG_LEN 1024
typedef struct Client{
	int fd;
	struct sockaddr_in addr;
	char name[NAME_LEN];
}Client;

//全局变量 每个线程都会访问 
Client gcls[MAX_CLIENTS] = {};
int size = 0;

pthread_rwlock_t lock; //读写锁

int init_server(const char *ip,unsigned short int port){
	int fd = socket(AF_INET,SOCK_STREAM,0);
	assert(fd != -1);
	struct sockaddr_in addr = {};
	addr.sin_family = AF_INET;
	addr.sin_port = htons(port);
	addr.sin_addr.s_addr = inet_addr(ip);
	socklen_t len = sizeof(addr);
	int ret = bind(fd,(const struct sockaddr*)&addr,len);
	assert(ret != -1);
	ret = listen(fd,MAX_CLIENTS);
	assert(ret != -1);
	return fd;
}

void broadcast(int fd,const char *msg){
	int i;
	pthread_rwlock_rdlock(&lock);
	for(i=0;i<size;i++){
		if(fd != gcls[i].fd){
			send(gcls[i].fd,msg,strlen(msg)+1,0);	
		}	
	}
	pthread_rwlock_unlock(&lock);
}

void remove_client(int fd){
	int i;
	pthread_rwlock_wrlock(&lock);
	for(i=0;i<size;i++){
		if(fd == gcls[i].fd){
			close(fd);
			gcls[i] = gcls[--size];
			break;
		}	
	}
	pthread_rwlock_unlock(&lock);
}
//线程函数
void* trans(void *arg){
	Client cls = *(Client*)arg;
	int ret = recv(cls.fd,cls.name,NAME_LEN,0);
	if(ret <= 0)
		return NULL;
	pthread_rwlock_wrlock(&lock);
	gcls[size++] = cls;
	pthread_rwlock_unlock(&lock);
	char msg[MSG_LEN] = {};
	strcpy(msg,cls.name);
	strcat(msg," 进入了聊天室,大家欢迎!");
	broadcast(cls.fd,msg);//广播信息
	strcpy(msg,cls.name);
	strcat(msg,":");
	int len = strlen(msg);
	while(true){
		ret = recv(cls.fd,msg+len,MSG_LEN-len,0);
		if(ret <= 0){//ret == 0对方客户端关闭
			msg[len-1] = '\0';
			strcat(msg," 退出了聊天室,大家欢送!");
			broadcast(cls.fd,msg);
			remove_client(cls.fd);
			break;//结束该线程了
		}
		broadcast(cls.fd,msg);
	}
	
	return NULL;	
}
void accept_client(int fd){
	Client cls = {};
	socklen_t len = sizeof(cls.addr);
	pthread_t id;
	int ret = 0;
	while(true){
		cls.fd = accept(fd,(struct sockaddr*)&cls.addr,&len);
		assert(cls.fd != -1);
		ret = pthread_create(&id,NULL,trans,(void*)&cls);
		assert(ret == 0);
	}
}

int main(int argc,char *argv[]){
	if(argc < 3){
		printf("%s ip port\n",argv[0]);
		return -1;
	}
	int fd = init_server(argv[1],atoi(argv[2]));
	accept_client(fd);
	return 0;	
}

chatclt.c

客户端沿用,后面对方法的优化都是针对服务器的构建

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <assert.h>
#include <errno.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>

#define NAME_LEN 48
#define MSG_LEN 1024

int connect_server(const char *ip,unsigned short int port){
	int fd = socket(AF_INET,SOCK_STREAM,0);
	assert(fd != -1);
	struct sockaddr_in addr = {};
	addr.sin_family = AF_INET;
	addr.sin_port = htons(port);
	addr.sin_addr.s_addr = inet_addr(ip);
	socklen_t len = sizeof(addr);
	int ret = connect(fd,(const struct sockaddr*)&addr,len);
	assert(ret != -1);
	return fd;
}
void *send_msg(void *arg){
	int fd = (int)arg;
	char msg[MSG_LEN] = {};
	int ret = 0;
	while(true){
		gets(msg);
		ret = send(fd,msg,strlen(msg)+1,0);
		if(ret <= 0)
			break;
	}
	return NULL;
}

void recv_msg(int fd){
	char msg[MSG_LEN] = {};
	int ret = 0;
	while(true){
		ret = recv(fd,msg,MSG_LEN,0);
		if(ret <= 0)
			break;
		printf("%s\n",msg);
	}
}
int main(int argc,char *argv[]){
	if(argc < 3){
		printf("%s ip port\n",argv[0]);
		return -1;
	}
	
	int fd = connect_server(argv[1],atoi(argv[2]));
	char name[NAME_LEN] = {};
	printf("input your name:");
	scanf("%s",name);
	scanf("%*c");
	int ret = send(fd,name,strlen(name)+1,0);
	if(ret <= 0){
		close(fd);
		exit(-1);
	}
	pthread_t id;
	ret = pthread_create(&id,NULL,send_msg,(void*)fd);
	assert(ret == 0);// assert(ret == 0);
	recv_msg(fd);
	close(fd);
	return 0;	
}



select创建法

#include<sys/select.h>
int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timeval* timeout)
参数
nfds:通常被设置为select所监听的所有文件描述符中的最大值+1
	指定被监听的文件描述符的总数
	因为文件描述符从0开始,所以+1
fd_set:文件描述符集合
	fd_set结构体仅包含一个整数数组,该数组的每个元素的每一位(bit)标记一个文件描述符
	fd_set能容纳的文件描述符数量由FD_SETSIZE指定
	限制了select所有处理的文件描述符数量
	类似sigset_t (信号集操作函数)
	
FD_ZERO(fd_set *fdset);//清除所有位
FD_SET(int fd,fd_set *fdset);//设置fd;
FD_CLR(int fd,fd_set *fdest);//清除fd
int FD_ISSET(int fd,fd_set *fdest);//测试有无文件描述符

read_fds,write_fds,excpetfds分别用于记录要监听是否可读可写,异常事件的文件描述符集合
select 函数调用返回时,内核将修改readfds,writefds,excepts文件描述符集合,保留有数据可读、可写、异常的文件描述符

timeout:
	设置select函数的超时时间,select本身就是阻塞的,timevalue可以决定阻塞多少时间
	返回select调用返回后剩余的时间,如果调用失败时timeout不确定
	如果timeout变量成员都为0,则select立即返回
	如果timeout取值为NULL,select将一直阻塞,直到某个文件描述符就绪
	struct timeval{
		long tv_sec;//秒数
		long tv_usec;//微秒数
	}

返回值:
	就绪是种定好的状态(可读/可写和异常)
select成功时返回就绪文件描述符的总和
如果在超时时间内任何文件描述符就绪,返回0
select失败返回-1并设置error
在select等待期间,程序接收到信号,select立即返回-1
errno为EINTR
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <errno.h>
#include <assert.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/select.h>

#define NAME_LEN 48
#define MAX_CLIENTS 100
#define MSG_LEN 1024
typedef struct Client{
	int fd;
	struct sockaddr_in addr;
	char name[NAME_LEN];
}Client;

//全局变量 每个线程都会访问 
Client gcls[MAX_CLIENTS] = {};
int size = 0;

int init_server(const char *ip,unsigned short int port){
	int fd = socket(AF_INET,SOCK_STREAM,0);
	assert(fd != -1);
	struct sockaddr_in addr = {};
	addr.sin_family = AF_INET;
	addr.sin_port = htons(port);
	addr.sin_addr.s_addr = inet_addr(ip);
	socklen_t len = sizeof(addr);
	int ret = bind(fd,(const struct sockaddr*)&addr,len);
	assert(ret != -1);
	ret = listen(fd,MAX_CLIENTS);
	assert(ret != -1);
	return fd;
}

void broadcast(int fd,const char *msg){
	int i;
	for(i=0;i<size;i++){
		if(fd != gcls[i].fd){
			send(gcls[i].fd,msg,strlen(msg)+1,0);	
		}	
	}
}

void remove_client(int fd){
	int i;
	for(i=0;i<size;i++){
		if(fd == gcls[i].fd){
			close(fd);
			gcls[i] = gcls[--size];
			break;
		}	
	}
}

void select_fd(int fd){
	Client cls = {};
	socklen_t len = sizeof(cls.addr);
	int ret = 0,i,cnt;
	fd_set readfds; //可读文件描述符集
	int maxfd = 0;  //记录最大的文件描述符
	while(true){
		FD_ZERO(&readfds);//文件描述符集合清空
		maxfd = fd;
		FD_SET(fd,&readfds);//服务器的fd  用于判断是否有客户端连接
		for(i=0;i<size;i++){
			FD_SET(gcls[i].fd,&readfds);//和客户端交互的文件描述符
			if(gcls[i].fd > maxfd)
				maxfd = gcls[i].fd;
		}
		cnt = select(maxfd+1,&readfds,NULL,NULL,NULL);//阻塞
		if(FD_ISSET(fd,&readfds)){//服务器的fd  有客户端连接上来了
			cls.fd = accept(fd,(struct sockaddr*)&cls.addr,&len);
			if(cls.fd != -1){
				gcls[size++] = cls;	//进入聊天室 没有发网名过来
			}
			--cnt;
		}
		for(i=0;i<size&&cnt>0;i++){
			if(FD_ISSET(gcls[i].fd,&readfds)){//确保有数据可读  线程池
				--cnt;
				char msg[MSG_LEN] = {};
				if(strcmp(gcls[i].name,"")==0){//发送网名
					ret = recv(gcls[i].fd,msg,MSG_LEN,0);
					if(ret <= 0){
						remove_client(gcls[i].fd);
						continue;
					}
					strcpy(gcls[i].name,msg);
					strcat(msg," 进入聊天室,大家欢迎!");
				}else{//发送数据
					strcpy(msg,gcls[i].name);
					strcat(msg,":");
					int len = strlen(msg);
					ret = recv(gcls[i].fd,msg+len,MSG_LEN-len,0);
					if(ret <= 0){
						msg[len-1] = '\0';
						strcat(msg," 退出群聊,大家欢送!");
					}
				}
				broadcast(gcls[i].fd,msg);
				if(ret <= 0){
					remove_client(gcls[i].fd);	
				}
			}	
		}
	}
}

int main(int argc,char *argv[]){
	if(argc < 3){
		printf("%s ip port\n",argv[0]);
		return -1;
	}
	int fd = init_server(argv[1],atoi(argv[2]));
	select_fd(fd);
	return 0;	
}

selet原理:
	把需要监听的文件描述符集合交给内核去测试,保留就绪的文件描述
	
	每一次都需要重新把所有需要监听的文件描述符加入到文件描述符集合中
	所以内核又需要遍历所有的文件描述符
	当select返回之后,还需要循环所有的文件描述符去判断是否就绪
	
	缺陷:
	当文件描述符数量增大时,效率其实是急剧下降的	
	select还受到FD_SETSIZE的限制 
	当select返回之后,没有并行,而是串行,一个一个接受客户端的数据,然后转发
	
因此产生了下一个解决方案 池



poll创建法

#include<poll.h>
int poll(struct pollfd* fds,nfds_t nfds,int timeout)

fds:是一个struct pollfd	结构体类型的数组
struct pollfd{
	int fd;//文件描述符
	short events;//注册的事件 可读 可写
	short revents;//实际发生的事件 有内核填充
};

POLLIN 数据可读
POLLOUT 数据可写 
nfds:
	数组长度
timeout:
	超时等待的毫秒数
	-1 阻塞
	0 立即返回

返回值:
	和select意义相同 就绪状态的文件描述符的个数
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <errno.h>
#include <assert.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <poll.h>

#define NAME_LEN 48
#define MAX_CLIENTS 100
#define MSG_LEN 1024
typedef struct Client{
	int fd;
	struct sockaddr_in addr;
	char name[NAME_LEN];
}Client;

Client gcls[MAX_CLIENTS+1] = {};//gcls[0] 不存储值
struct pollfd fds[MAX_CLIENTS+1] = {};
int size = 0;

int init_server(const char *ip,unsigned short int port){
	int fd = socket(AF_INET,SOCK_STREAM,0);
	assert(fd != -1);
	struct sockaddr_in addr = {};
	addr.sin_family = AF_INET;
	addr.sin_port = htons(port);
	addr.sin_addr.s_addr = inet_addr(ip);
	socklen_t len = sizeof(addr);
	int ret = bind(fd,(const struct sockaddr*)&addr,len);
	assert(ret != -1);
	ret = listen(fd,MAX_CLIENTS);
	assert(ret != -1);
	return fd;
}

void broadcast(int fd,const char *msg){
	int i;
	for(i=1;i<size;i++){
		if(fd != gcls[i].fd){
			send(gcls[i].fd,msg,strlen(msg)+1,0);	
		}	
	}
}

void accept_client(int fd){
	struct sockaddr_in addr = {};
	socklen_t len = sizeof(addr);
	int cfd = accept(fd,(struct sockaddr*)&addr,&len);
	if(cfd != -1){
		Client cls = {};
		cls.fd = cfd;
		cls.addr = addr;
		gcls[size] = cls;
		fds[size].fd = cfd;
		fds[size].events = POLLIN;
		++size;
	}
}

int recv_data(int index){
	int fd = fds[index].fd;	
	char msg[MSG_LEN] = {};
	int ret = 0;
	if(strcmp(gcls[index].name,"")==0){
		ret = recv(fd,msg,MSG_LEN,0);
		if(ret <= 0){
			return 0;
		}
		strcpy(gcls[index].name,msg);
		strcat(msg," 进入聊天室,大家欢迎!");
	}else{
		strcpy(msg,gcls[index].name);
		strcat(msg,":");
		int len = strlen(msg);
		ret = recv(fd,msg+len,MSG_LEN-len,0);
		if(ret <= 0){
			msg[--len] = '\0';
			strcat(msg," 退出聊天室,大家欢送!");	
		}
	}
	broadcast(fd,msg);
	if(ret <= 0)
		return 0;
	return 1;
}
void select_fd(int fd){
	int ret = 0,i,cnt;
	//fds  struct pollfd fds[]  
	fds[0].fd = fd;        //文件描述符
	fds[0].events = POLLIN;//监听的事件
	size = 1;
	while(true){
		cnt = poll(fds,size,-1);//
		for(i=0;i<size&&cnt>0;i++){
			if(fds[i].revents & POLLIN){//fds[i].fd就绪
				--cnt;
				if(fds[i].fd == fd){//客户端连接上来
					accept_client(fd);//接收客户端连接请求				
				}else{//数据
					if(recv_data(i)==0){
						gcls[i] = gcls[size-1];
						fds[i] = fds[size-1];
						--size;
						--i;
					}
				}
			}	
		}
	}
}

int main(int argc,char *argv[]){
	if(argc < 3){
		printf("%s ip port\n",argv[0]);
		return -1;
	}
	int fd = init_server(argv[1],atoi(argv[2]));
	select_fd(fd);
	return 0;	
}



poll和select很相似,但是更高效
因为poll文件描述符只需要加一次到集合中
poll通知用户的方式不一样
select每次都会把没有就绪的文件描述符从集合中删除
poll内核直接设置集合中每个数据的revents属性
通过(fds[i].revents & POLLIN)的值来判断是否就绪

相同的地方在于调用poll/select函数,需要遍历所有的文件描述符
判断是否就绪



epoll创建法

#include<sys/epoll.h>

linux特有的I/O复用函数
epoll有一组函数
epoll把用户关心的的文件描述符上的事件注册到内核的事件表中
不需要像select/poll每次都调用都要重传文件描述符集和事件集
epoll也不需要调用完成之后遍历所有的文件描述符集
epoll需要使用一个额外的文件描述符,用来唯一标识内核中的事件表

int epoll_create(int size);
	size参数,目前不起任何作用
	给内核一个提示,告诉内核事件表需要多大
	返回一个文件描述符,用于标识内核中的事件表

int epoll_ctl(int epfd,int op,int fd,struct epoll_event* event);
参数:
	epfd:epoll_create函数的返回值
	op:
		EPOLL_CTL_ADD:往事件表中注册fd的事件
		EPOLL_CTL_MOD:修改事件表中fd的注册事件
		EPOLL_CTL_DEL:删除事件表中fd的事件

fd:文件描述符
event:注册事件
	struct epoll_event{
		_uint23_t event;//epoll事件   EPOLLIN EPOLLOUT
		epoll_dtat_t data;//用户数据	
};
typedef union epoll_data{
	void *ptr;
	int fd;
	uint32_t u32;
	uint64_t u64;
}epoll_data_t;
返回值,成功返回0,失败返回-1 并设置errno,因为是union所以这里面的参数我们只能使用一个

int epoll_wait(int epfd,struct epoll_event *events,int maxevents,int timeout);
通过调用这个函数返回已经就绪的事件,通过events数组返回
成功返回就绪的文件描述符的个数,events数组中有几个有效数据
events数组 用于接收就绪的文件描述符事件
maxevents:数组的最大长度
timeout:超时等待
	0:立即返回
	-1:阻塞
epoll_wait函数如果检测到事件,就将所有就绪的事件从内核事件表中复制到events指向的数组中
所以events里面都是就绪的事件,极大提高了效率,不用循环去确定是否就绪

epoll两个模式
	LT模式
	电平触发
	poll/select
	如果没有将事件处理,每次epoll_wait都会通知

	ET模式
	边沿触发
	高效工作方式
	一个文件描述符如果有数据可读
	内核会检测到并通知应用程序
	应用程序如果没有立即处理该事件,下一次epoll_wait就不会再次向应该程序通告此事件
	EPOLLET
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <errno.h>
#include <assert.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>

#define NAME_LEN 48
#define MAX_CLIENTS 100
#define MSG_LEN 1024
typedef struct Client{
	int fd;
	struct sockaddr_in addr;
	char name[NAME_LEN];
}Client;

Client gcls[MAX_CLIENTS+1] = {};
int size = 0;

int init_server(const char *ip,unsigned short int port){
	int fd = socket(AF_INET,SOCK_STREAM,0);
	assert(fd != -1);
	struct sockaddr_in addr = {};
	addr.sin_family = AF_INET;
	addr.sin_port = htons(port);
	addr.sin_addr.s_addr = inet_addr(ip);
	socklen_t len = sizeof(addr);
	int ret = bind(fd,(const struct sockaddr*)&addr,len);
	assert(ret != -1);
	ret = listen(fd,MAX_CLIENTS);
	assert(ret != -1);
	return fd;
}

void broadcast(int fd,const char *msg){
	int i;
	for(i=0;i<size;i++){
		if(fd != gcls[i].fd){
			send(gcls[i].fd,msg,strlen(msg)+1,0);	
		}	
	}
}

void accept_client(int fd,int epfd){
	struct sockaddr_in addr = {};
	socklen_t len = sizeof(addr);
	int cfd = accept(fd,(struct sockaddr*)&addr,&len);
	if(cfd != -1){
		Client cls = {};
		cls.fd = cfd;
		cls.addr = addr;
		gcls[size] = cls;
		++size;
		struct epoll_event event = {};
		event.events = EPOLLIN;
		event.data.fd = cfd;
		int ret = epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&event);
		if(ret == -1){
			perror("epoll_ctl");	
		}
	}
}

int recv_data(int fd){
	int index = 0;
	for(;index<size;index++){
		if(gcls[index].fd == fd){
			break;	
		}	
	}
	char msg[MSG_LEN] = {};
	int ret = 0;
	if(strcmp(gcls[index].name,"")==0){
		ret = recv(fd,msg,MSG_LEN,0);
		if(ret <= 0){
			return 0;
		}
		strcpy(gcls[index].name,msg);
		strcat(msg," 进入聊天室,大家欢迎!");
	}else{
		strcpy(msg,gcls[index].name);
		strcat(msg,":");
		int len = strlen(msg);
		ret = recv(fd,msg+len,MSG_LEN-len,0);
		if(ret <= 0){
			msg[--len] = '\0';
			strcat(msg," 退出聊天室,大家欢送!");	
		}
	}
	broadcast(fd,msg);
	if(ret <= 0)
		return 0;
	return 1;
}
void select_fd(int fd){
	int epfd = epoll_create(MAX_CLIENTS);
	if(epfd == -1){
		perror("epoll_create");
		return;	
	}
	struct epoll_event event = {};
	event.events = EPOLLIN;  //读事件
	event.data.fd = fd;     //用户数据
	int ret = epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&event);
	if(ret == -1){
		perror("epoll_ctl");
		return ;
	}
	struct epoll_event events[MAX_CLIENTS+1] = {};
	int i;
	while(true){
		ret = epoll_wait(epfd,events,MAX_CLIENTS+1,-1);
		if(ret == -1){
			perror("epoll_wait");
			break;
		}
		for(i=0;i<ret;i++){
			if(events[i].data.fd == fd){//有客户端连接
				accept_client(fd,epfd);
			}else{//有数据需要接收
				if(events[i].events & EPOLLIN){
					ret = recv_data(events[i].data.fd);
					if(ret == 0){
						struct epoll_event ev = {};
						ev.events = EPOLLIN;
						ev.data.fd = events[i].data.fd;
						ret = epoll_ctl(epfd,EPOLL_CTL_DEL,events[i].data.fd,&ev);
						if(ret == -1){
							perror("epoll_ctl");	
						}
					}
				}
			}
		}
	}
}

int main(int argc,char *argv[]){
	if(argc < 3){
		printf("%s ip port\n",argv[0]);
		return -1;
	}
	int fd = init_server(argv[1],atoi(argv[2]));
	select_fd(fd);
	return 0;	
}



总结

事件集合
	select:
		用户通过三个参数分别传入可读、可写、异常的事件文件描述符集合
		内核 通过对这些参数的在线修改来反馈其中的就绪事件
		使得用户每次调用select函数都需要重置这三个参数
	poll:
		统一处理所有事件类型,因此只需要一个事件集参数
		用户可以通过结构体events传入事件
		内核通过修改结构体的revents来反馈其中就绪的事件
		用户不需要每次都重置events参数
	epoll:
		内核通过一个事件表,直接管理用户注册的所有事件
		因此每次调用epoll_wait时,无须反复传入用户注册的事件
		epoll-wait参数events仅用来反馈就绪的事件
		不需要遍历所有的文件描述符集合

应用程序索引文件描述符的时间复杂度
select O(n)    poll O(n)     epoll O(1)

最大支持的文件描述符(个数)
	select 受多方制约
	poll: 65535
	epoll:65535

工作模式
	select使用LT     poll LT     epoll 默认LT但是支持ET

内核实现和工作效率
	select:	采用轮询方式来检测就绪事件 算法时间复杂度O(n)
	poll: 采用轮询方式来检测就绪事件,算法时间复杂度O(n)
	epoll: 采用回调方式来检测就绪事件,算法时间复杂度为O(1)

常规用法 
复用I/O+线程(或者线程池)



版权声明:本文为weixin_45396618原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。