C++利用Socket實(shí)現(xiàn)主機(jī)間的UDP/TCP通信
前言
完整代碼放到github上了:cppSocketDemo
服務(wù)器端的代碼做了跨平臺(POSIX和WINDOWS),基于POSIX平臺(Linux、Mac OS X、PlayStation等)使用sys/socket.h庫,windows平臺使用winsock2.h庫。
客戶端代碼因?yàn)榛径荚趙indows運(yùn)行,所以沒做跨平臺,需要的話你可以參考服務(wù)器端代碼自己做一下。
文中寫的函數(shù)原型均為windows平臺,部分函數(shù)的返回類型或參數(shù)類型在POSIX會有不同。
頭文件
根據(jù)_WIN32標(biāo)志區(qū)分,導(dǎo)入頭文件。
#include<iostream> #include<cstring> #ifdef _WIN32 #include<winsock2.h> #else #include <sys/socket.h> #include <netinet/in.h> #include <sys/types.h> #include <netdb.h> #include <errno.h> #include <fcntl.h> #include <unistd.h> typedef int SOCKET; #endif
因?yàn)镻OSIX平臺的socket庫沒有SOCKET類型,所以我們手動定義一下。
UDP Socket
服務(wù)器端
對于windows,使用socket前需要手動開啟:
#ifdef _WIN32 WSADATA wsd; if(WSAStartup(MAKEWORD(2, 2), &wsd)){ std::cout << "WSAStartup Error" << std::endl; exit(-1); } #endif
WSAStartup第一個(gè)參數(shù)表示使用版本號。該函數(shù)會向第二個(gè)參數(shù)填入被激活的socket庫的信息。
SOCKET函數(shù)
SOCKET socket(int af, int type, int protocol);
參數(shù):
af:socket使用協(xié)議族,如AF_INET表示IPv4, AF_INET6表示IPv6等。
type:指明通過socket發(fā)送和接收分組的形式。如SOCK_STREAM表示有序、可靠的數(shù)據(jù)流分段;SOCK_DGRAM表示離散的報(bào)文;SOCK_RAW表示數(shù)據(jù)頭部可以由應(yīng)用層自定義。
protocol:指明發(fā)送數(shù)據(jù)使用什么協(xié)議。IPPROTO_UDP;IPPROTO_TCP;IPPROTO_IP;0表示根據(jù)socket類型選擇默認(rèn)協(xié)議。
通過socket函數(shù)創(chuàng)建并返回一個(gè)udp類型socket對象:
SOCKET udpSocket = socket(AF_INET, SOCK_DGRAM, 0);
bind函數(shù)
將一個(gè)socket綁定到一個(gè)地址和端口號,使用bind函數(shù):
int bind(SOCKET sock, const sockaddr *address, int address_len);
參數(shù):
sock: 綁定socket
address:注意是指發(fā)送數(shù)據(jù)包的源地址,而不是發(fā)送目的地址
address_len:存儲address的sockaddr結(jié)構(gòu)體大小
bind成功時(shí)返回0,出現(xiàn)錯(cuò)誤時(shí)返回-1
給端口號賦值0,將告訴socket庫找一個(gè)未被使用的端口并綁定
如果一個(gè)進(jìn)程試圖使用一個(gè)未綁定的socket發(fā)送數(shù)據(jù),網(wǎng)絡(luò)庫將自動為這個(gè)socket綁定一個(gè)可用的端口號。
所以對于服務(wù)器來說手動調(diào)用bind綁定是必須的,而對于客戶端來說通常是沒有必要的。
服務(wù)器端socket需要顯式綁定地址和端口,以便客戶端訪問:
sockaddr_in sain; sain.sin_family = AF_INET; //使用IPv4 sain.sin_addr.s_addr = htonl(INADDR_ANY); sain.sin_port = htons(atoi("50002")); ???????if(bind(udpSocket, (sockaddr *)&sain, sizeof(sockaddr)) == -1){ std::cout << "綁定失敗" << std::endl; }
recvfrom函數(shù)
從UDP Socket接收數(shù)據(jù)
int recvfrom(SOCKET s,char *buf,int len,int flags,struct sockaddr *from,int *fromlen);
參數(shù):
s: 查詢數(shù)據(jù)的socket。默認(rèn)情況下,
buf: 接收的數(shù)據(jù)包的緩沖區(qū)。
len: buf可以存儲的最大字節(jié)數(shù)。到達(dá)的數(shù)據(jù)包的剩余字節(jié)將被丟棄。
flags: 同sendto flags。
from: 指向發(fā)送者的地址和端口號的指針,該值由recvfrom函數(shù)寫入(每接收一個(gè)數(shù)據(jù)包寫入一次)。不要手動填寫。
fromlen: from所指向sockaddr的大小
如果recvfrom成功執(zhí)行會返回復(fù)制到buf的字節(jié)數(shù),發(fā)生錯(cuò)誤返回-1。
服務(wù)器通過recvfrom函數(shù)接收客戶端信息:
const size_t BufMaxSize = 1000; char buf[BufMaxSize] = {}; sockaddr fromAddr; #ifndef _WIN32 unsigned #endif int fromAddrLen = sizeof(sockaddr); std::cout << "等待接收..." << std::endl; while(true){ if(recvfrom(udpSocket, buf, BufMaxSize, 0, &fromAddr, &fromAddrLen) != -1){ std::cout << "接收到數(shù)據(jù):" << buf << std::endl; memset(buf, 0, sizeof(buf)); } else{ std::cout << "接收失敗或發(fā)生錯(cuò)誤!" << std::endl; return -1; } }
最后記得做關(guān)閉工作
#ifdef _WIN32 WSACleanup(); closesocket(udpSocket); #else close(udpSocket); #endif
客戶端
和服務(wù)器一樣的先激活:
WSADATA wsd; if(WSAStartup(MAKEWORD(2, 2), &wsd)){ std::cout << "WSAStartup Error" << std::endl; exit(-1); }
創(chuàng)建socket:
SOCKET udpSocket = socket(AF_INET, SOCK_DGRAM, 0);
將目標(biāo)遠(yuǎn)程主機(jī)的IP和端口信息填入sockaddr:
先寫一個(gè)工具函數(shù):
sockaddr GetSockAddr(uint8_t b1, uint8_t b2, uint8_t b3, uint8_t b4, uint16_t inPort){ sockaddr addr; sockaddr_in *addrin = reinterpret_cast<sockaddr_in*>(&addr); addrin->sin_family = AF_INET; addrin->sin_addr.S_un.S_un_b.s_b1 = b1; addrin->sin_addr.S_un.S_un_b.s_b2 = b2; addrin->sin_addr.S_un.S_un_b.s_b3 = b3; addrin->sin_addr.S_un.S_un_b.s_b4 = b4; addrin->sin_port = htons(inPort); return addr; }
sockaddr toAddr = GetSockAddr(127, 0, 0, 1, 50002);
sendTo函數(shù)
從UDP Socket發(fā)送數(shù)據(jù)
int sendto(SOCKET s,const char *buf,int len,int flags,const struct sockaddr *to,int tolen);
參數(shù):
s: 數(shù)據(jù)包應(yīng)該使用的socket,如果沒有綁定,socket庫將自動綁定一個(gè)可用的端口。
buf: 待發(fā)送數(shù)據(jù)的起始地址的指針??梢允侨魏文軌虮晦D(zhuǎn)為char*的數(shù)據(jù)類型。
len: 待發(fā)送數(shù)據(jù)的大小。盡量避免發(fā)送數(shù)據(jù)大于1300字節(jié)的數(shù)據(jù)包,詳見p75。
flags: 對控制發(fā)送的標(biāo)志進(jìn)行按位或運(yùn)算的結(jié)果,該值通常取0即可。
to: 目標(biāo)接收者的sockaddr。注意to的地址族必須和用于創(chuàng)建socket的地址族一致。
tolen:to的sockaddr的大小。對于IPv4,傳入sizeof(sockaddr_in)即可。
sendto操作成功返回等待發(fā)送的數(shù)據(jù)長度(說明成功進(jìn)入發(fā)送隊(duì)列),否則返回-1。
通過senTo函數(shù)發(fā)送數(shù)據(jù):
const size_t BufMaxSize = 1000; char buf[BufMaxSize] = {}; sockaddr toAddr = GetSockAddr(127, 0, 0, 1, 50002); int toAddrLen = sizeof(sockaddr); std::cout << ">>> "; while(true){ if(std::cin >> buf){ sendto(udpSocket, buf, strlen(buf), 0, &toAddr, toAddrLen); std::cout << "已發(fā)送!" <<std::endl; std::cout << ">>> "; memset(buf, 0, sizeof(buf)); } }
注意,這樣發(fā)送給linux服務(wù)器帶中文的字符串的話,可能出現(xiàn)亂碼,因?yàn)閘inux通常為UTF-8編碼,而windows通常為gb2312編碼,所以我們可以在客戶端實(shí)現(xiàn)兩個(gè)編碼轉(zhuǎn)換函數(shù),并在恰當(dāng)時(shí)機(jī)轉(zhuǎn)換:
//UTF-8轉(zhuǎn)GB2312 char* U2G(const char* utf8){ int len = MultiByteToWideChar(CP_UTF8, 0, utf8, -1, NULL, 0); wchar_t* wstr = new wchar_t[len+1]; memset(wstr, 0, len+1); MultiByteToWideChar(CP_UTF8, 0, utf8, -1, wstr, len); len = WideCharToMultiByte(CP_ACP, 0, wstr, -1, NULL, 0, NULL, NULL); char* str = new char[len+1]; memset(str, 0, len+1); WideCharToMultiByte(CP_ACP, 0, wstr, -1, str, len, NULL, NULL); if(wstr) delete[] wstr; return str; } //GB2312轉(zhuǎn)UTF-8 char* G2U(const char* gb2312){ int len = MultiByteToWideChar(CP_ACP, 0, gb2312, -1, NULL, 0); wchar_t* wstr = new wchar_t[len+1]; memset(wstr, 0, len+1); MultiByteToWideChar(CP_ACP, 0, gb2312, -1, wstr, len); len = WideCharToMultiByte(CP_UTF8, 0, wstr, -1, NULL, 0, NULL, NULL); char* str = new char[len+1]; memset(str, 0, len+1); WideCharToMultiByte(CP_UTF8, 0, wstr, -1, str, len, NULL, NULL); if(wstr) delete[] wstr; return str; }
發(fā)送改為:
char *buf_UTF8 = G2U(buf); sendto(udpSocket, buf_UTF8, strlen(buf_UTF8), 0, &toAddr, toAddrLen);
最后同樣的關(guān)閉操作:
closesocket(udpSocket); WSACleanup();
測試
注意,如果把服務(wù)器代碼放到windows下執(zhí)行,記得把客戶端的轉(zhuǎn)編碼代碼改下。
注意,編譯使用C++11以上編譯,鏈接時(shí)加入庫:
-lwsock32
將udpServer.cpp放到服務(wù)器上,服務(wù)器防火墻記得放行目標(biāo)端口或暫時(shí)關(guān)閉防火墻。
udpClient.cpp在本地(windows)。
udpClient中的目標(biāo)遠(yuǎn)程主機(jī)地址改為服務(wù)器ip地址,編譯運(yùn)行:
服務(wù)器:
客戶端:
TCP Socket(單客戶端連接)
服務(wù)器
同樣先激活winsock:
#ifdef _WIN32 WSADATA wsd; if(WSAStartup(MAKEWORD(2, 2), &wsd)){ std::cout << "WSAStartup Error" << std::endl; return -1; } #endif
創(chuàng)建tcp類型socket:
SOCKET tcpsocket = socket(AF_INET, SOCK_STREAM, 0);
綁定本機(jī)地址和指定端口號:
sockaddr_in sain; sain.sin_family = AF_INET; sain.sin_addr.s_addr = htonl(INADDR_ANY); sain.sin_port = htons(atoi("50002")); if(bind(tcpsocket, (sockaddr*)&sain, sizeof(sockaddr)) == -1){ std::cout << "bind Error" << std::endl; return -1; }
listen函數(shù)
用listen函數(shù)啟動監(jiān)聽,等待客戶端的連接:
int listen(SOCKET sock, int backlog);
backlog指隊(duì)列允許傳入的最大連接數(shù),超過最大值的連接都將被丟棄??梢允褂肧OMAXCONN表示默認(rèn)的backlog值。
函數(shù)執(zhí)行成功返回0,失敗返回-1。
使用listen函數(shù)開啟監(jiān)聽:
listen(tcpsocket, 10);
主機(jī)針對每個(gè)保持的TCP連接,都需要一個(gè)獨(dú)立的socket存儲連接狀態(tài)。
這里先做只能連接單個(gè)客戶端,創(chuàng)建連接客戶端的socket:
sockaddr clientAddr; // #ifndef _WIN32 unsigned #endif int clientAddrLen = sizeof(sockaddr);
accept函數(shù)
接受傳入的連接并繼續(xù)TCP握手過程,調(diào)用accept函數(shù):
SOCKET accept(SOCKET sock, sockaddr* addr, int* addrlen);
參數(shù):
sock: 接收傳入連接的監(jiān)聽socket
addr: 將被寫入請求連接的遠(yuǎn)程主機(jī)地址。同樣不要手動填寫
addrlen: 指向addr緩沖區(qū)大小的指針。當(dāng)真正寫入地址之后,accept會更新該值。
如果accept執(zhí)行成功,將創(chuàng)建并返回一個(gè)可以與遠(yuǎn)程主機(jī)通信的新socket。
接受傳入的連接并繼續(xù)TCP握手過程:
SOCKET linkSocket = accept(tcpsocket, &clientAddr, &clientAddrLen);
recv函數(shù)
調(diào)用recv函數(shù)從一個(gè)連接的TCP socket接收數(shù)據(jù):
int recv(SOCKET s,char *buf,int len,int flags);
參數(shù):
s: 待接收數(shù)據(jù)的socket
buf: 數(shù)據(jù)接收緩沖區(qū)。
len: 拷貝到buf中的數(shù)據(jù)的最大數(shù)量。
flags: 標(biāo)志位,大多數(shù)情況填0。
調(diào)用成功返回接收的數(shù)據(jù)大小。如果返回0,說明連接的另一端發(fā)送了一個(gè)FIN數(shù)據(jù)包,承諾沒有更多需要發(fā)送的數(shù)據(jù)。
如果發(fā)生錯(cuò)誤,返回-1
默認(rèn)情況下,如果socket的接收緩沖區(qū)中沒有數(shù)據(jù),recv函數(shù)將阻塞調(diào)用線程,直到數(shù)據(jù)流中的下一組數(shù)據(jù)到達(dá)或超時(shí)。
send函數(shù)
通過連接的socket使用send函數(shù)發(fā)送數(shù)據(jù):
因?yàn)檫B接的socket存儲了遠(yuǎn)程主機(jī)地址信息,所以不需要傳入地址參數(shù):
int send(SOCKET s,const char *buf,int len,int flags);
參數(shù):
s: 用于發(fā)送數(shù)據(jù)的socket
buf: 寫入緩沖區(qū)。注意:和UDP不同,是將數(shù)據(jù)放到socket的輸出緩沖區(qū)中,由socket庫來決定在將來某一時(shí)刻發(fā)出。
len: 傳輸?shù)淖止?jié)數(shù)量。注意:與UDP不同,不需要保持這個(gè)值低于鏈路層的MTU。
flags:標(biāo)志位,大多數(shù)情況下填0即可。
send調(diào)用成功返回發(fā)送數(shù)據(jù)的大小,如果發(fā)送錯(cuò)誤返回-1.
默認(rèn)情況下該函數(shù)會阻塞線程,直到調(diào)用超時(shí)或發(fā)送了足夠的數(shù)據(jù)。
非0的返回值不代表成功發(fā)送出去了,只說明數(shù)據(jù)被存入隊(duì)列中等待發(fā)送。
使用recv函數(shù)和send函數(shù)接收和響應(yīng)客戶端信息:
const size_t BufMaxLen = 1000; char buf[BufMaxLen] = {}; char sendBuf[BufMaxLen] = "服務(wù)器成功接收!"; while(true){ int ret = recv(linkSocket, buf, BufMaxLen, 0); if(ret > 0){ std::cout << "從客戶端接收到數(shù)據(jù):" << buf << std::endl; memset(buf, 0, BufMaxLen); send(linkSocket, sendBuf, strlen(sendBuf), 0); } else if(ret == 0){ std::cout << "客戶端停止發(fā)送數(shù)據(jù),準(zhǔn)備關(guān)閉連接..." << std::endl; break; } else{ std::cout << "recv發(fā)生錯(cuò)誤!" << std::endl; } }
最后關(guān)閉:
#ifdef _WIN32 closesocket(tcpsocket); closesocket(linkSocket); WSACleanup(); #else close(tcpsocket); close(linkSocket); #endif std::cout << "已關(guān)閉服務(wù)器Socket..." << std::endl;
客戶端
客戶端沒有新函數(shù),直接看代碼吧!
TCPSocketClient.cpp:
#include<iostream> #include<winsock2.h> #include<windows.h> #include<memory> #include<cstring> using namespace std; //UTF-8轉(zhuǎn)GB2312 char* U2G(const char* utf8){ int len = MultiByteToWideChar(CP_UTF8, 0, utf8, -1, NULL, 0); wchar_t* wstr = new wchar_t[len+1]; memset(wstr, 0, len+1); MultiByteToWideChar(CP_UTF8, 0, utf8, -1, wstr, len); len = WideCharToMultiByte(CP_ACP, 0, wstr, -1, NULL, 0, NULL, NULL); char* str = new char[len+1]; memset(str, 0, len+1); WideCharToMultiByte(CP_ACP, 0, wstr, -1, str, len, NULL, NULL); if(wstr) delete[] wstr; return str; } //GB2312轉(zhuǎn)UTF-8 char* G2U(const char* gb2312){ int len = MultiByteToWideChar(CP_ACP, 0, gb2312, -1, NULL, 0); wchar_t* wstr = new wchar_t[len+1]; memset(wstr, 0, len+1); MultiByteToWideChar(CP_ACP, 0, gb2312, -1, wstr, len); len = WideCharToMultiByte(CP_UTF8, 0, wstr, -1, NULL, 0, NULL, NULL); char* str = new char[len+1]; memset(str, 0, len+1); WideCharToMultiByte(CP_UTF8, 0, wstr, -1, str, len, NULL, NULL); if(wstr) delete[] wstr; return str; } sockaddr GetSockAddr(uint8_t b1, uint8_t b2, uint8_t b3, uint8_t b4, uint16_t inPort){ sockaddr addr; sockaddr_in *addrin = reinterpret_cast<sockaddr_in*>(&addr); addrin->sin_family = AF_INET; addrin->sin_addr.S_un.S_un_b.s_b1 = b1; addrin->sin_addr.S_un.S_un_b.s_b2 = b2; addrin->sin_addr.S_un.S_un_b.s_b3 = b3; addrin->sin_addr.S_un.S_un_b.s_b4 = b4; addrin->sin_port = htons(inPort); return addr; } int main(){ WSADATA wsd; if(WSAStartup(MAKEWORD(2, 2), &wsd)){ std::cout << "WSAStartup Error" << std::endl; return -1; } SOCKET tcpSocket = socket(AF_INET, SOCK_STREAM, 0); sockaddr serverAddr = GetSockAddr(127, 0, 0, 1, 50002); int serverAddrLen = sizeof(sockaddr); if(connect(tcpSocket, &serverAddr, serverAddrLen) == -1){ std::cout << "connect Error!" << std::endl; return -1; } std::cout << "已成功連接到服務(wù)器" << std::endl; //客戶端的socket就是用于連接的socket const int BufMaxLen = 1000; char sendBuf[BufMaxLen] = {}; char buf[BufMaxLen] = {}; std::cout << ">>> "; while(true){ if(std::cin >> sendBuf){ if(strcmp(sendBuf, "exit") == 0){ std::cout << "正在關(guān)閉連接..." << std::endl; break; } char *sendBuf_UTF8 = G2U(sendBuf); send(tcpSocket, sendBuf_UTF8, strlen(sendBuf_UTF8), 0); memset(sendBuf, 0, BufMaxLen); int ret = recv(tcpSocket, buf, BufMaxLen, 0); if(ret > 0){ std::cout << "從服務(wù)器接收到回應(yīng):" << U2G(buf) << std::endl; memset(buf, 0, BufMaxLen); } std::cout << ">>> "; } } shutdown(tcpSocket, SB_BOTH); closesocket(tcpSocket); WSACleanup(); std::cout << "已關(guān)閉客戶端Socket..." << std::endl; return 0; }
測試
注意,如果把服務(wù)器代碼放到windows下執(zhí)行,記得把客戶端的轉(zhuǎn)編碼代碼改下。
測試方式同上面UDP。
客戶端:
服務(wù)器:
TCP Socket(多客戶端連接)
服務(wù)端
使用多線程,每響應(yīng)一個(gè)客戶端連接為它創(chuàng)建一個(gè)線程。
多線程頭文件:
#include<thread>
將之前的響應(yīng)代碼搬到函數(shù)中作為線程入口:
void linkClientThread(SOCKET linkSocket, unsigned int linkId){ printf("客戶端(id:%d) 已連接!\n", linkId); const size_t BufMaxLen = 1000; char buf[BufMaxLen] = {}; char sendBuf[BufMaxLen] = "服務(wù)器成功接收!"; while(true){ int ret = recv(linkSocket, buf, BufMaxLen, 0); if(ret > 0){ printf("從客戶端(id:%d)接收到數(shù)據(jù):%s\n", linkId, buf); memset(buf, 0, BufMaxLen); send(linkSocket, sendBuf, strlen(sendBuf), 0); } else if(ret == 0){ printf("客戶端(id:%d)停止發(fā)送數(shù)據(jù),關(guān)閉連接...\n", linkId); break; } else{ printf("recv發(fā)生錯(cuò)誤!\n"); break; } } #ifdef _WIN32 closesocket(linkSocket); #else close(linkSocket); #endif }
當(dāng)接收到連接請求,為它單獨(dú)創(chuàng)建一個(gè)線程服務(wù):
while(true){ SOCKET linkSocket = accept(tcpsocket, &clientAddr, &clientAddrLen); std::thread linkThread(linkClientThread, linkSocket, ++linkId); linkThread.detach(); }
客戶端
客戶端直接繼續(xù)使用之前的tcpClient.cpp即可,沒有區(qū)別。
測試
同樣的注意,如果把服務(wù)器代碼放到windows下執(zhí)行,記得把客戶端的轉(zhuǎn)編碼代碼改下。
服務(wù)器還是使用linux系統(tǒng)的,所有客戶端在本地的windows執(zhí)行:
注意:server代碼在linux編譯時(shí)要加入-lpthread.h選項(xiàng):
g++ -g tcpServer_multiConnection.cpp -o tcpServer_multiConnection -std=c++11 -lpthread
客戶端1:
客戶端2:
服務(wù)器:
以上就是C++利用Socket實(shí)現(xiàn)主機(jī)間的UDP/TCP通信的詳細(xì)內(nèi)容,更多關(guān)于C++ Socket實(shí)現(xiàn)UDP/TCP通信的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
C語言實(shí)現(xiàn)數(shù)據(jù)結(jié)構(gòu)迷宮實(shí)驗(yàn)
這篇文章主要為大家詳細(xì)介紹了C語言實(shí)現(xiàn)數(shù)據(jù)結(jié)構(gòu)迷宮實(shí)驗(yàn),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-03-03基于QT設(shè)計(jì)一個(gè)春聯(lián)自動生成器
春節(jié)是中國最隆重的傳統(tǒng)節(jié)日,一到過年家家戶戶肯定是要貼春聯(lián);在春節(jié)前夕,會用大紅紙張,加上濃墨書寫祝福詞語。本文將利用Qt框架設(shè)計(jì)一個(gè)春聯(lián)自動生成器,需要的可以參考一下2022-01-01C語言:利用指針編寫程序,用梯形法計(jì)算給定的定積分實(shí)例
今天小編就為大家分享一篇C語言:利用指針編寫程序,用梯形法計(jì)算給定的定積分實(shí)例,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-12-12C++實(shí)現(xiàn)帶頭雙向循環(huán)鏈表的示例詳解
這篇文章主要介紹了如何利用C++實(shí)現(xiàn)帶頭雙向循環(huán)鏈表,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-12-12