Socket即套接字,用於網絡通訊,有三種模式:
- 流式套接字(SOCK_STREAM)
- 數據報套接字(SOCK_DGRAM)
- 原始套接字(SOCK_RAW)
輔助函數
Windows下提供了WSAXXX系列函數來輔助Socket的開發。爲了使用這些函數需要:
#include <WinSock2.h>
#include <WS2tcpip.h>
#include <Mswsock.h>
#pragma comment(lib, "Ws2_32.lib")
初始化
在使用Socket接口前,需要先做初始化,並在完成時做清理工作:
void initSocket() {
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
int nError = WSAGetLastError();
throw XuException("WSAStartup fail", nError);
}
}
void uninitSocket() {
WSACleanup();
}
地址與端口
通過Socket句柄,可以方便地獲取本地與對端的連接地址與端口:
std::string peerAddress(SOCKET sock_, int *pPort_) {
struct sockaddr_in addr;
int addrLen = sizeof(addr);
if (getpeername(sock_, (struct sockaddr*)&addr, &addrLen) == SOCKET_ERROR) {
int nError = WSAGetLastError();
throw XuException("getpeername fail", nError);
}
char szBuff[INET6_ADDRSTRLEN] = { 0 };
if (inet_ntop(addr.sin_family, &addr.sin_addr, szBuff, INET6_ADDRSTRLEN) == nullptr) {
int nError = WSAGetLastError();
throw XuException("inet_ntop fail", nError);
}
if(nullptr!= pPort_)
*pPort_ = ntohs(addr.sin_port);
return szBuff;
}
std::string localAddress(SOCKET sock_, int *pPort_) {
struct sockaddr_in addr;
int addrLen = sizeof(addr);
if (getsockname(sock_, (struct sockaddr*)&addr, &addrLen) == SOCKET_ERROR) {
int nError = WSAGetLastError();
throw XuException("getpeername fail", nError);
}
char szBuff[INET6_ADDRSTRLEN] = { 0 };
if (inet_ntop(addr.sin_family, &addr.sin_addr, szBuff, INET6_ADDRSTRLEN) == nullptr) {
int nError = WSAGetLastError();
throw XuException("inet_ntop fail", nError);
}
if (nullptr != pPort_)
*pPort_ = ntohs(addr.sin_port);
return szBuff;
}
連接時長
通過Socket句柄,也可以方便地獲取已連接的時長:
int connectedSeconds(SOCKET sock_){
DWORD dwSeconds = 0;
int nLen = sizeof(dwSeconds);
if (SOCKET_ERROR == getsockopt(sock_, SOL_SOCKET, SO_CONNECT_TIME, (char*)&dwSeconds, &nLen)) {
return -1;
}
return (int)dwSeconds;
}
TCP服務端
TCP是面向連接的,使用的是流式套接字。爲了完成實現連接,服務端需要:
- 建立Socket;
- 綁定端口與地址;
- 偵聽,並等待連接;
所有客戶端連接通過map來保存,以下是相關定義:
bool g_bStop = false;
SOCKET g_sockListen = INVALID_SOCKET;
map<int, SOCKET> g_mapConnected;
偵聽等待
爲了建立通訊連接,服務端需要先偵聽,等待客戶端連接的到來。
若要在指定地址上偵聽,則需要明確設定要偵聽的地址;若要在本機所有地址上偵聽,則只需設定偵聽地址爲INADDR_ANY
即可。
void acceptClientConnect(unsigned short nPort,
function<void(const string&, int)> funError,
function<void(int, const string&)> funMsg) {
g_mapConnected.clear();
string strError;
SOCKET sockListen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (INVALID_SOCKET == sockListen) {
strError = "socket";
goto FUN_CLEAN_UP;
}
SOCKADDR_IN sockAddr;
sockAddr.sin_family = AF_INET;
sockAddr.sin_port = htons(nPort);
// 偵聽迴環地址
//string strIP = "127.0.0.1";
//inet_pton(AF_INET, strIP.c_str(), &(sockAddr.sin_addr));
sockAddr.sin_addr.s_addr = INADDR_ANY; // 偵聽所有(0.0.0.0)
if (::bind(sockListen, (SOCKADDR*)&sockAddr, sizeof(sockAddr)) == SOCKET_ERROR) {
strError = "bind";
goto FUN_CLEAN_UP;
}
if (listen(sockListen, 1) == SOCKET_ERROR) {
strError = "listen";
goto FUN_CLEAN_UP;
}
g_sockListen = sockListen;
// to accept
{
string strAddr = localAddress(sockListen);
cout << "Listen at " << strAddr << ":" << nPort << endl;
while (!g_bStop) {
SOCKET sockConn = accept(sockListen, NULL, NULL);
if (INVALID_SOCKET == sockConn) {
strError = "accept";
goto FUN_CLEAN_UP;
}
// to start sock handler
thread thrClient(handleClientSocket, sockConn, funError, funMsg);
thrClient.detach();
}
}
FUN_CLEAN_UP:
int nError = WSAGetLastError();
//cout << strError << " fail: " << nError << endl;
if (nullptr != funError) {
funError(strError, nError);
}
return;
}
接收消息
客戶端連接成功後,啓動一個獨立的線程來接收消息;並把Socket句柄保存到map中,方便需要時(如給指定客戶端發消息)獲取。
void handleClientSocket(SOCKET sock,
function<void(const string&, int)> funError,
function<void(int, const string&)> funMsg) {
int nPort = 0;
string strAddr = peerAddress(sock, &nPort);
if (nullptr != funMsg) {
funMsg(nPort, "New connection from " + strAddr + ":" + std::to_string(nPort));
}
const int MaxBuffSize = 255;
char szBuffer[MaxBuffSize + 1] = { 0 };
g_mapConnected[nPort] = sock;
while (!g_bStop) {
int nRet = recv(sock, szBuffer, MaxBuffSize, 0);
if (nRet > 0) {
string strRecv(szBuffer, nRet);
sendMessage(sock, MSG_Ack);
if (nullptr != funMsg) {
funMsg(nPort, strRecv);
}
}
else {
int nError = 0;
string strError = "Connection " + strAddr + ":" + std::to_string(nPort);
if (0 == nRet) {
strError += " gracefully closed";
}
else {
strError += " recv fail";
nError = WSAGetLastError();
}
if (nullptr != funError) {
funError(strError, nError);
}
break;
}
}
g_mapConnected[nPort] = INVALID_SOCKET;
}
發送消息
通過send可以直接發送消息到指定客戶端:
void sendMessage(SOCKET sock, string strMsg) {
strMsg += MSG_Delim;
int nLen = send(sock, strMsg.c_str(), strMsg.length(), 0);
if (SOCKET_ERROR == nLen) {
int nError = WSAGetLastError();
cout << "!!!Send " << strMsg << " fail: " << nError << endl;
}
}