5-8 通讯代码精粹之收包解包实战

一:收包分析及包头结构定义

发包:采用 包头+包体,其中包头中记录着整个包【包头—+包体】的长度;
包头:就是一个结构;
a)一个包的长度不能超过30000个字节,必须要有最大值;
伪造恶意数据包,他规定这里300亿,我这个规定能够确保服务器程序不会处于非常危险的境地;
b)开始定义包头结构:COMM_PKG_HEADER
c)*********大家千万注意这个问题,不然会大错特错;
结构字节对齐问题;为了防止出现字节问题,所有在网络上传输的这种结构,必须都采用1字节对齐方式

二:收包状态宏定义

收包:粘包,缺包;
收包思路:先手包头->根据包头中的内容确定包体长度并收包体,收包状态(状态机);
定义几种收包的状态, 4种:0,1,2,3

三:收包实战代码

聚焦在ngx_wait_request_handler()函数;
同时设置好各种收包的状态:c->curStat = _PKG_HD_INIT; c->precvbuf = c->dataHeadInfo; c->irecvlen = sizeof(COMM_PKG_HEADER);
我们要求,客户端连入到服务器后,要主动地【客户端有义务】给服务器先发送数据包;服务器要主动收客户端的数据包;
服务器按照 包头 + 包体的格式来收包;

引入一个消息头【结构】STRUC_MSG_HEADER,用来记录一些额外信息
服务器 收包时, 收到: 包头+包体 ,我再额外附加一个消息头 ===》 消息头 + 包头 + 包体
再介绍一个分配和释放内存类CMemory;
本项目中不考虑内存池; 内存池:对于提高程序运行效率帮助有效;new非常快;
内存池主要功能就是 频繁的分配小块内存时 内存池可以节省额外内存开销【代价就是代码更复杂】;

四:遗留问题处理

inMsgRecvQueue,tmpoutMsgRecvQueue,clearMsgRecvQueue

五:测试服务器收包避免推诿扯皮

验证ngx_wait_request_handler()函数是否正常工作,准备写一个客户端程序;
windows vs2017 ,mfc程序
老师强制大家要写测试程序来测试;你可以写linux平台下的测试程序;
觉悟:服务器主程序员【重担压肩】;
防止扯皮,所以服务器端有必要自己书写一个客户端测试程序;
说明:windows vs2017 客户端测试代码,非常简陋,只用于演示目的,不具备商业代码质量;
客户端的 SendData()函数值得学习;
核心代码;MFCApplication3Dlg.cpp

源码:

ngx_comm.h


#ifndef __NGX_COMM_H__
#define __NGX_COMM_H__

//宏定义------------------------------------
#define _PKG_MAX_LENGTH     30000  //每个包的最大长度【包头+包体】不超过这个数字,为了留出一些空间,实际上编码是,包头+包体长度必须不大于 这个值-1000【29000】

//通信 收包状态定义
#define _PKG_HD_INIT         0  //初始状态,准备接收数据包头
#define _PKG_HD_RECVING      1  //接收包头中,包头不完整,继续接收中
#define _PKG_BD_INIT         2  //包头刚好收完,准备接收包体
#define _PKG_BD_RECVING      3  //接收包体中,包体不完整,继续接收中,处理后直接回到_PKG_HD_INIT状态
//#define _PKG_RV_FINISHED     4  //完整包收完,这个状态似乎没什么 用处

#define _DATA_BUFSIZE_       20  //因为我要先收包头,我希望定义一个固定大小的数组专门用来收包头,这个数字大小一定要 >sizeof(COMM_PKG_HEADER) ,所以我这里定义为20绰绰有余;
                                  //如果日后COMM_PKG_HEADER大小变动,这个数字也要做出调整以满足 >sizeof(COMM_PKG_HEADER) 的要求

//结构定义------------------------------------
#pragma pack (1) //对齐方式,1字节对齐【结构之间成员不做任何字节对齐:紧密的排列在一起】

//一些和网络通讯相关的结构放在这里
//包头结构
typedef struct _COMM_PKG_HEADER
{
	unsigned short pkgLen;    //报文总长度【包头+包体】--2字节,2字节可以表示的最大数字为6万多,我们定义_PKG_MAX_LENGTH 30000,所以用pkgLen足够保存下
	                            //包头中记录着整个包【包头—+包体】的长度

	unsigned short msgCode;   //消息类型代码--2字节,用于区别每个不同的命令【不同的消息】
	int            crc32;     //CRC32效验--4字节,为了防止收发数据中出现收到内容和发送内容不一致的情况,引入这个字段做一个基本的校验用	
}COMM_PKG_HEADER,*LPCOMM_PKG_HEADER;


#pragma pack() //取消指定对齐,恢复缺省对齐


#endif

ngx_c_socket_conn.cxx


//和网络 中 连接/连接池 有关的函数放这里
/*
王健伟老师 《Linux C++通讯架构实战》
商业级质量的代码,完整的项目,帮你提薪至少10K
*/
#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"
#include "ngx_c_memory.h"

//从连接池中获取一个空闲连接【当一个客户端连接TCP进入,我希望把这个连接和我的 连接池中的 一个连接【对象】绑到一起,后续 我可以通过这个连接,把这个对象拿到,因为对象里边可以记录各种信息】
lpngx_connection_t CSocekt::ngx_get_connection(int isock)
{
    lpngx_connection_t  c = m_pfree_connections; //空闲连接链表头

    if(c == NULL)
    {
        //系统应该控制连接数量,防止空闲连接被耗尽,能走到这里,都不正常
        ngx_log_stderr(0,"CSocekt::ngx_get_connection()中空闲链表为空,这不应该!");
        return NULL;
    }

    m_pfree_connections = c->data;                       //指向连接池中下一个未用的节点
    m_free_connection_n--;                               //空闲连接少1
    
    //(1)注意这里的操作,先把c指向的对象中有用的东西搞出来保存成变量,因为这些数据可能有用
    uintptr_t  instance = c->instance;            //常规c->instance在刚构造连接池时这里是1【失效】
    uint64_t   iCurrsequence = c->iCurrsequence;  //序号也暂存,后续用于恢复
    //....其他内容再增加


    //(2)把以往有用的数据搞出来后,清空并给适当值
    memset(c,0,sizeof(ngx_connection_t));                //注意,类型不要用成lpngx_connection_t,否则就出错了
    c->fd      = isock;                                  //套接字要保存起来,这东西具有唯一性    
    c->curStat = _PKG_HD_INIT;                           //收包状态处于 初始状态,准备接收数据包头【状态机】

    c->precvbuf = c->dataHeadInfo;                       //收包我要先收到这里来,因为我要先收包头,所以收数据的buff直接就是dataHeadInfo
    c->irecvlen = sizeof(COMM_PKG_HEADER);               //这里指定收数据的长度,这里先要求收包头这么长字节的数据

    c->ifnewrecvMem = false;                             //标记我们并没有new内存,所以不用释放	 
    c->pnewMemPointer = NULL;                            //既然没new内存,那自然指向的内存地址先给NULL
    //....其他内容再增加


    //(3)这个值有用,所以在上边(1)中被保留,没有被清空,这里又把这个值赋回来
    c->instance = !instance;                            //抄自官方nginx,到底有啥用,以后再说【分配内存时候,连接池里每个连接对象这个变量给的值都为1,所以这里取反应该是0【有效】;】
    c->iCurrsequence=iCurrsequence;++c->iCurrsequence;  //每次取用该值都增加1

    //wev->write = 1;  这个标记有没有 意义加,后续再研究
    return c;    
}

//归还参数c所代表的连接到到连接池中,注意参数类型是lpngx_connection_t
void CSocekt::ngx_free_connection(lpngx_connection_t c) 
{
    if(c->ifnewrecvMem == true)
    {
        //我们曾经给这个连接分配过内存,则要释放内存        
        CMemory::GetInstance()->FreeMemory(c->pnewMemPointer);
        c->pnewMemPointer = NULL;
        c->ifnewrecvMem = false;  //这行有用?
    }

    c->data = m_pfree_connections;                       //回收的节点指向原来串起来的空闲链的链头

    //节点本身也要干一些事
    ++c->iCurrsequence;                                  //回收后,该值就增加1,以用于判断某些网络事件是否过期【一被释放就立即+1也是有必要的】

    m_pfree_connections = c;                             //修改 原来的链头使链头指向新节点
    ++m_free_connection_n;                               //空闲连接多1    
    return;
}

//用户连入,我们accept4()时,得到的socket在处理中产生失败,则资源用这个函数释放【因为这里涉及到好几个要释放的资源,所以写成函数】
//我们把ngx_close_accepted_connection()函数改名为让名字更通用,并从文件ngx_socket_accept.cxx迁移到本文件中,并改造其中代码,注意顺序
void CSocekt::ngx_close_connection(lpngx_connection_t c)
{
    if(close(c->fd) == -1)
    {
        ngx_log_error_core(NGX_LOG_ALERT,errno,"CSocekt::ngx_close_connection()中close(%d)失败!",c->fd);  
    }
    c->fd = -1; //官方nginx这么写,这么写有意义;    
    ngx_free_connection(c); //把释放代码放在最后边,感觉更合适
    return;
}

ngx_c_socket.h


#ifndef __NGX_SOCKET_H__
#define __NGX_SOCKET_H__

#include <vector>      //vector
#include <list>        //list
#include <sys/epoll.h> //epoll
#include <sys/socket.h>

#include "ngx_comm.h"

//一些宏定义放在这里-----------------------------------------------------------
#define NGX_LISTEN_BACKLOG  511    //已完成连接队列,nginx给511,我们也先按照这个来:不懂这个数字的同学参考第五章第四节
#define NGX_MAX_EVENTS      512    //epoll_wait一次最多接收这么多个事件,nginx中缺省是512,我们这里固定给成512就行,没太大必要修改

typedef struct ngx_listening_s   ngx_listening_t, *lpngx_listening_t;
typedef struct ngx_connection_s  ngx_connection_t,*lpngx_connection_t;
typedef class  CSocekt           CSocekt;

typedef void (CSocekt::*ngx_event_handler_pt)(lpngx_connection_t c); //定义成员函数指针

//--------------------------------------------
//一些专用结构定义放在这里,暂时不考虑放ngx_global.h里了
struct ngx_listening_s  //和监听端口有关的结构
{
	int                       port;        //监听的端口号
	int                       fd;          //套接字句柄socket
	lpngx_connection_t        connection;  //连接池中的一个连接,注意这是个指针 
};

//以下三个结构是非常重要的三个结构,我们遵从官方nginx的写法;
//(1)该结构表示一个TCP连接【客户端主动发起的、Nginx服务器被动接受的TCP连接】
struct ngx_connection_s
{
	
	int                       fd;             //套接字句柄socket
	lpngx_listening_t         listening;      //如果这个链接被分配给了一个监听套接字,那么这个里边就指向监听套接字对应的那个lpngx_listening_t的内存首地址		

	//------------------------------------	
	unsigned                  instance:1;     //【位域】失效标志位:0:有效,1:失效【这个是官方nginx提供,到底有什么用,ngx_epoll_process_events()中详解】  
	uint64_t                  iCurrsequence;  //我引入的一个序号,每次分配出去时+1,此法也有可能在一定程度上检测错包废包,具体怎么用,用到了再说
	struct sockaddr           s_sockaddr;     //保存对方地址信息用的
	//char                      addr_text[100]; //地址的文本信息,100足够,一般其实如果是ipv4地址,255.255.255.255,其实只需要20字节就够

	//和读有关的标志-----------------------
	//uint8_t                   r_ready;        //读准备好标记【暂时没闹明白官方要怎么用,所以先注释掉】
	uint8_t                   w_ready;        //写准备好标记

	ngx_event_handler_pt      rhandler;       //读事件的相关处理方法
	ngx_event_handler_pt      whandler;       //写事件的相关处理方法
	
	//和收包有关
	unsigned char             curStat;                        //当前收包的状态
	char                      dataHeadInfo[_DATA_BUFSIZE_];   //用于保存收到的数据的包头信息			
	char                      *precvbuf;                      //接收数据的缓冲区的头指针,对收到不全的包非常有用,看具体应用的代码
	unsigned int              irecvlen;                       //要收到多少数据,由这个变量指定,和precvbuf配套使用,看具体应用的代码

	bool                      ifnewrecvMem;                   //如果我们成功的收到了包头,那么我们就要分配内存开始保存 包头+消息头+包体内容,这个标记用来标记是否我们new过内存,因为new过是需要释放的
	char                      *pnewMemPointer;                //new出来的用于收包的内存首地址,和ifnewrecvMem配对使用

	//--------------------------------------------------
	lpngx_connection_t        data;           //这是个指针【等价于传统链表里的next成员:后继指针】,指向下一个本类型对象,用于把空闲的连接池对象串起来构成一个单向链表,方便取用
};

//消息头,引入的目的是当收到数据包时,额外记录一些内容以备将来使用
typedef struct _STRUC_MSG_HEADER
{
	lpngx_connection_t pConn;         //记录对应的链接,注意这是个指针
	uint64_t           iCurrsequence; //收到数据包时记录对应连接的序号,将来能用于比较是否连接已经作废用
	//......其他以后扩展	
}STRUC_MSG_HEADER,*LPSTRUC_MSG_HEADER;

//------------------------------------
//socket相关类
class CSocekt
{
public:
	CSocekt();                                                         //构造函数
	virtual ~CSocekt();                                                //释放函数
public:    
    virtual bool Initialize();                                         //初始化函数

public:	
	int  ngx_epoll_init();                                             //epoll功能初始化
	//void ngx_epoll_listenportstart();                                  //监听端口开始工作 
	int  ngx_epoll_add_event(int fd,int readevent,int writeevent,uint32_t otherflag,uint32_t eventtype,lpngx_connection_t c);     
	                                                                   //epoll增加事件
	int  ngx_epoll_process_events(int timer);                          //epoll等待接收和处理事件

private:	
	void ReadConf();                                                   //专门用于读各种配置项	
	bool ngx_open_listening_sockets();                                 //监听必须的端口【支持多个端口】
	void ngx_close_listening_sockets();                                //关闭监听套接字
	bool setnonblocking(int sockfd);                                   //设置非阻塞套接字	

	//一些业务处理函数handler
	void ngx_event_accept(lpngx_connection_t oldc);                    //建立新连接
	void ngx_wait_request_handler(lpngx_connection_t c);               //设置数据来时的读处理函数
	void ngx_close_connection(lpngx_connection_t c);                   //通用连接关闭函数,资源用这个函数释放【因为这里涉及到好几个要释放的资源,所以写成函数】

	ssize_t recvproc(lpngx_connection_t c,char *buff,ssize_t buflen);  //接收从客户端来的数据专用函数
	void ngx_wait_request_handler_proc_p1(lpngx_connection_t c);       //包头收完整后的处理,我们称为包处理阶段1:写成函数,方便复用	                                                                   
	void ngx_wait_request_handler_proc_plast(lpngx_connection_t c);    //收到一个完整包后的处理,放到一个函数中,方便调用
	void inMsgRecvQueue(char *buf);                                    //收到一个完整消息后,入消息队列
	void tmpoutMsgRecvQueue(); //临时清除对列中消息函数,测试用,将来会删除该函数
	void clearMsgRecvQueue();                                          //清理接收消息队列

	//获取对端信息相关                                              
	size_t ngx_sock_ntop(struct sockaddr *sa,int port,u_char *text,size_t len);  //根据参数1给定的信息,获取地址端口字符串,返回这个字符串的长度

	//连接池 或 连接 相关
	lpngx_connection_t ngx_get_connection(int isock);                  //从连接池中获取一个空闲连接
	void ngx_free_connection(lpngx_connection_t c);                    //归还参数c所代表的连接到到连接池中	

private:
	int                            m_worker_connections;               //epoll连接的最大项数
	int                            m_ListenPortCount;                  //所监听的端口数量
	int                            m_epollhandle;                      //epoll_create返回的句柄

	//和连接池有关的
	lpngx_connection_t             m_pconnections;                     //注意这里可是个指针,其实这是个连接池的首地址
	lpngx_connection_t             m_pfree_connections;                //空闲连接链表头,连接池中总是有某些连接被占用,为了快速在池中找到一个空闲的连接,我把空闲的连接专门用该成员记录;
	                                                                        //【串成一串,其实这里指向的都是m_pconnections连接池里的没有被使用的成员】
	//lpngx_event_t                  m_pread_events;                     //指针,读事件数组
	//lpngx_event_t                  m_pwrite_events;                    //指针,写事件数组
	int                            m_connection_n;                     //当前进程中所有连接对象的总数【连接池大小】
	int                            m_free_connection_n;                //连接池中可用连接总数
	std::vector<lpngx_listening_t> m_ListenSocketList;                 //监听套接字队列
	struct epoll_event             m_events[NGX_MAX_EVENTS];           //用于在epoll_wait()中承载返回的所发生的事件

	//一些和网络通讯有关的成员变量
	size_t                         m_iLenPkgHeader;                    //sizeof(COMM_PKG_HEADER);		
	size_t                         m_iLenMsgHeader;                    //sizeof(STRUC_MSG_HEADER);
	//消息队列
	std::list<char *>              m_MsgRecvQueue;                     //接收数据消息队列 

};

#endif

ngx_c_memory.h


#ifndef __NGX_MEMORY_H__
#define __NGX_MEMORY_H__

#include <stddef.h>  //NULL
//内存相关类
class CMemory 
{
private:
	CMemory() {}  //构造函数,因为要做成单例类,所以是私有的构造函数

public:
	~CMemory(){};

private:
	static CMemory *m_instance;

public:	
	static CMemory* GetInstance() //单例
	{			
		if(m_instance == NULL)
		{
			//锁
			if(m_instance == NULL)
			{				
				m_instance = new CMemory(); //第一次调用不应该放在线程中,应该放在主进程中,以免和其他线程调用冲突从而导致同时执行两次new CMemory()
				static CGarhuishou cl; 
			}
			//放锁
		}
		return m_instance;
	}	
	class CGarhuishou 
	{
	public:				
		~CGarhuishou()
		{
			if (CMemory::m_instance)
			{						
				delete CMemory::m_instance; //这个释放是整个系统退出的时候,系统来调用释放内存的哦
				CMemory::m_instance = NULL;				
			}
		}
	};
	//-------

public:
	void *AllocMemory(int memCount,bool ifmemset);
	void FreeMemory(void *point);
	
};

#endif

ngx_c_socket_request.cxx


//和网络  中 客户端发送来数据/服务器端收包 有关的代码
/*
王健伟老师 《Linux C++通讯架构实战》
商业级质量的代码,完整的项目,帮你提薪至少10K
*/
#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"
#include "ngx_c_memory.h"

//来数据时候的处理,当连接上有数据来的时候,本函数会被ngx_epoll_process_events()所调用  ,官方的类似函数为ngx_http_wait_request_handler();
void CSocekt::ngx_wait_request_handler(lpngx_connection_t c)
{  
    //收包,注意我们用的第二个和第三个参数,我们用的始终是这两个参数,因此我们必须保证 c->precvbuf指向正确的收包位置,保证c->irecvlen指向正确的收包宽度
    ssize_t reco = recvproc(c,c->precvbuf,c->irecvlen); 
    if(reco <= 0)  
    {
        return;//该处理的上边这个recvproc()函数处理过了,这里<=0是直接return        
    }

    //走到这里,说明成功收到了一些字节(>0),就要开始判断收到了多少数据了     
    if(c->curStat == _PKG_HD_INIT) //连接建立起来时肯定是这个状态,因为在ngx_get_connection()中已经把curStat成员赋值成_PKG_HD_INIT了
    {        
        if(reco == m_iLenPkgHeader)//正好收到完整包头,这里拆解包头
        {   
            ngx_wait_request_handler_proc_p1(c); //那就调用专门针对包头处理完整的函数去处理把。
        }
        else
		{
			//收到的包头不完整--我们不能预料每个包的长度,也不能预料各种拆包/粘包情况,所以收到不完整包头【也算是缺包】是很可能的;
            c->curStat        = _PKG_HD_RECVING;                 //接收包头中,包头不完整,继续接收包头中	
            c->precvbuf       = c->precvbuf + reco;              //注意收后续包的内存往后走
            c->irecvlen       = c->irecvlen - reco;              //要收的内容当然要减少,以确保只收到完整的包头先
        } //end  if(reco == m_iLenPkgHeader)
    } 
    else if(c->curStat == _PKG_HD_RECVING) //接收包头中,包头不完整,继续接收中,这个条件才会成立
    {
        if(c->irecvlen == reco) //要求收到的宽度和我实际收到的宽度相等
        {
            //包头收完整了
            ngx_wait_request_handler_proc_p1(c); //那就调用专门针对包头处理完整的函数去处理把。
        }
        else
		{
			//包头还是没收完整,继续收包头
            //c->curStat        = _PKG_HD_RECVING;                 //没必要
            c->precvbuf       = c->precvbuf + reco;              //注意收后续包的内存往后走
            c->irecvlen       = c->irecvlen - reco;              //要收的内容当然要减少,以确保只收到完整的包头先
        }
    }
    else if(c->curStat == _PKG_BD_INIT) 
    {
        //包头刚好收完,准备接收包体
        if(reco == c->irecvlen)
        {
            //收到的宽度等于要收的宽度,包体也收完整了
            ngx_wait_request_handler_proc_plast(c);
        }
        else
		{
			//收到的宽度小于要收的宽度
			c->curStat = _PKG_BD_RECVING;					
			c->precvbuf = c->precvbuf + reco;
			c->irecvlen = c->irecvlen - reco;
		}
    }
    else if(c->curStat == _PKG_BD_RECVING) 
    {
        //接收包体中,包体不完整,继续接收中
        if(c->irecvlen == reco)
        {
            //包体收完整了
            ngx_wait_request_handler_proc_plast(c);
        }
        else
        {
            //包体没收完整,继续收
            c->precvbuf = c->precvbuf + reco;
			c->irecvlen = c->irecvlen - reco;
        }
    }  //end if(c->curStat == _PKG_HD_INIT)
    return;
}

//接收数据专用函数--引入这个函数是为了方便,如果断线,错误之类的,这里直接 释放连接池中连接,然后直接关闭socket,以免在其他函数中还要重复的干这些事
//参数c:连接池中相关连接
//参数buff:接收数据的缓冲区
//参数buflen:要接收的数据大小
//返回值:返回-1,则是有问题发生并且在这里把问题处理完毕了,调用本函数的调用者一般是可以直接return
//        返回>0,则是表示实际收到的字节数
ssize_t CSocekt::recvproc(lpngx_connection_t c,char *buff,ssize_t buflen)  //ssize_t是有符号整型,在32位机器上等同与int,在64位机器上等同与long int,size_t就是无符号型的ssize_t
{
    ssize_t n;
    
    n = recv(c->fd, buff, buflen, 0); //recv()系统函数, 最后一个参数flag,一般为0;     
    if(n == 0)
    {
        //客户端关闭【应该是正常完成了4次挥手】,我这边就直接回收连接连接,关闭socket即可 
        //ngx_log_stderr(0,"连接被客户端正常关闭[4路挥手关闭]!");
        ngx_close_connection(c);
        return -1;
    }
    //客户端没断,走这里 
    if(n < 0) //这被认为有错误发生
    {
        //EAGAIN和EWOULDBLOCK[【这个应该常用在hp上】应该是一样的值,表示没收到数据,一般来讲,在ET模式下会出现这个错误,因为ET模式下是不停的recv肯定有一个时刻收到这个errno,但LT模式下一般是来事件才收,所以不该出现这个返回值
        if(errno == EAGAIN || errno == EWOULDBLOCK)
        {
            //我认为LT模式不该出现这个errno,而且这个其实也不是错误,所以不当做错误处理
            ngx_log_stderr(errno,"CSocekt::recvproc()中errno == EAGAIN || errno == EWOULDBLOCK成立,出乎我意料!");//epoll为LT模式不应该出现这个返回值,所以直接打印出来瞧瞧
            return -1; //不当做错误处理,只是简单返回
        }
        //EINTR错误的产生:当阻塞于某个慢系统调用的一个进程捕获某个信号且相应信号处理函数返回时,该系统调用可能返回一个EINTR错误。
        //例如:在socket服务器端,设置了信号捕获机制,有子进程,当在父进程阻塞于慢系统调用时由父进程捕获到了一个有效信号时,内核会致使accept返回一个EINTR错误(被中断的系统调用)。
        if(errno == EINTR)  //这个不算错误,是我参考官方nginx,官方nginx这个就不算错误;
        {
            //我认为LT模式不该出现这个errno,而且这个其实也不是错误,所以不当做错误处理
            ngx_log_stderr(errno,"CSocekt::recvproc()中errno == EINTR成立,出乎我意料!");//epoll为LT模式不应该出现这个返回值,所以直接打印出来瞧瞧
            return -1; //不当做错误处理,只是简单返回
        }

        //所有从这里走下来的错误,都认为异常:意味着我们要关闭客户端套接字要回收连接池中连接;

        //errno参考:http://dhfapiran1.360drm.com        
        if(errno == ECONNRESET)  //#define ECONNRESET 104 /* Connection reset by peer */
        {
            //如果客户端没有正常关闭socket连接,却关闭了整个运行程序【真是够粗暴无理的,应该是直接给服务器发送rst包而不是4次挥手包完成连接断开】,那么会产生这个错误            
            //10054(WSAECONNRESET)--远程程序正在连接的时候关闭会产生这个错误--远程主机强迫关闭了一个现有的连接
            //算常规错误吧【普通信息型】,日志都不用打印,没啥意思,太普通的错误
            //do nothing

            //....一些大家遇到的很普通的错误信息,也可以往这里增加各种,代码要慢慢完善,一步到位,不可能,很多服务器程序经过很多年的完善才比较圆满;
        }
        else
        {
            //能走到这里的,都表示错误,我打印一下日志,希望知道一下是啥错误,我准备打印到屏幕上
            ngx_log_stderr(errno,"CSocekt::recvproc()中发生错误,我打印出来看看是啥错误!");  //正式运营时可以考虑这些日志打印去掉
        } 
        
        //ngx_log_stderr(0,"连接被客户端 非 正常关闭!");

        //这种真正的错误就要,直接关闭套接字,释放连接池中连接了
        ngx_close_connection(c);
        return -1;
    }

    //能走到这里的,就认为收到了有效数据
    return n; //返回收到的字节数
}


//包头收完整后的处理,我们称为包处理阶段1【p1】:写成函数,方便复用
void CSocekt::ngx_wait_request_handler_proc_p1(lpngx_connection_t c)
{
    CMemory *p_memory = CMemory::GetInstance();		

    LPCOMM_PKG_HEADER pPkgHeader;
    pPkgHeader = (LPCOMM_PKG_HEADER)c->dataHeadInfo; //正好收到包头时,包头信息肯定是在dataHeadInfo里;

    unsigned short e_pkgLen; 
    e_pkgLen = ntohs(pPkgHeader->pkgLen);  //注意这里网络序转本机序,所有传输到网络上的2字节数据,都要用htons()转成网络序,所有从网络上收到的2字节数据,都要用ntohs()转成本机序
                                                //ntohs/htons的目的就是保证不同操作系统数据之间收发的正确性,【不管客户端/服务器是什么操作系统,发送的数字是多少,收到的就是多少】
                                                //不明白的同学,直接百度搜索"网络字节序" "主机字节序" "c++ 大端" "c++ 小端"
    //恶意包或者错误包的判断
    if(e_pkgLen < m_iLenPkgHeader) 
    {
        //伪造包/或者包错误,否则整个包长怎么可能比包头还小?(整个包长是包头+包体,就算包体为0字节,那么至少e_pkgLen == m_iLenPkgHeader)
        //报文总长度 < 包头长度,认定非法用户,废包
        //状态和接收位置都复原,这些值都有必要,因为有可能在其他状态比如_PKG_HD_RECVING状态调用这个函数;
        c->curStat = _PKG_HD_INIT;      
        c->precvbuf = c->dataHeadInfo;
        c->irecvlen = m_iLenPkgHeader;
    }
    else if(e_pkgLen > (_PKG_MAX_LENGTH-1000))   //客户端发来包居然说包长度 > 29000?肯定是恶意包
    {
        //恶意包,太大,认定非法用户,废包【包头中说这个包总长度这么大,这不行】
        //状态和接收位置都复原,这些值都有必要,因为有可能在其他状态比如_PKG_HD_RECVING状态调用这个函数;
        c->curStat = _PKG_HD_INIT;
        c->precvbuf = c->dataHeadInfo;
        c->irecvlen = m_iLenPkgHeader;
    }
    else
    {
        //合法的包头,继续处理
        //我现在要分配内存开始收包体,因为包体长度并不是固定的,所以内存肯定要new出来;
        char *pTmpBuffer  = (char *)p_memory->AllocMemory(m_iLenMsgHeader + e_pkgLen,false); //分配内存【长度是 消息头长度  + 包头长度 + 包体长度】,最后参数先给false,表示内存不需要memset;
        c->ifnewrecvMem   = true;        //标记我们new了内存,将来在ngx_free_connection()要回收的
        c->pnewMemPointer = pTmpBuffer;  //内存开始指针

        //a)先填写消息头内容
        LPSTRUC_MSG_HEADER ptmpMsgHeader = (LPSTRUC_MSG_HEADER)pTmpBuffer;
        ptmpMsgHeader->pConn = c;
        ptmpMsgHeader->iCurrsequence = c->iCurrsequence; //收到包时的连接池中连接序号记录到消息头里来,以备将来用;
        //b)再填写包头内容
        pTmpBuffer += m_iLenMsgHeader;                 //往后跳,跳过消息头,指向包头
        memcpy(pTmpBuffer,pPkgHeader,m_iLenPkgHeader); //直接把收到的包头拷贝进来
        if(e_pkgLen == m_iLenPkgHeader)
        {
            //该报文只有包头无包体【我们允许一个包只有包头,没有包体】
            //这相当于收完整了,则直接入消息队列待后续业务逻辑线程去处理吧
            ngx_wait_request_handler_proc_plast(c);
        } 
        else
        {
            //开始收包体,注意我的写法
            c->curStat = _PKG_BD_INIT;                   //当前状态发生改变,包头刚好收完,准备接收包体	    
            c->precvbuf = pTmpBuffer + m_iLenPkgHeader;  //pTmpBuffer指向包头,这里 + m_iLenPkgHeader后指向包体 weizhi
            c->irecvlen = e_pkgLen - m_iLenPkgHeader;    //e_pkgLen是整个包【包头+包体】大小,-m_iLenPkgHeader【包头】  = 包体
        }                       
    }  //end if(e_pkgLen < m_iLenPkgHeader) 

    return;
}

//收到一个完整包后的处理【plast表示最后阶段】,放到一个函数中,方便调用
void CSocekt::ngx_wait_request_handler_proc_plast(lpngx_connection_t c)
{
    //把这段内存放到消息队列中来;
    inMsgRecvQueue(c->pnewMemPointer);
    //......这里可能考虑触发业务逻辑,怎么触发业务逻辑,这个代码以后再考虑扩充。。。。。。
    
    c->ifnewrecvMem    = false;            //内存不再需要释放,因为你收完整了包,这个包被上边调用inMsgRecvQueue()移入消息队列,那么释放内存就属于业务逻辑去干,不需要回收连接到连接池中干了
    c->pnewMemPointer  = NULL;
    c->curStat         = _PKG_HD_INIT;     //收包状态机的状态恢复为原始态,为收下一个包做准备                    
    c->precvbuf        = c->dataHeadInfo;  //设置好收包的位置
    c->irecvlen        = m_iLenPkgHeader;  //设置好要接收数据的大小
    return;
}

//---------------------------------------------------------------
//当收到一个完整包之后,将完整包入消息队列,这个包在服务器端应该是 消息头+包头+包体 格式
void CSocekt::inMsgRecvQueue(char *buf) //buf这段内存 : 消息头 + 包头 + 包体
{
    m_MsgRecvQueue.push_back(buf);	

    //....其他功能待扩充,这里要记住一点,这里的内存都是要释放的,否则。。。。。。。。。。日后增加释放这些内存的代码
    //...而且逻辑处理应该要引入多线程,所以这里要考虑临界问题
    //....

    //临时在这里调用一下该函数,以防止接收消息队列过大
    tmpoutMsgRecvQueue();   //.....临时,后续会取消这行代码

    //为了测试方便,因为本函数意味着收到了一个完整的数据包,所以这里打印一个信息
    ngx_log_stderr(0,"非常好,收到了一个完整的数据包【包头+包体】!");  
}

//临时函数,用于将Msg中消息干掉
void CSocekt::tmpoutMsgRecvQueue()
{
    //日后可能引入outMsgRecvQueue(),这个函数可能需要临界......
    if(m_MsgRecvQueue.empty())  //没有消息直接退出
    {
        return;
    }
    int size = m_MsgRecvQueue.size();
    if(size < 1000) //消息不超过1000条就不处理先
    {
        return; 
    }
    //消息达到1000条
    CMemory *p_memory = CMemory::GetInstance();		
    int cha = size - 500;
    for(int i = 0; i < cha; ++i)
    {
        //一次干掉一堆
        char *sTmpMsgBuf = m_MsgRecvQueue.front();//返回第一个元素但不检查元素存在与否
        m_MsgRecvQueue.pop_front();               //移除第一个元素但不返回	
        p_memory->FreeMemory(sTmpMsgBuf);         //先释放掉把;
    }        
    return;
}

ngx_c_socket_accept.cxx


//和网络 中 接受连接【accept】 有关的函数放这里
/*
王健伟老师 《Linux C++通讯架构实战》
商业级质量的代码,完整的项目,帮你提薪至少10K
*/
#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"

//建立新连接专用函数,当新连接进入时,本函数会被ngx_epoll_process_events()所调用
void CSocekt::ngx_event_accept(lpngx_connection_t oldc)
{
    //因为listen套接字上用的不是ET【边缘触发】,而是LT【水平触发】,意味着客户端连入如果我要不处理,这个函数会被多次调用,所以,我这里这里可以不必多次accept(),可以只执行一次accept()
         //这也可以避免本函数被卡太久,注意,本函数应该尽快返回,以免阻塞程序运行;
    struct sockaddr    mysockaddr;        //远端服务器的socket地址
    socklen_t          socklen;
    int                err;
    int                level;
    int                s;
    static int         use_accept4 = 1;   //我们先认为能够使用accept4()函数
    lpngx_connection_t newc;              //代表连接池中的一个连接【注意这是指针】
    
    //ngx_log_stderr(0,"这是几个\n"); 这里会惊群,也就是说,epoll技术本身有惊群的问题

    socklen = sizeof(mysockaddr);
    do   //用do,跳到while后边去方便
    {     
        if(use_accept4)
        {
            //以为listen套接字是非阻塞的,所以即便已完成连接队列为空,accept4()也不会卡在这里;
            s = accept4(oldc->fd, &mysockaddr, &socklen, SOCK_NONBLOCK); //从内核获取一个用户端连接,最后一个参数SOCK_NONBLOCK表示返回一个非阻塞的socket,节省一次ioctl【设置为非阻塞】调用
        }
        else
        {
            //以为listen套接字是非阻塞的,所以即便已完成连接队列为空,accept()也不会卡在这里;
            s = accept(oldc->fd, &mysockaddr, &socklen);
        }

        //惊群,有时候不一定完全惊动所有4个worker进程,可能只惊动其中2个等等,其中一个成功其余的accept4()都会返回-1;错误 (11: Resource temporarily unavailable【资源暂时不可用】) 
        //所以参考资料:https://blog.csdn.net/russell_tao/article/details/7204260
        //其实,在linux2.6内核上,accept系统调用已经不存在惊群了(至少我在2.6.18内核版本上已经不存在)。大家可以写个简单的程序试下,在父进程中bind,listen,然后fork出子进程,
               //所有的子进程都accept这个监听句柄。这样,当新连接过来时,大家会发现,仅有一个子进程返回新建的连接,其他子进程继续休眠在accept调用上,没有被唤醒。
        //ngx_log_stderr(0,"测试惊群问题,看惊动几个worker进程%d\n",s); 【我的结论是:accept4可以认为基本解决惊群问题,但似乎并没有完全解决,有时候还会惊动其他的worker进程】

        if(s == -1)
        {
            err = errno;

            //对accept、send和recv而言,事件未发生时errno通常被设置成EAGAIN(意为“再来一次”)或者EWOULDBLOCK(意为“期待阻塞”)
            if(err == EAGAIN) //accept()没准备好,这个EAGAIN错误EWOULDBLOCK是一样的
            {
                //除非你用一个循环不断的accept()取走所有的连接,不然一般不会有这个错误【我们这里只取一个连接,也就是accept()一次】
                return ;
            } 
            level = NGX_LOG_ALERT;
            if (err == ECONNABORTED)  //ECONNRESET错误则发生在对方意外关闭套接字后【您的主机中的软件放弃了一个已建立的连接--由于超时或者其它失败而中止接连(用户插拔网线就可能有这个错误出现)】
            {
                //该错误被描述为“software caused connection abort”,即“软件引起的连接中止”。原因在于当服务和客户进程在完成用于 TCP 连接的“三次握手”后,
                    //客户 TCP 却发送了一个 RST (复位)分节,在服务进程看来,就在该连接已由 TCP 排队,等着服务进程调用 accept 的时候 RST 却到达了。
                    //POSIX 规定此时的 errno 值必须 ECONNABORTED。源自 Berkeley 的实现完全在内核中处理中止的连接,服务进程将永远不知道该中止的发生。
                        //服务器进程一般可以忽略该错误,直接再次调用accept。
                level = NGX_LOG_ERR;
            } 
            else if (err == EMFILE || err == ENFILE) //EMFILE:进程的fd已用尽【已达到系统所允许单一进程所能打开的文件/套接字总数】。可参考:https://blog.csdn.net/sdn_prc/article/details/28661661   以及 https://bbs.csdn.net/topics/390592927
                                                        //ulimit -n ,看看文件描述符限制,如果是1024的话,需要改大;  打开的文件句柄数过多 ,把系统的fd软限制和硬限制都抬高.
                                                    //ENFILE这个errno的存在,表明一定存在system-wide的resource limits,而不仅仅有process-specific的resource limits。按照常识,process-specific的resource limits,一定受限于system-wide的resource limits。
            {
                level = NGX_LOG_CRIT;
            }
            ngx_log_error_core(level,errno,"CSocekt::ngx_event_accept()中accept4()失败!");

            if(use_accept4 && err == ENOSYS) //accept4()函数没实现,坑爹?
            {
                use_accept4 = 0;  //标记不使用accept4()函数,改用accept()函数
                continue;         //回去重新用accept()函数搞
            }

            if (err == ECONNABORTED)  //对方关闭套接字
            {
                //这个错误因为可以忽略,所以不用干啥
                //do nothing
            }
            
            if (err == EMFILE || err == ENFILE) 
            {
                //do nothing,这个官方做法是先把读事件从listen socket上移除,然后再弄个定时器,定时器到了则继续执行该函数,但是定时器到了有个标记,会把读事件增加到listen socket上去;
                //我这里目前先不处理吧【因为上边已经写这个日志了】;
            }            
            return;
        }  //end if(s == -1)

        //走到这里的,表示accept4()/accept()成功了        
        //ngx_log_stderr(errno,"accept4成功s=%d",s); //s这里就是 一个句柄了
        newc = ngx_get_connection(s); //这是针对新连入用户的连接,和监听套接字 所对应的连接是两个不同的东西,不要搞混
        if(newc == NULL)
        {
            //连接池中连接不够用,那么就得把这个socekt直接关闭并返回了,因为在ngx_get_connection()中已经写日志了,所以这里不需要写日志了
            if(close(s) == -1)
            {
                ngx_log_error_core(NGX_LOG_ALERT,errno,"CSocekt::ngx_event_accept()中close(%d)失败!",s);                
            }
            return;
        }
        //...........将来这里会判断是否连接超过最大允许连接数,现在,这里可以不处理

        //成功的拿到了连接池中的一个连接
        memcpy(&newc->s_sockaddr,&mysockaddr,socklen);  //拷贝客户端地址到连接对象【要转成字符串ip地址参考函数ngx_sock_ntop()】
        //{
        //    //测试将收到的地址弄成字符串,格式形如"192.168.1.126:40904"或者"192.168.1.126"
        //    u_char ipaddr[100]; memset(ipaddr,0,sizeof(ipaddr));
        //    ngx_sock_ntop(&newc->s_sockaddr,1,ipaddr,sizeof(ipaddr)-10); //宽度给小点
        //    ngx_log_stderr(0,"ip信息为%s\n",ipaddr);
        //}

        if(!use_accept4)
        {
            //如果不是用accept4()取得的socket,那么就要设置为非阻塞【因为用accept4()的已经被accept4()设置为非阻塞了】
            if(setnonblocking(s) == false)
            {
                //设置非阻塞居然失败
                ngx_close_connection(newc); //回收连接池中的连接(千万不能忘记),并关闭socket
                return; //直接返回
            }
        }

        newc->listening = oldc->listening;                    //连接对象 和监听对象关联,方便通过连接对象找监听对象【关联到监听端口】
        newc->w_ready = 1;                                    //标记可以写,新连接写事件肯定是ready的;【从连接池拿出一个连接时这个连接的所有成员都是0】            
        
        newc->rhandler = &CSocekt::ngx_wait_request_handler;  //设置数据来时的读处理函数,其实官方nginx中是ngx_http_wait_request_handler()
        //客户端应该主动发送第一次的数据,这里将读事件加入epoll监控,这样当客户端发送数据来时,会触发ngx_wait_request_handler()被ngx_epoll_process_events()调用
        if(ngx_epoll_add_event(s,                 //socket句柄
                                1,0,              //读,写 ,这里读为1,表示客户端应该主动给我服务器发送消息,我服务器需要首先收到客户端的消息;
                                0,//EPOLLET,          //其他补充标记【EPOLLET(高速模式,边缘触发ET)】
                                                    //后续因为实际项目需要,我们采用LT模式【水平触发模式/低速模式】
                                EPOLL_CTL_ADD,    //事件类型【增加,还有删除/修改】                                    
                                newc              //连接池中的连接
                                ) == -1)
        {
            //增加事件失败,失败日志在ngx_epoll_add_event中写过了,因此这里不多写啥;
            ngx_close_connection(newc);//回收连接池中的连接(千万不能忘记),并关闭socket
            return; //直接返回
        } 

        break;  //一般就是循环一次就跳出去
    } while (1);   

    return;
}


发布了358 篇原创文章 · 获赞 191 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/qq_39885372/article/details/105014450
5-8