传输层笔记
Posted bdstravel
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了传输层笔记相关的知识,希望对你有一定的参考价值。
传输层里比较重要的两个协议,一个是 TCP,一个是 UDP。对于不从事底层开发的人员来讲,或者对于开发应用的人来讲,最常用的就是这两个协议。由于面试的时候,这俩个是会是一起被问到的
1 TCP 和 UDP 有哪些区别?
一般面试的时候我问这两个协议的区别,大部分人会回答,TCP 是面向连接的,UDP 是面向无连接的。什么叫面向连接,什么叫无连接呢?在互通之前,面向连接的协议会先建立连接。例如,TCP 会三次握手,而 UDP 不会。为什么要建立连接呢?你 TCP 三次握手,我 UDP 也可以发三个包玩玩,有什么区别吗?
2 UDP 协议
- 不保证不丢失,不保证按照顺序到达
- 基于数据报的,一个一个发,一个一个收
- 随时想发就发 无状态服务
3 UDP 包头
我们来看一下UDP 包头
一个包的传输过程,当我发送一个UDP包到达目标主机后,进行MAC地址匹配,发现匹配,摘下MAC地址头部,将剩下包传输给IP层的代码,摘下IP头,看目标IP是否匹配,如果匹配接下来数据报怎么发送?
由于IP头会存放8位协议,这里面会存放到底是TCP还是UDP 。这里是UDP,然后我们按UDP头的格式,就能从数据里面,将它解析出来,解析出来数据交给下一层去处理
处理完传输层的事情,内核的事情就基本完事,里面的数据交给应用程序去处理,一台机器会有好多个程序
无论是TCP和UDP 传数据,应用程序都会监听一个端口,也就是这个端口,用来区分应用程序,所以端口不能冲突,然后根据端口号,将数据交给响应的应用程序
通过下面图可以看到UDP 包头非常简单
4 UDP 特点
- 沟通简单,不需要三次握手四次断开
- 谁都可以传给他数据,他也可以传给任何人数据,甚至可以同时传给多个人数据
- 它不会根据网络的情况进行发包的拥塞控制,无论网络丢包丢成啥样了,它该怎么发还怎么发。
5 UDP 使用场景
第一,需要资源少,在网络情况比较好的内网,或者对于丢包不敏感的应用
前面说的通过PXE自动安装系统 下载使用TFTP ,就是基于UDP协议,占有资源少
第二,不需要一对一沟通,建立连接,而是可以广播的应用
UDP 的不面向连接的功能,可以使得可以承载广播或者多播的协议。DHCP 就是一种广播的形式,就是基于 UDP 协议的,而广播包的格式前面说过了。
在后面云中网络部分,有一个协议 VXLAN,也是需要用到组播,也是基于 UDP 协议的。
第三,需要处理速度快,时延低,可以容忍少数丢包,但是要求即便网络拥塞,也毫不退缩,一往无前的时候
UDP 简单、处理速度快,不像 TCP 那样,操这么多的心,各种重传啊,保证顺序啊,前面的不收到,后面的没法处理啊。不然等这些事情做完了,时延早就上去了。而 TCP 在网络不好出现丢包的时候,拥塞控制策略会主动的退缩,降低发送速度,这就相当于本来环境就差,还自断臂膀,用户本来就卡,这下更卡了。
当前很多应用都是要求低时延的,它们可不想用 TCP 如此复杂的机制,而是想根据自己的场景,实现自己的可靠和连接保证。例如,如果应用自己觉得,有的包丢了就丢了,没必要重传了,就可以算了,有的比较重要,则应用自己重传,而不依赖于 TCP。有的前面的包没到,后面的包到了,那就先给客户展示后面的嘛,干嘛非得等到齐了呢?如果网络不好,丢了包,那不能退缩啊,要尽快传啊,速度不能降下来啊,要挤占带宽,抢在客户失去耐心之前到达
6 基于UDP的例子
列举几种“城会玩”的例子。
6.1 网页或者app的访问
QUIC
(全称 Quick UDP Internet Connections,快速 UDP 互联网连接)是 Google 提出的一种基于 UDP 改进的通信协议,其目的是降低网络通信的延迟,提供更好的用户互动体验。
6.2 流媒体协议
现在直播比较火,直播协议多使用 RTMP,这个协议我们后面的章节也会讲,而这个 RTMP 协议也是基于 TCP 的。TCP 的严格顺序传输要保证前一个收到了,下一个才能确认,如果前一个收不到,下一个就算包已经收到了,在缓存里面,也需要等着,因而,很多直播应用,都基于 UDP 实现了自己的视频传输协议。
6.3 实时游戏
游戏有一个特点,要求实时性比较高,慢一秒可能就要被别人干掉或者爆头
游戏对实时要求较为严格的情况下,采用自定义的可靠 UDP 协议,自定义重传策略,能够把丢包产生的延迟降到最低,尽量减少网络问题对游戏性造成的影响
6.4 IoT 物联网
一方面,物联网领域终端资源少,很可能只是个内存非常小的嵌入式系统,而维护 TCP 协议代价太大;另一方面,物联网对实时性要求也很高,而 TCP 还是因为上面的那些原因导致时延大。Google 旗下的 Nest 建立 Thread Group,推出了物联网通信协议 Thread,就是基于 UDP 协议的。
6.5 移动通信领域
在 4G 网络里,移动流量上网的数据面对的协议 GTP-U 是基于 UDP 的。因为移动网络协议比较复杂,而 GTP 协议本身就包含复杂的手机上线下线的通信协议。如果基于 TCP,TCP 的机制就显得非常多余,这部分协议我会在后面的章节单独讲解。
7 TCP 协议
我们讲的 UDP,基本上包括了传输层所必须的端口字段。它就像我们小时候一样简单,相信“网之初,性本善,不丢包,不乱序”。
后来呢,我们都慢慢长大,了解了社会的残酷,变得复杂而成熟,就像 TCP 协议一样。它之所以这么复杂,那是因为它秉承的是“性恶论”。它天然认为网络环境是恶劣的,丢包、乱序、重传,拥塞都是常有的事情,一言不合就可能送达不了,因而要从算法层面来保证可靠性。
8 TCP 协议包头格式
我们先来看 TCP 头的格式。从这个图上可以看出,它比 UDP 复杂得多。
首先,源端口号和目标端口号是不可少的,这一点和 UDP 是一样的。如果没有这两个端口号。数据就不知道应该发给哪个应用。
接下来是包的序号。为什么要给包编号呢?当然是为了解决乱序的问题。不编好号怎么确认哪个应该先来,哪个应该后到呢。编号是为了解决乱序问题。既然是社会老司机,做事当然要稳重,一件件来,面临再复杂的情况,也临危不乱。
还应该有的就是确认序号。发出去的包应该有确认,要不然我怎么知道对方有没有收到呢?如果没有收到就应该重新发送,直到送达。这个可以解决不丢包的问题。作为老司机,做事当然要靠谱,答应了就要做到,暂时做不到也要有个回复。
TCP 是靠谱的协议,但是这不能说明它面临的网络环境好。从 IP 层面来讲,如果网络状况的确那么差,是没有任何可靠性保证的,而作为 IP 的上一层 TCP 也无能为力,唯一能做的就是更加努力,不断重传,通过各种算法保证。也就是说,对于 TCP 来讲,IP 层你丢不丢包,我管不着,但是我在我的层面上,会努力保证可靠性。
这有点像如果你在北京,和客户约十点见面,那么你应该清楚堵车是常态,你干预不了,也控制不了,你唯一能做的就是早走。打车不行就改乘地铁,尽力不失约。
接下来有一些状态位。例如 SYN 是发起一个连接,ACK 是回复,RST 是重新连接,FIN 是结束连接等。TCP 是面向连接的,因而双方要维护连接的状态,这些带状态位的包的发送,会引起双方的状态变更。
不像小时候,随便一个不认识的小朋友都能玩在一起,人大了,就变得礼貌,优雅而警觉,人与人遇到会互相热情的寒暄,离开会不舍地道别,但是人与人之间的信任会经过多次交互才能建立。
还有一个重要的就是窗口大小。TCP 要做流量控制,通信双方各声明一个窗口,标识自己当前能够的处理能力,别发送的太快,撑死我,也别发的太慢,饿死我。
作为老司机,做事情要有分寸,待人要把握尺度,既能适当提出自己的要求,又不强人所难。除了做流量控制以外,TCP 还会做拥塞控制,对于真正的通路堵车不堵车,它无能为力,唯一能做的就是控制自己,也即控制发送的速度。不能改变世界,就改变自己嘛。
作为老司机,要会自我控制,知进退,知道什么时候应该坚持,什么时候应该让步。
通过对 TCP 头的解析,我们知道要掌握 TCP 协议,重点应该关注以下几个问题:
- 顺序问题 ,稳重不乱;
- 丢包问题,承诺靠谱;
- 连接维护,有始有终;
- 流量控制,把握分寸;拥塞控制
9 TCP 三次握手
tcp的连接建立 常常称之为 三次握手,比如
A:hello 我是A
B: 收到A的helo ,然后说 我是B hello
A: 你好B
也就是说我们都要保证A,B双方的消息有去也有回,就基本可以了。
三次握手还沟通了一个重要事情,就是TCP包的序号的问题
A 要告诉 B,我这面发起的包的序号起始是从哪个号开始的,B 同样也要告诉 A,B 发起的包的序号起始是从哪个号开始的。为什么序号不能都从 1 开始呢?因为这样往往会出现冲突。
例如,A 连上 B 之后,发送了 1、2、3 三个包,但是发送 3 的时候,中间丢了,或者绕路了,于是重新发送,后来 A 掉线了,重新连上 B 后,序号又从 1 开始,然后发送 2,但是压根没想发送 3,但是上次绕路的那个 3 又回来了,发给了 B,B 自然认为,这就是下一个包,于是发生了错误。
因而,每个连接都要有不同的序号。这个序号的起始序号是随着时间变化的,可以看成一个 32 位的计数器,每 4 微秒加一,如果计算一下,如果到重复,需要 4 个多小时,那个绕路的包早就死翘翘了,因为我们都知道 IP 包头里面有个 TTL,也即生存时间。
好了,双方终于建立了信任,建立了连接。前面也说过,为了维护这个连接,双方都要维护一个状态机,在连接建立的过程中,双方的状态变化时序图就像这样。
10 TCP 四次断开
现在我们说一下四次断开,大家好聚好散,还是朋友
A: B 我们分手吧
B:好,我知道了,分手就分手
B:A,你好狠心, 分吧分吧,把你的照片还给你
A:好的B,照片收到,祝你幸福
状态时序图
最后等待2MSL 的原因
- 有可能A 最后发的ACK B没有收到,那么B会认为我上次给你发的FIN ACK包 A是不是没有收到,等待2MSL ,为了能够让B 再次重发FIN ACK包给A
- 避免端口被新应用占用,收到上个连接中B发过来的包,避免产生混乱
等待的时间设为 2MSL,MSL 是 Maximum Segment Lifetime,报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。因为 TCP 报文基于是 IP 协议的,而 IP 头中有一个 TTL 域,是 IP 数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减 1,当此值为 0 则数据报将被丢弃,同时发送 ICMP 报文通知源主机。协议规定 MSL 为 2 分钟,实际应用中常用的是 30 秒,1 分钟和 2 分钟等
还有一个异常情况就是,B 超过了 2MSL 的时间,依然没有收到它发的 FIN 的 ACK,怎么办呢?按照 TCP 的原理,B 当然还会重发 FIN,这个时候 A 再收到这个包之后,A 就表示,我已经在这里等了这么长时间了,已经仁至义尽了,之后的我就都不认了,于是就直接发送 RST,B 就知道 A 早就跑了。
11 TCP状态机
12 Socket
Socket 这个名字很有意思,可以作插口或者插槽讲。虽然我们是写软件程序,但是你可以想象为弄一根网线,一头插在客户端,一头插在服务端,然后进行通信。所以在通信之前,双方都要建立一个 Socket
12.1 Socket 模拟实现
在网络层,Socket 函数需要指定到底是 IPv4 还是 IPv6,分别对应设置为 AF_INET 和 AF_INET6。另外,还要指定到底是 TCP 还是 UDP。还记得咱们前面讲过的,TCP 协议是基于数据流的,所以设置为 SOCK_STREAM,而 UDP 是基于数据报的,因而设置为 SOCK_DGRAM
12.2 基于 TCP 协议的 Socket 程序函数调用过程
我们可以现看一段代码实现
#通过一段代码模拟socker server服务端
#! /usr/bin/python
# -*- coding: utf-8 -*-
import socket
sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM) #用的是ipv4 并且是tcp
server_address = ("127.0.0.1",12345)
print "Staring up on %s:%s" % server_address
sock.bind(server_address)
sock.listen(5)
while True:
print "waiting .........."
connetion,client_address = sock.accept()
try:
print "Connection from ",client_address
data = connetion.recv(1024)
print "Receive %s" % data
finally:
connetion.close()
一般是先调用bind 函数,给这个Socket赋予一个IP地址和端口
然后调用listen函数进行监听。在 TCP 的状态图里面,有一个 listen 状态,当调用这个函数之后,服务端就进入了这个状态,这个时候客户端就可以发起连接了。
在内核中,为每个 Socket 维护两个队列。一个是已经建立了连接的队列,这时候连接三次握手已经完毕,处于 established 状态;一个是还没有完全建立连接的队列,这个时候三次握手还没完成,处于 syn_rcvd 的状态。
接下来,服务端调用 accept 函数,拿出一个已经完成的连接进行处理。如果还没有完成,就要等着。
客户端代码实现
#! /usr/bin/python
# -*- coding: utf-8 -*-
import socket
sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
def check_tcp_status(ip,port):
sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM) #ipv4 tcp协议
server_address = (ip,port)
print "connecting to %s:%s" % server_address,port
sock.connect(server_address) #connect方法
message = raw_input("pleas input: ")
print "Sending %s" % message
sock.sendall(message)
print "Closing socket"
sock.close()
if __name__ == "__main__":
print check_tcp_status("127.0.0.1",12345) #要访问的服务端ip和端口
在服务端等待的时候,客户端可以通过 connect 函数发起连接。先在参数中指明要连接的 IP 地址和端口号,然后开始发起三次握手。内核会给客户端分配一个临时的端口。一旦握手成功,服务端的 accept 就会返回另一个 Socket。
这是一个经常考的知识点,就是监听的 Socket 和真正用来传数据的 Socket 是两个,一个叫作监听 Socket,一个叫作已连接 Socket。
连接建立成功之后,双方开始通过 read 和 write 函数来读写数据,就像往一个文件流里面写东西一样。
说 TCP 的 Socket 就是一个文件流,是非常准确的。因为,Socket 在 Linux 中就是以文件的形式存在的。除此之外,还存在文件描述符。写入和读出,也是通过文件描述符。
在内核中,Socket 是一个文件,那对应就有文件描述符。每一个进程都有一个数据结构 task_struct,里面指向一个文件描述符数组,来列出这个进程打开的所有文件的文件描述符。文件描述符是一个整数,是这个数组的下标。
这个数组中的内容是一个指针,指向内核中所有打开的文件的列表。既然是一个文件,就会有一个 inode,只不过 Socket 对应的 inode 不像真正的文件系统一样,保存在硬盘上的,而是在内存中的。在这个 inode 中,指向了 Socket 在内核中的 Socket 结构。在这个结构里面,主要的是两个队列,一个是发送队列,一个是接收队列。在这两个队列里面保存的是一个缓存 sk_buff。这个缓存里面能够看到完整的包的结构。看到这个,是不是能和前面讲过的收发包的场景联系起来了?
12.3 基于UDP协议Socket 程序函数调用过程
对于 UDP 来讲,过程有些不一样。UDP 是没有连接的,所以不需要三次握手,也就不需要调用 listen 和 connect,但是,UDP 的交互仍然需要 IP 和端口号,因而也需要 bind。UDP 是没有维护连接状态的,因而不需要每对连接建立一组 Socket,而是只要有一个 Socket,就能够和多个客户端通信。也正是因为没有连接状态,每次通信的时候,都调用 sendto 和 recvfrom,都可以传入 IP 地址和端口
12.4 服务器连接数
我们先来算一下理论值,也就是最大连接数,系统会用一个四元组来标识一个 TCP 连接
本机IP, 本机端口, 对端IP, 对端端口
服务器的最大 TCP 连接数 = 客户端 IP 数×客户端端口数。对 IPv4,客户端的 IP 数最多为 2 的 32 次方,客户端的端口数最多为 2 的 16 次方,也就是服务端单机最大 TCP 连接数,约为 2 的 48 次方
当然,服务端最大并发 TCP 连接数远不能达到理论上限。首先主要是文件描述符限制,按照上面的原理,Socket 都是文件,所以首先要通过 ulimit 配置文件描述符的数目;另一个限制是内存,按上面的数据结构,每个 TCP 连接都要占用一定内存,操作系统是有限的
12.5 如果优化程序资源
- 多进程
- 多线程
- IO 多路复用,一个线程维护多个 Socket
- IO 多路复用,epoll函数事件通知机制
epoll 被称为解决 C10K 问题的利器
有个 C10K,它的意思是一台机器要维护 1 万个连接,就要创建 1 万个进程或者线程,那么操作系统是无法承受的。如果维持 1 亿用户在线需要 10 万台服务器 ,成本比较高,通过epoll 事件callback机制可以解决
13 TCP 保证可靠关键
累计确认: TCP为了保证顺序性,会给每一个包起始一个ID,在建立连接的时候,会计算起始ID是什么,然后按照ID顺序,一个一个发送,为了保证不丢包,对于发送的包要应答,接收端收到包后,不是一个一个应答,而是应答某个之前包的ID,表示包我都收到。这种模式叫做累计应答
或者累计确认
13.1 滑动窗口
在 TCP 里,接收端会给发送端报一个窗口的大小,叫 Advertised window
为了记录所有发送的包和接收的包,TCP 也需要发送端和接收端分别都有缓存来保存这些记录。发送端的缓存里是按照包的 ID 一个个排列,根据处理的情况分成四个部分
发送端缓存里记录的内容
- 第一部分:已经发送并且已经确认的包。
- 第二部分:已经发送但是尚未确认的包。
- 第三部分:尚未发送但是马上准备发送的包。
- 第四部分:尚未发送但是暂时不准备发送的包。
接收端缓存里记录的内容
- 第一部分:接受并且确认过的。也就是我领导交代给我,并且我做完的。
- 第二部分:还没接收,但是马上就能接收的。也即是我自己的能够接受的最大工作量。
- 第三部分:还没接收,也没法接收的。也即超过工作量的部分,实在做不完。
13.2 顺序问题与丢包问题
超时重试: 对于每一个发送的包,还没有进行ACK包确认的,都设置一个定时器,超过时间就重新尝试,时间必须大于往返时间RTT,否则也会引起不必要重传,也不宜过长,时间变长,那么访问时间就会变慢了
超时间加倍: 每当遇到一次超时重传的时候,都会将下一次超时时间间隔设为先前值的两倍。两次超时,就说明网络环境差,不宜频繁反复发送。
快速重传的机制:
第一种办法:
当接收方收到一个序号大于下一个所期望的报文段时,就会检测到数据流中的一个间隔,于是它就会发送冗余的 ACK,仍然 ACK 的是期望接收的报文段。而当客户端收到三个冗余的 ACK 后,就会在定时器过期之前,重传丢失的报文段
第二种办法:
Selective Acknowledgment (SACK)
: 这种方式需要在 TCP 头里加一个 SACK 的东西,可以将缓存的地图发送给发送方。例如可以发送 ACK6、SACK8、SACK9,有了地图,发送方一下子就能看出来是 7 丢了。
13.3 流量控制问题
发送端发送的每一个数据包,服务端都要给一个确认包(ACK).确认它收到了。 服务端给发送端发送的确认包(ACK包)中,同时会携带一个窗口的大小。
这个窗口的大小就代表目前服务器端的处理能力。(接收端最大缓存量-接收已确认但还未被应用层读取的部分)。 这个窗口的大小也是时时刻刻在变化的,可能接收方再发送数据包4的ACK时,窗口大小为9。
此时应用层的程序疯狂去接收已接收并且已确认的缓存,没准接收方再发送数据包5的ACK时,窗口的大小就变为了14了呢。
还要注意: 当接收方比较慢的时候,要防止低能窗口综合征,别空出一个字节来就赶快告诉发送方,然后马上又填满了,可以当窗口太小的时候,不更新窗口,直到达到一定大小,或者缓冲区一半为空,才更新窗口。
13.4 拥塞控制问题
也是通过窗口的大小来控制的,前面的滑动窗口 rwnd 是怕发送方把接收方缓存塞满,而拥塞窗口 cwnd,是怕把网络塞满。
这里有一个公式 LastByteSent
- 一个公式 LastByteSent - LastByteAcked <= min cwnd, rwnd ,是拥塞窗口和滑动窗口共同控制发送的速度。
拥塞控制作用: 就是在不堵塞,不丢包的情况下,尽量发挥带宽。
指数性的增长,线性增长
我们知道TCP通过一个timer采样了RTT并计算RTO,但是,如果网络上的延时突然增加,那么,TCP对这个事做出的应对只有重传数据,但是,重传会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包,于是,这个情况就会进入恶性循环被不断地放大。试想一下,如果一个网络内有成千上万的TCP连接都这么行事,那么马上就会形成“网络风暴”,TCP这个协议就会拖垮整个网络。这是一个灾难。
所以,TCP不能忽略网络上发生的事情,而无脑地一个劲地重发数据,对网络造成更大的伤害。对此TCP的设计理念是:TCP不是一个自私的协议,当拥塞发生的时候,要做自我牺牲。就像交通阻塞一样,每个车都应该把路让出来,而不要再去抢路了。(关于拥塞控制的论文请参看《Congestion Avoidance and Control》(PDF)
拥塞控制主要是四个算法:
- 1)慢启动;
- 2)拥塞避免;
- 3)拥塞发生;
- 4)快速恢复。
这四个算法不是一天都搞出来的,这个四算法的发展经历了很多时间,到今天都还在优化中。
备注:
- 1988年,TCP-Tahoe 提出了 1)慢启动,2)拥塞避免,3)拥塞发生时的快速重传
- 1990年,TCP Reno 在Tahoe的基础上增加了4)快速恢复
13.5 TCP BBR 拥塞算法
解决拥塞控制带来的俩个问题:
第一个问题是: 丢包并不代表着通道满了,也可能是管子本来就漏水。例如公网上带宽不满也会丢包,这个时候就认为拥塞了,退缩了,其实是不对的。
第二个问题是: TCP 的拥塞控制要等到将中间设备都填充满了,才发生丢包,从而降低速度,这时候已经晚了。其实 TCP 只要填满管道就可以了,不应该接着填,直到连缓存也填满。
为了优化这两个问题,后来有了 TCP BBR 拥塞算法。它企图找到一个平衡点,就是通过不断地加快发送速度,将管道填满,但是不要填满中间设备的缓存,因为这样时延会增加,在这个平衡点可以很好的达到高带宽和低时延的平衡。
以上是关于传输层笔记的主要内容,如果未能解决你的问题,请参考以下文章