完成埠(CompletionPort)之客戶端篇
**
完成埠之客戶端篇
**
首先說一下這篇文章的初衷。不久前工作中要用到網路通訊進行資料交換,既然要通訊當然要有伺服器和客戶端,於是乎把MFC中的CAsyncSocket搬過來用了,簡單的過載幾個函式就完成了資料收發,但是後續遇到了較多問題,首先多執行緒使用時很多時候無法觸發OnReceive事件,再加上接收到資料後需要較多的等待,所以整個介面都遭殃了,視窗卡到拖不動····
於是乎決定修煉一下網路程式設計,首選就是大名鼎鼎的完成埠模型了,於是看到了PiggyXP的一篇講完成埠的文章,講的很詳細也很易懂,看了之後猶如醍醐灌頂,但是原文中對資料傳送部分一筆略過,但是如果看懂這篇博文的話,我想自己加上傳送部分也就是幾分鐘的事,感謝PiggyXP,學習本文時如果對完成埠不瞭解的話還請大家先看PiggyXP博主關於完成埠的博文,本文用到了PiggyXP博主中的完成埠模型,方便大家參考學習,貼上原文地址:
現在說一說關於客戶端的事,一般情況我們的客戶端需要連線到伺服器,並和伺服器發生資料互動(資料傳送、資料接收),與伺服器不同的是客戶端的功能多樣化,比如我其中一個客戶端只想查詢網路時間,另一個客戶端只想關心伺服器的忙碌狀態,如果你將多種多樣的功能都整合到一個客戶端資料接收部分,無疑是不明智的決定,這樣不僅會使得客戶端變得很臃腫,而且程式碼更不好維護。何不讓不同的客戶端做不同的事,這樣分工明確。
網上找了一下關於客戶端的內容,但是資料少的可憐,人們大談闊論的幾乎都是伺服器端的設計,難道客戶端就不重要了嗎?
於是乎小弟不才決定搞一搞,既然完成埠可用作伺服器程式,為何就不可以用作客戶端程式呢?於是乎就有了本文···
接下來就讓我們一起把PiggyXP的完成埠模型稍加修改,讓其變成一個強大的完成埠模型的客戶端,讓其擁有更高效的處理能力,並支援多執行緒資料收發和超多的併發連線。。。
具體思路是這樣的
1、建立完成埠,啟動工作執行緒(CIOCPModel完成)
2、客戶端發起到某個伺服器的連線(CClient發起)
3、客戶端的連線申請提交給CIOCPModel,由CIOCPModel完成連線過程,並將該連線繫結到完成埠
4、繫結成功後即可享用完成埠的妙用了,資料到來後會自動呼叫CClient的OnReceive,資料傳送完畢後會呼叫OnSendComplete,Socket關閉時會呼叫OnClose;
5、從CIOCPClient派生你自己的子類實現你想要的功能即可
注:CIOCPModel是完成埠模型類
CIOCPClient是與完成埠介面的虛類
CClient是由CIOCPClient派生的子類
一起動手實現吧!
首先為CIOCPModel新增連結到伺服器的程式碼
SOCKET CIOCPModel::conn(CString ip,UINT nPort,CIOCPClient* pClient)
{
if(!isStart)
return INVALID_SOCKET;
struct sockaddr_in ServerAddress;
PER_SOCKET_CONTEXT* m_pNewContext = new PER_SOCKET_CONTEXT;
m_pNewContext->m_pClient = pClient;
m_pNewContext-> m_Socket = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
if (INVALID_SOCKET == m_pNewContext->m_Socket)
{
this->_ShowMessage(0,"初始化Socket失敗,錯誤程式碼: %d.\n", WSAGetLastError());
delete m_pNewContext;
return INVALID_SOCKET;
}
else
{
TRACE("WSASocket() 完成.\n");
}
// 填充地址資訊
ZeroMemory((char *)&ServerAddress, sizeof(ServerAddress));
ServerAddress.sin_family = AF_INET;
ServerAddress.sin_addr.s_addr = inet_addr(ip);
ServerAddress.sin_port = htons(nPort);
if(connect(m_pNewContext->m_Socket,(struct sockaddr*)&ServerAddress,sizeof(sockaddr_in)) == -1)
{
this->_ShowMessage(0,"連線到伺服器失敗!錯誤程式碼: %d/n", WSAGetLastError());
RELEASE_SOCKET( m_pNewContext->m_Socket );
delete m_pNewContext;
return INVALID_SOCKET;
}
// 將Socket繫結至完成埠中
if( NULL== CreateIoCompletionPort( (HANDLE)m_pNewContext->m_Socket, m_hIOCompletionPort,(DWORD)m_pNewContext, 0))
{
this->_ShowMessage(0,"繫結 Socket至完成埠失敗!錯誤程式碼: %d/n", WSAGetLastError());
RELEASE_SOCKET( m_pListenContext->m_Socket );
delete m_pNewContext;
return INVALID_SOCKET;
}
else
{
TRACE("Socket繫結完成埠 完成.\n");
}
PER_IO_CONTEXT* pNewIOContext = m_pNewContext->GetNewIoContext();
pNewIOContext->m_sockAccept = m_pNewContext->m_Socket;
if(NULL == _PostRecv(pNewIOContext))
{
m_pNewContext->RemoveContext(pNewIOContext);
RELEASE_SOCKET( m_pListenContext->m_Socket );
delete m_pNewContext;
return INVALID_SOCKET;
}
_AddToContextList(m_pNewContext);
return m_pNewContext->m_Socket;
}
同時還有關閉連線的disconn()函式
// 斷開客戶端到伺服器的連線
bool CIOCPModel::disconn(SOCKET sock)
{
if(!isStart)
return true;
return PostQueuedCompletionStatus(m_hIOCompletionPort, 0, (DWORD)CLOSE_SOCKET, (LPOVERLAPPED)sock);
}
isStart是CIOCPModel中一個標識完成埠是否啟動的變數
關鍵的部分來了,此功能能得以實現關鍵就是繫結完成埠時用到的CompletionKey,也就是一個PER_SOCKET_CONTEXT結構體,我在原文基礎上增加了一個變數 CIOCPClient* m_pClient; 來儲存客戶端指標,用於呼叫客戶端的資料處理過程,文章末尾我會加上完整客戶端的程式碼供大家參考。
這樣將每個連線成功的客戶端和完成埠繫結的時候就自動加入了客戶端的資訊,當有資料到來時,輕而易舉的就可以呼叫對應客戶端的處理過程了。
只需在工作執行緒中加入
case RECV_POSTED:
{
if(pSocketContext->m_pClient != NULL)
pSocketContext->m_pClient->OnReceive(pIoContext);
pIOCPModel->_DoRecv( pSocketContext,pIoContext );
}
break;
// SEND
case SEND_POSTED:
{
if(pSocketContext->m_pClient != NULL)
pSocketContext->m_pClient->OnSendComplete(pIoContext);
pIOCPModel->_DoSend(pSocketContext,pIoContext);
}
break;
我稍稍改裝了一下CIOCPModel類,加入了傳送資料部分,使用WSASend函式,所以在傳送完成後會進入case SEND_POSTED:中進行處理;如果不想使用非同步傳送,也可直接呼叫send函式進行阻塞傳送,那麼也就不會進入傳送完成的部分了
客戶端的虛父類如下:
#pragma once
#include "IOCPModel.h"
class CIOCPModel;
class CIOCPClient
{
public:
CIOCPClient();
CIOCPClient(CIOCPModel* pModel);
~CIOCPClient(void);
bool Connect(CString ip,UINT nPort);
bool Close();
void SetIOCPModel(CIOCPModel* pModel){m_pIOCPModel = pModel;}
virtual int SendMsg(const char* buff,DWORD dwByte,int nFlag = 0);
virtual void OnReceive(PER_IO_CONTEXT* pContext ) = 0;
virtual void OnSendComplete(PER_IO_CONTEXT* pContext) = 0;
virtual void OnClose() = 0;
protected:
CIOCPModel* m_pIOCPModel;
SOCKET m_sock;
bool isConnected;
};
#include "stdafx.h"
#include "IOCPClient.h"
CIOCPClient::CIOCPClient(void)
{
m_pIOCPModel = NULL;
m_sock = INVALID_SOCKET;
isConnected = false;
}
CIOCPClient::CIOCPClient(CIOCPModel* pModel)
{
m_pIOCPModel = pModel;
m_sock = INVALID_SOCKET;
isConnected = false;
}
CIOCPClient::~CIOCPClient(void)
{
if(isConnected)
this->Close();
}
bool CIOCPClient::Connect(CString ip,UINT nPort)
{
if(isConnected)
return true;
if(m_pIOCPModel != NULL)
{
m_sock = m_pIOCPModel->conn(ip,nPort,this);
if(m_sock == INVALID_SOCKET)
return FALSE;
else
{
isConnected = TRUE;
}
}
return isConnected;
}
bool CIOCPClient::Close()
{
if(isConnected && m_sock != INVALID_SOCKET && m_pIOCPModel)
{
if(m_pIOCPModel->disconn(m_sock))
{
isConnected = false;
m_sock = INVALID_SOCKET;
return true;
}
else
{
return false;
}
}
else
return false;
}
int CIOCPClient::SendMsg(const char* buff,DWORD dwByte,int nFlag)
{
if(isConnected && m_sock != INVALID_SOCKET && m_pIOCPModel)
{
return m_pIOCPModel->SendMsg(m_sock,buff,dwByte,nFlag);
}
return -1;
}
以上關鍵程式碼已經完成,剩下的就是從CIOCPClient派生自己的子類進行資料處理即可;
下面是完整的客戶端例項程式,需要的兄弟可以下載試試,由於本人才疏學淺,難免很多地方理解不到位,或者有錯漏的地方,如有高人路過還請指點,共同進步,謝謝!
完整客戶端例項程式碼下載地址:
http://download.csdn.net/detail/ylj135cool/8897737