在一台 2C2G 的云服务器上实测:单文件 HTTP 服务器,g++ 编译,监听 8080 端口,从编译到公网访问全流程跑通。

1. socket 七步流程

示例是一个约 60 行的单文件程序 http_server.cpp,纯 Linux socket API、零第三方依赖,放在你自己的项目目录里即可。

socket() → bind() → listen() → accept() → recv() → send() → close()
  建电话     插网线    开机待命    接电话     听需求     答复     挂断
步骤调用说明
1socket(AF_INET, SOCK_STREAM, 0)IPv4 + TCP,返回文件描述符(Linux 一切皆文件)
2bind(fd, &addr, sizeof)绑定地址;INADDR_ANY(0.0.0.0)= 监听所有网卡
3listen(fd, 16)开始待命;16 是 backlog 等待队列长度
4accept(fd, ...)阻塞等客户端,返回新 fd(每客户一个)
5recv(fd, buf, n, 0)读 HTTP 请求报文(原始文本)
6send(fd, resp, len, 0)回 HTTP 响应
7close(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++17C++ 标准版本(同 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. 安全组:云服务器的门禁

服务器内服务跑起来 ≠ 公网可访问。云服务器默认拦截所有外来端口,需要在控制台放行:

  1. ECS 控制台 → 实例 → 安全组 → 配置规则
  2. 入方向 → 手动添加:协议 TCP,端口 8080/8080,授权对象 0.0.0.0/0
  3. 保存即生效,不用重启服务器

💡 类比: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 服务单元——开机自启 + 崩溃自动拉起(生产标准做法)

接下来值得做的事:

  1. 用 nohup / systemd 解决守护运行,配合 tail -f 看日志
  2. 补现代 C++ 核心(智能指针 / RAII / CMake),把单文件工程改造成 CMake 项目
  3. 写 epoll 并发版 HTTP 服务器,解掉单线程排队瓶颈

9. 小结:这篇该带走什么

  • 能默写 socket 七步流程及每步作用
  • 会用 g++ 编译单文件 C++ 程序(-std / -O2 / -o)
  • 会配安全组放行端口,理解「门禁」模型
  • 掌握六步排错链路,能独立定位「访问不了」
  • 能解剖一条真实 HTTP 请求报文
  • 亲手复现单线程排队瓶颈,能解释 backlog 与吞吐量
  • 会用 ps / ss / kill / pkill 管理进程