MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Python 运行 TCP 服务器和客户端的技巧

2021-05-051.4k 阅读

一、TCP 基础概念

(一)TCP 协议概述

TCP(Transmission Control Protocol)即传输控制协议,是一种面向连接的、可靠的、基于字节流的传输层通信协议。在互联网协议族(Internet Protocol Suite)中,TCP 为应用层提供了可靠的数据传输服务。它通过一系列机制,如序列号、确认应答、重传机制、流量控制和拥塞控制等,确保数据能够准确无误、顺序地从发送端传输到接收端。

(二)TCP 连接建立与关闭

  1. 三次握手建立连接
    • 客户端发送一个 SYN(同步)包到服务器,其中包含客户端的初始序列号(Sequence Number,简称 seq),假设为 x。此时客户端进入 SYN_SENT 状态。
    • 服务器接收到 SYN 包后,会发送一个 SYN + ACK(确认)包给客户端。这个包中包含服务器的初始序列号 y,同时确认号(Acknowledgment Number,简称 ack)为客户端的序列号 x + 1。此时服务器进入 SYN_RCVD 状态。
    • 客户端收到服务器的 SYN + ACK 包后,会发送一个 ACK 包给服务器,确认号为服务器的序列号 y + 1,序列号为 x + 1。此时客户端和服务器都进入 ESTABLISHED 状态,连接建立成功。
  2. 四次挥手关闭连接
    • 主动关闭方(假设为客户端)发送一个 FIN(结束)包给服务器,序列号为 u,此时客户端进入 FIN_WAIT_1 状态。
    • 服务器收到 FIN 包后,会发送一个 ACK 包给客户端,确认号为 u + 1,序列号为 v。此时服务器进入 CLOSE_WAIT 状态,客户端进入 FIN_WAIT_2 状态。
    • 服务器处理完剩余数据后,会发送一个 FIN 包给客户端,序列号为 w,确认号仍为 u + 1。此时服务器进入 LAST_ACK 状态。
    • 客户端收到服务器的 FIN 包后,会发送一个 ACK 包给服务器,确认号为 w + 1,序列号为 u + 1。此时客户端进入 TIME_WAIT 状态,服务器进入 CLOSED 状态。经过一段时间(2MSL,MSL 为最长报文段寿命)后,客户端也进入 CLOSED 状态,连接彻底关闭。

二、Python 中的 TCP 编程模块

(一)socket 模块

Python 的 socket 模块是网络编程的基础模块,它提供了一套接口,用于在不同的网络协议(如 TCP、UDP 等)上进行通信。在 TCP 编程中,主要使用 socket.socket 类来创建套接字对象。通过调用套接字对象的不同方法,实现服务器和客户端的各种功能,如绑定地址、监听连接、接受连接、发送和接收数据等。

(二)selectors 模块(可选优化)

对于处理多个并发连接的场景,Python 的 selectors 模块提供了一种高效的 I/O 多路复用机制。它可以监控多个套接字的 I/O 事件(如可读、可写等),避免了在处理多个连接时使用多线程或多进程带来的资源开销和复杂性。通过使用 selectors 模块,可以实现单线程高效处理多个 TCP 连接,提高服务器的性能和并发处理能力。

三、Python 实现 TCP 服务器

(一)简单 TCP 服务器示例

import socket


def simple_tcp_server():
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_socket.bind(('127.0.0.1', 8888))
    server_socket.listen(5)
    print('Server is listening on port 8888')

    while True:
        client_socket, client_address = server_socket.accept()
        print(f'Accepted connection from {client_address}')
        data = client_socket.recv(1024)
        print(f'Received data: {data.decode()}')
        client_socket.sendall(b'Hello, client!')
        client_socket.close()


if __name__ == '__main__':
    simple_tcp_server()


在上述代码中:

  1. 首先通过 socket.socket(socket.AF_INET, socket.SOCK_STREAM) 创建一个基于 IPv4 和 TCP 协议的套接字对象 server_socketAF_INET 表示使用 IPv4 地址族,SOCK_STREAM 表示使用 TCP 协议。
  2. 然后使用 server_socket.bind(('127.0.0.1', 8888)) 将套接字绑定到本地地址 127.0.0.1(即回环地址,代表本机)和端口号 8888
  3. 接着通过 server_socket.listen(5) 使服务器进入监听状态,参数 5 表示允许的最大连接数。
  4. while True 循环中,使用 server_socket.accept() 接受客户端的连接。该方法会阻塞,直到有客户端连接进来,返回一个新的套接字对象 client_socket 用于与客户端通信,以及客户端的地址 client_address
  5. 使用 client_socket.recv(1024) 接收客户端发送的数据,最多接收 1024 字节。接收到的数据是字节类型,通过 decode() 方法将其转换为字符串并打印。
  6. 最后使用 client_socket.sendall(b'Hello, client!') 向客户端发送响应数据,sendall 方法会确保数据全部发送出去。发送完成后关闭与客户端的连接 client_socket.close()

(二)多线程 TCP 服务器示例

import socket
import threading


def handle_client(client_socket, client_address):
    print(f'Accepted connection from {client_address}')
    data = client_socket.recv(1024)
    print(f'Received data: {data.decode()}')
    client_socket.sendall(b'Hello, client!')
    client_socket.close()


def multi_threaded_tcp_server():
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_socket.bind(('127.0.0.1', 8888))
    server_socket.listen(5)
    print('Server is listening on port 8888')

    while True:
        client_socket, client_address = server_socket.accept()
        client_thread = threading.Thread(target=handle_client, args=(client_socket, client_address))
        client_thread.start()


if __name__ == '__main__':
    multi_threaded_tcp_server()


此代码与简单 TCP 服务器的主要区别在于,当接收到客户端连接时,不再在主线程中直接处理客户端通信,而是创建一个新的线程 client_thread 来处理。threading.Thread(target=handle_client, args=(client_socket, client_address)) 创建线程,target 参数指定线程要执行的函数 handle_clientargs 参数传入该函数需要的参数。这样,主线程可以继续监听新的连接,实现并发处理多个客户端连接。

(三)使用 selectors 模块的高效 TCP 服务器示例

import selectors
import socket


sel = selectors.DefaultSelector()


def accept(sock, mask):
    conn, addr = sock.accept()
    print('accepted', conn, 'from', addr)
    conn.setblocking(False)
    sel.register(conn, selectors.EVENT_READ, read)


def read(conn, mask):
    data = conn.recv(1024)
    if data:
        print('echoing', repr(data), 'to', conn)
        conn.sendall(data)
    else:
        print('closing', conn)
        sel.unregister(conn)
        conn.close()


def selector_based_tcp_server():
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_socket.bind(('127.0.0.1', 8888))
    server_socket.listen(5)
    server_socket.setblocking(False)
    sel.register(server_socket, selectors.EVENT_READ, accept)

    while True:
        events = sel.select()
        for key, mask in events:
            callback = key.data
            callback(key.fileobj, mask)


if __name__ == '__main__':
    selector_based_tcp_server()


  1. 首先创建一个 selectors.DefaultSelector() 对象 sel,用于管理 I/O 事件。
  2. accept 函数用于处理新的客户端连接。当服务器套接字有可读事件(即有新连接)时,调用该函数。它接受客户端连接,将新连接的套接字设置为非阻塞模式,并使用 sel.register(conn, selectors.EVENT_READ, read) 注册该套接字的可读事件,当该套接字有可读数据时,会调用 read 函数。
  3. read 函数处理客户端发送的数据。它接收数据并打印,然后将数据回显给客户端。如果没有接收到数据,说明客户端关闭了连接,此时取消注册该套接字并关闭连接。
  4. selector_based_tcp_server 函数中,创建服务器套接字并设置为非阻塞模式,然后注册服务器套接字的可读事件,回调函数为 accept。在 while True 循环中,使用 sel.select() 阻塞等待 I/O 事件发生。当有事件发生时,遍历事件列表,调用相应的回调函数处理事件。

四、Python 实现 TCP 客户端

(一)简单 TCP 客户端示例

import socket


def simple_tcp_client():
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client_socket.connect(('127.0.0.1', 8888))
    client_socket.sendall(b'Hello, server!')
    data = client_socket.recv(1024)
    print(f'Received data: {data.decode()}')
    client_socket.close()


if __name__ == '__main__':
    simple_tcp_client()


  1. 通过 socket.socket(socket.AF_INET, socket.SOCK_STREAM) 创建基于 IPv4 和 TCP 协议的客户端套接字对象 client_socket
  2. 使用 client_socket.connect(('127.0.0.1', 8888)) 连接到服务器,参数为服务器的地址和端口号。
  3. 使用 client_socket.sendall(b'Hello, server!') 向服务器发送数据。
  4. 通过 client_socket.recv(1024) 接收服务器返回的数据,并打印。
  5. 最后关闭客户端套接字 client_socket.close()

(二)持续通信的 TCP 客户端示例

import socket


def continuous_communication_tcp_client():
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client_socket.connect(('127.0.0.1', 8888))

    while True:
        message = input('Enter message to send (or "exit" to quit): ')
        if message.lower() == 'exit':
            break
        client_socket.sendall(message.encode())
        data = client_socket.recv(1024)
        print(f'Received data: {data.decode()}')

    client_socket.close()


if __name__ == '__main__':
    continuous_communication_tcp_client()


此客户端代码在简单客户端的基础上,增加了一个循环,允许用户持续输入消息并发送给服务器,同时接收服务器的响应。用户输入 “exit” 时,循环结束,客户端关闭连接。每次用户输入消息后,通过 client_socket.sendall(message.encode()) 将消息编码为字节类型并发送给服务器,然后使用 client_socket.recv(1024) 接收服务器的回复并打印。

五、TCP 编程中的常见问题与解决技巧

(一)粘包问题

  1. 问题描述 在 TCP 通信中,由于 TCP 是基于字节流的协议,当发送端连续发送多个数据包时,接收端可能会将多个数据包粘在一起接收,导致数据解析错误。例如,发送端依次发送 “Hello” 和 “World” 两个数据包,接收端可能一次性接收到 “HelloWorld”,而不是分两次分别接收到 “Hello” 和 “World”。
  2. 解决方法
    • 定长包:在发送数据前,将数据填充到固定长度。例如,如果规定每个数据包长度为 1024 字节,那么发送 “Hello” 时,后面填充 1019 个字节的空白字符。接收端每次按固定长度接收数据,这样就能准确区分每个数据包。
    • 包头 + 包体:在每个数据包前加上包头,包头中包含包体的长度等信息。接收端先接收包头,解析出包体长度,然后根据包体长度接收包体数据。例如,包头为 4 个字节,用于表示包体的长度,发送 “Hello” 时,包头为 0005(表示包体长度为 5 字节),包体为 “Hello”。接收端先接收 4 个字节的包头,解析出包体长度为 5,再接收 5 字节的包体。

(二)端口冲突问题

  1. 问题描述 当启动 TCP 服务器时,如果指定的端口号已经被其他程序占用,就会出现端口冲突错误,导致服务器无法正常启动。
  2. 解决方法
    • 检查并关闭占用端口的程序:在 Linux 系统中,可以使用 lsof -i :port_number 命令查看哪个程序占用了指定端口,然后使用 kill -9 pid 命令(pid 为占用端口程序的进程 ID)关闭该程序。在 Windows 系统中,可以使用 netstat -ano | findstr :port_number 命令查看占用端口的进程 ID,然后在任务管理器中结束该进程。
    • 更换端口号:如果无法关闭占用端口的程序,可以选择一个未被占用的端口号来启动服务器。

(三)网络延迟与超时处理

  1. 问题描述 在网络通信中,由于网络状况不稳定,可能会出现数据传输延迟甚至超时的情况。如果不进行适当处理,客户端和服务器可能会一直等待数据,导致程序无响应。
  2. 解决方法
    • 设置套接字超时:在 Python 中,可以使用 socket.settimeout(timeout) 方法设置套接字的超时时间。例如,client_socket.settimeout(5) 表示设置客户端套接字的超时时间为 5 秒。如果在 5 秒内没有接收到数据,会抛出 socket.timeout 异常,程序可以捕获该异常并进行相应处理,如提示用户网络超时,重新尝试连接等。
    • 心跳机制:对于长连接,为了保持连接的活性并检测网络是否正常,可以使用心跳机制。客户端和服务器定期互相发送心跳包(如每隔一定时间发送一个简单的 “ping” 包),如果一方在规定时间内没有收到对方的心跳包,则认为连接已断开,进行相应的重连或错误处理。

六、TCP 服务器和客户端的安全编程

(一)数据加密

在网络通信中,数据可能会被窃取或篡改。为了保证数据的安全性,可以使用加密算法对数据进行加密。在 Python 中,可以使用 cryptography 库来实现数据加密。例如,使用对称加密算法 AES(高级加密标准):

from cryptography.fernet import Fernet


# 生成密钥
key = Fernet.generate_key()
cipher_suite = Fernet(key)

# 加密数据
data = b'Sensitive information'
encrypted_data = cipher_suite.encrypt(data)

# 解密数据
decrypted_data = cipher_suite.decrypt(encrypted_data)
print(decrypted_data)


在 TCP 通信中,发送端可以在发送数据前对数据进行加密,接收端接收到数据后进行解密。这样即使数据在传输过程中被截取,没有密钥也无法获取真实数据。

(二)认证机制

为了确保连接的合法性,防止非法客户端连接服务器,可以实现认证机制。常见的认证方式有用户名和密码认证。在服务器端,可以维护一个用户列表,客户端连接时,发送用户名和密码,服务器验证通过后才允许进行后续通信。例如:

users = {
    'user1': 'password1',
    'user2': 'password2'
}


def authenticate(client_socket):
    client_socket.sendall(b'Enter username: ')
    username = client_socket.recv(1024).decode().strip()
    client_socket.sendall(b'Enter password: ')
    password = client_socket.recv(1024).decode().strip()
    if username in users and users[username] == password:
        client_socket.sendall(b'Authentication successful')
        return True
    else:
        client_socket.sendall(b'Authentication failed')
        return False


在服务器处理客户端连接的函数中,调用 authenticate 函数进行认证,认证通过后再进行其他数据交互操作。

(三)防止 DoS 攻击

DoS(Denial of Service,拒绝服务)攻击是一种常见的网络攻击方式,攻击者通过向服务器发送大量请求,耗尽服务器资源,导致服务器无法正常为合法用户提供服务。为了防止 DoS 攻击,可以采取以下措施:

  1. 限制连接速率:在服务器端设置每个 IP 地址的连接速率限制,例如,每分钟只允许某个 IP 地址建立一定数量的连接。可以使用计数器和时间戳来实现该功能。
  2. 验证码机制:在客户端连接服务器时,要求客户端输入验证码,只有输入正确验证码的客户端才能建立连接。这样可以防止自动化的攻击脚本大量连接服务器。

七、总结 TCP 服务器和客户端编程要点

  1. 服务器端
    • 选择合适的网络模块,如 socket 模块,根据需求选择是否使用 selectors 模块进行 I/O 多路复用优化。
    • 正确绑定服务器地址和端口,注意端口冲突问题。
    • 合理处理客户端连接,根据并发需求选择单线程、多线程或 I/O 多路复用方式。
    • 处理粘包问题,确保数据准确接收和解析。
    • 考虑安全编程,如数据加密、认证机制和防止 DoS 攻击。
  2. 客户端
    • 创建套接字并正确连接到服务器。
    • 处理网络延迟和超时问题,设置合适的超时时间。
    • 与服务器进行数据交互时,注意数据的编码和解码,以及粘包问题的处理。
    • 同样要考虑安全问题,如对发送和接收的数据进行加密和解密。

通过深入理解 TCP 协议原理,熟练运用 Python 的网络编程模块,并注意处理常见问题和安全编程,就能够编写出高效、稳定且安全的 TCP 服务器和客户端程序。