杰瑞科技汇

Windows网络编程案例教程如何快速上手实战?

Windows网络编程案例教程

目录

  1. 第一部分:基础准备

    Windows网络编程案例教程如何快速上手实战?-图1
    (图片来源网络,侵删)
    • 1 什么是套接字?
    • 2 Winsock库简介
    • 3 开发环境配置
    • 4 网络字节序
  2. 第二部分:核心API详解

    • 1 初始化与清理:WSAStartup()WSACleanup()
    • 2 创建套接字:socket()
    • 3 绑定地址和端口:bind()
    • 4 监听连接:listen()
    • 5 接受连接:accept()
    • 6 连接服务器:connect()
    • 7 数据传输:send()recv()
    • 8 关闭套接字:closesocket()
  3. 第三部分:经典案例 - Echo服务器与客户端

    • 1 案例目标
    • 2 服务器端代码详解
    • 3 客户端代码详解
    • 4 如何编译与运行
  4. 第四部分:进阶与最佳实践

    • 1 阻塞 vs. 非阻塞模式
    • 2 select() 模型简介
    • 3 错误处理的重要性
    • 4 代码重构与封装

第一部分:基础准备

1 什么是套接字?

套接字是网络编程的API,它就像一个“网络插座”,你的程序可以通过这个插座向网络上的另一个程序发送数据,或者从另一个程序接收数据,它隐藏了底层复杂的网络协议(如TCP/IP)细节,为程序员提供了一个统一的接口。

Windows网络编程案例教程如何快速上手实战?-图2
(图片来源网络,侵删)

在Windows中,我们主要使用两种套接字:

  • 流式套接字 (SOCK_STREAM):使用TCP协议,提供面向连接的、可靠的数据传输服务,数据按顺序、无差错地到达,就像打电话,必须先建立连接。
  • 数据报套接字 (SOCK_DGRAM):使用UDP协议,提供无连接的、尽最大努力的数据传输服务,数据可能丢失、重复或乱序,就像寄明信片,寄出后无法保证对方一定能按顺序收到。

本教程以最常用的TCP为例。

2 Winsock库简介

Windows下的网络编程接口叫做 Winsock (Windows Sockets),它起源于Unix的Berkeley Sockets (BSD Sockets) 接口,并进行了扩展以适应Windows环境。

要使用Winsock,你需要包含两个头文件:

Windows网络编程案例教程如何快速上手实战?-图3
(图片来源网络,侵删)
  • #include <winsock2.h>:核心Winsock函数和数据结构的定义。
  • #include <ws2tcpip.h>:包含更新的IP地址函数(如 inet_pton)。

你需要链接一个库文件:

  • #pragma comment(lib, "ws2_32.lib"):告诉链接器在编译时链接 ws2_32.lib

3 开发环境配置

以Visual Studio为例:

  1. 创建一个新的C++控制台应用程序项目。
  2. 在源代码文件中,添加上述头文件和库链接指令。
  3. 确保你的项目配置为使用Windows桌面开发工具集。

4 网络字节序

计算机在存储多字节数据(如端口号、IP地址)时,有两种方式:大端序(高位字节在前)和小端序(低位字节在前),网络协议规定使用大端序,也称为网络字节序

Windows提供了四个宏进行转换:

  • htons():Host to Network Short (16位)
  • htonl():Host to Network Long (32位)
  • ntohs():Network to Host Short
  • ntohl():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) 的指针。
    • namelenaddr 指向的结构体的大小。
  • 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 结构体的指针。
    • namelenname 指向的结构体大小。
  • 返回值:成功返回 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 如何编译与运行

  1. 编译

    • Visual Studio: 分别创建两个项目(一个控制台应用叫 EchoServer,一个叫 EchoClient),将上述代码分别粘贴到各自的 .cpp 文件中,然后直接点击“生成” -> “生成解决方案”。
    • 命令行 (cl.exe):
      cl EchoServer.cpp /link ws2_32.lib
      cl EchoClient.cpp /link ws2_32.lib

      这会生成 EchoServer.exeEchoClient.exe

  2. 运行

    1. 先启动服务器:在命令行中运行 EchoServer.exe,你会看到 "Server is listening on port 8888..."。
    2. 再启动客户端打开一个新的命令行窗口,运行 EchoClient.exe
    3. 交互:在客户端窗口中输入任意字符串(如 "Hello, Server!"),然后按回车,客户端会收到服务器的回显,并显示出来。
    4. 关闭:在客户端输入 "quit" 或直接关闭客户端窗口,服务器会检测到连接关闭并退出。

第四部分:进阶与最佳实践

1 阻塞 vs. 非阻塞模式

  • 阻塞模式:默认模式,当一个函数(如 accept, recv)没有完成其任务时,调用它的线程会暂停,直到任务完成或发生错误。
    • 优点:代码简单直观。
    • 缺点:效率低下,服务器在 accept 时会一直卡住,无法处理其他已连接的客户端;在 recv 时,如果客户端不发数据,服务器也会卡住。
  • 非阻塞模式:通过 ioctlsocket() 函数设置,当一个函数被调用时,它会立即返回。
    • 如果任务无法立即完成,它会返回一个错误码 WSAEWOULDBLOCK
    • 优点:线程不会被卡住,可以执行其他任务(如检查其他套接字)。
    • 缺点:需要程序员通过循环不断调用函数来检查,实现复杂。

2 select() 模型简介

select() 是一种经典的I/O多路复用技术,用于解决阻塞模式下的多客户端处理问题。 它允许你同时监视多个套接字,并告诉你哪些套接字“已准备好”可以进行读、写或异常操作,这样,一个线程就可以管理成百上千个连接,大大提高了效率。

3 错误处理的重要性

本教程中的错误处理非常基础,在生产环境中,必须对每一个可能失败的Winsock调用都进行错误检查,并根据错误码(通过 WSAGetLastError() 获取)采取适当的恢复或退出措施。WSAEINTR(被中断)、WSAECONNRESET(连接被重置)等都需要不同的处理逻辑。

4 代码重构与封装

在实际项目中,直接调用这些底层API会使代码变得臃肿且难以维护,更好的做法是:

  1. 创建一个 CSocketTcpConnection,将套接字句柄和相关的状态封装起来。
  2. 提供 Connect(), Send(), Receive(), Close() 等成员函数,并在内部处理错误检查和资源释放。
  3. 使用智能指针(如 std::unique_ptr)管理套接字生命周期,防止忘记调用 closesocket() 导致的资源泄露。

通过这种方式,你可以构建一个更健壮、更易用的网络库,上层业务逻辑代码会变得非常清晰。


这份教程为你打下了坚实的基础,网络编程是一个广阔的领域,掌握TCP/IP后,你还可以进一步学习UDP、原始套接字、SSL/TLS加密、以及更高级的I/O模型(如WSAAsyncSelect, WSAEventSelect, I/O Completion Ports等),祝你学习顺利!

分享:
扫描分享到社交APP
上一篇
下一篇