🌀 鱼油のB10g

✦ 不定期更新技术随想

✦ 分享奇妙发现

📌 近期动态:

探索AI和工具使用...

拼客学院 拼客全栈攻防 第8章 第2课——功能速记
哈喽,又见面啦!💻 上次咱们一起用 UDP 协议搞了个简单的文件传输小实验,是不是觉得不过瘾?UDP 虽然快,但它“丢三落四”的毛病可不适合所有场景。今天,咱们就来升级一下,搞点更可靠的——TCP 协议


为什么要用 TCP?

简单来说,TCP(传输控制协议)就是个更“负责任”的协议。它能保证你发出去的数据:

  • 完整无损地到达:数据包不会丢,少一个都不行。
  • 按顺序到达:先发的数据先到,不会乱套。
  • 可靠性高:有确认机制、重传机制等等,确保数据成功传输。

所以,像网页浏览、文件下载、邮件发送这些对数据完整性要求高的场景,都得靠 TCP。


今天咱们的目标是什么?

和上次类似,我们依然要搭建一个服务器客户端,用 TCP 协议来实现:

  • 服务器:监听连接,接收客户端请求后,发送当前时间和一张图片。
  • 客户端:连接服务器,接收时间,然后接收图片并保存。

同样的,程序执行时会用字符串清晰地说明连接到了哪台服务器和对应的端口,依然使用 IPv4。


第一步:搭建我们的“被攻击机”(TCP 服务器端)

TCP 服务器端相比 UDP 会稍微复杂一点点,因为它涉及“连接”的概念。

  1. 创建一个 TCP Socket:不同于 UDP,这里是 SOCK_STREAM
  2. 绑定 IP 地址和端口:和 UDP 一样。
  3. 开始监听连接:等待客户端的到来。
  4. 接受客户端连接:每当有客户端连接,服务器就会“牵手”成功,形成一个独立的连接。
  5. 循环接收和发送:在建立的连接上进行数据交换。
  6. 发送当前时间
  7. 发送图片

来来来,代码奉上!别忘了准备一张 test.jpg 图片放在和脚本同级目录下哦。

import socket
import datetime
import os
import time

# 服务器配置
SERVER_IP = '0.0.0.0'  # 监听所有可用的IP地址
SERVER_PORT = 12345    # 你可以选择一个不常用的端口

# 图片路径
IMAGE_PATH = 'test.jpg' # 替换成你的图片路径

def run_server():
    # 1. 创建 TCP Socket
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # 允许地址重用,避免端口被占用
    server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

    # 2. 绑定 IP 地址和端口
    server_socket.bind((SERVER_IP, SERVER_PORT))

    # 3. 开始监听连接,最多允许5个排队连接
    server_socket.listen(5)
    print(f"服务器已启动,正在监听 {SERVER_IP}:{SERVER_PORT}")
    print("等待客户端连接...")

    while True:
        # 4. 接受客户端连接
        # accept() 方法会阻塞,直到有客户端连接过来,然后返回一个新的socket对象和客户端地址
        client_socket, client_address = server_socket.accept()
        print(f"---")
        print(f"成功接受来自客户端的连接: {client_address[0]}:{client_address[1]}")

        try:
            # 5.1 发送当前时间
            current_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S").encode('utf-8')
            # 先发送时间数据的长度,让客户端知道要接收多少数据
            client_socket.sendall(len(current_time).to_bytes(4, 'big')) # 4字节表示长度
            client_socket.sendall(current_time)
            print(f"已发送当前时间: {current_time.decode('utf-8')}")
            time.sleep(0.1) # 短暂等待,确保客户端处理完时间

            # 5.2 发送图片
            if os.path.exists(IMAGE_PATH):
                file_size = os.path.getsize(IMAGE_PATH)
                # 先发送图片文件的大小
                client_socket.sendall(file_size.to_bytes(8, 'big')) # 8字节表示文件大小
                print(f"开始发送图片 '{IMAGE_PATH}', 大小: {file_size} 字节...")

                with open(IMAGE_PATH, 'rb') as f:
                    # 分块发送图片
                    while True:
                        image_chunk = f.read(4096) # 每次读取4KB
                        if not image_chunk:
                            break
                        client_socket.sendall(image_chunk)
                print(f"图片 '{IMAGE_PATH}' 已发送完成!")
            else:
                error_message = f"错误:图片文件 '{IMAGE_PATH}' 不存在!".encode('utf-8')
                client_socket.sendall(len(error_message).to_bytes(4, 'big'))
                client_socket.sendall(error_message)
                print(f"错误:图片文件 '{IMAGE_PATH}' 不存在!")

            print(f"---")
        except Exception as e:
            print(f"与客户端 {client_address} 通信时发生错误: {e}")
        finally:
            # 关闭当前客户端连接
            client_socket.close()
            print(f"客户端 {client_address} 连接已关闭。")

    # 服务器关闭前关闭主socket (通常服务器会一直运行)
    # server_socket.close()
    # print("服务器已关闭。")

if __name__ == "__main__":
    run_server()

代码解释:

  • socket.socket(socket.AF_INET, socket.SOCK_STREAM):这里的 SOCK_STREAM 明确指明使用 TCP 协议。
  • server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1):这个很重要!它允许我们快速重启服务器,避免“Address already in use”的错误。
  • server_socket.listen(5):让服务器进入监听状态,5 表示允许最多有5个客户端排队等待连接。
  • client_socket, client_address = server_socket.accept():这是 TCP 的核心。当有客户端连接时,accept() 会返回一个新的 socket 对象 (client_socket),这个 socket 专门用于和当前客户端进行通信,以及客户端的地址 (client_address)。
  • sendall():这是一个方便的方法,它会确保所有数据都发送出去,直到完全发送成功。
  • 数据长度预发送:在发送时间字符串和图片数据之前,我们都先发送了数据本身的长度。这是 TCP 通信中的常见做法,因为 TCP 是字节流协议,客户端需要知道何时停止接收数据。to_bytes(4, 'big') 将整数转换成4字节的大端序字节串。

第二步:打造我们的“攻击机”(TCP 客户端)

TCP 客户端也需要和服务器建立连接,然后才能进行数据交换。

  1. 创建一个 TCP Socket
  2. 连接服务器:这是 TCP 和 UDP 的主要区别之一。
  3. 接收时间数据:按照服务器发送的长度来接收。
  4. 接收图片数据:同样根据服务器发送的文件大小来接收,并保存。
  5. 关闭连接:完成通信后断开连接。

客户端代码来啦!

import socket
import os
import time

# 服务器配置 (要和服务器端的IP和端口一致哦!)
SERVER_IP = '127.0.0.1' # 如果服务器和客户端在同一台机器上,用127.0.0.1
SERVER_PORT = 12345

# 接收图片保存的路径
RECEIVED_IMAGE_PATH = 'received_image_tcp.jpg'

def run_client():
    # 1. 创建 TCP Socket
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    try:
        # 2. 连接服务器
        client_socket.connect((SERVER_IP, SERVER_PORT))
        print(f"成功连接到服务器: {SERVER_IP}:{SERVER_PORT}")

        # 3. 接收时间数据
        # 先接收时间数据的长度
        time_len_bytes = client_socket.recv(4)
        if not time_len_bytes:
            raise Exception("未接收到时间长度信息")
        time_len = int.from_bytes(time_len_bytes, 'big')

        # 接收时间数据
        current_time_data = client_socket.recv(time_len)
        print(f"收到服务器时间: {current_time_data.decode('utf-8')}")

        # 4. 接收图片数据
        # 先接收图片文件的大小
        file_size_bytes = client_socket.recv(8)
        if not file_size_bytes:
            raise Exception("未接收到图片大小信息")
        file_size = int.from_bytes(file_size_bytes, 'big')
        print(f"开始接收图片,大小: {file_size} 字节...")

        received_bytes = 0
        with open(RECEIVED_IMAGE_PATH, 'wb') as f:
            while received_bytes < file_size:
                # 接收数据,避免一次性接收过大导致内存问题
                chunk = client_socket.recv(4096)
                if not chunk:
                    # 如果服务器断开连接,或者没有更多数据了
                    break
                f.write(chunk)
                received_bytes += len(chunk)
                # 可以添加一个进度条,让用户看到接收进度
                # print(f"\r接收进度: {received_bytes}/{file_size} 字节", end="")
        print(f"\n图片接收完成!已保存到: {os.path.abspath(RECEIVED_IMAGE_PATH)}")


    except ConnectionRefusedError:
        print(f"连接被拒绝!请检查服务器 {SERVER_IP}:{SERVER_PORT} 是否已启动或防火墙设置。")
    except socket.timeout:
        print(f"连接服务器 {SERVER_IP}:{SERVER_PORT} 超时!")
    except Exception as e:
        print(f"客户端发生错误: {e}")
    finally:
        # 5. 关闭连接
        client_socket.close()
        print("客户端连接已关闭。")

if __name__ == "__main__":
    run_client()

代码解释:

  • client_socket.connect((SERVER_IP, SERVER_PORT)):这是 TCP 客户端建立连接的关键一步。
  • int.from_bytes(time_len_bytes, 'big'):将接收到的字节串(表示长度)转换回整数。
  • 按长度接收数据:客户端根据服务器预先发送的长度信息,精确地接收时间数据和图片数据,避免了 UDP 中可能出现的粘包或分包问题,这也是 TCP 可靠性的体现。

实战演练:怎么运行呢?

  1. 准备工作

    • 确保你的电脑上安装了 Python。
    • 在服务器端脚本所在的文件夹里,放一张名为 test.jpg 的图片(或者修改服务器代码中的 IMAGE_PATH 到你自己的图片路径)。
  2. 先启动服务器端

    • 打开一个命令行窗口 (CMD 或 PowerShell)。
    • 进入你存放 tcp_server.py 文件的目录。
    • 运行命令:python tcp_server.py
    • 你会看到服务器打印出“服务器已启动,正在监听...”之类的字样。
  3. 再启动客户端

    • 打开另一个命令行窗口。
    • 进入你存放 tcp_client.py 文件的目录。
    • 运行命令:python tcp_client.py
    • 如果一切顺利,你会看到客户端打印出连接成功、接收时间、接收图片完成的信息,并且在客户端脚本同目录下会生成一个 received_image_tcp.jpg 的图片文件。

小小的总结

通过这次 TCP 小实验,我们是不是更清楚地感受到了 TCP 的“可靠”和“负责”呢?虽然代码量稍微增加了一些,但换来的是数据传输的稳定和完整。

UDP 适合对速度要求高、少量数据、允许丢包的场景(比如直播、在线游戏)。
TCP 则适合对可靠性要求高、大数据量传输的场景(比如文件传输、网页浏览)。

希望这次的分享能让你对网络编程的两种主要协议有更深刻的理解!如果你有任何疑问,或者想玩点更高级的,比如多线程 TCP 服务器,欢迎在评论区告诉我!👍


posted on 2025-06-25 13:39  鱼油YOU  阅读(29)  评论(0)    收藏  举报