网络多人游戏架构与编程

978-7-115-45779-0
作者: 【美】Joshua Glazer(格雷泽) Sanjay Madhav(马达夫)
译者: 王晓慧张国鑫
编辑: 胡俊英

图书目录:

详情

本书系统化地介绍了网络游戏开发的全过程,从基础的网络协议到各个环节的具体实现都有所介绍。同时,书中还提供了游戏引擎、云开发和数据库等代码示例,帮助读者轻松掌握编程细节。无论是对游戏开发感兴趣的读者,还是游戏开发领域的高级程序员,都将从本书获益。

图书摘要

版权信息

书名:网络多人游戏架构与编程

ISBN:978-7-115-45779-0

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。


• 著    [美] Joshua Glazer Sanjay Madhav

  译    王晓慧 张国鑫

  责任编辑 胡俊英

• 人民邮电出版社出版发行  北京市丰台区成寿寺路11号

  邮编 100164  电子邮件 315@ptpress.com.cn

  网址 http://www.ptpress.com.cn

• 读者服务热线:(010)81055410

  反盗版热线:(010)81055315


Authorized translation from the English language edition, entitled Multiplayer Game Programming, 0134034309 by Joshua Glazer, Sanjay Madhav, published by Pearson Education, Inc, publishing as Addison Wesley Professional, Copyright © 2016 Pearson Education, Inc.

All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from Pearson Education, Inc.

CHINESE SIMPLIFIED language edition published by PEARSON EDUCATION ASIA LTD., and POSTS & TELECOMMUNICATIONS PRESS Copyright © 2017.

本书封面贴有Pearson Education(培生教育出版集团)激光防伪标签。无标签者不得销售。


网络多人游戏已经成为游戏产业的重要组成部分,本书是一本深入探讨关于网络多人游戏编程的图书。

全书分为13章,从网络游戏的基本概念、互联网、伯克利套接字、对象序列化、对象复制、网络拓扑和游戏案例、延迟、抖动和可靠性、改进的延迟处理、可扩展性、安全性、真实世界的引擎、玩家服务、云托管专用服务器等方面深入介绍了网络多人游戏开发的知识,既全面又详尽地剖析了众多核心概念。

本书的多数示例基于C++编写,适合对C++有一定了解的读者阅读。本书既可以作为大学计算机相关专业的指导教程,也可以作为普通读者学习网络游戏编程的参考指南。


王晓慧,博士,就职于北京科技大学工业设计系。主要研究方向为虚拟现实、大数据与信息可视化、人工智能、情感计算、人机交互等,在国内外学术期刊和会议上发表论文20余篇。主持国家自然科学基金、北京市社会科学基金、北京市科技计划项目及中国博士后科学基金等国家、省部级项目4项,主持中央高校基本科研业务费、CFF-腾讯犀牛鸟创意基金等项目5项,参与973项目、国家自然、国家社科等项目6项,获得软件著作权2项,出版译著《精通Unreal游戏引擎》。邮箱地址是xiaohui0506@foxmail.com。

张国鑫,博士,蓝因(北京)科技有限公司CEO。多年来一直从事虚拟现实、三维模型处理和人工智能相关的研究和工作,发表SCI论文多篇,取得发明专利4项,获得软件著作权3项,曾获得国家技术发明奖二等奖,教育部技术发明奖一等奖。邮箱地址是zgx@lanetechs.com。


 

 

 

今天,网络多人游戏已经成为游戏产业的一个重要组成部分,玩家的数量和投入的资金数额都是惊人的。到2014年为止,《英雄联盟》(League of Legends)月活跃用户已达到6700万。在写这本书的时候, 2015年DoTA 2的世界锦标赛的奖金已经超过1600万美元。《使命召唤》(Call of Duty)系列经常在发布后的头几天就打破10亿美元的销售额,其大受欢迎的部分原因在于它的多人模式。即使有的游戏曾经是单人模式,现在也都包含了网络多人成分,例如《侠盗猎车手》(Grand Theft Auto)。

本书深入探讨用于网络多人游戏编程的所有重要知识。首先介绍网络的基本知识——互联网是如何工作的,如何将数据发送给其他计算机。建立基本知识架构之后,本书讨论用于游戏数据传输的基本知识——如何准备用于网络传输的游戏数据,如何更新网络中的游戏对象,如何组织参与游戏的计算机。接着,本书论述如何补偿不可靠性和网络延迟,如何将游戏代码设计得同时具有可扩展性和安全性。第12章和第13章讲述如何集成玩家服务,以及使用云托管专用服务器,两个内容在今天的网络游戏中非常重要。

本书采用非常实用的方式。大部分章节不仅讨论概念,而且通过真实的代码让读者了解到网络游戏的工作原理。本书的配套网站提供两个不同游戏的源代码,一个是动作游戏,另一个是即时战略游戏(real-time strategy, RTS)。为了适应各个章节的内容演进,本书从头到尾会包含这两个游戏的多个版本。

本书中的大部分内容是基于南加州大学(University of Southern California,USC)的多人游戏编程课程的。正因为如此,本书包含的是被验证的行之有效的多人游戏编程学习方法。虽然这样说,但是本书不仅用于大学课程,书中的方法对于任何对网络游戏编程感兴趣的人来说都是有价值的。

虽然附录A包含了本书中所使用到的现代C++的一些内容,但是本书假设读者已经熟悉C++。本书进一步假设读者已经掌握在计算机课程中学习的标准数据结构。如果您不熟悉C++,或者想复习一下数据结构,有一本非常好的参考书——Programming Abstraction in C++,作者Eric Roberts。

本书还假设读者已经了解如何编写单人游戏。读者应该熟悉游戏循环、游戏对象模型、向量数学和基础游戏物理。如果读者不熟悉这些概念,建议先学习游戏编程入门书——Game Programming Algorithms and Techniques,作者Sanjay Madhav。

正如之前所提到的,本书既适用于大学课程,也适合想要学习网络游戏编程的程序员。即使是没有接触过网络游戏的游戏程序员,也能在这本书中找到大量有用的知识。

代码通常使用定点字体。小的代码片段可能出现在段落内,也可能是单独的一段:

std::cout << "Hello, world!" << std::endl;

长的代码段以代码清单的形式展现,如清单0.1所示。

清单0.1 代码清单示例

// Hello world program!
int main()
{
   std::cout << "Hello, world!" << std::endl;
   return 0;
}

为了提高可读性,示例代码与编程环境中的显示基本一致。

在本书中,读者会看到一些段落标记为注释、小窍门、边栏和警告。下面分别举例说明。

注释:

 

注释包含与正文上下文分离的有用信息,应该仔细阅读。

小窍门:

 

当在你的游戏代码中实现特定的系统时,小窍门将提供一些有用的提示。

警告:

 

警告非常重要,因为它包括一些常见的陷阱和需要注意的问题,同时包括解决这些问题的方法。

 

边栏

 

边栏通常包含与章节主要内容关系不太密切的更多讨论。这些内容针对各种各样的问题提供一些有趣的见解,但是其包含的内容往往不是章节教学目标所必需的。

本书中大量使用C++,是因为C++是游戏产业事实上的编程语言,被游戏引擎程序员广泛使用。尽管有的引擎允许游戏中的大部分代码使用其他语言,如Unity中使用C#,但是要谨记,这些引擎的底层代码仍然是用C++写的。因为本书从头开始讲解编写网络多人游戏,所以使用大部分游戏引擎所使用的语言。话虽这么说,即使读者使用其他语言编写游戏网络代码,其所有的核心思想仍然是一样的。尽管如此,还是建议读者学会C++,不然书中的示例代码对读者来说可能就没有多大意义了。

尽管刚开始的时候JavaScript是为了支持Netscape浏览器被匆忙赶出的脚本语言,它已经发展成为标准化、功能全面、有些函数式风格的语言。它在客户端语言中的流行帮助它跨越到了服务器端。在服务器端,它的第一级过程、简单的闭包语法和动态类型的性质,使其对于开发事件驱动的服务非常有效。JavaScript重构困难,性能比C++差,因此很难成为下一代游戏前端开发的首选语言。

对于服务器端来讲,这不是一个问题,因为扩大服务规模就像向右边拖动滑块这么简单。第13章中服务器端的例子使用JavaScript,理解这部分内容需要了解该语言的一些知识。截至本书写作的时候, JavaScript已经成为GitHub上最活跃的语言之一,其占比接近50%。仅仅为了赶时髦而跟随趋势并不是一个好主意,但是使用当下最流行的语言编程肯定有它的好处。

本书的配套网站提供了本书中所使用示例代码的链接。同时,网站还包括勘误表,幻灯片的链接和用于高校的示例教学大纲。

首先,我们感谢Pearson的整个团队指导完成这本书,包括我们的执行编辑Laura Lewin,劝说我们聚到一起写这本书;我们的助理编辑Olivia Basegio,协助我们保证过程的顺利进行;我们的开发编辑Michael Thurston,为提升内容质量出谋划策。我们还要感谢整个制作团队,包括制作编辑Andy Beaster和Cenveo公司。

我们的技术评论家Alexander Boczar、 Jonathan Rucker和 Jeff Tucker为保证这本书的准确性帮了很大的忙。感谢他们在百忙之中抽出时间审阅这本书。最后,我们感谢维尔福软件公司(Valve Software)允许我们写关于Steamworks SDK的知识并审读了第12章。

感谢Lori和McKinney给我无限的理解、支持、爱和微笑。你们是我最好的家人。由于写这本书,我陪你们的时间少了,但是现在,我终于完成了!感谢父母的养育之恩和对我的无限关爱,让我的英文能和代码一样流畅。感谢Beth做了很多不可思议的事情,有时还帮我照看我的猫。感谢大家庭在我写这本书时给我的支持和信任。感谢Charles和Naked Sky所有的程序员让我时刻保持警惕,在我犯愚蠢错误的时候及时指出。感谢Tian和Sam将我带到游戏产业中来。感谢Sensei Copping教会我如何有效工作。当然,感谢Sanjay把我带上了南加州大学(USC)的讲台,与我一起完成这个事情。如果没有你的智慧和冷静,我不能完成这本书,更不用说你写了这本书的一半。(再一次感谢Lori,以防你错过了第一句话!)

作者的著作数量与致谢的长度有关。因为在上一本书中我写了很长的致谢,所以这次我写得短一点。我首先感谢我的父母和妹妹。同时,感谢我在南加州大学(USC)信息技术专业(Information Technology Program)的同事。最后,感谢Joshua Glazer同意讲授“多人游戏编程”这门课程,因为如果不是因为这门课,这本书就不会诞生。

Joshua Glazer是Naked Sky Entertainment的创始人之一和CTO。 Naked Sky Entertainment是一个独立的游戏开发工作室,开发游戏机和电脑游戏,如RoboBlitzMicroBotTwister Mania和最近手机端的游戏Max AxeScrap Force。作为Naked Sky团队的领导人之一,他为许多外部的项目提供咨询,包括Epic Games公司的虚幻引擎(Unreal Engine)、Riot Games公司的《英雄联盟》(League of Legends)、THQ公司的《毁灭全人类》(Destroy All Humans)的特许经营,还包括艺电(Electronic Arts)、Midway、微软(Microsoft)和派拉蒙电影公司(Paramount Pictures)等公司的很多其他项目。

Joshua同时也是南加州大学(USC)的兼职讲师,在那里他很喜欢讲授关于多人游戏编程和游戏引擎开发的课程。

Sanjay Madhav是南加州大学(USC)的高级讲师,讲授许多编程和电子游戏编程的课程。他的主打课程是从2008年就开始讲授的本科生游戏编程课。此外,他还讲授许多其他课程,包括游戏引擎、数据结构和编译器开发。他同时也是Game Programming Algorithms and Techniques的作者。

在来到南加州大学(USC)之前,Sanjay在许多电子游戏开发公司做过程序员,包括艺电(Electronic Arts)、Neversoft和Pandemic Studios。他开发过的游戏有《荣誉勋章:血战太平洋》(Medal of Honor: Pacific Assault)、《托尼霍克极限滑板8》(Tony Hawk’s Project 8)、《指环王:征服》(Lord of the Rings: Conquest)和《破坏者》(The Saboteur),大部分都有某种形式的网络多人版。


虽然之前也有很多经典的案例,但是网络多人游戏的概念直到20世纪90年代才在主流玩家中得到普及。本章首先简要介绍多人游戏如何从20世纪70年代的早期网络游戏发展到如今巨大的产业。然后对20世纪90年代两款流行网络游戏《星际围攻:部落》(Starsiege: Tribes)和《帝国时代》(Age of Empires)的架构做概述。这些游戏中用到的许多技术今天仍然在使用,所以我们的讨论将对网络多人游戏设计的整体挑战提供一个深入的了解。

现代网络多人游戏起源于20世纪70年代的高校大型机系统。然而,这类游戏直到20世纪90年代中后期互联网接入普及才全面爆发。本节简要介绍网络游戏如何产生,以及从这类游戏诞生之后的近半个世纪中,它们所经历的多样化发展。

一些早期电子游戏具有本地多玩家的特点,意味着两个或两个以上玩家在一台计算机上玩游戏。例如,一些非常早期的游戏:《双人网球》(Tennis for Two,1958)和《太空战争》(Spacewar,1962)。本地多人游戏与单人游戏的编程在很大程度上是相似的,唯一的典型差异是对多视点和多个输入设备的支持。因为本地多人游戏的编程与单人游戏非常类似,本书就不在这上面花费时间了。

最早的网络多人游戏运行在由大型机系统组成的小网络中,此时网络多人游戏与本地多人游戏的区别是网络游戏有两台或更多的计算机在一个活动的游戏会话中彼此连接。一个典型的早期大型机网络是在伊利诺伊大学(University of Illinois)开发的PLATO(柏拉图)计算机系统。运行在柏拉图计算机系统上的一个典型的早期网络游戏是回合型策略游戏《帝国》
Empire,1973)。与Empire同时期出现的还有一款第一人称网络游戏
《迷宫战争》(Maze War)。对于哪个游戏最先出现的这个问题,至今没有明确的答案。

20世纪70年代末,随着个人计算机的出现,开发者们使用串口实现两台计算机之间的通信。串口允许一次传输1比特的数据,其主要功能是与打印机、调制解调器等外部设备进行通信。然而,使用这种方式实现两台计算机之间的连接和数据传输也是可行的。这使得创建跨越多台个人计算机的游戏会话成为可能,从而催生了最早的网络游戏。1980年12月的《BYTE杂志》发表了一篇文章,讲述如何使用BASIC语言编写所谓的多机游戏(Wasserman and Stryker 1980)。

使用串口的一个大的弊端是普通计算机最多只有两个串口(除非使用扩展卡)。这意味着要想使用串口连接两台以上的计算机,需要采用菊花链连接模式。菊花链连接模式可以看作是一种网络拓扑类型,我们将在第6章中详细介绍。

尽管20世纪80年代早期就出现了这一技术,但是那个时期出现的大部分游戏并没有以这种技术方式使用局域网络。直到20世纪90年代,用局域网连接多台计算机来玩游戏的想法才真正获得认可,我们将在本章后面继续讨论。

多用户网络游戏(multi-user dungeon,MUD)是多人游戏的一种,指许多玩家同时连接在一个虚拟世界中。这类游戏最早在重点高校的大型机系统中流行,MUD这个术语也是起源于艾塞克斯大学(Essex University)Rob Trushaw在1978年创建的游戏MUD。从某种程度上说,各种MUD也可以被认为是角色扮演类游戏《龙与地下城》(Dungeons and Dragons)的一个早期版本,尽管并非所有的MUD都是角色扮演类游戏。

个人计算机的性能提升之后,硬件制造商就开始提供允许两台计算机使用电话线进行通信的调制解调器。尽管按照现在的标准来看,它的传输速率极其低,但是这使得多用户网络游戏可以运行在除高校大型机系统之外的机器上。例如,一些多用户网络游戏运行于电子布告栏系统(bulletin board system,BBS),该系统允许多用户通过调制解调器连接到同一个系统中,这个系统可以运行包括游戏在内的很多应用。

局域网(local area network,LAN)是指在某一小区域内多台计算机的相互连接,用于连接的技术手段可以有所不同。例如,本章讨论的串口连接方式就是局域网的一个示例。然而,局域网的真正兴起是伴随着以太网的普及(我们将在第2章中详细讨论这一协议)。

尽管《毁灭战士》(Doom,1993)不是第一个支持局域网多玩家的游戏,但是从许多方面来说,它可以称为现代网络游戏的起源。这个由id Software[1] 开发的第一人称射击游戏的最初版本支持一个游戏会话中有4个玩家,可以选择是合作关系还是竞争关系。因为《毁灭战士》是一个快节奏的动作类游戏,所以需要实现本书中将要讲解到的许多关键概念。当然,这些技术自从1993年以来已经得到了很大的发展,但是该游戏的影响力是被广泛认可的。关于该游戏的创建和历史,详见本章末尾延伸的阅读资料中的Masters of Doom

许多支持局域网的多人游戏同时支持其他方式的网络连接,如调制解调器连接和在线网络连接。多年来,绝大部分的网络游戏都支持局域网,这导致了局域网联机游戏聚会的兴起。局域网联机游戏聚会指的是由一群人聚集在同一个地方,以局域网络连接各自的计算机,玩多人网络联机游戏。尽管一些网络多人游戏仍然支持局域网,但是最近几年开发者们有放弃局域网版本而追求在线版本的趋势。

在线游戏(online game)中,玩家通过一些大型网络将地理位置上有一定距离的计算机彼此连接起来。今天,在线游戏与网络游戏是同义词,但是“在线”的含义更广泛一些,包含一些早期的网络,如CompuServe,它最初没有连接到互联网上。

随着20世纪90年代末互联网的爆发,在线游戏也随之兴起。早些年出现的流行游戏包括id Software公司开发的《雷神之锤》(Quake,1996)和Epic Game公司开发的《虚幻》(Unreal,1998)等。

尽管在线游戏的实现方式看起来和局域网游戏类似,但是一个主要问题是延迟,也就是数据在网络中的传输时间。事实上,《雷神之锤》最初版本的设计是不支持互联网连接的,直到补丁QuakeWorld的出现才使得该游戏可以在互联网上可靠运行。延迟补偿的方法将在第7章和第8章中详细介绍。

随着21世纪第一个十年内Xbox Live和PlayStation Network等服务的创建,以及GameSpy和DWANGO等PC网络服务的完善,在线游戏在游戏机上也逐渐兴起。如今,这些在线游戏服务在高峰时段通常有数百万的活跃用户(尽管这些游戏机也提供视频及其他服务,并不是所有的这些活跃用户都在玩游戏)。第12章将讨论如何将其中一个玩家服务(Steam)融合到电脑游戏中。

即使在今天,大多数的多人在线游戏在每个游戏会话中仍然限制玩家的数量,一般支持4~32个玩家。然而,在大规模多人在线游戏(massively multiplayer online game,MMO)中,成百上千的玩家将同时出现在同一个游戏会话中。大部分的MMO游戏都是角色扮演游戏,称为MMORPG(MMO role-playing games)。但是,当然也存在其他类型的MMO游戏,例如第一人称射击类(MMO first-person shooters,MMOFPS)。

在许多方面,MMORPG可以看作是多用户网络游戏(MUD)的图形化改进。一些早期MMORPG的出现是在互联网的广泛使用之前,所以这些游戏运行在拨号网络中,如Quantum Link(即后来的America Online)和CompuServe。该类游戏的一个典型例子是《栖息地》(Habitat,1986),它实现了许多新颖的技术(详见延伸的阅读资料中Morningstar and Farmer 1991)。然而,直到互联网普及之后,该类游戏才得到了更多的关注。其中一个成功的案例是《网络创世纪》(Ultima Online,1997)。

其他的MMORPG,例如《无尽的任务》(EverQuest,1999)也很成功,但是Blizzard的《魔兽世界》(World of Warcraft,2004)的出现震惊了世界。曾几何时,Blizzard的这款MMORPG在全世界有超过1200万的活跃用户,该游戏已经成为流行文化的一部分,在2006年的电视动画《南方公园》(South Park)的某一集中描绘了这一场景。

构建一个大规模多人在线游戏是一个复杂的技术挑战,我们将在第9章中详细介绍这里面的部分挑战。然而,创建大规模多人在线游戏的大部分技术超出了本书的范围。当然,创建一个小规模网络游戏的基础对于大规模多人在线游戏的构建是尤为重要的。

随着游戏已经扩展到移动领域,多人游戏也随之进入。许多多人游戏在移动平台上是异步的,尤其是不要求实时传输数据的回合制游戏。在这个模型中,轮到玩家时,他们将会收到通知,所以有充足的时间让他们采取行动。在网络多人游戏最初出现时,该异步模型就已经存在了。一些BBS只有一个接入的电话线连接,这就意味着在一个时刻只能连接一个用户。因此,如果玩家想要连接,需要先排队等候,然后再断开。这样才能保证在后来的某一时刻,另外一个用户可以接入,在轮到他的时刻可以响应。

使用异步模型的移动网络游戏的一个例子是《填字游戏》(Words with Friends,2009)。从技术的角度来讲,异步网络游戏比实时网络游戏实现起来要简单。在移动平台上更是如此,因为应用程序接口(application program interface,API)有用于异步通信的内置功能。最初,在移动网络游戏中使用异步模型是有必要的,因为当时移动网络的可靠性远远低于有线网络。但是,随着Wi-Fi设备的普及和移动网络的发展,越来越多的实时网络游戏可以运行在移动设备上。使用实时网络通信的一个移动网络游戏示例是《炉石传说:魔兽英雄传》(Hearthstone: Heroes of Warcraft,2014)。

《星际围攻:部落》(Starsiege: Tribes)是1998年年底发布的基于科幻小说的第一人称射击游戏。发布时,它被认为既是快节奏的战斗类游戏,又包含大规模的玩家。一些游戏模式支持在局域网和互联网上同时运行128个玩家。想要体会到实现这个游戏的难度,可以想象在那个时期,绝大多数玩家使用的都是拨号服务的互联网连接。在最好的情况下,这些拨号用户的调制解调器的能力也只是56.6kbit/s。在《星际围攻:部落》这个游戏中,它真正支持的用户调制解调器速度可以到28.8kbit/s。用现在的标准看来,这个速度相当低。另外一个因素是拨号连接同样有相对比较高的延时—几百毫秒的延时是非常常见的。

在游戏中为低带宽设计的网络模型似乎不适用于当下的网络环境。然而,即使在今天,《星际围攻:部落》这个游戏中使用的模型仍有极大的可借鉴性。本节将总结《星际围攻:部落》中最初使用的网络模型,更深入的讨论请参考本章最后列出的Frohnmayer和Gift的文章。

如果本节中涉及的概念此刻没有完全理解,不要担心。本节的目的是从一定的高度来看网络多人游戏的架构,您将对所面临的大量技术挑战和所做决定有一个整体的认识。本节中涉及的所有内容都将在后面各章中详细介绍。此外,本书中将创建的一个游戏案例(Robo Cat Action)所使用的网络模型与《星际围攻:部落》非常类似。

创建网络游戏的首选之一是选择一个通信协议,也就是在两台计算机之间传输数据的约定。第2章将介绍互联网的工作原理和常用的协议。第3章将介绍通过这些协议实现通信的一个常用库。为了高效地理解当前的讨论,您只需要知道《星际围攻:部落》使用的是不可靠的协议,意思是在网络中传输的数据不能保证接收端一定能收到。

但是,当游戏需要发送对所有玩家都很重要的信息时,使用不可靠的协议就有很多问题。因此,工程师们需要考虑所发送的数据有不同类型。《星际围攻:部落》的开发者们最终将数据分为以下4种类型。

1.非保障数据。正如读者所想象的,该类数据不是游戏所必需的数据。所以当带宽有限时,游戏选择首先丢弃这些数据。

2.保障数据。该类数据需要保证其准确到达以及到达的顺序。用于对游戏至关重要的数据,例如,标识玩家发起攻击的事件。

3.最近的状态数据。该类数据用于只有最新版本的数据才是重要数据的场合,例如一个特定玩家的生命值。如果游戏知道了玩家当前的生命值,那么他5秒之前的生命值就不重要了。

4.最快保障数据。该类数据具有最高的优先级,在可靠传输的基础上,保证尽快到达。该类数据的一个典型例子是玩家的移动信息,该信息在一个非常短的时间内极其重要,因此需要尽快传输。

《星际围攻:部落》网络模型中许多实现机制都集中在提供这4种数据类型的传输上。

另外一个重要的设计方案是使用客户端-服务器模型(client-server model,C/S),而不是对等网络模型(peer-to-peer model)。在C/S模型中,玩家全部连接在一个中央服务器上,而在对等网络模型中,每个玩家都与其他所有玩家相连。正如在第6章中讨论的一样,对等网络模型需要O(n2)的带宽,意思是带宽是玩家数量的二次方增长速率。在这种情况下,当n为128时,使用对等网络模型将导致每个玩家只有极少的带宽。为了避免这个问题,《星际围攻:部落》使用C/S模型。在该结构中,每个玩家的带宽保持常数,服务器只需处理O(n)量级的带宽。然而,这意味着服务器需要允许许多接入的连接,在当时只有公司和高校才有这种连接。

接下来,《星际围攻:部落》将网络实现划分为许多不同的层,您可以将《星际围攻:部落》网络模型想象成夹心蛋糕,如图1.1所示。本节剩下的部分将简要描述每一层的构造。

图1.1 《星际围攻:部落》网络模型的主要组成部分

数据包是在网络中传输的有一定格式的数据集合。在《星际围攻:部落》网络模型中,平台数据包模块是最底层,只有这一层是针对特定平台的。其实,这一层是标准套接字API的封装,可以构建和发送不同的数据包格式。这一层的实现与第3章中实现的系统十分相似。

因为《星际围攻:部落》使用的是不可靠的协议,所以开发者们需要添加一些保证数据安全传输的机制。与第7章中讨论的方法类似,《星际围攻:部落》实现的是定制的可靠层。但是,该可靠层不是在平台数据包模块中处理的,而是在更高层管理器,如ghost管理器、移动管理器、事件管理器中来增强可靠性。

连接管理器(connection manager)的任务是将网络中两台计算机之间的连接抽象化。它从上层流管理器接收数据,再将数据传输给底层平台数据包模块。

连接管理器层仍然是不可靠的,它不能保证数据的可靠传输。但是,连接管理器可以保证投递状态通知的正确传输,也就是说,可以确认传输到连接管理器层的请求状态。这样,连接管理器层的上层(流管理器)就可以知道指定的数据是否被成功传输。

投递状态通知使用滑动窗口中接受域的位字段实现。尽管最初的《星际围攻:部落》网络模型没有详细讨论连接管理器的具体实现,但是第7章中将介绍一个类似系统的实现方法。

流管理器(stream manager)的任务是将数据发送给连接管理器,其中一个重要的部分是决定允许数据传输的最大速率。最大速率会根据网络连接的质量而有所不同。在最初发表的文献中有这样一个例子,一个具有28.8kbit/s调制解调器的用户可以将数据包传输速率设置为每秒10个数据包,每个数据包最多200字节,大约每秒2KB的数据。这个传输速率和数据包大小将根据客户端的连接被发送给服务器,以保证服务器不至于发送给客户端超出连接能力的数据。

因为许多其他系统要求流管理器发送数据,流管理器有责任把这些请求按照优先次序排好。在带宽限制的情况下,移动管理器、事件管理器和ghost管理器拥有最高的优先级。一旦流管理器决定发送哪些数据,数据包将会分派给连接管理器。接着,高层管理器将会通过流管理器的投递状态得到通知。

因为流管理器所执行的设置间隔和数据包大小的不同,一个数据包中很有可能包含不同类型的数据。例如,一个数据包可能同时包含来自移动管理器、事件管理器和ghost管理器的数据。

事件管理器(event manager)维持一个由游戏模拟层产生的事件队列。这些事件可以看作是远程过程调用(remote procedure call,RPC)的一种简单形式,RPC
是可以在远程计算机上执行的程序。我们将在第5章中讨论远程过程调用。

例如,当玩家发起攻击时,引发一个“玩家发起攻击”事件,该事件将被发送到事件管理器。该事件接着被发送到服务器,服务器确认和执行攻击。事件管理器负责将这些事件按照优先次序排列,它会给尽可能多的事件分配最高的优先级,直到满足下面的任意一个条件:数据包已满,事件队列为空,或者此刻有太多的活跃事件。

事件管理器也会追踪每一个被标记为可靠数据的传输记录。用这种方法,事件管理器很容易实现可靠性。如果可靠记录没有被确认,那么事件管理器重新将该事件放入事件队列中,重新传输一次。当然,也有很多标记为不可靠的数据,对于这些数据,则不需要追踪它们的传输记录。

ghost管理器也许是在支持128个玩家方面最重要的系统。从一个较高的层次来讲,ghost管理器的工作是复制被认为与指定客户端相关的动态对象。换句话说,服务器给客户端发送关于动态对象的信息,但是仅仅是服务器认为客户端需要知道的对象。游戏模拟层负责决定客户端必须知道什么以及最好知道什么。这赋予了游戏对象一个固有的优先级:“必须知道”的对象优先级最高,“最好知道”的对象优先级较低。为了决定一个对象是否与特定客户端有关,有许多不同的方法可以使用。第9章中将介绍其中的一些方法。总之,确定对象的相关性与游戏本身非常相关。

不管相关对象的集合是如何计算出来的,ghost管理器的任务是从服务器向客户端传输尽可能多的相关对象的状态。ghost管理器来保证最近的数据总是能成功地传输到所有的客户端,对于系统来讲是非常重要的。原因是这里游戏对象的信息通常包括健康状况、武器、弹药数量等,对于这些信息来说,只有最近的信息才是有用的。

当一个对象成为相关对象(或在范围内)时,ghost管理器将给该对象赋予一些信息,这里称为ghost记录。该记录包括唯一的ID、状态掩码、优先级、状态变换(是否该对象已经被标记为在范围内或范围外)。

对于ghost状态的传输,对象的优先级首先由状态变换决定,其次由优先级决定。一旦ghost管理器决定了要传输哪些对象,它们的数据将会被添加到出站数据包中,添加的方法与第5章中介绍的方法类似。

移动管理器的任务是尽快传输玩家的移动数据。如果您曾经玩过快节奏的多人游戏,您可能会体会到精确的移动信息是多么重要。如果一个玩家的位置信息到达晚了,这可能导致其他玩家击中的是该玩家之前的位置,而不是现在的位置,会产生很不好的游戏体验。就玩家而言,快速的移动更新对于减轻对延时的感知是一种重要的方法。

赋予移动管理器较高优先级的另外一个原因是输入数据的捕捉速度是30FPS,意思是每秒钟可以读取输入数据30次,所以最近的数据应该尽快发送出去。更高的优先级意味着,当移动数据可用时,流管理器总是首先给出站数据包添加所有的移动管理器数据。每一个客户端负责传输他们的移动信息到服务器。之后服务器在游戏模拟中使用这些移动信息,然后通知客户端这些移动信息的接收情况。

《星际围攻:部落》网络模型还有一些其他系统,不过这些系统并不是游戏总体设计的关键部分。例如,数据块管理器处理本质上静态的游戏对象的传输,与ghost系统所处理的动态对象不同。这好比一个静态工具(如炮塔),该对象不会移动,但是它在与玩家交互方面是有用的。

与《星际围攻:部落》一样,即时战略(real-time strategy,RTS)游戏《帝国时代》(Age of Empires)也是在20世纪90年代末发布的。也就是说《帝国时代》同样面临拨号网络中带宽和延时的限制。《帝国时代》使用了一个确定性锁步(deterministic lockstep)网络模型。在这个模型中,所有的计算机相互连接,即这个模型是对等网络模型。每个节点都同时运行一个有保证的确定性游戏模拟。锁步是因为节点之间使用通信机制来确保游戏过程中保持同步。与《星际围攻:部落》一样,即使确定性锁步网络模型已经存在了很多年,它仍然在现代即时战略游戏中广泛使用。本书中创建的另外一个游戏案例RoboCat RTS实现的就是确定性锁步网络模型。

为即时战略(RTS)游戏实现网络多人模式与第一人称射击(FPS)游戏的一个最大不同是相关节点的数量。在《星际围攻:部落》中,即使有多达128个玩家,但是在任何特定的时间点,与某一个客户端相关的玩家只有一小部分。这意味着《星际围攻:部落》中的ghost管理器很少需要一次发送多于二三十个ghost。

即时战略游戏,例如《帝国时代》,正好与此相反。尽管玩家的数量小得多(在最初版本中限制最多8个玩家同时在线),但是每一个玩家可以控制大量的游戏单元。最初版本的《帝国时代》限制每个玩家控制的单元数为50,之后增加到200。使用50的限制意味着,在一个大型的8个玩家的战争中,可能同时会有多达400个活跃单元。虽然很自然地想到,如果有某种相关性系统可以减少需要同步的单元,但是还是有必要考虑最坏的情况。假设在战争结束的时候给8个玩家的所有军队做一个特写,将会怎么样?这种情况下,将同时有几百个相关单元。即使每个单元发送很少的信息,依然很难保持同步。

为了解决这个问题,《帝国时代》的工程师决定同步每个玩家的命令,而不是同步单元。在实现过程中一个微妙而重要的区别是,即使是专业的即时战略游戏玩家,每分钟也不可能发出多于300条命令。也就是说即使在极端情况下,游戏每秒钟为每个玩家只需要传输几条命令。相比于在几百个单元之间传输信息来说,对带宽的需求更加可控。然而,因为游戏不再在网络中传输单元信息,游戏的每一个实例需要独立地执行每个玩家发出的命令。因为每个游戏实例执行独立的模拟,那么极为重要的是,每个游戏实例需要与其他游戏实例保持同步。这是在实现确定性锁步网络模型中最大的挑战。

因为每一个游戏实例都执行独立的模拟,所以使用点对点的拓扑结构是有意义的。正如在第6章中所讨论的,对等网络模型的优点是数据可以更快速地到达每一台计算机,因为不需要服务器作为中间人。但是,缺点是每个玩家需要向其他所有玩家发送信息,而不仅仅是一台服务器。例如,如果玩家A发出攻击命令,那么每一个游戏实例都需要被通知到这条攻击命令,否则它们的模拟结果就会不一致。

但是,还有另外一个关键因素需要考虑。不同的玩家运行游戏采用不同的帧速率,同时不同玩家的网络连接质量也不一样。回到刚刚那个例子,玩家A发出攻击命令,但是玩家A没有马上执行这个攻击命令。而是在玩家B、C和D同时准备好执行命令的时候,玩家A才执行攻击命令。这引入了一个难题:如果玩家A的游戏等待执行攻击命令的时间过长,那么游戏看起来响应相当迟缓。

解决这个问题的方法是引入一个轮班计时器,将命令存储在一个队列中。在轮班计时器方法中,首先选择轮班的长度,《帝国时代》中默认的时长是200毫秒。200毫秒之内的所有命令存储在一个缓冲区中。当200毫秒结束时,这个玩家的所有命令将通过网络传输给其他所有玩家。这个系统的另一个关键点是两个轮班之间的执行延迟。意思是,例如一个玩家在50轮发出的命令直到52轮时才被执行。在200毫秒轮班计时器的情况下,这意味着输入延迟,即从一个玩家发出命令到其作用在屏幕上,可能需要高达600毫秒。然而,这两轮的宽限为其他玩家接收和确认某一轮的指令提供了充足的时间。对于即时战略游戏来说,使用轮班机制看起来有点违反直觉,但是在许多不同的即时战略游戏中,包括《星际争霸II》(StarCraft II),您都会看到轮班计时器方法的痕迹。当然,现代游戏有了时间更短的轮班计时器,因为对于大部分用户来说,今天的带宽和延迟状况与20世纪90年代末相比好很多。

在轮班计时器方法中,有一个非常重要的边界情况需要考虑。如果其中一个玩家经历了一个滞后尖峰,导致所有玩家跟不上200毫秒计时器,怎么办?一些游戏可能暂时停止模拟,来看是否可以解决滞后尖峰问题。最后,如果他继续减缓其他玩家的游戏速度,可能会被强行退出。《帝国时代》也试图通过基于网络情况动态调整渲染帧率的方法来弥补这一情况,这样连接在一个非常慢的网络中的计算机可以为网络数据接收分配更多的时间,而花更少的时间来渲染图像。对于动态轮班调整的更多细节,请参考本章最后列出的Bettner和Terrano的文章。

传输由客户端发出的命令还有一个好处。这种方法只需要很少的额外内存和工作来保存在整个比赛过程中发出的命令。这使得实现可保存的比赛重播成为可能,例如在《帝国时代II》(Age of Empires II)中所展现的。在即时战略游戏中,重播功能非常流行,因为它使得玩家可以重新评估比赛来对战略获得更深入的理解。而在传输单元信息而不是命令的方法中,则需要非常多的内存和开支来实现重播功能。

仅仅使用轮班计时器方法不能保证节点之间的同步。因为每台计算机都是独立地接收和处理命令,至关重要的一点是,每台计算机的结果都是一样的。Bettner和Terrano在他们的文章中写道:“发现不同步错误的困难在于非常微妙的差异随着时间的推移会累加。当随机地图刚刚创建的时候,一只鹿的方位有轻微偏离,其觅食路径也有细微差别。几分钟之后,一个猎鹿的村民可能会轻微偏离方向,也可能没有猎中目标,一无所获回到家中。”

一个具体的例子是:大部分游戏有一些随机性的行为。例如,为了确定一个弓箭手是否击中一个步兵,如果游戏执行一个随机检查,将会怎么样?很可能玩家A的实例判断弓箭手击中了步兵,然而玩家B的实例判断弓箭手没有击中步兵。解决这个问题的办法是利用伪随机数生成器(pseudo-random number generator,PRNG)的这个“伪”字。因为所有的伪随机数生成器都使用某个特定的随机种子,所以保证玩家A和B得到的随机结果一样的方法是同步所有游戏实例的种子值。需要注意的是,一个种子仅仅保证得到一个特定的数字序列。所以每个游戏实例不仅要使用相同的种子,而且调用伪随机数生成器的次数也要一样,不然伪随机数生成器的数字将变得不同步。第6章将详细地阐述点对点配置中的伪随机数生成器同步。

检查同步的另一个隐含的优点是减少了玩家作弊的机会。例如,如果一个玩家额外给自己500个资源,那么其他的游戏实例能够立刻检测到游戏状态的不同步,然后将这个玩家踢出游戏。然而,与任何系统一样,存在一个取舍:每个游戏实例都模拟游戏中的每个单元,这就是说很可能通过作弊得到原本不可见的信息。这意味着能展示整个地图的所谓“地图作弊”在绝大多数即时战略游戏中仍然是一个常见的问题。这个问题和其他的安全问题将在第10章中讨论。

网络多人游戏历史悠久。最早起源于在大型机网络系统上运行的游戏,例如运行在柏拉图网络上的《帝国》(Empire,1973)。之后,这些游戏扩展到基于文本的多用户网络游戏。这些多用户网络游戏又扩展到电子布告栏系统,允许用户通过电话线拨号接入。

在20世纪90年代初期,以《毁灭战士》(Doom,1993)为首的局域网游戏在游戏产业中掀起了一阵风潮。这些游戏允许玩家在局域网内连接多台计算机,这些玩家可以是同伴也可以是敌人。随着20世纪90年代末互联网的爆发,在线游戏流行起来,例如《虚幻》(Unreal,1998)。21世纪早期,在线游戏开始在游戏主机中出现。在线游戏中的一种是大规模多人在线游戏,它可以在同一个游戏会话中支持成百上千个玩家。

《星际围攻:部落》(Starsiege: Tribes,1998)实现了一个网络架构,现代动作游戏仍然在沿用。它使用的是客户端-服务器模型,游戏中的每个玩家都连接到协调游戏的服务器上。在最底层,平台数据包模块将通过网络发送数据包这一过程抽象化。接着,连接管理器维持玩家和服务器之间的连接,并提供投递状态通知。流管理器从高层管理器(包括事件管理器、ghost管理器和移动管理器)获得数据,根据优先级,添加这些数据到待发送的数据包中。事件管理器保证重要的事件,例如“玩家开枪”,被相关的游戏部分接收。ghost管理器处理发送被认为与指定客户端相关的动态对象更新。移动管理器为每个玩家发送最近的移动信息。

《帝国时代》(Age of Empires,1997)实现了一个确定性锁步模型。游戏中的所有计算机以点对点的方式彼此连接。游戏并不将每个游戏单元的信息发送到网络中,而是发送命令。这些命令在每个机器节点上被独立评估。为了保证这些节点是同步的,使用轮班计时器来保存一段时间内的命令,随后再发送到网络。这些命令直到两轮之后再执行,这给每个节点足够的时间发送和接收命令。此外,重要的是,每一个节点运行一个确定性模拟,这意味着,比如说,需要同步伪随机数生成器。

1.本地多人游戏和网络多人游戏的区别是什么?

2.本地网络连接的3种不同类型是什么?

3.将网络游戏的运行从局域网转换到互联网的主要考虑是什么?

4. MUD是什么?它发展为什么类型的游戏?

5. MMO与标准在线游戏的区别是什么?

6.在《星际围攻:部落》模型中,哪个系统来提供可靠性?

7.描述一下,当数据包丢失时,《星际围攻:部落》网络模型中的ghost管理器如何重建最小必要的传输。

8.在《帝国时代》的点对点模型中,轮班计时器的目的是什么?每个节点传送什么信息到其他节点?

Bettner, Paul and Mark Terrano. “1500 Archers on a 28.8: Network Programming in Age of Empires and Beyond.” Presented at the Game Developer’s Conference, San Francisco, CA, 2001.

Frohnmayer, Mark and Tim Gift. “The Tribes Engine Networking Model.” Presented at the Game Developer’s Conference, San Francisco, CA, 2001.

Koster, Raph. “Online World Timeline.” Raph Koster’s Website . Last modified February 20, 2002.

Kushner, David. Masters of Doom: How Two Guys Created an Empire and Transformed Pop Culture. New York: Random House, 2003.

Morningstar, Chip and F. Randall Farmer. “The Lessons of Lucasfilm’s Habitat.” In Cyberspace: First Steps, edited by Michael Benedikt, 273-301. Cambridge: MIT Press, 1991.

Wasserman, Ken and Tim Stryker. “Multimachine Games.” Byte Magazine, December 1980, 24-40.

[1] id Software:从事电脑游戏以及游戏引擎方面开发的软件公司,创造了游戏Doom ——译者注。


本章简要叙述TCP/IP协议族和相关协议、互联网通信标准,就其中与多人游戏编程最相关的部分做了深入讨论。

今天我们所知道的互联网与1969年年底出现的四节点网络相去甚远。最初的网络是由美国高级研究计划署开发的,被称为“阿帕网”(ARPANET),用于帮助世界各地的科学家们访问地理上集中或分离的强大计算集群。

阿帕网使用了一项新发明的技术来实现这个目的,这个技术称为分组交换。在分组交换出现之前,长距离系统间传输信息通过一种称为电路交换的过程实现。使用电路交换的系统通过一个连续的电路发送数据,这个连续的电路由许多短电路拼接而成,在信息传输过程中,该电路要始终保持连通。例如,纽约向洛杉矶发送大量的数据,如电话通话,电路交换系统要将许多中间城市的短线路连接成一个连续的电路,在发送完所有信息之前,该电路始终保持连通。在这个例子中,可能使用到的线路包括纽约到芝加哥,芝加哥到丹佛,丹佛到洛杉矶。事实上,这些线路本身也是由距离更近的城市间较短的专用线路连接起来的。在信息传输完成,也就是通话结束之前,该线路始终被占用。之后,系统再将这些线路分配给其他信息传输使用。这为信息传输提供了非常高质量的服务。然而,它限制了这些线路用于最合适的地方,因为这些专用线路一个时刻只能用于一个目的,如图2.1所示。

然而,分组交换取消了电路一个时刻专用于一个传输的限制,提供更高的可用性。它的实现是将传输的信息拆分为小块,称为分组(数据包),基于一种叫作存储转发的技术将它们发送到共享的线路中。网络中的每个节点通过线路与其他节点相连,该线路可以在节点之间传输分组。每个节点存储到来的分组,然后转发给距离目的地更近的节点。例如,从纽约到洛杉矶的电话通话传输中,将通话数据拆分成较小的分组。将它们从纽约发送到芝加哥。当芝加哥节点收到一个分组,检查分组的目的地后,决定将该分组转发给丹佛节点。重复这个过程直到分组抵达洛杉矶,抵达接收者的电话。与电路交换最大的不同是其他的电话通话也可以在同一时间使用相同的线路。从纽约到洛杉矶的其他电话通话,从波士顿到西雅图的电话通话,或者是任意两地之间的通话,都可以在同一时间使用相同的线路传输分组。如图2.2所示,线路一次可以运载来自于许多传输路线中的分组,提高了可用性。

图2.1 电路交换

图2.2 分组交换

分组交换本身仅仅是一个概念。网络中的节点需要一个正式的协议集合来真正定义数据是如何打包成分组,又如何转发到网络中的。对于阿帕网络,BBN Report 1822中定义了这个协议集合,也被称为1822协议。经过了许多年,阿帕网不断发展,成为更大网络的一部分,这个更大网络现在被称为互联网。在这期间,1822协议也在演变,成为驱动今天互联网的协议。它们一起形成了一个协议集合,即TCP/IP协议族

TCP/IP协议族是一个美丽而又可怕的事物。美丽是因为在理论上,它包含塔状的一些抽象得极好的独立层,每一层由许多交换协议支持,来履行它们支持依赖层以及正确传播数据的职责。可怕是因为这些抽象往往被协议作者以性能、可扩展性或者一些值得做但会引入更多复杂性的理由破坏。

作为多人游戏开发者,我们的工作是理解TCP/IP协议族的美和丑,从而确保游戏的功能和效率。通常我们只接触最上层,但是要有效地做到这一点,了解底层以及底层如何影响上层是非常有帮助的。

有许多参考模型来解释互联网通信中层与层之间的交互关系。RFC 1122使用四层定义了早期互联网主机的需求:链路层、IP层、传输层(传送层)和应用层。另一个开放式系统互联(open systems interconnection,OSI)模型使用七层:物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。为了关注与游戏开发者最相关的事情,本书使用一个组合的五层网络模型,即物理层、链路层、网络层、传输层和应用层,如图2.3所示。每层有各自的职责,满足其上层的需求。代表性的职责包括:

图2.3 游戏开发者眼中的TCP/IP模型

然而每层执行其职责的方式并不是在该层的定义中确定的。事实上,每层都有各种各样的协议用于执行它的职责,一些协议和TCP/IP协议族一样古老,而一些协议是刚刚创造出来的。对于熟悉面向对象编程的读者来说,可以这样来想象层和协议之间的关系:每个层次是接口,协议或者协议的集合是接口的实现。理想情况下,一个层次的实现细节是脱离于协议族上层的,但是正如前面所提到的,事实上往往不是这样。本章剩余部分简要叙述TCP/IP协议族的每个层次以及实现它们所常用的协议。

TCP/IP模型最底层是最基本的支持层,即物理层。物理层为网络中的计算机或主机提供物理连接。信息传输所必需的是物理介质。Cat 6双绞线、电话线、同轴电缆和光纤等物理介质都可以提供物理层所需的连接。

请注意,物理连接不一定是可触摸的。任何一部移动电话、平板电脑或者笔记本电脑都可以说明,无线电波也可以为信息传播提供一个完美的物理媒介。有一天,倘若量子缠绕技术可以提供以极快的速度进行长距离信息传输的物理媒介,到那时,它将被网络的层次模型接受为物理层的一个有效实现方式。

链路层是网络模型中真正计算机科学发挥作用的开始。它的任务是提供一种网络实体之间通信的方法。即链路层必须提供一种方法,该方法可以实现源主机封装信息、通过物理层传输信息、目的主机接收封装好的信息并从中提取所需的信息。

链路层的数据传输单元称为。实体之间通过链路层彼此发送帧数据。更具体地说,链路层的职责包括:

请注意帧的传输是不可靠的。有许多因素影响电子信号无差错抵达接收方。物理介质的损坏、一些电子干扰或者设备故障都有可能导致帧丢失而无法投递。链路层不做任何操作来确认帧是否抵达接收方或者保证如果帧没有抵达重新发送。因此,链路层的通信是不可靠的。任何需要可靠数据传输的上层协议都必须自己来实现这一点。

对于每种被选择作为物理层实现的物理介质,都有对应的协议或协议族来提供链路层所需要的服务。例如,通过双绞线连接的主机可以使用以太网协议的一种进行通信,例如1000BASET;通过无线电波连接的主机可以使用短程无线网络协议(例如802.11g、802.11n、802.11ac)或者远程无线网络协议,例如3G或4G。表2.1列举了一些常用的物理介质和链路层协议的对应关系。

表2.1 物理介质和链路层协议的对应关系

物理介质

链路层协议

双绞线

Ethernet 10BASET, Ethernet 100BASET, Ethernet 1000BASET

双绞铜线

Ethernet over copper (EoC)

2.4GHz无线电波

802.11b, 802.11g, 802.11n

5GHz无线电波

802.11n, 802.11ac

850MHz无线电波

3G, 4G

光纤

光纤分布式数据接口(fiber distributed data interface,FDDI)Ethernet 10GBASESR,Ethernet 10GBASELR

同轴电缆

Ethernet over coax,有线电缆数据服务接口规范(data over cable service interface specification,DOCSIS)

由于链路层实现和物理层介质关系如此紧密,一些网络模型将这两层合并为一层。然而,因为一些物理介质支持的链路层协议不止一种,所以还是将它们视为两层比较好。

需要注意的是,两个远距离的主机之间的网络连接不会仅仅使用一种物理介质和一种链路层协议。正如下面要介绍的其他层一样,在传输一个数据包时可能会同时用到许多介质和链路层协议。所以,网络游戏中的数据传输也会使用到表中所列出的许多链路层协议。幸运的是,由于TCP/IP模型的抽象,链路层协议的细节大多隐藏在游戏背后。因此我们不需要探究现有链路层协议的内部工作细节。但是,有一个链路层协议族需要重点说明,该协议族既清晰地阐述了链路层的功能,同时几乎确定会以某种方式影响网络游戏编程者的工作,即以太网(Ethernet)。

注释:

以太网(Ethernet)不是一个协议,而是基于以太网蓝皮书的一组协议,以太网蓝皮书是由美国DEC、Intel和Xerox公司于1980年发布的。同时,现代的以太网协议是在IEEE 802.3基础上定义的。用于光纤、双绞线或者铜电缆上的以太网协议各种各样。各种类型的速度也不同:在撰写本书的时候,大部分的台式机支持吉比特以太网的速度,但是10GB的以太网标准已经出现并逐步流行。

为了给每台主机一个唯一标识,以太网引入了介质访问控制地址(media access control address)的概念,也就是MAC地址。MAC地址在理论上是一个48比特数字,唯一分配给连接在以太网网络中的每个硬件。这个硬件通常是网卡(network interface controller,NIC)。最初,网卡是扩展卡。但是在过去的几十年里,随着互联网的盛行,网卡已经嵌入到大多数的主板中。当主机需要创建网络中的多个连接,或者连接到多个网络时,添加额外的网卡作为扩展卡也是很常见的。这样一台主机就有了多个MAC地址,每一个MAC地址对应一个网卡。

为了保证MAC地址的唯一性,网卡生产商在硬件的生产过程中就将MAC地址烧到了网卡中。MAC地址中的前24比特叫作组织唯一标识符(organizationally unique identifier,OUI),是由IEEE给厂家分配的唯一代码。后24比特是由厂家自己分配的,来保证所生产的硬件是唯一标识的。这样,每一个生产的网卡都有一个硬编码,可以被寻址到的统一的唯一标识符。

MAC地址是一个如此有用的概念,所以不仅使用在以太网中。实际上,它已经用于大多数的IEEE 802系列的链路层协议中,包括无线网和蓝牙。

注释:

 

自MAC地址出现以来,它已经在两个重要方面得到了发展。第一,作为真正的唯一硬件标识符,它不再可靠,现在许多网卡允许软件任意修改MAC地址。第二,为了解决各种各样的问题,IEEE已经引入64比特MAC地址的概念,称为扩展的以太网接口标识符(extended unique identifier,EUI64)。必要时,通过在OUI的右侧插入两个字节FFFE将48比特MAC地址转换为EUI64。

给每台主机分配了唯一的MAC地址之后,定义了以太网链路层帧的格式,如图2.4所示。

对于每个数据包,其前导序列(preamble)和帧开始标志(start frame delimiter,SFD)都是一样的,包含十六进制值0x55 0x55 0x55 0x55 0x55 0x55 0x55 0xD5。它是一个二进制模式,帮助底层硬件同步和准备接收到来的帧。通常,网卡将前导序列和帧开始标志从数据包中剥离出来,剩下的字符被传递到以太网模块进行处理。

图2.4 以太网的帧格式

帧开始标志后面的6个字节表示帧的接收方的MAC地址。有一种特殊的MAC地址FF:FF:FF:FF:FF:FF称为广播地址,表示同时向局域网中的所有主机都发送该帧。

帧长度/类型域(length/type)是表示帧的长度或者类型的共用的域。当表示帧长度时,它指定了帧中数据部分的字符数。当表示帧类型时,它包含了一个以太网类型(EtherType),唯一标识了用于解释数据的协议。当以太网模块收到帧长度/类型域,它必须确定解释它的正确方法。为了帮助解释,以太网标准规定帧内封装的报文数据最大长度为1500字节。因为这是一次传输中所能传递数据的最大容量,所以称为最大传输单元(maximum transmission unit,MTU)。以太网标准还定义了以太网类型的最小值是0x0600,即1536。所以,如果帧长度/类型域的取值小于等于1500,它表示帧长度;如果取值大于等于1536,它表示协议类型。

注释:

 

虽然没有统一的标准,许多现代以太网网卡支持最大传输单元大于1500字节。这些巨型帧(jumbo frame)的长度通常可以高达9000字节。为了支持这个长度,他们在帧头指定一种以太网类型,然后基于传入的数据同时依赖底层硬件来计算帧的大小。

payload是帧内封装的报文数据。通常这是一个网络层数据包,通过在链路层交付到适当的主机。

帧检验序列(frame check sequence,FCS)是由两个地址域、帧长度/类型域、数据域和其他填充信息生成的循环冗余检验(cyclic redundancy check,CRC32)值。这样,当以太网硬件读入数据时,它可以检查任何发生在传输中的数据损坏和帧丢失。尽管以太网是不可靠传输,但还是尽力防止错误数据的传输。

通过物理层传输以太网数据包的具体方式根据介质不同而多种多样,这不是多人游戏编程者所关心的问题。只要知道,网络中的每台主机接收帧,读取帧之后确定它是否是接收者。如果是,提取帧中的数据域,并根据帧长度/类型域的取值处理这些数据。

 

起初,大部分小型的以太网网络使用集线器(hub)将多台主机连接起来。一些老的网络甚至使用长距离的同轴电缆连接计算机。在这些类型的网络中,以太网数据包的电子信号被发送到网络中的每台主机,由主机来决定该数据包是否发往自己。当网络规模增大时,这种方法已经失效。随着硬件成本的下降,大部分现代的网络使用交换机(switch)连接主机。交换机会记录连接在它们上面的主机的MAC地址,有时是IP地址,所以大部分的数据包可以尽可能地选择最短的路径到达接收方,而不需要访问网络中的每台主机。

链路层提供了将数据从一台可寻址的主机发送到另外一台或多台同样可寻址的主机的一种清晰的方式。因此,您可能不清楚为什么TCP/IP模型还需要更多的层次。这是因为链路层有许多不足需要上层来弥补:

网络层的任务是在链路层基础上提供一套逻辑地址的基础设施。这样,主机硬件可以很容易地更换,主机群可以划分为子网,在两个遥远的子网中的主机可以使用不同的链路层协议和物理介质相互发送信息。

 

今天,实现网络层需求最常用的协议是互联网协议第四版(Internet protocol version 4,IPv4)。IPv4定义了一个为每台主机单独标识的逻辑寻址系统,一个定义地址空间的逻辑分段作为物理子网的子网系统,一个在子网之间转发数据的路由系统。

2.5.1.1 IP地址和数据包结构

IPv4的核心是IP地址。IPv4的IP地址是32比特数字,通常以英文句号分隔的4个8比特数字的形式展示。例如www.usc.edu的IP 地址是128.125. 253.146,www.mit.edu的IP地址是23.193.142.184。这里英文句号读作“点(dot)”。互联网上的每台主机都有唯一的IP地址之后,发送方要想将数据包投递给接收方,只需在数据包的包头中指定接收方的IP地址即可。但是IP地址的唯一性有一个例外,我们将在后面的章节“网络地址转换”中解释。

IP地址定义之后,IPv4进而定义了IPv4数据包的结构。IPv4数据包包括包
头和数据,包头由实现网络层功能所必需的数据组成,数据包括所需要传输的上层数据。图2.5展示了一个IPv4数据包的结构。

图2.5 IPv4数据包结构

版本号(Version):长度4比特,标识目前采用的IP协议的版本号。对于IPv4,该取值为4。

IP数据包的包头长度(Header length):长度4比特,描述IP包头的长度,以32比特(4个字节)为单位。因为在IP数据包的包头中有变长的可选部分,所以IP数据包的包头长度是可变的。IP数据包的包头长度字段明确标识了包头到哪里结束,封装的数据从哪里开始。因为该域的长度是4比特,所以其最大值为15,意思是包头的长度最长是15个32比特字,即60个字节。因为包头有20个字节的必需信息,所以该字段的最小取值是5。

服务类型(Type of service):长度8比特,用于从拥塞控制到差异化服务识别的各种目的。更多的内容请参考本章最后列出的RFC 2474 和RFC 3168。

IP数据包的包总长(Total length):长度16比特,以字节为单位计算的IP数据包的长度,包括头部和数据。因为16比特所能标识的最大数是65535,所以IP数据包的最大长度是65535字节。因为IP数据包的包头至少是20个字节,所以IPv4数据包中的数据部分最大长度为65515字节。

片标识符(Fragment identification,16比特)片标记(Fragment flag,3比特片偏移(Fragment offset,13比特:用于重组分片数据包,将在后面的章节“分片”中解释。

生存时间(Time to live,TTL):长度8比特,用于限制数据包转发的次数,将在后面的章节“子网和间接路由”中解释。

协议(Protocol):长度8比特,标识用于解释数据内容所使用的协议。类似以太网帧中的以太网类型域(EtherType),用于解释上层封装数据。

头部检验和(Header checksum):长度16比特,用于IPv4头部的正确性检测。注意,这仅仅针对头部数据,数据部分如果需要确保完整性,则交由上层来保证。通常,这不是必须的,因为许多链路层协议已经包含一个帧的正确性检验,例如以太网帧头的帧检验序列(FCS)。

源地址(Source address):长度32比特,是数据包发送方的IP地址。目标地址(Destination address):长度32比特,既可以是数据包接收方的IP地址,也可以是发送给多台主机的特殊地址。

注释:

 

有一个令人困惑的地方:IP数据包的包头长度的单位是32比特,而IP数据包总长的单位是8比特。这说明节约带宽是多么重要。因为所有的数据包头长度都是4个字节的整数倍,它们的字节长度都能被4整除,所以字节长度的后两个比特永远是0。这样指定IP数据包的包头长度的单位是32比特可以节省两个比特。尽可能节约带宽是多人游戏编程的黄金法则。

2.5.1.2 直连路由和地址解析协议

为了理解IPv4是如何在不同链路层协议的网络中传输数据包的,首先需要理解它如何在使用一种链路层协议的网络中传输数据包。IPv4允许数据包的目标地址是IP地址。而使用链路层发送数据包时,帧中包含的是链路层能够理解的地址。考虑图2.6所示的网络,主机A如何向主机B发送数据。

图2.6 三台主机的网络

图2.6所示的网络中包含连接在以太网中的三台主机,每台主机有一个网卡。主机A想要给IP地址为18.19.0.2的主机B发送一个网络层数据包。主机A准备了一个包含源IP地址为18.19.0.1和目标IP地址为18.19.0.2的IPv4数据包。理论上,网络层接着将数据包交付给链路层进行实际的传输。不幸的是,以太网模块不能处理一个只包含IP地址的数据包,因为IP是网络层的概念。链路层需要一些方法来找出IP地址18.19.0.2所对应的MAC地址。幸运的是,有一个链路层协议提供了这样一种方法,即地址解析协议(address resolution protocol,ARP)。

注释:

 

ARP在技术上是一个链路层协议,因为它直接使用链路层的地址形式发送数据包,而不需要网络层提供的路由。然而,由于该协议包括了网络层的IP地址进而破坏了网络层的抽象,所以可以将其视为两层之间的桥梁,而不仅仅是一个链路层协议。

ARP包含两部分:用于查询IP地址所对应MAC地址的报文结构和记录它们之间映射关系的对应表。表2.2展示了一个ARP对应表的示例。

表2.2 从IP地址到MAC地址映射的ARP对应表

IP地址

MAC地址

18.19.0.1

01:01:01:00:00:10

18.19.0.3

01:01:01:00:00:30

当网络层需要使用链路层向一台主机发送数据包时,首先查询ARP对应表获取目标IP地址所对应的MAC地址。如果在对应表中找到了MAC地址,那么IP模块使用该MAC地址构造一个链路层帧,将该帧发送给链路层实现传输。但是,如果表中没有找到映射,那么ARP模块通过给链路层网络中所有可达的主机发送ARP报文(如图2.7所示),来获得对应的MAC地址。

图2.7 ARP报文格式

硬件类型(Hardware type):长度16比特,指明了链路层所使用的硬件接口类型,以太网的取值为1;

协议类型(Protocol type):长度16比特,指明了正在使用的网络层协议所对应的以太网类型(EtherType)值。例如IPv4为0x0800。

硬件地址长度(Hardware address length):长度8比特,指明了以字节为单位的链路层硬件地址长度。在大部分情况下,该取值为MAC地址的长度,即6个字节。

IP地址长度(Protocol address length):长度8比特,指明了以字节为单位的网络层协议逻辑地址长度。对于IPv4,该取值为IP地址的长度,即4个字节。

操作类型(Operation):长度16比特,取值是1或者2,表示报文是信息请求还是响应。

发送方硬件地址(Sender hardware address):长度可变,是报文发送方的硬件地址。发送方IP地址(Sender protocol address):长度可变,是报文发送方的网络层地址。这些地址的长度要与报文前面指定的长度一致。

目标硬件地址(Target hardware address)目标IP地址(Target protocol address):均为长度可变,分别是报文接收方的硬件地址和IP地址。如果报文类型是请求,那么目标硬件地址是未知的,被接收方忽略。

继续之前的例子,如果主机A不知道主机B的MAC地址,那么它准备的ARP请求报文的操作类型为1,发送方IP地址为18.19.0.1,发送方硬件地址为01:01:01:00:00:10,目标IP地址为18.19.0.2。然后将该ARP报文封装为一个以太网帧,发送到以太网广播地址FF:FF:FF:FF:FF:FF。回想一下,这个地址的意思是将以太网帧发送到网络中的每台主机。

当主机C收到此报文,由于它的IP地址与报文中的目标IP地址不符,所以它不做响应。但是,当主机B收到此报文,它的IP地址与报文中的目标IP地址一致,它将准备一个自己的ARP报文作为响应,该报文中的发送方地址是它自己,目标地址是主机A。当主机A收到该报文,主机A将主机B的MAC地址信息更新到ARP对应表中,然后将这个等待的IP数据包封装为以太网帧,发送给主机B的MAC地址。

注释:

 

当主机A向互联网上的所有主机广播它的初始ARP请求时,该请求包含主机A的MAC地址和IP地址。这给了网络上其他所有主机将主机A的信息更新到ARP对应表的机会,即使它们当前不需要该信息。这样做使它们和主机A的通信变得方便,因为之后不需要发送ARP请求报文了。

您可能会注意到,这个系统存在一个有趣的安全漏洞!一台恶意主机可以向所有IP地址发送ARP报文。如果没有一种方法来验证ARP信息的真实性,路由器可能无意中将本属于一台主机的数据包错误地发送给恶意主机。这不仅允许了嗅探数据包,还可能造成被窃取的数据包无法到达原本的目的主机,彻底扰乱了互联网的正常秩序。

2.5.1.3 子网和间接路由

假设有两家大公司Alpha公司和Bravo公司。每个公司都有它们自己的大型内部网络,分别是Alpha网络和Bravo网络。Alpha网络包含100台主机,主机A1到A100。Bravo网络也包含100台主机,主机B1到B100。两个公司希望它们的网络可以互联,以便相互发送消息,但是直接将它们的网络用链路层以太网电缆连接起来会带来一些问题。因为每一个以太网数据包都必须发送给互联网上的每台主机,那么如果Alpha网络和Bravo网络在链路层相连,会导致每个以太网数据包将发送给200台主机,而不是100台,导致整个互联网流量增加一倍。同时存在安全风险,Alpha网络的所有数据包都会发送给Bravo网络,而不仅仅只是需要发送给Bravo网络的数据包。

为了让Alpha公司和Bravo公司的网络有效互联,网络层引入了一种在链路层并不直接相连的主机间路由数据包的机制。事实上,互联网本身的设计初衷就是通过长距离线缆连接遍布全国的小型网络。互联网的前缀“inter”意思是“between(之间)”,就表示这些长距离连接。网络层的任务就是使网络之间的这种交互成为可能。图2.8展示了Alpha网络和Bravo网络之间的网络层连接。

主机R是一台特殊的主机,称为路由器。一个路由器有多个网卡,每个网卡有它自己的IP地址。在这个例子中,一个网卡连接Alpha网络,另一个网卡连接Bravo网络。注意,Alpha网络的所有IP地址的前缀都是18.19.100,Bravo网络的所有IP地址的前缀都是18.19.200。为了理解这为什么有助
于我们的实现目标,需要更详细地介绍子网。我们首先定义子网掩码(subnet mask)的概念。

图2.8 相互连接的Alpha网络和Bravo网络

子网掩码(subnet mask)是一个32比特数,通常写成以英文句号分隔的4个8比特数字,与IP地址的表示方法一样。如果主机的IP地址与子网掩码做按位与运算得到的结果相同,那么这些主机在同一个子网中。例如,一个子网的掩码是255.255.255.0,那么18.19.100.1和18.19.100.2都是这个子网的有效IP地址(如表2.3所示)。但是18.19.200.1不在这个子网中,因为它与子网掩码做按位与运算后得到的结果不同。

表2.3 P地址和子网掩码

主机

IP地址

子网掩码

IP地址与子网掩码按位与运算结果

A1

18.19.100.1

255.255.255.0

18.19.100.0

A2

18.19.100.2

255.255.255.0

18.19.100.0

B1

18.19.200.1

255.255.255.0

18.19.200.0

以二进制形式表示,子网掩码由1和0组成,且1和0分别连续,左边是1,右边是0,这样易读,而且便于二进制运算。表2.4中列举了一些典型的子网掩码和这些子网的主机数目。注意子网中有两个地址是保留的,不被其他主机使用。一个是网络地址,子网中任意的IP地址与子网掩码按位与操作的结果。另一个是广播地址,由子网掩码按位非操作的结果与网络地址按位或运算得到,即网络地址中不能定义子网的二进制位都设置为1。标记为广播地址的数据包应该被发送到子网中的每台主机。

表2.4 子网掩码举例

子网掩码

二进制形式的子网掩码

有效位数

主机数目最大值

255.255.255.248

11111111111111111111111111111000

29

6

255.255.255.192

11111111111111111111111111000000

26

62

255.255.255.0

11111111111111111111111100000000

24

254

255.255.0.0

11111111111111110000000000000000

16

65534

255.0.0.0

11111111000000000000000000000000

8

16777214

根据定义,子网是IP地址与子网掩码做按位与运算后结果相同的一群主机,所以子网可以通过子网掩码和网络地址定义。例如,Alpha网络的子网定义为网络地址为18.19.100.0,子网掩码为255.255.255.0。

有一种常见的方法来简化这些信息,称为无类别域间路由(classless inter- domain routing,CIDR)。二进制形式的子网掩码由n个1加(32-n)个0组成。因此,子网可以表示为它的网络地址,后跟一个斜杠,接着是子网掩码的有效位数。例如,图2.8中Alpha网络的子网写成CIDR的形式是18.19.100.0/24。

注释:

 

CIDR中的术语“无类别”出自这样一个事实:域间路由和地址块分配曾经基于三类特殊大小的子网。A类网络的子网掩码是255.0.0.0,B类网络的子网掩码是255.255.0.0,C类网络的子网掩码是255.255.255.0。对于更多CIDR的演变说明,请参考本章最后列出的RFC 1518。

子网定义之后,IPv4规范提供了一种在不同网络的主机之间传输数据包的方法。实现该方法的关键是每台主机IP模块中的路由表(routing table)。具体来说,当一台主机的IPv4模块向另一台远程主机发送数据包时,首先决定是使用ARP表及直连路由,还是使用间接路由。为了辅助这个过程,每个IPv4模块包含一个路由表。对于每一个可达的目标子网,路由表包含一行信息,内容是如何将数据包发送到这个子网。对于图2.8的网络,主机A1、B1和R的路由表如表2.5、表2.6和表2.7所示。

路由表中,目标子网指的是包含目标IP地址的子网,网关是指在当前子网通过链路层发送数据包的下一台主机的IP地址,要求这台主机可以通过直达路由可达。如果网关域为空,表示整个目标子网是可以通过直达路由可达的,数据包可以直接通过链路层发送。最后,网卡指的是转发数据包的网卡。通过这个机制,数据包可以通过一个链路层网络接收,再转发到另一个链路层网络。

表2.5 主机A1的路由表

行数

目标子网

网关

网卡

1

18.19.100.0/24

NIC 0 (18.19.100.2)

2

18.19.200.0/24

18.19.100.1

NIC 0 (18.19.100.2)

表2.6 主机B1的路由表

行数

目标子网

网关

网卡

1

18.19.200.0/24

NIC 0 (18.19.200.2)

2

18.19.100.0/24

18.19.200.1

NIC 0 (18.19.200.2)

表2.7 主机R的路由表

行数

目标子网

网关

网卡

1

18.19.100.0/24

NIC 0 (18.19.100.1)

2

18.19.200.0/24

NIC 1 (18.19.200.1)

当IP地址为18.19.100.2的主机A1发送数据包给IP地址为18.19.200.2的主机B1,将发生下面的过程:

1.主机A1创建一个IP数据包,其源地址是18.19.100.2,目标地址是18.19. 200.2。

2.主机A1的IP模块从上到下查找路由表的每一行,直到找到目标子网中包含IP地址为18.19.200.2的第一行。在这个例子中是第二行。注意路由表中行的顺序很重要,因为可能多行对应一个地址。

3.第二行的网关是18.19.100.1,所以主机A1使用地址解析协议(ARP)和它的以太网模块将数据包封装为以太网帧,然后将它发送到IP地址18.19.100.1对应的MAC地址,也就是发送到主机R。

4.主机R的以太网模块的网卡是0,IP地址是18.19.100.1,收到这个帧,发现其数据部分是一个IP数据包,所以将其传递给IP模块。

5.主机R的IP模块发现这个数据包的地址是18.19.200.2,所以试图将该数据包转发给18.19.200.2。

6.主机R的IP模块查找路由表,直到找到目标子网中包含IP地址为18.19.200.2的第一行。在这个例子中是第二行。

7.第二行网关域为空,意味着这个子网是直达路由。然而,网卡列表明18.19.200.1的IP地址使用的是网卡1。这是连接到Bravo网络的网卡。

8.主机R的IP模块将数据包发送给主机R网卡1的以太网模块。它使用地址解析协议(ARP)和以太网模块将数据包封装为以太网帧,然后将它发送到IP地址18.19.200.2对应的MAC地址。

9.主机B1的以太网模块收到这个帧,发现其数据部分是一个IP数据包,所以将其传递给IP模块。

10.主机B1的IP模块发现其目标IP地址是自己,发送数据部分到上层做进一步处理。

这个例子表明两个精心配置的网络如何通过间接路由进行通信,但是这些网络如何将数据包发送到互联网的其他网络呢?在这种情况下,他们首先需要从互联网服务提供商(internet service provider,ISP)获得一个有效的IP地址和网关。对于我们的目标,假设ISP分配给他们一个IP地址18.181.0.29和一个网关18.181.0.1。那么网络管理员必须在主机R上安装一个额外的网卡,使用这个分配的IP地址进行配置。最后,更新主机R和网络中所有主机的路由表。图2.9展示了这个新的网络配置,表2.8、表2.9和表2.10展示了修改后的路由表。

图2.9 连接在互联网上的Alpha网络和Bravo网络

表2.8 与互联网接入的主机A1的路由表

行数

目标子网

网关

网卡

1

18.19.100.0/24

NIC 0 (18.19.100.2)

2

18.19.200.0/24

18.19.100.1

NIC 0 (18.19.100.2)

3

0.0.0.0/0

18.19.100.1

NIC 0 (18.19.100.2)

表2.9 与互联网接入的主机B1的路由表

行数

目标子网

网关

网卡

1

18.19.200.0/24

NIC 0 (18.19.200.2)

2

18.19.100.0/24

18.19.200.1

NIC 0 (18.19.200.2)

3

0.0.0.0/0

18.19.200.1

NIC 0 (18.19.200.2)

表2.10 与互联网接入的主机R的路由表

行数

目标子网

网关

网卡

1

18.19.100.0/24

NIC 0 (18.19.100.1)

2

18.19.200.0/24

NIC 1 (18.19.200.1)

3

18.181.0.0/24

18.181.0.1

NIC 2 (18.181.0.29)

4

0.0.0.0/0

18.181.0.1

NIC 2 (18.181.0.29)

注释:

 

对于互联网来说,互联网服务提供商(ISP)并不是一个特殊的构造。它仅仅是一个大的组织,有非常非常多的IP地址。有趣的是,它的主要工作是将这些IP地址分成子网,将这些子网出租给其他组织使用。

目标子网0.0.0.0/0称为默认网络(default address),因为它定义了一个包含所有IP地址的子网。如果主机R收到一个数据包,其目标子网与路由表中的前三行都不一致,那么目标一定匹配最后一行。在这种情况下,数据包将通过对应的网卡转发给ISP的网关,它能够将数据包在网关之间转发,最终在该数据包的目标子网处终止。同样地,主机A1和B1也有新的条目作为默认网络,以便它们可以将数据包转发给主机R,进而转发给ISP。

数据包每经过一个路由器,IPv4头部的生存时间(TTL)的值减1。当TTL减少为0,路由器将会丢弃收到的TTL=0的IP数据包。避免IP数据包在网络中的无限循环和收发。改变TTL需要重新计算头部检验和(header checksum),增加了主机处理和转发数据包的时间。

TTL为0不是数据包被丢弃的唯一原因。例如,如果数据包到达路由器网卡的速度太快,网卡来不及处理可能会忽略它们。或者如果数据包到达一个有很多网卡的路由器,但是所有的数据包都需要从一个网卡转发出去,这个网卡来不及处理可能会忽略它们。在IP数据包从源到目的的整个转发路径上,这些只是数据包可能被丢弃的一些原因。所以,网络层的所有协议,包括IPv4,都是不可靠的。意思是不保证发送出去的IPv4数据包能够到达目标地址。即使数据包到达了,也不保证它们是顺序到达的,而且只到达一次。网络堵塞可能导致路由器选择不同的路径发送数据包,由于这些路径长度不同,所以可能引起后发送的数据包反而先到达。有时同样的数据包由多个路由器发送,导致多次到达。所以不可靠的意思是不保证数据传输和传输的顺序。

重要的IP地址

 

有两个特殊的IP地址值得一提。第一个是127.0.0.1,称为回路地址(loopback)或者本地地址(localhost address)。如果要求IP模块发送数据包到127.0.0.1,它不会发送到任何地方。而是处理为刚刚收到数据包,并将其发送到下一层进行处理。技术上,整个127.0.0.0/8的地址块均为本地地址,但是一些操作系统的防火墙默认只允许标记为127.0.0.1的数据包这样做。

第二个地址是255.255.255.255,称为受限的广播地址(zero network broadcast address)。意思是,数据包会被发送到相同链路层网络的所有主机,但不被路由器发送。通常的实现方法是将数据包打包成链路层帧,并发送到广播MAC地址FF:FF:FF:FF:FF:FF。

2.5.1.4 分片

正如之前所提到的,以太网帧的最大传输单元(maximum transmission units,MTU)是1500个字节。然而,之前也说过,IPv4包的最大传输单元是65535个字节。这带来了一个问题:如果IP数据包必须封装为链路层帧来传输,IP数据包的长度比链路层的最大传输单元长怎么办?答案是分片(fragmentation)。如果IP模块要传输的IP数据包比链路层的最大传输单元大,它就要被分割成一些数据包的长度为链路层最大传输单元的小片断。

IP分片数据包与普通的IP数据包类似,只需在头部设置一些值,即使用片标识符(fragment identification)、片标记(fragment flag)和片偏移(fragment offset)这三个域。当IP模块将IP数据包分割成一组小的片断时,为每一个片断创建一个新的IP数据包,并设置这些域的值。

片标识符(fragment identification,16比特)的值标识原始的数据包。这一组所有被拆分的分片数据包被标记相同的值。

片偏移(fragment offset,13比特)表示以8字节为单位,该IP数据包从开始到属于这个分片数据包的位置。这一组所有被拆分的分片数据包被标记不同的值。一个65535字节的数据包的最大片偏移取值是13个比特,所以要求所有的偏移是8字节的整数倍,因为这是偏移量所能取得的最高精度。

除了最后一个片断的所有分片数据包,片标记(fragment flag,3比特)都设置为0x4,称为MF(more fragments flag),表示还有更多的分片数据包。如果一台主机收到包含有该值的数据包,必须等收齐该组所有的分片数据包之后,才能将重建的数据包传输给上层。最后一个分片数据包不需要这个标记,因为片偏移是非零值,同样表明它是分片组中的一员。事实上,最后一个分片数据包不再使用这个片标记域,来表明原始数据包中再没有更多的分片数据包了。

注释:

 

片标记域还有另外一个功能。如果IP数据包的原始发送者将其设置为0x2,称为DF(don't fragment flag),指明在任何情况下,这个数据包都不能被分片。反之,如果IP模块必须通过链路层转发一个比最大传输单元大的数据包,这个数据包将被丢弃。

表2.11展示了一个大的IP数据包和使用以太网链路转发必须拆分成的三个分片数据包的头部相关域的取值。

表2.11 需要分片的IPv4数据包

原始数据包取值

分片数据包1取值

分片数据包2取值

分片数据包3取值

版本号(version)

4

4

4

4

IP数据包的包头长度(header length)

20

20

20

20

IP数据包的包总长(total length)

3020

1500

1500

60

标识符(identification)

0

12

12

12

片标记(fragment flags)

0

0x4

0x4

0

片偏移(fragment offset)

0

0

185

370

生存时间(time to live)

64

64

64

64

协议(Protocol)

17

17

17

17

源地址(Source Address)

18.181.0.29

18.181.0.29

18.181.0.29

18.181.0.29

目标地址(Destination Address)

181.10.19.2

181.10.19.2

181.10.19.2

181.10.19.2

数据(Payload)

3000 bytes

1480 bytes

1480 bytes

40 bytes

片标识符的取值均为12,表明这三个分片数据包都属于同一个原始数据包。12这个数的取值是任意的,但是极有可能这是主机发送的第12个被拆分的数据包。第一个分片数据包设置了片标记,片偏移取值为0,表明它包含原始包的起始数据。注意这个数据包的总长是1500。IP模块通常创建尽可能大的分片数据包,限制分片数据包的数量。因为IP头长度是20字节,所以分片数据包数据部分是1480字节。也就是说,第二个分片数据包从偏移为1480开始。但是,因为片偏移以8个字节为单位,所以第二个分片数据包的偏移量为1480/8=185。第二个分片数据包仍然需要设置片标记。最后,第三个分片数据包的偏移是370,不需要设置片标记,表明是最后一个分片数据包。第三个分片数据包的总长度是60,因为原始数据包的总长是3020,即有3000字节的数据,前两个分片数据包的数据长度都是1480,所以第三个分片数据包的数据长度是40。

这些分片数据包被发送出去之后,它们中的某些或者全部都有可能进一步被分片。如果到达目的主机的道路上经过最大传输单元更小的链路层,就会发生这一情况。

要想目标机器能够正确地处理数据包,每一个分片数据包都需要抵达目的主机,并且能够重建成原始数据包。因为网络堵塞,动态地改变路由表,或者其他原因,都有可能导致数据包乱序到达,或者与其他来自相同或者不同主机的其他数据包交错到达。但第一个分片数据包无论何时到达,接收方的IP模块都有足够的信息确定这个数据包是个分片数据包,而不是原始数据包。这个信息就是设置的片标记或者是非零的片偏移。这时候,接收方的IP模块创建一个64KB(最大的数据包长度)的缓冲区,将这个分片的数据部分拷贝到缓冲区合适的位置。使用发送方的IP地址和片标识来标记这个缓冲区,这样当其他分片数据包到达时,通过匹配发送方IP地址和片标识域来选取合适的缓冲区存储新分片数据包的数据。当没有设置片标记域的分片数据包到达时,接收方通过将这个分片数据包的数据长度加到原始数据包的偏移量上,计算原始数据包的总长度。当原始数据包的所有数据都到达了,IP模块将重建好的数据包发送给上层做进一步处理。

小窍门:

 

尽管IP数据包分片技术使得发送大数据包成为可能,但是存在两种低效率的情况。第一,增加了网络上发送的数据量。表2.11展示了一个3020字节的数据包被拆分成两个1500字节的数据包和一个60字节的数据包,合计3060字节。这虽然不是一个可怕的数字,但是这个数字可以累加。第二,如果一个分片数据包在传输过程中丢失了,那么接收方必须丢弃整个数据包。这意味着大的数据包丢失分片数据包的概率更大。所以,建议通过保证所有的IP数据包长度都小于链路层最大传输单元,尽量避免使用分片技术。这并不容易,因为两台主机之间有许多不同的链路层协议,想象一下从纽约到日本的数据包。但是两台主机之间的链路层协议至少有一个是以太网协议,这种情况是很有可能的,所以游戏开发者做这样的近似:数据包MTU的最小值为1500字节。这1500字节必须包括20字节的IP头、IP数据和任何协议,例如VPN或者IPSec,需要使用的额外数据。所以,最好将IP包的数据限制在1300字节以内。

乍一想,将数据包的长度限制得更小一些,例如100字节,可能会更好。因为如果1500字节的数据包可能不需要分片,那么100字节的数据包就更不需要分片了,对吗?这可能是对的,但是想一下,每个数据包需要20字节的头部数据。如果一个游戏只发送长度为100字节的数据包,那么20字节的IP头部数据将占用20%的带宽,这样效率太低。所以,一旦你决定当前最小MTU是1500,那么就发送大小尽可能接近1500字节的数据包。这意味着IP头数据只浪费了1.3%的带宽,比20%要好得多!

32比特地址的IPv4允许40亿个不同的IP地址。多亏本地网络和网络地址转换(将在后面的章节中介绍),使得比40亿更多的主机连接在互联网上。虽然如此,由于IP地址分配的方式和笔记本、移动设备和物联网的发展,32比特的IP地址已经被用完了。IPv6的创建解决了这个问题和一些IPv4使用过程中出现的低效率问题。

在接下来的几年里,IPv6对于游戏开发者来说还不重要。谷歌报道截至2014年7月,大约有4%的用户通过IPv6访问谷歌网站,这大概也说明了有多少终端用户通过IPv6接入互联网。同样地,游戏仍然需要处理IPv4中出现的各种奇怪的问题,虽然IPv6中已经解决。然而,随着下一代平台,例如Xbox One的普及,IPv6将最终取代IPv4,所以值得我们简要了解一下IPv6是什么。

IPv6最显著的新特征是新的IP地址长度是128比特,可以写成由冒号分隔的8组数,每一组是4个十六进制数。表2.12展示了三种格式的IPv6地址。

表2.12 典型的IPv6地址格式

格式

地址

完整形式

2001:4a60:0000:8f1:0000:0000:0000:1013

前导零压缩法

2001:4a60:0:8f1:0:0:0:1013

双冒号法

2001:4a60:0:8f1::1013

 

前导零压缩法,将每一段的前导零省略。此外,如果几个连续的段值都是0,那么这些0可以简记为两个冒号。因为地址是16字节,所以恢复完整形式时,只需将所有省略的数字用0代替。

IPv6地址的前64比特表示网络,称为网络前缀(prefix);剩下的64比特表示个体主机,称为接口ID (interface identifier)。每台主机有一个固定的IP地址很重要,例如当这台主机作为服务器时,网络管理员需要手动设置接口ID,与IPv4中手动设置IP地址一样。一台不需要远程客户端很容易找到的主机也可以随便设置接口ID,并向网络公布,因为64比特地址空间发生冲突的概率很低。通常来说,接口ID自动设置为网卡的64比特EUI,因为已经保证了它的唯一性。

邻居发现协议(neighbor discovery protocol,NDP)代替了地址解析协议(ARP)和动态主机配置协议(DHCP)的一些功能,将在本章后面介绍。使用NDP,路由器公布它们的网络前缀和路由表信息,主机查询和宣布它们的IP地址和链路层地址。关于NDP的更多知识,请参考本章最后列出的RFC 4861。

针对IPv4的另外一个改进是,IPv6不再支持路由层面的数据包分片技术。所以删除了IP头部所有与分片技术相关的域,节省了每个数据包的带宽。如果一个IPv6数据包到达路由器,发现对于链路层来说太大,那么路由器直接丢弃这个数据包,告知发送方这个数据包太大。由发送方来决定使用小一些的数据包重新发送。

关于IPv6的更多知识,请参考本章最后列出的RFC 2460。

 

网络层的任务是实现远程网络上两台遥远主机之间的通信,而传输层(transport layer,也称传送层)的任务是实现这些主机上单独进程之间的通信。因为一台主机上同时运行很多进程,只知道主机A给主机B发了一个IP数据包是远远不够的:当主机B收到这个IP数据包时,它需要发送给哪个进程做进一步处理。为了解决这个问题,传输层引入了端口(port)的概念。端口是16比特的无符号数,是一台特定主机的通信端点。如果将IP地址比作一栋楼的街道地址,那么端口就好像这栋楼的门牌地址。一个进程就可以看作能够从一个或多个房间收邮件的租户。使用传输层模块时,一个进程绑定一个特定的端口,告诉传输层模块它想获得所有发送到这个端口的内容。

正如之前所提到的,所有的端口都是16比特。理论上,一个进程可以绑定到任何端口,用于任何传输目的。然而,如果一台主机上的两个进程试图绑定同一个端口,就出现了问题。例如,一个网站服务器程序和一个邮件系统程序都绑定了端口20。如果传输层模块收到了目的端口是20的数据,那么将这个数据同时发送给这两个进程吗?如果是,那么网站服务器程序将收到的邮件数据解释为网站请求,邮件系统程序将收到的网站请求解释为邮件。这将导致混乱,所以如果需要多个进程绑定同一个端口,大部分实现都需要特定的标识。

为了避免进程争夺端口,互联网名称与数字地址分配机构(Internet Corporation for Assigned Names and Numbers,ICANN),也称为互联网数字分配机构(Internet Assigned Numbers Authority,IANA),负责端口号的注册,任何协议和应用开发者都可以注册所需要的端口。每一个传输层的协议只能注册一个端口号。端口号1024—49151称为用户端口(user port)或注册端口(registered port)。任何协议和应用开发者可以向IANA申请这个范围的端口号,审核之后,这个端口注册就被授予了。如果一个用户端口号已经被IANA注册给一个特定的应用或协议,那么其他应用或协议想要绑定这个端口都是不合法的,尽管大部分传输层的实现没有保证这一条。

端口0到1023称为系统端口(system port)或预留端口(reserved port)。这些端口与用户端口类似,但是IANA对这些端口的注册要求更加严格,需要更彻底的审查。这些端口特殊,是因为大部分的操作系统只允许root级别的进程才能绑定系统端口,需要更高安全级别的时候才使用。

最后,端口49152到65535称为动态端口(dynamic port)。IANA不负责这些端口的注册,任何进程使用它们都是公平的。如果一个进程试图绑定一个动态端口,发现该端口被占用,那么应该尝试查询其他动态端口,直到找到一个没被占用的端口为止。作为一个互联网的好公民,应该仅仅在建立多人游戏时使用动态端口,必要的时候向IANA申请一个用户端口的注册。

一旦应用程序已经确定了一个可以使用的端口,它必须使用一个传输层协议才能发送数据。表2.13列举了一些传输层协议和它们的IP协议号。作为游戏开发者,我们主要使用UDP和TCP。

表2.13 传输层协议举例

名称

缩写

协议号

传输控制协议(transmission control protocol)

TCP

6

用户数据报协议(user datagram protocol)

UDP

17

数据报拥塞控制协议(datagram congestion control protocol)

DCCP

33

流控制传输协议(stream control transmission protocol)

SCTP

132

小窍门:

 

IP地址和端口经常由冒号连接在一起,来表示一个完整的源地址或目的地址。所以一个发送至IP地址为18.19.20.21,端口为80的数据包,其目的地址可以写成18.19.20.21:80。

用户数据报协议(user datagram protocol,UDP)是一个轻量级的协议,封装数据并将其从一台主机的一个端口发送到另一台主机的一个端口。UDP数据报包含一个8字节的报头,后面跟着数据。图2.10显示了UDP报头的格式。

图2.10 UDP报头

源端口号(Source Port,16比特)标识数据发送方将UDP数据报发送出去的端口。当数据报接收方需要响应的时候,这个域非常有用。

目标端口号(Destination Port,16比特)是数据报的目标端口。UDP模块将数据报发送给与这个端口绑定的进程。

数据报长度(Length,16比特)是指包括报头和数据部分在内的总字节数。

检验和(Checksum,16比特)由UDP报头、数据部分和IP头的某些域计算得到。它是一个可选项,如果不做计算,取值为0。如果底层验证了数据,这个域可以被忽略。

UDP是一个非常廉价的协议。每个数据报都是一个独立的实体,两台主机之间没有依赖的共享状态。可以把它比喻为一张明信片,投到信箱里,然后就忘记了。UDP不提供堵塞网络的流量限制服务,不保证数据顺序传输和准确到达。与接下来我们要介绍的TCP完全不同。

 

与UDP建立两台主机之间离散的数据报传输不同,传输控制协议(transmission control protocol,TCP)是在两台主机之间创建持久性的连接,提供可靠数据流传输。这里的关键词是可靠。不同于我们之前介绍过的所有协议,TCP保证所有的数据都按序抵达接收方。为了做到这一点,它需要比UDP更大的头部数据和用于跟踪连接中每台主机的重要连接状态数据。接收者确认接收到的数据,发送者重新发送没有收到确认消息的数据。

TCP的数据传输单元称为TCP报文段(segment),指的是TCP用于传输大量的字节流,底层数据包封装这个数据流的每个单独的报文段。一个报文段包含TCP首部和段内数据部分。图2.11展示了报文段的结构。

源端口号(Source Port,16比特)和目标端口号(Destination Port,16比特)是传输层的端口号。

序列号(Sequence Number,32比特)是一个单调递增的数字。通过TCP所传输的每个字节都有一个连续的序列号,用于这个字节的唯一标识。这样,发送方可以标记所发送的数据,接收方可以确认。报文段的序列号是本报文段所发送的数据的第一个字节的序号。有一个例外是建立初始连接,将在后面“三次握手”中介绍。

图2.11 TCP首部

确认号(Acknowledgment Number,32比特)包含发送方期望收到的下一个字节的序列号。对所有序列号低于这个数字的数据做一个实际的确认:因为TCP保证所有的数据都是按序传输,主机期望收到的下一个字节的序列号通常比刚刚收到的前一个字节的序列号多1。一定要记住:这个数字的发送方并不是确认收到这个值对应的序列号,而是所有小于这个值的序列号。

数据偏移(Data Offset,4比特)表示以32比特为单位的TCP头部大小。TCP允许头部的最后添加一些可选的头部元素,所以从头部开始到报文段可以取值从20到64字节。

控制位(Control Bits,9比特)是关于头部的元数据。稍后在相关的地方进行讨论。

接收窗口(Receive Window,16比特)表示对于传入的数据,剩余缓冲空间的最大容量。对于流量控制非常有用,稍后继续讨论。

紧急指针(Urgent Pointer,16比特)表示TCP段数据的第一个字节和紧急数据的第一个字节之间的距离。只有在控制位中URG标志设置了时才有效。

注释:

  

许多RFC,包括定义主要传输层协议的,都明确地规定8个比特大小的数据块为一个位组(octet),而不是使用松散地定义一个字节为8个比特。一些使用时间过长而难以维护的平台使用包含比8比特更多或更少的字节,位组的标准化帮助确保这些平台之间的兼容性。现在这不是一个主要问题,因为与游戏开发者相关的所有平台中一个字节都是8个比特。

2.6.2.1 可靠性

图2.12展示了TCP进行两台主机之间可靠数据传输的一般方式。简言之,源主机给目的主机发送一个唯一标识的数据包。然后源主机等待来自目的主机的确认响应数据包。如果在一定的时间内没有收到期望的确认,则重新发送这个数据包。重复这个过程直到所有的数据都被发送和确认。

图2.12 TCP可靠数据传输的流程图

这个过程的具体细节要稍微复杂一些,但是值得深入理解,因为这些细节提供了一个可靠数据传输系统的优秀案例。因为TCP策略涉及重新发送数据和跟踪期望的序列号,所以每一台主机必须维护所有活跃TCP连接的状态。表2.14列出了一些需要维护的状态变量和它们在RFC 793中定义的标准缩写。初始化状态的过程始于两台主机之间的三次握手。

2.6.2.2 三次握手

图2.13展示了主机A和B之间的三次握手。图中,主机A通过发送第一个报文段发起连接。这个报文段包含一个SYN标志和一个随机选择的初始序列号1000。这是在告诉主机B,主机A想要开始一个TCP连接,从序列号1000开始,主机B应该初始化必要资源以保持连接状态。

表2.14 TCP状态变量

变量

缩写

定义

Send Next

SND.NXT

主机发送的下一个报文段的序列号

Send Unacknowledged

SND.UNA

主机发送的尚未确认的最早报文段的序列号

Send Window

SND.WND

在收到未确认数据的确认前,允许主机发送的数据量

Receive Next

RCV.NXT

主机期望收到的下一个报文段的序列号

Receive Window

RCV.WND

在缓冲区不溢出的情况下,当前主机能够接收的数据量

图2.13 TCP三次握手

如果主机B愿意并且能够开放这个连接,它将响应一个同时包含SYN标志和ACK标志的数据包。通过将确认号设置为主机A的初始序列号加1来确认主机A的序列号,意思是主机B期望从主机A发来的下一个报文段的序列号应该比前一个报文段的序列号增加1。另外,主机B随机选取一个序列号3000开始与主机A的数据流传输。需要重点强调的是主机A和B都挑选自己的随机起始序列号。这个连接中包含两个独立的数据流:从主机A到主机B的数据流,使用主机A的序列号;从主机B到主机A的数据流,使用主机B的序列号。报文段中SYN标志的意思是“嗨!我将会给你发送一个以这个报文段中的序列号加1为起始位置的数据流”。第二个报文段中的ACK标志和确认号的意思是“噢,顺便提一下,我收到了你发送的这个序列号之前的所有数据,所以这个序列号是我期望你发送给我的下一个报文段”。当主机A收到这个报文段,剩下要做的就是确认主机B的初始序列号,所以发送一个包含ACK标志的报文段,确认字段的取值是主机B的序列号加1,即3001。

注释:

 

当TCP报文段包含一个SYN标志或者FIN标志,序列号额外加1,有时被称为TCP幻影字节(TCP phantom byte)。

通过小心地发送和确认数据来建立可靠性。如果发生超时,主机A收不到SYN-ACK报文段,它就知道可能是主机B没有收到SYN报文段,或者是主机B的响应报文丢失了。无论是哪一种情况,主机A都重新发送初始报文段。如果主机B的确收到过这个SYN报文段,那么这是第二次收到,主机B就知道是因为主机A没有收到SYN-ACK响应报文段,那么它将重新发送SYN-ACK报文段。

2.6.2.3 数据传输

为了传输数据,主机在每个即将发送的报文段中包含数据载荷。每个报文段标记为数据中第一个字节的序列号。还记得每个字节都有一个连续的序列号,所以这实际上意味着报文段的序列号应该是上一个报文段的序列号加上上一个报文段的数据量。同时,每次报文段到达目的地,接收方都要发送一个确认数据包,包含一个取值为期望收到的下一个序列号的确认域。图2.14展示了一个简单的没有包丢失的TCP传输。主机A在第一个报文段中发送100个字节,主机B确认收到并发送自己的50个字节,接着主机A又发送了200个字节,然后主机B确认收到这200个字节,没有再发送额外的数据。

当报文段丢失或者乱序传输,事情就变得更复杂了。图2.15中,从主机A发送到主机B的1301报文段丢失了。主机A期望收到一个确认域为1301的ACK数据包。当一个特定的时间期限到了,主机A还没有收到ACK,那么它就知道发生了错误。1301报文段或者来自主机B的ACK丢失了。无论是哪一种情况,它知道需要重新发送1301报文段,直到收到来自主机B的确认。为了重新发送这个报文段,主机A需要有这个报文段数据的拷贝,这是TCP操作的一个关键组成部分:TCP模块必须存储发送出去的每一个字节,直到这个字节已经被接收方确认收到。只有当收到报文段的确认后,TCP模块才能将这个报文段从缓冲区中清除。

图2.14 没有数据包丢失的TCP传输

图2.15 TCP数据包丢失和重传

TCP保证数据按序到达,所以如果主机收到数据报的序列号不是所期望的,它有两个选择。最简单的是直接丢弃这个数据报,等待按序重传。另一个选择是缓存它,同时不确认这个报文,也不转发给应用层处理。而是主机根据它的序列号将它复制到本地流缓冲区的合适位置。然后当这个序列号之前的所有报文都抵达,主机再确认这个乱序的数据报,并发送到应用层处理,而不需要发送方重传。

在前面的例子中,主机A总是在发送更多数据之前等待确认。这是不常见的人为行为,目的是简化例子。并没有要求主机A必须停止传输,在每个报文段发送之后等待确认。事实上,如果有这个要求,TCP将是长距离传输中一个完全不可用的协议。

回想一下,以太网的最大传输单元(MTU)是1500个字节。IPv4头部至少占其中的20个字节,TCP头部又占用了至少20个字节,这意味着通过以太网传输的未被分片的数据部分最多是1460个字节,也被称为最大分段大小(maximum segment size,MSS)。如果TCP连接一次只传输一个未被确认的报文段,那么它的带宽将极其有限。事实上,应该是最大分段大小(MSS)除以发送方发送给接收方分组的时间加上接收方发送给发送方确认的时间(往返时间,round trip time,RTT)。全国各地的往返时间平均是30毫秒。这意味着在不考虑链路层速度干扰的情况下,TCP可以达到的最大国内带宽是1500字节/0.03秒,即50 kbit/s。这在1993年是一个非常好的速度,但是今天看来已经不是了。

为了避免这个问题,TCP连接允许一次有多个未被确认的报文段同时传输。但是,不得不限制报文段的数量,因为会带来另外一个问题。当传输层数据抵达主机,将存储在主机的缓冲区中,直到绑定在相应端口的进程来处理它。到那时,它将从缓冲区中删除。无论主机有多少可用的内存,缓冲区本身是有固定大小的。可以想象,一个很慢的CPU上的一个复杂进程处理数据的速度赶不上数据到达的速度,这时缓冲区将被填满,传入的数据会被丢弃。TCP协议遇到这种情况,意味着数据没有被确认,发送方的快速传输将导致快速重传。很有可能大部分的重传数据再一次被丢弃,因为接收主机使用同样慢的CPU,运行着相同复杂的进程。这将导致网络拥塞,互联网资源的巨大浪费。

为了防止这种灾难的发生,TCP实现一个称为流量控制(flow control)的
过程。流量控制防止一台快速传输的主机压制另外一台处理较慢的主机。TCP头部包含一个接收窗口域,来指明数据发送方有多少可用的接收缓冲区。这相当于告诉其他主机在停止等待确认之前还可以发送的最大数据量。图2.16展示了一台快速传输的主机A和一台处理较慢的主机B之间的数据报交换过程。

出于示意目的,这里最大分段大小(MSS)取值为100字节。主机B的初始SYN-ACK标志指定接收窗口大小为300字节,所以主机A在暂停并等待主机B的ACK之前,只能发送3个大小为100字节的报文段。当主机B最后发送一个ACK,它知道它的缓冲区中有100字节的数据不能被及时处理,所以告诉主机A它的接收窗口限制为200字节。主机A知道有200个字节已经在传输的路上,所以没有发送更多的数据,必须停止,直到收到来自主机B的ACK。等到主机B确认第二个数据报时,缓冲区中50字节的数据已经被处理,所以缓冲区中一共剩下150个字节的数据,还有150字节空闲。当它给主机A发送一个ACK,告诉主机A接收窗口的大小限制为150字节。此时主机A知道还有100个字节未被确认的数据在传输,但是接收窗口是150字节,所以再给主机B发送额外的50字节的报文段。

图2.16 TCP流量控制

流量控制以这种方式继续,主机B总是提醒主机A它可以接收的数据量,主机A决不能发送比主机B可以缓存的数据量更多的数据。基于这一思想,TCP数据流的理论带宽限制可以定义为以下公式:

接收窗口太小会成为TCP传输的瓶颈。为了避免这一点,应该选择一个足够大的接收窗口,这样理论带宽最大值总是比主机之间的链路层最大传输速率要大。

可以看到在图2.16中,主机B最后向主机A连续发送了两个ACK数据报。这没有非常有效地利用带宽,因为第二个ACK数据报中的确认号能够充分确认第一个ACK数据报中确认的所有字节。因为IP头部和TCP头部是独立的,所以浪费了从主机B到主机A的40字节带宽。如果把链路层帧计算在内,将浪费得更多。为了防止这种低效率的情况发生,TCP规范允许延迟确认(delayed acknowledgment)。根据规范,主机收到TCP报文段之后不用马上响应确认。而是等待500毫秒,或者接收到下一个报文段,哪一种情况先发生就采取哪种方式。在之前的例子中,如果主机B在收到序列号为1001的报文段之后的500毫秒内收到报文段1101,那么主机B只需要发送报文段1101的确认。在数据流稠密时,可以有效地减少一半的ACK,给接收主机更多的时间处理缓冲区的数据,因此在确认报文中包含更大的接收窗口。

流量控制帮助TCP保护处理较慢的主机不被数据淹没,但是没有办法阻止较慢的网络和路由器不被淹没。网络上的交通好比高速公路,在繁忙的路由器处会发生特别严重的堵塞,就像繁忙的入口、出口和立交桥。为了避免不必要的网络堵塞,TCP实现拥塞控制(congestion control),相当于许多高速公路入口处的红灯。为了降低拥塞,TCP模块主动限制网络中传输的未被确认的数据量。和流量控制非常类似,但不是设置目的主机的窗口大小限制,而是根据已经确认的和丢弃的数据报的数量计算限制本身。具体的算法是依赖于实现的,但通常是某种AIMD(additive increase,multiplicative decrease)系统[1] 。也就是说,当连接刚刚建立时,TCP模块设置避免拥塞的限制为最大分段大小(MSS)的很小的倍数,通常设置为两倍。然后,每当确认一个报文段,将限制增加一个最大分段大小(MSS)。对于一个理想的连接,这意味着每个往返时间(RTT)能够确认的数据报数量为所设置的限制值,这导致限制的取值变为两倍。但是,一旦数据报被丢弃,TCP模块马上将限制值减少一半,怀疑这个丢失是由网络拥塞导致的。使用这种方式,最终会达到一个平衡状态,在没有发生数据包丢失的情况下,发送方尽可能快地发送。

TCP还可以通过发送大小尽可能接近最大分段大小(MSS)的数据报来降低网络拥塞。因为数据报需要40字节的头部,所以发送许多小的报文段没有将它们合并为一个大的数据块效率高。这意味着TCP模块需要维护一个向外发送的缓冲区来收集上层要发送的数据。许多TCP的实现使用纳格算法(Nagle’s algorithm)来决定什么时候收集数据,什么时候发送报文段,它是一些规则的集合。习惯上,如果有未被确认的数据在传输,就收集数据,直到数据量大于最大分段大小(MSS)或拥塞控制窗口,取这两个值中的最小值。在那时,发送在这两个限制下的最大报文段。

小窍门:

 

当游戏使用TCP作为传输层协议时,纳格算法(Nagle’s algorithm)是游戏玩家的克星。尽管它减少了带宽的使用,但是明显增加了数据发送的延时。如果一个实时游戏需要向服务器发送很少量的更新,在有足够的更新累加起来填充最大分段大小(MSS)之前,游戏已经运行了许多帧了。这会使玩家感到游戏延时,仅仅是因为运行了纳格算法。因为这个原因,大部分的TCP实现提供一个选项来禁用这个拥塞控制功能。

2.6.2.4 断开连接

关闭TCP连接需要分别来自两端的终止请求和确认。当一台主机没有要发送的数据时,会发送一个FIN数据报,表示准备停止发送数据。所有在缓冲区中等待的数据包括FIN数据报仍然会被传输以及在必要的时候重传,直到被确认。但是,TCP模块不会接收来自上层的新数据。不过另外一台主机可以接收数据,并且确认所有收到的数据。当它没有要发送的数据时,也会发送一个FIN数据报。当之前准备关闭连接的主机收到这个FIN数据报和响应它自己FIN数据报的ACK时,或者ACK超时,TCP模块都会完全关闭连接并删除连接状态。

TCP/IP模型的最顶层是应用层(application layer),是我们多人游戏代码存在的地方。应用层也是许多依赖传输层进行端到端传输的网络基础协议的家。我们将在这里研究其中的几个。

给子网中的每一台主机分配唯一的IPv4地址在管理上是非常有挑战的,特别是当笔记本电脑和智能手机也开始接入的时候。动态主机配置协议(dynamic host configuration protocol,DHCP)通过允许主机在接入网络时请求自动配置信息来解决这个问题。

在接入网络时,主机创建一个DHCPDISCOVER消息,包含它自己的MAC地址,并使用UDP协议以广播的方式发送到255.255.255.255:67。因为这个消息会发送给子网中的每一台主机,任何DHCP服务器都会收到这个消息。如果DHCP服务器有可以提供给客户端的IP地址,就会准备一个DHCP OFFER数据包。这个数据包包含可提供的IP地址和这个客户端的MAC地址。此刻这个客户端没有IP地址,所以服务器不能直接将数据包发送给它。而是服务器通过UDP 68端口把这个数据包广播到整个子网。所有的DHCP客户端都会收到这个数据包,检查消息里面的MAC地址来判断自己是
否是期望的接收者。当正确的客户端收到这个消息,读取所提供的IP地
址并决定是否接受这个分配。如果接受,回复一个广播的DHCPREQUEST消息请求这个IP地址。如果这个IP地址仍然可用,服务器再一次回复一个广播的DHCP ACK消息。这个消息与客户端确认IP地址已经分配,并传达其他必要的网络信息,如子网掩码、路由器地址和推荐可使用的DNS名称服务器。

DHCP报文的具体格式和扩展信息,请参考本章最后列出的RFC 2131。

域名系统(domain name system,DNS)协议能够将域名和子域名翻译为IP地址。如果一个终端用户想要执行谷歌搜索,不需要在浏览器中输入74.125.224.112,而是输入www.google.com。为了将域名翻译为IP地址,他的浏览器向名称服务器的IP地址发送一个DNS查询,该IP地址是已经在他的计算机上配置好的。

名称服务器(name server)存储域名和IP地址之间的映射。例如,存储www. google.com应该解析为74.125.224.112。有成千上万的名称服务器在互联网上使用,大部分只是互联网域名和子域名中小子网的权威服务器。如果被查询的名称服务器不是该区域的权威服务器,它通常有一个指针指向一个更权威的名称服务器接着查询。第二次查询的结果通常被缓存,以便下一次可以马上回答这个域名的查询。

DNS协议的查询和响应通常通过UDP协议发送,使用端口号53。具体格式请参考本章最后列出的RFC 1035。

 

直到现在,我们讨论的所有IP地址都是公开可路由的。如果一个IP地址被称为公开可路由(publically routable)的,互联网上任意正确配置的路由器都可以给这个IP地址所在的主机发送数据包。这需要任何公开可路由地址都是唯一分配给一台主机的。如果两台或者更多的主机共享一个IP地址,那么发送给一台主机的数据包有可能会到达另外一台主机。如果一台主机给网络服务器发送请求,那么响应可能会发送给另外一台主机,导致彻底混乱。

为了保证公开可路由地址的唯一性,ICANN及其下属公司给大型机构分配独立的IP地址块,如特大企业、大学和互联网服务提供商。它们可以将这些地址分发给其成员和客户,保证每一个地址是唯一分配的。

因为IPv4仅仅支持32比特地址空间,所以只有4 294 967 296个可能的公开IP地址。今天我们使用的网络设备数量多得难以置信,ICANN分配IP地址的方式使得IP地址越来越稀缺。有时,网络管理员或用户可能会发现自己可分配的公开IP地址比所持有的IP地址少。例如,作为电子游戏开发者,我们至少有一部智能手机、一台笔记本电脑和一台电子游戏机,然而只从ISP购买了一个公开IP地址。如果每台设备都需要自己专用的公开IP地址,怎么办呢?每次我们有一台新设备要连接到互联网,都需要从ISP那里与其他用户争夺一个新的IP地址并为其付费。

幸运的是,可以将整个子网的主机通过一个共享的公开IP地址连接到互联网。网络地址转换(network address translation,NAT)可以实现这个功能。为了配置一个NAT网络,必须给网络中的每台主机分配一个本地可路由(privately routable)的IP地址。表2.15列举了一些IANA留为己用的IP地址块,保证这些块中的地址不会被作为公开IP地址分配出去。这样,任何用户都可以使用本地可路由的IP地址建立自己的本地网络,而不需要检查唯一性。网络之间的唯一性不是必需的,因为这些地址不是公开可路由的。也就是说,互联网上的公开路由器都没有如何到达本地IP地址的路由信息,所以多个本地网络内部使用相同的本地IP地址是没关系的。

表2.15 本地IP地址块

IP地址范围

子网

10.0.0.0—10.255.255.255

10.0.0.0/8

172.16.0.0—172.31.255.255

172.16.0.0/12

192.168.0.0—192.168.255.255

192.168.0.0/16

为了理解网络地址转换协议是如何工作的,以图2.17中电子游戏玩家的家庭网络为例。电子游戏机、智能手机和笔记本电脑都有内部的本地IP地址,这些地址是由网络的拥有者分配的,不需要咨询任何外部服务提供者。路由器同时有针对内部网卡的本地IP地址和针对外部网卡的ISP分配的公开IP地址。因为本地地址网卡连接的是本地网络,所以称为局域网(local area network,LAN)端口。公开地址网卡连接的是全球,所以称为广域网(wide area network,WAN)端口。

图2.17 有NAT支持的本地网络

在这个例子中,假设公开IP地址12.5.3.2运行着一个游戏服务,绑定端口200。拥有本地IP地址192.168.1.2的电子游戏机运行着一个游戏,绑定端口100。游戏机需要使用UDP协议向服务器发送一条消息,所以构建一个数据报,如图2.18所示,源地址是192.168.1.2:100,目的地址是12.5.3.2:200。如果路由器没有使用NAT,游戏机发送数据报到路由器的局域网端口,然后转发到互联网的广域网端口,最终抵达服务器。不过这时出现问题了。因为IP数据包的源地址是192.168.1.2,服务器无法发送一个响应数据包。还记得192.168.1.2是本地地址吧,所以互联网上的公开路由器都不能路由到这个地址。即使有一些路由器存储了这个IP地址的路由信息,也是毫无意义的,数据包不可能最终到达我们的游戏机,因为互联网上有成千上万台本地地址为192.168.1.2的主机。

图2.18 没有NAT的路由器

为了防止这个问题,路由器在路由IP数据包时,它的NAT模块会重写这个IP数据包,将本地IP地址192.168.1.2替换为路由器自己的公开IP地址18.19.20.21。这样解决了部分问题,但不是全部问题:仅重写IP地址产生的情况如图2.19所示。服务器看到数据报是直接来自路由器的公开IP地址,所以可以成功返回给路由器一个数据报。但是,路由器没有记录是谁发来的原始数据报,所以不知道将响应报文转发到哪里。

图2.19 带有地址重写的NAT路由器

为了能够给真正的内部主机返回响应,路由器需要一些机制来识别传入数据包的内部接收者。一种直观的方式是建立一个表来记录每个发出去的数据包的源IP地址。当收到来自外部IP地址的响应时,路由器查找哪台内部主机给这个地址发送数据包了,然后使用内部主机的IP地址重写数据包。但是,如果多台内部主机向同一台外部主机发送数据时,这种方法就失效了。路由器不能识别传入的数据是发送给哪台内部主机的。

所有现代NAT路由器采用的解决方案都暴力地破坏了网络层和传输层之间的抽象。通过同时重写IP头部的IP地址和传输层头部的端口号,路由器可以创建更精确的映射和标记系统。NAT表中记录这些映射关系。图2.20展示了从游戏机到服务器的数据包发送过程和成功返回响应的过程。

当游戏机的数据包到达路由器,NAT模块同时将源IP地址和源端口号记录在NAT表中新的一行。然后随机选取一个没有被使用的端口号用于标识源地址和源端口号的组合,并将这个数字写到NAT表的同一行。NAT模块使用路由器自己的IP地址和新选取的端口号重写数据包。重写的数据包到达服务器,这时服务器发送响应数据包,地址是路由器的公开IP地址和新选取的端口。然后NAT模块使用这个端口号查询原始的IP地址和端口,重写响应数据包并转发到正确的主机。

图2.20 带有地址和端口重写的NAT路由器

注释:

 

为了更加安全,许多路由器将原始的目的IP地址和端口添加到NAT表的表项中。这样,当响应数据包到达路由器,NAT模块首先使用数据包的源端口查询表项,然后证实响应数据包的源IP地址和端口与原始发送出去的数据包的目的IP地址和端口一致。如果不一致,那么发生了可疑的事情,数据包被丢弃不转发。

对于互联网用户来说,NAT是一个奇妙的福音,但是对于多人游戏开发者却是一件令人头疼的事情。想象一下,许多用户在家中有自己的私人网络,计算机和游戏机使用NAT连接到互联网上,很容易出现图2.21展示的这种情况。玩家A有主机A,隐藏在NAT A后。他想要在主机A上运行一个多人游戏服务器,想让他的朋友玩家B连接到他的服务器。玩家B有主机B,隐藏在NAT B后。因为NAT的原因,玩家B没有办法创建与主机A的连接。如果主机B给主机A的路由器发送数据包试图连接,在主机A的NAT表中没有这一表项,所以这个数据包直接被丢弃。

有一些解决该问题的方法。一种方法是玩家A在路由器上手动配置端口转发。这需要一些技术和信心,并不适合强迫玩家完成这一操作。第二种方法更加灵活,称为UDP对NAT的简单穿越方式(simple traversal of UDP through NAT,STUN)。

图2.21 典型的用户游戏设置

当使用STUN时,主机与第三方主机通信,如Xbox Live或者PlayStation网络服务器。第三方告诉主机如何彼此之间创建连接,这样它们的路由器NAT表中就得到了所需要的表项,所以它们将可以进行直接通信。图2.22展示了通信的流程。图2.23展示了数据包交换的细节和生成的NAT表。假设我们的游戏运行在UDP协议端口200,所有前往非路由器主机和来自于非路由器主机的通信都经过端口200。

图2.22 数据流

首先,主机A从端口200给IP地址为4.6.5.10的第三方服务器(主机N)发送数据包,宣布它想成为一台服务器。当数据包经过路由器A,路由器A在它的NAT表中添加一行表项,并用自己的公开IP地址作为源地址和随机数60000作为源端口重写数据包。然后路由器A将数据包转发给主机N。主机N收到这个数据包,记录这个情况:玩家A,使用地址为18.19.20.21: 60000的主机A,想要注册为一台多人游戏的服务器。

接着,主机B给主机N发送数据包,宣布玩家B想要连接玩家A的游戏。当数据包经过路由器B,更新路由器B的NAT表并重写数据包,类似于路由器A的NAT操作。重写的数据包被转发给主机N,主机N从这个数据包得知地址为12.12.6.5:62000的主机B想要连接主机A。

此刻,主机N知道了路由器A的公开IP地址和目的端口,送入这个端口的数据包将被路由器A转发给主机A。它也能给主机B发送应答包,请求主机B尝试直接连接。然而,我们想到,一些路由器会检查传入数据包的来源,来确认它们是从那个地址来的期望数据包。路由器A仅仅期望来自主机N的数据包,所以如果此刻主机B试图连接主机A,路由器A将阻止这个数据包,因为路由器A不期望任何来自主机B的响应。

图2.23 STUN数据包细节和NAT表

幸运的是,主机N也知道了路由器B的公开IP地址和端口号,可以将数据包转发给主机B。所以,它将这个信息发送给主机A。路由器A让这个信息通过,因为它的NAT表指明主机A期望收到来自主机N的响应。然后主机A使用从主机N收到的连接信息给主机B发送数据包。这看似疯狂,因为服务器试图连接客户端,而我们希望的恰恰相反。实际上更加疯狂,因为我们知道,路由器B不希望来自主机A的数据包,所以不允许数据包通过。我们为什么要这样浪费数据包呢?我们这样做只是为了在路由器A的NAT表中添加一个表项!

当数据包从主机A传输到主机B时,经过路由器A。路由器A查询NAT表,发现主机A的地址192.168.10.2:200已经映射到外部端口60000,所以为这个数据包选择这个端口。然后增加了额外的表项,说明192.168.10.2:200已经给12.12.6.5:62000发送了数据。这个额外的表项是关键。这个数据包可能永远无法到达主机B,但是在这之后,主机N可以回复主机B,告诉它直接通过18.19.20.21:60000连接主机A。主机B这样操作之后,当数据包到达路由器A,路由器A发现这确实是期望的来自12.12.6.5:62000的数据包。所以重写这个数据包的目的地址是192.168.10.2:200,并发送给主机A。从那时起,主机A和B可以使用它们已经交换过的公开IP地址和端口号直接通信了。

注释:

 

关于NAT,还有更多的知识值得一提。首先,并不是所有的NAT都使用上述
的NAT穿越技术。有些NAT给内部主机分配不一致的外部端口,这样的NAT称为对称NAT(symmetric NAT)。在对称NAT中,每一个即将发出的请求收到唯一的外部端口,即使发出这个请求的源IP地址和端口已经在NAT表中。这破坏了STUN,因为当主机A给主机B发送第一个数据包时,路由器A将使用一个新的外部端口。当主机B使用之前主机A连接主机N时使用的外部端口联系路由器A时,在NAT表中找不到匹配的表项,所以数据包被丢弃。

有时,不安全的对称NAT按序分配外部端口,所以机智的程序可以使用端口分配预测(port assignment prediction)方法在对称NAT上实现类似STUN的技术。安全一些的对称NAT使用随机端口分配的方法,这样不容易被预测。

STUN方法只适用于UDP协议。第3章将介绍TCP使用不同的端口分配系统,所以传输数据必然使用与侦听连接不同的端口。当使用TCP时,将使用被称为TCP打洞(TCP hole punching)的技术,前提是NAT路由器支持这种方式。本章最后列出的RFC 5128详细介绍了NAT穿越技术,包括TCP打洞。

最后,还有另外一个流行的方法允许NAT路由器穿越,称为因特网网关设备协议(Internet gateway device protocol,IGDP)。一些通用即插即用(universal plug and play,UPnP)的路由器使用这个协议允许局域网主机手动配置外部端口与内部端口的映射关系。但并不是所有的路由器都支持这种方式,并且学术界对其缺乏研究兴趣,所以这里不详细介绍。详细说明可以参考本章最后的阅读资料。

本章概述了互联网的内部工作机制。分组交换允许在同一条线路上同时进行多个传输,促成了阿帕网络,最终促成互联网。TCP/IP协议族的这种层次结构支撑着互联网,它包含五层,每一层为上层提供数据通路。

物理层提供信号传输的媒介,有时被认为是其上层数据链路层的一部分。数据链路层提供互联主机之间的通信。它需要一个硬件地址系统,这样主机可以被唯一编址,确定MTU,即一次可以传输的最大数据量。有许多协议提供基本的链路层服务,本章深入讨论了以太网协议,因为它是对于游戏开发者最重要的协议之一。

网络层在数据链路层的硬件地址之上提供逻辑地址系统,允许在不同数据链路层网络的主机之间的通信。IPv4,今天最基本的网络层协议,提供直接和间接路由系统,给对于链路层来说太大的数据包分片。IPv6的出现解决了地址空间有限的问题,并优化了IPv4数据传输的几个最大瓶颈。

传输层和传输层端口提供远处主机进程之间端到端的通信。TCP和UDP是传输层的基本协议,它们最本质的不同是:UDP是轻量级的、无连接的和不可靠的,而TCP要更重一些,需要稳定的连接,提供可靠的有序数据传输。TCP实现流量控制和拥塞控制机制来减少包丢失。

最顶层是应用层,包括DHCP协议、DNS协议和游戏代码。

为了促进以最少的管理代价建立本地网络,NAT允许整个网络共享一个公开IP地址。NAT的缺点是它阻止了服务器所需要的未经请求的连接,但是有许多技术,例如STUN和TCP打洞,提供了这个问题的解决方案。

本章提供了互联网工作的理论基础。第3章我们讲解实现主机之间通信的函数和数据结构,将证明这些知识很有用。

1.列出TCP/IP模型的5层并简要描述每一层。在一些模型中,哪一层被认为不是单独的一层?

2.为什么使用ARP协议?它是如何工作的?

3.解释一下有多个网卡的主机(如路由器)是如何在不同子网之间路由数据包的。

4. MTU代表什么?它是什么意思?以太网的MTU是什么?

5.解释一下包分片是如何工作的。假设数据链路层的MTU是400,举一个需要分成两帧的数据包头部的例子,分片后这两帧的头部是什么?

6.避免IP分片有什么好处?

7.不使用分片技术,发送尽可能大的数据包的好处是什么?

8.不可靠的数据传输和可靠的数据传输有什么区别?

9.描述一下建立连接时TCP握手的流程。交换了什么重要的数据?

10.描述一下TCP是如何做到可靠数据传输的。

11.公开可路由的IP地址和本地可路由的IP地址的区别是什么?

12. NAT是什么?使用NAT有哪些好处,有哪些开销?

13.解释一下客户端是如何使用NAT向公开可路由的服务器发送数据包并收到响应数据包的。

14. STUN是什么?为什么需要STUN?它是如何工作的?

Bell, Gordon. (1980, September). The Ethernet—A Local Area Network.

Braden, R. (Ed). (1989, October). Requirements for Internet Hosts—Application and Support.

Braden, R. (Ed). (1989, October). Requirements for Internet Hosts— Communication Layers.

Cotton, M., L. Eggert, J. Touch, M. Westerlund, and S. Cheshire. (2011, August). Internet Assigned Numbers Authority (IANA) Procedures for the Management of the Service Name and Transport Protocol Port Number Registry.

Deering, S., and R. Hinden. (1998, December). Internet Protocol, Version 6 (IPv6) Specification.

Drom, R. (1997, March). Dynamic Host Configuration Protocol.

Google IPv6 Statistics. (2014, August 9).

Information Sciences Institute. (1981, September). Transmission Control Protocol.

Internet Gateway Device Protocol. (2010, December).

Mockapetris, P. (1987, November). Domain Names—Concepts and Facilities.

Mockapetris, P. (1987, November). Domain Names—Implementation and Specifi-cation.

Nagle, John. (1984, January 6). Congestion Control in IP/TCP Internetworks.

Narten, T., E. Nordmark, W. Simpson, and H. Soliman. (2007, September). Neighbor Discovery for IP version 6 (IPv6).

Nichols, K., S. Blake, F. Baker, and D. Black. (1998, December). Definition of the Differentiated Services Field (DS Field) in the IPv4 and IPv6 Headers.

Port Number Registry. (2014, September 3 ).

Postel, J., and R. Reynolds. (1988, February). A Standard for the Transmission of IP Datagrams over IEEE 802 Networks.

Ramakrishnan, K., S. Floyd, and D. Black. (September 2001). The Addition of Explicit Congestion Notification (ECN) to IP.

Rekhter, Y., and T. Li. (1993, September ). An Architecture for IP Address Allocation with CIDR.

Rosenberg, J., J. Weinberger, C. Huitema, and R. Mahy. (2003, March). STUN—Simple Traversal of User Datagram Protocol (UDP).

Socolofsky, T., and C. Kale. (1991, January). A TCP/IP Tutorial.

[1] AIMD系统:当TCP发送方感受到端到端路径无拥塞时就线性增加其发送速度,当察觉到路径拥塞时就乘性减小其发送速度,称为加性增,乘性减,或者“和式增加,积式减少”。


相关图书

Python面向对象编程:构建游戏和GUI
Python面向对象编程:构建游戏和GUI
精通游戏测试(第3版)
精通游戏测试(第3版)
罗布乐思开发官方指南 从入门到实践
罗布乐思开发官方指南 从入门到实践
游戏引擎原理与实践 卷2 高级技术
游戏引擎原理与实践 卷2 高级技术
游戏数值设计
游戏数值设计
游戏引擎原理与实践 卷1 基础框架
游戏引擎原理与实践 卷1 基础框架

相关文章

相关课程