本文地址:https://www.cnblogs.com/oberon-zjt0806/p/14814144.html
WinSock 2 MSDN文件:https://docs.microsoft.com/en-us/windows/win32/winsock/windows-sockets-start-page-2
一個很好的範例(如果我的程式碼有問題,可供參考這個):https://github.com/gaohaoning/cpp_socket
什麼是WinSock?
這還不簡單,當然是Windows的襪子啦
WinSock(全稱Windows Sockets)是巨硬微軟提供的用於Windows平臺的C++網路連線庫。
簡單來說我們知道在Java中,我們可以通過JDK提供的java.net
庫來實現建立套接字的TCP/UDP傳輸:
public class Client
{
private String svrip = "";
private int port;
public AnotherClient(String serverIP,int port) {
svrip = serverIP;
this.port = port;
}
public void Start() {
Socket cltsock = null; // Client's socket
OutputStream ostrm = null; // O-stream for sending to server
BufferedWriter bwriter = null;
InputStream istrm = null;
BufferedReader breader = null;
try {
cltsock = new Socket(svrip,port);
System.out.println(String.format("Client at %s has started.", cltsock.getInetAddress().toString()));
istrm = cltsock.getInputStream();
// .....後面不寫了,意思意思得了
WinSock的功能與之類似,只不過因為結構僵化的C++標準委員會遲遲不將專用於C++網路連線的<network>
納入STL(標準庫)中,所以我們這裡只好委曲求全地使用Windows的標準,在Linux環境中,替代品為系統核心提供的<sys/socket.h>
庫。不過整體使用上,兩者區別並不那麼大。
Winsock比較常用的有兩類版本——1.1和2.x。本文這裡,當然了,標題也說了,以Winsock2為準,建立一個IPv4協議下的傳輸鏈路。
Workflow
嗯,自從因為各種原因對自己產生了一個巨大的否定以來,我發現總結一個具體的工作流程(Workflow)出來還是挺重要的一件事。
那麼,我們這裡就嘗試總結一下,建立一個連線併傳送訊息期間,Winsock都做了什麼:
Server Side(服務端)
先說Server這邊,從上面這張圖來看,他做了這麼幾件事:
-
初始化WSA
所謂WSA就是WinSock API,當然了,因為我們這裡用的是Winsock2,所以我們做的就是對Winsock2的初始化,畢竟只有初始化之後我們才能使用Winsock裡面的相關功能,當然具體來說這裡其實還包括一些其他的事情,比如版本比對之類的。 -
建立Server的套接字
當我們可以使用Winsock的相關功能後,我們就可以通過Winsock建立套接字以建立連線了。 -
繫結地址
這個先留著,放到程式碼裡再說…… -
啟動監聽
當套接字被啟動後,我們允許該套接字監聽是否有外部的客戶端的連線請求 -
接收Client的連線
有Client的時候我們獲得這位Client的套接字。 -
收發訊息
連線建立完成了,可以傳輸資料了 -
結束
打完收工!
初始化WSA
既然我們使用Winsock2,那麼我們首先做的必然就是對Winsock2的調取,原始的來說,Winsock2隸屬於Win32的系統庫,當你引入標頭檔案WinSock2.h
的時候你就已經獲得了Winsock2的結構宣告。當然,為了讓他能夠調取系統庫,我們需要把他靜態連結進來
#include <WinSock2.h>
#include <ws2tcpip.h> //???
#pragma comment(lib, "WS2_32.lib")
注意,上面第2行中,我又引入了一個ws2tcpip.h
,這是因為在Winsock2.h
中宣告的有關函式不再倡用了(Deprecated),這種情況下直接使用這類函式會直接觸發錯誤,當然,如果你執意要用的話,那就在最開始的時候補充一下抑制巨集:
//如果你非要使用Deprecated內容的話
#define _WINSOCK_DEPRECATED_NO_WARNINGS
// includes... pragma...
然後我們開始在Server的主程式裡初始化WSA,初始化過程主要經過:設定版本限制→初始化→獲得初始化資訊,因此我們首先設定我們能接受的WinSock庫的最低版本:
WORD verRequest = MAKEWORD(1, 1);
雖然說我們用的是WinSock2,不過我們這裡本著向下相容的原則,我們約定要是隻有1.1也行……
然後我們需要使用WSAStartup
函式來真正初始化WSA,這個函式接收兩個引數,一個版本約定和一個資料結構:
WSAData wData;
int $err = WSAStartup(wVersionRequest, &wData);
初始化期間,系統會根據你提供的Version Request來評估當前系統內的Winsock版本,並將結果寫進wData中,返回初始化失敗的錯誤程式碼,如果沒有錯誤返回0,因此,這裡我們在發生錯誤的情況下直接結束程式。
if($err != 0)
{
cout << "Initialization Failed" << endl;
WSACleanup();
ExitProcess($err);
}
如果到這裡沒進入if的話,那麼說明初始化成功了,我們可以輸出WSA的資訊看一眼:
cout << wData.szDescription << endl;
wData.szDescription
存放了初始化時系統獲得的API描述文字,在我的機器上,輸出的是
WINSOCK 2.0
很明顯,因為我用的WinSock2嘛,也就是說現在為止我係統中提供的WinSock庫並沒有什麼問題。
建立服務端套接字
既然WSA可以正常使用,那麼從這裡開始我們就正式的開始用WSA提供的功能了。
既然是服務端程式,那麼能想到的第一件事必然是:建立套接字(Socket)!
WinSock提供了SOCKET
型別和socket
函式來表示並建立一個套接字。我們先來看一下socket()
函式該怎麼用:
SOCKET socket(
_In_ int af,
_In_ int type,
_In_ int protocol
);
其實前面修飾符還有什麼_Must_inspect_result_
什麼的,這些我們先不管,撈乾的來看就是這樣的函式。
引數名稱 | 型別 | 用途 |
---|---|---|
af | int | 你的socket所使用的地址協議族,我們這裡只介紹IPv4的,所以這裡填入AF_INET 就好了 |
type | int | 你的socket建立連線所傳輸的資料形式,比較常用的有流式SOCK_STREAM 或者資料包SOCK_DGRAM ,當然還有其他的這裡暫時不作介紹 |
protocol | int | 你的socket使用的傳輸協議,可填入以IPPROTO_* 開頭的常量,也可以填入0 來自動選擇協議,自動選擇協議的規則與type 相關,例如type=SOCK_STREAM 那麼會使用TCP協議,如果是type=SOCK_DGRAM 那麼會選擇UDP協議 |
我們這裡以IPv4的TCP協議為例,那麼可以建立這樣的socket:
SOCKET sckSrv = socket(AF_INET, SOCK_STREAM, 0);
如果socket沒有成功建立,那麼函式會返回一個INVALID_SOCKET
為Server socket繫結地址
使用上面的方法建立的套接字僅包含地址協議的資訊,但這個套接字並不具備一個地址,出於某種原因,我們並不能直接操縱Socket實體本身來設定這些東西,因為SOCKET
型別說到底只是一個控制程式碼id,並不承載其他資訊。WinSock提供了專用於給socket繫結地址的函式bind
:
int bind(
SOCKET s,
const sockaddr *name,
int namelen
);
還是直接撈乾的看,這裡的SOCKET s
肯定就是剛才的sckSrv
,填進去就可以了。而name
這裡就需要特別注意一下了,name
要求你輸入的實際上是你的ip地址,但是這裡接受的型別是SOCKADDR *
,SOCKADDR
是用於表示地址的一個資料結構,包含地址協議族和具體的ip地址資訊,不過SOCKADDR
的結構很raw,不是很好構建,因此我們需要使用一個改進結構SOCKADDR_IN
,這個結構專用於IPv4(IPv6請使用SOCKADDR_IN6
):
typedef struct sockaddr_in {
short sin_family;
u_short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
} SOCKADDR_IN;
因此我們這裡如果要構建地址的話:
SOCKADDR_IN addrSrv;
addrSrv.sin_family = AF_INET;
注意,這裡的sin_addr
是個in_addr
型別,這個型別並不支援直接輸入我們所說的字面上的ip地址"xxx.xxx.xxx.xxx"這種的,因此如果你想用字串的ip地址輸入進去的話,就必須用inet_aton
做地址形式轉換。
#ifdef _WINSOCK_DEPRECATED_NO_WARNINGS
//指定為本機地址
addrSrv.sin_addr = inet_addr("127.0.0.1");
//或者你也可以指定為預設路由(0.0.0.0),向下面這樣寫
addrSrv.sin_addr = INADDR_ANY;
#endif
然而,如果你沒有解除Deprecation warning(沒有定義_WINSOCK_DEPRECATED_NO_WARNINGS
)的話,那麼直接使用inet_addr
會報錯,這種情況下如果堅持不新增抑制巨集的替代做法是使用inet_pton
來完成:
#if (!defined _WINSOCK_DEPRECATED_NO_WARNINGS) && (defined _WS2TCPIP_H_)
inet_pton(AF_INET, "127.0.0.1", &(addrSrv.sin_addr));
#endif
其實這個型別是個32位整數,如果你確定你的ip地址寫成32位整數是什麼樣子的話那麼沒有必要費勁巴力的做地址形式轉換,而直接賦值成0x????????
的形式,但是這樣程式碼可讀性很差,而且還涉及到本機到網路傳輸時的大小端點轉換問題(hton*
),而inet_*
轉換出來的地址碼是確定符合網路傳輸的形式(這話是巨硬msdn裡說的)。
最後我們不要忘了還得提供埠號:
addrSrv.sin_port = htons(17500); // 這個需要轉換為網路格式,埠號隨你喜歡
當然,如果你引入了<ws2tcpip.h>
,那麼你還可以繼續用更加通用的地址結構sockaddr_gen
,當然,不用也沒關係,使用這個結構(其實是個union)只是規避了接下來的型別轉換的問題。(注意,名字是小寫的,和之前的不一樣,之前的SOCKADDR
和SOCKADDR_IN
其實也可以全小寫):
#ifdef _WS2TCPIP_H_
sockaddr_gen gaddrSrv;
gaddrSrv.AddressIn = addrSrv;
上述內容其實是為了構建地址(不得不說確實挺麻煩,而且事情特別多,但是如果理清的話其實很容易),說白了就是準備填入name
這個引數,但是注意,由於bind
的name
引數是SOCKADDR *
型別,因此你不管用什麼型別折騰,最後都要轉換回這個raw型別:
// 如果沒定義gaddrSrv
bind(sckSrv, (SOCKADDR *)&addrSrv, sizeof(SOCKADDR));
// 如果定義了gaddrSrv
bind(sckSrv, &(gaddrSrv.Address), sizeof(SOCKADDR));
第三個引數的namelen
就指定成SOCKADDR
的大小即可(因為name指向SOCKADDR嘛)。
啟動服務端監聽
服務端嘛,你要服務的嘛,所以我們需要開啟服務端的監聽,讓sckSrv聽取外界是否有其他socket連入,使用listen
函式啟動監聽:
listen(sckSrv, SOMAXCONN);
第一個引數就是你的伺服器套接字,第二個引數是最大允許的連線等待佇列長度,SOMAXCONN
是最大的允許限制範圍了,設定為SOMAXCONN
可以理解為沒有數量限制(其實是有的,SOMAXCONN=0x7fffffff
,當然,WinSock允許使用SOMAXCONN_HINT
設定更大的值,但僅限於能夠在巨硬自己的TCP/IP協議服務供應器下使用,我們這裡不考慮這個東西)。
獲取連入的客戶端
啟用監聽後,Server將能夠獲得來自外界的連線請求,使用accept
原語來取得連入的客戶端資訊:
SOCKET accept(
SOCKET s,
sockaddr *addr,
int *addrlen
);
accept
原語令伺服器s
接收第一個等待連線的客戶端(如果沒有則阻塞這個伺服器程式,直至有第一個進入),並獲取這個客戶端的地址資訊,存入addr
中。
由於我們後面會需要Server向Client傳送資料,因此我們這裡獲得客戶端addr的行為是必需的,所以還是類似的方式,定義客戶端的addr結構:
SOCKADDR_IN addrCli;
SOCKET sckCli = accept(sckSrv, (SOCKADDR *) &addrCli, sizeof(SOCKADDR));
由於我們並不知道客戶端的任何網路地址資訊,因此我們只建立addrCli
但無需初始化,與addrSrv
類似,你也可以使用sockaddr_gen
型別,這裡不演示了。
再次強調,這裡的
accept
原語是阻塞的!如果需要非阻塞的accept原語,可能需要select
配合,但是本文暫時不討論這個。
當然,如果accept
的引數不正確(比如你的addrlen大小不對),那麼他有可能返回一個INVALID_SOCKET
收發資料
獲得了客戶端的socket,我們就說建立了從server到socket的連線,連線建立完成,我們就可以經由這個連線傳遞資料了(雙向地)。
當然了,直接使用send
和recv
原語就可以了,但是在使用之前,我們先確立緩衝區用於儲存收發的資料:
char sendbuf[1024] = "Hello, from SERVER.",
recvbuf[1024];
然後我們集中看一下send和recv原語:
int send(
SOCKET s,
const char *buf,
int len,
int flags
);
int recv(
SOCKET s,
char *buf,
int len,
int flags
);
應該很言簡意賅了,s
是你要傳送的client socket,flags
我們先不管,給0就可以,flags
主要控制訊息收發時的行為,我們這裡就按照一般的收發方法正常收發就可以了。我們這裡讓server先對連入的client傳送訊息,然後再傳送(留意這一點,下面我們寫客戶端的時候就需要把這個順序反過來,當然你也可以在這裡先收後發,那在客戶端那邊就還得反過來):
send(sckCli, sendbuf, sizeof(sendbuf), 0);
recv(sckCli, recvbuf, sizeof(recvbuf), 0);
就可以收發資訊了,如果沒有別的事情的話,那麼就可以……
關閉套接字、釋放WSA
善始善終是一種美德,特別是對於C++而言。如果沒有別的事情,我們想結束的話,那就需要關閉套接字,然後釋放掉WSA,這兩個很簡單:
closesocket(sckCli);
closesocket(sckSrv);
WSACleanup();
ExitProcess(0);
好了,服務端程式就寫的差不多了,程式碼會在文章最後整理。
Client Side(客戶端)
客戶端和服務端的寫法就差不太多了,但是注意幾點:
- 由於客戶端是積極連線服務端,因此客戶端一般不需要自己的ip地址,所以客戶端的socket不需要地址繫結,也不需要啟動監聽
- 服務端是
accept
原語,那麼客戶端就是connect
原語 - 客戶端只需要建立自己的socket,不需要考慮服務端
- 沒了
相比較剛才的過程而言,再補充說明幾個新加入的東西……
與服務端的積極連線
服務端是被動的accept外界的連線,那等的是誰呢,顯然就是客戶端主動的connect。
int connect(
SOCKET s,
const sockaddr *name,
int namelen
);
其中s
是客戶端自己的socket,雖然說客戶端的socket並不需要繫結地址,但是客戶端仍然要提供服務端的地址資訊(不然鬼知道你想跟誰連),也就是說addrSrv
仍然要提供。
然後再注意的一點就是收發和服務端應當是相反的。
同樣,程式碼我在後面彙總。
當你遇到錯誤時……
寫這兩個程式期間,你可能就已經開始執行除錯了,當然多數時候你不大可能一遍寫成(除非你是直接拷的程式碼),因此難以避免的你會遇到各種錯誤,有的和你的程式碼有關,有的可能與你的網路環境有關,前者你大可通過偵錯程式和文件來解決,但後者就很難捕捉了。
而WinSock用於返回錯誤的函式WSAGetLastError()
,主要用於返回錯誤程式碼,你可以根據msdn文件對照你的錯誤程式碼來定位你的錯誤。如果沒有任何錯誤,該函式應當返回0。
程式碼彙總
Server
#include <WinSock2.h>
#include <ws2tcpip.h> //???
#include <iostream>
#pragma comment(lib, "WS2_32.lib")
using namespace std;
int main(int argc, char **argv)
{
WORD verRequest = MAKEWORD(1, 1);
WSAData wData;
int $err = WSAStartup(wVersionRequest, &wData);
if($err != 0)
{
cout << "Initialization Failed" << endl;
WSACleanup();
ExitProcess($err);
}
cout << wData.szDescription << endl;
SOCKET sckSrv = socket(AF_INET, SOCK_STREAM, 0);
if(sckSrv == INVALID_SOCKET)
{
closesocket(sckSvr);
WSACleanup();
ExitProcess(INVALID_SOCKET);
}
SOCKADDR_IN addrSrv;
addrSrv.sin_family = AF_INET;
inet_pton(AF_INET, "127.0.0.1", &(addrSrv.sin_addr));
addrSrv.sin_port = htons(17500);
bind(sckSrv, (SOCKADDR *)&addrSrv, sizeof(SOCKADDR));
listen(sckSrv, SOMAXCONN);
SOCKADDR_IN addrCli;
SOCKET sckCli = accept(sckSrv, (SOCKADDR *) &addrCli, sizeof(SOCKADDR));
if(sckCli == INVALID_SOCKET)
{
closesocket(sckCli);
closeSocket(sckSvr)
WSACleanup();
ExitProcess(INVALID_SOCKET);
}
char sendbuf[1024] = "Hello, from SERVER.",
recvbuf[1024];
send(sckCli, sendbuf, sizeof(sendbuf), 0);
recv(sckCli, recvbuf, sizeof(recvbuf), 0);
cout << recvbuf << endl;
closesocket(sckCli);
closesocket(sckSrv);
WSACleanup();
ExitProcess(0);
}
Client
#include <WinSock2.h>
#include <ws2tcpip.h> //???
#include <iostream>
#pragma comment(lib, "WS2_32.lib")
using namespace std;
int main(int argc, char **argv)
{
WORD verRequest = MAKEWORD(1, 1);
WSAData wData;
int $err = WSAStartup(wVersionRequest, &wData);
if($err != 0)
{
cout << "Initialization Failed" << endl;
WSACleanup();
ExitProcess($err);
}
cout << wData.szDescription << endl;
SOCKET sckCli;
if(sckCli == INVALID_SOCKET)
{
closesocket(sckCli);
WSACleanup();
ExitProcess(INVALID_SOCKET);
}
SOCKADDR_IN addrSrv;
addrSrv.sin_family = AF_INET;
inet_pton(AF_INET, "127.0.0.1", &(addrSrv.sin_addr));
addrSrv.sin_port = htons(17500);
connect(sckCli, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR));
char sendbuf[1024] = "Hi, from CLIENT.",
recvbuf[1024];
recv(sckCli, recvbuf, sizeof(recvbuf), 0);
cout << recvbuf << endl;
send(sckCli, sendbuf, sizeof(sendbuf), 0);
closesocket(sckCli);
WSACleanup();
ExitProcess(0);
}