简介:基于windows平台下的,异步非阻塞,实现百万级高并发的io模型。
一.优缺点
优点:占用cpu使用率特别低,非阻塞,实现高并发
缺点:终身绑定制,只能绑定一个iocp,直到死亡
二.实现原理
通过使用线性池和共享文件实现。
1.线性池使得通信可以同时运行
2.共享文件就是将原来需要拷贝的数据的地址和缓冲区的地址映射为相同一块,解决了数据在内存中拷贝阻塞的问题
三.实现过程简述
创建监听套接字,交给完成端口管理,在创建连接套接字,与监听套接字关联,每一个连接套接字会绑定一个事件(这是一个结构体,事件为第一个元素),如果有人连接,触发事件,会由完成端口返回事件,通过事件可以找到对应套接字结构体,从而获取里面的内容
把winter套接字,缓存区,还有标志,以及关联的事件结构体,定义一个结构体mysocket,关联的事件放在结构体中第一个
我们先创建监听套接字,把他交给iocp管理,在创建一堆空闲winter套接字,这些套接字与监听套接字还有事件连接,观察iocp队列状态,当有人连接时,iocp会从返回一个winter套接字关联得事件的首地址。
事件首地址是mysocket结构体的首地址,我们知道结构体大小,就可以得到结构体内部包含的信息,包括对应的winter套接字,缓存区,还有标志。
我们接收到winter套接字后,这时我们投递接收消息请求,把winter的缓冲区与内存映射,把标志由连接改为可读,并把与winter对应的事件变为未触发,再把winter套接字交给iocp管理。
当接收到消息的时候,会返回接收到消息的winter套接字对应事件的结构体的首地址,我们还是可以通过这个地址得到结构体的信息,包括缓冲区中的接收到的信息
接收信息后需要再次投递消息请求。
完整代码
#include "ctcpnet.h"
int CTCPNet::m_EventNum=0;
CTCPNet::CTCPNet()
{
m_sock=NULL;
m_FlagQuit=NULL;
//m_EventNum=0;
//ZeroMemory(m_aryEvent,sizeof(m_aryEvent));
//ZeroMemory(m_arysocket,sizeof(m_arysocket));
}
//服务器想接收多个客户端通信
//1.同步阻塞+多线程
//2.单线程处理客户端:select 客户端
bool CTCPNet::InitNetWork()//初始化网络
{
//1.
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD(2, 2);
err = WSAStartup(wVersionRequested, &wsaData);
if (err != 0) {
return false;
}
if (LOBYTE(wsaData.wVersion) != 2 ||
HIBYTE(wsaData.wVersion) != 2)
{
UnInitNetWork();
return false;
}
//2.创建监听套接字
m_sock = socket(AF_INET,SOCK_STREAM,0);
if(m_sock == INVALID_SOCKET)
{
UnInitNetWork();
return false;
}
//3.绑定位置
sockaddr_in addrserver;
addrserver.sin_family = AF_INET;
addrserver.sin_port = htons(8899);
addrserver.sin_addr.S_un.S_addr = 0;
if(SOCKET_ERROR == bind(m_sock,( const sockaddr *)&addrserver,sizeof(addrserver)))
{
UnInitNetWork();
return 1;
}
//4.监听连接
if( SOCKET_ERROR == listen(m_sock,10))
{
UnInitNetWork();
return false;
}
//1.创建IOCP
m_hIOCP=CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,NULL,NULL);
if(m_hIOCP==NULL)
{
UnInitNetWork();
return false;
}
//2.将socketlisten监听套接字交给完成端口管理
CreateIoCompletionPort((HANDLE)m_sock,//交给完成端口管理的套接字
m_hIOCP,//完成端口句柄
(ULONG_PTR)m_sock,//完成键
0); //线程的个数 默认和系统处理器一样的
//3.创建一堆waiter,交给socketwaiter 准备好了
//获取CPU数量,假设读写时间一样,一般设置为cpu数量的二倍,效率最高
SYSTEM_INFO si;
GetSystemInfo(&si);
for(unsigned long i=0;i<si.dwNumberOfProcessors*2;i++)
{
PostAccept();
}
//4.创建线程池
for(unsigned long i=0;i<si.dwNumberOfProcessors*2;i++)
{
m_hThread=(HANDLE)_beginthreadex(0,0,&ThreadProc,this,0,0);
if(m_hThread)
m_lstThread.push_back(m_hThread);
}
return true;
}
bool CTCPNet::PostAccept()
{
//todo
Mysocket *pSocket=new Mysocket;
pSocket->m_nType=NT_ACCEPT;
pSocket->m_olp.hEvent=WSACreateEvent();
pSocket->m_sock=socket(AF_INET,SOCK_STREAM,0);
ZeroMemory(pSocket->m_szbuf,sizeof (pSocket->m_szbuf));
DWORD dwBytesReceived;
if(!AcceptEx(m_sock,//监听socket
pSocket->m_sock,//waiter
pSocket->m_szbuf,//缓冲区
0,//连接成功,立即返回
sizeof (sockaddr)+16,//本地地址长度
sizeof (sockaddr)+16,//远程地址长度
&dwBytesReceived,//接受传输字节数
&pSocket->m_olp//事件
))
{
if(WSAGetLastError()!=ERROR_IO_PENDING)
{
closesocket(pSocket->m_sock);
WSACloseEvent(pSocket->m_olp.hEvent);
delete pSocket;
return false;
}
}
m_lstSocket.push_back(pSocket);
return true;
}
unsigned _stdcall CTCPNet::ThreadProc(void *lpvoid)
{
CTCPNet *pthis=(CTCPNet*)lpvoid;
DWORD dwNumberOfBytesTransferred;
SOCKET sock;
Mysocket *pSocket;
BOOL bFlag;
//提高效率 采用线程池的后进先出原则 线程1干完活又来一个活还让1干
while(pthis->m_FlagQuit)
{
//观察IOCP状态
bFlag=GetQueuedCompletionStatus(pthis->m_hIOCP,
&dwNumberOfBytesTransferred,
(PULONG_PTR)&sock,
(LPOVERLAPPED*)&pSocket,
INFINITE);
if(!bFlag)
{
//psocket 包含的就是下线的客户端对应的waiter
auto ite=pthis->m_lstSocket.begin();
while(ite!=pthis->m_lstSocket.end())
if((*ite)==pSocket)
{
WSACloseEvent((*ite)->m_olp.hEvent);
closesocket((*ite)->m_sock);
delete *ite;
*ite=NULL;
pthis->m_lstSocket.erase(ite);
break;
}
ite++;
}
//校验参数
if(!sock||!pSocket)
continue;
switch(pSocket->m_nType)
{
case NT_ACCEPT:
//已经连接上了
//1.投递新的空闲waiter
pthis->PostAccept();
//2.投递接受数据请求
pthis->PostRecv(pSocket);
//3.将socket 交给iocp管理
CreateIoCompletionPort((HANDLE)pSocket->m_sock,//交给完成端口管理
pthis->m_hIOCP, //完成端口句柄
(ULONG_PTR)pSocket->m_sock,//完成键
0);//线程的个数 默认和系统处理器一样的个数
break;
case NT_READ:
//数据已经接收到了
pSocket->m_szbuf;//已经接收到的数据
//处理数据
pthis->PostRecv(pSocket);
break;
default:
break;
}
}
return 0;
}
bool CTCPNet::PostRecv(Mysocket* pSocket)
{
pSocket->m_nType=NT_READ;
WSABUF wb;
wb.buf=pSocket->m_szbuf;
wb.len=sizeof(pSocket->m_szbuf);
DWORD dwNumberOfBytesRecvd;
DWORD dwFlags=FALSE;
if(WSARecv(pSocket->m_sock,&wb,1,&dwNumberOfBytesRecvd,&dwFlags,&pSocket->m_olp,0))
{
if(WSAGetLastError()!=WSA_IO_PENDING)
return false;
}
return true;
}
unsigned _stdcall CTCPNet::ThreadRecv(void *lpvoid)
{
CTCPNet *pthis=(CTCPNet*)lpvoid;
pthis->RecvData();
return 0;
}
void CTCPNet::UnInitNetWork()
{
m_FlagQuit=false;
std::list<HANDLE>::iterator ite=m_lstThread.begin();
while(ite!=m_lstThread.end())
{
if(*ite)
{
if(WAIT_TIMEOUT==WaitForSingleObject(m_hThread,100))
TerminateThread(*ite,-1);
CloseHandle(*ite);
*ite=NULL;
}
ite++;
}
m_lstThread.clear();
std::list<Mysocket*>::iterator iteSocket=m_lstSocket.begin();
while(iteSocket!=m_lstSocket.end())
{
closesocket((*iteSocket)->m_sock);
WSACloseEvent((*iteSocket)->m_olp.hEvent);
delete *iteSocket;
*iteSocket=NULL;
iteSocket++;
}
m_lstSocket.clear();
if(m_sock)
{
closesocket(m_sock);
m_sock=0;
}
WSACleanup();
}
bool CTCPNet::SendDate(SOCKET sock,char *szbuf,int nlen)
{
if(sock==INVALID_SOCKET||!szbuf||nlen<=0)
return false;
//发送大小
if(send(sock,(char*)&nlen,sizeof(int),0)<=0)
return false;
//发送内容
if(send(sock,szbuf,nlen,0)<=0)
return false;
return true;
}
四.准备工作
1.将监听套接字交给iocp管理
创建完成端口
m_hiocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, NULL, NULL);
把监听套接字交给完成端口
CreateIoCompletionPort((HANDLE)m_sock,//套接字,文件句柄 m_hiocp, //已经存在的完成端口的句柄 (ULONG_PTR)&m_sock,//完成建,是一个附加参数,可以用来校验 0);//操作系统线程最大数量,0为操作系统处理器 //一样个数
2.准备一堆winter套接字(连接用的套接字,后面用winter代替),告诉监听套接字准备好了
这里会为winter匹配一块缓冲区,通信用的事件,标志(第一次连接,还是接收消息)
一般建立cpu核心数2倍的线程数cpu核心数的线程最快
获取核心数,并创建2倍线程数
SYSTEM_INFO si; GetSystemInfo(&si);//获取系统信息 for (int i = 0; i < (int)si.dwNumberOfProcessors * 2; i++) { //dwNumberOfProcessors为cpu核心数,一般线程数为cpu个数二倍效率最高 Postaccept();//创建winter的函数,在其中创建winter,与事件绑定,告诉 //监听套接字准备好了 }
包含事件和winter,缓冲区,标志的结构体
#define MAXSHULIANG 4096//windows一页的大小 #define NT_ACCEPT 0//宏,0为接收 #define NT_READ 1//宏,1为可读 //enum nettype{NT_ACCEPT= 0,NT_READ=1};也可使用枚举自定义类型 struct Mysocket { OVERLAPPED my_olp;//这个是一个包含事件的结构体,必须写在第一个, //这个结构体的地址,是mysocket的首地址,返回值为这个结构体 //可以带回其他数据,后面会解释 SOCKET sockwitnter;//套接字,连接 char buf[MAXSHULIANG];//一页字节大小的char数组,缓冲区 int ntype;//标志是第一次连接,还是接收消息 };
创建winter的函数
bool ctcnet::Postaccept()//创建套接字,并告诉监听套接字,准备好了 { Mysocket* psock = new Mysocket;//创建一块空间 psock->ntype = NT_ACCEPT;//初始标记为连接 psock->my_olp.hEvent = WSACreateEvent();//创建事件 psock->sockwitnter = socket(AF_INET, SOCK_STREAM, 0);//创建winter套接字 ZeroMemory(psock->buf, sizeof(psock->buf));//清空数组,缓冲区 DWORD lpdwBytesReceived;//用来接收实际收到的字节数,在这里没用上 if (!AcceptEx(m_sock,//监听套接字 psock->sockwitnter,//接收消息的套接字 psock->buf,//缓冲区 0,//0是连接就返回,其他是连接并且接收到信息后才返回 sizeof(sockaddr) + 16,//自己地址大小,为最大地址加16 sizeof(sockaddr) + 16,//客户端地址大小,为最大地址加16 &lpdwBytesReceived,//实际接受到的字节数 &psock->my_olp// 事件,用来通知的事件 ))//成功返回值是1,为0是失败 { if (WSAGetLastError() != ERROR_IO_PENDING) { //这种错误是成功了,但是还得一会,不算失败 closesocket(psock->sockwitnter);//失败关闭套接字 WSACloseEvent(psock->my_olp.hEvent);//关闭事件 delete psock;//释放空间 return false;//返回失败 } } my_lstsocket.push_back(psock);//成功后把它加入到链表里 return true; }
AcceptEx()作用是告诉监听套接字准备好了,同时与事件连接,当有人连接是iocp会返回winter对应的事件的地址。
3.创建线程池
for (int i = 0; i < (int)si.dwNumberOfProcessors * 2; i++)
{ //创建cpu核心数二倍的线程数
HANDLE hand = (HANDLE)_beginthreadex(0, 0, &ThreadProc, this, 0, 0);
if (hand)
m_listhandle.push_back(hand);
}
这里创建的线程起到了线程池的作用,详情见后面。
五.实现过程
1.观察iocp状态
SOCKET sock;//接收完成键的套接字
Mysocket* psock;//接收结构体的地址,上面说了返回的是事件的地址,
//这里的事件的地址是mysocket结构体的首地址,
//可以得出结构体的数据
DWORD lpNumberOfBytesTransferred;
bool flag = GetQueuedCompletionStatus(pthis->m_hiocp, //完成端口
&lpNumberOfBytesTransferred,
(PULONG_PTR) & sock, //完成键
(LPOVERLAPPED*)&psock, //返回的是事件的地址,
//这个事件是mysocket中的第一个元素,
//也是个结构体
INFINITE);
if (!flag)//校验,失败重新接收
continue;
if (!sock || !psock)//校验完成键,和返回的结构体
{ //失败代表着失去连接,从套接字链表中删除
auto ite = pthis->my_lstsocket.begin();
while (ite != pthis->my_lstsocket.end())
{
if (*ite = psock)
{
closesocket(psock->sockwitnter);
WSACloseEvent(psock->my_olp.hEvent);
delete* ite;
*ite = NULL;
pthis->my_lstsocket.erase(ite);
break;
}
ite++;
}
continue;
}
这里GetQueuedCompletionStatus()作用是观察iocp队列作用,是一个阻塞函数,当没有连接或者信息到来就会一直阻塞,函数返回来的值是事件结果体的地址,也就是mysocket的首地址。
2.下面用swich区分两种情况
1).如果有人连接我
返回一个准备好的winter套接字,分配线程,winter套接字投递接收消息请求(将winter套接字的缓冲区与内存中接收数据的地址变成同一块地址),然后把winter交给iocp管理。
//创建新的winter
pthis->Postaccept();//用了一个winter,为了保证高并发,在创建一个新的
//投递接收信息请求
pthis->Postrecv(psock);//一次之后,如果要接收消息,需要再次投递信息请求
//将套接字交给iocp管理
CreateIoCompletionPort((HANDLE)psock->sockwitnter,
pthis->m_hiocp, (ULONG_PTR)psock->sockwitnter, 0);
2).有消息到来
可以直接使用缓冲区的数据,然后重新投递接收消息请求,告诉监听套接字要接收数据
cout << psock->buf;//接收的信息在缓冲区,不需要再次拷贝
pthis->Postrecv(psock);//投递消息请求
投递消息请求,我的理解是把缓冲区和接收数据的地址变成同一块,映射内存,然后把套接字与事件绑定在一起。每次接收完数据后或者建立连接都要重新投递消息请求,相当于事件每一次触发后,投递消息请求把事件重新变为未触发,才能再次触发。
这里的缓冲区位置为堆区,因为我们是在堆区申请的mysocket结构体空间。
iocp队列中是某一个套接字被触发了返回的是对应事件的空间首地址,这块空间里有对应winter套接字,我们知道这个结构体的大小,我们可以获得这些信息。
投递消息请求
bool ctcnet ::Postrecv(Mysocket*psock) { psock->ntype = NT_READ;//连接过的winter状态变为可读 WSABUF we;//一个结构体 we.buf = psock->buf; we.len = sizeof(psock->buf); DWORD lpNumberOfBytesRecvd;//实际接收到的个数 DWORD qflog = false; if (WSARecv(psock->sockwitnter,//winter套接字 &we,//一个结构体,缓冲区,和缓冲区长度 1,//结构体个数 &lpNumberOfBytesRecvd,//实际接收到的个数 &qflog,//是否可以修改recv行为,一般不修改 &psock->my_olp,//用事件结构体发送消息 0))//完成历程,这里不管放一个0 {//返回0成功,非零失败 if (WSAGetLastError() != WSA_IO_PENDING)//错误码是WSA_IO_PENDING说明成功了, //但是还要等一会,不算失败 { return false; } } return true; }
六.封装网络
待续...