C++ TCP网络编程实战:客户端与服务端全流程解析

Mia
Mia
发布时间:2026-10-15| 6人浏览过
原创

TCP是面向连接的可靠传输协议,广泛应用于互联网数据交互。在C++开发中,理解Socket API的正确调用顺序是构建稳定网络应用的基础。本文将深入解析TCP Client/Server架构,从套接字创建到数据传输,再到连接关闭,提供标准且可运行的代码框架,帮助您规避常见的阻塞与错误处理陷阱。

TCP协议特性与基础架构

TCP(Transmission Control Protocol)是一种面向连接的流式协议。其核心特征包括:

  • 可靠性:通过校验和、确认应答(ACK)和超时重传机制保证数据无差错到达。
  • 面向连接:通信前必须通过“三次握手”建立虚拟连接,通信结束后需通过“四次挥手”断开。
  • 全双工通信:支持数据同时在双向通道传输。

TCP遵循典型的Client/Server(C/S)架构模式。服务器端被动等待请求,客户端主动发起连接。这种非对称性决定了双方在使用系统调用时的角色差异:服务器需要绑定固定端口并监听,而客户端则由系统自动分配临时端口。

C++ TCP客户端编程指南

TCP客户端的主要职责是发现服务器并发起连接。完整的操作生命周期包含以下五个步骤:

第1步:创建TCP套接字

首先需要使用 socket() 创建一个文件描述符。对于TCP协议,第二个参数必须指定为 SOCK_STREAM(流式套接字),以区别于UDP使用的 SOCK_DGRAM。

#include <sys/socket.h>
int sockfd = socket(AF_INET, SOCK_STREAM, 0);

第2步:连接服务器(Connect)

使用 connect() 函数向指定IP和端口的服务器发起连接请求。若未显式调用 bind(),内核会自动为该套接字分配一个可用的本地随机端口(Ephemeral Port)并完成必要的地址结构填充。

int connect(int socket, const struct sockaddr *address, socklen_t address_len);

参数说明:

  • socket:上一步创建的套接字描述符。
  • address:服务器端地址结构指针(通常为 struct sockaddr_in)。
  • address_len:地址结构体的大小。

第3步:发送数据(Send)

连接建立后,通过 send() 将请求报文发送至服务端。该函数返回实际发送的字节数,失败时返回-1。

ssize_t send(int socket, const void *buffer, size_t length, int flags);

注意:flags 通常设为0。在TCP协议中,禁止发送长度为0的报文,因为接收方无法区分这是数据还是连接终止信号。

第4步:接收响应(Recv)

阻塞模式下,recv() 会等待直至收到数据或连接断开。若成功接收,返回实际读入缓冲区的数据长度;若远端正常关闭连接,返回0;若发生错误,返回-1。

ssize_t recv(int socket, void *buffer, size_t length, int flags);

第5步:关闭连接(Close)

通信完成后,务必调用 close() 释放资源,这将触发TCP的四次挥手过程,通知对方连接已终结。

客户端完整代码示例

以下代码展示了连接至 IP 10.9.21.211 端口 8000 的服务器并回显响应的标准流程:

#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <arpa/inet.h>

int main(int argc, char const *argv[])
{
    // 1. 创建TCP套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("socket failed");
        return 1;
    }

    // 2. 配置服务器地址信息
    struct sockaddr_in ser_addr;
    memset(&ser_addr, 0, sizeof(ser_addr));
    ser_addr.sin_family = AF_INET;
    ser_addr.sin_port = htons(8000);
    // 假设服务器IP为 10.9.21.211
    inet_pton(AF_INET, "10.9.21.211", &ser_addr.sin_addr.s_addr);

    // 3. 发起连接
    if (connect(sockfd, (struct sockaddr *)&ser_addr, sizeof(ser_addr)) < 0) {
        perror("connect failed");
        close(sockfd);
        return 1;
    }

    // 4. 发送请求
    const char *msg = "hello tcp";
    send(sockfd, msg, strlen(msg), 0);

    // 5. 接收应答
    unsigned char buf[1500] = {0};
    ssize_t len = recv(sockfd, buf, sizeof(buf), 0);
    if (len > 0) {
        printf("服务器的应答:%s\n", buf);
    } else if (len == 0) {
        printf("服务器已关闭连接\n");
    }

    // 6. 清理资源
    close(sockfd);
    return 0;
}

C++ TCP服务端编程指南

服务端程序通常需要同时处理多个客户端的连接请求。其核心区别在于需要进行 bind 和 listen,并通过 accept 提取已完成的连接。

第1步:创建套接字

与客户端相同,创建 TCP 类型的流式套接字。

int sockfd = socket(AF_INET, SOCK_STREAM, 0);

第2步:绑定地址与端口(Bind)

服务器必须明确指定监听的 IP 和端口。INADDR_ANY 表示监听本机所有可用网卡接口。

struct sockaddr_in my_addr;
memset(&my_addr, 0, sizeof(my_addr));
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(8000);         // 监听端口
my_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听所有本地地址

if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(my_addr)) < 0) {
    perror("bind failed");
    return 1;
}

第3步:设置监听队列(Listen)

listen() 将套接字从主动模式切换为被动模式,并为尚未被 accept 处理的半连接/全连接创建内核队列。backlog 参数定义队列最大长度。

if (listen(sockfd, 10) < 0) {
    perror("listen failed");
    return 1;
}

第4步:接受新连接(Accept)

这是一个阻塞调用。当有新连接进入已连接队列时,accept 从中取出一个连接,并返回一个新的文件描述符(cfd)用于后续与该特定客户端的通信。

struct sockaddr_in cli_addr;
socklen_t cli_len = sizeof(cli_addr);
int cfd = accept(sockfd, (struct sockaddr *)&cli_addr, &cli_len);

重要提示:sockfd 仅用于 accept 获取新连接,不再直接参与数据传输。真正的数据收发应在新返回的 cfd 上进行。

第5步:数据收发循环(Send/Recv)

服务端通常在独立循环中处理每个 cfd:

  • 读取判断:recv() 返回 0 表示对端优雅关闭(FIN包),返回 -1 表示错误,其余为正整数表示数据长度。
  • 发送判断:send() 返回 0 在 TCP 中是不允许的,若发生则视为异常或立即断开。

第6步:资源回收

单个客户端通信结束后,关闭该客户端的连接描述符(cfd);服务结束时,关闭监听描述符(sockfd)。

服务端完整代码示例

以下代码实现了一个简单的Echo Server(回显服务器),接收客户端消息并将其原样发回:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 9000
#define BACKLOG 10

int main() {
    // 1. 创建套接字 (修正原稿中的SOCK_DGRAM错误,应为SOCK_STREAM)
    int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd < 0) {
        perror("socket");
        exit(1);
    }

    // 2. 绑定地址
    struct sockaddr_in my_addr;
    memset(&my_addr, 0, sizeof(my_addr));
    my_addr.sin_family = AF_INET;
    my_addr.sin_port = htons(PORT);
    my_addr.sin_addr.s_addr = htonl(INADDR_ANY);

    if (bind(listen_fd, (struct sockaddr *)&my_addr, sizeof(my_addr)) < 0) {
        perror("bind");
        close(listen_fd);
        exit(1);
    }

    // 3. 监听
    if (listen(listen_fd, BACKLOG) < 0) {
        perror("listen");
        close(listen_fd);
        exit(1);
    }

    printf("Server listening on port %d...\n", PORT);

    while (1) {
        // 4. 接受连接
        struct sockaddr_in cli_addr;
        socklen_t cli_len = sizeof(cli_addr);
        int conn_fd = accept(listen_fd, (struct sockaddr *)&cli_addr, &cli_len);

        if (conn_fd < 0) {
            perror("accept");
            continue;
        }

        // 打印客户端信息
        char ip[INET_ADDRSTRLEN];
        inet_ntop(AF_INET, &cli_addr.sin_addr, ip, INET_ADDRSTRLEN);
        uint16_t port = ntohs(cli_addr.sin_port);
        printf("New client connected: %s:%u\n", ip, port);

        // 5. 数据交换循环
        char buf[1500] = {0};
        while (1) {
            ssize_t n = recv(conn_fd, buf, sizeof(buf) - 1, 0);
            if (n > 0) {
                buf[n] = '\0'; // 确保字符串结束
                printf("Received: %s\n", buf);
                // Echo response
                send(conn_fd, buf, n, 0);
            } else if (n == 0) {
                printf("Client closed connection.\n");
                break;
            } else {
                perror("recv error");
                break;
            }
        }

        // 6. 关闭当前客户端连接
        close(conn_fd);
    }

    // 7. 关闭监听套接字
    close(listen_fd);
    return 0;
}

常见问题与注意事项

  1. 字节序转换:在设置 sin_port 时必须使用 htons(),IP地址建议由 inet_pton() 转换,以确保跨平台兼容性。
  2. 粘包问题:TCP是字节流协议,不保留消息边界。如果应用层需要固定长度或带分隔符的消息,必须在业务逻辑层实现封包与拆包,或使用变长字段长度解析。
  3. 非阻塞模式:上述代码均为阻塞I/O模型。在高并发场景下,建议使用 epoll 配合 select/poll 等IO多路复用技术来管理成千上万个并发连接。
以上就是C++ TCP网络编程客户端与服务端开发的详细内容,更多关于C++ Socket编程技巧的资料请关注本站其它相关文章!
相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。