从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,其有以下缺点:

  1. 调用select时,事件集合需要从内核态拷贝到内核态,返回时,又需要全部从内核态拷贝到用户态。
  2. 需要轮询所有fd
  3. 单个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)相关推荐

  1. socket网络编程--epoll小结

    http://www.cnblogs.com/wunaozai/p/3895860.html 以前使用的用于I/O多路复用为了方便就使用select函数,但select这个函数是有缺陷的.因为它所支持 ...

  2. 深入分析websocket协议,从3个方面设计网络应用层协议丨网络编程|网络IO|epoll|socket|网络协议丨c/c++linux服务器开发

    深入分析websocket协议,从3个方面设计网络应用层协议 视频讲解如下: 深入分析websocket协议,从3个方面设计网络应用层协议丨网络编程|网络IO|epoll|socket|网络协议丨c/ ...

  3. VMware设置静态ip地址及不同网络模式讲解【Linux网络问题】

    VMware设置静态ip地址及不同网络模式讲解 此处的静态IP配置选用的是使用NAT方式连接网络[如果之前配置有错误,可以尝试暴力方法:将虚拟机网络配置重新恢复为默认,然后从头开始配置] 1 将Lin ...

  4. 中级篇——虚拟机网络设置:桥接模式、NAT模式、仅主机模式3种网络模式讲解

    简介 虚拟机中常见的三种网络模式:桥接模式.NAT模式.仅主机模式,各有什么特点?如何设置和选用,本篇文章带你详细了解 新名词 宿主机:虚拟机存放寄托的主机,比如在win10主机中安装了Linux的虚 ...

  5. 【k8s】docker网络模式(必知)

    docker网络部分的视频我看了很多,讲解最透彻的还是https://www.bilibili.com/video/BV123411y7TB?p=8 获取本文方式:见谷粒商城文尾,备注[docker网 ...

  6. linux线程同步 epoll,Linux网络编程--epoll 模型原理详解以及实例

    1.简介 Linux I/O多路复用技术在比较多的TCP网络服务器中有使用,即比较多的用到select函数.Linux 2.6内核中有提高网络I/O性能的新方法,即epoll . epoll是什么?按 ...

  7. Bridge网络模式下Linux虚拟机和主机进行通信

    我的VMware版本是8.0.3.其他版本的设置应该大致相同. 1.注意我们的网络模式是Bridge 2.我们的网卡设置 3.目标虚拟机(Linux)的IP 4.我的主机的IP地址 5.网络调试助手的 ...

  8. 虚拟机3种网络模式(桥接、nat、Host-only)

    实例讲解虚拟机3种网络模式(桥接.nat.Host-only) 前言 很多人安装虚拟机的时候,经常遇到不能上网的问题,而vmware有三种网络模式,对初学者来说也比较眼花聊乱,今天我就来基于虚拟机3种 ...

  9. C#下的Raw Socket编程实现网络封包监视

    谈起socket编程,大家也许会想起QQ和IE,没错.还有许多网络工具如P2P.NetMeeting等在应用层实现的应用程序,也是用socket来实现的.Socket是一个网络编程接口,实现于网络应用 ...

最新文章

  1. C# 3.0 —— 扩展方法
  2. Codeforces Round #272 (Div. 2)
  3. xmlUtil 解析 创建
  4. Hyperledger Fabric1.0架构概览
  5. 关于推荐和机器学习的几个网站
  6. 面向对象三大特性总结
  7. wsl2安装cuda方法——官方教程走不通
  8. 欠定线性系统与正则化
  9. 解决webview无法调用支付宝
  10. TikTok API接口,关键词搜索用户
  11. 网络口碑营销推广怎么能提高消费者的信任
  12. Nuxt.js 如何做SEO优化
  13. 3.Android 仿QQ运动步数进度效果 keep运动效果(从入门到巅峰)
  14. Codeforces Round #800 (Div. 2) E. Keshi in Search of AmShZ
  15. 数据平台权限控制-基于猛犸
  16. sqlite数据库查询语句,数据库中是否存在某个表
  17. idea配置Tomcat时没有Artifacts选项
  18. Mac中iterm2显示彩色
  19. 当世事再没完美可远在岁月如歌中找你
  20. vuex之webApp购物流程实现

热门文章

  1. filebeat使用include_lines时,入库慢的解决方法
  2. 前端基础知识点:JS中的参数传递详解
  3. 网络红人“犀利哥”受聘广东顺德当时装模特
  4. IndentationError: unindent does not match any outer indentation level 解决办法
  5. Hexo在多台电脑上提交和更新
  6. 经典组件大更新,微软为Windows 11重新设计记事本
  7. JS遍历JSON,获取所有key/value
  8. 计算机等级考试一级MSOffice冲刺试题及答案
  9. 谷歌插件 程序包无效:CRX_HEADER_INVALID
  10. index客户主页+页面分页的模糊查询 and add知识