TX Text Control .NET Server

WebSocket Architecture

Topic

Every keystroke, selection and formatting command in the Document Editor travels from the browser over a WebSocket connection to your ASP.NET Core application and on to the backend process. Hosting the editor means keeping this path open: a URL that reaches the middleware, the middleware at the right place in the pipeline and servers, load balancers and firewalls that let the connection through.

Document Editor Architecture introduces the three parts, the life of an editing session and the configuration of the backend process. The sections below go one level deeper into the connection itself, for developers who host the editor or run into connection problems. The Document Viewer uses HTTP requests instead of WebSockets, so none of this applies to it.

The Path of a Connection

The Three Parts shows the chain from the browser to the backend process. Along this chain, a connection takes four steps:

  1. The browser opens the connection. The frontend connects to the WebSocket URL of your application and requests the sub-protocol TXTextControl.Web, which identifies it as a TX Text Control connection. While the ribbon tabs are loading, the editor opens a second connection to the same URL and closes it again when they have arrived.
  2. ASP.NET Core accepts it. app.UseWebSockets() enables WebSocket requests in the application, and app.UseTXWebSocketMiddleware() takes over those that belong to the editor.
  3. A handler forwards the traffic. For each connection, the middleware creates a WebSocketHandler that passes the messages between the browser and the backend process. A handler is never reused.
  4. The backend process does the work. The handler talks to TXTextControl.Web.Server.Core over a plain TCP connection on a fixed port.

The same path also serves the editor script over HTTP, for example /api/TXWebSocket/GetResource?name=tx-document-editor.min.js, which JavaScript pages and the Angular and React components load from the backend.

How the WebSocket URL Is Built

FrontendWhere the URL comes from
ASP.NET Core viewThe HTML helper builds it from the address of the page and TextControlSettings.APIRoute, which defaults to api/. The result is /api/TXWebSocket on the host of the page
JavaScriptThe mandatory webSocketURL setting of TXTextControl.init
Angular and ReactThe webSocketURL input or prop of the component
BlazorThe component runs in the application that hosts the middleware and needs no connection setting

A URL starts with wss when the backend runs on HTTPS and with ws when it runs on HTTP. A frontend on another origin, such as an Angular or React development server, connects without a CORS policy.

The middleware reacts to every request whose path contains /txwebsocket, regardless of case. That is why older articles that connect to /TXWebSocket without the api segment still work. Use /api/TXWebSocket, which matches the default of the HTML helper.

Host the Application in a Subfolder

By default, the HTML helper expects the handler at the root of the host. When the application runs in a subfolder, such as a virtual application in IIS, add the folder to APIRoute:

@using TXTextControl.Web.MVC

@Html.TXTextControl().TextControl(settings => {
    settings.APIRoute = "WebApplication120/api";
}).Render()

The editor then connects to /WebApplication120/api/TXWebSocket. TextControlSettings.StaticFilesPath sets the path of the static files in the same situation. A wrong route or static files path keeps an editor in a subfolder from connecting. The editor then shows "Can't connect to the server.", and the console of the browser's developer tools shows that it tried the root of the host instead of the subfolder:

A page at localhost/WebApplication120 whose Document Editor shows "Can't connect to the server. Please refresh the website to re-connect.", with the browser console reporting that the WebSocket connection to ws://localhost/api/TXWebSocket failed.

Add the Middleware to the Pipeline

The order of the calls in Program.cs decides which middleware sees a request first. The README of the TXTextControl.Web.DocumentEditor.Backend package places the WebSocket calls after app.UseRouting() and before any endpoint mappings. The complete Program.cs of an MVC application is in Configure Program.cs of the Getting Started tutorial; the part that matters here is the order in its pipeline:

app.UseHttpsRedirection();
app.UseRouting();

// Accept WebSocket requests
app.UseWebSockets();

// A middleware that checks the WebSocket handshake goes here

// Hand the connections of the editor to a WebSocketHandler
app.UseTXWebSocketMiddleware();

app.UseAuthorization();
app.MapStaticAssets();
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}")
    .WithStaticAssets();

The code works as follows:

  • UseWebSockets enables WebSocket support in ASP.NET Core. The editor can't connect without it, so it comes before UseTXWebSocketMiddleware.
  • A middleware that validates the handshake, for example an access token, goes between the two calls. It runs before the connection is established, and if it rejects the request, the handshake fails. WebSocket Security describes such a middleware.
  • UseTXWebSocketMiddleware without parameters connects to the backend process that DocumentEditorWorkerManager starts on the same machine with its default port.
  • Web API backends for Angular, React and JavaScript frontends use the same calls with MapControllers instead of the controller route.
  • The authentication and authorization of ASP.NET Core apply to the WebSocket handshake only when UseAuthentication and UseAuthorization come before UseTXWebSocketMiddleware. Require Signed-In Users shows the order.

Restrict the Origins

CORS policies don't protect the WebSocket connection. WebSocketOptions.AllowedOrigins, passed to UseWebSockets, accepts handshakes only from the origins you list; Restrict the Origins in WebSocket Security shows the code and what the list protects against. WebSocketOptions.KeepAliveInterval sets how often the server sends ping frames that keep proxies from closing idle connections; the default is two minutes.

Choose an Overload

UseTXWebSocketMiddleware has six overloads:

CallUse it when
UseTXWebSocketMiddleware()The backend process runs on the same machine with its default settings
UseTXWebSocketMiddleware(string hostNameOrAddress, int servicePort)The backend listens on another address or port; a host name is resolved with DNS
UseTXWebSocketMiddleware(IPAddress serviceAddress, int servicePort)The same with an IPAddress
UseTXWebSocketMiddleware(string webSocketPath)The middleware should react to a path part other than /txwebsocket
UseTXWebSocketMiddleware(string hostNameOrAddress, int servicePort, string webSocketPath) and UseTXWebSocketMiddleware(IPAddress serviceAddress, int servicePort, string webSocketPath)Both

A call with a single string, such as UseTXWebSocketMiddleware("127.0.0.1"), selects the overload for the path, not the host. Older articles call it this way because they were written before the path overloads existed. Always pass host and port together. The address and port must match the ones the backend process listens on, as Configure the Backend Process describes.

Reach a Session on the Server

Each open editor has its own WebSocketHandler, and your server code can find it by the connection ID of the editor. WebSocketHandler.GetInstance returns the handler, which can save the current document, load a document into the editor or work on the selection, without any action in the browser. The following controller saves the document of an editor as DOCX:

using Microsoft.AspNetCore.Mvc;
using TXTextControl.Web;

[ApiController]
[Route("[controller]")]
public class EditorSessionController : ControllerBase
{
    [HttpPost("Save")]
    public IActionResult Save([FromQuery] string connectionId)
    {
        // Find the handler of the editor with this connection ID
        WebSocketHandler handler = WebSocketHandler.GetInstance(connectionId);

        if (handler == null)
        {
            return NotFound();
        }

        handler.SaveText(out byte[] data, BinaryStreamType.WordprocessingML);

        // Store the data, for example in a database
        return Ok();
    }
}

The page sends the connection ID once the editor is ready:

TXTextControl.addEventListener("textControlLoaded", function () {
  const connectionId = TXTextControl.connectionID;

  document.getElementById("save").addEventListener("click", function () {
    fetch("/EditorSession/Save?connectionId=" + encodeURIComponent(connectionId), { method: "POST" });
  });
});

The code works as follows:

  • TXTextControl.connectionID returns the unique ID of the connection. It's available after the textControlLoaded event.
  • GetInstance returns the handler of the connection with that ID, or null when the editor is no longer connected.
  • SaveText writes the document into a byte array in the BinaryStreamType you pass. The handler also has LoadText to replace the document in the editor and a Selection property that loads and saves the selected part with Selection.Load and Selection.Save.

A background service that calls GetInstance and SaveText regularly keeps the last state of a document on the server even when the connection of the browser drops. The blog article Best Practices: Reliable Auto-Save in TX Text Control Using the WebSocketHandler and Background Services implements it, and How to Manipulate the Document using the WebSocketHandler edits the selection with ServerTextControl and loads the result back.

The Connection to the Backend Process

Between the middleware and TXTextControl.Web.Server.Core, the traffic is plain TCP on a fixed port: the backend process listens, and each handler connects to it. Configure the Backend Process sets its address, port and logging, and Run the Backend Separately moves it to another machine or container.

DocumentEditorWorkerManager passes ListenAddress and Port from DocumentEditorWorkerOptions to the process as the command-line arguments --listen-address and --port, and forwards the output of the process to the logging of your application. A backend that you start yourself accepts the same arguments: dotnet ./TXTextControl.Web.Server.Core.dll --listen-address <IP_ADDRESS> --port <PORT_NUMBER>. The blog article Configuring Web.Server.Core for TX Text Control Document Editor: Changing Ports and IP Versions walks through the settings.

Host the Connection

A WebSocket connection passes every server, proxy and load balancer between the browser and your application, and each of them has to allow it. WebSocket traffic uses the same security layer as the HTTP or HTTPS connection it starts from, and firewalls must let it through, as Browser Requirements describes. A session belongs to one backend process, so behind a load balancer every connection of an editor must reach the same node; configure sticky sessions, as Scaling and Load Balancing explains. The Deployment Guide covers the complete setup of each platform.

IIS, nginx and Azure App Service

ServerWhat the connection needsSetup
IISThe Windows feature WebSocket Protocol. On Windows desktop, select it under Internet Information Services > World Wide Web Services > Application Development Features in Turn Windows features on or off. IIS Express always has WebSockets enabled. A sub-application runs in a subfolder, so set APIRoute as described in Host the Application in a Subfolder.Prepare the Server
nginxThe Upgrade and Connection headers of the request, passed on to the application: the WebSocket handshake is an HTTP request that asks to upgrade the connection, and nginx doesn't forward these hop-by-hop headers by default. Without them, the handshake fails and the editor doesn't load.Configure nginx
Azure App ServiceWeb sockets switched on and 64 Bit as the platform on Windows, and Session affinity on several instancesAzure

YARP and Kubernetes

  • YARP. The YARP reverse proxy forwards WebSocket connections by default. For several destinations, enable SessionAffinity in the configuration of the cluster, so that the connections of an editor stay on one destination.
  • Kubernetes. Whether an ingress forwards WebSockets, how long it keeps idle connections open and how it keeps a client on one pod are settings of the ingress controller or Gateway API implementation you use. Check its documentation before you deploy the editor behind it.

Pass Tokens Without the URL

A token in the query string of the WebSocket URL ends up in the request logs of web servers and reverse proxies. DocumentEditorSettings.webSocketProtocols sends it in the Sec-WebSocket-Protocol header of the handshake instead. Access Tokens shows the frontend code, and WebSocket Security the middleware that checks the token.

Diagnose Connection Problems

When an editor doesn't connect or loses its connection, follow the path above from the browser to the backend: the browser console, the close reason of the webSocketClosed event, the URL and route, the backend process and its port, proxies and firewalls, and the versions of the client and server packages. WebSockets in Troubleshooting goes through these checks and their fixes.

Recognize Outdated Setups

Articles about earlier versions describe setups that the current packages replace: a Windows service as the backend up to version 32.0, backend files copied from the setup folder in 33.0, app.UseMiddleware<WebSocketMiddleware>() instead of app.UseTXWebSocketMiddleware() and port numbers of earlier versions. The address and port overloads of UseTXWebSocketMiddleware also replace hand-written middleware classes that create a WebSocketHandler themselves. A Setup From an Older Article Doesn't Connect in Troubleshooting and From the Old Document Editor Backend to the Backend Package describe how to update such an application.

Next Steps