Linux网络编程—TCP字典翻译系统

TCP 字典翻译系统

一、整体设计目标

Daemon,TcpServer,TcpClient三个文件配合工作,实现一个 TCP 字典翻译服务

  • 服务端(TcpServer)启动后进入守护进程模式,在后台运行
  • 客户端(TcpClient)连接服务端,发送英文单词
  • 服务端在线程池中查字典,把中文翻译返回给客户端
  • 客户端支持断线重连

TCP 的核心特点(与 UDP 对比):

  1. 面向连接:通信前必须"三次握手"建立连接
  2. 字节流:数据没有边界,可能粘包
  3. 可靠传输:确认机制、超时重传、序号排序
  4. 需要 accept:服务端用 listen socket 接受连接,每个连接产生新的 sockfd

二、Daemon.hpp 详细解析

2.1 为什么需要守护进程?

普通进程运行时绑定在终端上:

  • 终端关闭 → 进程收到 SIGHUP → 进程退出
  • 守护进程(Daemon)脱离终端,在后台独立运行
  • 服务器必须用守护进程,否则 SSH 断开服务就停了

2.2 文件头部

cpp 复制代码
#pragma once

#include <iostream>
#include <unistd.h>      // fork(), setsid(), chdir(), dup2(), close()
#include <cstdlib>       // exit()
#include <string>
#include <signal.h>      // signal()
#include <fcntl.h>       // open()
#include <sys/stat.h>    // 文件权限相关
#include <sys/types.h>

const std::string nullfile = "/dev/null";

/dev/null 是什么?

  • Linux 的"黑洞设备文件"
  • 写入的数据全部丢弃,读取返回 EOF
  • 守护进程不需要终端输入输出,所以把 stdin/stdout/stderr 都指向它

2.3 Daemon() 函数 - 逐行解析

cpp 复制代码
void Daemon(const std::string &cwd = "")
{
    // 第1步:忽略异常信号
    signal(SIGCHLD, SIG_IGN);
    signal(SIGPIPE, SIG_IGN);
    signal(SIGSTOP, SIG_IGN);

    // 第2步:创建子进程,父进程退出
    if(fork() > 0)
        exit(0);
    setsid();

    // 第3步:更改工作目录
    if(cwd.empty()) chdir(cwd.c_str());

    // 第4步:重定向标准输入输出到 /dev/null
    int fd = open(nullfile.c_str(), O_RDWR);
    if(fd < 0)
        exit(1);
    dup2(fd, STDIN_FILENO);
    dup2(fd, STDOUT_FILENO);
    dup2(fd, STDERR_FILENO);
    close(fd);
}
第1步:忽略异常信号
cpp 复制代码
signal(SIGCHLD, SIG_IGN);
signal(SIGPIPE, SIG_IGN);
signal(SIGSTOP, SIG_IGN);

signal() 函数

  • 接口:void (*signal(int signum, void (*handler)(int)))(int);
  • 参数1:要处理的信号编号
  • 参数2:处理方式(SIG_IGN = 忽略,SIG_DFL = 默认,或自定义函数指针)

三个信号分别为什么要忽略?

信号 触发场景 不忽略的后果 忽略后的效果
SIGCHLD 子进程退出时发给父进程 父进程需要 waitpid 回收,否则产生僵尸进程 内核自动回收子进程,不产生僵尸
SIGPIPE 向已关闭的 socket 写数据时 进程被默认终止 写操作返回 -1,进程继续运行
SIGSTOP Ctrl+Z 挂起进程 进程暂停 忽略挂起信号,防止意外暂停

SIGPIPE 对 TCP 服务器特别重要

  • 客户端突然断开连接,服务端还在 write()
  • 如果不忽略 SIGPIPE,服务端进程会被直接杀死
  • 忽略后,write() 返回 -1,errno = EPIPE,可以正常处理错误
第2步:fork() + setsid() - 脱离终端
cpp 复制代码
if(fork() > 0)
    exit(0);     // 父进程退出
setsid();        // 子进程创建新会话

为什么要 fork?

复制代码
调用 Daemon() 前:
  终端 (tty) ──绑定──> 进程 A (会话组长)
                         │
                         └── 进程 A 可以重新打开终端

fork() 后:
  终端 (tty) ──绑定──> 进程 A (父) ──exit(0) 退出
                         
  进程 B (子,不是会话组长) ──> setsid() ──> 进程 B (新会话组长,无终端)

setsid() 的作用

  • 接口:pid_t setsid(void);
  • 创建一个新的会话(Session)和进程组
  • 调用进程成为新会话的组长和新进程组的组长
  • 脱离控制终端:新会话没有控制终端,终端关闭不会影响它

为什么不能直接在父进程调 setsid()

  • setsid() 的调用进程不能是进程组长
  • 父进程通常是进程组长(因为它创建了子进程)
  • 所以必须 fork 出子进程,子进程不是组长,才能调 setsid()
第3步:更改工作目录
cpp 复制代码
if(cwd.empty()) chdir(cwd.c_str());

chdir() 的作用

  • 接口:int chdir(const char *path);
  • 更改进程的当前工作目录

为什么要改工作目录?

  • 如果工作目录是某个挂载点(如 U 盘),该目录被卸载时进程会出问题
  • 通常改成 /(根目录),确保不会被卸载
第4步:重定向标准 IO
cpp 复制代码
int fd = open(nullfile.c_str(), O_RDWR);
if(fd < 0)
    exit(1);
dup2(fd, STDIN_FILENO);
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
close(fd);

open("/dev/null", O_RDWR)

  • 以读写方式打开 /dev/null
  • 返回一个新的文件描述符 fd(例如 3)

dup2() 函数

  • 接口:int dup2(int oldfd, int newfd);
  • 作用:把 oldfd 复制到 newfd
  • 如果 newfd 之前打开着,先关闭它
  • 之后 newfdoldfd 指向同一个文件

重定向过程图解

复制代码
重定向前:
  fd 0 (stdin)  ──> 终端输入
  fd 1 (stdout) ──> 终端输出
  fd 2 (stderr) ──> 终端错误
  fd 3 (新打开)  ──> /dev/null

dup2(fd, STDIN_FILENO)   后:fd 0 ──> /dev/null
dup2(fd, STDOUT_FILENO)  后:fd 1 ──> /dev/null
dup2(fd, STDERR_FILENO)  后:fd 2 ──> /dev/null

close(fd)                后:fd 3 关闭(0/1/2 已经指向 /dev/null)

为什么守护进程要重定向标准 IO?

  • 守护进程没有终端,如果代码中有 cout / printf / cerr
  • 写入 fd 1/2 会失败(没有终端),可能导致进程崩溃
  • 重定向到 /dev/null 后,写入操作静默成功(数据丢弃),不会出错

三、TcpServer.hpp 详细解析

3.1 文件头部 - 头文件与依赖

cpp 复制代码
#pragma once
#include "Log.hpp"         // 日志模块
#include <cstdlib>         // exit()
#include <cstring>         // strerror()
#include <iostream>
#include <string>
#include <sys/socket.h>    // socket(), bind(), listen(), accept(), setsockopt()
#include <sys/types.h>
#include <arpa/inet.h>     // inet_aton(), inet_ntop(), htons(), ntohs()
#include <netinet/in.h>    // sockaddr_in
#include <unistd.h>        // close()
#include <sys/wait.h>      // waitpid()(多进程版本用)
#include <signal.h>        // signal()
#include <pthread.h>       // pthread_create()(多线程版本用)
#include "ThreadPool.hpp"  // 线程池
#include "Task.hpp"        // 任务类
#include <signal.h>        // 重复包含(无影响)
#include "Daemon.hpp"      // 守护进程

与 UDP 的对比

头文件 UDP TCP 原因
<sys/wait.h> TCP 版本有多进程方案
<pthread.h> TCP 版本有多线程方案
"ThreadPool.hpp" TCP 版本使用线程池
"Task.hpp" TCP 版本将业务逻辑封装为 Task
"Daemon.hpp" TCP 版本使用守护进程
<sys/socket.h> 都需要

3.2 全局常量与枚举

cpp 复制代码
const int DEFAULT_FD = -1;           // 默认 fd 值(表示未初始化)
const std::string DEFAULT_IP = "0.0.0.0";  // 默认绑定所有网卡
const int DEFAULT_BACKLOG = 10;      // 监听队列最大长度
extern Log logger;                   // 引用全局日志对象

DEFAULT_BACKLOG = 10 是什么?

  • listen() 的参数,表示"未完成连接队列 + 已完成连接队列"的总长度
  • 如果队列满了,新连接会被拒绝
  • 不建议太大:每个排队连接都占内核资源
cpp 复制代码
enum
{
    UsageError = 1,   // 命令行参数错误
    SocketError,      // socket() 失败 → 2
    BindError,        // bind()   失败 → 3
    ListenError       // listen() 失败 → 4
};

与 UDP 的对比

  • UDP 只有两个错误码(SOCKET_ERROR, BIND_ERROR
  • TCP 多了 ListenError,因为 TCP 需要调用 listen()
  • TCP 多了 UsageError,用于 test.cpp 中参数检查

3.3 前向声明与 ThreadData 结构体

cpp 复制代码
class TcpServer;  // 前向声明

为什么需要前向声明?

  • ThreadData 中要用 TcpServer *tcp_server 指针
  • TcpServer 类在 ThreadData 之后定义
  • 前向声明告诉编译器"这个类存在,只是还没定义"
  • 只能用指针/引用,不能用它的大小或成员
cpp 复制代码
class ThreadData
{
public:
    ThreadData(int fd, const std::string &ip, const uint16_t &port, TcpServer *tcp_server)
    : sockfd(fd)
    , client_ip(ip)
    , client_port(port)
    , tcp_server(tcp_server)
    {}
public:
    int sockfd;
    std::string client_ip;
    uint16_t client_port;
    TcpServer *tcp_server;
};

接口设计

  • 四个参数:文件描述符、客户端 IP、客户端端口、TcpServer 指针
  • tcp_server 指针用于在多线程版本中回调 TcpServer::Service() 方法

3.4 TcpServer类成员变量

cpp 复制代码
private:
    int _listensockfd;     // 监听 socket(专门用于 accept)
    uint16_t _port;        // 监听端口
    std::string _ip;       // 监听 IP

与 UDP 的关键区别

对比项 UDP TCP
Socket 数量 1 个(收发都用同一个) 多个(1 个 listen + N 个 connection)
成员变量名 _sockfd _listensockfd(强调"监听"用途)
在线用户表 _online_client ✘(TCP 每个连接独立处理,不需要全局管理)

为什么 TCP 不需要 _online_client

  • UDP 是无连接的,服务端需要记住所有客户端地址才能广播
  • TCP 每个连接有独立的 sockfd,通过 accept 就能拿到客户端地址
  • 当前版本是"一对一翻译"服务,不需要群聊

3.5 构造函数

cpp 复制代码
TcpServer(const uint16_t &port, const std::string &ip = DEFAULT_IP)
    : _port(port)
    , _ip(ip)
    , _listensockfd(DEFAULT_FD)  // 初始化为 -1
    {}

接口设计

  • 必须传 portip 有默认值 0.0.0.0
  • _listensockfd 初始化为 -1(表示"还没创建")

3.6 Init() 函数 - 初始化 Socket

cpp 复制代码
void Init()
{
    // 第1步:创建 TCP Socket
    _listensockfd = socket(AF_INET, SOCK_STREAM, 0);
    if(_listensockfd < 0)
    {
        logger(FATAL, "socket failed, errno: %d, errstr: %s", errno, strerror(errno));
        exit(SocketError);
    }
    logger(INFO, "SOCKET SUCCESS");
    
    // 第2步:设置地址重用
    int on = 1;
    setsockopt(_listensockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));

    // 第3步:构造地址结构体
    struct sockaddr_in local;
    memset(&local, 0, sizeof(local));
    local.sin_family = AF_INET;
    inet_aton(_ip.c_str(), &local.sin_addr);
    local.sin_port = htons(_port);

    // 第4步:绑定
    if(bind(_listensockfd, (struct sockaddr *)&local, sizeof(local)) < 0)
    {
        logger(FATAL, "bind failed, errno: %d, errstr: %s", errno, strerror(errno));
        exit(BindError);
    }
    logger(INFO, "BIND SUCCESS");
    
    // 第5步:监听(TCP 独有)
    if(listen(_listensockfd, DEFAULT_BACKLOG) < 0)
    {
        logger(FATAL, "listen failed, errno: %d, errstr: %s", errno, strerror(errno));
        exit(ListenError);
    }
    logger(INFO, "LISTEN SUCCESS");
}
第1步:socket() - 注意 SOCK_STREAM
cpp 复制代码
_listensockfd = socket(AF_INET, SOCK_STREAM, 0);

与 UDP 的唯一区别

  • UDP:SOCK_DGRAM(数据报)
  • TCP:SOCK_STREAM(字节流)
参数 UDP 值 TCP 值 含义
type SOCK_DGRAM SOCK_STREAM 数据报 vs 字节流
第2步:setsockopt() - 设置地址重用(TCP 新增)
cpp 复制代码
int on = 1;
setsockopt(_listensockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));

setsockopt() 接口

cpp 复制代码
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
参数 含义
sockfd _listensockfd 要设置的 Socket
level SOL_SOCKET 选项所在的协议层(通用 socket 层)
optname SO_REUSEADDR 选项名:允许地址重用
optval &on 指向选项值的指针(1 = 开启)
optlen sizeof(on) 选项值的长度

为什么需要 SO_REUSEADDR

  • TCP 连接关闭后,端口会进入 TIME_WAIT 状态(持续 1~2 分钟)
  • 在此期间,bind() 同一端口会失败(EADDRINUSE
  • SO_REUSEADDR 允许绑定处于 TIME_WAIT 状态的端口
  • 服务器重启时不用等待,可以立即绑定

UDP 不需要这个选项的原因

  • UDP 没有连接的概念,也就没有 TIME_WAIT 状态
  • 关闭后端口立即可用
第3步:构造地址 - inet_aton vs inet_addr
cpp 复制代码
local.sin_family = AF_INET;
inet_aton(_ip.c_str(), &local.sin_addr);   // 注意这里用 inet_aton
local.sin_port = htons(_port);

inet_aton()inet_addr() 的对比

特性 inet_addr() inet_aton()
返回值 in_addr_t(整数) int(1=成功,0=失败)
结果输出 返回值 写入第二个参数(in_addr 结构)
错误处理 返回 INADDR_NONE(-1),与广播地址 255.255.255.255 冲突 返回 0,无歧义
线程安全 安全 安全

inet_aton 更推荐使用,因为错误处理更清晰。

第4步:bind() - 与 UDP 相同
cpp 复制代码
bind(_listensockfd, (struct sockaddr *)&local, sizeof(local));

与 UDP 完全一致。

第5步:listen() - TCP 独有
cpp 复制代码
listen(_listensockfd, DEFAULT_BACKLOG);

listen() 接口

cpp 复制代码
int listen(int sockfd, int backlog);
参数 含义
sockfd 要监听的 Socket(_listensockfd
backlog 连接队列的最大长度(这里是 10)

listen() 做了什么?

复制代码
listen() 之前:
  _listensockfd 是一个"普通"的 TCP Socket
  可以 connect,但不能被 connect

listen() 之后:
  _listensockfd 变成"被动"Socket
  内核维护两个队列:
  
  ┌────────────────────────────────────────┐
  │  未完成连接队列(SYN 队列)              │
  │  客户端发了 SYN,服务器正在三次握手        │
  │  状态:SYN_RCVD                         │
  └────────────────────────────────────────┘
  ┌────────────────────────────────────────┐
  │  已完成连接队列(Accept 队列)           │
  │  三次握手完成,等待 accept() 取走         │
  │  状态:ESTABLISHED                      │
  └────────────────────────────────────────┘
  
  backlog = 两个队列总长度上限

为什么 UDP 不需要 listen()

  • UDP 是无连接的,不需要维护连接队列
  • UDP 的 Socket 直接就能收发数据

3.7 Run() 函数 - 主事件循环

cpp 复制代码
void Run()
{
    Daemon();                              // 第1步:变成守护进程
    ThreadPool<Task>::GetInstance()->Start();  // 第2步:启动线程池
    logger(INFO,"TcpServer is running...");
    
    for(;;)                                // 第3步:无限循环 accept
    {
        struct sockaddr_in client;
        socklen_t client_len = sizeof(client);
        
        int sockfd = accept(_listensockfd, (struct sockaddr *)&client, &client_len);
        if(sockfd < 0)
        {
            logger(WARNING, "accept failed, ...");
            continue;
        }
        
        uint16_t client_port = ntohs(client.sin_port);
        char client_ip[16];
        inet_ntop(AF_INET, &(client.sin_addr), client_ip, sizeof(client_ip));
        
        logger(INFO, "ACCEPT SUCCESS, sockfd: %d, client_ip: %s, client_port: %d", 
               sockfd, client_ip, client_port);
        
        // 第4步:把任务推入线程池
        ThreadPool<Task>::GetInstance()->Push(Task(sockfd, client_ip, client_port));
    }
}
第1步:Daemon()
cpp 复制代码
Daemon();

调用位置的意义

  • Run() 的第一行调用,此时 Socket 已创建并绑定
  • Daemon() 会 fork 子进程,子进程继承父进程的 Socket 和所有状态
  • 之后父进程退出,子进程作为守护进程继续运行

为什么不在 Init() 之前调?

  • 如果 Daemon()Init() 之前,fork 后父子进程都需要初始化
  • Init() 之后调,只有守护子进程进入 Run() 循环
第2步:启动线程池
cpp 复制代码
ThreadPool<Task>::GetInstance()->Start();

GetInstance() :单例模式获取线程池实例

Start():创建 5 个工作线程,等待任务

第3步:accept() - 接受新连接
cpp 复制代码
int sockfd = accept(_listensockfd, (struct sockaddr *)&client, &client_len);

accept() 接口

cpp 复制代码
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数 含义
sockfd 监听 Socket(_listensockfd
addr 输出参数,保存客户端地址
addrlen 输入输出参数,传入缓冲区大小,返回实际长度

返回值

  • 成功:返回新的 Socket 文件描述符(sockfd
  • 失败:返回 -1,设置 errno

accept() 到底做了什么?

复制代码
监听 Socket (_listensockfd):
  ├── 被动监听,不参与数据收发
  ├── accept() 从已完成队列中取出一个连接
  └── 取出后,返回一个新的 sockfd
  
连接 Socket (accept 返回的 sockfd):
  ├── 与特定客户端绑定
  ├── 用于实际的 read() / write()
  └── 每个客户端连接都有一个独立的 sockfd

与 UDP 的核心区别

对比项 UDP TCP
收数据 recvfrom() accept() + read()
发数据 sendto() write() / send()
Socket 数量 1 个 1 个 listen + N 个 connection
客户端地址 每次 recvfrom 获取 accept 时一次性获取
解析客户端地址
cpp 复制代码
uint16_t client_port = ntohs(client.sin_port);     // 网络→主机序
char client_ip[16];
inet_ntop(AF_INET, &(client.sin_addr), client_ip, sizeof(client_ip));

inet_ntop()inet_ntoa() 的对比

特性 inet_ntoa() inet_ntop()
返回值 char*(静态缓冲区) const char*(写入用户提供的缓冲区)
线程安全 ✘ 不安全 ✔ 安全
参数 in_addr 结构 地址族 + in_addr + 缓冲区 + 缓冲区大小
适用场景 单线程 多线程

为什么这里用 inet_ntop 而非 inet_ntoa

  • TCP 服务器是多线程的(线程池),如果用 inet_ntoa,多个线程同时调用会互相覆盖静态缓冲区
  • inet_ntop 把结果写入用户提供的 client_ip 数组,线程安全
第4步:推送任务到线程池
cpp 复制代码
ThreadPool<Task>::GetInstance()->Push(Task(sockfd, client_ip, client_port));

Task(sockfd, client_ip, client_port)

  • 构造一个 Task 对象,封装了连接信息和业务逻辑
  • Task 内部会 read() 客户端消息 → 查字典 → write() 返回翻译

Push() 的流程

  1. 加锁
  2. 任务入队列
  3. 唤醒一个等待的工作线程(pthread_cond_signal
  4. 解锁
Run() 中的注释代码 - 四个版本演进
cpp 复制代码
// v1: 单线程处理(服务端一次只能服务一个客户端)
Service(sockfd, client_ip, client_port);
close(sockfd);

// v2: 多进程处理
pid_t id = fork();
if(id == 0) {
    close(_listensockfd);    // 子进程不需要监听 Socket
    if(fork() > 0) exit(0);  // 子进程立刻退出,孙子进程成为孤儿(被 init 收养)
    Service(sockfd, ...);    // 孙子进程处理连接
    close(sockfd);
    exit(0);
}
waitpid(id, nullptr, 0);     // 父进程等待子进程(立即返回,因为子进程已退出)

// v3: 多线程处理
ThreadData *td = new ThreadData(sockfd, client_ip, client_port, this);
pthread_t tid;
pthread_create(&tid, nullptr, Routine, (void *)td);

// v4: 线程池(当前版本)
ThreadPool<Task>::GetInstance()->Push(Task(sockfd, client_ip, client_port));

四个版本对比

版本 方式 优点 缺点
v1 单线程 简单 无法并发,一个客户端占用时其他客户端等待
v2 多进程 隔离性好,子进程崩溃不影响父进程 fork 开销大,进程间通信复杂
v3 多线程 轻量,共享内存方便 频繁创建/销毁线程有开销
v4 线程池 线程复用,高效 实现稍复杂

v2 多进程的"两次 fork"技巧

复制代码
父进程 fork → 子进程1 (fork后立即 exit)  → 孙子进程 (孤儿,被 init 收养)
     │
     └── waitpid(子进程1) 立即返回
  • 子进程1 立刻退出,父进程的 waitpid 立即返回,可以继续 accept
  • 孙子进程变成孤儿进程,被 init(PID=1)收养
  • 孙子进程处理完连接后自动退出,由 init 回收

3.8 析构函数

cpp 复制代码
~TcpServer()
{}

为什么析构函数是空的?

  • 当前版本用 for(;;) 死循环,Run() 永远不会返回
  • TcpServer 对象永远不会被析构
  • 而且守护进程模式下,资源由 OS 统一回收

与 UDP 析构函数的对比

  • UDP 在析构时 close(_sockfd)
  • TCP 没有关闭 _listensockfd,因为永远不会执行到这里
  • 如果要改进,应该加 close(_listensockfd)

四、TcpClient.cpp 详细解析

4.1 文件头部

cpp 复制代码
#include <iostream>
#include <unistd.h>        // write(), read(), close(), sleep()
#include <sys/socket.h>    // socket(), connect()
#include <sys/types.h>
#include <arpa/inet.h>     // inet_pton(), htons()
#include <netinet/in.h>    // sockaddr_in
#include <string>
#include <cstring>         // strerror()

与服务端头文件的对比

  • 客户端没有 <signal.h><pthread.h>"Daemon.hpp"
  • 客户端是单线程同步模型,不需要多线程
  • 客户端不需要守护进程

4.2 Usage() 函数

cpp 复制代码
void Usage(const std::string &proc)
{
    std::cout<<"\n\rUsage: "<<proc<<" server_ip server_port"<<std::endl;
}

与 UDP 客户端 Usage 的对比

  • UDP:void Usage(std::string proc) ------ 传值
  • TCP:void Usage(const std::string &proc) ------ 传 const 引用
  • TCP 版本更规范,避免拷贝

4.3 main() 函数 - 逐段解析

第1段:检查参数与解析
cpp 复制代码
int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    
    std::string server_ip(argv[1]);
    uint16_t server_port = std::stoi(argv[2]);

参数说明

  • argv[0]:程序名
  • argv[1]:服务端 IP
  • argv[2]:服务端端口

与 UDP 客户端完全一致

第2段:构造服务端地址 - inet_pton
cpp 复制代码
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
inet_pton(AF_INET, server_ip.c_str(), &server.sin_addr);
server.sin_port = htons(server_port);

inet_pton()inet_addr() / inet_aton() 的对比

函数 输入 输出 支持协议 线程安全
inet_addr() C 字符串 返回 in_addr_t 仅 IPv4
inet_aton() C 字符串 写入 in_addr 仅 IPv4
inet_pton() 地址族 + C 字符串 写入 in_addr/in6_addr IPv4 + IPv6

inet_pton 接口

cpp 复制代码
int inet_pton(int af, const char *src, void *dst);
参数 含义
af AF_INET IPv4
src server_ip.c_str() IP 字符串
dst &server.sin_addr 输出:写入 in_addr 结构

返回值:1 = 成功,0 = 格式无效,-1 = 地址族不支持

为什么客户端用 inet_pton 而服务端用 inet_aton

  • 两种方式都可以正常工作
  • inet_pton 是更现代的接口,支持 IPv6,推荐使用
  • 代码中混用是因为学习目的,展示不同的 API
第3段:断线重连机制
cpp 复制代码
while(1)                          // 外层循环:支持重连
{    
    int cnt = 10;                 // 重连次数计数器
    int isreconnect = false;      // 是否需要重连标志
    
    do
    {
        sockfd = socket(AF_INET, SOCK_STREAM, 0);  // 每次重连都创建新 Socket
        if(sockfd < 0)
        {
            std::cout << "socket failed, ..." << std::endl;
            return 1;
        }
        
        int n = connect(sockfd, (struct sockaddr *)&server, sizeof(server));
        if(n < 0)
        {
            isreconnect = true;
            cnt--;
            std::cout << "connect failed, ..., reconnect cnt: " << cnt << std::endl;
            sleep(1);            // 等 1 秒再重试
        }
        else break;              // 连接成功,跳出 do-while
    } while(cnt && isreconnect);
    
    if(cnt == 0)                 // 重连 10 次都失败
    {
        std::cout << "connect failed, user offline" << std::endl;
        break;                   // 退出外层 while
    }

connect() 接口

cpp 复制代码
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数 含义
sockfd 客户端 Socket
addr 目标服务端地址
addrlen 地址长度

返回值:0 = 成功,-1 = 失败

connect() 做了什么?

  • 向服务端发送 SYN(第一次握手)
  • 等待服务端回复 SYN+ACK(第二次握手)
  • 发送 ACK(第三次握手)
  • 连接建立后返回 0

TCP 三次握手过程

复制代码
客户端                                服务端
  │                                    │
  │──── SYN (seq=x) ──────────────────►│  第一次握手
  │                                    │
  │◄─── SYN+ACK (seq=y, ack=x+1) ──────│  第二次握手
  │                                    │
  │──── ACK (ack=y+1) ────────────────►│  第三次握手
  │                                    │
  │        连接建立 (ESTABLISHED)        │

断线重连的设计

复制代码
┌─────────────────────────────────────────────┐
│  外层 while(1) 循环                          │
│  ┌─────────────────────────────────────┐    │
│  │  do-while 重连循环                    │    │
│  │  1. 创建 socket                       │    │
│  │  2. connect()                         │    │
│  │  3. 失败 → cnt--, sleep(1), 重试      │    │
│  │  4. 成功 → break                      │    │
│  │  5. cnt==0 → 放弃                    │    │
│  └─────────────────────────────────────┘    │
│  ┌─────────────────────────────────────┐    │
│  │  内层 while(1) 通信循环               │    │
│  │  1. getline 读输入                    │    │
│  │  2. write 发送                       │    │
│  │  3. read 接收回复                     │    │
│  │  4. write/read 失败 → break 回到外层  │    │
│  └─────────────────────────────────────┘    │
│  close(sockfd)                              │
└─────────────────────────────────────────────┘

为什么每次重连要创建新 Socket?

  • TCP 连接失败后,Socket 状态可能不可恢复
  • 旧 Socket 的资源可能还没完全释放
  • 创建新 Socket 最干净、最可靠
第4段:通信循环
cpp 复制代码
while(1)
{
    std::string message;
    std::cout<<"请输入消息: ";
    std::getline(std::cin, message);          // 读一行输入
    
    int n = write(sockfd, message.c_str(), message.size());  // 发送
    if(n < 0)
    {
        cnt--;
        isreconnect = true;
        std::cout << "write failed, ..." << std::endl;
        break;                                // 跳出通信循环,回到重连循环
    }
    
    char inbuffer[4096];
    n = read(sockfd, inbuffer, sizeof(inbuffer));  // 接收回复
    if(n > 0)
    {
        inbuffer[n] = '\0';
        std::cout<<"服务器回复: "<<inbuffer<<std::endl;
    }
    else break;                               // n<=0,服务端关闭或出错
}
close(sockfd);

write() 接口

cpp 复制代码
ssize_t write(int fd, const void *buf, size_t count);
参数 含义
fd Socket 文件描述符(connect 成功后的 sockfd)
buf 要发送的数据
count 数据长度

read() 接口

cpp 复制代码
ssize_t read(int fd, void *buf, size_t count);
参数 含义
fd Socket 文件描述符
buf 接收缓冲区
count 缓冲区大小

返回值含义(TCP 特有):

返回值 含义 原因
> 0 收到数据,返回字节数 正常
== 0 对方关闭连接 TCP 的 FIN 到达
< 0 出错 网络异常等

read 返回 0 的特殊含义

  • TCP 是面向连接的
  • 对端调用 close() 会发送 FIN
  • 本端 read 收到 FIN 后返回 0
  • 这是 TCP 判断"对方下线"的标准方式(UDP 没有)

为什么用 write/read 而非 send/recv

  • 在 Linux 中,Socket 是文件描述符
  • write(fd, ...)send(fd, ..., 0) 等价(flags=0 时)
  • read(fd, ...)recv(fd, ..., 0) 等价
  • write/read 更直观地体现了"一切皆文件"的 Linux 哲学

inbuffer[n] = '\0'

  • 与 UDP 客户端的 buffer[s] = 0 相同
  • 在收到的数据末尾加终止符,保证 cout 输出正确

通信失败后的处理

  • write 失败 → break 回到外层循环 → 尝试重连
  • read 返回 <= 0break 回到外层循环 → 尝试重连
  • close(sockfd) → 关闭旧连接,外层循环会创建新 Socket

五、辅助模块简述

5.1 Task.hpp - 任务封装

cpp 复制代码
class Task
{
public:
    Task(int sockfd, const std::string &client_ip, const uint16_t &client_port)
    : _sockfd(sockfd), _client_ip(client_ip), _client_port(client_port)
    {}
    
    void run()      // 业务逻辑
    {
        char buffer[4096];
        while(1)
        {
            ssize_t n = read(_sockfd, buffer, sizeof(buffer));
            if(n > 0)
            {
                buffer[n] = 0;
                std::string echo_string = init.translate(buffer);  // 查字典
                write(_sockfd, echo_string.c_str(), echo_string.size());
            }
            else if(n == 0)  // 客户端关闭
            {
                break;
            }
            else  // 出错
            {
                break;
            }
        }
        close(_sockfd);  // 关闭连接 Socket
    }
    
    void operator()()  // 仿函数,线程池调用
    {
        run();
    }
};

设计要点

  • operator() 让 Task 变成"可调用对象",线程池中 task() 就会调用 run()
  • run() 中循环 read/write,直到客户端断开
  • 客户端断开后 close(_sockfd) 释放连接资源

5.2 init.hpp - 字典初始化

cpp 复制代码
class Init
{
public:
    Init()  // 构造时加载字典文件
    {
        std::ifstream in("./dict.txt");
        while(std::getline(in, line))
        {
            Split(line, &part1, &part2);  // "apple:苹果" → "apple" + "苹果"
            _dict[part1] = part2;
        }
    }
    
    std::string translate(const std::string &key)
    {
        if(_dict.find(key) != _dict.end())
            return _dict[key];
        return "key not found";
    }
};

全局对象 Init init;Task.hpp 中定义,程序启动时自动加载字典。

5.3 ThreadPool.hpp - 线程池

cpp 复制代码
template<class T>
class ThreadPool
{
    // 单例模式
    static ThreadPool<T> *GetInstance();
    
    // 启动工作线程
    void Start();
    
    // 推入任务
    void Push(const T &in);
    
    // 工作线程函数
    static void* HanlderTask(void *arg)
    {
        while(1)
        {
            tp->Lock();
            while(tp->IsEmpty())
                tp->Wait();      // 无任务时等待
            T task = tp->Pop();   // 取出任务
            tp->Unlock();
            task();               // 执行任务(调用 Task::operator())
        }
    }
};

六、完整数据流图

复制代码
┌──────────────────────────────────────────────────────────────┐
│                        服务端 (test.cpp)                      │
│                                                              │
│  main()                                                      │
│    └── TcpServer                                             │
│          ├── Init()                                          │
│          │     ├── socket(AF_INET, SOCK_STREAM) → _listenfd  │
│          │     ├── setsockopt(SO_REUSEADDR)                  │
│          │     ├── bind(_listenfd, 0.0.0.0:8080)             │
│          │     └── listen(_listenfd, 10)                     │
│          │                                                   │
│          └── Run()                                           │
│                ├── Daemon()              ────── 守护进程      │
│                ├── ThreadPool.Start()    ────── 5个工作线程   │
│                └── for(;;)                                   │
│                      ├── accept() → sockfd (新连接)           │
│                      ├── inet_ntop() → client_ip             │
│                      └── Push(Task(sockfd, ip, port))         │
│                            │                                 │
│                            ▼                                 │
│                ┌─────────────────────────┐                   │
│                │  线程池工作线程           │                   │
│                │  Task::run()             │                   │
│                │  ├── read(sockfd)        │                   │
│                │  ├── init.translate()    │                   │
│                │  ├── write(sockfd)       │                   │
│                │  └── close(sockfd)       │                   │
│                └─────────────────────────┘                   │
└──────────────────────────────────────────────────────────────┘
                          ▲   │
                          │   │  TCP 字节流
                          │   ▼
┌──────────────────────────────────────────────────────────────┐
│                      客户端 (TcpClient.cpp)                    │
│                                                              │
│  main()                                                      │
│    └── while(1)  ────── 外层循环(支持重连)                   │
│          ├── do-while 重连                                   │
│          │     ├── socket(AF_INET, SOCK_STREAM)              │
│          │     ├── connect(sockfd, server_addr)              │
│          │     └── 失败 → sleep(1) → 重试(最多10次)         │
│          │                                                   │
│          └── while(1)  ────── 内层循环(通信)                │
│                ├── getline(cin, message)                     │
│                ├── write(sockfd, message)                    │
│                ├── read(sockfd, inbuffer)                    │
│                ├── cout << "服务器回复: " << inbuffer         │
│                └── 失败 → break → close(sockfd) → 回到外层    │
└──────────────────────────────────────────────────────────────┘

七、关键 API 对比表

7.1 TCP vs UDP API 对比

操作 UDP TCP 说明
创建 Socket socket(AF_INET, SOCK_DGRAM, 0) socket(AF_INET, SOCK_STREAM, 0) type 不同
绑定地址 bind() bind() 相同
监听 listen() TCP 独有
接受连接 accept() TCP 独有
发起连接 connect() TCP 客户端独有
发送数据 sendto() write() / send() UDP 需指定目标地址
接收数据 recvfrom() read() / recv() UDP 需获取发送方地址
关闭 close() close() 相同

7.2 TCP 服务端 API 调用链

复制代码
socket() → setsockopt() → bind() → listen() → accept() → read()/write() → close()

7.3 TCP 客户端 API 调用链

复制代码
socket() → connect() → write()/read() → close()

7.4 IP 地址转换函数对比

函数 方向 线程安全 支持 IPv6 使用位置
inet_addr() 字符串→整数 UDP 服务端
inet_aton() 字符串→in_addr TCP 服务端
inet_pton() 字符串→in_addr TCP 客户端
inet_ntoa() 整数→字符串 UDP 服务端
inet_ntop() in_addr→字符串 TCP 服务端

八、问题总结

8.1 为什么 TCP 比 UDP 多了 listen + accept?

复制代码
UDP(无连接):
  客户端 ──sendto──> 服务端(一个 Socket 收所有)
  
TCP(面向连接):
  客户端 ──connect──> 服务端 listen Socket
                         │
                    accept() 取出连接
                         │
                    返回新的 sockfd(专门服务这个客户端)
  • UDP 不需要维护连接状态,一个 Socket 收发所有数据
  • TCP 需要为每个客户端维护独立的连接状态(序号、窗口等),所以需要独立的 sockfd

8.2 为什么客户端不需要 bind?

cpp 复制代码
// TCP客户端需要绑定,但无需显式绑定,因为内核会在 connect() 时自动完成绑定随机端口
  • connect() 会触发三次握手
  • 内核在握手过程中自动为客户端 Socket 分配临时端口
  • 与 UDP 的 sendto 自动 bind 机制类似

8.3 read 返回 0 vs read 返回 -1

返回值 含义 原因 处理方式
> 0 正常收到数据 对端发来数据 处理数据
== 0 对端关闭连接 收到 FIN 包 break + close
< 0 出错 网络异常、EPIPE 等 break + close

UDP 的 recvfrom 不会返回 0(除非收到 0 字节数据报),因为 UDP 没有连接关闭的概念。

8.4 守护进程为什么要重定向标准 IO?

复制代码
普通进程:
  cout → fd 1 → 终端
  如果终端关闭 → fd 1 失效 → cout 抛异常 → 进程崩溃

守护进程:
  cout → fd 1 → /dev/null
  写入的数据被丢弃,操作成功
  进程不受终端关闭影响

8.5 为什么 setsockopt(SO_REUSEADDR) 很重要?

复制代码
没有 SO_REUSEADDR:
  服务端运行 → Ctrl+C 停止 → 端口进入 TIME_WAIT(1~2分钟)
  立即重启 → bind() 失败:EADDRINUSE → 必须等几分钟

有 SO_REUSEADDR:
  服务端运行 → Ctrl+C 停止 → 端口进入 TIME_WAIT
  立即重启 → bind() 成功 → 可以立即运行
相关推荐
bksczm1 小时前
linux之线程概念和控制
linux·开发语言·c++
爱网络爱Linux2 小时前
Linux proc 目录安全加固
linux·运维·rhce·rhca·红帽认证·proc 目录安全加固
JackieZhengChina2 小时前
零依赖轻量级JS悬浮提示插件title2tip-js的使用教程
开发语言·javascript·ecmascript
MindUp2 小时前
企业网盘权限模型解析:多层级访问控制与审计能力选型指南
java·linux·运维
hansang_IR2 小时前
【记录】「SCOI2016」三道模拟赛/26.7.12
c++·算法
持力行2 小时前
D-BUS会话总线
运维·网络
ICECREAM2 小时前
环形队列设计——基于共享内存的高效进程间通信
linux
pt10432 小时前
思科网络自动化-API常用数据编码格式(JSON/XML/YAML)课程与实践
linux·服务器·网络
kdxiaojie2 小时前
Linux 驱动研究 —— I2C (5)
linux·运维·笔记·学习