Windows Socket 程式設計離不開三個核心結構體,理解它們的記憶體佈局與轉換關係是寫出正確網路代碼的前提。
| 結構體 | 用途 | 關鍵欄位 |
|---|---|---|
sockaddr |
通用 Socket 位址結構,供 API 參數使用 | sa_family (地址族)、sa_data[14] (原始位址資料) |
sockaddr_in |
IPv4 專用,填寫方便 | sin_family、sin_port (網路序)、sin_addr (in_addr)、sin_zero[8] (填充對齊) |
in_addr |
32 位 IPv4 位址 | S_un.S_addr (u_long) |
使用規則:實際填充時使用 sockaddr_in,呼叫 bind/connect/accept 時強制轉型為 (sockaddr*)。這是歷史遺留設計,sockaddr 早期定義過大,後來為方便 IPv4 填寫才引入 sockaddr_in,兩者記憶體大小相同。
htons/htonl (Host→Network),ntohs/ntohl (Network→Host)。x86 為小端序,網路標準為大端序,**跨機器通訊務必轉換**。inet_addr("192.168.1.1") → u_long;inet_ntoa(in_addr) → char* (靜態緩衝區,非執行緒安全)。Server: socket() → bind() → listen() → accept() → send/recv → closesocket()
Client: socket() → connect() → send/recv → closesocket()
| 函式 | 關鍵參數 | 常見誤區 |
|---|---|---|
socket | AF_INET, SOCK_STREAM, IPPROTO_TCP | 回傳 INVALID_SOCKET 表失敗,非 0 |
bind | 本地 IP + Port | Server 必須 bind;Client 通常不需顯式 bind(connect 會隱式綁定暫時埠) |
listen | backlog (待處理佇列上限) | 常用 SOMAXCONN 讓系統決定 |
accept | 輸出參數回傳 Client 位址 | **阻塞**直到有連線;回傳**新 Socket** 專供該 Client 通訊 |
connect | 目標 IP + Port | 內部會自動挑選可用埠並 bind |
以下代碼已整理為可直接編譯的單檔版本,採用 WS2_32.lib,適用於 Visual Studio 或 MinGW-w64。
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")
#define PORT 4600
#define BACKLOG SOMAXCONN
int main() {
WSADATA wsa;
if (WSAStartup(MAKEWORD(2,2), &wsa) != 0) {
fprintf(stderr, "WSAStartup 失敗:%d\n", WSAGetLastError());
return 1;
}
SOCKET listenSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (listenSock == INVALID_SOCKET) {
fprintf(stderr, "socket 失敗:%d\n", WSAGetLastError());
WSACleanup(); return 1;
}
sockaddr_in svr = {0};
svr.sin_family = AF_INET;
svr.sin_addr.s_addr = INADDR_ANY; // 監聽所有介面
svr.sin_port = htons(PORT);
if (bind(listenSock, (sockaddr*)&svr, sizeof(svr)) == SOCKET_ERROR) {
fprintf(stderr, "bind 失敗:%d\n", WSAGetLastError());
closesocket(listenSock); WSACleanup(); return 1;
}
if (listen(listenSock, BACKLOG) == SOCKET_ERROR) {
fprintf(stderr, "listen 失敗:%d\n", WSAGetLastError());
closesocket(listenSock); WSACleanup(); return 1;
}
printf("Server 啟動,監聽埠 %d ...\n", PORT);
while (1) {
sockaddr_in cli = {0};
int cliLen = sizeof(cli);
SOCKET connSock = accept(listenSock, (sockaddr*)&cli, &cliLen);
if (connSock == INVALID_SOCKET) {
fprintf(stderr, "accept 失敗:%d\n", WSAGetLastError());
continue;
}
printf("Client 連入:%s:%d\n", inet_ntoa(cli.sin_addr), ntohs(cli.sin_port));
char buf[1024];
int n = recv(connSock, buf, sizeof(buf)-1, 0);
if (n > 0) {
buf[n] = '\0';
printf("收到:%s\n", buf);
send(connSock, buf, n, 0); // 原樣回傳
}
closesocket(connSock);
}
closesocket(listenSock);
WSACleanup();
return 0;
}
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")
#define SERVER_IP "127.0.0.1"
#define PORT 4600
int main() {
WSADATA wsa;
if (WSAStartup(MAKEWORD(2,2), &wsa) != 0) return 1;
SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sock == INVALID_SOCKET) { WSACleanup(); return 1; }
sockaddr_in svr = {0};
svr.sin_family = AF_INET;
svr.sin_port = htons(PORT);
inet_pton(AF_INET, SERVER_IP, &svr.sin_addr); // 比 inet_addr 更現代
if (connect(sock, (sockaddr*)&svr, sizeof(svr)) == SOCKET_ERROR) {
fprintf(stderr, "connect 失敗:%d\n", WSAGetLastError());
closesocket(sock); WSACleanup(); return 1;
}
const char* msg = "Hello from Client!";
send(sock, msg, (int)strlen(msg), 0);
char buf[1024];
int n = recv(sock, buf, sizeof(buf)-1, 0);
if (n > 0) { buf[n] = '\0'; printf("Server 回應:%s\n", buf); }
closesocket(sock);
WSACleanup();
return 0;
}
MAKEWORD(2,2)。send 可能發送不完整,recv 可能分多次讀取;生產環境需實作迴圈收發。select/WSAAsyncSelect/IOCP 非同步模型。htons、htonl、inet_pton 皆為必須。WSAGetLastError(),而非 GetLastError()。