Windows下C语言Socket编程从入门到实战:TCP/UDP通信完整教程
Socket是网络编程的核心接口,C语言在Windows上用它做客户端服务器通信时,和Linux有明显区别。Windows用Winsock库,初始化步骤多一步,头文件和链接库也不同。很多新手在编译时报一堆错误,或者程序一运行就卡死,多半卡在这些细节上。下面按实际操作顺序讲清楚,从环境搭建到能跑的代码,再到常见坑,适合刚接触网络编程的人一步步跟着做。
为什么Windows下C语言Socket要单独讲
Linux的socket直接用标准头文件就行,Windows必须先加载Winsock。不初始化就调用socket函数,程序直接崩溃。Winsock2是目前主流版本,支持IPv4和IPv6,兼容性也更好。实际项目里,聊天工具、游戏服务器、物联网设备控制,经常用C语言写底层通信模块,Windows环境又是很多开发者日常机器,所以这套流程必须熟练。
目标读者分两层:刚学C语言的人,按步骤复制代码就能跑通;有一定基础的人,可以直接看进阶部分改造成多线程或非阻塞模式。
开发环境准备
推荐用Visual Studio 2019或2022,社区版免费。也可以用MinGW-w64配合VS Code,但链接库配置稍微麻烦点。
Visual Studio步骤
- 新建空项目,选择C++空项目(C语言兼容)。
- 项目属性 → 链接器 → 输入 → 附加依赖项,填上
ws2_32.lib。 - 源文件后缀用
.c,或者在项目属性里把“编译为”设为C代码。
MinGW步骤
命令行编译时加-lws2_32:
gcc server.c -o server.exe -lws2_32
代码开头必须包含这两个头文件:
#include <winsock2.h>
#include <ws2tcpip.h>
ws2tcpip.h提供getaddrinfo等新接口,比老的gethostbyname更安全。
第一步:初始化Winsock
任何socket调用前都要做这件事,否则socket()直接返回无效值。
WSADATA wsaData;
int result = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (result != 0) {
printf("WSAStartup failed: %d\n", result);
return 1;
}
MAKEWORD(2,2)表示请求Winsock 2.2版本。用完程序退出前调用WSACleanup()释放资源。漏掉这步,多次运行程序会占端口或内存泄漏。
TCP服务器完整实现
服务器流程固定:创建socket → 绑定端口 → 监听 → 接受连接 → 收发数据 → 关闭。
下面是可直接编译运行的最小服务器,监听本地9999端口,收到客户端消息后原样返回。
#include <stdio.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
#define PORT 9999
#define BUF_SIZE 1024
int main() {
WSADATA wsa;
SOCKET listenSock, clientSock;
struct sockaddr_in serverAddr, clientAddr;
int addrLen = sizeof(clientAddr);
char buf[BUF_SIZE];
int recvLen;
// 1. 初始化
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) {
printf("WSAStartup error\n");
return 1;
}
// 2. 创建TCP套接字
listenSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (listenSock == INVALID_SOCKET) {
printf("socket error: %d\n", WSAGetLastError());
WSACleanup();
return 1;
}
// 3. 绑定地址
memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = INADDR_ANY; // 监听所有网卡
serverAddr.sin_port = htons(PORT);
if (bind(listenSock, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {
printf("bind error: %d\n", WSAGetLastError());
closesocket(listenSock);
WSACleanup();
return 1;
}
// 4. 开始监听
if (listen(listenSock, 5) == SOCKET_ERROR) {
printf("listen error\n");
closesocket(listenSock);
WSACleanup();
return 1;
}
printf("服务器启动,监听端口 %d...\n", PORT);
// 5. 接受连接(阻塞)
clientSock = accept(listenSock, (struct sockaddr*)&clientAddr, &addrLen);
if (clientSock == INVALID_SOCKET) {
printf("accept error\n");
closesocket(listenSock);
WSACleanup();
return 1;
}
printf("客户端已连接\n");
// 6. 收发数据
while (1) {
recvLen = recv(clientSock, buf, BUF_SIZE - 1, 0);
if (recvLen > 0) {
buf[recvLen] = '\0';
printf("收到: %s\n", buf);
send(clientSock, buf, recvLen, 0); // 回显
} else if (recvLen == 0) {
printf("客户端断开\n");
break;
} else {
printf("recv error: %d\n", WSAGetLastError());
break;
}
}
// 7. 清理
closesocket(clientSock);
closesocket(listenSock);
WSACleanup();
return 0;
}
关键点说明:
INADDR_ANY让服务器监听本机所有IP,笔记本有线无线都能连。htons把主机字节序转成网络字节序,端口号必须转,IP地址用inet_addr时也要转。accept会阻塞,直到有客户端连上来。想同时处理多个客户端,后面要用线程或select。
TCP客户端对应代码
客户端更简单:创建socket → 连接服务器 → 发送数据。
#include <stdio.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
#define SERVER_IP "127.0.0.1"
#define PORT 9999
#define BUF_SIZE 1024
int main() {
WSADATA wsa;
SOCKET sock;
struct sockaddr_in serverAddr;
char buf[BUF_SIZE];
int sendLen, recvLen;
WSAStartup(MAKEWORD(2, 2), &wsa);
sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sock == INVALID_SOCKET) {
printf("socket failed\n");
return 1;
}
memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(PORT);
inet_pton(AF_INET, SERVER_IP, &serverAddr.sin_addr); // 推荐用这个,老版本用inet_addr
if (connect(sock, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {
printf("connect error: %d\n", WSAGetLastError());
closesocket(sock);
WSACleanup();
return 1;
}
printf("已连接到服务器\n");
while (1) {
printf("输入消息(exit退出): ");
fgets(buf, BUF_SIZE, stdin);
if (strncmp(buf, "exit", 4) == 0) break;
sendLen = send(sock, buf, strlen(buf), 0);
if (sendLen == SOCKET_ERROR) {
printf("send error\n");
break;
}
recvLen = recv(sock, buf, BUF_SIZE - 1, 0);
if (recvLen > 0) {
buf[recvLen] = '\0';
printf("服务器回复: %s", buf);
}
}
closesocket(sock);
WSACleanup();
return 0;
}
先运行服务器,再运行客户端。输入文字就能看到回显。inet_pton比inet_addr更安全,支持IPv6,Windows Vista以后都可用。
UDP通信快速上手
UDP不需要连接,适合游戏位置同步、心跳包这类场景。改动很小:
- 创建socket时用
SOCK_DGRAM和IPPROTO_UDP。 - 服务器不需要
listen和accept,直接recvfrom。 - 客户端用
sendto指定目标地址。
服务器核心代码片段:
SOCKET sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
// bind 同TCP
char buf[BUF_SIZE];
struct sockaddr_in clientAddr;
int addrLen = sizeof(clientAddr);
int len = recvfrom(sock, buf, BUF_SIZE, 0, (struct sockaddr*)&clientAddr, &addrLen);
// 回发
sendto(sock, buf, len, 0, (struct sockaddr*)&clientAddr, addrLen);
UDP没有连接状态,发送后不管对方是否在线,适合实时性要求高但对丢包不敏感的场合。
多客户端处理:简单多线程方案
单线程accept只能处理一个客户端。Windows下用_beginthreadex最方便。
在服务器accept成功后:
#include <process.h>
unsigned __stdcall ClientThread(void* param) {
SOCKET client = (SOCKET)param;
char buf[BUF_SIZE];
int len;
while ((len = recv(client, buf, BUF_SIZE - 1, 0)) > 0) {
buf[len] = '\0';
printf("线程收到: %s\n", buf);
send(client, buf, len, 0);
}
closesocket(client);
return 0;
}
// 主循环
while (1) {
clientSock = accept(listenSock, NULL, NULL);
if (clientSock != INVALID_SOCKET) {
_beginthreadex(NULL, 0, ClientThread, (void*)clientSock, 0, NULL);
}
}
每个连接开一个线程,简单有效。连接数上百后建议改用IOCP或select,那是进阶话题。
常见错误与排查
-
编译通过但运行崩溃
没调用WSAStartup,或者链接库没加ws2_32.lib。用WSAGetLastError()打印错误码,10093是未初始化,10048是端口被占用。 -
bind失败,错误10048
端口被占用。换个端口,或者在任务管理器结束之前的进程。也可以加SO_REUSEADDR选项:int opt = 1; setsockopt(listenSock, SOL_SOCKET, SO_REUSEADDR, (char*)&opt, sizeof(opt)); -
客户端连接超时
防火墙拦截。Windows Defender防火墙临时关闭测试,或者在入站规则加端口放行。服务器IP写错也会这样,本地测试先用127.0.0.1。 -
中文乱码
控制台默认GBK,网络传输建议统一UTF-8,发送前转换,或者直接用英文测试。 -
recv返回0
对方正常关闭连接。返回-1再查WSAGetLastError,10054是对方强制断开。
调试技巧:用printf打印每一步返回值,配合Wireshark抓包看真实数据包,比盲目改代码快很多。
性能与安全注意点
- 生产环境不要用
INADDR_ANY直接暴露,绑定具体内网IP更安全。 - 收数据时一定检查缓冲区溢出,
recv返回值当长度用,别假设对方发多少。 - 超时设置:
setsockopt设置SO_RCVTIMEO和SO_SNDTIMEO,防止永久阻塞。 - 大文件传输建议分片,每次send不超过64KB,循环发完。
- 关闭顺序:先
shutdown(sock, SD_BOTH)再closesocket,数据发完再断。
进阶方向:非阻塞模式用ioctlsocket设FIONBIO,配合select做高并发;IPv6把AF_INET换成AF_INET6,地址结构用sockaddr_in6;加密通信在socket上再套TLS,OpenSSL或Windows自带SChannel都能用。
把上面服务器和客户端代码复制到两个项目里,分别编译运行,就能看到完整通信过程。改端口、加线程、换UDP,很快就能做出自己的网络工具。Windows下C语言Socket的核心就这些,细节掌握后,Linux移植只需要去掉Winsock初始化和换几个函数名。

还没有评论,来说两句吧...