Skip to content

Latest commit

 

History

History
566 lines (338 loc) · 50.5 KB

File metadata and controls

566 lines (338 loc) · 50.5 KB

一、网络编程与 Python

本书将着重于为使用 Internet 协议套件的网络编写程序。我们为什么选择这样做?好的,在 Python 标准库支持的协议集中,TCP/IP 协议是目前最广泛使用的。它包含互联网使用的主要协议。通过学习为 TCP/IP 编程,您将学习如何潜在地与几乎所有连接到网络电缆和电磁波的设备进行通信。

在本章中,我们将介绍一些有关 Python 网络和网络编程的概念和方法,我们将在本书中使用这些概念和方法。

本章分为两节。第一部分TCP/IP 网络简介介绍了基本的网络概念,重点介绍了 TCP/IP 协议栈。我们将研究网络的组成部分,互联网协议IP如何允许跨网络和网络之间的数据传输,以及 TCP/IP 如何为我们提供帮助我们开发网络应用的服务。本节旨在为这些基本区域提供基础,并作为参考点。如果您已经熟悉了 IP 地址、路由、TCP 和 UDP 以及协议栈层等概念,那么您可能希望跳到第二部分,使用 Python进行网络编程。

在第二部分中,我们将介绍使用 Python 进行网络编程的方法。我们将介绍主要的标准库模块,查看一些示例以了解它们与 TCP/IP 堆栈的关系,然后我们将讨论查找和使用满足我们网络需求的模块的一般方法。我们还将研究在编写通过 TCP/IP 网络进行通信的应用时可能遇到的几个一般问题。

TCP/IP 网络简介

互联网协议套件,通常称为 TCP/IP,是一组协议,旨在协同工作,在互联网络上提供端到端的消息传输。

以下讨论基于互联网协议版本 4IPv4)。由于 Internet 已用完 IPv4 地址,因此开发了一个新版本 IPv6,旨在解决这种情况。然而,尽管 IPv6 正在少数地区使用,但其部署进展缓慢,大多数互联网可能会在一段时间内使用 IPv4。本节我们将重点介绍 IPv4,然后在本章第二部分讨论 IPv6 的相关变化。

TCP/IP 在由互联网工程任务组IETF发布的名为征求意见RFCs的文件中进行了规定。RFC 涵盖了广泛的标准,TCP/IP 只是其中之一。可在 IETF 网站上免费获取,网址为,网址为www.IETF.org/rfc.html。每个 RFC 都有一个编号,IPv4 由 RFC 791 记录,其他相关的 RFC 将在我们进行过程中提及。

请注意,在本章中您将不会学习如何建立自己的网络,因为这是一个大主题,不幸的是,有些超出了本书的范围。但是,它至少应该使您能够与网络支持人员进行有意义的对话!

IP 地址

那么,让我们从你可能熟悉的东西开始,那就是 IP 地址。它们通常看起来像这样:

203.0.113.12

它们实际上是一个 32 位的数字,尽管它们通常与前面示例中所示的数字一样编写;它们以四个十进制数字的形式书写,这些数字由点分隔。这些数字有时被称为八位字节或字节,因为每个数字代表 32 位数字中的 8 位。因此,每个八位字节只能取 0 到 255 之间的值,因此有效 IP 地址的范围为 0.0.0.0 到 255.255.255.255。这种写入 IP 地址的方式称为点十进制表示法。

IP 地址执行两个主要功能。详情如下:

  • 它们唯一地为连接到网络的每个设备寻址
  • 它们有助于在网络之间路由流量

您可能已经注意到,您使用的网络连接设备具有分配给它们的 IP 地址。分配给网络设备的每个 IP 地址都是唯一的,没有两个设备可以共享一个 IP 地址。

网络接口

通过在终端上运行ip addr(或 Windows 上的ipconfig /all,您可以找到分配给您计算机的 IP 地址。在第 6 章IP 和 DNS中,我们将看到在使用 Python 时如何做到这一点。

如果我们运行其中一个命令,那么我们可以看到 IP 地址被分配到设备的网络接口。在 Linux 上,这些将有名称,例如eth0;在 Windows 上,这些将有短语,例如Ethernet adapter Local Area Connection

在 Linux 上运行ip addr命令时,您将获得以下输出:

$ ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN
 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
 inet 127.0.0.1/8 scope host lo
 valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
 link/ether b8:27:eb:5d:7f:ae brd ff:ff:ff:ff:ff:ff
 inet 192.168.0.4/24 brd 192.168.0.255 scope global eth0
 valid_lft forever preferred_lft forever

在前面的示例中,接口的 IP 地址出现在单词inet之后。

接口是设备与其网络媒体的物理连接。它可以是连接到网络电缆的网卡,也可以是使用特定无线技术的收音机。台式计算机可能只有一个网线接口,而智能手机可能至少有两个接口,一个用于连接 Wi-Fi 网络,另一个用于连接使用 4G 或其他技术的移动网络。

一个接口通常只分配一个 IP 地址,设备中的每个接口都有不同的 IP 地址。因此,回到上一节讨论的 IP 地址的用途,我们现在可以更准确地说,它们的第一个主要功能是唯一地寻址每个设备到网络的连接。

每个设备都有一个称为环回接口的虚拟接口,您可以在前面的列表中看到接口1。这个接口实际上没有连接到设备之外的任何东西,只有设备本身可以与其通信。虽然这听起来有点多余,但在进行本地网络应用测试时,它实际上非常有用,还可以用作进程间通信的一种手段。环回接口通常被称为本地主机,并且几乎总是为其分配 IP 地址 127.0.0.1。

分配 IP 地址

网络管理员可以通过以下两种方式之一将 IP 地址分配给设备:静态,设备的操作系统手动配置 IP 地址;或动态,设备的操作系统使用动态主机配置协议进行配置 DHCP)。

使用 DHCP 时,一旦设备第一次连接到网络,DHCP 服务器就会从预定义的池中自动为其分配地址。某些网络设备(如家庭宽带路由器)提供现成的 DHCP 服务器服务,否则必须由网络管理员设置 DHCP 服务器。DHCP 被广泛部署,对于不同设备可能频繁连接和断开连接的网络(如公共 Wi-Fi 热点或移动网络)尤其有用。

互联网上的 IP 地址

互联网是一个巨大的 IP 网络,每个通过它发送数据的设备都被分配了一个 IP 地址。

IP 地址空间由名为互联网分配号码管理局IANA的组织管理。IANA 决定 IP 地址范围的全球分配,并将地址块分配给全球的区域互联网注册中心RIRs),然后这些注册中心将地址块分配给国家和组织。接收组织有权在自己的网络中根据自己的喜好从分配的块中分配地址。

有一些特殊的 IP 地址范围。IANA 定义了专用地址的范围。这些范围永远不会分配给任何组织,因此任何人都可以将其用于其网络。专用地址范围如下所示:

  • 10.0.0.0 至 10.255.255.255
  • 172.16.0.0 至 172.31.255.255
  • 192.168.0.0 至 192.168.255.255

你可能会想,如果任何人都能使用它们,那么这是否意味着互联网上的设备最终将使用相同的地址,从而破坏 IP 的唯一寻址属性?这是一个很好的问题,通过禁止来自私人地址的流量通过公共互联网路由,这个问题得以避免。当使用专用地址的网络需要与公共互联网通信时,使用称为网络地址转换NAT)的技术,这实质上使来自专用网络的流量看起来来自单个有效的公共互联网地址,这有效地隐藏了互联网上的私人地址。稍后我们将讨论 NAT。

如果您检查家庭网络上的ip addripconfig /all输出,您将发现您的设备正在使用专用范围地址,该地址将由您的宽带路由器通过 DHCP 分配给它们。

下面几节我们将讨论网络流量,让我们了解一下它是什么。

许多协议,包括 Internet 协议套件中的主要协议,都采用了一种称为打包的技术来帮助管理通过网络传输的数据。

当一个打包协议被赋予一些要传输的数据时,它会将其分解成几个小单元——字节序列,通常有几千字节长,然后在每个单元的前面加上一些特定于协议的信息。前缀称为头,前缀和数据一起构成包。数据包中的数据通常称为其有效载荷

数据包包含的内容如下图所示:

Packets

有些协议对数据包使用替代术语,如帧,但我们现在将继续使用术语数据包。报头包括在另一个设备上运行的协议实现能够解释数据包是什么以及如何处理它所需的所有信息。例如,IP 分组报头中的信息包括源 IP 地址、目的地 IP 地址、分组的总长度以及报头中数据的校验和。

一旦创建,数据包就被发送到网络上,在那里它们被独立地路由到目的地。在分组中发送数据具有若干优点,包括多路复用(其中多个设备可以一次通过网络发送数据)、快速通知网络上可能发生的错误、拥塞控制和动态重新路由。

协议可以调用其他协议来为它们处理它们的数据包;将数据包传递到第二个协议以进行传递。当两个协议都采用分组时,会产生嵌套的数据包,如下图所示:

Packets

这被称为封装,我们将很快看到,它是构建网络流量的强大机制。

网络

网络是连接的网络设备的离散集合。网络的规模变化很大,可以由更小的网络组成。您家中的网络连接设备或大型办公楼中的网络连接计算机就是网络的例子。

定义网络的方法很多,有些松散,有些非常具体。根据上下文,网络可以由物理边界、行政边界、机构边界或网络技术边界来定义。

在本节中,我们将从一个网络的简化定义开始,然后以 IP 子网的形式进行更具体的定义。

因此,对于我们的简化定义,我们对网络的共同定义特征是,网络上的所有设备共享一个与互联网其余部分的连接点。在一些大型或专用网络中,您会发现存在不止一个连接点,但为了简单起见,我们将在这里坚持使用单个连接。

该连接点称为网关和通常采用称为路由器的特殊网络设备的形式。路由器的工作是引导网络之间的通信。它位于两个或多个网络之间,据说位于这些网络的边界。它总是有两个或两个以上的网络接口;它连接到的每个网络一个。路由器包含一组称为路由表的规则,它告诉路由器如何根据数据包的目的 IP 地址将通过它的数据包转发。

网关将数据包转发给另一个路由器,称为上游,通常位于网络的互联网服务提供商ISP)。ISP 的路由器属于第二类路由器,即它位于前面描述的网络之外,在网络网关之间路由流量。这些路由器由 ISP 和其他通信实体运行。它们通常是分层排列的,较高的区域层为一些国家或大陆的大部分地区路由流量,并形成互联网的主干网。

因为这些路由器可以位于多个网络之间,所以它们的路由表可能变得非常广泛,需要不断更新。下图显示了简化的图示:

Networks

前面的图为我们提供了一个布局的概念。每个 ISP 网关将 ISP 网络连接到区域路由器,每个家庭宽带路由器都有一个家庭网络连接到它。在现实世界中,当一个人走向顶端时,这种安排会变得更加复杂。ISP 通常会有多个网关将其连接到区域路由器,其中一些本身也将充当区域路由器。区域路由器也有比这里显示的更多的层,它们之间有许多连接,它们的排列方式比这个简单的层次结构复杂得多。根据 2005 年收集的数据对互联网的一部分进行了渲染,这是一个很好的例子,可以在上找到 http://en.wikipedia.org/wiki/Internet_backbone#/media/File:Internet_map_1024.jpg

IP 路由

我们提到路由器能够将流量路由到目标网络,并暗示这是通过使用 IP 地址和路由表实现的。但这里到底发生了什么?

对于路由器来说,一个显而易见的方法是为每个 IP 地址为每个路由器的路由表编程,以确定转发流量的正确路由器。然而,在实践中,40 多亿 IP 地址和不断变化的网络路由,这是一个完全不可行的方法。

那么,路由是如何完成的呢?答案在于 IP 地址的另一个属性。IP 地址可以解释为由两个逻辑部分组成:一个网络前缀和一个主机标识符。网络前缀唯一标识设备所在的网络,设备可以使用该前缀确定如何处理其生成或接收的流量以进行转发。网络前缀是 IP 地址的第一个n位,当它以二进制形式写入时(请记住,IP 地址实际上只是一个 32 位的数字)。n位由网络管理员提供,作为设备网络配置的一部分,同时提供其 IP 地址。

您将看到n是用两种方式之一编写的。它可以简单地附加到 IP 地址,用斜杠分隔,如下所示:

192.168.0.186/24

这被称为CIDR 符号。或者,也可以将其写入子网掩码,有时也称为子网掩码。这是您通常会看到在设备的网络配置中指定n的方式。子网掩码是以点十进制表示法写入的 32 位数字,就像 IP 地址一样。

255.255.255.0

此子网掩码相当于/24。通过在二进制中查看它,我们从中得到n。以下是一些例子:

255.0.0.0       = 11111111 00000000 00000000 00000000 = /8
255.192.0.0     = 11111111 11000000 00000000 00000000 = /10
255.255.255.0   = 11111111 11111111 11111111 00000000 = /24
255.255.255.240 = 11111111 11111111 11111111 11110000 = /28

n只是子网掩码中的 1 位数。(总是将最左边的位设置为 1,因为这允许我们通过对 IP 地址和子网掩码执行按位AND操作,以二进制形式快速获取网络前缀)。

那么,这对路由有什么帮助呢?当网络设备生成需要通过网络发送的网络流量时,它首先将目的地的 IP 地址与其自身的网络前缀进行比较。如果目标 IP 地址具有与发送设备相同的网络前缀,则发送设备将识别目标设备位于同一网络上,因此,它可以直接向其发送流量。如果网络前缀不同,则它会将消息发送到其默认网关,该网关会将消息转发到接收设备。

当路由器接收到必须转发的流量时,它首先检查目标 IP 地址是否与它所连接的任何网络的网络前缀匹配。如果是这种情况,那么它将直接将消息发送到该网络上的目标设备。如果没有,它将查阅路由表。如果它找到一个匹配的规则,那么它会将消息发送到它发现列出的路由器,如果没有定义明确的规则,那么它会将流量发送到它自己的默认网关。

当我们使用给定的网络前缀创建网络时,在 IP 地址的 32 位中,网络前缀右侧的数字可分配给网络设备。我们可以通过将 2 提高到可用比特数的幂来计算可用地址数。例如,在/28网络前缀中,我们剩下 4 位,这意味着有 16 个地址可用。实际上,我们能够分配更少的地址,因为计算范围内的两个地址总是保留的。这些是:范围内的第一个地址,称为网络地址,以及范围内的最后一个地址,称为广播地址

该地址范围由其网络前缀标识,称为子网。当 IANA、RIR 或 ISP 向组织分配 IP 地址块时,子网是分配的基本单位。组织将子网分配给其各种网络。

组织只需使用比分配给它们的网络前缀更长的网络前缀,就可以将它们的地址进一步划分为子网。他们这样做可能是为了更有效地利用自己的地址,也可能是为了创建一个网络层次结构,可以在整个组织中进行委派。

DNS

我们已经讨论了使用 IP 地址连接到网络设备。但是,除非您从事网络或系统管理工作,否则您不太可能经常看到 IP 地址,即使我们中的许多人每天都在使用 Internet。当我们浏览网页或发送电子邮件时,我们通常使用主机名或域名连接到服务器。这些必须以某种方式映射到服务器的 IP 地址。但这是如何做到的?

域名系统DNS)记录为 RFC 1035,是一个全球分布的主机名和 IP 地址之间映射的数据库。它是一个开放的分层系统,许多组织选择运行自己的 DNS 服务器。DNS 也是一种协议,设备使用它来查询 DNS 服务器,以便将主机名解析为 IP 地址(反之亦然)。

nslookup工具随大多数 Linux 和 Windows 系统提供,它允许我们在命令行上查询 DNS,如下所示:

$ nslookup python.org
Server:         192.168.0.4
Address:        192.168.0.4#53

Non-authoritative answer:
Name:   python.org
Address: 104.130.43.121

这里,我们确定python.org主机具有 IP 地址104.130.42.121。DNS 通过使用缓存服务器的分层系统来分配查找主机名的工作。连接到网络时,您的网络设备将通过 DHCP 或手动获得本地 DNS 服务器,并在进行 DNS 查找时查询此本地服务器。如果该服务器不知道 IP 地址,那么它将查询自己配置的更高层服务器,依此类推,直到找到答案。ISP 运行自己的 DNS 缓存服务器,宽带路由器通常也充当缓存服务器。在本例中,我的设备的本地服务器是192.168.0.4

设备的操作系统通常处理 DNS,并提供一个编程接口,应用使用该接口要求设备解析主机名和 IP 地址。Python 为此提供了一个接口,我们将在第 6 章IP 和 DNS中讨论。

协议栈还是互联网像蛋糕

互联网协议是组成互联网协议套件的一组协议的成员。套件中的每个协议都是为解决网络中的特定问题而设计的。我们刚刚看到 IP 如何解决寻址和路由问题。

套件中的核心协议设计为在堆栈中协同工作。也就是说,每个协议占据堆栈中的一层,其他协议位于该层的上下。所以,它就像蛋糕一样分层。每一层都向它上面的层提供一个特定的服务,同时根据封装原则向它们隐藏自己操作的复杂性。理想情况下,每一层只与它下面的层交互,以便从下面所有层的全部问题解决能力中获益。

Python 提供了与不同协议接口的模块。由于协议采用封装,我们通常只需要使用一个模块就可以利用底层堆栈的功能,从而避免较低层的复杂性。

TCP/IP 套件定义了四个层,尽管为了清晰起见通常使用五个层。下表给出了这些参数:

|

|

名称

|

示例协议

| | --- | --- | --- | | 5. | 应用层 | HTTP、SMTP、IMAP | | 4. | 传输层 | TCP,UDP | | 3. | 网络层 | 知识产权 | | 2. | 数据链路层 | 以太网、PPP、FDDI | | 1. | 物理层 | - |

第 1 层和第 2 层对应于 TCP/IP 套件的第一层。这两个底层处理底层网络基础设施和服务。

第 1 层对应于网络的物理介质,如电缆或 Wi-Fi 无线电。第 2 层提供从一个网络设备到另一个直接连接的网络设备的数据获取服务。该层可以使用各种第 2 层协议,如以太网或 PPP,只要第 3 层中的 Internet 协议可以要求它使用任何类型的可用物理介质将数据传输到网络中的下一个设备。

我们不需要关心最底层的两个层,因为在使用 Python 时很少需要与它们进行接口。它们的操作几乎总是由操作系统和网络硬件来处理。

第 3 层被不同地称为网络层和因特网层。它专门使用互联网协议。正如我们已经看到的,它的任务主要是网络寻址和路由。同样,在 Python 中,我们通常不直接与该层接口。

第 4 层和第 5 层对于我们来说更有趣。

第 4 层——TCP 和 UDP

第 4 层是我们可能希望在 Python 中使用的第一层。该层可以采用两种协议中的一种:传输控制协议**(TCP)和用户数据报协议**(UDP)。这两者都提供了在不同网络设备上的应用之间进行端到端数据传输的公共服务。****

****### 网络端口

尽管 IP 有助于将数据从一个网络设备传输到另一个网络设备,但它并没有为我们提供一种方法,让目标设备在收到数据后知道应该如何处理数据。一种可能的解决方案是对目标设备上运行的每个进程进行编程,以检查所有传入数据,看它们是否感兴趣,但这将很快导致明显的性能和安全问题。

TCP 和 UDP 通过引入端口的概念来提供答案。端口是一个端点,它连接到分配给网络设备的一个 IP 地址。端口由设备上运行的进程声明,然后该进程被称为在该端口上侦听。端口由 16 位数字表示,因此设备上的每个 IP 地址都有 65535 个进程可以声明的可能端口(端口号 0 保留)。端口一次只能由一个进程声明,即使一个进程一次可以声明多个端口。

当通过 TCP 或 UDP 通过网络发送消息时,发送应用在 TCP 或 UDP 数据包的报头中设置目标端口号。当消息到达目的地时,在接收设备上运行的 TCP 或 UDP 协议实现将读取端口号,然后将消息有效负载传递给正在该端口上侦听的进程。

在发送消息之前,需要知道端口号。这方面的主要机制是公约。除了管理 IP 地址空间外,IANA 还负责管理网络服务端口号的分配。

服务是一类应用,例如 web 服务器或 DNS 服务器,通常与应用协议绑定。端口分配给服务而不是特定的应用,因为它使服务提供商能够灵活地选择要使用哪种软件来提供服务,例如,不必担心仅仅因为服务器已经开始使用 Apache 而不是 IIS 而需要查找并连接到新端口号的用户。

大多数操作系统都包含此服务列表及其指定端口号的副本。在 Linux 上,这通常位于/etc/services,在 Windows 上,这通常位于c:\windows\system32\drivers\etc\services。完整列表也可在在线查看 http://www.iana.org/assignments/port-numbers

TCP 和 UDP 数据包头还可以包括源端口号。这对于 UDP 是可选的,但对于 TCP 是必需的。源端口号告诉服务器上的接收应用,当将数据发送回客户端时,它应该将应答发送到哪里。应用可以指定希望使用的源端口,或者如果尚未为 TCP 指定源端口,则在发送数据包时,操作系统会随机分配一个源端口。一旦操作系统有了一个源端口号,它就会将其分配给调用应用,并开始监听它以获得应答。如果在该端口上接收到应答,则将接收到的数据传递给发送应用。

因此,TCP 和 UCP 都通过提供端口为应用数据提供端到端传输,并且都使用 Internet 协议将数据传输到目标设备。现在,让我们看看它们的特点。

UDP

UDP 记录为 RFC 768。它刻意地简单:除了我们在上一节中描述的服务之外,它不提供其他服务。它只需要接收我们想要发送的数据,用目标端口号(以及可选的源端口号)对其进行打包,然后将其交给本地 Internet 协议实现进行交付。接收端的应用在数据打包的相同离散块中查看数据。

IP 和 UDP 都是所谓的无连接协议。这意味着,他们会尽最大努力交付数据包,但如果出现问题,他们会耸耸肩,继续交付下一个数据包。无法保证我们的数据包会到达目的地,如果传递失败,也不会发出错误通知。如果数据包真的成功了,那么就不能保证它们会按照发送时的相同顺序发送。由更高层的协议或发送应用来确定数据包是否到达以及是否处理任何问题。这些都是火与遗忘式的协议。

UDP 的典型应用是互联网电话和视频流。DNS 查询也使用 UDP 传输。

我们现在将介绍 UDP 更可靠的同级 TCP,然后讨论它们之间的区别,以及应用选择使用其中一种的原因。

TCP

传输控制协议记录为 RFC 761。与 UDP 相反,TCP 是一种基于连接的协议。在这样的协议中,在服务器和客户端执行了控制数据包的初始交换之前,不发送任何数据。这种交换称为握手。这就建立了一个连接,从此可以发送数据。接收方确认接收到的每个数据包,并通过发送称为ACK的数据包来确认。因此,TCP 总是要求数据包包含一个源端口号,因为它依赖于消息的连续双向交换。

从应用的角度来看,UDP 和 TCP 之间的关键区别在于应用不再看到离散块中的数据;TCP 连接将数据作为连续、无缝的字节流呈现给应用。如果我们发送的消息比一个典型的数据包大,那么事情就简单多了,但是这意味着我们需要开始考虑对我们的消息进行。使用 UDP 时,我们可以依靠它的打包来提供实现这一点的方法,而使用 TCP 时,我们必须确定一种机制来明确地确定消息的开始和结束位置。我们将在第 8 章客户机和服务器应用中了解更多信息。

TCP 提供以下服务:

  • 按订单交货
  • 回执确认
  • 错误检测
  • 流量和拥塞控制

通过 TCP 发送的数据保证按照发送顺序发送到接收应用。接收 TCP 实现在接收设备上缓冲接收到的数据包,然后等待,直到它能够以正确的顺序传递数据包,然后再将它们传递给应用。

由于数据包已确认,因此发送应用可以确保数据已到达并且可以继续发送数据。如果发送的数据包未收到 ACK,则在设定的时间段内,数据包将重新发送。如果仍然没有响应,那么 TCP 将继续以增加的间隔重新发送数据包,直到第二个更长的超时时间到期。此时,它将放弃并通知调用应用它遇到了问题。

TCP 报头包括报头数据和有效负载的校验和。这允许接收器验证数据包的内容是否在传输过程中被修改。

TCP 还包括一些算法,这些算法可以确保通信不会太快地发送给接收设备进行处理,这些算法还可以推断网络状况并调节传输速率以避免网络拥塞。

这些服务一起为应用数据提供了一个健壮可靠的传输系统。这是许多流行的高级协议(如 HTTP、SMTP、SSH 和 IMAP)依赖 TCP 的原因之一。

UDP 对 TCP

考虑到 TCP 的特性,您可能想知道像 UDP 这样的无连接协议的用途是什么。嗯,互联网仍然是一个相当可靠的网络,而且大部分数据包都能被传送。无连接协议在需要最小传输开销以及偶尔丢弃的数据包不是什么大问题的情况下非常有用。TCP 的可靠性和拥塞控制的代价是需要额外的数据包和往返,并且在数据包丢失时故意引入延迟以防止拥塞。这些可能会大幅增加延迟,而延迟是实时服务的主要敌人,但不会为它们带来任何实际好处。少量丢弃的数据包可能会导致媒体流中的瞬时故障或信号质量下降,但只要数据包继续出现,流通常可以恢复。

UDP 也是用于 DNS 的主要协议,这很有趣,因为大多数 DNS 查询都适合单个数据包,所以 TCP 的流功能通常不需要。DNS 通常也配置为不依赖于可靠连接。大多数设备都配置有多个 DNS 服务器,通常在短时间超时后将查询重新发送到第二台服务器比等待 TCP 回退期到期更快。

UDP 和 TCP 之间的选择取决于消息大小、延迟是否是一个问题以及应用希望自己执行多少 TCP 功能。

第 5 层–应用层

最后我们到达了最顶端。应用层在 IP 协议套件中故意保持开放,对于应用开发人员在 TCP 或 UDP(甚至 IP,尽管这些协议比较少见)之上开发的任何协议来说,它都是一个包罗万象的协议。应用层协议包括 HTTP、SMTP、IMAP、DNS 和 FTP。

协议甚至可以成为它们自己的层,其中一个应用协议构建在另一个应用协议之上。这方面的一个例子是简单对象访问协议SOAP),它定义了一个基于 XML 的协议,可以在几乎任何传输上使用,包括 HTTP 和 SMTP。

Python 有许多应用层协议的标准库模块和更多的第三方模块。如果我们编写低级服务器应用,那么我们将更有可能对 TCP 和 UDP 感兴趣,但如果不感兴趣,那么我们将使用应用层协议,我们将在接下来的几章中详细介绍其中的一些协议。

继续看 Python!

好了,我们的 TCP/IP 协议栈就到此为止了。我们将进入本章的下一节,在这里我们将了解如何开始使用 Python 以及如何处理我们刚刚介绍的一些主题。

Python 网络编程

在部分中,我们将了解 Python 中网络编程的一般方法。我们将了解 Python 如何让我们与网络堆栈接口,如何跟踪有用的模块,并介绍一些通用的网络编程技巧。

打碎几个鸡蛋

网络协议的层模型的威力在于,较高层可以轻松地在较低层提供的服务上构建,这使它们能够向网络添加新的服务。Python 在网络栈中提供了与不同级别协议接口的模块,支持高层协议的模块通过使用底层协议提供的接口遵循上述原则。我们怎样才能想象这一点?

好吧,有时候一个很好的方法就是打破它。那么,让我们来打破 Python 的网络堆栈。或者,更具体地说,让我们生成回溯。

是的,这意味着我们将要编写的第一段 Python 将生成一个异常。但是,这将是一个很好的例外。我们将从中学习。因此,启动 Python shell 并运行以下命令:

>>> import smtplib
>>> smtplib.SMTP('127.0.0.1', port=66000)

我们在这里干什么?我们正在导入smtplib,这是 Python 用于处理 SMTP 协议的标准库。SMTP 是一种应用层协议,用于发送电子邮件。然后,我们将尝试通过实例化一个SMTP对象来打开 SMTP 连接。我们希望连接失败,这就是为什么我们指定了端口号 66000,这是一个无效的端口。我们将为连接指定本地主机,因为这将导致它快速失败,而不是让它等待网络超时。

在运行上述命令时,您应该得到以下回溯:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python3.4/smtplib.py", line 242, in __init__
    (code, msg) = self.connect(host, port)
  File "/usr/lib/python3.4/smtplib.py", line 321, in connect
    self.sock = self._get_socket(host, port, self.timeout)
  File "/usr/lib/python3.4/smtplib.py", line 292, in _get_socket
    self.source_address)
  File "/usr/lib/python3.4/socket.py", line 509, in create_connection
    raise err
  File "/usr/lib/python3.4/socket.py", line 500, in create_connection
    sock.connect(sa)
ConnectionRefusedError: [Errno 111] Connection refused

这是在 Debian 7 机器上使用 Python 3.4.1 生成的。如果在 Windows 上运行,最后的错误消息将与此略有不同,但堆栈跟踪将保持不变。

检查它将揭示 Python 网络模块如何作为堆栈。我们可以看到调用堆栈从smtplib.py开始,然后向下移动到socket.pysocket模块是 Python 传输层的标准接口,它提供了与 TCP 和 UDP 交互以及通过 DNS 查找主机名的功能。我们将在第 7 章使用套接字编程第 8 章客户机和服务器应用中了解更多信息。

从前面的程序中可以清楚地看到,smtplib模块调用socket模块。应用层协议采用了传输层协议(在本例中为 TCP)。

在回溯的底部,我们可以看到异常本身和Errno 111。这是来自操作系统的错误消息。您可以通过/usr/include/asm-generic/errno.hasm/errno.h在某些系统上)查看错误消息编号 111(在 Windows 上,错误将是 WinError,因此您可以看到它显然是由操作系统生成的)来验证这一点。从这个错误消息中我们可以看到,socket模块再次调用,并要求操作系统为其管理 TCP 连接。

Python 的网络模块按照协议栈设计者的意图工作。它们调用堆栈中的较低级别来使用它们的服务来执行网络任务。我们可以使用对应用层协议(在本例中为 SMTP)的简单调用来工作,而不必担心底层网络层。这是实际的网络封装,我们希望在应用中尽可能多地利用它。

从顶部取下

在我们开始为新的网络应用编写代码之前,我们希望确保尽可能多地利用现有的堆栈。这意味着找到一个模块,该模块为我们想要使用的服务提供一个接口,并且是我们可以找到的最高层的模块。如果幸运的话,有人已经编写了一个模块,它提供了一个接口,可以提供我们所需要的确切服务。

让我们用一个例子来说明这个过程。让我们编写一个工具,从 IETF 下载征求意见RFC)文档,然后在屏幕上显示它们。

让我们简化 RFC 下载程序。我们将使其成为一个命令行程序,只接受 RFC 编号,以文本格式下载 RFC,然后将其打印到stdout

现在,有可能已经有人编写了一个模块来实现这一点,让我们看看是否可以找到任何东西。

我们首先要看的应该是 Python 标准库。库中的模块得到了很好的维护,并且有很好的文档记录。当我们使用标准库模块时,应用的用户不需要安装任何其他依赖项来运行它。

浏览图书馆参考资料https://docs.python.org 似乎没有显示任何与我们的要求直接相关的内容。这并不完全令人惊讶!

接下来我们将讨论第三方模块。Python 包索引,可在找到 https://pypi.python.org 是我们应该寻找这些的地方。在这里,围绕 RFC 客户端和 RFC 下载主题运行一些搜索似乎并没有发现任何有用的东西。下一个要看的地方将是谷歌,尽管再次强调,搜索并没有显示任何有希望的东西。这有点令人失望,但这就是为什么我们要学习网络编程,以填补这些空白!

我们还可以通过其他方式了解有用的第三方模块,包括邮件列表、Python 用户组、编程 Q&A 站点http://stackoverflow.com 和编程教材。

现在,让我们假设我们真的找不到用于下载 RFC 的模块。接下来呢?嗯,我们需要在网络堆栈中考虑较低的层次。这意味着我们需要确定我们需要使用的网络协议,以便自己掌握文本格式的 RFC。

RFC 的 IETF 登录页为http://www.ietf.org/rfc.html ,通过阅读它,我们可以确切地知道我们想要知道的。我们可以使用格式的 URL 访问 RFC 的文本版本 http://www.ietf.org/rfc/rfc741.txt 。本例中的 RFC 编号为 741。因此,我们可以使用 HTTP 获取 RFC 的文本格式。

现在,我们需要一个可以为我们讲 HTTP 的模块。我们应该再看看标准库。您会注意到,实际上有一个名为http的模块。听起来很有希望,但看看它的文档会告诉我们它是一个低级库,而被称为urllib的东西将被证明更有用。

现在,查看文档,我们发现它确实满足了我们的需要。它通过一个简单的 API 下载 URL 的目标。我们找到了协议模块。

下载 RFC

现在我们可以编写我们的程序了。为此,创建一个名为RFC_downloader.py的文本文件,并将以下代码保存到其中:

import sys, urllib.request

try:
    rfc_number = int(sys.argv[1])
except (IndexError, ValueError):
    print('Must supply an RFC number as first argument')
    sys.exit(2)

template = 'http://www.ietf.org/rfc/rfc{}.txt'
url = template.format(rfc_number)
rfc_raw = urllib.request.urlopen(url).read()
rfc = rfc_raw.decode()
print(rfc)

我们可以使用以下命令运行前面的代码:

$ python RFC_downloader.py 2324 | less

在 Windows 上,您需要使用more而不是less。RFC 可以运行到许多页面,因此我们在这里使用寻呼机。如果你尝试这个,那么你会看到一些关于咖啡壶遥控的有用信息。

让我们浏览一下我们的代码,看看到目前为止我们做了什么。

首先,我们导入模块并检查命令行上是否提供了 RFC 编号。然后,我们通过替换提供的 RFC 编号来构造 URL。接下来,主要活动urlopen()调用将为我们的 URL 构造一个 HTTP 请求,然后它将通过 Internet 与 IETF web 服务器联系并下载 RFC 文本。接下来,我们将文本解码为 Unicode,最后将其打印到屏幕上。

因此,我们可以很容易地从命令行查看我们喜欢的任何 RFC。现在回想起来,没有这样的模块并不完全令人惊讶,因为我们可以使用urllib完成大部分艰苦的工作!

深入观察

但是,如果 HTTP 是全新的,没有模块,比如urllib,我们可以用它来为我们讲 HTTP,那会怎么样?那么,我们将不得不再次退出堆栈,并为我们的目的使用 TCP。让我们根据这个场景修改我们的程序,如下所示:

import sys, socket

try:
    rfc_number = int(sys.argv[1])
except (IndexError, ValueError):
    print('Must supply an RFC number as first argument')
    sys.exit(2)

host = 'www.ietf.org'
port = 80
sock = socket.create_connection((host, port))

req = (
    'GET /rfc/rfc{rfcnum}.txt HTTP/1.1\r\n'
    'Host: {host}:{port}\r\n'
    'User-Agent: Python {version}\r\n'
    'Connection: close\r\n'
    '\r\n'
)
req = req.format(
    rfcnum=rfc_number,
    host=host,
    port=port,
    version=sys.version_info[0]
)
sock.sendall(req.encode('ascii'))
rfc_raw = bytearray()
while True:
    buf = sock.recv(4096)
    if not len(buf):
        break
    rfc_raw += buf
rfc = rfc_raw.decode('utf-8')
print(rfc)

第一个明显的变化是我们使用了socket而不是urllib。套接字是 Python 用于操作系统 TCP 和 UDP 实现的接口。命令行检查保持不变,但接下来我们将看到我们现在需要处理urllib以前为我们做的一些事情。

我们必须告诉套接字我们要使用哪个传输层协议。我们使用socket.create_connection()便利功能来实现这一点。此函数将始终创建 TCP 连接。您会注意到,我们必须明确提供socket用于建立连接的 TCP 端口号。为什么是 80?80 是 HTTP 上 web 服务的标准端口号。我们还必须将主机与 URL 分开,因为socket不了解 URL。

我们创建发送到服务器的请求字符串也比我们以前使用的 URL 复杂得多:它是一个完整的 HTTP 请求。在下一章中,我们将详细介绍这些。

接下来,我们处理 TCP 连接上的网络通信。我们使用sendall()调用将整个请求字符串发送到服务器。通过 TCP 发送的数据必须是原始字节,因此在发送之前,我们必须将请求文本编码为 ASCII。

然后,当服务器的响应到达while循环时,我们将其拼凑在一起。通过 TCP 套接字发送给我们的字节以连续流的形式呈现给我们的应用。所以,就像任何长度未知的流一样,我们必须迭代地读取它。在服务器发送所有数据并关闭连接后,recv()调用将返回空字符串。因此,我们可以将此作为发布和打印响应的条件。

我们的程序显然更复杂。与我们之前的相比,这在维护方面不是很好。此外,如果运行程序并查看输出 RFC 文本的开头,则会注意到开头有一些额外的行,如下所示:

HTTP/1.1 200 OK
Date: Thu, 07 Aug 2014 15:47:13 GMT
Content-Type: text/plain
Transfer-Encoding: chunked
Connection: close
Set-Cookie: __cfduid=d1983ad4f7Last-Modified: Fri, 27 Mar 1998 22:45:31 GMT
ETag: W/"8982977-4c9a-32a651f0ad8c0"

因为我们现在处理的是原始 HTTP 协议交换,所以我们看到 HTTP 在响应中包含的额外头数据。这与较低级别的数据包头具有类似的用途。HTTP 头包含有关响应的 HTTP 特定元数据,该元数据告诉客户端如何解释响应。在此之前,urllib为我们解析了它,将数据作为属性添加到响应对象中,并从输出数据中删除了头数据。我们还需要添加代码来完成这项工作,以使这个程序与我们的第一个程序一样有能力。

从代码中无法立即看出的是,我们还遗漏了urllib模块的错误检查和处理。虽然低级网络错误仍然会生成异常,但我们将不再捕获 HTTP 层中的任何问题,而urllib本可以这样做。

上述头文件的第一行中的200值是一个 HTTP状态码,它告诉我们 HTTP 请求或响应是否存在任何问题。200 意味着一切顺利,但其他代码,如臭名昭著的 404“未找到”可能意味着出了问题。urllib模块将为我们检查这些并引发异常。但在这里,我们需要自己处理这些问题。

因此,尽可能远地使用模块有明显的好处。我们生成的程序将不那么复杂,这将使它们编写起来更快,维护起来也更容易。这也意味着它们的错误处理将更加健壮,我们将受益于模块开发人员的专业知识。此外,我们还受益于模块在捕获意外和棘手的边缘问题时所经历的测试。在接下来的几章中,我们将讨论更多位于堆栈顶部的模块和协议。

TCP/IP 网络编程

总而言之,我们将研究 TCP/IP 网络的几个常见方面,这些方面可能会让以前没有遇到过的应用开发人员感到非常头疼。这些是:防火墙、网络地址转换,以及 IPv4 和 IPv6 之间的一些差异。

防火墙

防火墙是一种硬件或软件,它检查流经它的网络数据包,并根据数据包的属性过滤通过的数据包。它是一种安全机制,用于防止不需要的流量从网络的一部分移动到另一部分。防火墙可以位于网络边界,也可以作为应用在网络客户端和服务器上运行。例如,iptables 实际上是 Linux 的防火墙软件。你经常会发现在桌面反病毒程序中内置了防火墙。

过滤规则可以基于网络流量的任何属性。常用的属性有:传输层协议(即,流量是否使用 TCP 或 UDP)、源和目标 IP 地址以及源和目标端口号。

常见的过滤策略是拒绝所有入站流量,只允许与非常特定的参数匹配的流量。例如,一家公司可能有一个 web 服务器,它希望允许从 Internet 访问该服务器,但它希望阻止来自 Internet 的指向其网络上任何其他设备的所有流量。为此,它将在其网关的正前方或正后方放置一个防火墙,然后将其配置为阻止所有传入流量,除了具有 web 服务器的目标 IP 地址和目标端口号 80 的 TCP 流量(因为端口 80 是 HTTP 服务的标准端口号)。

防火墙也可以阻止出站流量。这样做可能是为了阻止恶意软件进入内部网络设备,使其无法呼叫总部或发送垃圾邮件。

因为防火墙会阻止网络通信,所以它们会给网络应用带来明显的问题。在通过网络测试应用时,我们需要确保设备之间存在的防火墙配置为允许应用的流量通过。通常,这意味着我们需要确保我们需要的端口在防火墙上打开,以便源 IP 地址和目标 IP 地址之间的通信能够自由流动。这可能需要与一两个 IT 支持团队进行一些谈判,可能需要查看我们的操作系统和本地网络路由器的文档。此外,我们需要确保我们的应用用户知道他们需要在自己的环境中执行的任何防火墙配置,以便使用我们的程序。

网络地址转换

前面我们讨论了私有 IP 地址范围。虽然它们可能非常有用,但也有一些小问题。禁止源地址或目标地址在专用范围内的数据包通过公共 Internet 路由!因此,如果没有帮助,使用专用范围地址的设备无法与使用公共互联网地址的设备进行通信。但是,通过网络地址转换NAT,我们可以解决这个问题。由于大多数家庭网络使用专用范围地址,NAT 很可能是您会遇到的问题。

尽管 NAT 可以在其他情况下使用,但它通常由公共 Internet 边界上的网关和使用专用 IP 地址的网络执行。当网关从网络接收到发送到 Internet 的数据包时,为了使来自网关网络的数据包能够在公共 Internet 上路由,它重写数据包的头并用自己的公共范围 IP 地址替换私有范围源 IP 地址。如果数据包包含 TCP 或 UDP 数据包,并且这些数据包包含一个源端口,那么它还可以打开一个新的源端口,用于侦听其外部接口,并重写数据包中的源端口号以匹配此新编号。

在执行这些重写时,它会记录新打开的源端口与内部网络上的源设备之间的映射。如果它接收到对新源端口的回复,那么它将反转转换过程并将接收到的数据包发送到内部网络上的原始设备。不应让发起网络设备知道其流量正在进行 NAT。

使用 NAT 有几个好处。内部网络设备被屏蔽,不受来自互联网的指向网络的恶意流量的影响,使用 NAT 设备的设备被提供一层隐私,因为它们的私有地址被隐藏,并且需要分配宝贵的公共 IP 地址的网络设备的数量减少。事实上,正是大量使用 NAT 使得互联网在 IPv4 地址用完的情况下仍能继续运行。

如果在设计时不考虑 NAT,NAT 会给网络应用带来一些问题。

如果传输的应用数据包括有关设备网络配置的信息,并且该设备位于 NAT 路由器后面,则如果接收设备假定应用数据与 IP 和 TCP/UDP 报头数据匹配,则可能会发生问题。NAT 路由器将重写 IP 和 TCP/UDP 报头数据,但不会重写应用数据。这是 FTP 协议中众所周知的问题。

FTP 与 NAT 之间的另一个问题是,在 FTP 活动模式下,协议操作的一部分涉及客户端打开一个端口进行侦听,服务器创建到该端口的新 TCP 连接(而不仅仅是常规回复)。当客户端位于 NAT 路由器后面时,这会失败,因为路由器不知道如何处理服务器的连接尝试。因此,在假设服务器可以创建到客户端的新连接时要小心,因为它们可能会被 NAT 路由器或防火墙阻止。通常,最好在服务器不可能建立到客户端的新连接的假设下进行编程。

IPv6

我们提到,前面的讨论是基于 IPv4 的,但有一个新版本称为 IPv6。IPv6 的最终目的是取代 IPv4,但这一过程在一段时间内不太可能完成。

由于大多数 Python 标准库模块现在已经更新,以支持 IPv6 并接受 IPv6 地址,因此在 Python 中迁移到 IPv6 不会对我们的应用产生太大影响。然而,也有一些小故障需要注意。

您将注意到 IPv6 中的主要区别是地址格式已更改。新协议的主要设计目标之一是缓解 IPv4 地址的全球短缺,并防止这种情况再次发生 IETF 将地址长度增加了四倍,达到 128 位,创建一个足够大的地址空间,为地球上的每个人提供 10 亿倍于整个 IPv4 地址空间的地址。

新格式 IP 地址的写入方式不同,它们如下所示:

2001:0db8:85a3:0000:0000:b81a:63d6:135b

注意使用了冒号和十六进制格式。

还有以更紧凑的形式写入 IPv6 地址的规则。这主要是通过省略连续零的运行来实现的。例如,前面示例中的地址可以缩短为:

2001:db8:85a3::b81a:63d6:135b

如果程序需要比较或解析文本格式的 IPv6 地址,则需要了解这些压缩规则,因为单个 IPv6 地址可以用多种方式表示。这些规则的详细信息可在 RFC 4291 中找到,可在上找到 http://www.ietf.org/rfc/rfc4291.txt

由于冒号在 URI 中使用时可能会导致冲突,因此以这种方式使用 IPv6 地址时,需要将其括在方括号中,例如:

http://[2001:db8:85a3::b81a:63d6:135b]/index.html

此外,在 IPv6 中,网络接口现在的标准做法是为其分配多个 IP 地址。IPv6 地址按其有效范围进行分类。作用域包括全局作用域(即公共 Internet)和链接本地作用域,该作用域仅对本地子网有效。IP 地址的范围可以通过检查其高阶位来确定。如果我们列举了用于特定目的的本地接口的 IP 地址,那么我们需要检查我们是否使用了用于我们打算使用的范围的正确地址。RFC4291 中有更多详细信息。

最后,IPv6 中的地址多得令人难以置信,我们的想法是每个设备(以及组件和细菌)都可以获得一个全球唯一的公共 IP 地址,NAT 将成为过去。虽然这在理论上听起来不错,但人们对这对用户隐私等问题的影响提出了一些担忧。因此,在协议中增加了旨在缓解这些担忧的内容(http://www.ietf.org/rfc/rfc3041.txt )。这是一个值得欢迎的进步;但是,它可能会给某些应用带来问题。因此,如果您计划让您的程序采用 IPv6,那么阅读 RFC 是值得的。

总结

在本章的第一部分中,我们介绍了使用 TCP/IP 进行网络连接的要点。我们讨论了网络堆栈的概念,并查看了 Internet 协议套件的主要协议。我们了解了 IP 如何解决在不同网络上的设备之间发送消息的问题,以及 TCP 和 UDP 如何在应用之间提供端到端传输。

在第二部分中,我们了解了在使用 Python 时通常是如何进行网络编程的。我们讨论了使用模块与尽可能远的网络堆栈上的服务接口的一般原则。我们还讨论了在哪里可以找到这些模块。我们查看了使用与不同层的网络堆栈接口的模块来完成简单网络任务的示例。

最后,我们讨论了 TCP/IP 网络编程中的一些常见陷阱以及可能采取的一些步骤。

这一章着重于网络理论方面的内容。但是,现在是时候深入 Python 并为我们提供一些应用层协议了。****