Windows下C语言socket编程怎么用?

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

Windows 下的 Socket 编程与 Linux/Unix 有一个关键区别:需要额外的初始化和清理工作,下面我将从基础概念、完整代码、步骤分解、关键函数和常见问题等多个方面进行阐述。

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

核心概念与准备工作

在 Windows 中,Socket API 并不是操作系统内核的一部分,而是由一个名为 Winsock 的 DLL(动态链接库)提供的,你必须显式地告诉程序加载并使用这个库。

关键区别:Winsock 初始化与清理

  • Linux/Unix: Socket 功能是系统调用的一部分,直接包含在 <sys/socket.h> 中,无需额外初始化。
  • Windows: 必须在创建任何 Socket 之前调用 WSAStartup(),并在程序结束时调用 WSACleanup()

一个完整的 TCP 服务器示例

下面是一个完整的、可运行的 TCP 服务器代码,它会在本地 8888 端口监听,等待客户端连接,接收到客户端消息后,将其转换为大写并发送回去。

#define WIN32_LEAN_AND_MEAN // 减少Windows头文件包含,提高编译速度
#include <windows.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>
// 链接 Winsock 库
#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 with error: %d\n", iResult);
        return 1;
    }
    // 2. 创建监听 Socket
    struct addrinfo* result = NULL;
    struct addrinfo hints;
    ZeroMemory(&hints, sizeof(hints));
    hints.ai_family = AF_INET;       // IPv4
    hints.ai_socktype = SOCK_STREAM; // TCP Socket
    hints.ai_protocol = IPPROTO_TCP; // TCP 协议
    hints.ai_flags = AI_PASSIVE;     // 用于绑定到本地地址
    // 解析本地地址和端口
    iResult = getaddrinfo(NULL, DEFAULT_PORT, &hints, &result);
    if (iResult != 0) {
        printf("getaddrinfo failed with error: %d\n", iResult);
        WSACleanup();
        return 1;
    }
    SOCKET ListenSocket = INVALID_SOCKET;
    ListenSocket = socket(result->ai_family, result->ai_socktype, result->ai_protocol);
    if (ListenSocket == INVALID_SOCKET) {
        printf("socket failed with error: %ld\n", WSAGetLastError());
        freeaddrinfo(result);
        WSACleanup();
        return 1;
    }
    // 3. 绑定 Socket 到本地地址和端口
    iResult = bind(ListenSocket, result->ai_addr, (int)result->ai_addrlen);
    if (iResult == SOCKET_ERROR) {
        printf("bind failed with error: %d\n", WSAGetLastError());
        freeaddrinfo(result);
        closesocket(ListenSocket);
        WSACleanup();
        return 1;
    }
    // 4. 开始监听
    iResult = listen(ListenSocket, SOMAXCONN);
    if (iResult == SOCKET_ERROR) {
        printf("listen failed with error: %d\n", WSAGetLastError());
        closesocket(ListenSocket);
        WSACleanup();
        return 1;
    }
    printf("Server is listening on port %s...\n", DEFAULT_PORT);
    // 5. 接受客户端连接
    struct addrinfo clientAddr;
    int clientAddrLen = sizeof(clientAddr);
    SOCKET ClientSocket = INVALID_SOCKET;
    ClientSocket = accept(ListenSocket, (struct sockaddr*)&clientAddr, &clientAddrLen);
    if (ClientSocket == INVALID_SOCKET) {
        printf("accept failed with error: %d\n", WSAGetLastError());
        closesocket(ListenSocket);
        WSACleanup();
        return 1;
    }
    printf("Client connected!\n");
    // 6. 与客户端通信
    char recvbuf[DEFAULT_BUFLEN];
    int recvbuflen = DEFAULT_BUFLEN;
    iResult = recv(ClientSocket, recvbuf, recvbuflen, 0);
    if (iResult > 0) {
        printf("Bytes received: %d\n", iResult);
        printf("Message from client: %s\n", recvbuf);
        // 将消息转换为大写并发送回去
        for (int i = 0; i < iResult; i++) {
            recvbuf[i] = toupper(recvbuf[i]);
        }
        iResult = send(ClientSocket, recvbuf, iResult, 0);
        if (iResult == SOCKET_ERROR) {
            printf("send failed with error: %d\n", WSAGetLastError());
            closesocket(ClientSocket);
            closesocket(ListenSocket);
            WSACleanup();
            return 1;
        }
        printf("Bytes sent: %d\n", iResult);
    } else if (iResult == 0) {
        printf("Connection closing...\n");
    } else {
        printf("recv failed with error: %d\n", WSAGetLastError());
        closesocket(ClientSocket);
        closesocket(ListenSocket);
        WSACleanup();
        return 1;
    }
    // 7. 关闭连接
    iResult = shutdown(ClientSocket, SD_SEND);
    if (iResult == SOCKET_ERROR) {
        printf("shutdown failed with error: %d\n", WSAGetLastError());
        closesocket(ClientSocket);
        closesocket(ListenSocket);
        WSACleanup();
        return 1;
    }
    // 清理资源
    closesocket(ClientSocket);
    closesocket(ListenSocket);
    freeaddrinfo(result);
    WSACleanup();
    printf("Server shutdown successfully.\n");
    return 0;
}

代码步骤详解

步骤 1: 包含头文件并链接库

#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>
// 告诉链接器在编译时链接 ws2_32.lib 这个库
#pragma comment(lib, "ws2_32.lib")
  • winsock2.h: 核心 Winsock 头文件。
  • ws2tcpip.h: 提供了更新的地址转换函数,如 inet_pton,比旧的 inet_addr 更安全。
  • #pragma comment(lib, "ws2_32.lib"): 这是一种方便的方式,让你不需要在 Visual Studio 的项目设置里手动添加库依赖。

步骤 2: 初始化 Winsock (WSAStartup)

WSADATA wsaData;
int iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
  • MAKEWORD(2, 2): 请求 Winsock 2.2 版本,这是一个标准的做法。
  • WSADATA: 一个结构体,用于返回 Winsock 的详细实现信息(如最高版本、当前版本等)。
  • 如果返回值不是 0,则初始化失败。

步骤 3: 创建 Socket (socket)

struct addrinfo hints, *result;
ZeroMemory(&hints, sizeof(hints));
hints.ai_family = AF_INET;       // IPv4
hints.ai_socktype = SOCK_STREAM; // TCP
hints.ai_protocol = IPPROTO_TCP; // TCP
hints.ai_flags = AI_PASSIVE;     // 表示这个Socket将用于绑定
getaddrinfo(NULL, DEFAULT_PORT, &hints, &result);
SOCKET ListenSocket = socket(result->ai_family, result->ai_socktype, result->ai_protocol);
  • 现代 Windows 编程推荐使用 getaddrinfo 来解析地址和端口,它会返回一个 addrinfo 链表,你只需要取第一个即可。
  • AF_INET: 使用 IPv4 地址族。
  • SOCK_STREAM: 创建一个流式套接字,即 TCP Socket。
  • IPPROTO_TCP: 明确指定使用 TCP 协议。
  • AI_PASSIVE: 告诉 getaddrinfo 返回的地址适合用于 bind 操作(即使用 INADDR_ANY)。

步骤 4: 绑定地址和端口 (bind)

bind(ListenSocket, result->ai_addr, (int)result->ai_addrlen);
  • 将创建的 ListenSocket 与一个特定的 IP 地址和端口号关联起来。
  • result->ai_addrgetaddrinfo 返回的地址信息。
  • result->ai_addrlen 是地址的长度。

步骤 5: 开始监听 (listen)

listen(ListenSocket, SOMAXCONN);
  • 将 Socket 转入被动监听模式,准备接受客户端的连接请求。
  • SOMAXCONN 是一个系统常量,表示允许的最大连接挂起队列长度。

步骤 6: 接受连接 (accept)

SOCKET ClientSocket = accept(ListenSocket, ...);
  • 服务器调用 accept 会阻塞程序,直到有一个客户端连接上来。
  • 当连接成功时,accept 会返回一个新的 ClientSocket后续的所有通信(收发数据)都通过这个新的 ClientSocket 进行,而原来的 ListenSocket 继续监听新的连接。

步骤 7: 数据收发 (send/recv)

// 接收数据
int iResult = recv(ClientSocket, recvbuf, recvbuflen, 0);
// 发送数据
iResult = send(ClientSocket, sendbuf, sendbuflen, 0);
  • recv: 从 ClientSocket 读取数据到缓冲区 recvbuf
  • send: 将缓冲区 sendbuf 中的数据通过 ClientSocket 发送给客户端。
  • 第四个参数通常为 0,表示使用默认行为。

步骤 8: 关闭和清理 (shutdown, closesocket, WSACleanup)

// 关闭 Socket 的发送功能,告诉对方我们不会再发送数据了
shutdown(ClientSocket, SD_SEND);
// 关闭 Socket
closesocket(ClientSocket);
closesocket(ListenSocket);
// 释放 getaddrinfo 分配的内存
freeaddrinfo(result);
// 卸载 Winsock DLL
WSACleanup();
  • shutdown: 优雅地关闭连接,可以选择关闭发送、接收或双向。
  • closesocket: 彻底关闭 Socket,释放资源。
  • freeaddrinfo: 释放 getaddrinfo 分配的内存。
  • WSACleanup: 非常重要! 减少对 Winsock DLL 的引用计数,当计数为 0 时,DLL 会被卸载。必须在程序结束前调用。

一个完整的 TCP 客户端示例

客户端的逻辑相对简单:连接服务器 -> 收发数据 -> 关闭。

#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#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 = WSAStartup(MAKEWORD(2, 2), &wsaData);
    if (iResult != 0) {
        printf("WSAStartup failed with error: %d\n", iResult);
        return 1;
    }
    // 创建用于连接的 Socket
    struct addrinfo* result = NULL,
        * ptr = NULL,
        hints;
    ZeroMemory(&hints, sizeof(hints));
    hints.ai_family = AF_INET;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = IPPROTO_TCP;
    // 解析服务器地址
    iResult = getaddrinfo(SERVER_IP, DEFAULT_PORT, &hints, &result);
    if (iResult != 0) {
        printf("getaddrinfo failed with error: %d\n", iResult);
        WSACleanup();
        return 1;
    }
    SOCKET ConnectSocket = INVALID_SOCKET;
    // 尝试连接到第一个找到的地址
    for (ptr = result; ptr != NULL; ptr = ptr->ai_next) {
        ConnectSocket = socket(ptr->ai_family, ptr->ai_socktype, ptr->ai_protocol);
        if (ConnectSocket == INVALID_SOCKET) {
            printf("socket failed with error: %ld\n", WSAGetLastError());
            WSACleanup();
            return 1;
        }
        // 连接到服务器
        iResult = connect(ConnectSocket, ptr->ai_addr, (int)ptr->ai_addrlen);
        if (iResult == SOCKET_ERROR) {
            closesocket(ConnectSocket);
            ConnectSocket = INVALID_SOCKET;
            continue; // 尝试下一个地址
        }
        break; // 连接成功,跳出循环
    }
    freeaddrinfo(result);
    if (ConnectSocket == INVALID_SOCKET) {
        printf("Unable to connect to server!\n");
        WSACleanup();
        return 1;
    }
    printf("Connected to server!\n");
    // 发送数据
    char sendbuf[] = "Hello from client!";
    iResult = send(ConnectSocket, sendbuf, (int)strlen(sendbuf), 0);
    if (iResult == SOCKET_ERROR) {
        printf("send failed with error: %d\n", WSAGetLastError());
        closesocket(ConnectSocket);
        WSACleanup();
        return 1;
    }
    printf("Bytes sent: %ld\n", iResult);
    // 接收数据
    char recvbuf[DEFAULT_BUFLEN];
    iResult = recv(ConnectSocket, recvbuf, DEFAULT_BUFLEN - 1, 0);
    if (iResult > 0) {
        printf("Bytes received: %d\n", iResult);
        recvbuf[iResult] = '\0'; // 确保字符串以 null 
        printf("Message from server: %s\n", recvbuf);
    } else if (iResult == 0) {
        printf("Connection closed by server\n");
    } else {
        printf("recv failed with error: %d\n", WSAGetLastError());
    }
    // 关闭连接
    iResult = shutdown(ConnectSocket, SD_SEND);
    if (iResult == SOCKET_ERROR) {
        printf("shutdown failed with error: %d\n", WSAGetLastError());
        closesocket(ConnectSocket);
        WSACleanup();
        return 1;
    }
    closesocket(ConnectSocket);
    WSACleanup();
    return 0;
}

常见问题与注意事项

  1. 编译错误

    c语言socket windows
    (图片来源网络,侵删)
    • LNK2025: 找不到外部符号,这通常是因为你没有链接 ws2_32.lib 库,请确保 #pragma comment(lib, "ws2_32.lib") 存在或在你的项目设置中添加了该库。
    • C2065: “WSAStartup”: 未声明的标识符,这通常是因为你忘记包含 winsock2.h 头文件。
  2. 运行时错误

    • WSAGetLastError(): 当一个 Socket 函数失败时,它会返回一个错误码(通常是 SOCKET_ERROR),调用 WSAGetLastError() 可以获取具体的错误原因,这对于调试至关重要。bind 失败可能是端口被占用。
    • 端口被占用: 如果服务器无法 bind 到指定端口,请检查是否有其他程序正在使用该端口,可以使用 netstat -ano | findstr "8888" (CMD) 或 netstat -an | grep 8888 (PowerShell) 来查看。
    • 防火墙: Windows 防火墙可能会阻止你的程序监听端口或进行网络连接,你可能需要为你的程序创建一个防火墙规则,允许其入站连接。
  3. 阻塞与非阻塞模式

    • 默认情况下,accept, recv, connect 等函数是阻塞的,即函数会一直等待,直到操作完成或发生错误。
    • 对于高性能服务器,通常会使用非阻塞模式I/O 多路复用(如 select)来同时处理多个连接,避免一个连接阻塞整个程序,Windows 也支持更高效的 WSAAsyncSelectWSAEventSelect 模型,以及跨平台的 epoll (Linux) / kqueue (BSD) 的 Windows 版本——IOCP (I/O Completion Ports),但对于初学者,阻塞模式是最容易理解的。
c语言socket windows
(图片来源网络,侵删)
-- 展开阅读全文 --
头像
C语言switch中default必须放在最后吗?
« 上一篇 2025-12-14
dede图集地址栏层次结构如何缩短?
下一篇 » 2025-12-14

相关文章

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

目录[+]