新闻  |   论坛  |   博客  |   在线研讨会
Python 套接字编程完整指南
ygtu | 2023-09-06 14:25:09    阅读:53   发布文章

推荐:使用NSDT场景编辑器快速搭建3D应用场景

连接设备以交换信息是网络的全部意义所在。套接字是有效网络通信的重要组成部分,因为它们是用于通过本地或全球网络以及同一台计算机上的不同进程在设备之间传输消息的基本概念。它们提供了一个低级接口,允许对要发送或接收的流量进行细粒度控制。

这种低级特性使得可以为特定用例创建性能非常高的通信通道(或自定义协议),这些用例可能存在于传统协议中,这些协议建立在套接字通信之上。

这就是套接字在依赖即时消息交换或处理大量数据的实时客户端-服务器应用程序中非常有用的原因。

在本文中,我们将介绍套接字编程的基础知识,并提供使用 Python 创建基于套接字的客户端和服务器应用程序的分步指南。因此,事不宜迟,让我们直接潜入!

网络基础知识

网络支持任何类型的通信和信息共享。

这是连接两个或多个设备以允许它们交换信息的过程。此类互连设备的集合称为网络。

我们可以在物理世界中观察到许多网络:航空公司或电力线网络或通过高速公路相互连接的城市就是一些很好的例子。

同样,信息技术领域有许多网络;其中最突出和最著名的是互联网,这是连接无数设备的全球网络网络,也是您现在可能正在使用的阅读本文的网络。

网络类型

互联网包含更多的网络,这些网络本身因规模或其他属性而异:例如,局域网 (LAN),它们通常将彼此靠近的计算机链接起来。公司或其他机构(银行、大学等)中的机器,甚至连接到路由器的家庭设备都构成了这样的网络。

还有更大或更小类型的网络,如PAN(个人局域网),可以简单地将您的智能手机通过蓝牙连接到笔记本电脑,MAN(城域网),可以互连整个城市的设备,以及WAN(广域网),可以覆盖整个国家或整个世界。是的,最大的WAN网络是互联网本身。

不言而喻,计算机网络可能非常复杂,由许多元素组成。最基本和最关键的原语之一是通信协议。

网络通信协议的类型

通信协议规定了如何以及以何种格式发送和接收信息的规则。这些协议被组装到一个层次结构中,以管理网络通信中涉及的各种任务。

换句话说,一些协议处理硬件接收、发送或路由数据包的方式,而其他协议则更高级别,例如,关注应用程序级通信等。

一些常用和广为人知的网络通信协议包括:

无线网络

链路层协议的示例,这意味着它非常靠近硬件,并负责在无线环境中将数据从一台设备物理发送到另一台设备。

IP(互联网协议)

IP 是一种网络层协议,主要负责路由数据包和 IP 寻址。

TCP(传输控制协议)

一种可靠的、面向连接的协议,可提供全双工通信并确保数据完整性和交付。这是一种传输层协议,用于管理连接、检测错误和控制信息流。

UDP(用户数据报协议)

与 TCP 来自同一协议套件的协议。主要区别在于 UDP 是一种更简单、更快速但不可靠的无连接协议,它不执行任何传递检查,并遵循“即发即弃”的范式。作为TCP,UPD也位于传输层。

HTTP(超文本传输协议)

一种应用层协议,也是网络上浏览器到服务器通信最常用的协议,特别用于为网站提供服务。不用说,您现在正在阅读的这篇文章也是通过HTTP提供的。HTTP协议建立在TCP之上,管理和传输与Web应用程序相关的信息,如用于传输元数据和cookie的标头,不同的HTTP方法(GET,POST,DELETE,UPDATE)等。

MQTT(消息队列遥测传输)

另一个应用程序级协议示例,用于处理能力和电池寿命有限的设备,在不可靠的网络条件下运行(例如,采矿现场的气体传感器或您家中的智能灯泡)。MQTT是IoT(物联网)中使用的标准消息传递协议。它既轻巧又易于使用,设计有内置的重传机制,可增强可靠性。如果您有兴趣将此协议与 Python 一起使用,可以阅读此 Python MQTT 指南,其中提供了 Paho MQTT 客户端的深入概述。

一个重要的观察结果是,上述所有协议都在引擎盖下使用套接字,但在顶部添加了自己的逻辑和数据处理。这是由于套接字是现代设备中任何网络通信的低级接口,我们将在下一节中讨论。

关键概念和术语

当然,在网络环境中还使用了许多其他重要概念和术语。以下是本教程其余部分可能出现的一些最突出问题的快速概述:

  • 数据包:计算机网络中数据传输的标准单位(可以通俗地将其与术语“消息”进行比较)。

  • 终结点:数据包到达的目标。

  • IP 地址:唯一标识网络上设备的数字标识符。IP 地址的示例为:192.168.0.0

  • 端口:一个数字标识符,用于唯一标识设备上运行的进程并处理特定的网络通信:例如,它通过HTTP为您的网站提供服务。IP 地址标识设备,端口标识应用程序(每个应用程序都是一个进程或由进程组成)。一些众所周知的端口示例是:端口 80,服务器应用程序通常使用它来管理 HTTP 流量,以及端口 443 用于 HTTPS(安全 HTTP)。

  • 网关:一种特殊的网络节点(设备),用作从一个网络到另一个网络的接入点。这些网络甚至可能使用不同的协议,因此网关可能需要执行一些协议转换。网关的一个例子可以是将家庭本地网络连接到互联网的路由器。

了解套接字什么是套接字?

套接字是位于相同或不同机器上的不同进程之间进行通信的接口(门)。在后一种情况下,我们谈论网络套接字。

网络套接字抽象出连接管理。您可以将它们视为连接处理程序。特别是在Unix系统中,套接字只是支持相同写读操作但通过网络发送所有数据的文件。

当套接字处于侦听或连接状态时,它始终绑定到 IP 地址和标识主机(计算机/设备)和进程的端口号的组合。

套接字连接的工作原理

套接字可以侦听传入连接或自行执行出站连接。建立连接后,侦听套接字(服务器套接字)将额外绑定到连接端的 IP 和端口。

或者,创建一个新的套接字,该套接字现在绑定到侦听器和请求者的两对 IP 地址和端口号。这样,不同计算机上的两个连接的套接字可以相互识别并共享单个连接以进行数据传输,而不会阻塞侦听套接字,同时侦听套接字会继续侦听其他连接。

对于连接套接字(客户端套接字),它会在连接启动时隐式绑定到设备的 IP 地址和随机可访问的端口号。然后,在建立连接时,与另一通信端的 IP 和端口的绑定方式与侦听套接字大致相同,但不创建新套接字。

网络上下文中的套接字

在本教程中,我们关注的不是套接字实现,而是套接字在网络上下文中的含义。

可以说套接字是一个连接端点(流量目的地),它一方面与主机的 IP 地址和为其创建套接字的应用程序的端口号相关联,另一方面,它与在建立连接的另一台计算机上运行的应用程序的 IP 地址和端口相关联。

套接字编程

当我们谈论套接字编程时,我们在代码中实例化套接字对象并对其执行操作(侦听、连接、接收、发送等)。在这种情况下,套接字只是我们在程序中创建的特殊对象,它们具有处理网络连接和流量的特殊方法。

在后台,这些方法调用操作系统内核,或者更具体地说,调用网络堆栈,网络堆栈是内核中负责管理网络操作的特殊部分。

套接字和客户端-服务器通信

现在,同样重要的是要提到套接字经常出现在客户端-服务器通信的上下文中。

这个想法很简单:套接字与连接有关;它们是连接处理程序。在 Web 上,每当您想要发送或接收一些数据时,您都会启动一个连接(通过称为套接字的接口启动)。

现在,您或您尝试连接的一方充当服务器,另一方充当客户端。当服务器向客户端提供数据时,客户端会主动连接并从服务器请求数据。服务器通过侦听套接字侦听新连接,建立新连接,获取客户端的请求,并在响应中将请求的数据传达给客户端。

另一方面,客户端使用它希望连接的服务器的 IP 地址和端口创建套接字,启动连接,将其请求传达给服务器,并接收数据作为响应。客户端和服务器套接字之间的这种无缝信息交换构成了各种网络应用程序的骨干。

套接字作为网络协议的基础

套接字形成骨干的事实也意味着在其上构建和使用各种协议。非常常见的是UDP和TCP,我们已经简要讨论过了。使用这些传输协议之一的套接字称为 UDP 或 TCP 套接字。

工控机插座

除了网络套接字,还有其他类型。例如,IPC(进程间通信)套接字。IPC 套接字用于在同一台计算机上的进程之间传输数据,而网络套接字可以在网络上执行相同的操作。

IPC 套接字的好处是它们避免了构建数据包和解析发送数据的路由的大量开销。由于在IPC发送方和接收方的上下文中是本地进程,因此通过IPC套接字进行通信通常具有较低的延迟。

Unix-sockets

IPC套接字的一个很好的例子是Unix套接字,与Unix中的所有内容一样,它只是文件系统上的文件。它们不是由 IP 地址和端口标识,而是由文件系统上的文件路径标识。

网络套接字作为 IPC 套接字

请注意,如果服务器和接收方都在本地主机上(即具有 IP 地址 127.0.0.1),您也可以使用网络套接字进行进程间通信。

当然,一方面,由于与网络堆栈处理数据相关的开销,这会增加额外的延迟,但另一方面,这使我们能够不必担心底层操作系统,因为网络套接字存在并在所有系统上工作,而不是特定于给定操作系统或操作系统系列的 IPC 套接字。

Python 套接字库

对于 Python 中的套接字编程,我们使用官方内置的 Python 套接字库,该库由用于创建、管理和使用套接字的函数、常量和类组成。此库的一些常用功能包括:

  • socket():创建一个新套接字。

  • bind():将套接字关联到特定地址和端口。

  • listen():开始侦听套接字上的传入连接。

  • accept():接受来自客户端的连接并返回用于通信的新套接字。

  • connect():建立与远程服务器的连接。

  • send():通过套接字发送数据。

  • recv():从套接字接收数据。

  • close():关闭套接字连接。

Python 套接字示例

让我们通过一个用 Python 编写的实际示例来了解套接字编程。在这里,我们的目标是连接两个应用程序并使它们相互通信。我们将使用 Python 套接字库创建一个服务器套接字应用程序,该应用程序将通过网络与客户端进行通信和交换信息。

注意事项和限制

但是请注意,出于教育目的,我们的示例进行了简化,应用程序将在本地运行,而不是通过实际网络进行通信 - 我们将使用环回本地主机地址将客户端连接到服务器。

这意味着客户端和服务器将在同一台计算机上运行,并且客户端将启动与运行它的同一台计算机的连接,尽管连接到表示服务器的不同进程。

在不同的计算机上运行

或者,您可以将应用程序放在两个不同的设备上,并将它们都连接到同一个Wi-Fi路由器,这将形成局域网。然后,在一台设备上运行的客户端可以连接到在另一台计算机上运行的服务器。

但是,在这种情况下,您需要知道路由器分配给设备的IP地址,并使用它们而不是本地主机(127.0.0.1)环回IP地址(要查看IP地址,请使用终端命令用于类Unix系统或-对于Windows)。获取应用程序的 IP 地址后,可以在代码中相应地更改它们,该示例仍然有效。ifconfigipconfig

无论如何,我们将从我们的例子开始。当然,如果你想跟上去,你需要安装Python

在 Python 中创建套接字服务器

让我们从创建一个套接字服务器(特别是Python TCP服务器,因为它将与TCP套接字一起工作,正如我们将看到的),它将与客户端交换消息。为了澄清术语,虽然从技术上讲,任何服务器都是套接字服务器,但由于套接字总是在后台使用来启动网络连接,因此我们使用短语“套接字服务器”,因为我们的示例明确使用了套接字编程。

因此,请按照以下步骤操作:

使用一些样板创建 python 文件
  • 创建一个名为server.py

  • 在 Python 脚本中导入模块。socket

  • 添加一个名为 的函数。我们将在那里添加大部分代码。将代码添加到函数时,不要忘记正确缩进它:run_server

实例化套接字对象

下一步,在 中使用该函数创建一个套接字对象。run_serversocket.socket()

第一个参数 () 指定 IPv4 的 IP 地址系列(其他选项包括:IPv6 系列和 Unix 套接字)socket.AF_INETAF_INET6AF_UNIX

第二个参数(表示我们正在使用TCP套接字。socket.SOCK_STREAM)

在使用TCP的情况下,操作系统将通过顺序数据传输,错误发现和重新传输以及流量控制创建可靠的连接。您不必考虑实现所有这些细节。

还有一个用于指定 UDP 套接字的选项:。这将创建一个套接字,该套接字在后台实现UDP的所有功能。socket.SOCK_DGRAM

如果你想更底层,并在套接字使用的TCP/IP网络层协议之上构建自己的传输层协议,你可以使用value作为第二个参数。在这种情况下,操作系统将不会为您处理任何更高级别的协议功能,如果需要,您必须自己实现所有标头、连接确认和重传功能。您还可以在文档中阅读其他值。socket.RAW_SOCKET

将服务器套接字绑定到 IP 地址和端口

定义主机名或服务器 IP 和端口,以指示服务器可从何处访问以及侦听传入连接的位置。在此示例中,服务器正在侦听本地计算机 - 这是由设置为 (也称为 localhost) 的变量定义的。server_ip127.0.0.1

该变量设置为 ,这是操作系统将标识服务器应用程序的端口号(建议对端口号使用大于 1023 的值,以避免与系统进程使用的端口发生冲突)。port8000

通过将套接字绑定到我们之前定义的 IP 地址和端口来准备套接字以接收连接。

侦听传入连接

使用该函数在服务器套接字中设置侦听状态,以便能够接收传入的客户端连接。listen

此函数接受调用的参数,该参数指定排队的未接受连接的最大数量。在此示例中,我们使用此参数的值。这意味着只有一个客户端可以与服务器交互。在服务器使用另一个客户端时执行的任何客户端的连接尝试都将被拒绝。backlog0

如果指定的值大于 ,例如,它会告诉操作系统在对客户端调用方法之前可以放入队列中的客户端数。01accept

调用一次,客户端将从队列中删除,不再计入此限制。一旦您看到代码的更多部分,这可能会变得更加清晰,但此参数本质上的作用可以说明如下:一旦您的侦听服务器收到连接请求,它将将此客户端添加到队列中并继续接受它的请求。如果在服务器能够在第一个客户端上进行内部调用之前,它收到来自第二个客户端的连接请求,则只要其中有足够的空间,它将把第二个客户端推送到同一队列。此队列的大小由积压工作参数控制。一旦服务器接受第一个客户端,该客户端就会从队列中删除,服务器开始与其通信。第二个客户端仍留在队列中,等待服务器获得空闲并接受连接。acceptaccept

如果省略 backlog 参数,它将设置为系统的默认值(在 Unix 下,您通常可以在文件中查看此默认值)。/proc/sys/net/core/somaxconn

接受传入连接

接下来,等待并接受传入的客户端连接。该方法将停止执行线程,直到客户端连接。然后它返回一个元组对,其中地址是客户端 IP 地址和端口的元组,并且是与客户端共享连接并可用于与其通信的新套接字对象。accept(conn, address)conn

accept创建一个新的套接字来与客户端通信,而不是将侦听套接字(在我们的示例中称为)绑定到客户端的地址并将其用于通信,因为侦听套接字需要侦听来自其他客户端的进一步连接,否则它将被阻止。当然,在我们的例子中,我们只处理一个客户端并在这样做时拒绝所有其他连接,但是一旦我们进入多线程服务器示例,这将更加相关。server

创建通信循环

一旦与客户端建立了连接(在调用方法之后),我们就会启动一个无限循环进行通信。在此循环中,我们对对象的方法执行调用。此方法从客户端接收指定数量的字节 - 在我们的例子中为 1024。acceptrecvclient_socket

1024 字节只是有效负载大小的常见约定,因为它是 <> 的幂,可能比其他任意值更适合优化目的。您可以随意更改此值。

由于从客户端接收到变量中的数据是原始二进制形式,因此我们使用该函数将其从字节序列转换为字符串。requestdecode

然后我们有一个 if 语句,如果我们收到消息,它会脱离通信循环。这意味着一旦我们的服务器在请求中收到字符串,它就会将确认发送回客户端并终止与客户端的连接。否则,我们将收到的消息打印到控制台。在我们的例子中,确认只是向客户端发送一个字符串。”close””close””closed”

请注意,我们在 if 语句中的字符串上使用的方法只是将其转换为小写。这样,我们就不在乎字符串最初是使用大写还是小写字符编写的。lowerrequestclose

将响应发送回客户端

现在我们应该处理服务器对客户端的正常响应(即当客户端不希望关闭连接时)。在 while 循环中,紧接着 ,添加以下行,这会将响应字符串(在我们的例子中)转换为字节并将其发送到客户端。这样,每当服务器收到来自客户端的消息时,它将发送字符串作为响应:print(f"Received: {request}")”accepted””close””accepted”

释放资源

一旦我们脱离了无限 while 循环,与客户端的通信就完成了,所以我们使用释放系统资源的方法关闭客户端套接字。我们还使用相同的方法关闭服务器套接字,这有效地关闭了我们的服务器。在现实世界中,我们当然可能希望我们的服务器继续侦听其他客户端,而不是在与单个客户端通信后关闭,但别担心,我们将在下面进一步介绍另一个示例。close

现在,在无限 while 循环之后添加以下行:

注意:不要忘记在文件末尾调用该函数。只需使用以下代码行:run_serverserver.py

完整的服务器套接字代码示例

以下是完整的源代码:server.py

import socket

def run_server():
# create a socket object
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

server_ip = "127.0.0.1"
port = 8000

# bind the socket to a specific address and port
server.bind((server_ip, port))
# listen for incoming connections
server.listen(0)
print(f"Listening on {server_ip}:{port}")

# accept incoming connections
client_socket, client_address = server.accept()
print(f"Accepted connection from {client_address[0]}:{client_address[1]}")

# receive data from the client
while True:
    request = client_socket.recv(1024)
    request = request.decode("utf-8") # convert bytes to string
    
    # if we receive "close" from the client, then we break
    # out of the loop and close the conneciton
    if request.lower() == "close":
        # send response to the client which acknowledges that the
        # connection should be closed and break out of the loop
        client_socket.send("closed".encode("utf-8"))
        break

    print(f"Received: {request}")

    response = "accepted".encode("utf-8") # convert string to bytes
    # convert and send accept response to the client
    client_socket.send(response)

# close connection socket with the client
client_socket.close()
print("Connection to client closed")
# close server socket
server.close()

run_server()

请注意,为了不使这个基本示例复杂化和复杂化,我们省略了错误处理。您当然希望添加 try-except 块,并确保始终关闭子句中的套接字。继续阅读,我们将看到一个更高级的示例。finally

在 Python 中创建客户端套接字

设置服务器后,下一步是设置一个客户端,该客户端将连接并向服务器发送请求。因此,让我们从以下步骤开始:

使用一些样板创建 python 文件
  • 创建一个名为client.py

  • 导入套接字库:

import socket

  • 定义我们将放置所有代码的函数:run_client

def run_client():
# your code will go here

实例化套接字对象

接下来,使用该函数创建一个 TCP 套接字对象,该对象充当客户端与服务器的联系点。socket.socket()

// create a socket object
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

连接到服务器套接字

指定服务器的 IP 地址和端口以便能够连接到它。这些地址和端口应与您之前设置的 IP 地址和端口匹配。server.py

server_ip = "127.0.0.1"  # replace with the server's IP address
server_port = 8000  # replace with the server's port number

使用客户端套接字对象上的方法与服务器建立连接。请注意,我们没有将客户端套接字绑定到任何 IP 地址或端口。这对客户端来说是正常的,因为将自动选择一个空闲端口并选取一个 IP 地址,该地址从系统的网络接口(在我们的例子中)提供到服务器的最佳路由,并将客户端套接字绑定到这些接口。connectconnect127.0.0.1

# establish connection with server
client.connect((server_ip, server_port))
创建通信循环

建立连接后,我们开始无限通信循环以向服务器发送多条消息。我们使用 Python 的内置函数从用户那里获取输入,然后将其编码为字节并修剪为最大 1024 字节。之后,我们使用.inputclient.send

while True:
# input message and send it to the server
msg = input("Enter message: ")
client.send(msg.encode("utf-8")[:1024])

处理服务器的响应

一旦服务器收到来自客户端的消息,它就会响应它。现在,在我们的客户端代码中,我们希望接收服务器的响应。为此,在通信循环中,我们使用该方法最多读取 1024 字节。然后我们使用将字节的响应转换为字符串,然后检查它是否等于值。如果是这种情况,我们将脱离循环,正如我们稍后看到的,这将终止客户端的连接。否则,我们将服务器的响应打印到控制台中。recvdecode”closed”

   # receive message from the server
    response = client.recv(1024)
    response = response.decode("utf-8")

    # if server sent us "closed" in the payload, we break out of the loop and close our socket
    if response.lower() == "closed":
        break

    print(f"Received: {response}")
释放资源

最后,在 while 循环之后,使用该方法关闭客户端套接字连接。这可确保正确释放资源并终止连接(即当我们收到消息并脱离 while 循环时)。close“closed”

// close client socket (connection to the server)
client.close()
print("Connection to server closed")

注意:同样,不要忘记在文件末尾调用我们上面实现的函数,如下所示:run_client

run_client()

完整的客户端套接字代码示例

以下是完整的代码:client.py

import socket

def run_client():
# create a socket object
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

server_ip = "127.0.0.1"  # replace with the server's IP address
server_port = 8000  # replace with the server's port number
# establish connection with server
client.connect((server_ip, server_port))

while True:
    # input message and send it to the server
    msg = input("Enter message: ")
    client.send(msg.encode("utf-8")[:1024])

    # receive message from the server
    response = client.recv(1024)
    response = response.decode("utf-8")

    # if server sent us "closed" in the payload, we break out of the loop and close our socket
    if response.lower() == "closed":
        break

    print(f"Received: {response}")

# close client socket (connection to the server)
client.close()
print("Connection to server closed")

run_client()

测试客户端和服务器

要测试我们上面编写的服务器和客户端实现,请执行以下操作:

  • 同时打开两个终端窗口。

  • 在一个终端窗口中,导航到文件所在的目录,然后运行以下命令以启动服务器:server.py

python server.py

这会将服务器套接字绑定到端口 127 上的本地主机地址 (0.0.1.8000),并开始侦听传入连接。

  • 在另一个终端中,导航到文件所在的目录并运行以下命令以启动客户端:client.py

python client.py

这将提示用户输入。然后,您可以键入消息并按 Enter 键。这会将您的输入传输到服务器并在其终端窗口中显示。服务器将向客户端发送响应,后者将再次要求您输入。这将一直持续到您将字符串发送到服务器。”close”

使用多个客户端 – 多线程

我们已经在前面的示例中看到了服务器如何响应来自单个客户端的请求,但是,在许多实际情况中,许多客户端可能需要同时连接到单个服务器。这就是多线程的用武之地。多线程用于需要同时(同时)处理多个任务(例如执行多个功能)的情况。

这个想法是生成一个线程,该线程是一组独立的指令,可以由处理器处理。线程比进程轻量级得多,因为它们实际上存在于进程本身中,您不必为自己分配大量资源。

python中多线程的局限性

请注意,Python 中的多线程是有限的。标准的Python实现(CPython)不能真正并行运行线程。由于全局解释器锁 (GIL),一次只允许执行一个线程。但是,这是一个单独的主题,我们不打算讨论。为了我们的示例,使用有限的CPython线程就足够了,并且可以理解这一点。但是,在实际场景中,如果您要使用Python,则应研究异步编程。我们现在不打算讨论它,因为它又是一个单独的主题,它通常会抽象出一些我们在本文中特别关注的低级套接字操作。

多线程服务器示例

让我们看一下下面的示例,了解如何将多线程添加到服务器中以处理大量客户端。请注意,这次我们还将使用 try-except-finally 块添加一些基本的错误处理。要开始使用,请按照以下步骤操作:

创建线程生成服务器函数

在您的 python 文件中,导入 and 模块以便能够同时使用套接字和线程:

定义函数,如上例所示,创建一个服务器套接字,绑定它并侦听传入的连接。然后调用无限 while 循环。这将始终侦听新的连接。获取传入连接并返回后,使用构造函数创建一个线程。该线程将执行我们稍后将要定义的函数,并作为参数传递给它(元组包含所连接客户端的 IP 地址和端口)。创建线程后,我们调用它开始执行。run_serveracceptacceptthreading.Threadhandle_clientclient_socketaddraddrstart

请记住,调用是阻塞的,因此在 while 循环的第一次迭代中,当我们到达 的行时,我们停止并等待客户端连接而不执行任何其他操作。一旦客户端连接,方法就会返回,我们继续执行:生成一个线程,该线程将处理所述客户端并转到下一次迭代,我们将再次在调用处停止等待另一个客户端连接。acceptacceptacceptaccept

在函数结束时,我们有一些错误处理,以确保服务器套接字始终关闭,以防发生意外情况。

def run_server():
server_ip = "127.0.0.1" # server hostname or IP address
port = 8000 # server port number
# create a socket object
try:
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# bind the socket to the host and port
server.bind((server_ip, port))
# listen for incoming connections
server.listen()
print(f"Listening on {server_ip}:{port}")

    while True:
        # accept a client connection
        client_socket, addr = server.accept()
        print(f"Accepted connection from {addr[0]}:{addr[1]}")
        # start a new thread to handle the client
        thread = threading.Thread(target=handle_client, args=(client_socket, addr,))
        thread.start()
except Exception as e:
    print(f"Error: {e}")
finally:
    server.close()

请注意,我们示例中的服务器仅在发生意外错误时才会停止。否则,它将无限期地侦听客户端,如果要停止它,则必须杀死终端。

创建客户端处理函数以在单独的线程中运行

现在,在函数上方,定义另一个名为 .此函数将是在每个客户端连接的单独线程中执行的函数。它接收客户端的套接字对象和元组作为参数。run_serverhandle_clientaddr

在这个函数中,我们执行与单线程示例中相同的操作以及一些错误处理:我们启动一个循环以使用 .recv

然后我们检查我们是否收到关闭的消息。如果是这样,我们使用字符串响应并通过中断循环来关闭连接。否则,我们将客户端的请求字符串打印到控制台中,然后继续进行下一个循环迭代以接收下一个客户端的消息。”closed”

在这个函数的末尾,我们有一些针对意外情况的错误处理(子句),还有一个使用.无论如何,此子句将始终执行,这可确保始终正确释放客户端套接字。

def handle_client(client_socket, addr):
try:
while True:
# receive and print client messages
request = client_socket.recv(1024).decode("utf-8")
if request.lower() == "close":
client_socket.send("closed".encode("utf-8"))
break
print(f"Received: {request}")
# convert and send accept response to the client
response = "accepted"
client_socket.send(response.encode("utf-8"))
except Exception as e:
print(f"Error when hanlding client: {e}")
finally:
client_socket.close()
print(f"Connection to client ({addr[0]}:{addr[1]}) closed")

当返回时,执行它的线程也将自动释放。handle_client

注意:不要忘记在文件末尾调用该函数。run_server

完整的多线程服务器代码示例

现在,让我们把完整的多线程服务器代码放在一起:

import socket
import threading

def handle_client(client_socket, addr):
try:
while True:
# receive and print client messages
request = client_socket.recv(1024).decode("utf-8")
if request.lower() == "close":
client_socket.send("closed".encode("utf-8"))
break
print(f"Received: {request}")
# convert and send accept response to the client
response = "accepted"
client_socket.send(response.encode("utf-8"))
except Exception as e:
print(f"Error when hanlding client: {e}")
finally:
client_socket.close()
print(f"Connection to client ({addr[0]}:{addr[1]}) closed")

def run_server():
server_ip = "127.0.0.1" # server hostname or IP address
port = 8000 # server port number
# create a socket object
try:
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# bind the socket to the host and port
server.bind((server_ip, port))
# listen for incoming connections
server.listen()
print(f"Listening on {server_ip}:{port}")

    while True:
        # accept a client connection
        client_socket, addr = server.accept()
        print(f"Accepted connection from {addr[0]}:{addr[1]}")
        # start a new thread to handle the client
        thread = threading.Thread(target=handle_client, args=(client_socket, addr,))
        thread.start()
except Exception as e:
    print(f"Error: {e}")
finally:
    server.close()

run_server()

注意:在实际代码中,为了防止在处理多线程服务器时出现争用情况或数据不一致等可能的问题,必须考虑线程安全和同步技术。然而,在我们的简单示例中,这不是问题。

具有基本错误处理的客户端示例

现在我们有一个能够同时处理多个客户端的服务器实现,我们可以使用上面第一个基本示例中所示的相同客户端实现来启动连接,或者我们可以稍微更新它并添加一些错误处理。您可以在下面找到代码,该代码与前面的客户端示例相同,但添加了 try-except 块:

import socket

def run_client():
# create a socket object
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

server_ip = "127.0.0.1"  # replace with the server's IP address
server_port = 8000  # replace with the server's port number
# establish connection with server
client.connect((server_ip, server_port))

try:
    while True:
        # get input message from user and send it to the server
        msg = input("Enter message: ")
        client.send(msg.encode("utf-8")[:1024])

        # receive message from the server
        response = client.recv(1024)
        response = response.decode("utf-8")

        # if server sent us "closed" in the payload, we break out of
        # the loop and close our socket
        if response.lower() == "closed":
            break

        print(f"Received: {response}")
except Exception as e:
    print(f"Error: {e}")
finally:
    # close client socket (connection to the server)
    client.close()
    print("Connection to server closed")

run_client()

测试多线程示例

如果要测试多客户端实现,请为客户端打开多个终端窗口,为服务器打开一个终端窗口。首先使用 启动服务器。之后,使用 .在服务器终端窗口中,您将看到新客户端如何连接到服务器。现在,您可以通过在相应的终端中输入文本来继续从不同的客户端发送消息,所有这些消息都将被处理并打印到服务器端的控制台。python server.pypython client.py

套接字编程在数据科学中的应用

虽然每个网络应用程序都使用操作系统在后台创建的套接字,但有许多系统严重依赖套接字编程,无论是用于某些特殊用例还是为了提高性能。但是套接字编程在数据科学的背景下究竟有什么用呢?嗯,每当需要快速接收或发送大量数据时,它肯定起着有意义的作用。因此,套接字编程主要用于数据收集和实时处理、分布式计算和进程间通信。但是,让我们仔细看看数据科学领域的一些特定应用。

实时数据收集

套接字广泛用于从不同来源收集实时数据以进行进一步处理、转发到数据库或分析管道等。例如,套接字可用于从金融系统或社交媒体API即时接收数据,以供数据科学家进行后续处理。

分布式计算

数据科学家可以使用套接字连接在多台机器上分配大型数据集的处理和计算。套接字编程通常用于Apache Spark和其他分布式计算框架中,用于节点之间的通信。

模型部署

在向用户提供机器学习模型时,可以使用套接字编程,从而即时提供预测和建议。为了促进实时决策,数据科学家可以使用基于套接字的高性能服务器应用程序,这些应用程序接收大量数据,使用经过训练的模型对其进行处理以提供预测,然后将结果快速返回给客户端。

进程间通信 (IPC)

套接字可用于IPC,它允许在同一台机器上运行的不同进程相互通信并交换数据。这在数据科学中很有用,可以在多个进程中分配复杂和资源密集型计算。事实上,Python 的子处理库经常用于此目的:它生成多个进程以利用多个处理器内核,并在执行繁重计算时提高应用程序性能。此类进程之间的通信可以通过IPC套接字实现。

协作与沟通

套接字编程允许数据科学家之间的实时通信和协作。为了促进有效的协作和知识共享,使用了基于套接字的聊天应用程序或协作数据分析平台。

值得一提的是,在上述许多应用程序中,数据科学家可能不会直接参与套接字的使用。他们通常使用库、框架和系统来抽象出套接字编程的所有低级细节。但是,在后台,所有这些解决方案都基于套接字通信并利用套接字编程。

套接字编程挑战和最佳实践

由于套接字是管理连接的低级概念,因此使用套接字的开发人员必须实现所有必需的基础结构,以创建健壮可靠的应用程序。这当然带来了很多挑战。但是,可以遵循一些最佳实践和一般准则来克服这些问题。以下是套接字编程最常遇到的一些问题,以及一些一般提示:

连接管理

一次处理多个连接;管理多个客户端并确保有效处理并发请求肯定具有挑战性且并非易事。它需要仔细的资源管理和协调,以避免瓶颈

最佳实践
  • 使用列表或字典等数据结构跟踪活动连接。或者使用连接池等高级技术,这也有助于提高可伸缩性。

  • 使用线程或异步编程技术同时处理多个客户端连接。

  • 正确关闭连接以释放资源并避免内存泄漏。

错误处理

处理连接失败、超时和数据传输问题等错误至关重要。处理这些错误并向客户端提供适当的反馈可能具有挑战性,尤其是在进行低级套接字编程时。

最佳实践
  • 使用 try-except-finally 块来捕获和处理特定类型的错误。

  • 提供信息丰富的错误消息,并考虑使用日志记录来帮助进行故障排除。

可扩展性和性能

确保最佳性能和最小化延迟是处理大容量数据流或实时应用程序时的关键问题。

最佳实践
  • 通过最大限度地减少不必要的数据处理和网络开销来优化代码以提高性能。

  • 实施缓冲技术以高效处理大型数据传输。

  • 考虑使用负载平衡技术在多个服务器实例之间分发客户端请求。

安全性和身份验证

保护基于套接字的通信并实施适当的身份验证机制可能很困难。确保数据隐私、防止未经授权的访问和防止恶意活动需要仔细考虑和实施安全协议。

最佳实践
  • 利用 SSL/TLS 安全协议,通过加密信息来确保数据传输的安全。

  • 通过实施安全的身份验证方法(如基于令牌的身份验证、公钥加密或用户名/密码)来确保客户端身份。

  • 确保机密数据(如密码或 API 密钥)得到保护和加密,或者理想情况下根本不存储(如果需要,仅存储其哈希值)。

网络可靠性和弹性

处理网络中断、带宽波动和不可靠的连接可能会带来挑战。保持稳定的连接、优雅地处理断开连接以及实施重新连接机制对于强大的网络应用程序至关重要。

最佳实践
  • 使用保持活动状态消息检测非活动或断开的连接。

  • 实施超时以避免无限期阻塞并确保及时响应处理。

  • 实现指数退避重新连接逻辑,以便在连接丢失时再次建立连接。

代码可维护性

最后但并非最不重要的一点是代码可维护性。由于套接字编程的低级性质,开发人员发现自己编写了更多的代码。这可能会很快变成一个无法维护的意大利面条代码,因此尽早组织和构建它并花费额外的精力来规划代码的体系结构至关重要。

最佳实践
  • 将代码分解为类或函数,理想情况下不应太长。

  • 通过模拟客户端和服务器实现来尽早编写单元测试

  • 请考虑使用更高级的库来处理连接,除非绝对必须使用套接字编程。

总结:Python 中的套接字编程

套接字是所有网络应用程序的组成部分。在本文中,我们研究了 Python 中的套接字编程。以下是要记住的要点:

  • 套接字是抽象连接管理的接口。

  • 套接字支持不同进程(通常是客户端和服务器)之间的通信,或者通过网络进行通信。

  • 在 Python 中,使用套接字是通过库完成的,该库为套接字对象提供了各种方法,如、、、。socketrecvsendlistenclose

  • 套接字编程在数据科学中具有各种有用的应用,包括数据收集、进程间通信和分布式计算。

  • 套接字编程面临的挑战包括连接管理、数据完整性、可伸缩性、错误处理、安全性和代码可维护性。

借助套接字编程技能,开发人员可以创建高效、实时的网络应用程序。通过掌握概念和最佳实践,他们可以充分利用套接字编程的潜力来开发可靠且可扩展的解决方案。

但是,套接字编程是一种非常低级的技术,很难使用,因为应用程序工程师必须考虑应用程序通信的每一个小细节。

如今,我们通常不需要直接使用套接字,因为它们通常由更高级别的库和框架处理,除非需要真正从应用程序中挤出性能或扩展它。

但是,了解套接字并深入了解底层工作原理可以提高开发人员或数据科学家的整体意识,并且始终是一个好主意。

原文链接:Python 套接字编程完整指南 (mvrlink.com)


*博客内容为网友个人发布,仅代表博主个人观点,如有侵权请联系工作人员删除。

参与讨论
登录后参与讨论
推荐文章
最近访客