Winsock是Windows操作系統(tǒng)上的套接字API,用于在網(wǎng)絡上進行數(shù)據(jù)通信。套接字通信是一種允許應用程序在計算機網(wǎng)絡上進行實時數(shù)據(jù)交換的技術(shù)。通過使用Windows提供的API,應用程序可以創(chuàng)建一個套接字來進行數(shù)據(jù)通信。這個套接字可以綁定到一個端口,以允許其他應用程序連接它。另外,Winsock可以使用TCP/IP、UDP等協(xié)議來完成不同類型的數(shù)據(jù)傳輸任務。在網(wǎng)絡應用程序開發(fā)中,套接字通信可以幫助應用程序開發(fā)者實現(xiàn)客戶端/服務端模型,并實現(xiàn)數(shù)據(jù)的可靠傳輸。
一般套接字通信需要經(jīng)歷,創(chuàng)建套接字(Socket),綁定(Bind),監(jiān)聽(Listen),接受(Accept),連接(Connect),發(fā)送數(shù)據(jù)(Send),接收數(shù)據(jù)(Receive),關(guān)閉(Close)等幾個關(guān)鍵步驟,當讀者需要使用網(wǎng)絡通信時需引入winsock2.h
頭文件,并通過#pragma comment(lib,"ws2_32.lib")
包含對應庫,需要注意的是該頭文件與windows.h
頭沖突,如果兩者同時存在則會出現(xiàn)編譯不通過的情況;
14.1.1 服務端通信
(1)WSAStartup(MAKEWORD(2, 0), &WSAData)
當讀者需要使用套接字編程時,不論是服務端還是客戶端都需要調(diào)用WSAStartup
初始化套接字庫,該函數(shù)接受兩個參數(shù)傳遞,第一個參數(shù)一般默認會傳遞MAKEWORD(2, 0)
它是一個宏,用于將兩個8位的字節(jié)合并成一個16位的字,在MAKEWORD(2, 0)
中,括號內(nèi)的數(shù)字分別代表高位字節(jié)(2)
和低位字節(jié)(0)
,宏會將它們合并成一個16位的無符號short
整型數(shù)據(jù),即0000001000000000
(二進制),表示Winsock
的版本號為2.0
。第二個參數(shù)WSADATA
結(jié)構(gòu)體,用于Winsock
初始化時存儲相關(guān)的信息,一般會在全局WSADATA WSAData;
直接定義得到。
#include <iostream>
#include <winsock2.h>
#include <WS2tcpip.h>
#pragma comment(lib,"ws2_32.lib")
// 定義結(jié)構(gòu)體
WSADATA WSAData;
// 啟動winsock中的WSAStartup()函數(shù)對Winsock DLL進行初始化
if (WSAStartup(MAKEWORD(2, 0), &WSAData) == SOCKET_ERROR)
{
std::cout << "WSA動態(tài)庫初始化失敗" << std::endl;
return 0;
}
(2)socket(AF_INET, SOCK_STREAM, 0)
通信的第二步則是調(diào)用Socket()
函數(shù),該函數(shù)是用于創(chuàng)建一個套接字的系統(tǒng)調(diào)用。在該函數(shù)中,給定三個參數(shù),分別為地址族(Address Family)
、套接字類型(Socket Type)
和協(xié)議(Protocol)
,套接字在初始化并完成時會返回一個SOCKET
類型的文件描述符句柄,此處我們將該句柄存儲至server_socket
變量內(nèi)。AF_INET
用于指定套接字地址族為IPv4
類型,SOCK_STREAM
則用于指定該套接字的類型為流式套接字,用于面向連接的可靠數(shù)據(jù)傳輸(TCP協(xié)議)。
// 服務進程創(chuàng)建套接字句柄(用于監(jiān)聽)
SOCKET server_socket;
// 調(diào)用socket()函數(shù)創(chuàng)建一個流套接字,參數(shù)(網(wǎng)絡地址類型,套接字類型,網(wǎng)絡協(xié)議)
if ((server_socket = socket(AF_INET, SOCK_STREAM, 0)) == ERROR)
{
std::cout << "Socket 創(chuàng)建失敗" << std::endl;
WSACleanup();
return 0;
}
(3)bind(server_socket, (LPSOCKADDR)&ServerAddr, sizeof(ServerAddr))
套接字編程的第三步則是綁定,套接字的綁定需要調(diào)用bind()
函數(shù)實現(xiàn),該函數(shù)接受三個參數(shù)傳遞,第一個參數(shù)是socket()
中創(chuàng)建的套接字文件描述符句柄,該參數(shù)用于指定針對哪一個套接字進行操作,第二個參數(shù)則是sockaddr_in
類型的結(jié)構(gòu)體,該結(jié)構(gòu)體內(nèi)用于指定需要綁定套接字的具體類型參數(shù)等信息,在如下代碼中我們通過ServerAddr.sin_family = AF_INET;
將套接字類型設(shè)置為了互聯(lián)網(wǎng)域模式,通過ServerAddr.sin_port = htons(9999);
指定了需要綁定的端口號,而ServerAddr.sin_addr.s_addr = inet_addr("0.0.0.0");
則用于指定了要綁定本機的那個網(wǎng)口,一般而言如果讀者需要在本機使用此處可填入127.0.0.1
而如果偵聽任意一個網(wǎng)口則可使用0.0.0.0
,第三個參數(shù)則是傳入結(jié)構(gòu)體的長度,此處通過sizeof(ServerAddr)
方法得到,最終將結(jié)構(gòu)體ServerAddr
直接填入綁定函數(shù)即可實現(xiàn)對網(wǎng)絡套接字的綁定。
// 結(jié)構(gòu)sockaddr_in用來標識TCP/IP協(xié)議下的地址,可強制轉(zhuǎn)換為sockaddr結(jié)構(gòu)
struct sockaddr_in ServerAddr;
// 字段sin_family必須設(shè)為AF_INET,表示該Socket處于Internet域
ServerAddr.sin_family = AF_INET;
// 字段sin_port用于指定服務端口,注意避免沖突
ServerAddr.sin_port = htons(9999);
// 字段sin_addr用于把一個IP地址保存為一個4字節(jié),無符號長整型,根據(jù)不同用法還可表示本地或遠程IP地址
// 該字段可以直接使用INADDR_ANY代表偵聽所有地址,也可指定地址
ServerAddr.sin_addr.s_addr = inet_addr("0.0.0.0");
// 調(diào)用bind()函數(shù)將本地地址綁定到所創(chuàng)建的套接字上,以在網(wǎng)絡上標識該套接字
if (bind(server_socket, (LPSOCKADDR)&ServerAddr, sizeof(ServerAddr)) == SOCKET_ERROR)
{
std::cout << "綁定套接字失敗" << std::endl;
closesocket(server_socket);
WSACleanup();
return 0;
}
(4)listen(server_socket, 10)
當套接字被綁定后,接下來則是偵聽套接字,通過調(diào)用listen()
函數(shù)將套接字置入監(jiān)聽模式并準備接受連接請求,該函數(shù)需要傳入兩個參數(shù),參數(shù)1為套接字套接字句柄,參數(shù)二為偵聽套接字最大連接數(shù),如果進入偵聽狀態(tài)則說明該套接字是等待連接狀態(tài),一旦服務器接受了連接,它可以使用返回的套接字對象與發(fā)起連接的客戶端進行通信。
// 將 ServerAddr.sin_addr 網(wǎng)絡字節(jié)序,轉(zhuǎn)為本機偵聽IP地址
char local_address[20];
inet_ntop(AF_INET, &ServerAddr.sin_addr, local_address, 16);
std::cout << "偵聽本地地址: " << local_address << " 偵聽本地端口: " << ntohs(ServerAddr.sin_port) << std::endl;
// 參數(shù)(已捆綁未連接的套接字描述字,正在等待連接的最大隊列長度)
if (listen(server_socket, 10) == SOCKET_ERROR)
{
std::cout << "偵聽套接字失敗" << std::endl;
closesocket(server_socket);
WSACleanup();
return 0;
}
(5)accept(server_socket, (LPSOCKADDR)0, (int*)0)
當一個套接字進入偵聽狀態(tài)后則下一步是需要等待有客戶端連接到本端,當服務器通過調(diào)用listen()
函數(shù)開始監(jiān)聽連接請求時,客戶端可以通過使用connect()
函數(shù)嘗試與服務器建立連接。一旦客戶端發(fā)送連接請求,服務器將收到通知。然后服務器可以使用accept()
函數(shù)接受連接請求并創(chuàng)建一個新的套接字對象,該對象可以用于與客戶端進行通信。
accept() 函數(shù)通常在一個循環(huán)中使用,以便服務器可以在等待新連接時繼續(xù)處理已連接的客戶端。每次調(diào)用accept()
函數(shù)時,如果有連接請求,則函數(shù)將阻塞直到一個連接請求被接受。一旦連接請求被接受,函數(shù)將返回一個新的套接字對象和客戶端的地址信息。
在接受連接請求并創(chuàng)建新的套接字對象之后,服務器可以使用該對象與客戶端進行通信。同時,服務器可以使用原始的server_socket
套接字對象來等待更多的連接請求,以便能夠接受更多的客戶端連接。
如下的代碼中當accept()
接收到等待消息時,則會將該句柄保存至message_socket
變量內(nèi),此時用戶只需要向該指針中發(fā)送recv()
或接收send()
數(shù)據(jù)即可,此時套接字通信即可正式被建立起來。
// 數(shù)據(jù)接收緩沖區(qū)
SOCKET message_socket;
char buf[8192] = {0};
while (1)
{
// 進入監(jiān)聽狀態(tài)后,調(diào)用accept()函數(shù)接收客戶端的連接請求,并把連接傳給msgsock套接字
// 原sock套接字繼續(xù)監(jiān)聽其他客戶機連接請求
if ((message_socket = accept(server_socket, (LPSOCKADDR)0, (int*)0)) == INVALID_SOCKET)
{
continue;
}
// 初始化數(shù)據(jù)接收緩沖區(qū)
memset(buf, 0, sizeof(buf));
// 接收客戶端發(fā)送過來的數(shù)據(jù)
bool ref = recv(message_socket, buf, 8192, 0);
if (ref != 0)
{
std::cout << "接收數(shù)據(jù): " << buf << std::endl;
}
// 關(guān)閉子套接字
closesocket(message_socket);
}
至此我們的服務端將被運行起來,需要注意的是服務端程序如果需要結(jié)束本次會話則需要手動調(diào)用closesocket(server_socket);
關(guān)閉一個套接字句柄,當整個進程執(zhí)行結(jié)束后讀者還需要調(diào)用WSACleanup()
終止對Winsock DLL的使用,并釋放資源。
14.1.2 客戶端通信
對于客戶端通信而言其流程與服務端通信基本保持一致,該流程分別是,創(chuàng)建套接字,連接到服務器,建立連接,發(fā)送數(shù)據(jù),關(guān)閉連接,對于初始化部分客戶端通信與服務端沒有任何區(qū)別,唯一的區(qū)別在于對于服務端而言一般是使用listen()
函數(shù)偵聽套接字,而對于客戶端而言則是使用connect()
函數(shù)連接到服務端,一旦連接建立成功,客戶端可以通過向服務器發(fā)送數(shù)據(jù)來與服務器進行通信。
在調(diào)用connect(socket_addr)
時,需要傳遞一個參數(shù)sockaddr
。sockaddr 是一個結(jié)構(gòu)體,包含了客戶端與服務器的地址信息,包括其IP
地址和端口號。在C/C++
中,sockaddr 結(jié)構(gòu)體通常被定義為sockaddr_in
結(jié)構(gòu)體,包含了IP
地址和端口號等信息。如果連接建立成功,connect() 函數(shù)將返回 0。如果連接失敗,則會返回一個錯誤代碼,其中最常見的錯誤是連接超時或目標主機拒絕連接。
一旦連接建立成功,客戶端可以使用新創(chuàng)建的套接字對象向服務器發(fā)送數(shù)據(jù),并使用recv()
函數(shù)從服務器接收數(shù)據(jù)。一般來說,在與服務器進行通信之前,客戶端套接字需要使用bind()
函數(shù)指定一個本地地址和端口,以確保數(shù)據(jù)可以正確地傳輸。
int main(int argc, char* argv[])
{
char buf[8192] = { 0 };
while (1)
{
std::cout << "發(fā)送數(shù)據(jù): ";
int inputLen = 0;
memset(buf, 0, sizeof(buf));
// 輸入以回車鍵為結(jié)束標識
while ((buf[inputLen++] = getchar()) != '\n'){ ; }
// 初始化
WSADATA WSAData;
if (WSAStartup(MAKEWORD(2, 0), &WSAData) == SOCKET_ERROR)
{
continue;
}
// 創(chuàng)建套接字
SOCKET client_socket;
if ((client_socket = socket(AF_INET, SOCK_STREAM, 0)) == SOCKET_ERROR)
{
WSACleanup();
continue;
}
// 填充通信結(jié)構(gòu)體
struct sockaddr_in ClientAddr;
ClientAddr.sin_family = AF_INET;
ClientAddr.sin_port = htons(9999);
ClientAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
// 連接到服務端
if (connect(client_socket, (LPSOCKADDR)&ClientAddr, sizeof(ClientAddr)) == SOCKET_ERROR)
{
closesocket(client_socket);
WSACleanup();
continue;
}
// 向服務端發(fā)送數(shù)據(jù)
send(client_socket, buf, 8192, 0);
// 關(guān)閉套接字
closesocket(client_socket);
WSACleanup();
}
return 0;
}
讀者可自行運行上述程序,啟動服務端與客戶端,并發(fā)送測試數(shù)據(jù)觀察變化,當發(fā)送數(shù)據(jù)后讀者應該能看到如下圖所示的提示信息;文章來源:http://www.zghlxwxcb.cn/news/detail-710596.html
文章來源地址http://www.zghlxwxcb.cn/news/detail-710596.html
到了這里,關(guān)于14.1 Socket 套接字編程入門的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!