目录
[网络基础概念 与 "协议"概念](#网络基础概念 与 “协议”概念)
[一、Linux 网络的核心前提:一切皆文件 → 网络通信 = 文件操作](#一、Linux 网络的核心前提:一切皆文件 → 网络通信 = 文件操作)
[二、网络诞生的背景:从 "单机" 到 "多机互联" 的必然性](#二、网络诞生的背景:从 “单机” 到 “多机互联” 的必然性)
[三、协议的本质:设备间的 "约定 / 规则"](#三、协议的本质:设备间的 “约定 / 规则”)
[五、Linux 中协议的实现:C 语言 + 结构体](#五、Linux 中协议的实现:C 语言 + 结构体)
总结
[进程端口号 与 TCP/UDP协议](#进程端口号 与 TCP/UDP协议)[二、为什么有 PID(进程 ID)还需要端口号?](#二、为什么有 PID(进程 ID)还需要端口号?)
[四、TCP/UDP 协议:传输层的两大核心协议](#四、TCP/UDP 协议:传输层的两大核心协议)
总结
[基于 UDP 协议实现服务端与用户端网络通信](#基于 UDP 协议实现服务端与用户端网络通信)[二、UdpServer.cc 核心函数解析](#二、UdpServer.cc 核心函数解析)
[三、UdpClient.cc 核心函数解析](#三、UdpClient.cc 核心函数解析)
总结
[基于 TCP 协议实现服务端与用户端网络通信(守护进程化)](#基于 TCP 协议实现服务端与用户端网络通信(守护进程化))[一、TCP 服务端 / 客户端代码整体逻辑](#一、TCP 服务端 / 客户端代码整体逻辑)
[test.cc(Json 的基本使用)](#test.cc(Json 的基本使用))
[JSON 代码全解析与网络传输核心作用](#JSON 代码全解析与网络传输核心作用)
[JsonCpp 核心类与函数解析](#JsonCpp 核心类与函数解析)
[结合 TCP/UDP 编程的实际应用流程](#结合 TCP/UDP 编程的实际应用流程)
核心知识点总结
[HTTP 协议](#HTTP 协议)[网址与 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 语言实现的,而结构体是协议的核心载体:
-
协议的 "约定" 可拆解为多个字段:比如 IP 协议约定了 "源 IP 地址、目标 IP 地址、数据长度、协议版本" 等字段;
-
用结构体描述协议字段:
// 简化的IP协议头结构体 struct ip_header { uint8_t version; // IP版本(如IPv4) uint8_t len; // 协议头长度 uint32_t src_ip; // 源IP地址 uint32_t dst_ip; // 目标IP地址 // 其他字段... }; -
传输逻辑:发送数据时,先填充协议结构体(如 IP 头),再把真实数据 "附在" 结构体后,组成完整数据包;接收方拿到数据包后,先解析结构体(协议头),再提取里面的真实数据。类比:快递单号(结构体)包含收件人地址、电话,快递员先看单号(解析结构体),再把里面的洗面奶(真实数据)交给你。
总结
- Linux 网络核心:网卡 = 文件,网络通信 = 文件读写,和单机文件操作逻辑统一;
- 网络诞生:解决单机数据交互的低效 / 高风险问题,从局域网演化到广域网;
- 协议本质:设备间的通信约定,无协议则数据无意义;
- 协议分层:底层协议解决 "数据怎么传",应用层协议解决 "数据怎么用";
- 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% 到达,哪怕牺牲一点效率。
总结
- 端口号是进程的 "网络标识",IP + 端口号实现全网进程唯一标识,数据传输时每一层加报头、逐跳更新 MAC,IP 全程不变;
- PID 是单机进程管理标识,端口号为网络通信设计,解决 PID 动态性和功能解耦问题;
- 端口绑定规则:一进程多端口(可行)、一端口多进程(不可行),核心保证唯一性;
- 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));
- 参数解析 :
_sockfd:socket函数返回的网络文件描述符;(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.0(INADDR_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 是操作系统的内核资源,必须显式关闭(或析构函数自动关闭),否则会导致资源泄漏(端口被占用、文件描述符耗尽)。
总结
- 套接字是进程间通信的统一接口,分域间、原始、网络三类,通过
struct sockaddr统一封装地址结构体; - 服务端核心流程:
socket(创建套接字)→bind(绑定 IP + 端口)→recvfrom(接收数据)→sendto(回显数据); - 客户端核心流程:
socket(创建套接字)→sendto(发送数据,自动绑定临时端口)→recvfrom(接收回显); - 字节序转换(
htons/inet_addr)是网络编程的核心细节,确保跨主机数据解析正确; - 服务端绑定
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* ip、struct in_addr* addr(输出型参数);返回:成功1,失败0 |
无静态区问题,推荐使用 |
inet_pton |
协议无关转换(IPv4/IPv6) | 参数:int af(AF_INET/AF_INET6)、const char* src、void* dst(输出);返回:成功1,格式错误0,失败-1 |
支持 IPv6,最通用、最安全 |
inet_ntoa |
in_addr 结构体 → 字符串 IP | 参数:struct in_addr in;返回:指向静态区的字符串指针 |
静态区覆盖问题,不推荐 |
inet_ntop |
协议无关转换(IPv4/IPv6) | 参数:int af、const 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实际是 "半连接队列 + 已完成队列" 的总和上限,用于限制待处理的连接请求数,避免服务器过载。
- 补充:Linux 下
- 返回值 :成功
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信号(默认终止进程);守护进程脱离会话后,不会收到该信号,因此能持续运行。
总结
- TCP 服务端核心流程:
socket(创建 fd)→bind(绑定 IP + 端口)→listen(转为被动套接字)→accept(获取连接)→多线程/进程处理通信→read/write(字节流读写); - TCP 客户端核心流程:
socket→connect(三次握手)→循环读写; - IP / 端口转换需注意静态区覆盖问题 ,优先使用
inet_aton/inet_pton/inet_ntop; - 守护进程通过
fork+setsid脱离终端,实现后台常驻; - 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 库的标准引入路径,包含Value 、Writer 、Reader等核心组件。
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 编程的实际应用流程
客户端(发送端)
- 定义 C++ 业务变量;
- 执行序列化操作,生成 JSON 字符串;
- 调用
write/sendto函数,将 JSON 字符串发送至服务端。
服务端(接收端)
- 调用
read/recvfrom函数,接收 JSON 字符串; - 执行反序列化操作,还原业务变量;
- 基于还原的变量执行业务逻辑处理。
代码执行结果
序列化后的JSON字符串:
{"desc":"this is a + oper","op":43,"x":100,"y":200}
反序列化结果:
100
200
+
this is a + oper
序列化生成纯字符串,满足网络传输要求;反序列化还原 C++ 变量,满足业务使用要求。
核心知识点总结
- Json::Value:JSON 数据的通用存储容器;
- Json::FastWriter + write() :实现序列化,完成 C++ 数据到 JSON 字符串的转换;
- Json::Reader + parse() :实现反序列化,完成 JSON 字符串到 C++ 数据的转换;
- 序列化与反序列化是网络编程的标配操作,解决内存数据无法直接跨主机传输的问题;
- 执行规则:发送端传输前序列化 ,接收端接收后反序列化。
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 可精准定位并获取网络资源,是网络资源访问的核心标识。
网络基础行为分类
网络数据交互分为两类核心行为:
- 下载行为:获取远端主机的资源至本地主机;
- 上传行为:将本地主机的资源传输至远端主机。
HTTP 请求报文格式
HTTP 请求由请求行、请求报头、空行、请求正文 四部分组成,是客户端向服务端发送的交互数据。请求行 包含请求方法 、URL 、HTTP 版本 ,以\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 与 Session 原理及安全机制
Cookie 的含义 Cookie 是服务端写入客户端本地的小型文本数据,以键:值格式存储,随每次 HTTP 请求自动发送至服务端,用于标识客户端状态。Cookie 的安全风险 Cookie 存储在本地,存在被窃取、篡改的风险,直接存储用户私密信息会导致隐私泄露。Session+Cookie 的安全方案
- 服务端为每个客户端创建独立Session,存储用户私密信息,生成唯一 SessionID;
- 服务端仅将SessionID写入客户端 Cookie;
- 客户端请求时,仅传递 SessionID,服务端通过 SessionID 匹配对应 Session 数据;
- 核心价值:避免用户私密信息直接在网络传输或本地存储,提升安全性。
常用 HTTP 请求报头解析
Host: 指定目标服务端的域名 / IP与端口,是 HTTP/1.1 必选字段。
**User-Agent:**记录客户端的浏览器类型、操作系统、设备型号,服务端据此适配响应内容。
Content-Type: 声明请求正文 的数据格式,如application/json、application/x-www-form-urlencoded。
Connection: 指定连接状态,keep-alive表示长连接,close表示短连接。
Accept: 声明客户端可接收的响应数据类型,如text/html、image/*。
**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。
- 客户端发起连接请求,服务端将公钥 s发送至客户端;
- 客户端本地生成对称加密密钥c ,使用公钥 s 对 c 加密,得到密文cccccc;
- 客户端将密文 cccccc 发送至服务端;
- 服务端使用私钥 ss 解密,获取对称密钥 c;
- 后续通信双方使用对称密钥 c 进行数据加密传输。该流程结合非对称加密的安全性与对称加密的高效性,但仍存在隐私泄露隐患。
中间人攻击的原理与危害
中间人攻击:通信双方的请求与响应均被第三方拦截篡改,形成客户端 - 中间人 - 服务端的虚假通信链路。
- 中间人持有自身非对称密钥对,公钥 m、私钥 mm;
- 客户端请求公钥时,中间人拦截服务端的公钥 s,替换为自身公钥 m 并发送给客户端;
- 客户端使用公钥 m 加密对称密钥 c,中间人可通过私钥 mm 解密获取 c;
- 中间人可窃取、篡改双方所有通信数据,且通信双方无法察觉。核心问题:客户端无法验证服务端公钥的合法性,无法识别公钥是否被篡改。
CA 证书与 CA 认证的安全解决方案
CA 机构 :权威第三方认证机构,负责验证服务端身份并发放CA 证书 。CA 证书:包含服务端信息、服务端公钥、CA 机构签名等内容,由 CA 机构加密签发。安全通信流程:
- 服务端向 CA 机构申请证书,不再直接发送公钥至客户端;
- 服务端将 CA 证书发送给客户端;
- 客户端内置权威 CA 机构的公钥,可对证书签名进行校验;
- 校验通过,确认证书合法,提取服务端真实公钥;校验失败,判定证书被篡改,终止连接。该机制通过权威认证解决公钥合法性验证问题,有效防范中间人攻击,保证 HTTPS 通信安全。