socket编程 netstat 大小端 rpc 协程 io yield

socket编程

客户端

角色:主动发起请求的一方。

特点:运行在用户本地(比如浏览器、App、桌面程序)。

需要通过网络连接到服务端。主要负责 界面展示 和 用户交互。

例子:

浏览器访问网页 → 浏览器就是客户端。

微信 App → 向服务器发消息的就是客户端。

服务端Server

角色:接收和响应请求的一方。

特点:

通常运行在远程服务器(数据中心、云端)。

提供 数据处理、业务逻辑、存储 等服务。

等待客户端的请求,然后返回结果。

例子:

网站的 Web 服务器(Nginx、Apache)。

游戏后台服务器,负责玩家状态同步。

微信的消息服务器。

工作过程(举例)

客户端(浏览器)向 服务端(网站服务器)发送 HTTP 请求:

"请给我 index.html 页面"

服务端 接收到请求,在磁盘/数据库里找到页面,返回给客户端。

客户端 收到响应,把页面展示出来。

server.py

IP 端口

这里的 IP 地址 是用来告诉程序:这个"服务器程序"要监听哪一个网卡的网络地址。

如果你写成空字符串 '',意思是:

"不挑网卡,所有本机的 IP 地址都监听"。

如果只写 IP='127.0.0.1',那服务端只接受本机的访问,外网别人连不上

  1. 什么是 IP?

想象你的电脑是一栋大楼。

IP 地址就是这栋楼的"楼地址"。

一台电脑可能有多个 IP,比如:

有线网卡:192.168.1.10

无线网卡:192.168.1.11

回环地址:127.0.0.1(作用:让你的电脑跟"自己"通信,只能自己跟自己)

  1. 什么是端口?

PORT = 50000

端口号就像 门牌号。

一台电脑上同时可以跑很多服务(例如浏览器、微信、FTP 服务...),它们靠端口号来区分。

50000 就是你给这个 TCP 服务起的"门牌号"。

客户端要连这个服务,就要知道:

👉 "你的电脑 IP 地址 + 端口号 50000"。

listenSocket.bind((IP, PORT))


listen(8)


accept










总结


recved = dataSocket.recv

接受来自客户端的消息。

缓冲区指:操作系统维护的 TCP 接收缓冲区


dataSocket.send

是服务端把一条消息回发给客户端。

网络传输的数据必须是 字节(bytes),不能直接传字符串




dataSocket.close() listenSocket.close()


client.py

ip 端口

IP = '127.0.0.1' # 服务器 IP(127.0.0.1 表示自己本机)

SERVER_PORT = 50000 # 服务器端口

BUFLEN = 1024 # 定义一次从socket缓冲区最多读入512个字节数据

socket(AF_INET, SOCK_STREAM)

服务端客户端用的协议要一样

创建 socket(电话机)

connect

连接服务器(拨号)

dataSocket.connect((IP, SERVER_PORT))

recv

缓冲区指:操作系统维护的 TCP 接收缓冲区



send


close

服务端 客户端send, recv



服务端和客户端的close都在while外怎么调到

客户端不会主动结束,它只会在 recv() 时发现服务端断开了(返回空字节串 b''),才会 break,然后执行 close()。

服务端同理,它也不会主动结束,只有当客户端断开时,recv() 返回空字节串,才会 break,然后 close()。

但是客户端里有一个exit的判断,如果用户输入exit,那么客户端会退出,紧接着,服务端收到客户端退出,服务端也退出

进程间通信




netstat



netstat输出详解

Local Address:

这个进程监听本机哪个ip的哪个端口

Foreign Address (远程地址)

例子

进程 是怎么知道绑定哪个端口并监听的




程序本身不会"自动知道"该监听哪个端口,都是程序员写在代码里,或者用户在配置文件里指定的。

不是每个进程都需要端口

一个端口只能有一个进程监听,一个进程可以监听多个端口



刚刚的客户端和服务端运行起来就是2个进程


运行

1、首先 看下有没有监听50000的端口

2、把服务端运行起来

3、在运行下netstat

4、运行客户端

5、此时服务端已经打印了内容

6、查看netstat,有3个

服务端启动后会有好几个socket,一个表示正在监听的socket,一个表示收发数据的socket




7、客户端输入消息

根据代码 客户端输入 exit,客户端会退出

也可以在命令行运行

应用消息格式

因为 TCP协议传输的是 字节流(bytes stream), 如果消息中没有指定 边界 或者 长度,接收方就不知道一个完整的消息从字节流的 哪里开始,到 哪里结束。

因为send是先发到缓冲区

示例1

客户端(AT)和服务端(RUS)之间传送 JSON 消息,但要用"长度+换行+消息体"的格式封装.

传输流程

**RUS(服务端)**定时发 report(报告 CPU/Mem 等信息)

**AT(客户端)**收到后必须回 report-ack(确认收到)

AT 还可以发 pause/resume 指令,RUS 收到后执行并回 cmd-ack

双方保持长连接,如果断开,客户端要重连

复制代码
# server_rus.py
import inspect
import json
import socket
import threading
import time

HOST = ""          # 绑定到本机所有网卡
PORT = 50000       # 服务端监听端口
REPORT_INTERVAL = 2.0  # 上报间隔:2 秒
LF = b"\n"         # 消息头和消息体之间的分隔符(换行符)
# =========================
# 工具函数:发送一条消息
# =========================
# socket.socket 表示的就是 socket 模块里的 socket 类
# sock: socket.socket → 表示 sock 参数应该是一个 socket 对象
def send_msg(sock: socket.socket, body_obj: dict):
    # 将消息体字典转成 JSON,编码为 UTF-8 字节串
    body = json.dumps(body_obj, ensure_ascii=False).encode("utf-8")
    # 计算长度,形成消息头:长度字符串 + 换行符
    header = str(len(body)).encode("utf-8") + LF
    # 发送完整消息(头+体)
    sock.sendall(header + body)

# =========================
# 工具函数:接收指定长度字节
# =========================
def recv_exact(sock: socket.socket, n: int) -> bytes:
    buf = bytearray()
    while len(buf) < n:
        chunk = sock.recv(n - len(buf))  # 一次可能收不到全部
        if not chunk:
            raise ConnectionError("peer closed")
        buf += chunk
    """
    API 规范:在网络编程中,通常约定 recv_* 系列函数返回 不可变的 bytes,这样外部使用时不会误修改。
    (比如 Python 自带的 socket.recv() 就返回 bytes)
    
    安全性:如果直接把 bytearray 返回,调用方可能改它的内容,结果影响到你以为已经"固定"的数据。
    转换成 bytes 后,这个数据就是只读的,保证了稳定性。
    """
    return bytes(buf)

# =========================
# 工具函数:接收一条消息
# =========================
def recv_msg(sock: socket.socket) -> dict:
    # 先读消息头:直到读到 LF(0x0A)
    # 创建一个空的可变字节序列
    header_bytes = bytearray()
    while True:
        b = sock.recv(1) # 一次读1个字节
        if not b:
            raise ConnectionError("peer closed")
        if b == LF:
            break
        header_bytes += b
    # 消息头就是 body 的长度(十进制)
    body_len = int(header_bytes.decode("utf-8").strip())
    # 按长度读完整消息体
    body = recv_exact(sock, body_len)
    # 解析 JSON
    return json.loads(body.decode("utf-8"))

# =========================
# 客户端连接的处理逻辑
# =========================
def handle_client(conn: socket.socket, addr):
    print(f"[RUS] client connected from {addr}")
    paused_until = 0.0  # 记录暂停结束时间
    """
    线程启动后,就会一直在这个 while 循环里运行。

    线程的生命周期 = 这个函数的执行过程。
    
    当 while 循环 条件不满足,函数执行到头,函数自然返回 → 线程就结束。
    
    换句话说:
    线程退出 = 线程运行的函数结束。
    daemon=True 解决的是 "进程退出时,子线程也要跟着退出"。

    stop 解决的是 "在进程还在运行时,我要提前停止某个子线程"。
    """
    stop = False        # 控制 reporter 线程退出

    # 一个子线程:定时上报数据
    def reporter():
        # nonlocal 是一个关键字,
        # 用来在嵌套函数(函数里面再定义函数)的情况下,
        # 声明某个变量不是局部变量,而是"外层函数作用域"的变量。
        nonlocal stop, paused_until
        while not stop:
            now = time.time()
            # 如果没有在暂停期,就发一条 report
            if now >= paused_until:
                payload = {
                    "type": "report",
                    "info": {"CPU Usage": "30%", "Mem usage": "53%"}
                }
                try:
                    send_msg(conn, payload)
                except Exception as e:
                    print("[RUS] send report failed:", e)
                    break
            time.sleep(REPORT_INTERVAL)

    # 启动上报线程
    t = threading.Thread(target=reporter, daemon=True)
    t.start()

    try:
        # 主循环:接收客户端消息
        while True:
            msg = recv_msg(conn)
            mtype = msg.get("type")
            if mtype == "report-ack":
                print("[RUS] got report-ack")
            elif mtype == "pause":
                duration = int(msg.get("duration", 0))
                paused_until = time.time() + max(0, duration)
                send_msg(conn, {"type": "cmd-ack", "code": 200, "info": "处理成功"})
            elif mtype == "resume":
                paused_until = 0.0
                send_msg(conn, {"type": "cmd-ack", "code": 200, "info": "处理成功"})
            else:
                send_msg(conn, {"type": "cmd-ack", "code": 400, "info": f"未知类型: {mtype}"})
    except Exception as e:
        print("[RUS] connection closed:", e)
    finally:
        stop = True
        conn.close()
        print(f"[RUS] client {addr} disconnected")

# =========================
# 主函数:监听并接收连接
# =========================
def main():
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        # SO_REUSEADDR = 允许端口在 TIME_WAIT 状态时被快速重用
        s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        s.bind((HOST, PORT))
        s.listen(8)
        print(f"[RUS] listening on {PORT} ...")
        while True:
            conn, addr = s.accept()
            # 每个客户端一个线程
            """
            为什么要用线程?

            因为一个服务器可能会有多个客户端同时连接。
            
            如果不用线程,服务端会一直卡在 handle_client,只能服务一个客户端。
            
            加上线程,每来一个客户端,就开一个新线程专门负责它;主线程继续去 accept() 等待其他客户端。
            """
            ss = threading.Thread(target=handle_client, args=(conn, addr), daemon=True)
            print("打个断点")
            # 服务端打断点进不去handle_client,不知道为什么
            ss.start()

if __name__ == "__main__":
    main()

# client_at.py
import json
import socket
import time

HOST = "127.0.0.1"  # 服务端地址
PORT = 50000        # 服务端端口
LF = b"\n"

# =========================
# 工具函数:发送消息
# =========================
def send_msg(sock: socket.socket, body_obj: dict):
    body = json.dumps(body_obj, ensure_ascii=False).encode("utf-8")
    header = str(len(body)).encode("utf-8") + LF
    sock.sendall(header + body)

# =========================
# 工具函数:接收指定长度字节
# =========================
def recv_exact(sock: socket.socket, n: int) -> bytes:
    buf = bytearray()
    while len(buf) < n:
        chunk = sock.recv(n - len(buf))
        if not chunk:
            raise ConnectionError("peer closed")
        buf += chunk
    return bytes(buf)

# =========================
# 工具函数:接收一条消息
# =========================
def recv_msg(sock: socket.socket) -> dict:
    header_bytes = bytearray()
    while True:
        b = sock.recv(1)
        if not b:
            raise ConnectionError("peer closed")
        if b == LF:
            break
        header_bytes += b
    body_len = int(header_bytes.decode("utf-8").strip())
    body = recv_exact(sock, body_len)
    return json.loads(body.decode("utf-8"))

# =========================
# 一次完整的会话(断线退出)
# =========================
def run_once():
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.connect((HOST, PORT))
        print("[AT] connected")

        # 连接后先发 resume 确保开始上报
        # send_msg(s, {"type": "resume"})

        last_cmd_time = 0
        while True:
            msg = recv_msg(s)
            mtype = msg.get("type")
            if mtype == "report":
                info = msg.get("info", {})
                print("[AT] report:", info)
                # 回 ACK
                send_msg(s, {"type": "report-ack"})
                # 每 10 秒发一个 pause,然后 resume
                now = time.time()
                if now - last_cmd_time > 10:
                    send_msg(s, {"type": "pause", "duration": 2})
                    last_cmd_time = now
            elif mtype == "cmd-ack":
                print("[AT] cmd-ack:", msg.get("code"), msg.get("info"))
            else:
                print("[AT] unknown message:", msg)

# =========================
# 主循环:断线自动重连
# =========================
def main():
    backoff = 1
    while True:
        try:
            run_once()
        except Exception as e:
            print("[AT] disconnected:", e)
            time.sleep(backoff)
            # 退避重连,最多等 10 秒
            backoff = min(backoff * 2, 10)

if __name__ == "__main__":
    main()

setsockopt = set socket option


header_bytes = bytearray()


示例2


复制代码
# server2.py
import socket       # 网络通信用
import struct       # 打包/解包二进制数据(大小端、字节序)

HOST: str = ''      # 监听所有网络接口(空字符串 = 任意 IP 地址)
PORT: int = 50000   # 服务端端口号

# 消息类型常量(方便代码里用名字而不是魔法数字)
TYPE_PAUSE: int = 0
TYPE_RESUME: int = 1
TYPE_CMD_ACK: int = 2
TYPE_REPORT: int = 3
TYPE_REPORT_ACK: int = 4

# 属性 ID(类似于"字段编号")
ATTR_CPU: int = 1
ATTR_MEM: int = 2
# 可以继续扩展,比如 ATTR_DISK = 3 等

# -------------------- 消息打包 --------------------
def encode_message(msg_type: int, body_items: list[tuple[int, bytes]]) -> bytes:
    """
    将一条消息编码成字节流
    msg_type: 消息类型
    body_items: 消息体字段,格式 [(attr_id, value_bytes), ...]

    消息格式:
    [2字节: body_len][1字节: msg_type][body_len字节: body]

    body 格式:
    重复出现 (attr_id:1字节, length:1字节, value: length字节)
    """
    body = b''
    for attr_id, val in body_items:
        length: int = len(val)
        if length > 255:
            raise ValueError("单个值长度不能超过255")
        # 按协议拼接:属性ID(1B) + 长度(1B) + 值
        body += struct.pack('!B', attr_id)  # !B = 大端无符号1字节
        body += struct.pack('!B', length)
        body += val

    # 消息头:body 长度(2B,大端) + 消息类型(1B)
    body_len: int = len(body)
    header: bytes = struct.pack('!H', body_len) + struct.pack('!B', msg_type)

    return header + body


# -------------------- 消息解包 --------------------
def decode_message(data: bytes) -> tuple[int, list[tuple[int, bytes]]]:
    """
    解码收到的完整消息
    返回: (msg_type, body_items)
    """
    if len(data) < 3:
        raise ValueError("消息太短,无法包含头部")

    body_len: int = struct.unpack('!H', data[0:2])[0]
    msg_type: int = data[2]

    if len(data) < 3 + body_len:
        raise ValueError("消息体不完整")

    body: bytes = data[3:3+body_len]
    items: list[tuple[int, bytes]] = []
    idx: int = 0

    while idx < len(body):
        attr_id: int = body[idx]
        length: int = body[idx + 1]
        val: bytes = body[idx + 2 : idx + 2 + length]
        items.append((attr_id, val))
        idx += 2 + length

    return msg_type, items


# -------------------- 客户端处理 --------------------
def handle_client(conn: socket.socket, addr: tuple[str, int]) -> None:
    """
    处理单个客户端连接
    conn: 客户端套接字
    addr: 客户端地址 (IP, port)
    """
    print(f"客户端连接:{addr}")
    try:
        while True:
            # 1. 先读消息头 (3字节)
            header: bytes = conn.recv(3)
            if not header:
                print(f"客户端 {addr} 关闭连接")
                break
            if len(header) < 3:
                print("头部接收不全,断开")
                break
            # 用 struct 模块把二进制字节流解包成整数
            # ! = network order(大端序)
            # H = unsigned short,占 2 字节
            # 注意它返回的是元组 (300,),因为 unpack 可以同时解多个值。
            # 从头部的前 2 个字节里,用大端序解出一个 16 位无符号整数,赋值给 body_len
            body_len: int = struct.unpack('!H', header[0:2])[0]
            # 只有一个字节不涉及谁先谁后
            msg_type: int = header[2]

            # 2. 再读消息体
            body: bytes = b''
            remain: int = body_len
            while remain > 0:
                chunk: bytes = conn.recv(remain)
                if not chunk:
                    break
                body += chunk
                """
                bytes 本质上就是一个 "定长的字节数组"。

                每个元素占 1 个字节。
                
                len() 返回的就是"数组里有多少个元素"。
                
                所以 len(b"ABC") == 3。
                len(字节串) 返回的是字节数,因为 bytes 就是"由很多字节组成的数组"
                """
                remain -= len(chunk)

            if len(body) < body_len:
                print("消息体接收不全,断开")
                break

            # 3. 解析消息体
            items: list[tuple[int, bytes]] = []
            idx: int = 0
            while idx < len(body):
                """
                在 Python 里,bytes 是一组二进制数,每个元素是 0~255 的整数:

                b = b"A"   # ASCII 'A' 的编码是 65
                print(b[0])    # 65 (int)
                
                
                即使你觉得它是"字符 A",Python 访问 b[0] 也只会给你 整数 65。
                单字节在 bytes 里本质就是数字(0--255)。

                如果你当它是"编号/长度",就存 int。
                
                如果你当它是"字符",就先取 int,再用 chr() 或 decode() 转成 str
                """
                attr_id: int = body[idx]
                length: int = body[idx + 1]
                val: bytes = body[idx + 2 : idx + 2 + length]
                items.append((attr_id, val))
                idx += 2 + length

            # 4. 打印收到的消息
            print(f"收到类型 {msg_type} 的消息,内容:{items}")

            # 5. 回复 ACK(确认)
            ack_msg: bytes = encode_message(TYPE_REPORT_ACK, [])
            conn.sendall(ack_msg)

    finally:
        conn.close()


# -------------------- 主函数 --------------------
def main() -> None:
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.bind((HOST, PORT))
        s.listen(5)
        print(f"服务端启动,在端口 {PORT}")
        while True:
            conn, addr = s.accept()
            # 简单起见:单线程处理客户端
            handle_client(conn, addr)


if __name__ == "__main__":
    main()



# client2.py
import socket   # 提供 TCP 网络通信
import struct   # 用于打包/解包二进制数据(支持大端、小端)
import time     # 用于 sleep

# ---------------- 配置 ----------------
HOST: str = '127.0.0.1'   # 服务端 IP(本机)
PORT: int = 50000         # 服务端端口

# ---------------- 消息类型 & 属性定义 ----------------
TYPE_REPORT: int = 3      # 消息类型:上报

ATTR_CPU: int = 1         # 属性 ID:CPU 使用率
ATTR_MEM: int = 2         # 属性 ID:内存使用率


# ---------------- 编码函数 ----------------
def encode_message(msg_type: int, body_items: list[tuple[int, bytes]]) -> bytes:
    """
    将 (msg_type, body_items) 编码成二进制消息
    - msg_type: 消息类型
    - body_items: [(attr_id, value_bytes), ...]
    """
    body: bytes = b''
    for attr_id, val in body_items:
        length: int = len(val)
        if length > 255:
            raise ValueError("单个值长度不能超过255 (因为长度字段是1字节)")
        # 拼接属性 (attr_id:1字节, length:1字节, value:长度字节)
        body += struct.pack('!B', attr_id)  # 属性 ID (1B, 无符号)
        body += struct.pack('!B', length)   # 值长度 (1B)
        body += val                         # 值本身 (字节串)
    # 消息头: body_len(2B) + msg_type(1B)
    body_len: int = len(body)
    header: bytes = struct.pack('!H', body_len) + struct.pack('!B', msg_type)
    return header + body


# ---------------- 解码函数 ----------------
def decode_message(data: bytes) -> tuple[int, list[tuple[int, bytes]]]:
    """
    解码一条消息
    返回 (msg_type, items)
    """
    if len(data) < 3:
        raise ValueError("消息太短")

    body_len: int = struct.unpack('!H', data[0:2])[0]  # 前2字节 = 消息体长度
    msg_type: int = data[2]                            # 第3字节 = 消息类型

    if len(data) < 3 + body_len:
        raise ValueError("消息体不完整")

    body: bytes = data[3:3+body_len]
    items: list[tuple[int, bytes]] = []
    idx: int = 0

    while idx < len(body):
        attr_id: int = body[idx]              # 属性 ID
        length: int = body[idx + 1]           # 值长度
        val: bytes = body[idx+2:idx+2+length] # 值(字节串)
        items.append((attr_id, val))
        idx += 2 + length

    return msg_type, items


# ---------------- 主函数 ----------------
def main() -> None:
    # 建立 TCP 连接
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.connect((HOST, PORT))  # 连接到服务端

        # 连续发 3 次测试数据
        for i in range(3):
            # 造一些 CPU / MEM 的模拟数据 (30%, 50% → 32%, 52%)
            cpu_usage: bytes = f"{30 + i}%".encode('ascii')
            mem_usage: bytes = f"{50 + i}%".encode('ascii')

            # 组装 body_items 列表
            body_items: list[tuple[int, bytes]] = [
                (ATTR_CPU, cpu_usage),
                (ATTR_MEM, mem_usage)
            ]

            # 打包成二进制消息
            msg: bytes = encode_message(TYPE_REPORT, body_items)

            # 发送给服务端
            s.sendall(msg)
            print(f"已发送报告消息: CPU={cpu_usage}, MEM={mem_usage}")

            # ---------------- 等待服务端回应 ----------------
            # 先收头部 (3字节)
            header: bytes = s.recv(3)
            if not header:
                print("没收到头部,断开")
                break
            if len(header) < 3:
                print("头部不足")
                break

            # 解析头部
            body_len: int = struct.unpack('!H', header[0:2])[0]
            msg_type: int = header[2]

            # 收消息体
            body: bytes = b''
            remain: int = body_len
            while remain > 0:
                chunk: bytes = s.recv(remain)
                if not chunk:
                    break
                body += chunk
                remain -= len(chunk)

            print(f"收到回应类型: {msg_type}, 内容 body={body}")

            # 每次发完,休眠 1 秒
            time.sleep(1)


# ---------------- 程序入口 ----------------
if __name__ == "__main__":
    main()

大小端





网络发数据,本质是把内存的字节顺着地址顺序(低地址-》高地址)拿出来发,给到字节流的第一个字节,第二个字节等,接收方接收到字节流,从第一个字节开始读,但是网络我们一般是大端序,所以如果机器是小端机器,直接从内存里拿出来的顺序是不能发的,后续有讲解


































































单字节









框架直接接收了


RPC(Remote Procedure Call,远程过程调用)




支持多个tcp客户端

python 复制代码
# 方案 A:捕获 Ctrl+C(KeyboardInterrupt)
try:
    while True:
        dataSocket, addr = listenSocket.accept()
        Thread(target=clientHandler, args=(dataSocket, addr), daemon=True).start()
except KeyboardInterrupt:
    print('\n收到退出信号,准备关闭...')
finally:
    listenSocket.close()
python 复制代码
# 方案 C:用上下文管理器,离开 with 自动 close
# 比如你按下 Ctrl+C,Python 抛出 KeyboardInterrupt 异常,打破循环。
# 或者 accept() 抛出别的异常。

with 会捕捉到"离开代码块"的动作,自动执行 close
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as listenSocket:
    listenSocket.bind(('', 50000))
    listenSocket.listen(8)
    while True:
        dataSocket, addr = listenSocket.accept()
        Thread(target=clientHandler, args=(dataSocket, addr), daemon=True).start()
# 走到这里 socket 已自动关闭
python 复制代码
# 方案 B:设置超时 + 外部停止条件
import threading, socket
stop = threading.Event()
"""
stop = threading.Event()

threading.Event() 可以理解成一个小旗子,初始状态是没举旗(False)。

任何线程都可以检查 stop.is_set() 来看旗子是不是举起来了。

别的线程也可以随时调用 stop.set() 把旗子举起来,表示"该停了"。

👉 你可以把它理解成一个全局的"停止信号"。
"""
listenSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listenSocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
listenSocket.bind(('', 50000))
listenSocket.listen(8)
listenSocket.settimeout(1.0)  # 每秒醒一次,看看是否要退出
"""
accept() 默认会一直等,直到有客户端连接。

加上 settimeout(1.0),就变成:

最多等 1 秒,如果没人连,就抛一个 timeout 异常
"""
try:
    while not stop.is_set():
    """
    while not stop.is_set():

    这就是循环条件。

     not stop.is_set() 的意思是:只要停止信号还没被设置,就一直循环。

     当别的地方调用 stop.set() → stop.is_set() 变成 True → 循环条件 not        True = False → 跳出循环。
    """
        try:
            dataSocket, addr = listenSocket.accept()
        except socket.timeout:
            continue
        Thread(target=clientHandler, args=(dataSocket, addr), daemon=True).start()
finally:
    listenSocket.close()

协程


io密集型 在io bound章节有介绍。

遇到 await,协程就 主动暂停,让事件循环去执行别的协程




await







遇到await,当前协程暂停,执行await后跟着的协程,事件循环调度能执行的协程

事件循环run




简单例子

https://liaoxuefeng.com/books/python/async-io/asyncio/index.html

python 复制代码
import asyncio

async def hello():
    print("Hello world!")
    # 异步调用asyncio.sleep(1):
    await asyncio.sleep(1)
    print("Hello again!")

asyncio.run(hello())



例子2
python 复制代码
import asyncio
import threading

async def hello(name):
    print(f"Hello {name}! ({threading.current_thread().name})")
    await asyncio.sleep(1)   # 模拟IO等待
    print(f"Hello {name} again! ({threading.current_thread().name})")
    return name

async def background():
    for i in range(5):
        await asyncio.sleep(0.2)   # 每0.2秒执行一次
        print(f"background tick {i} ({threading.current_thread().name})")

async def main():
    # 同时运行 hello("Bob"), hello("Alice"), background()
    results = await asyncio.gather(
        hello("Bob"),
        hello("Alice"),
        background()
    )
    print("gather results:", results)

asyncio.run(main())





asyncio.gather(...)


等待io在等待io有介绍

例子3
python 复制代码
import asyncio

async def wget(host):
    print(f"wget {host}...")
    # 连接80端口:
    reader, writer = await asyncio.open_connection(host, 80)
    # 发送HTTP请求:
    header = f"GET / HTTP/1.0\r\nHost: {host}\r\n\r\n"
    writer.write(header.encode("utf-8"))
    await writer.drain()

    # 读取HTTP响应:
    while True:
        """reader 是 asyncio 提供的 StreamReader 对象。
		   reader.readline() 返回一个 协程对象(coroutine),它的作用是:
			👉 一直等到从网络里读到"一整行"(直到 \n),再把这一行返回。
		"""
        line = await reader.readline()
        """
        在 HTTP 里:
        每一行必须以 回车+换行 (\r\n) 结尾。
	    所以"空行"的字节形式就是:b"\r\n"。
		这个空行就是 "头部结束,正文开始" 的标志
        """
        if line == b"\r\n":
            break
        print("%s header > %s" % (host, line.decode("utf-8").rstrip()))
    # Ignore the body, close the socket
    writer.close()
    await writer.wait_closed()
    print(f"Done {host}.")

async def main():
    await asyncio.gather(wget("www.sina.com.cn"), wget("www.sohu.com"), wget("www.163.com"))

asyncio.run(main())
asyncio.open_connection
await writer.drain()







await reader.readline()


await writer.wait_closed()


什么是io




io bound(io密集型)

等待io



异步io


yield




yield把值导出给外部,不会赋值给左侧,调用send给上一次的yield的表达式赋值,即给上一次yield的左侧赋值,如果不调用send直接next,上一次yield左边会是空值,相当于发送空值send(none)


多tcp协程实现

python 复制代码
# server.py  ------ 使用 asyncio 的"单线程多并发"TCP 回显服务器
import asyncio
import socket

IP = ""          # 监听所有网卡(0.0.0.0)
PORT = 50000     # 监听端口
BUFLEN = 4096    # 每次从连接中读取的最大字节数

# 这个协程会被"每个新连接"调用一次,相当于"每个客户端一个任务"
async def handle_echo(reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
    # 获取对端地址,打印日志用
    addr = writer.get_extra_info("peername")  # 形如 ('127.0.0.1', 54321)

    # 拿到底层 socket(可选,仅做一些 TCP 参数设置)
    sock: socket.socket = writer.get_extra_info("socket")
    if sock is not None:
        try:
            # 关闭 Nagle,降低小包延迟(可选优化,失败也无所谓)
            sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
        except OSError:
            pass

    print(f"客户端 {addr} 已连接")

    try:
        # 循环读取客户端发来的数据;读不到(对端关闭)就退出
        while True:
            # ★ await = 等待"IO 就绪"。当前协程挂起,事件循环可以去服务其他连接
            data = await reader.read(BUFLEN)  # read 返回 b"" 表示对端优雅关闭
            if not data:                      # 没数据 = 客户端断开了
                print(f"客户端 {addr} 关闭了连接")
                break

            # 这里就是业务逻辑:回显(原样发回去)
            writer.write(data)      # 写入发送缓冲(不保证立刻发出去)
            # ★ drain = 背压控制。如果缓冲区接近上限,这里会"等一会儿"
            await writer.drain()    # 避免你写太快把缓冲区撑爆
    except asyncio.CancelledError:
        # 如果服务器要关闭、或任务被取消(例如优雅退出),把异常继续抛出给上层
        raise
    except Exception as e:
        # 其他异常(网络错误、解码错误等)打印一下,不让任务悄悄死掉
        print(f"处理 {addr} 出错:{e}")
    finally:
        # ★ 关闭流程:先 close 发出关闭意图,再等待真正关闭完成
        writer.close()              # 发出"我要关了"的信号(可能还有尾数据在路上)
        try:
            await writer.wait_closed()  # 等系统把剩余数据发完、连接彻底关闭
        except Exception:
            pass
        print(f"客户端 {addr} 已断开")

# 顶层协程:启动服务器并一直服务
async def main():
    # 创建 TCP 服务器;每来一个连接,事件循环都会调用 handle_echo 创建一个任务去处理
    server = await asyncio.start_server(
        handle_echo, IP, PORT, backlog=2048, reuse_port=False
    )

    # 打印监听地址
    addrs = ", ".join(str(s.getsockname()) for s in server.sockets or [])
    print(f"服务端启动成功,监听:{addrs}")

    # async with 确保优雅关闭(例如 Ctrl+C)
    async with server:
        # 持续处理请求,直到被打断(Ctrl+C)或收到取消
        await server.serve_forever()

# 程序入口:创建事件循环,运行 main,结束后自动收尾
if __name__ == "__main__":
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        # 捕获 Ctrl+C,让输出更干净
        print("\n收到中断,准备退出...")

main



asyncio.start_server(...)

返回的是一个 协程对象

s.getsockname()




async with server:

await server.serve_forever()


handle_echo

handle_echo(reader: asyncio.StreamReader, writer: asyncio.StreamWriter)

writer.get_extra_info("peername")


writer.get_extra_info("socket")


读写

异常处理

优雅关闭





客户端

复制代码
# client.py
import asyncio

async def main():
    reader, writer = await asyncio.open_connection("127.0.0.1", 50000)
    for i in range(3):
        msg = f"hello {i}\n".encode()
        writer.write(msg)
        await writer.drain()
        data = await reader.read(4096)
        print("echo:", data.decode().rstrip())
    writer.close()
    await writer.wait_closed()

asyncio.run(main())

asyncio.open_connection(host, port)


UDP编程

服务端

python 复制代码
import socket
import json

BUFF_LEN = 1024
ADDR = ("", 18000)

server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server_socket.bind(ADDR)
server_socket.settimeout(1)

print("UDP服务端已启动,等待客户端消息...")

try:
    while True:
        try:
            data, client_addr = server_socket.recvfrom(BUFF_LEN)
        except socket.timeout:
            continue


        try:
            message = json.loads(data.decode('utf8'))
        except json.JSONDecodeError:
            print(f"来自 {client_addr} 的非法数据")
            continue

        print(f"来自 {client_addr} 的消息: {message}")

        # 构造回复
        reply = {
            'action': '返回信息',
            'info': f'服务端收到: {message.get("content", "")}'
        }

        server_socket.sendto(json.dumps(reply).encode('utf8'), client_addr)

finally:
    server_socket.close()
    print("服务端已关闭")

UDP 无连接,所以不需要 listen() 和 accept(),一个 socket 就可以收发所有客户端消息。

如果想测试 finally 的执行,最好在终端运行 Python,用 Ctrl+C 停止,而不是点击 Debug 停止按钮。

客户端

python 复制代码
import socket
import json

BUFF_LEN = 1024
SERVER_ADDR = ("127.0.0.1", 18000)

client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
client_socket.settimeout(2)

try:
    while True:
        content = input("请输入消息(exit退出): ")
        if content.lower() == "exit":
            break

        message = {
            'action': '发送消息',
            'content': content
        }

        client_socket.sendto(json.dumps(message).encode('utf8'), SERVER_ADDR)

        try:
            data, addr = client_socket.recvfrom(BUFF_LEN)
            reply = json.loads(data.decode('utf8'))
            print("服务端回复:", reply)
        except socket.timeout:
            print("接收服务端消息超时")

finally:
    client_socket.close()
    print("客户端已关闭")

client_socket.settimeout(2)

如果服务端没有回应,客户端会等待 2 秒后抛出 timeout

recvfrom 返回 (数据, 地址)

recvfrom 返回 (数据, 地址),UDP 是无连接的,所以服务端地址需要返回


和TCP不同, 服务端只需要一个socket进行通信即可,不需要 2个socket分别用来监听和通信:

TCP:需要先监听 listen_socket,然后每来一个客户端就 accept() 出一个新的 conn_socket

专门跟它通信。这样每个连接都有独立的通道。

UDP:没有连接的概念!所有消息都是独立的"信件"。

服务端用一个 socket 就能收所有客户端的数据。

通过 recvfrom() 返回的 addr(发送方地址)来区分不同客户端。

当不需要使用 UDP Socket 时,可以通过 socket 对象的 close 方法 关闭









相关推荐
PieroPc3 小时前
Python基于 Gradio 和 SQLite 开发的简单博客管理平台,支持局域网手机查看,给一个PC和手机 互联方式
python·sqlite·gradio
大模型铲屎官3 小时前
【LangChain 核心组件指南 | Agent篇】从零到精通:深度解析 create_agent 与 ReAct 智能体构建
人工智能·python·深度学习·langchain·大模型·agent·react智能体
MoRanzhi12033 小时前
基于 SciPy 的矩阵运算与线性代数应用详解
人工智能·python·线性代数·算法·数学建模·矩阵·scipy
伊织code3 小时前
Django - DRF
后端·python·django·drf
2501_915921433 小时前
uWSGI + HTTPS 实战指南,配置、证书、TLS 终止与调试全流程(适用于生产与真机抓包排查)
网络协议·http·ios·小程序·https·uni-app·iphone
王伯爵3 小时前
5G开户时切片配置参数详解
服务器·网络·5g
学习路上_write3 小时前
新版Pycharm添加导入anaconda的python解释器
开发语言·python·pycharm
Ronin3053 小时前
【Linux网络】Socket编程:UDP网络编程实现DictServer
linux·服务器·网络·udp
AAAAA92404 小时前
5G RedCap模组应用领域分析
网络·物联网·5g·信息与通信