第八十九讲 综合运用(4)

好吧,我们继续上一讲的内容呢,上一讲我们讲到Accept()函数,我们用这个函数来接收新连接进来的客户,当然连接进来的客户必须要发送一份数据过来,也就是第一份数据,由于我们在这里设置的op为ACCEPT_POSTED,线程函数会捕获到这个完成时间,于是通过op判断出来是ACCEPT_POSTED,将他传递被HandleIo函数(一个参数版本的HandleIo),这个函数会处理这个连接进来的客户,同时为这个客户分配一个对象,我们直接来看代码吧


//===============HandleIo==============

//处理连接进来的客户


void MyNet::HandleIo(PER_IO_DATA* pPerIoData)

{

//如果lpfnGetAcceptExSockaddrs成功返回,那么下面的两个SOCKADDR_IN的指针将会指向两端地址,LockSockAddr指向本地socket地址储存段的地址,RemoteSockAddr指向客户端地址的储存段

SOCKADDR_IN

*LockSockAddr = nullptr;

SOCKADDR_IN

*RemoteSockAddr = nullptr;

int

LockSockAddrLen, RemoteSockAddrLen;


lpfnGetAcceptExSockaddrs(

pPerIoData->_wsabuf.buf,

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

sizeof(SOCKADDR_IN)+16,

sizeof(SOCKADDR_IN)+16,

(SOCKADDR**)&

LockSockAddr, &

LockSockAddrLen,

(SOCKADDR**)&

RemoteSockAddr, &

RemoteSockAddrLen);


//我们将客户信息提取出来inet_ntoa(RemoteSockAddr->sin_addr)提取出来的是客户IP,RemoteSockAddr->sin_port提取出来的是服务器为客户分配的端口


std::cout <<"

Client: nIP:"

<<inet_ntoa(RemoteSockAddr->sin_addr) <<"

nPort:"

<<ntohs(RemoteSockAddr->sin_port)

<<"

nJoin this systemn"

;

std::cout <<pPerIoData->_wsabuf.buf <<std::endl;



bool update = false;


//是否让客户更新列表,这需要配合客户端使用


//pPerIoData->_wsabuf.buf此时储存的是客户端发过来的第一个数据,这里我们设定的是第一个数据表示着客户端的用户名,所以接下来我们将会判断用户名是否重复了


std::string regstr = pPerIoData->_wsabuf.buf;

//下面的if语句将判断现在连接进来的客户的用户名是否有重名,如果if判断为true,那么说明没有重复的,该用户可以使用该用户名(昵称)

if (std::find_if(v_Userinfo.begin(), v_Userinfo.end(), [&

](std::shared_ptr<PER_HANDLE_DATA>&

per){


return per->GetName() == regstr;

}) == v_Userinfo.end()){

//为新客户准备一个句柄对象

auto m_ClientData = std::make_shared<PER_HANDLE_DATA>();

if (m_ClientData == NULL){

MessageBox(NULL, L"

memery fail!!"

, L"

HandIo Error"

, MB_OK);

return;

}

//设置新客户的信息,socket,地址,昵称

m_ClientData->SetSocket(pPerIoData->_socket);

m_ClientData->SetAddr(*RemoteSockAddr);

m_ClientData->SetName(regstr);

//将设置好的对象放入容器中(可以理解为一个对象池)中管理

v_Userinfo.push_back(m_ClientData);

//将完成端口和客户套接字关联起来,以后该对象收发操作将会有线程来捕获

HANDLE hwnd = CreateIoCompletionPort((HANDLE)m_ClientData->GetSocket(), m_CompletionPort,

(DWORD)m_ClientData.get(), 0);

if (hwnd == NULL){

MessageBox(NULL, L"

CreteIoCompletion Fail!!"

, L"

HandIO Error"

, MB_OK);

}

//准备更新列表

update = true;

b_updateUser = true;

}

else{

//当然,如果新连接进来的客户使用了一个已被他人使用的昵称,那么服务器将作出反应,发送一"

ERROR"

标识符,在客户收到服务器反馈回来的"

ERROR"

时将会提示重新输入昵称,当然这些东西你们可以按自己的想法去定义,是反馈"

ERROR"

呢还是其他你们在定制客户端的时候说了算。

//大家注意到了,我们这里反馈消息时用的还是io操作,那是因为我们判断出昵称重复暂时没有为客户分配句柄对象,这时候我们已经接收到了第一个数据,所以这里op也不能再是ACCEPT_POSED了,这是我们发送数据,所以这里自然要用SEND_POSTED,这会在线程中捕获到,然后转投相应的操作。


std::string error = "

ERROR"

;

DWORD ByteTransfered = 0;

m_PerIoData->ReSet();

m_PerIoData->_op = SEND_POSTED;

m_PerIoData->_wsabuf.buf = const_cast<char*>(error.c_str());

WSASend(pPerIoData->_socket, &

m_PerIoData->_wsabuf, 1, &

ByteTransfered, 0, &

m_PerIoData->_ol, 0);

}


if (update){

//如果有新客户成功进来,这时我们就要更新用户列表了

UpdateUser();

update = false;

}

//处理好一个新客户之后,我们可以投递下一个连接了等待了

this->Accept(pPerIoData);

}


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


ok,既然上面说有客户连接进来我们就应该更新用户列表,那么我们接下来就看看这个UpdateUser()函数吧;


//============UpdateUser()=================



void MyNet::UpdateUser(){

//有可能会面临着高并发的访问(实际上这种情况很少),所以我们使用了同步机制std::unique_lock 是一中用对象管理资源的机制,简单点说就是在构造函数中锁定资源,在协构函数中释放资源,这是新C++11的新标准,其实C++11代替了很多Boost里面的东西(最近在看Boost,发觉里面很多有用的东西现在C++11都有补充了),std::recursive_mutex是一个循环递归锁,很多时候我们不小心就会造成死锁现象(这里就不举例子了),我们为了安全可以考虑使用std::recursive_mutex这个来循环锁,这样就避免了死锁现象


std::unique_lock<std::recursive_mutex>lok(m_rcmtx);


//大家可能会被下面的代码给吓住,我简单滴说下功能,下面的代码就是如果目前有用户存在,那么将所有用户的昵称发送给新连接进来的客户,然后又将新连接进来的客户的昵称发送给以前存在的每一个客户,说起来有些复杂,想想就知道做起来肯定不简单,但是我就用了三句代码就完成这件事,当然我这么说不是想表明我多牛X,只是想说其实如果好好利用STL,可以很方便的帮我们解决很多看似麻烦的大问题

if (!v_Userinfo.empty()){

std::for_each(v_Userinfo.begin(), v_Userinfo.end()-1, [&

](std::shared_ptr<PER_HANDLE_DATA>&

per){

std::string str = "

UserName:"

;

str += per->GetName();

this->SendMsg(v_Userinfo.at(v_Userinfo.size() – 1), str);

});

std::for_each(v_Userinfo.begin(), v_Userinfo.end() – 1, [&

](std::shared_ptr<PER_HANDLE_DATA>&

per){

std::string str = "

UserName:"

;

str += v_Userinfo.at(v_Userinfo.size() – 1)->GetName();

this->SendMsg(per, str);

});


//大家尤其注意这里了,这里我一开始的时候出错了,而且要命的是调试了很久居然没想到是这里出问题了,而且大家要是认真的话也看出了我们上面的两句代码和下面的代码里面使用的SendMsg函数有所不同,在上面我们使用的是以智能指针为参数的那个SendMsg,下面我们使用的是原始指针那个版本的SendMsg,那么着有什么区别呢?好吧,我们下面来说说这两个重置版本的SendMsg的具体区别


std::string str = "

UserName:"

;

str += v_Userinfo.at(v_Userinfo.size() – 1)->GetName();

this->SendMsg(v_Userinfo.at(v_Userinfo.size() – 1).get(), str);

}

}


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

上面既然引出了两个版本的SendMsg,那么我们就来看看这两个版本的区别在哪里


//==============SendMsg=======================



void MyNet::SendMsg(std::shared_ptr<PER_HANDLE_DATA>&

per, std::string&

str){

std::unique_lock<std::recursive_mutex>lok(m_rcmtx);

int ret = NO_ERROR;

int count = 0;

WSABUF
wsabuf;

DWORD
bytes;

wsabuf.buf = const_cast<char*>(str.c_str());

wsabuf.len = str.length() + 2;

ret = WSASend(per->GetSocket(), &

wsabuf, 1, &

bytes, 0, NULL, NULL);

if (ret == SOCKET_ERROR &

&

WSAGetLastError() != WSA_IO_PENDING)

{

std::cout <<"

SendMsg Fail And Error ID:"

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

}

}


=====下面的是一个重载版本=================


int MyNet::SendMsg(PER_HANDLE_DATA* perHandle, std::string&

str)

{

std::unique_lock<std::recursive_mutex>lok(m_rcmtx);

int ret = NO_ERROR;

int count = 0;

WSABUF
wsabuf;

DWORD
bytes;

wsabuf.buf = const_cast<char*>(str.c_str());

wsabuf.len = str.length() + 2;

if (perHandle->GetCount() <2){

m_PerIoData->_op = SEND_POSTED;

perHandle->AddCount();

}

ZeroMemory(&

m_PerIoData->_ol, sizeof(WSAOVERLAPPED));

ret = WSASend(perHandle->GetSocket(), &

wsabuf, 1, &

bytes, 0, &

m_PerIoData->_ol, NULL);

if (ret == SOCKET_ERROR)

{

if (WSAGetLastError() != WSA_IO_PENDING)

{

int d = WSAGetLastError();

std::cout <<d <<std::endl;

MessageBox(NULL, L"

SendMsg–>WSASend fail!!!"

, L"

SendMsg Error"

, MB_OK);

}

}


return ret;

}


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

看出来吧,这两个版本的区别很简单,使用智能指针版本的SendMsg只是简单的异步发送消息而已,而使用原始指针版本的SendMsg带有重叠操作,他会得到线程捕获,然后进入下一个投递操作,那么为什么会有两个版本呢?这件事说起来真的很痛苦,因为关于完成端口完整代码真心找不到,而且《windows网络编程第二版》那本书上也没有具体的实际开发用法,所以自己摸索了很久竟然没有找到一个bug的所在之处,最后恍然明白一件事,那就是比如说当服务器收到一个客户发来的消息时,他会想这个消息转发出去给所有的用户,那么这个转发过程中就完全没有必要使用完成端口,因为我们在完成端口没捕获到一个操作后都会投递下一个操作,所有如果我们对于每一个转发都使用了完成端口,最后的结果是会出现死循环抑或是各种空数据的收发,所以在上面的UpdateUser中其实我们只需要对最新连接进来的客户进行完成操作就行。

当然有发送,就必然会有接收,所以我们来看看RecvMsg的实现,其实他很简单,和完成版本的SendMsg差不多。


//========RecvMsg()======================



void MyNet::RecvMsg(PER_HANDLE_DATA* perHandle, PER_IO_DATA* perIoData){

std::unique_lock<std::recursive_mutex>lok(m_rcmtx);

DWORD
ByteTransfered, Flag = 0;

int ret = 0;

perIoData->ReSet();

if (perHandle->GetCount() <2){

//之所以有这个判断是因为我们不想在每个句柄上都有一大堆未完成的操作

perIoData->_op = RECV_POSTED;

perHandle->AddCount();

}

ret = WSARecv(perHandle->GetSocket(), &

perIoData->_wsabuf, 1,

&

ByteTransfered, &

Flag, &

(perIoData->_ol), 0);

if (ret == SOCKET_ERROR){

if (WSAGetLastError() != WSA_IO_PENDING){

std::cout <<"

WSARecv Fail And Error ID: "

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

}

}

}


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


接下来我们看看如何将接收到的消息转发给所有的用户。


//===========SendAllMsg()=============



void MyNet::SendAllMsg(std::string str){

std::unique_lock<std::recursive_mutex>lok(m_rcmtx);

if (v_Userinfo.empty())

return;

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

](std::shared_ptr<PER_HANDLE_DATA>&

per){

//因为只是很简单的转发,所以我们只需要调用智能指针版本的SendMsg就好

SendMsg(per, str);

});

}


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


从我们的分析看来,SendToAll这个函数已经很没有必要了,当如如果我们真想要的话,实现起来也相当的简单。

//=======SendToAll()===================



void MyNet::SendToAll(std::string&

str)

{

std::unique_lock<std::recursive_mutex>lok(m_rcmtx);

if (v_Userinfo.empty())

return;

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

](std::shared_ptr<PER_HANDLE_DATA>per){

//只需要将这里改为原始指针版本就好

SendMsg(per.get(), str);

});

}


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

可能大家在想,如果客户下线了怎么处理呢?很简单,我们把和该客户相关的对象关闭就行,CloseSocket就是完成这个功能的


//=====CloseSocket====================

//清除已关闭的客户端套接字


void MyNet::CloseSocket(PER_HANDLE_DATA* pPerIoData)

{

try{

std::unique_lock<std::recursive_mutex>lok(m_rcmtx);

std::string str = "

UserName:"

;


//UserName这里只作为一个标识符,可以根据定制客户端修改

str += pPerIoData->GetName();

//首先我们查找出需要关闭的客户在对象池中的位置

auto it = std::find_if(v_Userinfo.begin(), v_Userinfo.end(), [&

](std::shared_ptr<PER_HANDLE_DATA>&

sp){


return sp->GetSocket() == pPerIoData->GetSocket();

});

//如果找到了,那么就将关闭他的套接字同时将他移除对象池,由于没有资源在引用该对象,该对象便会自动协构,这就是智能指针的好处,当然我们还是可以手动释放资源的,std::share_ptr有个reset函数,他可以用于手动释放资源,但是我们没有必要这么做

if (it != v_Userinfo.end()){

closesocket((*it)->GetSocket());

(*it)->SetSocket(INVALID_SOCKET);

v_Userinfo.erase(it);

this->SendAllMsg(str);

}

//清除一个客户后我们还得更新列表框,当然这是框中程序的时候才用到的

b_updateUser = true;

}

catch (std::bad_alloc bad){

std::cout <<"

Exception!!!!n"

;

std::cout <<bad.what() <<std::endl;

}

}

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

大家也看到了吧,这个函数也相当的简单,不过这里大家也看出了我使用try—–catch模块,为什么呢?因为一开始没有考虑清楚,所以总是导致这里异常抛出,于是就在这里设置了异常捕获,不过现在给大家的版本这里是基本不会抛出异常的,因为我们我们控制了oi的操作,所以这里不会再有传递无效参数的情况并导致bad_alloc异常抛出的情况发生。


ok,现在还剩下一个极为重要的函数HandleIo(PER_HANDLE_DATA*, PER_IO_DATA*, DWORD),但是还是碍于篇幅问题,只能到这里了,改天再来说吧。


对了,和大家说一下,前面几讲忘记添加关键字回复,所以导致大家看不到修改后的内容,现在已经添加上去了,大家可以重新回复85以后的数字查看最新修改的内容了。


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

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


原文始发于微信公众号(

C/C++的编程教室

):第八十九讲 综合运用(4)

|

发表评论