高并发服务器模型

高并发服务器模型

1.高并发服务器模型--select

我们知道实现服务器的高并发,可以用多线程或多进程去实现。但还可以利用多路IO技术:select来实现,它可以同时监听多个文件描述符,将监控的文件描述符交给内核去处理。

现在来看看这个函数的原型:

c 复制代码
int select(int nfds,fd_set*readflds,fd_writefds,set*exceptfds,struct timeval*timeout)

nfds:最大的文件描述符+1;
readflds:读集合,是一个传入传出参数;
传入:指的是告诉内核哪些文件描述符需要监控;
传出:指的是告诉内核那些文件描述符发生了变化;

writefds:写文件描述符的集合(传入传出参数);
execptfds:异常文件描述符集合(传入传出参数);

timeout(超出时间):
NULL:表示永久阻塞,直到有事件发生。
0: 表示不阻塞,不管有无事件发生,立即返回。
>0: (1)表示阻塞,若没有超过时长就会一直阻塞。
(2)超过阻塞时间,没有事件发生,会返回。
(3)阻塞时间之内,有事件发生,返回。

过程如下图:

(1)通过操作得到监听文件描述符lfd,并且加入集合中。

(2)通过内核来监视lfd,如果lfd有行为,就说明有客户链接到来,把得到的通信文件描述符就交给内核监控。

内核发现哪个文件描述符有读行为,取出它进行通信,并把它取出集合。

下面是实现的代码:

c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/fcntl.h>

// 初始化服务端的监听端口。
int initserver(int port);

int main(int argc,char *argv[])
{
  if (argc != 2)
  {
    printf("usage: ./tcpselect port\\n"); return -1;
  }

  // 初始化服务端用于监听的socket。
  int listensock = initserver(atoi(argv[1]));
  printf("listensock=%d\\n",listensock);

  if (listensock < 0)
  {
    printf("initserver() failed.\\n"); return -1;
  }

  fd_set readfdset;  // 读事件的集合,包括监听socket和客户端连接上来的socket。
  int maxfd;  // readfdset中socket的最大值。

  // 初始化结构体,把listensock添加到集合中。
  FD_ZERO(&readfdset);

  FD_SET(listensock,&readfdset);
  maxfd = listensock;

  while (1)
  {
    // 调用select函数时,会改变socket集合的内容,所以要把socket集合保存下来,传一个临时的给select。
    fd_set tmpfdset = readfdset;

    int infds = select(maxfd+1,&tmpfdset,NULL,NULL,NULL);
    // printf("select infds=%d\\n",infds);

    // 返回失败。
    if (infds < 0)
    {
      printf("select() failed.\\n"); perror("select()"); break;
    }

    // 超时,在本程序中,select函数最后一个参数为空,不存在超时的情况,但以下代码还是留着。
    if (infds == 0)
    {
      printf("select() timeout.\\n"); continue;
    }

    // 检查有事情发生的socket,包括监听和客户端连接的socket。
    // 这里是客户端的socket事件,每次都要遍历整个集合,因为可能有多个socket有事件。
    for (int eventfd=0; eventfd <= maxfd; eventfd++)
    {
      if (FD_ISSET(eventfd,&tmpfdset)<=0) continue;

      if (eventfd==listensock)
      { 
        // 如果发生事件的是listensock,表示有新的客户端连上来。
        struct sockaddr_in client;
        socklen_t len = sizeof(client);
        int clientsock = accept(listensock,(struct sockaddr*)&client,&len);
        if (clientsock < 0)
        {
          printf("accept() failed.\\n"); continue;
        }

        printf ("client(socket=%d) connected ok.\\n",clientsock);

        // 把新的客户端socket加入集合。
        FD_SET(clientsock,&readfdset);

        if (maxfd < clientsock) maxfd = clientsock;

        continue;
      }
      else
      {
        // 客户端有数据过来或客户端的socket连接被断开。
        char buffer[1024];
        memset(buffer,0,sizeof(buffer));

        // 读取客户端的数据。
        ssize_t isize=read(eventfd,buffer,sizeof(buffer));

        // 发生了错误或socket被对方关闭。
        if (isize <=0)
        {
          printf("client(eventfd=%d) disconnected.\\n",eventfd);

          close(eventfd);  // 关闭客户端的socket。

          FD_CLR(eventfd,&readfdset);  // 从集合中移去客户端的socket。

          // 重新计算maxfd的值,注意,只有当eventfd==maxfd时才需要计算。
          if (eventfd == maxfd)
          {
            for (int ii=maxfd;ii>0;ii--)
            {
              if (FD_ISSET(ii,&readfdset))
              {
                maxfd = ii; break;
              }
            }

            printf("maxfd=%d\\n",maxfd);
          }

          continue;
        }

        printf("recv(eventfd=%d,size=%d):%s\\n",eventfd,isize,buffer);

        // 把收到的报文发回给客户端。
        write(eventfd,buffer,strlen(buffer));
      }
    }
  }

  return 0;
}

// 初始化服务端的监听端口。
int initserver(int port)
{
  int sock = socket(AF_INET,SOCK_STREAM,0);
  if (sock < 0)
  {
    printf("socket() failed.\\n"); return -1;
  }

  // Linux如下
  int opt = 1; unsigned int len = sizeof(opt);
  setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,len);
  setsockopt(sock,SOL_SOCKET,SO_KEEPALIVE,&opt,len);

  struct sockaddr_in servaddr;
  servaddr.sin_family = AF_INET;
  servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
  servaddr.sin_port = htons(port);

  if (bind(sock,(struct sockaddr *)&servaddr,sizeof(servaddr)) < 0 )
  {
    printf("bind() failed.\\n"); close(sock); return -1;
  }

  if (listen(sock,5) != 0 )
  {
    printf("listen() failed.\\n"); close(sock); return -1;
  }

  return sock;
}

2.高并发服务器模型--poll

除了select模型,还有poll模型,poll的做法和selec几乎是一样的。

poll 和 select 的实现非常类似,本质上的区别就是存放 fd 集合的数据结构不一样。select 在一个进程内可以维持最多 1024 个连接,poll 在此基础上做了加强,可以维持任意数量的连接。但本质上都是将需要检测的集合拷贝到内核中去,内核来轮询遍历整个集合,反复从头到尾的去查询,再将发生事件的所有的文件描述符拷贝到用户区,这样就导致随着并发量的增大,效率也会随之下降。

c 复制代码
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
功能:监视并等待多个文件描述符的属性变化

参数:

fds:指向一个结构体数组的第0个元素的指针,每个数组元素都是一个struct pollfd结构,用于指定测试某个给定的fd的条件

struct pollfd{
    int fd;            //文件描述符
    short events;    //等待的事件
    short revents;    //实际发生的事件
};
fds结构体参数说明:

fd:每一个 pollfd 结构体指定了一个被监视的文件描述符,可以传递多个结构体,指示 poll() 监视多个文件描述符。

events:指定监测fd的事件(输入、输出、错误),每一个事件有多个取值,如下:

revents:revents 域是文件描述符的操作结果事件,内核在调用返回时设置这个域。events 域中请求的任何事件都可能在 revents 域中返回.

注意:每个结构体的 events 域是由用户来设置,告诉内核我们关注的是什么,而 revents 域是返回时内核设置的,以说明对该描述符发生了什么事件

nfds:用来指定第一个参数数组元素个数

timeout:指定等待的毫秒数,无论 I/O 是否准备好,poll() 都会返回.

返回值:

成功时,poll() 返回结构体中 revents 域不为 0 的文件描述符个数;如果在超时前没有任何事件发生,poll()返回 0;

失败时,poll() 返回 -1,并设置 errno 为下列值之一:

EBADF:一个或多个结构体中指定的文件描述符无效。
EFAULT:fds 指针指向的地址超出进程的地址空间。
EINTR:请求的事件之前产生一个信号,调用可以重新发起。
EINVAL:nfds 参数超出 PLIMIT_NOFILE 值。
ENOMEM:可用内存不足,无法完成请求。

下面是代码的实现

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
 
#include <poll.h>
 
/* 定义服务器初始化函数 */
int server_init(char *ip, short port)
{
	int ret;
	int listenfd;
	struct sockaddr_in srvaddr;
 
	/* 创建套接字文件 */
	listenfd = socket(AF_INET, SOCK_STREAM, 0);
	if (listenfd == -1) {
		perror("server_init->socket");
		return -1;
	}
	printf("listenfd = %d\n", listenfd);
 
	/* 绑定服务器的ip地址和端口号 */
	memset(&srvaddr, 0, sizeof(srvaddr));
	srvaddr.sin_family = AF_INET;
	srvaddr.sin_port = htons(port);
	if (ip == NULL) 
		srvaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	else 
		srvaddr.sin_addr.s_addr = inet_addr(ip);
	ret = bind(listenfd, (const struct sockaddr *)&srvaddr, sizeof(srvaddr));
	if (ret == -1) {
		perror("server_init->bind");
		return -1;
	}
	printf("bind success\n");
 
	/* 启动监听 */
	ret = listen(listenfd, 1024);
	if (ret == -1) {
		perror("server_init->listen");
		return -1;
	}
 
	return listenfd;
}
 
/* 定义服务器等待客户端的连接请求,建立连接 */
int server_wait_client_connect(int listenfd)
{
	int connfd;
	socklen_t addrlen;
	struct sockaddr_in cltaddr;
 
	//accept(listenfd, NULL, NULL);
	addrlen = sizeof(cltaddr);
	connfd = accept(listenfd, (struct sockaddr *)&cltaddr, &addrlen);
	if (connfd == -1) {
		perror("accept");
		return -1;
	}
	printf("IP : %s connet success connfd = %d\n", inet_ntoa(cltaddr.sin_addr), connfd);
 
	return connfd;
}
 
 
 
int main()
{
	int ret;
	int listenfd;
	int connfd;
	char buf[256];
	int nfds;
	int i;
	int j;
	int fd;
	struct pollfd fds[128];
 
 
	/* 1. 服务器的初始化 */
	listenfd = server_init("127.0.0.1", 8888);
	//listenfd = server_init(NULL, 8888);
	if (listenfd == -1)
		exit(EXIT_FAILURE);
	printf("server init success\n");
 
	/* 创建集合,清空集合 */
	for (i = 0; i < 128; i++)
		fds[i].fd = -1;
 
	/* 将文件描述符listenfd及其对应事件添加到集合fds中 */
	fds[0].fd = listenfd;
	fds[0].events = POLLIN;
	nfds = 1;
 
	while(1) {
		/* c, 调用poll函数,检测是否有准备就绪的事件,如果没有事件准备就绪,函数一直阻塞。如果有事件准备就绪函数返回;*/
		ret = poll(fds, nfds, 5000);
		if (ret == -1) {
			perror("poll");
			return -1;
		} else if (ret == 0) {
			printf("timeout ......\n");
			continue;
		}
		
		for (i = 0; i < nfds; i++) {
			/* 读资源准备就绪,进行读IO操作 */
			if (fds[i].revents == POLLIN) {
				fd = fds[i].fd;
				if (fd == listenfd) { 	/* 监听套接字 */
					/* 2. 服务器等待客户端的连接请求,建立连接 */
					connfd = server_wait_client_connect(listenfd);
					if (connfd == -1)
						exit(EXIT_FAILURE);
					/* 连接成功,将文件描述符connfd及其对应事件添加到集合fds中 */
					fds[nfds].fd = connfd;
					fds[nfds].events = POLLIN;
					nfds ++;
					continue;
				} 
 
				/* 通信套接字 */
				/* 3. 服务器处理客户端的数据请求,并处理数据,反馈处理结果 */
				memset(buf, 0, sizeof(buf));
				ret = read(fd, buf, sizeof(buf));
				if (ret == -1) {
					perror("server->read");
					return -1;
				} else if (ret == 0) {
					/* 客户端退出的时候,需要将套接字从集合中删除 */
					for (j = i; j < nfds-1; j++)
						fds[j] = fds[j+1];
					close(fd);
					break;
				}
				printf("buf : %s\n", buf);
 
				ret = write(fd, buf, sizeof(buf));
				if (ret == -1) {
					perror("server->write");
					return -1;
				}
			} //if (fds[i].revents == POLLIN) end
			if (fds[i].events == POLLOUT) {
				
			}
 
		} 
	} 
 
	return 0;
}

3.epoll模型

select 和 poll 方式有一个很大的问题就是,我们不难看出来 select 是通过轮训的方式来查找是否可读或者可写,打个比方,如果同时有100万个连接都没有断开,而只有一个客户端发送了数据,所以这里它还是需要循环这么多次,造成资源浪费。所以后来出现了 epoll系统调用。

3.1 epoll原理

当某一个进程去调用epoll_creat()的时候,linux的内核会创建一个eventpoll的一个结构体,这个结构题中有两个成员和epoll的使用方式相关。

c 复制代码
struct eventpoll
{
      //红黑树的根节点,这颗树中存储这我们添加的所有的epoll中的事件。
      struct rb_root rbr;

    //双向链表rblist中存储的是要通过epoll_wait()返回个用户的满足的事件。
      struct list_head rbllist;
}

我们在调用epoll_creat()的时候,内核出来帮我们在epoll文件系统中创建了一个file节点,在内核中还创建了一棵红黑树用来存储加入的socket以外,还会建立一个rbllist双向链表,用来存储准备就绪的事件,当调用epoll_wait()的时候,就只需要观察这个双向链表中有无数据,如果没用就sleep阻塞等待,当阻塞时间超过timeout的时候,就直接返回。用数据的话直接把双向链表中的数据返回给用户。所以epoll非常高效。

所有添加到epoll中的事件都会与设备(如网卡)驱动程序建立回调关系,也就是说相应事件的发生时会调用这里的回调方法。这个回调方法在内核中叫做ep_poll_callback,它会把这样的事件放到上面的rdllist双向链表中。

在epoll中对于每一个事件都会建立一个epitem结构体,如下所示:

c 复制代码
struct epitem {
  ...
  //红黑树节点
  struct rb_node rbn;
  //双向链表节点
  struct list_head rdllink;
  //事件句柄等信息
  struct epoll_filefd ffd;
  //指向其所属的eventepoll对象
  struct eventpoll *ep;
  //期待的事件类型
  struct epoll_event event;
  ...
}; // 这里包含每一个事件对应着的信息。

当调用epoll_wait检查是否有发生事件的连接时,只是检查eventpoll对象中的rdllist双向链表是否有epitem元素而已,如果rdllist链表不为空,则这里的事件复制到用户态内存(使用共享内存提高效率)中,同时将事件数量返回给用户。因此epoll_waitx效率非常高。epoll_ctl在向epoll对象中添加、修改、删除事件时,从rbr红黑树中查找事件也非常快,也就是说epoll是非常高效的,它可以轻易地处理百万级别的并发连接。

当我们要把数据拷贝到用户区的时候,这就要涉及到用户态到内核态的转化, 会造成数据的多次拷贝。但是使用了mmap技术,建立了一片共享的内存,这样就不会造成内核态到用户态的内存拷贝,减小了开销。就如下图:

下面是相关的函数

c 复制代码
//创建一颗epoll树

int epoll_create(int size);
 
功能:调用epoll_create方法创建一个epoll的句柄,该句柄代表着一个事件表
参数:size参数现在并不起作用,它只是给内核一个提示,告诉内核事件表需要多大
返回值:1. 成功:返回epoll句柄,它会占用一个fd值(使用完也需要关闭)
失败:返回-1并设置errno值
c 复制代码
int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event)

fd表示文件描述符
op参数则指定操作类型,操作类型有一下三种:
EPOLL_CTL_ADD  //往事件表上注册fd的事件
EPOLL_CTL_MOD  //修改fd上的注册事件
EPOLL_CTL_DEL  //删除fd上的注册事件

event是需要传入一个结构体指针类型,表示一个事件,epoll_even结构体t定义如下:

c 复制代码
struct epoll_event
{
   
   _uint32_t events;   //epoll事件
   epolla_data_t data; //用户数据
}

events和poll支持的事件类型基本相同,还可以额外支持两个事件边缘触发和水平触发。epoll默认的触发方式是水平触发。

水平触发:水平触发就是只要缓冲区中有数据,就会一直触发,直到将缓冲区中的数据读完。

边沿触发:只有数据到来的时候才会触发,不管缓冲区中是否有数据。

当使用边沿触发(ET触发)的时候,我们要将监听的文件描述设置成非阻塞,因为我们每次不一定读完缓冲区的数据,而read函数是一个阻塞函数,就会一直阻塞住,导致死锁。

c 复制代码
typedef union epoll_data
{
    void* ptr;              //指定与fd相关的用户数据 
    int fd;                 //指定事件所从属的目标文件描述符 
    uint32_t u32;
    uint64_t u64;
} epoll_data_t;

这里的ptr指针是可以存储用户的数据的,他是一个viod*的万能指针。

c 复制代码
#include <sys/epoll.h>
int epoll_wait ( int epfd, struct epoll_event* events, int maxevents, int timeout );
epfd:epoll_create()返回的文件描述符
events:检测到事件,将所有就绪的事件从内核事件表中复制到它的第二个参数events指向的数组中。
maxevents:最多监听多少个事件。
timeout:为-1表示永久阻塞
=0 表示调用后立即返回
>0 表示在这个时间内无事件发生一直阻塞,有事件发生立即返回;超过时间也会返回。

3.2epoll反应堆

epoll反应堆利用了c++的封装的思想,封装了一个自己的结构体,每个结构体都用函数指针,通过不同的事件,来回调不同的函数。将ptr指针指向我们自己封装的结构体就可以实现自动回调函数啦。

下面是代码:

c 复制代码
/*
 *epoll基于非阻塞I/O事件驱动
 */
#include <stdio.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>
 
#define MAX_EVENTS  1024                                    //监听上限数
#define BUFLEN 4096
#define SERV_PORT   8080
 
void recvdata(int fd, int events, void *arg);
void senddata(int fd, int events, void *arg);
 
/* 描述就绪文件描述符相关信息 */
 
struct myevent_s {
    int fd;                                                 //要监听的文件描述符
    int events;                                             //对应的监听事件
    void *arg;                                              //泛型参数
    void (*call_back)(int fd, int events, void *arg);       //回调函数
    int status;                                             //是否在监听:1->在红黑树上(监听), 0->不在(不监听)
    char buf[BUFLEN];
    int len;
    long last_active;                                       //记录每次加入红黑树 g_efd 的时间值
};
 
int g_efd;                                                  //全局变量, 保存epoll_create返回的文件描述符
struct myevent_s g_events[MAX_EVENTS+1];                    //自定义结构体类型数组. +1-->listen fd
 
 
/*将结构体 myevent_s 成员变量 初始化*/
 
void eventset(struct myevent_s *ev, int fd, void (*call_back)(int, int, void *), void *arg)
{
    ev->fd = fd;
    ev->call_back = call_back;
    ev->events = 0;
    ev->arg = arg;
    ev->status = 0;
    memset(ev->buf, 0, sizeof(ev->buf));
    ev->len = 0;
    ev->last_active = time(NULL);                       //调用eventset函数的时间
 
    return;
}
 
/* 向 epoll监听的红黑树 添加一个 文件描述符 */
 
//eventadd(efd, EPOLLIN, &g_events[MAX_EVENTS]);
void eventadd(int efd, int events, struct myevent_s *ev)
{
    struct epoll_event epv = {0, {0}};
    int op;
    epv.data.ptr = ev;
    epv.events = ev->events = events;       //EPOLLIN 或 EPOLLOUT
 
    if (ev->status == 0) {                                          //已经在红黑树 g_efd 里
        op = EPOLL_CTL_ADD;                 //将其加入红黑树 g_efd, 并将status置1
        ev->status = 1;
    }
 
    if (epoll_ctl(efd, op, ev->fd, &epv) < 0)                       //实际添加/修改
        printf("event add failed [fd=%d], events[%d]\n", ev->fd, events);
    else
        printf("event add OK [fd=%d], op=%d, events[%0X]\n", ev->fd, op, events);
 
    return ;
}
 
/* 从epoll 监听的 红黑树中删除一个 文件描述符*/
 
void eventdel(int efd, struct myevent_s *ev)
{
    struct epoll_event epv = {0, {0}};
 
    if (ev->status != 1)                                        //不在红黑树上
        return ;
 
    //epv.data.ptr = ev;
    epv.data.ptr = NULL;
    ev->status = 0;                                             //修改状态
    epoll_ctl(efd, EPOLL_CTL_DEL, ev->fd, &epv);                //从红黑树 efd 上将 ev->fd 摘除
 
    return ;
}
 
/*  当有文件描述符就绪, epoll返回, 调用该函数 与客户端建立链接 */
 
void acceptconn(int lfd, int events, void *arg)
{
    struct sockaddr_in cin;
    socklen_t len = sizeof(cin);
    int cfd, i;
 
    if ((cfd = accept(lfd, (struct sockaddr *)&cin, &len)) == -1) {
        if (errno != EAGAIN && errno != EINTR) {
            /* 暂时不做出错处理 */
        }
        printf("%s: accept, %s\n", __func__, strerror(errno));
        return ;
    }
 
    do {
        for (i = 0; i < MAX_EVENTS; i++)                                //从全局数组g_events中找一个空闲元素
            if (g_events[i].status == 0)                                //类似于select中找值为-1的元素
                break;                                                  //跳出 for
 
        if (i == MAX_EVENTS) {
            printf("%s: max connect limit[%d]\n", __func__, MAX_EVENTS);
            break;                                                      //跳出do while(0) 不执行后续代码
        }
 
        int flag = 0;
        if ((flag = fcntl(cfd, F_SETFL, O_NONBLOCK)) < 0) {             //将cfd也设置为非阻塞
            printf("%s: fcntl nonblocking failed, %s\n", __func__, strerror(errno));
            break;
        }
 
        /* 给cfd设置一个 myevent_s 结构体, 回调函数 设置为 recvdata */
        eventset(&g_events[i], cfd, recvdata, &g_events[i]);   
        eventadd(g_efd, EPOLLIN, &g_events[i]);                         //将cfd添加到红黑树g_efd中,监听读事件
 
    } while(0);
 
    printf("new connect [%s:%d][time:%ld], pos[%d]\n", 
            inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), g_events[i].last_active, i);
    return ;
}
 
void recvdata(int fd, int events, void *arg)
{
    struct myevent_s *ev = (struct myevent_s *)arg;
    int len;
 
    len = recv(fd, ev->buf, sizeof(ev->buf), 0);            //读文件描述符, 数据存入myevent_s成员buf中
 
    eventdel(g_efd, ev);        //将该节点从红黑树上摘除
 
    if (len > 0) {
 
        ev->len = len;
        ev->buf[len] = '\0';                                //手动添加字符串结束标记
        printf("C[%d]:%s\n", fd, ev->buf);
 
        eventset(ev, fd, senddata, ev);                     //设置该 fd 对应的回调函数为 senddata
        eventadd(g_efd, EPOLLOUT, ev);                      //将fd加入红黑树g_efd中,监听其写事件
 
    } else if (len == 0) {
        close(ev->fd);
        /* ev-g_events 地址相减得到偏移元素位置 */
        printf("[fd=%d] pos[%ld], closed\n", fd, ev-g_events);
    } else {
        close(ev->fd);
        printf("recv[fd=%d] error[%d]:%s\n", fd, errno, strerror(errno));
    }
 
    return;
}
 
void senddata(int fd, int events, void *arg)
{
    struct myevent_s *ev = (struct myevent_s *)arg;
    int len;
 
    len = send(fd, ev->buf, ev->len, 0);                    //直接将数据 回写给客户端。未作处理
 
    eventdel(g_efd, ev);                                //从红黑树g_efd中移除
 
    if (len > 0) {
 
        printf("send[fd=%d], [%d]%s\n", fd, len, ev->buf);
        eventset(ev, fd, recvdata, ev);                     //将该fd的 回调函数改为 recvdata
        eventadd(g_efd, EPOLLIN, ev);                       //从新添加到红黑树上, 设为监听读事件
 
    } else {
        close(ev->fd);                                      //关闭链接
        printf("send[fd=%d] error %s\n", fd, strerror(errno));
    }
 
    return ;
}
 
/*创建 socket, 初始化lfd */
 
void initlistensocket(int efd, short port)
{
    struct sockaddr_in sin;
 
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    fcntl(lfd, F_SETFL, O_NONBLOCK);                                            //将socket设为非阻塞
 
	memset(&sin, 0, sizeof(sin));                                               //bzero(&sin, sizeof(sin))
	sin.sin_family = AF_INET;
	sin.sin_addr.s_addr = INADDR_ANY;
	sin.sin_port = htons(port);
 
	bind(lfd, (struct sockaddr *)&sin, sizeof(sin));
 
	listen(lfd, 20);
 
    /* void eventset(struct myevent_s *ev, int fd, void (*call_back)(int, int, void *), void *arg);  */
    eventset(&g_events[MAX_EVENTS], lfd, acceptconn, &g_events[MAX_EVENTS]);
 
    /* void eventadd(int efd, int events, struct myevent_s *ev) */
    eventadd(efd, EPOLLIN, &g_events[MAX_EVENTS]);
 
    return ;
}
 
int main(int argc, char *argv[])
{
    unsigned short port = SERV_PORT;
 
    if (argc == 2)
        port = atoi(argv[1]);                           //使用用户指定端口.如未指定,用默认端口
 
    g_efd = epoll_create(MAX_EVENTS+1);                 //创建红黑树,返回给全局 g_efd 
    if (g_efd <= 0)
        printf("create efd in %s err %s\n", __func__, strerror(errno));
 
    initlistensocket(g_efd, port);                      //初始化监听socket
 
    struct epoll_event events[MAX_EVENTS+1];            //保存已经满足就绪事件的文件描述符数组 
	printf("server running:port[%d]\n", port);
 
    int checkpos = 0, i;
    while (1) {
        /* 超时验证,每次测试100个链接,不测试listenfd 当客户端60秒内没有和服务器通信,则关闭此客户端链接 */
 
        long now = time(NULL);                          //当前时间
        for (i = 0; i < 100; i++, checkpos++) {         //一次循环检测100个。 使用checkpos控制检测对象
            if (checkpos == MAX_EVENTS)
                checkpos = 0;
            if (g_events[checkpos].status != 1)         //不在红黑树 g_efd 上
                continue;
 
            long duration = now - g_events[checkpos].last_active;       //客户端不活跃的世间
 
            if (duration >= 60) {
                close(g_events[checkpos].fd);                           //关闭与该客户端链接
                printf("[fd=%d] timeout\n", g_events[checkpos].fd);
                eventdel(g_efd, &g_events[checkpos]);                   //将该客户端 从红黑树 g_efd移除
            }
        }
 
        /*监听红黑树g_efd, 将满足的事件的文件描述符加至events数组中, 1秒没有事件满足, 返回 0*/
        int nfd = epoll_wait(g_efd, events, MAX_EVENTS+1, 1000);
        if (nfd < 0) {
            printf("epoll_wait error, exit\n");
            break;
        }
 
        for (i = 0; i < nfd; i++) {
            /*使用自定义结构体myevent_s类型指针, 接收 联合体data的void *ptr成员*/
            struct myevent_s *ev = (struct myevent_s *)events[i].data.ptr;  
 
            if ((events[i].events & EPOLLIN) && (ev->events & EPOLLIN)) {           //读就绪事件
                ev->call_back(ev->fd, events[i].events, ev->arg);
                //lfd  EPOLLIN  
            }
            if ((events[i].events & EPOLLOUT) && (ev->events & EPOLLOUT)) {         //写就绪事件
                ev->call_back(ev->fd, events[i].events, ev->arg);
            }
        }
    }
 
    /* 退出前释放所有资源 */
    return 0;
}
相关推荐
mengao123423 分钟前
centos 服务器 docker 使用代理
服务器·docker·centos
网络安全-杰克30 分钟前
网络安全概论
网络·web安全·php
C-cat.32 分钟前
Linux|进程程序替换
linux·服务器·microsoft
怀澈12234 分钟前
高性能服务器模型之Reactor(单线程版本)
linux·服务器·网络·c++
学Linux的语莫38 分钟前
Ansible Playbook剧本用法
linux·服务器·云计算·ansible
耗同学一米八1 小时前
2024 年河北省职业院校技能大赛网络建设与运维赛项样题二
运维·网络·mariadb
skywalk81631 小时前
树莓派2 安装raspberry os 并修改成固定ip
linux·服务器·网络·debian·树莓派·raspberry
co0t2 小时前
计算机网络(14)ip地址超详解
服务器·tcp/ip·计算机网络
C++忠实粉丝2 小时前
计算机网络socket编程(3)_UDP网络编程实现简单聊天室
linux·网络·c++·网络协议·计算机网络·udp
淡水猫.2 小时前
Fakelocation Server服务器/专业版 ubuntu
运维·服务器·ubuntu