杰瑞科技汇

python tcp struct

Of course! The combination of TCP, struct, and Python is a very common and powerful pattern for network programming. It's used to send data that isn't just plain text, like numbers, binary data, or complex data structures.

python tcp struct-图1
(图片来源网络,侵删)

Let's break it down into three parts and then combine them into a practical example.

The struct Module: Packing and Unpacking Binary Data

Python's built-in struct module converts between Python values and C-style data types represented as bytes objects. This is essential for creating a predictable binary format.

Key Functions:

  • struct.pack(format, ...): Takes a format string and Python values, and returns a bytes object.
  • struct.unpack(format, data): Takes a format string and a bytes object, and returns a tuple of unpacked values.
  • struct.calcsize(format): Calculates the size (in bytes) of the data structure described by the format string.

Format Characters (Common Examples): | Character | C Type | Python Type | Size (bytes) | | :-------- | :------------- | :---------- | :----------- | | x | pad byte | No value | 1 | | c | char | bytes (len 1)| 1 | | b | signed char | int | 1 | | B | unsigned char | int | 1 | | h | short | int | 2 | | H | unsigned short | int | 2 | | i | int | int | 4 | | I | unsigned int | int | 4 | | f | float | float | 4 | | d | double | float | 8 | | s | char[] | bytes | | | q | long long | int | 8 |

python tcp struct-图2
(图片来源网络,侵删)

Important: The format string can be prefixed with a character to specify byte order:

  • <: little-endian (most common on x86/x64)
  • >: big-endian (network byte order, often used for TCP/IP)
  • network byte order (same as >)
  • native byte order, size, and alignment
  • native byte order, standard size and alignment

TCP Sockets in Python

Python's socket module provides access to the BSD socket interface. The standard flow is:

Server:

  1. socket.socket(): Create a socket object.
  2. socket.bind(): Assign an address (IP, port) to the socket.
  3. socket.listen(): Enable the server to accept connections.
  4. socket.accept(): Block and wait for an incoming connection. Returns a new socket for the connection and the client's address.
  5. socket.recv(): Receive data from the client.
  6. socket.sendall(): Send data to the client.
  7. socket.close(): Close the connection.

Client:

  1. socket.socket(): Create a socket object.
  2. socket.connect(): Connect to the server's address.
  3. socket.sendall(): Send data to the server.
  4. socket.recv(): Receive data from the server.
  5. socket.close(): Close the connection.

The Challenge: Sending Structured Data

If you try to send a Python int directly, you'll get an error: TypeError: a bytes-like object is required, not 'int'. You must convert it to bytes.

The naive approach is to just pack the data and send it:

# This is problematic!
data = struct.pack('>i', 42) # 4 bytes
socket.sendall(data)

The problem is: How does the receiver know how many bytes to expect? The receiver's recv(1024) might get only 2 of those 4 bytes, or it might get your 4 bytes plus the first 4 bytes of the next message.

The Solution: A Message Header

The most robust solution is to prefix your message with a header that describes the length of the data to follow.

A common and simple structure is: [Header (4 bytes, message length)][Payload (N bytes)]

Let's create a simple protocol to send a single integer.


Complete Example: Client/Server for a Single Integer

Here is a full, working example where a client sends a single integer to a server, which then sends back the square of that integer.

The Server (server.py)

import socket
import struct
# Use a port above 1024 to avoid needing root/admin privileges
HOST = '127.0.0.1'  # Standard loopback interface address (localhost)
PORT = 65432        # Port to listen on (non-privileged ports are > 1023)
# Create a TCP socket
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.bind((HOST, PORT))
    s.listen()
    print(f"Server listening on {HOST}:{PORT}")
    # accept() waits for a new connection and returns a new socket object 'conn'
    # and the address of the client 'addr'
    conn, addr = s.accept()
    with conn:
        print(f"Connected by {addr}")
        while True:
            # First, receive the header (4 bytes) to get the payload size
            header = conn.recv(4)
            if not header:
                # If recv returns an empty object, the client has closed the connection
                break
            # Unpack the 4-byte unsigned integer from the header
            # The '!' prefix ensures network byte order (big-endian)
            payload_size = struct.unpack('!I', header)[0]
            print(f"Received header. Payload size: {payload_size} bytes.")
            # Now, receive the actual payload
            payload_data = b''
            # We need to keep receiving until we have the full payload
            while len(payload_data) < payload_size:
                # recv() returns up to 4096 bytes of data
                chunk = conn.recv(4096)
                if not chunk:
                    # Connection broken before full payload received
                    raise ConnectionError("Connection broken while receiving payload")
                payload_data += chunk
            print(f"Received full payload: {payload_data}")
            # Unpack the integer from the payload
            # We expect one 4-byte integer
            number = struct.unpack('!i', payload_data)[0]
            print(f"Unpacked number: {number}")
            # Calculate the result
            result = number * number
            # Pack the result into a new payload
            response_payload = struct.pack('!i', result)
            # Create a header for the response
            response_header = struct.pack('!I', len(response_payload))
            # Send the header and the payload
            conn.sendall(response_header + response_payload)
            print(f"Sent response: {result}")

The Client (client.py)

import socket
import struct
import time
HOST = '127.0.0.1'  # The server's hostname or IP address
PORT = 65432        # The port used by the server
# Create a TCP socket
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.connect((HOST, PORT))
    print(f"Connected to server at {HOST}:{PORT}")
    # The number we want to send
    number_to_send = 12345
    # 1. Pack the number into a payload
    payload = struct.pack('!i', number_to_send)
    # 2. Create a header that describes the payload's length
    # 'I' is an unsigned int, 4 bytes, which is perfect for lengths.
    header = struct.pack('!I', len(payload))
    # 3. Send the header first, then the payload
    print(f"Sending header: {len(payload)} bytes")
    s.sendall(header)
    print(f"Sending payload: {number_to_send}")
    s.sendall(payload)
    # 4. Receive the response from the server
    # First, get the header
    response_header = s.recv(4)
    if not response_header:
        raise ConnectionError("Server closed connection unexpectedly")
    # Unpack the header to get the response payload size
    response_payload_size = struct.unpack('!I', response_header)[0]
    print(f"Received response header. Payload size: {response_payload_size} bytes.")
    # Then, get the response payload
    response_payload = b''
    while len(response_payload) < response_payload_size:
        chunk = s.recv(4096)
        if not chunk:
            raise ConnectionError("Connection broken while receiving response")
        response_payload += chunk
    # Unpack the result
    result = struct.unpack('!i', response_payload)[0]
    print(f"Received result from server: {result}")

How to Run It

  1. Save the two code blocks above as server.py and client.py.
  2. Open two terminal windows.
  3. In the first terminal, run the server:
    python server.py

    You will see: Server listening on 127.0.0.1:65432

  4. In the second terminal, run the client:
    python client.py

Expected Output:

Server Terminal:

Server listening on 127.0.0.1:65432
Connected by ('127.0.0.1', 54321)
Received header. Payload size: 4 bytes.
Received full payload: b'{'
Unpacked number: 12345
Sent response: 152399025

Client Terminal:

Connected to server at 127.0.0.1:65432
Sending header: 4 bytes
Sending payload: 12345
Received response header. Payload size: 4 bytes.
Received result from server: 152399025

Advanced: Sending More Complex Data (e.g., a String and an Integer)

You can extend this pattern. To send a string and an integer, you would:

  1. Define a protocol: For example, [Header (4 bytes)][String Length (4 bytes)][String (N bytes)][Integer (4 bytes)].
  2. On the sender side:
    • Encode the string to bytes (e.g., my_string.encode('utf-8')).
    • Pack the string length, the string bytes, and the integer into a single payload.
    • Create a header for the total payload length.
    • Send header + payload.
  3. On the receiver side:
    • Receive the header to know the total length.
    • Receive the full payload.
    • Unpack the string length, then use that to slice out the string bytes.
    • Unpack the integer from the remaining bytes.
分享:
扫描分享到社交APP
上一篇
下一篇