在一台 2C2G 的云服务器上实测:单文件 HTTP 服务器,
g++编译,监听 8080 端口,从编译到公网访问全流程跑通。
1. socket 七步流程
示例是一个约 60 行的单文件程序 http_server.cpp,纯 Linux socket API、零第三方依赖,放在你自己的项目目录里即可。
socket() → bind() → listen() → accept() → recv() → send() → close()
建电话 插网线 开机待命 接电话 听需求 答复 挂断
| 步骤 | 调用 | 说明 |
|---|---|---|
| 1 | socket(AF_INET, SOCK_STREAM, 0) | IPv4 + TCP,返回文件描述符(Linux 一切皆文件) |
| 2 | bind(fd, &addr, sizeof) | 绑定地址;INADDR_ANY(0.0.0.0)= 监听所有网卡 |
| 3 | listen(fd, 16) | 开始待命;16 是 backlog 等待队列长度 |
| 4 | accept(fd, ...) | 阻塞等客户端,返回新 fd(每客户一个) |
| 5 | recv(fd, buf, n, 0) | 读 HTTP 请求报文(原始文本) |
| 6 | send(fd, resp, len, 0) | 回 HTTP 响应 |
| 7 | close(fd) | 挂断,回 while 等下一客 |
要点:
htons/htonl= host to network:字节序转网络序(大端),跨机通信必须转inet_ntoa(client.sin_addr):客户端 IP 二进制转点分字符串,打日志用- HTTP 响应就是约定格式的字符串:状态行 + 头部 + 空行 + 正文;
Content-Length必须等于正文字节数 ::前缀(如::socket)显式调用全局函数,防止被同名成员遮蔽
💡 关键顿悟:HTTP 本质是「约定好格式的字符串」,没有任何魔法。
recv收到的就是原始报文文本,自己解析、自己拼响应——这就是 Web 服务的底层真相。
2. 编译与运行
g++ -std=c++17 -O2 -o http_server http_server.cpp
./http_server 8080
| 选项 | 含义 |
|---|---|
-std=c++17 | C++ 标准版本(同 Qt .pro 里选 C++ 版本) |
-O2 | 优化等级 2(发布常用档,O = Optimization) |
-o | 指定输出文件名(不给默认叫 a.out) |
- 编译成功后
ls -l看到绿色可执行文件,权限位自带x——编译器自动给执行权限 - 运行后看到
HTTP server listening on 0.0.0.0:8080即就绪 - ⚠️ 前台运行,SSH 断线服务就死(见第 8 节)
3. 安全组:云服务器的门禁
服务器内服务跑起来 ≠ 公网可访问。云服务器默认拦截所有外来端口,需要在控制台放行:
- ECS 控制台 → 实例 → 安全组 → 配置规则
- 入方向 → 手动添加:协议 TCP,端口
8080/8080,授权对象0.0.0.0/0 - 保存即生效,不用重启服务器
💡 类比:
bind/listen是你把门打开,安全组是机房门口的保安——保安不放人,谁也到不了你门前。这是云服务器和虚拟机最不一样的一环(虚拟机没有这道门禁)。
4. 排错链路:访问不了怎么办
踩过坑之后总结出的固定排查顺序,任何「服务访问不了」都按这条链走:
浏览器访问失败
→ 1. 进程活着吗? ps aux | grep http_server
→ 2. 端口在听吗? ss -tlnp | grep 8080 (要看到 0.0.0.0:8080 LISTEN)
→ 3. 本机通吗? curl 127.0.0.1:8080 (在服务器上跑,不是本机!)
→ 4. 防火墙放行吗? systemctl status firewalld
→ 5. 云门禁放行吗? 控制台里的安全组规则
→ 6. 访问姿势对吗? http://IP:8080 手动带 http://,IP 别敲错
实测踩过的坑:
- 在 Windows 上
curl 127.0.0.1—— 回环地址测的是自己电脑,自测必须在服务器上跑 - IP 敲错一位数 —— 排错先怀疑最笨的原因(敲错、没保存、没重启),再怀疑复杂的
- 浏览器自动跳 https 而服务只有 http → 直接失败,地址栏手动敲全
http://
5. HTTP 报文解剖(真实日志)
服务运行时终端会打印每条连接的来源与请求行,一次浏览器访问往往产生多条连接:
| 日志特征 | 是谁 |
|---|---|
127.0.0.1 + curl | 服务器上自己的自测 |
| 家宽 IP + curl/8.x | 本机终端里的 curl |
家宽 IP + Edg/151,GET / | Edge 浏览器请求主页 |
同上,GET /favicon.ico | 浏览器自动请求网站图标 |
一条真实请求报文的结构:
GET / HTTP/1.1 <- 请求行:方法 路径 版本
Host: <你的服务器公网IP>:8080 <- 头部区:key: value
User-Agent: Mozilla/5.0 ... <- 自报身份(浏览器标识)
Accept: text/html,... <- 能接收的类型
(空行 = 头部结束,GET 无正文)
细节:
Sec-Purpose: prefetch;prerender:浏览器预判页面提前请求,所以开一个标签日志里可能有 2~3 条连接Connection: keep-alive:浏览器想复用连接;而示例的响应头回的是close(用完就关),所以每个请求都是新连接- 明文 HTTP 下通信两端的公网 IP 都是暴露的(服务器侧、访客侧各一个),这正是以后要学 HTTPS 的原因之一
6. 实验:单线程排队瓶颈
在 recv 前加 sleep(5) 模拟慢处理,重新编译运行,开两个浏览器标签同时访问:
- 标签 A:5 秒后出 Hello
- 标签 B(趁 A 处理时打开):转圈等待,直到 A 处理完才轮到它
日志里 9 个连接严格串行,每个间隔 5 秒——connection from 的打印时刻 = 开始被服务的时刻,不是到达时刻(到达更早,只是排在队列里)。
| 现象 | 本质 |
|---|---|
| 第二个标签转圈 | 连接在 backlog 队列排队,等前一个处理完 |
| 最终都能打开 | 队列(backlog=16)没满,只是慢 |
| 日志严格一条接一条 | while 循环:accept → 处理 → close,永不重叠 |
单线程吞吐量 = 1 / 处理耗时(本例每秒 0.2 个请求)。不加 sleep 时两个标签都能秒开,是因为处理太快(微秒级)感觉不到排队——「卡不卡」取决于处理耗时 × 到达密度,而不是「能不能同时处理」。
解法(网络编程的核心课题):多线程(每连接一线程)、线程池、epoll 事件循环。
7. 进程管理小命令
ps aux | grep http_server # ps = process status,看进程在不在
ss -tlnp | grep 8080 # ss = socket statistics,看端口监听
kill <pid> # 默认发 SIGTERM(15)请求退出
kill -9 <pid> # SIGKILL 强杀(不给进程收尾机会,慎用)
pkill http_server # 按名字杀,一步到位(杀所有同名进程)
实测坑:bind: Address already in use = 旧实例还占着端口,先 pkill 再启动。
ss -tlnp 输出解读:
LISTEN 0 16 0.0.0.0:8080 ...
↑ ↑
Recv-Q backlog——Recv-Q 是此刻排队等 accept 的连接数
8. 遗留问题与下一步
遗留:SSH 断线服务就死。 前台进程随终端会话结束而被杀。两条解法:
nohup ./http_server 8080 &——免疫挂断信号,后台运行- systemd 服务单元——开机自启 + 崩溃自动拉起(生产标准做法)
接下来值得做的事:
- 用 nohup / systemd 解决守护运行,配合
tail -f看日志 - 补现代 C++ 核心(智能指针 / RAII / CMake),把单文件工程改造成 CMake 项目
- 写 epoll 并发版 HTTP 服务器,解掉单线程排队瓶颈
9. 小结:这篇该带走什么
- 能默写 socket 七步流程及每步作用
- 会用
g++编译单文件 C++ 程序(-std/-O2/-o) - 会配安全组放行端口,理解「门禁」模型
- 掌握六步排错链路,能独立定位「访问不了」
- 能解剖一条真实 HTTP 请求报文
- 亲手复现单线程排队瓶颈,能解释 backlog 与吞吐量
- 会用
ps/ss/kill/pkill管理进程
豫公网安备41108202000982号