Protocol Buffers over a Local Socket#

This communication protocol allows a client running on the same machine as the game to interact with a kRPC server over a unix domain socket, rather than a TCP/IP connection.

A local socket carries the same messages as Protocol Buffers over TCP/IP over a cheaper path, bypassing the network stack. A client that makes many calls in quick succession completes more of them per physics update. A client that makes one call and then waits runs at the game’s update rate either way.

Note

If a client library is available for your language, you do not need to implement this protocol.

Note

Unix domain sockets are available on Linux, macOS, and Windows 10 1803 and Windows Server 2019 or later.

Sending and Receiving Messages#

Identical to Protocol Buffers over TCP/IP: messages are encoded according to the protobuf binary format, and are prefixed with their size in bytes encoded as a Protocol Buffers varint.

Connecting to the RPC Server#

The handshake is identical to Protocol Buffers over TCP/IP, differing only in how the connection is opened:

  1. Open a unix domain socket to the path the RPC server is listening on. The server window shows the path, which by default is krpc/rpc inside the directory named by the XDG_RUNTIME_DIR environment variable, or by LOCALAPPDATA on Windows. See Default Socket Paths for the whole rule.

  2. Send a ConnectionRequest message with its type field set to ConnectionRequest.RPC and its protocol_version field set to 1, exactly as for TCP/IP.

  3. Receive a ConnectionResponse message, check that status is ConnectionResponse.OK and protocol_version is 1, and keep the 16-byte client_identifier for the stream connection.

Connecting to the Stream Server#

As for Protocol Buffers over TCP/IP, open a second connection, this time to the stream server’s socket path (krpc/stream by default), and send a ConnectionRequest with its type field set to ConnectionRequest.STREAM, its protocol_version field set to 1, and its client_identifier field set to the value received above.

Connecting to the stream server is optional. If the client doesn’t require stream functionality, there is no need to connect.

Default Socket Paths#

The server and its clients each compute the default paths for themselves. They meet only by following the same rule. For a socket named rpc or stream:

  1. If XDG_RUNTIME_DIR is set and not empty, the path is krpc/<name> inside it. On Windows LOCALAPPDATA is read instead, as there is no runtime directory there.

  2. Otherwise the path is krpc-<user>/<name> inside /tmp, where <user> is the name of the account running the process. /tmp is fixed, as TMPDIR is set per process and would move the directory for the server or the client alone. Windows always names a directory in LOCALAPPDATA, and so never reaches this step.

The user name comes from the account database, as USER is unset in a process started without a login shell. A client whose language offers only the environment, and finds nothing there, reports an error. A path built without the user name would be shared between accounts.

Socket Paths#

A socket path is limited to 100 bytes, as the operating system copies the address into a fixed size structure. The limit counts the bytes a path takes rather than its characters, so a path using multi-byte characters reaches it sooner. The server reports a path that is too long.

The socket file is created when the server starts and removed when it stops. A killed server leaves the file behind, and the next server to use that path removes it before binding.

A stale file is indistinguishable from a live one, so the server connects to the path first and refuses to start if the connection succeeds. A path holding a file with content is left alone.

The permissions on the socket and on the directory holding it control access to the server. XDG_RUNTIME_DIR and LOCALAPPDATA are private to the account they belong to, so sockets placed there are reachable only by that user.

The /tmp fallback is created with whatever the process umask allows, which can leave the socket open to the user’s group. Set XDG_RUNTIME_DIR or configure the paths where that matters.

Invoking Remote Procedures#

See Messaging Protocol.

Examples#

The following Python code connects to the RPC server over a local socket using the name “Jeb”, then invokes the KRPC.GetStatus RPC and prints the server version number from the response. The message encoding is identical to the TCP/IP protocol; only the socket differs.

It is written for the POSIX socket API, so it runs on Linux and macOS. The protocol is the same on Windows, where the address family is reached through winsock, as Python’s socket module does not expose it.

import getpass
import os
import socket

from google.protobuf.internal.decoder import _DecodeVarint
from google.protobuf.internal.encoder import _VarintEncoder

from krpc.schema import KRPC_pb2 as KRPC


def encode_varint(value):
    """Encode an int as a protobuf varint"""
    data = []
    _VarintEncoder()(data.append, value, False)
    return b"".join(data)


def decode_varint(data):
    """Decode a protobuf varint to an int"""
    return _DecodeVarint(data, 0)[0]


def send_message(conn, msg):
    """Send a message, prefixed with its size, to a socket"""
    data = msg.SerializeToString()
    size = encode_varint(len(data))
    conn.sendall(size + data)


def recv_message(conn, msg_type):
    """Receive a message, prefixed with its size, from a socket"""
    # Receive the size of the message data
    data = b""
    while True:
        try:
            data += conn.recv(1)
            size = decode_varint(data)
            break
        except IndexError:
            pass
    # Receive the message data
    data = conn.recv(size)
    # Decode the message
    msg = msg_type()
    msg.ParseFromString(data)
    return msg


def default_path(name):
    """The path the server listens on, unless it was configured with another"""
    directory = os.environ.get("XDG_RUNTIME_DIR")
    if directory:
        return os.path.join(directory, "krpc", name)
    return os.path.join("/tmp", "krpc-" + getpass.getuser(), name)


# Open a unix domain socket to the RPC server
rpc_conn = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
rpc_conn.connect(default_path("rpc"))

# Send an RPC connection request
request = KRPC.ConnectionRequest()
request.type = KRPC.ConnectionRequest.RPC
request.protocol_version = 1
request.client_name = "Jeb"
send_message(rpc_conn, request)

# Receive the connection response
response = recv_message(rpc_conn, KRPC.ConnectionResponse)

# Check the connection was successful
if response.status != KRPC.ConnectionResponse.OK:
    raise RuntimeError("Connection failed: " + response.message)
if response.protocol_version != 1:
    raise RuntimeError("The server does not support protocol version 1")
print("Connected to RPC server")

# Invoke the KRPC.GetStatus RPC
call = KRPC.ProcedureCall()
call.service = "KRPC"
call.procedure = "GetStatus"
request = KRPC.Request()
request.calls.extend([call])
send_message(rpc_conn, request)

# Receive the response
response = recv_message(rpc_conn, KRPC.Response)

# Check for an error in the response
if response.HasField("error"):
    raise RuntimeError("ERROR: " + str(response.error))

# Check for an error in the results
assert len(response.results) == 1
if response.results[0].HasField("error"):
    raise RuntimeError("ERROR: " + str(response.results[0].error))

# Decode the return value as a Status message
status = KRPC.Status()
status.ParseFromString(response.results[0].value)

# Print out the Status message
print(status)