- 论坛徽章:
- 0
|
-----------------------------------------------------
Dillo网络浏览器中的并行网络编程
-----------------------------------------------------
Jorge Arellano-Cid
Horst H. von Brand
--------
概要
--------
网络编程面临很多在发送和接收数据时的资源延迟问题,这些程序中的问题直接
影响到了最终用户,很多知名的浏览器都是。这里我们引入了一个混合解决方案,
使用线程之间通过管道交流,并通过信号驱动I/O,这就使得住县城可以不被阻塞并
使得等待的时间区域重叠减少等待的总时间。
------------
简介
------------
Dillo工程并不是随意开始的 ,它的代码是基于一个叫gzilla的项目(一个由Raph
Levien 写的轻量级的网络浏览器)。全部的源代码在写得时候就已经经过了标准化
处理,网络引擎被替换成了更快速的全新实现。那以后,解析器也作了更改。数据流
的处理和和错误处理也划归到了CCC(Concomitant Control Chain)的控制之下。
以前的老旧的部件管理系统被重新实现了,我们叫它Dw。对于目前的源代码,我们
认为是非常稳定的测试版。可以在取得。你需要在GPL条
款下使用Dillo的源码。
这份文档介绍了关于混合解决方案的基本设计思路,Dillo使用这个方案解决了很多
潜在的问题。下面先介绍"延迟来源",后面将会对混合设计方案进行解说。
-------------
延迟源
-------------
网络编程在接受或者发送数据时将会面临许多的具有延迟性的源。在实际的网
络浏览器的会遇到如下的问题:
DNS请求:
处理解析一个域名的时间
初始化TCP链接
TCP协议的三次握手
发送请求:
花费在对远程主机回复的询问上
得到数据:
花费在等待接受数据以及接收数据的回复上
关闭TCP链接:
需要发送四个包来关闭序列化的TCP协议
在广域网环境下,上面列表中的每一项又都会关联到网络环境中的不可预料的
数秒的延迟。如果我们在每一个浏览页面都加上很多的链接(一个页面至少需要四
步),那么总的时间耗费将是相当可观的。
-----------------------------------
传统解决方案(阻塞式的)
-----------------------------------
传统的阻塞时的解决方案的主要面临一下的问题:
当一个发出一个操作,但是又不能被立即及执行时,处理器将会停下来等待直
到操作结束,这其间其他的事物都不能进行处理。
在等待一个特定的套接字的时候,其他链接的数据报可能已经到达,但是却必
须等待被处理。
网络浏览器需要处理很多小事务,如果等待的时间不能重复利用,那么用户在
使用时感觉到的延迟将会是很糟糕体验。
如果用户接口每次在进行网络操作的时候都停下来,那么那么整个程序就变得
不可响应,这绝不是用户需要的。
不使用时间交叠技术所引起的在绘制上的表现变得很慢,这是完全没有必要的。
(这一点可以由浏览器的核心函数来证明)
---------------------
Dillo 的混合解决方案
---------------------
Dillo大量的使用线程和信号驱动的I/O来交叠得到时间和计算任务。处理用
户接口事务的线程永远不会阻塞,这给了用户一个良好的使用体验。利用gtk,
使用一个成熟的部件框架来构建用户接口。对整体目标的最终实现时非常有用
的。所有的接口都是构建在这个混合解决方案之上的。
这个设计方案之所以被叫做"混合式的",是因为它使用了线程去处理DNS请
求和本地文件的读取,又使用了信号驱动的I/O来完成TCP的相关任务。线程化
的DNS方式是潜在并发的(依赖于底层实现),而同时I/O处理确是并行进行的。
(本地文件的读取和远程链接均是如此)
为了简化浏览器的结构,本地的文件被转换成HTTP流的形式并表现在浏览器
平台上。这样,远程的链接也可以同样的被正确处理。为了实现这个构想,我们
创建了一个线程。这个线程打开了一个通向浏览器的管道,并生成一个恰当HTTP
头,然后和文件一起发送给浏览器。这样,在浏览器看来不论是来自本地文件还是
远程数据,都是一样的。
处理一个远程链接会更加的复杂。这种情况下,浏览器就需要对URL进行缓存
管理。URL中的名称将被DNS引擎处理,然后一个TCP套接字就建立起来,HTTP
请求已经发出,最后会接收到远程返回的数据。前面提到的每一步都回出现错误,
这些错误都需要处理同时以某种方式停止程序的链接。出于性能上的原因,远程
的响应数据将被缓冲在本地,所以远程链接并不会直接把数据转交给浏览器。来
自远程的响应数据将被传递给缓存管理器,缓存管理器负责将数据转递给浏览器平
台。DNS引擎会缓存下DNS请求的处理结果。而HTTP请求会在另一个独立的线程
中进行处理,所以浏览器也不会因为HTTP请求而阻塞等待了。
这些提及到的行为不一会严格的按照顺序发生。甚至有可能很多URL会同时被处
理,为了能够交叠 等待和下载的时间,函数将直接从用户接口调用,使得用户的操
作可以立即得到响应。有时,会返回还没有被完全建立好的链接处理器。而另一种
情况,I/O是信号驱动的,当某一个描述符准备好了传输(读或者写),就会唤醒I/O
引擎。
浏览器内部的线程之间的数据传送器是由管道处理的,共享内存很少被用到。着
避免了绝大部分的同步操作。而并发是一个程序中容易出错的区域。Dillo处理它的
线程的方式,开发者可以认为这是在另一个独立的线程中进行控制的。通过在主线程
使用DNS引擎的回调函数,以及将通过管道家在文件独立出来,Dillo的线程处理方
式变得更加完善。
这样使用线程有三大好处:
浏览器不会因为他的某个子线程的阻塞而阻塞。例如,用户接口可以同时处
理名称解析和下载任务。
开发者不需要面对复杂的并发事务。并发事务处理起来是比较难的,只有较少
的开发者对此比较熟练。这样就会失去很对潜在的开发者。而他们很可能会
对这个开源项目做出贡献。
通过使代码绝大部分的序列化,像gdb这样的调试器就可以使用了。调试并
行程序也是很难的,而且也不容易得到一个好的工具。
出于简化和可移植性的关系,DNS请求在一个单独的线程中处理,标准C库
没有提供用来非阻塞的完成DNS请求的函数。必须重新实现,定制的DNS请求
实现是非阻塞的,这的确是一个复杂的任务。在程序的线程结构加入这样的机
制反而是很简单的。
使用一个线程或者管道来读取本地的文件处理中加上了缓冲步骤(必然需要
消耗一些反应时间)。但是他又有以下优点:
处理本地文件和远程链接使用同样的方式,大量的代码被重用了。
如果需要的话,在文件数据处理中加入一个预处理步骤很容易,实际上文件
被包装成了HTTP数据流。
-----------
DNS请求
-----------
Dillo使用一个线程来处理DNS请求,然后使用一个子线程等待DNS服务器的
回复。当回复到达时,一个回调函数被调用,然后程序继续执行前面在执行的任
务。令人感兴趣的是这个回调函数是在主线程中完成的,而子线程在执行完毕后
就简单得退出了。这是通过一个服务通道的设计来实现的。
服务通道
------------------
对于每一个通道都有 一个线程,而每一个通道都可以有各种的客户端。当程序
请求一个IP地址的时候,服务首先查看时候有缓存与其匹配,如果是客户端的回调
函数立即被调用,如果不是,客户端就被加入到一个队列中,一个线程被创建,用
来请求DNS,一个GTK+空闲的客户端就对这个队列每秒钟进行5次轮训,当都成功
执行了以后,这个通道的每个客户端都得到了服务。
这样的方案允许所有更多的处理能够在同一个线程继续执行,即在主线程。
.
------------------------
处理TCP链接
------------------------
建立一个 TCP 链接需要对三次握手非常熟悉。依赖于网络以及许多其他的问题,
会发生严重的网络延迟。
Dillo处理这样的链接是使用非阻塞套接字方案。基本上,需要一个套接字文件描述符
类型AF_INET以及非阻塞的I/O。当DNS服务器解析了名称后,套接字链接处理就从
connect(2)函数开始。
{我们使用Unix协定,在使用手册的相关章节有介绍,这种情况下,第二部分(系统调用)}
这个函数会立即返回一个EINPROGRESS错误。
在链接到达EINPROGRESS状态之后,套接字就在后台等待知道连接成功的建立(或
者失败)。当成功后,一个回调函数被调用来执行下面的步骤:设置I/O引擎,发送请求
并且期待回复(都是在后台执行)。
这种方案的优点在于:每一个请求的步骤都很快完成并且不阻塞浏览器。最后,只要
I/O是可用的,套接字就会产生一个信号。
----------------
处理询问
----------------
对于一个HTTP的URL,询问会被转换成一个简略传输(HTTP 请求)而且长的
可恢复的处理。请求并不都是短的,特别是当请求一个form(form中的所有数据
都在请求中)的时候是比较长的,同样请求一个CGI也是很长的。
不管请求有多长,请求的发送都是在后台执行的。这个线程师在TCP链接建立
的时候开始的,线程已经建立好了所有的传输框架。在这一点上,包的发送仅仅是
等待写信号(G_IO_OUT),然后就开始发送数据。当套接字为传输做好准备以后数
据的发送就使用IO_write进行。
--------------
数据取回
--------------
虽然在概念上和发送数据很相似,取回的数据和数据接收还是有很大不同的,取
回的数据轻易的旧可以超过请求的数据(比如下载图片和文件)。这是延迟的主要的
来源。在下载一个大型文件的时候,取回数据可能需要花费数秒或者数分钟的时间。
对于一个单独的文件的取回数据的操作,从建立一个TCP链接的框架开始,然后
简单的等待读取信号(G_IO_IN),取得信号后低级的I/O引擎就开始被调用,数据被
读取到一个预先分配的缓冲区然后一个适当的回调函数被调用。从技术上说,无论何
时G_IO_IN事件被创建,就可以使用IO_read函数从接字文件描述符接接收数据就。
这样的反复的处理直到得到一个EOF是才结束(或者出现错误)。
----------------------
关闭链接
----------------------
关闭一个TCP需要四个数据片断,不是一个印象深刻的数字,但是是两次往
返的次数,这很可信。当数据取回结束,套接字的关闭被处罚。仅仅是在套接字
文件描述符调用了O_close_fd。处理被分为了四步用来关闭套接字的两端,
一个是当请求发送完成,另一个是所以数据都已经接收到。由于写操作的关闭
同样会使得读操作的停止,所以,这样的设计目前并没有被采用。
低级I/O引擎
------------------------
Dillo I/O 是在后台实现的。这是通过使用低级文件描述符和信号实现的。任
何时候一个文件描述符在活动的时候,一个信号产生信号处理器就会管理I/O。
低级的I/O引擎被设计成为服务于后台文件描述符行为的内部抽象层。他仅仅会
被缓冲模式用到。更改高级的程序对URL事务处理应该使用缓冲。任何要在后台执
行的都应该通过I/O引擎来处理。比如TCP 套接字的,他们被创建并提交给
I/O引擎以便执行更多的操作。
提交处理的请求必须填写一个请求结构体并且让I/O引擎处理活动的文件描述符
,直到接收到一个会调函数最终处理数据。下面就是这个结构体:
typedef struct {
gint Key; /* 主键 (for klist) */
gint Op; /* IORead | IOWrite | IOWrites */
gint FD; /* 当前文件描述符 */
gint Flags; /* 标志数组 */
glong Status; /* 读取的字节数量,或者是一个错误号-errno */
void *Buf; /* 缓冲地址 */
size_t BufSize; /* 缓冲长度 */
void *BufStart; /* 私有的: 仅仅在 IO.c 中使用! */
void *ExtData; /* 外部数据引用(不会被 IO.c使用) */
void *Info; /* 这个IO的CCC 信息结构体 */
GIOChannel *GioCh; /* IO 通道 */
guint watch_id; /* glib's 事件源 id */
} IOData_t;
请求一个I/O操作,这个结构体必须填充好并传递给I/O引擎。
'Op' 和 'Buf' 和 'BufSize' 必须提供.
'ExtData' 可以提供.
'Status', 'FD' 和 'GioCh' 是 I/O 引擎内部使用的.
当在文件描述符中有了新的数据,'IO_callback'被调用。在I/O引擎处理
完成后立即通知上层进行处理
I/O 引擎传输缓冲
------------------------------
请求结构体的'Buf' 和 'BufSize'字段提供了每隔操作的传输的缓冲。这个缓
冲区必须是客户段设置的(为避免拷贝数据以提升性能)。
在读取时,客户段指定了取回数据的存放地点以及数量,在写出时,需要指定
需要发送的数据的片断的数量以及数据源。虽然这样做使事情变得有些复杂,但是
却大大增加了处理速度。例如,如果一个文档的大小是提前可知的,那么所有数据
的缓冲就可以以一次性建立好。可以避免内存的多次分配。更深一步,如果数据传
输的大小已知,而且数据传输是通过很多数据块进行的,那么客户段仅仅需要更新
'Buf' 和 'BufSize',这个buf和bufSize来自前面预先分配那一大块缓冲。(通过
增加buf块的大小)。另一方面,如果传输的文件大小不提前获取,接收的缓冲区
可以保持不变直到链接被关闭,但是客户端必须完成常规的缓冲区的复制以及重
新分配内存。
I/O引擎同样允许客户端在发送数据时指定一个完全长度的传输缓冲。数据配
装配成一个包或者不是都没有关系。把数据分成小块是I/O引擎的工作。
------------------------------------------
处理并发链接
------------------------------------------
前面的章节讲述了内部的单个的链接的工作流程。I/O引擎处理大部分这些
事务都是并行处理的。一个在Web页面很常规的下载行为,通常都回涉及到其
他的文件(比如图片)有时甚至是其他的站点。为了解析并完成页面的表现,另一
份文档必须被取回并表现出来。所以,同时发生多个下载链接并不常见。
即使套接字的行为可以达到一个很高的频度,浏览器也不会阻塞。注意I/O
引擎也是直接影响到执行因素。
很重要的一点,对于这样的一个多重的回调链式的I/O引擎,里面的每一个
函数都要求很快的能够返回。否则整个系统将阻塞直到阻塞的函数返回。
-----------
结束语
-----------
Dillo现在是一个很稳定的测试版,他已经有了令人印象深刻的表现。而且他
的交互式用户体验比其他的浏览器都要好。
Dillo的模块,以及他所依赖的GTK1使得它非常的小。并不是所有的HTML4.01
都被实现了,但是要实现他们也不是什么大问题。
Dillo的核心中的I/O引擎使用的POSIX和TCP/IP网络的一些高级特性使得它的
性能可能石油少数的骇客才能驾驭它并改进它。
一个干净的,严谨的,应用层解决方案基于干净的抽象是虚拟在每一个程序
中,一个好的具有良好支持性的框架会很有好处。
本文来自ChinaUnix博客,如果查看原文请点:http://blog.chinaunix.net/u1/35985/showart_400449.html |
|