Chapter3_第一个asyncio应用程序

1、用图示展示通过套接字实现基本的C/S基本通信流程

一图胜千言。以下是一个简单的服务器端和客户端交互的图示,展示了它们之间的基本通信流程:

(即在Python中使用套接字进行基本的网络编程.) 

+-------------------+                        +-------------------+
|    客户端 (Client) |                        |    服务器 (Server)  |
+-------------------+                        +-------------------+
           |                                          |
           |     创建套接字 (socket())                |
           |----------------------------------------> |
           |                                          |
           |                                          |
           |                                          |    创建套接字 (socket())
           |                                          |<----------------------
           |                                          |
           |                                          |    绑定 (bind())
           |                                          |<----------------------
           |                                          |
           |                                          |    监听 (listen())
           |                                          |<----------------------
           |                                          |
           |     连接 (connect())                     |
           |----------------------------------------> |
           |                                          |
           |                                          |    接受连接 (accept())
           |                                          |<----------------------
           |                                          |
           |                                          |
           |  发送数据 (send())                       |
           |----------------------------------------> |
           |                                          |
           |                                          |    接收数据 (recv())
           |                                          |<----------------------
           |                                          |
           |                                          |
           |                                          |
           |                                          |    发送数据 (send())
           |                                          |---------------------->|
           |                                          |
           | 接收数据 (recv())                        |
           |<---------------------------------------- |
           |                                          |
           |                                          |
           |     关闭连接 (close())                   |
           |----------------------------------------> |
           |                                          |
           |                                          |    关闭连接 (close())
           |                                          |<----------------------
           |                                          |

交互流程详细说明

  1. 创建套接字:

    • 服务器:使用socket()函数创建服务器套接字。
    • 客户端:使用socket()函数创建客户端套接字。
  2. 绑定和监听(服务器端):

    • 服务器:使用bind()函数将服务器套接字绑定到指定的IP地址和端口号。然后使用listen()函数开始监听连接请求。
  3. 连接(客户端发起):

    • 客户端:使用connect()函数连接到服务器的IP地址和端口号。
  4. 接受连接(服务器端):

    • 服务器:使用accept()函数接受客户端的连接请求。此操作会返回一个新的套接字对象,用于与客户端通信。
  5. 数据传输:

    • 客户端:使用send()函数发送数据到服务器。
    • 服务器:使用recv()函数接收客户端发送的数据。
    • 服务器:使用send()函数发送数据到客户端。
    • 客户端:使用recv()函数接收服务器发送的数据。
  6. 关闭连接:

    • 客户端:通信完成后,使用close()函数关闭客户端套接字。
    • 服务器:通信完成后,使用close()函数关闭与客户端通信的套接字。服务器通常继续监听新的连接请求。

 

2、如何理解套接字的概念、和用法

套接字的概念和作用

概念: 套接字(Socket)是网络编程中用于描述IP地址和端口的接口。它是网络通信的端点,通过它应用程序可以在网络上进行数据的发送和接收。

作用:

  1. 数据传输:套接字允许不同计算机之间进行数据交换。
  2. 网络编程:是网络通信的基础,通过套接字,开发者可以编写客户端和服务器程序。
  3. 跨平台通信:套接字提供一种标准化的通信方式,能够在不同操作系统之间进行数据传输。

通俗解释

可以把套接字想象成一个电话插孔,连接在一条网络“电话线”上。一个应用程序可以通过插入一个“电话插头”(创建一个套接字),

然后拨打一个号码(连接到一个IP地址和端口),与另一端的应用程序进行通信。服务器端相当于接听电话的一方,而客户端相当于拨打电话的一方。

Python代码举例

服务器端代码

import socket

# 创建一个套接字对象
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 绑定套接字到本地IP地址和端口
server_socket.bind(('localhost', 12345))

# 开始监听,允许最多1个连接
server_socket.listen(1)
print("服务器已启动,等待连接...")

# 接受连接
client_socket, client_address = server_socket.accept()
print(f"连接已建立:{client_address}")

# 接收客户端发送的数据
data = client_socket.recv(1024).decode() # 这里写成client_socket.并没有错,后面追问会详细解释。
print(f"收到的数据:{data}")

# 发送数据到客户端
client_socket.send("Hello from the server!".encode())

# 关闭套接字
client_socket.close()
server_socket.close()

客户端代码

import socket

# 创建一个套接字对象
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 连接到服务器
client_socket.connect(('localhost', 12345))

# 发送数据到服务器
client_socket.send("Hello from the client!".encode())

# 接收服务器发送的数据
data = client_socket.recv(1024).decode()
print(f"收到的数据:{data}")

# 关闭套接字
client_socket.close()

服务器和客户端的套接字说明

  1. 服务器套接字:

    • 创建和绑定:服务器套接字通过socket()创建,并使用bind()将其绑定到指定的IP地址和端口号。
    • 监听:通过listen()开始监听连接请求。
    • 接受连接:使用accept()接受客户端的连接请求,返回一个新的套接字对象用于与客户端通信。
    • 通信:使用返回的客户端套接字对象进行数据收发。
    • 关闭:完成通信后,关闭客户端套接字和服务器套接字。
  2. 客户端套接字:

    • 创建:客户端套接字通过socket()创建。
    • 连接:使用connect()连接到服务器的IP地址和端口号。
    • 通信:连接成功后,通过套接字进行数据发送和接收。
    • 关闭:通信完成后,关闭套接字。

 

3、追问2:这一段代码是不是写错了。

# 接收客户端发送的数据 

data = client_socket.recv(1024).decode() 
print(f"收到的数据:{data}")

我理解应该是: # 接收客户端发送的数据

data = server_socket.recv(1024).decode() 
print(f"收到的数据:{data}")

你提到的代码部分其实没有错误。让我解释一下原因,并重述一下代码的背景和正确用法。

在服务器-客户端模型中,服务器和客户端各自有各自的套接字对象。服务器套接字负责监听连接请求,而一旦连接被接受,会生成一个新的套接字对象用于与客户端通信。客户端的套接字对象用于与服务器进行通信。

以下是代码的正确结构和解释:

服务器端代码

import socket

# 创建一个套接字对象
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 绑定套接字到本地IP地址和端口
server_socket.bind(('localhost', 12345))

# 开始监听,允许最多1个连接
server_socket.listen(1)
print("服务器已启动,等待连接...")

# 接受连接
client_socket, client_address = server_socket.accept()
print(f"连接已建立:{client_address}")

# 接收客户端发送的数据
data = client_socket.recv(1024).decode()  # 使用client_socket接收数据
print(f"收到的数据:{data}")

# 发送数据到客户端
client_socket.send("Hello from the server!".encode())

# 关闭套接字
client_socket.close()
server_socket.close()

客户端代码

import socket

# 创建一个套接字对象
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 连接到服务器
client_socket.connect(('localhost', 12345))

# 发送数据到服务器
client_socket.send("Hello from the client!".encode())

# 接收服务器发送的数据
data = client_socket.recv(1024).decode()
print(f"收到的数据:{data}")

# 关闭套接字
client_socket.close()

关键点解释:

  1. 服务器端:

    • server_socket用于监听新的连接请求。
    • 一旦有客户端连接,server_socket.accept()会返回一个新的套接字对象client_socket,这个新套接字用于与客户端通信。
    • 因此,服务器在接收客户端发送的数据时,应该使用client_socket.recv(1024),而不是server_socket.recv(1024)
  2. 客户端:

    • 客户端使用client_socket与服务器通信。
    • 发送数据时,使用client_socket.send(data)
    • 接收数据时,使用client_socket.recv(1024)

结论:

你的原代码是正确的:

# 接收客户端发送的数据
data = client_socket.recv(1024).decode()
print(f"收到的数据:{data}")

使用的是client_socket,这是在accept()之后专门用于与客户端进行通信的套接字,而不是用于监听连接请求的server_socket

 

4、追问:为什么一旦有客户端连接,server_socket.accept()会返回一个新的套接字对象client_socket,这个新套接字用于与客户端通信。?? 难道不能使用原来的服务器套接字对象?

另外新的套接字对象为什么要命名为client_socket?而不是server_client? 这样不是更符合直觉吗?

这是因为在套接字编程中,监听通信的职责需要分离,以确保服务器能够同时处理多个客户端的连接请求。让我们详细解释一下为什么会有这种设计以及命名的原因。

服务器套接字的职责

  1. 监听套接字 (server_socket):

    • server_socket的主要职责是监听来自客户端的连接请求。
    • 它通过调用listen()方法进入监听状态,并通过accept()方法接受新的连接。
    • server_socket不直接用于数据传输。它的作用是等待并接受连接请求。
  2. 通信套接字 (client_socket):

    • 一旦server_socket接受了一个连接,它会返回一个新的套接字对象,即client_socket,用于与这个特定的客户端进行通信。
    • client_socket专门用于处理与客户端的数据传输,这样服务器可以继续监听新的连接请求。

为什么不使用原来的服务器套接字 (server_socket)?

  • 分离监听和通信:如果使用server_socket进行数据传输,服务器将无法继续监听其他客户端的连接请求。通过创建新的通信套接字 (client_socket),服务器可以同时处理多个客户端的连接。
  • 并发处理:服务器通常需要能够同时处理多个客户端的连接。使用独立的通信套接字 (client_socket) 可以让每个连接独立运行,便于在多线程或异步环境中进行并发处理。

命名为 client_socket 的原因

  • 表意清晰:命名为 client_socket 是为了表明这个套接字对象专门用于与特定客户端通信。它有助于区分监听套接字 (server_socket) 和通信套接字。
  • 惯例:这种命名方式是一种编程惯例,旨在使代码更易于理解和维护。尽管 server_client 也可以是一个有效的名称,但 client_socket 更加简洁并且在网络编程中广泛使用。

示例代码复述

以下是一个更详细的示例,说明如何在服务器端和客户端进行套接字通信:

服务器端代码

import socket

# 创建一个服务器套接字
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 绑定服务器套接字到本地IP地址和端口
server_socket.bind(('localhost', 12345))

# 开始监听连接请求,允许最多5个待处理连接
server_socket.listen(5)
print("服务器已启动,等待连接...")

while True:
    # 接受客户端连接请求
    client_socket, client_address = server_socket.accept()
    print(f"连接已建立:{client_address}")

    # 接收客户端发送的数据
    data = client_socket.recv(1024).decode()
    print(f"收到的数据:{data}")

    # 发送数据到客户端
    client_socket.send("Hello from the server!".encode())

    # 关闭客户端套接字
    client_socket.close()

客户端代码

import socket

# 创建一个客户端套接字
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 连接到服务器
client_socket.connect(('localhost', 12345))

# 发送数据到服务器
client_socket.send("Hello from the client!".encode())

# 接收服务器发送的数据
data = client_socket.recv(1024).decode()
print(f"收到的数据:{data}")

# 关闭客户端套接字
client_socket.close()

总结

  • 分离职责:服务器套接字 (server_socket) 用于监听连接请求,通信套接字 (client_socket) 用于与客户端进行数据传输。
  • 并发处理:通过创建新的通信套接字,服务器能够同时处理多个客户端的连接请求。
  • 命名惯例:命名为 client_socket 是为了清晰表达其用途,即与特定客户端的通信。
 
5、如下代码的理解
import socket

server_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()

在你的代码中,setsockopt 方法的使用有助于设置套接字选项,从而优化其行为

在这种情况下,你使用了 socket.SO_REUSEADDR 选项,这是一个常见且有用的套接字选项。让我详细解释一下这段代码的作用:

逐行解释

  1. 创建套接字对象:

server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

这行代码创建了一个新的套接字对象。

socket.AF_INET 表示使用IPv4地址族,

socket.SOCK_STREAM 表示这是一个TCP套接字。

 

        2.设置套接字选项:

server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  1. 这行代码设置套接字选项,使得在服务器关闭后,可以立即重新使用相同的地址和端口。具体来说:

    • socket.SOL_SOCKET:表示正在设置套接字级别的选项。
    • socket.SO_REUSEADDR:允许重用本地地址。
    • 1:启用该选项。

    作用:通常,当一个套接字被关闭后,它的端口可能会被标记为 "已占用" 一段时间(这个状态称为 TIME_WAIT)。启用 SO_REUSEADDR 选项可以让你的服务器在关闭后立即重新绑定到同一个地址和端口,而不会遇到 "地址已在使用" 的错误。

  2. 绑定地址和端口:

server_address = ('127.0.0.1', 8000)
server_socket.bind(server_address)
  1. 这行代码将服务器绑定到本地地址 '127.0.0.1' 和端口 8000127.0.0.1 是回环地址,表示服务器只能接受来自本地计算机的连接。

  2. 开始监听:

server_socket.listen()
  1. 这行代码使服务器进入监听状态,准备接受客户端的连接请求。默认情况下,listen() 的参数(待处理连接的最大数量)为 0,意味着使用系统默认值。

setsockopt 的使用场景

使用 setsockopt 方法设置 SO_REUSEADDR 选项的场景包括但不限于:

  • 开发和测试:在开发或测试过程中,服务器需要频繁重启。在这种情况下,SO_REUSEADDR 可以避免每次重启服务器时出现 "地址已在使用" 的错误。
  • 高可用性:在某些高可用性系统中,当主服务器故障后,备用服务器需要快速接管相同的地址和端口。

完整示例

以下是一个完整的服务器代码示例,展示了如何处理客户端连接和数据通信:

import socket

# 创建服务器套接字
server_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()
print("服务器已启动,等待连接...")

while True:
    # 接受客户端连接
    client_socket, client_address = server_socket.accept()
    print(f"连接已建立:{client_address}")

    # 接收客户端发送的数据
    data = client_socket.recv(1024).decode()
    print(f"收到的数据:{data}")

    # 发送数据到客户端
    client_socket.send("Hello from the server!".encode())

    # 关闭客户端套接字
    client_socket.close()

客户端代码示例

以下是对应的客户端代码示例:

import socket

# 创建客户端套接字
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 连接到服务器
server_ip = '127.0.0.1'
server_port = 8000
client_socket.connect((server_ip, server_port))

# 发送数据到服务器
client_socket.send("Hello from the client!".encode())

# 接收服务器发送的数据
data = client_socket.recv(1024).decode()
print(f"收到的数据:{data}")

# 关闭客户端套接字
client_socket.close()

网络配置注意事项

  1. 防火墙设置:

    • 确保服务器电脑的防火墙允许通过端口 8000 的入站和出站流量。
    • 在Windows上,可以通过高级防火墙设置来添加规则。
    • 在Linux上,可以使用 iptablesufw 来配置防火墙规则。
  2. 同一局域网内:

    • 确保两台电脑在同一个局域网内,否则它们无法通过局域网IP地址进行通信。
    • 如果两台电脑不在同一个局域网内,需要通过公网IP地址进行通信,并在路由器上进行端口转发配置,这超出了局域网内简单通信的范畴。

验证连接

  1. 运行服务器端代码: 在服务器电脑上运行服务器端代码,确保服务器正在监听。

  2. 运行客户端代码: 在客户端电脑上运行客户端代码,连接到服务器的实际IP地址,测试通信是否成功。

通过这些步骤,你可以在两台不同的电脑上成功模拟服务器和客户端的网络通信。

 

 6、如下代码的理解。 [from example3-7.py]

import selectors
import socket
from selectors import SelectorKey
from typing import List, Tuple

selector = selectors.DefaultSelector()

server_socket = socket.socket()
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

server_address = ('127.0.0.1', 8000)
server_socket.setblocking(False)
server_socket.bind(server_address)
server_socket.listen()

selector.register(server_socket, selectors.EVENT_READ)

while True:
    events: List[Tuple[SelectorKey, int]] = selector.select(timeout=1) #A

    if len(events) == 0: #B
        print('No events, waiting a bit more!')

    for event, _ in events:
        event_socket = event.fileobj #C

        if event_socket == server_socket: #D
            connection, address = server_socket.accept()
            connection.setblocking(False)
            print(f"I got a connection from {address}")
            selector.register(connection, selectors.EVENT_READ) #E
        else:
            data = event_socket.recv(1024) #F
            print(f"I got some data: {data}")
            event_socket.send(data)

下面逐段解释:

import selectors
import socket
from selectors import SelectorKey
from typing import List, Tuple
  • selectors:用于管理和监视多个socket对象的I/O事件。
  • socket:用于创建和操作网络连接。
  • SelectorKey:类型提示,表示 selectors 模块返回的键。
  • ListTuple:类型提示,用于指定列表和元组的类型。

初始化选择器和服务器套接字

selector = selectors.DefaultSelector()
  • 创建一个默认的选择器对象,用于监视多个socket对象的I/O事件。
server_socket = socket.socket()
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  • 创建一个TCP套接字。
  • 设置套接字选项,允许重用本地地址,以便服务器在关闭后可以立即重新绑定到相同的地址和端口。
server_address = ('127.0.0.1', 8000)
server_socket.setblocking(False)
server_socket.bind(server_address)
server_socket.listen()
  • 将服务器绑定到 127.0.0.1:8000,即本地回环地址上的端口8000。
  • 将套接字设置为非阻塞模式。
  • 开始监听传入连接。
selector.register(server_socket, selectors.EVENT_READ)
  • 注册服务器套接字,将其加入选择器进行监视,监视读事件。
while True:
    events: List[Tuple[SelectorKey, int]] = selector.select(timeout=1)  # A  # events里面到底是啥,这对于代码的理解至关重要,后面追问会详细解释
  • 开始一个无限循环来处理I/O事件。
  • 使用 selector.select(timeout=1) 方法等待I/O事件,超时时间为1秒。返回一个事件列表。
    if len(events) == 0:  # B
        print('No events, waiting a bit more!')
  • 如果没有事件发生,打印一条消息并继续等待。
    for event, _ in events:
        event_socket = event.fileobj  # C
  • 遍历所有发生的事件。
  • event.fileobj 是发生事件的套接字对象。
        if event_socket == server_socket:  # D
            connection, address = server_socket.accept()
            connection.setblocking(False)
            print(f"I got a connection from {address}")
            selector.register(connection, selectors.EVENT_READ)  # E
  • 检查事件套接字是否为服务器套接字。
  • 如果是服务器套接字,调用 accept 方法接受新的连接,并将其设置为非阻塞模式。
  • 打印新连接的地址。
  • 将新连接注册到选择器中,监视读事件。
        else:
            data = event_socket.recv(1024)  # F
            print(f"I got some data: {data}")
            event_socket.send(data)
  • 如果事件套接字不是服务器套接字,表示这是一个客户端连接的读事件。
  • 从客户端连接中接收数据。
  • 打印接收到的数据。
  • 将接收到的数据回送给客户端(回显)。

代码总结

这个服务器使用 selectors 模块实现了一个简单的非阻塞回显服务器。它能够同时处理多个客户端连接,而不会因为一个阻塞的I/O操作而阻塞整个服务器。

每当有新的客户端连接时,服务器接受连接并将其注册到选择器中进行监视。一旦有数据从客户端发送过来,服务器读取数据并将其回显给客户端。

 

7、events里面内容的理解

为了加深理解,让我们通过一些具体的例子来看 events 列表的内容。首先,回顾一下关键代码片段:

events: List[Tuple[SelectorKey, int]] = selector.select(timeout=1)  # A

if len(events) == 0:  # B
    print('No events, waiting a bit more!')

for event, _ in events:
    event_socket = event.fileobj  # C

    if event_socket == server_socket:  # D
        connection, address = server_socket.accept()
        connection.setblocking(False)
        print(f"I got a connection from {address}")
        selector.register(connection, selectors.EVENT_READ)  # E
    else:
        data = event_socket.recv(1024)  # F
        print(f"I got some data: {data}")
        event_socket.send(data)

示例:events 列表内容

示例1:没有事件发生

在没有任何客户端连接或数据传输的情况下,events 列表为空。

events = []

在这种情况下,程序会输出:

No events, waiting a bit more!

示例2:有一个新的客户端连接请求

假设有一个新的客户端尝试连接服务器selector.select(timeout=1) 会返回一个事件,表示服务器套接字准备好了接受新的连接。

events = [(SelectorKey(fileobj=server_socket, fd=3, events=1, data=None), 1)]
  • SelectorKey 包含了以下信息:
    • fileobj=server_socket:发生事件的套接字对象。
    • fd=3:文件描述符(因平台而异)。
    • events=1:事件类型(读事件)。
    • data=None:附加的用户数据(如果有的话)。

程序会处理这个事件,调用 accept 方法接受新的连接并注册该连接:

I got a connection from ('127.0.0.1', 54321)

示例3:客户端发送数据

假设有一个客户端已经连接并发送了一些数据selector.select(timeout=1) 会返回一个事件,表示该客户端套接字上有数据可读。

events = [(SelectorKey(fileobj=client_socket, fd=4, events=1, data=None), 1)]
  • fileobj=client_socket:发生事件的套接字对象。
  • fd=4:文件描述符(因平台而异)。
  • events=1:事件类型(读事件)。
  • data=None:附加的用户数据(如果有的话)。

程序会处理这个事件,调用 recv 方法读取数据并回显:

I got some data: b'Hello, server!'

小结

  • events 是一个包含元组的列表,每个元组表示一个发生的I/O事件。
  • 每个元组包含一个 SelectorKey 对象和一个事件掩码(整数)
  • SelectorKey 对象提供了发生事件的套接字对象及其相关信息。

解释每个关键点

  • A:selector.select(timeout=1) 等待最多1秒钟,返回发生事件的列表。
  • B:如果没有事件发生,events 列表为空,程序打印等待消息。
  • C:遍历每个事件,获取发生事件的套接字对象。
  • D:检查是否是服务器套接字的事件。如果是,接受新的连接。
  • E:将新连接设置为非阻塞模式并注册到选择器。
  • F:如果是客户端套接字的事件,读取并回显数据。

通过这些示例,便能更好地理解 events 列表的内容以及程序如何处理不同的I/O事件。

 

8、如何理解如下代码  [from example3-8.py]

import asyncio
import socket
from asyncio import AbstractEventLoop

async def echo(connection: socket,
               loop: AbstractEventLoop) -> None:
    while data := await loop.sock_recv(connection, 1024):
        await loop.sock_sendall(connection, data)


async def listen_for_connection(server_socket: socket,
                                loop: AbstractEventLoop):
    while True:
        connection, address = await loop.sock_accept(server_socket)
        connection.setblocking(False)
        print(f"Got a connection from {address}")
        asyncio.create_task(echo(connection, loop))


async def main():
    server_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.setblocking(False)
    server_socket.bind(server_address)
    server_socket.listen()

    await listen_for_connection(server_socket, asyncio.get_event_loop())

asyncio.run(main())

这段代码使用 asynciosocket 模块实现了一个异步的回显服务器。下面我会逐行解释这段代码。

导入模块

import asyncio
import socket
from asyncio import AbstractEventLoop
  • asyncio:Python的异步I/O框架,用于编写异步程序。
  • socket:用于创建和操作网络连接。
  • AbstractEventLoopasyncio 模块中的事件循环基类。

定义异步回显函数

async def echo(connection: socket,
               loop: AbstractEventLoop) -> None:
    while data := await loop.sock_recv(connection, 1024):
        await loop.sock_sendall(connection, data)
  • echo 是一个异步函数,负责处理客户端连接。
  • connection:客户端连接的套接字对象。
  • loop:事件循环对象。
  • while data := await loop.sock_recv(connection, 1024):使用事件循环异步接收数据,每次最多接收1024字节。
    • loop.sock_recvasyncio 提供的非阻塞 recv 方法。
  • await loop.sock_sendall(connection, data):将接收到的数据回送给客户端。
    • loop.sock_sendallasyncio 提供的非阻塞 sendall 方法。

定义异步监听函数

async def listen_for_connection(server_socket: socket,
                                loop: AbstractEventLoop):
    while True:
        connection, address = await loop.sock_accept(server_socket)
        connection.setblocking(False)
        print(f"Got a connection from {address}")
        asyncio.create_task(echo(connection, loop))
  • listen_for_connection 是一个异步函数,负责监听新的客户端连接。
  • server_socket:服务器套接字对象。
  • loop:事件循环对象。
  • while True:无限循环,持续监听新的连接。
  • connection, address = await loop.sock_accept(server_socket):使用事件循环异步接受新的连接。
    • loop.sock_acceptasyncio 提供的非阻塞 accept 方法。
  • connection.setblocking(False):将新连接设置为非阻塞模式。
  • print(f"Got a connection from {address}"):打印新连接的地址。
  • asyncio.create_task(echo(connection, loop)):创建一个新的任务处理客户端连接。

定义主函数

async def main():
    server_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.setblocking(False)
    server_socket.bind(server_address)
    server_socket.listen()

    await listen_for_connection(server_socket, asyncio.get_event_loop())
  • main 是主异步函数,负责初始化服务器套接字并启动监听。
  • server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM):创建一个TCP套接字。
  • server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1):设置套接字选项,允许重用本地地址。
  • server_address = ('127.0.0.1', 8000):服务器绑定到本地地址 127.0.0.1 的端口 8000
  • server_socket.setblocking(False):将服务器套接字设置为非阻塞模式。
  • server_socket.bind(server_address):将套接字绑定到指定地址和端口。
  • server_socket.listen():开始监听传入连接。
  • await listen_for_connection(server_socket, asyncio.get_event_loop()):启动监听协程,等待客户端连接。

运行主函数

asyncio.run(main())
  • asyncio.run(main()):运行主协程 main,启动异步事件循环。

小结

  1. 导入必要的模块和类。
  2. 定义 echo 协程:处理每个客户端连接,接收并回显数据。
  3. 定义 listen_for_connection 协程:持续监听新的客户端连接并启动 echo 协程处理每个连接。
  4. 定义 main 协程:初始化服务器套接字,绑定地址和端口,启动监听协程。
  5. 运行 main 协程:启动异步事件循环。

通过这种方式,服务器能够异步处理多个客户端连接,而不会因为一个阻塞操作而影响其他连接。

 

9、 这里会话的概念如何理解。最后会话是不是和session一个概念?

在网络通信中,会话(session)指的是客户端和服务器之间的一系列交互过程,从建立连接开始,到断开连接为止。

在这段时间内,双方可以进行多次数据交换。会话的主要目的是维持状态信息,以便在多次请求之间保持连续性。

会话的作用

  1. 管理状态:会话允许在多次请求之间保存状态信息。比如,用户登录后,会话可以保存用户的登录状态,在后续请求中识别该用户。
  2. 重用连接:会话可以重用底层的网络连接,从而减少连接建立和断开带来的开销,提高性能。
  3. 配置管理:会话可以集中管理请求的配置,如头信息(headers)、cookies、认证信息等。

通俗解释

你可以把会话想象成一次旅行。旅行的开始是你登上飞机(建立连接),结束是你下飞机(断开连接)。在这段时间内,你可以在不同的城市之间飞行(多次请求),而你的行李、机票信息、护照等(状态信息)都由航空公司帮你管理。

举例说明

假设你在网上购物:

  1. 开始会话:你打开购物网站,登录账户。
  2. 浏览商品:你浏览多个商品页面,每次点击都会发送请求。
  3. 添加购物车:你把喜欢的商品添加到购物车。
  4. 结账:你去结账页面,填写地址和支付信息。
  5. 结束会话:你下单并登出账户。

在这个过程中,会话帮助你保存了登录状态、购物车信息等,避免你每次请求都需要重新登录或重新添加购物车。

示例代码

下面是一个简单的示例,展示如何使用 aiohttpClientSession 进行多次请求,并保持会话状态:

import asyncio
import aiohttp

async def fetch(session, url):
    async with session.get(url) as response:
        return await response.text()

async def main():
    async with aiohttp.ClientSession() as session:
        url1 = 'https://jsonplaceholder.typicode.com/posts/1'
        url2 = 'https://jsonplaceholder.typicode.com/posts/2'
        
        # 第一次请求
        response1 = await fetch(session, url1)
        print(f'URL1 Response: {response1[:100]}...')  # 打印前100个字符
        
        # 第二次请求
        response2 = await fetch(session, url2)
        print(f'URL2 Response: {response2[:100]}...')  # 打印前100个字符

# 运行主程序
asyncio.run(main())

会话与Session

在网络编程和Web开发中,“会话”(session)和“Session”通常指的是同一个概念,即在客户端和服务器之间维持状态的机制。具体到 aiohttp 中的 ClientSession

  • ClientSession 表示一个会话对象,管理与服务器的多个请求,并保持状态(如Cookies、连接池等)。
  • 在Web开发中,如Flask或Django,Session 对象用于在用户和服务器之间保持会话状态,通常通过Cookies实现。

总结

  • 会话:表示客户端和服务器之间的一系列交互过程,保存状态信息。
  • 作用:管理状态、重用连接、配置管理。
  • 通俗解释:类似一次旅行,管理你的行李和信息。
  • 举例说明:如网上购物的过程。
  • 会话与Session:是同一个概念,在不同上下文中使用。ClientSession 管理HTTP请求,会话保持状态。
 
 
 
 
 
 
 

 

posted @ 2024-05-21 19:55  AlphaGeek  阅读(40)  评论(0)    收藏  举报