Go 网络编程(一)
一、架构
本章涵盖了分布式系统的主要架构特征。如果你对自己想要构建的东西没有一点概念,你就无法构建一个系统。如果你不知道它的工作环境,你就无法建造它。GUI 程序不同于批处理程序;游戏程序不同于商业程序;分布式程序不同于独立程序。他们都有自己的方法、共同的模式、通常会出现的问题以及常用的解决方案。
本章涵盖了分布式系统的高级架构方面。有许多方法来看待这样的系统,其中许多是处理。
协议层
分布式系统很难。这涉及到多台计算机,它们必须以某种方式连接起来。必须编写程序在系统中的每台计算机上运行,并且它们都必须合作来完成分布式任务。
处理复杂性的通常方法是把它分解成更小更简单的部分。这些部分有自己的结构,但它们也有与其他相关部分通信的定义方式。在分布式系统中,这些部分被称为协议层,它们有明确定义的功能。它们形成一个堆栈,每一层都与上一层和下一层通信。各层之间的通信由协议定义。
网络通信要求协议涵盖高层应用通信,一直到有线通信,以及协议层封装所处理的复杂性。
ISO 开放系统互连协议
尽管 OSI(开放系统互连)协议从未被正确实现,但它已经成为谈论和影响分布式系统设计的一个重要因素。通常如图 1-1 所示。
图 1-1。
The Open Systems Interconnect protocol
OSI 层
每层的功能自下而上如下:
- 物理层使用电、光或无线电技术传送比特流。
- 数据链路层将信息包放入网络帧中,以便通过物理层传输,然后再放回信息包中。
- 网络层提供交换和路由技术。
- 传输层在终端系统之间提供透明的数据传输,并负责端到端的错误恢复和流量控制。
- 会话层建立、管理和终止应用程序之间的连接。
- 表示层提供了与数据表示(如加密)差异的独立性。
- 应用层支持应用程序和最终用户流程。
TCP/IP 协议
当 OSI 模型被争论、辩论、部分实现和争论的时候,DARPA 互联网研究项目正忙于建立 TCP/IP 协议。这些都取得了巨大的成功,并导致了互联网(与资本)。这是一个简单得多的堆栈,如图 1-2 所示。
图 1-2。
The TCP/IP protocols
一些替代协议
尽管看起来很像,但 TCP/IP 协议并不是现存的唯一协议,从长远来看,它甚至可能不是最成功的。维基百科的网络协议列表(见 https://en.wikipedia.org/wiki/List_of_network_protocols_(OSI_model) )在每一个 ISO 层都有一个巨大的数字。其中许多协议已经过时或用处不大,但由于各种领域的技术进步,如太空互联网和物联网,总会有新协议的空间。
本书的重点是 TCP/IP(包括 UDP)层,但是您应该知道还有其他层。
建立关系网
网络是连接称为主机的终端系统的通信系统。连接机制可能是铜线、以太网、光纤或无线,但这不是我们在这里关心的。局域网(LAN)将相距很近的计算机连接起来,这些计算机通常属于一个家庭、小型组织或大型组织的一部分。
广域网(WAN)将计算机连接到更大的物理区域,如城市之间。还有其他类型,如城域网,个人局域网,甚至体域网。
互联网是两个或多个不同网络的连接,通常是局域网或广域网。内部网是所有网络都属于一个组织的互联网。
互联网和内部网有很大的不同。典型地,内部网将处于单一的管理控制之下,这将强加单一的一组一致的策略。另一方面,互联网不会由一个机构控制,对不同部分的控制甚至可能不兼容。
这种差异的一个小例子是,内部网通常被运行特定操作系统的标准化版本的少数供应商限制在计算机上。另一方面,互联网通常会有不同的计算机和操作系统的大杂烩。
这本书的技术适用于互联网。它们对内部网也是有效的,但在那里你也会发现专门的、不可移植的系统。
此外,还有所有互联网的“母亲”:互联网。这只是一个非常非常大的互联网,连接我们和谷歌,我的电脑和你的电脑,等等。
方法
网关是用于连接两个或多个网络的实体的通称。中继器在物理层工作,将信息从一个子网复制到另一个子网。网桥工作在数据链路层,在网络之间复制帧。路由器在网络层运行,不仅在网络间传输信息,还决定路由。
分组封装
OSI 或 TCP/IP 堆栈中各层之间的通信是通过将数据包从一层发送到下一层,然后最终通过网络完成的。每一层都有它必须保存的关于自己层的管理信息。当数据包向下传递时,它会将报头信息添加到从上一层接收的数据包中。在接收端,当数据包向上移动时,这些报头会被删除。
例如,TFTP(普通文件传输协议)将文件从一台计算机移动到另一台计算机。它使用 IP 协议之上的 UDP 协议,可以通过以太网发送。如图 1-3 所示。
图 1-3。
The TFTP (Trivial File Transfer Protocol)
通过以太网传输的数据包当然是最底层的。
连接模型
为了让两台计算机进行通信,它们必须建立一条路径,通过这条路径,它们可以在一个会话中至少发送一条消息。对此有两种主要模式:
- 面向连接
- 无连接传输模式
面向连接
为会话建立单个连接。双向通信沿着连接流动。当会话结束时,连接断开。这类似于电话交谈。TCP 就是一个例子。
无连接传输模式
在无连接系统中,消息是相互独立发送的。打个比方,普通邮件。无连接消息可能会无序到达。IP 协议就是一个例子。UDP 是 IP 之上的无连接协议,因为它的重量轻得多,所以经常被用作 TCP 的替代协议。
面向连接的传输可以建立在无连接传输之上——TCP over IP。无连接传输可以建立在面向连接的传输之上——HTTP over TCP。
这些可以有变化。例如,会话可能强制消息到达,但可能无法保证它们按照发送的顺序到达。然而,这两个是最常见的。
通信模型
在分布式系统中,会有许多组件运行,它们必须相互通信。有两种主要的模型,消息传递和远程过程调用。
信息传递
一些非过程化语言建立在消息传递的原则上。并发语言经常使用这种机制,最著名的例子可能是 UNIX 管道。UNIX 管道是字节的管道,但这不是固有的限制:微软的 PowerShell 可以沿其管道发送对象,Parlog 等并发语言可以在并发进程之间的消息中发送任意逻辑数据结构。
消息传递是分布式系统的基本机制。建立一个连接,然后输入一些数据。在另一端,找出消息是什么,并对其作出响应,可能发送回消息。这如图 1-4 所示。
图 1-4。
The message passing communications model
事件驱动系统以类似的方式工作。在底层,node.js运行一个事件循环,等待 I/O 事件,为这些事件分派处理程序并做出响应。在更高层次上,大多数用户界面系统使用事件循环等待用户输入,而在网络世界中,Ajax 使用XMLHttpRequest来发送和接收请求。
远程过程得
在任何系统中,都有信息和流控制从系统的一部分到另一部分的转移。在过程语言中,这可能由过程调用组成,其中信息被放在调用堆栈上,然后控制流被转移到程序的另一部分。
即使是过程调用,也会有变化。代码可以是静态链接的,以便控制从程序的可执行代码的一部分转移到另一部分。由于库例程的使用越来越多,在动态链接库(dll)中拥有这样的代码已经变得很常见,在动态链接库中,控制转移到一段独立的代码。
dll 与调用代码运行在同一台计算机上。将控制权转移给运行在不同机器上的过程是一个简单的(概念性的)步骤。这其中的机制并不简单!然而,这种控制模式导致了远程过程调用(RPC ),这将在后面的章节中详细讨论。如图 1-5 所示。
图 1-5。
The remote procedure call communications model
有很多这样的例子:一些基于特定的编程语言,比如 Go rpc 包(在第十三章中讨论)或者覆盖多种语言的 rpc 系统,比如 SOAP 和 Google 的 grpc。
分布式计算模型
在最高层次上,我们可以考虑分布式系统组件的等价或不等价。最常见的情况是非对称的:客户端向服务器发送请求,服务器做出响应。这是一个客户机-服务器系统。
如果两个组件是等价的,都能够发起和响应消息,那么我们就有了一个对等系统。注意,这是一个逻辑分类:一个对等体可能是 16000 核的超级计算机,另一个可能是手机。但是,如果两者的行为相似,那么他们就是同龄人。
如图 1-6 所示。
图 1-6。
Client-sever versus peer-to-peer systems
客户-服务器系统
客户端-服务器系统的另一个视图如图 1-7 所示。
图 1-7。
The client-server system
需要了解系统组件的开发人员可能持有这种观点。这也是用户可能持有的观点:浏览器的用户知道它正在她的系统上运行,但是正在与别处的服务器通信。
客户端-服务器应用程序
有些应用程序可能是无缝分布的,用户并不知道它是分布式的。用户将看到他们的系统视图,如图 1-8 所示。
图 1-8。
The user’s view of the system
服务器分布
客户机-服务器系统不必简单。基本模型是单客户端、单服务器系统,如图 1-9 所示。
图 1-9。
The single client, single server system
然而,你也可以有多个客户端,单个服务器,如图 1-10 所示。
图 1-10。
The multiple clients, single server system
在这个系统中,主服务器接收请求,并不是一次处理一个请求,而是将它们传递给其他服务器来处理。当可能有并发客户端时,这是一个常见的模型。
还有单客户端,多服务器,如图 1-11 所示。
图 1-11。
The single client, multiple servers system
当一个服务器需要充当其他服务器的客户端时,例如业务逻辑服务器从数据库服务器获取信息,这种类型的系统经常出现。当然,也可能有多个客户端和多个服务器。
沟通流程
前面的图表显示了系统高层组件之间的连接视图。数据将在这些组件之间流动,并且可以通过多种方式流动,这将在下面的部分中讨论。
同步通信
在同步通信中,一方将发送消息并阻塞,等待回复。这通常是实现起来最简单的模型,仅仅依赖于阻塞 I/O。但是,可能需要一个超时机制,以防某些错误意味着永远不会发送回复。
异步通信
在异步通信中,一方发送消息,而不是等待回复,继续进行其他工作。当回复最终到来时,它被处理。这可能是在另一个线程中或通过中断当前线程。这种应用程序更难构建,但使用起来更加灵活。
流式通信
在流式通信中,一方发送连续的消息流。在线视频就是一个很好的例子。该流可能需要实时处理,可能容忍或不容忍丢失,并且可以是单向的或允许反向通信,如在控制消息中。
发布/订阅
在发布/订阅系统中,各方订阅主题,其他人发布主题。正如 Twitter 所展示的那样,这可以是小规模的,也可以是大规模的。
成分分布
分解许多应用程序的一个简单而有效的方法是将它们看作由三部分组成:
- 演示组件
- 应用逻辑
- 数据存取
表示组件负责与用户的交互,包括显示数据和收集输入。它可以是具有按钮、列表、菜单等的现代 GUI 界面。,还是比较老的命令行风格的界面,提问得到答案。它还可以包含更广泛的交互风格,例如与诸如收银机、ATM 等物理设备的交互。它还可以涵盖与非人类用户的交互,如在机器对机器系统中。细节在这个层面并不重要。
应用程序逻辑负责解释用户的响应、应用业务规则、准备查询以及管理来自第三方组件的响应。
数据访问组件负责存储和检索数据。这通常会通过数据库,但不是必须的。
Gartner 分类
基于应用程序的这种三重分解,Gartner 考虑了如何在客户机-服务器系统中分配组件。他们提出了五种模型,如图 1-12 所示。
图 1-12。
Gartner’s five models
示例:分布式数据库
-
Gartner classification : 1 (see Figure 1-13)
图 1-13。
Gartner example 1
现代手机就是很好的例子。由于内存有限,他们可能会在本地存储一小部分数据库,这样他们通常可以快速响应。然而,如果需要的数据不是本地保存的,则可以向远程数据库请求该附加数据。
谷歌地图是另一个很好的例子。所有的地图都在谷歌的服务器上。当用户请求时,“附近”的地图也被下载到浏览器的一个小数据库中。当用户稍微移动地图时,所需的额外位已经在本地存储中,以便快速响应。
示例:网络文件服务
Gartner classification 2 允许远程客户端访问共享文件系统,如图 1-14 所示。
图 1-14。
Gartner example 2
这样的系统有很多例子:NFS、微软股份、DCE 等等。
示例:Web
Gartner 分类 3 的一个例子是带有 Java 小程序或 JavaScript、CGI 脚本或类似程序(Ruby on Rails 等)的 Web。)在服务器端。这是一个分布式超文本系统,有许多附加机制,如图 1-15 所示。
图 1-15。
Gartner example 3
示例:终端仿真
Gartner 分类 4 的一个例子是终端仿真。这允许远程系统作为本地系统的普通终端,如图 1-16 所示。
图 1-16。
Gartner example 4
Telnet 是这方面最常见的例子。
示例:安全外壳
UNIX 上的安全 shell 允许您连接到远程系统,在那里运行命令,并在本地显示演示。演示文稿在远程机器上准备,并在本地显示。在 Windows 下,远程桌面的行为类似。参见图 1-17 。
图 1-17。
Gartner example 4
三层模型
当然,如果您有两层,那么您可以有三层、四层或更多层。一些三层可能性如图 1-18 所示。
图 1-18。
Three-tier models
现代网络是最右边的一个很好的例子。后端由数据库组成,通常运行存储过程来保存一些数据库逻辑。中间层是 HTTP 服务器,如运行 PHP 脚本(或 Ruby on Rails,或 JSP 页面等)的 Apache。).这将管理一些逻辑,并将 HTML 页面等数据存储在本地。前端是在一些 JavaScript 的控制下显示页面的浏览器。在 HTML 5 中,前端可能也有一个本地数据库。
胖与瘦
一种常见的成分标签是“脂肪”或“瘦”。Fat 组件占用大量内存并进行复杂的处理。另一方面,薄的组件在这两方面都没什么用。似乎没有什么“正常”的尺寸成分,只有胖或瘦!
胖瘦是一个相对的概念。浏览器经常被贴上瘦的标签,因为它们所做的只是显示网页。然而,我的 Linux 机器上的 Firefox 占用了将近半个千兆字节的内存,我一点也不认为这很小!
中间件模型
中间件是连接分布式系统组件的“粘合剂”。中间件模型如图 1-19 所示。
图 1-19。
The middleware model
中间件的组件包括以下内容:
- TCP/IP 等网络服务
- 中间件层是使用网络服务的独立于应用程序的软件
- 数据库访问
- 身份等服务的管理者
- 安全模块
中间件示例
中间件的例子包括:
- 终端模拟器、文件传输和电子邮件等基本服务
- RPC 等基本服务
- 集成服务,如 DCE(分布式计算环境)
- 分布式对象服务,如 CORBA 和 OLE/ActiveX
- 移动对象服务,如 RMI 和 Jini
- 万维网
中间件功能
中间件的功能包括:
- 在不同计算机上启动进程
- 会话管理
- 允许客户端定位服务器的目录服务
- 远程数据访问
- 允许服务器处理多个客户端的并发控制
- 安全性和完整性
- 监视
- 本地和远程进程的终止
连续加工
Gartner 模型基于将应用程序分解为表示、应用程序逻辑和数据处理等组件。图 1-20 显示了更精细的细分。
图 1-20。
Breakdown of an application into its components of presentation
故障点
分布式应用程序运行在复杂的环境中。这使得它们比单台计算机上的独立应用程序更容易失败。故障点包括:
- 客户端错误
- 应用程序的客户端可能会崩溃
- 客户端系统可能有硬件问题
- 客户端的网卡可能会出现故障
- 网络错误
- 网络连接可能会导致超时
- 可能存在网络地址冲突
- 路由器等网络元素可能会出现故障
- 传输错误可能会丢失消息
- 客户端-服务器错误
- 客户端和服务器版本可能不兼容
- 服务器错误
- 服务器的网卡可能会出现故障
- 服务器系统可能有硬件问题
- 服务器软件可能会崩溃
- 服务器的数据库可能会损坏
设计应用程序时必须考虑到这些可能的故障。如果系统的其他部分出现故障,由一个组件执行的任何操作都必须是可恢复的。需要使用诸如事务和连续错误检查之类的技术来避免错误。应该注意的是,虽然独立的应用程序可能对可能发生的错误有很多控制,但分布式系统不是这样。例如,服务器无法控制网络或客户端错误,只能准备处理它们。在许多情况下,错误的原因可能不清楚:是客户端崩溃了还是网络中断了?
接受因素
分布式系统的验收因素与独立系统的验收因素相似。它们包括以下内容:
- 可靠性
- 表演
- 响应性
- 可量测性
- 容量
- 安全
目前,用户经常容忍比独立系统更糟糕的行为。“哦,网速慢”似乎是一个可以接受的借口。事实并非如此,开发人员不应该陷入这样的思维定势,认为他们控制下的因素会产生可忽略的影响。
透明度
分布式系统的“圣杯”提供以下功能:
- 访问透明性
- 位置透明性
- 迁移透明度
- 复制透明性
- 并发透明性
- 可扩展性透明性
- 绩效透明度
- 故障透明度
访问透明性
用户不应知道(或需要知道)对系统的全部或部分的访问是本地的还是远程的。
位置透明性
服务的位置并不重要。
迁移透明度
如果系统的一部分移动到另一个位置,对用户来说应该没有什么影响。
复制透明性
如果系统的一个或多个副本正在运行,应该没有关系。
并发透明性
同时运行的系统各部分之间不应有干扰。例如,如果我正在访问数据库,那么你不应该知道。
可扩展性透明性
系统上有一百万或一百万用户都没关系。
绩效透明度
性能不应受到任何系统或网络特征的影响。
故障透明度
该系统不应失败。如果部分失败,系统应该在用户不知道失败发生的情况下恢复。
这些透明度因素中的大多数在违反中比在遵守中观察到的多。有一些显著的例子几乎符合这些标准。例如,当您连接到 Google 时,您不知道(或不关心)服务器在哪里。使用亚马逊网络服务的系统能够根据需求进行伸缩。网飞有着看似残酷的测试策略,定期故意破坏其系统的大部分,以确保整体仍能正常工作。
分布式计算的八个谬误
Sun Microsystems 是一家在分布式系统中做了大量早期工作的公司,甚至有一句口头禅“网络就是计算机”。" Sun 的一些科学家根据他们多年的经验,提出了以下通常假定的谬误:
- 网络是可靠的。
- 延迟为零。
- 带宽是无限的。
- 网络很安全。
- 拓扑不会改变。
- 有一个管理员。
- 运输成本为零。
- 网络是同构的。
谬论:网络可靠
Bailis 和 Kingsbury 的一篇题为“网络是可靠的”(见 http://queue.acm.org/detail.cfm?id=2655736 )的论文检验了这一谬误。它发现了许多实例,例如微软报告他们的数据中心每天发生 5.2 次设备故障和 40.8 次链路故障。
中国政府使用“DNS 中毒”作为审查其认为不受欢迎的网站的技术之一。中国也运行一个 DNS 根服务器。2010 年,这台服务器配置错误,毒害了许多其他国家的 DNS 服务器。这使得许多非中文网站在中国内外都无法访问(见 http://www.pcworld.com/article/192658/article.html )。
还有许多其他可能的情况,例如使网站不可用的 DDS(分布式拒绝服务)攻击。在 Box Hill Institute,一个承包商曾经在连接我们的 DHCP 服务器和网络其余部分的光缆上开了一个反孔,于是我们就回家休息了。
网络不可靠。这意味着任何网络程序都必须准备好应对失败。这导致了 Java 的 RMI 和大多数后来的框架的设计选择,应用程序设计允许每个网络调用可能失败。
谬误:延迟为零
等待时间是发送信号和得到回复之间的延迟。在单进程系统中,延迟可能取决于函数调用返回之前在函数调用中执行的计算量,但在网络上,延迟通常是由简单地必须遍历传输并由途中的各种节点(如路由器)处理而引起的。
ping命令是显示延迟的好方法。从墨尔本到谷歌的澳大利亚服务器需要 20 毫秒。对百度中国服务器的一次 ping 大约需要 200 毫秒 1 。
相比之下,Williams(参见 http://www.eetimes.com/document.asp?doc_id=1200916 )讨论了 Linux 调度程序的延迟,得出平均延迟为 88 微秒。网络调用的延迟要大几千倍。
谬误:带宽是无限的
每个在下载发生时去沏杯茶或咖啡的人都知道这是一个谬论。我运行自己的网络服务器,在 ADSL2 上获得 800 Kbps 的上传速度。我很不幸,家里有 HFC,灾难性的澳大利亚国家宽带网络可能会将它升级到 1000 Kbps。三年后,到 2020 年。
与此同时,我使用本地无线连接,给我 75 Mbps 上下,它仍然不够快!
谬论:网络安全
科技公司大力推动将强大的加密技术用于所有网络通信,世界各国政府也同样大力推动“仅针对特定政府”的较弱系统或后门。这似乎同样适用于民主(我的意外拼写错误可能是准确的!)以及极权政府。
当然,除此之外,还有一般的“坏人”,窃取并出售数百万张信用卡号码和密码。
谬误:拓扑不会改变
确实如此。通常,这可能会影响延迟和带宽。但是路由或 IP 地址的硬编码越多,网络应用就越容易出故障。
谬误:只有一个管理员
那又怎样?一切正常的时候没问题。出了问题,问题就开始了——该怪谁,该由谁来解决?
多年来的一个主要研究课题是网格计算,它将计算任务分配给许多大学和研究机构来解决巨大的科学问题。这必须解决许多复杂的问题,因为不仅有多个管理员,而且还有不同的访问和安全问题、不同的维护计划等等。云计算的出现解决了许多这样的问题,减少了管理员和系统的数量,因此云计算比许多网格系统更有弹性。
谬论:运输成本为零
一旦我买了我的电脑,从 CPU 到显示器的传输成本是零(嗯,小电!).但是我们每个月都要向我们的 IP 提供商付费,因为他们必须建造服务器机房、铺设电缆等等。这只是一个必须考虑的成本。
谬误:网络是同质的
网络不是同质的,终端也不是,比如你和我的电脑、iPads、Android 设备和手机。更不用说物联网将无数互联设备带入画面。供应商不断尝试产品锁定,不断限制工作环境,试图简化他们的控制系统,这在一定程度上取得了成功。但当它们失败时,依赖同质性的系统也会失败。
结论
本章试图强调,与其他类型的计算相比,分布式计算有其独特的特点。忽视这些特征只会导致最终系统的失败。不断有人试图简化架构模型,最新的是“微服务”和“无服务器”计算,但最终复杂性仍然存在。
这些必须使用任何编程语言来解决,后续章节将考虑 Go 如何管理它们。
Footnotes 1
从我在澳大利亚墨尔本的位置,我看到平时间
PING google.com.au(216.58.203.99)56(84)字节的数据。
来自syd09s15-in-f3.1e100.net (216.58.203.99): icmp_seq=1 ttl=50 time=27.1 ms的 64 字节
来自syd09s15-in-f3.1e100.net (216.58.203.99): icmp_seq=2 ttl=50 time=19.7 ms的 64 字节
二、Go 语言概述
不断有编程语言被发明出来。有些是高度专业化的,有些是相当通用的,而第三组是为了填补广泛的,但在某种程度上利基领域。Go 创建于 2007 年,于 2009 年公开发布。它旨在成为一种系统编程语言,为生产网络和多处理系统扩充(或取代)C++和其他静态编译语言。
Go 加入了一组现代语言,包括 Rust、Swift、Julia 和其他几种语言。Go 的独特之处在于简单的语法、多个程序单元的快速编译、一种基于“结构化”类型的 O/O 编程形式,当然还有从 C、C++和 Java 的大型程序中吸取的经验教训。
2017 年初的语言流行度列表,如 TIOBE(参见 http://www.tiobe.com/tiobe-index/ ))将 Go 列为目前第 14 大最受欢迎的语言。PYPL(参见 http://pypl.github.io/PYPL.html )排在第 19 位。这与 20 多年前的 Java、Python、C、C++、JavaScript 等语言齐名。
这本书假设你是一个有经验的程序员,在某种程度上有一些或广泛的 Go 知识。这可以通过介绍性文本,如 Caleb Doxsey (O'Reilly)的《Go 入门》或 Karl Seguin 的《Go 小百科全书》,或者通过阅读更正式的文档,如位于 https://golang.org/ref/spec 的《Go 编程语言规范》。
如果你是一个有经验的程序员,你可以跳过这一章。如果没有,这一章指出了本书中用到的一些 Go 知识,但是你应该去别的地方获取必要的背景知识。在 Go 网站的 http://golang.org 上有几个教程:
- 入门指南
- Go 编程语言教程
- 有效 Go
- GoLang 教程
最好从 Go 编程语言网站安装 Go。在写这篇文章的时候,Go 1.8 刚刚发布。本书中的大多数例子将使用 Go 1.6 运行,有一些指向 Go 1.8 的指针。你实际上不需要安装 Go 来测试程序:Go 有一个“操场”,可以从主页进入,用来运行代码。还有几个 REPL(读取-评估-打印循环)环境,但这些都是第三方的。
这本书主要使用了 Go 标准库中的库和包( https://golang.org/pkg/ )。Go 团队还构建了另一组包作为“子库”,它们通常不像标准库那样支持。这些偶尔会用到。需要使用go get命令安装它们。这些包的名字包含一个“x”,比如golang.org/x/net/ipv4。
类型
有预定义的布尔、数字和字符串类型。数字类型包括uint32、int32、float32和其他大小的数字,以及字节(uint8和符文。符文和字符串在第七章中被广泛讨论,因为国际化的问题在分布式程序中很重要。
还有更复杂的类型,将在下面讨论。
切片和阵列
数组是单一类型的元素序列。切片是基础数组的片段。Go 中处理切片往往更方便。可以静态创建数组:
var x [128]int
或者动态地作为指针:
xp := new([128]int)
切片可以与其底层阵列一起创建:
x := make([]int, 50, 100)
或者
x := new([100]int)[0:50]
这最后两个都是类型[]int(如reflect.TypeOf(x)所示)。
数组或切片的元素通过它们的索引来访问:
x[1]
索引从 0 到len(x)-1。
可以通过使用数组或切片的较低(包括)和较高(不包括)索引来获取数组或切片的切片:
a := [5]int{-1, -2, -3, -4, -5}
s := a[1:4] // s is now [-2, -3, -4]
结构
结构类似于其他语言中的结构。在第四章中,我们考虑数据的序列化,并以下列结构为例:
type Person struct {
Name Name
Email []Email
}
type Name struct {
Family string
Personal string
}
type Email struct {
Kind string
Address string
}
复合结构可以声明如下:
person := Person{
Name: Name{Family: "Newmarch", Personal: "Jan"},
Email: []Email{Email{Kind: "home",
Address: "jan@newmarch.name"},
Email{Kind: "work",
Address: "j.newmarch@boxhill.edu.au"}}}
结构字段的可见性由字段名称的第一个字符的大小写控制。如果它是大写的,那么它在声明它的包之外是可见的;如果是小写,就不是。在前面的示例中,所有结构的所有字段都是可见的。
两颗北极指极星
指针的行为类似于其他语言中的指针。*操作符解引用一个指针,而&操作符接受一个变量的地址。Go 简化了指针的使用,这样大部分时间你就不用担心了。我们在本书中最多做的是检查指针值是否是nil,这通常意味着一个错误,或者相反,如果一个可能的错误值不是nil,如下一节所述。
功能
使用 Go 特有的符号来定义函数。在 Go 的声明语法博客中解释了为什么没有使用大家熟悉的 C 语法(或者其他语法)。我们让教科书来解释语法的细节。
每个 Go 程序必须有一个如下声明的main函数:
func main() { ... }
我们将经常使用如下定义的函数checkError:
func checkError(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
os.Exit(1)
}
}
它接受一个参数,没有返回值。它以小写字母开头,所以它对于声明它的包来说是局部的。
返回值的函数通常会返回一个错误状态和一个实际值,如第三章中的函数:
func readFully(conn net.Conn) ([]byte, error) { ... }
它将net.Conn作为参数,并返回一个字节数组和一个错误状态(如果没有发生错误,则返回nil)。
在本书中,没有比这更复杂的定义被使用。
地图
映射是一种类型的无序元素组,由另一种类型的键索引。我们在本书中不太使用映射,尽管有一处是在第十章中,其中一个 HTTP 请求的字段值可以通过使用字段名作为关键字的映射来访问。
方法
Go 不像 Java 这样的语言那样有类。然而,类型可以有与之关联的方法,这些方法的行为类似于更标准的 O/O 语言的方法。
我们将大量使用为各种网络类型定义的方法。这将从下一章的第一个程序开始。例如,类型IPMask被定义为一个字节数组:
type IPMask []byte
在这种类型上定义了许多函数,例如:
func (m IPMask) Size() (ones, bits int)
类型为IPMask的变量可以应用方法Size(),如下所示:
var m IPMask
...
ones, bits := m.Size()
学习如何使用网络相关类型的方法是本书的主要目的。
在本书中,我们不会过多地定义我们自己的方法。这是因为为了说明 Go 库,我们不需要很多自己的复杂类型。一个典型的应用是修饰打印一个类似前面定义的Person类型的类型:
func (p Person) String() string {
s := p.Name.Personal + " " + p.Name.Family
for _, v := range p.Email {
s += "\n" + v.Kind + ": " + v.Address
}
return s
}
在第十章中有更广泛的使用,其中使用了许多类型和这些类型上的方法。这是因为当我们构建更现实的系统时,我们确实需要自己的类型。
多线程
Go 有一个使用go命令启动额外线程的简单机制。在本书中,这就是我们所需要的。这里不需要复杂的任务,比如同步多个线程。
包装
Go 程序是从链接包中构建的。任何代码块使用的包都必须通过代码文件头部的import语句导入。我们自己的程序被声明在包main中。
除了第十章之外,这本书里几乎所有的程序都在main包里。
大多数包都是从标准库中导入的。有些是从golang.org/x/net/ipv4等子库导入的。
类型变换
本书中我们唯一需要担心的是字符串到字节数组的转换,反之亦然。要将字符串转换为字节数组,您需要:
var b []byte
b = []byte("string")
要将整个数组/切片转换为字符串,请使用以下命令:
var s string
s = string(b[:])
声明
一个函数或方法将由一组语句组成。这些包括赋值、if和switch语句、for和while循环,以及其他一些语句。
除了语法之外,它们在本质上与其他编程语言具有相同的含义。几乎所有的语句类型都将在后面的章节中用到。
GOPATH(高路径)
有两种组织项目工作空间的方法:将每个项目放在一个共享的工作空间中,或者为每个项目准备一个单独的工作空间。我的偏好是第二种,而显然大多数 Go 程序员的偏好是第一种。
环境变量GOPATH支持go工具的任何一种方式。这可以设置为一个目录列表(Linux/UNIX 中的一个:分隔列表,Windows 中的一个;分隔列表,以及 Plan9 中的一个列表)。如果未设置,默认为用户主目录中的目录go。
对于GOPATH中的每个目录,会有三个子目录——src、pkg和bin。目录src通常包含每个包名的一个目录,在这个目录下是这个包的源文件。例如,在第十章中,我们有一个完整的 web 服务器,它使用我们定义的dictionary和flashcards包。src/flashcards目录包含文件FlashCards.go。
运行 Go 程序
Go 程序必须有一个定义包main的文件。本书中的大多数程序都是在一个文件中定义的,比如第三章中的程序IP.go。运行它的最简单方法是从包含该文件的目录中运行:
go run IP.go <IP address>
或者,您可以构建一个可执行文件,然后运行它:
go build IP.go
./IP <IP address>
需要标准软件包以外的软件包的程序将需要设置GOPATH。例如,第十章中的程序要求(在 Linux 下):
export GOPATH=$PWD
go run Server.go <port>
标准库
Go 有一套广泛的标准库。例如,没有 C、Java 或 C++大,但是这些语言已经存在很长时间了。Go 包记录在 https://golang.org/pkg/ 中,我们将在本书中广泛使用它们,特别是net、crypto和encoding包。
此外,在同一个页面上还有一个包的子存储库组。这些不太稳定,但有时会有有用的包,我们偶尔会用到。
除此之外,还有大量用户贡献的包。在本书论述原理的正文中不会用到它们,但实际上你会发现它们中的许多非常有用。最后一章讨论了一些问题。
误差值
我们在上一章中讨论过,分布式编程和本地编程的一个主要区别是在执行过程中发生错误的可能性大大增加。局部函数调用可能因为简单的编程错误而失败,例如被零除;可能会发生更细微的错误,如内存不足错误,但它们可能发生的情况通常是可以预测的。
另一方面,几乎所有利用网络的功能都可能因为应用程序无法控制的原因而失败。因此,网络程序充满了错误检查。这是乏味的,但也是必要的。就像操作系统内核代码总是要进行错误检查一样——需要对错误进行管理。
在本书中,我们通常在客户端用适当的消息退出一个有错误的程序,对于服务器,尝试通过断开有问题的连接并继续运行来恢复。
像 C 这样的语言通常通过返回“非法”值(如负整数、空指针)或发出信号来发出错误信号。像 Java 这样的语言会引发异常,这会导致混乱的代码,而且通常会很慢。标准的 Go 函数在函数调用返回的额外参数中给出了一个错误。
例如,在下一章中,我们将讨论net包中的函数:
func ResolveIPAddr(net, addr string) (*IPAddr, error)
管理这种情况的典型代码是:
addr, err := net.ResolveIPAddr("ip", name)
if err != nil {
...
}
结论
这本书假设你了解 Go 编程语言。本章只是强调了后面章节需要的部分。
三、套接字级编程
世界上有很多种网络。这些网络从非常古老的网络(如串行链路)到由铜线和光纤构成的广域网,再到各种类型的无线网络,既用于计算机,也用于电信设备(如电话)。这些网络显然在物理链路层有所不同,但在许多情况下,它们在 OSI 堆栈的更高层也有所不同。
这些年来,IP 和 TCP/UDP 的“互联网堆栈”已经趋同。例如,蓝牙定义了物理层和协议层,但是在物理层和协议层之上是 IP 栈,因此相同的互联网编程技术可以在许多蓝牙设备上使用。类似地,开发物联网(IoT)无线技术,如 LoRaWAN 和 6LoWPAN,也包括 IP 堆栈。
IP 提供 OSI 堆栈的第 3 层网络,而 TCP 和 UDP 处理第 4 层。即使在互联网世界中,这些也不是最终的结论:SCTP(流控制传输协议)来自电信世界,挑战 TCP 和 UDP,而在星际空间中提供互联网服务需要新的、正在开发的协议,如 DTN(延迟容忍网络)。然而,IP、TCP 和 UDP 作为主要的网络技术在现在以及至少在未来相当长的一段时间内占据主导地位。Go 完全支持这种风格的编程
本章展示了如何使用 Go 进行 TCP 和 UDP 编程,以及如何为其他协议使用原始套接字。
TCP/IP 协议栈
OSI 模型是使用一个委员会过程设计的,在这个过程中,标准被建立,然后被实施。OSI 标准的一些部分是模糊的,一些部分不容易实现,一些部分还没有实现。
TCP/IP 协议是通过长期运行的 DARPA 项目设计的。这是通过 RFC(征求意见)后的实施来实现的。TCP/IP 是主要的 UNIX 网络协议。TCP/IP 代表传输控制协议/互联网协议。
TCP/IP 协议栈比 OSI 协议栈短,如图 3-1 所示。
图 3-1。
TCP/IP stack versus the OSI
TCP 是面向连接的协议,而 UDP(用户数据报协议)是无连接的协议。
IP 数据报
IP 层提供了一个无连接和不可靠的传输系统。它认为每个数据报独立于其他数据报。数据报之间的任何关联必须由较高层提供。
IP 层提供包含其自身报头的校验和。报头包括源地址和目的地址。
IP 层处理通过互联网的路由。它还负责将较大的数据报分解成较小的数据报进行传输,并在另一端重新组装。
用户数据报协议(User Datagram Protocol)
UDP 也是无连接和不可靠的。它添加到 IP 中的是数据报内容和端口号的校验和。这些用来给出一个客户机-服务器模型,稍后你会看到。
三氯苯酚
TCP 提供逻辑,在 IP 之上给出可靠的面向连接的协议。它提供了两个进程可以用来通信的虚电路。它还使用端口号来识别主机上的服务。
互联网地址
为了使用服务,您必须能够找到它。互联网对计算机等设备使用地址方案,以便对它们进行定位。这种编址方案最初是在只有少数几台相连的计算机时设计的,使用 32 位无符号整数,非常宽松地允许多达 2³² 地址。这些就是所谓的 IPv4 地址。近年来,连接的(或者至少是可直接寻址的)设备的数量有超过这个数字的危险,并且正在逐步过渡到 IPv6。这种转变是不完整的,例如在谷歌( https://www.google.com/intl/en/ipv6/statistics.html )的图表中显示的。遗憾的是,在我看来,很少有澳大利亚 IP 提供商支持 IPv6。
IPv4 地址
该地址是一个 32 位整数,给出了 IP 地址。该地址可解析为单个设备上的网络接口卡。地址通常写成十进制的四个字节,中间用点.隔开,如127.0.0.1或66.102.11.104。
任何设备的 IP 地址通常由两部分组成:设备所在网络的地址,以及设备在该网络中的地址。从前,网络地址和内部地址之间的划分很简单,是基于 IP 地址中使用的字节。
- 在 A 类网络中,第一个字节标识网络,后三个字节标识设备。只有 128 个 A 类网络,由互联网领域的早期参与者拥有,如 IBM、通用电气公司和麻省理工学院 1 。
- B 类网络使用前两个字节标识网络,后两个字节标识子网内的设备。这允许一个子网上最多有 2¹⁶ (65,536)台设备。
- C 类网络使用前三个字节标识网络,最后一个字节标识网络中的设备。这允许多达 2⁸(实际上是 254,而不是 256,因为底部和顶部地址是保留的)设备。
如果你想要,比如说,一个网络上有 400 台计算机,这个方案就不太管用。254 太小,而 65,536 (-2)太大。用二进制算术术语来说,你要 512 (-2)左右。这可以通过使用 23 位网络地址和 9 位设备地址来实现。同样,如果您想要多达 1024 (-2)个设备,您可以使用 22 位网络地址和 10 位设备地址。
给定一个设备的 IP 地址,并且知道网络地址使用了多少位 N,给出了一个相对简单的过程来提取网络地址和该网络内的设备地址。形成一个“网络掩码”,它是一个 32 位二进制数,前 N 位全为 1,其余全为 0。例如,如果网络地址使用 16 位,掩码为11111111111111110000000000000000。用二进制有点不方便,所以一般用十进制字节。16 位网络地址的网络掩码为255.255.0.0,24 位网络地址的网络掩码为255.255.255.0,23 位网络地址的网络掩码为255.255.254.0,22 位网络地址的网络掩码为255.255.252.0。
然后查找设备的网络,按位AND使用网络掩码查找其 IP 地址,而子网内的设备地址则按位AND使用 IP 地址查找掩码的补码。例如,IP 地址192.168.1.3的二进制值是11000000101010000000000100000011(使用 IP 地址子网掩码计算器)。如果使用 16 位网络掩码,网络为1100000010101000 0000000000000000(或192.168.0.0),而设备地址为0000000000000000 0000000100000011(或0.0.1.3)。
IPv6 地址
互联网的发展远远超出了最初的预期。最初慷慨的 32 位寻址方案即将耗尽。有一些令人不快的解决方法,如 NAT(网络地址转换)寻址,但最终我们将不得不切换到更宽的地址空间。IPv6 使用 128 位地址。用偶数字节来表示这样的地址变得很麻烦,所以使用十六进制数字,分成四个数字,并用冒号:隔开。典型的地址可能是FE80:CD00:0000:0CDE:1257:0000:211E:729C。
这些地址不好记!DNS 将变得更加重要。减少一些地址是有技巧的,比如前导零和重复数字。比如“localhost”就是0:0:0:0:0:0:0:1,可以简称为::1。
每个地址分为三部分:第一部分是用于互联网路由的网络地址,是地址的前 64 位。下一部分是 16 位网络掩码。这用于将网络划分为子网。它可以给出从一个子网(全 0)到 65,535 个子网(全 1)的任何值。最后一部分是器件组件,48 位。上述地址将是网络的FE80:CD00:0000:0CDE、子网的1257和设备的0000:211E:729C。
IP 地址类型
最后,我们可以开始使用一些 Go 语言网络包。包net定义了许多类型、功能和在 Go 网络编程中的使用方法。类型IP被定义为一个字节数组:
type IP []byte
有几个函数可以操作类型为IP的变量,但是在实践中你可能只使用其中的一部分。例如,函数ParseIP(String)将采用带点的 IPv4 地址或冒号的 IPv6 地址,而 IP 方法String()将返回一个字符串。请注意,您可能无法回到开始时的状态:字符串形式的0:0:0:0:0:0:0:1是::1。
说明这个过程的一个程序是IP.go:
/* IP
*/
package main
import (
"fmt"
"net"
"os"
)
func main() {
if len(os.Args) != 2 {
fmt.Fprintf(os.Stderr, "Usage: %s ip-addr\n", os.Args[0])
os.Exit(1)
}
name := os.Args[1]
addr := net.ParseIP(name)
if addr == nil {
fmt.Println("Invalid address")
} else {
fmt.Println("The address is ", addr.String())
}
os.Exit(0)
}
例如,这可以如下运行:
go run IP.go 127.0.0.1
以下是回应:
The address is 127.0.0.1
或者它可以运行为:
go run IP.go 0:0:0:0:0:0:0:1
有了这样的回应:
The address is ::1
IPMask 类型
IP 地址通常分为网络地址、子网和设备部分。网络地址和子网构成了设备部分的前缀。掩码是一个全二进制的 IP 地址,以匹配前缀长度,后跟全零。
为了处理屏蔽操作,可以使用以下类型:
type IPMask []byte
创建网络掩码最简单的函数是使用 CIDR 表示法,即 1 后面跟 0,最大位数为:
func CIDRMask(ones, bits int) IPMask
然后,IP 地址的方法可以使用掩码来查找该 IP 地址的网络:
func (ip IP) Mask(mask IPMask) IP
下面这个叫做Mask.go的程序就是一个例子:
/* Mask
*/
package main
import (
"fmt"
"net"
"os"
"strconv"
)
func main() {
if len(os.Args) != 4 {
fmt.Fprintf(os.Stderr, "Usage: %s dotted-ip-addr ones bits\n", os.Args[0])
os.Exit(1)
}
dotAddr := os.Args[1]
ones, _ := strconv.Atoi(os.Args[2])
bits, _ := strconv.Atoi(os.Args[3])
addr := net.ParseIP(dotAddr)
if addr == nil {
fmt.Println("Invalid address")
os.Exit(1)
}
mask := net.CIDRMask(ones, bits)
network := addr.Mask(mask)
fmt.Println("Address is ", addr.String(),
"\nMask length is ", bits,
"\nLeading ones count is ", ones,
"\nMask is (hex) ", mask.String(),
"\nNetwork is ", network.String())
os.Exit(0)
}
这可以编译成Mask并运行如下:
Mask <ip-address> <ones> <zeroes>
也可以直接运行,如下所示:
go run Mask.go <ip-address> <ones> <zeroes>
对于/24网络上的 IPv4 地址103.232.159.187,我们得到如下结果:
go run Mask.go 103.232.159.187 24 32
Address is 103.232.159.187
Mask length is 32
Leading ones count is 24
Mask is (hex) ffffff00
Network is 103.232.159.0
对于一个 IPv6 地址fda3:97c:1eb:fff0:5444:903a:33f0:3a6b,其中网络组件是fda3:97c:1eb,子网是fff0,设备部分是5444:903a:33f0:3a6b,我们得到如下:
go run Mask.go fda3:97c:1eb:fff0:5444:903a:33f0:3a6b 52 128
Address is fda3:97c:1eb:fff0:5444:903a:33f0:3a6b
Mask length is 128
Leading ones count is 52
Mask is (hex) fffffffffffff0000000000000000000
Network is fda3:97c:1eb:f000::
IPv4 网络掩码通常以 4 字节点符号表示,如255.255.255.0表示/24网络。有一个函数可以从这样一个 4 字节的 IPv4 地址创建一个掩码:
func IPv4Mask(a, b, c, d byte) IPMask
此外,有一种 IP 方法可以返回 IPv4 的默认掩码:
func (ip IP) DefaultMask() IPMask
注意掩码的字符串形式是一个十六进制数,例如ffffff00代表/24掩码。
以下名为IPv4Mask.go的程序说明了这些:
/* IPv4Mask
*/
package main
import (
"fmt"
"net"
"os"
)
func main() {
if len(os.Args) != 2 {
fmt.Fprintf(os.Stderr, "Usage: %s dotted-ip-addr\n", os.Args[0])
os.Exit(1)
}
dotAddr := os.Args[1]
addr := net.ParseIP(dotAddr)
if addr == nil {
fmt.Println("Invalid address")
os.Exit(1)
}
mask := addr.DefaultMask()
network := addr.Mask(mask)
ones, bits := mask.Size()
fmt.Println("Address is ", addr.String(),
"\nDefault mask length is ", bits,
"\nLeading ones count is ", ones,
"\nMask is (hex) ", mask.String(),
"\nNetwork is ", network.String())
os.Exit(0)
}
例如,运行以下命令:
go run Mask.go 192.168.1.3
在我的家庭网络中给出以下结果:
Address is 192.168.1.3
Default mask length is 32
Leading ones count is 24
Mask is (hex) ffffff00
Network is 192.168.1.0
IPAddr 类型
net 包中的许多其他函数和方法返回一个指向IPAddr的指针。这只是一个包含 IP(和 IPv6 地址可能需要的区域)的结构。
type IPAddr {
IP IP
Zone string
}
这种类型的主要用途是在 IP 主机名上执行 DNS 查找。对于具有多个网络接口的不明确 IPv6 地址,可能需要该区域。
func ResolveIPAddr(net, addr string) (*IPAddr, error)
其中net是ip、ip4或ip6中的一个。这表现在名为ResolveIP.go的节目中:
/* ResolveIP
*/
package main
import (
"fmt"
"net"
"os"
)
func main() {
if len(os.Args) != 2 {
fmt.Fprintf(os.Stderr, "Usage: %s hostname\n", os.Args[0])
fmt.Println("Usage: ", os.Args[0], "hostname")
os.Exit(1)
}
name := os.Args[1]
addr, err := net.ResolveIPAddr("ip", name)
if err != nil {
fmt.Println("Resolution error", err.Error())
os.Exit(1)
}
fmt.Println("Resolved address is ", addr.String())
os.Exit(0)
}
运行这个:
go run ResolveIP.go www.google.com
返回以下内容:
Resolved address is 172.217.25.164
如果网络类型的第一个参数ResolveIPAddr()被给定为ip6而不是ip,我得到这样的结果:
Resolved address is 2404:6800:4006:801::2004
你可能会得到不同的结果,这取决于从你的地址来看谷歌似乎住在哪里。
主机查找
ResolveIPAddr函数将对主机名执行 DNS 查找,并返回一个 IP 地址。它如何做到这一点取决于操作系统及其配置。例如,Linux/UNIX 系统可能使用/etc/resolv.conf或/etc/hosts,搜索顺序设置在/etc/nsswitch.conf中。
一些主机可能有多个 IP 地址,通常来自多个网络接口卡。他们也可能有多个主机名,充当别名。LookupHost函数将返回一片地址。
func LookupHost(name string) (cname string, addrs []string, err error)
其中一个地址将被标记为“标准”主机名。如果您想找到规范名称,请使用:
。
func LookupCNAME(name string) (cname string, err error)
.
对于 www.google.com ,它打印 IPv4 和 IPv6 地址:
172.217.25.164
2404:6800:4006:806::2004
这显示在以下名为LookupHost.go的程序中:
/* LookupHost
*/
package main
import (
"fmt"
"net"
"os"
)
func main() {
if len(os.Args) != 2 {
fmt.Fprintf(os.Stderr, "Usage: %s hostname\n", os.Args[0])
os.Exit(1)
}
name := os.Args[1]
addrs, err := net.LookupHost(name)
if err != nil {
fmt.Println("Error: ", err.Error())
os.Exit(2)
}
for _, s := range addrs {
fmt.Println(s)
}
os.Exit(0)
}
注意,这个函数返回字符串,而不是 IP 地址值。运行时:
go run LookupHost.go
它打印出类似这样的内容:
172.217.25.132
2404:6800:4006:807::2004
服务
服务在主机上运行。它们通常具有很长的生命周期,被设计用来等待请求并对请求做出响应。服务有很多种,他们向客户提供服务的方式也有很多种。互联网世界中的许多服务都基于两种通信方法——TCP 和 UDP——尽管还有其他通信协议,如 SCTP,正准备接管。许多其他类型的服务,如点对点、远程过程调用、通信代理等,都是建立在 TCP 和 UDP 之上的。
港口
服务存在于主机上。我们可以使用 IP 地址来定位主机。但是在每台计算机上可能有许多服务,需要一种简单的方法来区分它们。TCP、UDP、SCTP 和其他协议使用的方法是使用端口号。这是一个介于 1 和 65,535 之间的无符号整数,每个服务将自己与这些端口号中的一个或多个相关联。
有许多“标准”端口。Telnet 通常使用 TCP 协议的端口 23。DNS 通过 TCP 或 UDP 使用端口 53。FTP 使用端口 21 和 20,一个用于命令,另一个用于数据传输。HTTP 一般使用端口 80,但也经常使用端口 8000、8080、8088,都是用 TCP。X Window 系统通常使用 TCP 和 UDP 上的 6000-6007 端口。
在 UNIX 系统上,常用端口列在文件/etc/services中。Go 具有在所有系统上查找端口的功能:
func LookupPort(network, service string) (port int, err error)
网络参数是一个字符串,如"tcp"或"udp",而服务是一个字符串,如"telnet"或"domain"(用于 DNS)。
使用这个的程序是LookupPort.go:
/* LookupPort
*/
package main
import (
"fmt"
"net"
"os"
)
func main() {
if len(os.Args) != 3 {
fmt.Fprintf(os.Stderr,
"Usage: %s network-type service\n",
os.Args[0])
os.Exit(1)
}
networkType := os.Args[1]
service := os.Args[2]
port, err := net.LookupPort(networkType, service)
if err != nil {
fmt.Println("Error: ", err.Error())
os.Exit(2)
}
fmt.Println("Service port ", port)
os.Exit(0)
}
例如,运行LookupPort tcp telnet打印服务端口 23。
TCPAddr 类型
TCPAddr类型是包含 IP、端口和区域的结构。需要该区域来区分可能不明确的 IPv6 链路本地地址和站点本地地址,因为不同的网络接口卡(NIC)可能具有相同的 IPv6 地址。
type TCPAddr struct {
IP IP
Port int
Zone string
}
创建一个TCPAddr的函数是ResolveTCPAddr:
func ResolveTCPAddr(net, addr string) (*TCPAddr, error)
其中net是tcp、tcp4或tcp6中的一个,addr是由主机名或 IP 地址组成的字符串,后跟:后的端口号,如www.google.com:80或127.0.0.1:22。如果地址是 IPv6 地址,其中已经有冒号,那么主机部分必须用方括号括起来,例如[::1]:23。另一种特殊情况通常用于服务器,其中主机地址为零,因此 TCP 地址实际上只是端口名,如 HTTP 服务器的:80所示。
TCP 套接字
当您知道如何通过网络和端口 id 访问服务时,接下来该怎么办呢?如果您是一个客户端,您需要一个 API 来允许您连接到一个服务,然后向该服务发送消息并从该服务读取回复。
如果您是服务器,您需要能够绑定到一个端口并监听它。当消息进来时,您需要能够阅读它并写回给客户端。
net. TCPConn是 Go 类型,允许客户端和服务器之间的全双工通信。感兴趣的两种主要方法如下:
func (c *TCPConn) Write(b []byte) (n int, err error)
func (c *TCPConn) Read(b []byte) (n int, err error)
客户端和服务器都使用TCPConn来读取和写入消息。
注意,TCPConn实现了io.Reader和io.Writer接口,因此任何使用读取器或写入器的方法都可以应用于TCPConn。
TCP 客户端
一旦客户端为服务建立了 TCP 地址,它就“拨号”该服务。如果成功,拨号盘返回一个TCPConn用于通信。客户端和服务器就此交换消息。通常,客户端使用TCPConn向服务器写入请求,并从TCPConn读取响应。这种情况一直持续到任一端(或两端)关闭连接。客户端使用以下函数建立 TCP 连接:
func DialTCP(net string, laddr, raddr *TCPAddr) (c *TCPConn, err error)
其中laddr是本地地址,通常设置为nil,raddr是服务的远程地址。net字符串是"tcp4"、"tcp6"或"tcp"中的一种,这取决于您是想要 TCPv4 连接、TCPv6 连接还是不在乎。
一个简单的例子可以由客户机提供给 web (HTTP)服务器。我们将在后面的章节中更详细地讨论 HTTP 客户端和服务器,所以现在我们保持简单。
客户端可能发送的消息之一是HEAD消息。这将向服务器查询有关该服务器和该服务器上的文档的信息。服务器返回信息,但不返回文档本身。发送查询 HTTP 服务器的请求可能如下:
"HEAD / HTTP/1.0\r\n\r\n"
这要求提供关于根文档和服务器的信息。典型的回答可能是:
HTTP/1.1 200 OK
Server: nginx/1.10.0 (Ubuntu)
Date: Tue, 28 Feb 2017 10:33:01 GMT
Content-Type: text/html
Content-Length: 2152
Last-Modified: Mon, 13 Oct 2008 02:38:03 GMT
Connection: close
ETag: "48f2b48b-868"
Accept-Ranges: bytes
我们首先给程序(GetHeadInfo.go)建立一个 TCP 地址的连接,发送请求字符串,然后读取并打印响应。编译后,可以按如下方式调用它:
GetHeadInfo www.google.com:80
程序是GetHeadInfo.go:
/* GetHeadInfo
*/
package main
import (
"fmt"
"io/ioutil"
"net"
"os"
)
func main() {
if len(os.Args) != 2 {
fmt.Fprintf(os.Stderr, "Usage: %s host:port ", os.Args[0])
os.Exit(1)
}
service := os.Args[1]
tcpAddr, err := net.ResolveTCPAddr
("tcp4", service)
checkError(err)
conn, err := net.DialTCP("tcp", nil, tcpAddr)
checkError(err)
_, err = conn.Write([]byte("HEAD / HTTP/1.0\r\n\r\n"))
checkError(err)
result, err := ioutil.ReadAll(conn)
checkError(err)
fmt.Println(string(result))
os.Exit(0)
}
func checkError(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
os.Exit(1)
}
}
要注意的第一点是正在进行的几乎过量的错误检查。这对于网络程序来说是正常的:失败的机会远远大于独立程序。客户端、服务器或中间的任何路由器和交换机上的硬件可能会出现故障;通信可能被防火墙阻止;网络负载可能会导致超时;当客户端与服务器通信时,服务器可能会崩溃。将执行以下检查:
- 指定的地址中可能有语法错误。
- 连接到远程服务的尝试可能会失败。例如,请求的服务可能没有运行,或者可能没有这样的主机连接到网络。
- 虽然已经建立了连接,但是如果连接突然中断或者网络超时,对服务的写入可能会失败。
- 类似地,读取可能会失败。
从服务器读取需要注释。在这种情况下,我们基本上从服务器读取一个响应。这将由连接上的文件结尾终止。但是,它可能由几个 TCP 包组成,所以我们需要一直读取,直到文件结束。io/ioutil函数ReadAll将处理这些问题并返回完整的响应。(感谢golang-nuts邮件列表上的罗杰·佩佩。)
这涉及到一些语言问题。首先,大多数函数返回一个 dual 值,可能的错误作为第二个值。如果没有错误发生,那么这将是nil。在 C 语言中,如果可能的话,通过返回特殊值,如NULL、或-1、或零,可以获得相同的行为。在 Java 中,同样的错误检查是通过抛出和捕获异常来管理的,这会使代码看起来非常混乱。
日间服务员
我们可以建立的最简单的服务是日间服务。这是 RFC 867 定义的标准互联网服务,TCP 和 UDP 的默认端口都是 13。不幸的是,随着(合理的)对安全的偏执的增加,几乎没有任何网站再运行日间服务器了。没关系;我们可以自己造。(对于那些感兴趣的人,如果你在你的系统上安装了inetd,你通常会得到一个日间服务器。)
服务器在一个端口上注册并监听该端口。然后它阻塞一个“接受”操作,等待客户端连接。当客户端连接时,accept 调用返回,并带有一个连接对象。日间服务非常简单,只需将当前时间写入客户端,关闭连接,然后继续等待下一个客户端。
相关电话如下:
func ListenTCP(net string, laddr *TCPAddr) (l *TCPListener, err error)
func (l *TCPListener) Accept() (c Conn, err error)
参数net可以设置为字符串"tcp"、"tcp4"或"tcp6"中的一个。如果要监听所有网络接口,IP 地址应设置为零;如果只想监听某个网络接口,则应设置为该接口的 IP 地址。如果端口设置为零,那么操作系统将为您选择一个端口。否则,你可以自己选择。请注意,在 UNIX 系统上,您不能监听低于 1024 的端口,除非您是系统管理员、root 用户,而低于 128 的端口是由 IETF 标准化的。示例程序选择端口 1200 没有任何特殊原因。TCP 地址给定为:1200—所有接口,端口 1200。
程序是DaytimeServer.go:
/* DaytimeServer
*/
package main
import (
"fmt"
"net"
"os"
"time"
)
func main() {
service := ":1200"
tcpAddr, err := net.ResolveTCPAddr("tcp", service)
checkError(err)
listener, err := net.ListenTCP("tcp", tcpAddr)
checkError(err)
for {
conn, err := listener.Accept()
if err != nil {
continue
}
daytime := time.Now().String()
conn.Write([]byte(daytime)) // don't care about return value
conn.Close() // we're finished with this client
}
}
func checkError(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
os.Exit(1)
}
}
如果你运行这个服务器,它将只是在那里等待,不做太多。当客户端连接到它时,它将通过向它发送日间字符串来响应,然后返回等待下一个客户端。
请注意,与客户端相比,服务器中的错误处理发生了变化。服务器应该永远运行,这样,如果客户机出现任何错误,服务器就会忽略该客户机并继续运行。否则,客户端可能会试图破坏与服务器的连接并使其崩溃!
我们还没有建立客户。这很容易,只需更改以前的客户端以省略初始写入。或者,只需打开到主机 telnet 连接:
telnet localhost 1200
这将产生如下输出:
$telnet localhost 1200
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
2017-01-02 20:13:21.934698384 +1100 AEDTConnection closed by foreign host.
其中2017-01-02 20:13:21.934698384 +1100 AEDT是服务器的输出。
多线程服务器
echo是另一个简单的 IETF 服务。SimpleEchoServer.go程序只是读取客户端输入的内容并将其发送回来:
/* SimpleEchoServer
*/
package main
import (
"fmt"
"net"
"os"
)
func main() {
service := ":1201"
tcpAddr, err := net.ResolveTCPAddr("tcp4", service)
checkError(err)
listener, err := net.ListenTCP("tcp", tcpAddr)
checkError(err)
for {
conn, err := listener.Accept()
if err != nil {
continue
}
handleClient(conn)
conn.Close() // we're finished
}
}
func handleClient(conn net.Conn) {
var buf [512]byte
for {
n, err := conn.Read(buf[0:])
if err != nil {
return
}
fmt.Println(string(buf[0:]))
_, err2 := conn.Write(buf[0:n])
if err2 != nil {
return
}
}
}
func checkError(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
os.Exit(1)
}
}
虽然它可以工作,但是这个服务器有一个重要的问题:它是单线程的。当一个客户端打开了一个连接时,没有其他客户端可以连接。其他客户端被阻止,可能会超时。幸运的是,这很容易通过使客户端处理程序成为一个go例程来解决。我们还将关闭连接移到了处理程序中,因为它现在属于那里。这个程序叫做ThreadedEchoServer. go:
/* ThreadedEchoServer
*/
package main
import (
"fmt"
"net"
"os"
)
func main() {
service := ":1201"
tcpAddr, err := net.ResolveTCPAddr("tcp", service)
checkError(err)
listener, err := net.ListenTCP("tcp", tcpAddr)
checkError(err)
for {
conn, err := listener.Accept()
if err != nil {
continue
}
// run as a goroutine
go handleClient(conn)
}
}
func handleClient(conn net.Conn) {
// close connection on exit
defer conn.Close()
var buf [512]byte
for {
// read up to 512 bytes
n, err := conn.Read(buf[0:])
if err != nil {
return
}
fmt.Println(string(buf[0:]))
// write the n bytes read
_, err2 := conn.Write(buf[0:n])
if err2 != nil {
return
}
}
}
func checkError(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
os.Exit(1)
}
}
控制 TCP 连接
超时
如果客户端响应不够快,即没有及时向服务器写入请求,服务器可能希望客户端超时。这应该是一段很长的时间(几分钟),因为用户可能会慢慢来。相反,客户端可能希望服务器超时(在短得多的时间后)。两者都是这样做的:
func (c *IPConn) SetDeadline(t time.Time) error
这是在套接字上的任何读取或写入之前完成的。
活着
客户端可能希望保持与服务器的连接,即使它没有要发送的内容。它可以使用这个:
func (c *TCPConn) SetKeepAlive(keepalive bool) error
还有其他几种连接控制方法,记录在net包中。
UDP 数据报
在无连接协议中,每条消息都包含有关其来源和目的地的信息。没有使用长期套接字建立的“会话”。UDP 客户端和服务器使用数据报,数据报是包含源和目的信息的单独消息。除非客户端或服务器维护状态,否则这些消息不会维护状态。不保证消息会到达,或者可能会无序到达。
对于客户端来说,最常见的情况是发送一条消息并希望收到回复。对于服务器来说,最常见的情况是接收一条消息,然后向客户端发送一个或多个回复。但是,在对等的情况下,服务器可能只是将消息转发给其他对等方。
Go 的 TCP 和 UDP 处理之间的主要区别是如何处理来自多个客户端的数据包,没有 TCP 会话的缓冲来管理事情。需要的主要调用如下:
func ResolveUDPAddr(net, addr string) (*UDPAddr, error)
func DialUDP(net string, laddr, raddr *UDPAddr) (c *UDPConn, err error)
func ListenUDP(net string, laddr *UDPAddr) (c *UDPConn, err error)
func (c *UDPConn) ReadFromUDP(b []byte) (n int, addr *UDPAddr, err error
func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (n int, err error)
UDP 时间服务的客户端不需要做很多改变;将UDPDaytimeClient.go程序中的...TCP...调用改为...UDP...调用即可:
/* UDPDaytimeClient
*/
package main
import (
"fmt"
"net"
"os"
)
func main() {
if len(os.Args) != 2 {
fmt.Fprintf(os.Stderr, "Usage: %s host:port", os.Args[0])
os.Exit(1)
}
service := os.Args[1]
udpAddr, err := net.ResolveUDPAddr("udp", service)
checkError(err)
conn, err := net.DialUDP("udp", nil, udpAddr)
checkError(err)
_, err = conn.Write([]byte("anything"))
checkError(err)
var buf [512]byte
n, err := conn.Read(buf[0:])
checkError(err)
fmt.Println(string(buf[0:n]))
os.Exit(0)
}
func checkError(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal error ", err.Error())
os.Exit(1)
}
}
而服务器必须在程序UDPDaytimeServer.go中再做一些改变:
/* UDPDaytimeServer
*/
package main
import (
"fmt"
"net"
"os"
"time"
)
func main() {
service := ":1200"
udpAddr, err := net.ResolveUDPAddr("udp", service)
checkError(err)
conn, err := net.ListenUDP("udp", udpAddr)
checkError(err)
for {
handleClient(conn)
}
}
func handleClient(conn *net.UDPConn) {
var buf [512]byte
_, addr, err := conn.ReadFromUDP(buf[0:])
if err != nil {
return
}
daytime := time.Now().String()
conn.WriteToUDP([]byte(daytime), addr)
}
func checkError(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal error ", err.Error())
os.Exit(1)
}
}
服务器按如下方式运行:
go run UDPDaytimeServer.go
同一主机上的客户端运行如下:
go run UDPDaytimeClient.go localhost:1200
输出将是这样的:
2017-03-01 21:37:03.988603994 +1100 AEDT
服务器监听多个套接字
一个服务器可能试图监听多个客户机,不只是在一个端口上,而是在许多端口上。在这种情况下,它必须在端口之间使用某种轮询机制。
在 C 中,select()调用让内核完成这项工作。该调用需要许多文件描述符。该过程被暂停。当其中一个上的 I/O 就绪时,唤醒完成,该过程可以继续。这比忙轮询便宜。在 Go 中,您可以通过为每个端口使用不同的go例程来完成相同的任务。当较低级别的select()发现 I/O 已经为线程准备好时,线程将变得可运行。
Conn、PacketConn 和侦听器类型
到目前为止,我们已经区分了 TCP 的 API 和 UDP 的 API,例如使用分别返回TCPConn和UDPConn的DialTCP和DialUDP。Conn类型是一个接口,TCPConn和UDPConn都实现了这个接口。在很大程度上,您可以处理这个接口,而不是这两种类型。
您可以使用一个单独的功能来代替 TCP 和 UDP 的单独拨号功能:
func Dial(net, laddr, raddr string) (c Conn, err error)
net可以是tcp、tcp4(仅 IPv4)、tcp6(仅 IPv6)、udp、udp4(仅 IPv4)、udp6(仅 IPv6)、ip、ip4(仅 IPv4)和ip6(仅 IPv6)中的任何一个,以及几个特定于 UNIX 的,例如用于 UNIX 套接字的unix。它将返回一个适当的Conn接口实现。注意,这个函数采用一个字符串而不是地址作为raddr参数,这样使用它的程序可以避免首先计算出地址类型。
使用此功能可以对程序进行微小的更改。例如,从网页获取HEAD信息的早期程序可以重写为IPGetHeadInfo. go:
/* IPGetHeadInfo
*/
package main
import (
"bytes"
"fmt"
"io"
"net"
"os"
)
func main() {
if len(os.Args) != 2 {
fmt.Fprintf(os.Stderr, "Usage: %s host:port", os.Args[0])
os.Exit(1)
}
service := os.Args[1]
conn, err := net.Dial("tcp", service)
checkError(err)
_, err = conn.Write([]byte("HEAD / HTTP/1.0\r\n\r\n"))
checkError(err)
result, err := readFully(conn)
checkError(err)
fmt.Println(string(result))
os.Exit(0)
}
func checkError(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
os.Exit(1)
}
}
func readFully(conn net.Conn) ([]byte, error) {
defer conn.Close()
result := bytes.NewBuffer(nil)
var buf [512]byte
for {
n, err := conn.Read(buf[0:])
result.Write(buf[0:n])
if err != nil {
if err == io.EOF {
break
}
return nil, err
}
}
return result.Bytes(), nil
}
这可以在我自己的机器上运行,如下所示:
go run IPGetHeadInfo.go localhost:80
它打印了关于在端口 80 上运行的服务器的以下信息:
HTTP/1.1 200 OK
Server: nginx/1.10.0 (Ubuntu)
Date: Wed, 01 Mar 2017 10:42:39 GMT
Content-Type: text/html
Content-Length: 2152
Last-Modified: Mon, 13 Oct 2008 02:38:03 GMT
Connection: close
ETag: "48f2b48b-868"
Accept-Ranges: bytes
使用此函数可以类似地简化服务器的编写:
func Listen(net, laddr string) (l Listener, err error)
这将返回一个实现了Listener接口的对象。这个接口有一个方法:
func (l Listener) Accept() (c Conn, err error)
这将允许构建服务器。利用这一点,前面给出的多线程Echo服务器变成了ThreadedIPEchoServer. go:
/* ThreadedIPEchoServer
*/
package main
import (
"fmt"
"net"
"os"
)
func main() {
service := ":1200"
listener, err := net.Listen("tcp", service)
checkError(err)
for {
conn, err := listener.Accept()
if err != nil {
continue
}
go handleClient(conn)
}
}
func handleClient(conn net.Conn) {
defer conn.Close()
var buf [512]byte
for {
n, err := conn.Read(buf[0:])
if err != nil {
return
}
_, err2 := conn.Write(buf[0:n])
if err2 != nil {
return
}
}
}
func checkError(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
os.Exit(1)
}
}
如果你想写一个 UDP 服务器,有一个名为PacketConn的接口和一个返回实现的方法:
func ListenPacket(net, laddr string) (c PacketConn, err error)
这个接口有处理包读写的主要方法ReadFrom和WriteTo。
Go net包推荐使用这些接口类型,而不是具体的接口类型。但是通过使用它们,你会失去特定的方法,比如TCPConn的SetKeepAlive和UDPConn的SetReadBuffer,除非你进行类型转换。这是你的选择。
原始套接字和 IPConn 类型
本节涵盖了大多数程序员不太可能需要的高级材料。它处理原始套接字,允许程序员构建自己的 IP 协议,或者使用 TCP 或 UDP 以外的协议。
TCP 和 UDP 并不是唯一建立在 IP 层之上的协议。网站 http://www.iana.org/assignments/protocol-numbers 列出了其中的大约 140 个(这个列表通常可以在 UNIX 系统的文件/etc/protocols中找到)。在这个列表中,TCP 和 UDP 分别只排在第 6 和第 17 位。
Go 允许您构建所谓的原始套接字,使您能够使用这些其他协议之一进行通信,甚至构建自己的协议。但是它提供了最低限度的支持:它将连接主机,并在主机之间读写数据包。在下一章,我们将着眼于在 TCP 之上设计和实现你自己的协议;本节考虑的是同一类型的问题,但是是在 IP 层。
为了简单起见,我们使用最简单的例子:如何向主机发送 IPv4 ping 消息。Ping 使用 ICMP 协议中的echo命令。这是一个面向字节的协议,客户端向另一台主机发送字节流,主机进行回复。ICMP 数据包有效负载的格式如下:
- 第一个字节是 8,代表回应消息。
- 第二个字节是零。
- 第三和第四个字节是整个消息的校验和。
- 第五和第六个字节是一个任意的标识符。
- 第七个和第八个字节是一个任意的序列号。
- 数据包的其余部分是用户数据。
可以使用Conn.Write方法发送数据包,该方法用这个有效载荷准备数据包。收到的回复包括 IPv4 报头,占用 20 个字节。(例如,参见维基百科关于因特网控制消息协议 ICMP 的文章。)
下面这个名为Ping. go的程序将准备一个 IP 连接,向一个主机发送 ping 请求,并得到回复。您可能需要 root 访问权限才能成功运行它:
/* Ping
*/
package main
import (
"bytes"
"fmt"
"io"
"net"
"os"
)
// change this to my own IP address or set to 0.0.0.0
const myIPAddress = "192.168.1.2"
const ipv4HeaderSize = 20
func main() {
if len(os.Args) != 2 {
fmt.Println("Usage: ", os.Args[0], "host")
os.Exit(1)
}
localAddr, err := net.ResolveIPAddr("ip4", myIPAddress)
if err != nil {
fmt.Println("Resolution error", err.Error())
os.Exit(1)
}
remoteAddr, err := net.ResolveIPAddr("ip4", os.Args[1])
if err != nil {
fmt.Println("Resolution error", err.Error())
os.Exit(1)
}
conn, err := net.DialIP("ip4:icmp", localAddr, remoteAddr)
checkError(err)
var msg [512]byte
msg[0] = 8 // echo
msg[1] = 0 // code 0
msg[2] = 0 // checksum, fix later
msg[3] = 0 // checksum, fix later
msg[4] = 0 // identifier[0]
msg[5] = 13 // identifier[1] (arbitrary)
msg[6] = 0 // sequence[0]
msg[7] = 37 // sequence[1] (arbitrary)
len := 8
// now fix checksum bytes
check := checkSum(msg[0:len])
msg[2] = byte(check >> 8)
msg[3] = byte(check & 255)
// send the message
_, err = conn.Write(msg[0:len])
checkError(err)
fmt.Print("Message sent: ")
for n := 0; n < 8; n++ {
fmt.Print(" ", msg[n])
}
fmt.Println()
// receive a reply
size, err2 := conn.Read(msg[0:])
checkError(err2)
fmt.Print("Message received:")
for n := ipv4HeaderSize; n < size; n++ {
fmt.Print(" ", msg[n])
}
fmt.Println()
os.Exit(0)
}
func checkSum(msg []byte) uint16 {
sum := 0
// assume even for now
for n := 0; n < len(msg); n += 2 {
sum += int(msg[n])*256 + int(msg[n+1])
}
sum = (sum >> 16) + (sum & 0xffff)
sum += (sum >> 16)
var answer uint16 = uint16(^sum)
return answer
}
func checkError(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
os.Exit(1)
}
}
func readFully(conn net.Conn) ([]byte, error) {
defer conn.Close()
result := bytes.NewBuffer(nil)
var buf [512]byte
for {
n, err := conn.Read(buf[0:])
result.Write(buf[0:n])
if err != nil {
if err == io.EOF {
break
}
return nil, err
}
}
return result.Bytes(), nil
}
它使用目标地址作为参数来运行。接收到的消息与发送的消息的区别仅在于第一个类型字节以及第三和第四个校验和字节,如下所示:
Message sent: 8 0 247 205 0 13 0 37
Message received: 0 0 255 205 0 13 0 37
结论
本章考虑了 IP、TCP 和 UDP 级别的编程。如果您想要实现自己的协议或者为现有协议构建客户端或服务器,这通常是必要的。
Footnotes 1
最近,麻省理工学院将他们的 A 类网络归还给了游泳池。 http://www.iana.org/assignments/ipv4-address-space/ipv4-address-space.xml 。