悠闲博客-blog.yxrjt.cn

Python 网络编程第一章网络编程和 Python

更新时间:2025-09-25 13:24点击:40

第一章:网络编程和 Python

本书将重点关注编写使用互联网协议套件的网络程序。为什么我们选择这样做呢?嗯,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,是一组旨在共同工作以在互连网络上提供端到端消息传输的协议。

以下讨论基于互联网协议第 4 版IPv4)。由于互联网已经用尽了 IPv4 地址,已经开发了一个新版本 IPv6,旨在解决这种情况。然而,尽管 IPv6 在一些领域得到了应用,但其部署进展缓慢,大多数互联网可能会继续使用 IPv4。我们将在本节重点讨论 IPv4,然后在本章的第二部分讨论 IPv6 的相关变化。

TCP/IP 在称为请求评论RFCs)的文件中进行了规定,这些文件由互联网工程任务组IETF)发布。RFCs 涵盖了广泛的标准,而 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 地址。在第六章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)分配地址块,然后 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 由你的宽带路由器分配给它们的。

数据包

在接下来的章节中,我们将讨论网络流量,所以让我们先了解一下它是什么。

许多协议,包括互联网协议套件中的主要协议,使用一种称为数据包化的技术来帮助管理数据在网络上传输时的情况。

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

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


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

一旦创建,数据包被发送到网络上,然后独立路由到它们的目的地。以数据包形式发送数据有几个优点,包括多路复用(多个设备可以同时在网络上发送数据),快速通知网络上可能发生的错误,拥塞控制和动态重路由。

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


这被称为封装,正如我们很快将看到的,这是一种构造网络流量的强大机制。

网络

网络是一组连接的网络设备。网络的规模可以有很大的差异,它们可以由较小的网络组成。您家中连接到网络的设备或大型办公楼中连接到网络的计算机都是网络的例子。

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

在本节中,我们将从网络的简化定义开始,然后逐渐朝着更具体的 IP 子网定义发展。

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

这个连接点被称为网关,通常采用一种称为路由器的特殊网络设备的形式。路由器的工作是在网络之间传输流量。它位于两个或多个网络之间,并且被称为位于这些网络的边界。它总是有两个或更多个网络接口;每个网络都连接一个。路由器包含一组称为路由表的规则,告诉它如何根据数据包的目标 IP 地址将通过它传递的数据包进一步传递。

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

由于这些路由器可以位于许多网络之间,它们的路由表可能会变得非常庞大,并且需要不断更新。下图显示了一个简化的示例:


前面的图表给了我们一个布局的想法。每个 ISP 网关连接 ISP 网络到区域路由器,每个家庭宽带路由器都连接着一个家庭网络。在现实世界中,随着向顶部前进,这种布局变得更加复杂。ISP 通常会有多个连接它们到区域路由器的网关,其中一些也会充当区域路由器。区域路由器的层次也比这里显示的更多,它们之间有许多连接,这些连接的布局比这个简单的层次结构复杂得多。从 2005 年收集的数据中得出的互联网部分的渲染提供了一个美丽的插图,展示了这种复杂性,可以在en.wikipedia.org/wiki/Internet_backbone#/media/File:Internet_map_1024.jpg找到。

使用 IP 进行路由

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

路由器确定要转发流量到正确路由器的一种明显的方法可能是在每个路由器的路由表中为每个 IP 地址编程一个路由。然而,在实践中,随着 40 多亿个 IP 地址和不断变化的网络路由,这种方法被证明是完全不可行的。

那么,路由是如何完成的?答案在 IP 地址的另一个属性中。IP 地址可以被解释为由两个逻辑部分组成:网络前缀主机标识符。网络前缀唯一标识设备所在的网络,设备可以使用这个来确定如何处理它生成的流量,或者接收到的用于转发的流量。当 IP 地址以二进制形式写出时(记住 IP 地址实际上只是一个 32 位的数字),网络前缀是 IP 地址的前n位。这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 地址,尽管我们中的许多人每天都使用互联网。当我们浏览网页或发送电子邮件时,我们通常使用主机名或域名连接到服务器。这些必须以某种方式映射到服务器的 IP 地址。但是这是如何完成的呢?

作为 RFC 1035 记录的域名系统DNS)是主机名和 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 为此提供了一个接口,我们将在第六章中讨论IP 和 DNS

协议栈或为什么互联网就像蛋糕

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

套件中的核心协议被设计为在堆栈内一起工作。也就是说,套件中的每个协议都占据堆栈内的一层,并且其他协议位于该层的上方和下方。因此,它就像蛋糕一样分层。每一层为其上面的层提供特定的服务,同时隐藏其自身操作的复杂性,遵循封装的原则。理想情况下,每一层只与其下面的层进行接口,以便从下面的所有层的问题解决能力中获益。

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

TCP/IP 套件定义了四层,尽管通常为了清晰起见使用五层。这些列在下表中:

名称示例协议
5应用层HTTP,SMTP,IMAP
4传输层TCP,UDP
3网络层IP
2数据链路层以太网,PPP,FDDI
1物理层-

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

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

当使用 Python 时,我们不需要关注最低的两层,因为我们很少需要与它们进行交互。它们的操作几乎总是由操作系统和网络硬件处理。

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

第 4 层和第 5 层对我们的目的更有趣。

第 4 层 - TCP 和 UDP

第 4 层是我们可能想要在 Python 中使用的第一层。这一层可以使用两种协议之一:传输控制协议TCP)和用户数据报协议UDP)。这两种协议都提供了在不同网络设备上的应用程序之间端到端数据传输的常见服务。

网络端口

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

TCP 和 UDP 通过引入端口的概念提供了答案。端口是一个端点,附加到网络设备分配的 IP 地址之一。端口由设备上运行的进程占用,然后该进程被称为在该端口上监听。端口由一个 16 位数字表示,因此设备上的每个 IP 地址都有 65,535 个可能的端口,进程可以占用(端口号 0 被保留)。端口一次只能被一个进程占用,尽管一个进程可以同时占用多个端口。

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

在发送消息之前,需要知道端口号。这主要是通过约定来实现的。除了管理 IP 地址空间外,IANA 还负责管理端口号分配给网络服务。

服务是一类应用程序,例如 Web 服务器或 DNS 服务器,通常与应用程序协议相关联。端口分配给服务而不是特定的应用程序,因为这样可以让服务提供者灵活选择要使用的软件类型来提供服务,而不必担心用户需要查找和连接到新的端口号,仅仅是因为服务器开始使用 Apache 而不是 IIS,例如。

大多数操作系统都包含了这个服务列表及其分配的端口号的副本。在 Linux 上,通常可以在/etc/services找到,在 Windows 上,通常可以在c:windowssystem32driversetcservices找到。完整的列表也可以在www.iana.org/assignments/port-numbers上在线查看。

TCP 和 UDP 数据包头也可能包括源端口号。对于 UDP 来说,这是可选的,但对于 TCP 来说是强制的。源端口号告诉服务器上的接收应用程序在向客户端发送数据时应该将回复发送到哪里。应用程序可以指定它们希望使用的源端口,或者如果没有为 TCP 指定源端口,则在发送数据包时操作系统会随机分配一个。一旦操作系统有了源端口号,它就会将其分配给调用应用程序,并开始监听以获取回复。如果在该端口上收到回复,则接收到的数据将传递给发送应用程序。

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

UDP

UDP 的文档编号为 RFC 768。它故意简单:它除了我们在前一节中描述的服务之外,不提供任何服务。它只是获取我们要发送的数据,使用目标端口号(和可选的源端口号)对其进行数据包化,并将其交给本地互联网协议实现进行传递。接收端的应用程序以与数据包化时相同的离散块看到数据。

IP 和 UDP 都是所谓的无连接协议。这意味着它们试图尽最大努力交付它们的数据包,但如果出现问题,它们将只是耸耸肩并继续交付下一个数据包。我们的数据包到达目的地的保证,并且如果交付失败,也没有错误通知。如果数据包成功到达,也不能保证它们会按照发送顺序到达。这取决于更高层的协议或发送应用程序来确定数据包是否已到达以及如何处理任何问题。这些是一种“发射即忘”的协议。

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

我们现在将看一下 UDP 的更可靠的兄弟 TCP,然后讨论它们之间的区别,以及应用程序可能选择使用其中一个的原因。

TCP

传输控制协议的文档编号为 RFC 761。与 UDP 相反,TCP 是一种基于连接的协议。在这种协议中,直到服务器和客户端执行了初始的控制数据包交换之前,才会发送数据。这种交换被称为握手。这建立了一个连接,从那时起就可以发送数据。接收到的每个数据包都会得到接收方的确认,它通过发送一个称为ACK的数据包来进行确认。因此,TCP 总是要求数据包包括源端口号,因为它依赖于持续的双向消息交换。

从应用程序的角度来看,UDP 和 TCP 之间的关键区别是应用程序不再以离散的块看到数据;TCP 连接将数据呈现给应用程序作为连续的、无缝的字节流。如果我们发送的消息大于典型的数据包,这会使事情变得简单得多,但这意味着我们需要开始考虑我们的消息。虽然使用 UDP,我们可以依赖其数据包化来提供这样的手段,但是使用 TCP,我们必须决定一个机制来明确地确定我们的消息从哪里开始和结束。我们将在第八章中看到更多关于这一点,“客户端和服务器应用程序”。

TCP 提供以下服务:

  • 按顺序交付

  • 接收确认

  • 错误检测

  • 流和拥塞控制

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

由于数据包被确认,发送应用程序可以确保数据正在到达,并且可以继续发送数据。如果发送的数据包没有收到确认,那么在一定时间内数据包将被重新发送。如果仍然没有响应,那么 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 查找主机名的功能。我们将在第七章使用套接字编程和第八章客户端和服务器应用程序中学到更多。

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

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

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

从顶部开始

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

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

让我们保持 RFC 下载器简单。我们将把它制作成一个命令行程序,只接受 RFC 编号,下载 RFC 的文本格式,然后将其打印到stdout

现在,有可能有人已经为此编写了一个模块,所以让我们看看能否找到任何东西。

我们应该总是首先查看 Python 标准库。标准库中的模块得到了很好的维护和文档化。当我们使用标准库模块时,您的应用程序的用户不需要安装任何额外的依赖项来运行它。

docs.python.org库参考中查看,似乎没有显示与我们要求直接相关的内容。这并不完全令人惊讶!

因此,接下来我们将转向第三方模块。可以在pypi.python.org找到 Python 软件包索引,这是我们应该寻找这些模块的地方。在这里,围绕 RFC 客户端和 RFC 下载主题运行几次搜索似乎没有发现任何有用的东西。下一个要查找的地方将是 Google,尽管再次搜索没有发现任何有希望的东西。这有点令人失望,但这就是我们学习网络编程的原因,以填补这些空白!

还有其他方法可以找到有用的第三方模块,包括邮件列表、Python 用户组、编程问答网站stackoverflow.com和编程教材。

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

RFC 的 IETF 登陆页面是www.ietf.org/rfc.html,通过阅读它告诉我们确切的信息。我们可以使用形式为www.ietf.org/rfc/rfc741.txt的 URL 访问 RFC 的文本版本。在这种情况下,RFC 编号是 741。因此,我们可以使用 HTTP 获取 RFC 的文本格式。

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

现在,查看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 请求,然后它将通过互联网联系 IETF 网络服务器并下载 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
'
    'Host: {host}:{port}
'
    'User-Agent: Python {version}
'
    'Connection: close
'
    '
'
)
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。Socket 是 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=d1983ad4f7…
Last-Modified: Fri, 27 Mar 1998 22:45:31 GMT
ETag: W/"8982977-4c9a-32a651f0ad8c0"

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

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

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

因此,尽可能在堆栈的顶部使用模块是有明显好处的。我们的最终程序将更简单,这将使它们更快地编写,并更容易维护。这也意味着它们的错误处理将更加健壮,并且我们将受益于模块开发人员的专业知识。此外,我们还将受益于模块为捕捉意外和棘手的边缘情况问题而经历的测试。在接下来的几章中,我们将讨论更多位于堆栈顶部的模块和协议。

为 TCP/IP 网络编程

最后,我们将看一下 TCP/IP 网络中经常遇到的一些方面,这些方面可能会让以前没有遇到过它们的应用程序开发人员感到困惑。这些是:防火墙,网络地址转换以及 IPv4 和 IPv6 之间的一些差异。

防火墙

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

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

常见的过滤策略是拒绝所有入站流量,并仅允许符合非常特定参数的流量。例如,一家公司可能有一个希望允许从互联网访问的 Web 服务器,但希望阻止来自互联网的所有流量,这些流量指向其网络中的任何其他设备。为此,它将在其网关的正面或背面直接放置一个防火墙,然后配置它以阻止所有传入流量,除了目标 IP 地址为 Web 服务器的 TCP 流量和目标端口号为 80 的流量(因为端口 80 是 HTTP 服务的标准端口号)。

防火墙也可以阻止出站流量。这可能是为了阻止恶意软件从内部网络设备上找到家或发送垃圾邮件。

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

网络地址转换

早些时候,我们讨论了私有 IP 地址范围。虽然它们可能非常有用,但有一个小问题。源地址或目的地址在私有范围内的数据包被禁止在公共互联网上传输!因此,如果没有一些帮助,使用私有范围地址的设备无法与使用公共互联网上的地址的设备通信。然而,通过网络地址转换NAT),我们可以解决这个问题。由于大多数家庭网络使用私有范围地址,NAT 很可能是你会遇到的东西。

尽管 NAT 可以在其他情况下使用,但它最常见的用法是由一个位于公共互联网和使用私有范围 IP 地址的网络边界的网关执行。为了使来自网关网络的数据包在网关接收到发送到互联网的网络的数据包时能够在公共互联网上路由,它会重写数据包的头,并用自己的公共范围 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 位,从而创建了足够大的地址空间,以便为地球上的每个人提供比整个 IPv4 地址空间中的地址多十亿倍的地址。

新格式的 IP 地址写法不同,看起来像这样:

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

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

还有一些规则可以以更

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

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

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

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

此外,在 IPv6 中,网络接口现在标准做法是分配多个 IP 地址。IPv6 地址根据其有效范围进行分类。范围包括全局范围(即公共互联网)和链路本地范围,仅对本地子网有效。可以通过检查其高阶位来确定 IP 地址的范围。如果我们枚举用于特定目的的本地接口的 IP 地址,那么我们需要检查我们是否使用了正确的地址来处理我们打算使用的范围。RFC 4291 中有更多细节。

最后,随着 IPv6 中可用的地址数量之多,每个设备(和组件,和细菌)都可以被分配一个全球唯一的公共 IP 地址,NAT 将成为过去。尽管在理论上听起来很棒,但一些人对这对用户隐私等问题的影响提出了一些担忧。因此,为缓解这些担忧而设计的附加功能已添加到协议中(www.ietf.org/rfc/rfc3041.txt)。这是一个受欢迎的进展;然而,它可能会对一些应用程序造成问题。因此,如果您计划使用 IPv6 来使用您的程序,阅读 RFC 是值得的。

总结

在本章的第一部分,我们看了一下使用 TCP/IP 进行网络的基本知识。我们讨论了网络堆栈的概念,并研究了互联网协议套件的主要协议。我们看到了 IP 如何解决在不同网络上的设备之间发送消息的问题,以及 TCP 和 UDP 如何为应用程序提供端到端的传输。

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

最后,我们讨论了为 TCP/IP 网络编程的一些常见陷阱以及可以采取的一些措施来避免它们。

这一章在网络理论方面非常重要。但是,现在是时候开始使用 Python 并让一些应用层协议为我们工作了。

栏目分类

联系方式
  • help@yxrjt.cn
  • lgc@yxrjt.cn
  • admin@yxrjt.cn