计算机网络

计算机网络基础

OSI七层模型

物理层

数据链路层

网络层

传输层

会话层

表示层

应用层

TCP/IP模型

TCP协议

TCP

TCP的特点

全双工通信

TCP是全双工的(Full-Duplex),这意味着:

  • 双向通信:连接建立后,通信双方可以同时发送和接收数据
  • 独立的数据流:每个方向都有独立的序列号和确认号,互不干扰
  • 独立的流量控制:每个方向都有独立的接收窗口,可以独立控制数据流量
  • 同时操作:在接收数据的同时可以发送数据,反之亦然

示例:在HTTP/1.1的持久连接中,客户端可以在接收服务器响应数据的同时,继续向服务器发送新的请求数据。

三次握手

三次握手(Three-Way Handshake)是TCP建立连接的过程:

  1. 第一次握手(SYN):客户端发送SYN包(SYN=1, seq=x)到服务器,进入SYN_SENT状态
  2. 第二次握手(SYN+ACK):服务器收到SYN包后,发送SYN+ACK包(SYN=1, ACK=1, seq=y, ack=x+1),进入SYN_RCVD状态
  3. 第三次握手(ACK):客户端收到SYN+ACK包后,发送ACK包(ACK=1, seq=x+1, ack=y+1),进入ESTABLISHED状态;服务器收到ACK后也进入ESTABLISHED状态

为什么采用三次握手

image-20251226154739289

TCP采用三次握手主要有以下几个原因:

  1. 防止历史连接(旧连接)的干扰

    • 如果客户端发送的SYN包在网络中滞留,客户端会重传新的SYN包
    • 如果只使用两次握手,当滞留的旧SYN包到达服务器时,服务器会误认为这是新的连接请求,建立连接并发送数据
    • 三次握手中,客户端收到服务器的SYN+ACK后,可以通过ACK中的确认号判断这是否是历史连接,如果是历史连接则发送RST包终止连接
  2. 确保双方都能确认对方的发送和接收能力

    • 第一次握手:客户端发送SYN → 服务器确认客户端的发送能力
    • 第二次握手:服务器发送SYN+ACK → 客户端确认服务器的发送和接收能力
    • 第三次握手:客户端发送ACK → 服务器确认客户端的接收能力
    • 只有三次握手才能确保双方都确认了对方的双向通信能力
  3. 同步双方的初始序列号(ISN)

    • TCP需要为每个方向的数据流分配独立的序列号
    • 三次握手确保双方都确认了对方的初始序列号,为后续的数据传输做准备
  4. 防止资源浪费

    • 如果只使用两次握手,服务器在收到SYN后就建立连接并分配资源
    • 如果客户端的ACK丢失,服务器会一直等待,造成资源浪费
    • 三次握手确保客户端真正准备好通信后才建立连接

为什么不能是两次握手?

  • 两次握手无法防止历史连接的干扰
  • 两次握手无法确保客户端确认服务器的接收能力
  • 如果客户端的ACK丢失,服务器无法确认连接是否真正建立

为什么不能是四次握手?

  • 三次握手已经足够确保双方的双向通信能力
  • 四次握手会增加网络延迟,没有必要

四次挥手

四次挥手(Four-Way Handshake)是TCP关闭连接的过程:

  1. 第一次挥手(FIN):主动关闭方(通常是客户端)发送FIN包(FIN=1, seq=u)到被动关闭方,表示不再发送数据,进入FIN_WAIT_1状态
  2. 第二次挥手(ACK):被动关闭方收到FIN包后,发送ACK包(ACK=1, seq=v, ack=u+1)确认收到,进入CLOSE_WAIT状态;主动关闭方收到ACK后进入FIN_WAIT_2状态
  3. 第三次挥手(FIN):被动关闭方发送FIN包(FIN=1, ACK=1, seq=w, ack=u+1)到主动关闭方,表示也不再发送数据,进入LAST_ACK状态
  4. 第四次挥手(ACK):主动关闭方收到FIN包后,发送ACK包(ACK=1, seq=u+1, ack=w+1)确认收到,进入TIME_WAIT状态;被动关闭方收到ACK后进入CLOSED状态

为什么采用四次挥手

TCP采用四次挥手主要有以下几个原因:

  1. TCP是全双工通信

    • TCP连接建立后,通信双方都有独立的发送和接收通道
    • 一方关闭连接时,只是表示”我不再发送数据了”,但可能还需要接收对方的数据
    • 因此需要双方分别关闭自己的发送通道,这就是为什么需要四次挥手
  2. 被动关闭方需要时间处理剩余数据

    • 当主动关闭方发送FIN后,被动关闭方可能还有数据需要发送
    • 第二次挥手的ACK只是确认收到了关闭请求,但被动关闭方可能还需要:
      • 发送缓冲区中剩余的数据
      • 处理应用层的关闭逻辑
    • 只有等被动关闭方也准备好关闭时,才会发送FIN(第三次挥手)
  3. 为什么不能是三次挥手?

    • 如果被动关闭方在收到FIN后立即发送FIN+ACK(将第二次和第三次挥手合并),可能会出现问题:
      • 被动关闭方可能还有数据要发送,立即关闭会导致数据丢失
      • 无法保证被动关闭方已经处理完所有待发送的数据
    • 分开发送ACK和FIN,给被动关闭方缓冲时间来处理剩余数据
  4. TIME_WAIT状态的作用

    • 主动关闭方在发送最后一次ACK后进入TIME_WAIT状态,等待2MSL(Maximum Segment Lifetime,最大报文段生存时间)
    • 作用
      • 确保最后一个ACK能够到达被动关闭方:如果ACK丢失,被动关闭方会重传FIN,主动关闭方可以再次发送ACK
      • 防止”已失效的连接请求报文段”出现在本连接中:等待足够的时间,让网络中所有该连接的报文段都消失
    • 如果没有TIME_WAIT状态,可能会影响新建立的连接(如果端口被立即重用)

为什么不能是两次挥手?

  • 两次挥手无法保证双方都完成了数据的发送和接收
  • 无法处理被动关闭方还有数据要发送的情况
  • 无法保证连接的可靠关闭

为什么不能是三次挥手?

  • 三次挥手无法给被动关闭方足够的时间来处理剩余数据
  • 如果被动关闭方立即发送FIN+ACK,可能会导致数据丢失
  • 四次挥手的设计更符合TCP全双工通信的特性

如果CLOSE_WAIT大量出现说明什么?

CLOSE_WAIT大量出现说明被动关闭方(通常是服务器)的应用程序存在严重问题

  1. 问题的本质

    • CLOSE_WAIT是被动关闭方收到FIN包后的状态,正常情况下应该很快发送FIN包进入LAST_ACK状态
    • 大量CLOSE_WAIT连接说明应用程序没有正确关闭连接,导致连接一直停留在CLOSE_WAIT状态
    • 这是应用程序层面的bug,不是网络问题
  2. 常见原因

    • 代码bug:忘记调用close()shutdown()函数关闭套接字
    • 异常处理不当:程序异常退出或崩溃,没有执行清理代码
    • 资源泄漏:连接对象没有被正确释放,特别是在高并发场景下
    • 死锁或阻塞:应用程序在处理数据时发生死锁或长时间阻塞,无法执行关闭操作
    • 连接池配置问题:连接池没有正确管理连接的生命周期
  3. 带来的严重后果

    • 文件描述符耗尽:每个CLOSE_WAIT连接都占用一个文件描述符,大量CLOSE_WAIT会导致文件描述符耗尽,无法建立新连接
    • 内存泄漏:连接对象和缓冲区一直占用内存,导致内存泄漏
    • 服务不可用:当文件描述符耗尽时,服务无法接受新连接,导致服务不可用
    • 性能下降:系统资源被无效连接占用,影响系统整体性能

注意:CLOSE_WAIT状态本身没有超时机制,连接会一直停留在CLOSE_WAIT状态直到应用程序关闭连接。大量CLOSE_WAIT是严重问题,必须立即修复应用程序代码。

例如,手动管理事务但是没有释放链接
https://zhuanlan.zhihu.com/p/377044076

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func (c *MainController) update() (flag bool) {
o := orm.NewOrm()
o.Using("default")

o.Begin()//开启事务
nilMap := getMapNil()
if nilMap == nil {// 这里只检查了是否为nil,并没有进行rollback或者commit
return false
}

nilMap[10] = 1
nilMap[20] = 2
if nilMap == nil && len(nilMap) == 0 {
o.Rollback()
return false
}

sql := "update tb_user set name=%s where id=%d"
res, err := o.Raw(sql, "Bug", 2).Exec()
if err == nil {
num, _ := res.RowsAffected()
fmt.Println("mysql row affected nums: ", num)
o.Commit()
return true
}

o.Rollback()
return false
}

在 Go 等语言中,如果函数返回了,局部变量 o(ORM 实例)的作用域虽然结束,但它在底层的数据库连接仍然持有事务状态。数据库驱动(Driver)会认为该连接正忙于事务,因此不会将其放回连接池,也不会释放。

如果TIME_WAIT大量出现说明什么?

TIME_WAIT服务器主动关闭了大量连接,正处于协议保护期:

  1. TIME_WAIT的正常流程

    • 主动关闭方发送最后一个ACK后进入TIME_WAIT状态
    • 等待2MSL(Maximum Segment Lifetime,通常为1-4分钟)后自动进入CLOSED状态
    • 这是TCP协议设计的正常关闭流程,不是错误
  2. TIME_WAIT的作用

    • 确保最后一个ACK能够到达被动关闭方
    • 防止旧连接的报文段影响新连接
    • 这是TCP可靠性的保证
  3. 如果TIME_WAIT过多

    1. 大量的短连接存在
    2. 特别是 HTTP 请求中,如果 connection 头部取值被设置为 close 时,基本都由「服务端」发起主动关闭连接
    3. 而,TCP 四次挥手关闭连接机制中,为了保证 ACK 重发和丢弃延迟数据,设置 time_wait 为 2 倍的 MSL(报文最大存活时间)
    • 解决方案
      • 使用HTTP长连接(Keep-Alive)减少连接建立和关闭次数
      • 调整系统参数(如net.ipv4.tcp_tw_reuse)重用TIME_WAIT状态的端口
      • 使用连接池复用连接

CLOSE_WAIT vs TIME_WAIT的区别

  • CLOSE_WAIT:被动关闭方状态,超时说明应用程序有问题,需要修复代码
  • TIME_WAIT:主动关闭方状态,超时是正常现象,表示连接已安全关闭

访问一个Https网站背后会发生什么


计算机网络
https://yicizhang00.github.io/posts/基础理论/计算机网络/计算机网络基础/
作者
Yici Zhang
发布于
2025年8月13日
许可协议