动机、参考资料、涉及内容

socket 模块的使用与内部实现, 也包含一些计算机网络和并发编程的使用样例 (不包含完整成体系的理论)

FAQ

问题

在使用 TCP/IP 协议族作为 socket 的底层协议栈时, 客户端 socket 在调用 connect 和服务端在调用 accept 时, 底层发生的过程是什么?

回答

临时参考: https://chatgpt.com/share/89257ca9-7ef1-49c3-822a-d43689c51274

客户端调用 connect 表示需要触发三次握手的流程, 无论服务端是否调用 accept 方法, 服务端的内核都会自动响应三次握手的执行:

  • 第一次握手: 客户端向服务端发送一个 SYN 包, 此时, 客户端状态由 CLOSED 变为 SYN_SENT, 等待服务端的回应. 随后服务端接收到 SYN 包.
  • 第二次握手:即使服务端还没有调用 accept, 内核层面仍然会处理这个 SYN 包. 服务端会发送一个 SYN+ACK 包作为回应. 此时, 服务端状态由 LISTEN 变为 SYN_RECEIVED, 等待客户端的确认. 随后客户端收到 SYN+ACK 包
  • 第三次握手:客户端向服务端发送一个 ACK 包作为最后的确认, 此时, 客户端状态由 SYN_SENT 变为 ESTABLISHED, 表示连接已经建立. 随后服务端接收到 ACK 包, 服务端状态由 SYN_RECEIVED 变为 ESTABLISHED 状态, 连接至此完成.

服务端 accept 的作用是:

accept 方法的主要作用是在三次握手完成后, 将已经建立的连接从内核的队列中取出, 交给应用程序处理. 也就是说, accept 方法并不会影响三次握手的进行, 而是负责处理那些已经通过三次握手建立的连接.因此, 握手过程中服务端的 accept 方法是等待并处理已经建立的连接, 不影响也不实际处理连接的建立过程(这些由内核来完成).

问题

socket.socket 数据结构对象里各个字段的含义

答案

# <socket.socket fd=5, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 53880), raddr=('127.0.0.1', 12345)>
  • fd: 文件描述符
  • family, type:
  • proto: 未知
  • laddr: 本地地址(local address), 在服务端程序中, laddr 为服务端地址, 在应用端程序中, laddr 为应用端地址
  • raddr: 远程地址(remote address), 在服务端程序中, raddr 为应用端地址, 在应用端程序中, raddr 为服务端地址

问题

socket.socket 对象的 listen 方法的作用以及参数含义

答案

server_socket = socket.socket()
socket.bind(('localhost', 12345))
socket.listen(5)  # backlog=5
cat /proc/sys/net/ipv4/tcp_max_syn_backlog  # 256, 内核能提供的最大半连接池大小
cat /proc/sys/net/core/somaxconn   # 4096, 内核能提供的最大全连接池大小

backlog 是全连接池大小, 实际的全连接池大小为 max(backlog, somaxconn).

半连接池: 服务端在第二次握手后, 进入 SYN_RECEIVED 状态, 此时在内核中, 将连接对象放入半连接池 全连接池: 服务端在第二次握手后, 进入 SYN_RECEIVED 状态, 此时在内核中, 将半连接对象放入全连接池

因此如果请求需要进入半连接池时, 半连接池已满, 请求会丢失; 如果请求需要从半连接池进入全连接池, 请求也会丢失, 注意 accept 的作用就是从全连接池中取出连接. 半连接池的取出手段主要是第三次握手的建立或者收到第三次握手超时, 全连接池的取出手段主要是 accept 或者超时, 或者在被 accept 之前客户端 close 了.

例子

socketserver

参考https://mp.weixin.qq.com/s/EbipCRZnIuRYeSDyyEOuRQ

服务端

import socketserver

class ServiceHandler(socketserver.BaseRequestHandler):
    """
    内部提供了三个重要属性
        self.request: 已建立连接的 socket.socket 对象
        self.client_address: 客户端信息: (ip, port)
        self.server: 下面的 ThreadingTCPServer 对象

    必须实现 handle 方法, setup/finish 方法可选
    """

    def setup(self) -> None:
        """在执行 handle 之前调用,用于提前做一些连接相关的设置"""

    def finish(self) -> None:
        """在执行 handle 之后调用,用于资源释放等等"""
        self.request.close()

    def handle(self) -> None:
        client_ip, client_port = self.client_address
        while True:
            # self.request: socket.socket 对象
            # <socket.socket fd=4, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 12345), raddr=('127.0.0.1', 53880)>
            msg = self.request.recv(1024)
            if not msg:
                print(f"客户端(ip: {client_ip}, port: {client_port}) 已经断开连接")
                self.request.close()
                break
            print(f"客户端(ip: {client_ip}, port: {client_port}) 发来消息:",
                msg.decode("utf-8"))
            self.request.send("服务端收到, 你发的消息是: ".encode("utf-8") + msg)

# 这里的 ThreadingTCPServer 实例就是 ServiceHandler 里面的 self.server
server = socketserver.ThreadingTCPServer(("localhost", 12345), ServiceHandler)
server.serve_forever()
# 如果关闭监听, 可以调用: server.shutdown()

客户端

import socket

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # 返回主动套接字
client.connect(("localhost", 12345))

while True:
    data = input("请输入内容:")
    # <socket.socket fd=5, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 53880), raddr=('127.0.0.1', 12345)>
    if data.strip().lower() in ["q", "quit", "exit"]:
        client.close()
        print("Bye~~~")
        break
    client.send(data.encode("utf-8"))
    print(client.recv(1024).decode("utf-8"))