第八十八讲 综合运用(3)

在说今天的内容之前和大家说一下,由于后来改变了实现方案,所以重新封装了一下相关数据,就在前面两讲里面,大家可以回头去看一下,改动的地方还是有点多的。

今天我们来说一说关于主要的MyNet的实现,首先我们来看看线程函数的实现,C++11里面的线程和普通函数一样,开线程和写普通的成员函数一样,没有必要的限制,好吧,废话不多说,我们直接来看看吧:

//==============线程函数==================


void MyNet::CompletionThread(MyNet* pNet, HANDLE completionport){

pNet->m_mtx.lock();

PER_HANDLE_DATA* perHandle = nullptr;

WSAOVERLAPPED* pOverIo = nullptr;

PER_IO_DATA*
perIo = nullptr;

DWORD

ByteTransfered = 0, Flag = 0;

int

ret;

std::cout.sync_with_stdio();

std::cout <<"

Thread: "

<<std::this_thread::get_id() <<"

Has been start!!!n"

;


pNet->m_mtx.unlock();

while (true){


ret = GetQueuedCompletionStatus(completionport, &

ByteTransfered, (DWORD*)&

perHandle, (LPOVERLAPPED*)&

pOverIo, WSA_INFINITE);

perIo = CONTAINING_RECORD(pOverIo, PER_IO_DATA, _ol);

if (ret == FALSE){

ret = WSAGetOverlappedResult(perIo->_socket, &

perIo->_ol,

&

ByteTransfered, FALSE, &

Flag);

if (ret == FALSE &

&

WSAGetLastError() == WSA_IO_PENDING){

std::cout <<"

Error…..ErrorID:"

;

std::cout <<WSAGetLastError() <<std::endl;

return;

}

}

if (ByteTransfered == 0xFFFFFFFF)

break;


pNet->HandleIo(perHandle, perIo, ByteTransfered);

}

}


//======================================

现在大家也看到了吧,我们这个线程函数和以前我们用API创建的线程的区别了吧,我们可以任意的传递参数,就像上面一样,我们传递一个MyNet的指针,一个HANDLE,这里的HANDLE就是完成端口。记住,我们整个程序里面只需要一个完成端口,完成端口是windows的一个内核对象,所以windows会帮我们管理,所以我们会发现,主线程闲得蛋疼,工作线程就忙得像狗,当然想要让windows帮我处理收发消息我们得先准备一番,行,现在我们就来看看完成端口的创建:

PS:关于完成端口,网上完整的代码几乎找不到,所以这里也算是为大家展示一下完成端口的全称使用方法:


//============现在我们来初始化相关对象========

==============================================


void MyNet::init(){


m_listensocket = std::make_shared<SOCK_INFO>();

m_PerIoData = std::make_shared<PER_IO_DATA>();


assert(m_listensocket != nullptr&

&

m_PerIoData != nullptr);

WSADATA wsadata;

if (WSAStartup(0x0202, &

wsadata) != 0){

throw "

init socket fail!!"

;

}


m_listensocket->_socket = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);


if(m_listensocket->_socket == INVALID_SOCKET){

throw "

Create Socket Fail!!!"

;

}


m_CompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);

GetSystemInfo(&

m_systeminfo);

if (m_systeminfo.dwNumberOfProcessors >MAX_THREAD_COUNT)

m_systeminfo.dwNumberOfProcessors = MAX_THREAD_COUNT;


for (int i = 0;

i <m_systeminfo.dwNumberOfProcessors;

i++){

v_thread.push_back(std::thread(CompletionThread, this, m_CompletionPort));

}


for (auto&

th : v_thread){

th.detach();

}

}


==============================================

大家可能注意到了,这一次我们的init函数简单了不少,我们也没有初始化刚刚获取的智能指针,因为我们这一次写了自己的构造函数,相关的初始化都在构造函数中完成,所以这一次我们直接调用工厂函数std::make_share<T>()时,他会自动调用new T(),于是所有的初始化都会按我们写的构造函数执行。

WSAStartup初始化socket库,他需要两个参数,第一个unsigned long,第二个是WSADATA结构的指针,如果初始化成功,将返回0,如果返回非0值表示没有成功,unsigned long参数我们传递的是0x0202这个参数表示的意义是我们使用的WinSock2版本,如果想要了解得更深一些,可以参考《windows网络编程第二版》,这里大家只需要记住,如果要使用socket来进行网络编程的话,首先就是要加载socket库,WSAStartup就是完成这个功能的。

===============================


SOCKET WSASocket(

__in
int af,

__in
int type,

__in
int protocol,

__in LPWSAPROTOCOL_INFO lpProtocolInfo,

__in GROUP g,

__in DWORD dwFlags

);


================================

大家看了WSASocket这个函数将会创建一个SOCKET对象,af表示地址族,因为我们使用的IPv4版本,所以我们只能传递AF_INET给他就好,如果我们使用IPv6的话,就得传递AF_INET6给他,type当然就是socket类型,我们使用的TCP,所以自然用数据流既SOCK_STREAM,protocol指的网络协议,我们这里使是0表示使用和socket类型相关的协议,ipProtocolInfo和g我们都可以给他直接传递0,最后一个参数我们指明他是用重叠IO的,所以指定为WSA_FLAG_OVERLAPPED就好。如果该函数成功返回,socket将不再是INVALID_SOCKET。

====================================

HANDLE WINAPI CreateIoCompletionPort(

__in
HANDLE FileHandle,

__in_opt HANDLE ExistingCompletionPort,

__in
ULONG_PTR CompletionKey,

__in
DWORD NumberOfConcurrentThreads

);

==================================

CreateIoCompletionPort函数有两个功能,第一创建完成端口内核对象,我们上面的函数使用的就是该功能,大家记住一点,只要传递-1,0,0,0四个参数给他就能够完成这件事。第二个功能就是将相关句柄和完成端口关联在一起,我们下面会用第二个功能,所以大家记住,初始化的时候当然是创建,以后才是关联。

为什么上面给大家说创建的时候只需要传递-1,0,0,0呢?因为INVALID_HANDLE_VALUE就是-1,后面的NULL当然是0。

GetSystemInfo获取计算机信息,我们根据计算机的CPU核数来开辟线程,std::thread(CompletionThread, this, m_CompletionPort)开辟线程,就这么简单,我们同时将this和刚刚创建的完成端口传递进去就行,同时thread并没有复制构造和赋值操作,我们直接将新开辟的线程塞进vector中,大家可能回想为什么没有复制构造这个操作能够成功是,因为这里使用的是移动构造,他的效率远比复制构造赋值操作高,所以创建好线程后就被装进vector里面进去管理。


for (auto&

th : v_thread)我们使用范围for,这里使用auto的时候记得是引用,如同上面说的,因为thread不存在复制赋值等相关构造,所以如不引用就会出错,而且我们也有非用引用不可的理由,因为我们操纵的是thread本身,如果如果不是引用操纵就是他的副本(如果存在的话),th.detach();

这里一定是detach()而不是join(),因为如果是join的话程序就会在这里暂停,为什么呢?因为join的话就相当于普通函数的调用,非得等到返回才会继续执行,而用detach的话就不一样了,他将线程和句柄分离(纯属个人理解,但是很多时候我们又不得不用join等待他返回),所以不需要等待他返回。

//=====有了这个init,构造函数就简单了========


MyNet::MyNet(){

m_addrlen = sizeof(SOCKADDR_IN);

b_updateUser = false;

init();

}


=============================================

由于我们使用了智能指针,所以协构函数也相当简单


//===============协构函数=====================


MyNet::~MyNet(){

PostQueuedCompletionStatus(m_CompletionPort, 0xFFFFFFFF, 0, nullptr);

std::for_each(v_AcceptSocket.begin(), v_AcceptSocket.end(), [&

](std::shared_ptr<PER_IO_DATA>&

sp){

closesocket(sp->_socket);

sp->_socket = INVALID_SOCKET;

});

WSACleanup();

}


===============================================


虽然极为简单,但是还是有必要说一下:PostQueuedCompletionStatus这个是个投递函数,他将0小FFFFFFFF投递出来,GetQueuedCompletionStatus会捕获到这个数据,然后退出线程,接下来我们使用的标准库算法for_each(),再配合lambad表达式完成关闭socket的任务。最后清楚套接字库。


//====下面的几个操作函数都比较简单============


//设置回调函数


void MyNet::SetFun(OIFUN fun)

{

m_fun = fun;

}


//下面三个我们在窗口程序中会用到,也没什么好讲解


void MyNet::SetupdateUserinfo(bool bl){

b_updateUser = bl;

}


bool MyNet::GetupdateUserInfo(){


return b_updateUser;

}


std::vector<std::shared_ptr<PER_HANDLE_DATA>>&

MyNet::GetUserVector(){


return v_Userinfo;

}

==============================================


下面的CreatAndListen()开启监听同时将单Io句柄和完成端口绑定

//=======CreatAndListen()====================


void MyNet::CreatAndListen()

{

//设置监听地址和端口,当然我们在本机上监听,所以就用INADDR_ANY,当然这是只有一块网卡的情况下,端口为12000,0—1200不要使用,sin_family就是我们创建套接字时使用的地址族


m_listensocket->_addr.sin_family = AF_INET;

m_listensocket->_addr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);

m_listensocket->_addr.sin_port = htons(12000);


DWORD m_byte;

int ret = 0;


//bind将套接字绑定,绑定成功后就可以监听

ret = bind(m_listensocket->_socket, (SOCKADDR*)&

(m_listensocket->_addr), m_addrlen);

if (ret == SOCKET_ERROR){

throw "

bind fail"

;

}


//开始监听,SOMAXCONN在winsock2里面定义,当然我们可以随意传递一个整数进去

ret = listen(m_listensocket->_socket, SOMAXCONN);

if (ret == SOCKET_ERROR){

throw "

Listen Fail!!!"

;

}


//加载扩展函数,这里看上去复杂,其实很简单,当然大家还是去参考《windows网络编程第二版》上面说得很详细


GUID GuidAcceptEx = WSAID_ACCEPTEX;

GUID GuidGetAcceptExSockaddrs = WSAID_GETACCEPTEXSOCKADDRS;


ret = WSAIoctl(m_listensocket->_socket,

SIO_GET_EXTENSION_FUNCTION_POINTER,

&

GuidAcceptEx, sizeof(GuidAcceptEx),

&

lpfnAcceptEx, sizeof(lpfnAcceptEx),

&

m_byte, NULL, NULL);


if (ret == SOCKET_ERROR){

if (WSAGetLastError() != WSA_IO_PENDING){

std::cout <<WSAGetLastError() <<std::endl;

throw "

Loading AcceptEx fail!!!"

;

}

}


ret = WSAIoctl(m_listensocket->_socket,

SIO_GET_EXTENSION_FUNCTION_POINTER,

&

GuidGetAcceptExSockaddrs,

sizeof(GuidGetAcceptExSockaddrs),

&

lpfnGetAcceptExSockaddrs,

sizeof(lpfnGetAcceptExSockaddrs),

&

m_byte, NULL, NULL);


if (ret == SOCKET_ERROR)

throw "

Loading GetAcceptExSockaddr fail!!!"

;


//关联监听socket和完成端口

m_PerIoData->_socket = m_listensocket->_socket;

memcpy(&

m_PerIoData->_addr, &

m_listensocket->_addr, sizeof(SOCKADDR_IN));

ZeroMemory(&

m_PerIoData->_ol, sizeof(WSAOVERLAPPED));

m_PerIoData->_op = ACCEPT_POSTED;

//下面就是我们上面所说的第二个功能,第三个参数是一个相当重要的参数,因为我们传递进去的是什么,到时候我们在线程里面取出来的就是什么,这里说起来就相当复杂了,还是去参考《windows网络编程第二版》


HANDLE hrc = CreateIoCompletionPort((HANDLE)m_listensocket->_socket, m_CompletionPort, (DWORD)&

m_PerIoData, 0);

if (hrc == NULL)

{

std::cout <<"

Associate the socket fail!!"

<<std::endl;

}

//创建10个单IO句柄,专门用来连接客户的连接请求


for (int i = 0;

i<10;

i++){

auto AcceptSocket = std::make_shared<PER_IO_DATA>();

v_AcceptSocket.push_back(AcceptSocket);

Accept(AcceptSocket.get());

//大家还记得我们说引用计算的时候说到智能指针的get()成员函数吗?他返回一个原始指针,这里我们需要的就是一个原始指针。

}

}


===========================================

上面的CreatAndListen()函数用到很多偏僻的东西,这些东西不是C++的内容,比如完成端口,SOCKET这些相关操作都是win32的东西,当然我们要用C++来写windows应用程序,自然免不掉要用这些操作,但是如果我现在给大家讲解这些东西,会花费不少篇幅,到时会让程序显得断断续续,所以对于SOCKET的一些操作,大家可以看看相关数据,《windows网络编程第二版》上面说得很详细,所以我这里不花费太多时间来讲解这些东西了。


//===========Accept(PER_IO_DATA* pPerIoData)=====



void MyNet::Accept(PER_IO_DATA* pPerIoData)

{

//在投递连接之前我们得先初始化一些相关的对象


DWORD byte;

int
ret;


pPerIoData->_socket = socket(AF_INET, SOCK_STREAM, 0);

if (pPerIoData->_socket == INVALID_SOCKET){

std::cout <<"

Accept Socket is Invalid socket"

<<std::endl;

}

pPerIoData->ReSet();

//使用我们的相关成员函数来重置对象


pPerIoData->_op = ACCEPT_POSTED;

//使用扩展函数来投递一个连接操作

ret = lpfnAcceptEx(m_listensocket->_socket,

pPerIoData->_socket,

pPerIoData->_wsabuf.buf,

pPerIoData->_wsabuf.len – (sizeof(SOCKADDR_IN)+16) * 2,

sizeof(SOCKADDR_IN)+16,

sizeof(SOCKADDR_IN)+16,

&

byte,

&

pPerIoData->_ol);


if (ret == FALSE)

{

if (WSAGetLastError() != WSA_IO_PENDING)

{

std::cout <<WSAGetLastError() <<std::endl;

throw "

AcceptEx fail!!!"

;

}

}

}

=============================================

这个函数的作用是一旦投递成功之后,如果有客户连接进来,线程函数会通过GetQueuedCompletionStatus捕获到相关操作,然后进入下一步处理,当然,这个扩展函数他不但连接就会成功捕获,而且还必须收到一份数据才算是操作完成,这时GetQueuedCompletionStatus才会真正的捕获,然后我们再通过另一个扩展函数将客户信息抓出来。但是碍于篇幅的原因,今天只能到这里了。


ok,就先到这里吧,改天继续。

==========================================

回复D或者d直接查看目录,回复相应数字查看章节内容


原文始发于微信公众号(

C/C++的编程教室

):第八十八讲 综合运用(3)

|

发表评论