Linux C编程 — tcp通信,IO模型

发布于:2025-07-29 ⋅ 阅读:(13) ⋅ 点赞:(0)

tcp 

一 tcp(计算机网络)

  1. 可靠性:

    • TCP 的核心目标就是确保数据从发送端准确无误、完整有序地送达接收端。

    • 这是通过以下机制实现的:

      • 序列号和确认应答: 发送的每个字节都被分配一个唯一的序列号。接收方收到数据后,会向发送方发送一个确认应答,指明它期望收到的下一个字节的序列号。如果发送方在一定时间(超时重传时间)内没有收到确认,它会重传未得到确认的数据。

      • 校验和: 每个 TCP 报文段都包含一个校验和字段。接收方会计算校验和并与报文中的校验和进行比较。如果不匹配,说明数据在传输中损坏,接收方会丢弃该报文段,发送方最终会因未收到确认而重传。

      • 重传机制: 如上所述,通过超时重传和快速重传(当接收方收到失序的报文段时,会立即发送重复确认,提示发送方可能丢失了数据包)来应对丢包问题。

  2. 面向连接:

    • 在数据传输开始之前,TCP 要求通信双方(客户端和服务器)必须先建立一个逻辑连接。这个过程就是著名的 “三次握手”

    • 数据传输完成后,双方会执行 “四次挥手” 来有序地终止连接。

    • 这个连接状态(包括序列号、窗口大小、确认号等)在通信过程中由两端的主机维护。

  3. 基于字节流:

    • TCP 把应用层交下来的数据(无论是一个文件、一段文本还是其他形式)视为一连串无结构的字节流

    • TCP 不保留应用层消息的边界。发送方应用程序多次写入的数据,可能在接收方应用程序一次读取中全部收到;同样,发送方一次写入的大量数据,也可能被 TCP 分成多个报文段发送,接收方需要多次读取才能收齐。

    • 应用层需要自己定义消息边界(如使用特定的分隔符、固定长度消息头或在应用层协议中定义长度字段)。

  4. 流量控制:

    • 防止发送方发送数据过快,导致接收方来不及处理而缓冲区溢出,造成丢包。

    • 通过 滑动窗口机制 实现。接收方在每次确认应答中会通告自己的接收窗口大小,表示当前还能接收多少字节的数据。发送方发送的数据量不能超过这个通告的窗口大小。

  5. 拥塞控制:

    • 防止发送方发送数据过快,导致网络本身(路由器、链路)过载,造成全局性的网络性能下降和丢包。

    • 这是 TCP 最复杂的机制之一,主要算法包括:

      • 慢启动: 连接开始时或发生超时后,发送窗口从一个很小的值开始,然后指数增长(每收到一个确认,窗口大小增加一个 MSS)。

      • 拥塞避免: 当窗口增长到慢启动阈值后,进入拥塞避免阶段,窗口变为线性增长(每经过一个往返时间 RTT,窗口增加一个 MSS)。

      • 快速重传和快速恢复: 当发送方收到 3 个重复确认时(表明可能丢包,但网络仍有能力传输其他包),会立即重传丢失的数据包,并将慢启动阈值设为当前拥塞窗口的一半,然后直接进入拥塞避免阶段(快速恢复),避免了从慢启动开始的开销。

    • 核心思想是:TCP 主动探测网络可用带宽,在网络出现拥塞迹象(丢包)时,迅速减少发送速率,以缓解拥塞。

二 tcp报文

关键字段解释

  1. 源端口号 / 目标端口号: 标识发送和接收应用程序进程(如 HTTP=80, HTTPS=443, SSH=22)。IP地址+端口号唯一标识一个连接端点(套接字)。

  2. 序列号: 本报文段所发送数据的第一个字节在整个数据流中的序号。用于排序和确认。

  3. 确认号: 期望收到对方下一个报文段的第一个数据字节的序列号。表示该序号之前的所有数据都已正确接收。仅在 ACK 标志位为 1 时有效。

  4. 数据偏移: 指示 TCP 首部的长度(以 32 位字为单位),因为首部有可变长度的选项字段。最小 5 (20 字节),最大 15 (60 字节)。

  5. 控制标志: 6 个比特位:

    • URG: 紧急指针有效(很少用)。

    • ACK: 确认号字段有效。建立连接后,该标志位通常总是置 1。

    • PSH: 提示接收方应立即将数据交付给应用层(Push 功能,通常操作系统会优化,不一定严格执行)。

    • RST: 重置连接。表示发生严重错误,需要强制断开连接。

    • SYN: 同步序列号。用于发起连接请求(三次握手)。

    • FIN: 结束连接。用于请求终止连接(四次挥手)。

  6. 窗口大小: 接收方通告的接收窗口大小(字节数),用于流量控制。表示从确认号开始,接收方还能接收多少字节的数据。

  7. 校验和: 覆盖 TCP 首部、数据和伪首部(包含源/目标 IP 地址、协议号、TCP 长度)的校验和,用于检测传输错误。

  8. 紧急指针: 当 URG=1 时有效,指示本报文段中紧急数据的末尾位置(相对于序列号的偏移)。

  9. 选项: 可变长度,用于扩展功能。常见选项有:

    • 最大报文段长度: 在三次握手时协商双方能接受的单个 TCP 报文段的最大长度。

    • 窗口扩大因子: 用于扩展窗口大小(原窗口字段只有 16 位)。

    • 时间戳: 用于更精确计算往返时间和处理序列号回绕问题。

    • 选择性确认: 允许接收方告知发送方哪些不连续的数据块已经收到,提高重传效率。

  10. 数据: 应用层交付给 TCP 传输的实际数据。长度由 MSS 和网络 MTU 决定。

三 tcp通信

三 实践项目(C++面对对象版本)

版本1.

此版本实现了客户端client对服务端server的通信,(客户端对服务端)

具体代码,在文件的资源中

server.cpp
#include "server.h"
#include <thread>
#include <map>
#include <atomic>
#include <algorithm>

extern std::atomic<bool> show_client_msg;

// 构造函数,初始化端口和IP,默认IP为0.0.0.0
server::server(int port, const std::string &ip)
    : port(port), ip(ip), server_fd(-1), running(false)
{
    memset(&server_addr, 0, sizeof(server_addr)); // 清空地址结构体
    server_addr.sin_family = AF_INET;             // 地址族为IPv4
    server_addr.sin_port = htons(port);           // 设置端口号(网络字节序)

    // 显式处理特殊IP地址
    if (ip == "0.0.0.0")
    {
        server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听所有接口
    }
    else
    {
        server_addr.sin_addr.s_addr = inet_addr(ip.c_str()); // 设置指定IP
    }

    // 验证IP转换结果
    if (server_addr.sin_addr.s_addr == INADDR_NONE)
    {
        std::cerr << "Invalid IP address: " << ip << std::endl;
    }
}

// 析构函数,关闭所有套接字
server::~server()
{
    stop(); // 停止服务器,关闭所有连接
}

// 启动服务器,绑定端口并监听
bool server::start()
{
    server_fd = socket(AF_INET, SOCK_STREAM, 0); // 创建套接字
    if (server_fd < 0)
    {
        std::cerr << "Socket creation failed." << std::endl;
        return false;
    }

    int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); // 允许地址重用

    if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
    {
        perror("Bind failed");
        close(server_fd);
        return false;
    }

    if (listen(server_fd, MAX_CONNECTIONS) < 0)
    {
        perror("Listen failed");
        close(server_fd);
        return false;
    }

    running = true;
    std::cout << "Server started at " << ip << ":" << port << std::endl;
    return true;
}

// 停止服务器,关闭所有连接
void server::stop()
{
    running = false;
    // 关闭所有客户端线程
    for (auto &pair : client_threads)
    {
        if (pair.second.joinable())
            pair.second.join();
    }
    client_threads.clear();
    for (int fd : client_fds)
    {
        close(fd); // 关闭每个客户端套接字
    }
    client_fds.clear();
    if (server_fd != -1)
    {
        close(server_fd); // 关闭服务器套接字
        server_fd = -1;
    }
    std::cout << "Server stopped." << std::endl;
}

// 等待并接受客户端连接
void server::acceptClients()
{
    while (running)
    {
        struct sockaddr_in client_addr;
        socklen_t client_len = sizeof(client_addr);
        int client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len); // 接受客户端连接
        if (client_fd < 0)
        {
            if (running) // 仅在服务器运行时报告错误
                std::cerr << "Accept failed." << std::endl;
            continue;
        }
        client_fds.push_back(client_fd); // 保存客户端套接字
        std::cout << "Client connected: " << inet_ntoa(client_addr.sin_addr) << ":" << ntohs(client_addr.sin_port) << std::endl;
        // 为每个客户端创建线程处理消息
        client_threads[client_fd] = std::thread(&server::clientHandler, this, client_fd);
    }
}

// 客户端消息处理函数
void server::clientHandler(int client_fd)
{
    while (running)
    {
        std::string msg = receiveMsg(client_fd);
        if (msg.empty())
        {
            if (show_client_msg)
                std::cout << "Client disconnected: fd=" << client_fd << std::endl;
            close(client_fd);
            client_fds.erase(std::remove(client_fds.begin(), client_fds.end(), client_fd), client_fds.end());
            client_threads.erase(client_fd);
            break;
        }
        if (show_client_msg)
            std::cout << "收到客户端(" << client_fd << ")消息: " << msg << std::endl;
        // 可在此处添加自动回复、广播等逻辑
    }
}

// 接收指定客户端的信息
std::string server::receiveMsg(int client_fd)
{
    char buffer[1024] = {0};                                        // 接收缓冲区
    ssize_t bytes = recv(client_fd, buffer, sizeof(buffer) - 1, 0); // 接收数据
    if (bytes <= 0)
    {
        return "";
    }
    return std::string(buffer, bytes); // 返回接收到的消息
}

// 向指定客户端发送信息
bool server::sendMsg(int client_fd, const std::string &msg)
{
    ssize_t bytes = send(client_fd, msg.c_str(), msg.size(), 0); // 发送数据
    return bytes == (ssize_t)msg.size();
}

// 广播消息给所有客户端
void server::broadcastMsg(const std::string &msg)
{
    for (int fd : client_fds)
    {
        sendMsg(fd, msg); // 逐个发送消息
    }
}

// 获取当前已连接客户端数量
size_t server::getClientCount() const
{
    return client_fds.size();
}

// 获取服务器运行状态
bool server::isRunning() const
{
    return running;
}

版本2.

此版本实现了客户端client对c客户端client的通信,(客户端对客户端)客户端转发

版本3. 

实现客户端到客户端的群聊

四种io模型

概念

1. 阻塞模型

概念

  • 进程发起 I/O 操作后立即被阻塞,直到数据准备好并复制到用户空间。

  • 全程等待,期间 CPU 无法执行其他任务。

代码

int sockfd = socket(AF_INET, SOCK_STREAM, 0);
connect(sockfd, &addr, sizeof(addr));
char buffer[1024];
// 阻塞在此,直到数据就绪
int n = read(sockfd, buffer, sizeof(buffer)); 
process_data(buffer, n);

使用指南

  • 适用场景:简单客户端、低并发场景。

  • 优点:编程简单,逻辑清晰。

  • 缺点:每个连接需独立线程/进程,资源消耗大,无法支撑高并发。

2  非阻塞模型   non-blocking-io

概念

  • 进程发起 I/O 操作后立即返回(成功或错误)。

  • 通过轮询检查数据是否就绪,避免阻塞等待。

代码

fcntl(sockfd, F_SETFL, O_NONBLOCK); // 设为非阻塞
while (1) {
    int n = read(sockfd, buffer, sizeof(buffer));
    if (n > 0) {       // 数据就绪
        process_data(buffer, n);
        break;
    } else if (n == -1 && errno == EAGAIN) { 
        usleep(1000);  // 数据未就绪,短暂休眠后重试
    } else {
        // 处理错误
    }
}

使用指南

  • 适用场景:低延迟但连接数少的场景。

  • 优点:单线程可管理多个连接。

  • 缺点轮询消耗 CPU,不适用于大规模连接。

3  io多路复用

概念

  • 使用 select/poll/epoll 监控多个文件描述符(FD)。

  • 当任一 FD 就绪时,通知进程处理,避免轮询。

3.1 select

特点

  • 通过 fd_set 位图管理 FD,上限 FD_SETSIZE(通常 1024)。

  • 每次调用需重置 FD 集合

代码

fd_set readfds;
FD_ZERO(&readfds);
FD_SET(sockfd1, &readfds);
FD_SET(sockfd2, &readfds);

while (1) {
    fd_set tmp = readfds;
    int ret = select(max_fd+1, &tmp, NULL, NULL, NULL); // 阻塞等待
    if (FD_ISSET(sockfd1, &tmp)) {
        // 处理 sockfd1 数据
    }
    if (FD_ISSET(sockfd2, &tmp)) {
        // 处理 sockfd2 数据
    }
}
3.2 poll

特点

  • 使用 pollfd 结构体数组,无 FD 数量限制

  • 避免 select 的位图重置问题。

伪代码

struct pollfd fds[2];
fds[0].fd = sockfd1; fds[0].events = POLLIN;
fds[1].fd = sockfd2; fds[1].events = POLLIN;

while (1) {
    int ret = poll(fds, 2, -1); // 阻塞等待
    if (fds[0].revents & POLLIN) {
        // 处理 sockfd1
    }
    if (fds[1].revents & POLLIN) {
        // 处理 sockfd2
    }
}

共同缺点

  • 每次调用需传递所有 FD 到内核,性能随 FD 数量下降

  • 需遍历所有 FD 检查就绪状态(O(n))。

 4. epoll(Linux 特有)

概念

  • 基于事件驱动的 I/O 多路复用,高效处理海量连接

  • 核心 API:epoll_createepoll_ctlepoll_wait.

工作模式

  • 水平触发 (LT, Level-Triggered)
    就绪事件未处理时会持续通知(默认模式)。

  • 边缘触发 (ET, Edge-Triggered)
    仅在状态变化时通知一次,需一次性处理所有数据

代码

int epoll_fd = epoll_create1(0);
struct epoll_event ev, events[10];

// 添加 sockfd 到 epoll 监控
ev.events = EPOLLIN | EPOLLET; // ET 模式
ev.data.fd = sockfd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sockfd, &ev);

while (1) {
    int nfds = epoll_wait(epoll_fd, events, 10, -1); // 阻塞等待
    for (int i = 0; i < nfds; i++) {
        if (events[i].events & EPOLLIN) {
            int fd = events[i].data.fd;
            while (1) { // ET 模式需读尽数据
                int n = read(fd, buffer, sizeof(buffer));
                if (n <= 0) break; // 无数据或错误
                process_data(buffer, n);
            }
        }
    }
}

核心优势

  1. 高效数据结构
    使用红黑树管理 FD,哈希表存储就绪事件,时间复杂度 O(1)。

  2. 无需重复传递 FD
    通过 epoll_ctl 动态增删 FD。

  3. 支持海量连接
    仅受系统内存限制(1GB 内存约支持 10 万 FD)。

使用指南

  • 适用场景:高并发服务器(如 Nginx、Redis)。

  • 模式选择

    • LT:编程简单,适合低频大包场景。

    • ET:减少事件通知次数,需搭配非阻塞 I/O 和循环读写。

  • 最佳实践

    • ET 模式必须设置 FD 为非阻塞(O_NONBLOCK)。

    • 读操作需循环调用 read 直到 EAGAIN 错误。

    • 边缘触发下,一次性处理完所有数据。


总结对比

模型 性能 复杂度 适用场景
阻塞 I/O 简单 低并发客户端
非阻塞 I/O 中(轮询) 连接数少且低延迟
select/poll 跨平台,中等并发
epoll Linux 高并发服务器

关键结论

  • 现代高性能服务器首选 epoll(ET 模式 + 非阻塞 I/O)

  • Windows 平台使用 IOCP,FreeBSD 使用 kqueue


网站公告

今日签到

点亮在社区的每一天
去签到