09-Client-Server-Protocol.md 19 KB

Client/Server Protocol

This document describes the TCP protocol design for remote mode operation.

Protocol Overview

The Implexus client/server protocol uses a simple binary message format over TCP. Each message consists of a header and payload.

sequenceDiagram
    participant Client
    participant Server
    
    Client->>Server: Connect
    Server-->>Client: Welcome Message
    
    loop Operations
        Client->>Server: Request
        Server-->>Client: Response
    end
    
    Client->>Server: Disconnect
    Server-->>Client: Goodbye

Message Format

Header

All messages start with a common header:

Offset  Size  Field
0       4     Magic: 0x49 0x4D 0x50 0x58 ("IMPX")
4       1     Message type
5       4     Payload length (big-endian)
9       2     Request ID (for request/response matching)
11      ...   Payload

Message Types

Code Type Direction
0x00 WELCOME Server → Client
0x01 GOODBYE Server → Client
0x10 GET_ENTITY Client → Server
0x11 ENTITY_RESPONSE Server → Client
0x12 ENTITY_NOT_FOUND Server → Client
0x13 ENTITY_EXISTS Client → Server
0x14 BOOLEAN_RESPONSE Server → Client
0x20 CREATE_CONTAINER Client → Server
0x21 CREATE_DOCUMENT Client → Server
0x22 CREATE_CATEGORY Client → Server
0x23 CREATE_INDEX Client → Server
0x30 SET_PROPERTY Client → Server
0x31 GET_PROPERTY Client → Server
0x32 REMOVE_PROPERTY Client → Server
0x40 DELETE_ENTITY Client → Server
0x41 GET_CHILDREN Client → Server
0x42 GET_CHILD_NAMES Client → Server
0x50 QUERY_BY_TYPE Client → Server
0x51 QUERY_BY_EXPRESSION Client → Server
0x60 BEGIN_TRANSACTION Client → Server
0x61 COMMIT_TRANSACTION Client → Server
0x62 ROLLBACK_TRANSACTION Client → Server
0x70 ERROR Server → Client
0x7F SUCCESS Server → Client

Protocol Classes

Message Base

namespace Implexus.Protocol {

public interface Message : Object {
    public abstract uint8 message_type { get; }
    public abstract uint8[] serialize();
    public abstract void deserialize(uint8[] data) throws ProtocolError;
}

public class MessageHeader {
    public uint8[] magic { get; set; }
    public uint8 message_type { get; set; }
    public uint32 payload_length { get; set; }
    public uint16 request_id { get; set; }
    
    public static const int SIZE = 11;
    
    public MessageHeader() {
        magic = new uint8[] { 0x49, 0x4D, 0x50, 0x58 };
    }
    
    public uint8[] serialize() {
        var data = new uint8[SIZE];
        data[0] = magic[0];
        data[1] = magic[1];
        data[2] = magic[2];
        data[3] = magic[3];
        data[4] = message_type;
        data[5] = (uint8) (payload_length >> 24);
        data[6] = (uint8) (payload_length >> 16);
        data[7] = (uint8) (payload_length >> 8);
        data[8] = (uint8) payload_length;
        data[9] = (uint8) (request_id >> 8);
        data[10] = (uint8) request_id;
        return data;
    }
    
    public static MessageHeader deserialize(uint8[] data) throws ProtocolError {
        if (data.length < SIZE) {
            throw new ProtocolError.INVALID_MESSAGE("Header too short");
        }
        
        var header = new MessageHeader();
        header.magic = data[0:4];
        
        if (header.magic[0] != 'I' || header.magic[1] != 'M' || 
            header.magic[2] != 'P' || header.magic[3] != 'X') {
            throw new ProtocolError.INVALID_MESSAGE("Invalid magic");
        }
        
        header.message_type = data[4];
        header.payload_length = 
            ((uint32) data[5] << 24) |
            ((uint32) data[6] << 16) |
            ((uint32) data[7] << 8) |
            ((uint32) data[8]);
        header.request_id = (uint16) ((data[9] << 8) | data[10]);
        
        return header;
    }
}

} // namespace Implexus.Protocol

Request Interface

namespace Implexus.Protocol {

public interface Request : Object, Message {
    public abstract uint16 request_id { get; set; }
}

} // namespace Implexus.Protocol

Response Interface

namespace Implexus.Protocol {

public interface Response : Object, Message {
    public abstract uint16 request_id { get; set; }
    public abstract bool is_success { get; }
}

} // namespace Implexus.Protocol

Request/Response Classes

GetEntityRequest

namespace Implexus.Protocol {

public class GetEntityRequest : Object, Request {
    
    private uint16 _request_id;
    private Path _path;
    
    public uint8 message_type { get { return 0x10; } }
    
    public uint16 request_id {
        get { return _request_id; }
        set { _request_id = value; }
    }
    
    public Path path {
        get { return _path; }
        set { _path = value; }
    }
    
    public GetEntityRequest() {
        _path = new Path.root();
    }
    
    public GetEntityRequest.for_path(Path path) {
        _path = path;
    }
    
    public uint8[] serialize() {
        var writer = new ElementWriter();
        writer.write_string(_path.to_string());
        return writer.to_bytes();
    }
    
    public void deserialize(uint8[] data) throws ProtocolError {
        try {
            var reader = new ElementReader(data);
            var path_str = reader.read_string();
            _path = new Path(path_str);
        } catch (SerializationError e) {
            throw new ProtocolError.INVALID_MESSAGE("Failed to deserialize: %s", e.message);
        }
    }
}

} // namespace Implexus.Protocol

EntityResponse

namespace Implexus.Protocol {

public class EntityResponse : Object, Response {
    
    private uint16 _request_id;
    private EntityData _entity_data;
    
    public uint8 message_type { get { return 0x11; } }
    
    public uint16 request_id {
        get { return _request_id; }
        set { _request_id = value; }
    }
    
    public bool is_success { get { return true; } }
    
    public EntityData entity_data {
        get { return _entity_data; }
        set { _entity_data = value; }
    }
    
    public uint8[] serialize() {
        var writer = new ElementWriter();
        
        // Entity type
        writer.write_uint8((uint8) _entity_data.entity_type);
        
        // Path
        writer.write_string(_entity_data.path.to_string());
        
        // Type label (for documents)
        writer.write_string(_entity_data.type_label ?? "");
        
        // Expression (for category/index)
        writer.write_string(_entity_data.expression ?? "");
        
        // Properties (for documents)
        if (_entity_data.properties != null) {
            writer.write_dictionary(_entity_data.properties);
        } else {
            writer.write_null();
        }
        
        return writer.to_bytes();
    }
    
    public void deserialize(uint8[] data) throws ProtocolError {
        try {
            var reader = new ElementReader(data);
            
            var entity_type = (EntityType) reader.read_uint8();
            var path = new Path(reader.read_string());
            var type_label = reader.read_string();
            var expression = reader.read_string();
            
            _entity_data = new EntityData();
            _entity_data.entity_type = entity_type;
            _entity_data.path = path;
            _entity_data.type_label = type_label.length > 0 ? type_label : null;
            _entity_data.expression = expression.length > 0 ? expression : null;
            
            // Properties
            var props_element = reader.read_element();
            if (props_element != null && !props_element.is_null()) {
                // Convert to dictionary
            }
        } catch (SerializationError e) {
            throw new ProtocolError.INVALID_MESSAGE("Failed to deserialize: %s", e.message);
        }
    }
}

public class EntityData {
    public EntityType entity_type;
    public Path path;
    public string? type_label;
    public string? expression;
    public Invercargill.DataStructures.Dictionary<string, Invercargill.Element>? properties;
}

} // namespace Implexus.Protocol

ErrorResponse

namespace Implexus.Protocol {

public class ErrorResponse : Object, Response {
    
    private uint16 _request_id;
    private EngineError _error;
    
    public uint8 message_type { get { return 0x70; } }
    
    public uint16 request_id {
        get { return _request_id; }
        set { _request_id = value; }
    }
    
    public bool is_success { get { return false; } }
    
    public EngineError error {
        get { return _error; }
        set { _error = value; }
    }
    
    public uint8[] serialize() {
        var writer = new ElementWriter();
        
        // Error code
        writer.write_uint8((uint8) _error.code);
        
        // Message
        writer.write_string(_error.message);
        
        return writer.to_bytes();
    }
    
    public void deserialize(uint8[] data) throws ProtocolError {
        try {
            var reader = new ElementReader(data);
            
            var code = (EngineError.Code) reader.read_uint8();
            var message = reader.read_string();
            
            _error = new EngineError(code, message);
        } catch (SerializationError e) {
            throw new ProtocolError.INVALID_MESSAGE("Failed to deserialize: %s", e.message);
        }
    }
}

} // namespace Implexus.Protocol

MessageReader

namespace Implexus.Protocol {

public class MessageReader {
    
    private InputStream _stream;
    private uint8[] _header_buffer;
    
    public MessageReader(InputStream stream) {
        _stream = stream;
        _header_buffer = new uint8[MessageHeader.SIZE];
    }
    
    public Message? read_message() throws ProtocolError {
        try {
            // Read header
            size_t bytes_read;
            _stream.read_all(_header_buffer, out bytes_read);
            
            if (bytes_read == 0) {
                return null; // Connection closed
            }
            
            if (bytes_read < MessageHeader.SIZE) {
                throw new ProtocolError.INVALID_MESSAGE("Incomplete header");
            }
            
            var header = MessageHeader.deserialize(_header_buffer);
            
            // Read payload
            var payload = new uint8[header.payload_length];
            if (header.payload_length > 0) {
                _stream.read_all(payload, out bytes_read);
                if (bytes_read < header.payload_length) {
                    throw new ProtocolError.INVALID_MESSAGE("Incomplete payload");
                }
            }
            
            // Create message based on type
            return create_message(header, payload);
            
        } catch (IOError e) {
            throw new ProtocolError.IO_ERROR("Read error: %s", e.message);
        }
    }
    
    private Message create_message(MessageHeader header, uint8[] payload) throws ProtocolError {
        Message message;
        
        switch (header.message_type) {
            case 0x00: message = new WelcomeMessage(); break;
            case 0x11: message = new EntityResponse(); break;
            case 0x12: message = new EntityNotFoundResponse(); break;
            case 0x14: message = new BooleanResponse(); break;
            case 0x70: message = new ErrorResponse(); break;
            case 0x7F: message = new SuccessResponse(); break;
            default:
                throw new ProtocolError.UNKNOWN_MESSAGE_TYPE(
                    "Unknown message type: 0x%02X", header.message_type
                );
        }
        
        message.request_id = header.request_id;
        if (payload.length > 0) {
            message.deserialize(payload);
        }
        
        return message;
    }
    
    public Response read_response() throws ProtocolError {
        var message = read_message();
        if (message == null) {
            throw new ProtocolError.IO_ERROR("Connection closed");
        }
        if (!(message is Response)) {
            throw new ProtocolError.INVALID_MESSAGE("Expected response");
        }
        return (Response) message;
    }
}

} // namespace Implexus.Protocol

MessageWriter

namespace Implexus.Protocol {

public class MessageWriter {
    
    private OutputStream _stream;
    private uint16 _next_request_id;
    
    public MessageWriter(OutputStream stream) {
        _stream = stream;
        _next_request_id = 1;
    }
    
    public void write_message(Message message) throws ProtocolError {
        try {
            // Get payload
            var payload = message.serialize();
            
            // Create header
            var header = new MessageHeader();
            header.message_type = message.message_type;
            header.payload_length = (uint32) payload.length;
            header.request_id = message.request_id;
            
            // Write header
            var header_data = header.serialize();
            _stream.write(header_data);
            
            // Write payload
            if (payload.length > 0) {
                _stream.write(payload);
            }
            
            _stream.flush();
            
        } catch (IOError e) {
            throw new ProtocolError.IO_ERROR("Write error: %s", e.message);
        }
    }
    
    public uint16 write_request(Request request) throws ProtocolError {
        request.request_id = _next_request_id++;
        write_message(request);
        return request.request_id;
    }
}

} // namespace Implexus.Protocol

Server Implementation

Server Class

namespace Implexus.Server {

public class Server : Object {
    
    private Engine _engine;
    private SocketService _socket_service;
    private uint16 _port;
    private bool _running;
    
    public signal void client_connected();
    public signal void client_disconnected();
    
    public Server(Engine engine, uint16 port = 9090) {
        _engine = engine;
        _port = port;
        _running = false;
    }
    
    public void start() throws ServerError {
        try {
            _socket_service = new SocketService();
            _socket_service.add_inet_port(_port, null);
            _socket_service.incoming.connect(on_incoming);
            _running = true;
            
            print("Implexus server listening on port %d\n", _port);
            
        } catch (Error e) {
            throw new ServerError.STARTUP_FAILED("Failed to start server: %s", e.message);
        }
    }
    
    public void stop() {
        if (_socket_service != null) {
            _socket_service.stop();
            _socket_service = null;
        }
        _running = false;
    }
    
    private bool on_incoming(SocketConnection connection) {
        client_connected();
        
        // Handle in background
        new Thread<void*>("client", () => {
            handle_client(connection);
            return null;
        });
        
        return true;
    }
    
    private void handle_client(SocketConnection connection) {
        var input = connection.get_input_stream();
        var output = connection.get_output_stream();
        
        var reader = new MessageReader(input);
        var writer = new MessageWriter(output);
        
        try {
            // Send welcome
            var welcome = new WelcomeMessage();
            welcome.server_version = 1;
            writer.write_message(welcome);
            
            // Process requests
            while (true) {
                var message = reader.read_message();
                if (message == null) break;
                
                var response = process_request(message);
                writer.write_message(response);
            }
            
        } catch (ProtocolError e) {
            warning("Protocol error: %s", e.message);
        }
        
        client_disconnected();
    }
    
    private Response process_request(Message request) throws ProtocolError {
        try {
            switch (request.message_type) {
                case 0x10: // GET_ENTITY
                    return handle_get_entity((GetEntityRequest) request);
                case 0x13: // ENTITY_EXISTS
                    return handle_entity_exists((EntityExistsRequest) request);
                case 0x20: // CREATE_CONTAINER
                    return handle_create_container((CreateContainerRequest) request);
                // ... other handlers
                default:
                    return new ErrorResponse(EngineError.Code.PROTOCOL_ERROR, 
                        "Unknown request type");
            }
        } catch (EngineError e) {
            return new ErrorResponse.from_error(e);
        }
    }
    
    private Response handle_get_entity(GetEntityRequest request) throws EngineError {
        var entity = _engine.get_entity(request.path);
        
        var response = new EntityResponse();
        response.request_id = request.request_id;
        response.entity_data = entity_to_data(entity);
        
        return response;
    }
    
    private Response handle_entity_exists(EntityExistsRequest request) {
        var response = new BooleanResponse();
        response.request_id = request.request_id;
        response.value = _engine.entity_exists(request.path);
        return response;
    }
    
    private EntityData entity_to_data(Entity entity) {
        var data = new EntityData();
        data.entity_type = entity.entity_type;
        data.path = entity.path;
        data.type_label = entity.type_label;
        data.expression = entity.configured_expression;
        
        if (entity.entity_type == EntityType.DOCUMENT) {
            data.properties = new Invercargill.DataStructures.Dictionary<string, Invercargill.Element>();
            foreach (var key in entity.properties.keys) {
                data.properties.set(key, entity.get_property(key));
            }
        }
        
        return data;
    }
}

} // namespace Implexus.Server

Protocol Error

namespace Implexus.Protocol {

public errordomain ProtocolError {
    INVALID_MESSAGE,
    UNKNOWN_MESSAGE_TYPE,
    IO_ERROR,
    TIMEOUT,
    CONNECTION_CLOSED;
}

} // namespace Implexus.Protocol

Connection Flow

sequenceDiagram
    participant C as Client
    participant S as Server
    participant E as Engine
    
    C->>S: TCP Connect
    S->>C: WELCOME version=1
    
    C->>S: GET_ENTITY path=/users
    S->>E: get_entity with /users
    E-->>S: Entity
    S->>C: ENTITY_RESPONSE
    
    C->>S: CREATE_DOCUMENT path=/users/john type=User
    S->>E: create_document
    E-->>S: Document
    S->>C: ENTITY_RESPONSE
    
    C->>S: SET_PROPERTY path=/users/john name=email value=john@ex.com
    S->>E: set_property
    S->>C: SUCCESS
    
    C->>S: GET_ENTITY path=/nonexistent
    S->>E: get_entity
    E-->>S: throws ENTITY_NOT_FOUND
    S->>C: ENTITY_NOT_FOUND
    
    C->>S: TCP Close

Configuration

namespace Implexus.Server {

public class ServerConfiguration : Object {
    
    public uint16 port { get; set; default = 9090; }
    public int max_connections { get; set; default = 100; }
    public int timeout_seconds { get; set; default = 30; }
    public bool enable_tls { get; set; default = false; }
    public string? tls_cert_path { get; set; default = null; }
    public string? tls_key_path { get; set; default = null; }
}

} // namespace Implexus.Server