第103集 TCP客户端/服务器

学习目标

  1. 深入理解TCP协议的工作原理
  2. 掌握单线程TCP服务器的实现方法
  3. 学习多线程TCP服务器处理并发连接
  4. 了解线程池服务器的优势和实现
  5. 掌握TCP客户端的开发技巧
  6. 实现一个完整的TCP聊天程序

一、TCP协议深入理解

1.1 TCP三次握手建立连接

TCP采用三次握手(Three-way Handshake)机制建立可靠连接:

  1. 第一次握手:客户端发送SYN包(SYN=1, seq=X)到服务器,并进入SYN_SENT状态,等待服务器确认。
  2. 第二次握手:服务器收到SYN包,必须确认客户端的SYN(ACK=X+1),同时自己也发送一个SYN包(SYN=1, seq=Y),即SYN+ACK包,此时服务器进入SYN_RECV状态。
  3. 第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ACK=Y+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。

1.2 TCP四次挥手断开连接

TCP采用四次挥手(Four-way Wavehand)机制断开连接:

  1. 第一次挥手:客户端发送FIN包(FIN=1, seq=U),并进入FIN_WAIT_1状态,停止发送数据,但仍可接收数据。
  2. 第二次挥手:服务器收到FIN包,发送ACK包(ACK=U+1),并进入CLOSE_WAIT状态。客户端收到ACK后进入FIN_WAIT_2状态。
  3. 第三次挥手:服务器发送FIN包(FIN=1, seq=V),并进入LAST_ACK状态。
  4. 第四次挥手:客户端收到FIN包,发送ACK包(ACK=V+1),并进入TIME_WAIT状态,等待2MSL(最大段生存时间)后关闭连接。服务器收到ACK后立即关闭连接。

1.3 TCP的可靠性保障机制

  • 确认应答:每个数据包都需要对方确认收到
  • 超时重传:发送后一定时间内未收到确认则重传
  • 滑动窗口:控制发送速率,实现流量控制
  • 拥塞控制:慢开始、拥塞避免、快速重传、快速恢复
  • 数据校验:确保数据完整性
  • 顺序控制:通过序列号重组数据
  • 流量控制:通过窗口机制避免接收方缓冲区溢出

二、单线程TCP服务器

2.1 单线程服务器的工作原理

单线程TCP服务器一次只能处理一个客户端连接,在处理完当前连接后才能处理下一个连接。其工作流程如下:

  1. 创建socket对象
  2. 绑定IP地址和端口号
  3. 开始监听连接请求
  4. 接受客户端连接
  5. 处理客户端请求(接收数据、发送响应)
  6. 关闭客户端连接
  7. 重复步骤4-6

2.2 单线程服务器的实现

import socket

def single_thread_server():
    # 创建TCP socket对象
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    # 绑定IP地址和端口号
    server_socket.bind(('127.0.0.1', 8888))
    
    # 开始监听,最大连接数为5
    server_socket.listen(5)
    print("单线程服务器已启动,等待客户端连接...")
    
    while True:
        # 接受客户端连接
        client_socket, client_address = server_socket.accept()
        print(f"客户端 {client_address} 已连接")
        
        try:
            while True:
                # 接收客户端数据,最大1024字节
                data = client_socket.recv(1024)
                if not data:
                    break  # 客户端断开连接
                
                print(f"收到客户端 {client_address} 的消息: {data.decode('utf-8')}")
                
                # 发送响应数据
                response = f"服务器已收到: {data.decode('utf-8')}"
                client_socket.send(response.encode('utf-8'))
        except Exception as e:
            print(f"处理客户端 {client_address} 时发生错误: {e}")
        finally:
            # 关闭客户端连接
            client_socket.close()
            print(f"客户端 {client_address} 已断开连接")

if __name__ == "__main__":
    single_thread_server()

2.3 单线程服务器的局限性

  • 一次只能处理一个客户端连接
  • 当一个客户端连接占用大量时间时,其他客户端需要等待
  • 不适合处理并发连接的场景

三、多线程TCP服务器

3.1 多线程服务器的工作原理

多线程TCP服务器为每个客户端连接创建一个独立的线程,主线程负责接受连接请求,子线程负责处理具体的客户端请求。其工作流程如下:

  1. 创建socket对象
  2. 绑定IP地址和端口号
  3. 开始监听连接请求
  4. 接受客户端连接,创建新线程处理该连接
  5. 主线程继续接受新的连接请求
  6. 子线程处理客户端请求(接收数据、发送响应)
  7. 子线程关闭客户端连接

3.2 多线程服务器的实现

import socket
import threading

def handle_client(client_socket, client_address):
    """处理单个客户端连接的函数"""
    print(f"客户端 {client_address} 已连接")
    
    try:
        while True:
            # 接收客户端数据
            data = client_socket.recv(1024)
            if not data:
                break  # 客户端断开连接
            
            print(f"收到客户端 {client_address} 的消息: {data.decode('utf-8')}")
            
            # 发送响应数据
            response = f"服务器已收到: {data.decode('utf-8')}"
            client_socket.send(response.encode('utf-8'))
    except Exception as e:
        print(f"处理客户端 {client_address} 时发生错误: {e}")
    finally:
        # 关闭客户端连接
        client_socket.close()
        print(f"客户端 {client_address} 已断开连接")

def multi_thread_server():
    # 创建TCP socket对象
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    # 设置端口可重用,避免地址已被使用错误
    server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    
    # 绑定IP地址和端口号
    server_socket.bind(('127.0.0.1', 8888))
    
    # 开始监听,最大连接数为10
    server_socket.listen(10)
    print("多线程服务器已启动,等待客户端连接...")
    
    try:
        while True:
            # 接受客户端连接
            client_socket, client_address = server_socket.accept()
            
            # 创建新线程处理客户端连接
            client_thread = threading.Thread(
                target=handle_client, 
                args=(client_socket, client_address),
                daemon=True  # 设置为守护线程,主线程结束时自动退出
            )
            client_thread.start()
            print(f"当前活跃线程数: {threading.active_count()}")
    except KeyboardInterrupt:
        print("服务器已停止")
    finally:
        # 关闭服务器socket
        server_socket.close()

if __name__ == "__main__":
    multi_thread_server()

3.3 多线程服务器的优势与注意事项

优势:

  • 可以同时处理多个客户端连接
  • 每个客户端连接拥有独立的处理线程
  • 实现简单,易于理解和维护

注意事项:

  • 线程创建和销毁有一定的开销
  • 大量并发连接会导致大量线程,消耗系统资源
  • 需要注意线程安全问题
  • 可能出现线程饥饿和死锁问题

四、线程池TCP服务器

4.1 线程池服务器的工作原理

线程池服务器预先创建一定数量的线程,将客户端连接请求放入任务队列,线程池中的线程从队列中获取任务并处理。其工作流程如下:

  1. 创建固定数量的工作线程
  2. 创建任务队列用于存储客户端连接请求
  3. 主线程接受客户端连接,将连接放入任务队列
  4. 工作线程从队列中获取连接并处理
  5. 处理完成后,工作线程返回线程池,等待新的任务

4.2 线程池服务器的实现

import socket
import threading
import concurrent.futures

def handle_client(client_socket, client_address):
    """处理单个客户端连接的函数"""
    print(f"客户端 {client_address} 已连接")
    
    try:
        while True:
            # 接收客户端数据
            data = client_socket.recv(1024)
            if not data:
                break  # 客户端断开连接
            
            print(f"收到客户端 {client_address} 的消息: {data.decode('utf-8')}")
            
            # 发送响应数据
            response = f"服务器已收到: {data.decode('utf-8')}"
            client_socket.send(response.encode('utf-8'))
    except Exception as e:
        print(f"处理客户端 {client_address} 时发生错误: {e}")
    finally:
        # 关闭客户端连接
        client_socket.close()
        print(f"客户端 {client_address} 已断开连接")

def thread_pool_server():
    # 创建TCP socket对象
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    # 设置端口可重用
    server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    
    # 绑定IP地址和端口号
    server_socket.bind(('127.0.0.1', 8888))
    
    # 开始监听,最大连接数为20
    server_socket.listen(20)
    print("线程池服务器已启动,等待客户端连接...")
    
    # 创建线程池,最大线程数为5
    with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
        try:
            while True:
                # 接受客户端连接
                client_socket, client_address = server_socket.accept()
                
                # 提交任务到线程池
                executor.submit(handle_client, client_socket, client_address)
                print(f"当前活跃线程数: {threading.active_count()}")
        except KeyboardInterrupt:
            print("服务器已停止")
        finally:
            # 关闭服务器socket
            server_socket.close()

if __name__ == "__main__":
    thread_pool_server()

4.3 线程池服务器的优势

  • 减少线程创建和销毁的开销
  • 控制并发线程数量,避免系统资源耗尽
  • 提高响应速度,客户端连接可以立即得到处理
  • 更好的系统资源管理和利用

五、TCP客户端实现

5.1 TCP客户端的工作流程

  1. 创建socket对象
  2. 连接到服务器
  3. 发送数据到服务器
  4. 接收服务器响应
  5. 关闭连接

5.2 TCP客户端的实现

import socket

def tcp_client():
    # 创建TCP socket对象
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    try:
        # 连接到服务器
        client_socket.connect(('127.0.0.1', 8888))
        print("已连接到服务器")
        
        while True:
            # 输入要发送的数据
            message = input("请输入要发送的消息 (输入exit退出): ")
            if message.lower() == 'exit':
                break
            
            # 发送数据到服务器
            client_socket.send(message.encode('utf-8'))
            
            # 接收服务器响应
            response = client_socket.recv(1024)
            print(f"服务器响应: {response.decode('utf-8')}")
    except ConnectionRefusedError:
        print("连接被拒绝,请确保服务器已启动")
    except Exception as e:
        print(f"发生错误: {e}")
    finally:
        # 关闭客户端socket
        client_socket.close()
        print("已断开与服务器的连接")

if __name__ == "__main__":
    tcp_client()

六、综合应用:TCP聊天程序

6.1 聊天服务器实现

import socket
import threading

# 存储所有客户端连接
clients = []


def broadcast(message, sender_socket):
    """向所有客户端广播消息"""
    for client in clients:
        if client != sender_socket:
            try:
                client.send(message)
            except:
                # 移除无效连接
                clients.remove(client)
                client.close()


def handle_client(client_socket, client_address):
    """处理单个客户端连接"""
    print(f"客户端 {client_address} 已连接")
    clients.append(client_socket)
    
    try:
        while True:
            # 接收客户端消息
            message = client_socket.recv(1024)
            if not message:
                break
            
            print(f"客户端 {client_address} 发送: {message.decode('utf-8')}")
            
            # 广播消息给所有客户端
            broadcast(message, client_socket)
    except Exception as e:
        print(f"处理客户端 {client_address} 时发生错误: {e}")
    finally:
        # 清理资源
        if client_socket in clients:
            clients.remove(client_socket)
        client_socket.close()
        print(f"客户端 {client_address} 已断开连接")
        # 广播客户端离开信息
        broadcast(f"客户端 {client_address} 已离开聊天室".encode('utf-8'), None)


def chat_server():
    # 创建TCP socket对象
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    # 设置端口可重用
    server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    
    # 绑定IP地址和端口号
    server_socket.bind(('127.0.0.1', 9999))
    
    # 开始监听
    server_socket.listen(10)
    print("聊天服务器已启动,等待客户端连接...")
    
    try:
        while True:
            # 接受客户端连接
            client_socket, client_address = server_socket.accept()
            
            # 创建新线程处理客户端连接
            client_thread = threading.Thread(
                target=handle_client, 
                args=(client_socket, client_address),
                daemon=True
            )
            client_thread.start()
            
            # 广播新客户端加入信息
            broadcast(f"客户端 {client_address} 已加入聊天室".encode('utf-8'), None)
    except KeyboardInterrupt:
        print("服务器已停止")
    finally:
        # 关闭服务器socket
        server_socket.close()

if __name__ == "__main__":
    chat_server()

6.2 聊天客户端实现

import socket
import threading

def receive_messages(client_socket):
    """接收服务器消息的线程函数"""
    while True:
        try:
            # 接收服务器消息
            message = client_socket.recv(1024)
            if not message:
                print("与服务器的连接已断开")
                break
            print(message.decode('utf-8'))
        except Exception as e:
            print(f"接收消息时发生错误: {e}")
            break


def chat_client():
    # 创建TCP socket对象
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    try:
        # 连接到服务器
        client_socket.connect(('127.0.0.1', 9999))
        print("已连接到聊天服务器")
        print("输入消息发送给所有客户端,输入exit退出")
        
        # 创建接收消息的线程
        receive_thread = threading.Thread(
            target=receive_messages, 
            args=(client_socket,),
            daemon=True
        )
        receive_thread.start()
        
        # 发送消息
        while True:
            message = input()
            if message.lower() == 'exit':
                break
            client_socket.send(message.encode('utf-8'))
    except ConnectionRefusedError:
        print("连接被拒绝,请确保聊天服务器已启动")
    except Exception as e:
        print(f"发生错误: {e}")
    finally:
        # 关闭客户端socket
        client_socket.close()
        print("已断开与服务器的连接")

if __name__ == "__main__":
    chat_client()

七、TCP编程最佳实践

7.1 服务器端最佳实践

  1. 设置端口可重用:使用socket.SO_REUSEADDR选项避免地址已被使用错误
  2. 合理设置监听队列大小:根据系统资源和预期并发数设置合适的backlog值
  3. 使用非阻塞IO:对于高并发服务器,考虑使用非阻塞IO模型
  4. 实现超时机制:防止客户端长时间占用连接
  5. 处理异常:捕获并处理各种可能的异常,确保服务器稳定运行
  6. 优雅关闭:使用try-finally确保资源正确释放

7.2 客户端最佳实践

  1. 处理连接异常:捕获ConnectionRefusedError等异常
  2. 设置超时:避免长时间等待服务器响应
  3. 合理关闭连接:使用try-finally确保连接正确关闭
  4. 数据编码一致性:客户端和服务器使用相同的编码格式
  5. 避免频繁创建连接:重用连接可以提高性能

7.3 性能优化建议

  1. 使用线程池或进程池:避免频繁创建和销毁线程/进程
  2. 减少数据传输量:压缩数据或使用更高效的序列化方式
  3. 批量处理:将多个小请求合并为一个大请求
  4. 使用异步IO:对于高并发场景,考虑使用asyncio实现异步服务器
  5. 合理设置缓冲区大小:根据实际需求调整recv和send的缓冲区大小

八、常见问题与解决方案

8.1 连接被拒绝

问题:客户端连接服务器时出现ConnectionRefusedError
解决方案

  • 确保服务器已启动
  • 检查服务器IP地址和端口号是否正确
  • 检查防火墙设置,确保端口已开放

8.2 连接超时

问题:客户端连接服务器时出现超时
解决方案

  • 检查网络连接是否正常
  • 检查服务器是否响应缓慢
  • 考虑增加超时时间

8.3 数据不完整

问题:接收的数据不完整或被截断
解决方案

  • TCP是流式协议,需要自己处理数据边界
  • 实现消息定界机制(如使用分隔符或固定长度头部)
  • 循环接收直到获取完整数据

8.4 服务器崩溃

问题:服务器处理客户端请求时崩溃
解决方案

  • 完善异常处理,捕获所有可能的异常
  • 使用日志记录错误信息,便于调试
  • 考虑使用监控工具,及时发现服务器问题

九、总结

本集我们深入学习了TCP客户端/服务器编程:

  1. 理解了TCP协议的三次握手和四次挥手机制
  2. 实现了单线程TCP服务器,能够处理基本的客户端连接
  3. 学习了多线程TCP服务器,实现了并发连接处理
  4. 了解了线程池服务器的优势和实现方法
  5. 掌握了TCP客户端的开发技巧
  6. 实现了一个完整的TCP聊天程序

TCP编程是网络编程的基础,掌握TCP客户端/服务器开发对于构建网络应用至关重要。在实际开发中,我们需要根据应用的需求和规模选择合适的服务器模型,单线程服务器适用于简单应用,多线程服务器适用于中等规模应用,线程池服务器适用于大规模并发应用。

十、思考与练习

思考

  1. 三次握手和四次挥手的作用是什么?
  2. 单线程、多线程和线程池服务器各有什么优缺点?
  3. 如何处理TCP粘包问题?
  4. 如何实现TCP连接的心跳机制?

练习

  1. 改进单线程服务器,使其能够处理多个客户端连接
  2. 为多线程服务器添加客户端身份验证功能
  3. 实现一个文件传输服务器,支持客户端上传和下载文件
  4. 为聊天程序添加私聊功能,即可以指定接收消息的客户端
  5. 实现一个简单的HTTP服务器,支持静态文件访问
« 上一篇 socket编程基础 下一篇 » UDP客户端、服务器