Windows网络编程案例教程
目录
-
第一部分:基础准备
(图片来源网络,侵删)- 1 什么是套接字?
- 2 Winsock库简介
- 3 开发环境配置
- 4 网络字节序
-
第二部分:核心API详解
- 1 初始化与清理:
WSAStartup()和WSACleanup() - 2 创建套接字:
socket() - 3 绑定地址和端口:
bind() - 4 监听连接:
listen() - 5 接受连接:
accept() - 6 连接服务器:
connect() - 7 数据传输:
send()和recv() - 8 关闭套接字:
closesocket()
- 1 初始化与清理:
-
第三部分:经典案例 - Echo服务器与客户端
- 1 案例目标
- 2 服务器端代码详解
- 3 客户端代码详解
- 4 如何编译与运行
-
第四部分:进阶与最佳实践
- 1 阻塞 vs. 非阻塞模式
- 2
select()模型简介 - 3 错误处理的重要性
- 4 代码重构与封装
第一部分:基础准备
1 什么是套接字?
套接字是网络编程的API,它就像一个“网络插座”,你的程序可以通过这个插座向网络上的另一个程序发送数据,或者从另一个程序接收数据,它隐藏了底层复杂的网络协议(如TCP/IP)细节,为程序员提供了一个统一的接口。

在Windows中,我们主要使用两种套接字:
- 流式套接字 (
SOCK_STREAM):使用TCP协议,提供面向连接的、可靠的数据传输服务,数据按顺序、无差错地到达,就像打电话,必须先建立连接。 - 数据报套接字 (
SOCK_DGRAM):使用UDP协议,提供无连接的、尽最大努力的数据传输服务,数据可能丢失、重复或乱序,就像寄明信片,寄出后无法保证对方一定能按顺序收到。
本教程以最常用的TCP为例。
2 Winsock库简介
Windows下的网络编程接口叫做 Winsock (Windows Sockets),它起源于Unix的Berkeley Sockets (BSD Sockets) 接口,并进行了扩展以适应Windows环境。
要使用Winsock,你需要包含两个头文件:

#include <winsock2.h>:核心Winsock函数和数据结构的定义。#include <ws2tcpip.h>:包含更新的IP地址函数(如inet_pton)。
你需要链接一个库文件:
#pragma comment(lib, "ws2_32.lib"):告诉链接器在编译时链接ws2_32.lib。
3 开发环境配置
以Visual Studio为例:
- 创建一个新的C++控制台应用程序项目。
- 在源代码文件中,添加上述头文件和库链接指令。
- 确保你的项目配置为使用Windows桌面开发工具集。
4 网络字节序
计算机在存储多字节数据(如端口号、IP地址)时,有两种方式:大端序(高位字节在前)和小端序(低位字节在前),网络协议规定使用大端序,也称为网络字节序。
Windows提供了四个宏进行转换:
htons():Host to Network Short (16位)htonl():Host to Network Long (32位)ntohs():Network to Host Shortntohl():Network to Host Long
在将本机的端口号或IP地址传递给bind()等函数之前,必须使用htons()或htonl()进行转换。
第二部分:核心API详解
我们将按照服务器和客户端的通信流程来介绍这些函数。
1 初始化与清理:WSAStartup() 和 WSACleanup()
WSAStartup() 必须是第一个调用的Winsock函数,它向操作系统声明:“我准备使用Winsock了,请加载相关库并初始化。”
- 参数:
wVersionRequested:你请求使用的Winsock版本(如MAKEWORD(2, 2)表示请求2.2版本)。lpWSAData:一个指向WSADATA结构体的指针,用于返回加载的库的详细信息。
- 返回值:成功返回
0,失败返回错误码。
WSACleanup() 是最后一个调用的Winsock函数,它用于释放资源,通知操作系统可以卸载Winsock库了。
2 创建套接字:socket()
socket() 函数用于创建一个通信端点,即套接字。
- 参数:
af:地址族,通常是AF_INET(IPv4) 或AF_INET6(IPv6)。type:套接字类型,SOCK_STREAM(TCP) 或SOCK_DGRAM(UDP)。protocol:协议,设为0表示让系统自动选择(TCP为IPPROTO_TCP,UDP为IPPROTO_UDP)。
- 返回值:成功返回一个套接字句柄(一个
SOCKET类型的整数),失败返回INVALID_SOCKET。
3 绑定地址和端口:bind()
服务器端需要调用 bind() 将创建的套接字与一个特定的IP地址和端口号绑定,这样客户端才能知道连接到哪里。
- 参数:
s:由socket()返回的套接字。addr:指向sockaddr结构体的指针,这是一个通用地址结构,实际使用时通常转换为指向sockaddr_in(IPv4) 或sockaddr_in6(IPv6) 的指针。namelen:addr指向的结构体的大小。
sockaddr_in结构体:struct sockaddr_in { short sin_family; // 地址族,必须设为 AF_INET u_short sin_port; // 16位端口号,必须用 htons() 转换 struct in_addr sin_addr; // 32位IP地址 char sin_zero[8]; // 填充,不用管 };
4 监听连接:listen()
bind() 之后,服务器调用 listen() 开始进入监听状态,等待客户端的连接请求。
- 参数:
s:已绑定的套接字。backlog:等待连接队列的最大长度。
- 返回值:成功返回
0,失败返回SOCKET_ERROR。
5 接受连接:accept()
当 listen() 检测到有客户端连接请求时,服务器调用 accept() 来接受这个连接。accept() 会创建一个新的套接字专门用于与这个客户端通信,同时返回原始的监听套接字可以继续接受其他客户端的连接。
- 参数:
s:正在监听的套接字。addr:一个指向sockaddr结构体的指针,用于存放客户端的地址信息(可选,可设为NULL)。addrlen:指向addr大小的指针(可选,可设为NULL)。
- 返回值:成功返回一个新的套接字(用于通信),失败返回
INVALID_SOCKET。
6 连接服务器:connect()
客户端调用 connect() 主动与服务器建立连接。
- 参数:
s:客户端套接字。name:指向服务器sockaddr结构体的指针。namelen:name指向的结构体大小。
- 返回值:成功返回
0,失败返回SOCKET_ERROR。
7 数据传输:send() 和 recv()
一旦连接建立(服务器通过accept,客户端通过connect),就可以使用这对函数进行数据收发。
send():通过已连接的套接字发送数据。- 参数:
s(套接字),buf(数据缓冲区),len(数据长度),flags(通常设为0)。 - 返回值:成功返回实际发送的字节数,失败返回
SOCKET_ERROR,连接断开可能返回0。
- 参数:
recv():通过已连接的套接字接收数据。- 参数:
s(套接字),buf(接收数据的缓冲区),len(缓冲区大小),flags(通常设为0)。 - 返回值:成功返回实际接收的字节数,失败返回
SOCKET_ERROR,连接断开会返回0。
- 参数:
8 关闭套接字:closesocket()
当通信结束时,调用 closesocket() 关闭套接字,释放资源。
- 参数:
s(要关闭的套接字)。 - 返回值:成功返回
0,失败返回SOCKET_ERROR。
第三部分:经典案例 - Echo服务器与客户端
1 案例目标
- 服务器:在指定端口(如 8888)上监听客户端连接,当客户端连接后,等待客户端发送字符串,然后将收到的字符串原样回显给客户端。
- 客户端:连接到服务器的指定IP和端口,从用户输入读取一行字符串,发送给服务器,然后接收并打印服务器回显的字符串。
2 服务器端代码详解
// EchoServer.cpp
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>
#include <string>
// 链接 Winsock 库
#pragma comment(lib, "ws2_32.lib")
#define DEFAULT_PORT "8888"
#define DEFAULT_BUFFER_LENGTH 512
int main() {
// 1. 初始化 Winsock
WSADATA wsaData;
int iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0) {
printf("WSAStartup failed: %d\n", iResult);
return 1;
}
// 2. 创建监听套接字
SOCKET ListenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (ListenSocket == INVALID_SOCKET) {
printf("Error at socket(): %ld\n", WSAGetLastError());
WSACleanup();
return 1;
}
// 3. 绑定套接字
struct addrinfo hints;
struct addrinfo* result = NULL;
ZeroMemory(&hints, sizeof(hints));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
hints.ai_flags = AI_PASSIVE;
// 解析地址和端口
iResult = getaddrinfo(NULL, DEFAULT_PORT, &hints, &result);
if (iResult != 0) {
printf("getaddrinfo failed: %d\n", iResult);
freeaddrinfo(result);
closesocket(ListenSocket);
WSACleanup();
return 1;
}
// 绑定
iResult = bind(ListenSocket, result->ai_addr, (int)result->ai_addrlen);
if (iResult == SOCKET_ERROR) {
printf("bind failed: %d\n", WSAGetLastError());
freeaddrinfo(result);
closesocket(ListenSocket);
WSACleanup();
return 1;
}
freeaddrinfo(result); // 绑定后就可以释放了
// 4. 开始监听
iResult = listen(ListenSocket, SOMAXCONN);
if (iResult == SOCKET_ERROR) {
printf("listen failed: %d\n", WSAGetLastError());
closesocket(ListenSocket);
WSACleanup();
return 1;
}
printf("Server is listening on port %s...\n", DEFAULT_PORT);
// 5. 接受客户端连接
struct sockaddr_in clientAddr;
int clientAddrLen = sizeof(clientAddr);
SOCKET ClientSocket = accept(ListenSocket, (struct sockaddr*)&clientAddr, &clientAddrLen);
if (ClientSocket == INVALID_SOCKET) {
printf("accept failed: %d\n", WSAGetLastError());
closesocket(ListenSocket);
WSACleanup();
return 1;
}
printf("Client connected: %s\n", inet_ntoa(clientAddr.sin_addr));
// 6. 与客户端通信
char recvbuf[DEFAULT_BUFFER_LENGTH];
int recvbuflen = DEFAULT_BUFFER_LENGTH;
do {
iResult = recv(ClientSocket, recvbuf, recvbuflen, 0);
if (iResult > 0) {
printf("Bytes received: %d\n", iResult);
printf("Echo: %s", recvbuf); // 打印收到的消息
// Echo back to the client
iResult = send(ClientSocket, recvbuf, iResult, 0);
if (iResult == SOCKET_ERROR) {
printf("send failed: %d\n", WSAGetLastError());
closesocket(ClientSocket);
WSACleanup();
return 1;
}
printf("Bytes sent: %d\n", iResult);
} else if (iResult == 0) {
printf("Connection closing...\n");
} else {
printf("recv failed: %d\n", WSAGetLastError());
closesocket(ClientSocket);
WSACleanup();
return 1;
}
} while (iResult > 0); // 当recv返回0时,表示客户端关闭了连接
// 7. 关闭套接字
iResult = shutdown(ClientSocket, SD_SEND);
if (iResult == SOCKET_ERROR) {
printf("shutdown failed: %d\n", WSAGetLastError());
closesocket(ClientSocket);
WSACleanup();
return 1;
}
closesocket(ClientSocket);
closesocket(ListenSocket);
WSACleanup();
printf("Server shutdown.\n");
return 0;
}
3 客户端代码详解
// EchoClient.cpp
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>
#include <string>
#pragma comment(lib, "ws2_32.lib")
#define DEFAULT_PORT "8888"
#define DEFAULT_BUFFER_LENGTH 512
int main(int argc, char** argv) {
// 1. 初始化 Winsock
WSADATA wsaData;
int iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0) {
printf("WSAStartup failed: %d\n", iResult);
return 1;
}
// 2. 创建客户端套接字
SOCKET ConnectSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (ConnectSocket == INVALID_SOCKET) {
printf("Error at socket(): %ld\n", WSAGetLastError());
WSACleanup();
return 1;
}
// 3. 连接服务器
struct sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(atoi(DEFAULT_PORT));
// 将 "127.0.0.1" 转换为网络地址
inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);
iResult = connect(ConnectSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr));
if (iResult == SOCKET_ERROR) {
printf("connect failed: %d\n", WSAGetLastError());
closesocket(ConnectSocket);
WSACleanup();
return 1;
}
printf("Connected to server.\n");
// 4. 发送和接收数据
char sendbuf[DEFAULT_BUFFER_LENGTH];
char recvbuf[DEFAULT_BUFFER_LENGTH];
int recvbuflen = DEFAULT_BUFFER_LENGTH;
printf("Enter message to send (or 'quit' to exit): ");
gets_s(sendbuf, DEFAULT_BUFFER_LENGTH); // 注意:gets_s不安全,仅用于演示
iResult = send(ConnectSocket, sendbuf, (int)strlen(sendbuf), 0);
if (iResult == SOCKET_ERROR) {
printf("send failed: %d\n", WSAGetLastError());
closesocket(ConnectSocket);
WSACleanup();
return 1;
}
printf("Bytes sent: %d\n", iResult);
// 5. 接收回显数据
iResult = recv(ConnectSocket, recvbuf, recvbuflen, 0);
if (iResult > 0) {
printf("Bytes received: %d\n", iResult);
printf("Echo from server: %s\n", recvbuf);
} else if (iResult == 0) {
printf("Connection closed by server.\n");
} else {
printf("recv failed: %d\n", WSAGetLastError());
}
// 6. 关闭套接字
iResult = shutdown(ConnectSocket, SD_SEND);
if (iResult == SOCKET_ERROR) {
printf("shutdown failed: %d\n", WSAGetLastError());
closesocket(ConnectSocket);
WSACleanup();
return 1;
}
closesocket(ConnectSocket);
WSACleanup();
return 0;
}
4 如何编译与运行
-
编译:
- Visual Studio: 分别创建两个项目(一个控制台应用叫
EchoServer,一个叫EchoClient),将上述代码分别粘贴到各自的.cpp文件中,然后直接点击“生成” -> “生成解决方案”。 - 命令行 (cl.exe):
cl EchoServer.cpp /link ws2_32.lib cl EchoClient.cpp /link ws2_32.lib
这会生成
EchoServer.exe和EchoClient.exe。
- Visual Studio: 分别创建两个项目(一个控制台应用叫
-
运行:
- 先启动服务器:在命令行中运行
EchoServer.exe,你会看到 "Server is listening on port 8888..."。 - 再启动客户端:打开一个新的命令行窗口,运行
EchoClient.exe。 - 交互:在客户端窗口中输入任意字符串(如 "Hello, Server!"),然后按回车,客户端会收到服务器的回显,并显示出来。
- 关闭:在客户端输入 "quit" 或直接关闭客户端窗口,服务器会检测到连接关闭并退出。
- 先启动服务器:在命令行中运行
第四部分:进阶与最佳实践
1 阻塞 vs. 非阻塞模式
- 阻塞模式:默认模式,当一个函数(如
accept,recv)没有完成其任务时,调用它的线程会暂停,直到任务完成或发生错误。- 优点:代码简单直观。
- 缺点:效率低下,服务器在
accept时会一直卡住,无法处理其他已连接的客户端;在recv时,如果客户端不发数据,服务器也会卡住。
- 非阻塞模式:通过
ioctlsocket()函数设置,当一个函数被调用时,它会立即返回。- 如果任务无法立即完成,它会返回一个错误码
WSAEWOULDBLOCK。 - 优点:线程不会被卡住,可以执行其他任务(如检查其他套接字)。
- 缺点:需要程序员通过循环不断调用函数来检查,实现复杂。
- 如果任务无法立即完成,它会返回一个错误码
2 select() 模型简介
select() 是一种经典的I/O多路复用技术,用于解决阻塞模式下的多客户端处理问题。
它允许你同时监视多个套接字,并告诉你哪些套接字“已准备好”可以进行读、写或异常操作,这样,一个线程就可以管理成百上千个连接,大大提高了效率。
3 错误处理的重要性
本教程中的错误处理非常基础,在生产环境中,必须对每一个可能失败的Winsock调用都进行错误检查,并根据错误码(通过 WSAGetLastError() 获取)采取适当的恢复或退出措施。WSAEINTR(被中断)、WSAECONNRESET(连接被重置)等都需要不同的处理逻辑。
4 代码重构与封装
在实际项目中,直接调用这些底层API会使代码变得臃肿且难以维护,更好的做法是:
- 创建一个
CSocket或TcpConnection类,将套接字句柄和相关的状态封装起来。 - 提供
Connect(),Send(),Receive(),Close()等成员函数,并在内部处理错误检查和资源释放。 - 使用智能指针(如
std::unique_ptr)管理套接字生命周期,防止忘记调用closesocket()导致的资源泄露。
通过这种方式,你可以构建一个更健壮、更易用的网络库,上层业务逻辑代码会变得非常清晰。
这份教程为你打下了坚实的基础,网络编程是一个广阔的领域,掌握TCP/IP后,你还可以进一步学习UDP、原始套接字、SSL/TLS加密、以及更高级的I/O模型(如WSAAsyncSelect, WSAEventSelect, I/O Completion Ports等),祝你学习顺利!
