TCP/IP网络的数据传输通常建立在数据块的基础之上。从程序员的观点来看,发送数据意味着发出(或者提交)一系列“发送数据块”的请求。在系统级,发送单个数据块可以通过调用系统函数write() 或者SENDFILE() 来完成。在网络级可以看到更多的数据块,通常把它们叫做帧,帧再被包装上一定字节长度的报头然后通过线路在网络上传输。帧及其报头内部的信息是由若干协议层定义的,从OSI参考模型的物理层到应用层都可能会牵扯到。
因为在网络连接中是由程序员来选择最适当的应用协议,所以网络包的长度和顺序都在程序员的控制之下。同样的,程序员还必须选择这个协议在软件中得以实现的方式。TCP/IP协议自身已经有了多种可互操作的实现,所以在双方通信时,每一方都有它自身的低级行为,这也是程序员所应该知道的情况。
通常情况下,程序员不必关心操作系统和网络协议栈发送和接收网络数据的方法。系统内置算法定义了低级的数据组织和传输方式;然而,影响这些算法的行为以及对网络连接施加更大强度控制能力的方法也是有的。例如,如果某个应用协议使用了超时和重发机制,程序员就可以采取一定措施设定或者获取超时参数。他或她还可能需要增加发送和接收缓冲区的大小来保证网络上的信息流动不会中断。改变TCP/IP协议栈行为的一般的方法是采用所谓的TCP/IP选项。下面就让我们来看一看你该如何使用这些选项来优化数据传输。
有好几种选项都能改变TCP/IP协议栈的行为。使用这些选择能对在同一计算机上运行的其他应用程序产生不利的影响,因此普通用户通常是不能使用这些选项的(除了root用户以外)。我们在这里主要讨论能改变单个连接操作(用TCP/IP的术语来说就是套接字)的选项。
ioctl风格的getsockopt()和setsockopt()系统函数都提供了控制套接字行为的方式。比方说,为了在Linux上设置TCP_NODELAY选项,你可以如下编写代码:
intfd, on = 1;
…
/* 此处是创建套接字等操作,出于篇幅的考虑省略*/
…
setsockopt (fd, SOL_TCP, TCP_NODELAY, &on, sizeof (on));
尽管有许多TCP选项可供程序员操作,而我们却最关注如何处置其中的两个选项,它们是TCP_NODELAY 和 TCP_CORK,这两个选项都对网络连接的行为具有重要的作用。许多UNIX系统都实现了TCP_NODELAY选项,但是,TCP_CORK则是Linux系统所独有的而且相对较新;它首先在内核版本2.4上得以实现。此外,其他UNIX系统版本也有功能类似的选项,值得注意的是,在某种由BSD派生的系统上的TCP_NOPUSH选项其实就是TCP_CORK的一部分具体实现。
TCP_NODELAY和TCP_CORK基本上控制了包的“Nagle化”,Nagle化在这里的含义是采用Nagle算法把较小的包组装为更大的帧。John Nagle是Nagle算法的发明人,后者就是用他的名字来命名的,他在1984年首次用这种方法来尝试解决福特汽车公司的网络拥塞问题(欲了解详情请参看IETF RFC 896)。他解决的问题就是所谓的silly window syndrome ,中文称“愚蠢窗口症候群”,具体含义是,因为普遍终端应用程序每产生一次击键操作就会发送一个包,而典型情况下一个包会拥有一个字节的数据载荷以及40个字节长的包头,于是产生4000%的过载,很轻易地就能令网络发生拥塞,。 Nagle化后来成了一种标准并且立即在因特网上得以实现。它现在已经成为缺省配置了,但在我们看来,有些场合下把这一选项关掉也是合乎需要的。
现在让我们假设某个应用程序发出了一个请求,希望发送小块数据。我们可以选择立即发送数据或者等待产生更多的数据然后再一次发送两种策略。如果我们马上发送数据,那么交互性的以及客户/服务器型的应用程序将极大地受益。例如,当我们正在发送一个较短的请求并且等候较大的响应时,相关过载与传输的数据总量相比就会比较低,而且,如果请求立即发出那么响应时间也会快一些。以上操作可以通过设置套接字的TCP_NODELAY选项来完成,这样就禁用了Nagle算法。
另外一种情况则需要我们等到数据量达到最大时才通过网络一次发送全部数据,这种数据传输方式有益于大量数据的通信性能,典型的应用就是文件服务器。应用Nagle算法在这种情况下就会产生问题。但是,如果你正在发送大量数据,你可以设置TCP_CORK选项禁用Nagle化,其方式正好同TCP_NODELAY相反(TCP_CORK 和 TCP_NODELAY 是互相排斥的)。下面就让我们仔细分析下其工作原理。
假设应用程序使用SENDFILE()函数来转移大量数据。应用协议通常要求发送某些信息来预先解释数据,这些信息其实就是报头内容。典型情况下报头很小,而且套接字上设置了TCP_NODELAY。有报头的包将被立即传输,在某些情况下(取决于内部的包计数器),因为这个包成功地被对方收到后需要请求对方确认。这样,大量数据的传输就会被推迟而且产生了不必要的网络流量交换。
但是,如果我们在套接字上设置了TCP_CORK(可以比喻为在管道上插入“塞子”)选项,具有报头的包就会填补大量的数据,所有的数据都根据大小自动地通过包传输出去。当数据传输完成时,最好取消TCP_CORK 选项设置给连接“拔去塞子”以便任一部分的帧都能发送出去。这同“塞住”网络连接同等重要。
总而言之,如果你肯定能一起发送多个数据集合(例如HTTP响应的头和正文),那么我们建议你设置TCP_CORK选项,这样在这些数据之间不存在延迟。能极大地有益于WWW、FTP以及文件服务器的性能,同时也简化了你的工作。示例代码如下:
intfd, on = 1;
…
/* 此处是创建套接字等操作,出于篇幅的考虑省略*/
…
setsockopt (fd, SOL_TCP, TCP_CORK, &on, sizeof (on)); /* cork */
write (fd, …);
fprintf (fd, …);
SENDFILE (fd, …);
write (fd, …);
SENDFILE (fd, …);
…
on = 0;
setsockopt (fd, SOL_TCP, TCP_CORK, &on, sizeof (on)); /* 拔去塞子 */
不幸的是,许多常用的程序并没有考虑到以上问题。例如,Eric Allman编写的sendmail就没有对其套接字设置任何选项。
Apache HTTPD是因特网上最流行的Web服务器,它的所有套接字就都设置了TCP_NODELAY选项,而且其性能也深受大多数用户的满意。这是为什么呢?答案就在于实现的差别之上。由BSD衍生的TCP/IP协议栈(值得注意的是FreeBSD)在这种状况下的操作就不同。当在TCP_NODELAY 模式下提交大量小数据块传输时,大量信息将按照一次write()函数调用发送一块数据的方式发送出去。然而,因为负责请求交付确认的记数器是面向字节而非面向包(在Linux上)的,所以引入延迟的概率就降低了很多。结果仅仅和全部数据的大小有关系。而 Linux 在第一包到达之后就要求确认,FreeBSD则在进行如此操作之前会等待好几百个包。
在Linux系统上,TCP_NODELAY的效果同习惯于BSD TCP/IP协议栈的开发者所期望的效果有很大不同,而且在Linux上的Apache性能表现也会更差些。其他在Linux上频繁采用TCP_NODELAY的应用程序也有同样的问题。
你的数据传输并不需要总是准确地遵守某一选项或者其它选择。在那种情况下,你可能想要采取更为灵活的措施来控制网络连接:在发送一系列当作单一消息的数据之前设置TCP_CORK,而且在发送应立即发出的短消息之前设置TCP_NODELAY。
把零拷贝和SENDFILE() 系统函数结合起来(前文有述)可以显著地提升系统整体效率并且降低CPU负载。我们采用这一技术为Swsoft’s Virtuozzo公司开发了基于名称的主机托管子系统,实践经验表明,该技术可以在装备350-MHz Pentium II CPU的PC上实现每秒9000个HTTP请求,这一成绩在以前几乎是不可能实现的。性能上的提高显而易见。