基于VC开发epoll/linux 程序指南

1       概述.... 3

2       背景.... 3

3       总体思路.... 3

4       功能特点.... 4

5       开发工具和操作系统要求.... 4

5.1       Windows/VC.. 4

5.2       linux/g++. 4

6       开发人员要求.... 5

7       Linux针对epoll移植.... 5

8       VC工程源码兼容性调整步骤和方案.... 5

8.1       准备移植文件... 5

8.2       宏定义... 5

8.1       调用初始化函数... 5

8.1       调用socket-recv()函数调整... 6

8.2       调用socket-send()函数调整... 7

8.3       调用socket-closesocket()函数调整... 7

9       本方案的限制.... 8

10         成功案例.... 8

11    epoll移植头文件.... 9

12         实用案例-端口映射源码.... 12

 

  1.  概述

本文是讲解日如何采用VC进行epoll编程,这是基于VC和windows API开发跨平台程序的重要组成部分,阅读本文之前需要先阅读《基于VC开发linux服务器程序指南》,文档网址:

https://blog.****.net/freeland008/article/details/107214767

 

  1.  背景

目前在linux下进行socket编程,一般使用select多路复用和epoll模式,

Windows和linux基于select模式开发,调用的函数类似。 windows不支持epoll,而只支持完成端口。要实现海量TCP并发访问必须采用epoll模式,为了实现在VC上进行epoll编程,进而移植到linux/g++,作者提供了相应的解决方案,具体参考下面章节。

 

  1.  总体思路

1、作者基于VC把select相关函数封装成epoll函数,具体参考作者提供的源码

文件。

 

2、开发人员就可以使用VC调用作者提供的epoll函数进行开发,相关epoll函

数的接口参数和返回值、功能兼容linux下的epoll函数。

 

3、因为不能完全兼容linux epoll,需要在VC的代码中加入部分兼容代码,采用

预定义指令,这些兼容代码只对VC有效,当移植到linux上是,这些兼容代

码不需要编译和执行。

 

4、VC工程在window上编译、调试、测试过后,就可以移植到linux/g++,epoll

部分代码可做到100%兼容。

 

说明:

1、作者会提供2个移植源码文件:EpollMng.cpp、EpollMng.h。

2、这2个文件只用于VC工程,移植到linux的源码是不需要这2个文件的。

 

  1.  功能特点

作者提供的epoll封装函数有以下特点

 

  1. 支持epoll的函数有epoll_create()、epoll_ctl()、epoll_wait()
  2. 支持epoll定义的LT和ET模式
  3. 支持listen()、accept()、connect()等socket函数。

4、 作者封装的epoll内部的数据结构采用了数组和avl树数据结构,性能优异。

    开发人员完全可以在windows下用epoll取代select进行开发。

5、 支持多次调用epoll_create(),生成多个epoll句柄,每个poller支持绑定多

个socket。

  1.  开发工具和操作系统要求
  2.  Windows/VC

 

1、建议采用/VC 2008及以上版本,同时安装Visual Assist X开发助手。

2、操作系统

   1)桌面系统采用64位Win7及以上版本。

   2)服务器系统采用Windows 2008x64及以上版本

3、其他

    如果只是开发32位程序,建议安装VC6,同时安装Visual Assist X开发助

    手。

 

  1.  linux/g++

作者开发选用RedHat和CentOS 的linux,如果开发人员希望在其他厂商的linux上开发,需要进行进一步的测试

 

  1.  开发人员要求
  2. 精通Socket编程,尤其是异步编程。
  3. 精通epoll的工作机制,具体可参考epoll的相关文档。

3、 熟悉VC++编程。

 

  1.  Linux针对epoll移植

1、只需去掉VC源码下的宏定义即可。

   例如:#define  PORT_OS_WIN32

 

  1.  VC工程源码兼容性调整步骤和方案

 

  1.  准备移植文件

作者提供了epoll封装源码文件为: EpollMng.cpp、EpollMng.h,开发人员把这2个文件加入到自己VC工程。

 

  1.  宏定义

需要在源码中新增一个宏定义:

 

#define    PORT_OS_WIN32

 

这个宏定义是限制某些epoll的兼容函数只运行在Windows的VC工程,在linux/g++下无效。

 

    1.  调用初始化函数

在调用任何epoll函数前,必须调用且只调用一次epoll的初始化函数,一般代码放置在程序初始化阶段,调用代码如下。

 

#ifdef PORT_OS_WIN32

       epoll_init();

#endif

 

这个函数只对VC工程有效,linux忽略这个函数。

 

    1.  调用socket-recv()函数调整

因为采用epoll编程,socket必须是异步,在调用socket的recv()函数接收数据时,需要做以下调整。示例代码如下:

 

       while(true)

         {                                            

                   char pBuf[1024*8];

                   int nRet;

                   nRet = recv(hSocket, pBuf, 8*1024, 0);

                   if(nRet == 0)

                   {

                            return -1;

                   }

                   else if(nRet == SOCKET_ERROR)

                   {

                            nRet = WSAGetLastError();

                            if(nRet == WSAEWOULDBLOCK)

                            {

#ifdef PORT_OS_WIN32

                                     epoll_setrecvnull(nPollerID, hSocket);

#endif

                                     return 0;

                            }      

                            return -1;

                   }      

         }      

 

其中红色代码部分是必须要加的,也就是说当recv()接收完socket缓冲区中所有的数据时,recv()函数时会返回阻塞(无数据)的错误码,这时必须执行epoll_setrecvnull函数,用来通知epoll封装类对象,已经无数据可读。红色代码只在VC工程有效,在linux/g++中不被编译。

 

以上加入的红色代码部分纯粹是为了让VC支持epoll接口的编程机制。

 

    1.  调用socket-send()函数调整

 

因为采用epoll编程,socket必须是异步,在调用socket的send()函数发送 数据时,需要做以下调整。示例代码如下:

 

       while(true)

         {                                            

                   char pBuf[1024*8];

                   int nRet;

                   nRet = send(hSocket, pBuf, 8*1024, MSG_NOSIGNAL);

                   if(nRet == 0)

                   {

                            return -1;

                   }

                   else if(nRet == SOCKET_ERROR)

                   {

                            nRet = WSAGetLastError();

                            if(nRet == WSAEWOULDBLOCK)

                            {

#ifdef PORT_OS_WIN32

                                     epoll_setsendnull (nPollerID, hSocket);

#endif

                                     return 0;

                            }      

                            return -1;

                   }      

         }      

 

其中红色代码部分是必须要加的,也就是说当socket缓冲区满,再调用send()时,send()函数时会返回阻塞(缓冲满)的错误码,这时必须执行epoll_ setsendnull函数,用来通知epoll封装类对象,socket不可写。红色代码只在VC工程有效,在linux/g++中不被编译。

 

以上加入的红色代码部分纯粹是为了让VC支持epoll接口的编程机制。

 

 

    1. 调用socket-closesocket()函数调整

当在VC工程中调用closesocket函数关闭socket(linux调用close())时,如果该socket之前绑定到某个epoll句柄,需要对closesocket()函数附近的相关代码做调整。示例代码如下:

 

#ifdef PORT_OS_WIN32

                     epoll_event pEvent;                                                       

                     epoll_ctl(nPollerID, EPOLL_CTL_DEL, hSocket, &pEvent);

#endif

                     closesocket(hSocket);

 

其中红色代码部分是必须要加的,当在VC工程中调用closesocket()关闭socket时,需要把socket从epoll句柄中解除绑定。linux不需要执行此函数,因为linux调用close()关闭socket时会自动解除绑定。

 

以上加入的红色代码部分纯粹是为了让VC支持epoll接口的编程机制。

 

 

  1.  本方案的限制

由于作者是采用select函数封装成的epoll接口,在执行性能上稍低于原始的

select模式。如果进行并发测试,TCP连接超过上千个,性能会下降,主要是受限于select本身的限制。如果要进行大量TCP并发连接测试,还是要在linux上进行测试。

 

  1.  成功案例

作者开发的所有服务器程序的网络编程都是采用epoll模式,先是在Windows的VC上进行开发、调试、测试,然后再把工程源码复制到linux上进行编译、运行、测试。

 

1、内存数据库系统

 

2、WebServer产品

 

  1. 短信网关

 

4、MQTT服务器系统

 

5、其他软件。

 

作者提供基于此方案的工程源码:TCP端口映射程序和WebServer程序。

 

  1.  epoll移植头文件

以下文件是在VC源码工程中实现的epoll封装类的头文件(EpollMng.h),部分代码是从linux的头文件复制过来。EpollMng.cpp文件请从相关网址下载,或向作者索取。

 

#ifndef EPOLLMNG_H

#define EPOLLMNG_H

 

#include <winsock2.h>

#include <windows.h>

 

/* Flags to be passed to epoll_create2.  */

enum

{

         EPOLL_CLOEXEC = 02000000,

#define EPOLL_CLOEXEC EPOLL_CLOEXEC

         EPOLL_NONBLOCK = 04000

#define EPOLL_NONBLOCK EPOLL_NONBLOCK

};

 

 

enum EPOLL_EVENTS

{

         EPOLLIN = 0x001,

#define EPOLLIN EPOLLIN

         EPOLLPRI = 0x002,

#define EPOLLPRI EPOLLPRI

         EPOLLOUT = 0x004,

#define EPOLLOUT EPOLLOUT

         EPOLLRDNORM = 0x040,

#define EPOLLRDNORM EPOLLRDNORM

         EPOLLRDBAND = 0x080,

#define EPOLLRDBAND EPOLLRDBAND

         EPOLLWRNORM = 0x100,

#define EPOLLWRNORM EPOLLWRNORM

         EPOLLWRBAND = 0x200,

#define EPOLLWRBAND EPOLLWRBAND

         EPOLLMSG = 0x400,

#define EPOLLMSG EPOLLMSG

         EPOLLERR = 0x008,

#define EPOLLERR EPOLLERR

         EPOLLHUP = 0x010,

#define EPOLLHUP EPOLLHUP

         EPOLLRDHUP = 0x2000,

#define EPOLLRDHUP EPOLLRDHUP

         EPOLLONESHOT = (1 << 30),

#define EPOLLONESHOT EPOLLONESHOT

         EPOLLET = (1 << 31)

#define EPOLLET EPOLLET

};

 

/* Valid opcodes ( "op" parameter ) to issue to epoll_ctl().  */

#define EPOLL_CTL_ADD 1        /* Add a file decriptor to the interface.  */

#define EPOLL_CTL_DEL 2         /* Remove a file decriptor from the interface.  */

#define EPOLL_CTL_MOD 3       /* Change file decriptor epoll_event structure.  */

 

typedef union epoll_data

{

         void *ptr;

         int fd;

         unsigned int/*uint32_t*/ u32;

         unsigned __int64/*uint64_t*/ u64;

} epoll_data_t;

 

struct epoll_event

{

         unsigned int/*uint32_t*/ events;      /* Epoll events */

         epoll_data_t data; /* User data variable */

};

//////////////////////////////////////////////////////////////////////////

struct epoll_event_ex

{

         epoll_event event;        

         bool bCanSend;

         bool bCanRecv;           

};

//////////////////////////////////////////////////////////////////////////

class CEpoller

{

public:

         int m_nPeerHashSize;

         epoll_event_ex** m_pPeerArray;  

         HANDLE m_hPollerTree;

         HANDLE m_hEventTree;

         fd_set m_pReadSet;

         fd_set m_pWriteSet;

         fd_set m_pErrorSet;

public:

         epoll_event* m_pEventArray;

         int m_nEventIndex;

public:

         CEpoller();

         ~CEpoller();

public:

         int Init(int nMaxSize);

         int Destroy();

         int AddEvent(SOCKET hSocket, epoll_event* ev);

         int RemoveEvent(SOCKET hSocket);

         int ModifyEvent(SOCKET hSocket, epoll_event* ev);

         epoll_event_ex* GetEvent(SOCKET hSocket);

         int WaitEvent(epoll_event* pEventArray, int nMaxEvents);

         int FillSocket(int nTimeOut);

         int SetSendNull(SOCKET hSocket);

         int SetRecvNull(SOCKET hSocket);       

};

 

class CEpollMng

{

public:

         CRITICAL_SECTION m_cri;

         int m_nUNID;    

         int m_nMaxPoller;

         CEpoller** m_pPollerArray;

public:

         CEpollMng();

         ~CEpollMng();

public:

         int Init();

         int Destroy();

public:

         int CreatePoller(int nMaxSize);      

         int ClosePoller(int nPollerID);                 

         CEpoller* GetPoller(int nPollerID);

 

         int AddEvent(int nPollerID, SOCKET hSocket, epoll_event* ev);

         int RemoveEvent(int nPollerID, SOCKET hSocket);

         int ModifyEvent(int nPollerID, SOCKET hSocket, epoll_event* ev);

         int WaitEvent(int nPollerID, epoll_event* pEventArray, int nMaxEvents, int nTimeOut);

 

         int SetSendNull(int nPollerID, SOCKET hSocket);

         int SetRecvNull(int nPollerID, SOCKET hSocket);

};

//////////////////////////////////////////////////////////////////////////

int epoll_wininit();

int epoll_setsendnull(int nPollerID, SOCKET hSocket);

int epoll_setrecvnull(int nPollerID, SOCKET hSocket);

//////////////////////////////////////////////////////////////////////////

int epoll_create(int nMaxSize);

int epoll_ctl(int nPollerID, int nActionID, SOCKET hSocket, epoll_event* pEvent);

int epoll_wait(int nPollerID, epoll_event* pEventArray, int nMaxEvents, int nTimeOut);

 

#endif

 

 

  1.  实用案例-端口映射源码

1、下图为端口映射的源码工程,可100%迁移到linux/g++编译运行。

 

基于VC开发epoll/linux 程序指南
端口映射源码工程-VC2008

2、作者可提供windowsx64和CentOSx64源码工程。