台州网站开发公司,服务营销案例,做网站的思想体会,网站开发语言排名来源 | 杰哥的IT之旅做IT相关的工作#xff0c;肯定都离不开网络#xff0c;网络中最重要的协议是TCP。无论是实际工作还是笔试面试#xff0c;你看哪里能少得了TCP?我看过RFC中与TCP相关的文档#xff0c;也看过linux中TCP相关的源码#xff0c;也看过不少框架中的TCP相… 来源 | 杰哥的IT之旅做IT相关的工作肯定都离不开网络网络中最重要的协议是TCP。无论是实际工作还是笔试面试你看哪里能少得了TCP?我看过RFC中与TCP相关的文档也看过linux中TCP相关的源码也看过不少框架中的TCP相关的代码对TCP是有点感觉了。一直想找个时间来分享下TCP相关的知识如果大家有疑问欢迎相互交流。其实搞透了TCP之后发现它也就那么回事。考虑最简单的情况两台主机之间的通信。这个时候只需要一条网线把两者连起来规定好彼此的硬件接口如都用USB、电压10v、频率2.4GHz等这一层就是物理层这些规定就是物理层协议。我们当然不满足于只有两台电脑连接因此我们可以使用交换机把多个电脑连接起来如下图这样连接起来的网络称为局域网也可以称为以太网以太网是局域网的一种。在这个网络中我们需要标识每个机器这样才可以指定要和哪个机器通信。这个标识就是硬件地址MAC。硬件地址随机器的生产就被确定永久性唯一。在局域网中我们需要和另外的机器通信时只需要知道他的硬件地址交换机就会把我们的消息发送到对应的机器。这里我们可以不管底层的网线接口如何发送把物理层抽离在他之上创建一个新的层次这就是数据链路层。我们依然不满足于局域网的规模需要把所有的局域网联系起来这个时候就需要用到路由器来连接两个局域网但是如果我们还是使用硬件地址来作为通信对象的唯一标识那么当网络规模越来越大需要记住所有机器的硬件地址是不现实的同时一个网络对象可能会频繁更换设备这个时候硬件地址表维护起来更加复杂。这里使用了一个新的地址来标记一个网络对象IP地址。通过一个简单的寄信例子来理解IP地址。我住在北京市我朋友A住在上海市我要给朋友A写信写完信我会在信上写好我朋友A的地址并放到北京市邮局给信息附加目标IP地址并发送给路由器邮局会帮我把信运输到上海市当地邮局信息会经过路由传递到目标IP局域网的路由器上海市当地路由器会帮我把信交给朋友A局域网内通信因此这里IP地址就是一个网络接入地址朋友A的住址我只需要知道目标IP地址路由器就可以把消息给我带到。在局域网中就可以动态维护一个MAC地址与IP地址的映射关系根据目的IP地址就可以寻找到机器的MAC地址进行发送。这样我们不需管理底层如何去选择机器我们只需要知道IP地址就可以和我们的目标进行通信。这一层就是网络层。网络层的核心作用就是提供主机之间的逻辑通信。这样在网络中的所有主机在逻辑上都连接起来了上层只需要提供目标IP地址和数据网络层就可以把消息发送到对应的主机。一个主机有多个进程进程之间进行不同的网络通信如边和朋友开黑边和女朋友聊微信。我的手机同时和两个不同机器进行通信。那么当我的手机收到数据时如何区分是微信的数据还是王者的数据那么就必须在网络层之上再添加一层运输层运输层通过socket套接字将网络信息进行进一步的拆分不同的应用进程可以独立进行网络请求互不干扰。这就是运输层的最本质特点提供进程之间的逻辑通信。这里的进程可以是主机之间也可以是同个主机所以在android中socket通信也是进程通信的一种方式。现在不同的机器上的应用进程之间可以独立通信了那么我们就可以在计算机网络上开发出形形式式的应用如web网页的http文件传输ftp等等。这一层称为应用层。应用层还可以进一步拆分出表示层、会话层但他们的本质特点都没有改变完成具体的业务需求。和下面的四层相比他们并不是必须的可以归属到应用层中。最后对计网分层进行小结最底层物理层负责两个机器之间通过硬件的直接通信数据链路层使用硬件地址在局域网中进行寻址实现局域网通信网络层通过抽象IP地址实现主机之间的逻辑通信运输层在网络层的基础上对数据进行拆分实现应用进程的独立网络通信应用层在运输层的基础上根据具体的需求开发形形式式的功能。这里需要注意的是分层并不是在物理上的分层而是逻辑上的分层。通过对底层逻辑的封装使得上层的开发可以直接依赖底层的功能而无需理会具体的实现简便了开发。这种分层的思路也就是责任链设计模式通过层层封装把不同的职责独立起来更加方便开发、维护等等。okHttp中的拦截器设计模式也是这种责任链模式。运输层 本文主要是讲解TCP这里需要增加一些运输层的知识。本质提供进程通信在运输层之下的网络层是不知道该数据包属于哪个进程他只负责数据包的接收与发送。运输层则负责接收不同进程的数据交给网络层同时把网络层的数据拆分交给不同的进程。从上往下汇聚到网络层称为多路复用从下往上拆分称为多路拆分。运输层的表现受网络层的限制。这很好理解网络层是运输层的底层支持。所以运输层是无法决定自己带宽、时延等的上限。但可以基于网络层开发更多的特性如可靠传输。网络层只负责尽力把数据包从一端发送到另一端而不保证数据可以到达且完整。底层实现socket前面讲到最简单的运输层协议就是提供进程之间的独立通信 但底层的实现是socket之间的独立通信。在网络层中IP地址是一个主机逻辑地址而在运输层中socket是一个进程的逻辑地址当然一个进程可以拥有多个socket。应用进程可以通过监听socket来获取这个socket接受到的消息。socket并不是一个实实在在的东西而是运输层抽象出来的一个对象。运输层增加了端口这个概念来区分不同的socket。端口可以理解为一个主机上有很多的网络通信口每个端口都有一个端口号端口的数量由运输层协议确定。不同的运输层协议对socket有不同的定义方式。在UDP协议中使用目标IP目标端口号来定义一个socket在TCP中使用目标IP目标端口号源IP源端口号来定义一个socket。我们只需要在运输层报文的头部附加上这些信息目标主机就会知道我们要发送给哪个socket对应监听该socket的进程就可获得信息。运输层协议运输层的协议就是大名鼎鼎的TCP和UDP。其中UDP是最精简的运输层协议只实现了进程间的通信而TCP在UDP的基础上实现了可靠传输、流量控制、拥塞控制、面向连接等等特性同时也更加复杂。当然除此之外还有更多更优秀的运输层协议但目前广为使用的就是TCP和UDP。UDP在后面也会总结到。TCP协议首部 TCP协议表现在报文上就是会在应用层传输下来的数据前附加上一个TCP首部这个首部附加了TCP信息先来整体看一下这个首部的结构这张图是来自一位大学老师的课件 非常好用所以一直拿来学习。最下面部分表示了报文之间的关系TCP数据部分就是应用层传下来的数据。TCP首部固定长度是20字节下面还有4字节是可选的。内容很多但其中有一些我们比较熟悉的源端口目标端口。嗯socket不是还需要IP进行定位吗IP地址在网络层被附加了。讲完下面内容再回来看这些字段就熟悉了。TCP面向字节流特性 TCP并不是把应用层传输过来的数据直接加上首部然后发送给目标而是把数据看成一个字节 流给他们标上序号之后分部分发送。这就是TCP的面向字节流特性TCP会以流的形式从应用层读取数据并存放在自己的发送缓存区中同时为这些字节标上序号TCP会从发送方缓冲区选择适量的字节组成TCP报文通过网络层发送给目标目标会读取字节并存放在自己的接收方缓冲区中并在合适的时候交付给应用层面向字节流的好处是无需一次存储过大的数据占用太多内存坏处是无法知道这些字节代表的意义例如应用层发送一个音频文件和一个文本文件对于TCP来说就是一串字节流没有意义可言这会导致粘包以及拆包问题后面讲。可靠传输原理 前面讲到TCP是可靠传输协议也就是一个数据交给他他肯定可以完整无误地发送到目标地址除非网络炸了。他实现的网络模型如下对于应用层来说他就是一个可靠传输的底层支持服务而运输层底层采用了网络层的不可靠传输。虽然在网络层甚至数据链路层就可以使用协议来保证数据传输的可靠性但这样网络的设计会更加复杂、效率会随之降低。把数据传输的可靠性保证放在运输层会更加合适。可靠传输原理的重点总结一下有滑动窗口、超时重传、累积确认、选择确认、连续ARQ。停止等待协议要实现可靠传输最简便的方法就是我发送一个数据包给你然后你跟我回复收到我继续发送下一个数据包。传输模型如下这种“一来一去”的方法来保证传输可靠就是停止等待协议stop-and-wait。不知道还记不记得前面TCP首部有一个ack字段当他设置为1的时候表示这个报文是一个确认收到报文。然后再来考虑一种情况丢包。网络环境不可靠导致每一次发送的数据包可能会丢失如果机器A发送了数据包丢失了那么机器B永远接收不到数据机器A永远在等待。解决这个问题的方法是超时重传。当机器A发出一个数据包时便开始计时时间到还没收到确认回复就可以认为是发生了丢包便再次发送也就是重传。但重传会导致另一种问题如果原先的数据包并没有丢失只是在网络中待的时间比较久这个时候机器B会受到两个数据包那么机器B是如何辨别这两个数据包是属于同一份数据还是不同的数据这就需要前面讲过的方法给数据字节进行编号。这样接收方就可以根据数据的字节编号得出这些数据是接下来的数据还是重传的数据。在TCP首部有两个字段序号和确认号他们表示发送方数据第一个字节的编号和接收方期待的下一份数据的第一个字节的编号。前面讲到TCP是面向字节流但是他并不是一个字节一个字节地发送而是一次截取一整段。截取的长度受多种因素影响如缓存区的数据大小、数据链路层限制的帧大小等。连续ARQ协议停止等待协议已经可以满足可靠传输了但有一个致命缺点效率太低。发送方发送一个数据包之后便进入等待这个期间并没有干任何事浪费了资源。解决的方法是连续发送数据包。模型如下和停止等待最大的不同就是他会源源不断地发送接收方源源不断收到数据之后逐一进行确认回复。这样便极大地提高了效率。但同样带来了一些额外的问题:发送是否可以无限发送直到把缓冲区所有数据发送完不可以。因为需要考虑接收方缓冲区以及读取数据的能力。如果发送太快导致接收方无法接受那么只是会频繁进行重传浪费了网络资源。所以发送方发送数据的范围需要考虑到接收方缓冲区的情况。这就是TCP的流量控制 。解决方法是滑动窗口。基本模型如下发送方需要根据接收方的缓冲区大小设置自己的可发送窗口大小处于窗口内的数据表示可发送之外的数据不可发送。当窗口内的数据接收到确认回复时整个窗口会往前移动直到发送完成所有的数据在TCP的首部有一个窗口大小字段他表示接收方的剩余缓冲区大小让发送方可以调整自己的发送窗口大小。通过滑动窗口就可以实现TCP的流量控制不至于发送太快导致太多的数据丢失。连续ARQ带来的第二个问题是网络中充斥着和发送数据包一样数据量的确认回复报文因为每一个发送数据包必须得有一个确认回复。提高网络效率的方法是累积确认。接收方不需要逐个进行回复而是累积到一定量的数据包之后告诉发送方在此数据包之前的数据全都收到。例如收到 1234接收方只需要告诉发送方我收到4了那么发送方就知道1234都收到了。第三个问题是如何处理丢包情况。在停止等待协议中很简单直接一个超时重传就解决了。但连续ARQ中不太一样。例如接收方收到了 123 567六个字节编号为4的字节丢失了。按照累积确认的思路只能发送3的确认回复567都必须丢掉因为发送方会进行重传。这就是GBNgo-back-n) 思路。但是我们会发现只需要重传4即可这样不是很浪费资源所以就有了选择确认SACK。在TCP报文的选项字段可以设置已经收到的报文段每一个报文段需要两个边界来进行确定。这样发送方就可以根据这个选项字段只重传丢失的数据了。可靠传输小结到这里关于TCP的可靠传输原理就已经介绍的差不多。通过连续ARQ协议与发送-确认回复模式来保证每一个数据包都到达接收方通过给字节编号的方法来标记每一个数据是属于重传还是新的数据通过超时重传的方式来解决数据包在网络中丢失的问题通过滑动窗口来实现流量控制通过累积确认选择确认的方法来提高确认回复与重传的效率当然这只是可靠传输的冰山一角感兴趣可以再深入去研究拥塞控制 拥塞控制考虑的是另外一个问题避免网络过分拥挤导致丢包严重网络效率降低。拿现实的交通举例子高速公路同一时间可通行的汽车数量是一定的当节假日时就会发生严重的堵车。在TCP中数据包超时会进行重传也就是会进来更多的汽车这时候更堵最后导致的结果就是丢包-重传-丢包-重传。最后整个网络瘫痪了。这里的拥塞控制和前面的流量控制不是一个东西流量控制是拥塞控制的手段为了避免拥塞必须对流量进行控制。拥塞控制目的是限制每个主机的发送的数据量避免网络拥塞效率下降。就像广州等地限制车牌号出行是一个道理。不然大家都堵在路上谁都别想走。拥塞控制的解决方法是流量控制流量控制的实现是滑动窗口所以拥塞控制最终也是通过限制发送方的滑动窗口大小来限制流量。当然拥塞控制的手段不只是流量控制导致拥塞的因素有路由器缓存、带宽、处理器处理速度等等。提升硬件能力把4车道改成8车道是其中一个方法但毕竟硬件提升是有瓶颈的没办法不断提升还是需要从tcp本身来增加算法解决拥塞。拥塞控制的重点有4个慢开始、快恢复、快重传、拥塞避免。Y轴表示的是发送方窗口大小X轴表示的是发送的轮次不是字节编号。最开始的时候会把窗口设置一个较小的值然后每轮变为原来的两倍。这是慢开始。当窗口值到达ssthresh值这个值是需要通过实时网络情况设置的一个窗口限制值开始进入拥塞避免每轮把窗口值提升1慢慢试探网络的底线。如果发生了数据超时表示极可能发生了拥塞然后回到慢开始重复上面的步骤。如果收到三个相同的确认回复表示现在网络的情况不太好把ssthresh的值设置为原来的一半继续拥塞避免。这部分称为快恢复。如果收到丢包信息应该尽快把丢失的包重传一次这是快重传。当然窗口的最终上限是不能无限上涨的他不能超过接收方的缓存区大小。通过这个算法就可以在很大程度上避免网络拥挤。除此之外还可以让路由器在缓存即将满的时候告知发送方我快满了而不是等到出现了超时再进行处理这是主动队列管理AQM。此外还有很多方法但是上面的算法是重点。面向连接 这一小节讲的就是无人不晓的TCP三次握手与四次挥手这些经过前面的内容这一小节其实已经很好理解。TCP是面向连接的那连接是什么这里的连接并不是实实在在的连接而是通信双方彼此之间的一个记录。TCP是一个全双工通信也就是可以互相发送数据所以双方都需要记录对方的信息。根据前面的可靠传输原理TCP通信双方需要为对方准备一个接收缓冲区可以接收对方的数据、记住对方的socket知道怎么发送数据、记住对方的缓冲区来调整自己的窗口大小等等这些记录就是一个连接。在运输层小节中讲到运输层双方通信的地址是采用socket来定义的TCP也不例外。TCP的每一个连接只能有两个对象也就是两个socket而不能有三个。所以socket的定义需要源IP、源端口号、目标IP、目标端口号四个关键因素才不会发生混乱。假如TCP和UDP一样只采用目标IP目标端口号来定义socket那么就会出现多个发送方同时发送到同一个目标socket的情况。这个时候TCP无法区分这些数据是否来自不同的发送方就会导致出现错误。既然是连接就有两个关键要点建立连接、断开连接。建立连接建立连接的目的就是交换彼此的信息然后记住对方的信息。所以双方都需要发送彼此的信息给对方但前面的可靠传输原理告诉我们数据在网络中传输是不可靠的需要对方给予我们一个确认回复才可以保证消息正确到达。如下图机器B的确认收到和机器B信息可以进行合并减少次数而且发送机器B给机器A本身就代表了机器B已经收到了消息所以最后的示例图是步骤如下机器A发送syn包向机器B请求建立TCP连接并附加上自身的接收缓冲区信息等机器A进入SYN_SEND状态表示请求已经发送正在等待回复机器B收到请求之后根据机器A的信息记录下来并创建自身的接收缓存区向机器A发送synack的合成包同时自身进入SYN_RECV状态表示已经准备好了等待机器A 的回复就可以向A发送数据机器A收到回复之后记录机器B 的信息发送ack信息自身进入ESTABLISHED状态表示已经完全准备好了可以进行发送和接收机器B收到ACK数据之后进入ESTABLISHED状态。三次消息的发送称为三次握手。断开连接断开连接和三次握手类似直接上图1.机器A发送完数据之后向机器B请求断开连接自身进入FIN_WAIT_1状态表示数据发送完成且已经发送FIN包FIN标志位为12.机器B收到FIN包之后回复ack包表示已经收到但此时机器B可能还有数据没发送完成自身进入CLOSE_WAIT状态表示对方已发送完成且请求关闭连接自身发送完成之后可以关闭连接3.机器B数据发送完成之后发送FIN包给机器B 自身进入LAST_ACK状态表示等待一个ACK包即可关闭连接4.机器A收到FIN包之后知道机器B也发送完成了回复一个ACK包并进入TIME_WAIT状态TIME_WAIT状态比较特殊。当机器A收到机器B的FIN包时理想状态下确实是可以直接关闭连接了但是我们知道网络是不稳定的可能机器B 发送了一些数据还没到达比FIN包慢同时回复的ACK包可能丢失了机器B会重传FIN包如果此时机器A马上关闭连接会导致数据不完整、机器B无法释放连接等问题。所以此时机器A需要等待2个报文生存最大时长确保网络中没有任何遗留报文了再关闭连接5.最后机器A等待两个报文存活最大时长之后机器B 接收到ACK报文之后均关闭连接进入CLASED状态双方之间4次互相发送报文来断开连接的过程就是四次挥手。现在对于为什么握手是三次挥手是四次、一定要三次/四次吗、为什么要停留2msl再关闭连接等等这些问题就都解决了。UDP协议 运输层协议除了TCP还有大名鼎鼎的UDP。如果说TCP凭借他完善稳定的功能独树一帜那UDP就是精简主义乱拳打死老师傅。UDP只实现了运输层最少的功能进程间通信。对于应用层传下来的数据UDP只是附加一个首部就直接交给网络层了。UDP的头部非常简单只有三部分源端口、目标端口端口号用来区分主机的不同进程校验码用于校验数据包在传输的过程中没有出现错误例如某个1变成了0长度报文的长度所以UDP的功能也只有两个校验数据报是否发生错误、区分不同的进程通信。但TCP的功能虽然多但同时也是要付出相对应的代价。例如面向连接的特性在建立和断开连接的时候会有开销拥塞控制的特性会限制传输的上限等等。下面来罗列一下UDP的优缺点UDP的缺点无法保证消息完整、正确到达UDP是一个不可靠的传输协议缺少拥塞控制容易互相竞争资源导致网络系统瘫痪UDP的优点效率更快不需要建立连接以及拥塞控制连接更多的客户没有连接状态不需要为每个客户创建缓存等分组首部字节少开销小TCP首部固定首部是20字节而UDP只有8字节更小的首部意味着更大比例的数据部分在一些需要高效率允许可限度误差的场景下可以使用。如直播场景并不需要保证每个数据包都完整到达允许一定的丢包率这个时候TCP的可靠特性反而成为了累赘精简的UDP更高的效率是更加适合的选择可以进行广播UDP并不是面向连接的所以可以同时对多个进程进行发送报文UDP适用场景UDP适用于对传输模型需要应用层高度自定义、允许出现丢包、需要高效率的场景、需要广播例如视屏直播DNSRIP路由选择协议其他补充 分块传输我们可以发现运输层在传输数据的时候并不是把整个数据包加个首部直接发送过去而是会拆分成多个报文分开发送那他这样做原因是什么有读者可能会想到数据链路层限制了数据长度只能有1460。那数据链路层为什么要这么限制他的本质原因就是网络是不稳定的。如果报文太长那么极有可能在传输一般的时候突然中断了这个时候就要整个数据重传效率就降低了。把数据拆分成多个数据报那么当某个数据报丢失只需要重传该数据报即可。那是不是拆分得越细越好报文中数据字段长度太低会使得首部的占比太大这样首部就会成为网络传输最大的负担了。例如1000字节每个报文首部是40字节如果拆分成10个报文那么只需要传输400字节的首部而如果拆分成1000个那么需要传输40000字节的首部效率就极大地降低了。路由转换先看下图正常情况下主机A的数据包可以又 1-3-6-7路径进行传送如果路由3坏掉了那么可以从 1-4-6-7进行传送如果4也坏掉了那么只能从2-5-6-7传送如果5坏掉了那么就中断线路了可以看出来使用路由转发的好处是提高网络的容错率本质原因依旧是网络是不稳定的 。即使坏掉几个路由器网络依旧畅通。但是如果坏掉路由器6那就直接导致主机A和主机B无法通信所以要避免这种核心路由器的存在。使用路由的好处还有分流。如果一条线路太拥堵可以从别的路线进行传输提高效率。粘包与拆包在面向字节流那一小节讲过TCP不懂这些数据流的意义他只知道从应用层拿到数据流切割成一份份报文然后发送给目标对象。而如果应用层传输下来的是两个数据包那么极有可能出现这种情况应用层需要向目标进程发送两份数据一份音频一份文本TCP只知道接收到一个流并把流拆分成4段进行发送中间第二个报文的数据就出现两个文件的数据混在一起这就是粘包目标进程应用层在接收到数据之后需要把这些数据拆分成正确的两个文件就是拆包粘包与拆包都是应用层需要解决的问题可以在每个文件的最后附加上一些特殊的字节如换行符或者控制每个报文只包含一个文件的数据不足的用0补充等等。恶意攻击TCP的面向连接特点可能会被恶意的人利用对服务器进行攻击。前面我们知道当我们向一个主机发送syn包请求创建连接时服务器会为我们创建缓冲区等然后向我们返回synack报文如果我们伪造IP和端口向一个服务器进行海量的请求会使得服务器创建了大量的创建一半的TCP连接使得其无法正常响应用户的请求导致服务器瘫痪。解决的方法可以有限制IP的创建连接数、让创建一半的tcp连接在更短的时间内自行关闭、延缓接收缓冲区内存的分配等等。长连接我们向服务器的每一次请求都需要创建一个TCP连接服务器返回数据之后就会关闭连接如果在短时间内有大量的请求那么频繁创建TCP连接关闭TCP连接是一个很浪费资源的行为。所以我们可以让TCP连接不要关闭在这个期间进行请求提高效率。需要注意长连接维持时间、创建条件等避免被恶意利用创建大量的长连接消耗殆尽服务器的资源。最后 以前学习的时候觉得这些东西好像没什么卵用貌似就是用来考试的。事实上在没应用到的时候对这些知识很难有更深层次的认知例如现在我看上面的总结很多只是表面上的认知不知道他背后代表的真正含义。但当我学习的更加广泛、深入会对这些知识有越来越深刻的认识。有那么几个瞬间觉得哦原来那个东西是这样运用那个东西是这样的啊原来学了是真的有用。现在可能学了之后没有什么感觉但是当用到或者学到相关的应用时会有一个顿悟感会瞬间收获很多。来源https://juejin.cn/user/3931509313252552/postse往期推荐read 文件一个字节实际会发生多大的磁盘IO如何优雅保护 Kubernetes 中的 SecretsRedis 内存满了怎么办这样置才正确云原生的本手、妙手和俗手点分享点收藏点点赞点在看