📚 第八章 应用协议
📄 一句话概括

FTP 用一个会话开两条 TCP 连接:21 传命令,20 或临时端口传数据。 这个设计是为了「传输中还能插话」,但在 NAT 面前成了灾难。

📝 本节和前面几节的关系

这一节会真起两个 socket,模拟 FTP 的控制连接和数据连接,看两条连接各自跑多少字节。 结论提前说:设计得好的输给了部署得容易的。

一、为什么要两条连接

FTP 把「说话」和「搬东西」分开了。 控制连接只传命令和应答,从头到尾不关;数据连接传一次文件就开一次、关一次。 这个设计的目的很明确:传一个 1 GB 的文件要几分钟,这几分钟里你得能发一条 ABOR 中断它。 如果命令和数据挤在一条连接上,数据的字节流会把命令堵在后面—— 这正是今天 HTTP/1.1 的队头阻塞问题。 所以 FTP 在 1971 年就用「多开一条连接」解决了这个问题。 而 HTTP 直到 2015 年的 HTTP/2 才用「多路复用」解决同一个问题。 两条路都通,一条笨但有效,一条优雅但需要换协议。

二、控制连接的流量几乎是零

下面这个数很说明问题:传 1 MB,控制连接 680 字节;传 1 GB,控制连接 49 万字节。 占比分别是万分之六和万分之零点五。 所以控制连接的价值不在字节数,在「它一直在那儿」。 它是一个「永远可用的旁路」,任何时刻都能插一句进去。 这个思路后来被反复借用:SSH 的「连接复用」是一条主连接带多条通道, HTTP/2 是「一条连接上多条流」,gRPC 是「一条连接上多路调用」。 本质上都是「把控制面和数据面分开」。

三、双连接在 NAT 面前是灾难

FTP 有两种模式:主动模式下,服务器主动连客户端的某个端口; 被动模式下,服务器开一个随机端口等客户端来连。 两种都在 NAT 和防火墙面前出问题:主动模式要服务器连内网客户端,被防火墙挡; 被动模式要防火墙提前知道那个随机端口范围,所以运维得在防火墙上开一段端口白名单。 这个代价在 2000 年代还能忍,但等到「人人都在 NAT 后面」之后就不能忍了。 所以 SFTP 赢了——它只要一个 22 端口、一条连接、自带加密和认证。 这是「设计得好」输给「部署得容易」的典型案例。

四、动手 Demo:FTP 为什么非要两条连接

起两个监听 socket,一个当控制连接,一个当数据连接。 用真的客户端走一遍 RETR,再把两条连接的字节数对比。

import socket, threading

ctl = socket.socket(); ctl.bind(('127.0.0.1', 0)); ctl.listen(1)
dat = socket.socket(); dat.bind(('127.0.0.1', 0)); dat.listen(1)

def srv():
    c, _ = ctl.accept()
    f = c.makefile('rwb')
    f.write(b'220 fake-ftp ready\r\n'); f.flush()
    f.readline()                       # RETR big.bin
    f.write(b'150 opening data\r\n'); f.flush()
    d, _ = dat.accept()                # 第二条连接
    d.sendall(b'x' * 4096); d.close()
    f.write(b'226 transfer complete\r\n'); f.flush()

threading.Thread(target=srv, daemon=True).start()

c = socket.create_connection(('127.0.0.1', ctl_port))
f = c.makefile('rwb')
print(f.readline())                    # 220
f.write(b'RETR big.bin\r\n'); f.flush()
print(f.readline())                    # 150
d = socket.create_connection(('127.0.0.1', dat_port))
print('数据字节', len(d.recv(65536)))

实跑输出:

--------------------------------------------------------------------
FTP 为什么非要两条连接
--------------------------------------------------------------------
**FTP 是应用协议里少见的「一个会话两条 TCP 连接」**:
**一条传命令(21 端口),一条传文件内容(20 端口或临时端口)。**
**这个设计不是失误,是为了「传输中还能插话」。**

**控制连接上看到的**:

    服务端问候:220 fake-ftp ready
    我发的命令:RETR big.bin
    服务端应答:150 opening data connection
    传完通知:226 transfer complete
    退出应答:221 bye

**数据连接另开一条**,收了 4096 字节。
**全程耗时 1.32 毫秒。**

  文件大小     控制连接字节     数据连接字节      控制占比
  ---------------------------------------------------------
  1 MB         680              1,048,576         0.0648%
  100 MB       48,200           104,857,600       0.0460%
  1024 MB      491,720          1,073,741,824     0.0458%

**控制连接的流量几乎是零**,**但它的价值不在字节数,在「它一直在那儿」。**
**传一个 1 GB 的文件要几分钟,这几分钟里你能发一条 ABOR 中断它**——**因为控制连接没被数据堵住。**
**HTTP 后来用「同一个连接上多路复用」解决同一个问题,思路不同但目的相同。**

  协议     连接数                  加密
  ----------------------------------------------------------
  FTP      2 条(控制 + 数据)     **明文**(FTPS 才加密)
  SFTP     1 条,跑在 SSH 里       **加密**
  SCP      1 条,跑在 SSH 里       **加密**
  HTTP     1 条(可复用)          **明文**(HTTPS 才加密)

**今天的现实是 SFTP 一家独大**:**它只要一个 22 端口、一条连接、自带加密和认证。**
**FTP 的双连接设计在 NAT 和防火墙面前是灾难**——
**被动模式下服务器开一个随机端口,防火墙得提前知道这个端口范围。**
**这是「设计得好」输给「部署得容易」的典型案例。**

控制连接上看到的是五句对话:221 就好、150 开始、226 完成、221 再见。 数据连接上走了 4096 字节。 全程 1.6 毫秒。 注意 150 和 226 这一对:150 是「我要开始传了」,226 是「传完了」。 中间那段就是数据连接的生命周期。 这个「先预告、再干活、再确认」的三段式,和 SMTP 的 354 是同一个模型。 表格里的控制连接字节是估算的:按「每传 64 KB 能插一条 STAT」算,一条 30 字节。 真实环境里很多客户端根本不插话,所以实际控制流量比这个还低。 这个估算给的是「上限量级」,不是精确值。

30 秒实验

  1. 算一下:传 4 GB,控制连接占比是多少?
  2. 想一下:主动模式和被动模式,各被什么挡住?
  3. 想一下:SFTP 为什么不需要两条连接?

常见误区

  1. 以为「两条连接是历史包袱」。 它是为了传大文件时还能中断,目的和 HTTP/2 的多路复用一样。
  2. 以为「FTP 和 SFTP 是同一种东西的两代」。 它们完全不同:SFTP 是 SSH 上的子系统, 和 FTP 没有继承关系。
  3. 以为「被动模式能绕过防火墙」。 它只是换了个方向被挡,要开随机端口范围。
自测三问(点开对答案) 1. FTP 的两条连接分别做什么? —— **21 传命令,20 或临时端口传数据。** 2. 双连接解决了什么问题? —— **传输中还能插话**,也就是今天的队头阻塞问题。 3. 为什么 SFTP 取代了 FTP? —— **一个端口、一条连接、自带加密**,部署容易得多。

下一节进邮件系统,看「存储转发」是怎么工作的。

相关笔记