从零构建通讯器--5.5监听端口实战、epoll介绍及原理详析


新增文件:

1.net目录下增加文件ngx_c_socket.cxx和对应的makefile,还有修改config,mk
2.include目录下新增ngx_c_socket.h
3.nginx.cxx增加监听socket的初始化代码

(1)监听端口

引入新文件,net引入ngx_c_socket.cxx和头文件引入ngx_c_socket.h(放入socket相关的类)
更新文件nginx.conf,加监听的端口数量和端口具体信息
ngx_c_socket.cxx
//和网络 有关的函数放这里
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>    //uintptr_t
#include <stdarg.h>    //va_start....
#include <unistd.h>    //STDERR_FILENO等
#include <sys/time.h>  //gettimeofday
#include <time.h>      //localtime_r
#include <fcntl.h>     //open
#include <errno.h>     //errno
#include <sys/socket.h>
#include <sys/ioctl.h> //ioctl
#include <arpa/inet.h>

#include "ngx_c_conf.h"
#include "ngx_macro.h"
#include "ngx_global.h"
#include "ngx_func.h"
#include "ngx_c_socket.h"

//构造函数
CSocekt::CSocekt()
{
    m_ListenPortCount = 1;   //监听一个端口
    return;	
}

//释放函数
CSocekt::~CSocekt()
{
    //释放必须的内存
    std::vector<lpngx_listening_t>::iterator pos;	
	for(pos = m_ListenSocketList.begin(); pos != m_ListenSocketList.end(); ++pos) //遍历vector
	{		
		delete (*pos); //一定要把指针指向的内存干掉,不然内存泄漏
	}//end for
	m_ListenSocketList.clear(); 
    return;
}

//初始化函数【fork()子进程之前干这个事】
//成功返回true,失败返回false
bool CSocekt::Initialize()
{
    bool reco = ngx_open_listening_sockets();
    return reco;
}

//监听端口【支持多个端口】,这里遵从nginx的函数命名
//在创建worker进程之前就要执行这个函数;
bool CSocekt::ngx_open_listening_sockets()
{
    CConfig *p_config = CConfig::GetInstance();
    m_ListenPortCount = p_config->GetIntDefault("ListenPortCount",m_ListenPortCount); //取得要监听的端口数量
    
    int                isock;                //socket
    struct sockaddr_in serv_addr;            //服务器的地址结构体
    int                iport;                //端口
    char               strinfo[100];         //临时字符串 
   
    //初始化相关
    memset(&serv_addr,0,sizeof(serv_addr));  //先初始化一下
    serv_addr.sin_family = AF_INET;                //选择协议族为IPV4
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); //监听本地所有的IP地址;INADDR_ANY表示的是一个服务器上所有的网卡(服务器可能不止一个网卡)多个本地ip地址都进行绑定端口号,进行侦听。

    for(int i = 0; i < m_ListenPortCount; i++) //要监听这么多个端口
    {        
        //参数1:AF_INET:使用ipv4协议,一般就这么写
        //参数2:SOCK_STREAM:使用TCP,表示可靠连接【相对还有一个UDP套接字,表示不可靠连接】
        //参数3:给0,固定用法,就这么记
        isock = socket(AF_INET,SOCK_STREAM,0); //系统函数,成功返回非负描述符,出错返回-1
        if(isock == -1)
        {
            ngx_log_stderr(errno,"CSocekt::Initialize()中socket()失败,i=%d.",i);
            //其实这里直接退出,那如果以往有成功创建的socket呢?就没得到释放吧,当然走到这里表示程序不正常,应该整个退出,也没必要释放了 
            return false;
        }

        //setsockopt():设置一些套接字参数选项;
        //参数2:是表示级别,和参数3配套使用,也就是说,参数3如果确定了,参数2就确定了;
        //参数3:允许重用本地地址
        //设置 SO_REUSEADDR,目的第五章第三节讲解的非常清楚:主要是解决TIME_WAIT这个状态导致bind()失败的问题
        int reuseaddr = 1;  //1:打开对应的设置项
        if(setsockopt(isock,SOL_SOCKET, SO_REUSEADDR,(const void *) &reuseaddr, sizeof(reuseaddr)) == -1)
        {
            ngx_log_stderr(errno,"CSocekt::Initialize()中setsockopt(SO_REUSEADDR)失败,i=%d.",i);
            close(isock); //无需理会是否正常执行了                                                  
            return false;
        }
        //设置该socket为非阻塞
        if(setnonblocking(isock) == false)
        {                
            ngx_log_stderr(errno,"CSocekt::Initialize()中setnonblocking()失败,i=%d.",i);
            close(isock);
            return false;
        }

        //设置本服务器要监听的地址和端口,这样客户端才能连接到该地址和端口并发送数据        
        strinfo[0] = 0;
        sprintf(strinfo,"ListenPort%d",i);
        iport = p_config->GetIntDefault(strinfo,10000);
        serv_addr.sin_port = htons((in_port_t)iport);   //in_port_t其实就是uint16_t

        //绑定服务器地址结构体
        if(bind(isock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
        {
            ngx_log_stderr(errno,"CSocekt::Initialize()中bind()失败,i=%d.",i);
            close(isock);
            return false;
        }
        
        //开始监听
        if(listen(isock,NGX_LISTEN_BACKLOG) == -1)
        {
            ngx_log_stderr(errno,"CSocekt::Initialize()中listen()失败,i=%d.",i);
            close(isock);
            return false;
        }

        //可以,放到列表里来
        lpngx_listening_t p_listensocketitem = new ngx_listening_t; //千万不要写错,注意前边类型是指针,后边类型是一个结构体
        memset(p_listensocketitem,0,sizeof(ngx_listening_t));      //注意后边用的是 ngx_listening_t而不是lpngx_listening_t
        p_listensocketitem->port = iport;                          //记录下所监听的端口号
        p_listensocketitem->fd   = isock;                          //套接字木柄保存下来   
        ngx_log_error_core(NGX_LOG_INFO,0,"监听%d端口成功!",iport); //显示一些信息到日志中
        m_ListenSocketList.push_back(p_listensocketitem);          //加入到队列中
    } //end for(int i = 0; i < m_ListenPortCount; i++)    
    return true;
}

//设置socket连接为非阻塞模式【这种函数的写法很固定】:非阻塞,概念在五章四节讲解的非常清楚【不断调用,不断调用这种:拷贝数据的时候是阻塞的】
bool CSocekt::setnonblocking(int sockfd) 
{    
    int nb=1; //0:清除,1:设置  
    if(ioctl(sockfd, FIONBIO, &nb) == -1) //FIONBIO:设置/清除非阻塞I/O标记:0:清除,1:设置
    {
        return false;
    }
    return true;

    //如下也是一种写法,跟上边这种写法其实是一样的,但上边的写法更简单
    /* 
    //fcntl:file control【文件控制】相关函数,执行各种描述符控制操作
    //参数1:所要设置的描述符,这里是套接字【也是描述符的一种】
    int opts = fcntl(sockfd, F_GETFL);  //用F_GETFL先获取描述符的一些标志信息
    if(opts < 0) 
    {
        ngx_log_stderr(errno,"CSocekt::setnonblocking()中fcntl(F_GETFL)失败.");
        return false;
    }
    opts |= O_NONBLOCK; //把非阻塞标记加到原来的标记上,标记这是个非阻塞套接字【如何关闭非阻塞呢?opts &= ~O_NONBLOCK,然后再F_SETFL一下即可】
    if(fcntl(sockfd, F_SETFL, opts) < 0) 
    {
        ngx_log_stderr(errno,"CSocekt::setnonblocking()中fcntl(F_SETFL)失败.");
        return false;
    }
    return true;
    */
}

//关闭socket,什么时候用,我们现在先不确定,先把这个函数预备在这里
void CSocekt::ngx_close_listening_sockets()
{
    for(int i = 0; i < m_ListenPortCount; i++) //要关闭这么多个监听端口
    {  
        //ngx_log_stderr(0,"端口是%d,socketid是%d.",m_ListenSocketList[i]->port,m_ListenSocketList[i]->fd);
        close(m_ListenSocketList[i]->fd);
        ngx_log_error_core(NGX_LOG_INFO,0,"关闭监听端口%d!",m_ListenSocketList[i]->port); //显示一些信息到日志中
    }//end for(int i = 0; i < m_ListenPortCount; i++)
    return;
}

ngx_c_socket.h

#ifndef __NGX_SOCKET_H__
#define __NGX_SOCKET_H__

#include <vector>

//一些宏定义放在这里-----------------------------------------------------------
#define NGX_LISTEN_BACKLOG  511   //已完成连接队列,nginx给511,我们也先按照这个来:不懂这个数字的同学参考第五章第四节

//一些专用结构定义放在这里,暂时不考虑放ngx_global.h里了-------------------------
typedef struct ngx_listening_s  //和监听端口有关的结构
{
	int            port;   //监听的端口号
	int            fd;     //套接字句柄socket
}ngx_listening_t,*lpngx_listening_t;

//socket相关类
class CSocekt
{
public:
	CSocekt();                                            //构造函数
	virtual ~CSocekt();                                   //释放函数

public:
    virtual bool Initialize();                            //初始化函数

private:
	bool ngx_open_listening_sockets();                    //监听必须的端口【支持多个端口】
	void ngx_close_listening_sockets();                   //关闭监听套接字
	bool setnonblocking(int sockfd);                      //设置非阻塞套接字

private:
	int                            m_ListenPortCount;     //所监听的端口数量
	std::vector<lpngx_listening_t> m_ListenSocketList;    //监听套接字队列
};

#endif

(1.1)开启监听端口
主进程fork()子线程之前调用监听窗口

(2)epoll技术简介

(2.1)epoll概述
	(1)I/O多路复用:epoll就是一种典型的I/O多路复用技术:epoll技术的最大特点是支持高并发;
	(2)epoll和kquene技术类似:单独一台计算机支撑少则数万,多则数十上百万并发连接的核心技术;
	(3)10万个连接同一时刻,可能只有几十上百个客户端给你发送数据,epoll只处理这几十上百个客户端;
	(4)很多服务器程序用多进程,每一个进程对应一个连接;也有用多线程做的,每一个线程对应 一个连接;epoll事件驱动机制,在单独的进程或者单独的线程里运行,收集/处理事件;没有进程/线程之间切换的消耗,高效
	(5)适合高并发,融合epoll技术到项目中,作为大家将来从事服务器开发工作的立身之本;写小demo非常简单,难度只有1-10,但是要把epoll技术融合到商业的环境中,那么难度就会骤然增加10倍;
(2.2)学习epoll要达到的效果及一些说明
	(1)理解epoll的工作原理;面试考epoll技术的工作原理;
	(2)开始写代码
	(3)认可nginx epoll部分源码;并且能复用的尽量复用;
	(4)继续贯彻用啥讲啥的原则;  少就是多;

(3)epoll原理与函数介绍

(3.1)课件介绍

	https://github.com/wangbojing
	a)c1000k_test这里,测试百万并发的一些测试程序;一般以main();
	b)ntytcp:nty_epoll_inner.h,nty_epoll_rb.c
	epoll_create();
	epoll_ctl();
	epoll_wait();
	epoll_event_callback();
	c)总结:建议学习完老师的epoll实战代码之后,再来学习 这里提到的课件代码,事半功倍;

(3.2)epoll_create()函数

1)格式
int epoll_create(int size);,size: >0;(有人看过epoll代码,size大于0就行)
2) 功能
创建一个epoll对象,返回该对象的描述符【文件描述符】,这个描述符就代表这个epoll对象,后续会用到;(注意:这个epoll对象最终要用close(),因为文件描述符/句柄 总是关闭的;)先创建epoll对象,创建一颗空红黑树,一个空双向链表

a)struct eventpoll ep = (struct eventpoll)calloc(1, sizeof(struct eventpoll)); //申请空间创建结构体,new了一个eventpoll对象【开辟了一块内存】
b)RB_INIT(&ep->rbr); //等价于ep->rbr.rbh_root = NULL;
rbr结构成员:代表一颗红黑树的根节点[刚开始指向空],把rbr理解成红黑树的根节点的指针;
红黑树,用来保存 键【数字】/值【结构】,能够快速的通过你给key,把整个的键/值取出来;
c)让双向链表的根节点指向一个空
LIST_INIT(&ep->rdlist); //等价于ep->rdlist.lh_first = NULL;
d)总结
①创建了一个eventpoll结构对象,被系统保存到某个位置;
②rbr成员被初始化成指向一颗红黑树的根,有了这个根,就可以向红黑树中插入节点,或者说插入数据了【有了一个红黑树】;
③rdlist成员被初始化成指向一个双向链表的根【有了这个根,就可以向双向链表中插入节点,或者说插入数据了】;

(3.3)epoll_ctl()函数

1)格式:
int epoll_ctl(int efpd,int op,int sockid,struct epoll_event *event);
2)功能:
把一个socket以及这个socket相关的事件添加到这个epoll对象描述符中去,目的就是通过这个epoll对象来监视这个socket【客户端的TCP连接】上数据的来往情况;(注意:efpd:epoll_create()返回的epoll对象描述符;)
3)参数说明:

参数epfd:从epoll_create返回的epoll对象描述符

参数op:一个操作类型,可以理解为一个数字,添加/删除/修改 ,对应数字是1,2,3, EPOLL_CTL_ADD, EPOLL_CTL_DEL ,EPOLL_CTL_MOD
EPOLL_CTL_ADD添加事件:等于你往红黑树上添加一个节点,每个客户端连入服务器后,服务器都会产生 一个对应的socket,每个连接这个socket值都不重复所以,这个socket就作为红黑树中的key把这个节点添加到红黑树上去;
EPOLL_CTL_MOD:修改事件;你 用了EPOLL_CTL_ADD把节点添加到红黑树上之后,才存在修改;
EPOLL_CTL_DEL:是从红黑树上把这个节点干掉;这会导致这个socket【这个tcp链接】上无法收到任何系统通知事件(不是关闭这个TCP连接),所以这一项只有需要用时才用;

参数sockid:一个TCP连接。添加事件(往红黑树中增加节点)时,就可以通过sockid作为key往红黑树中增加节点

④event:向epoll_ctl函数传递信息。如要增加事件信息,就可以通过event参数讲具体事件传递进epoll_ctl函数。这里包括的是 一些事件信息;EPOLL_CTL_ADD和EPOLL_CTL_MOD都要用到这个event参数里边的事件信息;

4)具体代码原理:

(1)如果传递进来的是EPOLL_CTL_ADD

①如果传递进来的是EPOLL_CTL_ADD,则观察if(op == EPOLL_CTL_ADD)条件成立时所执行的代码,代码首先使用RB_FIND来查红黑树上是否已经有了该节点,如果有了,则直接返回;若没有,程序流程才继续往下走
(以下为wangbojing代码)在这里插入图片描述

②生成了一个epitem对象,大家注意这个结构epitem,这个结构对象,其实就是红黑的一个节点,也就是说,红黑树的每个节点都是 一个epitem对象;
a)epi = (struct epitem*)calloc(1, sizeof(struct epitem));
b) 把socket(TCP连接)保存到节点中;
epi->sockfd = sockid; //作为红黑树节点的key,保存在红黑树中
在这里插入图片描述
c) 我们要增加的事件也保存到节点中;
//void *memcpy(void *destin, void *source, unsigned n)
//从源内存地址的起始位置开始拷贝若干个字节到目标内存地址中,即从源source中拷贝n个字节到目标destin中
//与strcpy()不同的是,memcpy()会完整的复制n个字节,不会因为遇到字符串结束’\0’而结束
memcpy(&epi->event, event, sizeof(struct epoll_event));
在这里插入图片描述
d) 增加节点到红黑树 : epi = RB_INSERT(_epoll_rb_socket, &ep->rbr, epi); 【EPOLL_CTL_ADD】增加节点到红黑树中
epitem.rbn ,代表三个指针,分别指向红黑树的左子树,右子树,父亲;
在这里插入图片描述

(2)如果传递进来的是EPOLL_CTL_DEL

①RB_FIND找到sockid对应的红黑树节点,返回结构体指针epi
②从红黑树中把节点干掉 : epi = RB_REMOVE(_epoll_rb_socket, &ep->rbr, epi);【EPOLL_CTL_DEL】,从红黑树中把节点干掉,
在这里插入图片描述

(3)如果传递进来的是EPOLL_CTL_MOD

红黑树节点,修改这个节点中的内容;:EPOLL_CTL_MOD,找到红黑树节点,修改这个节点中的内容;
在这里插入图片描述

(3.4)epoll_wait()函数

1) 格式:
int epoll_wait(int epfd,struct epoll_event *events,int maxevents,int timeout);
2)功能
阻塞一小段时间并等待事件发生,返回事件集合,也就是获取内核的事件通知;说白了就是遍历这个双向链表,把这个双向链表里边的节点数据拷贝出去,拷贝完毕的就从双向链表里移除该节点。因为双向链表里记录的是所有事件的socket【TCP连接】;

①参数epfd:是epoll_create()返回的epoll对象描述符;
②参数events:是内存,也是数组,长度 是maxevents,表示此次epoll_wait调用可以收集到maxevents个已经就绪【已经准备好的】的读写事件;说白了,就是返回的是 实际 发生事件的tcp连接数目;(因为内存有限,可能100个TCP上有事件发生,但返回的数字却是80——小于100)
③参数timeout:阻塞等待的时长;

3)总体来说,这个函数就是到双向链表中,把此时同时连入的连接中有事情发生的连接拿过来,后续用read、write或send、recv之类的函数收发数据,只有发生了某个事件/某些事件的socket,才会出现在双向链表中(下个话题才讲谁把数据插入到双向链表中去)
4)epitem结构设计的高明之处:既能够作为红黑树中的节点,又能够作为双向链表中的节点;

epitem的rbn成员让节点有三个指针,指向左右子树和父结点;
epitem的rdlink成员有两个指针,在双向链表中指向前后节点
epitem的rdy成员用于标记该节点是否存在于双向链表中,所以当节点从双向链表中移除时,rdy成员被置为0

5)原理:

①while(ep->rdnum == 0 %%timeout !=0)循环,用于等待一小段时间,这一小段时间的内发生的事件的节点(socket连接),就会被操作系统放到双向链表中,做的是:将事件的节点放入双向链表
②等待的时间到达后,确定本次返回给调用本函数的调用者程序的事件数量
③第二个while(num!=0…)做的事:将事件的节点放入双向链表,需要注意的是:节点始终在红黑树中存着,但是否在双向链表中取决于该节点是否收到了事件,若收到了事件,rdy=0,标记该节点已经不在双向链表中,

//到双向链表中去取相关的事件通知
int epoll_wait(int epid, struct epoll_event *events, int maxevents, int timeout) {

	nty_tcp_manager *tcp = nty_get_tcp_manager();
	if (!tcp) return -1;

	//nty_socket_map *epsocket = &tcp->smap[epid];
	struct _nty_socket *epsocket = tcp->fdtable->sockfds[epid];
	if (epsocket == NULL) return -1;

	if (epsocket->socktype == NTY_TCP_SOCK_UNUSED) {
		errno = -EBADF;
		return -1;
	}

	if (epsocket->socktype != NTY_TCP_SOCK_EPOLL) {
		errno = -EINVAL;
		return -1;
	}

	struct eventpoll *ep = (struct eventpoll*)epsocket->ep;
	if (!ep || !events || maxevents <= 0) {
		errno = -EINVAL;
		return -1;
	}

	if (pthread_mutex_lock(&ep->cdmtx)) {
		if (errno == EDEADLK) {
			nty_trace_epoll("epoll lock blocked\n");
		}
		assert(0);
	}

	//(1)这个while用来等待一定的时间【在这段时间内,发生事件的TCP连接,相关的节点,会被操作系统扔到双向链表去【当然这个节点同时也在红黑树中呢】】
	//epitem的rdy成员用于标记该节点是否存在于双向链表中,所以当节点从双向链表中移除时,rdy成员被置为0
	//rdnum代表双向链表中的节点数量,就是多少个TCP连接收到事件了
	//将事件的节点放入双向链表
	while (ep->rdnum == 0 && timeout != 0) {

		ep->waiting = 1;
		if (timeout > 0) {

			struct timespec deadline;

			clock_gettime(CLOCK_REALTIME, &deadline);
			if (timeout >= 1000) {
				int sec;
				sec = timeout / 1000;
				deadline.tv_sec += sec;
				timeout -= sec * 1000;
			}

			deadline.tv_nsec += timeout * 1000000;

			if (deadline.tv_nsec >= 1000000000) {
				deadline.tv_sec++;
				deadline.tv_nsec -= 1000000000;
			}

			int ret = pthread_cond_timedwait(&ep->cond, &ep->cdmtx, &deadline);
			if (ret && ret != ETIMEDOUT) {
				nty_trace_epoll("pthread_cond_timewait\n");
				
				pthread_mutex_unlock(&ep->cdmtx);
				
				return -1;
			}
			timeout = 0;
		} else if (timeout < 0) {

			int ret = pthread_cond_wait(&ep->cond, &ep->cdmtx);
			if (ret) {
				nty_trace_epoll("pthread_cond_wait\n");
				pthread_mutex_unlock(&ep->cdmtx);

				return -1;
			}
		}
		ep->waiting = 0; 

	}

	pthread_mutex_unlock(&ep->cdmtx);

	//等一小段时间,等时间到达后,流程来到这里。。。。。。。。。。。。。。

	pthread_spin_lock(&ep->lock);

	int cnt = 0;

	//(1)取得事件的数量
	//ep->rdnum:代表双向链表里边的节点数量(也就是有多少个TCP连接来事件了)
	//maxevents:此次调用最多可以收集到maxevents个已经就绪【已经准备好】的读写事件
	//从双向链表中取出节点信息复制出来,复制到提供的events参数中,将该节点从双向链表中删除
	int num = (ep->rdnum > maxevents ? maxevents : ep->rdnum); //哪个数量少,就取得少的数字作为要取的事件数量
	int i = 0;
	
	while (num != 0 && !LIST_EMPTY(&ep->rdlist)) { //EPOLLET

		//(2)每次都从双向链表头取得 一个一个的节点
		struct epitem *epi = LIST_FIRST(&ep->rdlist);

		//(3)把这个节点从双向链表中删除【但这并不影响这个节点依旧在红黑树中】
		LIST_REMOVE(epi, rdlink); 

		//(4)这是个标记,标记这个节点【这个节点本身是已经在红黑树中】已经不在双向链表中;
		epi->rdy = 0;  //当这个节点被操作系统 加入到 双向链表中时,这个标记会设置为1。

		//(5)把事件标记信息拷贝出来;拷贝到提供的events参数中
		memcpy(&events[i++], &epi->event, sizeof(struct epoll_event));
		
		num --;
		cnt ++;       //拷贝 出来的 双向链表 中节点数目累加
		ep->rdnum --; //双向链表里边的节点数量减1
	}
	
	pthread_spin_unlock(&ep->lock);

	//(5)返回 实际 发生事件的 tcp连接的数目;
	return cnt; 
}

(3.5)内核向双向链表增加节点(操作系统做的事,epoll_event_callback函数)

1)下面四个事件会触发操作系统把节点插入双向链表:

a)客户端完成三路握手,操作系统会向双向链表中插入节点,这时服务器要调用accept()函数把该连接从已完成队列中取走;
b)当客户端关闭连接,操作系统回想双向链表中插入节点,这时服务器也要调用close()关闭对应的socket;
c)客户端发送数据来的,操作系统回想双向链表中插入节点,服务器要调用read(),recv()函数来收数据;
d)当可以发送数据时,操作系统回想双向链表中插入节点,服务武器可以调用send(),write()向客户端发送数据。

2)当上面这四种情况之一发生时,操作系统会调用epoll_event_callback函数向双向链表中插入一个节点。

①使用RB_FIND在红黑树中寻找节点
②如果节点已经在双向链表中,则叠加事件后返回
这段做法存疑,因为epi->event.events事件原本是操作系统关注的事情,实际上操作系统内核是要判断该事件是否是程序关注的事件,如果是,才向双向链表插入数据,待商榷
③epi->rdy=1,把该节点是否在双向链表中的标记设置为1(表示在双向链表中)
④执行代码LIST_INSERT_HEAD把这个节点链入双向链表的表头位置
⑤另外,epoll_wait函数,如果双向链表有数据,可能不需要等待参数timeout中指定的时间,可能epoll_wait会立即返回;如果没有数据,才会等待timeout这么长的时间。

//当发生客户端三路握手连入、可读、可写、客户端断开等情况时,操作系统会调用这个函数,用以往双向链表中增加一个节点【该节点同时 也在红黑树中】
int epoll_event_callback(struct eventpoll *ep, int sockid, uint32_t event) {

	struct epitem tmp;
	tmp.sockfd = sockid;

	//(1)根据给定的key【这个TCP连接的socket】从红黑树中找到这个节点
	struct epitem *epi = RB_FIND(_epoll_rb_socket, &ep->rbr, &tmp);
	if (!epi) {
		nty_trace_epoll("rbtree not exist\n");
		assert(0);
	}

	//(2)从红黑树中找到这个节点后,判断这个节点是否已经被连入到双向链表里【判断的是rdy标志】
	//0为false,非0的1为true
	if (epi->rdy) {
		//这个节点已经在双向链表里,那无非是把新发生的事件标志增加到现有的事件标志中
		epi->event.events |= event;
		return 1;
	} 

	//走到这里,表示 双向链表中并没有这个节点,那要做的就是把这个节点连入到双向链表中

	nty_trace_epoll("epoll_event_callback --> %d\n", epi->sockfd);
	
	pthread_spin_lock(&ep->lock);

	//(3)标记这个节点已经被放入双向链表中,我们刚才研究epoll_wait()的时候,从双向链表中把这个节点取走的时候,这个标志被设置回了0
	epi->rdy = 1;  

	//(4)把这个节点链入到双向链表的表头位置
	LIST_INSERT_HEAD(&ep->rdlist, epi, rdlink);

	//(5)双向链表中的节点数量加1,刚才研究epoll_wait()的时候,从双向链表中把这个节点取走的时候,这个数量减了1
	ep->rdnum ++;
	pthread_spin_unlock(&ep->lock);

	pthread_mutex_lock(&ep->cdmtx);

	pthread_cond_signal(&ep->cond);
	pthread_mutex_unlock(&ep->cdmtx);
	return 0;
}

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
网狐6.6 服务器源代码&共享组件&数据库,包括服务器端所有组件(内核除外,现在市面上还没有6.6内核),共享组件(公共服务,界面控件,界面资源),以及全部数据库文件和脚本文件,6.6较6.5有较大改动,是学习研究的上等程序代码。 结合本人上次发布的客户端组件,即为网狐66系统模块全部源代码!!!官方售价几万,网上有人卖几百到几千不等,本程序绝对6.6新版代码(带道具),识货的下。 总共140M,7个解压包:1~7。 #ifndef KERNEL_ENGINE_HEAD_HEAD_FILE #define KERNEL_ENGINE_HEAD_HEAD_FILE ////////////////////////////////////////////////////////////////////////// //包含文件 #include #include #include #include #include //平台文件 #include "..\..\模板库\Template.h" #include "..\..\公共文件\Constant.h" #include "..\..\公共文件\GlobalDef.h" #include "..\..\共享组件\公共服务\ComService.h" ////////////////////////////////////////////////////////////////////////// //ADO 定义 #import "MSADO15.DLL" rename_namespace("ADOCG") rename("EOF","EndOfFile") using namespace ADOCG; typedef _com_error CComError; //COM 错误 typedef _variant_t CDBVarValue; //数据库数值 ////////////////////////////////////////////////////////////////////////// //导出定义 //导出定义 #ifndef KERNEL_ENGINE_CLASS #ifdef KERNEL_ENGINE_DLL #define KERNEL_ENGINE_CLASS _declspec(dllexport) #else #define KERNEL_ENGINE_CLASS _declspec(dllimport) #endif #endif //模块定义 #ifndef _DEBUG #define KERNEL_ENGINE_DLL_NAME TEXT("KernelEngine.dll") //组件 DLL 名字 #else #define KERNEL_ENGINE_DLL_NAME TEXT("KernelEngineD.dll") //组件 DLL 名字 #endif ////////////////////////////////////////////////////////////////////////// //系统常量 //常量定义 #define TIME_CELL 200 //时间单元 #define TIMES_INFINITY DWORD(-1) //无限次数 #define MAX_ASYNCHRONISM_DATA 8192 //异步数据 ////////////////////////////////////////////////////////////////////////// //网络定义 //连接错误 #define CONNECT_SUCCESS 0 //连接成功 #define CONNECT_FAILURE 1 //连接失败 #define CONNECT_EXCEPTION 2 //参数异常 //关闭原因 #define SHUT_REASON_INSIDE 0 //内部原因 #define SHUT_REASON_NORMAL 1 //正常关闭 #define SHUT_REASON_REMOTE 2 //远程关闭 #define SHUT_REASON_TIME_OUT 3 //网络超时
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值