【手把手教你写服务器】监听端口功能的实现、epoll技术概述

1.监听端口功能的实现

ngx_c_socket.h:

#ifndef __NGX_SOCKET_H__
#define __NGX_SOCKET_H__

#include <vector>

// 一些宏定义放在这里
#define NGX_LISTEN_BACKLOG 511 // 已完成连接队列,nginx官方给的就是511

// 一些专用结构定义放在这里
typedef struct ngx_listening_s // 和监听端口有关的结构
{
	int port; // 监听的端口号
	int fd; // 套接字描述符
} 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

ngx_c_socket.cxx:

// 和网络有关的函数放这里

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <stdarg.h>
#include <unistd.h>
#include <sys/time.h>
#include <time.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#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)
	{
        delete (*pos); // 一定要把指针指向的内存干掉,不然内存泄漏
	}
    m_ListenSocketList.clear(); 
    return;
}

/*
函数功能:
初始化函数

调用时机:
在fork()子进程之前调用该函数

返回值:
成功返回true,失败返回false
*/
bool CSocekt::Initialize()
{
    bool reco = ngx_open_listening_sockets();
    return reco;
}

/*
函数功能:
监听端口(支持多个端口),从配置文件中读取要监听的端口数量

调用时机:
在创建worker进程之前就要执行该函数
*/
bool CSocekt::ngx_open_listening_sockets()
{
    CConfig* p_config = CConfig::GetInstance();
    m_ListenPortCount = p_config->GetIntDefault("ListenPortCount", m_ListenPortCount); // 取得要监听的端口数量
    
    int isock; // 套接字描述符
    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++) // 监听多个端口
    {
        // socket():系统函数,成功返回非负描述符,出错返回-1
        // 第一个参数:AF_INET,使用ipv4协议
        // 第二个参数:SOCK_STREAM,使用TCP,表示可靠连接
        // 第三个参数:给0
        isock = socket(AF_INET, SOCK_STREAM, 0);
        if (isock == -1)
        {
            ngx_log_stderr(errno, "CSocekt::Initialize()中socket()失败,i=%d.", i);
            return false;
        }

        // setsockopt():设置一些套接字参数选项
        // 第二个参数:表示级别,和第三个参数配套使用,也就是说,如果第三个参数确定了,第二个参数也就确定了
        // 第三个参数:允许重用本地地址,设置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); // 加入到队列中
    }

    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,文件控制相关函数,执行各种描述符控制操作
    // 第一个参数:所要设置的描述符,这里是套接字描述符sockfd
    // 第二个参数:用F_GETFL先获取描述符的一些标志信息
    int opts = fcntl(sockfd, F_GETFL);
    if (opts < 0)
    {
        ngx_log_stderr(errno, "CSocekt::setnonblocking()中fcntl(F_GETFL)失败.");
        return false;
    }
    opts |= O_NONBLOCK; // 把非阻塞标记加到原来的标记上,标记这是个非阻塞套接字
    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++) // 关闭多个监听端口
    {
        close(m_ListenSocketList[i]->fd);
        ngx_log_error_core(NGX_LOG_INFO, 0, "关闭监听端口%d!", m_ListenSocketList[i]->port); // 显示一些信息到日志中
    }
    return;
}

2.epoll技术概述

在这里插入图片描述

epoll 就是一种典型的 I/O 多路复用技术,最大特点是支持高并发。

很多服务器程序可能用多进程,每一个进程对应一个连接;也可能用多线程,每一个线程对应一个连接。epoll是事件驱动机制,在单独的进程或单独的线程里运行,收集或处理事件,没有进程或线程之间切换的消耗,十分高效。

2.1 epoll_create()函数

int epoll_create(int size); // 返回一个epoll对象的描述符

创建了一个 eventpoll 结构对象,被系统保存起来,其中 rbr 成员被初始化成指向一棵红黑树的根,rdlist 成员被初始化成指向一个双向链表的头。

在这里插入图片描述

2.2 epoll_ctl()函数

int epoll_ctl(int efpd, int op, int sockid, struct epoll_event *event);

函数功能:

把一个socket以及这个socket相关的事件添加到这个epoll对象描述符中去,目的是通过这个epoll对象来监视这个socket上数据的来往情况,当有数据来往时,系统会通知我们。

参数含义:

  • efpd:epoll_create() 返回的epoll对象描述符。

  • op:动作,添加 EPOLL_CTL_ADD、删除 EPOLL_CTL_DEL、修改 EPOLL_CTL_MOD,对应的数字分别是 1、2、3。

    • EPOLL_CTL_ADD:等价于往红黑树中增加节点
    • EPOLL_CTL_DEL:等价于从红黑树中删除节点
    • EPOLL_CTL_MOD:等价于修改已有的红黑树的节点
  • sockid:表示客户端连接,每个客户端连入服务器后,服务器都会产生一个对应的socket,每个连接的socket值都不重复,这个socket就是红黑树中的key。

  • event:事件信息,EPOLL_CTL_ADD 和 EPOLL_CTL_MOD 都要用到这个event参数里边的事件信息。

红黑树中的节点就是 epitem 结构。

在这里插入图片描述

在这里插入图片描述

2.3 epoll_wait()函数

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

函数功能:

阻塞一小段时间并等待事件发生,返回事件集合,也就是获取内核的事件通知。说白了就是遍历双向链表,把这个双向链表里边的节点数据拷贝出去,拷贝完毕的就从双向链表里移除,因为双向链表里记录的是所有有事件的socket。

在这里插入图片描述

参数含义:

  • epfd:epoll_create() 返回的epoll对象描述符。

  • events:数组,长度是 maxevents,表示此次epoll_wait()调用最多可以收集到maxevents个已经就绪的读写事件。

  • timeout:阻塞等待的时长。

epitem 结构设计的高明之处:既能够作为红黑树中的节点,又能够作为双向链表中的节点。

在这里插入图片描述

在这里插入图片描述

2.4 内核向双向链表增加节点

一般有四种情况会使操作系统把节点插入到双向链表中:

  • 当客户端完成三路握手,服务器要 accept()
  • 当客户端关闭连接,服务器要调用 close() 关闭
  • 当客户端发送来数据,服务器要调用 read()、recv() 函数来收数据
  • 当可以发送数据时,服务器可以调用 send()、write()
  • 其他情况
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值