从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,一个连接发送数据后,服务器将阻塞在accept
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;
}
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个线程左右 ,性能突破不了C10K
while (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) { // 线程入口函数,参数为connfd
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;
}
这种方法简单,方便管理,不用担心一个连接阻塞其他连接了,但是有一个问题:线程需要独立的运行栈和其他的开销,一个线程大约是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 ++) { // 遍历所有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);
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加入poll
fds[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); // 取connfd
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");
fds[connfd].fd = connfd; // 将connfd加入poll
fds[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) { // 无数据可读后,关闭该connfd
fds[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(为了兼容,以前就绪队列是固定的,后面改成链表了) 创建epfd
struct 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) { // 处理connfd
n = 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中移除connfd
ev.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 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);
}
//close(connfd);
}
// tip: 至此,都两个客户端同时连接accept时,第二个客户端只能连接成功,但发送不了数据
// 这是因为accept只取了一个连接,只有第一个连接能发送数据
#elif 0
printf("========waiting for client's request========\n");
while (1) {
// 把accept放到while循环里
// 新问题:每个连接都能发送数据,但只能发送一条数据
// 原因是while中又两个阻塞点:accept和recv,一个连接发送数据后,服务器将阻塞在accept
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;
}
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个线程左右 ,性能突破不了C10K
while (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 0
struct pollfd fds[POLL_SIZE] = {0}; // fd的数量可自定义
fds[0].fd = listenfd; // 先将listenfd加入poll
fds[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); // 取connfd
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");
fds[connfd].fd = connfd; // 将connfd加入poll
fds[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) { // 无数据可读后,关闭该connfd
fds[i].fd = -1;
close(i);
}
if (--nready == 0) break;
}
}
}
#else
int epfd = epoll_create(1); //int size(为了兼容,以前就绪队列是固定的,后面改成链表了) 创建epfd
struct 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) { // 处理connfd
n = 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中移除connfd
ev.events = EPOLLIN;
ev.data.fd = clientfd;
epoll_ctl(epfd, EPOLL_CTL_DEL, clientfd, &ev);
close(clientfd);
}
}
}
}
#endif
close(listenfd);
return 0;
}