此次讨论的是阻塞的fd
先写一个简单的server端
#include#include #include #include #include #include #include #include #include #include #include #define MAXLEN 4096 int main(){ int listenfd,connfd,n; struct sockaddr_in servaddr; char buf[MAXLEN]; if((listenfd = socket(AF_INET,SOCK_STREAM,0)) == -1)//创建fd { printf("create socket error:%s",strerror(errno)); return -1; } memset(&servaddr,0,sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(8888); if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr)) == -1)//绑定本机地址 { printf("bind socket err:%sn",strerror(errno)); return -1; } if(listen(listenfd,10) == -1)//监听 { printf("listen socket err:%sn",strerror(errno)); return -1; } struct sockaddr_in client; socklen_t len = sizeof(client); printf("----waiting for client requst----n"); if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) //等待客户端连接 { printf("accept socket err:%sn",strerror(errno)); return -1; } while(1) { n = recv(connfd,buf,MAXLEN,0); if(n > 0) { printf("----waiting---n"); buf[n] = ' '; printf("recv msg form client:%sn",buf); send(connfd,buf,n,0); } else if(n == 0) { printf("----close--n"); close(connfd); } } close(listenfd); return 0; }
使用客户端去连接的时候发现,这样只能进行一对一的服务,当有第二个客户端连接时sock在队列里,没有accpet去取出。
针对这种情况,进行一下改进,每有一个客户端请求,创建一个线程来accpet,来进行服务
//C10K
void *client_pth(void* cfd)
{
char buf[MAXLEN];
int connfd = *(int*)cfd;
while(1)
{
int n = recv(connfd,buf,MAXLEN,0);
if(n > 0)
{
buf[n] = ' ';
printf("recv client connfd %d:%sn",connfd,buf);
send(connfd,buf,n,0);
}
else if(n == 0)
{
printf("confd:%dn",connfd);
close(connfd);
break;
}
}
return NULL;
}
printf("----waiting for client request----n");
while(1)
{
printf("--creat--n ");
struct sockaddr_in client;
socklen_t len = sizeof(client);
if((connfd = accept(listenfd,(struct sockaddr*)&client,&len)) == -1)
{
printf("accept socket err:%sn",strerror(errno));
return -1;
}
printf("creat pthreadn");
pthread_t pid;
pthread_create(&pid,NULL,(void*)client_pth,(void*)&connfd);
}
但是问题来了,每来一个新的连接,就创建一个新的线程,当并发数较大时,数据拷贝频繁(缓存I/O、内核将数据拷贝到用户进程空间、阻塞), 进程/线程上下文切换消耗大,很有可能导致操作系统崩溃,这就是我们所说的C10K问题的本质,所以这种方式只适用于并发量较小的局域网或者内部使用。
select由这可以引入一个新方法,那就是select。select就是把所有的fd全部一个函数中去管理,select会阻塞等待某一个fd或多个fd的写事件、读事件或者异常事件触发,从而实现相应事件的操作。
这里我们举个例子,如一栋公寓,里面有很多户人家,他们要收发快递,多线程的方式是这么做的,每户人家都雇一个人专门为他收发快递,如果住户很多,会造成很大的人员开销,于是这些住户商量着,把收发快递的活全部交给一个人统一去管理,这就是select的处理方式
以下只针对读事件和写事件做处理
printf("----waiting for client request----n");
fd_set rfd,rset,wfd,wset;
FD_ZERO(&rfd);
FD_ZERO(&wfd);
FD_SET(listenfd,&rfd);
int max_fd = listenfd;
while(1)
{
printf("start n");
wset = wfd;
rset = rfd;
int num = select(max_fd+1,&rset,&wset,NULL,NULL);//监听每个fd的写事件和读事件
printf("num:%d,max_fd:%dn",num,max_fd);
if(FD_ISSET(listenfd,&rset))//当我们监听的fd有写事件过来时,代表有新的客户端请求
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
if((connfd = accept(listenfd,(struct sockaddr*)&client,&len)) == -1)
{
printf("accept socket err:%sn",strerror(errno));
return -1;
}
FD_SET(connfd,&rfd);
if(connfd > max_fd) max_fd = connfd;
printf("max_fd:%dn",max_fd);
if(--num == 0) continue;
}
for(int i = listenfd+1;i <= max_fd;i++)
{
printf("fd:%dn",i);
if(FD_ISSET(i,&rset))
{
n = recv(i,buf,MAXLEN,0);
if(n > 0)
{
buf[n] = ' ';
printf("recv buf:%sn",buf);
FD_SET(i,&wfd);
//send(i,buf,n,0);
}
else if(n == 0) //n=0代表连接断开
{
FD_CLR(i,&rfd); //读集合里除去这个fd
printf("close socket fd:%dn",i);
close(i);
}
//if(--num == 0) break;
}
else if(FD_ISSET(i,&wset))
{
printf("send fd:%dn",i);
//printf("send buf:%sn",buf);
send(i,buf,n,0);
memset(buf,' ',sizeof(buf));
FD_SET(i,&rfd);
FD_CLR(i,&wfd);//一定要清除写集合
}
}
}
这里有一个小坑,当写事件被触发后,如果不及时清除,会一直触发写事件,这是我不了解写事件的触发条件造成的,当套接口发送缓冲区有空间容纳数据(大多数时候缓冲区是不满的,会不断产生可写事件)所以每次写完之后要及时清除写集合。
但是,select的容纳个数也是有限的,在内核里FD_SET的最大数默认为1024,改的话需要重新编译内核,这不是一个好办法,或者是多创建几个select来扩容,这里先不谈。还有一种方法则是
poll我们知道select是通过一个集合去管理的fd,而集合都是有固定大小的,但poll是通过链表的形式管理,所以没有最大fd的限制。
下面实现poll:
#define POLL_SIZE 1024
printf("----waiting for client request----n");
struct pollfd fds[POLL_SIZE] = {0};
fds[0].fd = listenfd;
fds[0].events = POLLIN;
int max_fd = listenfd;
for(int i = 1; i < POLL_SIZE; i++)
{
fds[i].fd = -1;
}
while(1)
{
int num = poll(fds,max_fd+1,-1);
if(fds[0].revents & POLLIN)
{
struct sockaddr_in client;
socklen_t len;
if((connfd = accept(listenfd,(struct sockaddr*)&client,&len)) == -1)
{
printf("accept socket err:%sn",strerror(errno));
return -1;
}
printf("accpet fd:%dn",connfd);
fds[connfd].fd = connfd;
fds[connfd].events = POLLIN;
if(connfd > max_fd) max_fd = connfd;
if(--num == 0) continue;
}
for(int i = listenfd+1; i <= max_fd; i++)
{
printf("fd:%dn",i);
if(fds[i].revents & POLLIN)
{
n = recv(i,buf,MAXLEN,0);
if(n > 0)
{
buf[n] = ' ';
printf("recv buf:%sn",buf);
fds[i].events = POLLOUT;
}
else if(n == 0)
{
fds[i].fd = -1;
printf("close socket fd:%dn",i);
close(i);
}
if(--num == 0) break;
}
else if(fds[i].revents & POLLOUT)
{
printf("send fd:%dn",i);
send(i,buf,n,0);
//fds[i].events = 0;
fds[i].events = POLLIN;
if(--num == 0) break;
}
}
}
由代码可以看出select和poll的实现方式相差不大,都是通过阻塞等待某一个fd或多个fd的事件触发后,轮询查找相应的fd进行操作。由于每次事件触发,都会去遍历一遍fd,从而导致多次的无用遍历,造成额外的cpu开销。
接着上面收发快递的例子,select和poll的处理方式是由一个快递员统一管理收发快递,但没有告诉快递员某个快递应该送到哪一户,于是快递员只能一家一家的询问,很耗费快递员的精力,如果我们在收发快递的时候告诉快递员该往哪家送,那快递员的活就会轻松不少,这就是下面要实现的
epollepoll把select、poll的主动轮询为被动通知,当有事件发生时,被动接收通知。每个事件都有一个关联的fd,这样每次的事件操作都是有意义的
printf("----waiting for client request----n");
int epfd = epoll_create(1);
struct epoll_event events[POLL_SIZE] = {0};
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = listenfd;
epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);
while(1)
{
int num = epoll_wait(epfd,events,POLL_SIZE,-1);
if (num == -1) continue;
for(int i = 0; i < num; i++)
{
int clientfd = events[i].data.fd;
if(clientfd == listenfd)
{
struct sockaddr_in client;
socklen_t len;
if((connfd = accept(listenfd,(struct sockaddr*)&client,&len)) == -1)
{
printf("accept socket err:%sn",strerror(errno));
return -1;
}
printf("accpet fd:%dn",connfd);
ev.data.fd = connfd;
ev.events = EPOLLIN;
epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);
}
else if(events[i].events & EPOLLIN)
{
n = recv(clientfd,buf,MAXLEN,0);
if(n > 0)
{
buf[n] = ' ';
printf("recv buf:%sn",buf);
ev.data.fd = clientfd;
ev.events = EPOLLOUT;
epoll_ctl(epfd,EPOLL_CTL_MOD,clientfd,&ev);
}
else if(n == 0)
{
ev.data.fd = clientfd;
ev.events = EPOLLHUP;
epoll_ctl(epfd,EPOLL_CTL_DEL,clientfd,&ev);
close(clientfd);
}
}
else if(events[i].events & EPOLLOUT)
{
printf("send fd:%dn",clientfd);
send(clientfd,buf,n,0);
ev.data.fd = clientfd;
ev.events = EPOLLIN;
epoll_ctl(epfd,EPOLL_CTL_MOD,clientfd,&ev);
}
}
}
epoll方法能有效的解决poll和select的一些缺陷,如fd数目限制,cpu开销过大的问题,在海量连接并发的时候起到很大作用。
总结select、poll、epoll这三种都是io多路复用的用法,实际上在recv与send时还是堵塞的,本质上还是同步io,异步io的用法后续在更新



