Python的Socket编程

前言

  • 先将使用过程需要说明的要点记录下:
    • socket.socket() 创建了一个 socket 对象,并且支持 context manager type,使用 with 语句,这样就不用再手动调用 s.close() 来关闭 socket
    • 1024 是缓冲区数据大小限制最大值参数 bufsize
    • 标准库中的selectors 模块提供了高层且高效的 I/O 多路复用,基于原始的 select 模块构建
    • asyncio 使用单线程来处理多任务,使用事件循环来管理任务。通过使用 select(),可以创建自己的事件循环,更简单且同步化。当使用多线程时,要处理并发的情况,不得不面临使用 CPython 或者 PyPy 中的「全局解析器锁 GIL」,这有效地限制了可以并行完成的工作量。
    • 从 Python 3.3 开始,与 socket 或地址语义相关的错误会引发 OSError 或其子类之一的异常引用
    • 需要有不同的网络缓冲区,使用 recv() 方法不断的从缓冲区中读取数据,直到你的应用确定读取到了足够的数据
    • 一个通用的方案,很多协议都会用到它,包括 HTTP。在每条消息前面追加一个头信息,头信息中包括消息的长度和其它需要的字段。这样做的话我们只需要追踪头信息,当读到头信息时,就可以查到消息的长度并且读出所有字节然后使用它。

单客户端/单服务器模式

在这里插入图片描述

# client.py
import socket
host="127.0.0.1"
port=65432
with socket.socket(socket.AF_INET,socket.SOCK_STREAM) as sock:
    sock.connect((host,port)) #三次握手
    sock.sendall(b"hello world!")
    data = sock.recv(1024)
print("Recieved ",repr(data))
  • 单服务器
# server.py
import socket
host="127.0.0.1"
port=65432
with socket.socket(socket.AF_INET,socket.SOCK_STREAM) as sock:
    sock.bind((host,port))
    sock.listen()
    conn,addr = sock.accept()#阻塞,等待被连接
    with conn:
        print("Connected by ",addr)
        while True:
            data = conn.recv(1024)
            print(data)
            if not data:
                break
            conn.sendall(data)

多客户端/多服务器模式

# mult-client.py
import sys
import socket
import selectors
import types

sel = selectors.DefaultSelector()
messages = [b"Message 1 from client.", b"Message 2 from client."]


def start_connections(host, port, num_conns):
    server_addr = (host, port)
    for i in range(0, num_conns):
        connid = i + 1
        print("starting connection", connid, "to", server_addr)
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.setblocking(False)
        sock.connect_ex(server_addr)
        events = selectors.EVENT_READ | selectors.EVENT_WRITE
        data = types.SimpleNamespace(
            connid=connid,
            msg_total=sum(len(m) for m in messages),
            recv_total=0,
            messages=list(messages),
            outb=b"",
        )
        sel.register(sock, events, data=data)


def service_connection(key, mask):
    sock = key.fileobj
    data = key.data
    if mask & selectors.EVENT_READ:
        recv_data = sock.recv(1024)  # Should be ready to read
        if recv_data:
            print("received", repr(recv_data), "from connection", data.connid)
            data.recv_total += len(recv_data)
        if not recv_data or data.recv_total == data.msg_total:
            print("closing connection", data.connid)
            sel.unregister(sock)
            sock.close()
    if mask & selectors.EVENT_WRITE:
        if not data.outb and data.messages:
            data.outb = data.messages.pop(0)
        if data.outb:
            print("sending", repr(data.outb), "to connection", data.connid)
            sent = sock.send(data.outb)  # Should be ready to write
            data.outb = data.outb[sent:]


if len(sys.argv) != 4:
    print("usage:", sys.argv[0], "<host> <port> <num_connections>")
    sys.exit(1)

host, port, num_conns = sys.argv[1:4]
start_connections(host, int(port), int(num_conns))

try:
    while True:
        events = sel.select(timeout=1)
        if events:
            for key, mask in events:
                service_connection(key, mask)
        # Check for a socket being monitored to continue.
        if not sel.get_map():
            break
except KeyboardInterrupt:
    print("caught keyboard interrupt, exiting")
finally:
    sel.close()
  • 服务器端
import sys
import socket
import selectors
import types

sel = selectors.DefaultSelector()


def accept_wrapper(sock):
    conn, addr = sock.accept()  # Should be ready to read
    print("accepted connection from", addr)
    # 调用 sock.accept() 后立即再立即调 conn.setblocking(False) 来让 socket 进入非阻塞模式
    conn.setblocking(False)
    # 使用了 types.SimpleNamespace 类创建了一个对象用来保存我们想要的 socket 和数据
    data = types.SimpleNamespace(addr=addr, inb=b"", outb=b"")
    events = selectors.EVENT_READ | selectors.EVENT_WRITE
    sel.register(conn, events, data=data)

def service_connection(key, mask):
    # key 包含了 socket 对象「fileobj」和数据对象;
    sock = key.fileobj
    data = key.data
    # mask 包含了就绪的事件
    if mask & selectors.EVENT_READ:
        '''
        如果 socket 就绪而且可以被读取,mask & selectors.EVENT_READ 就为真,sock.recv() 会被调用。
        所有读取到的数据都会被追加到 data.outb 里面
        '''
        recv_data = sock.recv(1024)  # Should be ready to read
        if recv_data:
            data.outb += recv_data
        else:
        '''
        如果没有收到任何数据,表示客户端关闭了它的 socket 连接,这时服务端也应该关闭自己的连接。
        先调用 sel.unregister() 来撤销 select() 的监控
        '''
            print("closing connection to", data.addr)
            sel.unregister(sock)
            sock.close()
    if mask & selectors.EVENT_WRITE:
        if data.outb:
            print("echoing", repr(data.outb), "to", data.addr)
            # 任何接收并被 data.outb 存储的数据都将使用 sock.send() 方法打印出来
            sent = sock.send(data.outb)  # Should be ready to write
            data.outb = data.outb[sent:]


if len(sys.argv) != 3:
    print("usage:", sys.argv[0], "<host> <port>")
    sys.exit(1)

host, port = sys.argv[1], int(sys.argv[2])
lsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
lsock.bind((host, port))
lsock.listen()
print("listening on", (host, port))
lsock.setblocking(False)    # 配置 socket 为非阻塞模式
# 使用 selectors.EVENT_READ 读取到事件
# 注册 socket 监控
# data 来跟踪 socket 上发送或者接收的东西
sel.register(lsock, selectors.EVENT_READ, data=None)

try:
    while True:
        events = sel.select(timeout=None)   # 阻塞直到 socket I/O 就绪
        """
        sel.select(timeout=None)返回一个(key, events) 元组;
        key 包含了 socket 对象「fileobj」和数据对象;
        mask 包含了就绪的事件
        """
        for key, mask in events:
            # 新的客户端, 利用accept_wrapper()来接受新的 socket 对象并注册到 selector 上
            if key.data is None:
                accept_wrapper(key.fileobj) #key.fileobj=socket对象
            # 如果 key.data 不为空,旧的被接受的客户端,我们需要为它服务,接着 service_connection() 会传入 key 和 mask 参数并调用
            else:
                service_connection(key, mask)
except KeyboardInterrupt:
    print("caught keyboard interrupt, exiting")
finally:
    sel.close()

参考链接

Python网络编程(第3版)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章