本篇是基于IOCP理论基础上实现的IOCP模型。如果之前不是很懂IOCP理论的,请参考此篇博客:​​javascript:void(0)​​。

IOCP主要在于两方面:完成端口,重叠结构,完成键。
在实现之前我们首先要明确一点:一个sock对应一个完成建对应多个重叠IO(send,recv,connect,accept等操作都需要一个重叠结构)。每个操作之后都会投递IOCP完成队列,等待事件通知。

IOCP模型设计需求:
1.对于IOCP服务器而言,每个客户端的连接都是独立的,处理业务逻辑每个连接之间是互不干扰的。所以在最简单的基础上我们可以把连接的上下文绑定到完成键上面,避免对每个连接进行标记的繁琐操作,因为上文我们强调过sock和完成Key是一对一的关系的。
2.对于IOCP服务器而言,可能存在多端口监听,甚至设计到多协议服务器。此时不可能套协议都生成一个IO模型,这样既不方便维护也太繁琐。所以为了区分服务器实例之间的区别,我们需要标记每个服务器,并且对每个服务器之间的数据关系进行标记。因此需要定义一个id。如下:int m_nId。
3.C/S模型最基本的就是ip:port。所以需要这两个字段。
4.数据处理完之后需要回复客户端请求等一些列操作,所以为了方便处理需要保存NETHANDLE
5.当服务器关闭的时候应该通知所有的客户端,告知自己已经关闭。所以需要统计每一个连接。
需要如下字段: LINKBUF *m_plink_buf;//all sock link buf
6.由于IOCP模型为了通用,所以将客户端模型和服务器模型和二为一,为了方便内存释放(服务器和客户端释放Key的时候有点区别),所以需要标类型:char m_ctype;
7.为了尽可能的提高IOCP的性能,IOCP使用了AcceptEX,但是AcceptEX有弊端,只有当接收到第一笔数据才会触发Accept事件。否则就会一直等待下去。例如:我们在AccpetEx的时候指定监听队列为100,在Accept事件里面投递下一个Accept。如果存在恶意占用连接的话就会一直占用,一旦100队列满了之后就无法接收下一个连接了。所以需要优化,m_pcheckaccept_buf由此而生。后面讲解具体实现。
因此就得到这么一个Key。如下

///连接上下文
struct CheckItem;
struct LINKBUF;
#pragma pack(1)
class NETCLASS ILinkContext
{
public:
friend class iocp_tcp;
ILinkContext();
virtual ~ILinkContext();
virtual void on_close() = 0;//on connection close,主动断开不会收到通知
//for svr on client accepted,may onc sock call twice,because made a bad connect check
//if(Link == NULL) szip,sport is not userd,is Just establish a connection ,not transfer data
//if(Link != null) szip ,szport is remote clint info. is establis a connection and has transfer data
virtual void on_accepted(NETHANDLE nethandle,const char *szip,const unsigned short sport) = 0;
virtual void on_recv(const char *pdata,int ndatasize) = 0;//on recv data
virtual void on_connect_ok(NETHANDLE nethandle) = 0;//on client connect ok
virtual void on_connect_err(NETHANDLE nethandle,bool bactive/*是否为主本端动关闭*/) = 0;// on client connect err for example:connect timeout,you can chose reconnect
virtual void on_send_ok(const char *pdata,int ndatasize) = 0;//send data ok
virtual void on_send_err(const char *pdata,int ndatasize) = 0;//sedn data err you can resend a data,you need copy data
virtual ILinkContext* new_context() = 0;// new a connect context :func:new this
operator NETHANDLE() const;

protected:
char m_szip[16];//ip
unsigned short m_sport;//端口
int m_nId;//context的id
private:
NETHANDLE m_hNet;//connect a handle
void *m_stop_check;//stocp check accpet sock status handle
void *m_check_handle;//check accpet sock status handle
CheckItem *m_pcheckaccept_buf;//check sock stats buffer
int m_ncheckaccept_buf;
LINKBUF *m_plink_buf;//all sock link buf
char m_ctype;//0-svr 1-client,mark terminal type
};
#pragma pack()

AcceptEx优化:
1.标记所有的投递队列,然后启动一个线程检测投递队列状态,如果已经连接则立马投递一个Accept,并且把当前Item从队列中移除。(我们不做超时检验操作,只投递Accept,超时之后的操作由外部实现)。
2.因为是多线程的,所以需要加锁,因为用户可能连接上之后发送了数据。此时监听线程和完成队列都会接收到消息。所以需要预防重复投递的问题。最简单的实现方式就是实现一个线程安全的vector然后对vector加锁操作,但是vector在删除的时候可能耗时多一点并且会导致迭代器无效,从而导致检测线程无法安全遍历检测。如果使用map的话删除的时候效率很高,但是检测线程没办法安全遍历。那么怎么解决这个问题呢?所以上述引出了CheckItem,这个Item是线程安全的。还记得上述我们提过一个accept操作对应一个重叠结构,此时我们就可以通过重叠结构中引入CheckItem buffer的index来解决这个问题了。因为投递队列大小是固定的。所以在程序启动建立队列的时候我们就把每个索引应入到重叠结构中。
不管是检测线程还是完成队列的线程当有Accpet触发的时候都会伴随一个重叠结构。此时我们直接投递Accpet并且更新BufItem就行了。这样就可以完美的优化AccpetEx的弊病了。
2.1如果检测线程先检测到连接,则会更新Item,此时完成端口的线程收到Accept事件的时候对应Index的CheckItem的值已经和当前的SOCK不相等,说明检测线程优先并且已经投递了Accept。
2.2如果完成队列先检测到Accpet事件,则完成队列的CheckItem和当前sock匹配。说明没有投递Accept,完成队列需要投递Accpet并且更新Item。整个过程原子操作。