Web, WebSocket et images
Un script FL# atteint Internet avec les classes de .NET : System.Net.Http.HttpClient (avec HttpRequestMessage, HttpResponseMessage, HttpContent et ses variantes) et System.Net.WebSockets.ClientWebSocket. Elles ont les signatures de .NET 10 : le code et les bibliothèques écrits pour .NET marchent tels quels. Les images arrivent avec Texture.LoadFromUrl et ImageConversion.LoadImage.
Sur l'appareil d'un joueur, chaque adresse passe par la porte réseau du jeu : la première fois qu'un monde contacte un site, le joueur choisit Toujours, Cette fois ou Refuser dans une fenêtre (voir Les sites des mondes). Ta requête attend sa réponse. Sur le serveur, toutes les adresses sont autorisées et personne n'est sollicité.
HTTP
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using NodeWorld.Flux;
namespace MonMonde
{
public class Scores : FluxScript
{
private static readonly HttpClient http = new HttpClient { BaseAddress = new Uri("https://api.example.com/") };
protected override void Start()
{
_ = ChargeAsync();
}
private async Task ChargeAsync()
{
try
{
http.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
string json = await http.GetStringAsync("scores?top=10");
Debug.Log("Scores : " + json);
var corps = new StringContent("{\"player\":\"" + Player.name + "\",\"score\":42}", Encoding.UTF8, "application/json");
HttpResponseMessage reponse = await http.PostAsync("scores", corps);
Debug.Log("Enregistré : " + (int)reponse.StatusCode);
}
catch (HttpRequestException exception)
{
Debug.LogWarning("Les scores ne sont pas disponibles : " + exception.Message);
}
}
}
}Ce que tu peux faire :
- Toutes les méthodes (
GET,POST,PUT,PATCH,DELETE…), tous les en-têtes saufHostet ceux de la connexion, des corps en octets (ByteArrayContent,StreamContent,ReadOnlyMemoryContent), du texte (StringContent), des formulaires (FormUrlEncodedContent,MultipartFormDataContent). - Le statut, les en-têtes et le corps de la réponse :
ReadAsStringAsync,ReadAsByteArrayAsync,ReadAsStreamAsync. - Les redirections sont suivies (50 au plus, comme .NET), jamais de
httpsvershttp. Une redirection vers un autre site repasse par la porte. - Les réponses gzip et deflate sont décodées si tu le demandes :
new HttpClient(new HttpClientHandler { AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate }). HttpClient.Timeout(100 s par défaut) et unCancellationTokenarrêtent une requête.
Ce qui change par rapport à .NET sur un PC :
- Seulement
httpethttps. Le corps d'une réponse arrive en entier :ResponseHeadersReadle donne aussi en entier. - Pas de cookies (
UseCookiesrestefalse), pas de proxy, pas d'identifiants, pas de certificat client, pas de vérification de certificat à ta façon : le certificat d'un site doit être valide. - Seulement les méthodes asynchrones :
HttpClient.SendlèveNotSupportedException(un script n'attend jamais sur son fil). - Quand le joueur refuse le site, la requête échoue avec
HttpRequestException,HttpRequestError.ConnectionErroret un message qui nomme le site.
WebSocket
using System;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using NodeWorld.Flux;
namespace MonMonde
{
public class Chat : FluxScript
{
private ClientWebSocket socket;
protected override void Start()
{
_ = TourneAsync();
}
private async Task TourneAsync()
{
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("bonjour")), WebSocketMessageType.Text, true, CancellationToken.None);
var tampon = new byte[4096];
while (socket.State == WebSocketState.Open)
{
WebSocketReceiveResult resultat = await socket.ReceiveAsync(new ArraySegment<byte>(tampon), CancellationToken.None);
if (resultat.MessageType == WebSocketMessageType.Close) break;
Debug.Log(Encoding.UTF8.GetString(tampon, 0, resultat.Count));
}
}
protected override void OnDestroy()
{
socket?.Abort();
}
}
}ws et wss, messages texte et binaires, messages en plusieurs morceaux (endOfMessage), la fermeture en deux temps (CloseAsync, CloseOutputAsync), Abort. Options.KeepAliveInterval (30 s par défaut) garde la connexion éveillée. Pas d'extension de compression ; un serveur qui répond autre chose que 101 fait échouer la connexion, comme dans .NET.
Images
Texture affiche = await Texture.LoadFromUrl("https://example.com/affiche.png");
GetComponent<Renderer>().sharedMaterial.SetTexture("_BaseMap", affiche);
Debug.Log(affiche.width + " x " + affiche.height);Texture.LoadFromUrl charge un PNG ou un JPG en passant par la porte et le décode en texture du monde. ImageConversion.LoadImage(octets) décode des octets que tu as déjà (de GetByteArrayAsync, d'un WebSocket…) et donne null si ce n'est pas une image. Les textures sont détruites avec le monde, ou par Object.Destroy. Sur le serveur, il n'y a pas d'image : LoadImage donne null.
Ce qui est demandé au joueur
- Le choix est gardé par site : le domaine et ses sous-domaines. Autoriser
example.comautorise aussiapi.example.cometcdn.example.com. L'endroit où commence un domaine vient de la Public Suffix List : autorisermonnom.github.ion'autorise pas le reste degithub.io. Un port qui n'est pas le port standard fait partie du site :example.com:8443. - Une fenêtre par site : toutes tes requêtes vers lui attendent la même réponse, tant que le joueur n'a pas répondu, ou jusqu'à ce qu'il quitte le monde.
- Toujours et Refuser sont gardés sur l'appareil du joueur, pour tous les mondes, jusqu'à ce qu'il les retire dans Paramètres, Sites. Cette fois dure jusqu'à ce qu'il quitte le monde.
- Les grands sites vidéo (
youtube.com,youtu.be,twitch.tv,vimeo.com,soundcloud.com,dailymotion.com) sont autorisés sans fenêtre ; le joueur peut modifier cette liste. - Une adresse IP de la machine ou du réseau local du joueur écrite dans ton adresse (
192.168.1.20,127.0.0.1…) ouvre la fenêtre avec un avertissement. Un nom qui pointe vers elles est toujours refusé. - Le jeu résout le nom lui-même et se connecte à l'adresse qu'il a contrôlée : un nom qui change de réponse après le contrôle n'atteint rien de nouveau.
- En Play mode dans l'éditeur Unity, tout est autorisé, sans fenêtre.
Vidéos
Un VideoPlayer demande à la porte le site de son adresse avant de l'ouvrir (VideoState.Loading pendant ce temps). Un site refusé donne VideoState.Error avec VideoError.Refused. Tous les protocoles réseau du lecteur vidéo marchent (http, https, rtmp, rtsp, udp, mms, srt…) ; un fichier, l'écran ou un périphérique de capture, non.
Sur le serveur
Les scripts marqués [ServerOnly] ont les mêmes HttpClient et ClientWebSocket, avec toutes les adresses autorisées : aucun joueur n'est là pour répondre. Ils atteignent aussi la machine du serveur et son réseau local : l'hébergeur décide de ce qu'ils peuvent atteindre avec son pare-feu (voir Ports et pare-feu).
Journaux
Chaque requête écrit une ligne [Web] avec le site seulement, jamais le chemin : [Web] GET api.example.com: 200, 512 bytes. La fenêtre, la réponse du joueur, chaque ouverture et fermeture de WebSocket écrivent aussi la leur.
