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佔用的資源

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

源代碼

源碼親測可以運行

服務端 Server.cpp

#include "pch.h"
#include <iostream>
#include <WinSock2.h>
//#include <MSWSock.h>
using namespace std;

// 指定依賴庫路徑
#pragma comment(lib, "ws2_32.lib")

int main()
{
	WSADATA wsaData;           // 聲明一個結構體
	SOCKET  listeningSocket;   // 聲明一個監聽句柄
	SOCKET  newConnection;     // 聲明一個連接句柄
	SOCKADDR_IN serverAddr;    // 端點結構體
	SOCKADDR_IN clientAddr;
	int port = 5150;           // 端口號

	// 初始化socket
	WSAStartup(MAKEWORD(2,2), &wsaData);
	// 創建監聽socket相應客戶端請求
	listeningSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	// 填寫服務器地址信息
	// port=5150
	// ip 地址爲INNADDR_ANY,注意使用 hton 將 IP地址轉換爲網絡格式
	serverAddr.sin_family = AF_INET;
	serverAddr.sin_port = htons(port);
	serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);

	// 綁定監聽端口
	bind(listeningSocket, (SOCKADDR *)&serverAddr, sizeof(serverAddr));
	// 開始監聽,指定最大同時連接數
	listen(listeningSocket, 5);
	// 接受新連接
	int clientAddrLen = sizeof(clientAddr);
	newConnection = accept(listeningSocket, (SOCKADDR *)&clientAddr, &clientAddrLen);

	// 新連接建立後,就可以開始通信了
	cout << "========= new client has been connected ========" << endl;

	// 
	// 這裏放通信代碼
	// 

	// 通訊結束後,關閉連接
	// 並關閉監聽socket,然後退出程序
	closesocket(newConnection);
	closesocket(listeningSocket);

	// 釋放window socket dll 資源
	WSACleanup();

	return 0;                   
}

客戶端 Client.cpp

#include "pch.h"
#include <WinSock2.h>
#include <iostream>
#include <WS2tcpip.h>
using namespace std;

int main()
{
	WSADATA wsaData;
	SOCKET clientSocket;
	SOCKADDR_IN serverAddr;
	int port = 5150;

	// 加載 dll,初始化socket 2.2
	WSAStartup(MAKEWORD(2, 2), &wsaData);

	// 創建一個新連接
	clientSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	serverAddr.sin_family = AF_INET;
	serverAddr.sin_port = htons(port);
	serverAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");      // 與本機通信

	// 向服務器發送連接請求
	int result = connect(clientSocket, (SOCKADDR *)&serverAddr, sizeof(serverAddr));

	if (result == 0)
	{
		cout << "client has connect to server..." << endl;
	}

	// 關閉套接字,釋放資源
	closesocket(clientSocket);
	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 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章