6. Socket 多线程

概念

如果服务器在 accept 之后当场读完整个请求,第二个客户就要等第一个客户结束。并发服务器把 accept 和业务处理分开:一个线程专门接受连接,线程池里的工人各自处理自己的 Socket。 池的大小要有上限。每个连接都 new Thread,连接一多就会把进程打满。工人之间如果要修改连接计数、在线用户表,不能各写一个普通变量,要用后面的线程安全类,或者加锁。

  • 主循环只做 accept,然后把 Socket 提交给线程池。
  • 每个工人处理完必须关闭自己的连接,包括发生异常时。
  • 线程数固定,多出来的连接在队列里等待,队列也要有上限。
  • 共享计数使用原子变量。不要让多个工人同时执行 count++。

算法流程

下面的 UML 图说明 Java 例程和 Python 例程共同实现的流程。实线箭头表示调用或发送,虚线箭头表示返回或唤起。

接入与工人分工(UML 时序图)

sequenceDiagram participant Accept as 监听线程 participant Pool as 固定大小线程池 participant Worker as 工人线程 participant Client as 客户端 Accept->>Client: accept Accept->>Pool: 把这条连接提交给线程池 Note over Accept: 立刻回去接受下一个连接 Pool->>Worker: 分配空闲工人 Worker->>Worker: 活跃连接数加一 Worker->>Client: 读取并回写 Worker->>Worker: 活跃连接数减一 Worker->>Client: 关闭这条连接

Java 例程

import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

public class PoolServer {
    static final AtomicInteger ACTIVE = new AtomicInteger(); // 多个工人会同时改它,不能用普通 int

    static void handle(Socket socket) {
        ACTIVE.incrementAndGet();                 // 这个连接开始处理
        try {
            byte[] buf = new byte[256];
            int n = socket.getInputStream().read(buf); // 教学示例只读一块;真实协议要按边界读全
            if (n > 0) socket.getOutputStream().write(buf, 0, n); // 原样回写,n 才是实际读到的长度
        } catch (Exception ignored) {
            // 单个客户异常不能让整个服务器退出
        } finally {
            ACTIVE.decrementAndGet();
            try { socket.close(); } catch (Exception ignored) {} // 无论成功失败都关闭这条连接
        }
    }

    public static void main(String[] args) throws Exception {
        ExecutorService pool = Executors.newFixedThreadPool(8); // 同时最多 8 个连接在处理
        ServerSocket server = new ServerSocket(9000);
        while (true) {
            final Socket client = server.accept(); // 主线程只负责接入,不在这里读数据
            pool.submit(new Runnable() {           // 交给池里的空闲工人
                public void run() { handle(client); }
            });
        }
    }
}

Python 例程

import socket
from concurrent.futures import ThreadPoolExecutor
import threading

active = 0
lock = threading.Lock()               # 保护 active。count += 1 不是原子操作

def handle(conn):
    global active
    with lock:
        active += 1                   # 读改写必须放在同一把锁里
    try:
        data = conn.recv(256)         # 每个工人只处理自己 accept 到的那条连接
        if data:
            conn.sendall(data)        # 回显。空数据表示对端已经关闭
    finally:
        with lock:
            active -= 1               # 和加一使用同一把锁
        conn.close()                  # 异常时也要归还端口和文件描述符

def serve():
    server = socket.socket()
    server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server.bind(("127.0.0.1", 9000))
    server.listen(16)                 # 工人都忙时,已握手的连接先排在这里
    pool = ThreadPoolExecutor(max_workers=8)
    while True:
        conn, _ = server.accept()     # 接入线程不读业务数据,避免一个慢客户挡住后面的人
        pool.submit(handle, conn)     # 把这条连接交给池中线程