【问题标题】:Getting UdpClient Receive data back in the Main thread在主线程中获取 UdpClient 接收数据
【发布时间】:2014-06-28 14:24:27
【问题描述】:

我正在尝试将 LAN 服务器发现添加到我的 Unity 游戏中。我正在使用 UDP 广播请求。如果网络上的任何人是服务器,他们将响应一些服务器数据。我已经弄清楚了这部分,但我需要使用一些 Unity 函数(包括其他自定义的 monobehaviour 脚本)来生成数据包数据。

Unity 不允许从除主线程之外的任何线程访问其 API。

我正在使用UdpClient.BeginReceiveUdpClient.EndReceiveAsyncCallback 流。所以接收 AsyncCallback 函数(下面示例脚本中的AsyncRequestReceiveData)在另一个不允许我使用任何 Unity 函数的线程中运行。

我使用 flamy 的 this answer 作为我的脚本的基础。

当脚本听到请求时,我尝试在AsyncRequestReceiveData 中触发委托和事件,但它似乎在同一个单独的线程中触发。

由单独线程在主线程中触发的事件会很好,但我不确定如何做到这一点。


虽然脚本继承自 MonoBehaviour(Unity 3D 中的基础对象),但它应该可以独立运行。

脚本的输出应如下所示:

发送请求:requestingServers

收到的请求:requestingServers

这是准系统脚本:

using UnityEngine;
using System;
using System.Collections;
using System.Net;
using System.Net.Sockets;

public class TestUDP : MonoBehaviour {


    public delegate void RequestReceivedEventHandler(string message);
    public event RequestReceivedEventHandler OnRequestReceived;

    // Use this to trigger the event
    protected virtual void ThisRequestReceived(string message)
    {
        RequestReceivedEventHandler handler = OnRequestReceived;
        if(handler != null)
        {
            handler(message);
        }
    }


    public int requestPort = 55795;

    UdpClient udpRequestSender;
    UdpClient udpRequestReceiver;


    // Use this for initialization
    void Start () {

        // Set up the sender for requests
        this.udpRequestSender = new UdpClient();
        IPEndPoint requestGroupEP = new IPEndPoint(IPAddress.Broadcast, this.requestPort);
        this.udpRequestSender.Connect(requestGroupEP);


        // Set up the receiver for the requests
        // Listen for anyone looking for us
        this.udpRequestReceiver = new UdpClient(this.requestPort);
        this.udpRequestReceiver.BeginReceive(new AsyncCallback(AsyncRequestReceiveData), null);

        // Listen for the request
        this.OnRequestReceived += (message) => {
            Debug.Log("Request Received: " + message);
            // Do some more stuff when we get a request
            // Use `Network.maxConnections` for example
        };

        // Send out the request
        this.SendRequest();
    }

    void Update () {

    }


    void SendRequest()
    {
        try
        {
            string message = "requestingServers";

            if (message != "") 
            {
                Debug.Log("Sendering Request: " + message);
                this.udpRequestSender.Send(System.Text.Encoding.ASCII.GetBytes(message), message.Length);
            }
        }
        catch (ObjectDisposedException e)
        {
            Debug.LogWarning("Trying to send data on already disposed UdpCleint: " + e);
            return;
        }
    }


    void AsyncRequestReceiveData(IAsyncResult result)
    {
        IPEndPoint receiveIPGroup = new IPEndPoint(IPAddress.Any, this.requestPort);
        byte[] received;
        if (this.udpRequestReceiver != null) {
            received = this.udpRequestReceiver.EndReceive(result, ref receiveIPGroup);
        } else {
            return;
        }
        this.udpRequestReceiver.BeginReceive (new AsyncCallback(AsyncRequestReceiveData), null);
        string receivedString = System.Text.Encoding.ASCII.GetString(received);

        // Fire the event
        this.ThisRequestReceived(receivedString);

    }
}

我见过this question (Cleanest Way to Invoke Cross-Thread Events),但我似乎无法弄清楚它是如何工作的以及如何融入其中。

【问题讨论】:

  • @AgentShark Unity 3D 在我的情况下。不过,这个问题并不完全是 Unity 特有的。

标签: c# multithreading sockets unity3d udp


【解决方案1】:

该解决方案的工作原理是跟踪需要在主线程中执行的任务列表(一个队列),然后通过HandleTasks()Update() 中运行该队列;执行并从队列中删除每个。您可以通过调用QueueOnMainThread() 并传递一个匿名函数来添加到任务队列。

我从this answerPragmateek得到一些灵感

您可以使用单独的脚本来管理主线程任务,或者将其合并到您需要在主线程中运行某些内容的现有脚本中。

这是full source,解决了我原来的问题中的sn-p。

以下只是启动和运行队列的必要部分:

// We use this to keep tasks needed to run in the main thread
private static readonly Queue<Action> tasks = new Queue<Action>();


void Update () {
    this.HandleTasks();
}

void HandleTasks() {
    while (tasks.Count > 0)
    {
        Action task = null;

        lock (tasks)
        {
            if (tasks.Count > 0)
            {
                task = tasks.Dequeue();
            }
        }

        task();
    }
}

public void QueueOnMainThread(Action task)
{
    lock (tasks)
    {
        tasks.Enqueue(task);
    }
}

要在主线程上排队,只需传递一个 lambda 表达式:

this.QueueOnMainThread(() => {
    // We are now in the main thread...
    Debug.Log("maxConnections: " + Network.maxConnections);
});

如果您使用的是 WinForms,this article: Avoiding InvokeRequired 有一些很棒的编码模式。

【讨论】:

    【解决方案2】:

    此代码示例对您有用吗? X 可以从任何线程调用。

    //place in class where main thread object is
    void X(int sampleVariable)
    {
        if (this.InvokeRequired)
        {
            this.Invoke((MethodInvoker) (() => X(sampleVariable)));
            return;
        }
    
        // write main thread code here
    }
    

    【讨论】:

    • 这不起作用,并且大多数这些方法、类都在 System.Windows.Forms 中,这些在 Unity 中不可用。如果我使用 WinForms,我在 this article 中还发现了一些更好的替代模式。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多