跳到主要内容

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:

ModeValueTransportData
MUD0TCPAny LPC value except objects (arrays, mappings, etc.), serialized in the driver's save/restore format
STREAM1TCPRaw string data
DATAGRAM2UDPRaw string data
STREAM_BINARY3TCPBinary data as buffers
DATAGRAM_BINARY4UDPBinary data as buffers
STREAM_TLS5TLS over TCPRaw string data, encrypted
STREAM_TLS_BINARY6TLS over TCPBinary 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 1024–65535.

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