多线程实现并发访问
可以采用一用户一线程的方式来解决阻塞问题,针对每个用户的连接请求,主线程用于接受连接,而recv与send这种业务需求就开一个线程来解决,以下代码实现了为每一个访问的用户分配一个线程长期为他服务:
void proccess_client(void* arg){
int fd = *(int*)arg
while(1){
char buff[1024] = {0}
int count = recv(fd,buff,1024,0);
send(fd,buff,count,0);
}
}
while (1) {
printf("accept\n");
int clientfd = accept(sockfd, NULL,NULL);
printf("accept finshed: %d\n", clientfd);
pthread_t thid;
pthread_create(&thid, NULL, proccess_client, &clientfd);
}
select轮询
可以使用select函数进行操作,使用select主要流程为,创建一个fd_set集合,然后将serverfd放进去,然后执行select函数,每次执行select会将fd集合拷贝到内核中,如果说有就绪的FD,则返回一个有多少FD就绪的值,然后,使用FD_ISSET判断目标FD是否在就绪队列里面。代码如下:
fd_set rfds, rset;
FD_ZERO(&rfds);
FD_SET(sockfd, &rfds);
int maxfd = sockfd;
while (1) {
rset = rfds;
int nready = select(maxfd+1, &rset, NULL, NULL, NULL);
if (FD_ISSET(sockfd, &rset)) { // accept
int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
printf("accept finshed: %d\n", clientfd);
FD_SET(clientfd, &rfds); //
if (clientfd > maxfd) maxfd = clientfd;
}
int i = 0;
for (i = sockfd+1; i <= maxfd;i ++) { // i fd
if (FD_ISSET(i, &rset)) {
// recv
}
}
}
poll轮询
poll与select类似,不过其参数相比较select更少,使用起来更加方便,其原理同样是将集合中的fd通过内核调用copy_from_user拷贝到内核中,然后进行轮询查看是否有就绪的fd。代码如下:
struct pollfd fds[1024] = {0};
fds[serverfd].fd = serverfd;
fds[serverfd].events = POLLIN;
int maxfd = serverfd;
while (1) {
int nready = poll(fds, maxfd+1, -1);
if (fds[serverfd].revents & POLLIN) {
//如果说serverfd是就绪状态,可以执行accept获取连接
}
int i = 0;
for (i = serverfd+1; i <= maxfd;i ++) { // i fd
if (fds[i].revents & POLLIN) {
//用户触发事件,在这里进行处理
}
}
epoll
在高并发的情况下,如果使用select或poll,那么如果有百万级连接,就需要每次将百万数量的FD资源拷贝到内核中并进行遍历,但实际上,一个网络服务中,即使有百万用户,同一时刻正在发包的用户也绝对不会有这么多,那么,轮询遍历所有监听FD的方式就造成了极大的浪费,但来不及为性能浪费而哀悼,随之而来的是epoll,与select与poll使用轮询的方式来收集就绪FD不一样,epoll采用了事件驱动的形式,这也是reactor的理念,epoll使用一个FD整集与一个FD就绪队列,其中,所有就绪的FD都会被存放进就绪队列中而不是去遍历监听队列,其中,这个FD整集使用了红黑树,因为红黑树是一种强查找的数据结构。就绪队列使用了双向链表,因为对于每一个就绪的节点,只需要执行就可以了,并不依赖于查找,epoll代码如下:
int epfd = epoll_create(1);
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = sockfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);
while (1) {
struct epoll_event events[1024] = {0};
int nready = epoll_wait(epfd, events, 1024, -1);
int i = 0;
for (i = 0;i < nready;i ++) {
int connfd = events[i].data.fd;
if (connfd == sockfd) {
int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
ev.events = EPOLLIN;
ev.data.fd = clientfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &ev);
} else if (events[i].events & EPOLLIN) {
//处理就绪可读状态的FD
}
}
}
以上代码实现了一个epoll的server服务器,当有客户端进行连接时返回FD就绪数量,并且为每一个clientFD添加EPOLLIN事件,当fd可读时进入就绪队列。
epoll原理解析
首先我们需要知道,EPOLL存在两种触发机制:
EPOLLLT(水平触发):水平触发的机制为,只要数据没有处理完,就一直触发,直到处理完为止。
EPOLLET(边沿触发):边沿触发,只会在就绪时触发一次,比如说用户向服务器发送了3个包,那么此时就只会触发一次,想要读取完数据就需要使用循环。
//EPOLLLT
if (events[i].events & EPOLLIN) {
recv(...)
}
//EPOLLET
if (events[i].events & EPOLLIN) {
while(1){
int count = recv(...)
if(count == 0){
break;
}
}
}
在了解了epoll触发机制之后,我们需要知道epoll的数据结构,分别是eventpoll、epitem、epoll_entry
eventpoll
eventpoll是调用epoll_create之后内核创建的epoll实例,当执行epoll_ctl时,会分配一个红黑树节点epitem,并添加等待事件到socket的等待队列,并将epitem插入到红黑树中,对象内核代码结构体如下:
struct eventpoll {
spinlock_t lock;
struct mutex mtx;
/* Wait queue used by sys_epoll_wait() */
wait_queue_head_t wq;
/* Wait queue used by file->poll() */
wait_queue_head_t poll_wait;
/* List of ready file descriptors */
//这个链表存放了所有就绪的FD资源
struct list_head rdllist;
/* RB tree root used to store monitored fd structs */
//所有被加入进epoll监视的fd都会被存放进这个红黑树中
struct rb_root rbr;
struct epitem *ovflist;
struct wakeup_source *ws;
struct user_struct *user;
struct file *file;
int visited;
struct list_head visited_list_link;
};
从代码中可知,epoll中被监视的FD存放在红黑树结构中,相比于select与poll将FD整集拷贝到内核中进行遍历,epoll则不需要遍历,而是将就绪的fd放入就绪链表中,然后对就绪链表中的资源进行操作即可。
epitem
当调用epoll_ctl添加FD时,内核会创建一个epitem,并将这个item作为红黑树中的一个节点,epitem内核源码如下:
struct epitem {
/* RB tree node used to link this structure to the eventpoll RB tree */
struct rb_node rbn;
/* List header used to link this structure to the eventpoll ready list */
struct list_head rdllink;
/*
* Works together "struct eventpoll"->ovflist in keeping the
* single linked chain of items.
*/
struct epitem *next;
/* The file descriptor information this item refers to */
struct epoll_filefd ffd;
/* Number of active wait queue attached to poll operations */
int nwait;
/* List containing poll wait queues */
struct list_head pwqlist;
/* The "container" of this item */
struct eventpoll *ep;
/* List header used to link this item to the "struct file" items list */
struct list_head fllink;
/* wakeup_source used when EPOLLWAKEUP is set */
struct wakeup_source __rcu *ws;
/* The structure that describe the interested events and the source fd */
struct epoll_event event;
};
epoll_entry
每次当一个 fd 关联到一个 epoll 实例,就会有一个 eppoll_entry 产生。eppoll_entry 的结构如下:
/* Wait structure used by the poll hooks */
struct eppoll_entry {
/* List header used to link this structure to the "struct epitem" */
struct list_head llink;
/* The "base" pointer is set to the container "struct epitem" */
struct epitem *base;
/*
* Wait queue item that will be linked to the target file wait
* queue head.
*/
wait_queue_t wait;
/* The wait queue head that linked the "wait" wait queue item */
wait_queue_head_t *whead;
};