欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  网络运营

TCP协议:抽象泄漏定律

程序员文章站 2022-03-16 16:58:10
TCP协议是互联网的基石,我们每天都需要依靠它来构建各类互联网应用。也正是在这一协议中,时刻发生着一件近乎神奇的事情。 TCP是一种 可靠的 数据传输协议,也就是说,当你通过TCP协议在网络...

TCP协议是互联网的基石,我们每天都需要依靠它来构建各类互联网应用。也正是在这一协议中,时刻发生着一件近乎神奇的事情。

TCP是一种 可靠的 数据传输协议,也就是说,当你通过TCP协议在网络上传输一条消息时,它一定会到达目的地,而且不会失真或毁坏。

我们可以使用TCP来做很多事情,从浏览网页信息到收发邮件。TCP的可靠性使得东非贪污受贿的新闻能够一字一句地传递到世界各地。真是太棒了!

和TCP协议相比,IP协议也是一种传输协议,但它是 不可靠的 。没有人可以保证你的数据一定会到达目的地,或者在它到达前就已经被破坏了。如果你发送了一组消息,不要惊讶为何只有一半的消息到达,有些消息的顺序会不正确,甚至消息的内容被替换成了黑猩猩宝宝的图片,或是一堆无法阅读的垃圾数据,像极了*人的邮件标题。

这就是TCP协议神奇的地方:它是构建在IP协议之上的。换句话说,TCP协议能够 使用一个不可靠的工具来可靠地传输数据 。

为了更好地说明这有多么神奇,让我们设想下面的场景。虽然有些荒诞,但本质上是相同的。

假设我们用一辆辆汽车将百老汇的演员们运送到好莱坞,这是一条横跨美国的漫长线路。其中一些汽车出了交通事故,车上的演员在事故中死亡。有些演员则在车上酗酒嗑药,兴奋之余将自己的头发剃了,或是纹上了丑陋的纹身,这样一来就失去了他们原先的样貌,无法在好莱坞演出。更普遍的情况是,演员们没有按照出发的顺序到达目的地,因为他们走的都是不同的线路。现在再让我们设想有一个名为“好莱坞快线”的运输服务,在运送这些演员时能够保证三点:他们都能够到达;到达顺序和出发顺序一致;并且都完好无损。神奇的是,好莱坞快线除了用汽车来运输这些演员之外,没有任何其他的方法。所以它能做的就是检查每一个到达目的地的演员,看他们是否和原先的相貌一致。如果有所差别,它就立刻通知百老汇的办公室,派出该演员的双胞胎兄妹,重新发送过来。如果演员到达的顺序不同,好莱坞快线会负责重新排序。如果有一架UFO在飞往51区的途中不慎坠毁在内华达州,造成高速公路阻塞,这时所有打算从这条路经过的演员会绕道亚利桑那州。好莱坞快线不会告诉加利福尼亚州的导演路上发生了什么,只是这些演员到的比较迟而已。

这大致上就是TCP协议的神奇之处,计算机科学家们通常会将其称作为“抽象”:将复杂的问题用简单的方式表现出来。事实上,很多计算机编程工作都是在进行抽象。字符串库做了什么?它能让我们觉得计算机可以像处理数字那样处理文字。文件系统是什么?它让硬盘不再是一组高速旋转的磁性盘块,而是一个有着目录层级结构、能够按字节存储字符信息的设备。

我们继续说TCP。刚才我打了一个比方,有些人可能觉得那很疯狂。但是,当我说TCP协议可以保证消息一定能够到达,事实上并非如此。如果你的宠物蛇把网线给咬坏了,那即便是TCP协议也无法传输数据;如果你和网络管理员闹了矛盾,他将你的网口接到了一台负载很高的交换机上,那即便你的数据包可以传输,速度也会奇慢无比。

这就是我所说的“抽象泄漏”。TCP协议试图提供一个完整的抽象,将底层不可靠的数据传输包装起来,但是,底层的传输有时也会发生问题,即便是TCP协议也无法解决,这时你会发现,它也不是万能的。TCP协议就是“抽象泄漏定律”的示例之一,其实,几乎所有的抽象都是泄漏的。这种泄漏有时很小,有时会很严重。下面再举一些例子:

对于一个简单的操作,如循环遍历一个二维数组,当遍历的方式不同(横向或纵向),也会对性能造成很大影响,这主要取决于数组中数据的分布——按某个方向遍历时可能会产生更多的页缺失(page fault),而页缺失往往是非常消耗性能的。即使是汇编程序员,他们在编写代码时也会假设程序的内存空间是连续的,这是系统底层的虚拟内存机制提供的抽象,而这一机制在遇到页缺失时就会消耗更多时间。

SQL语言意图将过程式的数据库访问操作封装起来,你只需要告诉操作系统你想要的数据,系统会自动生成各个步骤并加以执行。但在有些情况下,某些SQL查询会比其逻辑等同的查询语句要慢得多。一个著名的示例是,对大多数SQL服务器,指定“WHERE a = b AND b = c AND a = c”要比单纯指定“WHERE a = b AND b = c”快的多,即便它们的结果集是一致的。在使用SQL时,我们不需要思考过程,只需关注定义。但有时,这种抽象会造成性能上的大幅下降,你需要去了解SQL语法分析器的工作原理,找出问题的原因,并想出应对措施,让自己的查询运行得更快。

即便有NFS、SMB这样的协议可以让你像在处理本地文件一样处理远程文件,如果网络传输很慢,或是完全中断了,程序员就需要手动处理这种情况。所以,这种“远程文件即本地文件”的抽象机制是存在泄漏的。这里举一个现实的例子:如果你将用户的home目录加载到NFS上(一次抽象),你的用户创建了.forward文件,用来转发他所有的电子邮件(二次抽象),当NFS服务器宕机,.forward文件会找不到,这样就无法转发邮件了,造成丢失。

C++的字符串处理类库相当于增加了一种基础数据类型:字符串,将各种操作细节封装起来,让程序员可以方便地使用它。几乎所有的C++字符串类都会重载+操作符,这样你就能用 s + “bar” 来拼接字符串了。但是,无论哪种类库都无法实现 “foo” + “bar” 这种语句,因为在C++中,字符串字面量(string literal)都是char *类型的。这就是一种泄漏。(有趣的是,C++语言的发展历程很大一部分是在争论字符串是否应该在语言层面支持。我个人并不太能理解这为何需要争论。)

当你在雨天开车,虽然你坐在车里,前窗有雨刷,车内有空调,这些措施将“天气”给抽象走了。但是,你还是要小心雨天的轮胎打滑,有时这雨下得太大,可见度很糟,所以你还是得慢行。也就是说,“天气”因素并没有被完全抽象走,它也是存在泄漏的。

抽象泄漏引发的麻烦之一是,它并没有完全简化我们的工作。当我指导别人学习C++时,我当然希望可以跳过char *和指针运算,直接讲解STL字符串类库的使用。但是,当某一天他写出了 “foo” + “bar” 这样的代码,并询问我为什么编译错误时,我还是需要告诉它char *的存在。或者说,当他需要调用一个Windows API,需要指定OUT LPTSTR参数,这时他就必须学习char *、指针、Unicode、wchar_t、TCHAR头文件等一系列知识,这些都是抽象泄漏。

在指导COM编程时,我希望可以直接让大家如何使用Visual Studio的代码生成向导。但将来如果出现问题,学员面对这些生成的代码会不知所从,这时还是要回过头来学习IUnknown、CLSID、ProgIDS等等。天呐!

在指导ASP.NET编程时,我希望可以直接告诉大家双击页面上的控件,在弹出的代码框中输入点击响应事件。的确,ASP.NET将处理点击的HTML代码抽象掉了,但问题在于,ASP.NET的设计者需要动用JS来模拟表单的提交,因为HTML中的标签是没有这一功能的。这样一来,如果终端用户将JS禁止了,这个程序将无法运行。初学者会不知所措,直至他了解ASP.NET的运作方式,了解它究竟将什么样的工作封装起来了,才能进一步排查。

由于抽象定律的存在,每当有人说自己发现了一款新的代码生成工具,能够大大提高我们的编程效率时,你会听很多人说“先学习手工编写,再去用工具生成”。代码生成工具是一种抽象,同样也会泄漏,唯一的解决方法是学习它的实现原理,即它抽象了什么。所以说抽象只是用于提高我们的工作效率的,而不会节省我们的学习时间。

这就形成了一个悖论:当我们拥有越来越高级的开发工具,越来越好的“抽象”,要成为一个高水平的程序员反而越来越困难了。

我在微软实习的第一年,是为Macintosh编写字符串处理类库。很普通的一个任务:编写 strcat 函数,返回一个指针,指向新字符串的尾部。几行C语言代码就能实现了,这些都是从K&R这本C语言编程书上学习到的。

如今,我在CityDesk供职,需要使用Visual Basic、COM、ATL、C++、InnoSetup、Internet Explorer原理、正则表达式、DOM、HTML、CSS、XML等等,这些相对于古老的K&R来说都是非常高级的工具,但是我仍然需要用到K&R的相关知识,否则会困难重重。

十年前,我们会想象未来能够出现各种新式的编程范型,简化我们的工作。的确,这些年我们创造的各类抽象使得开发复杂的大型软件变得比十五年前要简单得多,就像GUI和网络编程。现代的面向对象编程语言让我们的工作变得高效快速。但突然有一天,这种抽象泄漏出一个问题,解决它需要耗费两星期。如果你需要招录一个VB程序员,那不是一个好主意,因为当他碰到VB语言泄漏的问题时,他会变得寸步难行。

抽象泄漏定律正在阻碍我们前进。