Python网络、并发编程的深入解析与实战应用
1. 网络编程相关技术
1.1 文件描述符传递与工作进程
在网络编程中,文件描述符的传递有特定的应用场景。例如,
worker
函数用于连接服务器并接收文件描述符,处理客户端请求。以下是相关代码:
import socket
import struct
def recv_fd(sock):
msg, ancdata, flags, addr = sock.recvmsg(1, socket.CMSG_LEN(struct.calcsize('i')))
cmsg_level, cmsg_type, cmsg_data = ancdata[0]
sock.sendall(b'OK')
return struct.unpack('i', cmsg_data)[0]
def worker(server_address):
serv = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
serv.connect(server_address)
while True:
fd = recv_fd(serv)
print('WORKER: GOT FD', fd)
with socket.socket(socket.AF_INET, socket.SOCK_STREAM, fileno=fd) as client:
while True:
msg = client.recv(1024)
if not msg:
break
print('WORKER: RECV {!r}'.format(msg))
client.send(msg)
if __name__ == '__main__':
import sys
if len(sys.argv) != 2:
print('Usage: worker.py server_address', file=sys.stderr)
raise SystemExit(1)
worker(sys.argv[1])
1.2 事件驱动的输入输出
事件驱动的输入输出是一种将基本的输入输出操作转化为事件处理的技术。其核心是通过
select
函数来监控文件描述符的活动。
1.2.1 事件处理基类
首先定义了一个
EventHandler
基类,包含了文件描述符获取、接收和发送相关的方法:
class EventHandler:
def fileno(self):
'Возвращает ассоциированный файловый дескриптор'
raise NotImplemented('must implement')
def wants_to_receive(self):
'Возвращает True, если получение разрешено'
return False
def handle_receive(self):
'Выполняет операцию получения'
pass
def wants_to_send(self):
'Возвращает True, если отсылка запрошена'
return False
def handle_send(self):
'Отсылает исходящие данные'
pass
1.2.2 事件循环
事件循环通过
select
函数来监控哪些处理程序需要接收或发送数据:
import select
def event_loop(handlers):
while True:
wants_recv = [h for h in handlers if h.wants_to_receive()]
wants_send = [h for h in handlers if h.wants_to_send()]
can_recv, can_send, _ = select.select(wants_recv,
wants_send, [])
for h in can_recv:
h.handle_receive()
for h in can_send:
h.handle_send()
1.2.3 UDP服务器示例
以下是两个简单的UDP服务器示例,分别是时间服务器和回声服务器:
import socket
import time
class UDPServer(EventHandler):
def __init__(self, address):
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.sock.bind(address)
def fileno(self):
return self.sock.fileno()
def wants_to_receive(self):
return True
class UDPTimeServer(UDPServer):
def handle_receive(self):
msg, addr = self.sock.recvfrom(1)
self.sock.sendto(time.ctime().encode('ascii'), addr)
class UDPEchoServer(UDPServer):
def handle_receive(self):
msg, addr = self.sock.recvfrom(8192)
self.sock.sendto(msg, addr)
if __name__ == '__main__':
handlers = [UDPTimeServer(('', 14000)), UDPEchoServer(('', 15000))]
event_loop(handlers)
1.2.4 TCP服务器示例
TCP服务器的实现相对复杂,因为每个客户端连接都需要创建一个新的处理程序:
class TCPServer(EventHandler):
def __init__(self, address, client_handler, handler_list):
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
self.sock.bind(address)
self.sock.listen(1)
self.client_handler = client_handler
self.handler_list = handler_list
def fileno(self):
return self.sock.fileno()
def wants_to_receive(self):
return True
def handle_receive(self):
client, addr = self.sock.accept()
# Добавляет клиента в список обработчиков цикла событий
self.handler_list.append(self.client_handler(client, self.handler_list))
class TCPClient(EventHandler):
def __init__(self, sock, handler_list):
self.sock = sock
self.handler_list = handler_list
self.outgoing = bytearray()
def fileno(self):
return self.sock.fileno()
def close(self):
self.sock.close()
# Удалиться из списка обработчиков цикла событий
self.handler_list.remove(self)
def wants_to_send(self):
return True if self.outgoing else False
def handle_send(self):
nsent = self.sock.send(self.outgoing)
self.outgoing = self.outgoing[nsent:]
class TCPEchoClient(TCPClient):
def wants_to_receive(self):
return True
def handle_receive(self):
data = self.sock.recv(8192)
if not data:
self.close()
else:
self.outgoing.extend(data)
if __name__ == '__main__':
handlers = []
handlers.append(TCPServer(('', 16000), TCPEchoClient, handlers))
event_loop(handlers)
1.3 事件驱动输入输出的优缺点
-
优点
:可以处理大量的并发连接,无需使用线程或进程,通过
select函数可以监控大量的套接字并响应事件,事件按顺序处理,无需使用并发原语。 - 缺点 :不具备真正的并发能力,如果某个事件处理方法阻塞或进行长时间计算,会导致整个程序停止。调用非事件驱动风格的库函数也可能导致阻塞。
1.4 解决阻塞问题的方法
可以通过将工作发送到单独的线程或进程来解决阻塞或长时间计算的问题。以下是一个使用
concurrent.futures
模块的示例:
from concurrent.futures import ThreadPoolExecutor
import os
class ThreadPoolHandler(EventHandler):
def __init__(self, nworkers):
if os.name == 'posix':
self.signal_done_sock, self.done_sock = socket.socketpair()
else:
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('127.0.0.1', 0))
server.listen(1)
self.signal_done_sock = socket.socket(socket.AF_INET,
socket.SOCK_STREAM)
self.signal_done_sock.connect(server.getsockname())
self.done_sock, _ = server.accept()
server.close()
self.pending = []
self.pool = ThreadPoolExecutor(nworkers)
def fileno(self):
return self.done_sock.fileno()
# Функция обратного вызова, которая выполняется после завершения потока
def _complete(self, callback, r):
self.pending.append((callback, r.result()))
self.signal_done_sock.send(b'x')
# Запускает функцию в пуле потоков
def run(self, func, args=(), kwargs={}, *, callback):
r = self.pool.submit(func, *args, **kwargs)
r.add_done_callback(lambda r: self._complete(callback, r))
def wants_to_receive(self):
return True
# Запускает функции обратного вызова завершенной работы
def handle_receive(self):
# Вызывает все коллбэки в очереди
for callback, result in self.pending:
callback(result)
self.done_sock.recv(1)
self.pending = []
1.5 发送和接收大数组
可以使用内存视图(
memoryviews
)来发送和接收大数组,减少数据的复制:
# zerocopy.py
def send_from(arr, dest):
view = memoryview(arr).cast('B')
while len(view):
nsent = dest.send(view)
view = view[nsent:]
def recv_into(arr, source):
view = memoryview(arr).cast('B')
while len(view):
nrecv = source.recv_into(view)
view = view[nrecv:]
以下是测试代码:
# 服务器
from socket import *
s = socket(AF_INET, SOCK_STREAM)
s.bind(('', 25000))
s.listen(1)
c, a = s.accept()
import numpy
a = numpy.arange(0.0, 50000000.0)
send_from(a, c)
# 客户端
from socket import *
c = socket(AF_INET, SOCK_STREAM)
c.connect(('localhost', 25000))
import numpy
a = numpy.zeros(shape=50000000, dtype=float)
recv_into(a, c)
2. 并发编程相关技术
2.1 线程的启动和停止
2.1.1 基本线程启动
可以使用
threading
库来创建和启动线程,以下是一个简单的示例:
import time
from threading import Thread
def countdown(n):
while n > 0:
print('T-minus', n)
n -= 1
time.sleep(5)
t = Thread(target=countdown, args=(10,))
t.start()
2.1.2 线程状态检查和合并
可以检查线程是否存活,以及请求线程合并:
if t.is_alive():
print('Still running')
else:
print('Completed')
t.join()
2.1.3 守护线程
可以创建守护线程,守护线程在主线程结束时自动销毁:
t = Thread(target=countdown, args=(10,), daemon=True)
t.start()
2.1.4 线程终止
如果需要终止线程,可以让线程在特定点接收退出指示,以下是一个示例:
class CountdownTask:
def __init__(self):
self._running = True
def terminate(self):
self._running = False
def run(self, n):
while self._running and n > 0:
print('T-minus', n)
n -= 1
time.sleep(5)
c = CountdownTask()
t = Thread(target=c.run, args=(10,))
t.start()
c.terminate()
t.join()
2.1.5 处理阻塞操作
对于执行阻塞操作的线程,需要使用带超时的循环来正确处理,以下是一个示例:
class IOTask:
def terminate(self):
self._running = False
def run(self, sock):
# sock это сокет
sock.settimeout(5) # Установить тайм-аут
while self._running:
# Выполнить блокирующую операцию ввода-вывода с тайм-аутом
try:
data = sock.recv(8192)
break
except socket.timeout:
continue
# Продолжение обработки
...
# Завершено
return
2.2 线程启动状态的判断
可以使用
threading
库中的
Event
对象来判断线程是否启动,以下是一个示例:
from threading import Thread, Event
import time
def countdown(n, started_evt):
print('countdown starting')
started_evt.set()
while n > 0:
print('T-minus', n)
n -= 1
time.sleep(5)
started_evt = Event()
t = Thread(target=countdown, args=(10, started_evt))
print('Launching countdown')
t.start()
started_evt.wait()
print('countdown is running')
2.3 线程间通信
2.3.1 使用队列进行通信
可以使用
queue
库中的
Queue
对象来实现线程间的安全通信,以下是一个示例:
from queue import Queue
from threading import Thread
# Поток, который производит данные
def producer(out_q):
while True:
# Производим данные
...
out_q.put(data)
# Поток, который потребляет данные
def consumer(in_q):
while True:
# Получаем данные
data = in_q.get()
# Обрабатываем данные
...
q = Queue()
t1 = Thread(target=consumer, args=(q,))
t2 = Thread(target=producer, args=(q,))
t1.start()
t2.start()
2.3.2 处理生产者和消费者的关闭
可以使用特殊的“哨兵”值来协调生产者和消费者的关闭,以下是一个示例:
from queue import Queue
from threading import Thread
# Объект, который сигнализирует об отключении
_sentinel = object()
# Поток, который производит данные
def producer(out_q):
while running:
# Производим данные
...
out_q.put(data)
# Поместить стража в очередь, чтобы сигнализировать о завершении
out_q.put(_sentinel)
# Поток, который потребляет данные
def consumer(in_q):
while True:
# Получаем данные
data = in_q.get()
# Проверяем на предмет сигнала о завершении
if data is _sentinel:
in_q.put(_sentinel)
break
# Обрабатываем данные
...
2.3.3 自定义线程安全的数据结构
可以将数据结构包装在条件变量中,创建线程安全的优先级队列,以下是一个示例:
import heapq
import threading
class PriorityQueue:
def __init__(self):
self._queue = []
self._count = 0
self._cv = threading.Condition()
def put(self, item, priority):
with self._cv:
heapq.heappush(self._queue, (-priority, self._count, item))
self._count += 1
self._cv.notify()
def get(self):
with self._cv:
while len(self._queue) == 0:
self._cv.wait()
return heapq.heappop(self._queue)[-1]
2.3.4 队列的其他特性
Queue
对象还提供了一些其他特性,如
task_done()
和
join()
方法,以及非阻塞和超时操作:
from queue import Queue
from threading import Thread
# Поток, который производит данные
def producer(out_q):
while running:
# Произвести данные
...
out_q.put(data)
# Поток, который потребляет данные
def consumer(in_q):
while True:
# Получаем данные
data = in_q.get()
# Обрабатываем данные
...
# Сигнализируем о завершении
in_q.task_done()
q = Queue()
t1 = Thread(target=consumer, args=(q,))
t2 = Thread(target=producer, args=(q,))
t1.start()
t2.start()
# Ждем, пока все произведенные элементы будут потреблены
q.join()
import queue
q = queue.Queue()
try:
data = q.get(block=False)
except queue.Empty:
...
try:
q.put(item, block=False)
except queue.Full:
...
try:
data = q.get(timeout=5.0)
except queue.Empty:
...
2.4 关键区域的锁定
可以使用
threading
库中的
Lock
对象来锁定关键区域,避免竞态条件,以下是一个示例:
import threading
class SharedCounter:
'''
Объект счетчика, который может быть общим для нескольких потоков.
'''
def __init__(self, initial_value = 0):
self._value = initial_value
self._value_lock = threading.Lock()
def incr(self,delta=1):
'''
Инкрементирует счетчик с блокировкой.
'''
with self._value_lock:
self._value += delta
def decr(self,delta=1):
'''
Декрементирует счетчик с блокировкой.
'''
with self._value_lock:
self._value -= delta
2.5 避免死锁的锁定
可以使用上下文管理器来确保线程按顺序获取多个锁,避免死锁,以下是一个示例:
import threading
from contextlib import contextmanager
# Локальное для потока состояние для хранения информации
# об уже полученных блокировках
_local = threading.local()
@contextmanager
def acquire(*locks):
# Сортирует блокировки по идентификатору объекта
locks = sorted(locks, key=lambda x: id(x))
# Убеждается, что порядок блокировки ранее
# приобретенных блокировок не нарушен
acquired = getattr(_local,'acquired',[])
if acquired and max(id(lock) for lock in acquired) >= id(locks[0]):
raise RuntimeError('Lock Order Violation')
# Получает все блокировки
acquired.extend(locks)
_local.acquired = acquired
try:
for lock in locks:
lock.acquire()
yield
finally:
# Освобождает блокировки в порядке, обратном их получению
for lock in reversed(locks):
lock.release()
del acquired[-len(locks):]
import threading
x_lock = threading.Lock()
y_lock = threading.Lock()
def thread_1():
while True:
with acquire(x_lock, y_lock):
print('Thread-1')
def thread_2():
while True:
with acquire(y_lock, x_lock):
print('Thread-2')
t1 = threading.Thread(target=thread_1)
t1.daemon = True
t1.start()
t2 = threading.Thread(target=thread_2)
t2.daemon = True
t2.start()
2.6 线程特定状态的存储
可以使用
threading.local()
来存储线程特定的状态,以下是一个示例:
from socket import socket, AF_INET, SOCK_STREAM
import threading
class LazyConnection:
def __init__(self, address, family=AF_INET, type=SOCK_STREAM):
self.address = address
self.family = AF_INET
self.type = SOCK_STREAM
self.local = threading.local()
def __enter__(self):
if hasattr(self.local, 'sock'):
raise RuntimeError('Already connected')
self.local.sock = socket(self.family, self.type)
self.local.sock.connect(self.address)
return self.local.sock
def __exit__(self, exc_ty, exc_val, tb):
self.local.sock.close()
del self.local.sock
from functools import partial
def test(conn):
with conn as s:
s.send(b'GET /index.html HTTP/1.0\r\n')
s.send(b'Host: www.python.org\r\n')
s.send(b'\r\n')
resp = b''.join(iter(partial(s.recv, 8192), b''))
print('Got {} bytes'.format(len(resp)))
if __name__ == '__main__':
conn = LazyConnection(('www.python.org', 80))
t1 = threading.Thread(target=test, args=(conn,))
t2 = threading.Thread(target=test, args=(conn,))
t1.start()
t2.start()
t1.join()
t2.join()
2.7 线程池的创建
2.7.1 使用
ThreadPoolExecutor
创建线程池
可以使用
concurrent.futures
库中的
ThreadPoolExecutor
来创建线程池,以下是一个简单的TCP服务器示例:
from socket import AF_INET, SOCK_STREAM, socket
from concurrent.futures import ThreadPoolExecutor
def echo_client(sock, client_addr):
'''
Обрабатывает клиентское соединение
'''
print('Got connection from', client_addr)
while True:
msg = sock.recv(65536)
if not msg:
break
sock.sendall(msg)
print('Client closed connection')
sock.close()
def echo_server(addr):
pool = ThreadPoolExecutor(128)
sock = socket(AF_INET, SOCK_STREAM)
sock.bind(addr)
sock.listen(5)
while True:
client_sock, client_addr = sock.accept()
pool.submit(echo_client, client_sock, client_addr)
echo_server(('', 15000))
2.7.2 手动创建线程池
也可以使用
Queue
手动创建线程池,以下是一个示例:
from socket import socket, AF_INET, SOCK_STREAM
from threading import Thread
from queue import Queue
def echo_client(q):
'''
Обрабатывает клиентское соединение
'''
sock, client_addr = q.get()
print('Got connection from', client_addr)
while True:
msg = sock.recv(65536)
if not msg:
break
sock.sendall(msg)
print('Client closed connection')
sock.close()
def echo_server(addr, nworkers):
# Запускает клиентских воркеров
q = Queue()
for n in range(nworkers):
t = Thread(target=echo_client, args=(q,))
t.daemon = True
t.start()
# Запускаем сервер
sock = socket(AF_INET, SOCK_STREAM)
sock.bind(addr)
sock.listen(5)
while True:
client_sock, client_addr = sock.accept()
q.put((client_sock, client_addr))
echo_server(('', 15000), 128)
2.8 简单的并行编程
可以使用
concurrent.futures
库中的
ProcessPoolExecutor
来实现简单的并行编程,以下是一个查找访问
robots.txt
的主机的示例:
import gzip
import io
import glob
from concurrent import futures
def find_robots(filename):
'''
Находит в одном лог-файле все хосты, которые обращались к robots.txt
'''
robots = set()
with gzip.open(filename) as f:
for line in io.TextIOWrapper(f, encoding='ascii'):
fields = line.split()
if fields[6] == '/robots.txt':
robots.add(fields[0])
return robots
def find_all_robots(logdir):
'''
Находит все хосты во всех файлах
'''
files = glob.glob(logdir + '/*.log.gz')
all_robots = set()
with futures.ProcessPoolExecutor() as pool:
for robots in pool.map(find_robots, files):
all_robots.update(robots)
return all_robots
if __name__ == '__main__':
robots = find_all_robots('logs')
for ipaddr in robots:
print(ipaddr)
2.9 处理GIL问题
2.9.1 GIL的影响
全局解释器锁(GIL)限制了Python多线程程序在多核处理器上的性能,对于CPU密集型程序影响较大,但对于I/O密集型程序影响较小。
2.9.2 绕过GIL的策略
-
使用
multiprocessing模块 :可以创建进程池来绕过GIL,以下是一个示例:
import multiprocessing
# Выполняет тяжелые вычисления (завязана на CPU)
def some_work(args):
...
return result
# Поток, который вызывает вышеприведенную функцию
def some_thread():
while True:
...
r = pool.apply(some_work, (args))
...
# Инициализация пула
if __name__ == '__main__':
pool = multiprocessing.Pool()
- 编写C扩展 :可以将计算密集型任务转移到C模块中,并在C代码中释放GIL,以下是一个示例:
#include "Python.h"
...
PyObject *pyfunc(PyObject *self, PyObject *args) {
...
Py_BEGIN_ALLOW_THREADS
// Threaded C code
...
Py_END_ALLOW_THREADS
...
}
综上所述,Python提供了丰富的网络和并发编程工具,通过合理使用这些工具,可以提高程序的性能和可维护性。在实际应用中,需要根据具体的需求和场景选择合适的技术和方法。
3. 技术总结与应用建议
3.1 网络编程技术总结
| 技术点 | 描述 | 适用场景 | 代码示例 |
|---|---|---|---|
| 文件描述符传递 | 通过特定函数实现文件描述符在进程间传递 | 分布式系统中进程间通信 |
recv_fd
和
worker
函数
|
| 事件驱动的输入输出 |
将基本输入输出操作转化为事件处理,通过
select
监控文件描述符活动
| 处理大量并发连接 |
EventHandler
类及相关事件循环代码
|
| 发送和接收大数组 | 使用内存视图减少大数组数据复制 | 分布式计算中大数据传输 |
send_from
和
recv_into
函数
|
3.2 并发编程技术总结
| 技术点 | 描述 | 适用场景 | 代码示例 |
|---|---|---|---|
| 线程的启动和停止 |
使用
threading
库创建、启动、停止线程
| 简单并发任务 |
Thread
类相关操作
|
| 线程间通信 |
使用
Queue
对象实现线程间安全通信
| 生产者 - 消费者模型 |
producer
和
consumer
函数
|
| 关键区域的锁定 |
使用
Lock
对象锁定关键区域避免竞态条件
| 多线程访问共享资源 |
SharedCounter
类
|
| 避免死锁的锁定 | 使用上下文管理器确保线程按顺序获取多个锁 | 多线程需要获取多个锁的场景 |
acquire
函数
|
| 线程特定状态的存储 |
使用
threading.local()
存储线程特定状态
| 多线程操作共享对象时避免冲突 |
LazyConnection
类
|
| 线程池的创建 |
使用
ThreadPoolExecutor
或手动创建线程池
| 服务多个客户端或执行大量任务 |
echo_server
函数
|
| 简单的并行编程 |
使用
ProcessPoolExecutor
实现并行编程
| CPU密集型任务 |
find_all_robots
函数
|
| 处理GIL问题 |
使用
multiprocessing
模块或编写C扩展绕过GIL
| CPU密集型多线程程序 |
multiprocessing.Pool
和C代码示例
|
3.3 应用建议
-
网络编程
:
-
对于处理大量并发连接的场景,优先考虑事件驱动的输入输出技术,如使用
EventHandler和事件循环。 - 在进行大数据传输时,使用内存视图来减少数据复制,提高传输效率。
-
对于处理大量并发连接的场景,优先考虑事件驱动的输入输出技术,如使用
-
并发编程
:
- 对于I/O密集型任务,使用线程池可以有效提高程序性能。
-
对于CPU密集型任务,使用
ProcessPoolExecutor进行并行编程,避免GIL的影响。 - 在多线程访问共享资源时,务必使用锁机制来避免竞态条件,同时注意避免死锁。
4. 技术流程梳理
4.1 事件驱动输入输出流程
graph TD;
A[初始化事件处理程序列表] --> B[进入事件循环];
B --> C[筛选需要接收数据的处理程序];
B --> D[筛选需要发送数据的处理程序];
C --> E[使用select监控可接收数据的处理程序];
D --> F[使用select监控可发送数据的处理程序];
E --> G[对可接收数据的处理程序调用handle_receive方法];
F --> H[对可发送数据的处理程序调用handle_send方法];
G --> B;
H --> B;
4.2 线程池处理客户端连接流程
graph TD;
A[启动线程池] --> B[创建socket并绑定地址];
B --> C[开始监听客户端连接];
C --> D[接受客户端连接];
D --> E[将客户端连接任务提交到线程池];
E --> C;
4.3 并行编程处理流程
graph TD;
A[初始化ProcessPoolExecutor] --> B[获取任务数据];
B --> C[将任务提交到进程池];
C --> D[进程池并行处理任务];
D --> E[收集处理结果];
E --> F[合并处理结果];
5. 常见问题及解决方案
5.1 网络编程常见问题
-
问题
:事件驱动输入输出中某个处理程序阻塞,导致整个程序停止。
-
解决方案
:将可能阻塞的操作发送到单独的线程或进程中处理,如使用
ThreadPoolHandler。
-
解决方案
:将可能阻塞的操作发送到单独的线程或进程中处理,如使用
-
问题
:发送和接收大数组时,接收方不知道数据大小。
- 解决方案 :发送方先发送数据大小,接收方根据大小分配数组。
5.2 并发编程常见问题
-
问题
:多线程访问共享资源时出现竞态条件。
-
解决方案
:使用
Lock、RLock等锁对象锁定关键区域。
-
解决方案
:使用
-
问题
:多线程获取多个锁时出现死锁。
-
解决方案
:使用上下文管理器确保线程按顺序获取锁,如
acquire函数。
-
解决方案
:使用上下文管理器确保线程按顺序获取锁,如
-
问题
:GIL影响CPU密集型多线程程序性能。
-
解决方案
:使用
multiprocessing模块创建进程池或编写C扩展绕过GIL。
-
解决方案
:使用
6. 总结
Python的网络编程和并发编程提供了丰富的工具和技术,能够满足不同场景下的需求。在网络编程中,事件驱动的输入输出和大数组传输技术可以提高程序的并发处理能力和数据传输效率。在并发编程中,线程和进程的合理使用以及各种同步机制的应用可以确保程序的正确性和性能。同时,我们也需要注意GIL对CPU密集型多线程程序的影响,并采取相应的绕过策略。通过深入理解和掌握这些技术,我们可以编写出高效、稳定的Python程序。
在实际应用中,我们应该根据具体的需求和场景选择合适的技术和方法,同时注意避免常见问题的出现。希望本文介绍的技术和方法能够帮助你更好地进行Python网络编程和并发编程。
超级会员免费看
2万+

被折叠的 条评论
为什么被折叠?



