Windows網絡編程(一)

Windows網絡編程(一):創建鏈接

一、使用前的準備

  必須使用到的頭文件是winsock2.hwinsock.h兩個頭文件中選擇一個。

  需要注意的是,單純的引入這個頭文件是不能完成代碼的編譯的。因爲winsock需要使用靜態鏈接庫WSOCK32.LIB進行靜態鏈接。因此需要採用以下其中之一的方式將靜態庫進入,否則在編譯的時候會出現以下的錯誤:

NetDemo-12ac36.o : error LNK2019: 無法解析的外部符號 __imp_WSAStartup,該符號在函數 main 中被引用
NetDemo-12ac36.o : error LNK2019: 無法解析的外部符號 __imp_WSACleanup,該符號在函數 main 中被引用
NetDemo-12ac36.o : error LNK2019: 無法解析的外部符號 __imp_WSAGetLastError,該符號在函數 main 中被引用
  • 第一種方法是在代碼中進行連接:
#pragma comment(lib, "WSOCK32")
  • 第二種方法是在編譯是進行連接:
clang++ .\NetDemo.cpp -o NetDemo.exe -L F:\uCard\VC6.0green\VC98\Lib\ -lWSOCK32

二、使用中的初始化

  包含頭文件之後,需要的做的第二件事就是將winsock進行初始化,初始化的過程主要是使用這個函數加載合適的winsock DLL。然後才能進行使用,使用的初始化函數原型如下:

int WSAStartup(
	WORD wVersionRequested,
	LPWSADATA lpWSAData
);

  然後使用winsock進行socket編程,使用完winsock之後,最終需要將socket進行釋放資源,使用的是以下的函數原型如下:

int WSACleanup();

本函數原型的使用實例是:

WSAData wsadata;
WSAStartup(MAKEWORD(2, 2), &wsadata);
WSACleanup();

三、錯誤處理

  在進行編程的時候需要對在網絡中出現的問題進行錯誤檢查和錯誤處理,在這裏使用的函數原型如下所示:

int WSAGetLastError();

  這個函數接收的錯誤必須是在winsock被加載完成之後,才能進行錯誤的獲取,否則不能獲取錯誤。正是因爲這個原因,這個函數調用需要在WSAStartup之後獲取錯誤代碼。

四、很重要的一個概念——IP尋址

  理解結構體——SOCKADDR_IN,這個結構體主要的是以下結構:

struct sockaddr_in
{
    short 			sin_family; // 協議棧
    u_short 		sin_port;	// 端口號
    struct in_addr 	sin_addr;	// 自己的IP地址
    char 			sin_zero[8];// 用於填充的字節
};

  接下來分別介紹以上結構體中的各字段:

  • sin_familysin\_family:在IP協議棧中的值爲AF_INET
  • sin_portsin\_port:設置IP地址的端口號
  • sin_addrsin\_addr:IP地址的一個結構體:很重要,以下就是針對這個IP地址進行處理。

  主要的問題就是將點分十進制轉換成爲一個長整型函數,主要的函數原型就是以下的函數:

unsigned long inet_addr(const char FAR * cp);

  這樣可能存在一個問題,對於不同的主機廠商,他們設定的IP地址在本機中的序列可能有所不同,即我們所說的大端字節序和小端字節序的區別。然而在同一個網絡上就需要將以上的IP地址進行統一,在這裏我們統一使用大端字節序作爲網絡上IP的標準。

於是針對這一需求,就存在大量的函數用於解決這個問題,將主機字節序轉換成爲網絡字節序。下列的四個API都能實現以上的需求:

u_long htonl(u_long hostlong);// 返回值就是最終的數據
int WSAHtonl(SOCKET s, u_long hostlong, u_long FAR * lpnetlong);// 返回值放置在lpnetlong中
u_short htons(u_short hostshort);
int WSAHtons(SOCKET s, u_short hostshort, u_short FAR * lpnetshort);

實現反方向轉化的函數爲:

u_long htonl(u_long netlong);// 返回值就是最終的數據
int WSAHtonl(SOCKET s, u_long netlong, u_long FAR * lphostlong);// 返回值放置在lpnetlong中
u_short htons(u_short netshort);
int WSAHtons(SOCKET s, u_short netshort, u_short FAR * lphostshort);

敬請期待

五、很重要的socket連接概念

  socket在網絡編程中是一個很重要的概念,針對socket這個結構體有以下的結構:

SOCKET socket(
    int af;			// 協議的地址族
    int type;		// 套接字的類型
    int protocol;	// 使用的協議
);

  其實我們不難知道,socket是傳輸層的一個概念,因此以上的結構體如果我們採用TCP/IP協議族的話,那麼以上結構體中各字段的含義如下:

  • afaf:這個值就設置爲和以上相同AF_INET
  • typetype:這個值根據TCP和UDP的不同而設置爲不同的關鍵字——對於TCP這個字段的值設置爲SOCK_STREAMSOCK\_STREAM,對於UDP這個字段的值設置爲SOCK_DGRAMSOCK\_DGRAM ,其實也不難理解,因爲對於TCP的傳輸就是使用的流式數據傳輸;而針對UDP採用的是數據包的形式進行數據傳輸
  • protocolprotocol:這個就是協議了,根據使用的傳輸層協議設置這個字段,若採用的是TCP則本字段設置爲IPPROTO_TCPIPPROTO\_TCP,若使用的協議爲UDP,那麼本字段的值設置爲IPPROTO_UDPIPPROTO\_UDP

六、創建一個服務器的監聽模式

  這個過程讓我們來梳理一下,我們所說的服務器本質上就是一個進程,如果想要被客戶端連接,那麼服務器必須在一個已知名稱(其實就是一個socket上,我們知道socket就是IP地址+端口號,時刻注意這個需要綁定的地址服務就是服務器自己的IP)上進行監聽。那麼這樣一來整個服務器需要做的工作就比較明瞭了——

1. 服務器開啓TCP的過程

  • 將WinSock所提供的套接字和已知的名稱綁定起來。在這裏使用的APIbind()bind()
int bind(
	SOCKET 						s, // 等待客戶連接的套接字,是客戶的套接字
	const struct sockaddr FAR * name, // 就是一個普通的緩衝區。根據使用的協議必須實際地填充一個地址緩衝區,並在調用時,將其轉成一個sockaddr
	int 						namelen// 由協議決定的要傳遞的地址結構的長度
);
SOCKET 			s;
SOCKETADDR_IN 	tcpaddr;
int 			port = 5150;

s = socket(AF_INET, sOCK_STREAM, IPPROTO_TCP);
tcpaddr.sin_family = AF_INET;
tcpaddr.sin_port = htons(port);
tcpaddr.sin_addr.s_addr = htonl(INADDR_ANY);

bind(s, (SOCKADDR *)&tcpaddr, sizeof(tcpaddr));
  • 然後將綁定的已知的名稱進行監聽——這裏使用的APIlisten()listen(),以等待客戶端的連接,監聽的函數原型是以下:
int listen(
	SOCKET 	s, 		// 這個是被綁定的套接字
	int 	backlog // 被阻塞的連接的最大隊列長度 
);
  • 如果有客戶端連接到這個服務器,那麼服務器需要做的就是接受這個請求,並和他建立連接,這裏使用的APIaccept()accept()
SOCKET accept(
	SOCKET 					s,
	struct sockaddr FAR * 	addr, // 用來存儲請求客戶端的IP地址
	int FAR * 				addrlen // 
);

2. 客戶端連接服務器的API函數

客戶端創建socketsocket連接服務器的主要過程是以下幾步:

  1. 創建一個socketsocket

  2. 建立一個SOCKADDRSOCKADDR的地址結構,這個地址是準備連接到服務器的IP地址,在TCP/IPTCP/IP協議中,這個地址結構就是監聽服務器的IP地址和端口號

  3. 使用connectconnect初始化客戶機與服務器的連接

int connect(
	SOCKET 						s,
	const struct sockaddr FAR * name,
	int 						namelen
);

3. UDP的數據傳輸連接過程

對於UDPUDP連接的serverserver的主要工作的流程是以下的幾步:

1)初始化socketsocket

2)將socketsocket綁定自己的ipip地址和端口

3)recvfromrecvfrom以獲取客戶端的ipip地址,然後進行通信

4)sendtosendto發送數據

對於UDPUDP連接的clientclient的主要步驟是以下幾步:

1)初始化socketsocket

2)使用sendtosendto向服務器的socketsocket發送數據

存在的疑問就是:QQ用戶即使在對方離線的情況下,是怎樣能夠收取對方在自己離線時發送給自己的信息

七、關於socket編程中INADDR_ANY的理解

  實際上這個INADDR_ANY轉換成爲點分十進制是0.0.0.0,這個地址讓人感覺就比較迷茫。在網絡地址中設置0.0.0.0這個地址的用意實際上是這樣的——

  我們知道一臺主機可能有多個IP地址,就比如我們之前學到的在進行環回測試時候的保留地址127.0.0.1,這個地址是不可能出現在任何公網情況下的,當我們的主機連接網絡時,就會獲取路由器爲我們分配的一個IP地址,這樣一看,我們的就存在兩個IP。還有其他更多的情況是可能服務器有多個網卡什麼的可能會有更多的地址,因此一臺主機的IP地址可能存在多個

  即然存在以上的問題,(需求)那麼我們當然希望訪問主機的任意一個IP地址都能夠得到服務器的響應,這其實就是存在的問題。如果我們按照原來的思路,將服務器的socket只綁定在一個IP上,那麼結果就是——客戶端只能通過這個唯一的IP訪問服務器,剩下的IP就不能進行訪問了。按照原來的綁定方式,那麼只能將每個IP地址都綁定一個socket進行管理,這樣就會相當繁瑣,因此有以的方法——

  爲了解決這個問題,於是設置將服務器的socket與0.0.0.0進行綁定,這個特殊的IP地址表示的是本機的所有IP地址,這樣理解的話,也就是說我們使用一個socket就監聽了好多地址,而且最後只需要管理一個socket。這就大大簡化了最後的管理過程。

八、開始傳輸數據

兩個傳輸層協議的數據傳輸

(一)TCP的數據傳輸

1. 發送函數

在已經建立連接的套接字上發送數據的主要API是sendsend,這個函數的原型是以下的方式:

int send(
	SOCKET 				s,
	const char FAR * 	buf,//指向緩衝區的指針
	int 				len,//緩衝區的大小
	int 				flags
);
2. 接收函數

主要的套接字函數接收是主要是API是recvrecv,這個函數的原型是以下的方式:

int recv(
	SOCKET 		s,
	char FAR * 	buf,
	int 		len,
	int 		flags
);

(二)UDP的數據傳輸

1. 發送數據

在發送的過程中,使用的接口是sendtosendto,這個函數的原型是以下的方式:

int sendto(
	SOCKET s,
	const char FAR * buf,
    int len, 
    int flags,
    const struct sockaddr * FAR * to,
    int len
);
2. 接收數據

在接收的過程當中,使用的接口是recvfromrecvfrom,這個函數主要是以下的方式,通過這個函數可以獲取發送方的IP地址和端口號.

int recvfrom(
	SOCKET s,
	char FAR * buf,
	int len, 
	int flags,
	struct sockaddr FAR * from,
	int FAR * fromlen
);

九、錯誤處理

windowssocketwindows\quad socket編程當中,也不是所有的都是一帆風順的,因此對於錯誤的處理也應當足夠引起我們的重視,一般來說,我們可以通過函數來獲取winsockwinsock的錯誤類型,從而定位錯誤。

int WSAGetLastError(void);

在連接過程當中發生錯誤之後,可以調用這個函數,就可以獲取返回的錯誤的整數代碼。這些值可能存在於WINSOCK1.HWINSOCK1.HWINSOCK2.HWINSOCK2.H中。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章