
从零构建通讯器--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表示等到才返回发表评论
最新留言
哈哈,博客排版真的漂亮呢~
[***.90.31.176]2025年03月09日 17时05分44秒
关于作者

喝酒易醉,品茶养心,人生如梦,品茶悟道,何以解忧?唯有杜康!
-- 愿君每日到此一游!
推荐文章
面试别慌!阿里专家带你从【入门+基础+进阶+项目】攻破SpringBoot
2019-03-03
【Java面试】30个 Java 集合面试必备的问题和答案
2019-03-03
干了八年的阿里面试官,给大家分享我面试时最爱问的Java面试题
2019-03-03
华为鸿蒙到底是不是安卓系统套了个壳?
2019-03-03
redis知识点学习
2019-03-03
分布式理论基础知识点入门
2019-03-03
SpringCloud之消息总线(Spring Cloud Bus)刷新配置
2019-03-03
多线程之创建线程的两种方式
2019-03-03
fragment中recyclerview的重新加载问题
2019-03-03
window程序设计(1):第一个windows程序
2019-03-03
windows程序设计(4):文本输出
2019-03-03
JZOJ7月29日提高组反思
2019-03-03
21.2.3总结
2019-03-03
线性代数和数学期望杂题
2019-03-03
21.2.4总结
2019-03-03
【SSL_P2876】2017年东莞市信息学特长生测试题 工程
2019-03-03
【洛谷_P1433】吃奶酪
2019-03-03
【SSL_2020.10.28】区间和的和
2019-03-03
【学习笔记】NumPy数据存取与函数
2019-03-03