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 秒实验
- 算一下:传 4 GB,控制连接占比是多少?
- 想一下:主动模式和被动模式,各被什么挡住?
- 想一下:SFTP 为什么不需要两条连接?
常见误区
- 以为「两条连接是历史包袱」。 它是为了传大文件时还能中断,目的和 HTTP/2 的多路复用一样。
- 以为「FTP 和 SFTP 是同一种东西的两代」。 它们完全不同:SFTP 是 SSH 上的子系统, 和 FTP 没有继承关系。
- 以为「被动模式能绕过防火墙」。 它只是换了个方向被挡,要开随机端口范围。
自测三问(点开对答案)
1. FTP 的两条连接分别做什么? —— **21 传命令,20 或临时端口传数据。** 2. 双连接解决了什么问题? —— **传输中还能插话**,也就是今天的队头阻塞问题。 3. 为什么 SFTP 取代了 FTP? —— **一个端口、一条连接、自带加密**,部署容易得多。下一节进邮件系统,看「存储转发」是怎么工作的。
- ☐ (本节完,下一节 8.4.1 电子邮件的工作机制)