Linux ——— 网络开发核心知识与协议实现详解

目录

[网络基础概念 与 "协议"概念](#网络基础概念 与 “协议”概念)

[一、Linux 网络的核心前提:一切皆文件 → 网络通信 = 文件操作](#一、Linux 网络的核心前提:一切皆文件 → 网络通信 = 文件操作)

[二、网络诞生的背景:从 "单机" 到 "多机互联" 的必然性](#二、网络诞生的背景:从 “单机” 到 “多机互联” 的必然性)

[三、协议的本质:设备间的 "约定 / 规则"](#三、协议的本质:设备间的 “约定 / 规则”)

四、计算机网络的两类问题与对应协议

[五、Linux 中协议的实现:C 语言 + 结构体](#五、Linux 中协议的实现:C 语言 + 结构体)

总结
[进程端口号 与 TCP/UDP协议](#进程端口号 与 TCP/UDP协议)

一、进程端口号(Port)的核心定义与数据传输全过程

[二、为什么有 PID(进程 ID)还需要端口号?](#二、为什么有 PID(进程 ID)还需要端口号?)

三、端口号与进程的绑定规则

[四、TCP/UDP 协议:传输层的两大核心协议](#四、TCP/UDP 协议:传输层的两大核心协议)

总结
[基于 UDP 协议实现服务端与用户端网络通信](#基于 UDP 协议实现服务端与用户端网络通信)

UdpServer.cc(服务端)

UdpClient.cc(客户端)

一、套接字(Socket)编程的核心种类与本质

[二、UdpServer.cc 核心函数解析](#二、UdpServer.cc 核心函数解析)

[三、UdpClient.cc 核心函数解析](#三、UdpClient.cc 核心函数解析)

总结
[基于 TCP 协议实现服务端与用户端网络通信(守护进程化)](#基于 TCP 协议实现服务端与用户端网络通信(守护进程化))

TcpServer.cc(服务端)

TcpClient.cc(客户端)

[一、TCP 服务端 / 客户端代码整体逻辑](#一、TCP 服务端 / 客户端代码整体逻辑)

二、核心函数解析

总结
json(序列/反序列化)

[test.cc(Json 的基本使用)](#test.cc(Json 的基本使用))

[JSON 代码全解析与网络传输核心作用](#JSON 代码全解析与网络传输核心作用)

[JsonCpp 核心类与函数解析](#JsonCpp 核心类与函数解析)

序列化与反序列化的定义

序列化与反序列化在网络传输中的作用

序列化与反序列化的执行时机

[结合 TCP/UDP 编程的实际应用流程](#结合 TCP/UDP 编程的实际应用流程)

核心知识点总结
[HTTP 协议](#HTTP 协议)

Sock.hpp(封装套接字)

HTTPServer.hpp(封装HTTP服务端)

HTTPServer.cc(服务端主函数)

[网址与 URL 的核心本质](#网址与 URL 的核心本质)

网络基础行为分类

[HTTP 请求报文格式](#HTTP 请求报文格式)

[HTTP 响应报文格式](#HTTP 响应报文格式)

代码核心逻辑解析

[Web 根目录与 URL 路径映射](#Web 根目录与 URL 路径映射)

[GET 与 POST 请求方法的核心区别](#GET 与 POST 请求方法的核心区别)

[Cookie 与 Session 原理及安全机制](#Cookie 与 Session 原理及安全机制)

[常用 HTTP 请求报头解析](#常用 HTTP 请求报头解析)
[HTTPS 协议](#HTTPS 协议)

[HTTPS 与 HTTP 的核心区别](#HTTPS 与 HTTP 的核心区别)

明文、密文与密钥的基础概念

对称加密的定义与特点

非对称加密的定义、特点与公私钥机制

[MD5 算法与数据指纹](#MD5 算法与数据指纹)

[HTTPS 混合加密的通信流程](#HTTPS 混合加密的通信流程)

中间人攻击的原理与危害

[CA 证书与 CA 认证的安全解决方案](#CA 证书与 CA 认证的安全解决方案)


网络基础概念 与 "协议"概念

一、Linux 网络的核心前提:一切皆文件 → 网络通信 = 文件操作

Linux 系统的核心设计哲学是 **"一切皆文件"** ------ 所有硬件设备、系统资源都会被抽象成 "文件",并通过统一的文件操作接口(open/read/write/close)管理,网卡也不例外:

  • 网卡作为网络硬件,会被映射为系统中的设备文件(如/dev/net/tun);
  • 编程层面:往网卡文件写入数据 = 往网络中发送数据;从网卡文件读取数据 = 从网络中接收数据。举个例子:你用write()函数往网卡文件写入一串字节,这些字节会通过网线 / 无线信号发送到其他设备;用read()函数从网卡文件读字节,就是接收其他设备发来的网络数据。这种设计让网络编程和普通文件操作的逻辑高度统一,新手只需掌握文件操作,就能快速理解网络通信的核心。

二、网络诞生的背景:从 "单机" 到 "多机互联" 的必然性

1. 单机的局限性

你之前学习的进程、线程、文件操作,都属于 "单机范畴"------ 所有操作只在一台电脑内部完成。早期跨电脑的数据交互只能靠 U 盘、光驱、移动硬盘等物理介质:

  • 效率极低:需要线下传递介质,无法实时交互;
  • 风险高:介质易损坏、丢失,数据可能泄露或损坏。

2. 网络的演化

为解决跨机数据交互的痛点,网络应运而生,且逐步演化:

  • 局域网(LAN):最初是几台电脑通过数据线直连(小型私有网络,如家庭 / 办公室网络);
  • 广域网(WAN):随着电脑数量增加,通过交换机、路由器将多个局域网连接,形成更大的网络(如校园网、运营商网络);
  • 互联网:全球范围内的广域网互联,本质是 "最大的局域网"。
    核心结论 :局域网和广域网是相对概念------ 比如你家的 WiFi 是局域网,把全国的家庭 / 企业局域网连起来,就是广域网(互联网)。

三、协议的本质:设备间的 "约定 / 规则"

协议的核心是多台设备为了正常通信,提前约定好的 "规则集合" ------ 没有规则,设备间传输的只是一堆无意义的字节,就像和不懂中文的外国人交流,对方无法理解你的意图。

生活例子(你提到的座机约定)

  • 约定主体:你(学生)和家人;
  • 约定信号:电话响铃的次数;
  • 约定含义:1 声 = 报平安、2 声 = 要生活费、3 声及以上 = 有事需接电话;
  • 协议价值:无需额外沟通,仅通过 "响铃次数" 就能精准传递信息,避免误解。

计算机网络中的协议

比如两台电脑要传 "hello":

  • 约定 1:字节顺序是 "h→e→l→l→o",而非反向;
  • 约定 2:用 "0x00" 标识数据开始,"0xFF" 标识数据结束;
  • 约定 3:收到数据后回复 "OK" 表示接收成功。只有双方遵守这些约定,才能正确传输和解析数据。

四、计算机网络的两类问题与对应协议

多机通信会遇到 "技术问题"(数据怎么传)和 "应用问题"(数据怎么用),每一层都有专属协议解决,就像网购洗面奶的 "快递规则":

1. 技术问题(底层传输保障)

解决 "数据能准确、完整、定向传输" 的问题,对应网络底层协议:

核心问题 解决协议 生活类比
如何保证数据准确传到下一台设备(如路由器) 数据链路层协议(如以太网协议) 快递员把包裹从快递站 A 送到 B,需约定 "包裹怎么打包、怎么标识归属 B 站、损坏如何核对"
如何定位目标主机(把数据发对电脑) IP 协议(网络层) 网购时填写收货地址(省 / 市 / 区 / 小区),IP 地址就是主机的 "网络地址"
长距离传输数据丢失 / 乱序怎么办 TCP 协议(传输层) 快递中途丢件会补发、包裹乱序会重新整理,TCP 协议约定 "丢包重发、乱序重排"

2. 应用问题(上层数据处理)

解决 "收到数据后怎么解析、怎么用" 的问题,对应应用层协议:

  • HTTP/HTTPS:约定网页数据的格式(如请求头、响应体),浏览器靠它解析出文字、图片、视频;
  • FTP:约定文件传输规则,用于两台电脑间传文件;
  • SMTP:约定邮件发送 / 接收规则,用于发邮件。

核心类比:网购洗面奶的 "协议"

  • 核心数据:洗面奶(对应网络中要传输的真实业务数据);
  • 协议附加信息:快递盒(数据打包格式)、快递单号(地址 / 标识)、物流跟踪(丢件补发)→ 对应网络协议给数据加的 "协议头 / 标识";
  • 本质:没有这些 "快递协议",洗面奶可能丢、送错;没有网络协议,数据也会丢、错、无法解析。

五、Linux 中协议的实现:C 语言 + 结构体

Linux 内核中的所有网络协议(TCP/IP、以太网等)都是用 C 语言实现的,而结构体是协议的核心载体

  1. 协议的 "约定" 可拆解为多个字段:比如 IP 协议约定了 "源 IP 地址、目标 IP 地址、数据长度、协议版本" 等字段;

  2. 用结构体描述协议字段:

    复制代码
    // 简化的IP协议头结构体
    struct ip_header {
        uint8_t version;   // IP版本(如IPv4)
        uint8_t len;       // 协议头长度
        uint32_t src_ip;   // 源IP地址
        uint32_t dst_ip;   // 目标IP地址
        // 其他字段...
    };
  3. 传输逻辑:发送数据时,先填充协议结构体(如 IP 头),再把真实数据 "附在" 结构体后,组成完整数据包;接收方拿到数据包后,先解析结构体(协议头),再提取里面的真实数据。类比:快递单号(结构体)包含收件人地址、电话,快递员先看单号(解析结构体),再把里面的洗面奶(真实数据)交给你。

总结

  1. Linux 网络核心:网卡 = 文件,网络通信 = 文件读写,和单机文件操作逻辑统一;
  2. 网络诞生:解决单机数据交互的低效 / 高风险问题,从局域网演化到广域网;
  3. 协议本质:设备间的通信约定,无协议则数据无意义;
  4. 协议分层:底层协议解决 "数据怎么传",应用层协议解决 "数据怎么用";
  5. Linux 协议实现:用 C 语言编写,通过结构体描述协议的字段和规则。

进程端口号 与 TCP/UDP协议

一、进程端口号(Port)的核心定义与数据传输全过程

1. 端口号的本质

端口号是传输层给主机内进程分配的 "网络标识"(16 位整数,范围 0-65535),作用是:在一台主机(通过 IP 唯一标识)内,精准定位 "哪个进程要收发网络数据"。

  • IP 地址:定位全网唯一的主机(比如微信服务器的公网 IP 是固定的,能找到这台服务器);
  • 端口号:定位主机内唯一的进程(比如微信服务器用 8080 端口监听客户端请求);
  • 核心结论:IP + 端口号 = 全网唯一的进程(比如119.XX.XX.XX:8080就是微信服务器的通信进程)。

2. 数据传输全过程(以微信客户端→服务器为例)

你可以把这个过程理解为 "寄快递":客户端是寄件人,服务器是收件人,每一层协议都是 "快递包装 / 面单",具体步骤如下:

复制代码
微信客户端(你主机)                微信服务器(目标主机)
应用层:准备"消息内容"(有效载荷)
↓ 加应用层报头(如微信自定义格式)
传输层:加TCP/UDP报头(源端口:比如你的微信进程占56789;目的端口:服务器微信进程占8080)
↓ 报头+有效载荷向下传递
网络层:加IP报头(源IP:你的公网IP;目的IP:服务器公网IP)
↓ 报头+数据向下传递
数据链路层:加MAC报头(源MAC:你的网卡MAC;目的MAC:路由器出接口MAC)
↓ 交给路由器转发(逐跳更新MAC)
路由器转发:每经过一个路由器,重新封装MAC报头(源MAC换成路由器出接口MAC,目的MAC换成下一跳设备MAC),但IP报头全程不变(端到端)
↓ 到达服务器主机
数据链路层:拆MAC报头 → 传给网络层
网络层:拆IP报头 → 传给传输层
传输层:拆TCP/UDP报头,根据目的端口8080找到服务器微信进程
应用层:拆应用层报头 → 服务器微信进程拿到你的消息

核心类比

  • IP 地址 = 收件人所在的 "省市区街道门牌号"(定位到具体房子);
  • 端口号 = 房子里的 "房间号"(定位到具体的人);
  • MAC 地址 = 快递员每次送货的 "临时地址"(比如从小区门口到单元门,地址会变,但最终目的地不变)。

二、为什么有 PID(进程 ID)还需要端口号?

PID 是操作系统给主机内进程分配的 "本地标识",能标识单机内的唯一进程,但网络通信场景下必须用端口号,核心原因有 2 个:

1. 系统功能与网络功能解耦

  • PID 的作用:操作系统内核管理进程(比如调度、内存分配、杀死进程),是 "单机内部的管理标识",和网络无关;
  • 端口号的作用:专门用于网络通信,是 "跨机识别进程的标识",由传输层协议(TCP/UDP)管理;解耦价值:即使操作系统的进程管理逻辑变了(比如 PID 分配规则改了),只要端口号规则不变,网络通信不受影响,就像你家的 "户口本编号"(PID)和 "快递收货手机号"(端口号)各司其职,互不干扰。

2. PID 的动态性 vs 端口号的稳定性

  • PID:进程退出后重新启动,PID 会随机生成新值(比如微信关掉再打开,PID 从 1234 变成 5678);
  • 端口号:进程可以绑定固定端口号(比如微信服务器长期绑定 8080 端口),客户端无需每次重新适配,就像你的手机号(端口号)不变,别人总能联系到你,而你的身份证号可能因补办变更(类比 PID),但不影响通信。

三、端口号与进程的绑定规则

1. 一个进程可以绑定多个端口号

只要能保证 "数据收发的唯一性",一个进程可以监听多个端口,比如:

  • 微信服务器进程同时绑定 8080(处理安卓客户端)、8081(处理 iOS 客户端)、8082(处理 PC 客户端);
  • 核心逻辑:进程能识别不同端口的请求,分别处理,就像一个客服中心有多个电话号码(端口),但都由同一个客服团队(进程)接听。

2. 一个端口号不能绑定多个进程

端口号是主机内的唯一标识,若多个进程绑定同一个端口,会导致:

  • 传输层收到数据后,无法判断该发给哪个进程(比如 8080 端口同时绑定微信和 QQ 进程,服务器收到 "hello" 后,不知道该给微信还是 QQ);
  • 系统层面:操作系统会禁止这种操作(绑定失败,返回 "端口被占用" 错误),就像一个电话号码不能同时分给两个人,否则来电时无法确定接电话的人。

四、TCP/UDP 协议:传输层的两大核心协议

TCP 和 UDP 都是传输层协议,负责 "主机间的端到端数据传输",但设计理念和特性完全不同,且 "可靠 / 不可靠" 无褒贬之分,只是适配不同场景:

特性 TCP 协议 UDP 协议
连接性 面向连接(需三次握手建立连接) 无连接(直接发数据,无需建立连接)
传输可靠性 可靠传输(保证数据不丢、不乱序、不重复) 不可靠传输(不保证数据到达,可能丢包、乱序)
数据格式 面向字节流(数据无边界,像流水) 面向数据报(数据有明确边界,一个包就是一个完整单元)
效率 / 代价 效率低(需确认、重传、排序),代价高 效率高(无额外开销),代价低
典型场景 需保证数据完整的场景(微信消息、文件传输、网页加载) 允许少量丢包的场景(视频直播、语音通话、游戏实时数据)

关键补充:"可靠 / 不可靠" 的本质

  • TCP 的 "可靠":通过三次握手(建立连接)、确认应答(收到数据回复 ACK)、超时重传(丢包后重新发送)、排序(乱序后重新整理)等机制,保证数据 100% 到达且有序;
  • UDP 的 "不可靠":发数据前不建立连接,发完就不管,也不要求对方确认,丢包了也不重传;
  • 无褒贬之分:比如视频直播用 UDP------ 丢 1 个数据包不影响画面,若用 TCP 重传,反而会导致画面卡顿;而微信消息用 TCP------ 必须保证消息 100% 到达,哪怕牺牲一点效率。

总结

  1. 端口号是进程的 "网络标识",IP + 端口号实现全网进程唯一标识,数据传输时每一层加报头、逐跳更新 MAC,IP 全程不变;
  2. PID 是单机进程管理标识,端口号为网络通信设计,解决 PID 动态性和功能解耦问题;
  3. 端口绑定规则:一进程多端口(可行)、一端口多进程(不可行),核心保证唯一性;
  4. TCP/UDP 是传输层协议,TCP 可靠但效率低,UDP 不可靠但高效,适配不同业务场景,无优劣之分。

基于 UDP 协议实现服务端与用户端网络通信

UdpServer.cc(服务端)

复制代码
#include <iostream>       // 控制台输入输出头文件
#include <cstdlib>        // 包含exit()等系统函数
#include <cstring>        // 包含memset/bzero等内存操作函数
#include <sys/types.h>    // 系统类型定义(如socket相关类型)
#include <sys/socket.h>   // socket核心系统调用头文件(socket/bind/recvfrom/sendto)
#include <string>         // C++ string类头文件
#include <strings.h>      // 包含bzero等字符串操作函数
#include <netinet/in.h>   // 网络地址结构体(sockaddr_in)、字节序转换函数
#include <arpa/inet.h>    // 包含inet_addr等IP地址转换函数
#include <memory>         // 智能指针头文件(unique_ptr)
#include <unistd.h>       // 包含close等系统调用
using namespace std;      // 简化std::前缀

// 枚举错误码:标识不同阶段的错误类型,便于定位问题
enum
{
    SOCKET_ERR = 1,   // 创建socket失败的错误码
    BIND_ERR,         // 绑定端口失败的错误码
    RECVFROM_ERR      // 接收数据失败的错误码
};

// 全局常量定义
const string defaultip = "0.0.0.0";  // 默认绑定的IP(0.0.0.0表示绑定所有网卡)
const uint16_t defaultport = 8080;   // 默认绑定的端口号
const int size = 1024;               // 接收缓冲区大小

// UDP服务端类:封装UDP服务端的核心逻辑(创建socket、绑定、接收/发送数据)
class UdpServer
{
public:
    // 构造函数:初始化成员变量,默认端口为8080
    // 注:省略了IP参数,固定绑定0.0.0.0(所有网卡)
    UdpServer(const uint16_t port = defaultport)
        : _sockfd(-1)          // 初始化socket文件描述符为-1(无效值)
        , _ip(defaultip)       // 绑定所有网卡地址
        , _port(port)          // 绑定的端口号
        , _isrunning(false)    // 服务端运行状态:初始为停止
    {}

    // 初始化函数:创建socket、绑定端口
    void Init()
    {
        // 1. 创建UDP套接字
        // socket()参数说明:
        // AF_INET:使用IPv4协议族;SOCK_DGRAM:UDP协议(无连接、面向数据报);0:默认协议
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);  // AF_INET等价于PF_INET
        if(_sockfd < 0)  // 创建失败(返回-1)
        {
            perror("socket");  // 打印系统错误信息
            exit(SOCKET_ERR);  // 退出程序,错误码为SOCKET_ERR
        }
        cout << "socket create success, sockfd is : " << _sockfd << endl;

        // 2. 绑定socket到指定IP和端口(UDP必须绑定,否则无法接收数据)
        struct sockaddr_in local;  // IPv4网络地址结构体(存储本地绑定信息)
        bzero(&local, sizeof(local));  // 清空结构体(避免脏数据)
        local.sin_family = AF_INET;    // 协议族:IPv4
        // 端口转换:htons()将主机字节序(小端)转为网络字节序(大端),网络传输必须用网络字节序
        local.sin_port = htons(_port);
        // 绑定所有网卡:INADDR_ANY等价于inet_addr("0.0.0.0"),表示接收所有网卡的UDP数据
        local.sin_addr.s_addr = INADDR_ANY;  // 替代:inet_addr(_ip.c_str())(绑定指定IP)
        
        // bind()参数说明:
        // _sockfd:socket文件描述符;(sockaddr*)&local:绑定的地址结构体;sizeof(local):结构体大小
        int ret = bind(_sockfd, (const struct sockaddr*)&local, sizeof(local));
        if(ret < 0)  // 绑定失败(返回-1)
        {
            perror("bind");  // 打印系统错误信息
            exit(BIND_ERR);  // 退出程序,错误码为BIND_ERR
        }
        cout << "bind success, ret is : " << ret << endl;
    }

    // 运行函数:循环接收客户端数据,并回显数据给客户端
    void Run()
    {
        _isrunning = true;  // 标记服务端为运行状态
        char inbuffer[size];// 接收数据的缓冲区

        // 循环接收数据(服务端常驻运行)
        while(_isrunning)
        {
            struct sockaddr_in client;  // 存储客户端的地址信息(IP+端口)
            socklen_t len = sizeof(client);  // 客户端地址结构体的大小(需传引用,recvfrom会修改)

            // 3. 接收客户端数据:recvfrom()是UDP接收数据的核心函数(无连接,需获取客户端地址)
            // 参数说明:
            // _sockfd:socket描述符;inbuffer:接收缓冲区;sizeof(inbuffer)-1:缓冲区大小(留1字节存'\0');
            // 0:默认标志;(sockaddr*)&client:输出参数,存储发送端(客户端)的地址;&len:输入输出参数,结构体大小
            ssize_t n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0 , (struct sockaddr*)&client, &len);
            if(n < 0)  // 接收失败(返回-1)
            {
                perror("recvfrom");
                exit(RECVFROM_ERR);
            }

            inbuffer[n] = 0;  // 给接收的数据加字符串结束符,转为C风格字符串
            string info = inbuffer;  // 转为C++ string
            string echo_string = "Server echo#" + info;  // 构造回显字符串
            cout << echo_string << endl;  // 打印接收到的数据

            // 4. 回显数据给客户端:sendto()是UDP发送数据的核心函数(需指定目标地址)
            // 参数说明:
            // _sockfd:socket描述符;echo_string.c_str():发送的数据;echo_string.size():数据长度;
            // 0:默认标志;(sockaddr*)&client:目标地址(客户端);len:客户端地址结构体大小
            sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (const struct sockaddr*)&client, len);
        }

        _isrunning = false;  // 循环结束,标记为停止状态
    }

    // 析构函数:关闭socket文件描述符,释放系统资源
    ~UdpServer()
    {
        close(_sockfd);
    }

private:
    int _sockfd;          // 网络文件描述符(socket的唯一标识)
    string _ip;           // 绑定的IP地址(本例固定为0.0.0.0)
    uint16_t _port;       // 绑定的端口号
    bool _isrunning;      // 服务端运行状态标记
};

// 用法提示函数:当命令行参数输入错误时,提示正确用法
void Usage(string proc)
{
    cout << "\n\rUsage: " << proc << " port[1024+]\n" << endl;
}

// 主函数:解析命令行参数,创建并运行UDP服务端
int main(int argc, char* argv[])
{
    // 检查命令行参数:必须传入1个参数(端口号),格式为 ./UdpServer 8080
    if(argc != 2)
    {
        Usage(argv[0]);  // 提示用法(argv[0]是程序名)
        exit(0);         // 正常退出
    }

    uint16_t port = stoi(argv[1]);  // 将命令行参数转为端口号(uint16_t)

    // 用智能指针管理UdpServer对象(自动释放,避免内存泄漏)
    unique_ptr<UdpServer> svr(new UdpServer(port));

    svr->Init();  // 初始化(创建socket、绑定端口)
    svr->Run();   // 运行服务端(循环接收/发送数据)

    return 0;
}

UdpClient.cc(客户端)

复制代码
#include <iostream>       // 控制台输入输出头文件
#include <cstdlib>        // 包含exit()等系统函数
#include <cstring>        // 包含memset/bzero等内存操作函数
#include <sys/types.h>    // 系统类型定义
#include <sys/socket.h>   // socket核心系统调用头文件
#include <string>         // C++ string类头文件
#include <strings.h>      // 包含bzero等字符串操作函数
#include <netinet/in.h>   // 网络地址结构体、字节序转换函数
#include <arpa/inet.h>    // 包含inet_addr等IP地址转换函数
#include <memory>         // 智能指针头文件(本例未使用,保留)
#include <unistd.h>       // 包含close等系统调用
using namespace std;      // 简化std::前缀

// 用法提示函数:命令行参数输入错误时,提示正确用法
void Usage(string proc)
{
    // 提示格式:./UdpClient 服务端IP 服务端端口(如 ./UdpClient 127.0.0.1 8080)
    cout << "\n\rUsage: " << proc << " serverIP serverport\n" << endl;
}

// 主函数:创建UDP客户端,向服务端发送数据并接收回显
int main(int argc, char* argv[])
{
    // 检查命令行参数:必须传入2个参数(服务端IP、端口)
    if(argc != 3)
    {
        Usage(argv[0]);  // 提示用法
        exit(0);         // 正常退出
    }

    string serverIP = argv[1];        // 服务端IP地址(命令行参数1)
    uint16_t serverPort = stoi(argv[2]);  // 服务端端口号(命令行参数2)

    // 构造服务端地址结构体:存储要发送的目标地址(服务端)
    struct sockaddr_in server;
    bzero(&server, sizeof(server));  // 清空结构体

    server.sin_family = AF_INET;     // 协议族:IPv4
    server.sin_port = htons(serverPort);  // 端口转换:主机字节序→网络字节序
    // IP转换:inet_addr()将点分十进制IP(如127.0.0.1)转为网络字节序的整数
    server.sin_addr.s_addr = inet_addr(serverIP.c_str());
    
    socklen_t len = sizeof(server);  // 服务端地址结构体大小

    // 1. 创建UDP套接字(客户端无需绑定端口,系统会自动分配临时端口)
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if(sockfd < 0)  // 创建失败
    {
        perror("socket");
        exit(-1);
    }

    string message;   // 存储用户输入的消息
    char buffer[1024];// 接收服务端回显数据的缓冲区
    // 循环发送数据(客户端常驻运行,直到手动退出)
    while(true)
    {
        cout << "Please Enter@";      // 提示用户输入
        getline(cin, message);        // 获取用户输入的一行字符串(支持空格)

        // 2. 向服务端发送数据:sendto()指定目标地址为服务端
        sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server, len);

        // 存储发送端(服务端)的临时地址(实际用不到,仅满足recvfrom参数要求)
        struct sockaddr_in temp;
        socklen_t tempLen = sizeof(temp);

        // 3. 接收服务端的回显数据
        ssize_t s = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr*)&temp, &tempLen);
        if(s > 0)  // 接收成功(返回接收到的字节数)
        {
            buffer[s] = 0;  // 加字符串结束符
            cout << buffer << endl;  // 打印服务端的回显数据
        }
    }

    close(sockfd);  // 关闭socket(本例循环不会结束,实际不会执行)

    return 0;
}

一、套接字(Socket)编程的核心种类与本质

1. 套接字的本质

套接字(Socket)是操作系统提供的进程间通信(IPC)统一接口 ,核心作用是让进程能跨主机 / 跨本机通信,底层把不同通信场景的逻辑封装成统一的socket系列函数(socket/bind/recvfrom/sendto等),屏蔽了底层通信细节。

2. 套接字编程的三大种类

类型 用途 核心地址结构体 地址类型标识
域间套接字(Unix 域) 同一主机内的进程通信 struct sockaddr_un AF_UNIX
原始套接字 网络工具开发(如抓包) 自定义 AF_PACKET
网络套接字 跨主机的网络通信 struct sockaddr_in AF_INET

3. 地址结构体的统一封装

不同套接字的地址结构体格式不同,但系统统一封装为struct sockaddr(通用结构体),方便函数接口统一:

  • struct sockaddr(通用):16 位地址类型(如AF_INET) + 14 字节地址数据;
  • struct sockaddr_un(域间):16 位AF_UNIX + 108 字节本机路径名(如/tmp/sock);
  • struct sockaddr_in(网络):16 位AF_INET + 16 位端口号 + 32 位 IP 地址 + 8 字节填充;
  • 核心逻辑:bind/recvfrom/sendto等函数接收struct sockaddr*类型参数,内部会根据 "16 位地址类型" 判断是AF_INET(网络)还是AF_UNIX(域间),再解析对应结构体。

二、UdpServer.cc 核心函数解析

1. Init 函数:初始化 Socket 与绑定端口

Init函数是服务端的核心初始化逻辑,完成 "创建 Socket" 和 "绑定端口 / IP" 两大核心操作:

(1) socket 函数:创建 UDP 套接字

复制代码
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
  • 参数解析
    • AF_INET:指定使用IPv4 协议族(网络层用 IPV4 地址定位主机);
    • SOCK_DGRAM:指定套接字类型为数据报套接字(对应 UDP 协议,无连接、面向数据报);
    • 0:指定协议编号,0表示 "根据前两个参数自动选择默认协议"(此处自动选 UDP);
  • 返回值 :成功返回网络文件描述符(sockfd) (非负整数,类似文件操作的 fd),失败返回-1
  • 本质:向操作系统申请一个 "网络通信的文件描述符",后续所有网络操作都通过这个 fd 完成。

(2) local 结构体(sockaddr_in):封装绑定的地址信息

复制代码
struct sockaddr_in local;
bzero(&local, sizeof(local));  // 清空结构体,避免脏数据
local.sin_family = AF_INET;    // 协议族:IPv4
local.sin_port = htons(_port); // 端口号:主机字节序→网络字节序
local.sin_addr.s_addr = INADDR_ANY;  // 绑定所有网卡
  • 成员解析
    • sin_family:必须和socket函数的AF_INET一致,标识地址类型;
    • sin_port:要绑定的端口号(需转换为网络字节序);
      • htons函数作用:主机字节序(Host)→ 网络字节序(Network),短整型(Short) 。网络传输统一用大端序 (高位字节存低地址),而主机可能是小端序(如 x86 架构),因此必须转换:若主机是大端,htons不修改值;若主机是小端,htons反转字节序。
      • 为什么要转换:_port是主机内存中的存储形式,跨主机传输时必须统一为网络字节序,否则目标主机解析端口号会出错。
    • sin_addr.s_addr:绑定的 IP 地址(32 位整数);
      • INADDR_ANY:等价于inet_addr("0.0.0.0"),表示 "绑定本机所有网卡"(无论数据从哪个网卡进来,只要端口匹配就接收);
      • inet_addr函数作用:将 "点分十进制 IP 字符串(如 192.168.1.1)" 转为网络字节序的 32 位整数,供结构体存储。

(3) bind 函数:绑定 Socket 与地址(IP + 端口)

复制代码
int ret = bind(_sockfd, (const struct sockaddr*)&local, sizeof(local));
  • 参数解析
    • _sockfdsocket函数返回的网络文件描述符;
    • (const struct sockaddr*)&local:将struct sockaddr_in(网络专用)强制转为struct sockaddr(通用),因为bind函数的接口要求接收通用结构体(系统设计的统一接口);
    • sizeof(local):地址结构体的大小;
  • 返回值 :成功返回0,失败返回-1
  • 核心作用:告诉操作系统 "将这个 sockfd 与指定的 IP + 端口绑定",后续所有发往该 IP + 端口的 UDP 数据,都会交付给这个 sockfd 对应的进程。

2. Run 函数:循环接收 / 发送客户端数据

(1) client 结构体(sockaddr_in):存储客户端地址信息

复制代码
struct sockaddr_in client;
socklen_t len = sizeof(client);
  • 含义recvfrom函数的输出参数 ,无需手动填充字段 ------ 当服务端接收到数据时,recvfrom会自动解析数据包的源地址(客户端的 IP + 端口),并填充到client结构体中;
  • 为什么需要 :UDP 是无连接协议,服务端不知道数据来自哪个客户端,必须通过client获取客户端地址,才能用sendto回显数据。

(2) recvfrom 函数:接收 UDP 客户端数据

复制代码
ssize_t n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer)-1, 0, (struct sockaddr*)&client, &len);
  • 核心作用 :从sockfd读取 UDP 数据,并获取发送端(客户端)的地址;
  • 参数解析
    • _sockfd:网络文件描述符;
    • inbuffer:接收数据的缓冲区(存储客户端发来的字符串);
    • sizeof(inbuffer)-1:缓冲区大小(留 1 字节存'\0',避免字符串越界);
    • 0:默认标志(阻塞接收,无数据则等待);
    • (struct sockaddr*)&client:输出参数,存储客户端地址;
    • &len:输入输出参数,输入时是client结构体大小,输出时是实际解析的地址大小;
  • 返回值 :成功返回接收到的字节数 ,失败返回-1,连接关闭返回0(UDP 无连接,一般不会返回 0);
  • 后续操作inbuffer[n] = 0:给接收的字节加字符串结束符,转为 C 风格字符串。

(3) sendto 函数:回显数据给客户端

复制代码
sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (const struct sockaddr*)&client, len);
  • 核心作用:向指定地址(客户端)发送 UDP 数据;
  • 参数解析
    • _sockfd:网络文件描述符;
    • echo_string.c_str():要发送的字符串(C 风格);
    • echo_string.size():发送数据的字节长度;
    • 0:默认标志;
    • (const struct sockaddr*)&client:目标地址(客户端的 IP + 端口,由recvfrom填充);
    • len:客户端地址结构体的大小;
  • 返回值 :成功返回发送的字节数 ,失败返回-1
  • 本质:UDP 无连接,每次发送都需要指定目标地址(不像 TCP 只需建立连接后发送)。

3. 云服务器禁止直接 bind 公网 IP 的原因

  • 云服务器的公网 IP 是虚拟化的(由云厂商的网关 / 路由器映射),并非主机网卡的真实 IP(主机真实 IP 是内网 IP,如 172.17.0.2);
  • 若绑定固定公网 IP,会导致:① 云厂商调整公网 IP 时服务不可用;② 无法接收来自其他网卡的流量;
  • 解决方案:绑定0.0.0.0INADDR_ANY)------ 所有发往该服务器的 UDP 数据,只要端口匹配,无论公网 / 内网 IP,都会交付给绑定该端口的进程。

4. 端口号范围说明

  • [0, 1023]系统保留端口(特权端口),由固定应用层协议占用(如 80=HTTP、443=HTTPS、21=FTP),普通用户进程无权限绑定;
  • [1024, 65535]动态端口 ,普通程序建议绑定1024+(如代码中提示port[1024+]),避免和系统服务冲突。

三、UdpClient.cc 核心函数解析

1. 客户端是否需要 bind 端口?

  • 可以 bind :手动指定客户端端口(如bind(sockfd, &client_addr, sizeof(client_addr)));
  • 不需要 bind :客户端的端口号无需固定,操作系统会在客户端首次调用sendto时,自动分配一个临时端口(保证主机内唯一即可);
  • 核心原因:客户端的核心是 "找到服务端(IP + 端口)",而服务端只需 "回复到客户端的 IP + 临时端口",客户端端口号具体是多少不重要,无需手动绑定。

2. socket 函数(客户端)

复制代码
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
  • 含义 :和服务端socket函数完全一致,创建 UDP 套接字,返回网络文件描述符;
  • 区别 :客户端无需绑定端口,因此调用socket后直接使用,无需bind

3. sendto 函数(客户端)

复制代码
sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server, len);
  • 含义 :向服务端(server结构体封装的 IP + 端口)发送用户输入的消息;
  • 参数解析server结构体是客户端手动填充的服务端地址(IP + 端口),其余参数和服务端sendto一致;
  • 核心逻辑 :UDP 客户端首次调用sendto时,操作系统会自动为其分配一个临时端口,并绑定到sockfd,后续服务端的回显数据会发送到这个临时端口。

4. temp 结构体(struct sockaddr_in temp)

复制代码
struct sockaddr_in temp;
socklen_t tempLen = sizeof(temp);
ssize_t s = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr*)&temp, &tempLen);
  • 含义recvfrom的输出参数,用于存储 "回显数据的发送端地址"(即服务端的地址);
  • 为什么用临时变量 :客户端只关心接收回显数据,不需要使用服务端地址(仅满足recvfrom的参数要求),因此用temp占位即可。

5. recvfrom 函数(客户端)

  • 核心作用:接收服务端的回显数据;
  • 参数 / 返回值 :和服务端recvfrom一致,成功返回接收的字节数,失败返回-1
  • 逻辑:客户端发送数据后,阻塞等待服务端的回显,接收到后打印到控制台。

6. close 函数:释放 Socket 资源

  • 客户端close(sockfd):关闭客户端的网络文件描述符,释放操作系统为该套接字分配的资源(如临时端口、缓冲区等);
  • 服务端close(_sockfd):关闭服务端的监听套接字,释放绑定的端口和文件描述符;
  • 核心:Socket 是操作系统的内核资源,必须显式关闭(或析构函数自动关闭),否则会导致资源泄漏(端口被占用、文件描述符耗尽)。

总结

  1. 套接字是进程间通信的统一接口,分域间、原始、网络三类,通过struct sockaddr统一封装地址结构体;
  2. 服务端核心流程:socket(创建套接字)→ bind(绑定 IP + 端口)→ recvfrom(接收数据)→ sendto(回显数据);
  3. 客户端核心流程:socket(创建套接字)→ sendto(发送数据,自动绑定临时端口)→ recvfrom(接收回显);
  4. 字节序转换(htons/inet_addr)是网络编程的核心细节,确保跨主机数据解析正确;
  5. 服务端绑定0.0.0.0适配云服务器虚拟化场景,客户端无需手动绑定端口,由系统自动分配。

基于 TCP 协议实现服务端与用户端网络通信(守护进程化)

TcpServer.cc(服务端)

复制代码
#include <netinet/in.h>   // 提供sockaddr_in结构体、htonl/htons等字节序转换函数
#include <iostream>       // 标准输入输出(cout/cerr/perror)
#include <cstdlib>        // 提供exit()、atoi()等系统/通用函数
#include <cstring>        // 提供memset/bzero等内存操作函数
#include <sys/types.h>    // 定义socket相关基础类型(如pid_t/socklen_t)
#include <sys/socket.h>   // 提供socket/bind/listen/accept等核心网络调用
#include <string>         // C++字符串类(替代char*,更安全)
#include <arpa/inet.h>    // 提供inet_aton/inet_ntop等IP地址转换函数
#include <memory>         // C++智能指针(unique_ptr,自动管理内存)
#include <unistd.h>       // 提供close/read/write/fork等系统调用
#include <stdexcept>      // 异常处理(stoi转换失败时抛出异常)
#include <sys/wait.h>     // 提供waitpid()函数(等待子进程退出)
#include <pthread.h>      // 提供pthread_create/pthread_detach等线程操作函数
#include <signal.h>       // 信号处理(守护进程忽略信号)
#include <fcntl.h>        // 文件控制(open函数,重定向标准输入输出)
using namespace std;      // 简化std::前缀,提升代码可读性

// 守护进程化函数:将进程转为后台运行,脱离终端控制
// 参数cwd:守护进程的工作目录(默认空,不修改)
void Daemon(const string& cwd = "")
{
    // 1. 忽略无关信号:避免进程被意外终止
    // SIGCLD:子进程退出信号(忽略后系统自动回收子进程资源,避免僵尸进程)
    // SIGPIPE:向已关闭的套接字写数据触发的信号(忽略避免进程崩溃)
    // SIGSTOP:暂停进程信号(忽略避免进程被意外暂停)
    signal(SIGCLD, SIG_IGN);
    signal(SIGPIPE, SIG_IGN);
    signal(SIGSTOP, SIG_IGN);

    // 2. 创建子进程,父进程退出,脱离终端会话
    pid_t id = fork();
    if(id > 0)
    {
        exit(0);  // 父进程直接退出,子进程成为孤儿进程
    }

    // 子进程执行:创建独立会话,脱离原终端控制
    setsid();

    // 3. 更改进程工作目录(可选):避免原目录被删除/挂载影响进程
    if(!cwd.empty())
    {
        chdir(cwd.c_str());
    }

    // 4. 重定向标准输入/输出/错误到/dev/null(垃圾桶)
    // 守护进程无终端,关闭标准IO避免资源泄漏
    int fd = open("/dev/null", O_RDWR); // 打开空设备文件
    if(fd > 0)
    {
        dup2(fd, 0); // 重定向标准输入(0)到/dev/null
        dup2(fd, 1); // 重定向标准输出(1)到/dev/null
        dup2(fd, 2); // 重定向标准错误(2)到/dev/null
        close(fd);   // 关闭临时文件描述符
    }
}

// 枚举错误码:标准化不同阶段的错误类型,便于定位问题
enum ErrorCode
{
    SOCKET_ERR = 2,   // 创建socket套接字失败
    BIND_ERR,         // 绑定IP:端口失败
    LISTEN_ERR,       // 监听套接字失败
    ARG_ERR           // 命令行参数错误
};

// 全局常量定义:默认配置项
const uint16_t default_port = 8080;  // 默认监听端口
const string default_ip = "0.0.0.0"; // 默认监听IP(0.0.0.0表示监听所有网卡)
const int backlog = 10;              // listen的backlog参数:半连接队列大小

class TcpServer; // 前向声明:ThreadData中需要使用TcpServer指针

// 线程参数结构体:用于向线程函数传递客户端连接信息
class ThreadData
{
public:
    // 构造函数:初始化客户端fd、端口、IP,以及TcpServer对象指针
    ThreadData(const int& fd, const uint16_t& port, const string& ip, TcpServer* ts)
        : client_sockfd(fd)
        , client_port(port)
        , client_ip(ip)
        , tsptr(ts)
    {}

public:
    int client_sockfd;    // 客户端连接的套接字描述符
    uint16_t client_port; // 客户端端口号
    string client_ip;     // 客户端IP地址
    TcpServer* tsptr;     // TcpServer对象指针(用于调用Service方法)
};

// TCP服务器核心类:封装socket创建、绑定、监听、通信逻辑
class TcpServer
{
public:
    // 构造函数:初始化监听端口和IP(默认值)
    // 参数port:监听端口,默认8080;ip:监听IP,默认0.0.0.0
    TcpServer(const uint16_t port = default_port, const string ip = default_ip)
        : _listen_sockfd(-1) // 初始化监听套接字为无效值
        , _port(port)
        , _ip(ip)
    {}

    // 初始化函数:创建socket、绑定地址、设置监听
    void Init()
    {
        // 1. 创建TCP套接字(AF_INET=IPv4,SOCK_STREAM=TCP,0=默认协议)
        _listen_sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if(_listen_sockfd < 0) // 创建失败(fd < 0)
        {
            perror("socket create failed"); // 打印系统错误信息
            exit(SOCKET_ERR);               // 退出进程,错误码SOCKET_ERR
        }
        cout << "socket success, fd: " << _listen_sockfd << endl;

        // 2. 初始化服务端地址结构体(sockaddr_in)
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local)); // 清空结构体,避免脏数据
        local.sin_family = AF_INET;                // 协议族:IPv4
        local.sin_port = htons(_port);             // 端口:主机序转网络序(大端)
        inet_aton(_ip.c_str(), &(local.sin_addr)); // IP字符串转网络序结构体

        // 3. 绑定套接字与地址(将fd和IP:端口关联)
        int ret = bind(_listen_sockfd, (struct sockaddr*)&local, sizeof(local));
        if(ret < 0)
        {
            perror("bind failed");
            exit(BIND_ERR);
        }
        cout << "bind success, port: " << _port << endl;

        // 4. 设置套接字为监听状态(被动套接字,等待客户端连接)
        // backlog:半连接队列大小(未完成三次握手的连接数)
        ret = listen(_listen_sockfd, backlog);
        if(ret < 0)
        {
            perror("listen failed");
            exit(LISTEN_ERR);
        }
        cout << "listen success, backlog: " << backlog << endl;
    }

    // 启动服务器:进入主循环,接受客户端连接
    void Start()
    {
        Daemon(); // 进程转为守护进程(后台运行)

        cout << "TCP server start success, listen on " << _ip << ":" << _port << endl;
        while(true) // 无限循环,持续接受客户端连接
        {
            // 1. 接受客户端连接(阻塞调用,直到有新连接)
            struct sockaddr_in client; // 存储客户端地址信息
            socklen_t client_len = sizeof(client);
            int client_sockfd = accept(_listen_sockfd, (struct sockaddr*)&client, &client_len);
            
            if(client_sockfd < 0) // 接受连接失败(如被信号中断)
            {
                perror("accept failed");
                continue; // 跳过本次,继续等待下一个连接
            }

            // 2. 解析客户端IP和端口(网络序转主机序)
            uint16_t client_port = ntohs(client.sin_port); // 端口:网络序转主机序
            char client_ip[32] = {0};
            // IP结构体转字符串(网络序转点分十进制)
            inet_ntop(AF_INET, &client.sin_addr, client_ip, sizeof(client_ip));
            cout << "new client connect: " << client_ip << ":" << client_port << " (fd: " << client_sockfd << ")" << endl;

            // 3. 与客户端通信 --- 单进程版本(注释)
            // 缺点:同一时间只能处理一个客户端,其他客户端需等待
            // Service(client_sockfd, client_port, client_ip);

            // 3. 与客户端通信 --- 多进程版本(注释)
            // 优点:并发处理多个客户端;缺点:进程创建开销大
            // pid_t id = fork();
            // if(id == 0)
            // {
            //     // 子进程:关闭监听套接字(子进程无需监听)
            //     close(_listen_sockfd);
            //     // 创建孙子进程:子进程退出,孙子进程由系统领养,避免僵尸进程
            //     if(fork() > 0)  
            //     {
            //         exit(0); // 子进程退出
            //     }
            //     // 孙子进程处理客户端通信,退出后系统自动回收资源
            //     Service(client_sockfd, client_port, client_ip);
            //     exit(0);
            // }
            // // 主进程:关闭客户端fd(已交给子进程)
            // close(client_sockfd);
            // // 等待子进程退出(非阻塞,因为子进程立即退出)
            // pid_t rid = waitpid(id, nullptr, 0);

            // 3. 与客户端通信 --- 多线程版本(推荐)
            // 优点:线程创建开销小,并发效率高
            ThreadData* td = new ThreadData(client_sockfd, client_port, client_ip, this);
            pthread_t tid;
            // 创建线程:执行Routine函数,传递ThreadData参数
            pthread_create(&tid, nullptr, Routine, td);
        }
    }

    // 线程入口函数(静态函数:pthread_create要求)
    static void* Routine(void* args)
    {
        // 线程分离:线程退出后自动回收资源,无需主线程join
        pthread_detach(pthread_self());

        // 转换参数类型:void* -> ThreadData*
        ThreadData* td = static_cast<ThreadData*>(args);
        // 调用TcpServer的Service方法,处理客户端通信
        td->tsptr->Service(td->client_sockfd, td->client_port, td->client_ip);
        delete td; // 释放参数内存,避免泄漏
        return nullptr;
    }

    // 客户端通信核心函数:处理与单个客户端的读写逻辑
    void Service(const int& client_sockfd, const uint16_t& client_port, const string& client_ip)
    {
        char buffer[3096] = {0}; // 数据缓冲区(初始化避免脏数据)
        while(true)
        {
            // 读取客户端数据:留1位给'\0',避免字符串越界
            ssize_t n = read(client_sockfd, buffer, sizeof(buffer) - 1);
            
            if(n > 0) // 读取到有效数据
            {
                buffer[n] = '\0'; // 手动添加字符串结束符
                cout << "client [" << client_ip << ":" << client_port << "] say: " << buffer << endl;

                // 构造回显数据并发送给客户端
                string echo_msg = "TcpServer echo# " + string(buffer);
                write(client_sockfd, echo_msg.c_str(), echo_msg.size());
            }
            else if(n == 0) // 客户端正常关闭连接(read返回0)
            {
                cout << "client [" << client_ip << ":" << client_port << "] disconnect" << endl;
                break; // 退出循环,关闭fd
            }
            else // 读取错误(n == -1,如客户端异常断开)
            {
                perror("read failed");
                break; // 退出循环,关闭fd
            }
        }
        // 关闭客户端套接字,释放文件描述符
        close(client_sockfd);
        cout << "client fd " << client_sockfd << " closed" << endl;
    }

    // 析构函数:释放监听套接字资源
    ~TcpServer()
    {
        if(_listen_sockfd >= 0) // 检查fd有效性
        {
            close(_listen_sockfd);
            cout << "listen fd " << _listen_sockfd << " closed" << endl;
        }
    }

private:
    int _listen_sockfd;     // 监听套接字描述符(被动套接字)
    uint16_t _port;         // 监听端口
    string _ip;             // 监听IP
};

// 打印程序使用方法:提示用户正确的命令行参数格式
void Usage(const string& proc)
{
    cout << "\nUsage: " << proc << " port[1024+]\n" << endl;
}

// 主函数:程序入口,解析参数并启动服务器
int main(int argc, char* argv[])
{
    // 检查命令行参数数量:必须传入1个参数(端口)
    if(argc != 2)
    {
        Usage(argv[0]); // 打印使用方法
        exit(ARG_ERR);  // 退出,错误码ARG_ERR
    }

    // 解析端口参数:字符串转数字(处理非数字输入)
    uint16_t port;
    try
    {
        port = stoi(argv[1]); // C++11字符串转整数,失败抛出异常
    }
    catch(const exception& e)
    {
        cerr << "port must be a number!" << endl;
        Usage(argv[0]);
        exit(ARG_ERR);
    }

    // 检查端口合法性:1024以下为系统端口,普通用户无权限绑定
    if(port < 1024)
    {
        cerr << "port must be greater than 1024!" << endl;
        Usage(argv[0]);
        exit(ARG_ERR);
    }

    // 创建TcpServer对象(智能指针自动管理内存)
    unique_ptr<TcpServer> tcp_server(new TcpServer(port));

    // 初始化服务器(创建socket、绑定、监听)
    tcp_server->Init();
    // 启动服务器(进入主循环,接受客户端连接)
    tcp_server->Start();

    return 0;
}

TcpClient.cc(客户端)

复制代码
#include <iostream>       // 标准输入输出(cout/cerr)
#include <cstdlib>        // 提供exit()等系统函数
#include <cstring>        // 提供memset等内存操作函数
#include <sys/types.h>    // 定义socket相关基础类型
#include <sys/socket.h>   // 提供socket/connect等核心网络调用
#include <string>         // C++字符串类
#include <strings.h>      // 提供bzero等字符串操作函数(兼容旧系统)
#include <netinet/in.h>   // 提供sockaddr_in结构体、字节序转换函数
#include <arpa/inet.h>    // 提供inet_aton等IP地址转换函数
#include <memory>         // 智能指针头文件(本例未使用,保留扩展)
#include <unistd.h>       // 提供close/read/write等系统调用
using namespace std;      // 简化std::前缀

// 打印程序使用方法:提示用户正确的命令行参数格式
void Usage(string proc)
{
    // 格式:./TcpClient 服务端IP 服务端端口(如 ./TcpClient 127.0.0.1 8080)
    cout << "\n\rUsage: " << proc << " serverIP serverPort\n" << endl;
}

// 主函数:程序入口,创建客户端并与服务端通信
int main(int argc, char* argv[])
{
    // 检查命令行参数数量:必须传入2个参数(服务端IP+端口)
    if(argc != 3)
    {
        Usage(argv[0]); // 打印使用方法
        exit(0);        // 正常退出
    }

    // 1. 解析命令行参数:服务端IP和端口
    string serverIp = argv[1];                // 服务端IP字符串
    uint16_t serverPort = stoi(argv[2]);      // 服务端端口(字符串转数字)

    // 2. 创建TCP套接字(AF_INET=IPv4,SOCK_STREAM=TCP,0=默认协议)
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if(sockfd < 0) // 创建失败(fd < 0)
    {
        cout << "socket error" << endl;
        exit(-1);   // 异常退出
    }

    // 3. 初始化服务端地址结构体,建立连接
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server)); // 清空结构体,避免脏数据
    server.sin_family = AF_INET;                // 协议族:IPv4
    server.sin_port = htons(serverPort);        // 端口:主机序转网络序(大端)
    inet_aton(serverIp.c_str(), &(server.sin_addr)); // IP字符串转网络序结构体

    // 连接服务端(阻塞调用,直到连接成功/失败)
    int n = connect(sockfd, (struct sockaddr*)&server, sizeof(server));
    if(n < 0) // 连接失败
    {
        cout << "connect error" << endl;
        exit(-1);
    }

    // 4. 与服务端通信:循环读取用户输入并发送,接收服务端回显
    string message; // 存储用户输入的消息
    while(true)
    {
        cout << "client say# ";          // 提示用户输入
        getline(cin, message);           // 读取用户输入(支持空格)
        
        // 发送消息到服务端:字符串转char*,传递长度
        write(sockfd, message.c_str(), message.size());
        
        // 接收服务端回显数据
        char inbuffer[3096]; // 接收缓冲区
        int n = read(sockfd, inbuffer, sizeof(inbuffer));
        if(n > 0) // 读取到有效数据
        {
            inbuffer[n] = 0; // 手动添加字符串结束符
            cout << inbuffer << endl; // 打印服务端回显
        }
    }

    // 注:本例未处理退出逻辑,实际开发中需捕获信号(如Ctrl+C)关闭fd
    // close(sockfd);
    return 0;
}

一、TCP 服务端 / 客户端代码整体逻辑

1. TcpServer.cc 核心逻辑(服务端)

TCP 服务端的核心是被动监听客户端连接 ,并并发处理多个客户端的读写请求,整体流程如下:

  • 核心设计 :用类封装所有逻辑,通过Init完成初始化、Start进入主循环、Service处理单个客户端通信,支持单进程 / 多进程 / 多线程三种通信模式(推荐多线程)。
  • 关键特性:守护进程化(后台运行)、线程分离(自动回收资源)、错误容错(accept 失败不退出)。

2. TcpClient.cc 核心逻辑(客户端)

TCP 客户端的核心是主动发起连接 ,并与服务端交互(发消息 + 收回显),整体流程如下:

  • 核心设计:无类封装(简单场景),直接通过系统调用完成连接和通信,依赖 TCP 的面向连接特性,无需每次发送指定地址(区别于 UDP)。

二、核心函数解析

1. 创建 TCP 套接字:socket 函数

复制代码
_listen_sockfd = socket(AF_INET, SOCK_STREAM, 0); // 服务端
int sockfd = socket(AF_INET, SOCK_STREAM, 0);     // 客户端
参数 含义
AF_INET 指定使用IPv4 协议族(网络层通过 IPv4 地址定位主机)
SOCK_STREAM 指定套接字类型为流式套接字(对应 TCP 协议:面向连接、可靠、字节流)
0 协议编号,0表示 "根据前两个参数自动选择默认协议"(此处自动匹配 TCP)
  • 返回值 :成功返回非负的网络文件描述符(fd) ,失败返回-1

2. IP 地址转换函数(解决 inet_addr 的静态区覆盖问题)

TCP/IP 协议要求 IP 地址以网络字节序(大端) 传输,需将 "点分十进制字符串 IP" 与 "网络序整数 IP" 互转,核心函数对比:

函数 功能 参数 / 返回值 缺点 / 注意事项
inet_addr 字符串 IP → 网络序整数(in_addr_t) 参数:const char* ip(点分十进制 IP);返回:成功返回网络序整数,失败返回INADDR_NONE 内部用静态存储区,多次调用会覆盖之前的结果
inet_aton 字符串 IP → in_addr 结构体 参数:const char* ipstruct in_addr* addr(输出型参数);返回:成功1,失败0 无静态区问题,推荐使用
inet_pton 协议无关转换(IPv4/IPv6) 参数:int af(AF_INET/AF_INET6)、const char* srcvoid* dst(输出);返回:成功1,格式错误0,失败-1 支持 IPv6,最通用、最安全
inet_ntoa in_addr 结构体 → 字符串 IP 参数:struct in_addr in;返回:指向静态区的字符串指针 静态区覆盖问题,不推荐
inet_ntop 协议无关转换(IPv4/IPv6) 参数:int afconst void* src(网络序 IP)、char* dst(输出缓冲区)、socklen_t size;返回:成功返回dst指针,失败NULL 无静态区问题,推荐使用

关键补充inet_addr的坑

复制代码
// 错误示例:两次调用inet_addr,静态区被覆盖,ip1和ip2指向同一地址
in_addr_t ip1 = inet_addr("192.168.1.1");
in_addr_t ip2 = inet_addr("10.0.0.1"); 
// ip1和ip2的值都是10.0.0.1的网络序,因为静态区被第二次调用覆盖

inet_aton通过输出型参数解决该问题:

复制代码
struct in_addr addr1, addr2;
inet_aton("192.168.1.1", &addr1); // 写入addr1,无覆盖
inet_aton("10.0.0.1", &addr2);   // 写入addr2,独立存储

3. listen 函数:主动套接字→被动套接字

复制代码
ret = listen(_listen_sockfd, backlog);
  • 核心作用 :将socket创建的 "主动套接字" 转为被动套接字,告诉操作系统:该 fd 用于监听客户端的 TCP 连接请求(TCP 必须调用 listen 才能调用 accept)。
  • backlog 参数 :指定半连接队列(SYN 队列) 的最大长度(未完成三次握手的连接数上限)。
    • 补充:Linux 下backlog实际是 "半连接队列 + 已完成队列" 的总和上限,用于限制待处理的连接请求数,避免服务器过载。
  • 返回值 :成功0,失败-1

4. accept 函数:获取客户端连接

复制代码
int client_sockfd = accept(_listen_sockfd, (struct sockaddr*)&client, &client_len);
  • 核心含义 :从 "已完成三次握手的连接队列" 中取出一个连接,返回新的客户端 fd(client_sockfd)
    • _listen_sockfd:仅负责 "监听和获取连接",不做任何 IO 操作;
    • client_sockfd:专门用于和该客户端进行读写通信(核心 IO fd)。
  • 为什么失败不退出进程 :accept 失败通常是临时错误 (如被信号中断、连接队列空),不是致命错误;若退出进程,整个服务会不可用,因此用continue重新尝试即可。
  • 返回值 :成功返回客户端 fd(非负),失败返回-1

5. 字节序 / 端口转换函数

网络传输统一使用大端序(网络字节序),而主机(如 x86)多为小端序,需通过以下函数转换:

函数 功能 参数 / 返回值 适用场景
htons 主机序→网络序(16 位) 参数:uint16_t hostshort(主机序端口);返回:uint16_t(网络序端口) 端口转换(16 位)
ntohs 网络序→主机序(16 位) 参数:uint16_t netshort(网络序端口);返回:uint16_t(主机序端口) 解析客户端端口
htonl 主机序→网络序(32 位) 参数:uint32_t hostlong(主机序 IP);返回:uint32_t(网络序 IP) IP 地址转换
ntohl 网络序→主机序(32 位) 参数:uint32_t netlong(网络序 IP);返回:uint32_t(主机序 IP) 解析 IP 地址
stoi 字符串→整数 参数:const string& str(待转换字符串);返回:int(转换后整数);失败抛异常 解析命令行端口

补充跨平台函数(替代 htonl/ntohl,更通用):

  • htobe16/htobe32:主机序→大端序(16/32 位);
  • betoh16/betoh32:大端序→主机序(16/32 位)。

6. 与客户端通信的三个版本

版本 实现方式 优点 缺点
单进程版 主线程直接调用 Service 实现简单,无额外开销 同一时间仅处理一个客户端,并发差
多进程版 fork 子进程处理每个客户端 并发处理多个客户端 进程创建 / 销毁开销大,需处理僵尸进程
多线程版 创建线程处理每个客户端 线程开销远小于进程,并发效率高 需注意线程安全(本例无共享资源,无需加锁)

Service 函数核心逻辑 :TCP 是面向字节流 的,因此read/write操作和文件操作完全一致(Linux "一切皆文件"):

  • read(client_sockfd, buffer, ...):从客户端 fd 读取数据;
  • write(client_sockfd, echo_msg, ...):向客户端 fd 发送回显数据;
  • 循环终止条件:
    • read返回0:客户端正常关闭连接;
    • read返回-1:客户端异常断开(如网络中断)。

7. 客户端 connect 函数:发起 TCP 连接

复制代码
int n = connect(sockfd, (struct sockaddr*)&server, sizeof(server));
  • 核心作用 :主动向服务端发起 TCP 连接,触发三次握手
  • 参数
    • sockfd:客户端 socket fd;
    • server:服务端地址结构体(IP + 端口);
    • sizeof(server):结构体大小。
  • 返回值 :成功0(三次握手完成),失败-1(如服务端不可达、超时)。
  • 特性:默认阻塞调用,直到三次握手完成或超时。

8. 守护进程化:Daemon 函数

核心目的:让服务端脱离终端、后台常驻运行(24 小时不中断),不受终端关闭 / 登录退出影响。

(1) 核心步骤解析

步骤 代码 / 操作 目的
忽略异常信号 signal(SIGCLD/SIGPIPE/SIGSTOP, SIG_IGN) ① SIGCLD:子进程退出自动回收,避免僵尸进程;② SIGPIPE:向关闭的 socket 写数据不崩溃;③ SIGSTOP:避免进程被意外暂停
fork 子进程 + 父进程退出 pid_t id = fork(); if(id>0) exit(0); 子进程脱离原终端会话,成为 "孤儿进程"(由 init 进程领养)
创建独立会话 setsid(); 子进程成为新会话的组长,彻底脱离原终端控制(终端关闭不影响进程)
改工作目录(可选) chdir(cwd.c_str()); 避免原工作目录被删除 / 挂载,导致进程异常
重定向标准 IO dup2(fd, 0/1/2); 守护进程无终端,关闭 stdin/stdout/stderr,避免资源泄漏

(2) 关键问题:为什么父进程不能直接调用 setsid?

  • 会话(Session) :是一个或多个进程组的集合,有且仅有一个控制终端,会话组长进程无法创建新会话(系统规定);
  • 父进程是原会话的组长,因此必须先 fork 子进程(子进程不是组长),再让子进程调用setsid创建新会话,才能脱离终端。

(3) 会话的核心作用

终端关闭时,会向所属会话的所有进程发送SIGHUP信号(默认终止进程);守护进程脱离会话后,不会收到该信号,因此能持续运行。

总结

  1. TCP 服务端核心流程:socket(创建 fd)→bind(绑定 IP + 端口)→listen(转为被动套接字)→accept(获取连接)→多线程/进程处理通信read/write(字节流读写);
  2. TCP 客户端核心流程:socketconnect(三次握手)→循环读写
  3. IP / 端口转换需注意静态区覆盖问题 ,优先使用inet_aton/inet_pton/inet_ntop
  4. 守护进程通过fork+setsid脱离终端,实现后台常驻;
  5. TCP 是面向字节流的可靠协议,read/write与文件操作接口统一,区别于 UDP 的无连接、面向数据报特性。

json(序列/反序列化)

test.cc(Json 的基本使用)

复制代码
// 标准输入输出头文件,用于控制台打印
#include <iostream>
// JsonCpp 库核心头文件,包含所有JSON操作的类和函数(Value/Writer/Reader等)
#include <jsoncpp/json/json.h>

using namespace std;

int main()
{
    // ==================== 1. JSON 序列化 ====================
    // 定义:将 C++ 程序中的数据结构 → 转换为 JSON 格式的字符串
    // 作用:用于网络传输、文件存储等场景

    // Json::Value 是 JsonCpp 最核心的类
    // 作用:表示一个 JSON 对象/数组/数值/字符串,是所有JSON数据的载体
    // 可以理解为万能的JSON容器,支持键值对、数组、各种数据类型
    Json::Value root;

    // 向 JSON 对象中添加 键值对(key-value)
    // root["键名"] = 值 :自动根据值的类型(int/string/char等)存储到JSON对象中
    root["x"] = 100;        // 键:x,值:整型 100
    root["y"] = 200;        // 键:y,值:整型 200
    root["op"] = '+';      // 键:op,值:字符'+'(JSON无char类型,会存储为ASCII码)
    root["desc"] = "this is a + oper"; // 键:desc,值:字符串

    // Json::FastWriter 类:快速序列化器
    // 作用:将 Json::Value 对象 转换为 **紧凑无格式** 的JSON字符串(无换行、无缩进)
    // 特点:速度快,体积小,适合网络传输
    Json::FastWriter w;

    // FastWriter::write() 核心成员函数
    // 参数:Json::Value 对象
    // 返回值:std::string 类型的 JSON 字符串
    string res = w.write(root);

    // 打印序列化后的JSON字符串
    cout << "序列化后的JSON字符串:" << endl;
    cout << res << endl << endl;


    // ==================== 2. JSON 反序列化 ====================
    // 定义:将 JSON 格式的字符串 → 还原为 C++ 可操作的 Json::Value 对象
    // 作用:解析网络接收/文件读取的JSON数据

    // 定义空的Json::Value对象,用于存储反序列化后的结果
    Json::Value v;

    // Json::Reader 类:JSON 解析器
    // 作用:将 JSON 字符串 解析为 Json::Value 对象
    Json::Reader r;

    // Reader::parse() 核心成员函数
    // 参数1:待解析的JSON字符串(std::string)
    // 参数2:存储解析结果的Json::Value对象
    // 返回值:bool类型,解析成功返回true,失败返回false
    r.parse(res, v);

    // 从Json::Value对象中提取数据(必须指定数据类型)
    // Json::Value::asInt()    :提取 整型数据
    // Json::Value::asString() :提取 字符串数据
    int x = v["x"].asInt();
    int y = v["y"].asInt();
    // 字符'+'在JSON中存储为ASCII码,因此用asInt()获取后,强转为char
    char op = (char)v["op"].asInt();
    string desc = v["desc"].asString();

    // 打印反序列化后的数据
    cout << "反序列化结果:" << endl;
    cout << x << endl;
    cout << y << endl;
    cout << op << endl;
    cout << desc << endl;

    return 0;
}

JSON 代码全解析与网络传输核心作用

JSON ,是轻量级数据交换格式,也是网络传输中最常用的数据格式。该代码基于JsonCpp 库 实现序列化反序列化两大核心功能。

头文件解析

复制代码
#include <iostream>        // 控制台打印,用于调试输出
#include <jsoncpp/json/json.h>  // JsonCpp库核心头文件,封装所有JSON操作相关的类与函数

jsoncpp/json/json.h :Linux 环境下 JsonCpp 库的标准引入路径,包含ValueWriterReader等核心组件。

JsonCpp 核心类与函数解析

核心类:Json::Value

复制代码
Json::Value root;

Json::Value 是 JsonCpp 库的核心数据载体类,可存储整型、字符串、键值对、数组等所有 JSON 支持的数据类型,是 JSON 数据操作的基础容器。

键值对赋值用法

复制代码
root["x"] = 100;        // 存储整型数据,键为x
root["y"] = 200;        // 存储整型数据,键为y
root["op"] = '+';       // 存储字符数据,JSON无char类型,自动转换为ASCII码
root["desc"] = "this is a + oper"; // 存储字符串数据,键为desc

语法规则:JSON对象["键名"] = 数据值,库自动识别数据类型并完成存储。

序列化类:Json::FastWriter

复制代码
Json::FastWriter w;
string res = w.write(root);

Json::FastWriter :快速序列化工具类,用于将Json::Value 对象 转换为紧凑无格式的 JSON 字符串 ,具备体积小、传输效率高的特点,适配网络传输场景。

核心函数:write ()

复制代码
string write(const Value &root);
  • 参数:待序列化的Json::Value对象
  • 返回值:std::string 类型的JSON 字符串,可直接用于网络发送

反序列化类:Json::Reader

复制代码
Json::Reader r;
r.parse(res, v);

Json::Reader :JSON 解析工具类,用于将JSON 字符串 转换为可操作的Json::Value 对象

核心函数:parse ()

复制代码
bool parse(const string &document, Value &root);
  • 参数 1:待解析的JSON 字符串
  • 参数 2:存储解析结果的空Json::Value对象
  • 返回值:bool 类型,true表示解析成功,false表示解析失败

数据提取函数

复制代码
int x = v["x"].asInt();          // 提取整型数据
string desc = v["desc"].asString();// 提取字符串数据
char op = (char)v["op"].asInt();  // 提取ASCII码并强转为字符
  • asInt() :从 JSON 对象中提取整型数据
  • asString() :从 JSON 对象中提取字符串数据
  • 数据提取必须与存储类型严格匹配,否则会触发解析错误

序列化与反序列化的定义

序列化 :将 C++ 内存中的数据结构,转换为可传输、可存储 的 JSON 字符串。反序列化:将网络或文件中获取的 JSON 字符串,还原为 C++ 可直接操作的内存数据。

序列化与反序列化在网络传输中的作用

网络编程中,send/write等系统调用仅支持传输字节流 / 字符串 ,无法直接传输内存变量,序列化与反序列化是解决该问题的核心方案。不同主机的内存布局、字节序、数据类型长度存在差异,统一格式的 JSON 字符串可实现跨主机、跨语言的数据交互。

序列化与反序列化的执行时机

序列化 :执行位置为数据发送端,执行时机为网络传输数据之前 ,核心目的是将内存数据转换为可传输的字符串。反序列化 :执行位置为数据接收端,执行时机为网络数据接收完成之后,核心目的是将字符串还原为可使用的内存数据。

结合 TCP/UDP 编程的实际应用流程

客户端(发送端)

  1. 定义 C++ 业务变量;
  2. 执行序列化操作,生成 JSON 字符串;
  3. 调用write/sendto函数,将 JSON 字符串发送至服务端。

服务端(接收端)

  1. 调用read/recvfrom函数,接收 JSON 字符串;
  2. 执行反序列化操作,还原业务变量;
  3. 基于还原的变量执行业务逻辑处理。

代码执行结果

复制代码
序列化后的JSON字符串:
{"desc":"this is a + oper","op":43,"x":100,"y":200}

反序列化结果:
100
200
+
this is a + oper

序列化生成纯字符串,满足网络传输要求;反序列化还原 C++ 变量,满足业务使用要求。

核心知识点总结

  1. Json::Value:JSON 数据的通用存储容器;
  2. Json::FastWriter + write() :实现序列化,完成 C++ 数据到 JSON 字符串的转换;
  3. Json::Reader + parse() :实现反序列化,完成 JSON 字符串到 C++ 数据的转换;
  4. 序列化与反序列化是网络编程的标配操作,解决内存数据无法直接跨主机传输的问题;
  5. 执行规则:发送端传输前序列化 ,接收端接收后反序列化

HTTP 协议

Sock.hpp(封装套接字)

复制代码
// 防止头文件重复包含
#pragma once
#include <netinet/in.h>   // 网络地址结构体、字节序转换函数
#include <iostream>       // 控制台输入输出
#include <cstdlib>        // exit()等系统函数
#include <cstring>        // memset/bzero等内存操作函数
#include <sys/types.h>    // socket相关类型
#include <sys/socket.h>   // socket核心系统调用
#include <string>         // C++ string类
#include <arpa/inet.h>    // IP地址转换函数
#include <memory>         // 智能指针
#include <unistd.h>       // close等系统调用
#include <stdexcept>      // 异常处理(stoi转换)
#include <sys/wait.h>
#include <pthread.h>
#include <signal.h>
#include <fcntl.h>
using namespace std;

// listen函数的参数:全连接队列的最大长度
const int backlog = 10;

// 错误码枚举:标识套接字操作失败的类型
enum
{
    SOCK_ERR = 2,    // 创建socket失败
    BIND_ERR,        // 绑定端口失败
    LISTEN_ERR,      // 监听失败
    ACCEPT_ERR,      // 接受连接失败
};

// Sock类:封装TCP套接字的所有核心操作(创建/绑定/监听/接受/连接/关闭)
// 解耦网络操作,简化服务器/客户端代码
class Sock
{
public:
    // 默认构造函数
    Sock()
    {}

    // 析构函数
    ~Sock()
    {}

public:
    // 1. 创建TCP套接字
    void Socket()
    {
        // AF_INET:IPv4协议
        // SOCK_STREAM:TCP流式套接字
        // 0:默认协议
        _sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if(_sockfd < 0)
        {
            perror("socket");  // 打印系统错误信息
            exit(SOCK_ERR);    // 退出进程,返回错误码
        }
    }

    // 2. 绑定端口(服务端专用)
    void Bind(uint16_t port)
    {
        struct sockaddr_in local;  // 服务端地址结构体
        memset(&local, 0, sizeof(local));  // 清空结构体
        local.sin_family = AF_INET;       // IPv4协议
        local.sin_port = htons(port);      // 端口:主机序转网络序
        local.sin_addr.s_addr = INADDR_ANY;// 监听本机所有网卡

        // 绑定套接字和IP端口
        int ret = bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
        if(ret < 0)
        {
            perror("bind");
            exit(BIND_ERR);
        }
    }

    // 3. 将套接字设置为监听状态(服务端专用)
    void Listen()
    {
        // backlog:全连接队列长度
        int ret = listen(_sockfd, backlog);
        if(ret < 0)
        {
            perror("listen");
            exit(LISTEN_ERR);
        }
    }

    // 4. 接受客户端连接(服务端专用)
    // 输出参数:clientip-客户端IP,clientport-客户端端口
    // 返回值:新的套接字描述符,用于和客户端通信
    int Accept(string& clientip, uint16_t& clientport)
    {
        struct sockaddr_in peer;  // 存储客户端地址
        socklen_t len = sizeof(peer);
        // 阻塞等待客户端连接
        int newSockFd = accept(_sockfd, (struct sockaddr*)&peer, &len);
        if(newSockFd < 0)
        {
            perror("accept");
            exit(ACCEPT_ERR);
        }
        
        // 网络序IP转字符串格式
        char ipstr[64];
        inet_ntop(AF_INET, &(peer.sin_addr), ipstr, sizeof(ipstr));
        clientip = ipstr;

        // 网络序端口转主机序
        clientport = ntohs(peer.sin_port);

        return newSockFd;
    }

    // 5. 客户端连接服务端
    bool Connect(const string& ip, const uint16_t& port)
    {
        struct sockaddr_in peer;
        memset(&peer, 0, sizeof(peer));
        peer.sin_family = AF_INET;
        peer.sin_port = htons(port);
        // 字符串IP转网络序
        inet_pton(AF_INET, ip.c_str(), &(peer.sin_addr));

        // 发起连接
        int n = connect(_sockfd, (const sockaddr*)&peer, sizeof(peer));
        if(n == -1)
        {
            perror("connect");
            return false;
        }

        return true;
    }

    // 获取当前套接字文件描述符
    int GetFd()
    {
        return _sockfd;
    }

    // 关闭套接字,释放文件描述符
    void Close()
    {
        close(_sockfd);
    }

private:
    int _sockfd;  // 套接字文件描述符
};

HTTPServer.hpp(封装HTTP服务端)

复制代码
// 防止头文件重复包含
#pragma once
#include "Sock.hpp"
#include <pthread.h>

// 默认监听端口
static const uint16_t defaultport = 8080;

// 线程参数结构体:传递给新线程的参数(客户端套接字)
struct PthreadData
{
public:
    PthreadData(int fd)
        : _fd(fd)  // 初始化客户端通信套接字
    {}

public:
    int _fd;  // 用于和客户端通信的文件描述符
};

// HTTP服务器类:基于TCP实现最简单的HTTP服务器
class HTTPServer
{
public:
    // 构造函数:指定监听端口
    HTTPServer(const uint16_t port = defaultport)
        : _port(port)
    {}

    ~HTTPServer()
    {}
public:
    // 启动服务器
    bool start()
    {
        // 1. 创建、绑定、监听套接字(调用封装好的Sock类)
        _listensock.Socket();
        _listensock.Bind(_port);
        _listensock.Listen();

        // 2. 死循环:持续接受客户端连接
        while (true)
        {
            string clientip;
            uint16_t clientport;
            // 阻塞等待客户端连接
            int sockfd = _listensock.Accept(clientip, clientport);
            if(sockfd < 0)
                continue;

            // 3. 创建线程参数,传递客户端套接字
            PthreadData* pd = new PthreadData(sockfd);

            // 4. 创建新线程,处理客户端请求(主线程继续接受连接)
            pthread_t tid;
            pthread_create(&tid, nullptr, ThreadRun, pd);
        }
    }

    // 线程入口函数(静态函数:pthread_create要求)
    static void* ThreadRun(void* args)
    {
        // 线程分离:线程退出后自动释放资源,无需主线程等待
        pthread_detach(pthread_self());

        // 转换参数类型
        PthreadData* pd = static_cast<PthreadData*>(args);

        // 读取客户端发送的HTTP请求报文
        char buffer[10240];
        ssize_t n = read(pd->_fd, buffer, sizeof(buffer) - 1);
        if(n > 0)
        {
            buffer[n] = 0;
            // 打印客户端的HTTP请求
            cout << buffer;

            // ==================== 构造HTTP响应报文 ====================
            // 响应体:返回给浏览器的网页内容
            string text = "<html><body><h3>hello world</h3></body></html>";
            // 响应行:HTTP版本 状态码 状态描述
            string response_line = "HTTP/1.0 200 OK\r\n";
            // 响应头:声明响应体长度
            string response_header = "Content-Length: ";
            response_header += to_string(text.size());
            response_header += "\r\n";
            
            // 空行:分隔响应头和响应体(HTTP协议规定必须有)
            string blank_line = "\r\n";

            // 拼接完整的HTTP响应
            string response = response_line + response_header + blank_line + text;

            // 将响应发送给浏览器
            write(pd->_fd, response.c_str(), response.size());
        }

        // 释放资源
        delete pd;
        close(pd->_fd);  // 关闭客户端套接字
        return nullptr;
    }

private:
    uint16_t _port;    // 服务器监听端口
    Sock _listensock;  // 监听套接字(封装好的套接字对象)
};

HTTPServer.cc(服务端主函数)

复制代码
#include "HTTPServer.hpp"

// 打印程序使用方法
void Usage(const string& proc)
{
    cout << "\nUsage: " << proc << " port[1024+]\n" << endl;
}

// 主函数:程序入口
int main(int argc, char* argv[])
{
    // 校验命令行参数:必须传入端口号
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }

    // 解析命令行参数:端口号
    uint16_t port = stoi(argv[1]);

    // 创建HTTP服务器对象(智能指针自动管理内存)
    unique_ptr<HTTPServer> svr(new HTTPServer(port));

    // 启动服务器
    svr->start();

    return 0;
}

网址与 URL 的核心本质

域名的本质 日常使用的网址属于域名 ,域名仅为便于记忆的字符标识,网络通信底层依赖IP 地址 定位主机,域名需通过DNS 域名解析 转换为对应 IP 地址。默认端口规则 HTTP 协议默认使用80 端口 ,HTTPS 协议默认使用443 端口,该规则为通用标准,无需手动指定端口即可访问对应服务。

URL 的定义 URL 即统一资源定位符,是标识网络中任意资源的唯一字符串,通过 URL 可精准定位并获取网络资源,是网络资源访问的核心标识。

网络基础行为分类

网络数据交互分为两类核心行为:

  1. 下载行为:获取远端主机的资源至本地主机;
  2. 上传行为:将本地主机的资源传输至远端主机。

HTTP 请求报文格式

HTTP 请求由请求行、请求报头、空行、请求正文 四部分组成,是客户端向服务端发送的交互数据。请求行 包含请求方法URLHTTP 版本 ,以\r\n结尾,是请求的核心标识。

复制代码
GET /index.html HTTP/1.1\r\n

请求报头 采用键:值的结构化格式,每行以\r\n结尾,用于传递附加请求信息。关键字段:Content-Length ,标识请求正文 的字节长度。空行 固定为\r\n,无其他内容,用于分隔请求报头请求正文请求正文存储客户端上传的数据,非必选字段,无上传需求时可省略。

HTTP 响应报文格式

HTTP 响应由状态行、响应报头、空行、响应正文 四部分组成,是服务端向客户端返回的交互数据。状态行 包含HTTP 版本状态码状态码描述 ,以\r\n结尾。常见状态码:200 (请求成功)、404 (资源未找到)、500(服务端错误)。

复制代码
HTTP/1.1 200 OK\r\n

响应报头 采用键:值的结构化格式,每行以\r\n结尾,用于传递附加响应信息。关键字段:Content-Length ,标识响应正文 的字节长度。空行 固定为\r\n,用于分隔响应报头响应正文响应正文 服务端返回的核心资源,包含HTML、CSS、JS、图片、视频等内容,由浏览器解析渲染。

代码核心逻辑解析

Sock.hpp 套接字封装类

该类对 TCP 套接字的系统调用进行封装,解耦网络底层操作,提供统一的套接字接口。核心成员函数

复制代码
// 创建TCP流式套接字
void Socket()
{
    _sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if(_sockfd < 0) { perror("socket"); exit(SOCK_ERR); }
}
// 绑定IP与端口(监听所有网卡)
void Bind(uint16_t port)
{
    struct sockaddr_in local;
    memset(&local, 0, sizeof(local));
    local.sin_family = AF_INET;
    local.sin_port = htons(port);
    local.sin_addr.s_addr = INADDR_ANY;
    int ret = bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
    if(ret < 0) { perror("bind"); exit(BIND_ERR); }
}
// 设置监听状态
void Listen()
{
    int ret = listen(_sockfd, backlog);
    if(ret < 0) { perror("listen"); exit(LISTEN_ERR); }
}
// 接受客户端连接,返回通信套接字
int Accept(string& clientip, uint16_t& clientport)
{
    struct sockaddr_in peer;
    socklen_t len = sizeof(peer);
    int newSockFd = accept(_sockfd, (struct sockaddr*)&peer, &len);
    // 解析客户端IP与端口
    inet_ntop(AF_INET, &(peer.sin_addr), ipstr, sizeof(ipstr));
    clientport = ntohs(peer.sin_port);
    return newSockFd;
}
// 客户端发起连接
bool Connect(const string& ip, const uint16_t& port)
{
    struct sockaddr_in peer;
    inet_pton(AF_INET, ip.c_str(), &(peer.sin_addr));
    int n = connect(_sockfd, (const sockaddr*)&peer, sizeof(peer));
}

核心作用 :封装socket、bind、listen、accept、connect等系统调用,简化 HTTP 服务端的网络操作。

HTTPServer.hpp HTTP 服务端封装

基于封装的 Sock 类实现多线程 HTTP 服务,处理客户端请求并返回响应。启动逻辑

复制代码
bool start()
{
    _listensock.Socket();    // 创建套接字
    _listensock.Bind(_port); // 绑定端口
    _listensock.Listen();    // 启动监听
    while (true)
    {
        // 接受客户端连接
        int sockfd = _listensock.Accept(clientip, clientport);
        // 创建线程处理请求
        PthreadData* pd = new PthreadData(sockfd);
        pthread_create(&tid, nullptr, ThreadRun, pd);
    }
}

请求处理逻辑(线程入口)

复制代码
static void* ThreadRun(void* args)
{
    pthread_detach(pthread_self()); // 线程分离
    // 读取HTTP请求报文
    ssize_t n = read(pd->_fd, buffer, sizeof(buffer) - 1);
    // 构造HTTP响应报文
    string response_line = "HTTP/1.0 200 OK\r\n";
    string response_header = "Content-Length: " + to_string(text.size()) + "\r\n";
    string blank_line = "\r\n";
    string text = "<html><body><h3>hello world</h3></body></html>";
    string response = response_line + response_header + blank_line + text;
    // 发送响应至客户端
    write(pd->_fd, response.c_str(), response.size());
    // 释放资源
    delete pd;
    close(pd->_fd);
}

主函数逻辑解析命令行端口参数,创建 HTTPServer 对象并启动服务,完成服务端初始化。

Web 根目录与 URL 路径映射

Web 根目录 指服务端存储网页资源的根文件夹,是 URL 路径映射的本地对应目录。路径映射规则 URL 中的路径与本地 Web 根目录的路径一一对应:URL 为/a/b/hello.html时,服务端会定位至本地a/b/hello.html文件,读取文件内容并作为响应正文返回,浏览器接收后完成解析渲染。

GET 与 POST 请求方法的核心区别

GET 方法 请求参数直接拼接在URL 中,参数内容会明文显示在网址栏,无独立请求正文,仅用于获取资源。POST 方法 请求参数存储在请求正文中,不会显示在网址栏,安全性更高,适用于上传数据、提交表单等场景。

Cookie 的含义 Cookie 是服务端写入客户端本地的小型文本数据,以键:值格式存储,随每次 HTTP 请求自动发送至服务端,用于标识客户端状态。Cookie 的安全风险 Cookie 存储在本地,存在被窃取、篡改的风险,直接存储用户私密信息会导致隐私泄露。Session+Cookie 的安全方案

  1. 服务端为每个客户端创建独立Session,存储用户私密信息,生成唯一 SessionID;
  2. 服务端仅将SessionID写入客户端 Cookie;
  3. 客户端请求时,仅传递 SessionID,服务端通过 SessionID 匹配对应 Session 数据;
  4. 核心价值:避免用户私密信息直接在网络传输或本地存储,提升安全性。

常用 HTTP 请求报头解析

Host: 指定目标服务端的域名 / IP与端口,是 HTTP/1.1 必选字段。

**User-Agent:**记录客户端的浏览器类型、操作系统、设备型号,服务端据此适配响应内容。

Content-Type: 声明请求正文 的数据格式,如application/jsonapplication/x-www-form-urlencoded

Connection: 指定连接状态,keep-alive表示长连接,close表示短连接。

Accept: 声明客户端可接收的响应数据类型,如text/htmlimage/*

**Referer:**记录当前请求的来源页面 URL,用于防盗链、统计访问来源。


HTTPS 协议

HTTPS 与 HTTP 的核心区别

HTTPS 与 HTTP 的核心差异在于用户数据加密 ,HTTPS 侧重保护用户私密信息。应用层结构中,HTTPS 在 HTTP 请求与响应的基础上,新增SSL/TLS 加密解密层,所有传输数据均需经过加密处理,避免明文传输导致的信息泄露。

明文、密文与密钥的基础概念

明文 :待传输的原始数据,是通信双方需要交互的真实信息。密文 :经过加密算法处理后的数据,无密钥则无法识别真实内容。密钥 :加密与解密操作的核心参数,控制数据的加解密过程。示例:传输原始数字7 ,通过异或运算 与数字5 计算得到2 ,网络中实际传输2 。接收方通过相同密钥5 再次异或,还原出原始数据7 。此过程中7 为明文,2 为密文,5为密钥,异或运算承担加解密工作。

对称加密的定义与特点

对称加密 :加密与解密使用相同密钥 的加密方式。特点:算法公开计算量小加密速度快加密效率高,适用于大量数据的加密传输。缺陷:密钥需在网络中传输,存在密钥泄露风险,一旦密钥被获取,所有加密数据均可被破解。

非对称加密的定义、特点与公私钥机制

非对称加密 :加密与解密使用一对不同密钥 的加密方式,包含公钥私钥 。特点:算法复杂度高,运行速度慢于对称加密 ,安全性显著提升。公钥 :公开分发的密钥,可向所有通信方暴露。私钥 :自行保管的密钥,严格保密不对外传输。核心规则:公钥加密的数据,仅对应私钥可解密;私钥加密的数据,仅对应公钥可解密。该设计保证仅有私钥持有者,才能解密公钥加密的私密数据,防止传输内容被窃取。

MD5 算法与数据指纹

MD5 算法 :典型的哈希算法,可将任意长度的唯一原始数据,转换为固定长度、具备唯一性 的字符串,即数据指纹 。数据指纹具备不可逆特性,无法通过字符串还原原始数据,且相同数据的指纹结果唯一。服务端通常使用该算法生成Session ID,作为标识用户身份的唯一凭证。

HTTPS 混合加密的通信流程

服务端持有非对称加密的公钥 s私钥 ss

  1. 客户端发起连接请求,服务端将公钥 s发送至客户端;
  2. 客户端本地生成对称加密密钥c ,使用公钥 s 对 c 加密,得到密文cccccc
  3. 客户端将密文 cccccc 发送至服务端;
  4. 服务端使用私钥 ss 解密,获取对称密钥 c;
  5. 后续通信双方使用对称密钥 c 进行数据加密传输。该流程结合非对称加密的安全性与对称加密的高效性,但仍存在隐私泄露隐患。

中间人攻击的原理与危害

中间人攻击:通信双方的请求与响应均被第三方拦截篡改,形成客户端 - 中间人 - 服务端的虚假通信链路。

  1. 中间人持有自身非对称密钥对,公钥 m、私钥 mm;
  2. 客户端请求公钥时,中间人拦截服务端的公钥 s,替换为自身公钥 m 并发送给客户端;
  3. 客户端使用公钥 m 加密对称密钥 c,中间人可通过私钥 mm 解密获取 c;
  4. 中间人可窃取、篡改双方所有通信数据,且通信双方无法察觉。核心问题:客户端无法验证服务端公钥的合法性,无法识别公钥是否被篡改。

CA 证书与 CA 认证的安全解决方案

CA 机构 :权威第三方认证机构,负责验证服务端身份并发放CA 证书CA 证书:包含服务端信息、服务端公钥、CA 机构签名等内容,由 CA 机构加密签发。安全通信流程:

  1. 服务端向 CA 机构申请证书,不再直接发送公钥至客户端;
  2. 服务端将 CA 证书发送给客户端;
  3. 客户端内置权威 CA 机构的公钥,可对证书签名进行校验;
  4. 校验通过,确认证书合法,提取服务端真实公钥;校验失败,判定证书被篡改,终止连接。该机制通过权威认证解决公钥合法性验证问题,有效防范中间人攻击,保证 HTTPS 通信安全。
相关推荐
HalvmånEver2 小时前
MySQL数据库基础入门总结(从0到1)
linux·数据库·mysql
Lugas Luo2 小时前
Kernel 5.10 ATA 驱动分析与车载环境诊断
linux·嵌入式硬件
末日汐2 小时前
应用层协议HTTP
网络·网络协议·http
顶点多余2 小时前
死锁+线程安全
linux·开发语言·c++·系统安全
饺子大魔王的男人2 小时前
Linux 下 Apache RocketMQ 部署与公网访问实现指南
linux·apache·rocketmq
A.A呐2 小时前
【Linux第二十五章】高级IO
linux·运维·服务器
zzzsde2 小时前
【Linux】库的制作与使用(2)ELF&&静态链接
linux·运维·服务器
艾莉丝努力练剑2 小时前
【Linux线程】Linux系统多线程(二):线程的优缺点
linux·运维·服务器·c++·学习
HalvmånEver2 小时前
Linux:基于TCP Socket的客户端-服务器实现的远程命令行项目
linux·运维·服务器·网络·tcp/ip