目錄
引言:網絡編程基礎
一、socket介紹(套接字)
1.1 Berkeley Socket套接字
1.2 WinSocket套接字
1.3 WSAtartup函數
1.4 socket函數
1.5 字節序轉換
1.6 綁定套接字
1.7 監聽
1.8 連接
1.9 接收數據
1.10 發送數據
1.11 關閉套接字
二、UDP連接流程
2.1 接收數據
2.2 發送數據
三、阻塞與非阻塞模式
四、示例代碼
4.1 TCP協議代碼
4.2 UDP協議代碼
引言:網絡編程基礎
????????在網絡編程領域,實現高效、可靠的網絡通信至關重要。套接字(Socket)作為網絡通信的關鍵接口,在其中扮演著核心角色。從最初加利福尼亞大學Berkeley分校為UNIX系統開發的Berkeley Socket,到后來多家公司共同制定的Windows Sockets規范,套接字不斷發展完善。了解套接字的原理、相關函數的使用以及不同網絡協議(如TCP、UDP)的連接流程,對于開發穩定的網絡應用程序意義重大。
一、socket介紹(套接字)
1.1 Berkeley Socket套接字
????????套接字(Socket)最初是由加利福尼亞大學Berkeley分校專門為UNIX操作系統搞出來的網絡通信接口。時間回到20世紀80年代初,這所學校把美國國防部高研署提供的TCP/IP整合進了Unix系統里,緊接著,很快就開發出了TCP/IP應用程序接口(API),這個接口其實就是Socket(套接字)接口。后來,UNIX操作系統用的人越來越多,套接字也跟著火了起來,成了現在最常用的網絡通信應用程序接口之一。?
1.2 WinSocket套接字
????????在90年代初期,Sun Microsystems、JSB Corporation、FTP software、Microdyne還有Microsoft這幾家公司一起搞出了一套標準,叫做Windows Sockets規范。這個規范是對Berkeley Sockets的一個重要升級。具體來說,它新添了一些異步函數,還弄出了符合Windows消息驅動特點的網絡事件異步選擇機制。?
????????Windows Sockets規范是一套開放的網絡編程接口,能支持多種協議,專門用于Windows系統。在實際使用中,Windows Sockets規范主要有1.1版和2.0版這兩個版本。1.1版只能支持TCP/IP協議,而2.0版就厲害了,它能支持好幾種協議,并且對于之前的版本,也能很好地兼容,老程序也能正常使用。?
TCP連接流程
1. 包含必要的頭文件及庫:
#include <winsock2.h>
#pragma comment(lib,"ws2_32.lib")
2. 指定需要使用的Winsock規范的最高版本,并初始化Winsock,裝入Winsock.dll:
WSAStartup(MAKEWORD(2,2),&wsaDATA);
3. 創建套接字:
socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
4. 綁定IP和端口:
bind(sock,(sockaddr*)&addr,sizeof(sockaddr_in));
5. 監聽:
listen(sock,SOMAXCONN);
6. 連接客戶端:
accept(sock,(sockaddr*)&addrClient,&nAddrSize);
7. 接收數據:
recv(sockClient,buf,1024,0);
8. 發送數據:
send(Sock,message,nSize,O);
9. 在調用“closesocket”函數之后,但是在程序結束之前需要清理Winsock:
closesocket(sock); //關閉套接字
WSACleanup();
1.3 WSAtartup函數
????????不管是開發客戶端還是服務端的Socket應用程序,都必須先加載Windows Sockets動態庫,通常使用WSAtartup函數來實現這個功能。
int WSAStartup(WORD wVersionRequested,LPWSADATA lpWSAData
);
- `wVersionRequested`:通過`MAKEWORD(X,Y)`宏定義來表示,`X`(高位)表示次版本號,`Y`(低位)表示主版本號,即期望調用者使用的WinSocket版本。
- `lpWSAData`:指向`WSADATA`結構體,用于返回被加載動態庫的有關信息。
typedef struct WSAData {WORD wVersion; //期望調用者使用的WinSocket版本WORD wHighVersion;//DLL支持的最高版本char szDescription[WSADESCRIPTION_LEN + 1];//DLL的描述信息char szSystemStatus[WSASYS_STATUS_LEN + 1];//DLL的狀態信息unsigned short iMaxSockets;//一個進程可以打開套接字最多數量unsigned short iMaxUdpDg;char FAR* lpVendorInfo;//一個進程發送或接收的最大數據的長度
}WSADATA,*LPWSADATA;
1.4 socket函數
????????初始化WinSocket DLL后,通過`scoket`函數和`WSASocket`函數來創建套接字。該函數調用成功后,會返回一個新建的套接字句柄。
SOCKET WSAAPI socket(_In_ int af, //通信協議族_In_ int type, //套接字類型_In_ int protocol);//傳輸協議
af:
??? - `AF_INET`:internet協議(IP V4)
??? - `AF_IRDA`:紅外協議
??? - `AF_BTH`:藍牙協議
type:
??? - `SOCK_STREAM`:流式socket(需建立連接,通信過程可靠TCP)
??? - `SOCK_DGRAM`:數據報socket(無需建立連接,通訊過程不可靠UDP)
??? - `SOCK_RAW`:原始套接字
Protocol:
??? - 對于`SOCK_STREAM`套接字類型,該字段為`IPPROTO_TCP`或者`0`。
??? - 對于`SOCK_DGRAM`套接字類型,該字段為`IPPROTO_UDP`或者`0`。
(1)流套接字????????
????????流套接字能夠提供雙向的數據流服務,數據傳輸是有序的,不會重復,也不存在記錄邊界,特別適合處理大量數據的情況。在網絡傳輸層,它可以根據需要把數據分散成合適大小的數據包,或者把數據包集中起來。
????????使用流套接字通信時,雙方得先建立一條通路。這么做一方面能確定雙方之間的傳輸路線,另一方面能確保雙方都處于活動狀態,隨時可以互相響應。不過,建立這樣一個通信信道可不容易,要耗費不少資源。另外,大多數面向連接的協議,為了保證數據發送準確無誤,往往得做一些額外的計算來驗證數據的正確性,這又進一步增加了開銷 。
(2)數據報套接字????????
????????數據報套接字能實現雙向的數據流動。但它有個問題,沒辦法確保數據在傳輸過程中是可靠的,也不能保證數據按順序到達,還可能出現重復數據。打個比方,一個進程通過數據報套接字接收信息時,可能會發現收到的信息跟發送時的順序不一樣,甚至還會收到重復的內容。
????????數據報套接字在工作時不需要建立連接,發送端發送信息后,它不管接收端是不是在監聽,也不關心接收端有沒有按時收到信息。正因為這樣,數據報的可靠性比較差。所以在使用數據報套接字編程時,程序員得自己想辦法去管理數據報的順序,還要確保數據的可靠性 。
1.5 字節序轉換
????????不同的計算機有時使用不同的字節順序存儲數據。任何從Winsock函數對IP地址和端口號的引用,以及傳送給Winsock函數的IP地址和端口都是按照網絡順序組織的。
- 將32位數從網絡字節轉換成主機字節(大端到小端):
u_long ntohl(u_long hostlong);
- 將16位數從網絡字節轉換成主機字節(大端到小端):
u_short ntohs(u_short short);
- 將32位數從主機字節轉換成網絡字節(小端到大端):
u_long htonl(u_long hostlong);
- 將16位數從主機字節轉換成網絡字節(小端到大端):
U_short htons(u short short);
1.6 綁定套接字
`bind()`函數將套接字綁定到一個已知的地址上。
int bind(SOCKET s, //套接字struct sockaddr FAR*name,//地址結構體變量(IP,端口,協議簇int namelen //Sockaddr結構長度
);
示例:
sockaddr_in addr;
addr.sin_family = AF_INET; //地址家族
addr.sin_port = htons(1234); //端口號
addr.sin_addr.S_un.S_addr = inet_addr("192.168.1.100"); //IP地址 本地:127.0.0.1
//3.綁定套接字
nErrCode = bind(sock,(sockaddr*)&addr, //套接字sizeof(sockaddr_in));//IP定址結構體大小
1.7 監聽
`listen()`函數將套接字設置為監聽模式。
1.8 連接
`accept`函數實現接收一個連接請求。
SOCKET accept(SOCKET s, //監聽套接字struct sockaddr FAR*addr,int FAR*addrlen
);
該函數返回請求連接的套接字句柄。
示例:
SOCKET ClientSocket = accept(sock,(sockaddr*)&addrClient, //返回請求連接主機的地址&nAddrSize); //sockaddr_in的大小
示例2:域名解析
hostent *phostent;
in_addr in; //指向hostent結構的指針 //IPV4地址結構
if((phostent = gethostbyname("www.15pb.com"))==NULL){printf("gethostbyname()錯誤:%d",WSAGetLastError());
}
else{//拷貝4字節的IP地址到IPV4地址結構memcpy(&in,phostent->h_addr,4);printf("主機%s的IP地址是:",phostent->h_name);printf("%s",inet_ntoa(in));
1.9 接收數據
`Recv()`函數用于接收數據。
int recv(SOCKET s, char *buf, //接收數據緩沖區int len,int flags
);
????????該函數返回接收到的數據實際長度,最后一個參數可以是`0`、`MSG_PEEK`和`MSG_OOB`。
- `0`表示無特殊行為。
- `MSG_PEEK`表示會使有用的數據被復制到接收緩沖區,但沒有從系統中將其刪除。
- `MSG_OOB`表示處理帶外數據。
示例:
char buf[1024]={0};
nRecvSize = recv(sockClient, buf, 1024, 0);
1.10 發送數據
`send()`函數用于發送數據。
int send(SOCKET s, char *buf, //發送數據緩沖區int len,int flags
);
示例:
send(Sock, message, nSize, 0);
1.11 關閉套接字
????????`Closecocket()`函數關閉套接字,釋放所占資源。
closecocket(SOCKET s //要關閉的套接字);
????????當調用該函數釋放套接字后,如果再使用該套接字執行函數調用,則會失敗并返回`WSAENOTSOCK`錯誤。
二、UDP連接流程
1. 包含必要的頭文件及庫:
#include <winsock2.h>
#pragma comment(lib,"ws2_32.lib")
2. 指定需要使用的Winsock規范的最高版本,并初始化Winsock,裝入Winsock.dll:
WSAStartup(MAKEWORD(2,2),&wsaDATA);
3. 創建套接字:
socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);
4. 綁定IP和端口:
bind(sock,(sockaddr*)&addr,sizeof(sockaddr_in));
5. 接收數據:
recvfrom(sockClient,buf,1024,0,(sockaddr*)&fromAddr,&fromLen);
6. 發送數據:
sendto(Sock,message,nSize,0,(sockaddr*)&toAddr,toLen);
7. 在調用“closesocket”函數之后,但是在程序結束之前需要清理Winsock:
closesocket(sock); //關閉套接字
WSACleanup();
2.1 接收數據
`recvfrom()`函數用于接收數據,并且返回發送數據主機的地址。
recvfrom(SOCKET s, //用來接收數據的套接字char FAR*buf, //接收數據的緩沖區int len, //接收緩沖區的長度int flags, //一般為0struct sockaddr*to, //接收的地址結構int FAR*fromlen //sockaddr結構大小
);
注:函數的返回值,是接收到的大小。
2.2 發送數據
`sendto()`函數用于發送數據。
sendto(SOCKET s,const char FAR*buf,int len,int flags,const struct sockaddr*to,int tolen
);
- `s`:用來發送數據的套接字。
- `buf`:發送數據的緩沖區。
- `len`:要發送數據的長度。
- `flags`:一般為`0`。
- `to`:目標地址和端口。
- `tolen`:`sockaddr`結構大小。
如果該函數成功則返回發送數據的字節數。
需要注意以下兩點:
????????1. 在UDP編程里,從程序編寫的角度來看,很難明確區分出服務端和客戶端。簡單來說,誰提供服務,就把誰當作服務端。
????????2. 還有一點要留意,當剛創建好socket時,直接調用sendto函數是可行的,此時不需要手動綁定,系統會自動進行綁定操作。
三、阻塞與非阻塞模式
阻塞模式
????????在阻塞模式中,一旦執行操作函數,它就會一直處于等待狀態,不會馬上給出返回結果。執行這個函數的線程也會卡在這兒,只有當特定條件滿足了,函數才會返回。打個比方,就像快遞員通知你今天會有個快遞送達,卻沒告訴你具體時間。在阻塞模式下,你就只能一直在校門口干等著快遞,這一整天別的事都做不了。
非阻塞模式
????????要是處于非阻塞模式,操作函數執行后會立刻返回,執行這個函數的線程能接著往下運行。同樣拿快遞的例子來說,快遞員告知你今天有快遞,沒說具體時間,你不用一直守在校門口,而是每隔30分鐘去校門口瞅瞅快遞到了沒,要是沒到就回來繼續做自己原來的事。
????????為了避免線程長時間被阻塞,提升線程的使用效率,就出現了非阻塞模型。我們可以通過調用`ioctlsocket()`函數,來讓socket明確處于非阻塞模式 。
int ioctlsocket(_In_ SOCKET s,_In_ long cmd,_Inout_ u_long *argp
);
示例:
//2.設置套接字非阻塞模式
unsigned long ul = 1; //設置套接字選項
int nRet = ioctlsocket(sSocket,FIONBIO,&ul);
四、示例代碼
4.1 TCP協議代碼
服務端:
#include <winsock2.h> // Winsock庫頭文件
#include <ws2tcpip.h> // 提供IP地址轉換函數
#include <iostream> // 標準輸入輸出流
#pragma comment(lib, "Ws2_32.lib") // 鏈接Winsock庫// 初始化Winsock庫
bool InitWinSock() {WSADATA wsaData; // 用于存儲Winsock庫的初始化信息// 調用WSAStartup初始化Winsock庫,版本2.2if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {std::cerr << "WSAStartup failed!" << std::endl;return false; // 初始化失敗返回false}return true; // 初始化成功返回true
}// 創建并綁定套接字
SOCKET CreateAndBindSocket(const char* ip, int port) {// 創建TCP套接字SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);if (sock == INVALID_SOCKET) {std::cerr << "Socket creation failed: " << WSAGetLastError() << std::endl;return INVALID_SOCKET; // 創建套接字失敗返回INVALID_SOCKET}sockaddr_in addr{}; // 定義一個IPv4地址結構體addr.sin_family = AF_INET; // 地址族為IPv4addr.sin_port = htons(port); // 設置端口號,轉換為網絡字節序// 將IP地址從字符串轉換為二進制格式inet_pton(AF_INET, ip, &addr.sin_addr);// 綁定套接字到指定IP和端口if (bind(sock, (sockaddr*)&addr, sizeof(addr)) == SOCKET_ERROR) {std::cerr << "Bind failed: " << WSAGetLastError() << std::endl;closesocket(sock); // 綁定失敗關閉套接字return INVALID_SOCKET; // 返回INVALID_SOCKET}return sock; // 返回綁定成功的套接字
}// 主函數
int main() {// 初始化Winsock庫if (!InitWinSock()) return 1;const char* serverIp = "192.168.1.100"; // 服務器IP地址int serverPort = 1234; // 服務器端口號// 創建并綁定套接字SOCKET serverSocket = CreateAndBindSocket(serverIp, serverPort);if (serverSocket == INVALID_SOCKET) {WSACleanup(); // 釋放Winsock資源return 1;}// 開始監聽連接請求,SOMAXCONN為最大連接數if (listen(serverSocket, SOMAXCONN) == SOCKET_ERROR) {std::cerr << "Listen failed: " << WSAGetLastError() << std::endl;closesocket(serverSocket); // 監聽失敗關閉套接字WSACleanup(); // 釋放Winsock資源return 1;}std::cout << "Server listening on " << serverIp << ":" << serverPort << std::endl;// 主循環,處理客戶端連接while (true) {sockaddr_in clientAddr{}; // 存儲客戶端地址信息int clientAddrSize = sizeof(clientAddr);// 接受客戶端連接,返回客戶端套接字SOCKET clientSocket = accept(serverSocket, (sockaddr*)&clientAddr, &clientAddrSize);if (clientSocket == INVALID_SOCKET) {std::cerr << "Accept failed: " << WSAGetLastError() << std::endl;continue; // 接受失敗繼續循環}char clientIp[INET_ADDRSTRLEN]; // 存儲客戶端IP地址字符串// 將客戶端IP地址從二進制轉換為字符串格式inet_ntop(AF_INET, &clientAddr.sin_addr, clientIp, INET_ADDRSTRLEN);std::cout << "Client connected: " << clientIp << std::endl;char buffer[1024]; // 接收數據的緩沖區while (true) {// 接收客戶端發送的數據int bytesReceived = recv(clientSocket, buffer, sizeof(buffer), 0);if (bytesReceived <= 0) {std::cerr << "Client disconnected or recv failed: " << WSAGetLastError() << std::endl;break; // 接收失敗或客戶端斷開連接,退出循環}buffer[bytesReceived] = '\0'; // 確保字符串以NULL結尾std::cout << "Received: " << buffer << std::endl; // 打印接收到的數據}closesocket(clientSocket); // 關閉客戶端套接字}closesocket(serverSocket); // 關閉服務器套接字WSACleanup(); // 釋放Winsock資源return 0;
}
客戶端:
#include <winsock2.h> // Winsock庫頭文件
#include <ws2tcpip.h> // 提供IP地址轉換函數
#include <iostream> // 標準輸入輸出流
#pragma comment(lib, "Ws2_32.lib") // 鏈接Winsock庫// 初始化Winsock庫
bool InitWinSock() {WSADATA wsaData; // 用于存儲Winsock庫的初始化信息// 調用WSAStartup初始化Winsock庫,版本2.2if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {std::cerr << "WSAStartup failed!" << std::endl;return false; // 初始化失敗返回false}return true; // 初始化成功返回true
}// 主函數
int main() {// 初始化Winsock庫if (!InitWinSock()) return 1;const char* serverIp = "127.0.0.1"; // 服務器IP地址int serverPort = 1234; // 服務器端口號// 創建TCP套接字SOCKET clientSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);if (clientSocket == INVALID_SOCKET) {std::cerr << "Socket creation failed: " << WSAGetLastError() << std::endl;WSACleanup(); // 釋放Winsock資源return 1;}sockaddr_in serverAddr{}; // 定義一個IPv4地址結構體serverAddr.sin_family = AF_INET; // 地址族為IPv4serverAddr.sin_port = htons(serverPort); // 設置端口號,轉換為網絡字節序// 將IP地址從字符串轉換為二進制格式inet_pton(AF_INET, serverIp, &serverAddr.sin_addr);// 連接到服務器if (connect(clientSocket, (sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {std::cerr << "Connection failed: " << WSAGetLastError() << std::endl;closesocket(clientSocket); // 連接失敗關閉套接字WSACleanup(); // 釋放Winsock資源return 1;}std::cout << "Connected to server at " << serverIp << ":" << serverPort << std::endl;char message[1024]; // 存儲發送的消息std::cout << "Enter message: ";std::cin.getline(message, sizeof(message)); // 從標準輸入讀取消息// 發送消息到服務器if (send(clientSocket, message, static_cast<int>(strlen(message)), 0) == SOCKET_ERROR) {std::cerr << "Send failed: " << WSAGetLastError() << std::endl;}closesocket(clientSocket); // 關閉客戶端套接字WSACleanup(); // 釋放Winsock資源return 0;
}
4.2 UDP協議代碼
#include <winsock2.h> // Winsock庫頭文件
#include <ws2tcpip.h> // 提供IP地址轉換函數
#include <iostream> // 標準輸入輸出流
#pragma comment(lib, "Ws2_32.lib") // 鏈接Winsock庫// 函數名稱:InitWinSock
// 功能:初始化Winsock庫
// 返回值:成功返回TRUE,失敗返回FALSE
BOOL InitWinSock() {WSADATA wsaData; // 用于存儲Winsock庫的初始化信息// 調用WSAStartup初始化Winsock庫,版本2.2int nResult = WSAStartup(MAKEWORD(2, 2), &wsaData);if (nResult != 0) { // 初始化失敗std::cerr << "WSAStartup failed with error: " << nResult << std::endl;return FALSE;}return TRUE; // 初始化成功
}// 函數名稱:InitServer
// 功能:初始化UDP服務器
// 返回值:成功返回true,失敗返回false
bool InitServer() {// 1. 初始化Winsock庫if (!InitWinSock()) {return false;}// 2. 創建UDP套接字SOCKET serverSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);if (serverSocket == INVALID_SOCKET) {std::cerr << "Socket creation failed: " << WSAGetLastError() << std::endl;WSACleanup(); // 釋放Winsock資源return false;}// 3. 定義服務器地址sockaddr_in serverAddr{};serverAddr.sin_family = AF_INET; // 地址族為IPv4serverAddr.sin_port = htons(1234); // 設置端口號,轉換為網絡字節序// 將IP地址從字符串轉換為二進制格式inet_pton(AF_INET, "192.168.199.207", &serverAddr.sin_addr);// 4. 綁定套接字到指定IP和端口if (bind(serverSocket, (sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {std::cerr << "Bind failed: " << WSAGetLastError() << std::endl;closesocket(serverSocket); // 綁定失敗關閉套接字WSACleanup(); // 釋放Winsock資源return false;}std::cout << "Server started and listening on 192.168.199.207:1234" << std::endl;// 5. 主循環,接收客戶端數據while (true) {char buffer[1024] = {0}; // 接收數據的緩沖區sockaddr_in clientAddr{}; // 存儲客戶端地址信息int clientAddrSize = sizeof(clientAddr);// 接收客戶端發送的數據int bytesReceived = recvfrom(serverSocket, buffer, sizeof(buffer), 0,(sockaddr*)&clientAddr, &clientAddrSize);if (bytesReceived == SOCKET_ERROR) {std::cerr << "recvfrom failed: " << WSAGetLastError() << std::endl;break; // 接收失敗退出循環}// 打印接收到的數據buffer[bytesReceived] = '\0'; // 確保字符串以NULL結尾char clientIp[INET_ADDRSTRLEN];inet_ntop(AF_INET, &clientAddr.sin_addr, clientIp, INET_ADDRSTRLEN); // 將客戶端IP轉換為字符串std::cout << "Received from " << clientIp << ": " << buffer << std::endl;}// 6. 關閉套接字并釋放資源closesocket(serverSocket);WSACleanup();return true;
}// 主函數
int main() {if (!InitServer()) {std::cerr << "Server initialization failed!" << std::endl;return 1;}return 0;
}
??????? 這篇網絡編程基礎的內容全面介紹了套接字相關知識,涵蓋了Berkeley Socket和WinSocket的起源與發展,詳細闡述了TCP和UDP的連接流程,包括各個步驟中涉及的函數使用方法、參數含義,還介紹了阻塞與非阻塞模式的概念及設置方式,并通過完整的TCP和UDP協議示例代碼,讓讀者能夠更直觀地理解和實踐網絡編程中的關鍵操作。
?