This document describes the TCP protocol design for remote mode operation.
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
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
| 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 |
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
namespace Implexus.Protocol {
public interface Request : Object, Message {
public abstract uint16 request_id { get; set; }
}
} // namespace Implexus.Protocol
namespace Implexus.Protocol {
public interface Response : Object, Message {
public abstract uint16 request_id { get; set; }
public abstract bool is_success { get; }
}
} // namespace Implexus.Protocol
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
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
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
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
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
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
namespace Implexus.Protocol {
public errordomain ProtocolError {
INVALID_MESSAGE,
UNKNOWN_MESSAGE_TYPE,
IO_ERROR,
TIMEOUT,
CONNECTION_CLOSED;
}
} // namespace Implexus.Protocol
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
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