54·模块十二:网络编程进阶

网络编程:Socket 基础与 TCP 通信

networkingsocketstcp

第 54 课 - 网络编程:Socket 基础与 TCP 通信

1. 学习目标

通过本节课的学习,你将能够:

  • 理解 Socket(套接字)在网络编程中的基本概念和作用。
  • 掌握 TCP 协议建立连接(三次握手)和断开连接(四次挥手)的基本流程。
  • 使用 Python 的 socket 模块编写一个基础的 TCP 服务器。
  • 使用 Python 的 socket 模块编写一个基础的 TCP 客户端,与服务器进行通信。
  • 理解并应用 TCP 通信中可靠、有序的数据传输特性。

2. 核心概念

2.1 什么是 Socket?

想象一下,网络通信就像两个人打电话。Socket(套接字) 就像是电话机上的插口,它是网络通信的端点。你的程序通过这个"插口"与网络连接,进行数据的发送和接收。在 Python 中,socket 模块提供了对底层操作系统 Socket 接口的访问。

2.2 TCP 协议:可靠的数据传输

TCP(传输控制协议)是一种面向连接的、可靠的通信协议。它就像是一次有保证的电话通话

  • 面向连接:通信前必须先建立连接(三次握手),通信结束后要断开连接(四次挥手)。
  • 可靠传输:数据按顺序到达,如果丢失会自动重传。
  • 字节流:数据被看作一串无结构的字节流,而不是独立的消息包。

TCP 建立连接(三次握手)简化过程:

  1. 客户端向服务器发送:"我想连接你"(SYN)。
  2. 服务器回复:"好的,我同意,并且我也准备好了"(SYN-ACK)。
  3. 客户端再发送:"收到,我们开始通信吧"(ACK)。 这样,连接就建立了,双方可以开始互发数据。

3. 代码示例

我们将创建一个简单的 TCP 聊天程序。服务器可以接收客户端的消息并回复。

3.1 TCP 服务器代码 (tcp_server.py)

import socket
import threading

def handle_client(client_socket, address):
    """处理单个客户端连接的函数"""
    print(f"[新连接] 来自 {address} 的客户端已连接。")
    
    try:
        while True:
            # 接收客户端数据,最大 1024 字节
            data = client_socket.recv(1024)
            if not data:
                # 如果没有数据,说明客户端断开连接
                print(f"[断开] 客户端 {address} 已断开连接。")
                break
            
            # 打印接收到的消息
            message = data.decode('utf-8')
            print(f"[收到来自 {address} 的消息] {message}")
            
            # 向客户端发送回复
            reply = input("请输入服务器回复: ")
            client_socket.send(reply.encode('utf-8'))
            
    except ConnectionResetError:
        print(f"[异常] 客户端 {address} 强制断开连接。")
    finally:
        # 关闭该客户端的套接字
        client_socket.close()

def start_server(host='127.0.0.1', port=12345):
    """启动 TCP 服务器"""
    # 1. 创建 Socket 对象
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    # 2. 设置套接字选项,允许地址重用(避免"地址已被使用"错误)
    server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    
    # 3. 绑定地址(IP 和端口)
    server_socket.bind((host, port))
    
    # 4. 开始监听,参数 5 表示最大排队连接数
    server_socket.listen(5)
    print(f"[启动] 服务器正在监听 {host}:{port}...")
    
    try:
        while True:
            # 5. 接受客户端连接(阻塞式等待)
            client_socket, address = server_socket.accept()
            
            # 为每个客户端创建一个新线程来处理
            client_thread = threading.Thread(
                target=handle_client,
                args=(client_socket, address)
            )
            client_thread.daemon = True  # 设置为守护线程,主程序退出时线程自动结束
            client_thread.start()
            
            # 显示当前活动的连接数(不包括主线程)
            print(f"[活动连接] {threading.active_count() - 1}")
            
    except KeyboardInterrupt:
        print("\n[关闭] 服务器正在关闭...")
    finally:
        # 6. 关闭服务器套接字
        server_socket.close()

if __name__ == "__main__":
    start_server()

3.2 TCP 客户端代码 (tcp_client.py)

import socket

def start_client(host='127.0.0.1', port=12345):
    """启动 TCP 客户端"""
    # 1. 创建 Socket 对象 (AF_INET: IPv4, SOCK_STREAM: TCP)
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    try:
        # 2. 连接服务器
        print(f"[连接] 正在连接服务器 {host}:{port}...")
        client_socket.connect((host, port))
        print("[连接成功]")
        
        while True:
            # 3. 用户输入要发送的消息
            message = input("请输入要发送的消息 (输入 'quit' 退出): ")
            
            # 4. 检查是否要退出
            if message.lower() == 'quit':
                print("[退出] 正在断开连接...")
                break
            
            # 5. 发送数据(将字符串编码为字节)
            client_socket.send(message.encode('utf-8'))
            
            # 6. 接收服务器回复
            reply = client_socket.recv(1024).decode('utf-8')
            print(f"[服务器回复] {reply}")
            
    except ConnectionRefusedError:
        print("[错误] 无法连接服务器,请确认服务器是否已启动。")
    except Exception as e:
        print(f"[错误] 发生异常: {e}")
    finally:
        # 7. 关闭套接字
        client_socket.close()
        print("[连接已断开]")

if __name__ == "__main__":
    start_client()

运行步骤:

  1. 先运行 tcp_server.py,它将开始监听。
  2. 再运行一个或多个 tcp_client.py 实例。
  3. 在客户端输入消息,服务器会收到并提示你输入回复。
  4. 客户端将收到服务器的回复。

4. 实践练习

练习 1:修改客户端功能

要求: 修改 tcp_client.py,使其在发送消息前自动在消息末尾添加客户端的昵称(例如:"Alice: 你好")。昵称在程序启动时由用户输入。

预期输出(客户端部分):

请输入你的昵称: Alice
[连接成功]
请输入要发送的消息 (输入 'quit' 退出): 你好
[服务器回复] 你好Alice!

练习 2:服务器广播消息

要求:tcp_server.pyhandle_client 函数中,当收到一个客户端的消息时,不仅回复发送者,还将此消息广播给所有其他已连接的客户端(除了发送者自己)。你需要维护一个已连接客户端的列表。 提示: 使用全局列表和线程锁 (threading.Lock) 来安全地管理客户端连接。

练习 3:实现"断开连接"命令

要求: 在客户端添加一个 /exit 命令。当用户输入 /exit 时,客户端向服务器发送一个特殊的字符串(如 "__EXIT__"),然后正常关闭。服务器收到该字符串后,打印日志并关闭与该客户端的连接,而不是等待用户手动输入回复。

5. 常见错误

  1. Address already in use (地址已被使用)

    • 原因:服务器程序没有正确关闭,端口仍处于占用状态。
    • 解决:在 server_socket.bind() 前设置 server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1),或在服务器代码退出时确保 socket.close() 被调用。也可以稍等片刻再重新启动服务器。
  2. Connection refused (连接被拒绝)

    • 原因:客户端尝试连接时,服务器程序没有在运行,或者监听的 host/port 不正确。
    • 解决:确保服务器程序已启动,并且客户端连接的 (host, port) 与服务器 bind 的参数一致。
  3. BlockingIOError 或数据粘包

    • 原因:TCP 是字节流协议,没有消息边界。recv(1024) 可能一次收到不完整的消息,也可能一次收到多个消息粘在一起。
    • 解决:对于简单应用,可以约定消息以特定字符(如换行符 \n)结尾,或在消息头部包含固定长度的字节数来指示消息体长度。本课示例用于教学,未处理此复杂问题。
  4. 缓冲区大小设置不当

    • recv(1024) 的 1024 并不是每次发送数据的上限,它只是告诉操作系统一次最多从缓冲区读取多少字节。如果你需要发送大于 1024 字节的数据,客户端可能需要多次 send,服务器也需要循环 recv 直到收完整条消息。

6. 小结

  • Socket 是网络通信的编程接口,是应用层与传输层之间的桥梁。
  • TCP 是一种可靠的、面向连接的协议,适用于需要确保数据完整性和顺序的场景,如网页浏览、文件传输。
  • Python 的 socket 模块是进行网络编程的基础。
  • 一个基本的 TCP 服务器流程:创建 Socket -> 绑定地址 -> 监听 -> 接受连接 -> 收发数据 -> 关闭连接。
  • 一个基本的 TCP 客户端流程:创建 Socket -> 连接服务器 -> 收发数据 -> 关闭连接。
  • 对于需要同时服务多个客户端的服务器,通常需要使用多线程 (threading) 或异步编程(将在后续课程涉及)来处理并发连接。

下一课预告:我们将学习另一种常见的传输层协议——UDP。它无连接、不可靠,但在某些实时性要求高、允许少量丢包的场景下(如在线视频、语音、DNS查询)非常高效。

练习编辑器

python
Loading...

继续学习

完成本课后,建议继续学习下一课「网络编程:UDP 协议与简单应用」 以巩固所学知识。