Table of Contents
準備工作
Windows網絡編程一般是指 Windows Socket
編程(winsocket),它從UNIX Socket
發展而來。進行Windows網絡編程,首先需要添加依賴庫WS2_32.lib
或 WSOCK_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