Windows Socket TCP 编程笔记
1. 基本流程
1.1 TCP 服务端流程
| 步骤 |
函数 |
说明 |
| 0 |
WSAStartup |
初始化 Winsock(必须) |
| 1 |
socket |
创建空 socket |
| 2 |
bind |
绑定本地地址和端口 |
| 3 |
listen |
开始监听 |
| 4 |
accept |
接收客户端连接(阻塞) |
| 5 |
send / recv |
收发数据 |
| 6 |
closesocket |
关闭 socket |
| 7 |
WSACleanup |
清理 Winsock |
1.2 TCP 客户端流程
| 步骤 |
函数 |
说明 |
| 0 |
WSAStartup |
初始化 Winsock(必须) |
| 1 |
socket |
创建空 socket |
| 2 |
connect |
连接服务器 |
| 3 |
send / recv |
收发数据 |
| 4 |
closesocket |
关闭 socket |
| 5 |
WSACleanup |
清理 Winsock |
2. 核心 API 详解
2.1 初始化与清理(含版本校验)
1 2 3 4 5 6 7 8 9 10 11 12 13
| WSADATA wsaData; int iResult; iResult = WSAStartup(MAKEWORD(2, 2), &wsaData); if (iResult != 0) { printf("WSAStartup failed: %d\n", iResult); return 1; }
if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) { printf("Could not find a usable version of Winsock.dll\n"); WSACleanup(); return 1; }
|
说明:
MAKEWORD(2, 2) 表示请求 Winsock 2.2 版本
WSAStartup 成功后,通过 wsaData.wVersion 检查实际加载的版本
LOBYTE / HIBYTE 分别取出版本号的低字节(主版本)和高字节(副版本)
- 版本不符时应调用
WSACleanup 清理后再返回
2.2 创建 Socket
1
| SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);
|
| 参数 |
值 |
说明 |
| 地址族 |
AF_INET |
IPv4 |
| 类型 |
SOCK_STREAM |
流式套接字(TCP) |
| 协议 |
0 |
自动选择 TCP |
2.3 地址结构体
1 2 3 4 5
| SOCKADDR_IN addr; addr.sin_addr.S_un.S_addr = htonl(INADDR_ANY); addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); addr.sin_family = AF_INET; addr.sin_port = htons(6000);
|
字节序转换:
| 函数 |
作用 |
htonl |
32位主机序 → 网络序 |
htons |
16位主机序 → 网络序 |
ntohl |
32位网络序 → 主机序 |
ntohs |
16位网络序 → 主机序 |
inet_addr |
点分十进制 → 32位网络字节序 IP |
inet_ntoa |
32位网络字节序 IP → 点分十进制 |
2.4 服务端核心函数
bind(绑定)
1
| bind(sockServer, (sockaddr*)&addrServer, sizeof(addrServer))
|
将 socket 绑定到本地地址和端口。
listen(监听)
- 第一个参数:已绑定的 socket
- 第二个参数:未决连接队列的最大长度(已完成三次握手但尚未被 accept 的连接数)
accept(接收连接)
1
| SOCKET sockClient = accept(sockServer, (sockaddr*)&addrClient, &len)
|
- 返回值:一个新的 socket,用于与客户端通信(称为通信 socket)
sockServer 仍用于监听新的连接
sockServer 的 TCP 四元组:(本机IP, 本机端口, 0, 0) → 不完整,仅用于监听
sockClient 的 TCP 四元组:(本机IP, 本机端口, 客户端IP, 客户端端口) → 完整,用于通信
2.5 客户端核心函数
connect(连接)
1
| connect(sockClient, (sockaddr*)&addrServer, sizeof(addrServer))
|
主动连接服务器,发起三次握手。
2.6 收发数据
1 2 3 4 5
| send(sockClient, sendBuf, strlen(sendBuf), 0);
recv(sockClient, recvBuf, sizeof(recvBuf), 0);
|
| 参数 |
说明 |
| 第一个 |
socket 句柄 |
| 第二个 |
数据缓冲区 |
| 第三个 |
数据长度 |
| 第四个 |
标志(通常为 0) |
3. 循环收发(处理数据不完整问题)
核心问题:send 和 recv 不保证一次发送/接收完整数据,需要循环处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| int MySocketRecv(int sock, char* buf, int dataSize) { int numsRecvSoFar = 0; int numsRemainingToRecv = dataSize; printf("enter MySocketRecv\n"); while (true) { int bytesRead = recv(sock, &buf[numsRecvSoFar], numsRemainingToRecv, 0); printf("###bytesRead = %d,numsRecvSoFar = %d, numsRemainingToRecv = %d\n", bytesRead, numsRecvSoFar, numsRemainingToRecv); if (bytesRead == numsRemainingToRecv) { return 0; } else if (bytesRead > 0) { numsRecvSoFar += bytesRead; numsRemainingToRecv -= bytesRead; continue; } else if (bytesRead < 0 && errno == EAGAIN) { continue; } else { return -1; } } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| int MySocketSend(int socketNum, unsigned char* data, unsigned dataSize) { unsigned numBytesSentSoFar = 0; unsigned numBytesRemainingToSend = dataSize;
while (1) { int bytesSend = send(socketNum, (char const*)(&data[numBytesSentSoFar]), numBytesRemainingToSend, 0); if (bytesSend == numBytesRemainingToSend) { return 0; } else if (bytesSend > 0) { numBytesSentSoFar += bytesSend; numBytesRemainingToSend -= bytesSend; continue; } else if ((bytesSend < 0) && (errno == 11)) { continue; } else { return -1; } } }
|
4. 服务端完整代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
| #include <stdio.h> #include <stdlib.h>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
int main() { printf("TCP Server\n");
WSADATA wsaData; int iResult; iResult = WSAStartup(MAKEWORD(2, 2), &wsaData); if (iResult != 0) { printf("WSAStartup failed: %d\n", iResult); return 1; } if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) { printf("Could not find a usable version of Winsock.dll\n"); WSACleanup(); return 1; }
SOCKET sockServer = socket(AF_INET, SOCK_STREAM, 0); if (sockServer == INVALID_SOCKET) { printf("socket error: %d\n", GetLastError()); system("pause"); return -1; }
SOCKADDR_IN addrServer; addrServer.sin_addr.S_un.S_addr = htonl(INADDR_ANY); addrServer.sin_family = AF_INET; addrServer.sin_port = htons(6000); if (bind(sockServer, (sockaddr*)&addrServer, sizeof(addrServer)) == SOCKET_ERROR) { printf("bind error: %d\n", GetLastError()); system("pause"); return -1; }
if (listen(sockServer, 5) == SOCKET_ERROR) { printf("listen error: %d\n", GetLastError()); system("pause"); return -1; }
SOCKADDR_IN addrClient; int len = sizeof(addrClient);
while (true) { SOCKET sockClient = accept(sockServer, (sockaddr*)&addrClient, &len);
char sendBuf[100] = { 0 }; sprintf_s(sendBuf, sizeof(sendBuf), "Welcome %s\n", inet_ntoa(addrClient.sin_addr)); int iLen = send(sockClient, sendBuf, strlen(sendBuf), 0);
char recvBuf[100] = { 0 }; iLen = recv(sockClient, recvBuf, sizeof(recvBuf), 0); printf("recvBuf: %s\n", recvBuf);
closesocket(sockClient); }
closesocket(sockServer); WSACleanup(); system("pause");
return 0; }
|
5. 客户端完整代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| #include <stdio.h> #include <stdlib.h>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
int main() { printf("TCP Client\n");
WSADATA wsaData; int iResult; iResult = WSAStartup(MAKEWORD(2, 2), &wsaData); if (iResult != 0) { printf("WSAStartup failed: %d\n", iResult); return 1; } if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) { printf("Could not find a usable version of Winsock.dll\n"); WSACleanup(); return 1; }
SOCKET sockClient = socket(AF_INET, SOCK_STREAM, 0); if (sockClient == INVALID_SOCKET) { printf("socket error: %d\n", GetLastError()); system("pause"); return -1; }
SOCKADDR_IN addrServer; addrServer.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); addrServer.sin_family = AF_INET; addrServer.sin_port = htons(6000); if (connect(sockClient, (sockaddr*)&addrServer, sizeof(addrServer)) == SOCKET_ERROR) { printf("connect error: %d\n", GetLastError()); system("pause"); return -1; }
char recvBuf[100] = { 0 }; int iLen = recv(sockClient, recvBuf, sizeof(recvBuf), 0); printf("recvBuf: %s\n", recvBuf);
char sendBuf[100] = "Hello World"; iLen = send(sockClient, sendBuf, strlen(sendBuf), 0);
closesocket(sockClient); WSACleanup(); system("pause"); return 0; }
|
6. WSAStartup 版本校验说明
| 字段 |
含义 |
MAKEWORD(2, 2) |
构造版本号 2.2(高字节=副版本,低字节=主版本) |
wsaData.wVersion |
系统实际加载的 Winsock 版本 |
LOBYTE(wsaData.wVersion) |
获取主版本号(低字节) |
HIBYTE(wsaData.wVersion) |
获取副版本号(高字节) |
| 版本不符时的处理 |
调用 WSACleanup() 清理后返回 |
7. 关键点总结
| 要点 |
说明 |
| WSAStartup |
必须在所有 Socket 操作前调用,并校验版本号 |
| WSAStartup 失败 |
返回非 0 值,不应继续执行 Socket 操作 |
| 版本校验 |
请求的版本与实际版本不一致时需调用 WSACleanup 后退出 |
| 字节序转换 |
IP 和端口号需使用 hton* / ntoh* 转换 |
| 服务端 socket 类型 |
sockServer 用于监听,sockClient 用于通信(由 accept 创建) |
| TCP 四元组 |
完整连接由 (源IP, 源端口, 目标IP, 目标端口) 唯一标识 |
| send/recv 不完整 |
不能假设一次 send/recv 发送/接收完整数据,需要循环处理 |
| errno 检查 |
非阻塞模式下需检查 errno == EAGAIN(无数据可读)或 errno == 11(缓冲区满) |