You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Unity C#客户端与树莓派Python服务器TCP重连异常求助

解决TCP重连后发送指令导致连接断开的问题

看起来你遇到的是典型的TCP连接生命周期处理不当的问题,我们分Unity客户端和Python服务器两部分来逐一修复:

一、Unity客户端的问题与修复

你的客户端代码存在几个关键隐患:

  • 同步Receive会阻塞Unity主线程,导致UI冻结,连接断开时还会触发未捕获的异常
  • 断开连接后没有彻底清理旧的Socket实例,重新连接时可能复用失效对象
  • 发送指令前缺少连接状态检查,也没有异常捕获逻辑

修正后的Unity代码

using System.Collections.Generic;
using UnityEngine;
using System.Net;
using System.Net.Sockets;
using UnityEngine.UI;
using System.Threading;

public class Control : MonoBehaviour {
    public string IP = "192.168.8.100";
    public int Port = 6704;
    public Socket client;
    public Text display;
    private Thread receiveThread; // 子线程处理接收,避免阻塞主线程
    private bool isConnected = false;

    // 统一封装指令发送逻辑,减少重复代码
    private void SendCommand(string cmd)
    {
        if (!isConnected || client == null || !client.Connected)
        {
            Debug.LogError("未连接到服务器,无法发送指令");
            display.text = "未连接,无法发送";
            return;
        }

        try
        {
            byte[] data = System.Text.Encoding.ASCII.GetBytes(cmd);
            client.Send(data);
        }
        catch (SocketException ex)
        {
            Debug.LogError($"发送指令失败:{ex.Message}");
            HandleDisconnect();
        }
    }

    // 按钮点击方法简化
    public void Forward_Click() => SendCommand("1");
    public void Back_Click() => SendCommand("2");
    public void Right_Click() => SendCommand("3");
    public void Left_Click() => SendCommand("4");
    public void R_Turn_Click() => SendCommand("5");
    public void L_Turn_Click() => SendCommand("6");
    public void FR_Click() => SendCommand("7");
    public void FL_Click() => SendCommand("8");
    public void OnRelease() => SendCommand("0");

    public void Connecting()
    {
        // 先断开旧连接,避免资源冲突
        if (isConnected)
        {
            HandleDisconnect();
        }

        try
        {
            client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            client.Connect(IP, Port);
            isConnected = true;
            display.text = "已连接到服务器";

            // 启动子线程处理数据接收
            receiveThread = new Thread(ReceiveDataLoop);
            receiveThread.IsBackground = true;
            receiveThread.Start();
        }
        catch (SocketException ex)
        {
            Debug.LogError($"连接失败:{ex.Message}");
            display.text = "连接失败";
            HandleDisconnect();
        }
    }

    // 子线程循环接收数据
    private void ReceiveDataLoop()
    {
        byte[] buffer = new byte[1024];
        while (isConnected && client != null && client.Connected)
        {
            try
            {
                int receivedBytes = client.Receive(buffer);
                if (receivedBytes <= 0)
                {
                    Debug.Log("服务器断开连接");
                    HandleDisconnect();
                    break;
                }
                string receivedData = System.Text.Encoding.ASCII.GetString(buffer, 0, receivedBytes);
                Debug.Log($"从Python收到数据:{receivedData}");
                
                // Unity主线程才能更新UI,用Invoke切回主线程
                Invoke(nameof(UpdateDisplayWithData), 0, receivedData);
            }
            catch (SocketException ex)
            {
                Debug.LogError($"接收数据异常:{ex.Message}");
                HandleDisconnect();
                break;
            }
        }
    }

    private void UpdateDisplayWithData(string data)
    {
        display.text = $"收到:{data}";
    }

    public void Disconnect()
    {
        HandleDisconnect();
    }

    // 统一处理断开连接的资源清理
    private void HandleDisconnect()
    {
        isConnected = false;
        if (client != null)
        {
            try
            {
                client.Shutdown(SocketShutdown.Both);
            }
            catch { }
            client.Close();
            client = null;
        }
        if (receiveThread != null && receiveThread.IsAlive)
        {
            receiveThread.Abort();
            receiveThread = null;
        }
        display.text = "已断开连接";
    }

    // 场景销毁时确保清理所有资源
    private void OnDestroy()
    {
        HandleDisconnect();
    }
}

Unity代码修改说明

  • 用子线程处理接收:避免同步Receive阻塞主线程,解决UI冻结和异常问题
  • 统一SendCommand方法:增加连接状态校验,发送时捕获异常,出现问题自动触发断开逻辑
  • 新增HandleDisconnect:集中处理Socket、线程的资源清理,避免残留无效对象
  • 主线程更新UI:通过Invoke将UI操作切回主线程,符合Unity的线程安全要求

二、Python服务器的核心问题与修复

你的问题根源大概率在服务器端:新手常写的TCP服务器只接受一次连接,客户端断开后就退出监听,无法处理新连接。正确的TCP服务器需要循环监听新连接,并为每个客户端分配独立的处理线程。

正确的Python服务器示例代码

import socket
import threading
import RPi.GPIO as GPIO

# 继电器GPIO配置(根据你的硬件调整引脚)
RELAY_PINS = {
    "1": 17, "2": 18, "3": 27, "4": 22,
    "5": 23, "6": 24, "7": 25, "8": 4
}

def setup_relays():
    GPIO.setmode(GPIO.BCM)
    for pin in RELAY_PINS.values():
        GPIO.setup(pin, GPIO.OUT)
        GPIO.output(pin, GPIO.LOW) # 默认关闭继电器

def handle_client(client_socket, client_addr):
    print(f"新客户端连接:{client_addr}")
    try:
        while True:
            # 接收客户端指令
            data = client_socket.recv(1024).decode('ascii').strip()
            if not data:
                # 收到空数据表示客户端断开
                print(f"客户端 {client_addr} 断开连接")
                break
            
            print(f"收到指令:{data}")
            # 处理继电器逻辑
            if data in RELAY_PINS:
                # 切换继电器状态(可根据需求改为固定高低电平)
                current_state = GPIO.input(RELAY_PINS[data])
                GPIO.output(RELAY_PINS[data], GPIO.HIGH if current_state == GPIO.LOW else GPIO.LOW)
                client_socket.send(f"指令 {data} 已执行".encode('ascii'))
            elif data == "0":
                # 关闭所有继电器
                for pin in RELAY_PINS.values():
                    GPIO.output(pin, GPIO.LOW)
                client_socket.send("所有继电器已关闭".encode('ascii'))
            else:
                client_socket.send("无效指令".encode('ascii'))
    except Exception as e:
        print(f"处理客户端 {client_addr} 时出错:{e}")
    finally:
        client_socket.close()
        print(f"客户端 {client_addr} 连接已关闭")

def main():
    setup_relays()
    HOST = '0.0.0.0' # 监听所有网卡
    PORT = 6704

    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 允许复用端口,避免重启时端口占用
    server_socket.bind((HOST, PORT))
    server_socket.listen(5) # 最大等待连接数
    print(f"服务器启动,监听端口 {PORT}...")

    try:
        while True:
            # 循环接受新连接
            client_socket, client_addr = server_socket.accept()
            # 为每个客户端启动独立线程
            client_thread = threading.Thread(target=handle_client, args=(client_socket, client_addr))
            client_thread.daemon = True # 主线程退出时自动关闭子线程
            client_thread.start()
    except KeyboardInterrupt:
        print("服务器正在关闭...")
    finally:
        GPIO.cleanup()
        server_socket.close()

if __name__ == "__main__":
    main()

Python服务器修改说明

  • 循环监听新连接:while True持续调用accept(),确保客户端断开后服务器能继续接受新连接
  • 独立线程处理客户端:单个客户端的断开不会影响整个服务器的运行
  • 设置SO_REUSEADDR:允许服务器重启时立刻复用端口,避免"地址已在使用"的错误
  • 完善资源清理:客户端断开时关闭Socket,程序退出时清理GPIO资源

三、测试步骤

  1. 先启动Python服务器
  2. 在Unity中点击连接按钮,测试发送指令
  3. 断开Unity连接,重新连接后再次发送指令,此时不会再出现连接被终止的问题

按照这个方案修改后,你的TCP连接就能正常支持多次断开重连,指令发送也能稳定工作了。

内容的提问来源于stack exchange,提问作者Ghada

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.13 07:32:21