Uso de Bebop con un servidor C# TCP

Uso de Bebop con un servidor C# TCP

Viniendo de un fondo de desarrollo web, encuentro que HTTP se ha adaptado a la mayoría de mis necesidades de comunicación cliente/servidor de manera confiable. Honestamente, generalmente no pienso demasiado en el protocolo cuando trabajo con ASP.NET. Si bien HTTP es un protocolo robusto, la transmisión de datos a través de una conexión TCP conlleva costos generales. HTTP debe ser lo suficientemente flexible para soportar el potencial de múltiples interacciones cliente/servidor y la abundancia de formatos de archivo que podrían transmitirse.

En escenarios controlados, podemos renunciar a la ceremonia de un protocolo flexible como HTTP e ir una capa más abajo, hasta TCP. En este nivel, trataremos con bytes. Algunos pueden optar por tratar exclusivamente con bytes, pero es esencial seleccionar un formato de serialización por razones de seguridad y corrección.

Veremos cómo crear una aplicación de chat servidor/cliente TCP mientras se comunican entre ellos usando el formato de serialización Bebop.

Que es Bebop

Bebop es una nueva tecnología de serialización binaria basada en esquemas con soporte para múltiples plataformas y pilas de tecnología. Al momento de escribir esta publicación, Bebop tiene compiladores de esquemas multiplataforma para .NET y Node. El proyecto tiene como objetivo proporcionar el enfoque de serialización más rápido y eficiente para los desarrolladores, con la publicación inicial del blog casi duplicando el rendimiento de ProtoBuff de Google.

Los creadores de Rainway describen cómo pueden lograr el tipo de perfil de rendimiento descrito en la publicación.

Los desarrolladores definen sus mensajes utilizando la sintaxis del esquema Bebop, después de lo cual compilan el código de serialización y deserialización específico del idioma. La sintaxis del esquema sigue lenguajes basados ​​en C similares y ofrece varios tipos admitidos. Veamos un ejemplo.

[opcode(0x12345678)]
message ChatMessage {
    // awesome
    /* this seems to work */    
    1 -> string text;    
}

message NetworkMessage {
    1 -> uint64 incomingOpCode;
    2 -> byte[] incomingRecord;
}

Tenemos dos mensajes que podemos transmitir a través de una conexión TCP. El NetworkMessage es un contenedor para cualquier otro tipo de mensaje que queramos enviar. Como veremos más adelante en el ejemplo de C#, la biblioteca Bebop admite el manejo de mensajes por tipo y OpCode .

El compilador Bebop toma el esquema y define serializadores basados ​​en clases. Para utilizar estos serializadores de tipo, podemos acceder a cada clase individualmente usando el siguiente código C#.

var bytes = ChatMessage
	.Encode(new ChatMessage { Text = "Hello" });

var message = ChatMessage.Decode(bytes);

Console.WriteLine(message.Text);

La especificación del lenguaje del esquema admite muchas opciones, y los desarrolladores pueden obtener información al respecto en el sitio de documentación oficial.

Veamos cómo crear una solución de servidor y cliente de chat TCP rápida y eficiente que se comunique mediante Bebop.

La estructura de la solución Bebop

Al construir un servidor de bajo nivel, tenemos dos opciones de comunicación:TCP o UDP. Afortunadamente, usaremos un paquete NuGet que admita ambos. Para comenzar, creemos una nueva solución con tres proyectos:Client , Server y Contracts .

El Client y Server los proyectos deben ser aplicaciones de consola, mientras que el Contracts proyecto puede ser una biblioteca de clases. A continuación, convirtamos nuestras aplicaciones de consola en un dúo cliente/servidor habilitado para TCP. Primero, instalemos el NetCoreServer Paquete NuGet.

dotnet add package NetCoreServer

Ahora, instalemos el paquete Bebop en todos de nuestros proyectos.

dotnet add package bebop

Finalmente, necesitamos habilitar el Contracts proyecto la capacidad de compilar nuestros archivos Bebop. Empezamos agregando el bebop-tools paquete al Contracts proyecto.

dotnet add package bebop-tools

También necesitamos modificar nuestro .csproj archivo para incluir un nuevo ItemGroup elemento.


<ItemGroup>
    <Bebop Include="**/*.bop" 
           OutputDir="./Models/" 
           OutputFile="Records.g.cs" 
           Namespace="Cowboy.Contracts" />
</ItemGroup>

Ahora tenemos una solución lista para construir; Comencemos con el proyecto de Contratos.

Contratos Bebop

Como se explicó en una sección anterior, Bebop está basado en esquemas. Al restringir la comunicación, podemos optimizar la eficiencia y la seguridad de la serialización. En nuestro proyecto, creemos un nuevo archivo llamado ChatMessage.bop . Colocaremos el siguiente esquema en el archivo.

[opcode(0x12345678)]
message ChatMessage {
    // awesome
    /* this seems to work */    
    1 -> string text;    
}

message NetworkMessage {
    1 -> uint64 incomingOpCode;
    2 -> byte[] incomingRecord;
}

Cuando construimos nuestro proyecto, deberíamos ver un archivo C# recién generado con nuestros serializadores de tipo para NetworkMessage y ChatMessage . Por brevedad, excluiremos el código generado de este artículo. Ahora estamos listos para comenzar a configurar nuestro Server proyecto.

Aplicación Bebop Server

Tendremos que agregar una referencia a nuestro Contracts proyecto antes de continuar. El primer paso es crear un ChatServer clase. El ChatServer utilizará NetCoreServer para manejar conexiones y mensajes entrantes.

using System;
using System.Net;
using System.Net.Sockets;
using NetCoreServer;

namespace Server
{
    public class ChatServer : TcpServer
    {
        public ChatServer(IPAddress address, int port) : base(address, port) {}

        protected override TcpSession CreateSession() 
            => new ChatSession(this);

        protected override void OnError(SocketError error)
        {
            Console.WriteLine($"Chat TCP server caught an error with code {error}");
        }
    }
}

NetCoreServer opera en el concepto de sesiones, por lo que necesitaremos crear un nuevo ChatSession clase que realizará la mayor parte de la lógica de nuestro servidor de chat.

using System;
using System.Linq;
using System.Net.Sockets;
using Bebop.Runtime;
using Cowboy.Contracts;
using NetCoreServer;

namespace Server
{
    public class ChatSession : TcpSession
    {
        public ChatSession(TcpServer server) : base(server) {}

        protected override void OnConnected()
        {
            Console.WriteLine($"Chat TCP session with Id {Id} connected!");

            // Send invite message
            var message = "Hello from TCP chat! Please send a message or '!' to disconnect the client!";
            SendAsync(message);
        }

        protected override void OnDisconnected()
        {
            Console.WriteLine($"Chat TCP session with Id {Id} disconnected!");
        }

        protected override void OnReceived(byte[] buffer, long offset, long size)
        {
            var message = NetworkMessage.Decode(buffer);
            
            BebopMirror.HandleRecord(
                message.IncomingRecord.ToArray(),
                (uint)message.IncomingOpCode.GetValueOrDefault(),
                this
            );
        }

        protected override void OnError(SocketError error)
        {
            Console.WriteLine($"Chat TCP session caught an error with code {error}");
        }
    }
}

Podemos ver controladores de eventos esenciales que incluyen conexión, desconexión, errores y nuestra capacidad para recibir mensajes.

Bebop se envía con un sistema de manejo interno. Para este ejemplo, cambié entre usar y no usar los controladores de Bebop. La gente debe decidir qué enfoque funciona mejor para ellos. En este ejemplo, usaremos un ChatMessageHandler , y podemos ver la utilización del BebopMirror clase y el OpCode propiedad de nuestro NetworkMessage . En nuestro caso, estamos usando NetworkMessage como envoltorio para futuros tipos de mensajes en caso de que necesitemos enrutar diferentes solicitudes a través de la misma conexión. Veamos la implementación de nuestro controlador.

using System;
using System.Threading.Tasks;
using Bebop.Attributes;
using Bebop.Runtime;
using Cowboy.Contracts;

namespace Server
{
    [RecordHandler]
    public static class ChatMessageHandler
    {
        [BindRecord(typeof(BebopRecord<ChatMessage>))]
        public static Task HandleChatMessage(object state, ChatMessage message)
        {
            var session = (ChatSession) state;

            Console.WriteLine("Incoming: " + message.Text);

            // Multicast message to all connected sessions
            var response = ChatMessage.Encode(new ChatMessage {Text =$"Server says {message.Text}" });
            session.Server.Multicast(response);

            // If the buffer starts with '!' the disconnect the current session
            if (message.Text == "!")
                session.Disconnect();

            return Task.CompletedTask;
        }
    }
}

Podemos ver que nuestro controlador obtiene el ChatSession pasado como el state parámetro. El ChatSession nos permite comunicarnos con todos los clientes conectados. No usamos el NetworkMessage envoltorio en el controlador, pero podríamos si así lo decidiéramos.

Finalmente, actualicemos nuestro Program archivo para iniciar el servidor de chat.

using System;
using System.Net;
using Server;

// TCP server port
int port = 1111;
if (args.Length > 0)
    port = int.Parse(args[0]);

Console.WriteLine($"TCP server port: {port}\n");

// Create a new TCP chat server
var server = new ChatServer(IPAddress.Any, port);

// Start the server
Console.Write("Server starting...");
server.Start();
Console.WriteLine("Done!");
Console.WriteLine("Press Enter to stop the server or '!' to restart the server...");

// Perform text input
for (;;)
{
    string line = Console.ReadLine();
    if (string.IsNullOrEmpty(line))
        break;

    // Restart the server
    if (line == "!")
    {
        Console.Write("Server restarting...");
        server.Restart();
        Console.WriteLine("Done!");
        continue;
    }

    // Multicast admin message to all sessions
    line = "(admin) " + line;
    server.Multicast(line);
}

// Stop the server
Console.Write("Server stopping...");
server.Stop();
Console.WriteLine("Done!");

El servidor de chat comenzará a escuchar en el puerto 1111 para cualquier cliente. Escribamos ese cliente ahora.

Aplicación cliente Bebop

Este proyecto necesita una referencia al Contracts proyecto también. Nuestro primer paso es escribir una clase de controlador de cliente. NetCoreServer se envía con un TcpClient clase básica. Querremos heredar de esta clase e implementar nuestros controladores de eventos.

using System;
using System.Net.Sockets;
using System.Threading;
using Cowboy.Contracts;
using TcpClient = NetCoreServer.TcpClient;

namespace Client
{
    class ChatClient : TcpClient
    {
        public ChatClient(string address, int port) : base(address, port) {}

        public void DisconnectAndStop()
        {
            stop = true;
            DisconnectAsync();
            while (IsConnected)
                Thread.Yield();
        }

        protected override void OnConnected()
        {
            Console.WriteLine($"Chat TCP client connected a new session with Id {Id}");
        }

        protected override void OnDisconnected()
        {
            Console.WriteLine($"Chat TCP client disconnected a session with Id {Id}");

            // Wait for a while...
            Thread.Sleep(1000);

            // Try to connect again
            if (!stop)
                ConnectAsync();
        }

        protected override void OnReceived(byte[] buffer, long offset, long size)
        {
            var record = ChatMessage.Decode(buffer);
            Console.WriteLine(record.Text);
        }

        protected override void OnError(SocketError error)
        {
            Console.WriteLine($"Chat TCP client caught an error with code {error}");
        }

        private bool stop;
    }
}

Como podemos ver en el código, estamos utilizando el ChatMessage serializador directamente. Envolviendo nuestro mensaje en NetworkMessage o usando ChatMessage trabajar. Dicho esto, debemos emparejar correctamente tanto el cliente como el servidor con el tipo de mensaje que hemos elegido.

Finalmente, actualicemos el Client Program del proyecto .

using System;
using Client;
using Cowboy.Contracts;

// TCP server address
string address = "127.0.0.1";
if (args.Length > 0)
    address = args[0];

// TCP server port
int port = 1111;
if (args.Length > 1)
    port = int.Parse(args[1]);

Console.WriteLine($"TCP server address: {address}");
Console.WriteLine($"TCP server port: {port}");

Console.WriteLine();

// Create a new TCP chat client
var client = new ChatClient(address, port);

// Connect the client
Console.Write("Client connecting...");
client.ConnectAsync();
Console.WriteLine("Done!");

Console.WriteLine("Press Enter to stop the client or '!' to reconnect the client...");

// Perform text input
for (;;)
{
    string line = Console.ReadLine();
    if (string.IsNullOrEmpty(line))
        break;

    // Disconnect the client
    if (line == "!")
    {
        Console.Write("Client disconnecting...");
        client.DisconnectAsync();
        Console.WriteLine("Done!");
        continue;
    }

    // Send the entered text to the chat server
    var message = NetworkMessage.Encode(new NetworkMessage {
        IncomingOpCode = BaseChatMessage.OpCode,
        IncomingRecord= ChatMessage.EncodeAsImmutable(
            new ChatMessage { Text = line }
        )
    });
    
    client.SendAsync(message);
}

// Disconnect the client
Console.Write("Client disconnecting...");
client.DisconnectAndStop();
Console.WriteLine("Done!");

¡Eso es todo! ¡Hemos creado con éxito una aplicación de chat cliente/servidor que puede comunicarse usando Bebop!

Ejecutando la muestra

Primero iniciamos el Server aplicación, que comienza a escuchar en el puerto 1111 para cualquier cliente. En este punto, podemos ejecutar cualquier número de Client proyectos.

Aquí podemos ver el protocolo funcionando según lo previsto. Impresionante verdad!

Conclusión

Bebop es una tecnología de serialización basada en esquemas que hace que la escritura de soluciones basadas en TCP/UDP sea más eficiente y segura. Como vio en este ejemplo, no se necesita mucho para crear una muestra de trabajo, y con bebop-tools , los desarrolladores de .NET deberían encontrar la experiencia del tiempo de desarrollo perfecta. Bebop también es compatible con JavaScript, por lo que los usuarios de TypeScript o JavaScript estándar no deberían tener problemas para crear soluciones de pila políglota.

Para acceder a esta solución completa, diríjase a mi repositorio de GitHub y pruébelo.

Espero que hayas encontrado esta publicación interesante.