当一个网络包从一个网口经过时,可以把连接拿进来处理,有的网口配置了混杂模式,凡是经过的都处理。
当进入后调用process_layer2(buffer)
,当然这只是个名称,这个函数具体的工作内容是,从Buffer中摘掉二层的头,看看根据里面的内容做什么操作。
假设这个包的MAC地址和你的相符,那说明是给你的,调用`process_layer3(buffer)。这个时候Buffer里面往往就没有二层的头了,因为已经在上一个函数的处理过程中拿掉了,或者将开始的偏移量移动了一下。在这个函数里摘掉三层的头,看看是发给自己的,还是希望转发出去的。
如果IP地址不是自己的那就转发出去。根据IP头里面的标示,拿掉三层头,进行下一步处理。
如果是TCP的,则会调用process_tcp(buffer)
。这时候Buffer里面没有三层的头了,需要查看四层的头,看这是一个发起还是一个应答,又或者是一个正常的数据包,然后分别由不同的逻辑处理。如果是发起或者应答,接下来可能要发送一个回复包;如果是一个正常的数据包,就需要交给上层了。
即交给应用去处理。在四层的头里面有端口号,不同的应用监听不同的端口号。如果发现浏览器正在监听这个端口,那发给浏览器就行了。
浏览器解析HTML,显示出页面。浏览器会捕获点击动作,并发起另一个HTTP请求,使用端口号将请求发送出去。
然后调用send_tcp(buffer)
。Buffer里的内容就说HTTP请求的内容。这个函数里面加上TCP头,记录下源端口号。
然后调用send_layer3(buffer)
。在新增加一个IP头,记录下源IP地址和目标IP地址。
然后调用send_layer2(buffer)
。Buffer里面已经有了HTTP的头和内容、TCP的头,以及IP的头。这个函数里面要再增加一个MAC头,记录下源MAC地址,得到的就是本机MAC地址和目标MAC地址。不知道目标MAC地址的话可以通过协议获得。
只要是在网络上跑的包,都是完整的。可以有下层没上层,绝对不可能有上层没下层。所以对于TCP协议来说,三次握手也好,重试也好,只要想发出包,就要有IP层和MAC层,不然是发不出去的。
一般认为,TCP是面向连接的,UDP是面向无连接的。
所谓的建立连接,是为了在客户端和服务端直接维护连接,建立一定的数据结构维护双方交互状态,用这样的数据结构来保证所谓的面向连接的状态。
TCP提供可靠交互。通过TCP发送的包,无差错、不丢失、不重复、并且按顺序到达。IP包是没有任何可靠性保障的,一旦发出去就只能随它去。UDP继承了IP包的特性,不保证无差错,不保证按顺序到达。
TCP是面向字节流的。发送的时候是一个流,没头没尾,因为TCP已经维护了一个状态。UDP是基于数据包的,一个包一个包的发,一个一个的接。
TCP可以拥有阻塞控制。可以意识到包丢失或者网络环境不好,可以根据情况调整自身的行为,发送快慢。UDP不会,应用让发就发。
TCP是一个有状态的服务,里面记录了发送了没有,接到了没有,发到哪了,接到哪里了,错一点都不行。UDP发出去就是发出去了,不会记录其他信息。
UDP三大特点:
UDP三大使用场景:
UDP使用案例:
TCP是靠谱的协议,从IP层面来讲,如果网络实在差,作为IP的上一层TCP也无能为力,唯一能做的就是通过算法各种重传;对于TCP来讲,IP层你丢不丢包我不管,但是在我的层面尽量保持可靠。
也即,"请求->应答->应答之应答"。
建立在假设网络通路非常不可靠的基础是,A发起一个连接,当第一个链接杳无音信的时候,有很多种可能性,包丢了、超时了、绕路了、B没有响应等。
当B应答请求的时候也面临同样的问题,所以至少要等到A再次回答的时候才能建立连接。
三次握手除了建立连接外,主要还为了沟通一件事情,TCP包的序号的问题。
A要告诉B,我这面发起的包的序号是多少,B也同样要告诉A。为什么不每次都从1开始呢?因为怕出现冲突。
比如A连接上B后,发送了1,2,3三个包,但是发送3的时候断线了,过后当A重新连接上B之后当然不能发送第四个包继续以1为序号发送。
因此,每个连接都要有不同的序号。这个序号可以看成是随着时间变化的,可以看成一个32位的计数器,每4ms加一,如果要重复序号需要4个多小时。
也即,"A:请求下线->B:应答下线->B:完成工作并下线->A:收到下线"。
当A发送下线请求的时候,只是A要下线,并且不再发送数据,但是此时B有可能还没有做完自己的事情,还是可以发送数据的,A等B做完工作并主动关闭。
这是正常情况下的关闭连接,但是其中后两步可能会出现问题,比如一方先跑路了,这时就可能出现另一方无限等待的可能,需要设置超时时间来解决。
为了保证顺序性,每个包都有一个ID,在建立连接的时候会商定起始ID是什么,然后会按照顺序发送。为了防止丢失,发送的包都要应答,但是不是每个包都应答一次,而是会应答某个ID之前的所有包,这种模式成为累计应答。
为了记录所有的发送包和接收包,TCP需要发送端和接收端分别缓存这些记录。
发送端记录分为四种:
接收端记录分为三种:
一种方法是超时重试,对每一个发送了,但是没有收到回复的包都设有一个定时器,超时重试。这个超时时间需要由自适应重传算法根据网络状况变动取样算出。
即网络拥塞问题,发送方如何判断网络是否满呢?对于TCP来讲,网络对他来说就是一个黑盒。TCP发送包常被比喻为往一个水管里注水,而TCP的拥塞控制就是,在不拥堵,不丢包的情况下,尽量发挥带宽。
如果我们设置发送窗口,使得发送但是未确认的包正好等于通道容量,就能装满整个管道。
TCP的拥塞控制就是用来避免包丢失和超时重传。一旦出现这两个问题就说明速度太快了。
这个问题有点像往瓶子里灌水,一开始要慢慢的倒,之后加速,也叫慢启动。
但是仍然有两个问题:
第一个是,丢包不代表管道满了,有可能本来就是管子漏。这个时候认为管道满了就不对。
第二个是,TCP的中间缓存设备都填满了,才发生丢包,降低速度,这事已经晚了,应该在管道满了的时候就降低速率,而不是等缓存满了才降低。
为了优化这两个问题,后来有了TCP BBR拥塞算法。它企图找到一个平衡点,就是不断加速发送,将管道填满,但是不要填满中间设备的缓存,因为这样会增加延迟,在这个平衡点可以达到很高的带宽有和低延迟的平衡。
当请求一个URL的时候,浏览器会将域名发送给DNS服务器,并获取到IP地址。HTTP协议是基于TCP协议的,所以接下来先建立TCP连接,三次握手。并且在可以在多次请求中复用TCP连接。
请求的格式:
HTTP的报文分三部分,请求行,首部,正文实体。
已经拼凑好了发送格式,接下来浏览器就会把它交给传输层。
HTTP是基于TCP的,所以使用面向连接的方式发送请求,通过stream二进制流方式传给对方。当到达TCP层时,会把二进制流转换为报文段发送给服务器。
TCP层没法送一个报文的时候,都需要加上自己的地址和目标地址,将这两个信息放到IP头里,交给IP层进行传输。
IP层需要查看自己和目标地址是不是在同一个局域网,如果是,就发送ARP请求获取目标地址的MAC地址,将源MAC和目标MAC放入MAC头发送出去即可。如果不是在同一个局域网,和刚才的步骤一样,只不过是请求网关的MAC并发送给网关。
网关收到包发现MAC符合,取出目标IP,根据路由协议找到下一目标路由器,并获取路由器的MAC地址,然后发出去。
经过多次路由,最终达到了目标局域网。在局域网内获取目标MAC地址并发出去。
目标机器发现MAC符合,就将包收进来;发现IP符合,根据IP中的协议项,知道了上层是TCP协议,于是解析TCP头,里面有序号,看看这一系列包是不是我要的,如果是就放入缓存中并返回一个回复ACK,如果不是就丢弃。
TCP头里面还有端口号,HTTP的服务器正在监听这个端口号。于是目标机器将包发送给服务器,服务器看到是要请求一个网页,于是把这个网页发给客户端。
构建好了返回的HTTP报文,接下来就是把报文发出去。还是交给socket发送,还是交给TCP层,让TCP将返回的HTML也分成一个个小段,并保证每个段都可以到达。
返回的过程和发送过程一样,虽然两次不一定走相同的路径,但是逻辑一样,并且到达客户端。
HTTP2.0对HTTP头进行一定压缩,将原来每次都要携带的key/value在两端建立索引表,相同的头只发送索引即可。
并且将一个TCP连接中,切分成多个流,每个流都有自己的ID。
还将所有的传输信息分割为更小的消息和帧,并对他们采用二进制格式编码。
通过这两种机制,HTTP2.0的客户端可以将多个请求分到不同的流中,然后将请求拆分成帧,进行二进制传输。帧可以打乱顺序,然后根据标示重新组装,并且可以根据优先级决定先处理哪个流的数据。
QUIC协议通过基于UDP自定义类似TCP的连接、重试、多路复用、流量控制技术进一步提升性能。
非对称加密的公匙私匙用来传递对称加密的密匙,真正双方大数据量的通信是通过对称加密进行的。
过程除了加密解密之外,其他过程和HTTP是一样的。
可能会遇到重放和篡改问题。有了加密,黑客截获了包也打不开,但是可以重发N次。这个解决方案往往是通过Timestamp和Nonce随机数联合起来,做一个不可逆的签名来保证。
内容主要来自极客时间