Windows網絡編程(一)基礎

準備工作

Windows網絡編程一般是指 Windows Socket 編程(winsocket),它從UNIX Socket 發展而來。進行Windows網絡編程,首先需要添加依賴庫WS2_32.libWSOCK_32.lib,加載動態庫ws2_32.dll,放入C:/Windows/System32。然後使用時在源文件中包含頭文件:

#include <WinSock2.h>
// #include <MSWSOCK.h>
// #include <winsock.h>

說明:
有些接口已經棄用,採用新的接口,具體是哪些,後面會慢慢指出。

  • 如何引入依賴庫?
#pragma comment(lib, "ws2_32.lib");    // 源文件中添加

也可以在配置文件中添加:屬性----鏈接器----輸入----ws2_32.lib.

socket

socket 套接字是應用層到傳輸層的接口,表示一個連接的兩端,每個端由IP地址和端口port組成,即socket是由兩端點的ip和端口port組成的

  • 套接字類型 SOCKET 定義

    typedef unsigned int SOCKET;   // 句柄
    
  • 端口

    端口是傳輸層的概念,每個端口對應一個 process 進程,因此一條連接表示一個進程與另一個進程建立聯繫。

  • 套接字類型

    一般使用兩種套接字:TCP 流套接字,UDP 數據報套接字。前者提供可靠的、無重複的、有序的數據流服務,後者提供不可靠傳輸。

C/S模式

winsocket 一般採用C/S模式

  • Server 端流程

1、初始化winsocket
2、建立socket
3、綁定服務端地址(bind)
4、開始監聽(listen)
5、然後與客戶端建立連接(accept)
6、然後與客戶端進行通信(send, recv)
7、當通信完成以後,關閉連接
8、釋放winsocket的有關資源

  • Client 端流程

1、初始化winsocket
2、建立socket
3、與服務器進行連接(connect)
4、與服務器進行通信(send, recv)
5、當通信完成以後,關閉連接
6、釋放winsocket佔用的資源

話不多說,先上一段代碼,再小段分析

源代碼

源碼親測可以運行

服務端

// win_server.cpp
// compiler with: VS2017
#include "pch.h"
#include <stdio.h>  
#include <winsock2.h>  // 必須包含windwos.h之前
#include <Windows.h>
// #include <process.h>  /* _beginthreadex */

// 指定依賴庫目錄
#pragma comment(lib,"ws2_32.lib") 
// 設置端口號
constexpr auto PORT = 6000;

// C/S 端連接情況分析
// Server 端收發數據情況
DWORD WINAPI clientProc(LPARAM lparam)
{
	SOCKET sockClient = (SOCKET)lparam;
	char buf[1024];
	while (TRUE)
	{
		memset(buf, 0, sizeof(buf));
		// 接收客戶端的一條數據 
		int ret_recv = recv(sockClient, buf, sizeof(buf), 0);
		//檢查是否接收失敗
		if (SOCKET_ERROR == ret_recv)
		{
			printf("socket recv failed\n");
			closesocket(sockClient);
			return -1;
		}
		// 0 代表客戶端主動斷開連接
		if (ret_recv == 0)
		{
			printf("client close connection\n");
			closesocket(sockClient);
			return -1;
		}

		// 發送數據
		int ret_send = send(sockClient, buf, strlen(buf), 0);
		//檢查是否發送失敗
		if (SOCKET_ERROR == ret_send)
		{
			printf("socket send failed\n");
			closesocket(sockClient);
			return -1;
		}
	}
	closesocket(sockClient);
	return 0;
}

// 網絡環境初始化
// 加載 dll,初始化socket
bool InitNetEnv()
{
	WSADATA wsa;
	if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
	{
		printf("WSAStartup failed\n");
		return false;
	}
	return true;
}

int main(int argc, char * argv[])
{
	if (!InitNetEnv())
	{
		return -1;
	}
	// 初始化完成,創建一個TCP的socket
	SOCKET sServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	//檢查是否創建失敗
	if (sServer == INVALID_SOCKET)
	{
		printf("socket failed\n");
		return -1;
	}
	printf("Create socket OK\n");
	//進行綁定操作
	SOCKADDR_IN addrServ;
	addrServ.sin_family = AF_INET; // 協議簇爲IPV4的
    // 端口  因爲本機是小端模式,網絡是大端模式,調用htons把本機字節序轉爲網絡字節序
	addrServ.sin_port = htons(PORT); 
    // ip地址,INADDR_ANY表示綁定電腦上所有網卡IP
	addrServ.sin_addr.S_un.S_addr = INADDR_ANY; 
    
	//完成綁定操作
	int ret = bind(sServer, (sockaddr *)&addrServ, sizeof(sockaddr));
	//檢查綁定是否成功
   
	if (SOCKET_ERROR == ret)
	{
		printf("socket bind failed\n");
		WSACleanup(); // 釋放網絡環境
		closesocket(sServer); // 關閉網絡連接
		return -1;
	}
	printf("socket bind OK\n");
	// 綁定成功,進行監聽
    
	ret = listen(sServer, 10);
	//檢查是否監聽成功
	if (SOCKET_ERROR == ret)
	{
		printf("socket listen failed\n");
		WSACleanup();
		closesocket(sServer);
		return -1;
	}
	printf("socket listen OK\n");
	// 監聽成功
	sockaddr_in addrClient; // 用於保存客戶端的網絡節點的信息
	int addrClientLen = sizeof(sockaddr_in);
	while (TRUE)
	{
		//新建一個socket,用於客戶端
		SOCKET *sClient = new SOCKET;
		//等待客戶端的連接
		*sClient = accept(sServer, (sockaddr*)&addrClient, &addrClientLen);
		if (INVALID_SOCKET == *sClient)
		{
			printf("socket accept failed\n");
			WSACleanup();
			closesocket(sServer);
			delete sClient;
			return -1;
		}
		//創建線程爲客戶端做數據收發
		//_beginthreadex(NULL, 0, &clientProc, NULL, CREATE_SUSPENDED, (LPVOID)*sClient);
		CreateThread(0, 0, (LPTHREAD_START_ROUTINE)clientProc, (LPVOID)*sClient, 0, 0);
	}
	closesocket(sServer);
	WSACleanup();
	return 0;
}

客戶端

// win_client.cpp
// compile with: VS2017
#include "pch.h"
#include <stdio.h>
#include <winsock2.h>
#include <Windows.h>

#pragma warning(disable:4996)
#pragma comment(lib,"ws2_32.lib")
constexpr auto PORT = 6000;

int main(int argc, char * argv[])
{
	//初始化網絡環境
	WSADATA wsa;
	if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
	{
		printf("WSAStartup failed\n");
		return -1;
	}
	// 初始化完成,創建一個TCP的socket
	SOCKET sServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (sServer == INVALID_SOCKET)
	{
		printf("socket failed\n");
		return -1;
	}
	//指定連接的服務端信息
	SOCKADDR_IN addrServ;
	addrServ.sin_family = AF_INET;
	addrServ.sin_port = htons(PORT);
	//客戶端只需要連接指定的服務器地址,127.0.0.1是本機的迴環地址
	addrServ.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");

	// 服務器 Bind 客戶端是進行連接
	int ret = connect(sServer, (SOCKADDR*)&addrServ, sizeof(SOCKADDR));//開始連接
	if (SOCKET_ERROR == ret)
	{
		printf("socket connect failed\n");
		WSACleanup();
		closesocket(sServer);
		return -1;
	}
	//連接成功後,就可以進行通信了
	char szBuf[1024];
	memset(szBuf, 0, sizeof(szBuf));
	sprintf_s(szBuf, sizeof(szBuf), "Hello server");
	//當服務端是recv的時候,客戶端就需要send,若兩端同時進行收發則會卡在這裏,因爲recv和send是阻塞的
	ret = send(sServer, szBuf, strlen(szBuf), 0);
	if (SOCKET_ERROR == ret)
	{
		printf("socket send failed\n");
		closesocket(sServer);
		return -1;
	}

	ret = recv(sServer, szBuf, sizeof(szBuf), 0);
	if (SOCKET_ERROR == ret)
	{
		printf("socket recv failed\n");
		closesocket(sServer);
		return -1;
	}
	printf("%s\n", szBuf);

	closesocket(sServer);
	WSACleanup();
	return 0;
}

源碼分析

  • WSAData 結構體
typedef struct WSAData {
        WORD                    wVersion;       // 版本號
        WORD                    wHighVersion;
#ifdef _WIN64
        unsigned short          iMaxSockets;
        unsigned short          iMaxUdpDg;
        char FAR *              lpVendorInfo;
        char                    szDescription[WSADESCRIPTION_LEN+1];
        char                    szSystemStatus[WSASYS_STATUS_LEN+1];
#endif
} WSADATA, FAR * LPWSADATA;
  • SOCKADDR_IN 結構體
typedef struct sockaddr_in {
    USHORT sin_port;
    IN_ADDR sin_addr;
    CHAR sin_zero[8];
} SOCKADDR_IN;
  • 服務端需要 bind 的原因

無連接(connect)的服務端、客戶端和面向連接的服務端通過 bind 來配置本地信息;而有連接的客戶端通過調用 connect 函數在socket 數據結構中保存本地和遠端信息,不需要調用 bind()。

  • 需要初始化 WASStartup()的原因

之所以需要初始化winsocket,是因爲Winsock的服務是以動態連接庫Winsock DLL形式實現的,所以必須先調用初始化函數(WSAStartup)對Winsock DLL進行初始化,協商Winsock的版本支持,並分配必要的資源; // 在Linux環境中不需要該初始化步驟。

數據傳輸

在建立起連接的基礎上,發送數據可以用接口 send / WSASend,接收數據可以用 recv / WSARecv

  • 對於 send 而言,發送數據的長度一般有限制,因爲緩衝區或者 TCP/IP 的窗口大小有所限制,所以需要根據窗口大小來設定發送數據的長度。
  • 對於 recv 而言,流套接字是一個不間斷的數據流,在讀取它時,應用程序通常不會關心應該讀取多少數據,如果所有消息長度都一樣,這應該簡單處理,如讀取 1024 字節。
char recvBuff[2048];
int ret;    // 讀取的數據長度
int nLeft;  // 剩餘空間
int idx;    // 緩衝區數組下標
nLeft = 1024;
idx = 0;
while (nLeft > 0)
{
    ret = recv(socket1, &recvBuff[idx], nLfet, 0);
    if (ret == SOCKET_ERROR){
        // error 讀取失敗
        std::cout << "Error when receive message.";
    }
    idx += ret;
    nLeft -= ret;
}

  • 如果接收的消息長度不同,則按照發送端的協議來通知接收端,告知接收端即將到來的消息長度多少;比如,在消息的前幾個字節設定標記,表示數據長度。

關閉連接

數據傳輸完成,關閉套接字,釋放資源。

shutdown();    // 中斷連接
closeSocket(socket_name);
WASCleanup();  // 釋放 dll 

符號解釋

WSAStartup

初始化 DLL,加載 socket,在Windows中,socket 以 dll 形式實現,dll 內部有一個計數器,第一次調用是真正加載 dll,後面再次調用 WSAStartup 是計數器加 1 ;與 WSAStartup 綁定使用的是 WSACleanup(),相反的,該函數只有最後一次調用纔是真正卸載 dll,釋放資源,前面的每次調用都是計數器減 1。

WSAStartup() 定義如下

int WSAAPI WSAStartup(              // WSAStartup 結構體
    _In_ WORD wVersionRequested,    // 高字節:指出副版本號,低字節:主版本號
    _Out_ LPWSADATA lpWSAData       // 指向win socket 實現的細節
    );

sin_family

表示地址家族。使用 TCP/IP 協議的應用程序必須設置 AF_INET,來告訴系統使用 IP 地址家族 。

sin_port

指定服務的端口號。1024–49151範圍內的數據被作爲服務端口號,可以由用戶自定義。 sin_zero字段作爲填充字段。以便使得該結構與SOCKADDR結構長度相同。

inet_addr

把本機IP的主機字節序轉化爲網絡字節序。

該接口已經棄用,採用 inet_pton 或其他代替。

htonl / htons

host to net long/short htonl 和 htons 函數實現主機字節順序和網絡字節序的轉換功能。H代表host,主機。N代表net,L代表long,S代表short。不能使用htonl轉換short。

同理,網絡字節序—> 主機字節序 :ntohl 、ntohs

af

表示協議使用的地址家族,創建TCP或UDP的套接字時使用AF_INET 地址家族。

type

socket套接字類型,有三種套接字類型:SOCK_STREAM / SOCKET_DGRAM / SOCK_ARM,分別表示數據流,數據包,原始套接字。

C/S 通信

ClientServer 要進行通信,首先需要建立連接;而客戶端要連上服務端,首先需要開啓服務端 Serverbind好服務端的IP地址和端口port,並設置監聽listen,這時才能運行客戶端程序,連接 上服務端,進行數據傳輸。

在Windows下進行的C/S通信,需要開啓兩個編譯器分別編譯運行 server.cpp 和 client.cpp,一般都有用 vs 吧,那就同時開啓兩個 vs ,不要將客戶端和服務端程序寫在同一個項目中。

以上僅爲個人所學所得,僅供參考,歡迎不吝指正。

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