浏览器是怎样向网卡发送数据的

疯狂的技术宅 前端先锋

从浏览器到浏览器内核


当我们在浏览器的地址栏中输入地址并回车后,浏览器可能会做一些预处理,比如 Chrome 会根据历史统计来预估所输入字符对应的网站,比如输入了“bai”,根据之前的历史发现会有很大的概率会访问 www.baidu.com ,因此就会在输入回车前就马上开始建立 TCP 链接甚至渲染了,这里面还有很多其它策略,感兴趣的同学推荐阅读 High Performance Networking in Chrome(http://aosabook.org/en/posa/high-performance-networking-in-chrome.html)。

接着是输入 URL 后的“回车”,这时浏览器会对 URL 进行检查,首先判断协议,如果是 http 就按照 Web 来处理,另外还会对这个 URL 进行安全检查,然后直接调用浏览器内核中的对应方法,比如 WebView 中的 loadUrl 方法。

在浏览器内核中首先会检查缓存,然后设置 UA 等 HTTP 信息,接着调用不同平台下的网络请求的方法。

浏览器和浏览器内核是两个不同的概念,浏览器指的是 Chrome、Firefox,而浏览器内核则是 Blink、WebKit、Gecko等,浏览器内核只负责渲染,GUI 及网络连接等跨平台工作则是由浏览器实现的

发送HTTP 请求


因为网络的底层实现与内核相关,所以在这里需要针对不同平台进行处理,从应用层角度来看主要是做两件事情:

  1. 通过 DNS 查询 IP
  2. 通过 Socket 发送数据 接下来就分别介绍这两方面的内容。

DNS 查询


DNS 查询其实是基于 UDP 来实现的,这里我们通过一个具体例子来了解它的查找过程,以下是使用 dig fex.baidu.com +trace 命令得到的结果:


 1> dig fex.baidu.com +trace
 2
 3; <<>> DiG 9.11.4-P2-RedHat-9.11.4-16.P2.el7_8.6 <<>> fex.baidu.com +trace
 4;; global options: +cmd
 5.            11950   IN  NS  f.root-servers.net.
 6.            11950   IN  NS  k.root-servers.net.
 7.            11950   IN  NS  l.root-servers.net.
 8.            11950   IN  NS  m.root-servers.net.
 9.            11950   IN  NS  b.root-servers.net.
10.            11950   IN  NS  c.root-servers.net.
11.            11950   IN  NS  e.root-servers.net.
12.            11950   IN  NS  a.root-servers.net.
13.            11950   IN  NS  g.root-servers.net.
14.            11950   IN  NS  d.root-servers.net.
15.            11950   IN  NS  i.root-servers.net.
16.            11950   IN  NS  h.root-servers.net.
17.            11950   IN  NS  j.root-servers.net.
18;; Received 251 bytes from 192.168.0.1#53(192.168.0.1) in 12 ms
19
20com.            172800  IN  NS  a.gtld-servers.net.
21com.            172800  IN  NS  b.gtld-servers.net.
22com.            172800  IN  NS  c.gtld-servers.net.
23com.            172800  IN  NS  d.gtld-servers.net.
24com.            172800  IN  NS  e.gtld-servers.net.
25com.            172800  IN  NS  f.gtld-servers.net.
26com.            172800  IN  NS  g.gtld-servers.net.
27com.            172800  IN  NS  h.gtld-servers.net.
28com.            172800  IN  NS  i.gtld-servers.net.
29com.            172800  IN  NS  j.gtld-servers.net.
30com.            172800  IN  NS  k.gtld-servers.net.
31com.            172800  IN  NS  l.gtld-servers.net.
32com.            172800  IN  NS  m.gtld-servers.net.
33com.            86400   IN  DS  30909 8 2 E2D3C916F6DEEAC73294E8268FB5885044A833FC5459588F4A9184CF C41A5766
34com.            86400   IN  RRSIG   DS 8 1 86400 20200625050000 20200612040000 48903 . OwfRn9tBOE2btL/z3HG5PQVyTXu2OUcZGLi9svkHFV0tomeI1p9bHhqr GF/UDjf5a8VXNRoaSsSEQfgqwJT3UAOANK1vb3e+5jH2bV3Hg6/MAGG0 SuBfKv8Y1fjGgiLNC3NKmTWJ28WABHngymnGDpuqoC6xKmkVoD14ON7E uHbBGxC0Uxt6D5R3WfbAAfbzZXzyPcD3WK1OpGaL6ASMB2xvdAZIkp/Z l8QDmqZd86RX7haiVhxVG0mMrWxsN7XL2jVyRRFFl9UkApMk9/thPwNK Rgkd4BPCvMPZTvsb+mPZA4InLxP6oPliZQm5sIWH8fEiyS+LgEReROzG sqrpyw==
35;; Received 1173 bytes from 198.97.190.53#53(h.root-servers.net) in 206 ms
36
37baidu.com.        172800  IN  NS  ns2.baidu.com.
38baidu.com.        172800  IN  NS  ns3.baidu.com.
39baidu.com.        172800  IN  NS  ns4.baidu.com.
40baidu.com.        172800  IN  NS  ns1.baidu.com.
41baidu.com.        172800  IN  NS  ns7.baidu.com.
42CK0POJMG874LJREF7EFN8430QVIT8BSM.com. 86400 IN NSEC3 1 1 0 - CK0Q1GIN43N1ARRC9OSM6QPQR81H5M9A NS SOA RRSIG DNSKEY NSEC3PARAM
43CK0POJMG874LJREF7EFN8430QVIT8BSM.com. 86400 IN RRSIG NSEC3 8 2 86400 20200618045106 20200611034106 39844 com. nL7GSwad11x22Ff4/a3sjIA27DplTa0SZWNb9jnTs0+PYEehVKCT4a2g TWgi5YHeqolDbwsK9oy7Hy1ZO3yhlhWUUAIyE5DE+iKuJCnD6fIvmXdq lXsBBvUHK6wtHzIAPJ8PbCAl/PwSNjpZUZvv4YcEtLWU14yTsPPAM/wB BxatwSt88sQrwYrLKqjnojEsmKVX1yi98pdT87BI/zKxzQ==
44HPVU6NQB275TGI2CDHPDMVDOJC9LNG86.com. 86400 IN NSEC3 1 1 0 - HPVVN3Q5E5GOQP2QFE2LEM4SVB9C0SJ6 NS DS RRSIG
45HPVU6NQB275TGI2CDHPDMVDOJC9LNG86.com. 86400 IN RRSIG NSEC3 8 2 86400 20200619061821 20200612050821 39844 com. Iz4sOw47dg/aDbs/T9JSAXDiE88bqoj/kYbDQW5dO9NnQicyC5ZqEj0o l1hxJHirVmJtCIXevkSy3eH1rrOH/Ni+oLlWZEBzQnucFK1C4WdBylF2 0OsgaG/AyHSD+9tWgMcQY+i28WBpxmxXvDLHY0oWb89UHMpcduqCh5+n YXnbOHzjvaER/hX1ljveDo0z+HJIBtgY6/0NeFFY0ZWkcA==
46;; Received 761 bytes from 192.43.172.30#53(i.gtld-servers.net) in 248 ms
47
48fex.baidu.com.        600 IN  CNAME   sugar.n.shifen.com.
49n.shifen.com.        86400   IN  NS  ns5.n.shifen.com.
50n.shifen.com.        86400   IN  NS  ns2.n.shifen.com.
51n.shifen.com.        86400   IN  NS  ns4.n.shifen.com.
52n.shifen.com.        86400   IN  NS  ns3.n.shifen.com.
53n.shifen.com.        86400   IN  NS  ns1.n.shifen.com.
54;; Received 241 bytes from 112.80.248.64#53(ns3.baidu.com) in 32 ms

可以看到这是一个逐步缩小范围的查找过程,首先由本机所设置的 DNS 服务器( 192.168.0.1 )向 DNS 根节点查询负责 .com 区域的域务器,然后通过其中一个负责 .com 的服务器查询负责 baidu.com 的服务器,最后由其中一个 baidu.com 的域名服务器查询 www.baidu.com 域名的地址。

你在查询某些域名的时会可能会发现和上面不一样,最后将会看到有个奇怪的服务器抢先返回结果。。。

这里为了方便描述,忽略了很多不同的情况,比如 127.0.0.1 其实走的是 loopback,和网卡设备没关系;比如 Chrome 会在浏览器启动的时预先查询 10 个你有可能访问的域名;还有 Hosts 文件、缓存时间 TTL(Time to live)的影响等。

通过 Socket 发送数据


有了 IP 地址,就可以通过 Socket API 来发送数据了,这时可以选择 TCP 或 UDP 协议,具体使用方法这里就不介绍了,推荐阅读 Beej’s Guide to Network Programming(http://beej-zhcn.netdpi.net/)。

HTTP 常用的是 TCP 协议,由于涉及到 TCP 协议的具体细节的资料很容易就能找到,所以本文就不赘述了,只在这里谈一下 TCP 的 队首阻塞 问题:假设客户端发送了 3 个 TCP 片段(segments),编号分别是 1、2、3,如果编号为 1 的包传输时丢了,那么即便编号 2 和 3 已经到达也只能等待,因为 TCP 协议需要保证先后顺序,这个问题在 HTTP pipelining 下更严重,因为 HTTP pipelining 可以让多个 HTTP 请求通过一个 TCP 发送,比如发送两张图片,可能第二张图片的数据已经全收到了,但还得等第一张图片的数据传到。

为了解决 TCP 协议的性能问题,Chrome 团队提出了 QUIC 协议,它是基于 UDP 实现的可靠传输,比起 TCP,它能减少很多往返(round trip)时间,还有前向纠错码等功能。目前 Gmail、Google Search、blogspot、Youtube 等几乎大部分 Google 产品都在使用 QUIC,你可以在 Chrome 中的 chrome://flags/#enable-quic 页面找到它的配置。

虽然国内很多大厂也在研究 QUIC 的应用,但离大范围普及还有较长的一段距离,因为如果针对 TCP 进行优化,需要升级系统内核。

浏览器对同一个域名有连接数是有限制得,[一般是 6 个(http://www.browserscope.org/?category=network&v=top),Chrome 团队曾做过实验,发现从 6 改成 10 后性能反而下降了,造成这个现象的因素有很多,如建立连接的开销、拥塞控制等问题,而像 SPDY、HTTP 2.0 协议尽管只使用一个 TCP 连接来传输数据,但性能反而更好,而且还能实现请求优先级。

另外,因为 HTTP 请求是纯文本格式的,所以在 TCP 的数据段中可以直接分析 HTTP 的文本。

Socket 在内核中的实现


前面说到浏览器的跨平台库通过调用 Socket API 来发送数据,那么 Socket API 是如何实现的呢?

以 Linux 为例,它实现在 socket.c(http://lxr.linux.no/linux+v3.14.4/net/socket.c) 中,如果你想深入研究一下,推荐看 Linux kernel map(http://www.makelinux.net/kernel_map/),它标注出了关键路径的函数,方便学习从协议栈到网卡驱动的实现。

底层网络协议的具体例子


接下来如果继续介绍 IP 协议和 MAC 协议可能会把大家搞晕,所以下面用 tcpdump 来通过具体例子讲解,以下是在请求百度首页时抓取到的网络数据:

可以看到最前面的三次通信是 TCP 协议的三次握手过程,在第四次通信中被选中的部分为 HTTP 协议(Hypertext Transfer Protocol),在 HTTP 之前有 54 字节(0x36),这就是底层网络协议所带来的开销,我们接下来对这些协议进行分析。

在 HTTP 之上是 TCP 协议(Transmission Control Protocol),它的具体内容如下图所示:

通过底部的二进制数据,可以看到 TCP 协议是加在 HTTP 文本前面的,它有 20 个字节,其中定义了本地端口(Source port)和目标端口(Destination port)、顺序序号(Sequence Number)、窗口长度等信息,以下是 TCP 协议各个部分数据的完整介绍:


 1 0                   1                   2                   3
 2 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
 3+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 4|          Source Port          |       Destination Port        |
 5+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 6|                        Sequence Number                        |
 7+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 8|                    Acknowledgment Number                      |
 9+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
10|  Data |           |U|A|E|R|S|F|                               |
11| Offset| Reserved  |R|C|O|S|Y|I|            Window             |
12|       |           |G|K|L|T|N|N|                               |
13+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
14|           Checksum            |         Urgent Pointer        |
15+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
16|                    Options                    |    Padding    |
17+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
18|                             data                              |
19+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
20

具体每个字段的作用这里就不介绍了,感兴趣的同学可以通过阅读 RFC 793(http://tools.ietf.org/html/rfc793),并结合抓包分析来理解。

需要注意的是,在 TCP 协议中并没有 IP 地址信息,因为这是在上一层的 IP 协议中定义的,如下图所示:

IP 协议同样是在 TCP 前面的,它也有 20 字节,在这里指明了版本号(Version)为 4,源(Source) IP 为 192.168.1.106,目标(Destination) IP 为 119.75.217.56,因此 IP 协议最重要的作用就是确定 IP 地址。

因为 IP 协议中可以查看到目标 IP 地址,所以如果发现某些特定的 IP 地址,某些路由器就会。。。

但是,光靠 IP 地址是无法进行通信的,因为 IP 地址并不和某台设备绑定,比如你的笔记本的 IP 在家中是 192.168.0.11,但到公司就变成 10.0.11.11 了,所以在底层通信时需要使用一个固定的地址,这就是 MAC(media access control) 地址,每个网卡出厂时的 MAC 地址都是固定且唯一的。

因此再往上就是 MAC 协议,它有 14 字节,如下所示:

当一台电脑加入网络时,需要通过 ARP 协议告诉其它网络设备它的 IP 及对应的 MAC 地址是什么,这样其它设备就能通过 IP 地址来查找对应的设备了。

现在我们搞清了标题中的问题,不过这里面还有大量的细节没介绍,建议大家通过下面的书籍进一步学习。

扩展学习


怎样为你的 Vue.js 单页应用提速

10 个实用的 JavaScript 小技巧

它改变了 JavaScript 的体系结构——Webpack 5 Module Federation

与 JavaScript 模块相关的所有知识点

我们是怎样优化 V8 中的指针压缩的

当一个模块被导入两次时,会发生什么?

ReactJS 与 VueJS:两种流行前端 JS 框架之战

十分钟搞定 TypeScript + webpack 配置

18 个漂亮的 Bootstrap 模板

前端程序员要懂的 UI 设计知识