您的位置:首页 > 理论基础 > 计算机网络

网络安全相关知识与计算机互联网基础知识

2015-01-28 16:19 741 查看
流量劫持:流量劫持-浮层登录框的隐患

==================================================================================================================================

计算机与互联网基础知识:

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

从触屏到CPU

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

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

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

以Nexus5为例,它所使用的触屏控制器是SynapticsS3350B,总线接口为I²C,以下是
Synaptics触摸屏和处理器连接的示例:



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

CPU内部的处理

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

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



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



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

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

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

从CPU到操作系统内核

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


shell@flo:/$cat/proc/interrupts

CPU0

17:0GICdg_timer

294:1973609msmgpioelan-ktf3k

314:679msmgpioKEY_POWER

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

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

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

130|shell@flo:/$getevent-lt/dev/input/event0

[414624.658986]EV_ABSABS_MT_TRACKING_ID0000835c

[414624.659017]EV_ABSABS_MT_TOUCH_MAJOR0000000b

[414624.659047]EV_ABSABS_MT_PRESSURE0000001d

[414624.659047]EV_ABSABS_MT_POSITION_X000003f0

[414624.659078]EV_ABSABS_MT_POSITION_Y00000588

[414624.659078]EV_SYNSYN_REPORT00000000

[414624.699239]EV_ABSABS_MT_TRACKING_IDffffffff

[414624.699270]EV_SYNSYN_REPORT00000000

从操作系统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
PerformanceNetworkinginChrome。

接着是输入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+tracefex.baidu.com
命令得到的结果(省略了一些):


;<<>>DiG9.8.3-P1<<>>+tracefex.baidu.com

;;globaloptions:+cmd

.11157INNSg.root-servers.net.

.11157INNSi.root-servers.net.

.11157INNSj.root-servers.net.

.11157INNSa.root-servers.net.

.11157INNSl.root-servers.net.

;;Received228bytesfrom8.8.8.8#53(8.8.8.8)in220ms

com.172800INNSa.gtld-servers.net.

com.172800INNSc.gtld-servers.net.

com.172800INNSm.gtld-servers.net.

com.172800INNSh.gtld-servers.net.

com.172800INNSe.gtld-servers.net.

;;Received503bytesfrom192.36.148.17#53(192.36.148.17)in185ms

baidu.com.172800INNSdns.baidu.com.

baidu.com.172800INNSns2.baidu.com.

baidu.com.172800INNSns3.baidu.com.

baidu.com.172800INNSns4.baidu.com.

baidu.com.172800INNSns7.baidu.com.

;;Received201bytesfrom192.48.79.30#53(192.48.79.30)in1237ms

fex.baidu.com.7200INCNAMEfexteam.duapp.com.

fexteam.duapp.com.300INCNAMEduapp.n.shifen.com.

n.shifen.com.86400INNSns1.n.shifen.com.

n.shifen.com.86400INNSns4.n.shifen.com.

n.shifen.com.86400INNSns2.n.shifen.com.

n.shifen.com.86400INNSns5.n.shifen.com.

n.shifen.com.86400INNSns3.n.shifen.com.

;;Received258bytesfrom61.135.165.235#53(61.135.165.235)in2ms

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

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

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

通过Socket发送数据

有了IP地址,就可以通过SocketAPI来发送数据了,这时可以选择TCP或UDP协议,具体使用方法这里就不介绍了,推荐阅读Beej'sGuidetoNetworkProgramming。

HTTP常用的是TCP协议,由于TCP协议的具体细节到处都能看到,所以本文就不介绍了,这里谈一下TCP的Head-of-lineblocking问题:假设客户端的发送了3个TCP片段(segments),编号分别是1、2、3,如果编号为1的包传输时丢了,即便编号2和3已经到达也只能等待,因为TCP协议需要保证顺序,这个问题在HTTPpipelining下更严重,因为HTTPpipelining可以让多个HTTP请求通过一个TCP发送,比如发送两张图片,可能第二张图片的数据已经全收到了,但还得等第一张图片的数据传到。

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

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

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

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

Socket在内核中的实现

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

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

底层网络协议的具体例子

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



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

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



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


0123

01234567890123456789012345678901

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

|SourcePort|DestinationPort|

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

|SequenceNumber|

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

|AcknowledgmentNumber|

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

|Data||U|A|E|R|S|F||

|Offset|Reserved|R|C|O|S|Y|I|Window|

|||G|K|L|T|N|N||

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

|Checksum|UrgentPointer|

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

|Options|Padding|

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

|data|

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

具体各个字段的作用这里就不介绍了,感兴趣的读者可以阅读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.1.1
,但到公司就变成
172.22.22.22
了,所以在底层通信时需要使用一个固定的地址,这就是
MAC(mediaaccesscontrol)地址,每个网卡出厂时的MAC地址都是固定且唯一的。

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



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

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

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

扩展学习

计算机网络:自顶向下方法与Internet特色》

计算机网络》

Web性能权威指南》

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

从内核到网络适配器(NetworkInterfaceCard)

前面说到调用SocketAPI后内核会对数据进行底层协议栈的封装,接下来启动DMA控制器,它将从内存中读取数据写入网卡。

以Nexus5为例,它使用的是博通BCM4339芯片通信,接口采用了SD卡一样的SDIO,但这个芯片的细节并没有公开资料,所以这里就不讨论了。

连接Wi-Fi路由

Wi-Fi网卡需要通过Wi-Fi路由来与外部通信,原理是基于无线电,通过电流变化来产生无线电,这个过程也叫「调制」,而反过来无线电可以引起电磁场变化,从而产生电流变化,利用这个原理就能将无线电中的信息解读出来就叫「解调」,其中单位时间内变化的次数就称为频率,目前在Wi-Fi中所采用的频率分为2.4GHz和5GHz两种。

在同一个Wi-Fi路由下,因为采用的频率相同,同时使用时会发生冲突,为了解决这个问题,Wi-Fi采用了被称为CSMA/CA的方法,简单来说就是在传输前先确认信道是否已被使用,没有才发送数据。

而同样基于无线电原理的2G/3G/LTE也会遇到类似的问题,但它并没有采用Wi-Fi那样的独占方案,而是通过频分(FDMA)、时分(TDMA)和码分(CDMA)来进行复用,具体细节这里就不展开了。

以小米路由为例,它使用的芯片是BCM4709,这个芯片由ARM
Cortex-A9处理器及流量(Flow)硬件加速组成,使用硬件芯片可以避免经过操作系统中断、上下文切换等操作,从而提升了性能。

路由器中的操作系统可以基于OpenWrt或DD-WRT来开发的,具体细节我不太了解,所以就不展开了。

因为内网设备的IP都是类似
192.168.1.x
这样的内网地址,外网无法直接向这个地址发送数据,所以网络数据在经过路由时,路由会修改相关地址和端口,这个操作称为NAT映射。

最后家庭路由一般会通过双绞线连接到运营商网络的。

运营商网络内的路由

数据过双绞线发送到运营商网络后,还会经过很多个中间路由转发,读者可以通过traceroute命令或者在线可视化工具来查看这些路由的ip和位置。

当数据传递到这些路由器后,路由器会取出包中目的地址的前缀,通过内部的转发表查找对应的输出链路,而这个转发表是如何得到的呢?这就是路由器中最重要的选路算法了,可选的有很多,我对这方面并不太了解,看起来维基百科上的词条列得很全。

主干网间的传输

对于长线的数据传输,通常使用光纤作为介质,光纤是基于光的全反射来实现的,使用光纤需要专门的发射器通过电致发光(比如LED)将电信号转成光,比起前面介绍的无线电和双绞线,光纤信号的抗干扰性要强得多,而且能耗也小很多。

既然是基于光来传输数据,数据传输速度也就取决于光的速度,在真空中的光速接近于30万千米/秒,由于光纤包层(cladding)中的折射率(refractiveindex)为1.52,所以实际光速是20万千米/秒左右,从首都机场飞往广州白云机场的距离是1967千米,按照这个距离来算需要花费10毫秒才能抵达。这意味着如果你在北京,服务器在广州,等你发出数据到服务器返回数据至少得等20毫秒,实际情况预计是2-3倍,因为这其中还有各个节点路由处理的耗时,比如我测试了一个广州的IP
发现平均延迟为60毫秒。

这个延迟是现有科技无法解决的(除非找到超过光速的方法),只能通过CDN来让传输距离变短,或尽量减少串行的来回请求(比如TCP建立连接所需的3次握手)。

IDC内网

数据通过光纤最终会来到服务器所在的IDC机房,进入IDC内网,这时可以先通过分光器将流量镜像一份出来方便进行安全检查等分析,还能用来进行。。。

这里的带宽成本很高,是按照峰值来结算的,以每月每Gbps(注意这里指的是bit,而不是Byte)为单位,北京这边价格在十万人民币以上,一般网站使用1G到10G不等。

接下来光纤中的数据将进入集群(Cluster)交换机,然后再转发到机架(Rack)顶部的交换机,最后通过这个交换机的端口将数据发往机架中的服务器,可以参考下图(来自OpenCompute):



上图左边是正面,右边是侧面,可以看到顶部为交换机所留的位置。

以前这些交换机的内部实现是封闭的,相关厂商(如思科、Juniper等)会使用特定的处理器和操作系统,外界难以进行灵活控制,甚至有时候需要手工配置,但这几年随着OpenFlow技术的流行,也出现了开放交换机硬件(Open
SwitchHardware),比如Intel的网络平台,推荐感兴趣的读者建议看看它的视频,比文字描述清晰多了。

需要注意的是,一般网络书中提到的交换机都只具备二层(MAC协议)的功能,但在IDC中的交换器基本上都具备三层(IP协议)的功能,所以不需要有专门的路由了。

最后,因为CPU处理的是电气信号,所以光纤中的光线需要先使用相关设备通过光电效应将光信号转成电信号,然后进入服务器网卡。

服务器CPU

前面说到数据已经到达服务器网卡了,接着网卡会将数据拷贝到内存中(DMA),然后通过中断来通知CPU,目前服务器端的CPU基本上都是IntelXeon,不过这几年出现了一些新的架构,比如在存储领域,百度使用ARM架构来提升存储密度,因为
ARM的功耗比Xeon低得多。而在高性能领域,Google最近在尝试基于POWER架构的
CPU来开发的服务器,最新的POWER8处理器可以并行执行96个线程,所以对高并发的应用应该很有帮助。

扩展学习

TheDatacenter
asaComputer

OpenComputer

软件定义网络》

大话无线通信》

第四个问题:服务器接收到数据后会进行哪些处理?

为了避免重复,这里将不再介绍操作系统,而是直接进入后端服务进程,由于这方面有太多技术选型,所以我只挑几个常见的公共部分来介绍。

负载均衡

请求在进入到真正的应用服务器前,可能还会先经过负责负载均衡的机器,它的作用是将请求合理地分配到多个服务器上,同时具备具备防攻击等功能。

负载均衡具体实现有很多种,有直接基于硬件的F5,有操作系统传输层(TCP)上的LVS,也有在应用层(HTTP)实现的反向代理(也叫七层代理),接下来将介绍LVS及反向代理。

负载均衡的策略也有很多,如果后面的多个服务器性能均衡,最简单的方法就是挨个循环一遍(Round-Robin),其它策略就不一一介绍了,可以参考LVS中的算法。

LVS

LVS的作用是从对外看来只有一个IP,而实际上这个IP后面对应是多台机器,因此也被成为VirtualIP。

前面提到的NAT也是一种LVS中的工作模式,除此之外还有DR和TUNNEL,具体细节这里就不展开了,它们的缺点是无法跨网段,所以百度自己开发了BVS系统。

反向代理

方向代理是工作在HTTP上的,具体实现可以基于HAProxy或Nginx,因为反向代理能理解HTTP协议,所以能做非常多的事情,比如:

进行很多统一处理,比如防攻击策略、放抓取、SSL、gzip、自动性能优化等

应用层的分流策略都能在这里做,比如对/xx路径的请求分到a服务器,对/yy路径的请求分到b服务器,或者按照cookie进行小流量测试等

缓存,并在后端服务挂掉的时候显示友好的404页面

监控后端服务是否异常

⋯⋯

Nginx的代码写得非常优秀,从中能学到很多,对高性能服务端开发感兴趣的读者一定要看看。

WebServer中的处理

请求经过前面的负载均衡后,将进入到对应服务器上的WebServer,比如Apache、Tomcat、Node.JS等。

以Apache为例,在接收到请求后会交给一个独立的进程来处理,我们可以通过编写Apache扩展来处理,但这样开发起来太麻烦了,所以一般会调用PHP等脚本语言来进行处理,比如在CGI下就是将HTTP中的参数放到环境变量中,然后启动PHP进程来执行,或者使用FastCGI来预先启动进程。

(等后续有空再单独介绍Node.JS中的处理)

进入后端语言

前面说到WebServer会调用后端语言进程来处理HTTP请求(这个说法不完全正确,有很多其它可能),那么接下来就是后端语言的处理了,目前大部分后端语言都是基于虚拟机的,如PHP、Java、JavaScript、Python等,但这个领域的话题非常大,难以讲清楚,对PHP感兴趣的读者可以阅读我之前写的HHVM
介绍文章,其中提到了很多虚拟机的基础知识。

Web框架(Framework)

如果你的PHP只是用来做简单的个人主页「PersonalHomePage」,倒没必要使用Web框架,但如果随着代码的增加会变得越来越难以管理,所以一般网站都会会基于某个Web框架来开发,因此在后端语言执行时首先进入Web框架的代码,然后由框架再去调用应用的实现代码。

可选的Web框架非常多,这里就不一一介绍了。

读取数据

这部分不展开了,从简单的读写文件到数据中间层,这里面可选的方案实在太多。

扩展学习

深入理解Nginx》

Python源码剖析》

深入理解Java虚拟机》

数据库系统实现》

第五个问题:服务器返回数据后浏览器如何处理?

前面说到服务端处理完请求后,结果将通过网络发回客户端的浏览器,从本节开始将介绍浏览器接收到数据后的处理,值得一提的是这方面之前有一篇不错的文章How
BrowsersWork,所以很多内容我不想再重复介绍,因此将重点放在那篇文章所忽略的部分。

从01到字符

HTTP请求返回的HTML传递到浏览器后,如果有gzip会先解压,然后接下来最重要的问题是要知道它的编码是什么,比如同样一个「中」字,在UTF-8编码下它的内容其实是「111001001011100010101101」也就是「E4B8AD」,而在GBK下则是「1101011011010000」,也就是「D6D0」,如何才能知道文件的编码?可以有很多判断方法:

用户设置,在浏览器中可以指定页面编码

HTTP协议中

<meta>
中的charset属性值

对于JS和CSS

对于iframe

如果在这些地方都没指明,浏览器就很难处理,在它看来就是一堆「0」和「1」,比如「中文」,它在UTF-8下有6个字节,如果按照GBK可以当成「涓枃」这3个汉字来解释,浏览器怎么知道到底是「中文」还是「涓枃」呢?

不过正常人一眼就能认出「涓枃」是错的,因为这3个字太不常见了,所以有人就想到通过判断常见字的方法来检测编码,典型的比如Mozilla的UniversalCharsetDetection,不过这东东误判率也很高,所以还是指明编码的好。

这样后续对文本的操作就是基于「字符」(Character)的了,一个汉字就是一个字符,不用再关心它究竟是2个字节还是3个字节。

外链资源的加载

(待补充,这里有调度策略)

JavaScript的执行

(后续再单独介绍,推荐大家看R大去年整理的这个帖子,里面有非常多相关资料,另外我两年前曾讲过JavaScript
引擎中的性能优化,虽然有些内容不太正确了,但也可以看看)

从字符到图片

二维渲染中最复杂的要数文字显示了,虽然想想似乎很简单,不就是将某个文字对应的字形(glyph)找出来么?在中文和英文中这样做是没问题的,因为一个字符就对应一个字形(glyph),在字体文件中找到字形,然后画上去就可以了,但在阿拉伯语中是不行的,因为它有有连体形式。

(以后续再单独介绍,这里非常复杂)

跨平台2D绘制库

在不同操作系统中都提供了自己的图形绘制API,比如MacOSX下的Quartz,Windows下的GDI以及Linux下的Xlib,但它们相互不兼容,所以为了方便支持跨平台绘图,在Chrome中使用了Skia库。

(以后再单独介绍,Skia内部实现调用层级太多,直接讲代码可能不适合初学者)

GPU合成

(以后续再单独介绍,虽然简单来说就是靠贴图,但还得介绍OpenGL以及GPU芯片,内容太长)

扩展学习

这节内容是我最熟悉,结果反而因为这样才想花更多时间写好,所以等到以后再发出来好了,大家先可以先看看以下几个站点:

Chromium

MozillaHacks

Surfin'Safari

第六个问题:浏览器如何将页面展现出来?

前面提到浏览器已经将页面渲染成一张图片了,接下来的问题就是如何将这张图片展示在屏幕上。

Framebuffer

以Linux为例,在应用中控制屏幕最直接的方法是将图像的bitmap写入
/dev/fb0
文件中,这个文件实际上一个内存区域的映射,这段内存区域称为Framebuffer。

需要注意的是在硬件加速下,如OpenGL是不经过Framebuffer的。

从内存到LCD

在手机的SoC中通常都会有一个LCD控制器,当Framebuffer准备好后,CPU会通过AMBA内部总线通知
LCD控制器,然后这个控制器读取Framebuffer中的数据,进行格式转换、伽马校正等操作,最终通过DSI、HDMI等接口发往LCD显示器。

OMAP5432为例,下图是它所支持的一种并行数据传输:



LCD显示

最后简单介绍一下LCD的显示原理。

首先,要想让人眼能看见,就必须有光线进入,要么通过反射、要么有光源,比如Kindle所使用的E-ink屏幕本身是不发光的,所以必须在有光线的地方才能阅读,它的优点是省电,但限制太大,所以几乎所有LCD都会自带光源。

目前LCD中通常使用LED作为光源,LED接上电源后,在电压的作用下,内部的正负电子结合会释放光子,从而产生光,这种物理现象叫电致发光(Electroluminescence),这在前面介绍光纤时也介绍过。

以下是iPodTouch2拆开后的样子:(来自Wikipedia):



在上图中可以看到6盏LED,这就是整个屏幕的光源,这些光源将通过反射的反射输出到屏幕中。

有了光源还得有色彩,在LED中通常做法是使用彩色滤光片(Colorfilter)来将LED光源转成不同颜色。

另外直接使用三种颜色的LED也是可行的,它能避免了滤光导致的光子浪费,降低耗电,很适用于智能手表这样的小屏幕,Apple收购的LuxVue公司就采用的是这种方式,感兴趣的话可以去研究它的专利

LCD屏幕上的每个物理像素点实际上是由红、绿、蓝3种色彩的点组成,每个颜色点能单独控制,下面是用显微镜放大后的情况(来自Wikipedia):



从上图可以看到每3种颜色的滤光片都全亮的时候就是白色,都灭就是黑色,如果你仔细看还能看到有些点并不是完全黑,这是字体上的反锯齿效果。

通过这3种颜色亮度的不同组合就能产生出各种色彩,如果每个颜色点能产生256种亮度,就能生成256*256*256=16777216种色彩。

并不是所有显示器的亮度都能达到256,在选择显示器时有个参数是8-Bit或6-Bit面板,其中8-Bit的面板能在物理上达到256种亮度,而6-Bit的则只有64种,它需要靠刷新率控制(Frame
ratecontrol)技术来达到256的效果。

如何控制这些颜色点的亮度?这就要靠液晶体了,液晶体的特性是当有电流通过时会发生旋转,从而将部分光线挡住,所以只要通过电压控制液晶体的转动就能控制这个颜色点的亮度,目前手机屏幕中通常使用TFT控制器来对其进行控制,在TFT中最著名的要数IPS面板。

这些过滤后的光线大部分会直接进入眼睛,有些光还会在其它表面上经过漫(diffuse)反射或镜面(specular)反射后再进入眼睛,加上环境光的影响,要真正算出有多少光到眼睛是一个积分问题,感兴趣的读者可以研究基于物理的渲染。

当光线进入眼睛后,接下来就是生物学的领域了,所以我们到此结束。

扩展学习

《ComputerGraphics,3rdEdition:Principles
andPractices》

交互式计算机图形学》

==================================================================================================================================

HTTP的POST参数提交和上传的不同与Mojolicious的实现
对于HTTP协议,我们在使用POST上传的时候,其实是有好几种不同的处理方式的,所以对于客户端和服务器端,也分别都有不同的处理.正常普通的网页在提交参数上传到服务器的时候,主要会根据内容的不同来使用不同的处理.所体现在不同的地方在Content-Type的类型.

比如我们常常用Mojolicious处理这类接收到的参数和内容的时候,会让很多人晕掉,所以我在这,基于协议的头,来给大家介绍一下在参数和上传的时候有什么不同.

客户端,比如浏览器网页中的form的表格的参数的不同,客户端比如Linux命令行的curl的参数的不同和程序接口提交参数的不同,HTTP协议在上传的时候,大约会有三种不同,这些体现在Content-Type的三种类型:

application/x-www-form-urlencoded

multipart/form-data

post的body的内容

下面我们来详细介绍
[title3]1.application/x-www-form-urlencoded默认[/title3]

浏览器:在HTML中form有个参数是enctype属性用于指定编码方式,常用有前面讲的两种:application/x-www-form-urlencoded和multipart/form-data.但默认的时候,我们并不指定.不指定的时候,默认是"application/x-www-form-urlencoded",所以其实,我们平时都是使用的这种格式来提交数据.因为是默认就不写出来了.
注意,这个会对空格和特别的符号进行url的encode.

程序:我们现在以Mojo::UserAgent这个模块为例子,我们提交一个参数args值为test.

$ua->post('http://www.php-oa.com/a/b'=>form=>{args=>'test'});


命令:

$curl-svo/dev/null-d"args=test"http://www.php-oa.com/a/b


HTTP协议状态

这个时候所发送的HTTP的头和内容分别如下.body中会存着参数,会有一个特别的Header.

--Blockingrequest(http://www.php-oa.com/a/b)
--Connect(http:www.php-oa.com:80)
--Client>>>Server(http://www.php-oa.com/a/b)
POST/a/bHTTP/1.1
User-Agent:Mojolicious(Perl)
Connection:keep-alive
Content-Type:application/x-www-form-urlencoded
Accept-Encoding:gzip
Content-Length:9
Host:www.php-oa.com

args=test


服务端接收方式

这个时候,服务器会根据因为是POST的方法,并且头部的Content-Type:application/x-www-form-urlencoded会去解析body的参数.这样在Mojolicious服务器

post'/a/b'=>sub{
my$self=shift;
my$foo=$self->param('args');
$self->render(text=>"Hellofrom$foo.");
}


[title3]2.multipart/form-data大文件,媒体文件[/title3]

对于比较大的,有一些二进制数据和象视频文件之类大文件,建议使用这种方式上传.

浏览器:

普通的HTTP的写法如果要使用enctype的话,只要象下面一样就行.

<formaction="/path/to/login"enctype="multipart/form-data">
<inputdisabled="disabled"name="first_name"type="text"/>
<inputvalue="Ok"type="submit"/>
</form>


客户端:

在Mojo::UserAgent考虑得非常周全,当你提交的内容中包含二进制文件之类时,就会自动帮你转换成"multipart/form-data"格式提交.这个格式会生成一个随机字符来分割不同参数.区分是否使用这种格式主要是,当你提交的参数中,又是一个引用,并且引中可以使用content来指定内容或者file来指定路径.

$ua->post('http://www.php-oa.com/a/b'=>form=>{args=>'test',args_file=>{file=>'/root/.bash_history'}});

#or
$ua->post('http://www.php-oa.com/a/b'=>form=>{args=>'test',args_file=>{content=>'test'}});


HTTP协议状态

这个地方我们可以见到Content-Type:multipart/form-data的请求头,告诉文件和参数是这种格式上传过来的.并且boundary用于指定一个参数之间的分割符.

--Blockingrequest(http://www.php-oa.com/a/b)
--Connect(http:www.php-oa.com:80)
--Client>>>Server(http://www.php-oa.com/a/b)
POST/a/bHTTP/1.1
User-Agent:Mojolicious(Perl)
Content-Type:multipart/form-data;boundary=WRoHX
Connection:keep-alive
Accept-Encoding:gzip
Content-Length:14428
Host:www.php-oa.com

--WRoHX
Content-Disposition:form-data;name="args"
test
--WRoHX
Content-Disposition:form-data;name="args_file";filename=".bash_history"
........文件本身


服务器接收方式

在后端的服务器接收的时候Mojolicious想得非常周到.对于这种格式能自动解析,并且全程异步.不会多占内存.这个会自动给大的文件使用一个叫Mojo::Upload的对象来处理,我们可以通过$self->req->upload('args_file');这个方法取得这个内容的对象,这个内容的对象是Mojo::Asset::File这个对象,存文件和取大文件之类可以直接调用.

post'/a/b'=>sub{
my$self=shift;
my$upload=$self->req->upload('args_file');
my$foo=$self->param('args');
$self->render(text=>"Hellofrom$foo.");
}


[title3]

3.POST的body的内容[/title3]

最后一种,是有时我们做大文件上传,和提交内容之类.这个时候,整个body都是文件本体.参数象get一样通过url传过去.

这个就不用抓头了,没任何转换,直接整个body是个大文件.

客户端提交:

我们来看看客户端在这个时候怎么上传送.同样,我们使用Mojo::UserAgent为例子.

my$ua=Mojo::UserAgent->new;
$ua->transactor->add_generator(stream=>sub{
my($transactor,$tx,$path)=@_;
$tx->req->content->asset(Mojo::Asset::File->new(path=>$path));
});
$ua->post('http://www.php-oa.com/a/b'=>stream=>'/root/.bash_history');


服务器接收

这个时候,在服务器端怎么接收啦?

post'/a/b'=>sub{
my$self=shift;
my$body=$self->req->body;
my$foo=$self->param('args');
$self->render(text=>"Hellofrom$foo.");
}


这个,我们直接取请求的body就可以了,但这有个小问题,这是这个文件上传完,这个body会存着所有的文件,比如这个上传的文件有1G,这个1G就都会占着内存.这个情况,Mojolicious并没有实现事件来根据块取文件.晚点,我有个有于大文件上传的文章,会分享我在Mojolicious中实现异步以块方式存储文件.这样用户上传多少,我存多少,并不会占用更多的内存.

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: