其实是我调式了 N 久的一个 BUG, 最后发现这原来是 TCP 的 Feature. 文章为我转我自己, 原文链接在底部.
我相信绝大多数人都会写 TCP 的服务端代码, 就自己而言, 已经几乎机械式地在写如下代码(就如定式一般):
ln, err := net.Listen("tcp", ":3000")
for {
conn, err := ln.Accept()
...
}
Good! conn
对象到手! 之后便可以安心地从 conn 对象中读取数据, 或写入数据.
但是有没有考虑过一个问题, 如果在 Listen 后不调用 Accept, 会发生什么事? 这并非是无事找事的异想天开, 在现实中, 有很多种情况会导致代码 Accept 失败, 比如 too many open files
发生时.
这是本次实验的服务端伪代码, 可以看到, 在 Listen 端口后, 代码只使用了一个循环 Sleep 将进程永久挂起.
func main() {
listen, err := net.Listen("tcp", ":3000")
for {
time.Sleep(time.Second)
}
}
客户端伪代码主要执行三个步骤: 连接服务器, 等待 10 秒后向服务器发送数据, 关闭连接.
func main() {
conn, err := net.Dial("tcp", "127.0.0.1:3000")
log.Println("Dial conn", conn, err)
time.Sleep(time.Second * 10)
n, err := io.WriteString(conn, "ping")
log.Println("Write", n, "bytes,", "error is", err)
err := conn.Close()
log.Println("Close", err)
}
如此这般, 执行程序!
2020/03/30 17:57:45 Dial conn &{{0xc0000a2080}}
2020/03/30 17:57:45 Write 4 bytes, error is <nil>
2020/03/30 17:57:45 Close <nil>
客户端连接服务器成功未报错, 发送数据成功未报错, 关闭连接成功亦未报错. 重新执行客户端代码, 这次让我们在执行的时候用 netstat 工具查看连接状态. 这里分为三个步骤.
客户端连接到服务器后
tcp 0 0 127.0.0.1:56428 127.0.0.1:8080 ESTABLISHED 18063/client
tcp 0 0 127.0.0.1:8080 127.0.0.1:56428 ESTABLISHED -
客户端调用 Close 后
tcp 0 0 127.0.0.1:56428 127.0.0.1:8080 FIN_WAIT2 -
tcp 5 0 127.0.0.1:8080 127.0.0.1:56428 CLOSE_WAIT -
客户端进程退出后
tcp 5 0 127.0.0.1:8080 127.0.0.1:56428 CLOSE_WAIT -
注意最后的 CLOSE_WAIT, 它将永远存在, 直到服务端进程退出.
当客户端连接服务端后, 通过 netstat 看到连接状态为 ESTABLISHED, 这说明 TCP 三次握手已经成功, 也就是说 TCP 连接已经在网络上建立了起来. 可得知 TCP 握手并不是 Accept 函数的职责.
阅读操作系统的 Accept 函数文档: http://man7.org/linux/man-pages/man2/accept.2.html, 在第一段落中有如下描述:
It extracts the first connection request on the queue of pending connections for the listening socket, sockfd, creates a new connected socket, and returns a new file descriptor referring to that socket.
翻译: 它从 connections 队列中取出第一个 connection, 并返回引用该 connection 的一个新的文件描述符.
验证了我的想法, 无论是否调用 Accept, connection 都已经建立起来了, Accept 只是将该 connection 包装成一个文件描述符, 供程序 Read, Write 和 Close. 那么关于第二步为什么客户端能 Write 成功就很容易解释了, 因为 connection 早已被建立(数据应该被暂存在服务端的接受缓冲区).
接着再分析 CLOSE_WAIT. 正常情况下 CLOSE_WAIT 在 TCP 挥手过程中持续时间极短, 如果出现则表明"被动关闭 TCP 连接的一方未调用 Close 函数". 观察下图的 TCP 挥手过程, 得知"即使被动关闭一方未调用 Close, 依然会响应 FIN 包发出 ACK 包", 因此主动关闭一方处于 FIN_WAIT2 是理所当然的.
+---------+ ---------\ active OPEN
| CLOSED | \ -----------
+---------+<---------\ \ create TCB
| ^ \ \ snd SYN
passive OPEN | | CLOSE \ \
------------ | | ---------- \ \
create TCB | | delete TCB \ \
V | \ \
+---------+ CLOSE | \
| LISTEN | ---------- | |
+---------+ delete TCB | |
rcv SYN | | SEND | |
----------- | | ------- | V
+---------+ snd SYN,ACK / \ snd SYN +---------+
| |<----------------- ------------------>| |
| SYN | rcv SYN | SYN |
| RCVD |<-----------------------------------------------| SENT |
| | snd ACK | |
| |------------------ -------------------| |
+---------+ rcv ACK of SYN \ / rcv SYN,ACK +---------+
| -------------- | | -----------
| x | | snd ACK
| V V
| CLOSE +---------+
| ------- | ESTAB |
| snd FIN +---------+
| CLOSE | | rcv FIN
V ------- | | -------
+---------+ snd FIN / \ snd ACK +---------+
| FIN |<----------------- ------------------>| CLOSE |
| WAIT-1 |------------------ | WAIT |
+---------+ rcv FIN \ +---------+
| rcv ACK of FIN ------- | CLOSE |
| -------------- snd ACK | ------- |
V x V snd FIN V
+---------+ +---------+ +---------+
|FINWAIT-2| | CLOSING | | LAST-ACK|
+---------+ +---------+ +---------+
| rcv ACK of FIN | rcv ACK of FIN |
| rcv FIN -------------- | Timeout=2MSL -------------- |
| ------- x V ------------ x V
\ snd ACK +---------+delete TCB +---------+
------------------------>|TIME WAIT|------------------>| CLOSED |
+---------+ +---------+
最后, 当客户端进程退出后, 客户端保留的 FIN_WAIT2 状态自然被释放, 但服务端由于未获得 connection 的文件描述符无法主动调用 Close 函数, 因此服务端的 CLOSE_WAIT 将一直持续直到服务端进程退出.
在本文的例子中, 服务端没有能力进行处理(代码中没有拿到 conn), 因为 connection 归操作系统管.
但是如果程序是因为 too many open files
等错误导致 Accept 失败, 那么当操作系统的文件描述符数量下降时 Accept 函数将可以成功, 因此应用程序可以拿到引用该 connection 的文件描述符, 在程序代码中按照正常逻辑 Close 掉该文件描述符即可释放该 connection.
这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。
V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。
V2EX is a community of developers, designers and creative people.