观前提醒:本文主要内容为使用Python在局域网内建立TCP连接并传输图片信息,计算机为一块旭日X3和一块英伟达Orin NX。

一、什么是TCP


TCP(传输控制协议)是一种可靠的、面向连接的协议,它确保数据包的顺序传输和完整性。当需要确保数据完全无误地传送到目的地时,TCP 是一个不错的选择。在我们的示例中,一台计算机将作为服务器来接收图片,另一台计算机作为客户端来发送图片。

那TCP通信的流程是什么呢?

1.1  三次握手(Three-Way Handshake)

在传输任何数据之前,客户端和服务器之间首先进行连接建立的过程,客户端会和服务端进行三次握手。

分别是:

  1. SYN:客户端发送一个带有 SYN(同步序列编号)标志的数据包,以初始化一个连接。

  2. SYN-ACK:服务器接收到 SYN 请求后,发送一个带有 SYN 和 ACK(确认)标志的数据包作为响应。

  3. ACK:客户端收到服务器的 SYN-ACK 响应后,再发送一个带有 ACK 标志的数据包来确认,并且连接就此建立。

1.2  数据传输

一旦三次握手完成,连接就建立了,数据可以在客户端和服务器之间传输。

 

  • 数据段:客户端和服务器可以开始交换数据段。TCP 保证了数据的顺序和完整性。

  • 确认:接收方在收到一个或多个数据段后,会发送 ACK 来确认收到。

1.3 流量控制

TCP 使用窗口大小(window size)来进行流量控制,确保发送方不会溢出接收方的缓冲区。

 

窗口调整:如果接收方处理数据的速度跟不上发送方的发送速度,它可以通过调整窗口大小来告知发送方减慢发送速度。

 

1.4 拥塞控制

当网络拥塞时,TCP 会自动降低数据的发送速率。

下面是 TCP 中用于控制网络拥塞的一些算法:

慢启动、拥塞避免、快速重传、快速恢复

1.5 错误检测和重传

TCP 提供了校验和,用来检测数据传输中的错误。如果检测到错误,或者某个数据段未被确认,会触发重传机制。

 

1.6. 四次挥手(Four-Way Handshake)

当数据传输完成,任一方都可以发起连接的终止。

 

  • FIN:发起关闭连接的一方会发送一个带有 FIN 标志的数据包。

  • ACK:接收到 FIN 的另一方会发送 ACK 来确认。

  • FIN:之后,接收 FIN 的一方也发送一个 FIN 来表示它也准备关闭连接。

  • ACK:最初发起 FIN 的一方发送 ACK 确认,然后连接关闭。

1.7 时间等待(TIME_WAIT)

最终关闭连接之前,TCP 保持一个时间等待状态,以确保所有的数据包都已经被确认,防止在网络中延迟的数据包在连接关闭后到达。

 

通过上述步骤,TCP 保证了在不可靠的网络环境中可靠的、按序的和无差错的数据传输。

image.png

TCP通信流程

得益于我们站在了巨人肩膀上,我们不需要专注于上述的流程,可以使用一些封装之后的函数来实现我们的目标

二、服务端实现(接收端)

服务器端程序将在等待接收图片的计算机上运行。它将监听一个 TCP 端口并等待客户端的连接。一旦建立连接,服务器就准备接收文件。

王子公主们请看代码:

import socket
from PIL import Image
import io

# 服务器的 IP 地址和端口号
server_ip = '192.168.3.96'
server_port = 8080

# 创建一个 TCP/IP 套接字
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 绑定套接字到 IP 地址和端口号
server_address = (server_ip, server_port)
sock.bind(server_address)

# 监听传入连接
sock.listen(1)

while True:
    print('等待连接...')
    connection, client_address = sock.accept()
    
    try:
        print(f"连接成功: {client_address}")
        
        # 接收图片大小
        size_data = connection.recv(4)
        size = int.from_bytes(size_data, byteorder='big')
        print(f"接收到的图片大小为: {size} 字节")
        
        # 接收图片数据
        image_data = b''
        while len(image_data) < size:
            packet = connection.recv(4096)
            if not packet:
                break
            image_data += packet
        
        # 将接收到的数据转换为图像并保存
        image = Image.open(io.BytesIO(image_data))
        image.save('received_image.jpg')
        print("图片接收并保存成功")
        
    finally:
        # 清理连接
        connection.close()

运行程序,程序会监听8080端口,等待连接

image.png

接收图片后会再次进入监听状态

image.png

跟我学,下面咱们来一句句分析代码

导入模块:

import socket
from PIL import Image
import io

  • socket:这个模块提供了访问BSD套接字接口的方法,用于创建能够建立TCP/IP网络连接的套接字对象。

  • PIL (Python Imaging Library),特别是 Image:用于图像处理。在这里,它用于处理通过网络接收的图像数据。

  • io:io 模块提供了处理各种类型的I/O(输入/输出)的主要功能。这里它用于从接收到的图像数据创建一个字节流。

 

服务器IP和端口:

server_ip = '192.168.3.96'
server_port = 8080

这些行定义了服务器监听传入连接的IP地址和端口号。IP地址应该是运行服务器的机器的地址,端口号是一个任意的数字,用于将网络流量指向特定的应用程序。

 

创建套接字:

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

这行代码使用AF_INET地址族(表示IPv4)和SOCK_STREAM套接字类型(表示TCP)来创建一个新的套接字。

 

绑定套接字:

server_address = (server_ip, server_port)
sock.bind(server_address)

服务器的IP地址和端口号被打包成一个元组,并传递给sock.bind()方法,该方法将套接字绑定到指定的地址和端口。这是服务器接收发送到该IP和端口的数据所必需的。

 

监听连接:

sock.listen(1)

服务器被设置为监听传入连接。参数1表示最大排队连接数(积压工作量)。

 

接受连接:

print('等待连接...')
connection, client_address = sock.accept()

服务器使用sock.accept()等待传入连接,此方法将阻塞直到收到连接。一旦建立连接,它将返回一个新的套接字对象connection和客户端的地址client_address。

接下来的代码块是在一个try语句中,确保即使在出现异常的情况下也能够正确地关闭连接。

 

接收数据:

size_data = connection.recv(4)
size = int.from_bytes(size_data, byteorder='big')

这段代码首先接收一个4字节的数据,这4字节表示接下来要接收的图像数据的大小。int.from_bytes函数用于将接收到的字节转换为一个整数(大小表示)。

 

读取图像数据:

image_data = b''
while len(image_data) < size:
    packet = connection.recv(4096)
    if not packet:
        break
    image_data += packet

接下来的循环读取指定大小的图像数据。每次循环从连接中接收最多4096字节的数据,并将其追加到image_data变量中。如果connection.recv返回空数据,表示连接已经关闭,循环将中断。

 

处理图像数据:

image = Image.open(io.BytesIO(image_data))
image.save('received_image.jpg')

一旦所有数据被接收,代码使用PIL库来处理图像数据。io.BytesIO用于创建一个字节流,Image.open用这个字节流来打开图像,然后图像被保存到磁盘。

 

清理:

connection.close()

在finally块中,无论发生何种情况,代码都会关闭连接,以确保没有资源泄漏。

三、客户端实现(发送端)

完整代码如下:

import socket
import os

# 图片文件路径
image_path = 'download.jpg'  # 替换为实际图片路径

# 2 号计算机的 IP 地址和端口号
server_ip = '192.168.3.96'
server_port = 8080

# 创建一个 TCP/IP 套接字
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 连接 2 号计算机上的服务器
server_address = (server_ip, server_port)
sock.connect(server_address)

try:
    # 读取图片文件并发送
    with open(image_path, 'rb') as image_file:
        image_data = image_file.read()
    
    # 发送图片大小
    sock.sendall(len(image_data).to_bytes(4, byteorder='big'))
    
    # 发送图片数据
    sock.sendall(image_data)
    
    print(f"图片 {image_path} 发送成功")
finally:
    # 关闭套接字
    sock.close()

这个客户端程序的核心功能是将本地的图片文件作为二进制数据流发送到服务器。

在发送数据前,客户端首先发送了数据的大小,这样服务器端就知道应该接收多少数据,这是一个常见的做法,以确保服务器能够正确地接收完整的文件数据。

 

客户端程序运行前要先运行服务端程序

不然就会:

image.png

连接成功后会显示:

image.png

下面详细分析代码的每个部分:

设置图片路径和服务器信息:

image_path = 'download.jpg'  # 替换为实际图片路径
server_ip = '192.168.3.96'
server_port = 8080

image_path 变量指定了要发送的图片文件的路径。在实际使用中,需要替换为实际的图片文件路径。

server_ip`和 server_port`定义了服务器的 IP 地址和端口号。这是客户端需要连接到的目的地。

 

创建 TCP/IP 套接字:

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

这行代码创建了一个新的套接字对象。`AF_INET` 指定了使用 IPv4 协议,`SOCK_STREAM` 指定了使用 TCP 协议。

 

连接到服务器:

server_address = (server_ip, server_port)
sock.connect(server_address)

server_address是一个包含 IP 地址和端口的元组。

sock.connect()方法用这个地址和端口来建立 TCP 连接。

 

发送图片数据:

with open(image_path, 'rb') as image_file:
    image_data = image_file.read()

这里使用 `with` 语句和 `open` 函数以二进制读取模式(`'rb'`)打开图片文件。`with` 语句确保文件在操作完成后会被正确关闭。`read()` 方法读取整个文件的内容并将其存储在 `image_data` 变量中。

 

发送图片大小:

sock.sendall(len(image_data).to_bytes(4, byteorder='big'))

len(image_data)获取图片数据的字节大小。

to_bytes(4, byteorder='big') 将这个大小转换为 4 字节的二进制表示。`byteorder='big'` 表示使用大端字节序。

 

发送图片数据:

sock.sendall(image_data)

sendall()`方法确保所有给定的数据都被发送到网络。如果必要,该方法会调用底层的 `send` 方法多次,直到全部数据发送完毕。

 

终端输出:

print(f"图片 {image_path} 发送成功")

这行代码在命令行输出发送成功的信息。

 

关闭套接字:

sock.close()

无论数据发送成功或者出现异常,`finally` 块确保套接字在结束前被关闭,这是一个很好的实践,可以释放网络资源和端口。

 

注:这张图片今天被我传了几千次

download.jpg