当前位置:首页>python>Python 并发与 asyncio 3.2 使用 Telnet 连接服务器

Python 并发与 asyncio 3.2 使用 Telnet 连接服务器

  • 2026-10-21 13:27:27
Python 并发与 asyncio 3.2 使用 Telnet 连接服务器

3.2 使用 Telnet 连接服务器

我们前面那个简单的接受连接的例子,让我们无从下手去连接。虽然有很多命令行工具可以读写服务器的数据,但一个存在已久且广受欢迎的应用就是 Telnet。

Telnet 最早于1969年开发,是“电传打字机网络”的缩写。它会建立一个到指定服务器和主机的 TCP 连接。一旦建立连接,就会开启一个终端,我们可以自由地发送和接收字节,所有内容都会在终端上显示出来。

在 Mac OS 上,你可以用 Homebrew 安装 telnet,命令是 brew install telnet(参见 https://brew.sh/(https://brew.sh/) 安装 Homebrew)。在 Linux 发行版上,你需要使用系统包管理器安装(如 apt-get install telnet)。在 Windows 上,建议使用 PuTTY,可以从 https://putty.org(https://putty.org) 下载。

注意 使用 PuTTY 时,为了使本书中的代码示例正常工作,你需要开启本地行编辑功能。在 PuTTY 配置窗口左侧的“终端”部分,将“本地行编辑”设置为“强制开启”。

要连接我们在列表 3.1 中构建的服务器,我们可以在命令行中使用 telnet 命令,并指定连接到本地主机的 8000 端口:

telnet localhost 8000

执行后,你会在终端看到一些输出,告诉你已成功连接。然后,Telnet 会显示一个光标,让你输入内容,并按 [Enter] 发送数据到服务器。

telnet localhost 8000Trying 127.0.0.1...Connected to localhost.Escape character is '^]'.

在你的服务器应用的控制台输出中,你应该能看到类似下面的信息,表明我们已经成功与 Telnet 客户端建立连接:

I got a connection from ('127.0.0.1', 56526)!

你还会看到一条 Connection closed by foreign host 消息,这是服务器代码退出时发出的,表示服务器已关闭与客户端的连接。现在,我们终于有了连接服务器并读写字节的方法,但目前的服务器还不能自己读或发送任何数据。我们可以通过客户端套接字的 sendall 和 recv 方法来实现这一点。

3.2.1 读写套接字中的数据

现在,我们已经创建了一个能接受连接的服务器,接下来看看如何从连接中读取数据。socket 类有一个名为 recv 的方法,我们可以用它从特定套接字获取数据。这个方法接收一个整数,表示我们希望一次读取多少字节。这一点很重要,因为我们不能一次性读取套接字中的所有数据;我们需要缓冲,直到读到输入的末尾。

在这个例子中,我们将输入结束标记为回车加换行符,即 '\r\n'。这正是用户在 Telnet 中按 [Enter] 时附加的内容。为了演示缓冲区在小消息上的工作方式,我们故意设置了较小的缓冲区大小。在实际应用中,我们会使用更大的缓冲区,比如 1024 字节。通常我们希望使用较大的缓冲区,因为这能利用操作系统层面的缓冲,效率远高于在应用层手动实现。

列表 3.2 从套接字读取数据

import socketserver_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)server_address = ('127.0.0.1', 8000)server_socket.bind(server_address)server_socket.listen()try:    connection, client_address = server_socket.accept()    print(f'I got a connection from {client_address}!')    buffer = b''    while buffer[-2:] != b'\r\n':        data = connection.recv(2)        if not data:            break        else:            print(f'I got data: {data}!')            buffer = buffer + data    print(f"All the data is: {buffer}")finally:    server_socket.close()

在上面的代码中,我们像之前一样用 server_socket.accept 等待连接。一旦获得连接,我们就尝试接收两个字节并存入缓冲区。然后进入一个循环,每次迭代都检查缓冲区是否以回车加换行符结尾。如果没结束,我们就再获取两个字节,打印收到的字节,并将其追加到缓冲区。如果收到了 '\r\n',我们就跳出循环,打印出从客户端收到的完整消息。我们还在 finally 块中关闭了服务器套接字。这确保即使在读取数据时发生异常,连接也会被关闭。如果你用 telnet 连接这个应用并发送消息 'testing123',你会看到如下输出:

I got a connection from ('127.0.0.1', 49721)!I got data: b'te'!I got data: b'st'!I got data: b'in'!I got data: b'g1'!I got data: b'23'!I got data: b'\r\n'!All the data is: b'testing123\r\n'

现在,我们能从套接字读取数据了,那怎么把数据发回给客户端呢?套接字有一个名为 sendall 的方法,它会接收一个消息并将其写回给客户端。我们可以修改列表 3.2 的代码,通过在缓冲区填满后调用 connection.sendall(buffer) 来回显客户端发送的消息:

    while buffer[-2:] != b'\r\n':        data = connection.recv(2)        if not data:            break        else:            print(f'I got data: {data}!')            buffer = buffer + data    print(f"All the data is: {buffer}")    connection.sendall(buffer)

现在,当你连接这个应用并通过 Telnet 发送消息时,应该能在 Telnet 终端上看到该消息被打印回来。我们已经用套接字搭建了一个非常基本的回显服务器!

目前这个应用一次只能处理一个客户端,但多个客户端可以连接到同一个服务器套接字。让我们来调整这个例子,让它能同时允许多个客户端连接。通过这样做,我们将展示为什么使用阻塞套接字无法正确支持多个客户端。

3.2.2 允许多连接及阻塞的风险

一个处于监听模式的套接字可以同时允许多个客户端连接。这意味着我们可以反复调用 socket.accept,每当一个客户端连接时,我们就能得到一个新的连接套接字,用于读写与该客户端的数据。掌握了这一点,我们就可以很直接地修改之前的例子来处理多个客户端。我们无限循环,不断调用 socket.accept 来监听新连接。每次拿到连接后,我们就把它加入到已有的连接列表中。然后,我们遍历每个连接,接收数据并立即写回客户端连接。

列表 3.3 允许多个客户端连接

import socketserver_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)server_address = ('127.0.0.1', 8000)server_socket.bind(server_address)server_socket.listen()connections = []try:    while True:        connection, client_address = server_socket.accept()        print(f'I got a connection from {client_address}!')        connections.append(connection)        for connection in connections:            buffer = b''            while buffer[-2:] != b'\r\n':                data = connection.recv(2)                if not data:                    break                else:                    print(f'I got data: {data}!')                    buffer = buffer + data            print(f"All the data is: {buffer}")            connection.send(buffer)finally:    server_socket.close()

我们可以尝试这样操作:先用 telnet 建立一个连接并输入一条消息。然后,再用第二个 telnet 客户端连接并发送另一条消息。然而,如果你这么做了,会立刻发现问题。第一个客户端工作正常,能像预期那样回显消息,但第二个客户端却收不到任何回显。这是因为套接字的默认阻塞行为造成的。accept 和 recv 方法会一直阻塞,直到收到数据。这意味着,一旦第一个客户端连接,我们就会阻塞等待它发送第一条回显消息。这就导致其他客户端被卡住,必须等到第一个客户端发送数据后,循环才会继续(见图 3.2)。

图 3.2 使用阻塞套接字时,客户端1连接后,客户端2会被阻塞,直到客户端1发送数据。

这显然不是令人满意的用户体验;我们创造的东西无法在有多于一个用户时良好扩展。我们可以通过将套接字设置为非阻塞模式来解决这个问题。当我们把套接字标记为非阻塞时,其方法在没有数据可接收时不会阻塞,而是立即返回,继续执行下一行代码。

最新文章

随机文章