Python + 网络编程

分类: 网络编程

Python Socket 网络编程:从入门到精通

Socket(套接字)是计算机网络通信的基石。在 Python 中,socket 模块提供了访问 BSD 套接字接口的途径。无论是构建简单的聊天室,还是复杂的 Web 服务器,理解 Socket 都是必修课。


第一部分:核心概念与基础 (教程篇)

1. 什么是 Socket?

简单来说,Socket 是网络上运行的两个程序之间双向通信链路的一个端点。

  • 服务端 (Server): 监听特定端口,等待连接。
  • 客户端 (Client): 主动连接服务端的 IP 和端口。

2. 创建 Socket 对象

所有网络通信的第一步都是创建一个 socket 对象。

Python

import socket

# 创建一个 TCP/IP socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

3. 地址家族 (Address Family)与 套接字类型 (Socket Type)

socket.socket(family, type) 中,你通常会用到以下参数:

地址家族 (Family): | 常量 | 说明 | 适用场景 | | :--- | :--- | :--- | | socket.AF_INET | IPv4 (默认) | 互联网通信 (如 192.168.1.1) | | socket.AF_INET6 | IPv6 | 下一代互联网通信 | | socket.AF_UNIX | Unix Domain Socket | 同一台机器进程间通信 (Linux/Mac) |

套接字类型 (Type): | 常量 | 说明 | 协议 | 特点 | | :--- | :--- | :--- | :--- | | socket.SOCK_STREAM | 流式套接字 (默认) | TCP | 面向连接、可靠、顺序数据流 (HTTP, FTP) | | socket.SOCK_DGRAM | 数据报套接字 | UDP | 无连接、不可靠、速度快 (视频流, DNS) |


第二部分:Socket 方法详解 (文档篇)

这里详细列出了服务端和客户端常用的方法及其扩展用法。

1. 服务端常用方法

s.bind(address)

将套接字绑定到地址。

  • 参数: address 取决于地址家族。对于 AF_INET,它是一个元组 (host, port)
  • 注意: host'' 表示绑定所有可用接口;端口 0 表示随机分配一个空闲端口。

s.listen(backlog)

开始监听传入连接。

  • 参数: backlog (int) 指定在拒绝新连接之前,系统允许挂起的未接受连接的最大数量(排队长度)。通常设为 5 或更大。

s.accept()

接受一个连接。

  • 行为: 此方法是阻塞的(除非设置了非阻塞),它会一直等待直到有客户端连接。
  • 返回值: 返回一个元组 (conn, address)
  • conn: 一个新的 socket 对象,用于通过该连接发送/接收数据。
  • address: 客户端的地址 (IP, Port)。

2. 客户端常用方法

s.connect(address)

连接到远程套接字。

  • 参数: (host, port) 元组。
  • 错误: 如果连接失败,会抛出 ConnectionRefusedError 等异常。

s.connect_ex(address)

connect() 的扩展版本,出错时不抛出异常,而是返回错误码(返回 0 表示成功)。常用于端口扫描器。

3. 公共传输方法 (TCP)

s.send(bytes)

发送数据。

  • 重要: 返回已发送的字节数。注意send() 并不保证发送所有数据,可能只发送了一部分,需要应用程序自己处理循环发送。

s.sendall(bytes)

推荐使用。 持续发送数据,直到所有数据发送完毕或发生错误。成功返回 None

s.recv(bufsize)

接收数据。

  • 参数: bufsize 指定一次最多接收的数据量(通常是 1024 或 4096)。
  • 返回值: bytes 对象。如果返回空字节 b'',表示对方已关闭连接。

4. 公共传输方法 (UDP)

s.sendto(bytes, address)

发送 UDP 数据。不需要建立连接。

s.recvfrom(bufsize)

接收 UDP 数据。

  • 返回值: (bytes, address),包含数据和发送方地址。

5. 高级/扩展设置 (Options & Control)

s.setsockopt(level, optname, value)

设置套接字选项。最常用的一个是解决 "Address already in use" 错误:

Python

# 允许端口复用 (在服务器重启时非常有用)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

s.settimeout(value)

设置阻塞套接字操作的超时时间(秒)。如果超时,抛出 socket.timeout 异常。

s.setblocking(bool)

  • True (默认): 阻塞模式。
  • False: 非阻塞模式。如果操作不能立即完成(如 recv 没有数据),会抛出 BlockingIOError

s.close()

关闭套接字,释放资源。建议放在 finally 块中或使用 with 语句。


第三部分:实战演练 - 完整的 TCP Echo 服务

下面我们将编写一个健壮的 TCP Echo 系统。服务端接收客户端发送的消息,加上时间戳后原样返回。

1. 服务端代码 (Server)

这个服务端示例包含了:

  • 地址复用设置(防止重启报错)。
  • 循环接收客户端(处理完一个再处理下一个)。
  • 优雅的退出机制。

Python

import socket
import time

# 配置信息
HOST = '127.0.0.1'  # 监听本地回环地址
PORT = 65432        # 监听的非特权端口 (> 1023)

def start_server():
    # 1. 创建 Socket 对象 (IPv4, TCP)
    # 使用 with 语句可以确保 socket 最后会被正确关闭
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:

        # [扩展写法] 设置端口复用,避免服务端重启时报 "Address already in use"
        s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

        # 2. 绑定地址和端口
        try:
            s.bind((HOST, PORT))
            print(f"[*] 服务端启动,监听 {HOST}:{PORT}")
        except OSError as e:
            print(f"[!] 绑定失败: {e}")
            return

        # 3. 开始监听,backlog 设为 5
        s.listen(5)
        print("[*] 等待客户端连接...")

        while True:
            try:
                # 4. 接受连接 (阻塞式)
                # conn 是新的 socket 对象,用于和该特定客户端通信
                # addr 是客户端的 (IP, Port)
                conn, addr = s.accept()

                # 使用 with 管理连接 socket
                with conn:
                    print(f"[+] 已连接: {addr}")

                    while True:
                        # 5. 接收数据 (一次最多 1024 字节)
                        data = conn.recv(1024)

                        # 如果 data 为空,说明客户端断开了连接
                        if not data:
                            print(f"[-] 客户端 {addr} 断开连接")
                            break

                        # 处理数据 (解码 -> 处理 -> 编码)
                        msg_str = data.decode('utf-8')
                        print(f"    收到消息: {msg_str}")

                        # 构造响应:加上时间戳
                        timestamp = time.strftime("%H:%M:%S", time.localtime())
                        response = f"[{timestamp}] 服务器收到: {msg_str}"

                        # 6. 发送数据 (sendall 保证数据发完)
                        conn.sendall(response.encode('utf-8'))

            except KeyboardInterrupt:
                print("\n[!] 服务器正在停止...")
                break
            except Exception as e:
                print(f"[!] 发生错误: {e}")
                break

if __name__ == "__main__":
    start_server()

2. 客户端代码 (Client)

这个客户端示例包含了:

  • 用户交互输入。
  • 异常处理(处理连接被拒绝的情况)。

Python

import socket

# 必须与服务端一致
HOST = '127.0.0.1' 
PORT = 65432

def start_client():
    print(f"[*] 正在尝试连接 {HOST}:{PORT} ...")

    # 1. 创建 Socket
    try:
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            # 2. 连接服务端
            s.connect((HOST, PORT))
            print("[+] 连接成功!输入 'exit' 退出。")

            while True:
                # 获取用户输入
                msg = input(">>> 请输入消息: ")
                if not msg: continue
                if msg.lower() == 'exit':
                    break

                # 3. 发送数据 (记得 encode 转为 bytes)
                s.sendall(msg.encode('utf-8'))

                # 4. 接收响应
                # 注意:TCP 是流式协议,但在这种简单交互下,recv 通常能收到完整包
                data = s.recv(1024)

                if not data:
                    print("[!] 服务器关闭了连接")
                    break

                print(f"    {data.decode('utf-8')}")

    except ConnectionRefusedError:
        print("[!] 连接失败:目标计算机拒绝连接 (请检查服务端是否开启)")
    except Exception as e:
        print(f"[!] 发生错误: {e}")

if __name__ == "__main__":
    start_client()

第四部分:常见问题与注意事项

  1. Bytes vs String: 网络传输的是字节流 (bytes)。 - 发送前必须编码:str.encode('utf-8') - 接收后必须解码:bytes.decode('utf-8')
  2. TCP 粘包与分包: recv(1024) 并不意味着一定能收到 1024 字节,也不意味着只收到对方发送的一次 send。TCP 是流(stream),没有边界。 - 解决方案: 在实际工程中,通常会在消息头部加上由固定字节(如 4 字节)表示的消息长度,接收方先读长度,再根据长度读取消息体。
  3. 阻塞 (Blocking): 默认情况下,accept()recv() 都会卡住程序,直到有事件发生。如果需要同时处理多个客户端,通常需要使用多线程 (threading)IO 多路复用 (select/selectors 模块)
  4. 防火墙: 如果在局域网或公网测试失败,请检查防火墙是否放行了你绑定的端口。