Spring中的aware接口详情
335
2022-10-07
深入理解web协议(二):DNS、WebSocket(哪两项协议用于控制从web)
本文系统性的讲述了 DNS 协议与 WebSocket 协议的重要细节。
一、DNS
1、Linux dig命令
我们首先通过 Linux 下的dig命令来了解一下 DNS 是怎么做域名解析的。我们首先输入命令:
dig baidu.com
看下标注的红框,从左到右依次代表:
域名的名称 也就是服务器名称 网络类型,DNS协议在设计的时候考虑到了其他网络类型,但是目前位置这个值还是写死的IN 你就理解成是互联网就可以了。这个值一般不变 标识域名对应何种类型的地址,A 就代表ip的地址。
这里可能有人会问了,这个域名的后面为啥还有个“.”?我们输入的明明是baidu.com不是baidu.com.啊 。
这里要提一下:
末尾的.代表的就是根域名,每个域名都有根域名,所以通常我们会省略它。
根域名的下一级叫顶级域名,比如我们熟知的.com与.net。
再下一级就是次级域名了,比如例子中的.baidu。这个次级域名只要你有钱是可以随便注册的。
最后这个Wireshark的捕获过滤器无法设置DNS协议,又因为DNS是基于 UDP协议的,所以这里捕获过滤器我们就设置为 UDP就好。
然后就可以在一堆 UDP报文中找到我们想看的DNS报文了,我们在浏览器中输入airbnb.com:
这里要注意左边有两个箭头,向右的箭头代表“请求”,向左的箭头就代表该“请求”的回复了。
这些DNS报文经过 Wireshark的解析以后,格式已经帮我们分析好了,所以看起来很清晰。也很简单。这里我们不再详细分析DNS的二进制报文格式,有兴趣的可以自行查找相关资料。在我们上述展示的DNS报文抓包截图的时候,细心的同学已经发现了,我们DNS报文的查询地址是172.22.3.102。一般而言,大部分公司内部网络都会提供一个统一的DNS服务器,这个地址就是内部的DNS服务器地址了,有图为证:
我们当然也可以使用其他DNS查询,比如使用著名的谷歌DNS
3、传统DNS服务查询的缺点
经过上述的分析看起来DNS的查询过程好像比较简单,但实际上DNS带来的性能或者安全问题很多很多。我们首先来还原一下完整的DNS查询过程(假设我们想访问csdn的网站):
通过上述的一次完整的DNS交互过程,我们可以至少得出三个结论:
DNS服务器是可以做负载均衡的。当然前提条件是你这个域名得自己建一个DNS服务器。一般大厂都会自建。 DNS的查询是一个递归的过程,弱网的情况,这个时间会变的很漫长。且DNS使用的是 UDP传输协议,弱网有直接查询失败的可能。 DNS的查询过程不可控,比如说本地DNS服务器完全可以返回一个错误的ip地址。比如你访问了一个京东的链接,然后返回给你的ip地址是拼多多的。
这还只是表面上看出来的传统DNS查询的缺点,实际上现在我们每天大部分的流量都来自于移动网络。移动网络中,传统DNS服务暴露出来的问题更多:
前文我们说过本地DNS服务器会缓存域名的ip,但这个缓存时效我们控制不了,全靠运营商的操守。有可能发生我们ip地址已经变化,但是本地DNS服务器返回的还是老ip的情况。 有些运营商为了节省运营商和运营商之间的流量计算成本,会偷偷的将一些静态页面缓存。当用户访问这些页面的时候 往往访问的是这些静态页面的缓存服务器的地址。此时不管我们的页面更新了多少内容,对于用户来说都是老的页面。 运营商在某些场景,例如人口集中的地铁站,演唱会,足球场附近等等,一旦发现自己的用户太多,本地DNS服务器压力巨大的时候,就会手动设置将本地DNS服务器向根域名服务器 查询 然后递归查询 DNS的过程 修改成:直接向另外一个运营商(假设这个运营商名字为B)的DNS服务器地址进行查询,B的DNS服务器就会返回一个B的地址,此时运营商A的用户访问的ip地址就是运营商B的ip了,这种跨运营商访问的场景速度会非常慢。 某些宽带提供方的NAT服务非常不稳定,大家都知道我们在家上网的时候 本机地址其实就是一个内网地址,我们之所以能访问外部的网络是因为这些宽带提供方提供了一层网关来负责NAT,这个NAT会将我们的内网地址转换成一个外网的地址,NAT之后的ip,某些权威DNS服务器就无法判断这个ip到底属于哪个运营商。也会带来跨运营商访问的问题。 除了自己的DNS服务器,其他公共DNS服务器的缓存时效都不可控,这对双机房部署,异地多活,多域名等策略都会有影响。 弱网环境下,因为DNS使用的传输协议是不可靠的 UDP,又因为DNS查询的过程是一个递归的过程,所以DNS查询在弱网环境下是有概率失败的。
4、HTTPDNS
基于上述缺点,越来越多的大厂使用了HTTPDNS的这种技术(据腾讯的公开资料显示,15年腾讯每天的localDNS失败次数就达到了80w次,接入HTTPDNS以后,用户平均访问延迟下降超过10%,访问失败率下降了超过五分之一,用户访问体验的效果提升非常显著):
这种技术的原理其实挺简单的,无非就是让我们的手机App 发起一个HTTP请求(这个请求地址多数使用ip直连,如果使用域名那么依然针对此请求依然有传统DNS的问题),这个请求可以携带用户所在的运营商,地理位置,精确到省市,然后服务器根据这些信息 返回一个最佳的ip地址给App,然后App将这个域名-ip的映射关系设置到我们的okwebview DNS的源码,而iOS这点做的显然就比Android好一些,对于iOS来说webview的HTTP就是一个正常的HTTPrequest 与原生的代码并没有任何区别。对于Android客户端来说,接入HTTPDNS也不是一件特别容易的事。即使现在拥有了ok我们直接用的ip 但是 服务端返回的set cookie头部信息却带上的域名,这里也要额外处理。优点:因为是拦截器的实现机制,所以很容易做开关进行降级处理。
方案二:
通过okclass HttpDNS implements DNS {
private static final DNS SYSTEM = DNS.SYSTEM;
@Override
public List
这种方案就不存在拦截器哪种缺点,因为本质上这种方案和系统的DNS查询方案并无二致,无非系统的是 UDP去localDNS找,我们的是用HTTP去 HTTP服务器上找。这种方案可以解决方案一的所有缺点,但是有一个问题就是一旦这个HTTPDNS返回的结果有问题,那么很难降级。且okDNS服务器返回的地址有误,那么在一定时间范围内后续针对这个域名的访问都会有问题。
前面我们说过Android自身webview的机制导致HTTPDNS很难在webview中起到作用,但是仍旧有一些方法可以尽量规避掉webview中loacalDNS速度慢的缺点。例如我们可以在html中设置预加载静态资源的DNS请求,而不用等到真正请求这些资源的时候才会查找DNS。
考虑到实际上webview和App自身代码使用的DNS缓存都是操作系统中的同一块存储区域,我们也可以统计出我们常用web页面中频繁请求的url的域名,在App一启动的时机,就提前访问这些域名,这样等到热点web页面在加载的时候,如果操作系统DNS缓存已经有了对应的ip,则可以省略一次DNS的查询。
5、DNS真的是基于UDP协议的吗?
其实DNS协议真的不是完全基于UDP协议的,DNS的协议里面其实有主DNS服务器和辅DNS服务器的概念,辅DNS服务器在启动的时候会主动去主DNS服务器上拉取最新的该区域DNS信息。这个拉取的过程采用的就是TCP协议,而不是UDP协议。也就是协议文档中说的zone transfer。
这里有人可能会想到,为什么不用UDP协议来完成这个过程,因为UDP协议最大只能传送512个byte的数据,而辅DNS要拉取该区域的DNS信息很容易就超过这个最大报文数量的限制,所以这里采用的就是TCP协议来完成拉取数据的操作。
二、WebSocket
1、有HTTP轮询为什么还需要 WebSocket 技术?
很多人不明白为什么一定要用 WebSocket,明明我轮询HTTP请求一样可以完成需求。这句话本身并不错,可以用 WebSocket 的地方确实全部都可以用轮询HTTP请求来替代。但是其背后的效率却天差地别。
我们可以把 WebSocket 看成是 HTTP 协议为了支持长连接所打的一个大补丁,它和 HTTP 有一些共性,是为了解决 HTTP 本身无法解决的某些问题而做出的一个改良设计。在以前 HTTP 协议中所谓的 keep-alive 长连接是指在一次 TCP 连接中完成多个 HTTP 请求,但是对每个请求仍然要单独发 header;所谓的轮询是指从客户端不断主动的向服务器发 HTTP 请求查询是否有新数据。这种模式有三个缺点:
除了真正的数据部分外,服务器和客户端还要大量交换 HTTP header,信息交换效率很低。 因为HTTP是无状态的,每次请求服务端都要通过客户端传递来的参数来查询这个请求到底是谁的,例如每次都要查询一下这个userId下面有多少存款,买过几部手机等等,对服务器的宝贵的计算资源是一种浪费。 轮询的时间间隔不好设置,设置高了,用户的界面响应不及时,设置的太低,又怕流量消耗大,服务器扛不住。
当然轮询也有优点就是实现成本极低,几乎不需要客户端和服务端有额外的开发成本。WebSocket在首次使用的时候还是需要做一些基础设施改造的(例如nginx相应的配置)。WebSocket的实现成本:虽然说现代服务器编程中默认都提供了WebSocket的实现,但是我们知道考虑到扩展性等因素,我们通常都不会直接和源服务器打交道,而是和代理服务器打交道。对WebSocket来说同样如此,所以这里对于首次实现WebSocket的团队是有一定技术成本。
上图是一个简单的服务器架构图,客户端发出去的请求经过一台专门做负载均衡的代理服务器以后将这些请求逐一转发到对应的源服务器上。而对于WebSocket来说 情况则变的稍微有点复杂:
相比纯 HTTP来说,WebSocket通常会增加一层专门的消息分发系统提高消息的处理效率。通常是Kafka或者是RabbitMQ。
2、Wireshark解析WebSocket报文
首先 来看一下WebSocket的帧格式。我们首先设置一下 Wireshark的捕获器:
设置一下我们想要捕获的域名和端口号。注意WebSocket是可以复用HTTP端口号的。connect,然后发消息,服务器返回我们发送的消息,最后我们主动断开连接。
WebSocket是一个基于帧的协议,所以这里我们着重分析一下WebSocket的帧格式,每个帧头部的 第4-第7个 bit位,这4个bit 代表的就是Opcode,比较重要的就是几个值:
2:代表这是二进制帧, 1:代表这是一个文本帧, 8:代表关闭帧。
3、WebSocket连接的建立过程
这里有人就要问了,既然WebSocket是能保证长连接(tcp)的,那么这条长连接是由谁发起的?看下图:
这个抓包结果显示的说明了WebSocket下面使用的tcp连接是交给HTTP1.1来发起的。来详细看一下,这里我用箭头标注的都是必须要设置的HTTP头部信息,否则是无法完成WebSocket连接的建立的。
此外我们还需要注意Sec-WebSocket-Accept,和Sec-WebSocket-Key 这2个头部信息。
客户端生成一个随机数以后用base64加密以后放到Sec-WebSocket-Key头部信息中,然后服务器接受到这个信息,用这个值与rfc中规定的一个魔法字符串:“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”拼起来,然后再使用sha-1加密 再经过base64 以后计算出来的值 放到Sec-WebSocket-Accept头部中返回给客户端。
这么做的原因是带来一些基础的保障,前面我们说过WebSocket连接的建立是依托HTTP消息的,为了防止这个WebSocket连接的建立是调用者无心误触发或者其他异常情况,所以这里有一次额外的数据校验的过程。
4、WebSocket连接的断开过程
看完连接,我们再看一下断开连接,与WebSocket的连接不同,WebSocket的断开连接是有明确步骤的,需要先断开WebSocket的连接,然后才是tcp的断开连接。
可以看出来,断开连接的步骤是客户端先发了一个断开连接的帧,然后服务端再给客户端发一个确认断开WebSocket连接的帧。最后就是tcp的四次挥手了,保证了tcp连接的彻底断开。
另外HTTP1.1中保持长连接的方法其实就是一个定时器,定时器大概时间为60s,如果60s都没有HTTP消息,那么这个tcp连接就断掉了。WebSocket虽然也是利用了HTTP1.1的消息来保证tcp的连接,但是保证这条tcp连接不被断开的方法却又不是定时器了,与mqtt xmpp等协议类似,WebSocket保持长连接的方法也是利用了心跳包。
在RFC协议中,规定了opcode 为0x9 0xA的帧为心跳帧,但是往往 这个关于心跳包的协议却很少有人遵守,很多时候人们选择间隔一段时间发送一个任意帧(当然这个任意帧的内容需要客户端和服务端提前约定好)来保证心跳包的建立。比如前文中我们拿来做例子的而是:
图中可以看出来这个心跳包大概是30s发送一次,而且并没有使用rfc中约定好的0x9或者0xA的所谓ping pong的心跳帧,而是就用了最简单的文本帧来表示。
上图所示,左边的就是WebSocket 服务端发起的心跳包,opcode的值还是text文本帧的意思,只不过文本的内容很特殊。右边就是WebSocket客户端回复的心跳包。
5、WebSocket的代理缓存污染
这里要注意的是 Wireshark抓包的时候,最右边有一个masked的标识,这通常代表这一个WebSocket的帧是由客户端发送给服务端的,这是一个掩码的标识。在WebSocket协议中只要是客户端发起的消息,都必须经过这个随机的masking-key的掩码计算之后才能传输。这是为了解决代理缓存污染的问题。
注意这里问题的核心是实现不当的代理服务器,所谓实现不当的代理服务器就是指没有完整实现好WebSocket协议的代理服务器。而不是真正意义上恶意的代理服务器,恶意的代理服务器,用mask帧的技术是无法避免的。
所谓mask掩码技术就是指浏览器在发送WebSocket帧的时候必须生成一个随机的mask-key,在帧的二进制中将传输的内容与这个mask-key做异或操作。得出来的值才可以在网络中传输。
当我们的服务器接收到这个WebSocket帧以后就可以用这个mask-key来反异或,从而就可以得出真正的内容了,这是最低成本实现检测WebSocket帧是否遭到篡改的方案。例如:我们用WebSocket 传输一个 文本帧,内容为字符串vivo,vivo的ascii码的16进制为:76 69 76 6f。而这个消息,这次浏览器生成的mask-key 为 23 68 c0 a3。
我们将这2个值进行异或操作:
可以得到值为55 01 b6 cc。然后看一下抓包的帧内容里面是不是这个值:
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。
发表评论
暂时没有评论,来抢沙发吧~