c语言socket windows

老刘

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步骤

  1. 新建空项目,选择C++空项目(C语言兼容)。
  2. 项目属性 → 链接器 → 输入 → 附加依赖项,填上ws2_32.lib
  3. 源文件后缀用.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_ptoninet_addr更安全,支持IPv6,Windows Vista以后都可用。

UDP通信快速上手

UDP不需要连接,适合游戏位置同步、心跳包这类场景。改动很小:

  • 创建socket时用SOCK_DGRAMIPPROTO_UDP
  • 服务器不需要listenaccept,直接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,那是进阶话题。

常见错误与排查

  1. 编译通过但运行崩溃
    没调用WSAStartup,或者链接库没加ws2_32.lib。用WSAGetLastError()打印错误码,10093是未初始化,10048是端口被占用。

  2. bind失败,错误10048
    端口被占用。换个端口,或者在任务管理器结束之前的进程。也可以加SO_REUSEADDR选项:

    int opt = 1;
    setsockopt(listenSock, SOL_SOCKET, SO_REUSEADDR, (char*)&opt, sizeof(opt));
  3. 客户端连接超时
    防火墙拦截。Windows Defender防火墙临时关闭测试,或者在入站规则加端口放行。服务器IP写错也会这样,本地测试先用127.0.0.1

  4. 中文乱码
    控制台默认GBK,网络传输建议统一UTF-8,发送前转换,或者直接用英文测试。

  5. recv返回0
    对方正常关闭连接。返回-1再查WSAGetLastError,10054是对方强制断开。

调试技巧:用printf打印每一步返回值,配合Wireshark抓包看真实数据包,比盲目改代码快很多。

性能与安全注意点

  • 生产环境不要用INADDR_ANY直接暴露,绑定具体内网IP更安全。
  • 收数据时一定检查缓冲区溢出,recv返回值当长度用,别假设对方发多少。
  • 超时设置:setsockopt设置SO_RCVTIMEOSO_SNDTIMEO,防止永久阻塞。
  • 大文件传输建议分片,每次send不超过64KB,循环发完。
  • 关闭顺序:先shutdown(sock, SD_BOTH)closesocket,数据发完再断。

进阶方向:非阻塞模式用ioctlsocketFIONBIO,配合select做高并发;IPv6把AF_INET换成AF_INET6,地址结构用sockaddr_in6;加密通信在socket上再套TLS,OpenSSL或Windows自带SChannel都能用。

把上面服务器和客户端代码复制到两个项目里,分别编译运行,就能看到完整通信过程。改端口、加线程、换UDP,很快就能做出自己的网络工具。Windows下C语言Socket的核心就这些,细节掌握后,Linux移植只需要去掉Winsock初始化和换几个函数名。

文章版权声明:文章内容均来源于各大短视频平台搜集以及修改和删减新增,如有侵权或者违规,请联系站长进行删除,如需转载或复制请以超链接形式并注明出处。

发表评论

快捷回复: 表情:
AddoilApplauseBadlaughBombCoffeeFabulousFacepalmFecesFrownHeyhaInsidiousKeepFightingNoProbPigHeadShockedSinistersmileSlapSocialSweatTolaughWatermelonWittyWowYeahYellowdog
验证码
评论列表 (暂无评论,1人围观)

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

目录[+]