Windows C语言Socket如何实现网络通信?

99ANYc3cd6
预计阅读时长 52 分钟
位置: 首页 C语言 正文

这篇指南将从基础概念开始,逐步深入,提供一个完整的客户端/服务器示例,并解释关键的 API 和注意事项。

windows c语言 socket
(图片来源网络,侵删)

目录

  1. 核心概念
    • 什么是 Socket?
    • 网络字节序
    • Winsock 库
  2. 服务器端编程步骤
    • 第 1 步:初始化 Winsock (WSAStartup)
    • 第 2 步:创建套接字 (socket)
    • 第 3 步:绑定地址和端口 (bind)
    • 第 4 步:监听连接 (listen)
    • 第 5 步:接受连接 (accept)
    • 第 6 步:收发数据 (send / recv)
    • 第 7 步:关闭和清理 (closesocket, WSACleanup)
  3. 客户端编程步骤
    • 第 1 步:初始化 Winsock (WSAStartup)
    • 第 2 步:创建套接字 (socket)
    • 第 3 步:连接服务器 (connect)
    • 第 4 步:收发数据 (send / recv)
    • 第 5 步:关闭和清理 (closesocket, WSACleanup)
  4. 完整代码示例
    • 服务器端代码 (server.c)
    • 客户端代码 (client.c)
    • 如何编译和运行
  5. 关键 API 函数详解
  6. 常见错误与调试技巧
  7. 进阶话题

核心概念

什么是 Socket?

Socket(套接字)是操作系统提供的一种用于网络通信的 API,它就像一个“电话插座”,应用程序可以通过它来发送和接收数据,在 C 语言中,我们使用一组特定的函数来操作这个“插座”。

网络字节序

计算机在内存中存储多字节数据(如端口号 8080 或 IP 地址)时有两种方式:

  • 大端序:高位字节存储在低地址。
  • 小端序:低位字节存储在低地址。

不同的 CPU 架构可能使用不同的字节序,为了确保网络通信中数据的一致性,网络协议规定使用大端序(也称为网络字节序)

在将端口号或 IP 地址地址结构体赋值之前,必须使用 htons() (host to network short) 和 htonl() (host to network long) 函数将主机字节序转换为网络字节序,接收数据时,则使用 ntohs()ntohl() 进行反向转换。

windows c语言 socket
(图片来源网络,侵删)

Winsock 库

Windows 和 Linux/Unix 的 Socket API 有细微差别,在 Windows 上,Socket 功能并不是操作系统内核的一部分,而是由一个名为 Winsock 的动态链接库(ws2_32.dll)提供的。

在 Windows 上进行 Socket 编程,必须:

  1. 链接库:在编译时链接 ws2_32.lib
  2. 包含头文件:在代码中包含 <winsock2.h>
  3. 初始化与清理:程序开始时调用 WSAStartup 初始化,结束时调用 WSACleanup 清理。

服务器端编程步骤

服务器端的工作流程是:创建 -> 绑定 -> 监听 -> 接受 -> 通信 -> 关闭

第 1 步:初始化 Winsock (WSAStartup)

在使用任何 Winsock 函数之前,必须先调用 WSAStartup 来告诉操作系统:“我要用 Winsock 了,请加载相关库”。

windows c语言 socket
(图片来源网络,侵删)
#include <winsock2.h>
#include <ws2tcpip.h>
// #pragma comment(lib, "ws2_32.lib") // 在 Visual Studio 中可以直接这样链接
int main() {
    WSADATA wsaData;
    int result = WSAStartup(MAKEWORD(2, 2), &wsaData); // 请求 Winsock 2.2 版本
    if (result != 0) {
        printf("WSAStartup failed: %d\n", result);
        return 1;
    }
    // ... 其他代码 ...
    WSACleanup(); // 程序结束时调用
    return 0;
}

第 2 步:创建套接字 (socket)

创建一个“通信端点”,即 Socket。

SOCKET ListenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (ListenSocket == INVALID_SOCKET) {
    printf("Error at socket(): %ld\n", WSAGetLastError());
    WSACleanup();
    return 1;
}
  • AF_INET: 使用 IPv4 地址族。
  • SOCK_STREAM: 提供面向连接的、可靠的数据流服务(即 TCP)。
  • IPPROTO_TCP: 明确指定使用 TCP 协议。

第 3 步:绑定地址和端口 (bind)

将创建的 Socket 与本机的 IP 地址和端口号关联起来,这样客户端才知道要连接到哪里。

sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(8888); // 将端口号 8888 转换为网络字节序
serverAddr.sin_addr.s_addr = INADDR_ANY; // 监听所有可用的网络接口
if (bind(ListenSocket, (sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {
    printf("bind failed: %d\n", WSAGetLastError());
    closesocket(ListenSocket);
    WSACleanup();
    return 1;
}
  • sockaddr_in: 用于 IPv4 的地址结构体。
  • INADDR_ANY: 一个特殊的常量,表示绑定到本机所有的网络接口(包括 0.0.1 和局域网 IP)。

第 4 步:监听连接 (listen)

将 Socket 设置为“监听”模式,等待客户端的连接请求。

if (listen(ListenSocket, SOMAXCONN) == SOCKET_ERROR) {
    printf("listen failed: %d\n", WSAGetLastError());
    closesocket(ListenSocket);
    WSACleanup();
    return 1;
}
  • SOMAXCONN: 表示允许的最大连接请求数,系统会自动选择一个合适的值。

第 5 步:接受连接 (accept)

服务器进入阻塞状态,等待客户端发起连接,一旦有客户端连接,accept 会返回一个新的、专门用于与该客户端通信的 Socket。

sockaddr_in clientAddr;
int clientAddrSize = sizeof(clientAddr);
printf("Waiting for a client to connect...\n");
SOCKET ClientSocket = accept(ListenSocket, (sockaddr*)&clientAddr, &clientAddrSize);
if (ClientSocket == INVALID_SOCKET) {
    printf("accept failed: %d\n", WSAGetLastError());
    closesocket(ListenSocket);
    WSACleanup();
    return 1;
}
printf("Client connected from %s:%d\n", 
       inet_ntoa(clientAddr.sin_addr), 
       ntohs(clientAddr.sin_port));
  • ListenSocket 仍然保持监听状态,可以继续接受其他客户端的连接。
  • ClientSocket 是一个全新的 Socket,后续的通信都通过它进行。

第 6 步:收发数据 (send / recv)

使用 ClientSocket 与已连接的客户端进行数据交换。

const char* sendMsg = "Hello from server!";
int sendResult = send(ClientSocket, sendMsg, strlen(sendMsg), 0);
if (sendResult == SOCKET_ERROR) {
    printf("send failed: %d\n", WSAGetLastError());
    closesocket(ClientSocket);
}
char recvbuf[512];
int recvResult = recv(ClientSocket, recvbuf, 512, 0);
if (recvResult > 0) {
    printf("Bytes received: %d\n", recvResult);
    recvbuf[recvResult] = '\0'; // 确保字符串以 null 
    printf("Client says: %s\n", recvbuf);
} else if (recvResult == 0) {
    printf("Connection closing...\n");
} else {
    printf("recv failed: %d\n", WSAGetLastError());
}

第 7 步:关闭和清理 (closesocket, WSACleanup)

通信结束后,关闭所有打开的 Socket,并调用 WSACleanup 释放 Winsock 资源。

closesocket(ClientSocket);
closesocket(ListenSocket);
WSACleanup();

客户端编程步骤

客户端的工作流程是:创建 -> 连接 -> 通信 -> 关闭

第 1 步:初始化 Winsock (WSAStartup)

与服务器端完全相同。

第 2 步:创建套接字 (socket)

与服务器端完全相同。

第 3 步:连接服务器 (connect)

客户端主动向服务器的 IP 地址和端口号发起连接请求。

sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(8888); // 服务器的端口号
// 将 IP 地址字符串转换为网络格式
inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);
if (connect(ClientSocket, (sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {
    printf("connect failed: %d\n", WSAGetLastError());
    closesocket(ClientSocket);
    WSACleanup();
    return 1;
}
  • inet_pton: 将 "点分十进制" 的 IP 地址字符串(如 "127.0.0.1")转换为 sockaddr_in 结构体需要的格式。

第 4 步:收发数据 (send / recv)

连接成功后,就可以使用 sendrecv 与服务器自由通信了。

第 5 步:关闭和清理 (closesocket, WSACleanup)

与服务器端类似,关闭 Socket 并清理 Winsock。


完整代码示例

服务器端代码 (server.c)

#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")
#define DEFAULT_PORT "8888"
#define DEFAULT_BUFLEN 512
int main() {
    WSADATA wsaData;
    int iResult;
    // 1. 初始化 Winsock
    iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
    if (iResult != 0) {
        printf("WSAStartup failed: %d\n", iResult);
        return 1;
    }
    // 2. 创建监听 Socket
    SOCKET ListenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (ListenSocket == INVALID_SOCKET) {
        printf("Error at socket(): %ld\n", WSAGetLastError());
        WSACleanup();
        return 1;
    }
    // 3. 绑定 Socket 到地址和端口
    sockaddr_in serverAddr;
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_addr.s_addr = INADDR_ANY;
    serverAddr.sin_port = htons(atoi(DEFAULT_PORT));
    iResult = bind(ListenSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr));
    if (iResult == SOCKET_ERROR) {
        printf("bind failed: %d\n", WSAGetLastError());
        closesocket(ListenSocket);
        WSACleanup();
        return 1;
    }
    // 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. 接受客户端连接
    sockaddr_in clientAddr;
    int clientAddrSize = sizeof(clientAddr);
    SOCKET ClientSocket = accept(ListenSocket, (SOCKADDR*)&clientAddr, &clientAddrSize);
    if (ClientSocket == INVALID_SOCKET) {
        printf("accept failed: %d\n", WSAGetLastError());
        closesocket(ListenSocket);
        WSACleanup();
        return 1;
    }
    printf("Client connected from %s:%d\n", 
           inet_ntoa(clientAddr.sin_addr), 
           ntohs(clientAddr.sin_port));
    // 6. 收发数据
    const char* sendMsg = "Hello from server!";
    iResult = send(ClientSocket, sendMsg, strlen(sendMsg), 0);
    if (iResult == SOCKET_ERROR) {
        printf("send failed: %d\n", WSAGetLastError());
        closesocket(ClientSocket);
        WSACleanup();
        return 1;
    }
    printf("Sent %ld bytes.\n", iResult);
    char recvbuf[DEFAULT_BUFLEN];
    iResult = recv(ClientSocket, recvbuf, DEFAULT_BUFLEN, 0);
    if (iResult > 0) {
        printf("Bytes received: %d\n", iResult);
        recvbuf[iResult] = '\0';
        printf("Client says: %s\n", recvbuf);
    } else if (iResult == 0) {
        printf("Connection closing...\n");
    } else {
        printf("recv failed: %d\n", WSAGetLastError());
    }
    // 7. 关闭和清理
    closesocket(ClientSocket);
    closesocket(ListenSocket);
    WSACleanup();
    return 0;
}

客户端代码 (client.c)

#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")
#define DEFAULT_PORT "8888"
#define DEFAULT_BUFLEN 512
#define SERVER_IP "127.0.0.1"
int main() {
    WSADATA wsaData;
    int iResult;
    // 1. 初始化 Winsock
    iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
    if (iResult != 0) {
        printf("WSAStartup failed: %d\n", iResult);
        return 1;
    }
    // 2. 创建客户端 Socket
    SOCKET ClientSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (ClientSocket == INVALID_SOCKET) {
        printf("Error at socket(): %ld\n", WSAGetLastError());
        WSACleanup();
        return 1;
    }
    // 3. 连接服务器
    sockaddr_in serverAddr;
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_port = htons(atoi(DEFAULT_PORT));
    inet_pton(AF_INET, SERVER_IP, &serverAddr.sin_addr);
    iResult = connect(ClientSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr));
    if (iResult == SOCKET_ERROR) {
        printf("connect failed: %d\n", WSAGetLastError());
        closesocket(ClientSocket);
        WSACleanup();
        return 1;
    }
    printf("Connected to server at %s:%s\n", SERVER_IP, DEFAULT_PORT);
    // 4. 收发数据
    char recvbuf[DEFAULT_BUFLEN];
    iResult = recv(ClientSocket, recvbuf, DEFAULT_BUFLEN, 0);
    if (iResult > 0) {
        printf("Bytes received: %d\n", iResult);
        recvbuf[iResult] = '\0';
        printf("Server says: %s\n", recvbuf);
    } else if (iResult == 0) {
        printf("Connection closing...\n");
    } else {
        printf("recv failed: %d\n", WSAGetLastError());
    }
    const char* sendMsg = "Hello from client!";
    iResult = send(ClientSocket, sendMsg, strlen(sendMsg), 0);
    if (iResult == SOCKET_ERROR) {
        printf("send failed: %d\n", WSAGetLastError());
        closesocket(ClientSocket);
        WSACleanup();
        return 1;
    }
    printf("Sent %ld bytes.\n", iResult);
    // 5. 关闭和清理
    closesocket(ClientSocket);
    WSACleanup();
    return 0;
}

如何编译和运行

  1. 使用 Visual Studio (推荐)

    • 创建一个新的 "空项目" (Empty Project)。
    • 将上述两个代码分别保存为 server.cclient.c,并添加到项目中。
    • 在项目属性中,确保 "配置属性" -> "链接器" -> "输入" -> "附加依赖项" 中包含 ws2_32.lib,或者直接在每个 .c 文件开头加上 #pragma comment(lib, "ws2_32.lib")
    • 分别将 server.cclient.c 设为“启动项”,然后生成并运行。
  2. 使用 GCC (MinGW) 命令行

    • 打开命令提示符或 PowerShell。

    • 使用以下命令编译:

      # 编译服务器
      gcc server.c -o server.exe -lws2_32
      # 编译客户端
      gcc client.c -o client.exe -lws2_32
    • 先运行服务器:

      server.exe

      (会看到 "Server is listening on port 8888...")

    • 再打开一个新的 命令提示符窗口,运行客户端:

      client.exe

关键 API 函数详解

函数 描述 参数 返回值
WSAStartup 初始化 Winsock 库。 wVersionRequested: 请求的 Winsock 版本 (如 MAKEWORD(2,2))。
lpWSAData: 指向 WSADATA 结构体的指针。
成功返回 0,失败返回错误码。
WSACleanup 卸载 Winsock 库,释放资源。 无返回值。
socket 创建一个 Socket。 af: 地址族 (如 AF_INET)。
type: Socket 类型 (如 SOCK_STREAM)。
protocol: 使用的协议 (如 IPPROTO_TCP)。
成功返回 SOCKET 句柄,失败返回 INVALID_SOCKET
bind 将 Socket 与本地 IP 地址和端口绑定。 s: Socket 句柄。
name: 指向 sockaddr 结构体的指针。
namelen: sockaddr 结构体的大小。
成功返回 0,失败返回 SOCKET_ERROR
listen 将 Socket 设为监听模式,准备接受连接。 s: 已绑定的 Socket 句柄。
backlog: 请求队列的最大长度。
成功返回 0,失败返回 SOCKET_ERROR
accept 接受一个客户端的连接请求,并返回一个新的通信 Socket。 s: 处于监听状态的 Socket 句柄。
addr: (可选) 指向 sockaddr 结构体的指针,用于存储客户端地址。
addrlen: 指向 addr 大小的指针。
成功返回新的 SOCKET 句柄,失败返回 INVALID_SOCKET
connect 客户端主动连接服务器。 s: 客户端 Socket 句柄。
name: 指向服务器 sockaddr 结构体的指针。
namelen: sockaddr 结构体的大小。
成功返回 0,失败返回 SOCKET_ERROR
send 通过已连接的 Socket 发送数据。 s: 已连接的 Socket 句柄。
buf: 指向要发送数据的缓冲区。
len: 要发送数据的字节数。
flags: 选项 (通常为 0)。
成功返回实际发送的字节数,失败返回 SOCKET_ERROR,连接断开返回 0
recv 通过已连接的 Socket 接收数据。 s: 已连接的 Socket 句柄。
buf: 指向接收数据的缓冲区。
len: 缓冲区大小。
flags: 选项 (通常为 0)。
成功返回接收到的字节数,失败返回 SOCKET_ERROR,连接断开返回 0
closesocket 关闭一个 Socket。 s: 要关闭的 Socket 句柄。 成功返回 0,失败返回 SOCKET_ERROR
htons / htonl 主机字节序转网络字节序 (short / long)。 hostshort / hostlong: 主机字节序的值。 网络字节序的值。
ntohs / ntohl 网络字节序转主机字节序 (short / long)。 netshort / netlong: 网络字节序的值。 主机字节序的值。
inet_addr / inet_pton 将 IP 地址字符串 (如 "192.168.1.1") 转换为 32 位网络字节序的二进制格式。 cp / str: IP 地址字符串。
addr: 指向存储结果的 in_addr 结构体的指针。
inet_addr 返回 in_addr,失败返回 INADDR_NONE
inet_pton 成功返回 1,失败返回 0-1
inet_ntoa / inet_ntop 将 32 位网络字节序的二进制 IP 地址格式转换为点分十进制字符串。 in: 指向 in_addr 结构体的指针。
str: 指向存储结果的字符串缓冲区。
inet_ntoa 返回指向字符串的指针。
inet_ntop 成功返回 0,失败返回 NULL

常见错误与调试技巧

  1. 10048 (WSAEADDRINUSE)

    • 错误: 地址在使用中。
    • 原因: 端口 8888 已经被另一个程序占用,可能是你上次运行的程序没有正常关闭。
    • 解决: 修改代码中的端口号,或者找到并关闭占用该端口的程序(在命令行使用 netstat -ano | findstr "8888" 查找 PID,然后用 taskkill /PID <PID> /F 强制结束)。
  2. 10061 (WSAECONNREFUSED)

    • 错误: 连接被拒绝。
    • 原因: 客户端尝试连接,但服务器没有在监听指定的端口,或者防火墙阻止了连接。
    • 解决: 确保服务器已经成功启动并正在监听,检查防火墙设置。
  3. 10013 (WSAEACCES)

    • 错误: 权限被拒绝。
    • 原因: 程序没有权限绑定到小于 1024 的端口(这些是特权端口),或者防火墙阻止了绑定。
    • 解决: 使用大于 1024 的端口号。
  4. 如何调试?

    • 打印错误信息: 几乎所有 Winsock 函数失败时,都可以用 WSAGetLastError() 获取具体的错误码,并用 printf 打印出来,这是最直接的调试方法。
    • 使用网络分析工具: Wireshark 是一个强大的网络抓包工具,你可以用它来查看你的程序是否真的在发送和接收数据,以及数据包的格式是否正确。
    • 检查返回值: 每一步关键操作(socket, bind, listen, accept, connect, send, recv)后,都应检查其返回值,判断是否成功。

进阶话题

  • 多线程/多进程模型: 上面的例子是单线程的,一次只能处理一个客户端,在实际应用中,你需要使用多线程(一个主线程负责 accept,每个客户端连接一个新线程)或 I/O 多路复用模型(如 select)来同时处理多个客户端。
  • 非阻塞模式: 默认情况下,acceptrecv 是阻塞的,即如果没有事件发生,程序会卡住,可以设置 Socket 为非阻塞模式,这样函数会立即返回,然后你可以通过轮询或事件通知的方式来处理 I/O。
  • UDP 编程: UDP 是无连接的协议,编程模型更简单:socket -> bind -> sendto / recvfrom -> closesocket,不需要 listenaccept
  • 异步 I/O (IOCP): 对于高性能服务器,Windows 提供了 I/O 完成端口机制,这是最复杂但也最高效的并发模型。
-- 展开阅读全文 --
头像
switch的default必须放在最后吗?
« 上一篇 2025-12-06
dede banner幻灯广告插件模块如何安装使用?
下一篇 » 2025-12-06

相关文章

取消
微信二维码
支付宝二维码

目录[+]