Menu
Woocommerce Menu

你所不知道的,到页面加载完成的过程中都发生了什么事情

0 Comment


你所不知道的 HSTS

2015/10/24 · HTML5 ·
HSTS

原文出处:
李靖(@Barret李靖)   

很多人听说过也看到过 301、302,但是几乎从来没有看到过 303 和 307
的状态码。今天在淘宝首页看到了 307 状态码,于是摸索了一把。

减少HTTP请求(大型网站优化技术)

2015/11/26 · HTML5 ·
HTTP

原文出处: Kelly   

在网站开发过程中,对于页面的加载效率一般都想尽办法求快。那么,怎么让才能更快呢?减少页面请求 是一个优化页面加载速度很好的方法。上一篇博文我们讲解了
“利用将小图标合成一张背景图来减少HTTP请求”,那么,这一篇博文将讲解 
“ 将图片转成二进制并生成Base64编码,可以在网页中通过url查看图片”。

一、为何选择将图片转成二进制并生成Base64编码,可以在网页中通过url查看图片的方法减少HTTP请求数?

为什么我会讲解
“将图片转成二进制并生成Base64编码,可以在网页中通过url查看图片”
这一种方式来减少HTTP请求,进而优化页面呢?这里呢,是涉及到移动端的图标使用。上一篇博文所讲的方法能否使用于手机端的网页呢?

但是,它会出现一个问题:背景图+css显示图标时,图标本身无法缩放,比如背景图中64px*64px的图标,显示到界面时必须设置icon的大小也是64*64。在PC网页中这通常不会有什么问题,但在移动端设备上就完全行不通。同样是4英寸的手机屏幕,其分辨率有可能是320*400,也可能是640*800,甚至也可能是1920*1080。这样64px*64px的图标在不同的设备上看起来的大小就会差别非常明显。

幸运的是,手机上的浏览器基本对此做了优化,会把设备模拟成更低的分辨率。比如在1136*640的IPHONE
5中获取$(window).width(),取出来的是320而不是640,这样一个宽度为160px的图片占用的是屏幕宽度的一半,而不是1/4。手机设备这样处理是为了解决兼容性问题。除了网页,包括手机上app的界面,在retina屏幕上和非retina屏幕上的大小是完全一样的,都是因为对分辨率做了处理。

但是,移动设备这样的处理方式并不能完全解决问题,因为机器的假设性猜测在很多时候是不合适的,尤其是在android设备中。为了更好地控制元素显示的大小,解决的办法就是用pt代替ps,px是对应屏幕的分辨率,而pt是针对人眼睛实际感觉的大小,无论在何种分辨率的设备上,72pt固定是1英寸。

HTML的img标签元素的src属性不只是可以指定url,也可以指定图片的二进制数据流。然后通过img元素的自动缩放功能,指定img的大小,就可以实现在不同分辨率的设备上显示一致的图标大小。

二、使用Base64编码减少页面请求数

当我们的一个页面中要传入很多图片时,特别是一些小图标,十几K、几K,甚至是字节级别大小的小图标,这些小图标都会增加HTTP请求,假如多了,就会给服务器带来很大的压力。比如要下载一些一两K大的小图标,其实请求时带上的额外信息有可能比图标的大小还要大。所以,在请求越多时,在网络传输的数据自然就越多了,传输的数据自然也就变慢了。而这里,我们采用Base64的编码方式将图片直接嵌入到网页中,而不是从外部载入,这样就减少了HTTP请求。当然了,它有一个小缺点,就是使当前页面的大小变大了(对于优化来说,其实这个可以忽略,影响不大)。看一下下图,小图标大小为2.4k,等待响应时间是14ms,而接受数据,也就是下载时间约为0ms;可想而知,在有大量小图标下载的时候,这样的方式去优化能大大提高网站的性能(在jquery
mobile和天猫的手机站上面都有用到此技术)。

银河国际网址手机版 1

三、开发思路

将小图标放在以icon_开头的文件夹里(以区分不用生成base64的图片的文件夹)—>用程序去遍历文件夹图片
—>将每张图片的base64编码放在一个js对象里—>在HTML页面的img标签里
使用属性 icon-data = ‘图标名(不带后缀)’来显示图片 —>
JS文件写一个函数对icon-data属性进行转换,转换成src属性,然后值就通过icon-data的属性值获得图标名,然后进行相应的替换得到相应图标的base64编码
—> 显示图片

四、代码实现

XHTML

<?php $pathinfo = pathinfo($_SERVER[‘SCRIPT_FILENAME’]);
define(‘ROOT’, $pathinfo[‘dirname’]); function generateIcon_mobile()
{ $imgRoot = ROOT.”/img/mobile”; $iterator = new
DirectoryIterator($imgRoot); foreach ($iterator as $file) { if
($file->isDot()) continue; $filename = $file->getFilename();
//识别出是否以icon_开头的文件夹,如果是,则对此文件夹的图标进行base64编码处理
if ($file->isDir() && 0 === strncasecmp(‘icon_’, $filename, 5)) {
generateIconMobileCallback(“$imgRoot/$filename”, ROOT.”/js/mobile”); } }
} function generateIconMobileCallback($iconDir, $styleSaveDir) {
//保存成js的文件名 $saveName = array_pop(explode(‘/’, $iconDir));
//JS文件保存路径 $styleSavePath = $styleSaveDir.’/’.$saveName.’.js’;
//将当前目录下的所有文件及MD5组成一个识别字符串 $fileMap = array();
$iterator = new DirectoryIterator($iconDir); foreach ($iterator as
$file) { if ($file->isDot()) continue; $fileName =
$file->getFilename(); if ($file->isDir()) {
generateIconMobileCallback($iconDir.’/’.$fileName,
$styleSaveDir.’/’.$fileName); } else { $fileMap[$fileName] =
md5_file($file->getRealPath()); } } ksort($fileMap); $fileMapStr =
json_encode($fileMap); //确保目录可写
ensure_writable_dir($styleSaveDir); //js文件句柄 $wirteHandle =
fopen($styleSavePath, ‘w’); //当前小图标文件夹的相对路径
$iconSaveRelative = substr($iconDir, strlen(ROOT));
//写入,初始化保存数据的对象 fwrite($wirteHandle, “/** icon in dir:
$iconSaveRelative/ */ \nif(typeof(\$iconData) == ‘undefined’)
\$iconData={};”); foreach ($fileMap as $fileName => $md5) {
//当前图片的绝对路径 $fullPathName = “$iconDir/$fileName”;
//取得路径信息 $pathInfo = pathinfo($fullPathName);
//取得文件名(没有后缀) $fileNameNoExt = $pathInfo[‘filename’];
//取得图片信息 $imageSize = getimagesize($fullPathName);
//取得文件的后缀 switch ($imageSize[2]) { case IMAGETYPE_GIF:
$imageType = ‘gif’; break; case IMAGETYPE_JPEG: $imageType = ‘jpg’;
break; case IMAGETYPE_PNG: $imageType = ‘png’; break; default:
$imageType = ‘jpg’; break; } //取得图片资源 $readHandle =
fopen($fullPathName, ‘r’); //将图片转成二进制并生成Base64编码 $base64 =
base64_encode(fread($readHandle, filesize($fullPathName))); //关闭资源
fclose($readHandle); //将Base64编码写入js文件中 fwrite($wirteHandle,
“\n\$iconData.$fileNameNoExt=\”data:image/$imageType;base64,$base64\”;”);
} //最后换个行 fwrite($wirteHandle, “\n”); //关闭资源
fclose($wirteHandle); //处理成功的图标文件夹给予提示 echo
‘<p>’.$iconSaveRelative. ‘ saved</p>’; } /** *
确保文件夹存在并可写 * * @param string $dir */ function
ensure_writable_dir($dir) { if(!file_exists($dir)) { mkdir($dir,
0766, true); @chmod($dir, 0766); @chmod($dir, 0777); } else
if(!is_writable($dir)) { @chmod($dir, 0766); @chmod($dir, 0777);
if(!@is_writable($dir)) { throw new
BusinessLogicException(“目录不可写”, $dir); } } }
generateIcon_mobile(); ?> <!DOCTYPE html> <html>
<head> <title></title> </head> <body>
<br> <br> <br>
<div>我们直接引入所生成的js文件,测试一下是否成功</div>
<br> <div>直接在img标签里加入 icon-data = ‘图标文件名’ 例如
<\img icon-data=”tryit”>,查看效果</div> <br>
<br> <br> <img icon-data=”tryit”> <script
src=”js/mobile/icon_pink.js”></script> <script
src=”js/mobile/jquery.all.min.js”></script> <script
src=”js/mobile/attrHandle.js”></script> </body>
</html>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
<?php
    $pathinfo = pathinfo($_SERVER[‘SCRIPT_FILENAME’]);
    define(‘ROOT’, $pathinfo[‘dirname’]);
 
    function generateIcon_mobile() {
        $imgRoot = ROOT."/img/mobile";
        $iterator = new DirectoryIterator($imgRoot);
        foreach ($iterator as $file) {
            if ($file->isDot()) continue;
            $filename = $file->getFilename();
 
            //识别出是否以icon_开头的文件夹,如果是,则对此文件夹的图标进行base64编码处理
            if ($file->isDir() && 0 === strncasecmp(‘icon_’, $filename, 5)) {
                generateIconMobileCallback("$imgRoot/$filename", ROOT."/js/mobile");
            }
        }
 
    }
 
    function generateIconMobileCallback($iconDir, $styleSaveDir) {
        //保存成js的文件名
        $saveName = array_pop(explode(‘/’, $iconDir));
        //JS文件保存路径
        $styleSavePath = $styleSaveDir.’/’.$saveName.’.js’;
 
        //将当前目录下的所有文件及MD5组成一个识别字符串
        $fileMap = array();
        $iterator = new DirectoryIterator($iconDir);
        foreach ($iterator as $file) {
            if ($file->isDot()) continue;
            $fileName = $file->getFilename();
            if ($file->isDir()) {
                generateIconMobileCallback($iconDir.’/’.$fileName, $styleSaveDir.’/’.$fileName);
            } else {
                $fileMap[$fileName] = md5_file($file->getRealPath());
            }
        }
        ksort($fileMap);
        $fileMapStr = json_encode($fileMap);
 
        //确保目录可写
        ensure_writable_dir($styleSaveDir);
 
        //js文件句柄
        $wirteHandle = fopen($styleSavePath, ‘w’);
        //当前小图标文件夹的相对路径
        $iconSaveRelative = substr($iconDir, strlen(ROOT));
        //写入,初始化保存数据的对象
        fwrite($wirteHandle, "/** icon in dir: $iconSaveRelative/ */ \nif(typeof(\$iconData) == ‘undefined’) \$iconData={};");
        foreach ($fileMap as $fileName => $md5) {
            //当前图片的绝对路径
            $fullPathName = "$iconDir/$fileName";
            //取得路径信息
            $pathInfo = pathinfo($fullPathName);
            //取得文件名(没有后缀)
            $fileNameNoExt = $pathInfo[‘filename’];
            //取得图片信息
            $imageSize = getimagesize($fullPathName);
 
            //取得文件的后缀
            switch ($imageSize[2]) {
                case IMAGETYPE_GIF:
                    $imageType = ‘gif’;
                    break;
                case IMAGETYPE_JPEG:
                    $imageType = ‘jpg’;
                    break;
                case IMAGETYPE_PNG:
                    $imageType = ‘png’;
                    break;
 
                default:
                    $imageType = ‘jpg’;
                    break;
            }
 
            //取得图片资源
            $readHandle = fopen($fullPathName, ‘r’);
            //将图片转成二进制并生成Base64编码
            $base64 = base64_encode(fread($readHandle, filesize($fullPathName)));
            //关闭资源
            fclose($readHandle);
            //将Base64编码写入js文件中
            fwrite($wirteHandle, "\n\$iconData.$fileNameNoExt=\"data:image/$imageType;base64,$base64\";");
        }
        //最后换个行
        fwrite($wirteHandle, "\n");
        //关闭资源
        fclose($wirteHandle);
 
        //处理成功的图标文件夹给予提示
        echo ‘<p>’.$iconSaveRelative. ‘ saved</p>’;  
    }
 
    /**
    * 确保文件夹存在并可写
    *
    * @param string $dir
    */
    function ensure_writable_dir($dir) {
        if(!file_exists($dir)) {
            mkdir($dir, 0766, true);
            @chmod($dir, 0766);
            @chmod($dir, 0777);
        }
        else if(!is_writable($dir)) {
            @chmod($dir, 0766);
            @chmod($dir, 0777);
            if(!@is_writable($dir)) {
                throw new BusinessLogicException("目录不可写", $dir);
            }
        }
    }
    generateIcon_mobile();
?>
 
<!DOCTYPE html>
<html>
<head>
    <title></title>
</head>
<body>
<br>
<br>
<br>
 
<div>我们直接引入所生成的js文件,测试一下是否成功</div>
<br>
<div>直接在img标签里加入 icon-data = ‘图标文件名’  例如  <\img icon-data="tryit">,查看效果</div>
<br>
<br>
<br>
    <img icon-data="tryit">
    <script src="js/mobile/icon_pink.js"></script>
    <script src="js/mobile/jquery.all.min.js"></script>
    <script src="js/mobile/attrHandle.js"></script>
</body>
</html>

然后这里附上属性转换的JS代码

JavaScript

$(function(){ setIconData(); }); function setIconData() { if
(typeof($iconData != ‘undefined’)) {
$(‘img[icon-data]’).each(function() { var self = $(this); var name =
self.attr(‘icon-data’); if (typeof($iconData[name]) != ‘undefined’) {
self.attr(‘src’, $iconData[name]); self.removeAttr(‘icon-data’); } });
} }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$(function(){
    setIconData();
});
 
function setIconData() {
    if (typeof($iconData != ‘undefined’)) {
        $(‘img[icon-data]’).each(function() {
            var self = $(this);
            var name = self.attr(‘icon-data’);
            if (typeof($iconData[name]) != ‘undefined’) {
                self.attr(‘src’, $iconData[name]);
                self.removeAttr(‘icon-data’);
            }
        });
    }
}

 

五、实现效果

  这是页面输入效果,小图标正常显示出来了

 

银河国际网址手机版 2

 

这里我们自动生成的JS文件是这样子的格式:

银河国际网址手机版 3

 

页面调用的代码:

银河国际网址手机版 4

 

JS对img的icon-data属性转换处理的代码:

银河国际网址手机版 5

 

我们对比下用base64编码和不用base64时所花费的时间:

先看不用的速度

银河国际网址手机版 6

再看我们用了base64编码的速度   银河国际网址手机版 7

 

假如一个页面有很多小图标,那么这种方式对网站的性能优化会有大大的提升。如今此种优化方案是用在我现在的项目中移动端,而上一篇博文讲解的生成背景图的优化方案用在我们项目中的PC端。优化效果是很明显的!当然了,base64编码这种方法也可以用在PC端,我们的项目为啥将它用在手机端,本博文开头部分也有对其做解释。这里测试我就直接在PC端测试,手机端测试也是一个样的。

这里我补充一点:

(1)所生成的base64的js文件是在开发中就生成的了,而不是在用户访问时才去生成,我把HTML代码和PHP代码写在一个文件里是方便,在真实项目中是分开的;

(2)使用此种优化技术有它的优点,当然也会有它的缺点,只有适合自己项目的优化技术才是好技术;

(3)此中优化技术建议使用在手机端(可以解决背景图优化方式所不能解决的问题),而PC端的则用合并小图标生成背景图的方式(看此文:);

(4)此种优化技术一般用于小图标(十几K以下),也就是HTTP响应时间远远大于下载时间的时候,用此方法优化会看到明显的效果;

(5)当然可以配合其他优化技术一起使用,效果更明显,比如缓存等。

 

这一次就分享那么多给大家,代码我都贴上了,而且很多都标上了注释,方便大家理解。

如果此博文中有哪里讲得让人难以理解,欢迎留言交流,若有讲解错的地方欢迎指出。

如果您觉得您能在此博文学到了新知识,请为我顶一个,如文章中有解释错的地方,欢迎指出。

  互相学习,共同进步!

2 赞 2 收藏
评论

银河国际网址手机版 8

从输入 URL 到页面加载完成的过程中都发生了什么事情?

2015/10/03 · HTML5,
JavaScript · 6
评论 ·
HTTP,
浏览器

原文出处:
百度FEX/吴多益(@吴多益)   

背景  本文来自于之前我发的一篇微博:

银河国际网址手机版 9

不过写这篇文章并不是为了帮大家准备面试,而是想借这道题来介绍计算机和互联网的基础知识,让读者了解它们之间是如何关联起来的。

为了便于理解,我将整个过程分为了六个问题来展开。

中间人劫持

起因是这样,https 使用的是 443 端口进行数据传输,而浏览器的默认端口是

  1. 劫持者首先劫持用户的 80
    端口,当用户向目标页发起请求时,劫持者模拟正常的 https
    请求向源服务器获取数据,然后通过 80
    端口返回给用户,大概可以看下下面两张图:

银河国际网址手机版 10

用户一般不会在地址栏输入   ,而是习惯性输入
taobao.com  ,此时浏览器走的是
http,请求到达服务器之后,服务器告诉浏览器 302 跳转

Location:

1
Location: https://www.taobao.com

然后浏览器重新请求,通过 HTTPS 方式,443
端口通讯。而正因为用户不是直接输入 https:// 链接,劫持者利用这一点:

银河国际网址手机版 11

只要能够劫持你的网络,比如路由劫持、DNS劫持,就可以作为中间人注入代码、替换广告。。。(上了
https 也拗不过电信,真是日了够了)

这种劫持出现在两种情况下:

  • 用户没有通过准确的方式访问页面,除非输入 https:// ,否则浏览器默认以 http 方式访问
  • HTTPS 页面的链接中包含 http,这个 http 页面可能被劫持

第一个问题:从输入 URL 到浏览器接收的过程中发生了什么事情?

启用 HSTS

HSTS,HTTP Strict Transport
Security,简单说就是强制客户端使用 HTTPS 访问页面。其原理就是:

  • 在服务器响应头中添加  Strict-Transport-Security ,可以设置  max-age
  • 用户访问时,服务器种下这个头
  • 下次如果使用 http 访问,只要 max-age
    未过期,客户端会进行内部跳转,可以看到 307 Redirect Internel
    的响应码
  • 变成 https 访问源服务器

这个过程有效避免了中间人对 80
端口的劫持。但是这里存在一个问题:如果用户在劫持状态,并且没有访问过源服务器,那么源服务器是没有办法给客户端种下
Strict-Transport-Security  响应头的(都被中间人挡下来了)。

启用 HSTS 不仅仅可以有效防范中间人攻击,同时也为浏览器节省来一次 302/301
的跳转请求,收益还是很高的。我们的很多页面,难以避免地出现 http
的链接,比如 help 中的链接、运营填写的链接等,这些链接的请求都会经历一次
302,对于用户也是一样,收藏夹中的链接保存的可能也是 http 的。

从触屏到 CPU

首先是「输入
URL」,大部分人的第一反应会是键盘,不过为了与时俱进,这里将介绍触摸屏设备的交互。

触摸屏一种传感器,目前大多是基于电容(Capacitive)来实现的,以前都是直接覆盖在显示屏上的,不过最近出现了
3 种嵌入到显示屏中的技术,第一种是 iPhone 5 的 In-cell,它能减小了 0.5
毫米的厚度,第二种是三星使用的 On-cell 技术,第三种是国内厂商喜欢用的
OGS
全贴合技术,具体细节可以阅读这篇文章。

当手指在这个传感器上触摸时,有些电子会传递到手上,从而导致该区域的电压变化,触摸屏控制器芯片根据这个变化就能计算出所触摸的位置,然后通过总线接口将信号传到
CPU 的引脚上。

以 Nexus 5 为例,它所使用的触屏控制器是 Synaptics
S3350B,总线接口为 I²C,以下是
Synaptics
触摸屏和处理器连接的示例:银河国际网址手机版 12

左边是处理器,右边是触摸屏控制器,中间的 SDA 和 SCL 连线就是 I²C
总线接口。

307 状态码

在 GET、HEAD 这些幂等的请求方式上,302、303、307 没啥区别,而对于 POST
就不同了,大部分浏览器 都会302 会将 POST 请求转为 GET,而 303
是规范强制规定将 POST 转为 GET 请求,请求地址为 header
头中的 Location,307 则不一样,规范要求浏览器继续向 Location 的地址
POST 内容。

而在 HSTS 中,307 可以被缓存,缓存时间根据 max-age 而定,一般建议缓存 1
年甚至更长。

CPU 内部的处理

移动设备中的 CPU 并不是一个单独的芯片,而是和 GPU
等芯片集成在一起,被称为 SoC(片上系统)。

前面提到了触屏和 CPU
的连接,这个连接和大部分计算机内部的连接一样,都是通过电气信号来进行通信的,也就是电压高低的变化,如下面的时序图:银河国际网址手机版 13

在时钟的控制下,这些电流会经过 MOSFET 晶体管,晶体管中包含
N 型半导体和 P 型半导体,通过电压就能控制线路开闭,然后这些 MOSFET
构成了 CMOS,接着再由 CMOS
实现「与」「或」「非」等逻辑电路门,最后由逻辑电路门上就能实现加法、位移等计算,整体如下图所示(来自《计算机体系结构》):银河国际网址手机版 14

除了计算,在 CPU
中还需要存储单元来加载和存储数据,这个存储单元一般通过触发器(Flip-flop)来实现,称为寄存器。

以上这些概念都比较抽象,推荐阅读「How to Build an 8-Bit
Computer」这篇文章,作者基于晶体管、二极管、电容等原件制作了一个
8 位的计算机,支持简单汇编指令和结果输出,虽然现代 CPU
的实现要比这个复杂得多,但基本原理还是一样的。

另外其实我也是刚开始学习 CPU
芯片的实现,所以就不在这误人子弟了,感兴趣的读者请阅读本节后面推荐的书籍。

HSTS 存在的坑

  • 纯 IP 的请求,HSTS 没法处理,比如 http://2.2.2.2 ,
    即便响应头中设置了 STS,浏览器也不会理会(未测试)
  • HSTS 只能在 80 和 443 端口之间切换,如果服务是 8080 端口,即便设置了
    STS,也无效(未测试)
  • 如果浏览器证书错误,一般情况会提醒存在安全风险,然是依然给一个链接进入目标页,而
    HSTS 则没有目标页入口,所以一旦证书配置错误,就是很大的故障了
  • 如果服务器的 HTTPS 没有配置好就开启了 STS
    的响应头,并且还设置了很长的过期时间,那么在你服务器 HTTPS
    配置好之前,用户都是没办法连接到你的服务器的,除非 max-age 过期了。
  • HSTS 能让你的网站在 ssllab 上到 A+(这不是坑)

从 CPU 到操作系统内核

前面说到触屏控制器将电气信号发送到 CPU 对应的引脚上,接着就会触发 CPU
的中断机制,以 Linux
为例,每个外部设备都有一标识符,称为中断请求(IRQ)号,可以通过 /proc/interrupts 文件来查看系统中所有设备的中断请求号,以下是
Nexus 7 (2013) 的部分结果:

shell@flo:/ $ cat /proc/interrupts CPU0 17: 0 GIC dg_timer 294: 1973609
msmgpio elan-ktf3k 314: 679 msmgpio KEY_POWER

1
2
3
4
5
shell@flo:/ $ cat /proc/interrupts
            CPU0
  17:          0       GIC  dg_timer
294:    1973609   msmgpio  elan-ktf3k
314:        679   msmgpio  KEY_POWER

因为 Nexus 7 使用了 ELAN 的触屏控制器,所以结果中的 elan-ktf3k
就是触屏的中断请求信息,其中 294 是中断号,1973609
是触发的次数(手指单击时会产生两次中断,但滑动时会产生上百次中断)。

为了简化这里不考虑优先级问题,以 ARMv7
架构的处理器为例,当中断发生时,CPU
会停下当前运行的程序,保存当前执行状态(如 PC 值),进入 IRQ
状态),然后跳转到对应的中断处理程序执行,这个程序一般由第三方内核驱动来实现,比如前面提到的
Nexus 7
的驱动源码在这里 touchscreen/ektf3k.c。

这个驱动程序将读取 I²C
总线中传来的位置数据,然后通过内核的 input_report_abs 等方法记录触屏按下坐标等信息,最后由内核中的input
子模块将这些信息都写进 /dev/input/event0 这个设备文件中,比如下面展示了一次触摸事件所产生的信息:

130|shell@flo:/ $ getevent -lt /dev/input/event0 [ 414624.658986]
EV_ABS ABS_MT_TRACKING_ID 0000835c [ 414624.659017] EV_ABS
ABS_MT_TOUCH_MAJOR 0000000b [ 414624.659047] EV_ABS
ABS_MT_PRESSURE 0000001d [ 414624.659047] EV_ABS
ABS_MT_POSITION_X 000003f0 [ 414624.659078] EV_ABS
ABS_MT_POSITION_Y 00000588 [ 414624.659078] EV_SYN SYN_REPORT
00000000 [ 414624.699239] EV_ABS ABS_MT_TRACKING_ID ffffffff [
414624.699270] EV_SYN SYN_REPORT 00000000

1
2
3
4
5
6
7
8
9
130|shell@flo:/ $ getevent -lt /dev/input/event0
[  414624.658986] EV_ABS       ABS_MT_TRACKING_ID   0000835c
[  414624.659017] EV_ABS       ABS_MT_TOUCH_MAJOR   0000000b
[  414624.659047] EV_ABS       ABS_MT_PRESSURE      0000001d
[  414624.659047] EV_ABS       ABS_MT_POSITION_X    000003f0
[  414624.659078] EV_ABS       ABS_MT_POSITION_Y    00000588
[  414624.659078] EV_SYN       SYN_REPORT           00000000
[  414624.699239] EV_ABS       ABS_MT_TRACKING_ID   ffffffff
[  414624.699270] EV_SYN       SYN_REPORT           00000000

小结

本文简单说明了 HSTS 的基本原理和相关内容,他在全站 https
下有一个较大的正向作用,推荐使用。

P.S:在 Chrome
中打开 chrome://net-internals/#hsts,添加域名之后,可以让浏览器强制对该域名启用
https,所有的 http 请求都会内部转到 https。

1 赞 收藏
评论

银河国际网址手机版 8

从操作系统 GUI 到浏览器

前面提到 Linux
内核已经完成了对硬件的抽象,其它程序只需要通过监听 /dev/input/event0 文件的变化就能知道用户进行了哪些触摸操作,不过如果每个程序都这么做实在太麻烦了,所以在图像操作系统中都会包含
GUI 框架来方便应用程序开发,比如 Linux 下著名的 X。

但 Android 并没有使用 X,而是自己实现了一套 GUI
框架,其中有个 EventHub 的服务会通过 epoll 方式监听 /dev/input/ 目录下的文件,然后将这些信息传递到
Android
的窗口管理服务(WindowManagerService)中,它会根据位置信息来查找相应的
app,然后调用其中的监听函数(如 onTouch 等)。

就这样,我们解答了第一个问题,不过由于时间有限,这里省略了很多细节,想进一步学习的读者推荐阅读以下书籍。

扩展学习

  • 《计算机体系结构》
  • 《计算机体系结构:量化研究方法》
  • 《计算机组成与设计:硬件/软件接口》
  • 《编码》
  • 《CPU自制入门》
  • 《操作系统概念》
  • 《ARMv7-AR
    体系结构参考手册》
  • 《Linux内核设计与实现》
  • 《精通Linux设备驱动程序开发》

第二个问题:浏览器如何向网卡发送数据?

从浏览器到浏览器内核

前面提到操作系统 GUI
将输入事件传递到了浏览器中,在这过程中,浏览器可能会做一些预处理,比如
Chrome
会根据历史统计来预估所输入字符对应的网站,比如输入了「ba」,根据之前的历史发现
90% 的概率会访问「www.baidu.com 」,因此就会在输入回车前就马上开始建立
TCP 链接甚至渲染了,这里面还有很多其它策略,感兴趣的读者推荐阅读 High
Performance Networking in
Chrome。

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

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

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

HTTP 请求的发送

因为网络的底层实现是和内核相关的,所以这一部分需要针对不同平台进行处理,从应用层角度看主要做两件事情:通过
DNS 查询 IP、通过 Socket 发送数据,接下来就分别介绍这两方面的内容。

DNS 查询

应用程序可以直接调用 Libc
提供的 getaddrinfo() 方法来实现
DNS 查询。

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

; <<>> DiG 9.8.3-P1 <<>> +trace fex.baidu.com ;;
global options: +cmd . 11157 IN NS g.root-servers.net. . 11157 IN NS
i.root-servers.net. . 11157 IN NS j.root-servers.net. . 11157 IN NS
a.root-servers.net. . 11157 IN NS l.root-servers.net. ;; Received 228
bytes from 8.8.8.8#53(8.8.8.8) in 220 ms com. 172800 IN NS
a.gtld-servers.net. com. 172800 IN NS c.gtld-servers.net. com. 172800 IN
NS m.gtld-servers.net. com. 172800 IN NS h.gtld-servers.net. com. 172800
IN NS e.gtld-servers.net. ;; Received 503 bytes from
192.36.148.17#53(192.36.148.17) in 185 ms baidu.com. 172800 IN NS
dns.baidu.com. baidu.com. 172800 IN NS ns2.baidu.com. baidu.com. 172800
IN NS ns3.baidu.com. baidu.com. 172800 IN NS ns4.baidu.com. baidu.com.
172800 IN NS ns7.baidu.com. ;; Received 201 bytes from
192.48.79.30#53(192.48.79.30) in 1237 ms fex.baidu.com. 7200 IN CNAME
fexteam.duapp.com. fexteam.duapp.com. 300 IN CNAME duapp.n.shifen.com.
n.shifen.com. 86400 IN NS ns1.n.shifen.com. n.shifen.com. 86400 IN NS
ns4.n.shifen.com. n.shifen.com. 86400 IN NS ns2.n.shifen.com.
n.shifen.com. 86400 IN NS ns5.n.shifen.com. n.shifen.com. 86400 IN NS
ns3.n.shifen.com. ;; Received 258 bytes from
61.135.165.235#53(61.135.165.235) in 2 ms

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
; <<>> DiG 9.8.3-P1 <<>> +trace fex.baidu.com
;; global options: +cmd
.           11157   IN  NS  g.root-servers.net.
.           11157   IN  NS  i.root-servers.net.
.           11157   IN  NS  j.root-servers.net.
.           11157   IN  NS  a.root-servers.net.
.           11157   IN  NS  l.root-servers.net.
;; Received 228 bytes from 8.8.8.8#53(8.8.8.8) in 220 ms
 
com.            172800  IN  NS  a.gtld-servers.net.
com.            172800  IN  NS  c.gtld-servers.net.
com.            172800  IN  NS  m.gtld-servers.net.
com.            172800  IN  NS  h.gtld-servers.net.
com.            172800  IN  NS  e.gtld-servers.net.
;; Received 503 bytes from 192.36.148.17#53(192.36.148.17) in 185 ms
 
baidu.com.      172800  IN  NS  dns.baidu.com.
baidu.com.      172800  IN  NS  ns2.baidu.com.
baidu.com.      172800  IN  NS  ns3.baidu.com.
baidu.com.      172800  IN  NS  ns4.baidu.com.
baidu.com.      172800  IN  NS  ns7.baidu.com.
;; Received 201 bytes from 192.48.79.30#53(192.48.79.30) in 1237 ms
 
fex.baidu.com.      7200    IN  CNAME   fexteam.duapp.com.
fexteam.duapp.com.  300 IN  CNAME   duapp.n.shifen.com.
n.shifen.com.       86400   IN  NS  ns1.n.shifen.com.
n.shifen.com.       86400   IN  NS  ns4.n.shifen.com.
n.shifen.com.       86400   IN  NS  ns2.n.shifen.com.
n.shifen.com.       86400   IN  NS  ns5.n.shifen.com.
n.shifen.com.       86400   IN  NS  ns3.n.shifen.com.
;; Received 258 bytes from 61.135.165.235#53(61.135.165.235) in 2 ms

可以看到这是一个逐步缩小范围的查找过程,首先由本机所设置的 DNS
服务器(8.8.8.8)向 DNS 根节点查询负责 .com
区域的域务器,然后通过其中一个负责 .com 的服务器查询负责 baidu.com
的服务器,最后由其中一个 baidu.com 的域名服务器查询 fex.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 常用的是 TCP 协议,由于 TCP
协议的具体细节到处都能看到,所以本文就不介绍了,这里谈一下 TCP 的
Head-of-line blocking 问题:假设客户端的发送了 3 个 TCP
片段(segments),编号分别是 1、2、3,如果编号为 1
的包传输时丢了,即便编号 2 和 3 已经到达也只能等待,因为 TCP
协议需要保证顺序,这个问题在 HTTP pipelining 下更严重,因为 HTTP
pipelining 可以让多个 HTTP 请求通过一个 TCP
发送,比如发送两张图片,可能第二张图片的数据已经全收到了,但还得等第一张图片的数据传到。

为了解决 TCP 协议的性能问题,Chrome
团队去年提出了 QUIC 协议,它是基于
UDP 实现的可靠传输,比起 TCP,它能减少很多来回(round
trip)时间,还有前向纠错码(Forward Error Correction)等功能。目前 Google
Plus、 Gmail、Google Search、blogspot、Youtube 等几乎大部分 Google
产品都在使用 QUIC,可以通过 chrome://net-internals/#spdy 页面来发现。

虽然目前除了 Google 还没人用 QUIC,但我觉得挺有前景的,因为优化 TCP
需要升级系统内核(比如 Fast
Open)。

浏览器对同一个域名有连接数限制,大部分是
6,我以前认为将这个连接数改大后会提升性能,但实际上并不是这样的,Chrome
团队有做过实验,发现从 6 改成 10
后性能反而下降了,造成这个现象的因素有很多,如建立连接的开销、拥塞控制等问题,而像
SPDY、HTTP 2.0 协议尽管只使用一个 TCP
连接来传输数据,但性能反而更好,而且还能实现请求优先级。

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

Socket 在内核中的实现

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

以 Linux
为例,它的实现在这里 socket.c,目前我还不太了解,推荐读者看看 Linux
kernel
map,它标注出了关键路径的函数,方便学习从协议栈到网卡驱动的实现。

底层网络协议的具体例子

接下来如果继续介绍 IP 协议和 MAC
协议可能很多读者会晕,所以本节将使用 Wireshark 来通过具体例子讲解,以下是我请求百度首页时抓取到的网络数据:银河国际网址手机版 16

最底下是实际的二进制数据,中间是解析出来的各个字段值,可以看到其中最底部为
HTTP 协议(Hypertext Transfer Protocol),在 HTTP 之前有 54
字节(0x36),这就是底层网络协议所带来的开销,我们接下来对这些协议进行分析。

在 HTTP 之上是 TCP 协议(Transmission Control
Protocol),它的具体内容如下图所示:银河国际网址手机版 17

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

0 1 2 3 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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
Source Port | Destination Port |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
Sequence Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
Acknowledgment Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Data
| |U|A|E|R|S|F| | | Offset| Reserved |R|C|O|S|Y|I| Window | | |
|G|K|L|T|N|N| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
Checksum | Urgent Pointer |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
Options | Padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | data
| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

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

银河国际网址手机版,具体各个字段的作用这里就不介绍了,感兴趣的读者可以阅读 RFC
793,并结合抓包分析来理解。

需要注意的是,在 TCP 协议中并没有 IP 地址信息,因为这是在上一层的 IP
协议中定义的,如下图所示:银河国际网址手机版 18

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.1.1,但到公司就变成172.22.22.22 了,所以在底层通信时需要使用一个固定的地址,这就是
MAC(media access control) 地址,每个网卡出厂时的 MAC
地址都是固定且唯一的。

因此再往上就是 MAC 协议,它有 14
字节,如下所示:银河国际网址手机版 19

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

最顶上的 Frame 是代表 Wireshark 的抓包序号,并不是网络协议

就这样,我们解答了第二个问题,不过其实这里面还有很多很多细节没介绍,建议大家通过下面的书籍进一步学习。

扩展学习

  • 《计算机网络:自顶向下方法与Internet特色》
  • 《计算机网络》
  • 《Web性能权威指南》

第三个问题:数据如何从本机网卡发送到服务器?

标签:,

发表评论

电子邮件地址不会被公开。 必填项已用*标注

相关文章

网站地图xml地图