阿巩
别看标题不正经,内容可是相当硬核哦~
面向大厂jd(职位描述)学习一直是阿巩秉承的原则,技术岗位的jd通常有要求"熟悉TCP/IP等网络协议"这样的字样。我们普遍对TCP的了解是“TCP是一种面向连接的、可靠的、基于字节流的传输层通信协议”。那么连接是如何创建的呢?是什么支持了TCP的可靠性呢?两主机性能差很多时,TCP又是如何做流量控制的呢?TCP做拥塞控制有哪些方式呢?等等问题需要我们去探索,事不宜迟,日拱一卒,让我们开始吧!
当用户在浏览器中输入一个url,通过DNS查询拿到ip地址,之后就是我们TCP建立连接的过程了,三次握手及四次挥手过程图示如下:
那么,为什么需要三次握手呢?
TCP连接的双方要确保各自的收发消息的能力都是正常的。
首先客户端第一次发送握手消息到服务端;服务端接收到握手消息后把ack和自己的syn一同发送给客户端,这是第二次握手;当客户端接收到服务端发送来的第二次握手消息后,客户端可以确认“服务端的收发能力OK,客户端的收发能力OK”,但是服务端只能确认“客户端的发送OK,服务端的接收OK”,同时也为了避免建立重复连接,所以还需要第三次握手,服务端收到客户端发送的第三次握手消息后,就能够确定“服务端的发送OK,客户端的接收OK”。至此,客户端和服务端都能够确认自己和对方的收发能力OK,TCP连接建立完成。
下面我们来用python写一个demo体会下客户端与服务端之间通信的过程。
服务端代码:
# echo_server.py
import socket
HOST = 'localhost'
PORT = 10001
def echo_server():
"""Echo Server的Server端"""
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 将对象绑定到指定的主机和端口上
s.bind((HOST, PORT))
# 只接受1个连接
s.listen(1)
while True:
# accept表示接受用户端的连接
conn, addr = s.accept()
# 输出客户端地址
print(f'Connected by {addr}')
while True:
data = conn.recv(1024)
if not data:
break
conn.sendall(data)
conn.close()
s.close()
if __name__ == '__main__':
echo_server()
客户端代码:
# echo_client.py
import socket
HOST = 'localhost'
PORT = 10001
def echo_client():
"""Echo Server的client端"""
# 封装socket.socket,包括IPV4(AF_INET)、TCP协议(SOCK_STREAM)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 将socket对象建立连接,包含主机名HOST和端口PORT
s.connect((HOST, PORT))
while True:
# 接收用户输入的数据并发送给服务端
data = input('input > ')
# 设定退出条件
if data == 'exit':
break
# 发送数据到服务端
s.sendall(data.encode())
# 接受服务端数据
data = s.recv(1024)
# 判断输入
if not data:
break
else:
print(data.decode('utf-8'))
s.close()
if __name__ == '__main__':
echo_client()
我们再来看开篇词提到的第二个问题:TCP的哪些机制保证了可靠性呢?
保证可靠性的机制有:校验和、建立连接时的三次握手和断开连接时的四次挥手、确认应答和序列号(ACK+SYN)、超时重传、流量控制、拥塞控制。
确认应答和序列号(ACK+SYN):三次握手建立连接的首要目的是同步序列号。只有同步了序列号才有可靠的传输。因此SYN 的全称即为 Synchronize Sequence Numbers。
超时重传:TCP 必须保证每一个报文都能够到达对方,它采用的机制就是:报文发出后,必须收到接收方返回的 ACK 确认报文,如果在一段时间内(称为 RTO,retransmission timeout)没有收到,这个报文还得重新发送,直到收到 ACK 为止。
流量控制:如果我们发送一个报文,收到 ACK 确认后,再发送下一个报文,发送每个报文都需要经历一个 RTT 时延,但是这样的发送方式速度非常慢。提速的方式很简单,并行地批量发送报文,再批量确认报文即可。然而当接受方性能不如发送方,或者系统繁忙资源紧张时,报文无法得到及时处理,只能被丢掉。因此引入了滑动窗口,即接收方把它的处理能力告诉发送方,限制其发送速度即可。
接收主机的处理能力很强时,也无法通过增加发送方的发送窗口来提升发送速度,因为网络的传输速度是有限的,它会直接丢弃超过其处理能力的报文。发送方只有在重传RTO时间超时后才会发现报文被丢弃然后重传报文。 然而解决这个问题办法便是拥塞控制。
拥塞控制:TCP拥塞控制是TCP协议的核心,它大致分为四个阶段:慢启动、拥塞避免、快速重传和快速恢复。
慢启动:TCP连接会穿过许多网络,由于不清楚网络传输能力,为了避免发送超过网络负载的报文,TCP 只能先调低发送窗口。让发送速度变慢是通过拥塞窗口(cwnd)实现的,它用于避免网络出现拥塞。拥塞窗口一开始是一个很小的值,然后每 RTT 时间翻倍。
接收方的处理能力同样会反馈给发送方,这个处理是通过 rwnd 来表示的。如果不考虑网络拥塞,发送窗口就等于对方的接收窗口,而考虑了网络拥塞后,发送窗口则应当是拥塞窗口(cwnd)与对方接收窗口(rwnd)的最小值。
导致慢启动阶段结束的3种场景:
当拥塞窗口的增长到达了慢启动阈值,很可能出现网络拥塞,为了避免拥塞,TCP 拥塞控制就进入了下一个阶段,拥塞避免。
拥塞避免:拥塞避免阶段,此时拥塞窗口不能再以指数方式增长,而是要以线性方式增长。
快速重传和快速恢复:TCP传输的是字节流,是有序的,这也就意味着当接收方收到不连续的报文时,就可能发生了报文丢失或者延迟。等待超时重传太耗时,这便到了快速重传和快速恢复的工作阶段。
当连续收到 3 个重复 ACK 时,发送方便得到了网络发生拥塞的明确信号,通过重复 ACK 报文的序号,我们知道丢失了哪个报文,这样,不等待定时器的触发,立刻重发丢失的报文,可以让发送速度下降得慢一些,这就是快速重传算法。
如何分析常见的TCP问题?
dstat检查工具
$ dstat
dstat显示了CPU、磁盘 I/O、 网络和内存的整体使用情况以及中断次数(int)和上下文切换次数(csw)两个关键指标。
针对TCP,可以用dstat --tcp查看相关指标
$ dstat --tcp
了解整体状况后,使用ss命令查看各个TCP连接:
我们能查看到每个 TCP 连接的状态(State)、接收队列大小(Recv-Q)、发送队列大小(Send-Q)、本地 IP 和端口(Local Address:Port )、远端 IP 和端口(Peer Address:Port)以及打开该 TCP 连接的进程信息。
查看系统的网络状态,比如说系统中是否存在丢包,以及是什么原因引起了丢包,这时候我们就需要 netstat -s或者它的替代工具 nstat。
详细内容我们将通过tcpdump抓包来看。
END