21

I need to send HTTP requests with all the standard RESTful methods and access to the body of the request in order to send/receive JSON with it. I've looked into,

WebRequest.HttpWebRequest

This works almost perfectly, but there are cases where, for example, if the server is down the function GetResponse can take several seconds to return- since it is a synchronous method- freezing the application for that period. The asynchronous version of this method, BeginGetResponse, does not seem to work asynchronously (in Unity anyway) as it still freezes the application for that period.

UnityEngine.WWW#

Only supports POST and GET requests for some reason- but I also need PUT and DELETE (standard RESTful methods) so I didn't bother looking into it any further.

System.Threading

In order to run WebRequest.HttpWebRequest.GetResponse without freezing the application I looked into using threads. Threads seem to work in the editor (but seem extremely volatile- if you don't stop a thread when the application exits it keeps running in the editor forever even when you stop it), and when built to an iOS device crash it as soon as I try to start a thread (I forgot to write down the error and I don't have access to it right now).

Run threads in a native iOS app with a bridge to the Unity app

Ridiculous, not even going to attempt this.

UniWeb

This. I would like to know how they managed it.

Here is an example of the WebRequest.BeginGetResponse method I am trying,

// The RequestState class passes data across async calls.
public class RequestState
{
   const int BufferSize = 1024;
   public StringBuilder RequestData;
   public byte[] BufferRead;
   public WebRequest Request;
   public Stream ResponseStream;
   // Create Decoder for appropriate enconding type.
   public Decoder StreamDecode = Encoding.UTF8.GetDecoder();

   public RequestState()
   {
      BufferRead = new byte[BufferSize];
      RequestData = new StringBuilder(String.Empty);
      Request = null;
      ResponseStream = null;
   }     
}

public class WebRequester
{
    private void ExecuteRequest()
    {
        RequestState requestState = new RequestState();
        WebRequest request = WebRequest.Create("mysite");
        request.BeginGetResponse(new AsyncCallback(Callback), requestState);
    }

    private void Callback(IAsyncResult ar)
    {
      // Get the RequestState object from the async result.
      RequestState rs = (RequestState) ar.AsyncState;

      // Get the WebRequest from RequestState.
      WebRequest req = rs.Request;

      // Call EndGetResponse, which produces the WebResponse object
      //  that came from the request issued above.
      WebResponse resp = req.EndGetResponse(ar);
    }
}

... based on this: http://msdn.microsoft.com/en-us/library/86wf6409(v=vs.71).aspx

fordeka
  • 979
  • 2
  • 7
  • 22
  • 1
    Please show an example of how you are using BeginGetResponse... may be problem with your code too (genereal comment, not sure if Unity has special behavior for this call). – Alexei Levenkov Sep 01 '12 at 03:58
  • Example added. Unity uses a customized version of Mono 2.6 I believe. – fordeka Sep 01 '12 at 05:02
  • Hey mister, your question is clearly quite confusing and only answered by yourself, but I'm facing the exact same issue with unity and http requests. Instead of threads, I'm trying `coroutines`. I don't think thread is the best path, but I also still couldn't quite figure it out with coroutines. Do you have any updates on this? Have you tried my path? Can you share your solution? – cregox Sep 13 '12 at 13:15
  • When used in a coroutine the offending method still freezes the application until it returns- my theory is that the method freezes the thread that it is called on. I don't think coroutines involve threading so anything called in them gets executed on the main thread anyway, freezing the application like if you called it anywhere else. Calling the method in a thread worked fine, just be very careful to manually close your threads as unity/mono doesn't seem to do it automatically. – fordeka Sep 13 '12 at 17:41
  • You've answered quite fast @Ford, but for some reason I didn't get the notification. I had much success using coroutines for parallel processes with `WWW` and `LoadLevelAsync`, so I figured it might be possible here too. But now I trust, as you said, it doesn't actually create its own thread indeed, so it's actually just making asynchronous calls in the same thread... Anyway, if you got this figured out, could you share your solution with us? Or is it over 3 pages wide, like your msdn link? – cregox Sep 25 '12 at 11:36
  • Coroutines still operate on the same thread that called them. So if code in your coroutine is blocking, it will block your application. LoadLevelAsync works, for example, because it is an asynchronous method. Coroutines work be saving their call stack at the point at which you yield out of them. When you come back into your coroutine method, it resumes execution at the exact point where you left, maintaining all the local stack state it had when it yielded. But it's still on the same thread. It's like user-controlled thread scheduling. This is very different from actual system threads. – juanpaco Feb 12 '13 at 03:10
  • Don't blame .Net threads for your failure to read the manual... Threads have an `IsBackground` property that you can set before starting them. If it's set to True, the thread will die when the application does. It defaults to False. – Basic Dec 14 '16 at 01:42

4 Answers4

11

Ok, I finally managed to write my own solution. We basically need a RequestState, a Callback Method and a TimeOut Thread. Here I'll just copy what was done in UnifyCommunity (now called unity3d wiki). This is outdated code, but smaller than what's there, so more convenient to show something here. Now I've removed (in the unit3d wiki) System.Action and static for performance and simplicity:

Usage

static public ThisClass Instance;
void Awake () {
    Instance = GetComponent<ThisClass>();
}
static private IEnumerator CheckAvailabilityNow () {
    bool foundURL;
    string checkThisURL = "http://www.example.com/index.html";
    yield return Instance.StartCoroutine(
        WebAsync.CheckForMissingURL(checkThisURL, value => foundURL = !value)
        );
    Debug.Log("Does "+ checkThisURL +" exist? "+ foundURL);
}

WebAsync.cs

using System;
using System.IO;
using System.Net;
using System.Threading;
using System.Collections;
using UnityEngine;

/// <summary>
///  The RequestState class passes data across async calls.
/// </summary>
public class RequestState
{
    public WebRequest webRequest;
    public string errorMessage;

    public RequestState ()
    {
        webRequest = null;
        errorMessage = null;
    }
}

public class WebAsync {
    const int TIMEOUT = 10; // seconds

    /// <summary>
    /// If the URLs returns 404 or connection is broken, it's missing. Else, we suppose it's fine.
    /// </summary>
    /// <param name='url'>
    /// A fully formated URL.
    /// </param>
    /// <param name='result'>
    /// This will bring 'true' if 404 or connection broken and 'false' for everything else.
    /// Use it as this, where "value" is a System sintaxe:
    /// value => your-bool-var = value
    /// </param>
    static public IEnumerator CheckForMissingURL (string url, System.Action<bool> result) {
        result(false);

        Uri httpSite = new Uri(url);
        WebRequest webRequest = WebRequest.Create(httpSite);

        // We need no more than HTTP's head
        webRequest.Method = "HEAD";
        RequestState requestState = new RequestState();

        // Put the request into the state object so it can be passed around
        requestState.webRequest = webRequest;

        // Do the actual async call here
        IAsyncResult asyncResult = (IAsyncResult) webRequest.BeginGetResponse(
            new AsyncCallback(RespCallback), requestState);

        // WebRequest timeout won't work in async calls, so we need this instead
        ThreadPool.RegisterWaitForSingleObject(
            asyncResult.AsyncWaitHandle,
            new WaitOrTimerCallback(ScanTimeoutCallback),
            requestState,
            (TIMEOUT *1000), // obviously because this is in miliseconds
            true
            );

        // Wait until the the call is completed
        while (!asyncResult.IsCompleted) { yield return null; }

        // Deal up with the results
        if (requestState.errorMessage != null) {
            if ( requestState.errorMessage.Contains("404") || requestState.errorMessage.Contains("NameResolutionFailure") ) {
                result(true);
            } else {
                Debug.LogWarning("[WebAsync] Error trying to verify if URL '"+ url +"' exists: "+ requestState.errorMessage);
            }
        }
    }

    static private void RespCallback (IAsyncResult asyncResult) {

        RequestState requestState = (RequestState) asyncResult.AsyncState;
        WebRequest webRequest = requestState.webRequest;

        try {
            webRequest.EndGetResponse(asyncResult);
        } catch (WebException webException) {
            requestState.errorMessage = webException.Message;
        }
    }

    static private void ScanTimeoutCallback (object state, bool timedOut)  { 
        if (timedOut)  {
            RequestState requestState = (RequestState)state;
            if (requestState != null) 
                requestState.webRequest.Abort();
        } else {
            RegisteredWaitHandle registeredWaitHandle = (RegisteredWaitHandle)state;
            if (registeredWaitHandle != null)
                registeredWaitHandle.Unregister(null);
        }
    }
}
cregox
  • 17,674
  • 15
  • 85
  • 116
  • So this works great in Editor and probably standalone, but WebRequest will only work on iOS by using full .NET 2.0 (not subset) and it looks disabling all/most stripping, which makes the app way too big for my use unfortunately. – Michael Jun 23 '13 at 19:50
  • @Michael yeah, I haven't tested that, but it must be true. Set `API Compatibility Level` to **`.NET 2.0`** and `Stripping Level` to **`Disabled`** to be sure this will work. – cregox Jun 25 '13 at 17:19
  • Update: Im Using WebRequest with .net 2.0 subset which seems fine in Unity 5.1.2. I tested on iOS, Android and Windows phone. using .NET subset also reduced the executable on iOS by about 5mb but that was a debug build. – Dev2rights Aug 06 '15 at 09:25
1

I got threading to work on iOS- I believe it was crashing due to ghost threads or something. Restarting the device seems to have fixed the crashing so I'll just use WebRequest.HttpWebRequest with threads.

fordeka
  • 979
  • 2
  • 7
  • 22
-1
// javascript in the web player not ios, android or desktop you could just run the following code:

var jscall:String;
    jscall="var reqScript = document.createElement('script');";
    jscall+="reqScript.src = 'synchmanager_secure2.jsp?userid="+uid+"&token="+access_token+"&rnd='+Math.random()*777;";
    jscall+="document.body.appendChild(reqScript);";
Application.ExternalEval(jscall);
// cs
string jscall;
    jscall="var reqScript = document.createElement('script');";
    jscall+="reqScript.src = 'synchmanager_secure2.jsp?userid="+uid+"&token="+access_token+"&rnd='+Math.random()*777;";
    jscall+="document.body.appendChild(reqScript);";
    Application.ExternalEval(jscall);

// then update your object using the your return in a function like this
// json return object always asynch
function sendMyReturn(args){
     var unity=getUnity();
     unity.SendMessage("object", "function", args );
}
sendMyReturn(args);

or you can send it via a AJAX function prewritten custom headers for security purposes with this you would need signed headers and a signed request back from the server I prefer md5 signatures comparatively they are not so big

Rod
  • 52,748
  • 3
  • 38
  • 55
robert
  • 7
  • 2
-1

There is a way of doing this asynchronously, without using IEnumerator and yield return stuff. Check out the eDriven framework.

HttpConnector class: https://github.com/dkozar/eDriven/blob/master/eDriven.Networking/Rpc/Core/HttpConnector.cs

I've been using JsonFX with HttpConnector all the time, for instance in this WebPlayer demo: http://edrivenunity.com/load-images

Not having PUT and DELETE is not a big issue, since all of it could be done using GET and POST. For instance I'm successfully communicating with Drupal CMS using its REST service.

Danko Kozar
  • 221
  • 2
  • 8