udp和tcp作为传输层的两大重要协议,是众多学习网络编程者不可错过的学习内容,协议的概念想必不用再过多解释,即程序员和程序员之间进行网络通讯时的标准,那么经历了应用层,也就是肉眼能看到、用户能直接操作的层,接下来是传输层,所谓传输,就是管理client和server如何中的数据是如何传输,怎么传输,所以这两个协议也是管理这些的。
1.端口号
端口号标识了一个主机上进行通信的不同应用程序。也就是说数据从对端主机应用层的某一个端口自顶向下传来,经过应用层、传输层、数据链路层、物理层的层层打包,再经过路由的路径选择,一步一步到达本主机,再经历一遍刚刚的过程,不过要反过来,经过物理层、数据链路层、传输层、应用层的层层解包,最终被应用层的协议读到正文数据,而也是被某个端口读到的,所以我们不难发现端口就好像是一个机器上的一个个小部门,也可以负责数据的通讯。
而为了好好管理这些可以通讯的端口,可以让它们有序、正确的为我们服务,即收发数据,将为这些端口编号,经常使用的那些端口也就成为大家耳熟能详的了,比如http协议的默认端口号8080。
上图为各个应用层协议及每个协议默认的端口号,而下层就是传输层+数据链路层,所以不难发现ip地址+端口号就可以确定某个应用。
那么端口号的划分返回为0~1023,知名的应用层协议由于为了用户使用的方便性,不可轻易改变,确定后要一直使用,所以HTTP,SSH,FTP等这些广为人知的应用层协议,它们的端口号都是固定的。而1024~65535,这些端口号为操作系统动态分配的端口号。客服端程序的端口号,就是由操作系统从这个范围自动分配的。
所以我们来回答两个问题:
①一个进程是否可以bind多个端口号?
可以。我们可以将应用层的服务当作一个个进程,只要经过此端口访问到的进程都是指向同一个进程,不会造成差错。
②一个端口号是否可以bind多个进程?
不可以。一个端口号只能绑定一个进程,如果一个端口号绑定了多个进程,那么用户经过唯一指定的ip路径+端口号访问到的资源将不一致,服务与服务之间会造成冲突。
2.netstat -nltp指令
在Linux系统中,使用netstat可以查询到本机上正在运行的网络服务,并可以查询到它们使用的是什么协议,端口号是什么,目前所处的状态等。
而通常会在netstat后跟上-nltp,n(number)代表能显示成数字就显示成数字,不带n就按照文件名显示。l(listen)代表查询监听状态下的套接字,不带l就查询已经建立好的。t(tcp)代表查询使用tcp协议的套接字进程。t也可以替换成u(udp),也就是使用udp协议的套接字进程。p(process)代表这些网络套接字本质都是进程。
3.udp协议
已经了解到udp协议是传输层的两大主要协议之一,那么既然是管理传输的,自然而然会对其传输的细节产生好奇,数据究竟是如何交付给上层的?谁在管理?传输的格式是怎样的?了解这些问题之前,需要先来看一下udp协议的报头:
udp协议的报头由三部分五部分构成,源端口号和目的端口号代表此数据从对端主机的哪个端口来,要到本机的哪个端口去。16位udp长度代表此数据包总共有多少个字节,由于协议的报头已经占了8个字节,所以此长度最小就为8,再有就是数据的长度。16位校验和和tcp的校验和功能相同,主要是做一些校验工作,不必过多了解。
由于Linux内核是C语言写的,那么从C语言的角度看,udp报头又可以用一个熟悉的方式来表示:
struct udp_hdr
{
uint32_t src_port:16;
uint32_t dst_port:16;
uint32_t total:16;
uint32_t check:16;
}
了解过udp协议报头后,上述的问题也就可以回答了,数据是如何做到封装和解包的?答:将报头和有效载荷分离。数据如何做到向上交付?答:根据目的端口号,交付给上层应用。
4.udp协议的传输特点
udp协议在传输网络数据时类似于寄信,我们都知道平时在写信给别人时信尽可能多写,因为寄一次信并不容易,尽可能将想表达的内容写出来,对方读到的也是完整的信件内容,如果还有没有表达完的内容,中间就算丢失了,产生损坏了,送信的人根本不管,所以udp协议下数据的传输没有可靠性。
所以udp协议的特点:①无连接:知道对端ip+port和本端的ip+port就直接传输,不需要建立连接。②不可靠:没有确认机制,没有重传机制,如果因为网络问题造成该报文无法发送给对方,udp协议也不会给应用层发送任何错误信息,换而言之发生错误后用户根本无法得知。③ 面向数据报:不能够灵活地控制读写的次数和数量,类似上面讲的送信机制,送多少次收多少次,读就读完整报文,不能多收或少收,即不能多读数据也不能少读数据。
5.面向数据报
应用层交给udp多长的报文,udp原样发送,既不会拆分,也不会合并。
如果发送端调用一次send发送100个字节,那么接收端也必须对应的调用一次recvfrom接收这100个字节的数据,而不能循环调用recvfrom,更不能自己选择接收的字节数,如果不想产生丢包问题,就必须要调用,且一次就要读完。
所以可以体会到,貌似数据的传输不归应用层管,也不归用户关,看似我们将数据发送给对方,可是数据要在下层经历层层打包解包,而数据何时发、怎么发、发多少,这个问题将全权交给传输层协议来管理。
6.udp协议的缓冲区
udp协议并没有真正意义上的发送缓冲区,由于udp协议面向数据报的特点,调用send函数会将数据交给内核,由内核将数据传给网络层协议进行后续的传输动作。
udp有接收缓冲区,但是这个接收缓冲区不能保证接收udp报文和发送udp报文的顺序一致,如果缓冲区满了,再到达udp的数据就会被丢弃。
udp协议既可以读也可以写,send和recvfrom可以同时调用,属于全双工协议。
7.基于udp的应用层协议
NFS:网络文件系统、TFTP:简单文件传输协议、DHCP:动态主机配置协议、BOOTP:启动协议(用于无盘设备启动)、DNS:域名解析协议
8.TCP协议
了解过udp的传输特点和机制后,进入到传输层应用范围最广的tcp协议,为什么说tcp如此适用呢?因为它弥补了许多udp做不到的功能,传输数据更加安全可靠,更适合用户传输数据。首先还是先来看tcp协议的报头:
相比udp协议,tcp协议增加了许多报头数据,udp报头数据只占8字节,而tcp报头要占20字节,如果加上选项,则要占60字节,当然选项是可选择的,并不作为了解重点,而其他部分则是学习tcp的重点了。
9.4位首部长度
作为tcp协议报头打头阵的数据,用来表示tcp报头的长度,只有4位,范围:0000~1111,代表0~15,单位为4字节,所以最大值为正好为60,也就是tcp报文的最大长度,那么如果不用选项的话,只有20字节,用20除以4等于5,也就是4位首部长度,所以4位首部长度经常用0101来填写,如果tcp协议带选项,那么选项的长度就应该是4位首部长度的值减去标准长度的值。
10.tcp协议确认应答机制
之前说过tcp协议弥补了udp协议不可靠的特点,那么是哪些机制使得tcp协议更加可靠呢?其一就是tcp独有的确认应答机制,我们知道udp协议是面向数据报的,收发数据不会进行任何报告,这样就会导致用户和操作系统根本不知道数据有没有正常收到,有没有正常发送,而tcp协议的确认应答机制顾名思义就是基于序号对已发数据和接收数据进行应答,通过应答来保证上一条数据已经被对方读到了。
tcp协议并不是百分之百可靠的,但只要一条消息有应答就代表该消息百分之百被对方收到了,所以tcp的确认应答机制是常规可靠的。
所以序号和确认序号,就是针对对方发来的数据进行排序,依次带上编号进行应答,是对历史确认报文的序号+1,收到确认应答的tcp报文之后,可以通过确认序号来辨别是对哪个报文的确认。
例如发送了13号,代表13号之前的报文已经全部收到,下次就可以从13号以后的报文开始发送。
那如果client端发送了3000个报文,但是0~1000,2000~3000server都受到了,但是1000~2000的报文丢失了,那么确认序号该怎么发送呢?答:发送2000!并不会因为丢失了中间数据而忘记它,这也是tcp协议可靠的代表性之一。
无论是确认应答还是正式的正文的报文,都是完整的tcp报文,也就是说即使是给对方发送确认序号,发送的也是一个完整的tcp报文。
那么可以发现这个序号有两个,既然只负责确认,这个序号可以只有一个吗?不可以,因为tcp协议同udp协议一样为全双工协议,既要负责给对方发数据也要负责收数据,互相确认,这样的动作可以同时进行。双方通信时,一个报文既可以携带要发送的数据,也可能携带对历史报文的确认。
11.tcp协议的缓冲区
udp协议的缓冲区相较tcp协议有很大的区别,此区别由于它们本身协议的特性决定,因为udp协议是面向数据报的,tcp协议是面向字节流的,即udp发送报文要么不发,要么一次性发完,而tcp协议则是由缓冲区决定,我们又知道传输数据的机制完全交由传输层决定,发多少、怎么发,完全交给tcp协议管理,所以tcp协议相较udp很明显的区别之一:由缓冲区来管理发送数据的多少。
tcp协议是自带发送缓冲区和接收缓冲区的(malloc的两端空间)
write/read:与其叫做发送接收接口,不如理解成拷贝函数应用层进行send,并不是把数据发送到网络上,而是把数据拷贝到tcp的发送缓冲区。
了解过这些我们肯定又对“数据发送并不受用户控制”这个概念有了更加深刻的理解,所以我们在发送数据时,是应用层将数据拷贝给tcp的发送缓冲区,由tcp来做决定发送的时间,发送多少的决策,而这些决策又是由众多外界因素决定,例如对方接收缓冲区的大小、网络状况等等。而对端也是一样,接收缓冲区接收到了对方发来的数据,不会立即交付上层,而是暂存在接收缓冲区,再由应用层使用read等函数向上读取。
12.16位窗口大小
知道tcp协议在收发数据时是有缓冲区这个概念后,接着引出窗口这个概念,因为tcp协议收发数据是面向字节流的,而字节流和数据包明显的不同就是,数据报不发则已,要发就全发,而字节流发与不发,发的多与少统统由tcp协议决定,所以tcp一定要有一个工具是用来管理字节流的收发,这就是缓冲区的作用,而发多少,收多少,这就是由窗口大小来决定了。
在应答报文中,可以在报头上填上自己的接收缓冲区的剩余大小,换句话说也就是接收能力,并通过对方缓冲区的剩余大小来决定接下来的发送速度和发送多少。
我们知道数据的接收和发送并不接受用户的控制,而是全权交给tcp来决定,换句话说我们肉眼所看到的“数据已经发出”,或者“数据已经收到”,其实不然,虽然看似数据已经发出,但是其实数据是存在tcp的发送缓冲区中,而tcp在等待很多东西,比如对方缓冲区的窗口大小,网速问题等,而如果接收方的窗口大小不够存正在处于发送端发送缓冲区的数据,则先不会发送,会通过下文即将要介绍的标志位中的PSH报文,通知对方上层尽快接收处于接收缓冲区的数据,发送端还有数据没有发完,所以窗口大小是和缓冲区相辅使用的。
如果一方不断发数据的过程中,另一方来不及接收,那么数据就只能被丢弃。
13.6位标志位
tcp协议站在发送和接收的角度是面向字节流的,因为它是按照缓冲区的大小和接受能力,将数据(在底层其实也就是字节流)依次放在缓冲区中,但是站在大体角度来说,与udp不同的是,udp只有收发数据具有明显特点,所以udp协议叫做面向数据报,而tcp站在不同的角度是会有不同的特点的,站在收发数据的角度tcp是面向字节流,而如果站在tcp建立链接的角度来看,tcp协议则是面向链接的!
什么是面向链接,即链接之于tcp协议来说非常重要,可以说tcp协议的主要构成就是链接,通过许多接收端和发送端的链接来完成数据的传送。
在编写网络套接字时,socket通信前要先调用connect函数,这就是建立链接的过程,如何建立链接?链接是如何一步一步建立好的?即tcp的三次握手!
①tcp的三次握手
在任何时刻,在已经建立好链接的双方,都会有成千上百的数据被发送,而发数据之前最重要的就是确保收发双方主机的健康情况,不确定这些,也就不会给收发数据的双方提供保障。
而之前说过,tcp报文不论是携带正文与否,都是一个正常的tcp报文,那么之于server而言,如何区分这些报文,究竟是要建立链接的报文,还是要和我正常通信的报文,就是sever端的一大问题了,即采用的是6位标志位来区分。
那么先来介绍这6个标志位分别代表什么吧:
1.ACK:表明发送的报文就是一个确认的报文,而并不是链接断开/请求的报文。
2.SYN:表明发来的是一个链接请求的报文,要进行三次握手。
3.RST:重制链接异常
4.PSH:告知对方尽快将接收缓冲区的数据进行向上交付。
5.URG:表明该报文中携带了紧急数据,需要被优先处理(要结合16位紧急指针来处理)。
6.FIN:表明发来的是一个链接断开的报文,要进行四次挥手。
所以当tcp要进行三次握手时,需要一方先发送SYN,表明想和对方建立链接,而对方收到请求后,也需要给对端发送回对于本次申请的回应,并且也向对方申请建立链接的请求,也就是ACK+SYN,前面的ACK是针对第一次SYN的回应,第二个SYN是向申请方同样申请建立链接,而第一次申请的主机由于自己的请求对方已经收到并且也收到了相同的建立链接的请求,所以也要给对方回应ACK,代表了对第二个SYN的回应,至此,三次握手已经完成,代表双方主机已经建立完成链接,可以正常通信了。
其实看似是三次握手,其实是四次,只不过第二次将SYN+ACK合起来使用。
而之前提到过,即使是链接请求的报文,之于tcp的角度,也和正常携带数据的报文一般无二,而发送数据是有成本的,最简单而言,要为其创建管理相关报文的空间,而且管理它们也是有成本的,要为其在内核创建相关的数据结构,维护双方的链接是有成本的(时间+空间)。
所以要想攻击某个服务器时,可以冒充client端对其不停的发送SYN,发送完后并不接收server端的ACK,也就是说server要一直为我建立着链接,但是三次握手一直处于待完成的阶段,一直占用着server端的内存,消耗其内部资源。
但是实际情况是如果client端发送大量的SYN时,server端并不会认为链接已经建立完成,也就不会发回SYN+ACK,所以此时server端不会为此套接字创建数据结构,server端的资源也就不会被浪费。
②tcp的四次挥手
明白了建立,接下来了解链接断开的过程成本就会小很多了,也就是tcp的四次挥手,那么四次挥手的过程也是由收发双方共同完成,以client为例,先发送FIN请求链接断开,server发送ACK,再由server端继续发送FIN,最后由client发送ACK,完成四次挥手。
其实不难发现,三次握手看似是三次,其实是四次,而四次挥手看似是四次,其实也可以像三次挥手一样当成三次处理。
所以之于tcp协议角度的三次握手和四次挥手也是tcp自动完成的,用户层完全不参与,用户的发送行为,完全不会影响tcp的收发逻辑。
所以为什么是三次握手呢?为什么不是两次/一次?有两个理由:①确认双方主机是否健康。②验证全双工,三次握手可以看到双方收发数据的最小次数。
说完链接建立和断开的过程,标志位其实已经了解了三个了,其他的例如RST,意为在发送报文的过程中,如果client已经发送,但是server端等待了很长时间都没有收到,就会给client端发送一个RST,代表链接产生异常,已经很久没有收到你的数据了,可能产生网络问题、数据丢失等问题。
14.超时重传
当发送完对应的报文,对方主机没有ACK,那么对方就一定没有收到对应的数据吗?可能是数据真的丢失了,也可能是主机A没有收到应答,但是总而言之主机A都会认为是数据丢失了。
①那么如果数据丢失了,tcp协议将会采用超时重传,也就是结合RST标志位,向对方主机发送数据异常的标志,一定时间后要求会重传数据包,对方也就收到了。
②那么如果是应答ACK丢失了,主机B再重传一份,主机A可能会收到重复数据,收到重复数据之于可靠的tcp协议来说也是不可靠的。
那么如何避免收到重复数据呢:按照序号去重。
网络都是虚拟的,网络快慢也是不断变化的,所以网络通信的效率肯定是变化的,发送的数据得到的应答的时间也是浮动的,超时重传的时间一定也是浮动的。
在最理想的情况下,找到一个最小时间,保证“确认应答一定能在这个时间内返回”,但是这个时间的长短,随着网络情况的变化,是有差异的,如果超时时间设的太长,会影响整体的重传效率,但如果设置的太短,有可能会频繁发送重复的包。
所以tcp协议为了保证无论在任何环境下都能比较高性能的通信,因此会动态计算这个最大超时时间。Linux系统中超时以500ms为一个单位进行计算,每次判定超时重发的时间都是以500ms的整数倍。如果重发一次之后,仍然得不到应答,等待2*500ms再进行重传,下一次则是4*500ms,以此类推。如果累积到一定重传次数,tcp协议会认为网络或对端主机出现异常,会强制关闭链接。
15.TIME_WAIT状态
请求/断开链接的本质:双方达成链接都应该请求/断开的共识,就是一个通知对方的机制。
三次握手和四次挥手是协商请求/断开链接的最小次数。
那么经过四次挥手,主动断开链接的一方要进入TIME_WAIT状态,即代表等待对方确认,等待两个MSL的时间后回到CLOSE_WAIT状态。
MSL代表了报文在网络中的最大生存时间,那么为什么是2MSL呢?①尽量保证历史发送数据在网络中消散(在发送最后一个ACK之前,双方已经进入到最后一次挥手阶段,历史数据不会增多,其存活时间最多为2MSL)。②尽量保证最后一个ACK被对方收到。
所以之前的一个端口只被一个进程绑定就不难理解了。
16.滑动窗口
在tcp协议的收发缓冲区中,由于数据的发送数量和时间全权交由tcp协议决定,所以如何管理收发是很大的问题,除了确认序号外,滑动窗口也是管理tcp收发数据的另一大机制。
窗口代表了在收发缓冲区中的数据并不是一次性发送,而是处于窗口中的数据可以发送,但是收发的数量由滑动来决定,当数据发送一些后,窗口的起始向后移动,窗口的末尾向后移动,也就模拟了动态的滑动过程,也就是窗口越大,网络的吞吐量就越大。
例如,win_start += 确认序号,代表了已经发送了的数据,win_end += win,win代表了对方接收缓冲区的大小,总体代表了后续可以发送的数据。
17.快重传和超时重传
我们都知道丢包这个概念,即在网络状态不稳定的过程中,数据经过网络的传送可能会发生丢失的情况,那么tcp协议针对这种情况也有独有的处理手段,一般情况,丢失数据会分为这两种情况:
情况一:数据包已经到达,但是ACK丢失了
这种情况不必担心,因为可以通过后序ACK的补发进行确认。
情况二:数据包直接丢失
如果数据包直接丢失,那么确认序号则会在此断开,例如此次发送的数据序号位1000,则对端发送的确认序号将会是1001,即使2000~9000的数据都已经安全到达,对端还是依旧会发送1001来提醒发送端1000号报文丢失了,那么对端将会发送3个同样的确认应答序号,在发送端收到这3个序号后,则发送端得知数据已经丢失,那么则会采取重传的机制将数据重新补发。
这种机制就叫做“快重传”。
那么快重传和超时重传有什么区别呢?快重传在场景中采用的概率极高,而且速度也比超时重传快很多,但是即使是这样,也不能取消超时重传,超时重传代表了收发双方其中某一方已经很长时间没有回应了,作为兜底的重发机制!
18.流量控制