从零构建通讯器--5.5监听端口实战、epoll介绍及原理详析
发布日期:2021-05-04 18:23:31 浏览次数:20 分类:技术文章

本文共 8820 字,大约阅读时间需要 29 分钟。

(1)监听端口

引入新文件,net引入ngx_c_socket.cxx和头文件引入ngx_c_socket.h(放入socket相关的类)
更新文件nginx.conf,加监听的端口数量和端口具体信息
ngx_c_socket.cxx

//和网络 有关的函数放这里#include 
#include
#include
#include
//uintptr_t#include
//va_start....#include
//STDERR_FILENO等#include
//gettimeofday#include
//localtime_r#include
//open#include
//errno#include
#include
//ioctl#include
#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
::iterator pos; for(pos = m_ListenSocketList.begin(); pos != m_ListenSocketList.end(); ++pos) //遍历vector { delete (*pos); //一定要把指针指向的内存干掉,不然内存泄漏 }//end for m_ListenSocketList.clear(); return;}//初始化函数【fork()子进程之前干这个事】//成功返回true,失败返回falsebool 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 
//一些宏定义放在这里-----------------------------------------------------------#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
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()函数
格式:int epoll_create(int size);,size: >0;
功能:创建一个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()函数
格式:int epoll_ctl(int efpd,int op,int sockid,struct epoll_event event);
功能:把一个socket以及这个socket相关的事件添加到这个epoll对象描述符中去,目的就是通过这个epoll对象来监视这个socket【客户端的TCP连接】上数据的来往情况;
注意:efpd: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链接】上无法收到任何系统通知事件;
补充:
sockid:表示客户端连接,就是你从accept();这个是红黑树里边的key;,先以key为关键字查找红黑树中的节点
event:事件信息,这里包括的是 一些事件信息;EPOLL_CTL_ADD和EPOLL_CTL_MOD都要用到这个event参数里边的事件信息;
原理:
a)epi = (struct epitem
)calloc(1, sizeof(struct epitem));
b)
①增加节点到红黑树 : epi = RB_INSERT(_epoll_rb_socket, &ep->rbr, epi); 【EPOLL_CTL_ADD】增加节点到红黑树中
epitem.rbn ,代表三个指针,分别指向红黑树的左子树,右子树,父亲;
②从红黑树中把节点干掉 : epi = RB_REMOVE(_epoll_rb_socket, &ep->rbr, epi);【EPOLL_CTL_DEL】,从红黑树中把节点干掉,
③红黑树节点,修改这个节点中的内容;:EPOLL_CTL_MOD,找到红黑树节点,修改这个节点中的内容;
(3.4)epoll_wait()函数—>>从双向链表中拷贝事件,把在红黑树上的事件干掉
格式:int epoll_wait(int epfd,struct epoll_event *events,int maxevents,int timeout);
//功能:阻塞一小段时间并等待事件发生,返回事件集合,也就是获取内核的事件通知;
//说白了就是遍历这个双向链表,把这个双向链表里边的节点数据拷贝出去,拷贝完毕的就从双向链表里移除;
//因为双向链表里记录的是所有有数据/有事件的socket【TCP连接】;
//参数epfd:是epoll_create()返回的epoll对象描述符;
//参数events:是内存,也是数组,长度 是maxevents,表示此次epoll_wait调用可以手机到的maxevents个已经继续【已经准备好的】的读写事件;
//说白了,就是返回的是 实际 发生事件的tcp连接数目;
//参数timeout:阻塞等待的时长;
//epitem结构设计的高明之处:既能够作为红黑树中的节点,又能够作为双向链表中的节点;
(3.5)内核向双向链表增加节点
/a)客户端完成三路握手;服务器要accept();
//b)当客户端关闭连接,服务器也要调用close()关闭;
//c)客户端发送数据来的;服务器要调用read(),recv()函数来收数据;
//d)当可以发送数据时;服务武器可以调用send(),write();
//e)其他情况;写实战代码再说;
(3.6)源码阅读额外说明
补充:timeut表示等到才返回

上一篇:从零构建通讯器--5.6 通讯代码精粹之epoll函数实战1(连接池)
下一篇:关于信号的截断备忘录

发表评论

最新留言

哈哈,博客排版真的漂亮呢~
[***.90.31.176]2025年03月09日 17时05分44秒