fluffos/docs/concepts/general/socket_efuns.md
Yucong Sun 89060c5a6b
docs: fully-expandable generated sidebar + Chinese docs via Docusaurus i18n (#1246)
* docs: fully-expandable generated sidebar, replacing index.md link pages

Rework docs navigation so the sidebar expands to every page of every
reference tree, instead of terminating at generated index.md link lists:

- New docs/gen_sidebar.py (replaces gen_index.py + update_index.sh):
  walks efun/, apply/, stdlib/, concepts/, driver/, cli/ and zh-CN/ and
  emits sidebars.generated.json — a full Docusaurus category tree per
  directory. Category landing pages are now `generated-index` card pages
  (title/description/slug), so all generated index.md files are deleted.
  --check mode verifies freshness; new .github/workflows/docs-sidebar.yml
  runs it in CI.
- New docs/sidebar_meta.json holds curated presentation: category labels,
  one-line descriptions (shown on the landing cards), explicit ordering
  (driver/cli/concepts read top-down from user-facing to internals) and
  per-page label overrides.
- sidebars.ts becomes a hand-authored skeleton (Getting Started, lpc/,
  Historical) that splices in the generated trees.

Content reorganization (from a docs-wide review):
- Move misplaced efun pages out of efun/general: terminal/protocol efuns
  (act_mxp, send_zmp, request_term_*) to interactive/, debugging efuns
  (check_memory, dump_*, clear_debug_level, destructed_objects) to
  internals/, shallow_inherit_list to system/.
- Delete stub duplicates superseded by complete pages elsewhere:
  general/parse_{add_synonym,dump,my_rules,remove}, contrib/{shuffle,
  element_of}.

Modernize key pages with MDX:
- index.mdx: landing page with a card grid linking each doc section.
- build.mdx: per-platform <Tabs> (Ubuntu/macOS/Windows/Alpine+Docker),
  admonitions, VitePress [[toc]] leftover removed, stale per-platform CI
  workflow links updated to the unified ci.yml.
- ffi-plan.md: GitHub-style [!CAUTION] alert converted to an admonition.

`npm run build` passes clean (onBrokenLinks: throw, no warnings).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174GM2azvAHBmvyESwxm5om

* docs: serve the Chinese corpus through Docusaurus i18n

Move the zh-CN/ directory out of the default docs tree and into a proper
Docusaurus locale (i18n/zh-CN/docusaurus-plugin-content-docs/current/):

- The flat zh-CN/efun/ directory (333 pages) is re-homed to mirror the
  categorized English layout (name-matched 1:1; `hash` maps to strings/
  per its own frontmatter). apply/ pages map 1:1; the stray English-text
  zh-CN/apply/master/view_errors.md documents a MudOS-era apply that no
  longer exists in the driver and is dropped; stdlib/db/database_zh.md
  becomes the i18n translation of stdlib/db/database.md; the Chinese
  build guide becomes the translation of build.mdx.
- Untranslated pages automatically fall back to English content under
  /zh-CN/, so the whole site is navigable in either locale from the new
  navbar locale dropdown.
- Both locales share one sidebar. Generated sidebar items now carry
  stable `key` fields (the directory/doc path) so translation keys are
  unique (both efun/ and stdlib/ have an "Arrays" category, crypto and
  strings both document `hash`). Category labels, generated-index
  titles/descriptions, navbar and footer are translated in
  i18n/zh-CN/...; theme UI strings come from Docusaurus' bundled
  zh-Hans translations. Translated landing page at /zh-CN/.
- The "中文文档" sidebar section, the zh-CN tree in gen_sidebar.py /
  sidebar_meta.json, and its slice of sidebars.generated.json are gone.
- Relative .md-file links on pages that render in both locales break
  the localized build (the file->permalink map points at the localized
  copy), so concepts/, the two socket_*_option pages and the config.md
  generator now emit extension-less route links instead.
- zh interactive.md/objects.md get explicit slugs like their English
  counterparts (a doc named after its parent directory is otherwise a
  Docusaurus category-index doc, colliding with the generated-index
  route).

`npm run build` builds both locales clean (onBrokenLinks: throw).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174GM2azvAHBmvyESwxm5om

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-11 17:20:25 -04:00

11 KiB
Raw Permalink Blame History

title
general / socket_efuns

socket_efuns

LPC sockets let an object open network connections directly from LPC, using the socket_* efuns. They make it possible to write network services — telnet bridges, inter-mud communication, external clients, and so on — entirely in the mudlib.

Include the mode and option constants with #include <socket.h>, and the error codes with #include <socket_err.h>.

Socket I/O is asynchronous: efuns like socket_connect() and socket_write() return immediately, and the driver later invokes callback functions on your object when network events occur (data arrives, the socket is writable, the connection closes). You register these callbacks when you create, connect, or accept a socket.

Socket modes

socket_create() takes a mode that determines the transport and how data is framed:

Mode Value Transport Data
MUD 0 TCP Any LPC value except objects (arrays, mappings, etc.), serialized in the driver's save/restore format
STREAM 1 TCP Raw string data
DATAGRAM 2 UDP Raw string data
STREAM_BINARY 3 TCP Binary data as buffers
DATAGRAM_BINARY 4 UDP Binary data as buffers
STREAM_TLS 5 TLS over TCP Raw string data, encrypted
STREAM_TLS_BINARY 6 TLS over TCP Binary data, encrypted

MUD mode is STREAM with automatic serialization of LPC values; it is more convenient but slower and heavier than STREAM, so use it only when you need to exchange structured data. TCP modes (MUD, STREAM, the TLS modes) are reliable and ordered. DATAGRAM (UDP) is connectionless and unreliable — a datagram may be silently lost with no error reported — so it is only appropriate when occasional loss is acceptable.

Note that with STREAM, a message may arrive split across several read callbacks (in order); the receiver must be prepared to reassemble it.

Serialization in MUD mode

MUD mode is the only mode that transfers structured LPC values, and how it does so matters. It serializes the value with the driver's own save/restore encoding — the same textual format produced by save_object()/restore_object() — and puts a 4-byte length prefix in front of it; the receiving side decodes that back into an LPC value before invoking the read callback. Arrays, mappings, and nested combinations are supported (up to a nesting depth of 100). Objects cannot be sent and yield EETYPENOTSUPP.

Because this encoding is specific to the LP driver family, MUD mode is really meant for communication between two LP MUDs that both understand it (historically, the inter-mud services that shipped with MudOS). A peer that is not an LP driver receives an opaque, length-prefixed blob and would have to implement a compatible decoder to use it — there is no generic adapter built in.

The other modes do not serialize structured data. STREAM and DATAGRAM put strings on the wire as raw bytes; the *_BINARY modes send buffers as raw bytes. Do not expect to hand an array to a STREAM socket and have it arrive as an array: at best you get a raw, non-portable byte dump of its numeric elements, and mappings are rejected outright. If you need to move structured data between muds, use MUD mode; if you are speaking a byte- or line-oriented protocol (telnet, HTTP, a custom text protocol), use STREAM and handle framing and encoding yourself.

Return values and errors

Every socket efun returns a status. EESUCCESS (1) means success; any negative value is an error or warning. socket_error() converts an error code to a human-readable string. The codes are defined in <socket_err.h>; common ones include EEADDRINUSE (port already bound), EEFDRANGE/EEBADF (bad descriptor), EESECURITY (a security check failed), and the flow-control codes EECALLBACK, EEWOULDBLOCK, and EEALREADY described below.

socket_create() and socket_accept() are the exceptions: on success they return a non-negative socket descriptor rather than EESUCCESS.

Creating a socket

socket_create(int mode, string|function read_callback, void|string|function close_callback) returns a socket descriptor (>= 0) or a negative error.

Sockets are a finite resource, ultimately bounded by the process's file-descriptor limit, so always close sockets you are finished with. When an object is destructed, its sockets are closed automatically. Each open socket has a unique descriptor; a common idiom is to use it as a key into a mapping of per-socket state.

Client/server model

Connection-oriented modes use the client/server model. The server creates a socket, binds it to a well-known port, and listens for connections. The client creates a socket and connects to that port. A port is an integer from 1 to 65535; ports below 1024 are typically reserved, so mudlib services usually use 102465535.

Server: bind, listen, accept

#include <socket.h>
#include <socket_err.h>

int listen_fd;

void create() {
    listen_fd = socket_create(STREAM, "read_callback", "close_callback");
    if (listen_fd < 0) {
        write("socket_create: " + socket_error(listen_fd) + "\n");
        return;
    }

    int err = socket_bind(listen_fd, 12345);
    if (err != EESUCCESS) {
        write("socket_bind: " + socket_error(err) + "\n");
        socket_close(listen_fd);
        return;
    }

    err = socket_listen(listen_fd, "listen_callback");
    if (err != EESUCCESS) {
        write("socket_listen: " + socket_error(err) + "\n");
        socket_close(listen_fd);
    }
}

// Called when a client connects. Accept it to obtain a new socket
// dedicated to that connection; the listening socket keeps listening.
void listen_callback(int fd) {
    int ns = socket_accept(fd, "read_callback", "write_callback");
    if (ns < 0)
        write("socket_accept: " + socket_error(ns) + "\n");
}

void read_callback(int fd, mixed data) {
    socket_write(fd, "You said: " + data);
}

void write_callback(int fd) {
    // The socket is ready to accept more data (see Flow control).
}

void close_callback(int fd) {
    // The peer closed the connection.
}

socket_bind()'s port may be 0, which asks the system to pick any free port — useful for clients, which do not care which local port they use. Binding a port already in use fails with EEADDRINUSE.

socket_accept() returns a new descriptor for the accepted connection; the listening socket is used only to accept, never to transfer data.

Client: connect

The connection target is a single string of "address port". socket_connect() returns immediately; the outcome arrives later via a callback.

#include <socket.h>
#include <socket_err.h>

int fd;

void create() {
    fd = socket_create(STREAM, "read_callback", "close_callback");
    if (fd < 0) {
        write("socket_create: " + socket_error(fd) + "\n");
        return;
    }

    int err = socket_connect(fd, "138.96.19.14 12345",
                             "read_callback", "write_callback");
    if (err != EESUCCESS) {
        write("socket_connect: " + socket_error(err) + "\n");
        socket_close(fd);
    }
}

void write_callback(int fd) {
    socket_write(fd, "hello");
}

void read_callback(int fd, mixed data) {
    write("Received: " + data + "\n");
}

If socket_connect() returns EESUCCESS, exactly one of three things will happen later: the read callback fires (data arrived), the write callback fires (the connection is up and writable), or the close callback fires (the connection failed or was refused). If it returns an error, no callback will fire.

Callbacks

Callbacks may be given as function names (strings) or function pointers. Their signatures are:

void read_callback(int fd, mixed data);              // STREAM / MUD / TLS
void read_callback(int fd, mixed data, string addr); // DATAGRAM modes
void listen_callback(int fd);
void write_callback(int fd);
void close_callback(int fd);

For MUD mode, data may be any LPC type that was sent; it is the receiver's responsibility to validate it. For DATAGRAM modes, the sender's address is passed as a third argument.

Flow control

A computer can generate data far faster than a network can send it, so each socket has a limited send buffer. When it fills, the socket is flow controlled and you must stop writing until it drains. The rule is simple: after a write reports the buffer is full, wait for the write callback before sending more.

socket_write() communicates this through its return value:

  • EESUCCESS — the data was sent (or buffered with room to spare); you may keep writing.
  • EECALLBACK — the data was buffered but the buffer is now full; stop writing until the write callback fires.
  • EEWOULDBLOCK — the data was not buffered; the write callback must fire before you retry. Prefer a call_out() to retry, giving the system a chance to recover.
  • EEALREADY — you wrote while already flow controlled; the data was not buffered. A correctly written application should never see this.

A client is flow controlled until its first write callback, so it must wait for that callback before its first write. A server may write as soon as it has accepted the connection.

Security

Socket use is gated by two mechanisms.

First, the master object's valid_socket() apply is consulted for every socket operation. It receives the calling object, the operation name, and an info array ({ fd, owner, address, port }), and returns 1 to allow or 0 to deny. If the apply does not exist, access is denied. A permissive implementation looks like:

int valid_socket(object caller, string operation, mixed *info) {
    return 1;
}

Second, each socket is owned by the object that created it. Socket efuns compare the caller against the owner and abort if they differ, so one object cannot operate on another's sockets. A violation of either check returns EESECURITY. Ownership can be transferred deliberately with socket_release() and socket_acquire().

Efun reference