高并发服务器模型

news2024/10/24 1:55:58

高并发服务器模型

  • 1.高并发服务器模型--select
  • 2.高并发服务器模型--poll
  • 3.epoll模型
    • 3.1 epoll原理
    • 3.2epoll反应堆

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

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

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

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有行为,就说明有客户链接到来,把得到的通信文件描述符就交给内核监控。
在这里插入图片描述
内核发现哪个文件描述符有读行为,取出它进行通信,并把它取出集合。

在这里插入图片描述
下面是实现的代码:

#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 在此基础上做了加强,可以维持任意数量的连接。但本质上都是将需要检测的集合拷贝到内核中去,内核来轮询遍历整个集合,反复从头到尾的去查询,再将发生事件的所有的文件描述符拷贝到用户区,这样就导致随着并发量的增大,效率也会随之下降。

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:可用内存不足,无法完成请求。

下面是代码的实现

#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的使用方式相关。

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结构体,如下所示:

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技术,建立了一片共享的内存,这样就不会造成内核态到用户态的内存拷贝,减小了开销。就如下图:

在这里插入图片描述
下面是相关的函数

//创建一颗epoll树

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

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定义如下:

struct epoll_event
{
   
   _uint32_t events;   //epoll事件
   epolla_data_t data; //用户数据
}

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

水平触发:水平触发就是只要缓冲区中有数据,就会一直触发,直到将缓冲区中的数据读完。
边沿触发:只有数据到来的时候才会触发,不管缓冲区中是否有数据。

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

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

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

#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指针指向我们自己封装的结构体就可以实现自动回调函数啦。

下面是代码:

/*
 *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;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1498093.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【框架学习 | 第二篇】暴打MyBatis-Plus——MyBatis的升级版本

教程来源链接&#xff1a;https://www.quanxiaoha.com/mybatis-plus/mybatis-plus-tutorial.html 教程作者&#xff1a;犬小哈 文章目录 1.Mybatis Plus介绍1.1Mybatis和Mybatis Plus的区别是什么1.1.1什么是Mybatis?1.1.2区分Mybatis Plus和Mybatis 1.2Mybatis Plus特点1.3支…

【C语言】终の指针(前篇)

个人主页点这里~ 指针初阶点这里~ 指针初阶2.0点这里~ 指针进阶点这里~ 终の指针 一、回调函数二、qsort函数1、整形比较2、结构数据比较①结构体②-> 的使用③结构数据比较 一、回调函数 回调函数就是⼀个通过函数指针调用的函数。 把一个函数的指针作为参数传递给另一…

勾股定理的七种经典证明

据说勾股定理约有500种证明方法&#xff0c;下面介绍几种经典的证明方法。 一、切割重拼法。 顾名思义&#xff0c;就是将图形切割成其他形式的图形&#xff0c;然后通过拼图转换为另一种图形&#xff0c;这个过程中图形的面积是不变的。 “赵爽弦图”是这种方法的经典应用&…

Mysql案例之GROUP_CONCAT函数详解

Hello&#xff0c;大家好&#xff0c;我是灰小猿&#xff0c;一个超会写bug的程序员&#xff01; 今天这篇文章记录一个最近开发中遇到的mysql实战场景&#xff0c;觉得还挺典型的&#xff0c;就在此做一下记录。 先看一下举例场景&#xff1a; mysql中学生表与学科表通过关…

Linux设备模型(九) - bus/device/device_driver/class

一&#xff0c;设备驱动模型 1&#xff0c;概述 在前面写的驱动中&#xff0c;我们发现编写驱动有个固定的模式只有往里面套代码就可以了&#xff0c;它们之间的大致流程可以总结如下&#xff1a; 实现入口函数xxx_init()和卸载函数xxx_exit() 申请设备号 register_chrdev_r…

首发:鸿蒙面试真题分享【独此一份】

最早在23年华为秋季发布会中&#xff0c;就已经宣布了“纯血鸿蒙”。而目前鸿蒙处于星河版中&#xff0c;加速了各大互联网厂商的合作。目前已经有200参与鸿蒙的原生应用开发当中。对此各大招聘网站上的鸿蒙开发需求&#xff0c;每日都在增长中。 2024大厂面试真题 目前的鸿蒙…

OpenHarmony教程指南—ArkUI中组件、通用、动画、全局方法的集合

介绍 本示例为ArkUI中组件、通用、动画、全局方法的集合。 本示例使用 Tabs容器组件搭建整体应用框架&#xff0c;每个 TabContent内容视图 使用 div容器组件 嵌套布局&#xff0c;在每个 div 中使用 循环渲染 加载此分类下分类导航数据&#xff0c;底部导航菜单使用 TabCont…

LeetCode 2917.找出数组中的 K-or 值:基础位运算

【LetMeFly】2917.找出数组中的 K-or 值&#xff1a;基础位运算 力扣题目链接&#xff1a;https://leetcode.cn/problems/find-the-k-or-of-an-array/ 给你一个下标从 0 开始的整数数组 nums 和一个整数 k 。 nums 中的 K-or 是一个满足以下条件的非负整数&#xff1a; 只有…

如何合理布局子图--确定MATLAB的subplot子图位置参数

确定MATLAB的subplot子图位置参数 目录 确定MATLAB的subplot子图位置参数摘要1. 问题描述2. 计算过程2.1 确定子图的大小和间距2.2 计算合适的figure大小2.3 计算每个子图的position数据 3. MATLAB代码实现3.1 MATLAB代码3.2 绘图结果 4. 总结 摘要 在MATLAB中&#xff0c;使用…

网络编程套接字(1)—网络编程基础

目录 一、为什么需要网络编程? 二、什么是网络编程 三、网络编程中的基本概念 1、发送端和接收端 2、请求和响应 3、客户端和服务端 四、常见的客户端服务端模型 1、一问一答模型 2、一问多答模型 3、多问一答模型 4、多问多答模型 一、为什么需要网络编程? 为什么…

(二十二)从零开始搭建k8s集群——高可用kubernates集群搭建上篇

前言 本节内容分为上、中、下三篇&#xff0c;上篇主要是关于搭建k8s的基础环境&#xff0c;包括服务器基本环境的配置&#xff08;网络、端口、主机名、防火墙、交换分区、文件句柄数等&#xff09;、docker环境部署安装配置、镜像源配置等。中篇会介绍k8s的核心组件安装、k8…

rk3568 恢复出厂设置横屏

author daisy.skye的博客_CSDN博客-嵌入式,Qt,Linux领域博主 daisy.skye_嵌入式,Linux,Qt-CSDN博客daisy.skye擅长嵌入式,Linux,Qt,等方面的知识https://blog.csdn.net/qq_40715266?typeblog 在使用rk3568开发过程&#xff0c;虽然显示的方向已经改成了横屏&#xff0c;但是恢…

4.1k star,官方出品的redis桌面管理工具——redislnsight

导航 令人抓狂的大key加载RedisInsight 简介RedisInsight的亮点GitHub 地址安装和使用RedisInsight 下载安装 使用RedisInsight redis数据库可视化直观的CLI&#xff08;Command-Line Interface&#xff09;日志分析和命令分析 结语参考 令人抓狂的大key加载 工欲善其事必先利…

JavaScript基础4之原型的原型继承、原型链和理解对象的数据属性、访问器属性

JavaScript基础 原型原型继承问题解决 原型链isPrototypeOf()Object.getPrototypeOf() 理解对象数据属性访问器属性 原型 原型继承 继承是面向对象编程的另一个特征&#xff0c;通过继承进一步提升代码封装的程度&#xff0c;JavaScript中大多是借助原型对象实现继承的特性。…

sudo command not found

文章目录 一句话Intro其他操作 一句话 sudo 某命令 改成 sudo -i 某命令 试试。 -i 会把当前用户的环境变量带过去&#xff0c;这样在sudo的时候&#xff0c;有更高的权限&#xff0c;有本用户的环境变量(下的程序命令)。 -i, --login run login shell as the target user; a …

软件测试相关概念和bug的相关总结

文章目录 什么是测试什么是需求测试用例(CASE)什么是BUG软件的生命周期开发模型瀑布模型螺旋模型增量模型和迭代模型 敏捷测试模型v模型W模型(双V模型) 软件测试的生命周期如何描述一个bugbug的级别bug的生命周期.产生争执怎么办 什么是测试 测试是测试人员用来检验软件的实际运…

全自动玻璃切割机控制系统设计

目 录 摘 要 I Abstract II 引 言 1 1 玻璃切割机控制系统设计 4 1.1系统方案选择 4 1.2玻璃切割机的工作原理 4 1.3工艺过程 5 1.4玻璃切割机的控制要求 6 2硬件设计 8 2.1控制部分设计 8 2.2驱动部分设计 8 2.2.1步进电机及驱动器的选型 8 2.2.2步进电机驱动器接口电路设计 …

VM 虚拟机 ubuntu 解决无法连接网络问题

添加网卡法 就是在虚拟机的设置那里多增加一个网卡

每日OJ题_链表②_力扣24. 两两交换链表中的节点

目录 力扣24. 两两交换链表中的节点 解析代码 力扣24. 两两交换链表中的节点 24. 两两交换链表中的节点 难度 中等 给你一个链表&#xff0c;两两交换其中相邻的节点&#xff0c;并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题&#xff08;即&…

顺序表以及单链表

目录 1顺序表&#xff08;规范&#xff09; 2单链表&#xff08;规范&#xff09; 3总结 1顺序表&#xff08;规范&#xff09; #include<iostream> using namespace std; #define MAXSIZE 100 #define ok -1 #define error -2 typedef int Status; typedef int…