网络原理基础概念

401 阅读41分钟

基础概念

OSI七层网络模型?TCP/IP协议族四层网络模型?

OSI七层:物理层,链路层,网络层,传输层,会话层,表达层,应用层。

TCP/IP协议:链路层,网络层,传输层,应用层。后续我们的介绍和讨论都是基于TCP/IP协议族。

链路层

链路层的职责是什么?

链路层通常包括操作系统中的设备驱动程序和计算机中对应的网络接口卡,负责处理与电缆的物理接口细节。在TCP/IP协议,主要为上层IP模块发送数据和接收数据。 为ARP模块和RARP模块发送请求和应答。在TCP/IP协议中,ARP,RARP,IP都属于网络层。

网络层

什么是ARP协议?

ARP协议,地址解析协议。它是用于将IP地址转换成对应主机的MAC地址(以太网接口硬件地址)。机器之间在硬件层次上进行数据帧的交换必须有正确的接口地址。 所以内核在发送数据之前必须知道目的端的硬件地址才能发送数据。在局域网LAN如果目的主机在同一网络,则会发ARP广播,目的主机收到广播消息后,会返回对应的硬件地址。 返回的硬件地址会被存在arp高速缓存中。下一次则无需再发广播获取。如果目的IP不是在局域网LAN那又如何?首先比较混淆的一点是ARP获取的目的主机MAC地址,这里的目的主机并不是指 目的IP地址对应的主机。这就涉及到IP路由选择的概念,如果目的IP选择默认的下一站路由,则IP数据报会先发送给下一站路由再由其转发。 那么通过ARP获取的就是这个下一站路由IP对应的MAC地址。我们可以通过arp -a命令查看当前arp高速缓存。通过tcpdump arp我们可以查看arp的请求和应答。

什么是RARP协议?

既然有地址解析协议,就有RARP逆地址解析协议。发送主机发送RARP广播,收到广播的RARP服务器给源主机发送包含IP地址的响应。现在这个协议已过时,取而代之的是DHCP协议。

什么是IP协议?

我们可以从很多书中看到这样的定义:IP协议是TCP/IP协议族的网络层协议,提供不可靠无连接无顺序的数据报传输服务。这个定义其实已经把协议的特点讲得很清楚。 不可靠指的是IP数据报并不保证到达目的主机。无连接指的是,IP层不会维护任何关于数据报的状态信息,每个数据报处理相互独立。无顺序指的是因为每一个数据报都是独立处理, 而且传递过程是逐跳传递,每一个数据报的路由选择都可能不同,甚至有的可能中途丢弃,所以数据报到达目的主机的顺序是无法保证的。

IP数据报数据结构?

鉴于目前大部分还是IPv4,而且把完整的TCP/IP学完再回头看IPv6相关也是触类旁通,所以后续的介绍我们都基于IPv4。IP数据报数据结构,网上或相关书籍都有图, 这里不再展示,我们在脑海中想象即可。

4位协议版本号+4位首部长度+8位服务类型+16位数据报总长度。这里我们就有一个疑惑,首部长度才4位,首部长度最多只能15字节吗?实际首部长度这里指的是32字长即4字节。 也就是首部长度最多可以有60字节。且首部长度必须是4字节的整数倍对齐。8位服务类型,前3位表示优先级,第4位为0,后面4位标志分别代表最小时延, 最大吞吐量,最高可靠性,最小费用,依赖具体实现。16位数据报总长度,也就是IP数据报最长可以有65535字节。通过首部长度和总长度,就可以定位内容的起始位置。

16位标识+3位标志+13位片偏移。这三个字段主要是完成数据报分片作用。我们前面直到IP数据报最多可以有65535字节,那意味着我们能发这么大的数据报吗? 以太网对数据帧长度有最大的限制为1500字节,这就是MTU最大传输单元,数据报长度大于MTU则需要进行分片。到达目的主机后再进行重组。 那么如何分片在目的主机才能明确知道怎么重组呢。16位标识标识分片属于同一个数据报。13位片偏移则表明分片的数据对应在数据报的哪个位置。13位片偏移的单位是8字节。 3个标志,分别是0,DF,MF。DF=0,表示可以分片,MF=1表示后面还有分片,MF=0表示分片是结尾分片。如果数据报在中间路由器需要分片而DF又设置为1,则数据报会被丢弃。 每个设备都有自己的MTU,那么数据传输过程中间可能也会进行分片,那么尽量避免分片就需要知道到达目的主机的最小路径MTU。通过设置DF=1,中间路由器如果需要对IP数据报 进行分片,而DF又设置禁止分片,则会返回ICMP不可达错误,带上当前路由器的MTU。源主机把路径MTU设置为该MTU后重发,以此类推。

8位生存时间ttl+8位上层协议+16位首部检验和。ttl每经过一个路由器就减1,当为0时,则丢弃该数据报并发回一份ICMP超时报文。上层协议则指明了数据 部分应该交给哪个协议处理。16位首部检验和则是为了避免数据报有损坏。数据报发出时,首先将检验和设置为0,然后将首部按16位取反相加,保存结果到 首部检验和。当收到数据报时,通过把首部按16位取反相加,等于自己加上自己的取反。结果全为1表示检验通过,否则即有损坏丢弃该数据报。

32位源IP地址+32位目的IP地址+可选项。

内容部分。承载如ICMP报文,TCP报文,UDP报文。

IP地址有几类?

IP地址格式为前置位+网络号+主机号。可以分为5类地址。

A类地址:0 + 7位网络号 + 24位主机号。B类地址:10 + 14位网络号 + 16位主机号。C类地址:110 + 21位网络号 + 8位主机号。D类地址:1110 + 28位多播组号。 E类地址:11110 + 27位备用。

IP地址分为公网地址和私有地址,私有地址只在局域网中使用。A类私有地址,127.X.X.X,10.X.X.X。B类私有地址,172.16.0.0到172.31.255.255。 C类私有地址,192.0.0.0到192.168.255.255。

在每一类地址,可以进一步向主机位借位划分子网。在局域网中,可以将地址划分成多个网段,由子网掩码标识对应地址的网络号。处于同一个网段的主机可以进行相互通信, 处于同一网段的主机才能接收彼此的ARP广播。通过子网掩码可以判断目的主机是否属于同一个网段,如果不属于同一个网段,则数据需要发给网关,由网关负责转发。 通过划分子网,可以减小路由表,减少广播域,节约ip地址。互联网能够访问的只有公网IP地址,而我们家庭上网时,经常看到使用的是私有地址。这是通过NAT技术实现, 将局域网的私有地址映射到一个公有地址,并负责数据的转发和接收。

IP层如何路由选择?

当收到上层的数据报,IP层会检查目的主机与源主机是否直接相连或在同一个共享网络上。如果是,则IP数据报直接发到目的主机,如果不是则发给默认的路由器上。 IP层会在内存中存储路由表,通过路由表决定IP数据报的发送。IP层可以配置成主机也可以配置成路由器,只有配置成路由器功能,收到的数据报目的地址不是 本机时才会进行转发,否则会丢弃。

我们知道需要通过路由表来路由,那路由表结构是什么样的?又有什么路由规则?我们可以执行netstat -r命令查看主机的路由表。每一行路由表项包含: 目的IP地址Destination,下一站路由Gateway,标志Flags表明目的IP地址是网络地址还是主机地址和表明下一站路由是路由器还是直接相连,为数据报传输的网络接口Netif。 路由选择首先判断目的IP是否能找到直接相连的,再不行就找同一网络的,最后有个兜底的就是默认下一站路由器。标志位U表示路由可用,G表示路由是到一个网关,H表示路由到一个主机, D表示路由是由重定向报文创建的,M表示路由被重定向报文修改。那路由表是怎么生成和维护的?

路由表如何初始化和维护?

系统每当初始化一个接口时,就为接口自动创建一个直接路由。根据系统不同,可以找到对应的静态配置文件。也可以通过route命令,手动添加路由表项。 当没有设置默认下一站路由,又匹配不到其他表项时,如果数据报是由本地主机产生,则会收到主机ICMP不可达差错或ICMP网络不可达差错。如果是转发的数据, 则会收到ICMP主机不可达差错。当IP数据报发送给默认下一站路由器,而路由器检索下一站路由时,发现下一站路由B与源主机在同一个局域网, 则会发送一份ICMP重定向差错报文告知源主机,以后应该直接发给更直接的下一站路由。

主机引导的时候,会发送ICMP路由发现报文,收到通告报文后则停止发送随后监听来自路由器的请求报文,通告报文可以修改路由表。路由器启动时会定期广播发送通告报文。 路由器上有一个路由守护进程,运行选路协议,与相邻的路由器通信,动态更新内核路由表。各个路由器之间的选路协议,称为内部网关协议IGP。 对应的实现有RIP选路信息协议,OSPF最短路优先协议。对于动态维护路由表我们可以暂时不去深入了解。

什么是ICMP协议?

前面我们提到了ICMP协议,Internet控制报文协议。它被认为是IP层的一个附属协议。主要是用来传递差错信息和查询信息。ICMP报文是在IP数据报内容部分。 ICMP报文的结构也比较简单:8位类型+8位代码+16位检验和+内容部分。这里的16位检验和,检验的是整个ICMP报文,其检验方法同IP首部检验和一样。常见的ICMP查询报文 主要有:回显请求和应答,路由器请求和通告报文,时间戳请求和应答,地址掩码请求和应答。常见的ICMP差错报文主要有:网络不可达,主机不可达,端口不可达, 重定向差错报文,超时报文

ping程序原理?

前面我们提到了ICMP常见的查询报文有回显请求和应答。ping程序正是通过ICMP报文实现的。通过在报文中携带标识序号和时间戳。ping程序收到应答的时候, 根据当前时间减去报文中的时间戳,就能得出往返时间RTT,也能查看网络数据报收发情况。

traceroute程序原理?

前面我们讲到ping测试网络连通情况。那我们如何知道数据报在到达目的之间都经过了哪些中间路由。traceroute程序提供了这个功能。我们知道IP 数据报ttl为0时就会被丢弃并发回一份超时报文,而这份报文中会带上当前路由器的IP地址。所以traceroute程序可以发送IP数据报,通过设置ttl选项1,2...逐渐递增, 就能得到每一个中间路由器的地址。traceroute选择发送UDP数据报,设置一个不可能的值大于30000作为UDP端口。当到达主机时,则会产生一份ICMP端口不可达差错, traceroute就能知道探测结束。

传输层

IP层为上层的数据传输,提供了封装,分片,重组,路由选择等底层传输细节功能,使上层可以专注于数据本身。下面我们来看看传输层两个重要的协议。 UDP用户数据报协议和TCP传输控制协议。

什么是UDP协议?

UDP协议是一个比较简单的协议。它为上层提供无连接不可靠无顺序的数据报服务。UDP数据报结构:16位源端口号+16位目的端口号+16位UDP长度+ 16位UDP检验和+数据部分。在IP数据报的基础上,它增加了端口部分,同时增加IP内容部分(UDP数据报)的检验和。检验和方法与IP一样。UDP数据报按16位对齐。 通过netstat -p udp可以查看运行接收udp数据报到的服务器和收发情况。当系统接收数据报的速率大于处理速率时就可能产生ICMP源站抑制差错。 DNS域名系统一般会用UDP实现。

什么是DNS域名系统?

DNS域名系统本质是用于TCP/IP应用程序的分布式数据库,提供主机名和IP地址的转换和有关电子邮件的选路信息。从应用角度,对DNS的访问是通过一个地址解析器完成, 在Unix主机,主要提供两个库函数gethostbyname,gethostbyaddr。DNS的名字空间和Unix文件系统相似,顶级域如.com,.edu,再划分二级域,可以继续划分。每一个区域的 授权机构被委派后,就由其负责向该区域提供多个名字服务器。DNS有特定的查询和响应报文格式,可以通过UDP或TCP传输。当我们购买一台云主机时获得一个公网地址,再注册一个域名, 这时候我们就需要让域名解析到我们对应的服务器IP上。这个过程可以理解为向名字服务器添加一条资源记录,当其他设备访问域名时就会先通过区域的DNS服务器查询, 如果查不到则会一级一级往上查询,最终就能查到域名对应的IP地址。

什么是TCP协议?

TCP传输控制协议,同样,我们先看定义:TCP提供面向连接可靠的字节流服务。面向连接意味着两个使用TCP应用交换数据时需要先建立连接,并维护连接的状态信息。 TCP提供可靠性,对比我们前面说的IP协议,UDP协议的不可靠(不保证数据报到达丢了就丢了),TCP会有一系列策略,其中很容易想到的就是确认和重发策略。 TCP提供的是字节流服务而不是数据报服务。TCP底层还是IP的数据报,但不同于UDP的独立数据报处理,TCP在同一个连接的数据的是连续的,只是传输过程的IP数据报是独立。 TCP接收端会重组和去重成字节流提交给上层应用。

TCP有什么策略来提供可靠性?

数据分割:应用数据会被分割成TCP认为最适合发送的报文段传递给IP层。

确认与重传:TCP发出发出一个报文段后,会启动一个定时器,并等待目的端发回的确认。如果超时,则会重发这个报文段。

延迟确认:TCP接收端收到报文段后并不是立刻发送确认,而是启动定时器略微推迟,尽可能由接收端返回数据时捎带上ACK,提高网络利用效率。

数据检验:TCP会检验报文段的首部和数据,如果检验失败,则丢弃该数据报并等待发送端超时重发。

重排去重:TCP会对收到的数据进行重新排序,并丢弃重复的数据,将收到的数据以正确的顺序提交给应用层。

流量控制:TCP连接是全双工,有读,写两个固定大小的缓冲区。通过滑动窗口协议,TCP接收端会通告接收窗口大小,避免发送端发送过快导致接收端缓冲区溢出。

TCP数据报结构?

16位源端口号+16位目的端口号。一条连接可以由网络四元组(源IP,目的IP,源端口,目的端口)确定。

32位序号。建立一个连接时,系统会为该连接分配一个初始序号ISN。不同方向有各自的初始序号。后续的数据按字节计数。SYN和FIN占用一个序号。

32位确认号。最近一个接收成功的序号+1,表示期望接收的下一个序号。ACK标志设置为1时,该字段才有效。

4位首部长度+保留6位+6位标志位+16位窗口大小。首部长度同IP首部长度一样,单位都是4字节。标志位URG表示TCP进入紧急模式,ACK表示确认序号有效, PSH表示正常数据,RST表示复位连接,SYN表示这是一个同步序号用来建立连接,FIN表示发送端完成发送任务。16位窗口大小单位为字节,表示接收端期望接收 的字节。

16位检验和+16位紧急指针。检验和覆盖整个TCP首部和数据。当URG标志为1则表明进入紧急模式。紧急指针是一个正偏移量,和序号段中的值相加表示紧急数据最后一个字节的序号。

可选项。常见的可选项为最长报文段大小MSS。在连接通信的第一个报文段中指明这个选项,指明本端能接收的最大长度报文段。发送端将不发送超过该大小的报文段,通常是为了避免分段。

内容部分。

TCP如何建立连接?

TCP是面向连接,在交换数据之前需要建立一个连接。我们可以用tcpdump工具抓个包看一下这个建立连接的过程。tcpdump tcp and host liuda666.club

TCP三次握手

看前3个报文段(后面我们称分组),这个过程就是TCP三次握手过程。

  1. 请求端执行主动打开,发送一个SYN分组,带上初始序号,窗口大小,最大分段大小MSS,这时发送端进入SYN_SENT状态。

  2. 响应端收到SYN后执行被动打开,发回一个SYN分组,带上响应端的初始序号,窗口大小,最大分段大小MSS。同时设置了ACK位置,确认序号为请求端初始序号+1, tcpdump用相对序号1表示。此时响应端进入SYN_RCVD状态。

  3. 请求端收到ACK进入ESTABLISHED状态,同时对响应端发送的SYN,发回一个ACK响应。当响应端收到ACK则也进入ESTABLISHED状态,连接就算建立了。

如果步骤1,请求端发送SYN丢失了怎么办?之前说过TCP的重传确认机制,所以请求端会以指数退避的方式超时重传,第一次重传在6s左右,第二次在24s左右, 直到达到系统设置的最长超时时间返回连接超时错误。

当我们试图连接的目的IP和端口并没有对应的TCP服务监听时,会收到响应端的RST重置连接分组,这一点相比于UDP会返回ICMP不可达差错不同。 如果步骤2响应端发送的捎带ACK的SYN分组丢失,在没有收到步骤3的ACK前,也会一直超时重传。当步骤2收到步骤1重复的SYN,即使没有超时也会进行快速重传的操作。

对于请求端,在收到步骤2的ACK时候连接状态处于ESTABLISHED状态,并发回ACK。此时请求端已经可以开始发送数据。但对于响应端来说,如果没有收到ACK, 则没有进入ESTABLISHED状态,因为还没确认请求端的接收能力。这时如果请求开始发送数据,则会把ACK捎带过来,响应端则能够进入ESTABLISHED状态。 如果一直没有收到请求端的最后一个ACK,则响应端会重复步骤2超时重传,请求端收到后会重发ACK。所以我们可以看出,TCP连接的建立为什么是三次握手。

假想一下,TCP两边同时打开的情况是怎样,原理是一样的。主动打开发送SYN,两边同时进入SYN_SENT状态。当收到对方的SYN,进入SYN_RCVD状态,就再发送己方的SYN并带上ACK。当双方都收到对方的ACK则进入ESTABLISHED状态。

当TCP三次握手完成后,会被放入呼入连接队列,等待应用层接收accept从该队列移出。应用层可以指明backlog,该参数与队列长度正相关,姑且认为就是队列长度。 对于新的连接请求,如果该TCP监听端点的队列还有空间,则TCP模块将对SYN进行确认并完成后续连接的建立,但应用层只有等待三次握手第三个分组到达之后才能知道连接的存在, 也就是还没放入队列中。如果请求端已进入ESTABLISHED状态,则发送的数据,将会放在连接的接收缓冲区中。如果队列没有空间,则试图建立新连接的SYN分组到达,TCP将丢弃不理会, 由请求端超时重发。

TCP连接如何终止?

正常终止一个连接需要四次握手,这是由TCP半关闭造成的。TCP连接是全双工的,所以每个方向都需要单独关闭。当数据发送完毕是,通过发送FIN分组终止这个方向的连接。 收到请求端的FIN,响应端知道请求端不会再有数据过来,但响应端还可以继续发送数据给请求端,直到发送完毕再发送FIN。

TCP四次挥手

通过tcpdump抓包,可以看下这个过程是怎么发生的。

  1. 首先是主动关闭端,发送FIN,FIN占用一个序号,seq2854,主动端进入FIN_WAIT1状态。。可以看到,第一个FIN下一个的ACK并不是终止序列的一部分,它是对之前数据的ACK。 真正ACK是由接着的一个FIN捎带上了,这是因为被动端已经没有数据发送所以进入CLOSE_WAIT状态后,立刻发送FIN捎带ACK进入LAST_ACK状态。我们暂且忽略按正常的讲。

  2. 被动端收到FIN后进入CLOSE_WAIT状态,发送ACK,并等待不再有数据需要发送。主动端收到被动端的ACK2855后,进入FIN_WAIT2状态。等待被动端发送FIN,告知结束数据发送。

  3. 被动端发送FIN,seq4314,进入LAST_ACK状态。主动端收到后,进入TIME_WAIT状态,并发送ACK4315。进入TIME_WAIT状态后,等待2MSL(最大报文段生存时间)后进入CLOSED状态终止。

  4. 被动端收到ACK后,进入CLOSED状态,连接正常终止。

对于终止连接,主要有shutdown和close系统调用。shutdown关闭读时,接收缓冲区数据将被丢弃,后续数据也会被丢弃,read会返回结束EOF。 但是如果对方有新的数据到达,则会响应ACK然后悄悄丢弃。shutdown关闭写时,这就是半关闭,不管socket的引用计数,都会直接关闭写, 将发送缓冲区已有的数据发送出去,然后发送FIN给对端。如果再对socket进行write则会抛出EPIPE错误。shutdown关闭读写。相当于既关闭读又关闭写。 close关闭,是将socket的引用计数减去1。当socket引用计数为0时,会向对端发送FIN,然后回收连接和资源。当对端发送分组时,则会响应一个RST复位连接分组。

如果步骤2,主动端进入FIN_WAIT2状态后,等待接收被动端的FIN,如果被动端一直没有发送,或者FIN丢失了。那主动端会一直处于FIN_WAIT2,被动端会一直处于CLOSE_WAIT。 如果进行半关闭,这是由应用层来决定的。如果进行全关闭,则会设置一个定时器,超过等待时间则直接进入CLOSED状态。

对于步骤3,主动端收到FIN后进入TIME_WAIT状态,为什么要等待2倍MSL时间才进入CLOSED状态?我们可以想一下如果四次握手,最后一个ACK丢失了, 而主动端不等待2倍MSL就进入CLOSED状态。那么被动端超时重传,主动端则会返回一个RST复位报文段。所以为了TCP的正常终止, TIME_WAIT需要等待一去(ACK)一回(重发的FIN)最多两个报文段的MSL。处于TIME_WAIT状态时,定义这个连接的插口对网络四元组不能再被使用。 处于该状态时,网络中迟到的数据分组到达时都会被丢弃。因为等待2倍MSL,所以在被动端发送FIN之前的所有数据分组都会在网络中消亡。如果不等待, 同样的插口对建立新连接,当老的数据分组到达时就会被错误认为是该连接的数据分组。所以总结就是两点:为了保证TCP连接的正常终止,为了使网络中迷途 的数据分组正常过期。

连接建立后,客户端拔掉网线或断电,会发生什么?这时服务端的状态还是ESTABLISHED,并不知道对方发生了什么。当服务端发送数据报给客户端,如果客户端已经断电或断网了, 则没有客户端给服务端发回响应。服务端将一直重发直到超时。如果客户端已经恢复,服务端发送数据到客户端,这时客户端并不存在该连接,则会返回RST复位报文段。这种情况 称为半打开连接。所以为了避免这种情况,在保持长连接的应用需要从应用层建立心跳机制。TCP提供了保活定时器,通过设置keep-alive,但连接超过一定时间没有数据交互, 则发送探测分组。

如果连接双方同时关闭,同时发出FIN,进入FIN_WAIT1,收到对方FIN时进入CLOSING状态,并发回ACK。收到对方ACK时,则进入TIME_WAIT状态。 通过设置SO_LINGER可以使关闭连接时发送的是RST复位报文段而不是FIN报文段。

TCP数据的传输过程?

我们把连接想象成一个管道,这个管道在不同节点之间的宽度都不一样。这个宽度取决于不同线路,一般用带宽b/s。因为TCP是双向的, 所以一条管道的容量=带宽b/s * 报文段的往返时间RTT。为了提高网络传输效率,减少网络中的阻塞,就需要尽可能避免发送小分组。

Nagle算法要求TCP连接上最多有一个未被确认的未完成的小分组,在该分组确认前不能发送其他分组。等待确认期间,TCP发送端会收集这些小分组,在确认到达时 合并成一个分组发出去。确认越快,则发送越快。对于低时延要求高的应用,可以设置TCP_NODELAY选项禁用Nagle算法。

在TCP传输过程中,每一个分组就需要一个ACK吗?实际并不是这样的,使用TCP滑动窗口协议时,接收方发回的ACK是累积的,表示已经接收到ACK-1序号的字节了。 滑动窗口协议时TCP接收方流量控制的一种方式,与确认序号一起使用。对于TCP发送方,根据接收方发回的通告窗口+确认序号,确定发送窗口的左右边界。如果还有可用窗口,则可以 继续发送数据,否则就不能,需要等到发送窗口内已发出数据的确认返回,才能再次滑动发送窗口。如果某一时刻,发送方发送窗口为0,而接收方已经消费了数据,发回ACK不带数据时, 如果ACK丢失了,因为TCP不会对不带数据的ACK进行超时重传,所以两边会陷入死锁的状态。为了避免这种情况,TCP发送方当窗口变为0时会启动坚持定时器,周期性地向接收方查询, 这些发出去的报文段称为窗口探查,它会带上1个字节的数据(只有发送方有数据发送时才会进行窗口探查)。

目前我们了解了发送方根据接收方的通告窗口大小来发送数据。这种方式在局域网是可以的,但是在广域网,则需要考虑中间路由器和速率较慢的链路。一些中间路由器 需要缓存分组,并有可能耗尽存储空间。为了避免网络拥塞,TCP支持使用慢启动算法。慢启动为发送方增加了拥塞窗口cwnd。建立连接是,cwnd初始化位1个分组大小。 每收到一个新的ACK,cwnd就增加1个分组大小。发送方取cwnd和通告窗口的较小值作为上限。拥塞窗口是发送方的一种流量控制方式。可以看出,慢启动会使发送方拥塞 窗口呈指数增长,尽快达到上限。

那么如果慢启动达到了拥塞窗口上限后,网络出现了拥塞,有时会中间路由器会达到处理极限,丢弃一些分组,这时如何处理?TCP拥塞避免算法是一种处理丢失分组 的方法,该方法基于假定分组收到损坏而丢失的可能性比较小,分组丢失就意味着网络发生拥塞。分组丢失的两个判断:超时和收到重复的ACK。拥塞避免算法和慢启动 通常在一起实现,对于一个连接维持两个变量,拥塞窗口cwnd和慢启动门限ssthresh。

  1. 对于一个连接cwnd初始化为1个分组(cwnd单位为分组),ssthresh为最大IP报文大小65535。运行过程中,cwnd和通告窗口大小的较小值为当前实际窗口。 如果cwnd < ssthresh就进行慢启动,否则就进行拥塞避免。

  2. 拥塞发生时(超时或收到重复确认),ssthresh被设置为当前实际窗口的一半,但最少为2个分组大小。如果超时引起的拥塞,则cwnd设置为1个分组, 即执行慢启动。如果是重复确认引起的拥塞,则会执行快速重传,在收到新数据确认后,则执行快速恢复。

  3. 慢启动是每收到新数据的1个ACK,cwnd增加一个分组。拥塞避免是每收到新数据的1个ACK,cwnd增加1/cwnd个分组

我们可以想象这个过程,一开始慢启动cwnd窗口呈指数增长,当大于通告窗口时,当前窗口为通告窗口上限。当大于ssthresh时,进入拥塞避免,呈线性增长。 这时如果出现了分组超时,则认为网络拥塞,将ssthresh设置为当前窗口的一半,把cwnd设置为1分组,然后再次执行慢启动指数增长。这样,当网络畅通时, 发送窗口就能一直增大提高传输效率,当网络拥塞时,则迅速减小发送窗口,避免再继续增加网络负担。

什么是快速重传和快速恢复?前面我们提到网络拥塞有两种判断,一个是超时引起,一个是收到重复的确认。当接收方收到失序的分组时会立即确认,这时就 可能让发送方收到重复的确认。如何确定是因为重新排序引起还是丢失分组引起。TCP是这样判定,当收到连续3个或以上的重复ACK时,就认为是丢失分组引起。 于是TCP会立刻重传分组,而无须等待超时,这就是快速重传算法。随后执行拥塞避免而非慢启动,这就是快速恢复算法。当能收到重复确认时,说明还有数据持续到接收端, 不需要像超时引起直接设置cwnd为1个分组减小数据流,而是继续保持适当的窗口大小。

  1. 收到第3个重复ACK时,判定拥塞,ssthresh设置为当前拥塞窗口cwnd一半。重传丢失的分组,设置cwnd为ssthresh加上3个分组大小。

  2. 每次收到另一个重复的ACK,cwnd增加1个分组大小,并重传丢失的分组。

  3. 当下一个数据确认到达时,cwnd设置为ssthresh,随后执行拥塞避免,cwnd线性增长。

对于每个连接TCP管理4个不同的定时器:重传定时器,坚持定时器,保活定时器,2MSL定时器。其中,只有保活定时器,我们还没讲到。前面我们提到当连接 建立后,其中一方断电断网的情况。如果连接没有数据流动,则无法检测连接的状况。保活定时器,对于非活动连接,每隔一段时间发送一个探查保文段,如果正常 响应则重置定时器。如果对方断电断网且没恢复,则中间路由会返回ICMP不可达差错。但TCP并不处理ICMP错误,则发送方继续超时重传,直到连接超时。如果 对方恢复,则会因为无法识别连接,返回RST复位保文段。

如何设计服务器程序?

一个简单的服务器程序:socket()创建socket插口返回描述符,bind()把描述符对应的socket绑定地址端口信息,listen()把socket设置为被动指示内核 接收指向该socket的连接请求,accept()从已完成三次握手的连接队列中移出一个连接,返回连接的描述符。随后进行连接的读写,完成后关闭连接。

对于多进程服务器:对每一个连接fork一个子进程,父进程关闭连接描述符减少引用计数,子进程关闭listenfd减少引用计数。然后子进程对该连接进行处理。 子进程结束时会发送SIGCHLD信号,父进程设置信号处理器,及时回收子进程避免子进程成为僵尸进程。

对于多线程服务器:对每一个连接创建一个线程,由单独的线程去处理连接。主线程只负责检查新连接的到来。

对于多进程和多线程,系统资源是有限的,所以如果同一时间有大量请求,这样的设计就可能出现瓶颈。而且多进程和多线程模型,会产生上下文切换, 对于性能也会大打折扣。创建socket返回的是描述符,这跟打开文件返回描述符类似。对于Unix系统,一切皆文件。所以,可以把socket当成类文件处理。 在Unix系统,有五种IO模型。

  1. 阻塞式IO:发起系统调用时,如果内核无数据准备好,则陷入阻塞等待。当内核数据准备好,则将数据复制到用户空间缓冲区,返回成功。用户程序再进行处理。

  2. 非阻塞式IO:通过设置非阻塞选项,内核如果无数据准备好,不陷入阻塞等待,而是返回一个错误EAGAIN或EWOULDBLOCK,这样用户程序可以选择轮询或其他方式接着处理。

  3. IO复用模型:通过select,poll或epoll(Linux专属)阻塞在这两个系统调用,而不是真正的IO系统调用上。通过监控一系列描述符的IO事件是否 就绪,再对就绪的描述符执行IO操作。

  4. 信号驱动式IO:对于socket可以设置开启信号驱动IO功能,通过安装一个信号处理器函数,捕获SIGIO信号。IO系统调用会立即返回,当数据准备好时, 内核就为该进程产生一个SIGIO信号。在信号处理器函数就进行对应的IO操作。

  5. 异步IO:POSIX aio系列系统调用,告知内核启动某个操作,并在内核完成整个操作后,发送异步通知。与信号驱动IO的区别在于,信号驱动只是通知有数据 准备就绪,还需要我们进一步读取将数据从内核缓冲区拷贝到用户缓冲区。而异步IO则是内核已经帮我们完成整个操作了才产生,异步通知可以是信号形式。

阻塞式IO和非阻塞式IO实现都比较简单,但是效率都不高。信号驱动IO和异步IO实现又比较复杂。IO复用模型是一个不错的选择,现在的很多高并发服务器基本 都会使用。相关的系统调用主要又select,poll,epoll,kqueue。

select:把对描述符添加到感兴趣的事件描述符集,传入内核,让内核分别测试读,写,异常等事件是否就绪。调用会一直阻塞,直到有事件就绪或者超时。 当返回如果没有超时错误,则依次检查各个描述符是否有事件。select系统调用有描述符上限,取决于系统设置FD_SETSIZE,通常为1024。select是通过 传入参数的位图,内部一个循环遍历所有描述符检查是否有对应事件就绪,然后修改位图返回。如果没有则会陷入阻塞等待唤醒,唤醒后,再次遍历检查。 所以select有几个明显的缺点:内核需要遍历检查指定的文件描述符。需要在用户空间和内核空间拷贝事件注册结构,描述符比较多的时候,会消耗cpu用于来回拷贝。 调用完成后,程序也需要遍历检查。

poll:相比与select按事件分类划分描述符集。poll参数结构则是为每一个描述符创建一个对象,并设置每一个对象所关注的读写事件。poll函数传入一个 参数对象数组。事件就绪时,通过与对应事件进行&位运算就能知道描述符是否就绪。poll的底层原理基本与select一样,但相比与select,不用每次都 重新初始化,也能细分事件,关注更多事件类型。

epoll:首先epoll_create创建一个epoll实例,指定想要检查的描述符数量,返回的是一个文件描述符。通过epoll_ctl可以修改文件描述符的兴趣列表。 每个注册到epoll实例的描述符需要占用一小段不能被交换的内核空间内存。系统调用epoll_wait让内核监视指定的打开文件描述符, 返回epoll实例中当前就绪的多个文件描述符信息,拷贝到用户传入的一段空间中。通过与事件掩码的&位运算可以知道对应的事件就绪。相比于select 和poll,epoll不需要在用户空间和内核空间拷贝所有的关注对象,而且epoll是通过内核在打开文件描述符上下文相关联的列表中记录该描述符, 之后每次IO操作使得文件描述符成为就绪态时,内核就在epoll描述符的就绪列表中添加一个元素。epoll可以同时支持水平触发和边缘触发。

什么是水平触发和边缘触发?水平触发:如果文件描述符上可以非阻塞地执行IO系统调用,此时认为已经就绪。边缘触发:如果文件描述符自上次状态检查有了新的IO活动 触发通知。

采用水平触发通知时,可以在任意时刻检查文件描述符的就绪状态。当我们确定了文件描述符处于就绪状态时就可以进行一些IO操作,然后重复检查是否就绪。 所以水平触发模式,允许在任意时刻重复检查,没有必要每次文件描述符就绪的时候就尽可能多的执行IO。

采用边缘触发通知时,只有当IO事件发生时才会收到通知,在另一个IO事件来前不会收到任何新的通知。所以当收到描述符就绪通知时,需要尽可能多地执行IO操作, 如果不这样的话就得等到下一次IO事件发生才会获得通知。对于需要尽可能多地执行IO操作,每个被检查的描述符应该设置为非阻塞模式。在得到IO事件通知后重复执行。

select和poll只支持水平触发。epoll则支持水平触发和边缘触发。epoll实现过程:通过注册描述符和感兴趣的事件。epoll注册到需要监视的文件描述符的上下文关联中。 当被监视的文件描述符有IO操作的时候,就会通知epoll,epoll会将对应描述符监视对象添加到可用链表。epoll_wait每次会遍历从可用链表取出节点,调用poll检查文件描述符 的事件就绪,如果事件就绪且为感兴趣的,则添加到就绪链表,在LT模式下重新加回可用链表,ET模式下则不重新加回。如果节点事件不感兴趣或没有事件则移出可用链表。

应用层

HTTP

http超文本传输协议,基于TCP连接进行报文传输,基于请求响应模式的无状态协议。

http请求报文格式:请求行,请求头header,包含数据的body。http响应报文:状态行,响应头header,包含数据的body

常见的http方法:get,post,put,delete,options。常见的响应码和描述符:1xx信息提示,2xx成功,3xx重定向,4xx客户端错误,5xx服务器错误

一次http请求过程:解析主机名,dns查询ip,获取端口号,建立tcp连接。发送请求报文,返回响应报文,关闭连接。http性能优化无非就在这几个环节。 DNS查询,TCP连接建立耗时,TCP慢启动和拥塞控制。

http协议可以升级成为websocket协议,http请求的时候,通过请求头部设置Connection: Upgrade Upgrader: websocket申请协议升级, 服务器返回101响应表示协议转换成功。后续在该tcp连接就可以进行全双工的通信,传输的是websocket的报文。

HTTPS

网上关于https的文章很多,这里只是个人的总结和简化。https本质是协商生成一对对称密钥然后对http报文进行加密传输的过程。

  1. 客户端发送可选的加密套件,客户端生成的随机串1
  2. 服务端发送选中的加密套件,服务端生成的随机串2,服务端的证书,证书包含公钥
  3. 客户端进行证书可信验证,取出证书公钥,生成pre-master-secret并用公钥进行加密,发送给服务端,并通知后续使用对称密钥通信。
  4. 服务端收到后用私钥解密,pre-master-secret,并通知客户端后续使用对称密钥通信
  5. 客户端,服务端都有随机串1,随机串2,pre-master-secret。客户端和服务端用协商好的加密套件生成对称密钥
  6. 客户端和服务端传输数据使用对称密钥进行加密和解密

HTTP1.0 HTTP1.1 HTTP2.0

影响一个http网络请求的因素主要有两个:带宽和延迟

http1.1对比http1.0主要是使用了长连接,多个请求可以复用同一个连接,避免一个请求响应就关闭连接。http1.1默认开启 Connection:keep-alive。相比于http2.0多路复用,多个请求可以同时发送。

http2相比还有首部压缩,支持服务器推送等区别。首部压缩要求客户端和服务端都维护一份首部字典,并支持动态更新。

nginx工作模型

推荐阅读Nginx开发从入门到精通

nginx使用多进程和IO多路复用模型支持高并发。nginx主要由一个master进程和多个worker进程组成。master进程负责处理与外界的信号通信, 与各worker进程已信号方式进行通信,监控worker进程的运行状态,worker进程退出时发送SIGCHLD信号,master进程可以重启新的worker。worker进程 主要负责处理网络事件,处理连接请求。

nginx启动master进程时,创建listen socket绑定到服务地址。然后fork worker进程继承该监听描述符listenfd。我们假设使用epoll,每一个worker 进程在注册listenfd之前需要获得accept_mutex互斥锁,没有获取到互斥锁的worker进程则继续等待其他描述符就绪。当worker进程注册的listenfd就绪时, 进行accept获取连接描述符,并注册到epoll实例,listenfd从epoll注册表移除,并释放accept_mutex。accept_mutex解决惊群问题,避免当listenfd就绪时, 多个worker进程同时被唤醒进行accept。

nginx master收到HUP信号时,会重新加载配置,创建新的日志文件,然后启动新的worker,然后通知旧worker下线。如果需要升级nginx,则需要先给 nginx主进程发送USR2信号,nginx会把旧的pid文件重命名为old.pid。启动一个新的nginx实例,使用新的pid文件,新旧服务同时运行。然后发送quit信号给 旧的nginx实例,使旧的nginx实例优雅退出。

nginx反向代理原理:client连接到nginx,nginx会缓存从client收到的数据,直到一个完整的请求收集完成,然后nginx与上游服务器建立连接,发送数据, 上游服务器发回数据,则nginx会一边接受一边转发给client。request_time表示client请求到响应的完整事件。upstream_time表示nginx请求上游服务器到响应 的完整时间。

小结

参考资料:《TCP-IP详解》、《UNIX网络编程》、《HTTP权威指南》