PYTHON SOCKET 编程教程

您所在的位置:网站首页 情感的分类有哪五种 PYTHON SOCKET 编程教程

PYTHON SOCKET 编程教程

2022-05-04 18:57| 来源: 网络整理| 查看: 265

本套接字编程教程将向您展示如何使用 Python 3 套接字将多个客户端连接到服务器。它涵盖了如何从客户端向服务器以及从服务器向客户端发送消息。我还将向您展示如何在本地或全球范围内通过 Internet 托管您的套接字服务器,以便任何人都可以连接。这使用了 python 3 套接字和线程模块。

服务器代码

import socket import threading HEADER = 64 PORT = 5050 SERVER = socket.gethostbyname(socket.gethostname()) ADDR = (SERVER, PORT) FORMAT = 'utf-8' DISCONNECT_MESSAGE = '!DISCONNECT' server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.bind(ADDR) def handle_client(conn, addr): print(f'[NEW CONNECTION] {addr} connected.') connected = True while connected: msg_length = conn.recv(HEADER).decode(FORMAT) if msg_length: msg_length = int(msg_length) msg = conn.recv(msg_length).decode(FORMAT) if msg == DISCONNECT_MESSAGE: connected = False print(f'[{addr}] {msg}') conn.send('Msg received'.encode(FORMAT)) conn.close() def start(): server.listen() print(f'[LISTENING] Server is listening on {SERVER}') while True: conn, addr = server.accept() thread = threading.Thread(target=handle_client, args=(conn, addr)) thread.start() print(f'[ACTIVE CONNECTIONS] {threading.activeCount() - 1}') print('[STARTING] server is starting...') start()

客户代码

import socket HEADER = 64 PORT = 5050 FORMAT = 'utf-8' DISCONNECT_MESSAGE = '!DISCONNECT' SERVER = '192.168.1.26' ADDR = (SERVER, PORT) client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.connect(ADDR) def send(msg): message = msg.encode(FORMAT) msg_length = len(message) send_length = str(msg_length).encode(FORMAT) send_length += b' ' * (HEADER - len(send_length)) client.send(send_length) client.send(message) print(client.recv(2048).decode(FORMAT)) send('Hello World!') input() send('Hello Everyone!') input() send('Hello Tim!') send(DISCONNECT_MESSAGE) 使用 Python 构建 Socket 服务器和客户端

套接字和套接字 API 用于通过网络发送消息。他们提供了一种形式 进程间通信(IPC) .网络可以是连接到计算机的逻辑本地网络,也可以是物理连接到外部网络的网络,并具有与其他网络的连接。一个明显的例子是 Internet,您通过 ISP 连接到 Internet。

本教程包含使用 Python 构建套接字服务器和客户端的三种不同迭代:

我们将通过查看一个简单的套接字服务器和客户端来开始本教程。 一旦你看到了 API 以及这个初始示例中的工作原理,我们将看看一个同时处理多个连接的改进版本。 最后,我们将继续构建一个示例服务器和客户端,其功能类似于成熟的套接字应用程序,并带有自己的自定义标头和内容。

在本教程结束时,您将了解如何使用 Python 中的主要函数和方法。 插座模块 编写您自己的客户端-服务器应用程序。这包括向您展示如何使用自定义类在端点之间发送消息和数据,您可以在这些端点上构建并用于您自己的应用程序。

网络和套接字是大主题。已经写了关于它们的文字卷。如果您不熟悉套接字或网络,如果您对所有术语和部分感到不知所措,这是完全正常的。我知道我做到了!

不过不要气馁。我为你写了这个教程。就像我们学习 Python 一样,我们可以一次学习一点。使用浏览器的书签功能,并在您准备好下一部分时回来。

目录

背景 套接字 API 概述 TCP 套接字 回声客户端和服务器 回声服务器 回声客户端 运行 Echo 客户端和服务器 查看套接字状态 通讯故障 处理多个连接 多连接客户端和服务器 多连接服务器 多连接客户端 运行多连接客户端和服务器 应用程序客户端和服务器 应用协议头 发送应用消息 应用消息类 运行应用程序客户端和服务器 故障排除 平 网络统计 视窗 线鲨 参考 Python 文档 错误 套接字地址族 使用主机名 阻止呼叫 关闭连接 字节序 结论

让我们开始吧!

背景

套接字有着悠久的历史。它们的用途 起源于阿帕网 1971 年,后来成为 1983 年发布的伯克利软件分发 (BSD) 操作系统中的 API,称为 伯克利插座 .

当互联网在 1990 年代随着万维网起飞时,网络编程也是如此。 Web 服务器和浏览器并不是唯一利用新连接网络和使用套接字的应用程序。各种类型和规模的客户端-服务器应用程序得到广泛使用。

今天,虽然套接字 API 使用的底层协议已经发展了多年,我们看到了新的协议,但低级 API 保持不变。

最常见的套接字应用程序类型是客户端-服务器应用程序,其中一侧充当服务器并等待来自客户端的连接。这是我将在本教程中介绍的应用程序类型。更具体地说,我们将查看套接字 API 互联网插座 ,有时称为 Berkeley 或 BSD 套接字。还有 Unix 域套接字 ,它只能用于同一主机上的进程之间的通信。

套接字 API 概述

Python的 插座模块 提供了一个接口 伯克利套接字 API .这是我们将在本教程中使用和讨论的模块。

此模块中的主要套接字 API 函数和方法是:

socket() bind() listen() accept() connect() connect_ex() send() recv() close()

Python 提供了一个方便且一致的 API,可以直接映射到这些系统调用,即它们的 C 对应物。我们将在下一节中看看这些是如何一起使用的。

作为其标准库的一部分,Python 还具有使使用这些低级套接字函数更容易的类。虽然本教程中没有涉及,但请参阅 套接字服务器模块 ,网络服务器框架。还有许多模块可以实现更高级别的 Internet 协议,例如 HTTP 和 SMTP。有关概述,请参阅 互联网协议和支持 .

TCP 套接字

正如你很快就会看到的,我们将使用 |_+_| 创建一个套接字对象。并将套接字类型指定为 |_+_|。当你这样做时,使用的默认协议是 传输控制协议 (TCP) .这是一个很好的默认值,可能是您想要的。

为什么要使用 TCP?传输控制协议 (TCP):

是否可靠: 在网络中丢弃的数据包被发送方检测并重新传输。 具有有序数据传递: 您的应用程序按照发送者写入的顺序读取数据。

相比之下, 用户数据报协议 (UDP) 使用 |_+_| 创建的套接字不可靠,接收方读取的数据可能与发送方的写入顺序不一致。

为什么这很重要?网络是一种尽力而为的交付系统。无法保证您的数据会到达目的地,或者您会收到发送给您的数据。

网络设备(例如,路由器和交换机)具有有限的可用带宽和它们自身固有的系统限制。它们有 CPU、内存、总线和接口数据包缓冲区,就像我们的客户端和服务器一样。 TCP让您免于担心 数据包丢失 、无序到达的数据以及许多其他在您通过网络进行通信时总会发生的事情。

在下图中,让我们看看 TCP 的套接字 API 调用顺序和数据流:

TCP套接字流

左列代表服务器。右侧是客户端。

从左上角开始,请注意服务器为设置侦听套接字而进行的 API 调用:

socket.socket() socket.SOCK_STREAM socket.SOCK_DGRAM socket()

一个监听套接字就像它听起来的那样。它侦听来自客户端的连接。当客户端连接时,服务器调用 |_+_|接受或完成连接。

客户端调用|_+_|建立与服务器的连接并发起三向握手。握手步骤很重要,因为它确保连接的每一端都可以在网络中访问,换句话说,客户端可以访问服务器,反之亦然。可能只有一台主机(客户端或服务器)可以访问另一台主机。

中间是往返部分,其中使用对 |_+_| 的调用在客户端和服务器之间交换数据。和 |_+_|。

在底部,客户端和服务器 |_+_|他们各自的插座。

回声客户端和服务器

现在您已经了解了套接字 API 以及客户端和服务器如何通信的概述,让我们创建我们的第一个客户端和服务器。我们将从一个简单的实现开始。服务器将简单地将它接收到的任何内容回显给客户端。

回声服务器

这是服务器,|_+_|:

bind()

让我们遍历每个 API 调用,看看发生了什么。

|_+_|创建一个支持 上下文管理器类型 ,所以你可以在一个 |_+_|陈述 .无需调用 |_+_|:

listen()

传递给的参数 accept() 指定 地址族 和插座类型。 |_+_|是 Internet 地址族 IPv4 . |_+_|是 TCP 的套接字类型,该协议将用于在网络中传输我们的消息。

|_+_|用于将套接字与特定的网络接口和端口号相关联:

accept()

传递给 |_+_| 的值取决于套接字的地址族。在这个例子中,我们使用 |_+_| (IPv4)。所以它需要一个 2 元组:|_+_|。

|_+_|可以是主机名、IP 地址或空字符串。如果使用 IP 地址,|_+_|应该是 IPv4 格式的地址字符串。 IP地址|_+_|是标准的 IPv4 地址 环回 接口,因此只有主机上的进程才能连接到服务器。如果传递空字符串,服务器将接受所有可用 IPv4 接口上的连接。

|_+_|应该是 |_+_|-|_+_| 的整数(|_+_| 保留)。这是 TCP端口 接受来自客户端的连接的编号。如果端口是,某些系统可能需要超级用户权限

这是关于使用带有 |_+_| 的主机名的注意事项:

如果在 IPv4/v6 套接字地址的主机部分使用主机名,程序可能会显示不确定性行为,因为 Python 使用从 DNS 解析返回的第一个地址。套接字地址将以不同的方式解析为实际的 IPv4/v6 地址,具体取决于 DNS 解析和/或主机配置的结果。对于确定性行为,请在主机部分使用数字地址。 (来源)

稍后我将在使用主机名中对此进行更多讨论,但这里值得一提。现在,只需了解使用主机名时,您可能会看到不同的结果,具体取决于名称解析过程返回的内容。

它可以是任何东西。第一次运行应用程序时,它可能是地址 |_+_|。下次是不同的地址时,|_+_|。第三次,它可能是 |_+_|,依此类推。

继续服务器示例,|_+_|使服务器能够 |_+_|连接。它使它成为一个监听套接字:

connect()

|_+_|有一个 |_+_|范围。它指定系统在拒绝新连接之前允许的未接受连接数。从 Python 3.5 开始,它是可选的。如果未指定,则默认 |_+_|值被选中。

如果您的服务器同时收到大量连接请求,请增加 |_+_| value 可能有助于设置挂起连接的队列的最大长度。最大值取决于系统。例如,在 Linux 上,请参阅 send() .

|_+_|阻塞并等待传入​​连接。当客户端连接时,它返回一个表示连接的新套接字对象和一个保存客户端地址的元组。元组将包含 |_+_|用于 IPv4 连接或 |_+_|对于 IPv6。有关元组值的详细信息,请参阅参考部分中的套接字地址族。

必须理解的一件事是我们现在有一个来自 |_+_| 的新套接字对象。这很重要,因为它是您将用于与客户端通信的套接字。它不同于服务器用来接受新连接的监听套接字:

recv()

获取客户端socket对象后|_+_|来自|_+_|,无限|_+_| loop 用于循环阻塞对 |_+_| 的调用。这会读取客户端发送的任何数据并使用 |_+_| 回显它。

如果|_+_|返回一个空的 close() 对象,|_+_|,然后客户端关闭连接并终止循环。 |_+_|语句与 |_+_| 一起使用在块的末尾自动关闭套接字。

回声客户端

现在让我们看看客户端,|_+_|:

echo-server.py

与服务器相比,客户端非常简单。它创建一个套接字对象,连接到服务器并调用 |_+_|发送它的消息。最后,它调用 |_+_|读取服务器的回复,然后打印出来。

运行 Echo 客户端和服务器

让我们运行客户端和服务器以查看它们的行为并检查发生了什么。

笔记: 如果您在从命令行获取示例或您自己的代码时遇到问题,请阅读 如何使用 Python 编写自己的命令行命令? 如果您使用的是 Windows,请检查 Python Windows 常见问题解答 .

打开终端或命令提示符,导航到包含脚本的目录,然后运行服务器:

#!/usr/bin/env python3 import socket HOST = '127.0.0.1' # Standard loopback interface address (localhost) PORT = 65432 # Port to listen on (non-privileged ports are > 1023) with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.bind((HOST, PORT)) s.listen() conn, addr = s.accept() with conn: print('Connected by', addr) while True: data = conn.recv(1024) if not data: break conn.sendall(data)

您的终端将出现挂起。那是因为服务器在调用中被阻塞(挂起):

socket.socket()

它正在等待客户端连接。现在打开另一个终端窗口或命令提示符并运行客户端:

with

在服务器窗口中,您应该看到:

s.close()

在上面的输出中,服务器打印了 |_+_|从 |_+_| 返回的元组。这是客户端的 IP 地址和 TCP 端口号。当您在机器上运行它时,端口号 |_+_| 很可能会有所不同。

查看套接字状态

要查看主机上套接字的当前状态,请使用 |_+_|。默认情况下,它在 macOS、Linux 和 Windows 上可用。

这是启动服务器后 macOS 的 netstat 输出:

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: pass # Use the socket object without calling s.close().

注意|_+_|是 |_+_|。如果|_+_|用过|_+_|代替 |_+_|,netstat 会显示:

socket()

|_+_|是 |_+_|,这意味着所有支持地址族的可用主机接口都将用于接受传入连接。在这个例子中,在对 |_+_| 的调用中,|_+_|使用 (IPv4)。您可以在 |_+_| 中看到这一点列:|_+_|。

我已经修剪了上面的输出以仅显示回显服务器。您可能会看到更多输出,具体取决于您运行它的系统。需要注意的是|_+_|、|_+_| 和|_+_| 列。在上面的最后一个示例中,netstat 显示回显服务器在所有接口 (|_+_|) 上的端口 65432 上使用 IPv4 TCP 套接字 (|_+_|),并且处于侦听状态 (|_+_ |)。

另一种查看此信息以及其他有用信息的方法是使用 |_+_| (列出打开的文件)。默认情况下,它在 macOS 上可用,如果还没有,可以使用包管理器在 Linux 上安装:

AF_INET

|_+_|给你 |_+_|, |_+_| (进程ID)和|_+_|与 |_+_| 一起使用时打开的 Internet 套接字的(用户 ID)选项。以上是回显服务器进程。

|_+_|和|_+_|有很多可用的选项,并且根据您运行它们的操作系统而有所不同。检查 |_+_|页面或文档。他们绝对值得花一点时间去了解。你会得到回报。在 macOS 和 Linux 上,使用 |_+_|和 |_+_|。对于 Windows,使用 |_+_|。

当尝试连接到没有侦听套接字的端口时,您会看到以下常见错误:

SOCK_STREAM

指定的端口号错误或服务器未运行。或者,路径中可能有防火墙阻止了连接,这很容易忘记。您可能还会看到错误 |_+_|。添加防火墙规则,允许客户端连接到 TCP 端口!

参考部分中有一个常见错误列表。

通讯故障

让我们仔细看看客户端和服务器是如何相互通信的:

套接字环回接口

当使用 环回 接口(IPv4地址|_+_|或IPv6地址|_+_|),数据永远不会离开主机或接触外部网络。在上图中,回送接口包含在主机内部。这代表了环回接口的内部特性以及传输它的连接和数据对于主机来说是本地的。这就是为什么您还会听到环回接口和 IP 地址 |_+_|或 |_+_|称为本地主机。

应用程序使用环回接口与主机上运行的其他进程进行通信,并确保安全性和与外部网络的隔离。因为它是内部的并且只能从主机内部访问,所以它没有暴露。

如果您有一个使用自己的私有数据库的应用程序服务器,您就可以看到这一点。如果它不是其他服务器使用的数据库,则它可能配置为仅侦听环回接口上的连接。如果是这种情况,网络上的其他主机将无法连接到它。

当您使用除 |_+_| 以外的 IP 地址时或 |_+_|在您的应用程序中,它可能绑定到 以太网 连接到外部网络的接口。这是您通往本地主机王国之外的其他主机的门户:

套接字以太网接口

在外面小心点。这是一个肮脏、残酷的世界。在从 localhost 的安全范围冒险之前,请务必阅读使用主机名部分。即使您不使用主机名而仅使用 IP 地址,也有一个安全说明适用。

处理多个连接

echo 服务器肯定有其局限性。最大的问题是它只为一个客户服务,然后就退出了。 echo 客户端也有这个限制,但还有一个额外的问题。当客户端进行以下调用时,有可能是 |_+_|将只返回一个字节,|_+_|来自 |_+_|:

bind()

|_+_| |_+_| 的参数上面使用的是一次接收的最大数据量。这并不意味着|_+_|将返回 |_+_|字节。

|_+_|也有这种行为。 |_+_|返回发送的字节数,可能小于传入数据的大小。你负责检查这个并调用|_+_|根据需要多次发送所有数据:

应用程序负责检查所有数据是否已发送;如果仅传输了部分数据,则应用程序需要尝试传输剩余数据。 (来源)

我们通过使用 |_+_| 避免了这样做:

与 send() 不同,此方法继续从字节发送数据,直到所有数据都已发送或发生错误为止。成功时不返回任何内容。 (来源)

此时我们有两个问题:

我们如何同时处理多个连接? 我们需要调用|_+_|和|_+_|直到发送或接收所有数据。

我们做什么?有很多方法可以 并发 .最近,一种流行的方法是使用 异步输入/输出 . |_+_|在 Python 3.4 中被引入标准库。传统的选择是使用 线程 .

并发的问题是很难做到正确。有许多微妙之处需要考虑和防范。所需要的只是其中之一表现出来,您的应用程序可能会突然以不那么微妙的方式失败。

我这样说并不是为了吓唬你远离学习和使用并发编程。如果您的应用程序需要扩展,那么如果您想使用多个处理器或一个内核,这是必要的。但是,在本教程中,我们将使用比线程更传统且更易于推理的东西。我们将使用系统调用的祖父: HOST = '127.0.0.1' # Standard loopback interface address (localhost) PORT = 65432 # Port to listen on (non-privileged ports are > 1023) # ... s.bind((HOST, PORT)) .

|_+_|允许您检查多个套接字上的 I/O 完成情况。所以你可以打电话给|_+_|查看哪些套接字已准备好读取和/或写入的 I/O。但这是 Python,所以还有更多。我们将使用 选择器 标准库中的模块,因此使用最有效的实现,无论我们碰巧运行在哪个操作系统上:

该模块允许基于选择模块原语的高级和高效 I/O 多路复用。鼓励用户改用此模块,除非他们想要精确控制所使用的操作系统级原语。 (来源)

尽管通过使用 |_+_|,我们无法并发运行,根据您的工作负载,这种方法可能仍然足够快。这取决于您的应用程序在为请求提供服务时需要做什么以及它需要支持的客户端数量。

bind() 使用单线程协作多任务和事件循环来管理任务。使用 |_+_|,我们将编写我们自己的事件循环版本,尽管更简单和同步。当使用多线程时,即使你有并发,我们目前也必须使用 GIL 和 CPython 和 PyPy .无论如何,这有效地限制了我们可以并行执行的工作量。

我说这一切是为了解释使用 |_+_|可能是一个完美的选择。不要觉得你必须使用 |_+_|、线程或最新的异步库。通常,在网络应用程序中,您的应用程序受 I/O 限制:它可能在本地网络、网络另一端的端点、磁盘等上等待。

如果您从启动 CPU 密集型工作的客户端收到请求,请查看 并发.期货 模块。它包含类 进程池执行器 它使用进程池异步执行调用。

如果您使用多个进程,操作系统能够安排您的 Python 代码在多个处理器或内核上并行运行,而无需 GIL。有关想法和灵感,请参阅 PyCon 演讲 John Reese - Thinking Outside the GIL with AsyncIO and Multiprocessing - PyCon 2018:

在下一节中,我们将查看解决这些问题的服务器和客户端的示例。他们使用 |_+_|同时处理多个连接并调用 |_+_|和|_+_|根据需要多次。

多连接客户端和服务器

在接下来的两节中,我们将创建一个使用 |_+_| 处理多个连接的服务器和客户端。从创建的对象 选择器 模块。

多连接服务器

首先我们来看多连接服务器,|_+_|。这是设置侦听套接字的第一部分:

socket.AF_INET

这个服务器和回显服务器最大的区别是对|_+_|的调用以非阻塞模式配置套接字。对此套接字的调用将不再阻塞。当它与 |_+_| 一起使用时,正如你将在下面看到的,我们可以等待一个或多个套接字上的事件,然后在它准备好时读写数据。

|_+_|使用 |_+_| 注册要监控的套接字对于您感兴趣的事件。对于侦听套接字,我们需要读取事件:|_+_|。

|_+_|用于存储您想要的任何任意数据以及套接字。当 |_+_| 时返回返回。我们将使用 |_+_|跟踪套接字上发送和接收的内容。

接下来是事件循环:

(host, port)

host 阻塞,直到有套接字准备好进行 I/O。它返回一个 (key, events) 元组列表,每个套接字一个。 |_+_|是一个 选择键 |_+_|包含 |_+_|属性。 |_+_|是套接字对象,|_+_|是准备好的操作的事件掩码。

如果|_+_|是|_+_|,那么我们知道它来自监听套接字,我们需要|_+_|连接。我们将调用我们自己的 |_+_|包装函数以获取新的套接字对象并将其注册到选择器。我们一会儿再看。

如果|_+_|不是|_+_|,那么我们知道它是一个已经被接受的客户端套接字,我们需要为它服务。 |_+_|然后调用并传递 |_+_|和|_+_|,其中包含我们在套接字上操作所需的一切。

让我们看看我们的|_+_|功能:

host

由于监听套接字是为事件|_+_| 注册的,它应该准备好读取。我们称|_+_|然后立即调用 |_+_|将套接字置于非阻塞模式。

请记住,这是此版本服务器的主要目标,因为我们不希望它被阻塞。如果它阻塞了,那么整个服务器就会停止,直到它返回。这意味着其他套接字正在等待。这是您不希望服务器处于可怕的挂起状态。

接下来,我们使用类 |_+_| 创建一个对象来保存我们想要与套接字一起包含的数据。由于我们想知道客户端连接何时准备好进行读写,因此这两个事件都使用以下设置:

127.0.0.1

|_+_|然后将掩码、套接字和数据对象传递给 |_+_|。

现在让我们看看|_+_|查看客户端连接在准备好时如何处理:

port

这是简单的多连接服务器的核心。 |_+_|是 |_+_|从 |_+_| 返回包含套接字对象 (|_+_|) 和数据对象。 |_+_|包含准备好的事件。

如果套接字已准备好读取,则 |_+_|是真的,而且 |_+_|叫做。读取的任何数据都附加到 |_+_|所以它可以稍后发送。

注意 |_+_|如果没有收到数据则阻塞:

1

这意味着客户端已经关闭了他们的套接字,所以服务器也应该关闭。但是不要忘记先调用|_+_|所以它不再被 |_+_| 监控。

当套接字准备好写入时,对于健康的套接字应该总是如此,任何接收到的数据都存储在 |_+_| 中。使用 |_+_| 回显给客户端。然后从发送缓冲区中删除发送的字节:

65535多连接客户端

现在让我们看看多连接客户端,|_+_|。它与服务器非常相似,但它不是侦听连接,而是通过 |_+_| 启动连接:

0

|_+_|从命令行读取,这是要创建到服务器的连接数。就像服务器一样,每个套接字都设置为非阻塞模式。

|_+_|用于代替 |_+_|因为|_+_|会立即引发 |_+_|例外。 |_+_|最初返回一个错误指示符 |_+_|,而不是在连接过程中引发异常。一旦连接完成,套接字就准备好读取和写入,并由 |_+_| 返回。

付款无法发送现金应用程序

套接字设置好后,我们要与套接字一起存储的数据是使用类|_+_| 创建的。客户端将发送到服务器的消息使用 |_+_| 复制因为每个连接都会调用 |_+_|并修改列表。跟踪客户端需要发送、已发送和接收的内容以及消息中的总字节数所需的一切都存储在对象 |_+_| 中。

让我们看看|_+_|。它与服务器基本相同:

1024

有一个重要的区别。它跟踪它从服务器接收到的字节数,以便它可以关闭它的连接端。当服务器检测到这一点时,它也会关闭它的连接端。

请注意,通过这样做,服务器依赖于行为良好的客户端:服务器希望客户端在完成发送消息后关闭其连接端。如果客户端没有关闭,服务器将保持连接打开。在实际应用程序中,您可能希望在服务器中防止这种情况发生,并防止客户端连接在一定时间后未发送请求时累积。

运行多连接客户端和服务器

现在让我们运行 |_+_|和 |_+_|。它们都使用命令行参数。您可以不带参数运行它们以查看选项。

对于服务器,传递一个 |_+_|和|_+_|数字:

bind()

对于客户端,还将要创建的连接数传递给服务器,|_+_|:

10.1.2.3

以下是在端口 65432 上侦听环回接口时的服务器输出:

192.168.0.1

以下是客户端创建与上述服务器的两个连接时的输出:

172.16.7.8应用程序客户端和服务器

与我们开始时相比,多连接客户端和服务器示例绝对是一个改进。但是,让我们再进一步,在最终实现中解决前面的 multiconn 示例的缺点:应用程序客户端和服务器。

我们想要一个能够适当处理错误的客户端和服务器,这样其他连接就不会受到影响。显然,如果没有捕获到异常,我们的客户端或服务器不应该在愤怒中崩溃。这是我们目前还没有讨论过的。为了简洁明了,我特意在示例中省略了错误处理。

现在您已经熟悉了基本 API、非阻塞套接字和 |_+_|,我们可以添加一些错误处理并讨论房间里的大象,我一直在那边的大窗帘后面对您隐藏起来.是的,我说的是我在介绍中提到的自定义类。我知道你不会忘记的。

首先,让我们解决错误:

所有错误都会引发异常。可以引发无效参数类型和内存不足情况的正常异常;从 Python 3.3 开始,与套接字或地址语义相关的错误引发 |_+_|或其子类之一。 (来源)

我们需要抓住|_+_|。关于错误我没有提到的另一件事是超时。您会在文档的许多地方看到它们的讨论。超时发生并且是正常错误。主机和路由器重新启动,交换机端口坏了,电缆坏了,电缆被拔掉了,你能想到的。您应该为这些和其他错误做好准备,并在您的代码中处理它们。

房间里的大象呢?正如套接字类型 |_+_| 所暗示的那样,使用 TCP 时,您正在从连续的字节流中读取数据。这就像从磁盘上的文件中读取,但实际上您是从网络中读取字节。

但是,与读取文件不同,没有 listen() .换句话说,你不能重新定位套接字指针(如果有的话),也不能随意移动读取的数据,只要你愿意。

当字节到达您的套接字时,就会涉及网络缓冲区。一旦你阅读了它们,它们需要被保存在某个地方。打电话|_+_|再次从套接字读取下一个可用字节流。

这意味着您将分块读取套接字。您需要致电|_+_|并将数据保存在缓冲区中,直到您读取了足够的字节以获得对您的应用程序有意义的完整消息。

由您来定义和跟踪消息边界的位置。就 TCP 套接字而言,它只是向网络发送和从网络接收原始字节。它对这些原始字节的含义一无所知。

这让我们定义了一个应用层协议。什么是应用层协议?简而言之,您的应用程序将发送和接收消息。这些消息是您的应用程序的协议。

换句话说,您为这些消息选择的长度和格式定义了应用程序的语义和行为。这与我在上一段中解释的有关从套接字读取字节的内容直接相关。当您使用 |_+_| 读取字节时,您需要了解读取了多少字节并确定消息边界在哪里。

这是怎么做的?一种方法是始终发送固定长度的消息。如果它们总是相同的大小,那么这很容易。当您将那个字节数读入缓冲区时,您就知道您有一个完整的消息。

但是,对于需要使用填充来填充它们的小消息,使用固定长度的消息是低效的。此外,您仍然面临如何处理不适合一条消息的数据的问题。

在本教程中,我们将采用通用方法。许多协议都使用的一种方法,包括 HTTP。我们将使用包含内容长度以及我们需要的任何其他字段的标头作为消息的前缀。通过这样做,我们只需要跟上标题。一旦我们读取了标头,我们就可以对其进行处理以确定消息内容的长度,然后读取该字节数以使用它。

我们将通过创建一个自定义类来实现这一点,该类可以发送和接收包含文本或二进制数据的消息。您可以为自己的应用程序改进和扩展它。最重要的是,您将能够看到有关如何完成此操作的示例。

我需要提及可能影响您的有关套接字和字节的内容。正如我们之前谈到的,当通过套接字发送和接收数据时,您正在发送和接收原始字节。

如果您接收数据并希望在将其解释为多个字节的上下文中使用它,例如一个 4 字节的整数,您需要考虑它可能采用非机器 CPU 原生的格式。另一端的客户端或服务器的 CPU 可能使用与您自己的字节顺序不同的字节顺序。如果是这种情况,您需要在使用之前将其转换为主机的本机字节顺序。

此字节顺序称为 CPU 的 字节序 .有关详细信息,请参阅参考部分中的字节顺序。我们将通过将 Unicode 用于我们的消息标头并使用 UTF-8 编码来避免这个问题。由于 UTF-8 使用 8 位编码,因此不存在字节顺序问题。

你可以在 Python’s 中找到解释 编码和 Unicode 文档。请注意,这仅适用于文本标题。我们将使用在要发送的内容(消息有效负载)的标头中定义的显式类型和编码。这将允许我们以任何格式传输我们想要的任何数据(文本或二进制)。

您可以使用 |_+_| 轻松确定机器的字节顺序。例如,在我的英特尔笔记本电脑上,会发生这种情况:

accept()

如果我在虚拟机中运行它 模仿 大端 CPU (PowerPC),然后会发生这种情况:

s.listen() conn, addr = s.accept()

在此示例应用程序中,我们的应用程序层协议将标头定义为具有 UTF-8 编码的 Unicode 文本。对于消息中的实际内容,消息有效负载,如果需要,您仍然必须手动交换字节顺序。

这将取决于您的应用程序以及它是否需要处理来自具有不同字节序的机器的多字节二进制数据。您可以通过添加额外的标头并使用它们来传递参数来帮助您的客户端或服务器实现二进制支持,类似于 HTTP。

如果这还没有意义,请不要担心。在下一节中,您将看到所有这些是如何工作和组合在一起的。

应用协议头

让我们完全定义协议头。协议头是:

变长文本 Unicode 编码为 UTF-8 使用 JSON 序列化的 Python 字典

协议头字典中所需的头或子头如下:

这些标头将消息的有效负载中的内容通知给接收者。这允许您在提供足够信息的同时发送任意数据,以便接收方可以正确解码和解释内容。由于标头在字典中,因此可以通过根据需要插入键/值对来轻松添加其他标头。

发送应用消息

还是有点问题。我们有一个可变长度的头部,它很好而且很灵活,但是用|_+_| 读取时如何知道头部的长度?

当我们之前谈到使用 |_+_|和消息边界,我提到固定长度的标头可能效率低下。确实如此,但我们将使用一个小的 2 字节固定长度标头来作为包含其长度的 JSON 标头的前缀。

您可以将其视为发送消息的混合方法。实际上,我们通过首先发送头的长度来引导消息接收过程。这使得我们的接收者很容易解构消息。

为了让您更好地了解消息格式,让我们看一下整个消息:

套接字应用程序消息

消息以 2 个字节的固定长度标头开始,该标头是网络字节顺序中的整数。这是下一个标头的长度,即可变长度的 JSON 标头。一旦我们用 |_+_| 读取了 2 个字节,我们就知道我们可以将这 2 个字节作为整数处理,然后在解码 UTF-8 JSON 标头之前读取该字节数。

JSON 标头包含附加标头的字典。其中之一是 |_+_|,这是消息内容的字节数(不包括 JSON 标头)。一旦我们调用了 |_+_|并阅读 |_+_|字节,我们已经到达消息边界并读取整个消息。

应用消息类

终于有回报了!让我们看看|_+_|类,看看它是如何与 |_+_| 一起使用的当套接字上发生读写事件时。

对于这个示例应用程序,我必须想出客户端和服务器将使用什么类型的消息的想法。在这一点上,我们远远超出了玩具回声客户端和服务器。

为简单起见并演示实际应用程序中的工作方式,我创建了一个应用程序协议来实现基本搜索功能。客户端发送搜索请求,服务器查找匹配项。如果客户端发送的请求未被识别为搜索,则服务器假定它是一个二进制请求并返回一个二进制响应。

在阅读以下部分、运行示例并试验代码后,您将看到事情是如何工作的。然后你可以使用 |_+_|类作为起点并对其进行修改以供您自己使用。

我们真的离 multiconn 客户端和服务器示例不远了。事件循环代码在 |_+_| 中保持不变和 |_+_|。我所做的是将消息代码移动到名为 |_+_| 的类中并添加了支持读取、写入和处理标题和内容的方法。这是使用类的一个很好的例子。

正如我们之前讨论的,您将在下面看到,使用套接字涉及保持状态。通过使用类,我们将所有状态、数据和代码捆绑在一个有组织的单元中。当连接开始或接受时,为客户端和服务器中的每个套接字创建一个类的实例。

对于包装器和实用程序方法,客户端和服务器的类基本相同。它们以下划线开头,例如 |_+_|。这些方法简化了类的工作。他们通过让其他方法保持更短的时间来帮助其他方法 干燥 原则。

服务器的 |_+_| class 的工作方式与客户端的基本相同,反之亦然。区别在于客户端发起连接并发送请求消息,然后处理服务器的响应消息。相反,服务器等待连接,处理客户端的请求消息,然后发送响应消息。

它看起来像这样:

这是文件和代码布局:

消息入口点

我想讨论一下 |_+_| class 的工作原理是首先提到它的设计中对我来说不是很明显的一个方面。只有在重构它至少五次之后,我才达到现在的状态。为什么?管理状态。

在 |_+_| 之后对象被创建,它与一个使用 |_+_| 监视事件的套接字相关联:

listen()

笔记: 本节中的一些代码示例来自服务器的主脚本和 |_+_|类,但本节和讨论同样适用于客户。当客户的版本不同时,我会展示并解释它。

当套接字上的事件准备好时,它们由 |_+_| 返回。然后我们可以使用 |_+_| 来获取对消息对象的引用。 |_+_| 上的属性对象并在 |_+_| 中调用方法:

backlog

查看上面的事件循环,你会看到 |_+_|坐在驾驶座上。它是阻塞的,在循环的顶部等待事件。它负责在套接字上准备好处理读写事件时唤醒。这意味着,间接地,它还负责调用方法 |_+_|。这就是我说方法时的意思|_+_|是入口点。

让我们看看 |_+_| 是什么方法做:

backlog

那很好:|_+_|很简单。它只能做两件事:调用|_+_|和 |_+_|。

这让我们回到管理状态。经过几次重构,我决定如果另一种方法依赖于具有特定值的状态变量,那么它们只会从 |_+_| 调用。和 |_+_|。这使逻辑尽可能简单,因为事件进入套接字进行处理。

这似乎很明显,但该类的前几次迭代混合了一些检查当前状态的方法,并根据它们的值调用其他方法来处理 |_+_| 之外的数据。或 |_+_|。最后,事实证明这太复杂了,无法管理和跟上。

您绝对应该修改该类以满足您自己的需要,以便它最适合您,但我建议您将状态检查和对依赖于该状态的方法的调用保留到 |_+_|和|_+_|如果可能,方法。

让我们看看|_+_|。这是服务器的版本,但客户端的版本是一样的。它只是使用不同的方法名称,|_+_|而不是 |_+_|:

backlog

|_+_|首先调用方法。它调用 |_+_|从套接字读取数据并将其存储在接收缓冲区中。

请记住,当 |_+_|被调用时,构成完整消息的所有数据可能尚未到达。 |_+_|可能需要再次调用。这就是为什么在调用适当的方法来处理消息之前对消息的每个部分进行状态检查的原因。

在方法处理它的部分消息之前,它首先检查以确保已将足够的字节读入接收缓冲区。如果有,它会处理其各自的字节,将它们从缓冲区中删除,并将其输出写入下一个处理阶段使用的变量。由于一条消息有三个组成部分,所以有三个状态检查和 |_+_|方法调用:

接下来我们看|_+_|。这是服务器的版本:

/proc/sys/net/core/somaxconn

|_+_|首先检查 |_+_|。如果存在且尚未创建响应,则 |_+_|叫做。 |_+_|设置状态变量 |_+_|并将响应写入发送缓冲区。

|_+_|方法调用|_+_|如果发送缓冲区中有数据。

请记住,当 |_+_|被调用时,发送缓冲区中的所有数据可能尚未排队等待传输。套接字的网络缓冲区可能已满,并且 |_+_|可能需要再次调用。这就是为什么要进行状态检查的原因。 |_+_|应该只被调用一次,但预计 |_+_|将需要多次调用。

|_+_|的客户端版本类似:

accept()

由于客户端向服务器发起连接并先发送请求,状态变量|_+_|被检查。如果请求尚未排队,则调用 |_+_|。 |_+_|创建请求并将其写入发送缓冲区。它还设置状态变量 |_+_|所以它只被调用一次。

就像服务器一样,|_+_|电话|_+_|如果发送缓冲区中有数据。

客户端版本|_+_|的显着差异是最后一次检查请求是否已排队。这将在客户端主脚本一节中详细解释,但这样做的原因是为了告诉 |_+_|停止监视套接字的写事件。如果请求已排队并且发送缓冲区为空,那么我们就完成了写入并且我们只对读取事件感兴趣。没有理由通知套接字是可写的。

我将通过给您留下一个想法来结束本节。本节的主要目的是解释|_+_|正在呼叫 |_+_|类通过方法 |_+_|并描述如何管理状态。

这很重要,因为 |_+_|将在连接的生命周期内多次调用。因此,请确保任何只应调用一次的方法要么自己检查状态变量,要么由调用者检查该方法设置的状态变量。

服务器主脚本

在服务器的主脚本 |_+_| 中,从命令行读取参数,指定要侦听的接口和端口:

(host, port)

例如,要侦听端口 |_+_| 上的环回接口,请输入:

(host, port, flowinfo, scopeid)

|_+_| 使用空字符串监听所有接口。

创建套接字后,调用 |_+_|使用选项 |_+_|:

accept()

设置此套接字选项可避免错误 |_+_|。您将在启动服务器时看到这一点,并且同一端口上以前使用过的 TCP 套接字在 时间的等待 状态。

例如,如果服务器主动关闭了一个连接,它将保留在 |_+_|状态两分钟或更长时间,具体取决于操作系统。如果您尝试在 |_+_| 之前再次启动服务器状态过期,你会得到一个 |_+_| |_+_| 除外。这是一种保护措施,可确保网络中的任何延迟数据包不会传送到错误的应用程序。

事件循环会捕获任何错误,以便服务器可以保持运行并继续运行:

conn, addr = s.accept() with conn: print('Connected by', addr) while True: data = conn.recv(1024) if not data: break conn.sendall(data)

当一个客户端连接被接受时,一个 |_+_|创建对象:

conn

|_+_|对象在对 |_+_| 的调用中与套接字相关联并且最初设置为仅监视读取事件。读取请求后,我们将修改它以仅侦听写入事件。

在服务器中采用这种方法的一个优点是,在大多数情况下,当套接字健康且没有网络问题时,它将始终是可写的。

如果我们告诉 |_+_|还要监控|_+_|,事件循环会立即唤醒并通知我们这是这种情况。然而,此时,还没有理由醒来打电话给|_+_|在插座上。由于尚未处理请求,因此没有要发送的响应。这会消耗和浪费宝贵的 CPU 周期。

服务器消息类

在消息入口点部分,我们研究了 |_+_|当套接字事件通过 |_+_| 准备就绪时,对象被调用。现在让我们看看在套接字上读取数据并且消息的一个组件或片段准备好由服务器处理时会发生什么。

服务器的消息类别在 |_+_| 中。

这些方法按照消息处理发生的顺序出现在类中。

当服务器至少读取了 2 个字节时,可以处理定长头:

accept()

固定长度的标头是一个 2 字节的网络(大端)字节顺序整数,包含 JSON 标头的长度。 struct.unpack() 用于读取值,解码它,并将其存储在 |_+_| 中。处理完它负责的那条消息后,|_+_|将其从接收缓冲区中删除。

就像固定长度的头一样,当接收缓冲区中有足够的数据来包含 JSON 头时,它也可以被处理:

while

方法|_+_|被调用以将 JSON 标头解码和反序列化为字典。由于 JSON 头被定义为 UTF-8 编码的 Unicode,|_+_|在调用中进行了硬编码。结果保存到|_+_|。处理完它负责的那条消息后,|_+_|将其从接收缓冲区中删除。

接下来是消息的实际内容或有效负载。它由 |_+_| 中的 JSON 标头描述。当|_+_|字节在接收缓冲区中可用,请求可以被处理:

conn.recv()

将消息内容保存到|_+_|后变量,|_+_|将其从接收缓冲区中删除。然后,如果内容类型是 JSON,它会对其进行解码和反序列化。如果不是,对于这个示例应用程序,它假定它是一个二进制请求并简单地打印内容类型。

最后一件事|_+_|确实是修改选择器以仅监视写入事件。在服务器的主脚本 |_+_| 中,套接字最初设置为仅监视读取事件。现在请求已被完全处理,我们不再对阅读感兴趣。

现在可以创建响应并将其写入套接字。当套接字可写时,|_+_|从 |_+_| 调用:

conn.sendall()

响应是通过调用其他方法创建的,具体取决于内容类型。在这个示例应用程序中,当 |_+_| 时,对 JSON 请求进行了简单的字典查找。您可以为自己的应用程序定义在此处调用的其他方法。

创建响应消息后,状态变量|_+_|设置为|_+_|不叫 |_+_|再次。最后,响应被附加到发送缓冲区。这是由 |_+_| 看到和发送的。

需要弄清楚的一个棘手问题是如何在写入响应后关闭连接。我打电话给|_+_|在方法 |_+_| 中:

conn.recv()

虽然它有点隐藏,但我认为这是一个可以接受的权衡,因为 |_+_|类每个连接只处理一条消息。写入响应后,服务器就没有什么可做的了。它完成了它的工作。

客户端主脚本

在客户端的主脚本 |_+_| 中,从命令行读取参数并用于创建请求并启动与服务器的连接:

bytes

下面是一个例子:

b''

在从命令行参数创建代表请求的字典后,主机、端口和请求字典被传递给 |_+_|:

with

为服务器连接创建一个套接字以及一个 |_+_|对象使用 |_+_|字典。

和服务器一样,|_+_|对象与 |_+_| 调用中的套接字相关联。但是,对于客户端,套接字最初设置为监视读取和写入事件。写入请求后,我们将修改它以仅侦听读取事件。

这种方法为我们提供了与服务器相同的优势:不浪费 CPU 周期。请求发送后,我们不再对写入事件感兴趣,因此没有理由唤醒并处理它们。

客户端消息类

在消息入口点部分,我们查看了当套接字事件准备好时如何通过 |_+_| 调用消息对象。现在让我们看看在套接字上读取和写入数据并且消息准备好由客户端处理后会发生什么。

这些方法按照消息处理发生的顺序出现在类中。

客户端的第一个任务是对请求进行排队:

conn

根据命令行传递的内容,用于创建请求的字典位于客户端的主脚本 |_+_| 中。当 |_+_| 时,请求字典作为参数传递给类。对象被创建。

请求消息被创建并附加到发送缓冲区,然后由 |_+_| 看到并通过它发送。状态变量|_+_|设置为|_+_|不再被调用。

请求发送后,客户端等待服务器的响应。

客户端读取和处理消息的方法与服务器相同。当从套接字读取响应数据时,|_+_|头方法被称为:|_+_|和 |_+_|。

区别在于最后|_+_|的命名方法以及它们正在处理响应而不是创建响应的事实:|_+_|、|_+_| 和 |_+_|。

最后,但同样重要的是,最后一次调用 |_+_|:

echo-client.py消息类总结

我将结束 |_+_|课堂讨论通过提到一些重要的事情来注意一些支持方法。

该类引发的任何异常都被主脚本在其 |_+_| 中捕获。条款:

#!/usr/bin/env python3 import socket HOST = '127.0.0.1' # The server's hostname or IP address PORT = 65432 # The port used by the server with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.connect((HOST, PORT)) s.sendall(b'Hello, world') data = s.recv(1024) print('Received', repr(data))

注意最后一行:|_+_|。

这是一条非常重要的线路,原因不止一个!它不仅确保套接字关闭,而且 |_+_|也从|_+_| 的监视中移除套接字。这大大简化了类中的代码并降低了复杂性。如果有异常或者我们自己明确提出异常,我们知道 |_+_|将负责清理工作。

方法|_+_|和|_+_|还包含一些有趣的东西:

s.sendall()

注意 |_+_|行:|_+_|。

|_+_|也有一个。这些行很重要,因为它们捕获临时错误并使用 |_+_| 跳过它。临时错误是套接字何时 堵塞 ,例如,如果它正在等待网络或连接的另一端(其对等方)。

通过 |_+_|, |_+_| 捕获并跳过异常最终会再次给我们打电话,我们将获得另一个读取或写入数据的机会。

运行应用程序客户端和服务器

在完成所有这些艰苦的工作之后,让我们玩得开心并进行一些搜索!

在这些示例中,我将运行服务器,以便它通过为 |_+_| 传递一个空字符串来侦听所有接口。争论。这将允许我运行客户端并从另一个网络上的虚拟机连接。它模拟大端 PowerPC 机器。

首先,让我们启动服务器:

s.recv()

现在让我们运行客户端并输入搜索。让我们看看能不能找到他:

$ ./echo-server.py

我的终端正在运行一个使用 Unicode (UTF-8) 文本编码的 shell,因此上面的输出与表情符号打印得很好。

让我们看看我们是否能找到小狗:

conn, addr = s.accept()

注意 |_+_| 中的请求通过网络发送的字节串线。如果您查找以十六进制打印的代表小狗表情符号的字节,则更容易查看:|_+_|。我以前可以 输入表情符号 用于搜索,因为我的终端使用编码为 UTF-8 的 Unicode。

这表明我们正在通过网络发送原始字节,它们需要由接收器解码才能正确解释。这就是为什么我们不厌其烦地创建包含内容类型和编码的标头的原因。

这是上面两个客户端连接的服务器输出:

$ ./echo-client.py Received b'Hello, world'

看看|_+_|行查看写入客户端套接字的字节。这是服务器的响应消息。

如果|_+_|,您还可以测试向服务器发送二进制请求参数不是|_+_|:

$ ./echo-server.py Connected by ('127.0.0.1', 64623)

由于请求的 |_+_|不是 |_+_|,服务器将其视为自定义二进制类型并且不执行 JSON 解码。它只是打印 |_+_|并将前 10 个字节返回给客户端:

addr故障排除

不可避免地,有些事情行不通,你会想知道该怎么做。别担心,它发生在我们所有人身上。希望在本教程、您的调试器和最喜欢的搜索引擎的帮助下,您将能够重新开始使用源代码部分。

如果没有,你的第一站应该是 Python 的 插座模块 文档。确保您阅读了您正在调用的每个函数或方法的所有文档。

有时,这不仅仅与源代码有关。源代码可能是正确的,它只是另一台主机、客户端或服务器。或者它可能是网络,例如,路由器、防火墙或其他一些玩中间人的网络设备。

对于这些类型的问题,额外的工具是必不可少的。以下是一些可能会有所帮助或至少提供一些线索的工具和实用程序。

|_+_|将通过发送一个主机来检查主机是否处于活动状态并连接到网络 ICMP 回声请求。它直接与操作系统的 TCP/IP 协议栈通信,因此它独立于主机上运行的任何应用程序工作。

以下是在 macOS 上运行 ping 的示例:

s.accept()

请注意输出末尾的统计信息。当您尝试发现间歇性连接问题时,这会很有帮助。例如,有没有丢包?有多少延迟(请参阅往返时间)?

如果您和另一台主机之间有防火墙,则可能不允许 ping 的回显请求。一些防火墙管理员实施了强制执行此操作的策略。这个想法是他们不希望他们的主机被发现。如果是这种情况并且您添加了防火墙规则以允许主机进行通信,请确保这些规则还允许 ICMP 在它们之间传递。

ICMP 是 |_+_| 使用的协议,但它也是 TCP 和其他低级协议用于传递错误消息的协议。如果您遇到奇怪的行为或连接缓慢,这可能是原因。

ICMP 消息由类型和代码标识。为了让您了解它们携带的重要信息,这里有一些:

看文章 路径 MTU 发现 有关分片和 ICMP 消息的信息。这是一个可能导致我之前提到的奇怪行为的例子。

网络统计

在查看套接字状态一节中,我们了解了 |_+_|可用于显示有关套接字及其当前状态的信息。此实用程序可用于 macOS、Linux 和 Windows。

我没有提到列|_+_|和|_+_|在示例输出中。这些列将显示保存在排队等待传输或接收的网络缓冲区中的字节数,但由于某种原因尚未被远程或本地应用程序读取或写入。

换句话说,字节在操作系统队列中的网络缓冲区中等待。原因之一可能是应用程序受 CPU 限制或无法调用 |_+_|或 |_+_|并处理字节。或者可能存在影响通信的网络问题,例如拥塞或网络硬件或电缆故障。

为了证明这一点并在看到错误之前查看我可以发送多少数据,我编写了一个连接到测试服务器并重复调用 |_+_| 的测试客户端。测试服务器从不调用 |_+_|。它只是接受连接。这会导致服务器上的网络缓冲区被填满,最终在客户端上引发错误。

首先,我启动了服务器:

64623

然后我运行了客户端。让我们看看错误是什么:

netstat

这是|_+_|在客户端和服务器仍在运行时输出,客户端多次打印出上面的错误消息:

$ netstat -an Active Internet connections (including servers) Proto Recv-Q Send-Q Local Address Foreign Address (state) tcp4 0 0 127.0.0.1.65432 *.* LISTEN

第一个条目是服务器(|_+_| 有端口 65432):

Local Address

注意|_+_|:|_+_|。

第二个条目是客户端(|_+_| 有端口 65432):

127.0.0.1.65432

注意|_+_|:|_+_|。

客户端确实在尝试写入字节,但服务器没有读取它们。这导致服务器的网络缓冲队列在接收端被填满,​​而客户端的网络缓冲队列在发送端被填满。

视窗

如果您使用 Windows,有一套实用程序,如果您还没有,则绝对应该检查一下: Windows 系统内部 .

其中之一是|_+_|。 TCPView 是一个图形 |_+_|对于 Windows。除了地址、端口号和套接字状态之外,它还会显示发送和接收的数据包和字节数的运行总数。与 Unix 实用程序 |_+_| 一样,您也可以获得进程名称和 ID。检查其他显示选项的菜单。

TCPView截图

线鲨

有时您需要查看线路上发生了什么。忘记应用程序日志所说的内容或从库调用返回的值是什么。您想查看网络上实际发送或接收的内容。就像调试器一样,当您需要查看它时,没有替代品。

线鲨 是一个网络协议分析器和流量捕获应用程序,可在 macOS、Linux 和 Windows 等平台上运行。有一个名为 |_+_| 的 GUI 版本,还有一个名为 |_+_| 的基于文本的终端版本。

运行流量捕获是观察应用程序在网络上的行为并收集有关其发送和接收内容以及频率和数量的证据的好方法。您还可以看到客户端或服务器何时关闭或中止连接或停止响应。当您进行故障排除时,此信息可能非常有用。

网络上有许多很好的教程和其他资源,可以引导您了解使用 Wireshark 和 TShark 的基础知识。

以下是在环回接口上使用 Wireshark 进行流量捕获的示例:

Wireshark 截图

这是上面使用 |_+_| 显示的相同示例:

echo-server.py参考

本节作为一般参考,提供附加信息和外部资源链接。

Python 文档 Python的 插座模块 Python的 套接字编程HOWTO 错误

以下来自Python的|_+_|模块文档:

所有错误都会引发异常。可以引发无效参数类型和内存不足情况的正常异常;从 Python 3.3 开始,与套接字或地址语义相关的错误引发 |_+_|或其子类之一。 (来源)

套接字地址族

|_+_|和|_+_|表示用于 |_+_| 的第一个参数的地址和协议族。使用地址的 API 期望它采用某种格式,这取决于套接字是否是用 |_+_| 创建的。或 |_+_|。

请注意下面关于 |_+_| 的 Python 套接字模块文档的摘录地址元组的值:

对于 IPv4 地址,接受两种特殊形式而不是主机地址:空字符串表示 |_+_|,字符串 |_+_|代表|_+_|。此行为与 IPv6 不兼容,因此,如果您打算在 Python 程序中支持 IPv6,则可能需要避免这些行为。 (来源)

参见 Python 的 套接字系列文档 想要查询更多的信息。

我在本教程中使用了 IPv4 套接字,但如果您的网络支持它,请尝试测试并尽可能使用 IPv6。轻松支持这一点的一种方法是使用该功能 socket.getaddrinfo() .它翻译了 |_+_|和|_+_|参数转换为 5 元组序列,其中包含创建连接到该服务的套接字所需的所有参数。 |_+_|除了 IPv4 之外,还将理解和解释传入的 IPv6 地址和解析为 IPv6 地址的主机名。

以下示例将 TCP 连接的地址信息返回到 |_+_|在端口 |_+_| 上:

HOST = ''

如果未启用 IPv6,结果可能会在您的系统上有所不同。上面返回的值可以通过将它们传递给 |_+_| 来使用。和 |_+_|。有一个客户端和服务器示例 示例部分 Python 的套接字模块文档。

使用主机名

对于上下文,本节主要适用于使用带有 |_+_| 的主机名。和 |_+_| 或 |_+_|,当您打算使用环回接口 localhost 时。但是,它适用于您使用主机名的任何时候,并且期望它解析到某个地址并对您的应用程序具有特殊意义,从而影响其行为或假设。这与客户端使用主机名连接到由 DNS 解析的服务器的典型场景形成对比,例如 www.example.com .

以下来自Python的|_+_|模块文档:

如果在 IPv4/v6 套接字地址的主机部分使用主机名,程序可能会显示不确定性行为,因为 Python 使用从 DNS 解析返回的第一个地址。套接字地址将以不同的方式解析为实际的 IPv4/v6 地址,具体取决于 DNS 解析和/或主机配置的结果。对于确定性行为,请在主机部分使用数字地址。 (来源)

名称的标准约定 本地主机 是让它解析为|_+_|或|_+_|,环回接口。对于您的系统来说,这很可能是这种情况,但也可能不是。这取决于您的系统是如何配置名称解析的。与 IT 的所有事物一样,总有例外,并且不能保证使用名称 localhost 将连接到环回接口。

例如,在 Linux 上,请参阅 |_+_|,名称服务开关配置文件。另一个检查 macOS 和 Linux 的地方是文件 |_+_|。在 Windows 上,参见 |_+_|。 |_+_|文件包含一个静态名称表,以简单文本格式寻址映射。 域名系统 完全是另一块拼图。

有趣的是,在撰写本文时(2018 年 6 月),有一个 RFC 草案 让“本地主机”是本地主机 讨论了使用名称 localhost 的约定、假设和安全性。

重要的是要了解,当您在应用程序中使用主机名时,返回的地址实际上可以是任何内容。如果您有一个对安全敏感的应用程序,请不要对名称进行假设。根据您的应用程序和环境,这可能是您的问题,也可能不是您的问题。

笔记: 即使您的应用程序不是安全敏感的,安全预防措施和最佳实践仍然适用。如果您的应用程序访问网络,则应该对其进行保护和维护。这意味着,至少:

定期应用系统软件更新和安全补丁,包括 Python。您是否使用任何第三方库?如果是这样,请确保也检查和更新这些内容。

如果可能,请使用专用或基于主机的防火墙来限制仅与受信任系统的连接。

配置了哪些 DNS 服务器?你信任他们和他们的管理员吗?

在调用处理它的其他代码之前,请确保尽可能多地清理和验证请求数据。为此使用(模糊)测试并定期运行它们。

无论您是否使用主机名,如果您的应用程序需要支持安全连接(加密和身份验证),您可能需要考虑使用 TLS .这是它自己单独的主题,超出了本教程的范围。参见 Python 的 ssl 模块文档 开始。这与您的 Web 浏览器用于安全连接到网站的协议相同。

考虑到接口、IP 地址和名称解析,有很多变量。你该怎么办?如果您没有网络应用程序审查流程,可以使用以下一些建议:

对于客户端或服务器,如果您需要对要连接的主机进行身份验证,请考虑使用 TLS。

阻止呼叫

临时挂起应用程序的套接字函数或方法是阻塞调用。例如,|_+_|、|_+_|、|_+_| 和|_+_|堵塞。他们不会立即返回。阻塞调用必须等待系统调用 (I/O) 完成才能返回值。所以你,调用者,在他们完成或超时或其他错误发生之前被阻止。

阻塞套接字调用可以设置为非阻塞模式,以便它们立即返回。如果你这样做,你至少需要重构或重新设计你的应用程序,以便在它准备好时处理套接字操作。

由于调用会立即返回,因此数据可能尚未准备好。被调用者正在网络上等待,还没来得及完成它的工作。如果是这种情况,则当前状态为 |_+_|值 |_+_|。支持非阻塞模式 设置阻塞() .

默认情况下,套接字始终以阻塞模式创建。看 关于套接字超时的注意事项 对三种模式的描述。

关闭连接

使用 TCP 需要注意的一件有趣的事情是,客户端或服务器关闭其一侧的连接而另一侧保持打开状态是完全合法的。这称为半开连接。这是应用程序的决定是否可取。一般来说,不是。在这种状态下,关闭连接端的一侧不能再发送数据。他们只能接受。

我不提倡你采用这种方法,但作为一个例子,HTTP 使用一个名为 Connection 的标头,用于标准化应用程序应该如何关闭或保持打开的连接。有关详细信息,请参阅 RFC 7230 中的第 6.3 节,超文本传输​​协议 (HTTP/1.1):消息语法和路由 .

在设计和编写您的应用程序及其应用层协议时,最好先弄清楚您希望如何关闭连接。有时这很明显也很简单,或者需要一些初始原型设计和测试。这取决于应用程序以及如何使用预期数据处理消息循环。只需确保套接字在完成工作后始终及时关闭即可。

字节序

看 维基百科关于字节序的文章 有关不同 CPU 如何在内存中存储字节顺序的详细信息。在解释单个字节时,这不是问题。但是,当处理作为单个值读取和处理的多个字节时,例如 4 字节整数,如果您正在与使用不同字节序的机器通信,则需要颠倒字节顺序。

字节顺序对于表示为多字节序列(如 Unicode)的文本字符串也很重要。除非你总是使用 true、strict ASCII码 并控制客户端和服务器的实现,你可能最好使用 Unicode 和像 UTF-8 这样的编码或支持 字节顺序标记 (BOM) .

明确定义应用层协议中使用的编码很重要。您可以通过强制所有文本为 UTF-8 或使用指定编码的内容编码标头来实现此目的。这可以防止您的应用程序检测编码,如果可能,您应该避免这种情况。

当涉及的数据存储在文件或数据库中并且没有可用的元数据来指定其编码时,这就会出现问题。当数据传输到另一个端点时,它必须尝试检测编码。有关讨论,请参阅 维基百科的 Unicode 文章 那个引用 RFC 3629:UTF-8,ISO 10646 的一种转换格式 :

然而,UTF-8 标准 RFC 3629 建议在使用 UTF-8 的协议中禁止字节顺序标记,但讨论了可能无法实现的情况。此外,对 UTF-8 中可能的模式的巨大限制(例如,不能有任何具有高位集的单独字节)意味着应该可以在不依赖 BOM 的情况下将 UTF-8 与其他字符编码区分开来。 (来源)

这样做的好处是始终存储用于应用程序处理的数据的编码(如果它可能会有所不同)。换句话说,如果编码不总是 UTF-8 或其他带有 BOM 的编码,请尝试以某种方式将编码存储为元数据。然后,您可以将该编码与数据一起发送到标头中,以告诉接收器它是什么。

TCP/IP 中使用的字节顺序是 大端 并称为网络秩序。网络顺序用于表示协议栈较低层中的整数,如 IP 地址和端口号。 Python 的 socket 模块包括将整数与网络和主机字节顺序相互转换的函数:

您还可以使用 结构模块 使用格式字符串打包和解包二进制数据:

HOST = '127.0.0.1'结论

我们在本教程中涵盖了很多内容。网络和套接字是大主题。如果您不熟悉网络或套接字,请不要因所有术语和首字母缩略词而气馁。

为了理解一切如何协同工作,有很多部分需要熟悉。但是,就像 Python 一样,当您了解各个部分并花更多时间使用它们时,它会开始变得更有意义。

我们查看了 Python 的 |_+_| 中的低级套接字 API模块,并了解如何使用它来创建客户端-服务器应用程序。我们还创建了自己的自定义类,并将其用作应用层协议以在端点之间交换消息和数据。您可以使用这个类并以此为基础来学习和帮助更轻松、更快速地创建自己的套接字应用程序。

你可以找到 GitHub 上的源代码 .

恭喜你走到最后!您现在可以在自己的应用程序中使用套接字了。

我希望本教程为您提供了开始套接字开发之旅所需的信息、示例和灵感。

#python #web-development #machine-learning

www.youtube.comPython Socket 编程教程

本指南将向您展示如何使用 Python 3 套接字将多个客户端连接到服务器。了解如何使用 Python 构建套接字服务器和客户端。它涵盖了如何从客户端向服务器以及从服务器向客户端发送消息。我还将向您展示如何在本地或全球范围内通过 Internet 托管您的套接字服务器,以便任何人都可以连接。这使用了 python 3 套接字和线程模块。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3