C++利用Socket實現(xiàn)主機間的UDP/TCP通信
前言
完整代碼放到github上了:cppSocketDemo
服務(wù)器端的代碼做了跨平臺(POSIX和WINDOWS),基于POSIX平臺(Linux、Mac OS X、PlayStation等)使用sys/socket.h庫,windows平臺使用winsock2.h庫。
客戶端代碼因為基本都在windows運行,所以沒做跨平臺,需要的話你可以參考服務(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
因為POSIX平臺的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第一個參數(shù)表示使用版本號。該函數(shù)會向第二個參數(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表示離散的報文;SOCK_RAW表示數(shù)據(jù)頭部可以由應(yīng)用層自定義。
protocol:指明發(fā)送數(shù)據(jù)使用什么協(xié)議。IPPROTO_UDP;IPPROTO_TCP;IPPROTO_IP;0表示根據(jù)socket類型選擇默認協(xié)議。
通過socket函數(shù)創(chuàng)建并返回一個udp類型socket對象:
SOCKET udpSocket = socket(AF_INET, SOCK_DGRAM, 0);
bind函數(shù)
將一個socket綁定到一個地址和端口號,使用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成功時返回0,出現(xiàn)錯誤時返回-1
給端口號賦值0,將告訴socket庫找一個未被使用的端口并綁定
如果一個進程試圖使用一個未綁定的socket發(fā)送數(shù)據(jù),網(wǎng)絡(luò)庫將自動為這個socket綁定一個可用的端口號。
所以對于服務(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。默認情況下,
buf: 接收的數(shù)據(jù)包的緩沖區(qū)。
len: buf可以存儲的最大字節(jié)數(shù)。到達的數(shù)據(jù)包的剩余字節(jié)將被丟棄。
flags: 同sendto flags。
from: 指向發(fā)送者的地址和端口號的指針,該值由recvfrom函數(shù)寫入(每接收一個數(shù)據(jù)包寫入一次)。不要手動填寫。
fromlen: from所指向sockaddr的大小
如果recvfrom成功執(zhí)行會返回復(fù)制到buf的字節(jié)數(shù),發(fā)生錯誤返回-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ā)生錯誤!" << 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)遠程主機的IP和端口信息填入sockaddr:
先寫一個工具函數(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庫將自動綁定一個可用的端口。
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)志進行按位或運算的結(jié)果,該值通常取0即可。
to: 目標(biāo)接收者的sockaddr。注意to的地址族必須和用于創(chuàng)建socket的地址族一致。
tolen:to的sockaddr的大小。對于IPv4,傳入sizeof(sockaddr_in)即可。
sendto操作成功返回等待發(fā)送的數(shù)據(jù)長度(說明成功進入發(fā)送隊列),否則返回-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)亂碼,因為linux通常為UTF-8編碼,而windows通常為gb2312編碼,所以我們可以在客戶端實現(xiàn)兩個編碼轉(zhuǎn)換函數(shù),并在恰當(dāng)時機轉(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以上編譯,鏈接時加入庫:
-lwsock32

將udpServer.cpp放到服務(wù)器上,服務(wù)器防火墻記得放行目標(biāo)端口或暫時關(guān)閉防火墻。
udpClient.cpp在本地(windows)。
udpClient中的目標(biāo)遠程主機地址改為服務(wù)器ip地址,編譯運行:
服務(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);
綁定本機地址和指定端口號:
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指隊列允許傳入的最大連接數(shù),超過最大值的連接都將被丟棄。可以使用SOMAXCONN表示默認的backlog值。
函數(shù)執(zhí)行成功返回0,失敗返回-1。
使用listen函數(shù)開啟監(jiān)聽:
listen(tcpsocket, 10);
主機針對每個保持的TCP連接,都需要一個獨立的socket存儲連接狀態(tài)。
這里先做只能連接單個客戶端,創(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: 將被寫入請求連接的遠程主機地址。同樣不要手動填寫
addrlen: 指向addr緩沖區(qū)大小的指針。當(dāng)真正寫入地址之后,accept會更新該值。
如果accept執(zhí)行成功,將創(chuàng)建并返回一個可以與遠程主機通信的新socket。
接受傳入的連接并繼續(xù)TCP握手過程:
SOCKET linkSocket = accept(tcpsocket, &clientAddr, &clientAddrLen);
recv函數(shù)
調(diào)用recv函數(shù)從一個連接的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ā)送了一個FIN數(shù)據(jù)包,承諾沒有更多需要發(fā)送的數(shù)據(jù)。
如果發(fā)生錯誤,返回-1
默認情況下,如果socket的接收緩沖區(qū)中沒有數(shù)據(jù),recv函數(shù)將阻塞調(diào)用線程,直到數(shù)據(jù)流中的下一組數(shù)據(jù)到達或超時。
send函數(shù)
通過連接的socket使用send函數(shù)發(fā)送數(shù)據(jù):
因為連接的socket存儲了遠程主機地址信息,所以不需要傳入地址參數(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庫來決定在將來某一時刻發(fā)出。
len: 傳輸?shù)淖止?jié)數(shù)量。注意:與UDP不同,不需要保持這個值低于鏈路層的MTU。
flags:標(biāo)志位,大多數(shù)情況下填0即可。
send調(diào)用成功返回發(fā)送數(shù)據(jù)的大小,如果發(fā)送錯誤返回-1.
默認情況下該函數(shù)會阻塞線程,直到調(diào)用超時或發(fā)送了足夠的數(shù)據(jù)。
非0的返回值不代表成功發(fā)送出去了,只說明數(shù)據(jù)被存入隊列中等待發(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ā)生錯誤!" << 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)一個客戶端連接為它創(chuàng)建一個線程。
多線程頭文件:
#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ā)生錯誤!\n");
break;
}
}
#ifdef _WIN32
closesocket(linkSocket);
#else
close(linkSocket);
#endif
}當(dāng)接收到連接請求,為它單獨創(chuàng)建一個線程服務(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編譯時要加入-lpthread.h選項:
g++ -g tcpServer_multiConnection.cpp -o tcpServer_multiConnection -std=c++11 -lpthread
客戶端1:

客戶端2:

服務(wù)器:

以上就是C++利用Socket實現(xiàn)主機間的UDP/TCP通信的詳細內(nèi)容,更多關(guān)于C++ Socket實現(xiàn)UDP/TCP通信的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
visual studio code 編譯運行html css js文件的教程
這篇文章主要介紹了visual studio code 如何編譯運行html css js文件,本文通過圖文實例代碼給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-03-03
C++?opencv利用grabCut算法實現(xiàn)摳圖示例
這篇文章主要為大家介紹了C++?opencv利用grabCut算法實現(xiàn)摳圖的代碼示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-05-05
C++如何調(diào)用opencv完成運動目標(biāo)捕捉詳解
OpenCV作為機器視覺開源庫,使用起來非常不錯,這篇文章主要給大家介紹了關(guān)于C++如何調(diào)用opencv完成運動目標(biāo)捕捉的相關(guān)資料,文中通過實例代碼介紹的非常詳細,需要的朋友可以參考下2022-05-05
C++實現(xiàn)LeetCode(94.二叉樹的中序遍歷)
這篇文章主要介紹了C++實現(xiàn)LeetCode(94.二叉樹的中序遍歷),本篇文章通過簡要的案例,講解了該項技術(shù)的了解與使用,以下就是詳細內(nèi)容,需要的朋友可以參考下2021-07-07

