連接
接收
發送
在《[C++]-Windows下Socket連接之服務端》中介紹了Windows下Socket編程的一些基本知識與服務端實現,現在介紹一下客戶端的實現。相比於服務端,客戶端流程相對簡單些,主要就是:
-
連接服務端;
-
收發消息
此客戶端實現,除發送接口外,其他的都使用IOCP(I/O Completion Port,I/O完成端口)接口WSAXXX
。IOCP是性能良好的I/O模型,可以支持大併發(通過完成端口,避免大量線程的創建),更適合在服務端使用。
等待服務端應答及退出事件,都是通過C++條件變量實現的(參見《條件變量condition_variable存在的一些問題》)
XuEvent g_evtQuit;
XuEvent g_evtAck;
連接
與服務端相似,在使用socket前,需要先通過initSocket
初始化,並在退出前做清理工作。
連接時,先設定要連接的地址與端口,然後發起連接,成功後進行收發處理即可。
SOCKET openConnection(const string &strHost, unsigned short nPort,
function<void(const string&, int)> funError,
function<void(const string&)> funMsg) {
string strError;
SOCKADDR_IN sockAddr;
SOCKET sockClient = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, WSA_FLAG_OVERLAPPED);
if (INVALID_SOCKET == sockClient) {
goto FUN_CLEANUP;
}
// to connect
sockAddr.sin_family = AF_INET;
sockAddr.sin_port = htons(nPort);
inet_pton(AF_INET, strHost.c_str(), &(sockAddr.sin_addr));
if (SOCKET_ERROR == WSAConnect(sockClient, (SOCKADDR*)&sockAddr, sizeof(sockAddr), nullptr, nullptr, nullptr, nullptr)) {
goto FUN_CLEANUP;
}
g_bStop = false;
{
thread thrRecv(receiveMsg, sockClient, funError, funMsg);
thrRecv.detach();
thread thrAlive(heartBeat, sockClient, funError);
thrAlive.detach();
}
return sockClient;
FUN_CLEANUP:
int nError = WSAGetLastError();
if (nullptr != funError) {
funError(strError, nError);
}
return INVALID_SOCKET;
}
接收
接收消息使用的完成端口WSARecv實現的,接受者通過通過事件等待,在有消息到達時系統會主動通知。
接收返回WSA_IO_PENDING
錯誤時,不是真正的錯誤,只是告訴上層當前未收到消息,需要等待,在有消息到達時會觸發通知。
void receiveMsg(SOCKET sock,
function<void(const string&, int)> funError,
function<void(const string&)> funMsg) {
int nPort = 0;
string strAddr = XuNet::peerAddress(sock, &nPort);
cout << "Connected " << strAddr << ":" << nPort << endl;
string strError;
const int MaxBufSize = 255;
char szBuff[MaxBufSize + 1] = { 0 };
int nRet = 0, nRecvError = 0;
WSABUF wsaBuf;
WSAOVERLAPPED overLapped;
ZeroMemory(&overLapped, sizeof(overLapped));
overLapped.hEvent = WSACreateEvent();
if(NULL == overLapped.hEvent){
nRecvError = WSAGetLastError();
strError = "WSACreateEvent";
goto FUN_CLEANUP;
}
wsaBuf.buf = szBuff;
wsaBuf.len = MaxBufSize;
while (!g_bStop) {
DWORD dwFlag = 0;
DWORD dwRecved = 0;
nRet = WSARecv(sock, &wsaBuf, 1, &dwRecved, &dwFlag, &overLapped, NULL);
if (SOCKET_ERROR == nRet) {
nRecvError = WSAGetLastError();
if (WSA_IO_PENDING != nRecvError) {
strError = "WSARecv";
goto FUN_CLEANUP;
}
nRecvError = 0;
}
nRet = WSAWaitForMultipleEvents(1, &overLapped.hEvent, TRUE, INFINITE, FALSE);
if (WSA_WAIT_FAILED == nRet) {
nRecvError = WSAGetLastError();
strError = "WSAWaitForMultipleEvents";
goto FUN_CLEANUP;
}
nRet = WSAGetOverlappedResult(sock, &overLapped, &dwRecved, FALSE, &dwFlag);
if(FALSE == nRet){
nRecvError = WSAGetLastError();
strError = "WSAGetOverlappedResult";
goto FUN_CLEANUP;
}
if (dwRecved > 0) {
string strMsg(szBuff, dwRecved);
if (strMsg.substr(0, 3) == MSG_Ack) {
g_evtAck.notifyAll();
}
else if (nullptr != funMsg) {
funMsg(strMsg);
}
}
WSAResetEvent(overLapped.hEvent);
}
FUN_CLEANUP:
WSACloseEvent(overLapped.hEvent);
if (nullptr != funError) {
funError(strError, nRecvError);
}
}
發送
通過定時向服務端發送心跳包,及時發現連接問題(服務端會根據心跳,做超時處理)。發送完消息後,會等待服務端做一個應答,只有接收到服務端應答後纔會真正確認發送成功。
bool sendMessage(SOCKET sock, string strMsg, int nWaitSecs) {
strMsg += MSG_Delim;
g_evtAck.reset();
int nLen = send(sock, strMsg.c_str(), strMsg.length(), 0);
if (SOCKET_ERROR == nLen) {
int nError = WSAGetLastError();
cout << "!!!Send " << strMsg << " fail: " << nError << endl;
return false;
}
return g_evtAck.wait(nWaitSecs);
}
心跳包:
void heartBeat(SOCKET sock, function<void(const string&, int)> funError) {
int nLostAck = 0;
int nCount = 0;
while (!g_bStop) {
if (sendMessage(sock, MSG_Alive + std::to_string(++nCount), 1)) {
nLostAck = 0;
}
else {
if (++nLostAck > HeartBeat_MaxLost) {
if (nullptr != funError) {
funError("too many Ack of heartBeat lost", -1);
}
break;
}
}
if (g_evtQuit.wait(HeartBeat_Seconds)) {
cout << "evtQuit got signal" << endl;
break;
}
}
closesocket(sock);
}