Web, WebSocket and images

Since SDK 0.2.1

An FL# script reaches the internet with the classes of .NET: System.Net.Http.HttpClient (with HttpRequestMessage, HttpResponseMessage, HttpContent and its kinds) and System.Net.WebSockets.ClientWebSocket. They have the signatures of .NET 10, so code and libraries written for .NET work as they are. Images come with Texture.LoadFromUrl and ImageConversion.LoadImage.

On a player's device, every address goes through the network gate of the game: the first time a world contacts a site, the player chooses Always, This time or Deny in a window (see Sites a world contacts). Your request waits for that answer. On the server, every address is allowed and nobody is asked.

HTTP

csharp
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using NodeWorld.Flux;

namespace MyWorld
{
    public class Scores : FluxScript
    {
        private static readonly HttpClient http = new HttpClient { BaseAddress = new Uri("https://api.example.com/") };

        protected override void Start()
        {
            _ = LoadAsync();
        }

        private async Task LoadAsync()
        {
            try
            {
                http.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
                string json = await http.GetStringAsync("scores?top=10");
                Debug.Log("Scores: " + json);

                var body = new StringContent("{\"player\":\"" + Player.name + "\",\"score\":42}", Encoding.UTF8, "application/json");
                HttpResponseMessage answer = await http.PostAsync("scores", body);
                Debug.Log("Saved: " + (int)answer.StatusCode);
            }
            catch (HttpRequestException exception)
            {
                Debug.LogWarning("The scores are not available: " + exception.Message);
            }
        }
    }
}

What you can do:

  • Every method (GET, POST, PUT, PATCH, DELETE…), every header except Host and the connection ones, bodies in bytes (ByteArrayContent, StreamContent, ReadOnlyMemoryContent), text (StringContent), forms (FormUrlEncodedContent, MultipartFormDataContent).
  • The status, the headers and the body of the answer: ReadAsStringAsync, ReadAsByteArrayAsync, ReadAsStreamAsync.
  • Redirects are followed (50 at most, as .NET), never from https to http. A redirect to another site goes through the gate again.
  • gzip and deflate answers are decoded when you ask for it: new HttpClient(new HttpClientHandler { AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate }).
  • HttpClient.Timeout (100 s by default) and a CancellationToken stop a request.

What is different from .NET on a PC:

  • Only http and https. The body of an answer arrives whole: ResponseHeadersRead gives it whole too.
  • No cookies (UseCookies stays false), no proxy, no credentials, no client certificate, no custom certificate check: a site's certificate must be valid.
  • Only the asynchronous methods: HttpClient.Send throws NotSupportedException (a script never waits on its thread).
  • When the player denies the site, the request fails with HttpRequestException, HttpRequestError.ConnectionError, and a message that names the site.

WebSocket

csharp
using System;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using NodeWorld.Flux;

namespace MyWorld
{
    public class Chat : FluxScript
    {
        private ClientWebSocket socket;

        protected override void Start()
        {
            _ = RunAsync();
        }

        private async Task RunAsync()
        {
            socket = new ClientWebSocket();
            socket.Options.AddSubProtocol("chat");
            await socket.ConnectAsync(new Uri("wss://chat.example.com/room"), CancellationToken.None);
            await socket.SendAsync(new ArraySegment<byte>(Encoding.UTF8.GetBytes("hello")), WebSocketMessageType.Text, true, CancellationToken.None);

            var buffer = new byte[4096];
            while (socket.State == WebSocketState.Open)
            {
                WebSocketReceiveResult result = await socket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
                if (result.MessageType == WebSocketMessageType.Close) break;
                Debug.Log(Encoding.UTF8.GetString(buffer, 0, result.Count));
            }
        }

        protected override void OnDestroy()
        {
            socket?.Abort();
        }
    }
}

ws and wss, text and binary messages, messages in pieces (endOfMessage), the close handshake (CloseAsync, CloseOutputAsync), Abort. Options.KeepAliveInterval (30 s by default) keeps the connection awake. No compression extension; a server that answers anything but 101 fails the connection, as in .NET.

Images

csharp
Texture picture = await Texture.LoadFromUrl("https://example.com/poster.png");
GetComponent<Renderer>().sharedMaterial.SetTexture("_BaseMap", picture);
Debug.Log(picture.width + " x " + picture.height);

Texture.LoadFromUrl loads a PNG or JPG through the gate and decodes it into a texture of the world. ImageConversion.LoadImage(bytes) decodes bytes you already have (from GetByteArrayAsync, a WebSocket…) and gives null when they are not an image. The textures are destroyed with the world, or by Object.Destroy. On the server there is no picture: LoadImage gives null.

What the player is asked

  • The choice is kept per site: the domain and its subdomains. Allowing example.com also allows api.example.com and cdn.example.com. Where a domain starts comes from the Public Suffix List: allowing myname.github.io does not allow the rest of github.io. A port that is not the standard one is part of the site: example.com:8443.
  • One window per site: all your requests to it wait for the same answer, as long as the player has not answered, or until they leave the world.
  • Always and Deny are kept on the player's device, for every world, until they remove them in Settings, Sites. This time lasts until they leave the world.
  • The big video sites (youtube.com, youtu.be, twitch.tv, vimeo.com, soundcloud.com, dailymotion.com) are allowed without a window; the player can change that list.
  • An IP address of the player's machine or local network written in your address (192.168.1.20, 127.0.0.1…) shows the window with a warning. A name that points there is always refused.
  • The game resolves the name itself and connects to the address it checked: a name that changes its answer after the check reaches nothing new.
  • In Play mode of the Unity editor, everything is allowed, without a window.

Videos

A VideoPlayer asks the gate for the site of its address before it opens it (VideoState.Loading meanwhile). A denied site gives VideoState.Error with VideoError.Refused. Every network protocol of the video player works (http, https, rtmp, rtsp, udp, mms, srt…); a file, a screen or a capture device does not.

On the server

Scripts marked [ServerOnly] have the same HttpClient and ClientWebSocket, with every address allowed: no player is there to answer. They also reach the server machine and its local network: the host decides what they may reach with its firewall (see Ports and firewall).

Logs

Each request writes one [Web] line with the site only, never the path: [Web] GET api.example.com: 200, 512 bytes. The window, the player's answer and each WebSocket opening and closing write theirs too.