从socket开始讲解网络模式(epoll)
从socket开始讲解网络模式
windows采用IOCP网络模型,而linux采用epoll网络模型(Linux得以实现高并发,并被作为服务器首选的重要原因),接下来讲下epoll模型对网络编程高并发的作用
简单的socket连接
socket连接交互的流程如图:
服务端中个api的作用:
- socket(IPv4/IPv6,TCP/UDP,0):创建socket套接字,获取
listenfd
- bind(listenfd,服务器地址,服务器地址长度):将套接字绑定服务器地址
- listen(listenfd,size): 该套接字最多连接size个连接
- accept(listenfd,客户端信息,len):客户端使用connect()后,与服务端进行三次握手,三次握手成功后,生成一个连接文件描述符
connfd
- recv(connfd,buff,len,0):从该连接的的buff中读取len字节数据 (对应图中的read())
- send(connfd,buff,n,0): 读完数据后向buff写数据,以回应客户端。(对应图中的write())
原始代码实现:
int main(int argc, char **argv)
{int listenfd, connfd, n;struct sockaddr_in servaddr;char buff[MAXLNE];if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { // 监听tcp连接printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);return 0;}memset(&servaddr, 0, sizeof(servaddr)); // 服务地址servaddr.sin_family = AF_INET;servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 将uint32值转换为网络字节顺序,0.0.0.0系统将调用默认ip地址,为啥不是127.0.0.1呢servaddr.sin_port = htons(9999); // 将整型变量转换成网络字节顺序,通过端口socket绑定到某一进程// 当用socket()函数创建套接字以后,套接字在名称空间(网络地址族)中存在,但没有任何地址给它赋值// bind()把用addr指定的地址赋值给sockfd。addrlen指定了以addr所指向的地址结构体的字节长度。一般来说,该操作称为“给套接字命名”。if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);return 0;}// socket创建套接字=》bind(),给套接字地址=》listen监听连接=》accept// 该监听fd最多连接10个连接if (listen(listenfd, 10) == -1) {printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);return 0;}struct sockaddr_in client;socklen_t len = sizeof(client);if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);return 0;}printf("========waiting for client's request========\n");while (1) {n = recv(connfd, buff, MAXLNE, 0);if (n > 0) {buff[n] = '\0';printf("recv msg from client: %s\n", buff);send(connfd, buff, n, 0);} else if (n == 0) {close(connfd);}}
}
现在加入有两个客户端同时连接服务器,第二个客户端能连接成功,但发送不了数据,只有第一个客户端能发送数据。这是因为第二个客户端发送连接请求后,被服务器监听并成功连接,但是accept只取了第一个客户端的connfd,然后服务器就一直在while(1){}里跑了,只有第一个连接能发送数据。
解决方法,将accept()放入while(1)循环里
int main(int argc, char **argv)
{int listenfd, connfd, n;struct sockaddr_in servaddr;char buff[MAXLNE];if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { // 监听tcp连接printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);return 0;}memset(&servaddr, 0, sizeof(servaddr)); // 服务地址servaddr.sin_family = AF_INET;servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 将uint32值转换为网络字节顺序,0.0.0.0系统将调用默认ip地址,为啥不是127.0.0.1呢servaddr.sin_port = htons(9999); // 将整型变量转换成网络字节顺序,通过端口socket绑定到某一进程// 当用socket()函数创建套接字以后,套接字在名称空间(网络地址族)中存在,但没有任何地址给它赋值// bind()把用addr指定的地址赋值给sockfd。addrlen指定了以addr所指向的地址结构体的字节长度。一般来说,该操作称为“给套接字命名”。if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);return 0;}// socket创建套接字=》bind(),给套接字地址=》listen监听连接=》accept// 该监听fd最多连接10个连接if (listen(listenfd, 10) == -1) {printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);return 0;}printf("========waiting for client's request========\n");while (1) {// 把accept放到while循环里// 新问题:每个连接都能发送数据,但只能发送一条数据// 原因是while中又两个阻塞点:accept和recv,一个连接发送数据后,服务器将阻塞在acceptstruct sockaddr_in client; socklen_t len = sizeof(client);if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);return 0;}n = recv(connfd, buff, MAXLNE, 0);if (n > 0) {buff[n] = '\0';printf("recv msg from client: %s\n", buff);send(connfd, buff, n, 0);} else if (n == 0) {close(connfd);}//close(connfd);}
}
产生了新问题:服务端无法正常接收数据,原因是while中又两个阻塞点:accept和recv,一个连接发送数据后,服务器将阻塞在accept
如:客户端A连接服务器=》客户端B连接服务器=>客户端B发送5次数据“B”(此时服务将无法读取这5个B,因为客户端A连接服务器后,还阻塞在recv(),等待读取A的数据),如果此时 =》 客户端A发送数据"A" => 服务器将会读取A,然后通过accept()获取客户端B的connfd,再读取客户端B之前发送的五个“B”
为每个socket连接设置一个线程
多线程:来一个连接新建一个线程,把connfd传给入口函数,接收发送数据
// 为方便讲解,listen()以上的代码略,最后会有一个整体的代码
int main(){...// 客户端不多,可以用这种方法,客户端太多就不行// 如一个线程分配8M的栈空间,1G的内存只能分配128个线程左右 ,性能突破不了C10Kwhile (1) {struct sockaddr_in client;socklen_t len = sizeof(client);if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);return 0;}pthread_t threadid;pthread_create(&threadid, NULL, client_routine, (void*)&connfd);}
}void *client_routine(void *arg) { // 线程入口函数,参数为connfdint connfd = *(int *)arg;char buff[MAXLNE];while (1) {int n = recv(connfd, buff, MAXLNE, 0);if (n > 0) {buff[n] = '\0';printf("recv msg from client: %s\n", buff);send(connfd, buff, n, 0);} else if (n == 0) {close(connfd);break;}}return NULL;
}
这种方法简单,方便管理,不用担心一个连接阻塞其他连接了,但是有一个问题:线程需要独立的运行栈和其他的开销,一个线程大约是8M的栈空间,4G的内存最多只能支持512个连接,无法达到C10K级的并发。
select网络模型
多线程的方式并发量上不去,能不能用少数的线程处理所有fd呢?
select: 当一个fd接收到数据时,服务器能知道是哪个连接发的数据,并进行处理,其处理流程为(select用的很少,可直接看下一节的poll网络模型):
- 创建事件集合,事件分为三类:可读、可写、异常,集合为长度为1024的bit-set,当有事件发生时,将对应的bit位设置为1
- 进入while(1){}循环
- 调用select(),将所有fd拷贝到内核,select会轮询所有fd是否有事件触发,触发了就置为1,并分别把(可读、可写、异常)事件从内核返回到用户态,这里可只考虑可读事件
- 对fd进行进行处理:socket中的fd分为两类listenfd和connfd,需分别处理
- listenfd对应的bit-set[listenfd]为1,且为可读事件时,说明服务器监听到了新的连接,需要将该connfd加入到事件集合中
- listenfd是一个bit-map的做法,01固定,为保准输入输出,从3开始递增分配(3,4,5,6),如果4回收了,再从4分配,所以listenfd比所有connfd都小
- 遍历bit-set,判断事件类型做出处理,如bit-set[connfd]=1,表示该fd有可读事件,就recv()读取数据,并send()响应客户端
代码如下,注意下select()函数参数的意义:
int main(){// 对fd的处理包括两部分,listenfd和读写fd// 因为首先要将rset拷贝到内核,再全部拷贝出来,开销太大fd_set rfds, rset; FD_ZERO(&rfds); // 先把bit-set清空FD_SET(listenfd, &rfds); // 将listenfd加入 rfds读事件集合 int max_fd = listenfd; // 当前管理的所有文件描述符的最大值,也就bit-set的长度while (1) {rset = rfds; // 为啥还要弄这两个变量:防止读集合rfds在select被修改了// 第二、三、四个参数:要管理哪些文件描述符的读、写、异常的事件,放到相应的集合// 第五个超时时间:如果隔这么久一直没有事件发生,就返回,为NULL就是没有事件一直阻塞(select自带阻塞)// 调用select需要把fd从用户态拷贝到内核态,而且需要在内核遍历传递进来的所有fd// 把rfds给内核,rset是返回给用户的发生事件的文件描述符int nready = select(max_fd+1, &rset, &wset, NULL, NULL); // 返回事件的数量,这里其实只有读事件if (FD_ISSET(listenfd, &rset)) { // listenfd是否在读事件集合中struct sockaddr_in client;socklen_t len = sizeof(client);if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) { // 将connfd加入到读事件集合printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);return 0;}FD_SET(connfd, &rfds); // 将connfd加入读事件集合if (connfd > max_fd) max_fd = connfd;if (--nready == 0) continue; // 全部加完了,去对事件进行操作}int i = 0;for (i = listenfd+1;i <= max_fd;i ++) { // 遍历所有fdif (FD_ISSET(i, &rset)) { // n = recv(i, buff, MAXLNE, 0);if (n > 0) {buff[n] = '\0';printf("recv msg from client: %s\n", buff);send(i, buff, n, 0);} else if (n == 0) { //FD_CLR(i, &rfds); // 从读事件集合删除close(i);}if (--nready == 0) break;} }}
}
一个select能管理1024个fd,那么多弄几个select(一个进程或线程一个select),就能到达C10K了,但很难达到C1000K,其有以下缺点:
- 调用select时,事件集合需要从内核态拷贝到内核态,返回时,又需要全部从内核态拷贝到用户态。
- 需要轮询所有fd
- 单个select支持的fd太少了,默认为1024
poll网络模型
和select模型很像,区别就是用pollfd结构(fd的数量可自定义)代替了select的bit_set结构,
pollfd结构为:
struct pollfd{int fd; /* File descriptor to poll. */short int events; /* Types of events poller cares about. */short int revents; /* Types of events that actually occurred. */};
其处理流程为:
- 定义pollfd列表,其中第一个元素为listenfd
- 初始化每个列表元素,fd为-1,event为(POLLIN、POLLOUT、POLLPRI等),select将事件分为三类,poll统一管理
- 进入while(1){}循环
- 接下里的对fd的处理流程和select一样了
- 调用
poll()
, 把fd都拷贝到内核,轮询后拷贝回用户态 - 如果listenfd有可读事件发生,将connfd加入到poll
- 遍历所有fd,如果fd有可读事件发生,recv()读取数据并send()响应客户端,数据读取完成后关闭connfd
- 调用
代码实现:
int main(){...struct pollfd fds[POLL_SIZE] = {0}; // fd的数量可自定义fds[0].fd = listenfd; // 先将listenfd加入pollfds[0].events = POLLIN; // select将事件分为三类,poll将这三类事件统一管理int max_fd = listenfd;int i = 0;for (i = 1;i < POLL_SIZE;i ++) {fds[i].fd = -1; // 将poll中的fd置为-1}while (1) {int nready = poll(fds, max_fd+1, -1); // 把fd拷贝到内核,再拷贝出来if (fds[0].revents & POLLIN) { // 如果listenfd在poll中,且有可读事件发生(也就是来连接了),revents实际发生的事件,pollout为可写事件struct sockaddr_in client;socklen_t len = sizeof(client); // 取connfdif ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);return 0;}printf("accept \n");fds[connfd].fd = connfd; // 将connfd加入pollfds[connfd].events = POLLIN;if (connfd > max_fd) max_fd = connfd;if (--nready == 0) continue;}//int i = 0;for (i = listenfd+1;i <= max_fd;i ++) {if (fds[i].revents & POLLIN) { // fd i 发生了且为POLLIN类型n = recv(i, buff, MAXLNE, 0);if (n > 0) {buff[n] = '\0';printf("recv msg from client: %s\n", buff);send(i, buff, n, 0);} else if (n == 0) { // 无数据可读后,关闭该connfdfds[i].fd = -1;close(i);}if (--nready == 0) break;}}}
}
poll解决的问题:没有最大文件描述符数量的限制
。
但fd在内核态与用户态的来回拷贝,以及需要轮询所有fd,使得其监听事件的开销过大,无法支持太大的并发量,且poll不像select可以跨平台,其只能在Linux平台使用
epoll网络模型
select和poll都是只需调用一个函数,epoll需要调用三个:epoll_create、epoll_ctl(ADD, DEL, MOD)、epoll_wait
epoll结构为:
struct epoll_event
{uint32_t events; /* Epoll events */epoll_data_t data; /* User data variable */
} __EPOLL_PACKED;
其处理流程为:
- epoll_create(): 创建epfd,创建红黑树以及就绪列表(链表)
- 将listendfd绑定可读事件,加入epoll (listenfd和connfd都只需要从用户态拷贝到内核态一次了)
- 进入while(1){}循环
- epoll_wait():将就绪列表从内核态拷贝到用户态(从内核态拷贝到用户态只要拷贝就绪的事件了)
- 遍历就绪列表(不需要遍历全部fd了)
- 如果是listenfd有可读事件,将connfd加入到epoll中
- 如果是connfd有可读事件,读取数据,并send(),读完了从epoll中移除,并关闭该fd
代码实现:
int main(){int epfd = epoll_create(1); //int size(为了兼容,以前就绪队列是固定的,后面改成链表了) 创建epfdstruct epoll_event events[POLL_SIZE] = {0}; // 这里POLL_SIZE就是每次取事件的最大数量,小一点没关系(如50),因为即使百万并发,活跃的也就1w,多跑几次了就是了struct epoll_event ev;ev.events = EPOLLIN;ev.data.fd = listenfd;epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev); // 将listenfd加入epoll,拷贝到内核:只需要拷贝一次,不需要拷贝出来while (1) {int nready = epoll_wait(epfd, events, POLL_SIZE, 5); // 取事件, 拷贝到用户态:只拷贝就绪的事件了if (nready == -1) {continue;}int i = 0;// 遍历就绪队列for (i = 0;i < nready;i ++) {int clientfd = events[i].data.fd;if (clientfd == listenfd) { // 处理listenfd.如果监听到了多个连接,也只一个一个地取,多取几次就是了struct sockaddr_in client;socklen_t len = sizeof(client);if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);return 0;}printf("accept\n");ev.events = EPOLLIN;ev.data.fd = connfd;epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);} else if (events[i].events & EPOLLIN) { // 处理connfdn = recv(clientfd, buff, MAXLNE, 0);if (n > 0) {buff[n] = '\0';printf("recv msg from client: %s\n", buff);send(clientfd, buff, n, 0);} else if (n == 0) { // 读完了就从epoll中移除connfdev.events = EPOLLIN;ev.data.fd = clientfd;epoll_ctl(epfd, EPOLL_CTL_DEL, clientfd, &ev);close(clientfd);}}}}close(listenfd);return 0;
}
epoll解决了select面临的三大问题,可实现C1000K的并发量
完整代码
#include <errno.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>#include <sys/select.h>
#include <sys/poll.h>
#include <sys/epoll.h>#include <pthread.h>#define MAXLNE 4096
#define POLL_SIZE 1024//8m * 4G = 128 , 512
//C10k
void *client_routine(void *arg) { //int connfd = *(int *)arg;char buff[MAXLNE];while (1) {int n = recv(connfd, buff, MAXLNE, 0);if (n > 0) {buff[n] = '\0';printf("recv msg from client: %s\n", buff);send(connfd, buff, n, 0);} else if (n == 0) {close(connfd);break;}}return NULL;
}int main(int argc, char **argv)
{int listenfd, connfd, n;struct sockaddr_in servaddr;char buff[MAXLNE];if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { // 监听tcp连接printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);return 0;}memset(&servaddr, 0, sizeof(servaddr)); // 服务地址servaddr.sin_family = AF_INET;servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 将uint32值转换为网络字节顺序,0.0.0.0系统将调用默认ip地址,为啥不是127.0.0.1呢servaddr.sin_port = htons(9999); // 将整型变量转换成网络字节顺序,通过端口socket绑定到某一进程// 当用socket()函数创建套接字以后,套接字在名称空间(网络地址族)中存在,但没有任何地址给它赋值// bind()把用addr指定的地址赋值给sockfd。addrlen指定了以addr所指向的地址结构体的字节长度。一般来说,该操作称为“给套接字命名”。if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);return 0;}// socket创建套接字=》bind(),给套接字地址=》listen监听连接=》accept// 该监听fd最多连接10个连接if (listen(listenfd, 10) == -1) {printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);return 0;}#if 0struct sockaddr_in client;socklen_t len = sizeof(client);if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);return 0;}printf("========waiting for client's request========\n");while (1) {n = recv(connfd, buff, MAXLNE, 0);if (n > 0) {buff[n] = '\0';printf("recv msg from client: %s\n", buff);send(connfd, buff, n, 0);} else if (n == 0) {close(connfd);}//close(connfd);}// tip: 至此,都两个客户端同时连接accept时,第二个客户端只能连接成功,但发送不了数据// 这是因为accept只取了一个连接,只有第一个连接能发送数据
#elif 0printf("========waiting for client's request========\n");while (1) {// 把accept放到while循环里// 新问题:每个连接都能发送数据,但只能发送一条数据// 原因是while中又两个阻塞点:accept和recv,一个连接发送数据后,服务器将阻塞在acceptstruct sockaddr_in client; socklen_t len = sizeof(client);if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);return 0;}n = recv(connfd, buff, MAXLNE, 0);if (n > 0) {buff[n] = '\0';printf("recv msg from client: %s\n", buff);send(connfd, buff, n, 0);} else if (n == 0) {close(connfd);}//close(connfd);}#elif 0// 多线程:来一个连接新建一个线程,把connfd传给入口函数,接收发送数据// 客户端不多,可以用这种方法,客户端太多就不行// 如一个线程分配8M的栈空间,1G的内存只能分配128个线程左右 ,性能突破不了C10Kwhile (1) {struct sockaddr_in client;socklen_t len = sizeof(client);if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);return 0;}pthread_t threadid;pthread_create(&threadid, NULL, client_routine, (void*)&connfd);}#elif 0// 所以能不能用少数的线程处理所有fd呢 // 当一个fd接收到数据时,服务器能知道是哪个连接发的数据,并进行处理// // fd_set就是个bit-set,第n个bit有事件到达,就将第n个bit位置1// 由于是bit-set,能监听的fd是固定的,要改还得去内核改,默认是1024// 对fd的处理包括两部分,listenfd和读写fd// 一个select能管理1024个fd,那么多弄几个select(一个进程或线程一个select),就能到达C10K了,但很难达到C1000K// 因为首先要将rset拷贝到内核,再全部拷贝出来,开销太大fd_set rfds, rset, wfds, wset; FD_ZERO(&rfds); // 先把bit-set清空// 设置listenfd-set (也就是我们要监控哪些集合,这个集合copy到内核);还有个集合是哪些fd置1了(这个集合从内核copy出来)FD_SET(listenfd, &rfds); // 将listenfd加入 rfds读事件集合 FD_ZERO(&wfds); // 写事件集合int max_fd = listenfd; // 当前管理的所有文件描述符的最大值,也就bit-set的长度while (1) {rset = rfds; // 为啥还要弄这两个变量:防止读集合rfds在select被修改了wset = wfds;// 第二、三个参数:要管理哪些文件描述符的读(写)的事件,放到相应的集合// 第四个是异常事件,第五个超时时间:如果隔这么久一直没有事件发生,就返回,为空就是没有事件一直阻塞(select自带阻塞)// 调用select需要把fd从用户态拷贝到内核态,而且需要在内核遍历传递进来的所有fd// 把rfds和wfds给内核,rset和wset是返回给用户的发生事件的文件描述符int nready = select(max_fd+1, &rset, &wset, NULL, NULL); // 返回事件的数量,这里其实只有读事件if (FD_ISSET(listenfd, &rset)) { // listenfd是否在读事件集合中struct sockaddr_in client;socklen_t len = sizeof(client);if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) { // 将connfd加入到读事件集合printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);return 0;}FD_SET(connfd, &rfds); // 将connfd加入读事件集合if (connfd > max_fd) max_fd = connfd;if (--nready == 0) continue; // 全部加完了,去对读写事件进行操作}int i = 0;// listenfd是一个bit-map的做法,01固定,从3开始递增分配(3,4,5,6),如果4回收了,再从4分配for (i = listenfd+1;i <= max_fd;i ++) { // 遍历所有fd,依次进行读写操作,,这里不应该是可读可写事件集合吗if (FD_ISSET(i, &rset)) { // n = recv(i, buff, MAXLNE, 0);if (n > 0) {buff[n] = '\0';printf("recv msg from client: %s\n", buff);FD_SET(i, &wfds); // 数据读完要加入写事件集合??一次没读完怎么办?//reactor//send(i, buff, n, 0);} else if (n == 0) { //FD_CLR(i, &rfds); // 从读事件集合删除//printf("disconnect\n");close(i);}if (--nready == 0) break;} else if (FD_ISSET(i, &wset)) {send(i, buff, n, 0);FD_SET(i, &rfds); // 发送完了这个fd为啥要加入读事件集合,为啥不从写事件集合删除??}}}#elif 0struct pollfd fds[POLL_SIZE] = {0}; // fd的数量可自定义fds[0].fd = listenfd; // 先将listenfd加入pollfds[0].events = POLLIN; // select将事件分为三类,poll将这三类事件统一管理int max_fd = listenfd;int i = 0;for (i = 1;i < POLL_SIZE;i ++) {fds[i].fd = -1; // 将poll中的fd置为-1}while (1) {int nready = poll(fds, max_fd+1, -1); // 把fd拷贝到内核,再拷贝出来if (fds[0].revents & POLLIN) { // 如果listenfd在poll中,且有可读事件发生(也就是来连接了),revents实际发生的事件,pollout为可写事件struct sockaddr_in client;socklen_t len = sizeof(client); // 取connfdif ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);return 0;}printf("accept \n");fds[connfd].fd = connfd; // 将connfd加入pollfds[connfd].events = POLLIN;if (connfd > max_fd) max_fd = connfd;if (--nready == 0) continue;}//int i = 0;for (i = listenfd+1;i <= max_fd;i ++) {if (fds[i].revents & POLLIN) { // fd i 发生了且为POLLIN类型n = recv(i, buff, MAXLNE, 0);if (n > 0) {buff[n] = '\0';printf("recv msg from client: %s\n", buff);send(i, buff, n, 0);} else if (n == 0) { // 无数据可读后,关闭该connfdfds[i].fd = -1;close(i);}if (--nready == 0) break;}}}
#elseint epfd = epoll_create(1); //int size(为了兼容,以前就绪队列是固定的,后面改成链表了) 创建epfdstruct epoll_event events[POLL_SIZE] = {0}; // 这里POLL_SIZE就是就绪队列的大小了,小一点没关系(如50),因为即使百万并发,活跃的也就1w,多跑几次了就是了struct epoll_event ev;ev.events = EPOLLIN;ev.data.fd = listenfd;epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev); // 将listenfd加入epoll,拷贝到内核:只需要拷贝一次,不需要拷贝出来while (1) {int nready = epoll_wait(epfd, events, POLL_SIZE, 5); // 取事件, 拷贝到用户态:只拷贝就绪的事件了if (nready == -1) {continue;}int i = 0;// 遍历就绪队列for (i = 0;i < nready;i ++) {int clientfd = events[i].data.fd;if (clientfd == listenfd) { // 处理listenfdstruct sockaddr_in client;socklen_t len = sizeof(client);if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);return 0;}printf("accept\n");ev.events = EPOLLIN;ev.data.fd = connfd;epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);} else if (events[i].events & EPOLLIN) { // 处理connfdn = recv(clientfd, buff, MAXLNE, 0);if (n > 0) {buff[n] = '\0';printf("recv msg from client: %s\n", buff);send(clientfd, buff, n, 0);} else if (n == 0) { // 读完了就从epoll中移除connfdev.events = EPOLLIN;ev.data.fd = clientfd;epoll_ctl(epfd, EPOLL_CTL_DEL, clientfd, &ev);close(clientfd);}}}}#endifclose(listenfd);return 0;
}
从socket开始讲解网络模式(epoll)相关推荐
- socket网络编程--epoll小结
http://www.cnblogs.com/wunaozai/p/3895860.html 以前使用的用于I/O多路复用为了方便就使用select函数,但select这个函数是有缺陷的.因为它所支持 ...
- 深入分析websocket协议,从3个方面设计网络应用层协议丨网络编程|网络IO|epoll|socket|网络协议丨c/c++linux服务器开发
深入分析websocket协议,从3个方面设计网络应用层协议 视频讲解如下: 深入分析websocket协议,从3个方面设计网络应用层协议丨网络编程|网络IO|epoll|socket|网络协议丨c/ ...
- VMware设置静态ip地址及不同网络模式讲解【Linux网络问题】
VMware设置静态ip地址及不同网络模式讲解 此处的静态IP配置选用的是使用NAT方式连接网络[如果之前配置有错误,可以尝试暴力方法:将虚拟机网络配置重新恢复为默认,然后从头开始配置] 1 将Lin ...
- 中级篇——虚拟机网络设置:桥接模式、NAT模式、仅主机模式3种网络模式讲解
简介 虚拟机中常见的三种网络模式:桥接模式.NAT模式.仅主机模式,各有什么特点?如何设置和选用,本篇文章带你详细了解 新名词 宿主机:虚拟机存放寄托的主机,比如在win10主机中安装了Linux的虚 ...
- 【k8s】docker网络模式(必知)
docker网络部分的视频我看了很多,讲解最透彻的还是https://www.bilibili.com/video/BV123411y7TB?p=8 获取本文方式:见谷粒商城文尾,备注[docker网 ...
- linux线程同步 epoll,Linux网络编程--epoll 模型原理详解以及实例
1.简介 Linux I/O多路复用技术在比较多的TCP网络服务器中有使用,即比较多的用到select函数.Linux 2.6内核中有提高网络I/O性能的新方法,即epoll . epoll是什么?按 ...
- Bridge网络模式下Linux虚拟机和主机进行通信
我的VMware版本是8.0.3.其他版本的设置应该大致相同. 1.注意我们的网络模式是Bridge 2.我们的网卡设置 3.目标虚拟机(Linux)的IP 4.我的主机的IP地址 5.网络调试助手的 ...
- 虚拟机3种网络模式(桥接、nat、Host-only)
实例讲解虚拟机3种网络模式(桥接.nat.Host-only) 前言 很多人安装虚拟机的时候,经常遇到不能上网的问题,而vmware有三种网络模式,对初学者来说也比较眼花聊乱,今天我就来基于虚拟机3种 ...
- C#下的Raw Socket编程实现网络封包监视
谈起socket编程,大家也许会想起QQ和IE,没错.还有许多网络工具如P2P.NetMeeting等在应用层实现的应用程序,也是用socket来实现的.Socket是一个网络编程接口,实现于网络应用 ...
最新文章
- C# 3.0 —— 扩展方法
- Codeforces Round #272 (Div. 2)
- xmlUtil 解析 创建
- Hyperledger Fabric1.0架构概览
- 关于推荐和机器学习的几个网站
- 面向对象三大特性总结
- wsl2安装cuda方法——官方教程走不通
- 欠定线性系统与正则化
- 解决webview无法调用支付宝
- TikTok API接口,关键词搜索用户
- 网络口碑营销推广怎么能提高消费者的信任
- Nuxt.js 如何做SEO优化
- 3.Android 仿QQ运动步数进度效果 keep运动效果(从入门到巅峰)
- Codeforces Round #800 (Div. 2) E. Keshi in Search of AmShZ
- 数据平台权限控制-基于猛犸
- sqlite数据库查询语句,数据库中是否存在某个表
- idea配置Tomcat时没有Artifacts选项
- Mac中iterm2显示彩色
- 当世事再没完美可远在岁月如歌中找你
- vuex之webApp购物流程实现
热门文章
- filebeat使用include_lines时,入库慢的解决方法
- 前端基础知识点:JS中的参数传递详解
- 网络红人“犀利哥”受聘广东顺德当时装模特
- IndentationError: unindent does not match any outer indentation level 解决办法
- Hexo在多台电脑上提交和更新
- 经典组件大更新,微软为Windows 11重新设计记事本
- JS遍历JSON,获取所有key/value
- 计算机等级考试一级MSOffice冲刺试题及答案
- 谷歌插件 程序包无效:CRX_HEADER_INVALID
- index客户主页+页面分页的模糊查询 and add知识