Linux高性能服务器编程.pdf
http://www.100md.com
2020年1月9日
![]() |
| 第1页 |
![]() |
| 第7页 |
![]() |
| 第19页 |
![]() |
| 第28页 |
![]() |
| 第40页 |
![]() |
| 第113页 |
参见附件(5999KB,337页)。
Linux高性能服务器编程,这是一本关于Linux编程的书籍教程,作者为Linux资深开发工程师,全书一共有三大篇十七个章节,读者们可以在这里深入浅出了解Linux编程思想。

Linux高性能服务器编程简介
本书是Linux服务器编程领域的经典著作,由资深Linux软件开发工程师撰写,从网络协议、服务器编程核心要素、原理机制、工具框架等多角度全面阐释了编写高性能Linux服务器应用的方法、技巧和思想。不仅理论全面、深入,抓住了重点和难点,还包含两个综合性案例,极具实战意义。
全书共17章,分为3个部分:部分对Linux服务器编程的核心基础——TCP/IP协议进行了深入的解读和阐述,包括TCP/IP协议族、TCP/IP协议,以及一个经典的TCP/IP通信案例;二部分对高性能服务器编程的核心要素进行了全面深入的剖析,包Linux网络编程API、高级I/O函数、Linux服务器程序规范、高性能服务器程序框架、I/O复用、信号、定时器、高性能I/O框架库Libevent、多进程编程、多线程编程、进程池和线程池等内容,原理、技术与方法并重;三部分从侧重实战的角度讲解了高性能服务器的优化与监测,包服务器的调制、调试和测试,以及各种实用系统监测工具的使用等内容。
Linux高性能服务器编程作者
游双,Linux软件开发工程师,对Linux网络编程,尤其是服务器端的编程,有非常深入的研究,实战经验也十分丰富。曾就职于摩托罗拉,担任高级Linux软件工程师。此外,他还精通C++、Android、QT等相关的技术。活跃于Chinaunix等专业技术社区,发表了大量关于Linux网络编程的文章,深受社区欢迎。
Linux高性能服务器编程目录
第一篇、TCP/IP协议详解
第1章TCP/IP协议族
第2章IP协议详解
第3章TCP协议详解
第4章TCP/IP通信案例:访问Internet上的Web服务器第二篇、深入解析高性能服务器编程第5章Linux网络编程基础API
第6章高级IO函数
第7章Linux服务器程序规范
第8章 高性能服务器程序框架
第9章I/O复用
第10章信号
第11章 定时器
第12章高性能VO框架库Libevent
第13章多进程编程
第14章多线程编程
第15章进程池和线程池
第三篇、高性能服务器优化与监则
第16章服务器调制、调试和测试
第17章系统监测工具
Linux高性能服务器编程截图


腾讯微博 @yanfabook
新浪微博 @研发书局
官方网址:www.hzmedia.com.cn
客服信箱:service@bbbvip.com
客服热线:+ 86-10-68995265
版权所有,侵权必究
制作与发行。
本书纸版由机械工业出版社于2013年出版,电子版由华章分社(北京华章图文信息有限公司)全球范围内
ISBN:978-7-111-42519-9
游双 著
Linux高性能服务器编程
加工、传播自负法律后果。
本书仅供个人学习之用,请勿用于商业用途。如对本书有兴趣,请购买正版书籍。任何对本书籍的修改、目 录 前言
为什么要写这本书
读者对象
本书特色
如何阅读本书
勘误和支持
致谢
第一篇 TCPIP协议详解
第1章 TCPIP协议族
1.1 TCPIP协议族体系结构以及主要协议
1.2 封装
1.3 分用
1.4 测试网络
1.5 ARP协议工作原理
1.6 DNS工作原理
1.7 socket和TCPIP协议族的关系
第2章 IP协议详解
2.1 IP服务的特点
2.2 IPv4头部结构
2.3 IP分片
2.4 IP路由
2.5 IP转发
2.6 重定向
2.7 IPv6头部结构
第3章 TCP协议详解
3.1 TCP服务的特点
3.2 TCP头部结构
3.3 TCP连接的建立和关闭
3.4 TCP状态转移
3.5 复位报文段
3.6 TCP交互数据流
3.7 TCP成块数据流
3.8 带外数据
3.9 TCP超时重传
3.10 拥塞控制
第4章 TCPIP通信案例:访问Internet上的Web服务器
4.1 实例总图
4.2 部署代理服务器
4.3 使用tcpdump抓取传输数据包
4.4 访问DNS服务器
4.5 本地名称查询
4.6 HTTP通信
4.7 实例总结
第二篇 深入解析高性能服务器编程
第5章 Linux网络编程基础API
5.1 socket地址API
5.2 创建socket
5.3 命名socket
5.4 监听socket
5.5 接受连接
5.6 发起连接
5.7 关闭连接5.8 数据读写
5.9 带外标记
5.10 地址信息函数
5.11 socket选项
5.12 网络信息API
第6章 高级IO函数
6.1 pipe函数
6.2 dup函数和dup2函数
6.3 readv函数和writev函数
6.4 sendfile函数
6.5 mmap函数和munmap函数
6.6 splice函数
6.7 tee函数
6.8 fcntl函数
第7章 Linux服务器程序规范
7.1 日志
7.2 用户信息
7.3 进程间关系
7.4 系统资源限制
7.5 改变工作目录和根目录
7.6 服务器程序后台化
第8章 高性能服务器程序框架
8.1 服务器模型
8.2 服务器编程框架
8.3 IO模型
8.4 两种高效的事件处理模式
8.4.3 模拟Proactor模式
8.5 两种高效的并发模式
8.6 有限状态机
8.7 提高服务器性能的其他建议
第9章 IO复用
9.1 select系统调用
9.2 poll系统调用
9.3 epoll系列系统调用
9.4 三组IO复用函数的比较
9.5 IO复用的高级应用一:非阻塞connect
9.6 IO复用的高级应用二:聊天室程序
9.7 IO复用的高级应用三:同时处理TCP和UDP服务
9.8 超级服务xinetd
第10章 信号
10.1 Linux信号概述
10.2 信号函数
10.3 信号集
10.4 统一事件源
10.5 网络编程相关信号
第11章 定时器
11.1 socket选项SO_RCVTIMEO和SO_SNDTIMEO
11.2 SIGALRM信号
11.3 IO复用系统调用的超时参数
11.4 高性能定时器
第12章 高性能IO框架库Libevent
12.1 IO框架库概述
12.2 Libevent源码分析
第13章 多进程编程13.1 fork系统调用
13.2 exec系列系统调用
13.3 处理僵尸进程
13.4 管道
13.5 信号量
13.6 共享内存
13.7 消息队列
13.8 IPC命令
13.9 在进程间传递文件描述符
第14章 多线程编程
14.1 Linux线程概述
14.2 创建线程和结束线程
14.3 线程属性
14.4 POSIX信号量
14.5 互斥锁
14.6 条件变量
14.7 线程同步机制包装类
14.8 多线程环境
第15章 进程池和线程池
15.1 进程池和线程池概述
15.2 处理多客户
15.3 半同步半异步进程池实现
15.4 用进程池实现的简单CGI服务器
15.5 半同步半反应堆线程池实现
15.6 用线程池实现的简单Web服务器
第三篇 高性能服务器优化与监测
第16章 服务器调制、调试和测试
16.1 最大文件描述符数
16.2 调整内核参数
16.3 gdb调试
16.4 压力测试
第17章 系统监测工具
17.1 tcpdump
17.2 lsof
17.3 nc
17.4 strace
17.5 netstat
17.6 vmstat
17.7 ifstat
17.8 mpstat
参考文献前言
为什么要写这本书
目前国内计算机书籍的一个明显弊病就是内容宽泛而空洞。很多书籍长篇大论,恨不得囊括所有最新
的技术,但连一个最基本的技术细节也无法解释清楚。有些书籍给读者展现的是网络上随处可见的知识,基本没有自己的观点,甚至连一点自己的总结都没有。反观大师们的经典书籍,整本书只专注于一个问
题,而且对每个技术细节的描述都是精雕细琢。最关键的是,我们在阅读这些经典书籍时,似乎是在用心
与一位编程高手交流,这绝对是一种享受。
我们把问题缩小到计算机网络编程领域。关于计算机网络编程的相关书籍,不得不提的是已故网络教
育巨匠W·Richard Stevens先生的《TCPIP协议详解》(三卷本),以及《UNIX网络编程》(两卷本)。作
为一名网络程序员,即使没有阅读过这几本书,也应该听说过它们。但这几本书中的内容实在是太庞大
了,没有耐心的读者根本不可能把它们全部读完。而且对于英文不太好的朋友来说,选择阅读其翻译版本
又有失原汁原味。
基于以上两点原因,笔者编写了这本《Linux高性能服务器编程》。本书是笔者多年来学习网络编程之
总结,是在充分理解大师的作品并融入自己的理解和见解后写成的。本书讨论的主题和定位很明确。简单
来说就是:如何通过各种手段编写高性能的服务器程序。
网络技术是在不断向前发展的,比如Linux提供的epoll机制就是在内核2.6版本之后才正式引入的。但
是,编程思想却可以享用一辈子。我们在不断学习并使用新技术,不断适应新环境的同时,书中提到的网
络编程思想能让我们看得更远,想得更多。笔者相信,没有谁会认为W·Richard Stevens先生的网络编程书籍
过时了。读者对象
阅读本书之前,读者需要了解基本的计算机网络知识,并具有一定的Linux系统编程和C++编程基础,否则阅读起来会有些困难。本书读者对象主要包括:
·Linux网络应用程序开发人员
·Linux系统程序开发人员
·CC++程序开发人员
·对网络编程技术感兴趣,或希望参与网络程序开发的人员
·开设相关课程的大专院校师生本书特色
本书的特点:不求内容宽泛,但求专而精,深入地剖析服务器编程的要素;不求内容精准,但求融入
笔者自己的理解和观点,可谓“另眼”看服务器编程。
如何提高服务器程序性能是本书要着重讨论的。第6、8、9、11、12、15、16等章中都用了相当的篇幅
讨论这一主题。其论述方法是:首先,探讨提高服务器程序性能的一般原则,比如使用“池”以牺牲空间换
取效率,使用零拷贝函数以避免内核和用户空间的切换等;其次,介绍一些高效的编程模式及其应用,比
如使用有限状态机来分析用户数据,使用进程池或线程池来处理用户请求;最后,探讨如何通过调整系统
参数来从服务器程序外部提高其整体性能。
光说不练假把式。如果没有实例,或者只是给出几个“Hello World”,那么本书就真没有出版的必要
了。笔者要做的是让读者能真正把理论和实践完美地结合起来。在写作本书之前,笔者阅读了不少开源社
区的优秀服务器软件的源代码,自己也写过相当多的小型服务器程序。这些软件中那些最精彩的部分,在
书中都有充分的体现。比如第15章给出的两个实例——用进程池实现的简单CGI服务器和用线程池实现的简
单Web服务器,就充分展现了如何利用各种提高服务器性能的手段来高效地解决实际问题。
此外,为了帮助读者进一步把书中的知识融汇到实际项目中,笔者还特意编写了一个较为完整的负载
均衡服务器程序springsnail。该程序能从所有逻辑服务器中选取负荷最小的一台来处理新到的客户连接。在
这个程序中,使用了进程池、有限状态机、高效数据结构来提高其性能;同时,细致地封装了每个函数和
模块,使之更符合实际工程项目。由于篇幅的限制,笔者未将该程序的源代码列在书中,读者可从华章网
站[1]
上下载它。
[1]参见华章网站www.hzbook.com。——编辑注如何阅读本书
本书分为三篇:
第一篇(第1~4章)介绍TCPIP协议族及各种重要的网络协议。只有很好地理解了底层TCPIP通信的
过程,才能编写出高质量的网络应用程序。毕竟,坚实的基础设施造就稳固的上层建筑。
第二篇(第5~15章)细致地剖析了服务器编程的各主要方面,其中对每个重要的概念、模型以及函数
等都以实例代码的形式加以阐述。这一篇又可细分为如下四个部分:
·第一部分(第5~7章)介绍Linux操作系统为网络编程提供的众多API。这些API就像是基本的音符,我们通过组织它们来谱写优美的旋律。
·第二部分(第8章)探讨高性能服务器程序的一般框架。在这一部分中,我们将服务器程序解构为
IO单元、逻辑单元和存储单元三个部件,并重点介绍了IO单元、逻辑单元的几种高效实现模式。此外,我
们还探讨了提高服务器性能的其他建议。
·第三部分(第9~12章)深入剖析服务器程序的IO单元。我们将探讨IO单元需要处理的IO事件、信
号事件和定时事件,并介绍一款优秀的开源IO框架库——Libevent。
·第四部分(第13~15章)深入剖析服务器程序的逻辑单元。这一部分我们要讨论多线程、多进程编
程,以及高性能逻辑处理模型——进程池和线程池,并给出相应的实例代码。
第三篇(第16~17章)探讨如何从系统的角度优化和监测服务器性能。本篇的内容涉及服务器程序的
调制、调试和测试,以及诸多常用系统监测工具的使用。勘误和支持
由于作者的水平有限,加之编写时间仓促,书中难免会出现一些错误或者不准确的地方,恳请读者批
评指正。书中的全部源文件都可以从华章网站下载。如果您有更多的宝贵意见或建议,也欢迎发送邮件至
邮箱pjhq87@gmail.com,期待能够得到您的真挚反馈。致谢
首先要感谢伟大的网络教育导师W·Richard Stevens先生,他的书籍帮助了无数的网络程序开发人员,也
给笔者指明了学习的道路。
感谢机械工业出版社华章公司的编辑杨福川老师和孙海亮老师,是他们在这两年多的时间中始终支持
着我的写作,因为有了他们的鼓励、帮助和引导,笔者才能顺利完成全部书稿。
感谢好友史正政,他对编程充满热爱,也无私地为本书提供了原材料。
谨以此书献给我最亲爱的家人和朋友,以及那些为计算机网络教育做出巨大贡献的大师们,还有正在
为自己的未来努力拼搏、充满朝气和活力的IT工程师们!
游双
于北京第一篇 TCPIP协议详解
第1章 TCPIP协议族
第2章 IP协议详解
第3章 TCP协议详解
第4章 TCPIP通信案例:访问Internet上的Web服务器第1章 TCPIP协议族
现在Internet(因特网)使用的主流协议族是TCPIP协议族,它是一个分层、多协议的通信体系。本章
简要讨论TCPIP协议族各层包含的主要协议,以及它们之间是如何协作完成网络通信的。
TCPIP协议族包含众多协议,我们无法一一讨论。本书将在后续章节详细讨论IP协议和TCP协议,因为
它们对编写网络应用程序具有最直接的影响。本章则简单介绍其中几个相关协议:ICMP协议、ARP协议和
DNS协议,学习它们对于理解网络通信很有帮助。读者如果想要系统地学习网络协议,那么RFC(Request
For Comments,评论请求)文档无疑是首选资料。
1.1 TCPIP协议族体系结构以及主要协议
TCPIP协议族是一个四层协议系统,自底而上分别是数据链路层、网络层、传输层和应用层。每一层
完成不同的功能,且通过若干协议来实现,上层协议使用下层协议提供的服务,如图1-1所示。
图 1-1 TCPIP协议族体系结构及主要协议
1.1.1 数据链路层
数据链路层实现了网卡接口的网络驱动程序,以处理数据在物理媒介(比如以太网、令牌环等)上的
传输。不同的物理网络具有不同的电气特性,网络驱动程序隐藏了这些细节,为上层协议提供一个统一的
接口。
数据链路层两个常用的协议是ARP协议(Address Resolve Protocol,地址解析协议)和RARP协议
(Reverse Address Resolve Protocol,逆地址解析协议)。它们实现了IP地址和机器物理地址(通常是MAC
地址,以太网、令牌环和802.11无线网络都使用MAC地址)之间的相互转换。
网络层使用IP地址寻址一台机器,而数据链路层使用物理地址寻址一台机器,因此网络层必须先将目标机器的IP地址转化成其物理地址,才能使用数据链路层提供的服务,这就是ARP协议的用途。RARP协议
仅用于网络上的某些无盘工作站。因为缺乏存储设备,无盘工作站无法记住自己的IP地址,但它们可以利
用网卡上的物理地址来向网络管理者(服务器或网络管理软件)查询自身的IP地址。运行RARP服务的网络
管理者通常存有该网络上所有机器的物理地址到IP地址的映射。
由于ARP协议很重要,所以我们将在后面章节专门讨论它。
1.1.2 网络层
网络层实现数据包的选路和转发。WAN(Wide Area Network,广域网)通常使用众多分级的路由器来
连接分散的主机或LAN(Local Area Network,局域网),因此,通信的两台主机一般不是直接相连的,而
是通过多个中间节点(路由器)连接的。网络层的任务就是选择这些中间节点,以确定两台主机之间的通
信路径。同时,网络层对上层协议隐藏了网络拓扑连接的细节,使得在传输层和网络应用程序看来,通信
的双方是直接相连的。
网络层最核心的协议是IP协议(Internet Protocol,因特网协议)。IP协议根据数据包的目的IP地址来决
定如何投递它。如果数据包不能直接发送给目标主机,那么IP协议就为它寻找一个合适的下一跳(next
hop)路由器,并将数据包交付给该路由器来转发。多次重复这一过程,数据包最终到达目标主机,或者由
于发送失败而被丢弃。可见,IP协议使用逐跳(hop by hop)的方式确定通信路径。我们将在第2章详细讨
论IP协议。
网络层另外一个重要的协议是ICMP协议(Internet Control Message Protocol,因特网控制报文协议)。
它是IP协议的重要补充,主要用于检测网络连接。ICMP协议使用的报文格式如图1-2所示。
图 1-2 ICMP报文格式
图1-2中,8位类型字段用于区分报文类型。它将ICMP报文分为两大类:一类是差错报文,这类报文主
要用来回应网络错误,比如目标不可到达(类型值为3)和重定向(类型值为5);另一类是查询报文,这
类报文用来查询网络信息,比如ping程序就是使用ICMP报文查看目标是否可到达(类型值为8)的。有的
ICMP报文还使用8位代码字段来进一步细分不同的条件。比如重定向报文使用代码值0表示对网络重定向,代码值1表示对主机重定向。ICMP报文使用16位校验和字段对整个报文(包括头部和内容部分)进行循环
冗余校验(Cyclic Redundancy Check,CRC),以检验报文在传输过程中是否损坏。不同的ICMP报文类型
具有不同的正文内容。我们将在第2章详细讨论主机重定向报文,其他ICMP报文格式请参考ICMP协议的标
准文档RFC 792。
需要指出的是,ICMP协议并非严格意义上的网络层协议,因为它使用处于同一层的IP协议提供的服务
(一般来说,上层协议使用下层协议提供的服务)。
1.1.3 传输层
传输层为两台主机上的应用程序提供端到端(end to end)的通信。与网络层使用的逐跳通信方式不
同,传输层只关心通信的起始端和目的端,而不在乎数据包的中转过程。图1-3展示了传输层和网络层的这
种区别。图 1-3 传输层和网络层的区别
图1-3中,垂直的实线箭头表示TCPIP协议族各层之间的实体通信(数据包确实是沿着这些线路传递
的),而水平的虚线箭头表示逻辑通信线路。该图中还附带描述了不同物理网络的连接方法。可见,数据
链路层(驱动程序)封装了物理网络的电气细节;网络层封装了网络连接的细节;传输层则为应用程序封
装了一条端到端的逻辑通信链路,它负责数据的收发、链路的超时重连等。
传输层协议主要有三个:TCP协议、UDP协议和SCTP协议。
TCP协议(Transmission Control Protocol,传输控制协议)为应用层提供可靠的、面向连接的和基于流
(stream)的服务。TCP协议使用超时重传、数据确认等方式来确保数据包被正确地发送至目的端,因此
TCP服务是可靠的。使用TCP协议通信的双方必须先建立TCP连接,并在内核中为该连接维持一些必要的数
据结构,比如连接的状态、读写缓冲区,以及诸多定时器等。当通信结束时,双方必须关闭连接以释放这
些内核数据。TCP服务是基于流的。基于流的数据没有边界(长度)限制,它源源不断地从通信的一端流
入另一端。发送端可以逐个字节地向数据流中写入数据,接收端也可以逐个字节地将它们读出。
UDP协议(User Datagram Protocol,用户数据报协议)则与TCP协议完全相反,它为应用层提供不可靠、无连接和基于数据报的服务。“不可靠”意味着UDP协议无法保证数据从发送端正确地传送到目的端。
如果数据在中途丢失,或者目的端通过数据校验发现数据错误而将其丢弃,则UDP协议只是简单地通知应
用程序发送失败。因此,使用UDP协议的应用程序通常要自己处理数据确认、超时重传等逻辑。UDP协议
是无连接的,即通信双方不保持一个长久的联系,因此应用程序每次发送数据都要明确指定接收端的地址
(IP地址等信息)。基于数据报的服务,是相对基于流的服务而言的。每个UDP数据报都有一个长度,接
收端必须以该长度为最小单位将其所有内容一次性读出,否则数据将被截断。
SCTP协议(Stream Control Transmission Protocol,流控制传输协议)是一种相对较新的传输层协议,它
是为了在因特网上传输电话信号而设计的。本书不讨论SCTP协议,感兴趣的读者可参考其标准文档RFC
2960。
我们将在第3章详细讨论TCP协议,并附带介绍UDP协议。
1.1.4 应用层
应用层负责处理应用程序的逻辑。数据链路层、网络层和传输层负责处理网络通信细节,这部分必须
既稳定又高效,因此它们都在内核空间中实现,如图1-1所示。而应用层则在用户空间实现,因为它负责处
理众多逻辑,比如文件传输、名称查询和网络管理等。如果应用层也在内核中实现,则会使内核变得非常
庞大。当然,也有少数服务器程序是在内核中实现的,这样代码就无须在用户空间和内核空间来回切换
(主要是数据的复制),极大地提高了工作效率。不过这种代码实现起来较复杂,不够灵活,且不便于移
植。本书只讨论用户空间的网络编程。
应用层协议很多,图1-1仅列举了其中的几个:
ping是应用程序,而不是协议,前面说过它利用ICMP报文检测网络连接,是调试网络环境的必备工
具。
telnet协议是一种远程登录协议,它使我们能在本地完成远程任务,本书后续章节将会多次使用telnet客
户端登录到其他服务上。
OSPF(Open Shortest Path First,开放最短路径优先)协议是一种动态路由更新协议,用于路由器之间
的通信,以告知对方各自的路由信息。
DNS(Domain Name Service,域名服务)协议提供机器域名到IP地址的转换,我们将在后面简要介绍
DNS协议。
应用层协议(或程序)可能跳过传输层直接使用网络层提供的服务,比如ping程序和OSPF协议。应用
层协议(或程序)通常既可以使用TCP服务,又可以使用UDP服务,比如DNS协议。我们可以通
过etcservices文件查看所有知名的应用层协议,以及它们都能使用哪些传输层服务。1.2 封装
上层协议是如何使用下层协议提供的服务的呢?其实这是通过封装(encapsulation)实现的。应用程序
数据在发送到物理网络上之前,将沿着协议栈从上往下依次传递。每层协议都将在上层数据的基础上加上
自己的头部信息(有时还包括尾部信息),以实现该层的功能,这个过程就称为封装,如图1-4所示。
图 1-4 封装
经过TCP封装后的数据称为TCP报文段(TCP message segment),或者简称TCP段。前文提到,TCP协
议为通信双方维持一个连接,并且在内核中存储相关数据。这部分数据中的TCP头部信息和TCP内核缓冲区
(发送缓冲区或接收缓冲区)数据一起构成了TCP报文段,如图1-5中的虚线框所示。图 1-5 TCP报文段封装过程
当发送端应用程序使用send(或者write)函数向一个TCP连接写入数据时,内核中的TCP模块首先把这
些数据复制到与该连接对应的TCP内核发送缓冲区中,然后TCP模块调用IP模块提供的服务,传递的参数包
括TCP头部信息和TCP发送缓冲区中的数据,即TCP报文段。关于TCP报文段头部的细节,我们将在第3章
讨论。
经过UDP封装后的数据称为UDP数据报(UDP datagram)。UDP对应用程序数据的封装与TCP类似。不
同的是,UDP无须为应用层数据保存副本,因为它提供的服务是不可靠的。当一个UDP数据报被成功发送
之后,UDP内核缓冲区中的该数据报就被丢弃了。如果应用程序检测到该数据报未能被接收端正确接收,并打算重发这个数据报,则应用程序需要重新从用户空间将该数据报拷贝到UDP内核发送缓冲区中。
经过IP封装后的数据称为IP数据报(IP datagram)。IP数据报也包括头部信息和数据部分,其中数据部
分就是一个TCP报文段、UDP数据报或者ICMP报文。我们将在第2章详细讨论IP数据报的头部信息。
经过数据链路层封装的数据称为帧(frame)。传输媒介不同,帧的类型也不同。比如,以太网上传输
的是以太网帧(ethernet frame),而令牌环网络上传输的则是令牌环帧(token ring frame)。以以太网帧为
例,其封装格式如图1-6所示。
图 1-6 以太网帧封装
以太网帧使用6字节的目的物理地址和6字节的源物理地址来表示通信的双方。关于类型(type)字段,我们将在后面讨论。4字节CRC字段对帧的其他部分提供循环冗余校验。帧的最大传输单元(Max Transmit Unit,MTU),即帧最多能携带多少上层协议数据(比如IP数据
报),通常受到网络类型的限制。图1-6所示的以太网帧的MTU是1500字节。正因为如此,过长的IP数据报
可能需要被分片(fragment)传输。
帧才是最终在物理网络上传送的字节序列。至此,封装过程完成。1.3 分用
当帧到达目的主机时,将沿着协议栈自底向上依次传递。各层协议依次处理帧中本层负责的头部数
据,以获取所需的信息,并最终将处理后的帧交给目标应用程序。这个过程称为分用(demultiplexing)。
分用是依靠头部信息中的类型字段实现的。标准文档RFC 1700定义了所有标识上层协议的类型字段以及每
个上层协议对应的数值。图1-7显示了以太网帧的分用过程。
图 1-7 以太网帧的分用过程
因为IP协议、ARP协议和RARP协议都使用帧传输数据,所以帧的头部需要提供某个字段(具体情况取
决于帧的类型)来区分它们。以以太网帧为例,它使用2字节的类型字段来标识上层协议(见图1-6)。如
果主机接收到的以太网帧类型字段的值为0x800,则帧的数据部分为IP数据报(见图1-4),以太网驱动程序
就将帧交付给IP模块;若类型字段的值为0x806,则帧的数据部分为ARP请求或应答报文,以太网驱动程序
就将帧交付给ARP模块;若类型字段的值为0x835,则帧的数据部分为RARP请求或应答报文,以太网驱动
程序就将帧交付给RARP模块。
同样,因为ICMP协议、TCP协议和UDP协议都使用IP协议,所以IP数据报的头部采用16位的协议
(protocol)字段来区分它们。
TCP报文段和UDP数据报则通过其头部中的16位的端口号(port number)字段来区分上层应用程序。比
如DNS协议对应的端口号是53,HTTP协议(Hyper-Text Transfer Protocol,超文本传送协议)对应的端口号
是80。所有知名应用层协议使用的端口号都可在etcservices文件中找到。
帧通过上述分用步骤后,最终将封装前的原始数据送至目标服务(图1-7中的ARP服务、RARP服务、ICMP服务或者应用程序)。这样,在顶层目标服务看来,封装和分用似乎没有发生过。1.4 测试网络
为了深入理解网络通信和网络编程,我们准备了图1-8所示的测试网络,其中包括两台主机A和B,以及
一个连接到因特网的路由器。后文如没有特别声明,所有测试硬件指的都是该网络。我们将使用机器名来
标识测试机器。
图 1-8 测试网络
该测试网络主要用于分析ARP协议、IP协议、ICMP协议、TCP协议和DNS协议。我们通过抓取该网络
上的以太网帧,查看其中的以太网帧头部、IP数据报头部、TCP报文段头部信息,以获取网络通信的细节。
这样,以理论结合实践,我们就清楚TCPIP通信具体是如何进行的了。作者编写的多个客户端、服务器程
序都是使用该网络来调试和测试的。
对于路由器,我们仅列出了其LAN网络IP地址(192.168.1.1),而忽略了ISP(Internet Service
Provider,因特网服务提供商)给它分配的WAN网络IP地址,因为全书的讨论都不涉及它。1.5 ARP协议工作原理
ARP协议能实现任意网络层地址到任意物理地址的转换,不过本书仅讨论从IP地址到以太网地址
(MAC地址)的转换。其工作原理是:主机向自己所在的网络广播一个ARP请求,该请求包含目标机器的
网络地址。此网络上的其他机器都将收到这个请求,但只有被请求的目标机器会回应一个ARP应答,其中
包含自己的物理地址。
1.5.1 以太网ARP请求应答报文详解
以太网ARP请求应答报文的格式如图1-9所示。
图 1-9 以太网ARP请求应答报文
图1-9所示以太网ARP请求应答报文各字段具体介绍如下。
·硬件类型字段定义物理地址的类型,它的值为1表示MAC地址。
·协议类型字段表示要映射的协议地址类型,它的值为0x800,表示IP地址。
·硬件地址长度字段和协议地址长度字段,顾名思义,其单位是字节。对MAC地址来说,其长度为6;
对IP(v4)地址来说,其长度为4。
·操作字段指出4种操作类型:ARP请求(值为1)、ARP应答(值为2)、RARP请求(值为3)和
RARP应答(值为4)。
·最后4个字段指定通信双方的以太网地址和IP地址。发送端填充除目的端以太网地址外的其他3个字
段,以构建ARP请求并发送之。接收端发现该请求的目的端IP地址是自己,就把自己的以太网地址填进
去,然后交换两个目的端地址和两个发送端地址,以构建ARP应答并返回之(当然,如前所述,操作字段
需要设置为2)。
由图1-9可知,ARP请求应答报文的长度为28字节。如果再加上以太网帧头部和尾部的18字节(见图1-
6),则一个携带ARP请求应答报文的以太网帧长度为46字节。不过有的实现要求以太网帧数据部分长度至
少为46字节(见图1-4),此时ARP请求应答报文将增加一些填充字节,以满足这个要求。在这种情况下,一个携带ARP请求应答报文的以太网帧长度为64字节。
1.5.2 ARP高速缓存的查看和修改
通常,ARP维护一个高速缓存,其中包含经常访问(比如网关地址)或最近访问的机器的IP地址到物
理地址的映射。这样就避免了重复的ARP请求,提高了发送数据包的速度。
Linux下可以使用arp命令来查看和修改ARP高速缓存。比如,ernest-laptop在某一时刻(注意,ARP高速
缓存是动态变化的)的ARP缓存内容如下(使用arp-a命令):
Kongming20(192.168.1.109)at 08:00:27:53:10:67[ether]on eth0
·(192.168.1.1)at 14:e6:e4:93:5b:78[ether]on eth0
其中,第一项描述的是另一台测试机器Kongming20(注意,其IP地址、MAC地址都与图1-8描述的一致),第二项描述的是路由器。下面两条命令则分别删除和添加一个ARP缓存项:
sudo arp-d 192.168.1.109删除Kongming20对应的ARP缓存项
sudo arp-s 192.168.1.109 08:00:27:53:10:67添加Kongming20对应的ARP缓存项
1.5.3 使用tcpdump观察ARP通信过程
为了清楚地了解ARP的运作过程,我们从ernest-laptop上执行telnet命令登录Kongming20的echo服务(已
经开启),并用tcpdump(详见第17章)抓取这个过程中两台测试机器之间交换的以太网帧。具体的操作过
程如下:
sudo arp-d 192.168.1.109清除ARP缓存中Kongming20对应的项
sudo tcpdump-i eth0-ent'(dst 192.168.1.109 and src 192.168.1.108)or
(dst 192.168.1.108 and src 192.168.1.109)'如无特殊声明,抓包都在机器ernest-laptop上执行
telnet 192.168.1.109 echo开启另一个终端执行telnet命令
Trying 192.168.1.109...
Connected to 192.168.1.109.
Escape character is'^]'.
^](回车)输入Ctrl+]并回车
telnet>quit(回车)
Connection closed.
在执行telnet命令之前,应先清除ARP缓存中与Kongming20对应的项,否则ARP通信不被执行,我们也
就无法抓取到期望的以太网帧。当执行telnet命令并在两台通信主机之间建立TCP连接后(telnet输
出“Connected to 192.168.1.109”),输入Ctrl+]以调出telnet程序的命令提示符,然后在telnet命令提示符后输
入quit,退出telnet客户端程序(因为ARP通信在TCP连接建立之前就已经完成,故我们不关心后续内容)。
tcpdump抓取到的众多数据包中,只有最靠前的两个和ARP通信有关系,现在将它们列出(数据包前面的编
号是笔者加入的,后同):
1.00:16:d3:5c:b9:e3>ff:ff:ff:ff:ff:ff,ethertype ARP(0x0806),length 42:Request who-has 192.168.1.109 tell
192.168.1.108,length 28
2.08:00:27:53:10:67>00:16:d3:5c:b9:e3,ethertype ARP(0x0806),length 60:Reply 192.168.1.109 is-at 08:00:27:53:10:67,length 46
由tcpdump抓取的数据包本质上是以太网帧,我们通过该命令的众多选项来控制帧的过滤(比如用dst和
src指定通信的目的端IP地址和源端IP地址)和显示(比如用-e选项开启以太网帧头部信息的显示)。
第一个数据包中,ARP通信的源端的物理地址是00:16:d3:5c:b9:e3(ernest-laptop),目的端的物理地址
是ff:ff:ff:ff:ff:ff,这是以太网的广播地址,用以表示整个LAN。该LAN上的所有机器都会收到并处理这样的
帧。数值0x806是以太网帧头部的类型字段的值,它表示分用的目标是ARP模块。该以太网帧的长度为42字
节(实际上是46字节,tcpdump未统计以太网帧尾部4字节的CRC字段),其中数据部分长度为28字
节。“Request”表示这是一个ARP请求,“who-has 192.168.1.109 tell 192.168.1.108”则表示是ernest-laptop要查
询Kongming20的IP地址。
第二个数据包中,ARP通信的源端的物理地址是08:00:27:53:10:67(Kongming20),目的端的物理地址
是00:16:d3:5c:b9:e3(ernest-laptop)。“Reply”表示这是一个ARP应答,“192.168.1.109 is-at
08:00:27:53:10:67”则表示目标机器Kongming20报告其物理地址。该以太网帧的长度为60字节(实际上是64
字节),可见它使用了填充字节来满足最小帧长度。
为了便于理解,我们将上述讨论用图1-10来详细说明。图 1-10 ARP通信过程
关于该图,需要说明三点:
第一,我们将两次传输的以太网帧按照图1-6所描述的以太网帧封装格式绘制在图的下半部分。
第二,ARP请求和应答是从以太网驱动程序发出的,而并非像图中描述的那样从ARP模块直接发送到
以太网上,所以我们将它们用虚线表示,这主要是为了体现携带ARP数据的以太网帧和其他以太网帧(比
如携带IP数据报的以太网帧)的区别。
第三,路由器也将接收到以太网帧1,因为该帧是一个广播帧。不过很显然,路由器并没有回应其中的
ARP请求,正如前文讨论的那样。1.6 DNS工作原理
我们通常使用机器的域名来访问这台机器,而不直接使用其IP地址,比如访问因特网上的各种网站。
那么如何将机器的域名转换成IP地址呢?这就需要使用域名查询服务。域名查询服务有很多种实现方式,比如NIS(Network Information Service,网络信息服务)、DNS和本地静态文件等。本节主要讨论DNS。
1.6.1 DNS查询和应答报文详解
DNS是一套分布式的域名服务系统。每个DNS服务器上都存放着大量的机器名和IP地址的映射,并且
是动态更新的。众多网络客户端程序都使用DNS协议来向DNS服务器查询目标主机的IP地址。DNS查询和
应答报文的格式如图1-11所示。
图 1-11 DNS查询和应答报文
16位标识[1]
字段用于标记一对DNS查询和应答,以此区分一个DNS应答是哪个DNS查询的回应。
16位标志字段用于协商具体的通信方式和反馈通信状态。DNS报文头部的16位标志字段的细节如图1-
12所示。图 1-12 DNS报文头部的标志字段
图1-12中各标志的含义分别是:
·QR,查询应答标志。0表示这是一个查询报文,1表示这是一个应答报文。
·opcode,定义查询和应答的类型。0表示标准查询,1表示反向查询(由IP地址获得主机域名),2表
示请求服务器状态。
·AA,授权应答标志,仅由应答报文使用。1表示域名服务器是授权服务器。
·TC,截断标志,仅当DNS报文使用UDP服务时使用。因为UDP数据报有长度限制,所以过长的DNS
报文将被截断。1表示DNS报文超过512字节,并被截断。
·RD,递归查询标志。1表示执行递归查询,即如果目标DNS服务器无法解析某个主机名,则它将向
其他DNS服务器继续查询,如此递归,直到获得结果并把该结果返回给客户端。0表示执行迭代查询,即如
果目标DNS服务器无法解析某个主机名,则它将自己知道的其他DNS服务器的IP地址返回给客户端,以供
客户端参考。
·RA,允许递归标志。仅由应答报文使用,1表示DNS服务器支持递归查询。
·zero,这3位未用,必须都设置为0。
·rcode,4位返回码,表示应答的状态。常用值有0(无错误)和3(域名不存在)。
接下来的4个字段则分别指出DNS报文的最后4个字段的资源记录数目。对查询报文而言,它一般包含1
个查询问题,而应答资源记录数、授权资源记录数和额外资源记录数则为0。应答报文的应答资源记录数则
至少为1,而授权资源记录数和额外资源记录数可为0或非0。
查询问题的格式如图1-13所示。
图 1-13 DNS查询问题的格式
图1-13中,查询名以一定的格式封装了要查询的主机域名。16位查询类型表示如何执行查询操作,常
见的类型有如下几种:?类型A,值是1,表示获取目标主机的IP地址。
·类型CNAME,值是5,表示获得目标主机的别名。
·类型PTR,值是12,表示反向查询。
16位查询类通常为1,表示获取因特网地址(IP地址)。
应答字段、授权字段和额外信息字段都使用资源记录(Resource Record,RR)格式。资源记录格式如
图1-14所示。
图 1-14 资源记录格式
图1-14中,32位域名是该记录中与资源对应的名字,其格式和查询问题中的查询名字段相同。16位类
型和16位类字段的含义也与DNS查询问题的对应字段相同。
32位生存时间表示该查询记录结果可被本地客户端程序缓存多长时间,单位是秒。
16位资源数据长度字段和资源数据字段的内容取决于类型字段。对类型A而言,资源数据是32位的IPv4
地址,而资源数据长度则为4(以字节为单位)。
至此,我们简要地介绍了DNS协议。我们将在后面给出一个DNS通信的具体例子。DNS协议的更多细
节请参考其RFC文档(DNS协议存在诸多RFC文档,每个RFC文档介绍其一个侧面,比如RFC 1035介绍的
是域名的实现和规范,RFC 1886则描述DNS协议对IPv6的扩展支持)。
1.6.2 Linux下访问DNS服务
我们要访问DNS服务,就必须先知道DNS服务器的IP地址。Linux使用etcresolv.conf文件来存放DNS服
务器的IP地址。机器ernest-laptop上,该文件的内容如下:
Generated by Network Manager
nameserver 219.239.26.42
nameserver 124.207.160.106其中的两个IP地址分别是首选DNS服务器地址和备选DNS服务器地址。文件中的注释语句“Generated by
Network Manager”告诉我们,这两个DNS服务器地址是由网络管理程序写入的。
Linux下一个常用的访问DNS服务器的客户端程序是host,比如下面的命令是向首选DNS服务器
219.239.26.42查询机器www.baidu.com的IP地址:
host-t A www.baidu.com
www.baidu.com is an alias for www.a.shifen.com.
www.a.shifen.com has address 119.75.217.56
www.a.shifen.com has address 119.75.218.77
host命令的输出告诉我们,机器名www.baidu.com是www.a.shifen.com.的别名,并且该机器名对应两个
IP地址。host命令使用DNS协议和DNS服务器通信,其-t选项告诉DNS协议使用哪种查询类型。我们这里使
用的是A类型,即通过机器的域名获得其IP地址(但实际上返回的资源记录中还包含机器的别名)。关于
host命令的详细使用方法,请参考其man手册。
1.6.3 使用tcpdump观察DNS通信过程
为了看清楚DNS通信的过程,下面我们将从ernest-laptop上运行host命令以查询主机www.baidu.com对应
的IP地址,并使用tcpdump抓取这一过程中LAN上传输的以太网帧。具体的操作过程如下:
sudo tcpdump-i eth0-nt-s 500 port domain
host-t A www.baidu.com
这一次执行tcpdump抓包时,我们使用“port domain”来过滤数据包,表示只抓取使用domain(域名)服
务的数据包,即DNS查询和应答报文。tcpdump的输出如下:
1.IP 192.168.1.108.34319>219.239.26.42.53:57428+A?www.baidu.com.(31)
2.IP 219.239.26.42.53>192.168.1.108.34319:57428 344 CNAME www.a.shifen.com.,A 119.75.218.77,A 119.75.217.56(226)
这两个数据包开始的“IP”指出,它们后面的内容描述的是IP数据报。tcpdump以“IP地址.端口号”的形式
来描述通信的某一端;以“>”表示数据传输的方向,“>”前面是源端,后面是目的端。可见,第一个数据包
是测试机器ernest-laptop(IP地址是192.168.1.108)向其首选DNS服务器(IP地址是219.239.26.42)发送的
DNS查询报文(目标端口53是DNS服务使用的端口,这一点我们在前面介绍过),第二个数据包是服务器
反馈的DNS应答报文。
第一个数据包中,数值57428是DNS查询报文的标识值,因此该值也出现在DNS应答报文中。“+”表示
启用递归查询标志。“A?”表示使用A类型的查询方式。“www.baidu.com”则是DNS查询问题中的查询名。括
号中的数值31是DNS查询报文的长度(以字节为单位)。
第二个数据包中,“344”表示该报文中包含3个应答资源记录、4个授权资源记录和4个额外信息记
录。“CNAME www.a.shifen.com.,A 119.75.218.77,A 119.75.217.56”则表示3个应答资源记录的内容。其中
CNAME表示紧随其后的记录是机器的别名,A表示紧随其后的记录是IP地址。该应答报文的长度为226字
节。
注意 我们抓包的时候没有开启tcpdump的-X选项(或者-x选项)。如果使用-X选项,我们将能看到
DNS报文的每一个字节,也就能明白上面31字节的查询报文和226字节的应答报文的具体含义。限于篇幅,这里不再讨论,读者不妨自己分析。
[1]“标识”和“标志”在《现代汉语词典(第5版)》中表示同一含义,但是在本书中(计算机业界也是如
此),它们为两个概念,代表不同的含义,读者在阅读时应严格区分。1.7 socket和TCPIP协议族的关系
前文提到,数据链路层、网络层、传输层协议是在内核中实现的。因此操作系统需要实现一组系统调
用,使得应用程序能够访问这些协议提供的服务。实现这组系统调用的API(Application Programming
Interface,应用程序编程接口)主要有两套:socket和XTI。XTI现在基本不再使用,本书仅讨论socket。图
1-1显示了socket与TCPIP协议族的关系。
由socket定义的这一组API提供如下两点功能:一是将应用程序数据从用户缓冲区中复制到TCPUDP内
核发送缓冲区,以交付内核来发送数据(比如图1-5所示的send函数),或者是从内核TCPUDP接收缓冲区
中复制数据到用户缓冲区,以读取数据;二是应用程序可以通过它们来修改内核中各层协议的某些头部信
息或其他数据结构,从而精细地控制底层通信的行为。比如可以通过setsockopt函数来设置IP数据报在网络
上的存活时间。我们将在第5章详细讨论这一组API。
值得一提的是,socket是一套通用网络编程接口,它不但可以访问内核中TCPIP协议栈,而且可以访问
其他网络协议栈(比如X.25协议栈、UNIX本地域协议栈等)。第2章 IP协议详解
IP协议是TCPIP协议族的核心协议,也是socket网络编程的基础之一。本章从两个方面较为深入地探讨
IP协议:
·IP头部信息。IP头部信息出现在每个IP数据报中,用于指定IP通信的源端IP地址、目的端IP地址,指
导IP分片和重组,以及指定部分通信行为。
·IP数据报的路由和转发。IP数据报的路由和转发发生在除目标机器之外的所有主机和路由器上。它们
决定数据报是否应该转发以及如何转发。
由于32位表示的IP地址即将全部使用完,因此人们开发出了新版本的IP协议,称为IPv6协议,而原来的
版本则称为IPv4协议。本章前面部分的讨论都是基于IPv4协议的,只在最后一节简要讨论IPv6协议。
在开始讨论前,我们先简单介绍一下IP服务。
2.1 IP服务的特点
IP协议是TCPIP协议族的动力,它为上层协议提供无状态、无连接、不可靠的服务。
无状态(stateless)是指IP通信双方不同步传输数据的状态信息,因此所有IP数据报的发送、传输和接
收都是相互独立、没有上下文关系的。这种服务最大的缺点是无法处理乱序和重复的IP数据报。比如发送
端发送出的第N个IP数据报可能比第N+1个IP数据报后到达接收端,而同一个IP数据报也可能经过不同的路
径多次到达接收端。在这两种情况下,接收端的IP模块无法检测到乱序和重复,因为这些IP数据报之间没有
任何上下文关系。接收端的IP模块只要收到了完整的IP数据报(如果是IP分片的话,IP模块将先执行重
组),就将其数据部分(TCP报文段、UDP数据报或者ICMP报文)上交给上层协议。那么从上层协议来
看,这些数据就可能是乱序的、重复的。面向连接的协议,比如TCP协议,则能够自己处理乱序的、重复
的报文段,它递交给上层协议的内容绝对是有序的、正确的。
虽然IP数据报头部提供了一个标识字段(见后文)用以唯一标识一个IP数据报,但它是被用来处理IP分
片和重组的,而不是用来指示接收顺序的。
无状态服务的优点也很明显:简单、高效。我们无须为保持通信的状态而分配一些内核资源,也无须
每次传输数据时都携带状态信息。在网络协议中,无状态是很常见的,比如UDP协议和HTTP协议都是无状
态协议。以HTTP协议为例,一个浏览器的连续两次网页请求之间没有任何关联,它们将被Web服务器独立
地处理。
无连接(connectionless)是指IP通信双方都不长久地维持对方的任何信息。这样,上层协议每次发送数
据的时候,都必须明确指定对方的IP地址。
不可靠是指IP协议不能保证IP数据报准确地到达接收端,它只是承诺尽最大努力(best effort)。很多
种情况都能导致IP数据报发送失败。比如,某个中转路由器发现IP数据报在网络上存活的时间太长(根据IP
数据报头部字段TTL判断,见后文),那么它将丢弃之,并返回一个ICMP错误消息(超时错误)给发送
端。又比如,接收端发现收到的IP数据报不正确(通过校验机制),它也将丢弃之,并返回一个ICMP错误
消息(IP头部参数错误)给发送端。无论哪种情况,发送端的IP模块一旦检测到IP数据报发送失败,就通知
上层协议发送失败,而不会试图重传。因此,使用IP服务的上层协议(比如TCP协议)需要自己实现数据确
认、超时重传等机制以达到可靠传输的目的。2.2 IPv4头部结构
2.2.1 IPv4头部结构
IPv4的头部结构如图2-1所示。其长度通常为20字节,除非含有可变长的选项部分。
图 2-1 IPv4头部结构
4位版本号(version)指定IP协议的版本。对IPv4来说,其值是4。其他IPv4协议的扩展版本(如SIP协
议和PIP协议),则具有不同的版本号(它们的头部结构也和图2-1不同)。
4位头部长度(header length)标识该IP头部有多少个32 bit字(4字节)。因为4位最大能表示15,所以
IP头部最长是60字节。
8位服务类型(Type Of Service,TOS)包括一个3位的优先权字段(现在已经被忽略),4位的TOS字
段和1位保留字段(必须置0)。4位的TOS字段分别表示:最小延时,最大吞吐量,最高可靠性和最小费
用。其中最多有一个能置为1,应用程序应该根据实际需要来设置它。比如像ssh和telnet这样的登录程序需
要的是最小延时的服务,而文件传输程序ftp则需要最大吞吐量的服务。
16位总长度(total length)是指整个IP数据报的长度,以字节为单位,因此IP数据报的最大长度为65
535(216-1)字节。但由于MTU的限制,长度超过MTU的数据报都将被分片传输,所以实际传输的IP数据
报(或分片)的长度都远远没有达到最大值。接下来的3个字段则描述了如何实现分片。
16位标识(identification)唯一地标识主机发送的每一个数据报。其初始值由系统随机生成;每发送一
个数据报,其值就加1。该值在数据报分片时被复制到每个分片中,因此同一个数据报的所有分片都具有相
同的标识值。3位标志字段的第一位保留。第二位(Don’t Fragment,DF)表示“禁止分片”。如果设置了这个位,IP
模块将不对数据报进行分片。在这种情况下,如果IP数据报长度超过MTU的话,IP模块将丢弃该数据报并
返回一个ICMP差错报文。第三位(More Fragment,MF)表示“更多分片”。除了数据报的最后一个分片
外,其他分片都要把它置1。
13位分片偏移(fragmentation offset)是分片相对原始IP数据报开始处(仅指数据部分)的偏移。实际
的偏移值是该值左移3位(乘8)后得到的。由于这个原因,除了最后一个IP分片外,每个IP分片的数据部分
的长度必须是8的整数倍(这样才能保证后面的IP分片拥有一个合适的偏移值)。
8位生存时间(Time To Live,TTL)是数据报到达目的地之前允许经过的路由器跳数。TTL值被发送端
设置(常见的值是64)。数据报在转发过程中每经过一个路由,该值就被路由器减1。当TTL值减为0时,路由器将丢弃数据报,并向源端发送一个ICMP差错报文。TTL值可以防止数据报陷入路由循环。
8位协议(protocol)用来区分上层协议,我们在第1章讨论过。etcprotocols文件定义了所有上层协议对
应的protocol字段的数值。其中,ICMP是1,TCP是6,UDP是17。etcprotocols文件是RFC 1700的一个子
集。
16位头部校验和(header checksum)由发送端填充,接收端对其使用CRC算法以检验IP数据报头部(注
意,仅检验头部)在传输过程中是否损坏。
32位的源端IP地址和目的端IP地址用来标识数据报的发送端和接收端。一般情况下,这两个地址在整个
数据报的传递过程中保持不变,而不论它中间经过多少个中转路由器。关于这一点,我们将在第4章进一步
讨论。
IPv4最后一个选项字段(option)是可变长的可选信息。这部分最多包含40字节,因为IP头部最长是60
字节(其中还包含前面讨论的20字节的固定部分)。可用的IP选项包括:
·记录路由(record route),告诉数据报途经的所有路由器都将自己的IP地址填入IP头部的选项部分,这样我们就可以跟踪数据报的传递路径。
·时间戳(timestamp),告诉每个路由器都将数据报被转发的时间(或时间与IP地址对)填入IP头部
的选项部分,这样就可以测量途经路由之间数据报传输的时间。
·松散源路由选择(loose source routing),指定一个路由器IP地址列表,数据报发送过程中必须经过
其中所有的路由器。
·严格源路由选择(strict source routing),和松散源路由选择类似,不过数据报只能经过被指定的路
由器。
关于IP头部选项字段更详细的信息,请参考IP协议的标准文档RFC 791。不过这些选项字段很少被使
用,使用松散源路由选择和严格源路由选择选项的例子大概仅有traceroute程序。此外,作为记录路由IP选
项的替代品,traceroute程序使用UDP报文和ICMP报文实现了更可靠的记录路由功能,详情请参考文档RFC
1393。
2.2.2 使用tcpdump观察IPv4头部结构
为了深入理解IPv4头部中每个字段的含义,我们从测试机器ernest-laptop上执行telnet命令登录本机,并
用tcpdump抓取这个过程中telnet客户端程序和telnet服务器程序之间交换的数据包。具体的操作过程如下:
sudo tcpdump-ntx-i lo抓取本地回路上的数据包
telnet 127.0.0.1开启另一个终端执行telnet命令登录本机
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is'^]'.
Ubuntu 9.10
ernest-laptop login:ernest输入用户名并回车
Password:输入密码并回车此时观察tcpdump输出的第一个数据包,其内容如代码清单2-1所示。
代码清单2-1 用tcpdump抓取数据包
IP 127.0.0.1.41621>127.0.0.1.23:Flags[S],seq 3499745539,win 32792,options[mss 16396,sackOK,TS val 40781017 ecr 0,nop,wscale 6],length 0
0x0000:4510 003c a5da 4000 4006 96cf 7f00 0001
0x0010:7f00 0001 a295 0017 d099 e103 0000 0000
0x0020:a002 8018 fe30 0000 0204 400c 0402 080a
0x0030:026e 44d9 0000 0000 0103 0306
该数据包描述的是一个IP数据报。由于我们是使用telnet登录本机的,所以IP数据报的源端IP地址和目
的端IP地址都是“127.0.0.1”。telnet服务器程序使用的端口号是23(参见etcservices文件),而telnet客户端
程序使用临时端口号41621与服务器通信。关于临时端口号,我们将在第3章讨
论。“Flags”、“seq”、“win”和“options”描述的都是TCP头部信息,这也将在第3章讨论。“length”指出该IP数
据报所携带的应用程序数据的长度。
这次抓包我们开启了tcpdump的-x选项,使之输出数据包的二进制码。此数据包共包含60字节,其中前
20字节是IP头部,后40字节是TCP头部,不包含应用程序数据(length值为0)。现在我们分析IP头部的每个
字节,如表2-1所示。由表2-1可见,telnet服务选择使用具有最小延时的服务,并且默认使用的传输层协议是TCP协议(回顾
第1章讨论的分用)。这些都符合我们通常的理解。这个IP数据报没有被分片,因为它没有携带任何应用程
序数据。接下来我们将抓取并讨论被分片的IP数据报。
[1]此列中的空格表示我们并不关心相应字段的十进制值。2.3 IP分片
前文曾提到,当IP数据报的长度超过帧的MTU时,它将被分片传输。分片可能发生在发送端,也可能
发生在中转路由器上,而且可能在传输过程中被多次分片,但只有在最终的目标机器上,这些分片才会被
内核中的IP模块重新组装。
IP头部中的如下三个字段给IP的分片和重组提供了足够的信息:数据报标识、标志和片偏移。一个IP数
据报的每个分片都具有自己的IP头部,它们具有相同的标识值,但具有不同的片偏移。并且除了最后一个
分片外,其他分片都将设置MF标志。此外,每个分片的IP头部的总长度字段将被设置为该分片的长度。
以太网帧的MTU是1500字节(可以通过ifconfig命令或者netstat命令查看),因此它携带的IP数据报的
数据部分最多是1480字节(IP头部占用20字节)。考虑用IP数据报封装一个长度为1481字节的ICMP报文
(包括8字节的ICMP头部,所以其数据部分长度为1473字节),则该数据报在使用以太网帧传输时必须被
分片,如图2-2所示。
图 2-2 携带ICMP报文的IP数据报被分片
图2-2中,长度为1501字节的IP数据报被拆分成两个IP分片,第一个IP分片长度为1500字节,第二个IP
分片的长度为21字节。每个IP分片都包含自己的IP头部(20字节),且第一个IP分片的IP头部设置了MF标
志,而第二个IP分片的IP头部则没有设置该标志,因为它已经是最后一个分片了。原始IP数据报中的ICMP
头部内容被完整地复制到了第一个IP分片中。第二个IP分片不包含ICMP头部信息,因为IP模块重组该ICMP
报文的时候只需要一份ICMP头部信息,重复传送这个信息没有任何益处。1473字节的ICMP报文数据的前
1472字节被IP模块复制到第一个IP分片中,使其总长度为1500字节,从而满足MTU的要求;而多出的最后1
字节则被复制到第二个IP分片中。
需要指出的是,ICMP报文的头部长度取决于报文的类型,其变化范围很大。图2-2以8字节为例,因为
后面的例子用到了ping程序,而ping程序使用的ICMP回显和应答报文的头部长度是8字节。
为了看清楚IP分片的具体过程,考虑从ernest-laptop来ping机器Kongming20,每次传送1473字节的数据
(这是ICMP报文的数据部分)以强制引起IP分片,并用tcpdump抓取这一过程中双方交换的数据包。具体
操作过程如下:sudo tcpdump-ntv-i eth0 icmp只抓取ICMP报文
ping Kongming20-s 1473用-s选项指定每次发送1473字节的数据
下面我们考察tcpdump输出的一个IP数据报的两个分片,其内容如下:
1.IP(tos 0x0,ttl 64,id 61197,offset 0,flags[+],proto ICMP(1),length 1500)192.168.1.108>192.168.1.110:ICMP echo request,id
41737,seq 1,length 1480
2.IP(tos 0x0,ttl 64,id 61197,offset 1480,flags[none],proto ICMP(1),length 21)192.168.1.108>192.168.1.110:icmp
这两个IP分片的标识值都是61197,说明它们是同一个IP数据报的分片。第一个分片的片偏移值为0,而
第二个则是1480。很显然,第二个分片的片偏移值实际上也是第一个分片的ICMP报文的长度。第一个分片
设置了MF标志以表示还有后续分片,所以tcpdump输出“flags[+]”。而第二个分片则没有设置任何标志,所
以tcpdump输出“flags[none]”。这个两个分片的长度分别为1500字节和21字节,这与图2-2描述的一致。
最后,IP层传递给数据链路层的数据可能是一个完整的IP数据报,也可能是一个IP分片,它们统称为IP
分组(packet)。本书如无特殊声明,不区分IP数据报和IP分组。2.4 IP路由
IP协议的一个核心任务是数据报的路由,即决定发送数据报到目标机器的路径。为了理解IP路由过程,我们先简要分析IP模块的基本工作流程。
2.4.1 IP模块工作流程
IP模块基本工作流程如图2-3所示。
图 2-3 IP模块基本工作流程
我们从右往左来分析图2-3。当IP模块接收到来自数据链路层的IP数据报时,它首先对该数据报的头部
做CRC校验,确认无误之后就分析其头部的具体信息。
如果该IP数据报的头部设置了源站选路选项(松散源路由选择或严格源路由选择),则IP模块调用数据
报转发子模块来处理该数据报。如果该IP数据报的头部中目标IP地址是本机的某个IP地址,或者是广播地
址,即该数据报是发送给本机的,则IP模块就根据数据报头部中的协议字段来决定将它派发给哪个上层应
用(分用)。如果IP模块发现这个数据报不是发送给本机的,则也调用数据报转发子模块来处理该数据
报。数据报转发子模块将首先检测系统是否允许转发,如果不允许,IP模块就将数据报丢弃。如果允许,数据报转发子模块将对该数据报执行一些操作,然后将它交给IP数据报输出子模块。我们将在后面讨论数
据报转发的具体过程。
IP数据报应该发送至哪个下一跳路由(或者目标机器),以及经过哪个网卡来发送,就是IP路由过程,即图2-3中“计算下一跳路由”子模块。IP模块实现数据报路由的核心数据结构是路由表。这个表按照数据报
的目标IP地址分类,同一类型的IP数据报将被发往相同的下一跳路由器(或者目标机器)。我们将在后面讨
论IP路由过程。
IP输出队列中存放的是所有等待发送的IP数据报,其中除了需要转发的IP数据报外,还包括封装了本机
上层数据(ICMP报文、TCP报文段和UDP数据报)的IP数据报。
图2-3中的虚线箭头显示了路由表更新的过程。这一过程是指通过路由协议或者route命令调整路由表,使之更适应最新的网络拓扑结构,称为IP路由策略。我们将在后面简单讨论它。
2.4.2 路由机制
要研究IP路由机制,需要先了解路由表的内容。我们可以使用route命令或netstat命令查看路由表。在测
试机器ernest-laptop上执行route命令,输出内容如代码清单2-2所示。
代码清单2-2 路由表实例
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
default 192.168.1.1 0.0.0.0 UG 0 0 0 eth0
192.168.1.0255.255.255.0 U 1 0 0 eth0
该路由表包含两项,每项都包含8个字段,如表2-2所示。代码清单2-2所示的路由表中,第一项的目标地址是default,即所谓的默认路由项。该项包含一个“G”标
志,说明路由的下一跳目标是网关,其地址是192.168.1.1(这是测试网络中路由器的本地IP地址)。另外一
个路由项的目标地址是192.168.1.0,它指的是本地局域网。该路由项的网关地址为,说明数据报不需要路
由中转,可以直接发送到目标机器。
那么路由表是如何按照IP地址分类的呢?或者说给定数据报的目标IP地址,它将匹配路由表中的哪一项
呢?这就是IP的路由机制,分为3个步骤:
1)查找路由表中和数据报的目标IP地址完全匹配的主机IP地址。如果找到,就使用该路由项,没找到
则转步骤2。
2)查找路由表中和数据报的目标IP地址具有相同网路ID的网络IP地址(比如代码清单2-2所示的路由表
中的第二项)。如果找到,就使用该路由项;没找到则转步骤3。
3)选择默认路由项,这通常意味着数据报的下一跳路由是网关。
因此,对于测试机器ernest-laptop而言,所有发送到IP地址为192.168.1.的机器的IP数据报都可以直接发送到目标机器(匹配路由表第二项),而所有访问因特网的请求都将通过网关来转发(匹配默认路由
项)。
2.4.3 路由表更新
路由表必须能够更新,以反映网络连接的变化,这样IP模块才能准确、高效地转发数据报。route命令
可以修改路由表。我们看如下几个例子(在机器ernest-laptop上执行):
sudo route add-host 192.168.1.109 dev eth0
sudo route del-net 192.168.1.0 netmask 255.255.255.0
sudo route del default
sudo route add default gw 192.168.1.109 dev eth0
第1行表示添加主机192.168.1.109(机器Kongming20)对应的路由项。这样设置之后,所有从ernest-
laptop发送到Kongming20的IP数据报将通过网卡eth0直接发送至目标机器的接收网卡。第2行表示删除网络
192.168.1.0对应的路由项。这样,除了机器Kongming20外,测试机器ernest-laptop将无法访问该局域网上的
任何其他机器(能访问到Kongming20是由于执行了上一条命令)。第3行表示删除默认路由项,这样做的后
果是无法访问因特网。第4行表示重新设置默认路由项,不过这次其网关是机器Kongming20(而不是能直接
访问因特网的路由器)!经过上述修改后的路由表如下:
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
Kongming20255.255.255.255 UH 0 0 0 eth0
default Kongming20 0.0.0.0 UG 0 0 0 eth0
这个新的路由表中,第一个路由项是主机路由项,所以它被设置了“H”标志。我们设计这样一个路由表
的目的是为后文讨论ICMP重定向提供环境。
通过route命令或其他工具手工修改路由表,是静态的路由更新方式。对于大型的路由器,它们通常通
过BGP(Border Gateway Protocol,边际网关协议)、RIP(Routing Information Protocol,路由信息协议)、OSPF等协议来发现路径,并更新自己的路由表。这种更新方式是动态的、自动的。这部分内容超出了本书
的讨论范围,感兴趣的读者可阅读参考资料1。2.5 IP转发
前文提到,不是发送给本机的IP数据报将由数据报转发子模块来处理。路由器都能执行数据报的转发
操作,而主机一般只发送和接收数据报,这是因为主机上procsysnetipv4ip_forward内核参数默认被设置
为0。我们可以通过修改它来使能主机的数据报转发功能(在测试机器Kongming20上以root身份执行):
echo 1>procsysnetipv4ip_forward
对于允许IP数据报转发的系统(主机或路由器),数据报转发子模块将对期望转发的数据报执行如下
操作:
1)检查数据报头部的TTL值。如果TTL值已经是0,则丢弃该数据报。
2)查看数据报头部的严格源路由选择选项。如果该选项被设置,则检测数据报的目标IP地址是否是本
机的某个IP地址。如果不是,则发送一个ICMP源站选路失败报文给发送端。
3)如果有必要,则给源端发送一个ICMP重定向报文,以告诉它一个更合理的下一跳路由器。
4)将TTL值减1。
5)处理IP头部选项。
6)如果有必要,则执行IP分片操作。2.6 重定向
图2-3显示了ICMP重定向报文也能用于更新路由表,因此本节我们简要讨论ICMP重定向。
2.6.1 ICMP重定向报文
ICMP重定向报文格式如图2-4所示。
图 2-4 ICMP重定向报文格式
我们在1.1节讨论过ICMP报文头部的3个固定字段:8位类型、8位代码和16位校验和。ICMP重定向报文
的类型值是5,代码字段有4个可选值,用来区分不同的重定向类型。本书仅讨论主机重定向,其代码值为
1。
ICMP重定向报文的数据部分含义很明确,它给接收方提供了如下两个信息:
·引起重定向的IP数据报(即图2-4中的原始IP数据报)的源端IP地址。
·应该使用的路由器的IP地址。
接收主机根据这两个信息就可以断定引起重定向的IP数据报应该使用哪个路由器来转发,并且以此来
更新路由表(通常是更新路由表缓冲,而不是直接更改路由表)。
procsysnetipv4confallsend_redirects内核参数指定是否允许发送ICMP重定向报文,而procsysnetipv4confallaccept_redirects内核参数则指定是否允许接收ICMP重定向报文。一般来说,主机
只能接收ICMP重定向报文,而路由器只能发送ICMP重定向报文。
2.6.2 主机重定向实例
2.4.3节中,我们把机器ernest-laptop的网关设置成了机器Kongming20,2.5节中我们又使能了
Kongming20的数据报转发功能,因此机器ernest-laptop将通过Kongming20来访问因特网,比如在ernest-
laptop上执行如下ping命令:
ping www.baidu.com
PING www.a.shifen.com(119.75.217.56)56(84)bytes of data.
From Kongming20(192.168.1.109):icmp_seq=1 Redirect Host(New nexthop:192.168.1.1)
64 bytes from 119.75.217.56:icmp_seq=1 ttl=54 time=6.78 ms---www.a.shifen.com ping statistics---
1 packets transmitted,1 received,0%packet loss,time 0msrtt minavgmaxmdev=6.7896.7896.7890.000 ms
从ping命令的输出来看,Kongming20给ernest-laptop发送了一个ICMP重定向报文,告诉它请通过
192.168.1.1来访问目标机器,因为这对ernest-laptop来说是更合理的路由方式。当主机ernest-laptop收到这样
的ICMP重定向报文后,它将更新其路由表缓冲(使用命令route-Cn查看),并使用新的路由方式来发送后
续数据报。上面讨论的重定向过程可用图2-5来总结。
图 2-5 主机重定向过程2.7 IPv6头部结构
IPv6协议是网络层技术发展的必然趋势。它不仅解决了IPv4地址不够用的问题,还做了很大的改进。比
如,增加了多播和流的功能,为网络上多媒体内容的质量提供精细的控制;引入自动配置功能,使得局域
网管理更方便;增加了专门的网络安全功能等。本节简要地讨论IPv6头部结构,它的更多细节请参考其标
准文档RFC 2460。
2.7.1 IPv6固定头部结构
IPv6头部由40字节的固定头部和可变长的扩展头部组成。图2-6所示是IPv6的固定头部结构。
图 2-6 IPv6固定头部结构
4位版本号(version)指定IP协议的版本。对IPv6来说,其值是6。
8位通信类型(traffic class)指示数据流通信类型或优先级,和IPv4中的TOS类似。
20位流标签(flow label)是IPv6新增加的字段,用于某些对连接的服务质量有特殊要求的通信,比如
音频或视频等实时数据传输。
16位净荷长度(payload length)指的是IPv6扩展头部和应用程序数据长度之和,不包括固定头部长
度。
8位下一个包头(next header)指出紧跟IPv6固定头部后的包头类型,如扩展头(如果有的话)或某个
上层协议头(比如TCP,UDP或ICMP)。它类似于IPv4头部中的协议字段,且相同的取值有相同的含义。
8位跳数限制(hop limit)和IPv4中的TTL含义相同。IPv6用128位(16字节)来表示IP地址,使得IP地址的总量达到了2128个。所以有人说,“IPv6使得地球
上的每粒沙子都有一个IP地址”。
32位表示的IPv4地址一般用点分十进制来表示,而IPv6地址则用十六进制字符串表示,比
如“FE80:0000:0000:0000:1234:5678:0000:0012”。可见,IPv6地址用“:”分割成8组,每组包含2字节。但这种
表示方法过于麻烦,通常可以使用所谓的零压缩法来将其简写,也就是省略连续的、全零的组。比如,上
面的例子使用零压缩法可表示为“FE80::1234:5678:0000:0012”。不过零压缩法对一个IPv6地址只能使用一
次,比如上面的例子中,字节组“5678”后面的全零组就不能再省略,否则我们就无法计算每个“::”之间省略
了多少个全零组。
2.7.2 IPv6扩展头部
可变长的扩展头部使得IPv6能支持更多的选项,并且很便于将来的扩展需要。它的长度可以是0,表示
数据报没使用任何扩展头部。一个数据报可以包含多个扩展头部,每个扩展头部的类型由前一个头部(固
定头部或扩展头部)中的下一个报头字段指定。目前可以使用的扩展头部如表2-3所示。
注意 IPv6协议并不是IPv4协议的简单扩展,而是完全独立的协议。用以太网帧封装的IPv6数据报和
IPv4数据报具有不同的类型值。第1章提到,IPv4数据报的以太网帧封装类型值是0x800,而IPv6数据报的以
太网帧封装类型值是0x86dd(见RFC 2464)。第3章 TCP协议详解
TCP协议是TCPIP协议族中另一个重要的协议。和IP协议相比,TCP协议更靠近应用层,因此在应用程
序中具有更强的可操作性。一些重要的socket选项都和TCP协议相关。
本章从如下四方面来讨论TCP协议:
·TCP头部信息。TCP头部信息出现在每个TCP报文段中,用于指定通信的源端端口号、目的端端口
号,管理TCP连接,控制两个方向的数据流。
·TCP状态转移过程。TCP连接的任意一端都是一个状态机。在TCP连接从建立到断开的整个过程中,连接两端的状态机将经历不同的状态变迁。理解TCP状态转移对于调试网络应用程序将有很大的帮助。
·TCP数据流。通过分析TCP数据流,我们就可以从网络应用程序外部来了解应用层协议和通信双方交
换的应用程序数据。这一部分将讨论两种类型的TCP数据流:交互数据流和成块数据流。TCP数据流中有一
种特殊的数据,称为紧急数据,我们也将简单讨论之。
·TCP数据流的控制。为了保证可靠传输和提高网络通信质量,内核需要对TCP数据流进行控制。这一
部分讨论TCP数据流控制的两个方面:超时重传和拥塞控制。
不过在详细讨论TCP协议之前,我们先简单介绍一下TCP服务的特点,以及它和UDP服务的区别。
3.1 TCP服务的特点
传输层协议主要有两个:TCP协议和UDP协议。TCP协议相对于UDP协议的特点是:面向连接、字节流
和可靠传输。
使用TCP协议通信的双方必须先建立连接,然后才能开始数据的读写。双方都必须为该连接分配必要
的内核资源,以管理连接的状态和连接上数据的传输。TCP连接是全双工的,即双方的数据读写可以通过
一个连接进行。完成数据交换之后,通信双方都必须断开连接以释放系统资源。
TCP协议的这种连接是一对一的,所以基于广播和多播(目标是多个主机地址)的应用程序不能使用
TCP服务。而无连接协议UDP则非常适合于广播和多播。
我们在1.1节中简单介绍过字节流服务和数据报服务的区别。这种区别对应到实际编程中,则体现为通
信双方是否必须执行相同次数的读、写操作(当然,这只是表现形式)。当发送端应用程序连续执行多次
写操作时,TCP模块先将这些数据放入TCP发送缓冲区中。当TCP模块真正开始发送数据时,发送缓冲区中
这些等待发送的数据可能被封装成一个或多个TCP报文段发出。因此,TCP模块发送出的TCP报文段的个数
和应用程序执行的写操作次数之间没有固定的数量关系。
当接收端收到一个或多个TCP报文段后,TCP模块将它们携带的应用程序数据按照TCP报文段的序号
(见后文)依次放入TCP接收缓冲区中,并通知应用程序读取数据。接收端应用程序可以一次性将TCP接收
缓冲区中的数据全部读出,也可以分多次读取,这取决于用户指定的应用程序读缓冲区的大小。因此,应
用程序执行的读操作次数和TCP模块接收到的TCP报文段个数之间也没有固定的数量关系。
综上所述,发送端执行的写操作次数和接收端执行的读操作次数之间没有任何数量关系,这就是字节
流的概念:应用程序对数据的发送和接收是没有边界限制的。UDP则不然。发送端应用程序每执行一次写
操作,UDP模块就将其封装成一个UDP数据报并发送之。接收端必须及时针对每一个UDP数据报执行读操
作(通过recvfrom系统调用),否则就会丢包(这经常发生在较慢的服务器上)。并且,如果用户没有指定
足够的应用程序缓冲区来读取UDP数据,则UDP数据将被截断。
图3-1和图3-2显示了TCP字节流服务和UDP数据报服务的上述区别。两图中省略了传输层以下的通信细
节。图 3-1 TCP字节流服务
图 3-2 UDP数据报服务
TCP传输是可靠的。首先,TCP协议采用发送应答机制,即发送端发送的每个TCP报文段都必须得到接
收方的应答,才认为这个TCP报文段传输成功。其次,TCP协议采用超时重传机制,发送端在发送出一个
TCP报文段之后启动定时器,如果在定时时间内未收到应答,它将重发该报文段。最后,因为TCP报文段最
终是以IP数据报发送的,而IP数据报到达接收端可能乱序、重复,所以TCP协议还会对接收到的TCP报文段
重排、整理,再交付给应用层。
UDP协议则和IP协议一样,提供不可靠服务。它们都需要上层协议来处理数据确认和超时重传。3.2 TCP头部结构
TCP头部信息出现在每个TCP报文段中,用于指定通信的源端端口,目的端端口,管理TCP连接等,本
节详细介绍TCP的头部结构,包括固定头部结构和头部选项。
3.2.1 TCP固定头部结构
TCP头部结构如图3-3所示,其中的诸多字段为管理TCP连接和控制数据流提供了足够的信息。
图 3-3 TCP头部结构
16位端口号(port number):告知主机该报文段是来自哪里(源端口)以及传给哪个上层协议或应用
程序(目的端口)的。进行TCP通信时,客户端通常使用系统自动选择的临时端口号,而服务器则使用知
名服务端口号。1.3节中提到过,所有知名服务使用的端口号都定义在etcservices文件中。
32位序号(sequence number):一次TCP通信(从TCP连接建立到断开)过程中某一个传输方向上的字
节流的每个字节的编号。假设主机A和主机B进行TCP通信,A发送给B的第一个TCP报文段中,序号值被系
统初始化为某个随机值ISN(Initial Sequence Number,初始序号值)。那么在该传输方向上(从A到B),后续的TCP报文段中序号值将被系统设置成ISN加上该报文段所携带数据的第一个字节在整个字节流中的偏
移。例如,某个TCP报文段传送的数据是字节流中的第1025~2048字节,那么该报文段的序号值就是
ISN+1025。另外一个传输方向(从B到A)的TCP报文段的序号值也具有相同的含义。
32位确认号(acknowledgement number):用作对另一方发送来的TCP报文段的响应。其值是收到的
TCP报文段的序号值加1。假设主机A和主机B进行TCP通信,那么A发送出的TCP报文段不仅携带自己的序
号,而且包含对B发送来的TCP报文段的确认号。反之,B发送出的TCP报文段也同时携带自己的序号和对A
发送来的报文段的确认号。4位头部长度(header length):标识该TCP头部有多少个32bit字(4字节)。因为4位最大能表示15,所
以TCP头部最长是60字节。
6位标志位包含如下几项:
·URG标志,表示紧急指针(urgent pointer)是否有效。
·ACK标志,表示确认号是否有效。我们称携带ACK标志的TCP报文段为确认报文段。
·PSH标志,提示接收端应用程序应该立即从TCP接收缓冲区中读走数据,为接收后续数据腾出空间
(如果应用程序不将接收到的数据读走,它们就会一直停留在TCP接收缓冲区中)。
·RST标志,表示要求对方重新建立连接。我们称携带RST标志的TCP报文段为复位报文段。
·SYN标志,表示请求建立一个连接。我们称携带SYN标志的TCP报文段为同步报文段。
·FIN标志,表示通知对方本端要关闭连接了。我们称携带FIN标志的TCP报文段为结束报文段。
16位窗口大小(window size):是TCP流量控制的一个手段。这里说的窗口,指的是接收通告窗口
(Receiver Window,RWND)。它告诉对方本端的TCP接收缓冲区还能容纳多少字节的数据,这样对方就
可以控制发送数据的速度。
16位校验和(TCP checksum):由发送端填充,接收端对TCP报文段执行CRC算法以检验TCP报文段在
传输过程中是否损坏。注意,这个校验不仅包括TCP头部,也包括数据部分。这也是TCP可靠传输的一个重
要保障。
16位紧急指针(urgent pointer):是一个正的偏移量。它和序号字段的值相加表示最后一个紧急数据的
下一字节的序号。因此,确切地说,这个字段是紧急指针相对当前序号的偏移,不妨称之为紧急偏移。
TCP的紧急指针是发送端向接收端发送紧急数据的方法。我们将在后面讨论TCP紧急数据。
3.2.2 TCP头部选项
TCP头部的最后一个选项字段(options)是可变长的可选信息。这部分最多包含40字节,因为TCP头部
最长是60字节(其中还包含前面讨论的20字节的固定部分)。典型的TCP头部选项结构如图3-4所示。
图 3-4 TCP头部选项的一般结构
选项的第一个字段kind说明选项的类型。有的TCP选项没有后面两个字段,仅包含1字节的kind字段。
第二个字段length(如果有的话)指定该选项的总长度,该长度包括kind字段和length字段占据的2字节。第
三个字段info(如果有的话)是选项的具体信息。常见的TCP选项有7种,如图3-5所示。图 3-5 7种TCP选项
kind=0是选项表结束选项。
kind=1是空操作(nop)选项,没有特殊含义,一般用于将TCP选项的总长度填充为4字节的整数倍。
kind=2是最大报文段长度选项。TCP连接初始化时,通信双方使用该选项来协商最大报文段长度(Max
Segment Size,MSS)。TCP模块通常将MSS设置为(MTU-40)字节(减掉的这40字节包括20字节的TCP头
部和20字节的IP头部)。这样携带TCP报文段的IP数据报的长度就不会超过MTU(假设TCP头部和IP头部都
不包含选项字段,并且这也是一般情况),从而避免本机发生IP分片。对以太网而言,MSS值是
1460(1500-40)字节。
kind=3是窗口扩大因子选项。TCP连接初始化时,通信双方使用该选项来协商接收通告窗口的扩大因
子。在TCP的头部中,接收通告窗口大小是用16位表示的,故最大为65 535字节,但实际上TCP模块允许的
接收通告窗口大小远不止这个数(为了提高TCP通信的吞吐量)。窗口扩大因子解决了这个问题。假设TCP
头部中的接收通告窗口大小是N,窗口扩大因子(移位数)是M,那么TCP报文段的实际接收通告窗口大小
是N乘2M,或者说N左移M位。注意,M的取值范围是0~14。我们可以通过修
改procsysnetipv4tcp_window_scaling内核变量来启用或关闭窗口扩大因子选项。
和MSS选项一样,窗口扩大因子选项只能出现在同步报文段中,否则将被忽略。但同步报文段本身不
执行窗口扩大操作,即同步报文段头部的接收通告窗口大小就是该TCP报文段的实际接收通告窗口大小。
当连接建立好之后,每个数据传输方向的窗口扩大因子就固定不变了。关于窗口扩大因子选项的细节,可
参考标准文档RFC 1323。kind=4是选择性确认(Selective Acknowledgment,SACK)选项。TCP通信时,如果某个TCP报文段丢
失,则TCP模块会重传最后被确认的TCP报文段后续的所有报文段,这样原先已经正确传输的TCP报文段也
可能重复发送,从而降低了TCP性能。SACK技术正是为改善这种情况而产生的,它使TCP模块只重新发送
丢失的TCP报文段,不用发送所有未被确认的TCP报文段。选择性确认选项用在连接初始化时,表示是否支
持SACK技术。我们可以通过修改procsysnetipv4tcp_sack内核变量来启用或关闭选择性确认选项。
kind=5是SACK实际工作的选项。该选项的参数告诉发送方本端已经收到并缓存的不连续的数据块,从
而让发送端可以据此检查并重发丢失的数据块。每个块边沿(edge of block)参数包含一个4字节的序号。
其中块左边沿表示不连续块的第一个数据的序号,而块右边沿则表示不连续块的最后一个数据的序号的下
一个序号。这样一对参数(块左边沿和块右边沿)之间的数据是没有收到的。因为一个块信息占用8字节,所以TCP头部选项中实际上最多可以包含4个这样的不连续数据块(考虑选项类型和长度占用的2字节)。
kind=8是时间戳选项。该选项提供了较为准确的计算通信双方之间的回路时间(Round Trip Time,RTT)的方法,从而为TCP流量控制提供重要信息。我们可以通过修改procsysnetipv4tcp_timestamps内核
变量来启用或关闭时间戳选项。
3.2.3 使用tcpdump观察TCP头部信息
在2.3节中,我们利用tcpdump抓取了一个数据包并分析了其中的IP头部信息,本节分析其中与TCP协议
相关的部分(后面的分析中,我们将所有tcpdump抓取到的数据包都称为TCP报文段,因为TCP报文段既是
数据包的主要内容,也是我们主要讨论的对象)。为了方便阅读,先将该TCP报文段的内容复制于代码清
单3-1中。
代码清单3-1 用tcpdump抓取数据包
IP 127.0.0.1.41621>127.0.0.1.23:Flags[S],seq 3499745539,win 32792,options[mss 16396,sackOK,TS val 40781017 ecr 0,nop,wscale
6],length 0
0x0000:4510 003c a5da 4000 4006 96cf 7f00 0001
0x0010:7f00 0001 a295 0017 d099 e103 0000 0000
0x0020:a002 8018 fe30 0000 0204 400c 0402 080a
0x0030:026e 44d9 0000 0000 0103 0306
tcpdump输出Flags[S],表示该TCP报文段包含SYN标志,因此它是一个同步报文段。如果TCP报文段包
含其他标志,则tcpdump也会将该标志的首字母显示在“Flags”后的方括号中。
seq是序号值。因为该同步报文段是从127.0.0.1.41621(客户端IP地址和端口号)到127.0.0.1.23(服务器
IP地址和端口号)这个传输方向上的第一个TCP报文段,所以这个序号值也就是此次通信过程中该传输方向
的ISN值。并且,因为这是整个通信过程中的第一个TCP报文段,所以它没有针对对方发送来的TCP报文段
的确认值(尚未收到任何对方发送来的TCP报文段)。
win是接收通告窗口的大小。因为这是一个同步报文段,所以win值反映的是实际的接收通告窗口大
小。
options是TCP选项,其具体内容列在方括号中。mss是发送端(客户端)通告的最大报文段长度。通过
ifconfig命令查看回路接口的MTU为16436字节,因此可以预想到TCP报文段的MSS为16396(16436-40)字
节。sackOK表示发送端支持并同意使用SACK选项。TS val是发送端的时间戳。ecr是时间戳回显应答。因为
这是一次TCP通信的第一个TCP报文段,所以它针对对方的时间戳的应答为0(尚未收到对方的时间戳)。
紧接着的nop是一个空操作选项。wscale指出发送端使用的窗口扩大因子为6。
接下来我们分析tcpdump输出的字节码中TCP头部对应的信息,它从第21字节开始,如表3-1所示。从表3-1中可见,TCP报文段头部的二进制码和tcpdump输出的TCP报文段描述信息完全对应。在后面的
tcpdump输出中,我们将省略大部分TCP头部信息,仅显示序号、确认号、窗口大小以及标志位等与主题相
关的字段。
[1]此列中的空格表示我们并不关心相应字段的十进制值。3.3 TCP连接的建立和关闭
本节我们讨论建立和关闭TCP连接的过程。
3.3.1 使用tcpdump观察TCP连接的建立和关闭
首先从ernest-laptop上执行telnet命令登录Kongming20的80端口,然后抓取这一过程中客户端和服务器交
换的TCP报文段。具体操作过程如下:
sudo tcpdump-i eth0-nt'(src 192.168.1.109 and dst 192.168.1.108)or(src 192.168.1.108 and dst 192.168.1.109)'
telnet 192.168.1.109 80
Trying 192.168.1.109...
Connected to 192.168.1.109.
Escape character is'^]'.
^](回车)输入ctrl+]并回车
telnet>quit(回车)
Connection closed.
当执行telnet命令并在两台通信主机之间建立TCP连接后(telnet输出“Connected to 192.168.1.109”),输
入Ctrl+]以调出telnet程序的命令提示符,然后在telnet命令提示符后输入quit以退出telnet客户端程序,从而结
束TCP连接。整个过程中(从连接建立到结束),tcpdump输出的内容如代码清单3-2所示。
代码清单3-2 建立和关闭TCP连接的过程
1.IP 192.168.1.108.60871>192.168.1.109.80:Flags[S],seq 535734930,win 5840,length 0
2.IP 192.168.1.109.80>192.168.1.108.60871:Flags[S.],seq 2159701207,ack 535734931,win 5792,length 0
3.IP 192.168.1.108.60871>192.168.1.109.80:Flags[.],ack 1,win 92,length 0
4.IP 192.168.1.108.60871>192.168.1.109.80:Flags[F.],seq 1,ack 1,win 92,length 0
5.IP 192.168.1.109.80>192.168.1.108.60871:Flags[.],ack 2,win 91,length 0
6.IP 192.168.1.109.80>192.168.1.108.60871:Flags[F.],seq 1,ack 2,win 91,length 0
7.IP 192.168.1.108.60871>192.168.1.109.80:Flags[.],ack 2,win 92,length 0
因为整个过程并没有发生应用层数据的交换,所以TCP报文段的数据部分的长度(length)总是0。为
了更清楚地表示建立和关闭TCP连接的整个过程,我们将tcpdump输出的内容绘制成图3-6所示的时序图。图 3-6 TCP连接的建立和关闭时序图
第1个TCP报文段包含SYN标志,因此它是一个同步报文段,即ernest-laptop(客户端)向
Kongming20(服务器)发起连接请求。同时,该同步报文段包含一个ISN值为535734930的序号。第2个TCP
报文段也是同步报文段,表示Kongming20同意与ernest-laptop建立连接。同时它发送自己的ISN值为
2159701207的序号,并对第1个同步报文段进行确认。确认值是535734931,即第1个同步报文段的序号值加
1。前文说过,序号值是用来标识TCP数据流中的每一字节的。但同步报文段比较特殊,即使它并没有携带
任何应用程序数据,它也要占用一个序号值。第3个TCP报文段是ernest-laptop对第2个同步报文段的确认。
至此,TCP连接就建立起来了。建立TCP连接的这3个步骤被称为TCP三次握手。
从第3个TCP报文段开始,tcpdump输出的序号值和确认值都是相对初始ISN值的偏移。当然,我们可以
开启tcpdump的-S选项来选择打印序号的绝对值。
后面4个TCP报文段是关闭连接的过程。第4个TCP报文段包含FIN标志,因此它是一个结束报文段,即
ernest-laptop要求关闭连接。结束报文段和同步报文段一样,也要占用一个序号值。Kongming20用TCP报文
段5来确认该结束报文段。紧接着Kongming20发送自己的结束报文段6,ernest-laptop则用TCP报文段7给予
确认。实际上,仅用于确认目的的确认报文段5是可以省略的,因为结束报文段6也携带了该确认信息。确
认报文段5是否出现在连接断开的过程中,取决于TCP的延迟确认特性。延迟确认将在后面讨论。在连接的关闭过程中,因为ernest-laptop先发送结束报文段(telnet客户端程序主动退出),故称ernest-
laptop执行主动关闭,而称Kongming20执行被动关闭。
一般而言,TCP连接是由客户端发起,并通过三次握手建立(特殊情况是所谓同时打开[1])的。TCP连
接的关闭过程相对复杂一些。可能是客户端执行主动关闭,比如前面的例子;也可能是服务器执行主动关
闭,比如服务器程序被中断而强制关闭连接;还可能是同时关闭(和同时打开一样,非常少见)。
3.3.2 半关闭状态
TCP连接是全双工的,所以它允许两个方向的数据传输被独立关闭。换言之,通信的一端可以发送结
束报文段给对方,告诉它本端已经完成了数据的发送,但允许继续接收来自对方的数据,直到对方也发送
结束报文段以关闭连接。TCP连接的这种状态称为半关闭(half close)状态,如图3-7所示。
图 3-7 半关闭状态
请注意,在图3-7中,服务器和客户端应用程序判断对方是否已经关闭连接的方法是:read系统调用返
回0(收到结束报文段)。当然,Linux还提供其他检测连接是否被对方关闭的方法,这将在后续章节讨
论。
socket网络编程接口通过shutdown函数提供了对半关闭的支持,我们将在后续章节讨论它。这里强调一下,虽然我们介绍了半关闭状态,但是使用半关闭的应用程序很少见。
3.3.3 连接超时
前面我们讨论的是很快建立连接的情况。如果客户端访问一个距离它很远的服务器,或者由于网络繁
忙,导致服务器对于客户端发送出的同步报文段没有应答,此时客户端程序将产生什么样的行为呢?显
然,对于提供可靠服务的TCP来说,它必然是先进行重连(可能执行多次),如果重连仍然无效,则通知
应用程序连接超时。
为了观察连接超时,我们模拟一个繁忙的服务器环境,在ernest-laptop上执行下面的操作:
sudo iptables-F
sudo iptables-I INPUT-p tcp--syn-i eth0-j DROP
iptable命令用于过滤数据包,这里我们利用它来丢弃所有接收到的连接请求(丢弃所有同步报文段,这
样客户端就无法得到任何确认报文段)。
接下来从Kongming20上执行telnet命令登录到ernest-laptop,并用tcpdump抓取这个过程中双方交换的
TCP报文段。具体操作如下:
sudo tcpdump-n-i eth0 port 23仅抓取telnet客户端和服务器交换的数据包
date;telnet 192.168.1.108;date在telnet命令前后都执行date命令,以计算超时时间
Mon Jun 11 21:23:35 CST 2012
Trying 192.168.1.108...
telnet:connect to address 192.168.1.108:Connection timed out
Mon Jun 11 21:24:38 CST 2012
从两次date命令的输出来看,Kongming20建立TCP连接的超时时间是63s。本次tcpdump的输出如代码清
单3-3所示。
代码清单3-3 TCP超时重连
1.21:23:35.612136 IP 192.168.1.109.39385>192.168.1.108.telnet:Flags[S],seq 1355982096,length 0
2.21:23:36.613146 IP 192.168.1.109.39385>192.168.1.108.telnet:Flags[S],seq 1355982096,length 0
3.21:23:38.617279 IP 192.168.1.109.39385>192.168.1.108.telnet:Flags[S],seq 1355982096,length 0
4.21:23:42.625140 IP 192.168.1.109.39385>192.168.1.108.telnet:Flags[S],seq 1355982096,length 0
5.21:23:50.641344 IP 192.168.1.109.39385>192.168.1.108.telnet:Flags[S],seq 1355982096,length 0
6.21:24:06.673331 IP 192.168.1.109.39385>192.168.1.108.telnet:Flags[S],seq 1355982096,length 0
这次抓包我们保留了tcpdump输出的时间戳(不使用其-t选项),以便推理Linux的超时重连策略。
我们一共抓取到6个TCP报文段,它们都是同步报文段,并且具有相同的序号值,这说明后面5个同步
报文段都是超时重连报文段。观察这些TCP报文段被发送的时间间隔,它们分别为1s、2s、4s、8s和16s(由
于定时器精度的问题,这些时间间隔都有一定偏差),可以推断最后一个TCP报文段的超时时间是
32s(63s-16s-8s-4s-2s-1s)。因此,TCP模块一共执行了5次重连操作,这是
由procsysnetipv4tcp_syn_retries内核变量所定义的。每次重连的超时时间都增加一倍。在5次重连均失败
的情况下,TCP模块放弃连接并通知应用程序。
在应用程序中,我们可以修改连接超时时间,具体方法将在本书后续章节中进行介绍。3.4 TCP状态转移
TCP连接的任意一端在任一时刻都处于某种状态,当前状态可以通过netstat命令(见第17章)查看。本
节我们要讨论的是TCP连接从建立到关闭的整个过程中通信两端状态的变化。图3-8是完整的状态转移图,它描绘了所有的TCP状态以及可能的状态转换。图 3-8 TCP状态转移过程
图3-8中的粗虚线表示典型的服务器端连接的状态转移;粗实线表示典型的客户端连接的状态转移。
CLOSED是一个假想的起始点,并不是一个实际的状态。3.4.1 TCP状态转移总图
我们先讨论服务器的典型状态转移过程,此时我们说的连接状态都是指该连接的服务器端的状态。
服务器通过listen系统调用(见第5章)进入LISTEN状态,被动等待客户端连接,因此执行的是所谓的
被动打开。服务器一旦监听到某个连接请求(收到同步报文段),就将该连接放入内核等待队列中,并向
客户端发送带SYN标志的确认报文段。此时该连接处于SYN_RCVD状态。如果服务器成功地接收到客户端
发送回的确认报文段,则该连接转移到ESTABLISHED状态。ESTABLISHED状态是连接双方能够进行双向
数据传输的状态。
当客户端主动关闭连接时(通过close或shutdown系统调用向服务器发送结束报文段),服务器通过返
回确认报文段使连接进入CLOSE_WAIT状态。这个状态的含义很明确:等待服务器应用程序关闭连接。通
常,服务器检测到客户端关闭连接后,也会立即给客户端发送一个结束报文段来关闭连接。这将使连接转
移到LAST_ACK状态,以等待客户端对结束报文段的最后一次确认。一旦确认完成,连接就彻底关闭了。
下面讨论客户端的典型状态转移过程,此时我们说的连接状态都是指该连接的客户端的状态。
客户端通过connect系统调用(见第5章)主动与服务器建立连接。connect系统调用首先给服务器发送一
个同步报文段,使连接转移到SYN_SENT状态。此后,connect系统调用可能因为如下两个原因失败返回:
·如果connect连接的目标端口不存在(未被任何进程监听),或者该端口仍被处于TIME_WAIT状态的
连接所占用(见后文),则服务器将给客户端发送一个复位报文段,connect调用失败。
·如果目标端口存在,但connect在超时时间内未收到服务器的确认报文段,则connect调用失败。
connect调用失败将使连接立即返回到初始的CLOSED状态。如果客户端成功收到服务器的同步报文段
和确认,则connect调用成功返回,连接转移至ESTABLISHED状态。
当客户端执行主动关闭时,它将向服务器发送一个结束报文段,同时连接进入FIN_WAIT_1状态。若此
时客户端收到服务器专门用于确认目的的确认报文段(比如图3-6中的TCP报文段5),则连接转移至
FIN_WAIT_2状态。当客户端处于FIN_WAIT_2状态时,服务器处于CLOSE_WAIT状态,这一对状态是可能
发生半关闭的状态。此时如果服务器也关闭连接(发送结束报文段),则客户端将给予确认并进入
TIME_WAIT状态。关于TIME_WAIT状态的含义,我们将在下一节讨论。
图3-8还给出了客户端从FIN_WAIT_1状态直接进入TIME_WAIT状态的一条线路(不经过FIN_WAIT_2
状态),前提是处于FIN_WAIT_1状态的服务器直接收到带确认信息的结束报文段(而不是先收到确认报文
段,再收到结束报文段)。这种情况对应于图3-6中的服务器不发送TCP报文段5。
前面说过,处于FIN_WAIT_2状态的客户端需要等待服务器发送结束报文段,才能转移至TIME_WAIT
状态,否则它将一直停留在这个状态。如果不是为了在半关闭状态下继续接收数据,连接长时间地停留在
FIN_WAIT_2状态并无益处。连接停留在FIN_WAIT_2状态的情况可能发生在:客户端执行半关闭后,未等
服务器关闭连接就强行退出了。此时客户端连接由内核来接管,可称之为孤儿连接(和孤儿进程类似)。
Linux为了防止孤儿连接长时间存留在内核中,定义了两个内核变量:procsysnetipv4tcp_max_orphans
和procsysnetipv4tcp_fin_timeout。前者指定内核能接管的孤儿连接数目,后者指定孤儿连接在内核中生
存的时间。
至此,我们简单地讨论了服务器和客户端程序的典型TCP状态转移路线。对应于图3-6所示的TCP连接
的建立与断开过程,客户端和服务器的状态转移如图3-9所示。图 3-9 TCP连接的建立和断开过程中客户端和服务器的状态变化
图3-8还描绘了其他非典型的TCP状态转移路线,比如同时关闭与同时打开,本书不予讨论。
3.4.2 TIME_WAIT状态
从图3-9来看,客户端连接在收到服务器的结束报文段(TCP报文段6)之后,并没有直接进入CLOSED
状态[1]
,而是转移到TIME_WAIT状态。在这个状态,客户端连接要等待一段长为2MSL(Maximum
Segment Life,报文段最大生存时间)的时间,才能完全关闭。MSL是TCP报文段在网络中的最大生存时
间,标准文档RFC 1122的建议值是2 min。
TIME_WAIT状态存在的原因有两点:
·可靠地终止TCP连接。
·保证让迟来的TCP报文段有足够的时间被识别并丢弃。
第一个原因很好理解。假设图3-9中用于确认服务器结束报文段6的TCP报文段7丢失,那么服务器将重
发结束报文段。因此客户端需要停留在某个状态以处理重复收到的结束报文段(即向服务器发送确认报文
段)。否则,客户端将以复位报文段来回应服务器,服务器则认为这是一个错误,因为它期望的是一个像
TCP报文段7那样的确认报文段。在Linux系统上,一个TCP端口不能被同时打开多次(两次及以上)。当一个TCP连接处于TIME_WAIT
状态时,我们将无法立即使用该连接占用着的端口来建立一个新连接。反过来思考,如果不存在
TIME_WAIT状态,则应用程序能够立即建立一个和刚关闭的连接相似的连接(这里说的相似,是指它们具
有相同的IP地址和端口号)。这个新的、和原来相似的连接被称为原来的连接的化身(incarnation)。新的
化身可能接收到属于原来的连接的、携带应用程序数据的TCP报文段(迟到的报文段),这显然是不应该
发生的。这就是TIME_WAIT状态存在的第二个原因。
另外,因为TCP报文段的最大生存时间是MSL,所以坚持2MSL时间的TIME_WAIT状态能够确保网络
上两个传输方向上尚未被接收到的、迟到的TCP报文段都已经消失(被中转路由器丢弃)。因此,一个连
接的新的化身可以在2MSL时间之后安全地建立,而绝对不会接收到属于原来连接的应用程序数据,这就是
TIME_WAIT状态要持续2MSL时间的原因。
有时候我们希望避免TIME_WAIT状态,因为当程序退出后,我们希望能够立即重启它。但由于处在
TIME_WAIT状态的连接还占用着端口,程序将无法启动(直到2MSL超时时间结束)。考虑一个例子:在
测试机器ernest-laptop上以客户端方式运行nc(用于创建网络连接的工具,见第17章)命令,登录本机的
Web服务,且明确指定客户端使用12345端口与服务器通信。然后从终端输入Ctrl+C终止客户端程序,接着
又立即重启nc程序,以完全相同的方式再次连接本机的Web服务。具体操作如下:
nc-p 12345 192.168.1.108 80
ctrl+C中断客户端程序
nc-p 12345 192.168.1.108 80重启客户端程序,重新建立连接
nc:bind failed:Address already in use输出显示连接失败,因为12345端口仍被占用
netstat-nat用netstat命令查看连接状态
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 192.168.1.108:12345 192.168.1.108:80 TIME_WAIT
这里我们使用netstat命令查看连接的状态。其输出显示,客户端程序被中断后,连接进入TIME_WAIT
状态,12345端口仍被占用,所以客户端重启失败。
对客户端程序来说,我们通常不用担心上面描述的重启问题。因为客户端一般使用系统自动分配的临
时端口号来建立连接,而由于随机性,临时端口号一般和程序上一次使用的端口号(还处于TIME_WAIT状
态的那个连接使用的端口号)不同,所以客户端程序一般可以立即重启。上面的例子仅仅是为了说明问
题,我们强制客户端使用12345端口,这才导致立即重启客户端程序失败。
但如果是服务器主动关闭连接后异常终止,则因为它总是使用同一个知名服务端口号,所以连接的
TIME_WAIT状态将导致它不能立即重启。不过,我们可以通过socket选项SO_REUSEADDR来强制进程立
即使用处于TIME_WAIT状态的连接占用的端口,这将在第5章讨论。
[1]请读者根据语境判断连接的状态是指客户端状态还是服务器状态,后同。3.5 复位报文段
在某些特殊条件下,TCP连接的一端会向另一端发送携带RST标志的报文段,即复位报文段,以通知对
方关闭连接或重新建立连接。本节讨论产生复位报文段的3种情况。
3.5.1 访问不存在的端口
3.4.1小节提到,当客户端程序访问一个不存在的端口时,目标主机将给它发送一个复位报文段。考虑
从Kongming20上执行telnet命令登录ernest-laptop上一个不存在的54321端口,并用tcpdump抓取该过程中两台
主机交换的TCP报文段。具体操作过程如下:
sudo tcpdump-nt-i eth0 port 54321仅抓取发送至和来自54321端口的TCP报文段
telnet 192.168.1.108 54321
Trying 192.168.1.108...
telnet:connect to address 192.168.1.108:Connection refused
telnet程序的输出显示连接被拒绝了,因为这个端口不存在。tcpdump抓取到的TCP报文段内容如下:
1.IP 192.168.1.109.42001>192.168.1.108.54321:Flags[S],seq 21621375,win 14600,length 0
2.IP 192.168.1.108.54321>192.168.1.109.42001:Flags[R.],seq 0,ack 21621376,win 0,length 0
由此可见,ernest-laptop针对Kongming20的连接请求(同步报文段)回应了一个复位报文段(tcpdump
输出R标志)。因为复位报文段的接收通告窗口大小为0,所以可以预见:收到复位报文段的一端应该关闭
连接或者重新连接,而不能回应这个复位报文段。
实际上,当客户端程序向服务器的某个端口发起连接,而该端口仍被处于TIME_WAIT状态的连接所占
用时,客户端程序也将收到复位报文段。
3.5.2 异常终止连接
前面讨论的连接终止方式都是正常的终止方式:数据交换完成之后,一方给另一方发送结束报文段。
TCP提供了异常终止一个连接的方法,即给对方发送一个复位报文段。一旦发送了复位报文段,发送端所
有排队等待发送的数据都将被丢弃。
应用程序可以使用socket选项SO_LINGER来发送复位报文段,以异常终止一个连接。我们将在第5章讨
论SO_LINGER选项。
3.5.3 处理半打开连接
考虑下面的情况:服务器(或客户端)关闭或者异常终止了连接,而对方没有接收到结束报文段(比
如发生了网络故障),此时,客户端(或服务器)还维持着原来的连接,而服务器(或客户端)即使重
启,也已经没有该连接的任何信息了。我们将这种状态称为半打开状态,处于这种状态的连接称为半打开
连接。如果客户端(或服务器)往处于半打开状态的连接写入数据,则对方将回应一个复位报文段。
举例来说,我们在Kongming20上使用nc命令模拟一个服务器程序,使之监听12345端口,然后从ernest-
laptop运行telnet命令登录到该端口上,接着拔掉ernest-laptop的网线,并在Kongming20上中断服务器程序。
显然,此时ernest-laptop上运行的telnet客户端程序维持着一个半打开连接。然后接上ernest-laptop的网线,并
从客户端程序往半打开连接写入1字节的数据“a”。同时,运行tcpdump程序抓取整个过程中telnet客户端和nc
服务器交换的TCP报文段。具体操作过程如下:
nc-l 12345在Kongming20上运行服务器程序
sudo tcpdump-nt-i eth0 port 12345
telnet 192.168.1.109 12345在ernest-laptop上运行客户端程序
Trying 192.168.1.109...
Connected to 192.168.1.109.
Escape character is'^]'.此时断开ernest-laptop的网线,并重启服务器
a(回车)向半打开连接输入字符a
Connection closed by foreign host.telnet的输出显示,连接被服务器关闭了。tcpdump抓取到的TCP报文段内容如下:
1.IP 192.168.1.108.55100>192.168.1.109.12345:Flags[S],seq 3093809365,length 0
2.IP 192.168.1.109.12345>192.168.1.108.55100:Flags[S.],seq 1495337791,ack 3093809366,length 0
3.IP 192.168.1.108.55100>192.168.1.109.12345:Flags[.],ack 1,length 0
4.IP 192.168.1.108.55100>192.168.1.109.12345:Flags[P.],seq 1:4,ack 1,length 3
5.IP 192.168.1.109.12345>192.168.1.108.55100:Flags[R],seq 1495337792,length 0
该输出内容中,前3个TCP报文段是正常建立TCP连接的3次握手的过程。第4个TCP报文段由客户端发
送给服务器,它携带了3字节的应用程序数据,这3字节依次是:字母“a”、回车符“\r”和换行符“\n”。不过因
为服务器程序已经被中断,所以Kongming20对客户端发送的数据回应了一个复位报文段5。3.6 TCP交互数据流
前面讨论了TCP连接及其状态,从本节开始我们讨论通过TCP连接交换的应用程序数据。TCP报文段所
携带的应用程序数据按照长度分为两种:交互数据和成块数据。交互数据仅包含很少的字节。使用交互数
据的应用程序(或协议)对实时性要求高,比如telnet、ssh等。成块数据的长度则通常为TCP报文段允许的
最大数据长度。使用成块数据的应用程序(或协议)对传输效率要求高,比如ftp。本节我们讨论交互数据
流。
考虑如下情况:在ernest-laptop上执行telnet命令登录到本机,然后在shell命令提示符后执行ls命令,同
时用tcpdump抓取这一过程中telnet客户端和telnet服务器交换的TCP报文段。具体操作过程如下:
tcpdump-nt-i lo port 23
telnet 127.0.0.1
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is'^]'.
Ubuntu 9.10
ernest-laptop login:ernest(回车)输入用户名并回车
Password:(回车)输入密码并回车
ernest@ernest-laptop:~ls(回车)
上述过程将引起客户端和服务器交换很多TCP报文段。下面我们仅列出我们感兴趣的、执行ls命令产生
的tcpdump输出,如代码清单3-4所示。
代码清单3-4 TCP交互数据流
1.IP 127.0.0.1.58130>127.0.0.1.23:Flags[P.],seq 1408334812:1408334813,ack 1415955507,win 613,length 1
2.IP 127.0.0.1.23>127.0.0.1.58130:Flags[P.],seq 1:2,ack 1,win 512,length 1
3.IP 127.0.0.1.58130>127.0.0.1.23:Flags[.],ack 2,win 613,length 0
4.IP 127.0.0.1.58130>127.0.0.1.23:Flags[P.],seq 1:2,ack 2,win 613,length 1
5.IP 127.0.0.1.23>127.0.0.1.58130:Flags[P.],seq 2:3,ack 2,win 512,length 1
6.IP 127.0.0.1.58130>127.0.0.1.23:Flags[.],ack 3,win 613,length 0
7.IP 127.0.0.1.58130>127.0.0.1.23:Flags[P.],seq 2:4,ack 3,win 613,length 2
8.IP 127.0.0.1.23>127.0.0.1.58130:Flags[P.],seq 3:176,ack 4,win 512,length 173
9.IP 127.0.0.1.58130>127.0.0.1.23:Flags[.],ack 176,win 630,length 0
10.IP 127.0.0.1.23>127.0.0.1.58130:Flags[P.],seq 176:228,ack 4,win 512,length 52
11.IP 127.0.0.1.58130>127.0.0.1.23:Flags[.],ack 228,win 630,length 0
TCP报文段1由客户端发送给服务器,它携带1个字节的应用程序数据,即字母“l”。TCP报文段2是服务
器对TCP报文段1的确认,同时回显字母“l”。TCP报文段3是客户端对TCP报文段2的确认。第4~6个TCP报
文段是针对字母“s”的上述过程。TCP报文段7传送的2字节数据分别是:客户端键入的回车符和流结束符
(EOF,本例中是0x00)。TCP报文段8携带服务器返回的客户查询的目录的内容(ls命令的输出),包括
该目录下文件的文件名及其显示控制参数。TCP报文段9是客户端对TCP报文段8的确认。TCP报文段10携带
的也是服务器返回给客户端的数据,包括一个回车符、一个换行符、客户端登录用户的PS1环境变量(第一
级命令提示符)。TCP报文段11是客户端对TCP报文段10的确认。
在上述过程中,客户端针对服务器返回的数据所发送的确认报文段(TCP报文段6、9和11)都不携带
任何应用程序数据(长度为0),而服务器每次发送的确认报文段(TCP报文段2、5、8和10)都包含它需
要发送的应用程序数据。服务器的这种处理方式称为延迟确认,即它不马上确认上次收到的数据,而是在
一段延迟时间后查看本端是否有数据需要发送,如果有,则和确认信息一起发出。因为服务器对客户请求
处理得很快,所以它发送确认报文段的时候总是有数据一起发送。延迟确认可以减少发送TCP报文段的数
量。而由于用户的输入速度明显慢于客户端程序的处理速度,所以客户端的确认报文段总是不携带任何应
用程序数据。前文曾提到,在TCP连接的建立和断开过程中,也可能发生延迟确认。
上例是在本地回路运行的结果,在局域网中也能得到基本相同的结果,但在广域网就未必如此了。广
域网上的交互数据流可能经受很大的延迟,并且,携带交互数据的微小TCP报文段数量一般很多(一个按
键输入就导致一个TCP报文段),这些因素都可能导致拥塞发生。解决该问题的一个简单有效的方法是使
用Nagle算法。
Nagle算法要求一个TCP连接的通信双方在任意时刻都最多只能发送一个未被确认的TCP报文段,在该TCP报文段的确认到达之前不能发送其他TCP报文段。另一方面,发送方在等待确认的同时收集本端需要发
送的微量数据,并在确认到来时以一个TCP报文段将它们全部发出。这样就极大地减少了网络上的微小TCP
报文段的数量。该算法的另一个优点在于其自适应性:确认到达得越快,数据也就发送得越快。3.7 TCP成块数据流
下面考虑用FTP协议传输一个大文件。在ernest-laptop上启动一个vsftpd服务器程序(升级的、安全版的
ftp服务器程序),并执行ftp命令登录该服务器上,然后在ftp命令提示符后输入get命令,从服务器下载一个
几百兆的大文件。同时用tcpdump抓取这一个过程中ftp客户端和vsftpd服务器交换的TCP报文段。具体操作
过程如下:
sudo tcpdump-nt-i eth0 port 20vsftpd服务器程序使用端口号20
ftp 127.0.0.1
Connected to 127.0.0.1.
220(vsFTPd 2.3.0)
Name(127.0.0.1:ernest):ernest(回车)输入用户名并回车
331 Please specify the password.
Password:(回车)输入密码并回车
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
ftp>get bigfile(回车)获取大文件bigfile
代码清单3-5是该过程的部分tcpdump输出。
代码清单3-5 TCP成块数据流
1.IP 127.0.0.1.20>127.0.0.1.39651:Flags[.],seq 205783041:205799425,ack 1,win 513,length 16384
2.IP 127.0.0.1.20>127.0.0.1.39651:Flags[.],seq 205799425:205815809,ack 1,win 513,length 16384
3.IP 127.0.0.1.20>127.0.0.1.39651:Flags[.],seq 205815809:205832193,ack 1,win 513,length 16384
4.IP 127.0.0.1.20>127.0.0.1.39651:Flags[P.],seq 205832193:205848577,ack 1,win 513,length 16384
5.IP 127.0.0.1.20>127.0.0.1.39651:Flags[.],seq 205848577:205864961,ack 1,win 513,length 16384
6.IP 127.0.0.1.20>127.0.0.1.39651:Flags[.],seq 205864961:205881345,ack 1,win 513,length 16384
7.IP 127.0.0.1.20>127.0.0.1.39651:Flags[.],seq 205881345:205897729,ack 1,win 513,length 16384
8.IP 127.0.0.1.20>127.0.0.1.39651:Flags[P.],seq 205897729:205914113,ack 1,win 513,length 16384
9.IP 127.0.0.1.20>127.0.0.1.39651:Flags[.],seq 205914113:205930497,ack 1,win 513,length 16384
10.IP 127.0.0.1.20>127.0.0.1.39651:Flags[.],seq 205930497:205946881,ack 1,win 513,length 16384
11.IP 127.0.0.1.20>127.0.0.1.39651:Flags[.],seq 205946881:205963265,ack 1,win 513,length 16384
12.IP 127.0.0.1.20>127.0.0.1.39651:Flags[P.],seq 205963265:205979649,ack 1,win 513,length 16384
13.IP 127.0.0.1.20>127.0.0.1.39651:Flags[.],seq 205979649:205996033,ack 1,win 513,length 16384
14.IP 127.0.0.1.20>127.0.0.1.39651:Flags[.],seq 205996033:206012417,ack 1,win 513,length 16384
15.IP 127.0.0.1.20>127.0.0.1.39651:Flags[.],seq 206012417:206028801,ack 1,win 513,length 16384
16.IP 127.0.0.1.20>127.0.0.1.39651:Flags[P.],seq 206028801:206045185,ack 1,win 513,length 16384
17.IP 127.0.0.1.39651>127.0.0.1.20:Flags[.],ack 205815809,win 30084,length 0
18.IP 127.0.0.1.39651>127.0.0.1.20:Flags[.],ack 206045185,win 27317,length 0
注意,客户端发送的最后两个TCP报文段17和18,它们分别是对TCP报文段2和16的确认(从序号值和
确认值来判断)。由此可见,当传输大量大块数据的时候,发送方会连续发送多个TCP报文段,接收方可
以一次确认所有这些报文段。那么发送方在收到上一次确认后,能连续发送多少个TCP报文段呢?这是由
接收通告窗口(还需要考虑拥塞窗口,见后文)的大小决定的。TCP报文段17说明客户端还能接收30
084×64字节(本例中窗口扩大因子为6),即1 925 376字节的数据。而在TCP报文段18中,接收通告窗口大
小为1 748 288字节,即客户端能接收的数据量变小了。这表明客户端的TCP接收缓冲区有更多的数据未被
应用程序读取而停留在其中,这些数据都来自TCP报文段3~16中的一部分。服务器收到TCP报文段18后,它至少(因为接收通告窗口可能扩大)还能连续发送的未被确认的报文段数量是1 748 28816 384个,即106
个(但一般不会连续发送这么多)。其中,16 384是成块数据的长度(见TCP报文段1~16的length值),很
显然它小于但接近MSS规定的16 396字节。
另外一个值得注意的地方是,服务器每发送4个TCP报文段就传送一个PSH标志(tcpdump输出标志P)
给客户端,以通知客户端的应用程序尽快读取数据。不过这对服务器来说显然不是必需的,因为它知道客
户端的TCP接收缓冲区中还有空闲空间(接收通告窗口大小不为0)。
下面我们修改系统的TCP接收缓冲区和TCP发送缓冲区的大小(如何修改将在第16章介绍),使之都为
4096字节,然后重启vsftpd服务器,并再次执行上述操作。此次tcpdump的部分输出如代码清单3-6所示。
代码清单3-6 修改TCP接收和发送缓冲区大小后的TCP成块数据流
1.IP 127.0.0.1.20>127.0.0.1.45227:Flags[.],seq 5195777:5197313,ack 1,win 3072,length 1536
2.IP 127.0.0.1.20>127.0.0.1.45227:Flags[.],seq 5197313:5198849,ack 1,win 3072,length 1536
3.IP 127.0.0.1.45227>127.0.0.1.20:Flags[.],ack 5198849,win 3072,length 04.IP 127.0.0.1.20>127.0.0.1.45227:Flags[P.],seq 5198849:5200385,ack 1,win 3072,length 1536
5.IP 127.0.0.1.45227>127.0.0.1.20:Flags[.],ack 5200385,win 3072,length 0
从同步报文段(未在代码清单3-6中列出)得知在这次通信过程中,客户端和服务器的窗口扩大因子都
为0,因而客户端和服务器每次通告的窗口大小都是3072字节(没超过4096字节,预料之中)。因为每个成
块数据的长度为1536字节,所以服务器在收到上一个TCP报文段的确认之前最多还能再发送1个TCP报文
段,这正是TCP报文段1~3描述的情形。3.8 带外数据
有些传输层协议具有带外(Out Of Band,OOB)数据的概念,用于迅速通告对方本端发生的重要事
件。因此,带外数据比普通数据(也称为带内数据)有更高的优先级,它应该总是立即被发送,而不论发
送缓冲区中是否有排队等待发送的普通数据。带外数据的传输可以使用一条独立的传输层连接,也可以映
射到传输普通数据的连接中。实际应用中,带外数据的使用很少见,已知的仅有telnet、ftp等远程非活跃程
序。
UDP没有实现带外数据传输,TCP也没有真正的带外数据。不过TCP利用其头部中的紧急指针标志和紧
急指针两个字段,给应用程序提供了一种紧急方式。TCP的紧急方式利用传输普通数据的连接来传输紧急
数据。这种紧急数据的含义和带外数据类似,因此后文也将TCP紧急数据称为带外数据。
我们先来介绍TCP发送带外数据的过程。假设一个进程已经往某个TCP连接的发送缓冲区中写入了N字
节的普通数据,并等待其发送。在数据被发送前,该进程又向这个连接写入了3字节的带外数据“abc”。此
时,待发送的TCP报文段的头部将被设置URG标志,并且紧急指针被设置为指向最后一个带外数据的下一
字节(进一步减去当前TCP报文段的序号值得到其头部中的紧急偏移值),如图3-10所示。
图 3-10 TCP发送缓冲区中的紧急数据
由图3-10可见,发送端一次发送的多字节的带外数据中只有最后一字节被当作带外数据(字母c),而
其他数据(字母a和b)被当成了普通数据。如果TCP模块以多个TCP报文段来发送图3-10所示TCP发送缓冲
区中的内容,则每个TCP报文段都将设置URG标志,并且它们的紧急指针指向同一个位置(数据流中带外
数据的下一个位置),但只有一个TCP报文段真正携带带外数据。
现在考虑TCP接收带外数据的过程。TCP接收端只有在接收到紧急指针标志时才检查紧急指针,然后根
据紧急指针所指的位置确定带外数据的位置,并将它读入一个特殊的缓存中。这个缓存只有1字节,称为带
外缓存。如果上层应用程序没有及时将带外数据从带外缓存中读出,则后续的带外数据(如果有的话)将
覆盖它。
前面讨论的带外数据的接收过程是TCP模块接收带外数据的默认方式。如果我们给TCP连接设置了
SO_OOBINLINE选项,则带外数据将和普通数据一样被TCP模块存放在TCP接收缓冲区中。此时应用程序
需要像读取普通数据一样来读取带外数据。那么这种情况下如何区分带外数据和普通数据呢?显然,紧急
指针可以用来指出带外数据的位置,socket编程接口也提供了系统调用来识别带外数据(见第5章)。
至此,我们讨论了TCP模块发送和接收带外数据的过程。至于内核如何通知应用程序带外数据的到
来,以及应用程序如何发送和接收带外数据,将在后续章节讨论。3.9 TCP超时重传
在3.6节~3.8节中,我们讲述了TCP在正常网络情况下的数据流。从本节开始,我们讨论异常网络状况
下(开始出现超时或丢包),TCP如何控制数据传输以保证其承诺的可靠服务。
TCP服务必须能够重传超时时间内未收到确认的TCP报文段。为此,TCP模块为每个TCP报文段都维护
一个重传定时器,该定时器在TCP报文段第一次被发送时启动。如果超时时间内未收到接收方的应答,TCP
模块将重传TCP报文段并重置定时器。至于下次重传的超时时间如何选择,以及最多执行多少次重传,就
是TCP的重传策略。我们通过实例来研究Linux下TCP的超时重传策略。
在ernest-laptop上启动iperf服务器程序,然后从Kongming20上执行telnet命令登录该服务器程序。接下
来,从telnet客户端发送一些数据(此处是“1234”)给服务器,然后断开服务器的网线并再次从客户端发送
一些数据给服务器(此处是“12”)。同时,用tcpdump抓取这一过程中客户端和服务器交换的TCP报文段。
具体操作过程如下:
sudo tcpdump-n-i eth0 port 5001
iperf-s在ernest-laptop上执行
telnet 192.168.1.108 5001在Kongming20上执行
Trying 192.168.1.108...
Connected to 192.168.1.108.
Escape character is'^]'.
1234发送完之后断开服务器网线
12
Connection closed by foreign host
iperf是一个测量网络状况的工具,-s选项表示将其作为服务器运行。iperf默认监听5001端口,并丢弃该
端口上接收到的所有数据,相当于一个discard服务器。上述操作过程的部分tcpdump输出如代码清单3-7所
示。
代码清单3-7 TCP超时重传
1.18:44:57.580341 IP 192.168.1.109.38234>192.168.1.108.5001:Flags[S],seq 2381272950,length 0
2.18:44:57.580477 IP 192.168.1.108.5001>192.168.1.109.38234:Flags[S.],seq 466032301,ack 2381272951,length 0
3.18:44:57.580498 IP 192.168.1.109.38234>192.168.1.108.5001:Flags[.],ack 1,length 0
4.18:44:59.866019 IP 192.168.1.109.38234>192.168.1.108.5001:Flags[P.],seq 1:7,ack 1,length 6
5.18:44:59.866165 IP 192.168.1.108.5001>192.168.1.109.38234:Flags[.],ack 7,length 0
6.18:45:25.028933 IP 192.168.1.109.38234>192.168.1.108.5001:Flags[P.],seq 7:11,ack 1,length 4
7.18:45:25.230034 IP 192.168.1.109.38234>192.168.1.108.5001:Flags[P.],seq 7:11,ack 1,length 4
8.18:45:25.639407 IP 192.168.1.109.38234>192.168.1.108.5001:Flags[P.],seq 7:11,ack 1,length 4
9.18:45:26.455942 IP 192.168.1.109.38234>192.168.1.108.5001:Flags[P.],seq 7:11,ack 1,length 4
10.18:45:28.092425 IP 192.168.1.109.38234>192.168.1.108.5001:Flags[P.],seq 7:11,ack 1,length 4
11.18:45:31.362473 IP 192.168.1.109.38234>192.168.1.108.5001:Flags[P.],seq 7:11,ack 1,length 4
12.18:45:33.100888 ARP,Request who-has 192.168.1.108 tell 192.168.1.109,length 28
13.18:45:34.098156 ARP,Request who-has 192.168.1.108 tell 192.168.1.109,length 28
14.18:45:35.100887 ARP,Request who-has 192.168.1.108 tell 192.168.1.109,length 28
15.18:45:37.902034 ARP,Request who-has 192.168.1.108 tell 192.168.1.109,length 28
16.18:45:38.903126 ARP,Request who-has 192.168.1.108 tell 192.168.1.109,length 28
17.18:45:39.901421 ARP,Request who-has 192.168.1.108 tell 192.168.1.109,length 28
18.18:45:44.440049 ARP,Request who-has 192.168.1.108 tell 192.168.1.109,length 28
19.18:45:45.438840 ARP,Request who-has 192.168.1.108 tell 192.168.1.109,length 28
20.18:45:46.439932 ARP,Request who-has 192.168.1.108 tell 192.168.1.109,length 28
21.18:45:50.976710 ARP,Request who-has 192.168.1.108 tell 192.168.1.109,length 28
22.18:45:51.974134 ARP,Request who-has 192.168.1.108 tell 192.168.1.109,length 28
23.18:45:52.973939 ARP,Request who-has 192.168.1.108 tell 192.168.1.109,length 28
TCP报文段1~3是三次握手建立连接的过程,TCP报文段4~5是客户端发送数据“1234”(应用程序数据
长度为6,包括回车、换行两个字符,后同)及服务器确认的过程。TCP报文段6是客户端第一次发送数
据“12”的过程。因为服务器的网线被断开,所以客户端无法收到TCP报文段6的确认报文段。此后,客户端
对TCP报文段6执行了5次重传,它们是TCP报文段7~11,这可以从每个TCP报文段的序号得知。此后,数
据包12~23都是ARP模块的输出内容,即Kongming20查询ernest-laptop的MAC地址。
我们保留了tcpdump输出的时间戳,以便推理TCP的超时重传策略。观察TCP报文段6~11被发送的时间
间隔,它们分别为0.2 s、0.4 s、0.8 s、1.6 s和3.2 s。由此可见,TCP一共执行5次重传,每次重传超时时间都
增加一倍(因此,和TCP超时重连的策略相似)。在5次重传均失败的情况下,底层的IP和ARP开始接管,直到telnet客户端放弃连接为止。Linux有两个重要的内核参数与TCP超时重传相关:procsysnetipv4tcp_retries1
和procsysnetipv4tcp_retries2。前者指定在底层IP接管之前TCP最少执行的重传次数,默认值是3。后者指
定连接放弃前TCP最多可以执行的重传次数,默认值是15(一般对应13~30 min)。在我们的实例中,TCP
超时重传发生了5次,连接坚持的时间是15 min(可以用date命令来测量)。
虽然超时会导致TCP报文段重传,但TCP报文段的重传可以发生在超时之前,即快速重传,这将在下一
节中讨论。3.10 拥塞控制
3.10.1 拥塞控制概述
TCP模块还有一个重要的任务,就是提高网络利用率,降低丢包率,并保证网络资源对每条数据流的
公平性。这就是所谓的拥塞控制。
TCP拥塞控制的标准文档是RFC 5681,其中详细介绍了拥塞控制的四个部分:慢启动(slow start)、拥塞避免(congestion avoidance)、快速重传(fast retransmit)和快速恢复(fast recovery)。拥塞控制算法
在Linux下有多种实现,比如reno算法、vegas算法和cubic算法等。它们或者部分或者全部实现了上述四个部
分。procsysnetipv4t ......
新浪微博 @研发书局
官方网址:www.hzmedia.com.cn
客服信箱:service@bbbvip.com
客服热线:+ 86-10-68995265
版权所有,侵权必究
制作与发行。
本书纸版由机械工业出版社于2013年出版,电子版由华章分社(北京华章图文信息有限公司)全球范围内
ISBN:978-7-111-42519-9
游双 著
Linux高性能服务器编程
加工、传播自负法律后果。
本书仅供个人学习之用,请勿用于商业用途。如对本书有兴趣,请购买正版书籍。任何对本书籍的修改、目 录 前言
为什么要写这本书
读者对象
本书特色
如何阅读本书
勘误和支持
致谢
第一篇 TCPIP协议详解
第1章 TCPIP协议族
1.1 TCPIP协议族体系结构以及主要协议
1.2 封装
1.3 分用
1.4 测试网络
1.5 ARP协议工作原理
1.6 DNS工作原理
1.7 socket和TCPIP协议族的关系
第2章 IP协议详解
2.1 IP服务的特点
2.2 IPv4头部结构
2.3 IP分片
2.4 IP路由
2.5 IP转发
2.6 重定向
2.7 IPv6头部结构
第3章 TCP协议详解
3.1 TCP服务的特点
3.2 TCP头部结构
3.3 TCP连接的建立和关闭
3.4 TCP状态转移
3.5 复位报文段
3.6 TCP交互数据流
3.7 TCP成块数据流
3.8 带外数据
3.9 TCP超时重传
3.10 拥塞控制
第4章 TCPIP通信案例:访问Internet上的Web服务器
4.1 实例总图
4.2 部署代理服务器
4.3 使用tcpdump抓取传输数据包
4.4 访问DNS服务器
4.5 本地名称查询
4.6 HTTP通信
4.7 实例总结
第二篇 深入解析高性能服务器编程
第5章 Linux网络编程基础API
5.1 socket地址API
5.2 创建socket
5.3 命名socket
5.4 监听socket
5.5 接受连接
5.6 发起连接
5.7 关闭连接5.8 数据读写
5.9 带外标记
5.10 地址信息函数
5.11 socket选项
5.12 网络信息API
第6章 高级IO函数
6.1 pipe函数
6.2 dup函数和dup2函数
6.3 readv函数和writev函数
6.4 sendfile函数
6.5 mmap函数和munmap函数
6.6 splice函数
6.7 tee函数
6.8 fcntl函数
第7章 Linux服务器程序规范
7.1 日志
7.2 用户信息
7.3 进程间关系
7.4 系统资源限制
7.5 改变工作目录和根目录
7.6 服务器程序后台化
第8章 高性能服务器程序框架
8.1 服务器模型
8.2 服务器编程框架
8.3 IO模型
8.4 两种高效的事件处理模式
8.4.3 模拟Proactor模式
8.5 两种高效的并发模式
8.6 有限状态机
8.7 提高服务器性能的其他建议
第9章 IO复用
9.1 select系统调用
9.2 poll系统调用
9.3 epoll系列系统调用
9.4 三组IO复用函数的比较
9.5 IO复用的高级应用一:非阻塞connect
9.6 IO复用的高级应用二:聊天室程序
9.7 IO复用的高级应用三:同时处理TCP和UDP服务
9.8 超级服务xinetd
第10章 信号
10.1 Linux信号概述
10.2 信号函数
10.3 信号集
10.4 统一事件源
10.5 网络编程相关信号
第11章 定时器
11.1 socket选项SO_RCVTIMEO和SO_SNDTIMEO
11.2 SIGALRM信号
11.3 IO复用系统调用的超时参数
11.4 高性能定时器
第12章 高性能IO框架库Libevent
12.1 IO框架库概述
12.2 Libevent源码分析
第13章 多进程编程13.1 fork系统调用
13.2 exec系列系统调用
13.3 处理僵尸进程
13.4 管道
13.5 信号量
13.6 共享内存
13.7 消息队列
13.8 IPC命令
13.9 在进程间传递文件描述符
第14章 多线程编程
14.1 Linux线程概述
14.2 创建线程和结束线程
14.3 线程属性
14.4 POSIX信号量
14.5 互斥锁
14.6 条件变量
14.7 线程同步机制包装类
14.8 多线程环境
第15章 进程池和线程池
15.1 进程池和线程池概述
15.2 处理多客户
15.3 半同步半异步进程池实现
15.4 用进程池实现的简单CGI服务器
15.5 半同步半反应堆线程池实现
15.6 用线程池实现的简单Web服务器
第三篇 高性能服务器优化与监测
第16章 服务器调制、调试和测试
16.1 最大文件描述符数
16.2 调整内核参数
16.3 gdb调试
16.4 压力测试
第17章 系统监测工具
17.1 tcpdump
17.2 lsof
17.3 nc
17.4 strace
17.5 netstat
17.6 vmstat
17.7 ifstat
17.8 mpstat
参考文献前言
为什么要写这本书
目前国内计算机书籍的一个明显弊病就是内容宽泛而空洞。很多书籍长篇大论,恨不得囊括所有最新
的技术,但连一个最基本的技术细节也无法解释清楚。有些书籍给读者展现的是网络上随处可见的知识,基本没有自己的观点,甚至连一点自己的总结都没有。反观大师们的经典书籍,整本书只专注于一个问
题,而且对每个技术细节的描述都是精雕细琢。最关键的是,我们在阅读这些经典书籍时,似乎是在用心
与一位编程高手交流,这绝对是一种享受。
我们把问题缩小到计算机网络编程领域。关于计算机网络编程的相关书籍,不得不提的是已故网络教
育巨匠W·Richard Stevens先生的《TCPIP协议详解》(三卷本),以及《UNIX网络编程》(两卷本)。作
为一名网络程序员,即使没有阅读过这几本书,也应该听说过它们。但这几本书中的内容实在是太庞大
了,没有耐心的读者根本不可能把它们全部读完。而且对于英文不太好的朋友来说,选择阅读其翻译版本
又有失原汁原味。
基于以上两点原因,笔者编写了这本《Linux高性能服务器编程》。本书是笔者多年来学习网络编程之
总结,是在充分理解大师的作品并融入自己的理解和见解后写成的。本书讨论的主题和定位很明确。简单
来说就是:如何通过各种手段编写高性能的服务器程序。
网络技术是在不断向前发展的,比如Linux提供的epoll机制就是在内核2.6版本之后才正式引入的。但
是,编程思想却可以享用一辈子。我们在不断学习并使用新技术,不断适应新环境的同时,书中提到的网
络编程思想能让我们看得更远,想得更多。笔者相信,没有谁会认为W·Richard Stevens先生的网络编程书籍
过时了。读者对象
阅读本书之前,读者需要了解基本的计算机网络知识,并具有一定的Linux系统编程和C++编程基础,否则阅读起来会有些困难。本书读者对象主要包括:
·Linux网络应用程序开发人员
·Linux系统程序开发人员
·CC++程序开发人员
·对网络编程技术感兴趣,或希望参与网络程序开发的人员
·开设相关课程的大专院校师生本书特色
本书的特点:不求内容宽泛,但求专而精,深入地剖析服务器编程的要素;不求内容精准,但求融入
笔者自己的理解和观点,可谓“另眼”看服务器编程。
如何提高服务器程序性能是本书要着重讨论的。第6、8、9、11、12、15、16等章中都用了相当的篇幅
讨论这一主题。其论述方法是:首先,探讨提高服务器程序性能的一般原则,比如使用“池”以牺牲空间换
取效率,使用零拷贝函数以避免内核和用户空间的切换等;其次,介绍一些高效的编程模式及其应用,比
如使用有限状态机来分析用户数据,使用进程池或线程池来处理用户请求;最后,探讨如何通过调整系统
参数来从服务器程序外部提高其整体性能。
光说不练假把式。如果没有实例,或者只是给出几个“Hello World”,那么本书就真没有出版的必要
了。笔者要做的是让读者能真正把理论和实践完美地结合起来。在写作本书之前,笔者阅读了不少开源社
区的优秀服务器软件的源代码,自己也写过相当多的小型服务器程序。这些软件中那些最精彩的部分,在
书中都有充分的体现。比如第15章给出的两个实例——用进程池实现的简单CGI服务器和用线程池实现的简
单Web服务器,就充分展现了如何利用各种提高服务器性能的手段来高效地解决实际问题。
此外,为了帮助读者进一步把书中的知识融汇到实际项目中,笔者还特意编写了一个较为完整的负载
均衡服务器程序springsnail。该程序能从所有逻辑服务器中选取负荷最小的一台来处理新到的客户连接。在
这个程序中,使用了进程池、有限状态机、高效数据结构来提高其性能;同时,细致地封装了每个函数和
模块,使之更符合实际工程项目。由于篇幅的限制,笔者未将该程序的源代码列在书中,读者可从华章网
站[1]
上下载它。
[1]参见华章网站www.hzbook.com。——编辑注如何阅读本书
本书分为三篇:
第一篇(第1~4章)介绍TCPIP协议族及各种重要的网络协议。只有很好地理解了底层TCPIP通信的
过程,才能编写出高质量的网络应用程序。毕竟,坚实的基础设施造就稳固的上层建筑。
第二篇(第5~15章)细致地剖析了服务器编程的各主要方面,其中对每个重要的概念、模型以及函数
等都以实例代码的形式加以阐述。这一篇又可细分为如下四个部分:
·第一部分(第5~7章)介绍Linux操作系统为网络编程提供的众多API。这些API就像是基本的音符,我们通过组织它们来谱写优美的旋律。
·第二部分(第8章)探讨高性能服务器程序的一般框架。在这一部分中,我们将服务器程序解构为
IO单元、逻辑单元和存储单元三个部件,并重点介绍了IO单元、逻辑单元的几种高效实现模式。此外,我
们还探讨了提高服务器性能的其他建议。
·第三部分(第9~12章)深入剖析服务器程序的IO单元。我们将探讨IO单元需要处理的IO事件、信
号事件和定时事件,并介绍一款优秀的开源IO框架库——Libevent。
·第四部分(第13~15章)深入剖析服务器程序的逻辑单元。这一部分我们要讨论多线程、多进程编
程,以及高性能逻辑处理模型——进程池和线程池,并给出相应的实例代码。
第三篇(第16~17章)探讨如何从系统的角度优化和监测服务器性能。本篇的内容涉及服务器程序的
调制、调试和测试,以及诸多常用系统监测工具的使用。勘误和支持
由于作者的水平有限,加之编写时间仓促,书中难免会出现一些错误或者不准确的地方,恳请读者批
评指正。书中的全部源文件都可以从华章网站下载。如果您有更多的宝贵意见或建议,也欢迎发送邮件至
邮箱pjhq87@gmail.com,期待能够得到您的真挚反馈。致谢
首先要感谢伟大的网络教育导师W·Richard Stevens先生,他的书籍帮助了无数的网络程序开发人员,也
给笔者指明了学习的道路。
感谢机械工业出版社华章公司的编辑杨福川老师和孙海亮老师,是他们在这两年多的时间中始终支持
着我的写作,因为有了他们的鼓励、帮助和引导,笔者才能顺利完成全部书稿。
感谢好友史正政,他对编程充满热爱,也无私地为本书提供了原材料。
谨以此书献给我最亲爱的家人和朋友,以及那些为计算机网络教育做出巨大贡献的大师们,还有正在
为自己的未来努力拼搏、充满朝气和活力的IT工程师们!
游双
于北京第一篇 TCPIP协议详解
第1章 TCPIP协议族
第2章 IP协议详解
第3章 TCP协议详解
第4章 TCPIP通信案例:访问Internet上的Web服务器第1章 TCPIP协议族
现在Internet(因特网)使用的主流协议族是TCPIP协议族,它是一个分层、多协议的通信体系。本章
简要讨论TCPIP协议族各层包含的主要协议,以及它们之间是如何协作完成网络通信的。
TCPIP协议族包含众多协议,我们无法一一讨论。本书将在后续章节详细讨论IP协议和TCP协议,因为
它们对编写网络应用程序具有最直接的影响。本章则简单介绍其中几个相关协议:ICMP协议、ARP协议和
DNS协议,学习它们对于理解网络通信很有帮助。读者如果想要系统地学习网络协议,那么RFC(Request
For Comments,评论请求)文档无疑是首选资料。
1.1 TCPIP协议族体系结构以及主要协议
TCPIP协议族是一个四层协议系统,自底而上分别是数据链路层、网络层、传输层和应用层。每一层
完成不同的功能,且通过若干协议来实现,上层协议使用下层协议提供的服务,如图1-1所示。
图 1-1 TCPIP协议族体系结构及主要协议
1.1.1 数据链路层
数据链路层实现了网卡接口的网络驱动程序,以处理数据在物理媒介(比如以太网、令牌环等)上的
传输。不同的物理网络具有不同的电气特性,网络驱动程序隐藏了这些细节,为上层协议提供一个统一的
接口。
数据链路层两个常用的协议是ARP协议(Address Resolve Protocol,地址解析协议)和RARP协议
(Reverse Address Resolve Protocol,逆地址解析协议)。它们实现了IP地址和机器物理地址(通常是MAC
地址,以太网、令牌环和802.11无线网络都使用MAC地址)之间的相互转换。
网络层使用IP地址寻址一台机器,而数据链路层使用物理地址寻址一台机器,因此网络层必须先将目标机器的IP地址转化成其物理地址,才能使用数据链路层提供的服务,这就是ARP协议的用途。RARP协议
仅用于网络上的某些无盘工作站。因为缺乏存储设备,无盘工作站无法记住自己的IP地址,但它们可以利
用网卡上的物理地址来向网络管理者(服务器或网络管理软件)查询自身的IP地址。运行RARP服务的网络
管理者通常存有该网络上所有机器的物理地址到IP地址的映射。
由于ARP协议很重要,所以我们将在后面章节专门讨论它。
1.1.2 网络层
网络层实现数据包的选路和转发。WAN(Wide Area Network,广域网)通常使用众多分级的路由器来
连接分散的主机或LAN(Local Area Network,局域网),因此,通信的两台主机一般不是直接相连的,而
是通过多个中间节点(路由器)连接的。网络层的任务就是选择这些中间节点,以确定两台主机之间的通
信路径。同时,网络层对上层协议隐藏了网络拓扑连接的细节,使得在传输层和网络应用程序看来,通信
的双方是直接相连的。
网络层最核心的协议是IP协议(Internet Protocol,因特网协议)。IP协议根据数据包的目的IP地址来决
定如何投递它。如果数据包不能直接发送给目标主机,那么IP协议就为它寻找一个合适的下一跳(next
hop)路由器,并将数据包交付给该路由器来转发。多次重复这一过程,数据包最终到达目标主机,或者由
于发送失败而被丢弃。可见,IP协议使用逐跳(hop by hop)的方式确定通信路径。我们将在第2章详细讨
论IP协议。
网络层另外一个重要的协议是ICMP协议(Internet Control Message Protocol,因特网控制报文协议)。
它是IP协议的重要补充,主要用于检测网络连接。ICMP协议使用的报文格式如图1-2所示。
图 1-2 ICMP报文格式
图1-2中,8位类型字段用于区分报文类型。它将ICMP报文分为两大类:一类是差错报文,这类报文主
要用来回应网络错误,比如目标不可到达(类型值为3)和重定向(类型值为5);另一类是查询报文,这
类报文用来查询网络信息,比如ping程序就是使用ICMP报文查看目标是否可到达(类型值为8)的。有的
ICMP报文还使用8位代码字段来进一步细分不同的条件。比如重定向报文使用代码值0表示对网络重定向,代码值1表示对主机重定向。ICMP报文使用16位校验和字段对整个报文(包括头部和内容部分)进行循环
冗余校验(Cyclic Redundancy Check,CRC),以检验报文在传输过程中是否损坏。不同的ICMP报文类型
具有不同的正文内容。我们将在第2章详细讨论主机重定向报文,其他ICMP报文格式请参考ICMP协议的标
准文档RFC 792。
需要指出的是,ICMP协议并非严格意义上的网络层协议,因为它使用处于同一层的IP协议提供的服务
(一般来说,上层协议使用下层协议提供的服务)。
1.1.3 传输层
传输层为两台主机上的应用程序提供端到端(end to end)的通信。与网络层使用的逐跳通信方式不
同,传输层只关心通信的起始端和目的端,而不在乎数据包的中转过程。图1-3展示了传输层和网络层的这
种区别。图 1-3 传输层和网络层的区别
图1-3中,垂直的实线箭头表示TCPIP协议族各层之间的实体通信(数据包确实是沿着这些线路传递
的),而水平的虚线箭头表示逻辑通信线路。该图中还附带描述了不同物理网络的连接方法。可见,数据
链路层(驱动程序)封装了物理网络的电气细节;网络层封装了网络连接的细节;传输层则为应用程序封
装了一条端到端的逻辑通信链路,它负责数据的收发、链路的超时重连等。
传输层协议主要有三个:TCP协议、UDP协议和SCTP协议。
TCP协议(Transmission Control Protocol,传输控制协议)为应用层提供可靠的、面向连接的和基于流
(stream)的服务。TCP协议使用超时重传、数据确认等方式来确保数据包被正确地发送至目的端,因此
TCP服务是可靠的。使用TCP协议通信的双方必须先建立TCP连接,并在内核中为该连接维持一些必要的数
据结构,比如连接的状态、读写缓冲区,以及诸多定时器等。当通信结束时,双方必须关闭连接以释放这
些内核数据。TCP服务是基于流的。基于流的数据没有边界(长度)限制,它源源不断地从通信的一端流
入另一端。发送端可以逐个字节地向数据流中写入数据,接收端也可以逐个字节地将它们读出。
UDP协议(User Datagram Protocol,用户数据报协议)则与TCP协议完全相反,它为应用层提供不可靠、无连接和基于数据报的服务。“不可靠”意味着UDP协议无法保证数据从发送端正确地传送到目的端。
如果数据在中途丢失,或者目的端通过数据校验发现数据错误而将其丢弃,则UDP协议只是简单地通知应
用程序发送失败。因此,使用UDP协议的应用程序通常要自己处理数据确认、超时重传等逻辑。UDP协议
是无连接的,即通信双方不保持一个长久的联系,因此应用程序每次发送数据都要明确指定接收端的地址
(IP地址等信息)。基于数据报的服务,是相对基于流的服务而言的。每个UDP数据报都有一个长度,接
收端必须以该长度为最小单位将其所有内容一次性读出,否则数据将被截断。
SCTP协议(Stream Control Transmission Protocol,流控制传输协议)是一种相对较新的传输层协议,它
是为了在因特网上传输电话信号而设计的。本书不讨论SCTP协议,感兴趣的读者可参考其标准文档RFC
2960。
我们将在第3章详细讨论TCP协议,并附带介绍UDP协议。
1.1.4 应用层
应用层负责处理应用程序的逻辑。数据链路层、网络层和传输层负责处理网络通信细节,这部分必须
既稳定又高效,因此它们都在内核空间中实现,如图1-1所示。而应用层则在用户空间实现,因为它负责处
理众多逻辑,比如文件传输、名称查询和网络管理等。如果应用层也在内核中实现,则会使内核变得非常
庞大。当然,也有少数服务器程序是在内核中实现的,这样代码就无须在用户空间和内核空间来回切换
(主要是数据的复制),极大地提高了工作效率。不过这种代码实现起来较复杂,不够灵活,且不便于移
植。本书只讨论用户空间的网络编程。
应用层协议很多,图1-1仅列举了其中的几个:
ping是应用程序,而不是协议,前面说过它利用ICMP报文检测网络连接,是调试网络环境的必备工
具。
telnet协议是一种远程登录协议,它使我们能在本地完成远程任务,本书后续章节将会多次使用telnet客
户端登录到其他服务上。
OSPF(Open Shortest Path First,开放最短路径优先)协议是一种动态路由更新协议,用于路由器之间
的通信,以告知对方各自的路由信息。
DNS(Domain Name Service,域名服务)协议提供机器域名到IP地址的转换,我们将在后面简要介绍
DNS协议。
应用层协议(或程序)可能跳过传输层直接使用网络层提供的服务,比如ping程序和OSPF协议。应用
层协议(或程序)通常既可以使用TCP服务,又可以使用UDP服务,比如DNS协议。我们可以通
过etcservices文件查看所有知名的应用层协议,以及它们都能使用哪些传输层服务。1.2 封装
上层协议是如何使用下层协议提供的服务的呢?其实这是通过封装(encapsulation)实现的。应用程序
数据在发送到物理网络上之前,将沿着协议栈从上往下依次传递。每层协议都将在上层数据的基础上加上
自己的头部信息(有时还包括尾部信息),以实现该层的功能,这个过程就称为封装,如图1-4所示。
图 1-4 封装
经过TCP封装后的数据称为TCP报文段(TCP message segment),或者简称TCP段。前文提到,TCP协
议为通信双方维持一个连接,并且在内核中存储相关数据。这部分数据中的TCP头部信息和TCP内核缓冲区
(发送缓冲区或接收缓冲区)数据一起构成了TCP报文段,如图1-5中的虚线框所示。图 1-5 TCP报文段封装过程
当发送端应用程序使用send(或者write)函数向一个TCP连接写入数据时,内核中的TCP模块首先把这
些数据复制到与该连接对应的TCP内核发送缓冲区中,然后TCP模块调用IP模块提供的服务,传递的参数包
括TCP头部信息和TCP发送缓冲区中的数据,即TCP报文段。关于TCP报文段头部的细节,我们将在第3章
讨论。
经过UDP封装后的数据称为UDP数据报(UDP datagram)。UDP对应用程序数据的封装与TCP类似。不
同的是,UDP无须为应用层数据保存副本,因为它提供的服务是不可靠的。当一个UDP数据报被成功发送
之后,UDP内核缓冲区中的该数据报就被丢弃了。如果应用程序检测到该数据报未能被接收端正确接收,并打算重发这个数据报,则应用程序需要重新从用户空间将该数据报拷贝到UDP内核发送缓冲区中。
经过IP封装后的数据称为IP数据报(IP datagram)。IP数据报也包括头部信息和数据部分,其中数据部
分就是一个TCP报文段、UDP数据报或者ICMP报文。我们将在第2章详细讨论IP数据报的头部信息。
经过数据链路层封装的数据称为帧(frame)。传输媒介不同,帧的类型也不同。比如,以太网上传输
的是以太网帧(ethernet frame),而令牌环网络上传输的则是令牌环帧(token ring frame)。以以太网帧为
例,其封装格式如图1-6所示。
图 1-6 以太网帧封装
以太网帧使用6字节的目的物理地址和6字节的源物理地址来表示通信的双方。关于类型(type)字段,我们将在后面讨论。4字节CRC字段对帧的其他部分提供循环冗余校验。帧的最大传输单元(Max Transmit Unit,MTU),即帧最多能携带多少上层协议数据(比如IP数据
报),通常受到网络类型的限制。图1-6所示的以太网帧的MTU是1500字节。正因为如此,过长的IP数据报
可能需要被分片(fragment)传输。
帧才是最终在物理网络上传送的字节序列。至此,封装过程完成。1.3 分用
当帧到达目的主机时,将沿着协议栈自底向上依次传递。各层协议依次处理帧中本层负责的头部数
据,以获取所需的信息,并最终将处理后的帧交给目标应用程序。这个过程称为分用(demultiplexing)。
分用是依靠头部信息中的类型字段实现的。标准文档RFC 1700定义了所有标识上层协议的类型字段以及每
个上层协议对应的数值。图1-7显示了以太网帧的分用过程。
图 1-7 以太网帧的分用过程
因为IP协议、ARP协议和RARP协议都使用帧传输数据,所以帧的头部需要提供某个字段(具体情况取
决于帧的类型)来区分它们。以以太网帧为例,它使用2字节的类型字段来标识上层协议(见图1-6)。如
果主机接收到的以太网帧类型字段的值为0x800,则帧的数据部分为IP数据报(见图1-4),以太网驱动程序
就将帧交付给IP模块;若类型字段的值为0x806,则帧的数据部分为ARP请求或应答报文,以太网驱动程序
就将帧交付给ARP模块;若类型字段的值为0x835,则帧的数据部分为RARP请求或应答报文,以太网驱动
程序就将帧交付给RARP模块。
同样,因为ICMP协议、TCP协议和UDP协议都使用IP协议,所以IP数据报的头部采用16位的协议
(protocol)字段来区分它们。
TCP报文段和UDP数据报则通过其头部中的16位的端口号(port number)字段来区分上层应用程序。比
如DNS协议对应的端口号是53,HTTP协议(Hyper-Text Transfer Protocol,超文本传送协议)对应的端口号
是80。所有知名应用层协议使用的端口号都可在etcservices文件中找到。
帧通过上述分用步骤后,最终将封装前的原始数据送至目标服务(图1-7中的ARP服务、RARP服务、ICMP服务或者应用程序)。这样,在顶层目标服务看来,封装和分用似乎没有发生过。1.4 测试网络
为了深入理解网络通信和网络编程,我们准备了图1-8所示的测试网络,其中包括两台主机A和B,以及
一个连接到因特网的路由器。后文如没有特别声明,所有测试硬件指的都是该网络。我们将使用机器名来
标识测试机器。
图 1-8 测试网络
该测试网络主要用于分析ARP协议、IP协议、ICMP协议、TCP协议和DNS协议。我们通过抓取该网络
上的以太网帧,查看其中的以太网帧头部、IP数据报头部、TCP报文段头部信息,以获取网络通信的细节。
这样,以理论结合实践,我们就清楚TCPIP通信具体是如何进行的了。作者编写的多个客户端、服务器程
序都是使用该网络来调试和测试的。
对于路由器,我们仅列出了其LAN网络IP地址(192.168.1.1),而忽略了ISP(Internet Service
Provider,因特网服务提供商)给它分配的WAN网络IP地址,因为全书的讨论都不涉及它。1.5 ARP协议工作原理
ARP协议能实现任意网络层地址到任意物理地址的转换,不过本书仅讨论从IP地址到以太网地址
(MAC地址)的转换。其工作原理是:主机向自己所在的网络广播一个ARP请求,该请求包含目标机器的
网络地址。此网络上的其他机器都将收到这个请求,但只有被请求的目标机器会回应一个ARP应答,其中
包含自己的物理地址。
1.5.1 以太网ARP请求应答报文详解
以太网ARP请求应答报文的格式如图1-9所示。
图 1-9 以太网ARP请求应答报文
图1-9所示以太网ARP请求应答报文各字段具体介绍如下。
·硬件类型字段定义物理地址的类型,它的值为1表示MAC地址。
·协议类型字段表示要映射的协议地址类型,它的值为0x800,表示IP地址。
·硬件地址长度字段和协议地址长度字段,顾名思义,其单位是字节。对MAC地址来说,其长度为6;
对IP(v4)地址来说,其长度为4。
·操作字段指出4种操作类型:ARP请求(值为1)、ARP应答(值为2)、RARP请求(值为3)和
RARP应答(值为4)。
·最后4个字段指定通信双方的以太网地址和IP地址。发送端填充除目的端以太网地址外的其他3个字
段,以构建ARP请求并发送之。接收端发现该请求的目的端IP地址是自己,就把自己的以太网地址填进
去,然后交换两个目的端地址和两个发送端地址,以构建ARP应答并返回之(当然,如前所述,操作字段
需要设置为2)。
由图1-9可知,ARP请求应答报文的长度为28字节。如果再加上以太网帧头部和尾部的18字节(见图1-
6),则一个携带ARP请求应答报文的以太网帧长度为46字节。不过有的实现要求以太网帧数据部分长度至
少为46字节(见图1-4),此时ARP请求应答报文将增加一些填充字节,以满足这个要求。在这种情况下,一个携带ARP请求应答报文的以太网帧长度为64字节。
1.5.2 ARP高速缓存的查看和修改
通常,ARP维护一个高速缓存,其中包含经常访问(比如网关地址)或最近访问的机器的IP地址到物
理地址的映射。这样就避免了重复的ARP请求,提高了发送数据包的速度。
Linux下可以使用arp命令来查看和修改ARP高速缓存。比如,ernest-laptop在某一时刻(注意,ARP高速
缓存是动态变化的)的ARP缓存内容如下(使用arp-a命令):
Kongming20(192.168.1.109)at 08:00:27:53:10:67[ether]on eth0
·(192.168.1.1)at 14:e6:e4:93:5b:78[ether]on eth0
其中,第一项描述的是另一台测试机器Kongming20(注意,其IP地址、MAC地址都与图1-8描述的一致),第二项描述的是路由器。下面两条命令则分别删除和添加一个ARP缓存项:
sudo arp-d 192.168.1.109删除Kongming20对应的ARP缓存项
sudo arp-s 192.168.1.109 08:00:27:53:10:67添加Kongming20对应的ARP缓存项
1.5.3 使用tcpdump观察ARP通信过程
为了清楚地了解ARP的运作过程,我们从ernest-laptop上执行telnet命令登录Kongming20的echo服务(已
经开启),并用tcpdump(详见第17章)抓取这个过程中两台测试机器之间交换的以太网帧。具体的操作过
程如下:
sudo arp-d 192.168.1.109清除ARP缓存中Kongming20对应的项
sudo tcpdump-i eth0-ent'(dst 192.168.1.109 and src 192.168.1.108)or
(dst 192.168.1.108 and src 192.168.1.109)'如无特殊声明,抓包都在机器ernest-laptop上执行
telnet 192.168.1.109 echo开启另一个终端执行telnet命令
Trying 192.168.1.109...
Connected to 192.168.1.109.
Escape character is'^]'.
^](回车)输入Ctrl+]并回车
telnet>quit(回车)
Connection closed.
在执行telnet命令之前,应先清除ARP缓存中与Kongming20对应的项,否则ARP通信不被执行,我们也
就无法抓取到期望的以太网帧。当执行telnet命令并在两台通信主机之间建立TCP连接后(telnet输
出“Connected to 192.168.1.109”),输入Ctrl+]以调出telnet程序的命令提示符,然后在telnet命令提示符后输
入quit,退出telnet客户端程序(因为ARP通信在TCP连接建立之前就已经完成,故我们不关心后续内容)。
tcpdump抓取到的众多数据包中,只有最靠前的两个和ARP通信有关系,现在将它们列出(数据包前面的编
号是笔者加入的,后同):
1.00:16:d3:5c:b9:e3>ff:ff:ff:ff:ff:ff,ethertype ARP(0x0806),length 42:Request who-has 192.168.1.109 tell
192.168.1.108,length 28
2.08:00:27:53:10:67>00:16:d3:5c:b9:e3,ethertype ARP(0x0806),length 60:Reply 192.168.1.109 is-at 08:00:27:53:10:67,length 46
由tcpdump抓取的数据包本质上是以太网帧,我们通过该命令的众多选项来控制帧的过滤(比如用dst和
src指定通信的目的端IP地址和源端IP地址)和显示(比如用-e选项开启以太网帧头部信息的显示)。
第一个数据包中,ARP通信的源端的物理地址是00:16:d3:5c:b9:e3(ernest-laptop),目的端的物理地址
是ff:ff:ff:ff:ff:ff,这是以太网的广播地址,用以表示整个LAN。该LAN上的所有机器都会收到并处理这样的
帧。数值0x806是以太网帧头部的类型字段的值,它表示分用的目标是ARP模块。该以太网帧的长度为42字
节(实际上是46字节,tcpdump未统计以太网帧尾部4字节的CRC字段),其中数据部分长度为28字
节。“Request”表示这是一个ARP请求,“who-has 192.168.1.109 tell 192.168.1.108”则表示是ernest-laptop要查
询Kongming20的IP地址。
第二个数据包中,ARP通信的源端的物理地址是08:00:27:53:10:67(Kongming20),目的端的物理地址
是00:16:d3:5c:b9:e3(ernest-laptop)。“Reply”表示这是一个ARP应答,“192.168.1.109 is-at
08:00:27:53:10:67”则表示目标机器Kongming20报告其物理地址。该以太网帧的长度为60字节(实际上是64
字节),可见它使用了填充字节来满足最小帧长度。
为了便于理解,我们将上述讨论用图1-10来详细说明。图 1-10 ARP通信过程
关于该图,需要说明三点:
第一,我们将两次传输的以太网帧按照图1-6所描述的以太网帧封装格式绘制在图的下半部分。
第二,ARP请求和应答是从以太网驱动程序发出的,而并非像图中描述的那样从ARP模块直接发送到
以太网上,所以我们将它们用虚线表示,这主要是为了体现携带ARP数据的以太网帧和其他以太网帧(比
如携带IP数据报的以太网帧)的区别。
第三,路由器也将接收到以太网帧1,因为该帧是一个广播帧。不过很显然,路由器并没有回应其中的
ARP请求,正如前文讨论的那样。1.6 DNS工作原理
我们通常使用机器的域名来访问这台机器,而不直接使用其IP地址,比如访问因特网上的各种网站。
那么如何将机器的域名转换成IP地址呢?这就需要使用域名查询服务。域名查询服务有很多种实现方式,比如NIS(Network Information Service,网络信息服务)、DNS和本地静态文件等。本节主要讨论DNS。
1.6.1 DNS查询和应答报文详解
DNS是一套分布式的域名服务系统。每个DNS服务器上都存放着大量的机器名和IP地址的映射,并且
是动态更新的。众多网络客户端程序都使用DNS协议来向DNS服务器查询目标主机的IP地址。DNS查询和
应答报文的格式如图1-11所示。
图 1-11 DNS查询和应答报文
16位标识[1]
字段用于标记一对DNS查询和应答,以此区分一个DNS应答是哪个DNS查询的回应。
16位标志字段用于协商具体的通信方式和反馈通信状态。DNS报文头部的16位标志字段的细节如图1-
12所示。图 1-12 DNS报文头部的标志字段
图1-12中各标志的含义分别是:
·QR,查询应答标志。0表示这是一个查询报文,1表示这是一个应答报文。
·opcode,定义查询和应答的类型。0表示标准查询,1表示反向查询(由IP地址获得主机域名),2表
示请求服务器状态。
·AA,授权应答标志,仅由应答报文使用。1表示域名服务器是授权服务器。
·TC,截断标志,仅当DNS报文使用UDP服务时使用。因为UDP数据报有长度限制,所以过长的DNS
报文将被截断。1表示DNS报文超过512字节,并被截断。
·RD,递归查询标志。1表示执行递归查询,即如果目标DNS服务器无法解析某个主机名,则它将向
其他DNS服务器继续查询,如此递归,直到获得结果并把该结果返回给客户端。0表示执行迭代查询,即如
果目标DNS服务器无法解析某个主机名,则它将自己知道的其他DNS服务器的IP地址返回给客户端,以供
客户端参考。
·RA,允许递归标志。仅由应答报文使用,1表示DNS服务器支持递归查询。
·zero,这3位未用,必须都设置为0。
·rcode,4位返回码,表示应答的状态。常用值有0(无错误)和3(域名不存在)。
接下来的4个字段则分别指出DNS报文的最后4个字段的资源记录数目。对查询报文而言,它一般包含1
个查询问题,而应答资源记录数、授权资源记录数和额外资源记录数则为0。应答报文的应答资源记录数则
至少为1,而授权资源记录数和额外资源记录数可为0或非0。
查询问题的格式如图1-13所示。
图 1-13 DNS查询问题的格式
图1-13中,查询名以一定的格式封装了要查询的主机域名。16位查询类型表示如何执行查询操作,常
见的类型有如下几种:?类型A,值是1,表示获取目标主机的IP地址。
·类型CNAME,值是5,表示获得目标主机的别名。
·类型PTR,值是12,表示反向查询。
16位查询类通常为1,表示获取因特网地址(IP地址)。
应答字段、授权字段和额外信息字段都使用资源记录(Resource Record,RR)格式。资源记录格式如
图1-14所示。
图 1-14 资源记录格式
图1-14中,32位域名是该记录中与资源对应的名字,其格式和查询问题中的查询名字段相同。16位类
型和16位类字段的含义也与DNS查询问题的对应字段相同。
32位生存时间表示该查询记录结果可被本地客户端程序缓存多长时间,单位是秒。
16位资源数据长度字段和资源数据字段的内容取决于类型字段。对类型A而言,资源数据是32位的IPv4
地址,而资源数据长度则为4(以字节为单位)。
至此,我们简要地介绍了DNS协议。我们将在后面给出一个DNS通信的具体例子。DNS协议的更多细
节请参考其RFC文档(DNS协议存在诸多RFC文档,每个RFC文档介绍其一个侧面,比如RFC 1035介绍的
是域名的实现和规范,RFC 1886则描述DNS协议对IPv6的扩展支持)。
1.6.2 Linux下访问DNS服务
我们要访问DNS服务,就必须先知道DNS服务器的IP地址。Linux使用etcresolv.conf文件来存放DNS服
务器的IP地址。机器ernest-laptop上,该文件的内容如下:
Generated by Network Manager
nameserver 219.239.26.42
nameserver 124.207.160.106其中的两个IP地址分别是首选DNS服务器地址和备选DNS服务器地址。文件中的注释语句“Generated by
Network Manager”告诉我们,这两个DNS服务器地址是由网络管理程序写入的。
Linux下一个常用的访问DNS服务器的客户端程序是host,比如下面的命令是向首选DNS服务器
219.239.26.42查询机器www.baidu.com的IP地址:
host-t A www.baidu.com
www.baidu.com is an alias for www.a.shifen.com.
www.a.shifen.com has address 119.75.217.56
www.a.shifen.com has address 119.75.218.77
host命令的输出告诉我们,机器名www.baidu.com是www.a.shifen.com.的别名,并且该机器名对应两个
IP地址。host命令使用DNS协议和DNS服务器通信,其-t选项告诉DNS协议使用哪种查询类型。我们这里使
用的是A类型,即通过机器的域名获得其IP地址(但实际上返回的资源记录中还包含机器的别名)。关于
host命令的详细使用方法,请参考其man手册。
1.6.3 使用tcpdump观察DNS通信过程
为了看清楚DNS通信的过程,下面我们将从ernest-laptop上运行host命令以查询主机www.baidu.com对应
的IP地址,并使用tcpdump抓取这一过程中LAN上传输的以太网帧。具体的操作过程如下:
sudo tcpdump-i eth0-nt-s 500 port domain
host-t A www.baidu.com
这一次执行tcpdump抓包时,我们使用“port domain”来过滤数据包,表示只抓取使用domain(域名)服
务的数据包,即DNS查询和应答报文。tcpdump的输出如下:
1.IP 192.168.1.108.34319>219.239.26.42.53:57428+A?www.baidu.com.(31)
2.IP 219.239.26.42.53>192.168.1.108.34319:57428 344 CNAME www.a.shifen.com.,A 119.75.218.77,A 119.75.217.56(226)
这两个数据包开始的“IP”指出,它们后面的内容描述的是IP数据报。tcpdump以“IP地址.端口号”的形式
来描述通信的某一端;以“>”表示数据传输的方向,“>”前面是源端,后面是目的端。可见,第一个数据包
是测试机器ernest-laptop(IP地址是192.168.1.108)向其首选DNS服务器(IP地址是219.239.26.42)发送的
DNS查询报文(目标端口53是DNS服务使用的端口,这一点我们在前面介绍过),第二个数据包是服务器
反馈的DNS应答报文。
第一个数据包中,数值57428是DNS查询报文的标识值,因此该值也出现在DNS应答报文中。“+”表示
启用递归查询标志。“A?”表示使用A类型的查询方式。“www.baidu.com”则是DNS查询问题中的查询名。括
号中的数值31是DNS查询报文的长度(以字节为单位)。
第二个数据包中,“344”表示该报文中包含3个应答资源记录、4个授权资源记录和4个额外信息记
录。“CNAME www.a.shifen.com.,A 119.75.218.77,A 119.75.217.56”则表示3个应答资源记录的内容。其中
CNAME表示紧随其后的记录是机器的别名,A表示紧随其后的记录是IP地址。该应答报文的长度为226字
节。
注意 我们抓包的时候没有开启tcpdump的-X选项(或者-x选项)。如果使用-X选项,我们将能看到
DNS报文的每一个字节,也就能明白上面31字节的查询报文和226字节的应答报文的具体含义。限于篇幅,这里不再讨论,读者不妨自己分析。
[1]“标识”和“标志”在《现代汉语词典(第5版)》中表示同一含义,但是在本书中(计算机业界也是如
此),它们为两个概念,代表不同的含义,读者在阅读时应严格区分。1.7 socket和TCPIP协议族的关系
前文提到,数据链路层、网络层、传输层协议是在内核中实现的。因此操作系统需要实现一组系统调
用,使得应用程序能够访问这些协议提供的服务。实现这组系统调用的API(Application Programming
Interface,应用程序编程接口)主要有两套:socket和XTI。XTI现在基本不再使用,本书仅讨论socket。图
1-1显示了socket与TCPIP协议族的关系。
由socket定义的这一组API提供如下两点功能:一是将应用程序数据从用户缓冲区中复制到TCPUDP内
核发送缓冲区,以交付内核来发送数据(比如图1-5所示的send函数),或者是从内核TCPUDP接收缓冲区
中复制数据到用户缓冲区,以读取数据;二是应用程序可以通过它们来修改内核中各层协议的某些头部信
息或其他数据结构,从而精细地控制底层通信的行为。比如可以通过setsockopt函数来设置IP数据报在网络
上的存活时间。我们将在第5章详细讨论这一组API。
值得一提的是,socket是一套通用网络编程接口,它不但可以访问内核中TCPIP协议栈,而且可以访问
其他网络协议栈(比如X.25协议栈、UNIX本地域协议栈等)。第2章 IP协议详解
IP协议是TCPIP协议族的核心协议,也是socket网络编程的基础之一。本章从两个方面较为深入地探讨
IP协议:
·IP头部信息。IP头部信息出现在每个IP数据报中,用于指定IP通信的源端IP地址、目的端IP地址,指
导IP分片和重组,以及指定部分通信行为。
·IP数据报的路由和转发。IP数据报的路由和转发发生在除目标机器之外的所有主机和路由器上。它们
决定数据报是否应该转发以及如何转发。
由于32位表示的IP地址即将全部使用完,因此人们开发出了新版本的IP协议,称为IPv6协议,而原来的
版本则称为IPv4协议。本章前面部分的讨论都是基于IPv4协议的,只在最后一节简要讨论IPv6协议。
在开始讨论前,我们先简单介绍一下IP服务。
2.1 IP服务的特点
IP协议是TCPIP协议族的动力,它为上层协议提供无状态、无连接、不可靠的服务。
无状态(stateless)是指IP通信双方不同步传输数据的状态信息,因此所有IP数据报的发送、传输和接
收都是相互独立、没有上下文关系的。这种服务最大的缺点是无法处理乱序和重复的IP数据报。比如发送
端发送出的第N个IP数据报可能比第N+1个IP数据报后到达接收端,而同一个IP数据报也可能经过不同的路
径多次到达接收端。在这两种情况下,接收端的IP模块无法检测到乱序和重复,因为这些IP数据报之间没有
任何上下文关系。接收端的IP模块只要收到了完整的IP数据报(如果是IP分片的话,IP模块将先执行重
组),就将其数据部分(TCP报文段、UDP数据报或者ICMP报文)上交给上层协议。那么从上层协议来
看,这些数据就可能是乱序的、重复的。面向连接的协议,比如TCP协议,则能够自己处理乱序的、重复
的报文段,它递交给上层协议的内容绝对是有序的、正确的。
虽然IP数据报头部提供了一个标识字段(见后文)用以唯一标识一个IP数据报,但它是被用来处理IP分
片和重组的,而不是用来指示接收顺序的。
无状态服务的优点也很明显:简单、高效。我们无须为保持通信的状态而分配一些内核资源,也无须
每次传输数据时都携带状态信息。在网络协议中,无状态是很常见的,比如UDP协议和HTTP协议都是无状
态协议。以HTTP协议为例,一个浏览器的连续两次网页请求之间没有任何关联,它们将被Web服务器独立
地处理。
无连接(connectionless)是指IP通信双方都不长久地维持对方的任何信息。这样,上层协议每次发送数
据的时候,都必须明确指定对方的IP地址。
不可靠是指IP协议不能保证IP数据报准确地到达接收端,它只是承诺尽最大努力(best effort)。很多
种情况都能导致IP数据报发送失败。比如,某个中转路由器发现IP数据报在网络上存活的时间太长(根据IP
数据报头部字段TTL判断,见后文),那么它将丢弃之,并返回一个ICMP错误消息(超时错误)给发送
端。又比如,接收端发现收到的IP数据报不正确(通过校验机制),它也将丢弃之,并返回一个ICMP错误
消息(IP头部参数错误)给发送端。无论哪种情况,发送端的IP模块一旦检测到IP数据报发送失败,就通知
上层协议发送失败,而不会试图重传。因此,使用IP服务的上层协议(比如TCP协议)需要自己实现数据确
认、超时重传等机制以达到可靠传输的目的。2.2 IPv4头部结构
2.2.1 IPv4头部结构
IPv4的头部结构如图2-1所示。其长度通常为20字节,除非含有可变长的选项部分。
图 2-1 IPv4头部结构
4位版本号(version)指定IP协议的版本。对IPv4来说,其值是4。其他IPv4协议的扩展版本(如SIP协
议和PIP协议),则具有不同的版本号(它们的头部结构也和图2-1不同)。
4位头部长度(header length)标识该IP头部有多少个32 bit字(4字节)。因为4位最大能表示15,所以
IP头部最长是60字节。
8位服务类型(Type Of Service,TOS)包括一个3位的优先权字段(现在已经被忽略),4位的TOS字
段和1位保留字段(必须置0)。4位的TOS字段分别表示:最小延时,最大吞吐量,最高可靠性和最小费
用。其中最多有一个能置为1,应用程序应该根据实际需要来设置它。比如像ssh和telnet这样的登录程序需
要的是最小延时的服务,而文件传输程序ftp则需要最大吞吐量的服务。
16位总长度(total length)是指整个IP数据报的长度,以字节为单位,因此IP数据报的最大长度为65
535(216-1)字节。但由于MTU的限制,长度超过MTU的数据报都将被分片传输,所以实际传输的IP数据
报(或分片)的长度都远远没有达到最大值。接下来的3个字段则描述了如何实现分片。
16位标识(identification)唯一地标识主机发送的每一个数据报。其初始值由系统随机生成;每发送一
个数据报,其值就加1。该值在数据报分片时被复制到每个分片中,因此同一个数据报的所有分片都具有相
同的标识值。3位标志字段的第一位保留。第二位(Don’t Fragment,DF)表示“禁止分片”。如果设置了这个位,IP
模块将不对数据报进行分片。在这种情况下,如果IP数据报长度超过MTU的话,IP模块将丢弃该数据报并
返回一个ICMP差错报文。第三位(More Fragment,MF)表示“更多分片”。除了数据报的最后一个分片
外,其他分片都要把它置1。
13位分片偏移(fragmentation offset)是分片相对原始IP数据报开始处(仅指数据部分)的偏移。实际
的偏移值是该值左移3位(乘8)后得到的。由于这个原因,除了最后一个IP分片外,每个IP分片的数据部分
的长度必须是8的整数倍(这样才能保证后面的IP分片拥有一个合适的偏移值)。
8位生存时间(Time To Live,TTL)是数据报到达目的地之前允许经过的路由器跳数。TTL值被发送端
设置(常见的值是64)。数据报在转发过程中每经过一个路由,该值就被路由器减1。当TTL值减为0时,路由器将丢弃数据报,并向源端发送一个ICMP差错报文。TTL值可以防止数据报陷入路由循环。
8位协议(protocol)用来区分上层协议,我们在第1章讨论过。etcprotocols文件定义了所有上层协议对
应的protocol字段的数值。其中,ICMP是1,TCP是6,UDP是17。etcprotocols文件是RFC 1700的一个子
集。
16位头部校验和(header checksum)由发送端填充,接收端对其使用CRC算法以检验IP数据报头部(注
意,仅检验头部)在传输过程中是否损坏。
32位的源端IP地址和目的端IP地址用来标识数据报的发送端和接收端。一般情况下,这两个地址在整个
数据报的传递过程中保持不变,而不论它中间经过多少个中转路由器。关于这一点,我们将在第4章进一步
讨论。
IPv4最后一个选项字段(option)是可变长的可选信息。这部分最多包含40字节,因为IP头部最长是60
字节(其中还包含前面讨论的20字节的固定部分)。可用的IP选项包括:
·记录路由(record route),告诉数据报途经的所有路由器都将自己的IP地址填入IP头部的选项部分,这样我们就可以跟踪数据报的传递路径。
·时间戳(timestamp),告诉每个路由器都将数据报被转发的时间(或时间与IP地址对)填入IP头部
的选项部分,这样就可以测量途经路由之间数据报传输的时间。
·松散源路由选择(loose source routing),指定一个路由器IP地址列表,数据报发送过程中必须经过
其中所有的路由器。
·严格源路由选择(strict source routing),和松散源路由选择类似,不过数据报只能经过被指定的路
由器。
关于IP头部选项字段更详细的信息,请参考IP协议的标准文档RFC 791。不过这些选项字段很少被使
用,使用松散源路由选择和严格源路由选择选项的例子大概仅有traceroute程序。此外,作为记录路由IP选
项的替代品,traceroute程序使用UDP报文和ICMP报文实现了更可靠的记录路由功能,详情请参考文档RFC
1393。
2.2.2 使用tcpdump观察IPv4头部结构
为了深入理解IPv4头部中每个字段的含义,我们从测试机器ernest-laptop上执行telnet命令登录本机,并
用tcpdump抓取这个过程中telnet客户端程序和telnet服务器程序之间交换的数据包。具体的操作过程如下:
sudo tcpdump-ntx-i lo抓取本地回路上的数据包
telnet 127.0.0.1开启另一个终端执行telnet命令登录本机
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is'^]'.
Ubuntu 9.10
ernest-laptop login:ernest输入用户名并回车
Password:输入密码并回车此时观察tcpdump输出的第一个数据包,其内容如代码清单2-1所示。
代码清单2-1 用tcpdump抓取数据包
IP 127.0.0.1.41621>127.0.0.1.23:Flags[S],seq 3499745539,win 32792,options[mss 16396,sackOK,TS val 40781017 ecr 0,nop,wscale 6],length 0
0x0000:4510 003c a5da 4000 4006 96cf 7f00 0001
0x0010:7f00 0001 a295 0017 d099 e103 0000 0000
0x0020:a002 8018 fe30 0000 0204 400c 0402 080a
0x0030:026e 44d9 0000 0000 0103 0306
该数据包描述的是一个IP数据报。由于我们是使用telnet登录本机的,所以IP数据报的源端IP地址和目
的端IP地址都是“127.0.0.1”。telnet服务器程序使用的端口号是23(参见etcservices文件),而telnet客户端
程序使用临时端口号41621与服务器通信。关于临时端口号,我们将在第3章讨
论。“Flags”、“seq”、“win”和“options”描述的都是TCP头部信息,这也将在第3章讨论。“length”指出该IP数
据报所携带的应用程序数据的长度。
这次抓包我们开启了tcpdump的-x选项,使之输出数据包的二进制码。此数据包共包含60字节,其中前
20字节是IP头部,后40字节是TCP头部,不包含应用程序数据(length值为0)。现在我们分析IP头部的每个
字节,如表2-1所示。由表2-1可见,telnet服务选择使用具有最小延时的服务,并且默认使用的传输层协议是TCP协议(回顾
第1章讨论的分用)。这些都符合我们通常的理解。这个IP数据报没有被分片,因为它没有携带任何应用程
序数据。接下来我们将抓取并讨论被分片的IP数据报。
[1]此列中的空格表示我们并不关心相应字段的十进制值。2.3 IP分片
前文曾提到,当IP数据报的长度超过帧的MTU时,它将被分片传输。分片可能发生在发送端,也可能
发生在中转路由器上,而且可能在传输过程中被多次分片,但只有在最终的目标机器上,这些分片才会被
内核中的IP模块重新组装。
IP头部中的如下三个字段给IP的分片和重组提供了足够的信息:数据报标识、标志和片偏移。一个IP数
据报的每个分片都具有自己的IP头部,它们具有相同的标识值,但具有不同的片偏移。并且除了最后一个
分片外,其他分片都将设置MF标志。此外,每个分片的IP头部的总长度字段将被设置为该分片的长度。
以太网帧的MTU是1500字节(可以通过ifconfig命令或者netstat命令查看),因此它携带的IP数据报的
数据部分最多是1480字节(IP头部占用20字节)。考虑用IP数据报封装一个长度为1481字节的ICMP报文
(包括8字节的ICMP头部,所以其数据部分长度为1473字节),则该数据报在使用以太网帧传输时必须被
分片,如图2-2所示。
图 2-2 携带ICMP报文的IP数据报被分片
图2-2中,长度为1501字节的IP数据报被拆分成两个IP分片,第一个IP分片长度为1500字节,第二个IP
分片的长度为21字节。每个IP分片都包含自己的IP头部(20字节),且第一个IP分片的IP头部设置了MF标
志,而第二个IP分片的IP头部则没有设置该标志,因为它已经是最后一个分片了。原始IP数据报中的ICMP
头部内容被完整地复制到了第一个IP分片中。第二个IP分片不包含ICMP头部信息,因为IP模块重组该ICMP
报文的时候只需要一份ICMP头部信息,重复传送这个信息没有任何益处。1473字节的ICMP报文数据的前
1472字节被IP模块复制到第一个IP分片中,使其总长度为1500字节,从而满足MTU的要求;而多出的最后1
字节则被复制到第二个IP分片中。
需要指出的是,ICMP报文的头部长度取决于报文的类型,其变化范围很大。图2-2以8字节为例,因为
后面的例子用到了ping程序,而ping程序使用的ICMP回显和应答报文的头部长度是8字节。
为了看清楚IP分片的具体过程,考虑从ernest-laptop来ping机器Kongming20,每次传送1473字节的数据
(这是ICMP报文的数据部分)以强制引起IP分片,并用tcpdump抓取这一过程中双方交换的数据包。具体
操作过程如下:sudo tcpdump-ntv-i eth0 icmp只抓取ICMP报文
ping Kongming20-s 1473用-s选项指定每次发送1473字节的数据
下面我们考察tcpdump输出的一个IP数据报的两个分片,其内容如下:
1.IP(tos 0x0,ttl 64,id 61197,offset 0,flags[+],proto ICMP(1),length 1500)192.168.1.108>192.168.1.110:ICMP echo request,id
41737,seq 1,length 1480
2.IP(tos 0x0,ttl 64,id 61197,offset 1480,flags[none],proto ICMP(1),length 21)192.168.1.108>192.168.1.110:icmp
这两个IP分片的标识值都是61197,说明它们是同一个IP数据报的分片。第一个分片的片偏移值为0,而
第二个则是1480。很显然,第二个分片的片偏移值实际上也是第一个分片的ICMP报文的长度。第一个分片
设置了MF标志以表示还有后续分片,所以tcpdump输出“flags[+]”。而第二个分片则没有设置任何标志,所
以tcpdump输出“flags[none]”。这个两个分片的长度分别为1500字节和21字节,这与图2-2描述的一致。
最后,IP层传递给数据链路层的数据可能是一个完整的IP数据报,也可能是一个IP分片,它们统称为IP
分组(packet)。本书如无特殊声明,不区分IP数据报和IP分组。2.4 IP路由
IP协议的一个核心任务是数据报的路由,即决定发送数据报到目标机器的路径。为了理解IP路由过程,我们先简要分析IP模块的基本工作流程。
2.4.1 IP模块工作流程
IP模块基本工作流程如图2-3所示。
图 2-3 IP模块基本工作流程
我们从右往左来分析图2-3。当IP模块接收到来自数据链路层的IP数据报时,它首先对该数据报的头部
做CRC校验,确认无误之后就分析其头部的具体信息。
如果该IP数据报的头部设置了源站选路选项(松散源路由选择或严格源路由选择),则IP模块调用数据
报转发子模块来处理该数据报。如果该IP数据报的头部中目标IP地址是本机的某个IP地址,或者是广播地
址,即该数据报是发送给本机的,则IP模块就根据数据报头部中的协议字段来决定将它派发给哪个上层应
用(分用)。如果IP模块发现这个数据报不是发送给本机的,则也调用数据报转发子模块来处理该数据
报。数据报转发子模块将首先检测系统是否允许转发,如果不允许,IP模块就将数据报丢弃。如果允许,数据报转发子模块将对该数据报执行一些操作,然后将它交给IP数据报输出子模块。我们将在后面讨论数
据报转发的具体过程。
IP数据报应该发送至哪个下一跳路由(或者目标机器),以及经过哪个网卡来发送,就是IP路由过程,即图2-3中“计算下一跳路由”子模块。IP模块实现数据报路由的核心数据结构是路由表。这个表按照数据报
的目标IP地址分类,同一类型的IP数据报将被发往相同的下一跳路由器(或者目标机器)。我们将在后面讨
论IP路由过程。
IP输出队列中存放的是所有等待发送的IP数据报,其中除了需要转发的IP数据报外,还包括封装了本机
上层数据(ICMP报文、TCP报文段和UDP数据报)的IP数据报。
图2-3中的虚线箭头显示了路由表更新的过程。这一过程是指通过路由协议或者route命令调整路由表,使之更适应最新的网络拓扑结构,称为IP路由策略。我们将在后面简单讨论它。
2.4.2 路由机制
要研究IP路由机制,需要先了解路由表的内容。我们可以使用route命令或netstat命令查看路由表。在测
试机器ernest-laptop上执行route命令,输出内容如代码清单2-2所示。
代码清单2-2 路由表实例
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
default 192.168.1.1 0.0.0.0 UG 0 0 0 eth0
192.168.1.0255.255.255.0 U 1 0 0 eth0
该路由表包含两项,每项都包含8个字段,如表2-2所示。代码清单2-2所示的路由表中,第一项的目标地址是default,即所谓的默认路由项。该项包含一个“G”标
志,说明路由的下一跳目标是网关,其地址是192.168.1.1(这是测试网络中路由器的本地IP地址)。另外一
个路由项的目标地址是192.168.1.0,它指的是本地局域网。该路由项的网关地址为,说明数据报不需要路
由中转,可以直接发送到目标机器。
那么路由表是如何按照IP地址分类的呢?或者说给定数据报的目标IP地址,它将匹配路由表中的哪一项
呢?这就是IP的路由机制,分为3个步骤:
1)查找路由表中和数据报的目标IP地址完全匹配的主机IP地址。如果找到,就使用该路由项,没找到
则转步骤2。
2)查找路由表中和数据报的目标IP地址具有相同网路ID的网络IP地址(比如代码清单2-2所示的路由表
中的第二项)。如果找到,就使用该路由项;没找到则转步骤3。
3)选择默认路由项,这通常意味着数据报的下一跳路由是网关。
因此,对于测试机器ernest-laptop而言,所有发送到IP地址为192.168.1.的机器的IP数据报都可以直接发送到目标机器(匹配路由表第二项),而所有访问因特网的请求都将通过网关来转发(匹配默认路由
项)。
2.4.3 路由表更新
路由表必须能够更新,以反映网络连接的变化,这样IP模块才能准确、高效地转发数据报。route命令
可以修改路由表。我们看如下几个例子(在机器ernest-laptop上执行):
sudo route add-host 192.168.1.109 dev eth0
sudo route del-net 192.168.1.0 netmask 255.255.255.0
sudo route del default
sudo route add default gw 192.168.1.109 dev eth0
第1行表示添加主机192.168.1.109(机器Kongming20)对应的路由项。这样设置之后,所有从ernest-
laptop发送到Kongming20的IP数据报将通过网卡eth0直接发送至目标机器的接收网卡。第2行表示删除网络
192.168.1.0对应的路由项。这样,除了机器Kongming20外,测试机器ernest-laptop将无法访问该局域网上的
任何其他机器(能访问到Kongming20是由于执行了上一条命令)。第3行表示删除默认路由项,这样做的后
果是无法访问因特网。第4行表示重新设置默认路由项,不过这次其网关是机器Kongming20(而不是能直接
访问因特网的路由器)!经过上述修改后的路由表如下:
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
Kongming20255.255.255.255 UH 0 0 0 eth0
default Kongming20 0.0.0.0 UG 0 0 0 eth0
这个新的路由表中,第一个路由项是主机路由项,所以它被设置了“H”标志。我们设计这样一个路由表
的目的是为后文讨论ICMP重定向提供环境。
通过route命令或其他工具手工修改路由表,是静态的路由更新方式。对于大型的路由器,它们通常通
过BGP(Border Gateway Protocol,边际网关协议)、RIP(Routing Information Protocol,路由信息协议)、OSPF等协议来发现路径,并更新自己的路由表。这种更新方式是动态的、自动的。这部分内容超出了本书
的讨论范围,感兴趣的读者可阅读参考资料1。2.5 IP转发
前文提到,不是发送给本机的IP数据报将由数据报转发子模块来处理。路由器都能执行数据报的转发
操作,而主机一般只发送和接收数据报,这是因为主机上procsysnetipv4ip_forward内核参数默认被设置
为0。我们可以通过修改它来使能主机的数据报转发功能(在测试机器Kongming20上以root身份执行):
echo 1>procsysnetipv4ip_forward
对于允许IP数据报转发的系统(主机或路由器),数据报转发子模块将对期望转发的数据报执行如下
操作:
1)检查数据报头部的TTL值。如果TTL值已经是0,则丢弃该数据报。
2)查看数据报头部的严格源路由选择选项。如果该选项被设置,则检测数据报的目标IP地址是否是本
机的某个IP地址。如果不是,则发送一个ICMP源站选路失败报文给发送端。
3)如果有必要,则给源端发送一个ICMP重定向报文,以告诉它一个更合理的下一跳路由器。
4)将TTL值减1。
5)处理IP头部选项。
6)如果有必要,则执行IP分片操作。2.6 重定向
图2-3显示了ICMP重定向报文也能用于更新路由表,因此本节我们简要讨论ICMP重定向。
2.6.1 ICMP重定向报文
ICMP重定向报文格式如图2-4所示。
图 2-4 ICMP重定向报文格式
我们在1.1节讨论过ICMP报文头部的3个固定字段:8位类型、8位代码和16位校验和。ICMP重定向报文
的类型值是5,代码字段有4个可选值,用来区分不同的重定向类型。本书仅讨论主机重定向,其代码值为
1。
ICMP重定向报文的数据部分含义很明确,它给接收方提供了如下两个信息:
·引起重定向的IP数据报(即图2-4中的原始IP数据报)的源端IP地址。
·应该使用的路由器的IP地址。
接收主机根据这两个信息就可以断定引起重定向的IP数据报应该使用哪个路由器来转发,并且以此来
更新路由表(通常是更新路由表缓冲,而不是直接更改路由表)。
procsysnetipv4confallsend_redirects内核参数指定是否允许发送ICMP重定向报文,而procsysnetipv4confallaccept_redirects内核参数则指定是否允许接收ICMP重定向报文。一般来说,主机
只能接收ICMP重定向报文,而路由器只能发送ICMP重定向报文。
2.6.2 主机重定向实例
2.4.3节中,我们把机器ernest-laptop的网关设置成了机器Kongming20,2.5节中我们又使能了
Kongming20的数据报转发功能,因此机器ernest-laptop将通过Kongming20来访问因特网,比如在ernest-
laptop上执行如下ping命令:
ping www.baidu.com
PING www.a.shifen.com(119.75.217.56)56(84)bytes of data.
From Kongming20(192.168.1.109):icmp_seq=1 Redirect Host(New nexthop:192.168.1.1)
64 bytes from 119.75.217.56:icmp_seq=1 ttl=54 time=6.78 ms---www.a.shifen.com ping statistics---
1 packets transmitted,1 received,0%packet loss,time 0msrtt minavgmaxmdev=6.7896.7896.7890.000 ms
从ping命令的输出来看,Kongming20给ernest-laptop发送了一个ICMP重定向报文,告诉它请通过
192.168.1.1来访问目标机器,因为这对ernest-laptop来说是更合理的路由方式。当主机ernest-laptop收到这样
的ICMP重定向报文后,它将更新其路由表缓冲(使用命令route-Cn查看),并使用新的路由方式来发送后
续数据报。上面讨论的重定向过程可用图2-5来总结。
图 2-5 主机重定向过程2.7 IPv6头部结构
IPv6协议是网络层技术发展的必然趋势。它不仅解决了IPv4地址不够用的问题,还做了很大的改进。比
如,增加了多播和流的功能,为网络上多媒体内容的质量提供精细的控制;引入自动配置功能,使得局域
网管理更方便;增加了专门的网络安全功能等。本节简要地讨论IPv6头部结构,它的更多细节请参考其标
准文档RFC 2460。
2.7.1 IPv6固定头部结构
IPv6头部由40字节的固定头部和可变长的扩展头部组成。图2-6所示是IPv6的固定头部结构。
图 2-6 IPv6固定头部结构
4位版本号(version)指定IP协议的版本。对IPv6来说,其值是6。
8位通信类型(traffic class)指示数据流通信类型或优先级,和IPv4中的TOS类似。
20位流标签(flow label)是IPv6新增加的字段,用于某些对连接的服务质量有特殊要求的通信,比如
音频或视频等实时数据传输。
16位净荷长度(payload length)指的是IPv6扩展头部和应用程序数据长度之和,不包括固定头部长
度。
8位下一个包头(next header)指出紧跟IPv6固定头部后的包头类型,如扩展头(如果有的话)或某个
上层协议头(比如TCP,UDP或ICMP)。它类似于IPv4头部中的协议字段,且相同的取值有相同的含义。
8位跳数限制(hop limit)和IPv4中的TTL含义相同。IPv6用128位(16字节)来表示IP地址,使得IP地址的总量达到了2128个。所以有人说,“IPv6使得地球
上的每粒沙子都有一个IP地址”。
32位表示的IPv4地址一般用点分十进制来表示,而IPv6地址则用十六进制字符串表示,比
如“FE80:0000:0000:0000:1234:5678:0000:0012”。可见,IPv6地址用“:”分割成8组,每组包含2字节。但这种
表示方法过于麻烦,通常可以使用所谓的零压缩法来将其简写,也就是省略连续的、全零的组。比如,上
面的例子使用零压缩法可表示为“FE80::1234:5678:0000:0012”。不过零压缩法对一个IPv6地址只能使用一
次,比如上面的例子中,字节组“5678”后面的全零组就不能再省略,否则我们就无法计算每个“::”之间省略
了多少个全零组。
2.7.2 IPv6扩展头部
可变长的扩展头部使得IPv6能支持更多的选项,并且很便于将来的扩展需要。它的长度可以是0,表示
数据报没使用任何扩展头部。一个数据报可以包含多个扩展头部,每个扩展头部的类型由前一个头部(固
定头部或扩展头部)中的下一个报头字段指定。目前可以使用的扩展头部如表2-3所示。
注意 IPv6协议并不是IPv4协议的简单扩展,而是完全独立的协议。用以太网帧封装的IPv6数据报和
IPv4数据报具有不同的类型值。第1章提到,IPv4数据报的以太网帧封装类型值是0x800,而IPv6数据报的以
太网帧封装类型值是0x86dd(见RFC 2464)。第3章 TCP协议详解
TCP协议是TCPIP协议族中另一个重要的协议。和IP协议相比,TCP协议更靠近应用层,因此在应用程
序中具有更强的可操作性。一些重要的socket选项都和TCP协议相关。
本章从如下四方面来讨论TCP协议:
·TCP头部信息。TCP头部信息出现在每个TCP报文段中,用于指定通信的源端端口号、目的端端口
号,管理TCP连接,控制两个方向的数据流。
·TCP状态转移过程。TCP连接的任意一端都是一个状态机。在TCP连接从建立到断开的整个过程中,连接两端的状态机将经历不同的状态变迁。理解TCP状态转移对于调试网络应用程序将有很大的帮助。
·TCP数据流。通过分析TCP数据流,我们就可以从网络应用程序外部来了解应用层协议和通信双方交
换的应用程序数据。这一部分将讨论两种类型的TCP数据流:交互数据流和成块数据流。TCP数据流中有一
种特殊的数据,称为紧急数据,我们也将简单讨论之。
·TCP数据流的控制。为了保证可靠传输和提高网络通信质量,内核需要对TCP数据流进行控制。这一
部分讨论TCP数据流控制的两个方面:超时重传和拥塞控制。
不过在详细讨论TCP协议之前,我们先简单介绍一下TCP服务的特点,以及它和UDP服务的区别。
3.1 TCP服务的特点
传输层协议主要有两个:TCP协议和UDP协议。TCP协议相对于UDP协议的特点是:面向连接、字节流
和可靠传输。
使用TCP协议通信的双方必须先建立连接,然后才能开始数据的读写。双方都必须为该连接分配必要
的内核资源,以管理连接的状态和连接上数据的传输。TCP连接是全双工的,即双方的数据读写可以通过
一个连接进行。完成数据交换之后,通信双方都必须断开连接以释放系统资源。
TCP协议的这种连接是一对一的,所以基于广播和多播(目标是多个主机地址)的应用程序不能使用
TCP服务。而无连接协议UDP则非常适合于广播和多播。
我们在1.1节中简单介绍过字节流服务和数据报服务的区别。这种区别对应到实际编程中,则体现为通
信双方是否必须执行相同次数的读、写操作(当然,这只是表现形式)。当发送端应用程序连续执行多次
写操作时,TCP模块先将这些数据放入TCP发送缓冲区中。当TCP模块真正开始发送数据时,发送缓冲区中
这些等待发送的数据可能被封装成一个或多个TCP报文段发出。因此,TCP模块发送出的TCP报文段的个数
和应用程序执行的写操作次数之间没有固定的数量关系。
当接收端收到一个或多个TCP报文段后,TCP模块将它们携带的应用程序数据按照TCP报文段的序号
(见后文)依次放入TCP接收缓冲区中,并通知应用程序读取数据。接收端应用程序可以一次性将TCP接收
缓冲区中的数据全部读出,也可以分多次读取,这取决于用户指定的应用程序读缓冲区的大小。因此,应
用程序执行的读操作次数和TCP模块接收到的TCP报文段个数之间也没有固定的数量关系。
综上所述,发送端执行的写操作次数和接收端执行的读操作次数之间没有任何数量关系,这就是字节
流的概念:应用程序对数据的发送和接收是没有边界限制的。UDP则不然。发送端应用程序每执行一次写
操作,UDP模块就将其封装成一个UDP数据报并发送之。接收端必须及时针对每一个UDP数据报执行读操
作(通过recvfrom系统调用),否则就会丢包(这经常发生在较慢的服务器上)。并且,如果用户没有指定
足够的应用程序缓冲区来读取UDP数据,则UDP数据将被截断。
图3-1和图3-2显示了TCP字节流服务和UDP数据报服务的上述区别。两图中省略了传输层以下的通信细
节。图 3-1 TCP字节流服务
图 3-2 UDP数据报服务
TCP传输是可靠的。首先,TCP协议采用发送应答机制,即发送端发送的每个TCP报文段都必须得到接
收方的应答,才认为这个TCP报文段传输成功。其次,TCP协议采用超时重传机制,发送端在发送出一个
TCP报文段之后启动定时器,如果在定时时间内未收到应答,它将重发该报文段。最后,因为TCP报文段最
终是以IP数据报发送的,而IP数据报到达接收端可能乱序、重复,所以TCP协议还会对接收到的TCP报文段
重排、整理,再交付给应用层。
UDP协议则和IP协议一样,提供不可靠服务。它们都需要上层协议来处理数据确认和超时重传。3.2 TCP头部结构
TCP头部信息出现在每个TCP报文段中,用于指定通信的源端端口,目的端端口,管理TCP连接等,本
节详细介绍TCP的头部结构,包括固定头部结构和头部选项。
3.2.1 TCP固定头部结构
TCP头部结构如图3-3所示,其中的诸多字段为管理TCP连接和控制数据流提供了足够的信息。
图 3-3 TCP头部结构
16位端口号(port number):告知主机该报文段是来自哪里(源端口)以及传给哪个上层协议或应用
程序(目的端口)的。进行TCP通信时,客户端通常使用系统自动选择的临时端口号,而服务器则使用知
名服务端口号。1.3节中提到过,所有知名服务使用的端口号都定义在etcservices文件中。
32位序号(sequence number):一次TCP通信(从TCP连接建立到断开)过程中某一个传输方向上的字
节流的每个字节的编号。假设主机A和主机B进行TCP通信,A发送给B的第一个TCP报文段中,序号值被系
统初始化为某个随机值ISN(Initial Sequence Number,初始序号值)。那么在该传输方向上(从A到B),后续的TCP报文段中序号值将被系统设置成ISN加上该报文段所携带数据的第一个字节在整个字节流中的偏
移。例如,某个TCP报文段传送的数据是字节流中的第1025~2048字节,那么该报文段的序号值就是
ISN+1025。另外一个传输方向(从B到A)的TCP报文段的序号值也具有相同的含义。
32位确认号(acknowledgement number):用作对另一方发送来的TCP报文段的响应。其值是收到的
TCP报文段的序号值加1。假设主机A和主机B进行TCP通信,那么A发送出的TCP报文段不仅携带自己的序
号,而且包含对B发送来的TCP报文段的确认号。反之,B发送出的TCP报文段也同时携带自己的序号和对A
发送来的报文段的确认号。4位头部长度(header length):标识该TCP头部有多少个32bit字(4字节)。因为4位最大能表示15,所
以TCP头部最长是60字节。
6位标志位包含如下几项:
·URG标志,表示紧急指针(urgent pointer)是否有效。
·ACK标志,表示确认号是否有效。我们称携带ACK标志的TCP报文段为确认报文段。
·PSH标志,提示接收端应用程序应该立即从TCP接收缓冲区中读走数据,为接收后续数据腾出空间
(如果应用程序不将接收到的数据读走,它们就会一直停留在TCP接收缓冲区中)。
·RST标志,表示要求对方重新建立连接。我们称携带RST标志的TCP报文段为复位报文段。
·SYN标志,表示请求建立一个连接。我们称携带SYN标志的TCP报文段为同步报文段。
·FIN标志,表示通知对方本端要关闭连接了。我们称携带FIN标志的TCP报文段为结束报文段。
16位窗口大小(window size):是TCP流量控制的一个手段。这里说的窗口,指的是接收通告窗口
(Receiver Window,RWND)。它告诉对方本端的TCP接收缓冲区还能容纳多少字节的数据,这样对方就
可以控制发送数据的速度。
16位校验和(TCP checksum):由发送端填充,接收端对TCP报文段执行CRC算法以检验TCP报文段在
传输过程中是否损坏。注意,这个校验不仅包括TCP头部,也包括数据部分。这也是TCP可靠传输的一个重
要保障。
16位紧急指针(urgent pointer):是一个正的偏移量。它和序号字段的值相加表示最后一个紧急数据的
下一字节的序号。因此,确切地说,这个字段是紧急指针相对当前序号的偏移,不妨称之为紧急偏移。
TCP的紧急指针是发送端向接收端发送紧急数据的方法。我们将在后面讨论TCP紧急数据。
3.2.2 TCP头部选项
TCP头部的最后一个选项字段(options)是可变长的可选信息。这部分最多包含40字节,因为TCP头部
最长是60字节(其中还包含前面讨论的20字节的固定部分)。典型的TCP头部选项结构如图3-4所示。
图 3-4 TCP头部选项的一般结构
选项的第一个字段kind说明选项的类型。有的TCP选项没有后面两个字段,仅包含1字节的kind字段。
第二个字段length(如果有的话)指定该选项的总长度,该长度包括kind字段和length字段占据的2字节。第
三个字段info(如果有的话)是选项的具体信息。常见的TCP选项有7种,如图3-5所示。图 3-5 7种TCP选项
kind=0是选项表结束选项。
kind=1是空操作(nop)选项,没有特殊含义,一般用于将TCP选项的总长度填充为4字节的整数倍。
kind=2是最大报文段长度选项。TCP连接初始化时,通信双方使用该选项来协商最大报文段长度(Max
Segment Size,MSS)。TCP模块通常将MSS设置为(MTU-40)字节(减掉的这40字节包括20字节的TCP头
部和20字节的IP头部)。这样携带TCP报文段的IP数据报的长度就不会超过MTU(假设TCP头部和IP头部都
不包含选项字段,并且这也是一般情况),从而避免本机发生IP分片。对以太网而言,MSS值是
1460(1500-40)字节。
kind=3是窗口扩大因子选项。TCP连接初始化时,通信双方使用该选项来协商接收通告窗口的扩大因
子。在TCP的头部中,接收通告窗口大小是用16位表示的,故最大为65 535字节,但实际上TCP模块允许的
接收通告窗口大小远不止这个数(为了提高TCP通信的吞吐量)。窗口扩大因子解决了这个问题。假设TCP
头部中的接收通告窗口大小是N,窗口扩大因子(移位数)是M,那么TCP报文段的实际接收通告窗口大小
是N乘2M,或者说N左移M位。注意,M的取值范围是0~14。我们可以通过修
改procsysnetipv4tcp_window_scaling内核变量来启用或关闭窗口扩大因子选项。
和MSS选项一样,窗口扩大因子选项只能出现在同步报文段中,否则将被忽略。但同步报文段本身不
执行窗口扩大操作,即同步报文段头部的接收通告窗口大小就是该TCP报文段的实际接收通告窗口大小。
当连接建立好之后,每个数据传输方向的窗口扩大因子就固定不变了。关于窗口扩大因子选项的细节,可
参考标准文档RFC 1323。kind=4是选择性确认(Selective Acknowledgment,SACK)选项。TCP通信时,如果某个TCP报文段丢
失,则TCP模块会重传最后被确认的TCP报文段后续的所有报文段,这样原先已经正确传输的TCP报文段也
可能重复发送,从而降低了TCP性能。SACK技术正是为改善这种情况而产生的,它使TCP模块只重新发送
丢失的TCP报文段,不用发送所有未被确认的TCP报文段。选择性确认选项用在连接初始化时,表示是否支
持SACK技术。我们可以通过修改procsysnetipv4tcp_sack内核变量来启用或关闭选择性确认选项。
kind=5是SACK实际工作的选项。该选项的参数告诉发送方本端已经收到并缓存的不连续的数据块,从
而让发送端可以据此检查并重发丢失的数据块。每个块边沿(edge of block)参数包含一个4字节的序号。
其中块左边沿表示不连续块的第一个数据的序号,而块右边沿则表示不连续块的最后一个数据的序号的下
一个序号。这样一对参数(块左边沿和块右边沿)之间的数据是没有收到的。因为一个块信息占用8字节,所以TCP头部选项中实际上最多可以包含4个这样的不连续数据块(考虑选项类型和长度占用的2字节)。
kind=8是时间戳选项。该选项提供了较为准确的计算通信双方之间的回路时间(Round Trip Time,RTT)的方法,从而为TCP流量控制提供重要信息。我们可以通过修改procsysnetipv4tcp_timestamps内核
变量来启用或关闭时间戳选项。
3.2.3 使用tcpdump观察TCP头部信息
在2.3节中,我们利用tcpdump抓取了一个数据包并分析了其中的IP头部信息,本节分析其中与TCP协议
相关的部分(后面的分析中,我们将所有tcpdump抓取到的数据包都称为TCP报文段,因为TCP报文段既是
数据包的主要内容,也是我们主要讨论的对象)。为了方便阅读,先将该TCP报文段的内容复制于代码清
单3-1中。
代码清单3-1 用tcpdump抓取数据包
IP 127.0.0.1.41621>127.0.0.1.23:Flags[S],seq 3499745539,win 32792,options[mss 16396,sackOK,TS val 40781017 ecr 0,nop,wscale
6],length 0
0x0000:4510 003c a5da 4000 4006 96cf 7f00 0001
0x0010:7f00 0001 a295 0017 d099 e103 0000 0000
0x0020:a002 8018 fe30 0000 0204 400c 0402 080a
0x0030:026e 44d9 0000 0000 0103 0306
tcpdump输出Flags[S],表示该TCP报文段包含SYN标志,因此它是一个同步报文段。如果TCP报文段包
含其他标志,则tcpdump也会将该标志的首字母显示在“Flags”后的方括号中。
seq是序号值。因为该同步报文段是从127.0.0.1.41621(客户端IP地址和端口号)到127.0.0.1.23(服务器
IP地址和端口号)这个传输方向上的第一个TCP报文段,所以这个序号值也就是此次通信过程中该传输方向
的ISN值。并且,因为这是整个通信过程中的第一个TCP报文段,所以它没有针对对方发送来的TCP报文段
的确认值(尚未收到任何对方发送来的TCP报文段)。
win是接收通告窗口的大小。因为这是一个同步报文段,所以win值反映的是实际的接收通告窗口大
小。
options是TCP选项,其具体内容列在方括号中。mss是发送端(客户端)通告的最大报文段长度。通过
ifconfig命令查看回路接口的MTU为16436字节,因此可以预想到TCP报文段的MSS为16396(16436-40)字
节。sackOK表示发送端支持并同意使用SACK选项。TS val是发送端的时间戳。ecr是时间戳回显应答。因为
这是一次TCP通信的第一个TCP报文段,所以它针对对方的时间戳的应答为0(尚未收到对方的时间戳)。
紧接着的nop是一个空操作选项。wscale指出发送端使用的窗口扩大因子为6。
接下来我们分析tcpdump输出的字节码中TCP头部对应的信息,它从第21字节开始,如表3-1所示。从表3-1中可见,TCP报文段头部的二进制码和tcpdump输出的TCP报文段描述信息完全对应。在后面的
tcpdump输出中,我们将省略大部分TCP头部信息,仅显示序号、确认号、窗口大小以及标志位等与主题相
关的字段。
[1]此列中的空格表示我们并不关心相应字段的十进制值。3.3 TCP连接的建立和关闭
本节我们讨论建立和关闭TCP连接的过程。
3.3.1 使用tcpdump观察TCP连接的建立和关闭
首先从ernest-laptop上执行telnet命令登录Kongming20的80端口,然后抓取这一过程中客户端和服务器交
换的TCP报文段。具体操作过程如下:
sudo tcpdump-i eth0-nt'(src 192.168.1.109 and dst 192.168.1.108)or(src 192.168.1.108 and dst 192.168.1.109)'
telnet 192.168.1.109 80
Trying 192.168.1.109...
Connected to 192.168.1.109.
Escape character is'^]'.
^](回车)输入ctrl+]并回车
telnet>quit(回车)
Connection closed.
当执行telnet命令并在两台通信主机之间建立TCP连接后(telnet输出“Connected to 192.168.1.109”),输
入Ctrl+]以调出telnet程序的命令提示符,然后在telnet命令提示符后输入quit以退出telnet客户端程序,从而结
束TCP连接。整个过程中(从连接建立到结束),tcpdump输出的内容如代码清单3-2所示。
代码清单3-2 建立和关闭TCP连接的过程
1.IP 192.168.1.108.60871>192.168.1.109.80:Flags[S],seq 535734930,win 5840,length 0
2.IP 192.168.1.109.80>192.168.1.108.60871:Flags[S.],seq 2159701207,ack 535734931,win 5792,length 0
3.IP 192.168.1.108.60871>192.168.1.109.80:Flags[.],ack 1,win 92,length 0
4.IP 192.168.1.108.60871>192.168.1.109.80:Flags[F.],seq 1,ack 1,win 92,length 0
5.IP 192.168.1.109.80>192.168.1.108.60871:Flags[.],ack 2,win 91,length 0
6.IP 192.168.1.109.80>192.168.1.108.60871:Flags[F.],seq 1,ack 2,win 91,length 0
7.IP 192.168.1.108.60871>192.168.1.109.80:Flags[.],ack 2,win 92,length 0
因为整个过程并没有发生应用层数据的交换,所以TCP报文段的数据部分的长度(length)总是0。为
了更清楚地表示建立和关闭TCP连接的整个过程,我们将tcpdump输出的内容绘制成图3-6所示的时序图。图 3-6 TCP连接的建立和关闭时序图
第1个TCP报文段包含SYN标志,因此它是一个同步报文段,即ernest-laptop(客户端)向
Kongming20(服务器)发起连接请求。同时,该同步报文段包含一个ISN值为535734930的序号。第2个TCP
报文段也是同步报文段,表示Kongming20同意与ernest-laptop建立连接。同时它发送自己的ISN值为
2159701207的序号,并对第1个同步报文段进行确认。确认值是535734931,即第1个同步报文段的序号值加
1。前文说过,序号值是用来标识TCP数据流中的每一字节的。但同步报文段比较特殊,即使它并没有携带
任何应用程序数据,它也要占用一个序号值。第3个TCP报文段是ernest-laptop对第2个同步报文段的确认。
至此,TCP连接就建立起来了。建立TCP连接的这3个步骤被称为TCP三次握手。
从第3个TCP报文段开始,tcpdump输出的序号值和确认值都是相对初始ISN值的偏移。当然,我们可以
开启tcpdump的-S选项来选择打印序号的绝对值。
后面4个TCP报文段是关闭连接的过程。第4个TCP报文段包含FIN标志,因此它是一个结束报文段,即
ernest-laptop要求关闭连接。结束报文段和同步报文段一样,也要占用一个序号值。Kongming20用TCP报文
段5来确认该结束报文段。紧接着Kongming20发送自己的结束报文段6,ernest-laptop则用TCP报文段7给予
确认。实际上,仅用于确认目的的确认报文段5是可以省略的,因为结束报文段6也携带了该确认信息。确
认报文段5是否出现在连接断开的过程中,取决于TCP的延迟确认特性。延迟确认将在后面讨论。在连接的关闭过程中,因为ernest-laptop先发送结束报文段(telnet客户端程序主动退出),故称ernest-
laptop执行主动关闭,而称Kongming20执行被动关闭。
一般而言,TCP连接是由客户端发起,并通过三次握手建立(特殊情况是所谓同时打开[1])的。TCP连
接的关闭过程相对复杂一些。可能是客户端执行主动关闭,比如前面的例子;也可能是服务器执行主动关
闭,比如服务器程序被中断而强制关闭连接;还可能是同时关闭(和同时打开一样,非常少见)。
3.3.2 半关闭状态
TCP连接是全双工的,所以它允许两个方向的数据传输被独立关闭。换言之,通信的一端可以发送结
束报文段给对方,告诉它本端已经完成了数据的发送,但允许继续接收来自对方的数据,直到对方也发送
结束报文段以关闭连接。TCP连接的这种状态称为半关闭(half close)状态,如图3-7所示。
图 3-7 半关闭状态
请注意,在图3-7中,服务器和客户端应用程序判断对方是否已经关闭连接的方法是:read系统调用返
回0(收到结束报文段)。当然,Linux还提供其他检测连接是否被对方关闭的方法,这将在后续章节讨
论。
socket网络编程接口通过shutdown函数提供了对半关闭的支持,我们将在后续章节讨论它。这里强调一下,虽然我们介绍了半关闭状态,但是使用半关闭的应用程序很少见。
3.3.3 连接超时
前面我们讨论的是很快建立连接的情况。如果客户端访问一个距离它很远的服务器,或者由于网络繁
忙,导致服务器对于客户端发送出的同步报文段没有应答,此时客户端程序将产生什么样的行为呢?显
然,对于提供可靠服务的TCP来说,它必然是先进行重连(可能执行多次),如果重连仍然无效,则通知
应用程序连接超时。
为了观察连接超时,我们模拟一个繁忙的服务器环境,在ernest-laptop上执行下面的操作:
sudo iptables-F
sudo iptables-I INPUT-p tcp--syn-i eth0-j DROP
iptable命令用于过滤数据包,这里我们利用它来丢弃所有接收到的连接请求(丢弃所有同步报文段,这
样客户端就无法得到任何确认报文段)。
接下来从Kongming20上执行telnet命令登录到ernest-laptop,并用tcpdump抓取这个过程中双方交换的
TCP报文段。具体操作如下:
sudo tcpdump-n-i eth0 port 23仅抓取telnet客户端和服务器交换的数据包
date;telnet 192.168.1.108;date在telnet命令前后都执行date命令,以计算超时时间
Mon Jun 11 21:23:35 CST 2012
Trying 192.168.1.108...
telnet:connect to address 192.168.1.108:Connection timed out
Mon Jun 11 21:24:38 CST 2012
从两次date命令的输出来看,Kongming20建立TCP连接的超时时间是63s。本次tcpdump的输出如代码清
单3-3所示。
代码清单3-3 TCP超时重连
1.21:23:35.612136 IP 192.168.1.109.39385>192.168.1.108.telnet:Flags[S],seq 1355982096,length 0
2.21:23:36.613146 IP 192.168.1.109.39385>192.168.1.108.telnet:Flags[S],seq 1355982096,length 0
3.21:23:38.617279 IP 192.168.1.109.39385>192.168.1.108.telnet:Flags[S],seq 1355982096,length 0
4.21:23:42.625140 IP 192.168.1.109.39385>192.168.1.108.telnet:Flags[S],seq 1355982096,length 0
5.21:23:50.641344 IP 192.168.1.109.39385>192.168.1.108.telnet:Flags[S],seq 1355982096,length 0
6.21:24:06.673331 IP 192.168.1.109.39385>192.168.1.108.telnet:Flags[S],seq 1355982096,length 0
这次抓包我们保留了tcpdump输出的时间戳(不使用其-t选项),以便推理Linux的超时重连策略。
我们一共抓取到6个TCP报文段,它们都是同步报文段,并且具有相同的序号值,这说明后面5个同步
报文段都是超时重连报文段。观察这些TCP报文段被发送的时间间隔,它们分别为1s、2s、4s、8s和16s(由
于定时器精度的问题,这些时间间隔都有一定偏差),可以推断最后一个TCP报文段的超时时间是
32s(63s-16s-8s-4s-2s-1s)。因此,TCP模块一共执行了5次重连操作,这是
由procsysnetipv4tcp_syn_retries内核变量所定义的。每次重连的超时时间都增加一倍。在5次重连均失败
的情况下,TCP模块放弃连接并通知应用程序。
在应用程序中,我们可以修改连接超时时间,具体方法将在本书后续章节中进行介绍。3.4 TCP状态转移
TCP连接的任意一端在任一时刻都处于某种状态,当前状态可以通过netstat命令(见第17章)查看。本
节我们要讨论的是TCP连接从建立到关闭的整个过程中通信两端状态的变化。图3-8是完整的状态转移图,它描绘了所有的TCP状态以及可能的状态转换。图 3-8 TCP状态转移过程
图3-8中的粗虚线表示典型的服务器端连接的状态转移;粗实线表示典型的客户端连接的状态转移。
CLOSED是一个假想的起始点,并不是一个实际的状态。3.4.1 TCP状态转移总图
我们先讨论服务器的典型状态转移过程,此时我们说的连接状态都是指该连接的服务器端的状态。
服务器通过listen系统调用(见第5章)进入LISTEN状态,被动等待客户端连接,因此执行的是所谓的
被动打开。服务器一旦监听到某个连接请求(收到同步报文段),就将该连接放入内核等待队列中,并向
客户端发送带SYN标志的确认报文段。此时该连接处于SYN_RCVD状态。如果服务器成功地接收到客户端
发送回的确认报文段,则该连接转移到ESTABLISHED状态。ESTABLISHED状态是连接双方能够进行双向
数据传输的状态。
当客户端主动关闭连接时(通过close或shutdown系统调用向服务器发送结束报文段),服务器通过返
回确认报文段使连接进入CLOSE_WAIT状态。这个状态的含义很明确:等待服务器应用程序关闭连接。通
常,服务器检测到客户端关闭连接后,也会立即给客户端发送一个结束报文段来关闭连接。这将使连接转
移到LAST_ACK状态,以等待客户端对结束报文段的最后一次确认。一旦确认完成,连接就彻底关闭了。
下面讨论客户端的典型状态转移过程,此时我们说的连接状态都是指该连接的客户端的状态。
客户端通过connect系统调用(见第5章)主动与服务器建立连接。connect系统调用首先给服务器发送一
个同步报文段,使连接转移到SYN_SENT状态。此后,connect系统调用可能因为如下两个原因失败返回:
·如果connect连接的目标端口不存在(未被任何进程监听),或者该端口仍被处于TIME_WAIT状态的
连接所占用(见后文),则服务器将给客户端发送一个复位报文段,connect调用失败。
·如果目标端口存在,但connect在超时时间内未收到服务器的确认报文段,则connect调用失败。
connect调用失败将使连接立即返回到初始的CLOSED状态。如果客户端成功收到服务器的同步报文段
和确认,则connect调用成功返回,连接转移至ESTABLISHED状态。
当客户端执行主动关闭时,它将向服务器发送一个结束报文段,同时连接进入FIN_WAIT_1状态。若此
时客户端收到服务器专门用于确认目的的确认报文段(比如图3-6中的TCP报文段5),则连接转移至
FIN_WAIT_2状态。当客户端处于FIN_WAIT_2状态时,服务器处于CLOSE_WAIT状态,这一对状态是可能
发生半关闭的状态。此时如果服务器也关闭连接(发送结束报文段),则客户端将给予确认并进入
TIME_WAIT状态。关于TIME_WAIT状态的含义,我们将在下一节讨论。
图3-8还给出了客户端从FIN_WAIT_1状态直接进入TIME_WAIT状态的一条线路(不经过FIN_WAIT_2
状态),前提是处于FIN_WAIT_1状态的服务器直接收到带确认信息的结束报文段(而不是先收到确认报文
段,再收到结束报文段)。这种情况对应于图3-6中的服务器不发送TCP报文段5。
前面说过,处于FIN_WAIT_2状态的客户端需要等待服务器发送结束报文段,才能转移至TIME_WAIT
状态,否则它将一直停留在这个状态。如果不是为了在半关闭状态下继续接收数据,连接长时间地停留在
FIN_WAIT_2状态并无益处。连接停留在FIN_WAIT_2状态的情况可能发生在:客户端执行半关闭后,未等
服务器关闭连接就强行退出了。此时客户端连接由内核来接管,可称之为孤儿连接(和孤儿进程类似)。
Linux为了防止孤儿连接长时间存留在内核中,定义了两个内核变量:procsysnetipv4tcp_max_orphans
和procsysnetipv4tcp_fin_timeout。前者指定内核能接管的孤儿连接数目,后者指定孤儿连接在内核中生
存的时间。
至此,我们简单地讨论了服务器和客户端程序的典型TCP状态转移路线。对应于图3-6所示的TCP连接
的建立与断开过程,客户端和服务器的状态转移如图3-9所示。图 3-9 TCP连接的建立和断开过程中客户端和服务器的状态变化
图3-8还描绘了其他非典型的TCP状态转移路线,比如同时关闭与同时打开,本书不予讨论。
3.4.2 TIME_WAIT状态
从图3-9来看,客户端连接在收到服务器的结束报文段(TCP报文段6)之后,并没有直接进入CLOSED
状态[1]
,而是转移到TIME_WAIT状态。在这个状态,客户端连接要等待一段长为2MSL(Maximum
Segment Life,报文段最大生存时间)的时间,才能完全关闭。MSL是TCP报文段在网络中的最大生存时
间,标准文档RFC 1122的建议值是2 min。
TIME_WAIT状态存在的原因有两点:
·可靠地终止TCP连接。
·保证让迟来的TCP报文段有足够的时间被识别并丢弃。
第一个原因很好理解。假设图3-9中用于确认服务器结束报文段6的TCP报文段7丢失,那么服务器将重
发结束报文段。因此客户端需要停留在某个状态以处理重复收到的结束报文段(即向服务器发送确认报文
段)。否则,客户端将以复位报文段来回应服务器,服务器则认为这是一个错误,因为它期望的是一个像
TCP报文段7那样的确认报文段。在Linux系统上,一个TCP端口不能被同时打开多次(两次及以上)。当一个TCP连接处于TIME_WAIT
状态时,我们将无法立即使用该连接占用着的端口来建立一个新连接。反过来思考,如果不存在
TIME_WAIT状态,则应用程序能够立即建立一个和刚关闭的连接相似的连接(这里说的相似,是指它们具
有相同的IP地址和端口号)。这个新的、和原来相似的连接被称为原来的连接的化身(incarnation)。新的
化身可能接收到属于原来的连接的、携带应用程序数据的TCP报文段(迟到的报文段),这显然是不应该
发生的。这就是TIME_WAIT状态存在的第二个原因。
另外,因为TCP报文段的最大生存时间是MSL,所以坚持2MSL时间的TIME_WAIT状态能够确保网络
上两个传输方向上尚未被接收到的、迟到的TCP报文段都已经消失(被中转路由器丢弃)。因此,一个连
接的新的化身可以在2MSL时间之后安全地建立,而绝对不会接收到属于原来连接的应用程序数据,这就是
TIME_WAIT状态要持续2MSL时间的原因。
有时候我们希望避免TIME_WAIT状态,因为当程序退出后,我们希望能够立即重启它。但由于处在
TIME_WAIT状态的连接还占用着端口,程序将无法启动(直到2MSL超时时间结束)。考虑一个例子:在
测试机器ernest-laptop上以客户端方式运行nc(用于创建网络连接的工具,见第17章)命令,登录本机的
Web服务,且明确指定客户端使用12345端口与服务器通信。然后从终端输入Ctrl+C终止客户端程序,接着
又立即重启nc程序,以完全相同的方式再次连接本机的Web服务。具体操作如下:
nc-p 12345 192.168.1.108 80
ctrl+C中断客户端程序
nc-p 12345 192.168.1.108 80重启客户端程序,重新建立连接
nc:bind failed:Address already in use输出显示连接失败,因为12345端口仍被占用
netstat-nat用netstat命令查看连接状态
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 192.168.1.108:12345 192.168.1.108:80 TIME_WAIT
这里我们使用netstat命令查看连接的状态。其输出显示,客户端程序被中断后,连接进入TIME_WAIT
状态,12345端口仍被占用,所以客户端重启失败。
对客户端程序来说,我们通常不用担心上面描述的重启问题。因为客户端一般使用系统自动分配的临
时端口号来建立连接,而由于随机性,临时端口号一般和程序上一次使用的端口号(还处于TIME_WAIT状
态的那个连接使用的端口号)不同,所以客户端程序一般可以立即重启。上面的例子仅仅是为了说明问
题,我们强制客户端使用12345端口,这才导致立即重启客户端程序失败。
但如果是服务器主动关闭连接后异常终止,则因为它总是使用同一个知名服务端口号,所以连接的
TIME_WAIT状态将导致它不能立即重启。不过,我们可以通过socket选项SO_REUSEADDR来强制进程立
即使用处于TIME_WAIT状态的连接占用的端口,这将在第5章讨论。
[1]请读者根据语境判断连接的状态是指客户端状态还是服务器状态,后同。3.5 复位报文段
在某些特殊条件下,TCP连接的一端会向另一端发送携带RST标志的报文段,即复位报文段,以通知对
方关闭连接或重新建立连接。本节讨论产生复位报文段的3种情况。
3.5.1 访问不存在的端口
3.4.1小节提到,当客户端程序访问一个不存在的端口时,目标主机将给它发送一个复位报文段。考虑
从Kongming20上执行telnet命令登录ernest-laptop上一个不存在的54321端口,并用tcpdump抓取该过程中两台
主机交换的TCP报文段。具体操作过程如下:
sudo tcpdump-nt-i eth0 port 54321仅抓取发送至和来自54321端口的TCP报文段
telnet 192.168.1.108 54321
Trying 192.168.1.108...
telnet:connect to address 192.168.1.108:Connection refused
telnet程序的输出显示连接被拒绝了,因为这个端口不存在。tcpdump抓取到的TCP报文段内容如下:
1.IP 192.168.1.109.42001>192.168.1.108.54321:Flags[S],seq 21621375,win 14600,length 0
2.IP 192.168.1.108.54321>192.168.1.109.42001:Flags[R.],seq 0,ack 21621376,win 0,length 0
由此可见,ernest-laptop针对Kongming20的连接请求(同步报文段)回应了一个复位报文段(tcpdump
输出R标志)。因为复位报文段的接收通告窗口大小为0,所以可以预见:收到复位报文段的一端应该关闭
连接或者重新连接,而不能回应这个复位报文段。
实际上,当客户端程序向服务器的某个端口发起连接,而该端口仍被处于TIME_WAIT状态的连接所占
用时,客户端程序也将收到复位报文段。
3.5.2 异常终止连接
前面讨论的连接终止方式都是正常的终止方式:数据交换完成之后,一方给另一方发送结束报文段。
TCP提供了异常终止一个连接的方法,即给对方发送一个复位报文段。一旦发送了复位报文段,发送端所
有排队等待发送的数据都将被丢弃。
应用程序可以使用socket选项SO_LINGER来发送复位报文段,以异常终止一个连接。我们将在第5章讨
论SO_LINGER选项。
3.5.3 处理半打开连接
考虑下面的情况:服务器(或客户端)关闭或者异常终止了连接,而对方没有接收到结束报文段(比
如发生了网络故障),此时,客户端(或服务器)还维持着原来的连接,而服务器(或客户端)即使重
启,也已经没有该连接的任何信息了。我们将这种状态称为半打开状态,处于这种状态的连接称为半打开
连接。如果客户端(或服务器)往处于半打开状态的连接写入数据,则对方将回应一个复位报文段。
举例来说,我们在Kongming20上使用nc命令模拟一个服务器程序,使之监听12345端口,然后从ernest-
laptop运行telnet命令登录到该端口上,接着拔掉ernest-laptop的网线,并在Kongming20上中断服务器程序。
显然,此时ernest-laptop上运行的telnet客户端程序维持着一个半打开连接。然后接上ernest-laptop的网线,并
从客户端程序往半打开连接写入1字节的数据“a”。同时,运行tcpdump程序抓取整个过程中telnet客户端和nc
服务器交换的TCP报文段。具体操作过程如下:
nc-l 12345在Kongming20上运行服务器程序
sudo tcpdump-nt-i eth0 port 12345
telnet 192.168.1.109 12345在ernest-laptop上运行客户端程序
Trying 192.168.1.109...
Connected to 192.168.1.109.
Escape character is'^]'.此时断开ernest-laptop的网线,并重启服务器
a(回车)向半打开连接输入字符a
Connection closed by foreign host.telnet的输出显示,连接被服务器关闭了。tcpdump抓取到的TCP报文段内容如下:
1.IP 192.168.1.108.55100>192.168.1.109.12345:Flags[S],seq 3093809365,length 0
2.IP 192.168.1.109.12345>192.168.1.108.55100:Flags[S.],seq 1495337791,ack 3093809366,length 0
3.IP 192.168.1.108.55100>192.168.1.109.12345:Flags[.],ack 1,length 0
4.IP 192.168.1.108.55100>192.168.1.109.12345:Flags[P.],seq 1:4,ack 1,length 3
5.IP 192.168.1.109.12345>192.168.1.108.55100:Flags[R],seq 1495337792,length 0
该输出内容中,前3个TCP报文段是正常建立TCP连接的3次握手的过程。第4个TCP报文段由客户端发
送给服务器,它携带了3字节的应用程序数据,这3字节依次是:字母“a”、回车符“\r”和换行符“\n”。不过因
为服务器程序已经被中断,所以Kongming20对客户端发送的数据回应了一个复位报文段5。3.6 TCP交互数据流
前面讨论了TCP连接及其状态,从本节开始我们讨论通过TCP连接交换的应用程序数据。TCP报文段所
携带的应用程序数据按照长度分为两种:交互数据和成块数据。交互数据仅包含很少的字节。使用交互数
据的应用程序(或协议)对实时性要求高,比如telnet、ssh等。成块数据的长度则通常为TCP报文段允许的
最大数据长度。使用成块数据的应用程序(或协议)对传输效率要求高,比如ftp。本节我们讨论交互数据
流。
考虑如下情况:在ernest-laptop上执行telnet命令登录到本机,然后在shell命令提示符后执行ls命令,同
时用tcpdump抓取这一过程中telnet客户端和telnet服务器交换的TCP报文段。具体操作过程如下:
tcpdump-nt-i lo port 23
telnet 127.0.0.1
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is'^]'.
Ubuntu 9.10
ernest-laptop login:ernest(回车)输入用户名并回车
Password:(回车)输入密码并回车
ernest@ernest-laptop:~ls(回车)
上述过程将引起客户端和服务器交换很多TCP报文段。下面我们仅列出我们感兴趣的、执行ls命令产生
的tcpdump输出,如代码清单3-4所示。
代码清单3-4 TCP交互数据流
1.IP 127.0.0.1.58130>127.0.0.1.23:Flags[P.],seq 1408334812:1408334813,ack 1415955507,win 613,length 1
2.IP 127.0.0.1.23>127.0.0.1.58130:Flags[P.],seq 1:2,ack 1,win 512,length 1
3.IP 127.0.0.1.58130>127.0.0.1.23:Flags[.],ack 2,win 613,length 0
4.IP 127.0.0.1.58130>127.0.0.1.23:Flags[P.],seq 1:2,ack 2,win 613,length 1
5.IP 127.0.0.1.23>127.0.0.1.58130:Flags[P.],seq 2:3,ack 2,win 512,length 1
6.IP 127.0.0.1.58130>127.0.0.1.23:Flags[.],ack 3,win 613,length 0
7.IP 127.0.0.1.58130>127.0.0.1.23:Flags[P.],seq 2:4,ack 3,win 613,length 2
8.IP 127.0.0.1.23>127.0.0.1.58130:Flags[P.],seq 3:176,ack 4,win 512,length 173
9.IP 127.0.0.1.58130>127.0.0.1.23:Flags[.],ack 176,win 630,length 0
10.IP 127.0.0.1.23>127.0.0.1.58130:Flags[P.],seq 176:228,ack 4,win 512,length 52
11.IP 127.0.0.1.58130>127.0.0.1.23:Flags[.],ack 228,win 630,length 0
TCP报文段1由客户端发送给服务器,它携带1个字节的应用程序数据,即字母“l”。TCP报文段2是服务
器对TCP报文段1的确认,同时回显字母“l”。TCP报文段3是客户端对TCP报文段2的确认。第4~6个TCP报
文段是针对字母“s”的上述过程。TCP报文段7传送的2字节数据分别是:客户端键入的回车符和流结束符
(EOF,本例中是0x00)。TCP报文段8携带服务器返回的客户查询的目录的内容(ls命令的输出),包括
该目录下文件的文件名及其显示控制参数。TCP报文段9是客户端对TCP报文段8的确认。TCP报文段10携带
的也是服务器返回给客户端的数据,包括一个回车符、一个换行符、客户端登录用户的PS1环境变量(第一
级命令提示符)。TCP报文段11是客户端对TCP报文段10的确认。
在上述过程中,客户端针对服务器返回的数据所发送的确认报文段(TCP报文段6、9和11)都不携带
任何应用程序数据(长度为0),而服务器每次发送的确认报文段(TCP报文段2、5、8和10)都包含它需
要发送的应用程序数据。服务器的这种处理方式称为延迟确认,即它不马上确认上次收到的数据,而是在
一段延迟时间后查看本端是否有数据需要发送,如果有,则和确认信息一起发出。因为服务器对客户请求
处理得很快,所以它发送确认报文段的时候总是有数据一起发送。延迟确认可以减少发送TCP报文段的数
量。而由于用户的输入速度明显慢于客户端程序的处理速度,所以客户端的确认报文段总是不携带任何应
用程序数据。前文曾提到,在TCP连接的建立和断开过程中,也可能发生延迟确认。
上例是在本地回路运行的结果,在局域网中也能得到基本相同的结果,但在广域网就未必如此了。广
域网上的交互数据流可能经受很大的延迟,并且,携带交互数据的微小TCP报文段数量一般很多(一个按
键输入就导致一个TCP报文段),这些因素都可能导致拥塞发生。解决该问题的一个简单有效的方法是使
用Nagle算法。
Nagle算法要求一个TCP连接的通信双方在任意时刻都最多只能发送一个未被确认的TCP报文段,在该TCP报文段的确认到达之前不能发送其他TCP报文段。另一方面,发送方在等待确认的同时收集本端需要发
送的微量数据,并在确认到来时以一个TCP报文段将它们全部发出。这样就极大地减少了网络上的微小TCP
报文段的数量。该算法的另一个优点在于其自适应性:确认到达得越快,数据也就发送得越快。3.7 TCP成块数据流
下面考虑用FTP协议传输一个大文件。在ernest-laptop上启动一个vsftpd服务器程序(升级的、安全版的
ftp服务器程序),并执行ftp命令登录该服务器上,然后在ftp命令提示符后输入get命令,从服务器下载一个
几百兆的大文件。同时用tcpdump抓取这一个过程中ftp客户端和vsftpd服务器交换的TCP报文段。具体操作
过程如下:
sudo tcpdump-nt-i eth0 port 20vsftpd服务器程序使用端口号20
ftp 127.0.0.1
Connected to 127.0.0.1.
220(vsFTPd 2.3.0)
Name(127.0.0.1:ernest):ernest(回车)输入用户名并回车
331 Please specify the password.
Password:(回车)输入密码并回车
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
ftp>get bigfile(回车)获取大文件bigfile
代码清单3-5是该过程的部分tcpdump输出。
代码清单3-5 TCP成块数据流
1.IP 127.0.0.1.20>127.0.0.1.39651:Flags[.],seq 205783041:205799425,ack 1,win 513,length 16384
2.IP 127.0.0.1.20>127.0.0.1.39651:Flags[.],seq 205799425:205815809,ack 1,win 513,length 16384
3.IP 127.0.0.1.20>127.0.0.1.39651:Flags[.],seq 205815809:205832193,ack 1,win 513,length 16384
4.IP 127.0.0.1.20>127.0.0.1.39651:Flags[P.],seq 205832193:205848577,ack 1,win 513,length 16384
5.IP 127.0.0.1.20>127.0.0.1.39651:Flags[.],seq 205848577:205864961,ack 1,win 513,length 16384
6.IP 127.0.0.1.20>127.0.0.1.39651:Flags[.],seq 205864961:205881345,ack 1,win 513,length 16384
7.IP 127.0.0.1.20>127.0.0.1.39651:Flags[.],seq 205881345:205897729,ack 1,win 513,length 16384
8.IP 127.0.0.1.20>127.0.0.1.39651:Flags[P.],seq 205897729:205914113,ack 1,win 513,length 16384
9.IP 127.0.0.1.20>127.0.0.1.39651:Flags[.],seq 205914113:205930497,ack 1,win 513,length 16384
10.IP 127.0.0.1.20>127.0.0.1.39651:Flags[.],seq 205930497:205946881,ack 1,win 513,length 16384
11.IP 127.0.0.1.20>127.0.0.1.39651:Flags[.],seq 205946881:205963265,ack 1,win 513,length 16384
12.IP 127.0.0.1.20>127.0.0.1.39651:Flags[P.],seq 205963265:205979649,ack 1,win 513,length 16384
13.IP 127.0.0.1.20>127.0.0.1.39651:Flags[.],seq 205979649:205996033,ack 1,win 513,length 16384
14.IP 127.0.0.1.20>127.0.0.1.39651:Flags[.],seq 205996033:206012417,ack 1,win 513,length 16384
15.IP 127.0.0.1.20>127.0.0.1.39651:Flags[.],seq 206012417:206028801,ack 1,win 513,length 16384
16.IP 127.0.0.1.20>127.0.0.1.39651:Flags[P.],seq 206028801:206045185,ack 1,win 513,length 16384
17.IP 127.0.0.1.39651>127.0.0.1.20:Flags[.],ack 205815809,win 30084,length 0
18.IP 127.0.0.1.39651>127.0.0.1.20:Flags[.],ack 206045185,win 27317,length 0
注意,客户端发送的最后两个TCP报文段17和18,它们分别是对TCP报文段2和16的确认(从序号值和
确认值来判断)。由此可见,当传输大量大块数据的时候,发送方会连续发送多个TCP报文段,接收方可
以一次确认所有这些报文段。那么发送方在收到上一次确认后,能连续发送多少个TCP报文段呢?这是由
接收通告窗口(还需要考虑拥塞窗口,见后文)的大小决定的。TCP报文段17说明客户端还能接收30
084×64字节(本例中窗口扩大因子为6),即1 925 376字节的数据。而在TCP报文段18中,接收通告窗口大
小为1 748 288字节,即客户端能接收的数据量变小了。这表明客户端的TCP接收缓冲区有更多的数据未被
应用程序读取而停留在其中,这些数据都来自TCP报文段3~16中的一部分。服务器收到TCP报文段18后,它至少(因为接收通告窗口可能扩大)还能连续发送的未被确认的报文段数量是1 748 28816 384个,即106
个(但一般不会连续发送这么多)。其中,16 384是成块数据的长度(见TCP报文段1~16的length值),很
显然它小于但接近MSS规定的16 396字节。
另外一个值得注意的地方是,服务器每发送4个TCP报文段就传送一个PSH标志(tcpdump输出标志P)
给客户端,以通知客户端的应用程序尽快读取数据。不过这对服务器来说显然不是必需的,因为它知道客
户端的TCP接收缓冲区中还有空闲空间(接收通告窗口大小不为0)。
下面我们修改系统的TCP接收缓冲区和TCP发送缓冲区的大小(如何修改将在第16章介绍),使之都为
4096字节,然后重启vsftpd服务器,并再次执行上述操作。此次tcpdump的部分输出如代码清单3-6所示。
代码清单3-6 修改TCP接收和发送缓冲区大小后的TCP成块数据流
1.IP 127.0.0.1.20>127.0.0.1.45227:Flags[.],seq 5195777:5197313,ack 1,win 3072,length 1536
2.IP 127.0.0.1.20>127.0.0.1.45227:Flags[.],seq 5197313:5198849,ack 1,win 3072,length 1536
3.IP 127.0.0.1.45227>127.0.0.1.20:Flags[.],ack 5198849,win 3072,length 04.IP 127.0.0.1.20>127.0.0.1.45227:Flags[P.],seq 5198849:5200385,ack 1,win 3072,length 1536
5.IP 127.0.0.1.45227>127.0.0.1.20:Flags[.],ack 5200385,win 3072,length 0
从同步报文段(未在代码清单3-6中列出)得知在这次通信过程中,客户端和服务器的窗口扩大因子都
为0,因而客户端和服务器每次通告的窗口大小都是3072字节(没超过4096字节,预料之中)。因为每个成
块数据的长度为1536字节,所以服务器在收到上一个TCP报文段的确认之前最多还能再发送1个TCP报文
段,这正是TCP报文段1~3描述的情形。3.8 带外数据
有些传输层协议具有带外(Out Of Band,OOB)数据的概念,用于迅速通告对方本端发生的重要事
件。因此,带外数据比普通数据(也称为带内数据)有更高的优先级,它应该总是立即被发送,而不论发
送缓冲区中是否有排队等待发送的普通数据。带外数据的传输可以使用一条独立的传输层连接,也可以映
射到传输普通数据的连接中。实际应用中,带外数据的使用很少见,已知的仅有telnet、ftp等远程非活跃程
序。
UDP没有实现带外数据传输,TCP也没有真正的带外数据。不过TCP利用其头部中的紧急指针标志和紧
急指针两个字段,给应用程序提供了一种紧急方式。TCP的紧急方式利用传输普通数据的连接来传输紧急
数据。这种紧急数据的含义和带外数据类似,因此后文也将TCP紧急数据称为带外数据。
我们先来介绍TCP发送带外数据的过程。假设一个进程已经往某个TCP连接的发送缓冲区中写入了N字
节的普通数据,并等待其发送。在数据被发送前,该进程又向这个连接写入了3字节的带外数据“abc”。此
时,待发送的TCP报文段的头部将被设置URG标志,并且紧急指针被设置为指向最后一个带外数据的下一
字节(进一步减去当前TCP报文段的序号值得到其头部中的紧急偏移值),如图3-10所示。
图 3-10 TCP发送缓冲区中的紧急数据
由图3-10可见,发送端一次发送的多字节的带外数据中只有最后一字节被当作带外数据(字母c),而
其他数据(字母a和b)被当成了普通数据。如果TCP模块以多个TCP报文段来发送图3-10所示TCP发送缓冲
区中的内容,则每个TCP报文段都将设置URG标志,并且它们的紧急指针指向同一个位置(数据流中带外
数据的下一个位置),但只有一个TCP报文段真正携带带外数据。
现在考虑TCP接收带外数据的过程。TCP接收端只有在接收到紧急指针标志时才检查紧急指针,然后根
据紧急指针所指的位置确定带外数据的位置,并将它读入一个特殊的缓存中。这个缓存只有1字节,称为带
外缓存。如果上层应用程序没有及时将带外数据从带外缓存中读出,则后续的带外数据(如果有的话)将
覆盖它。
前面讨论的带外数据的接收过程是TCP模块接收带外数据的默认方式。如果我们给TCP连接设置了
SO_OOBINLINE选项,则带外数据将和普通数据一样被TCP模块存放在TCP接收缓冲区中。此时应用程序
需要像读取普通数据一样来读取带外数据。那么这种情况下如何区分带外数据和普通数据呢?显然,紧急
指针可以用来指出带外数据的位置,socket编程接口也提供了系统调用来识别带外数据(见第5章)。
至此,我们讨论了TCP模块发送和接收带外数据的过程。至于内核如何通知应用程序带外数据的到
来,以及应用程序如何发送和接收带外数据,将在后续章节讨论。3.9 TCP超时重传
在3.6节~3.8节中,我们讲述了TCP在正常网络情况下的数据流。从本节开始,我们讨论异常网络状况
下(开始出现超时或丢包),TCP如何控制数据传输以保证其承诺的可靠服务。
TCP服务必须能够重传超时时间内未收到确认的TCP报文段。为此,TCP模块为每个TCP报文段都维护
一个重传定时器,该定时器在TCP报文段第一次被发送时启动。如果超时时间内未收到接收方的应答,TCP
模块将重传TCP报文段并重置定时器。至于下次重传的超时时间如何选择,以及最多执行多少次重传,就
是TCP的重传策略。我们通过实例来研究Linux下TCP的超时重传策略。
在ernest-laptop上启动iperf服务器程序,然后从Kongming20上执行telnet命令登录该服务器程序。接下
来,从telnet客户端发送一些数据(此处是“1234”)给服务器,然后断开服务器的网线并再次从客户端发送
一些数据给服务器(此处是“12”)。同时,用tcpdump抓取这一过程中客户端和服务器交换的TCP报文段。
具体操作过程如下:
sudo tcpdump-n-i eth0 port 5001
iperf-s在ernest-laptop上执行
telnet 192.168.1.108 5001在Kongming20上执行
Trying 192.168.1.108...
Connected to 192.168.1.108.
Escape character is'^]'.
1234发送完之后断开服务器网线
12
Connection closed by foreign host
iperf是一个测量网络状况的工具,-s选项表示将其作为服务器运行。iperf默认监听5001端口,并丢弃该
端口上接收到的所有数据,相当于一个discard服务器。上述操作过程的部分tcpdump输出如代码清单3-7所
示。
代码清单3-7 TCP超时重传
1.18:44:57.580341 IP 192.168.1.109.38234>192.168.1.108.5001:Flags[S],seq 2381272950,length 0
2.18:44:57.580477 IP 192.168.1.108.5001>192.168.1.109.38234:Flags[S.],seq 466032301,ack 2381272951,length 0
3.18:44:57.580498 IP 192.168.1.109.38234>192.168.1.108.5001:Flags[.],ack 1,length 0
4.18:44:59.866019 IP 192.168.1.109.38234>192.168.1.108.5001:Flags[P.],seq 1:7,ack 1,length 6
5.18:44:59.866165 IP 192.168.1.108.5001>192.168.1.109.38234:Flags[.],ack 7,length 0
6.18:45:25.028933 IP 192.168.1.109.38234>192.168.1.108.5001:Flags[P.],seq 7:11,ack 1,length 4
7.18:45:25.230034 IP 192.168.1.109.38234>192.168.1.108.5001:Flags[P.],seq 7:11,ack 1,length 4
8.18:45:25.639407 IP 192.168.1.109.38234>192.168.1.108.5001:Flags[P.],seq 7:11,ack 1,length 4
9.18:45:26.455942 IP 192.168.1.109.38234>192.168.1.108.5001:Flags[P.],seq 7:11,ack 1,length 4
10.18:45:28.092425 IP 192.168.1.109.38234>192.168.1.108.5001:Flags[P.],seq 7:11,ack 1,length 4
11.18:45:31.362473 IP 192.168.1.109.38234>192.168.1.108.5001:Flags[P.],seq 7:11,ack 1,length 4
12.18:45:33.100888 ARP,Request who-has 192.168.1.108 tell 192.168.1.109,length 28
13.18:45:34.098156 ARP,Request who-has 192.168.1.108 tell 192.168.1.109,length 28
14.18:45:35.100887 ARP,Request who-has 192.168.1.108 tell 192.168.1.109,length 28
15.18:45:37.902034 ARP,Request who-has 192.168.1.108 tell 192.168.1.109,length 28
16.18:45:38.903126 ARP,Request who-has 192.168.1.108 tell 192.168.1.109,length 28
17.18:45:39.901421 ARP,Request who-has 192.168.1.108 tell 192.168.1.109,length 28
18.18:45:44.440049 ARP,Request who-has 192.168.1.108 tell 192.168.1.109,length 28
19.18:45:45.438840 ARP,Request who-has 192.168.1.108 tell 192.168.1.109,length 28
20.18:45:46.439932 ARP,Request who-has 192.168.1.108 tell 192.168.1.109,length 28
21.18:45:50.976710 ARP,Request who-has 192.168.1.108 tell 192.168.1.109,length 28
22.18:45:51.974134 ARP,Request who-has 192.168.1.108 tell 192.168.1.109,length 28
23.18:45:52.973939 ARP,Request who-has 192.168.1.108 tell 192.168.1.109,length 28
TCP报文段1~3是三次握手建立连接的过程,TCP报文段4~5是客户端发送数据“1234”(应用程序数据
长度为6,包括回车、换行两个字符,后同)及服务器确认的过程。TCP报文段6是客户端第一次发送数
据“12”的过程。因为服务器的网线被断开,所以客户端无法收到TCP报文段6的确认报文段。此后,客户端
对TCP报文段6执行了5次重传,它们是TCP报文段7~11,这可以从每个TCP报文段的序号得知。此后,数
据包12~23都是ARP模块的输出内容,即Kongming20查询ernest-laptop的MAC地址。
我们保留了tcpdump输出的时间戳,以便推理TCP的超时重传策略。观察TCP报文段6~11被发送的时间
间隔,它们分别为0.2 s、0.4 s、0.8 s、1.6 s和3.2 s。由此可见,TCP一共执行5次重传,每次重传超时时间都
增加一倍(因此,和TCP超时重连的策略相似)。在5次重传均失败的情况下,底层的IP和ARP开始接管,直到telnet客户端放弃连接为止。Linux有两个重要的内核参数与TCP超时重传相关:procsysnetipv4tcp_retries1
和procsysnetipv4tcp_retries2。前者指定在底层IP接管之前TCP最少执行的重传次数,默认值是3。后者指
定连接放弃前TCP最多可以执行的重传次数,默认值是15(一般对应13~30 min)。在我们的实例中,TCP
超时重传发生了5次,连接坚持的时间是15 min(可以用date命令来测量)。
虽然超时会导致TCP报文段重传,但TCP报文段的重传可以发生在超时之前,即快速重传,这将在下一
节中讨论。3.10 拥塞控制
3.10.1 拥塞控制概述
TCP模块还有一个重要的任务,就是提高网络利用率,降低丢包率,并保证网络资源对每条数据流的
公平性。这就是所谓的拥塞控制。
TCP拥塞控制的标准文档是RFC 5681,其中详细介绍了拥塞控制的四个部分:慢启动(slow start)、拥塞避免(congestion avoidance)、快速重传(fast retransmit)和快速恢复(fast recovery)。拥塞控制算法
在Linux下有多种实现,比如reno算法、vegas算法和cubic算法等。它们或者部分或者全部实现了上述四个部
分。procsysnetipv4t ......
您现在查看是摘要介绍页, 详见PDF附件(5999KB,337页)。





