mirror of
https://github.com/alfishe/amiga-bootcamp.git
synced 2026-06-12 16:16:28 +00:00
1057 lines
35 KiB
Markdown
1057 lines
35 KiB
Markdown
[← Home](../README.md) · [Networking](README.md)
|
||
|
||
# bsdsocket.library — BSD Sockets on AmigaOS: Event Loops, WaitSelect, and Non-Blocking I/O
|
||
|
||
## Overview
|
||
|
||
`bsdsocket.library` is the AmigaOS implementation of the BSD socket API, provided by third-party TCP/IP stacks (AmiTCP, Miami, Roadshow). Unlike Unix where sockets are kernel-managed file descriptors accessed via system calls, Amiga sockets live entirely in user space — each task opens its own private library base with isolated socket state. This means **no context switches** for socket operations, but also **no memory protection** between the application and the stack.
|
||
|
||
The defining feature of the Amiga socket API is [`WaitSelect()`](#waitselect-deep-dive) — a `select()` replacement that simultaneously waits on socket I/O **and** Exec signal bits (window events, timers, ARexx). A single event loop handles network, GUI, and timing without threads — a design that made responsive networked applications possible on a 7 MHz 68000.
|
||
|
||
Key constraints:
|
||
- **Per-task `SocketBase`** — never share between tasks; each task must `OpenLibrary` its own copy
|
||
- **Big-endian** — all multi-byte network values are Motorola byte order; `htons()` / `htonl()` are no-ops but must still be used for portability
|
||
- **No IPv6** — classic Amiga stacks are IPv4-only
|
||
- **User-space stack** — any crash in the stack process (or a misbehaving app) can corrupt socket state for the entire system
|
||
|
||
---
|
||
|
||
## Architecture
|
||
|
||
### Where bsdsocket.library Sits
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
subgraph "Application Task"
|
||
APP["Your Code\n(main task)"]
|
||
SB["SocketBase\n(private per task)"]
|
||
APP <-->|"LVO calls\nsocket(), recv()"| SB
|
||
end
|
||
|
||
subgraph "TCP/IP Stack Process"
|
||
BSS["bsdsocket.library\n(user-space daemon)"]
|
||
TCP["TCP state machine"]
|
||
UDP["UDP"]
|
||
IP["IP / routing"]
|
||
ARP["ARP"]
|
||
SANA["SANA-II IORequest layer"]
|
||
end
|
||
|
||
subgraph "Hardware"
|
||
NIC["Network Card\n(Ethernet/PPP)"]
|
||
end
|
||
|
||
SB <-->|"IPC / shared mem\n(no syscall)"| BSS
|
||
BSS --> TCP --> IP --> ARP --> SANA
|
||
BSS --> UDP --> IP
|
||
SANA --> NIC
|
||
|
||
style SB fill:#e8f4fd,stroke:#2196f3,color:#333
|
||
style BSS fill:#fff9c4,stroke:#f9a825,color:#333
|
||
style SANA fill:#c8e6c9,stroke:#2e7d32,color:#333
|
||
```
|
||
|
||
### The Per-Task Library Base Model
|
||
|
||
Every task that calls `OpenLibrary("bsdsocket.library", ...)` receives a **distinct library base** with its own socket descriptor table, error state, and tag configuration. There is no global fd table in kernel space. This is elegant for cooperative multitasking but fragile:
|
||
|
||
| Aspect | Unix Kernel Socket | Amiga bsdsocket.library |
|
||
|---|---|---|
|
||
| API entry | System call (trap) | Library call (JSR through LVO) |
|
||
| fd table | Per-process, kernel-managed | Per-opener, in stack process memory |
|
||
| Context switch | Yes (user→kernel→user) | No — stays in user space |
|
||
| Cross-task sharing | fd inherited on fork | **Forbidden** — each task needs own `SocketBase` |
|
||
| Protection | Memory-isolated kernel | None — stack runs in user space |
|
||
| Crash impact | Kernel panic (rare) | Stack dies, all sockets lost |
|
||
|
||
> [!WARNING]
|
||
> `bsdsocket.library` runs in the same address space as your application. A stray write through a bad pointer can corrupt the TCP state machine. Use Enforcer or a memory-protected emulator when debugging network code.
|
||
|
||
---
|
||
|
||
## Per-Task Setup
|
||
|
||
### Opening the Library and Configuring Error Handling
|
||
|
||
```c
|
||
/* proto/socket.h, netinet/in.h — stack-provided headers */
|
||
#include <proto/socket.h>
|
||
#include <netinet/in.h>
|
||
|
||
struct Library *SocketBase = NULL;
|
||
LONG myErrno = 0;
|
||
|
||
BOOL initNetwork(void)
|
||
{
|
||
/* Minimum API version: 4 covers all functions used here */
|
||
SocketBase = OpenLibrary("bsdsocket.library", 4);
|
||
if (!SocketBase)
|
||
{
|
||
Printf("No TCP/IP stack running. Start AmiTCP, Miami, or Roadshow.\n");
|
||
return FALSE;
|
||
}
|
||
|
||
/* Route errno into our own variable so we can use it like Unix errno */
|
||
SocketBaseTags(
|
||
SBTM_SETVAL(SBTC_ERRNOPTR(sizeof(LONG))), (ULONG)&myErrno,
|
||
SBTM_SETVAL(SBTC_LOGTAGPTR), (ULONG)"MyApp",
|
||
TAG_DONE);
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
void cleanupNetwork(void)
|
||
{
|
||
if (SocketBase)
|
||
{
|
||
CloseLibrary(SocketBase);
|
||
SocketBase = NULL;
|
||
}
|
||
}
|
||
```
|
||
|
||
> [!CAUTION]
|
||
> **Never share `SocketBase` between tasks.** Each task MUST `OpenLibrary` its own copy. Sharing causes socket state corruption and random crashes. This is the #1 Amiga networking bug.
|
||
|
||
---
|
||
|
||
## Data Structures
|
||
|
||
### Socket Address
|
||
|
||
```c
|
||
/* netinet/in.h */
|
||
struct sockaddr_in {
|
||
UBYTE sin_len; /* V39+ Roadshow; 0 on older stacks */
|
||
UBYTE sin_family; /* AF_INET */
|
||
UWORD sin_port; /* Network byte order — use htons() */
|
||
struct in_addr sin_addr; /* IP address in network byte order */
|
||
UBYTE sin_zero[8]; /* Padding to match struct sockaddr */
|
||
};
|
||
|
||
struct in_addr {
|
||
ULONG s_addr; /* Big-endian 32-bit IPv4 address */
|
||
};
|
||
```
|
||
|
||
### fd_set and timeval
|
||
|
||
```c
|
||
/* sys/time.h */
|
||
struct timeval {
|
||
LONG tv_sec; /* seconds */
|
||
LONG tv_usec; /* microseconds */
|
||
};
|
||
|
||
/* sys/socket.h */
|
||
#define FD_SETSIZE 64
|
||
|
||
typedef struct fd_set {
|
||
ULONG fds_bits[FD_SETSIZE / 32]; /* Bitmap of descriptors */
|
||
} fd_set;
|
||
|
||
#define FD_ZERO(set) /* clear all bits */
|
||
#define FD_SET(fd, set) /* set bit for fd */
|
||
#define FD_CLR(fd, set) /* clear bit for fd */
|
||
#define FD_ISSET(fd, set) /* test bit for fd */
|
||
```
|
||
|
||
> [!NOTE]
|
||
> `FD_SETSIZE` is typically 64 on Amiga stacks. If you need more concurrent sockets, check your stack's headers — Roadshow may support larger values. The `nfds` parameter to `WaitSelect` is the **highest socket number + 1**, not the count of sockets.
|
||
|
||
---
|
||
|
||
## API Reference
|
||
|
||
### Core Socket Lifecycle
|
||
|
||
| LVO | Function | Description |
|
||
|---|---|---|
|
||
| −30 | `socket(domain, type, protocol)` | Create a socket descriptor |
|
||
| −36 | `bind(sock, addr, addrlen)` | Bind to local address/port |
|
||
| −42 | `listen(sock, backlog)` | Mark socket as passive listener |
|
||
| −48 | `accept(sock, addr, addrlen)` | Accept incoming connection |
|
||
| −54 | `connect(sock, addr, addrlen)` | Initiate outgoing connection |
|
||
| −174 | `CloseSocket(sock)` | Close a socket (NOT `close()`) |
|
||
| −84 | `shutdown(sock, how)` | Partially close (send/receive/both) |
|
||
|
||
### Data Transfer
|
||
|
||
| LVO | Function | Description |
|
||
|---|---|---|
|
||
| −66 | `send(sock, buf, len, flags)` | Send on connected socket |
|
||
| −60 | `sendto(sock, buf, len, flags, addr, addrlen)` | Send datagram to address |
|
||
| −78 | `recv(sock, buf, len, flags)` | Receive from connected socket |
|
||
| −72 | `recvfrom(sock, buf, len, flags, from, fromlen)` | Receive datagram with source |
|
||
|
||
### I/O Multiplexing and Control
|
||
|
||
| LVO | Function | Description |
|
||
|---|---|---|
|
||
| −180 | `WaitSelect(nfds, rd, wr, ex, timeout, sigmask)` | `select()` + Exec signal integration |
|
||
| −186 | `IoctlSocket(d, request, argp)` | Socket control (FIONBIO, FIONREAD, etc.) |
|
||
| −90 | `setsockopt(...)` | Set socket options |
|
||
| −96 | `getsockopt(...)` | Get socket options |
|
||
|
||
### Name Resolution
|
||
|
||
| LVO | Function | Description |
|
||
|---|---|---|
|
||
| −102 | `gethostbyname(name)` | Resolve hostname to IP (blocking) |
|
||
| −108 | `gethostbyaddr(addr, len, type)` | Reverse DNS lookup |
|
||
| −210 | `inet_addr(cp)` | Parse dotted-decimal string to `in_addr` |
|
||
| −216 | `Inet_NtoA(in)` | Format `in_addr` to dotted-decimal string |
|
||
| −252 | `getservbyname(name, proto)` | Resolve service name to port |
|
||
|
||
### Error and Configuration
|
||
|
||
| LVO | Function | Description |
|
||
|---|---|---|
|
||
| −168 | `Errno()` | Return last error code for this task |
|
||
| −270 | `SocketBaseTagList(tags)` | Configure per-task behavior |
|
||
|
||
---
|
||
|
||
## WaitSelect Deep Dive
|
||
|
||
`WaitSelect()` is the single most important function in Amiga network programming. It replaces both Unix `select()` and the need for threads in most applications.
|
||
|
||
```c
|
||
LONG WaitSelect(LONG nfds,
|
||
fd_set *readfds, fd_set *writefds, fd_set *exceptfds,
|
||
struct timeval *timeout,
|
||
ULONG *sigmask);
|
||
```
|
||
|
||
### The Bidirectional Sigmask
|
||
|
||
The `sigmask` parameter is **bidirectional** — a pattern unique to AmigaOS:
|
||
|
||
- **On entry**: `*sigmask` contains the Exec signal bits you also want to wait for (e.g., window port signal, Ctrl-C, timer signal)
|
||
- **On exit**: `*sigmask` contains which of those signals actually fired
|
||
|
||
```c
|
||
ULONG sigmask = winSig | timerSig | SIGBREAKF_CTRL_C;
|
||
|
||
LONG result = WaitSelect(maxFd + 1, &readfds, NULL, NULL, &tv, &sigmask);
|
||
|
||
/* sigmask now tells us WHICH non-socket events occurred */
|
||
if (sigmask & winSig) { /* handle window events */ }
|
||
if (sigmask & timerSig) { /* handle timer tick */ }
|
||
if (sigmask & SIGBREAKF_CTRL_C) { running = FALSE; }
|
||
```
|
||
|
||
### Timeout Behavior and the Reinitialization Requirement
|
||
|
||
> [!WARNING]
|
||
> `WaitSelect` may **modify** the `struct timeval` you pass in, decrementing it by the elapsed time (like POSIX `select`). You must **reinitialize `timeout` before every call**. Reusing the same struct after a timeout will eventually collapse to zero and cause busy-polling.
|
||
|
||
```c
|
||
/* CORRECT: reinitialize timeout every iteration */
|
||
while (running)
|
||
{
|
||
struct timeval tv;
|
||
tv.tv_sec = 1; /* 1 second */
|
||
tv.tv_usec = 0;
|
||
|
||
fd_set rfds = masterReadSet; /* copy, because WaitSelect modifies */
|
||
ULONG sigmask = mySignals;
|
||
|
||
LONG n = WaitSelect(maxFd + 1, &rfds, NULL, NULL, &tv, &sigmask);
|
||
/* ... */
|
||
}
|
||
```
|
||
|
||
### Return Value Semantics
|
||
|
||
| Return | Meaning |
|
||
|---|---|
|
||
| `> 0` | Number of socket descriptors ready |
|
||
| `0` | Timeout expired — no sockets ready, no signals received |
|
||
| `< 0` | Error — call `Errno()` to get code (`EINTR`, `EBADF`, etc.) |
|
||
|
||
### The fd_set Destruction Rule
|
||
|
||
Like POSIX `select()`, `WaitSelect` **modifies** the `fd_set` arguments. Only descriptors that are ready remain set. You must reinitialize or copy your fd_sets on every call.
|
||
|
||
```c
|
||
/* WRONG: reusing the same fd_set without re-init */
|
||
FD_SET(sock, &readfds);
|
||
while (running) {
|
||
WaitSelect(sock + 1, &readfds, NULL, NULL, &tv, &sigmask);
|
||
/* After first success, readfds is modified! */
|
||
}
|
||
|
||
/* CORRECT: rebuild or copy each iteration */
|
||
while (running) {
|
||
fd_set rfds;
|
||
FD_ZERO(&rfds);
|
||
FD_SET(sock, &rfds);
|
||
WaitSelect(sock + 1, &rfds, NULL, NULL, &tv, &sigmask);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Event Loop Patterns
|
||
|
||
### Pattern 1: Single Socket + GUI (The Classic)
|
||
|
||
A responsive client application that handles both network data and window events without threads:
|
||
|
||
```c
|
||
ULONG winSig = 1L << window->UserPort->mp_SigBit;
|
||
ULONG ctrlSig = SIGBREAKF_CTRL_C;
|
||
BOOL running = TRUE;
|
||
|
||
while (running)
|
||
{
|
||
struct timeval tv = { 1, 0 }; /* 1 second timeout */
|
||
fd_set rfds;
|
||
FD_ZERO(&rfds);
|
||
FD_SET(sock, &rfds);
|
||
|
||
ULONG sigmask = winSig | ctrlSig;
|
||
|
||
LONG n = WaitSelect(sock + 1, &rfds, NULL, NULL, &tv, &sigmask);
|
||
|
||
if (n > 0 && FD_ISSET(sock, &rfds))
|
||
{
|
||
char buf[4096];
|
||
LONG got = recv(sock, buf, sizeof(buf) - 1, 0);
|
||
if (got > 0)
|
||
{
|
||
buf[got] = '\0';
|
||
/* Process network data */
|
||
}
|
||
else if (got == 0)
|
||
{
|
||
/* Peer closed connection */
|
||
running = FALSE;
|
||
}
|
||
else /* got < 0 */
|
||
{
|
||
LONG err = Errno();
|
||
if (err != EINTR)
|
||
{
|
||
Printf("recv error: %ld\n", err);
|
||
running = FALSE;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (sigmask & winSig)
|
||
{
|
||
struct IntuiMessage *imsg;
|
||
while ((imsg = (struct IntuiMessage *)GetMsg(window->UserPort)))
|
||
{
|
||
switch (imsg->Class)
|
||
{
|
||
case IDCMP_CLOSEWINDOW:
|
||
running = FALSE;
|
||
break;
|
||
/* ... other IDCMP classes ... */
|
||
}
|
||
ReplyMsg((struct Message *)imsg);
|
||
}
|
||
}
|
||
|
||
if (sigmask & ctrlSig)
|
||
{
|
||
Printf("*** Break\n");
|
||
running = FALSE;
|
||
}
|
||
}
|
||
```
|
||
|
||
### Pattern 2: Multi-Socket Server with Dynamic Clients
|
||
|
||
A TCP server that accepts new connections and monitors all clients in one loop:
|
||
|
||
```c
|
||
#define MAX_CLIENTS 16
|
||
|
||
struct Client {
|
||
LONG sock;
|
||
BOOL active;
|
||
} clients[MAX_CLIENTS] = {0};
|
||
|
||
LONG listenSock;
|
||
ULONG winSig, ctrlSig;
|
||
BOOL running = TRUE;
|
||
|
||
while (running)
|
||
{
|
||
fd_set rfds;
|
||
FD_ZERO(&rfds);
|
||
FD_SET(listenSock, &rfds);
|
||
LONG maxFd = listenSock;
|
||
|
||
for (int i = 0; i < MAX_CLIENTS; i++)
|
||
{
|
||
if (clients[i].active)
|
||
{
|
||
FD_SET(clients[i].sock, &rfds);
|
||
if (clients[i].sock > maxFd) maxFd = clients[i].sock;
|
||
}
|
||
}
|
||
|
||
struct timeval tv = { 0, 500000 }; /* 500ms */
|
||
ULONG sigmask = winSig | ctrlSig;
|
||
|
||
LONG n = WaitSelect(maxFd + 1, &rfds, NULL, NULL, &tv, &sigmask);
|
||
|
||
/* Accept new connection? */
|
||
if (n > 0 && FD_ISSET(listenSock, &rfds))
|
||
{
|
||
struct sockaddr_in clientAddr;
|
||
LONG addrLen = sizeof(clientAddr);
|
||
LONG newSock = accept(listenSock,
|
||
(struct sockaddr *)&clientAddr, &addrLen);
|
||
if (newSock >= 0)
|
||
{
|
||
int added = 0;
|
||
for (int i = 0; i < MAX_CLIENTS; i++)
|
||
{
|
||
if (!clients[i].active)
|
||
{
|
||
clients[i].sock = newSock;
|
||
clients[i].active = TRUE;
|
||
added = 1;
|
||
break;
|
||
}
|
||
}
|
||
if (!added)
|
||
{
|
||
Printf("Server full, dropping connection\n");
|
||
CloseSocket(newSock);
|
||
}
|
||
}
|
||
}
|
||
|
||
/* Check client sockets */
|
||
for (int i = 0; i < MAX_CLIENTS; i++)
|
||
{
|
||
if (clients[i].active && FD_ISSET(clients[i].sock, &rfds))
|
||
{
|
||
char buf[1024];
|
||
LONG got = recv(clients[i].sock, buf, sizeof(buf), 0);
|
||
if (got > 0)
|
||
{
|
||
/* Echo back */
|
||
send(clients[i].sock, buf, got, 0);
|
||
}
|
||
else
|
||
{
|
||
/* Disconnect or error */
|
||
CloseSocket(clients[i].sock);
|
||
clients[i].active = FALSE;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* Handle window events / Ctrl-C ... */
|
||
}
|
||
```
|
||
|
||
### Pattern 3: Three-Source Loop (Socket + Timer + GUI)
|
||
|
||
Add a `timer.device` IORequest for periodic tasks (keepalive pings, timeouts, animation frames):
|
||
|
||
```c
|
||
/* Setup timer device (see timer.md for full details) */
|
||
struct timerequest *tr;
|
||
struct MsgPort *timerPort = CreateMsgPort();
|
||
/* OpenDevice(TIMERNAME, UNIT_MICROHZ, (struct IORequest *)tr, 0) ... */
|
||
|
||
ULONG timerSig = 1L << timerPort->mp_SigBit;
|
||
ULONG winSig = 1L << window->UserPort->mp_SigBit;
|
||
|
||
while (running)
|
||
{
|
||
/* Re-queue timer for next tick */
|
||
tr->tr_time.tv_secs = 0;
|
||
tr->tr_time.tv_micro = 16667; /* ~60 Hz */
|
||
SendIO((struct IORequest *)tr);
|
||
|
||
fd_set rfds;
|
||
FD_ZERO(&rfds);
|
||
FD_SET(sock, &rfds);
|
||
|
||
struct timeval tv = { 5, 0 }; /* 5 second safety timeout */
|
||
ULONG sigmask = winSig | timerSig | SIGBREAKF_CTRL_C;
|
||
|
||
LONG n = WaitSelect(sock + 1, &rfds, NULL, NULL, &tv, &sigmask);
|
||
|
||
if (n > 0 && FD_ISSET(sock, &rfds))
|
||
{
|
||
/* Handle socket data */
|
||
}
|
||
|
||
if (sigmask & timerSig)
|
||
{
|
||
WaitIO((struct IORequest *)tr); /* reclaim IORequest */
|
||
/* 60 Hz tick: update UI, send keepalive, etc. */
|
||
}
|
||
|
||
if (sigmask & winSig) { /* handle IDCMP */ }
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Non-Blocking Async I/O
|
||
|
||
### Setting Non-Blocking Mode
|
||
|
||
Use `IoctlSocket()` with `FIONBIO` to enable non-blocking I/O:
|
||
|
||
```c
|
||
ULONG on = 1;
|
||
if (IoctlSocket(sock, FIONBIO, (char *)&on) < 0)
|
||
{
|
||
Printf("IoctlSocket(FIONBIO) failed: %ld\n", Errno());
|
||
}
|
||
```
|
||
|
||
> [!NOTE]
|
||
> Non-blocking UDP sockets work reliably on all stacks. Non-blocking TCP is supported by Miami and Roadshow; early AmiTCP versions had issues with non-blocking TCP `connect()`. Test on your target stack.
|
||
|
||
### The Non-Blocking Connect Pattern
|
||
|
||
A blocking `connect()` can hang for minutes on an unreachable host. The non-blocking pattern:
|
||
|
||
```c
|
||
/* 1. Create non-blocking socket */
|
||
LONG sock = socket(AF_INET, SOCK_STREAM, 0);
|
||
ULONG on = 1;
|
||
IoctlSocket(sock, FIONBIO, (char *)&on);
|
||
|
||
/* 2. Initiate connection (will likely return EINPROGRESS) */
|
||
LONG rc = connect(sock, (struct sockaddr *)&sa, sizeof(sa));
|
||
if (rc < 0 && Errno() != EINPROGRESS)
|
||
{
|
||
Printf("connect failed immediately: %ld\n", Errno());
|
||
CloseSocket(sock);
|
||
return;
|
||
}
|
||
|
||
/* 3. Wait for writable with timeout */
|
||
fd_set wfds;
|
||
struct timeval tv = { 10, 0 }; /* 10 second timeout */
|
||
|
||
FD_ZERO(&wfds);
|
||
FD_SET(sock, &wfds);
|
||
|
||
rc = WaitSelect(sock + 1, NULL, &wfds, NULL, &tv, NULL);
|
||
if (rc > 0 && FD_ISSET(sock, &wfds))
|
||
{
|
||
/* 4. Check SO_ERROR to confirm success */
|
||
LONG so_err = 0;
|
||
LONG optlen = sizeof(so_err);
|
||
getsockopt(sock, SOL_SOCKET, SO_ERROR, (char *)&so_err, &optlen);
|
||
|
||
if (so_err == 0)
|
||
{
|
||
Printf("Connected!\n");
|
||
/* Optionally restore blocking mode */
|
||
ULONG off = 0;
|
||
IoctlSocket(sock, FIONBIO, (char *)&off);
|
||
}
|
||
else
|
||
{
|
||
Printf("Connection failed: %ld\n", so_err);
|
||
CloseSocket(sock);
|
||
}
|
||
}
|
||
else if (rc == 0)
|
||
{
|
||
Printf("Connection timeout\n");
|
||
CloseSocket(sock);
|
||
}
|
||
```
|
||
|
||
### Handling EWOULDBLOCK
|
||
|
||
With non-blocking sockets, `send()` and `recv()` return `-1` with `EWOULDBLOCK` (or `EAGAIN`) when the operation would block:
|
||
|
||
```c
|
||
LONG n = recv(sock, buf, sizeof(buf), 0);
|
||
if (n < 0)
|
||
{
|
||
LONG err = Errno();
|
||
if (err == EWOULDBLOCK || err == EAGAIN)
|
||
{
|
||
/* No data ready — normal for non-blocking */
|
||
/* Will be picked up by next WaitSelect cycle */
|
||
}
|
||
else
|
||
{
|
||
/* Real error */
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## DNS Resolution
|
||
|
||
`gethostbyname()` is **synchronous and blocking** — it does not return until the DNS response arrives (or times out). On Amiga this typically takes 2–5 seconds per failed query.
|
||
|
||
```c
|
||
struct hostent *he = gethostbyname("www.amiga.org");
|
||
if (he)
|
||
{
|
||
struct in_addr addr;
|
||
CopyMem(he->h_addr, &addr, sizeof(addr));
|
||
Printf("Host: %s IP: %s\n", he->h_name, Inet_NtoA(addr.s_addr));
|
||
|
||
/* Multiple addresses are common for load balancing */
|
||
char **p;
|
||
for (p = he->h_addr_list; *p; p++)
|
||
{
|
||
CopyMem(*p, &addr, sizeof(addr));
|
||
Printf(" Addr: %s\n", Inet_NtoA(addr.s_addr));
|
||
}
|
||
}
|
||
else
|
||
{
|
||
/* gethostbyname does not set errno — use h_errno equivalent
|
||
* On Amiga, check Errno() anyway; some stacks set it.
|
||
*/
|
||
Printf("DNS lookup failed (errno=%ld)\n", Errno());
|
||
}
|
||
```
|
||
|
||
> [!WARNING]
|
||
> `gethostbyname()` returns a pointer to a **static buffer** owned by the stack. The result is valid only until the next call to `gethostbyname()` or `gethostbyaddr()` in the same task. Copy the address data before calling again.
|
||
|
||
---
|
||
|
||
## Decision Guides
|
||
|
||
### Blocking vs Non-Blocking vs WaitSelect
|
||
|
||
| Approach | When to Use | Caveats |
|
||
|---|---|---|
|
||
| **Blocking** | Simple scripts, single-connection tools | GUI freezes; no signal handling during I/O |
|
||
| **Non-blocking + WaitSelect** | GUI apps, servers, multi-socket code | More complex; must handle `EWOULDBLOCK` |
|
||
| **WaitSelect only (blocking sockets)** | Most Amiga applications | Simpler logic; `WaitSelect` tells you when data is ready |
|
||
|
||
### Do I Need Threads?
|
||
|
||
Almost never on AmigaOS. `WaitSelect` + signals provides the same concurrency model as `select()` + event loop on Unix. Threads add complexity in a non-memory-protected OS where a crash in any task brings down the system.
|
||
|
||
| Scenario | Amiga Approach |
|
||
|---|---|
|
||
| Handle GUI while downloading | `WaitSelect` on socket + window port |
|
||
| Timeout a slow connect | Non-blocking `connect` + `WaitSelect` on writefds |
|
||
| Server with many clients | Single loop monitoring all sockets in `readfds` |
|
||
| Background file transfer | Separate task with its own `SocketBase`, communicate via MsgPort |
|
||
|
||
---
|
||
|
||
## Historical Context & Modern Analogies
|
||
|
||
### Why Amiga Went User-Space
|
||
|
||
When the Amiga launched in 1985, TCP/IP was an academic curiosity. By the early 1990s, Commodore had added no networking to the OS — the `bsdsocket.library` model emerged from third-party developers (AmiTCP, 1991) who had to work within the existing Exec architecture. Running the stack as a user-space process was not a design choice but a necessity: there was no kernel to extend.
|
||
|
||
Surprisingly, this had advantages:
|
||
- **No syscall overhead** — socket calls are simple JSR instructions
|
||
- **Stack swapability** — users could switch between AmiTCP, Miami, and Roadshow without rebooting (in some cases)
|
||
- **No kernel panics** — a stack crash doesn't bring down the OS (though it kills all network state)
|
||
|
||
And disadvantages:
|
||
- **Zero protection** — any program can corrupt the stack's memory
|
||
- **No kernel buffer cache** — data copies between stack and app add overhead
|
||
- **Single-address-space fragility** — the 68000 has no MMU on most models
|
||
|
||
### Competitive Landscape (1992–1996)
|
||
|
||
| Platform | Networking Model | Socket API |
|
||
|---|---|---|
|
||
| Amiga | User-space stack (bsdsocket) | BSD sockets via library |
|
||
| Atari ST | No standard; STiNG / MiNT later | MiNT had BSD sockets in kernel |
|
||
| Macintosh | MacTCP (user-space) | MacTCP API (not BSD) |
|
||
| DOS | Trumpet/WATTCP packet drivers | BSD-like but fragmented |
|
||
| Windows 3.1 | Winsock 1.1 (user DLL) | BSD-compatible |
|
||
| Linux 1.0 | Kernel TCP/IP | True BSD sockets |
|
||
|
||
The Amiga's approach was most similar to MacTCP and Winsock — both user-space networking layers — but unlike those, AmigaOS had no OS vendor providing the stack. Third-party competition drove rapid innovation (Miami's GUI, Roadshow's stability) but also fragmentation.
|
||
|
||
### Modern Analogies
|
||
|
||
| Amiga Concept | Modern Equivalent | Analogy Strength |
|
||
|---|---|---|
|
||
| `WaitSelect` + signals | `epoll`/`kqueue` + event loop | Strong — single-threaded multiplexing |
|
||
| `bsdsocket.library` | `libuv`, `libevent` | Moderate — both user-space, but libuv is a library not a daemon |
|
||
| Per-task `SocketBase` | Thread-local storage | Weak — TLS is per-thread, SocketBase is per-opener |
|
||
| SANA-II device | NDIS / Linux net_device | Strong — standardized driver interface |
|
||
| User-space stack | DPDK, netmap | Moderate — both bypass kernel, but DPDK is for performance |
|
||
|
||
The key insight for modern developers: Amiga network programming feels like writing an `epoll`-based server in C on Linux, except there's no kernel boundary. The mental model of "register descriptors with a multiplexer, then dispatch events" translates directly.
|
||
|
||
---
|
||
|
||
## Practical Examples
|
||
|
||
### Complete Non-Blocking HTTP Client with Timeout
|
||
|
||
```c
|
||
#include <proto/exec.h>
|
||
#include <proto/dos.h>
|
||
#include <proto/socket.h>
|
||
#include <netinet/in.h>
|
||
#include <sys/time.h>
|
||
|
||
struct Library *SocketBase = NULL;
|
||
LONG myErrno = 0;
|
||
|
||
LONG httpGet(const char *host, UWORD port, const char *path)
|
||
{
|
||
LONG sock = socket(AF_INET, SOCK_STREAM, 0);
|
||
if (sock < 0) return -1;
|
||
|
||
/* Non-blocking connect with 10-second timeout */
|
||
ULONG nb = 1;
|
||
IoctlSocket(sock, FIONBIO, (char *)&nb);
|
||
|
||
struct hostent *he = gethostbyname(host);
|
||
if (!he) { CloseSocket(sock); return -1; }
|
||
|
||
struct sockaddr_in sa = {0};
|
||
sa.sin_family = AF_INET;
|
||
sa.sin_port = htons(port);
|
||
CopyMem(he->h_addr, &sa.sin_addr, he->h_length);
|
||
|
||
LONG rc = connect(sock, (struct sockaddr *)&sa, sizeof(sa));
|
||
if (rc < 0 && myErrno != EINPROGRESS)
|
||
{
|
||
CloseSocket(sock);
|
||
return -1;
|
||
}
|
||
|
||
/* Wait for connection */
|
||
fd_set wfds;
|
||
FD_ZERO(&wfds);
|
||
FD_SET(sock, &wfds);
|
||
struct timeval tv = { 10, 0 };
|
||
|
||
rc = WaitSelect(sock + 1, NULL, &wfds, NULL, &tv, NULL);
|
||
if (rc <= 0 || !FD_ISSET(sock, &wfds))
|
||
{
|
||
CloseSocket(sock);
|
||
return -1;
|
||
}
|
||
|
||
LONG so_err = 0;
|
||
LONG optlen = sizeof(so_err);
|
||
getsockopt(sock, SOL_SOCKET, SO_ERROR, (char *)&so_err, &optlen);
|
||
if (so_err != 0) { CloseSocket(sock); return -1; }
|
||
|
||
/* Restore blocking for simple send/recv */
|
||
nb = 0;
|
||
IoctlSocket(sock, FIONBIO, (char *)&nb);
|
||
|
||
/* Send request */
|
||
char req[256];
|
||
snprintf(req, sizeof(req),
|
||
"GET %s HTTP/1.0\r\nHost: %s\r\n\r\n", path, host);
|
||
send(sock, req, strlen(req), 0);
|
||
|
||
/* Receive response */
|
||
char buf[4096];
|
||
LONG total = 0;
|
||
LONG n;
|
||
while ((n = recv(sock, buf, sizeof(buf) - 1, 0)) > 0)
|
||
{
|
||
buf[n] = '\0';
|
||
Printf("%s", buf);
|
||
total += n;
|
||
}
|
||
|
||
CloseSocket(sock);
|
||
return total;
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
SocketBase = OpenLibrary("bsdsocket.library", 4);
|
||
if (!SocketBase) {
|
||
Printf("No TCP/IP stack\n");
|
||
return 20;
|
||
}
|
||
SocketBaseTags(
|
||
SBTM_SETVAL(SBTC_ERRNOPTR(sizeof(LONG))), (ULONG)&myErrno,
|
||
TAG_DONE);
|
||
|
||
httpGet("www.example.com", 80, "/");
|
||
|
||
CloseLibrary(SocketBase);
|
||
return 0;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## When to Use / When NOT to Use
|
||
|
||
### When to Use bsdsocket.library
|
||
|
||
- Any Amiga application that communicates over TCP/IP or UDP
|
||
- GUI applications that must remain responsive during network I/O — `WaitSelect` makes this natural
|
||
- Servers handling up to a few dozen concurrent connections — the single-event-loop model scales well here
|
||
- Applications integrating network I/O with IDCMP, ARexx, or timer events
|
||
|
||
### When NOT to Use bsdsocket.library
|
||
|
||
- **High-throughput file transfers** — the user-space stack with 680x0 CPU copying can bottleneck below 1 MB/s even on Fast Ethernet. Consider SANA-II direct frame access for custom protocols.
|
||
- **Real-time streaming** — `WaitSelect` timer granularity and stack latency introduce jitter. For 50/60 Hz synchronized data, use UDP with carefully sized buffers or bypass TCP entirely.
|
||
- **IPv6 networks** — classic Amiga stacks do not support IPv6. For dual-stack environments, use a NAT64 gateway or run a modern TCP/IP stack under emulation.
|
||
- **Memory-constrained 512 KB systems** — the TCP/IP stack itself consumes 100–300 KB. On unexpanded A500 systems, networking is impractical.
|
||
|
||
---
|
||
|
||
## Best Practices & Antipatterns
|
||
|
||
### Best Practices
|
||
|
||
1. Always open `bsdsocket.library` at version 4 or higher for full API coverage
|
||
2. Always configure `SBTC_ERRNOPTR` so you can read errors like Unix `errno`
|
||
3. Always use `CloseSocket()`, never AmigaDOS `Close()` — sockets are not file handles
|
||
4. Always reinitialize `struct timeval` before each `WaitSelect` call
|
||
5. Always copy `fd_set` structures before passing to `WaitSelect`; the function modifies them
|
||
6. Always check `SO_ERROR` after a non-blocking `connect` succeeds in `writefds`
|
||
7. Always call `CloseSocket()` on every socket before `CloseLibrary(SocketBase)`
|
||
8. Copy `gethostbyname()` results immediately — the returned pointer is to static stack memory
|
||
9. Use `SIGBREAKF_CTRL_C` in your `WaitSelect` sigmask for clean shutdown
|
||
10. Test on your target stack (AmiTCP vs Miami vs Roadshow) — behavior varies subtly
|
||
|
||
### Named Antipatterns
|
||
|
||
#### "The Shared SocketBase" — Passing SocketBase Between Tasks
|
||
|
||
```c
|
||
/* BAD: Task A opens, Task B uses */
|
||
struct Library *SocketBase; /* global */
|
||
|
||
/* Task A */
|
||
SocketBase = OpenLibrary("bsdsocket.library", 4);
|
||
|
||
/* Task B (separate task!) */
|
||
socket(AF_INET, SOCK_STREAM, 0); /* Crash or silent corruption */
|
||
```
|
||
|
||
```c
|
||
/* CORRECT: Each task opens its own */
|
||
/* Task A */
|
||
struct Library *sbA = OpenLibrary("bsdsocket.library", 4);
|
||
|
||
/* Task B */
|
||
struct Library *sbB = OpenLibrary("bsdsocket.library", 4);
|
||
```
|
||
|
||
#### "The Stale fd_set" — Reusing fd_set After WaitSelect
|
||
|
||
```c
|
||
/* BAD */
|
||
FD_SET(sock, &readfds);
|
||
while (running) {
|
||
WaitSelect(sock + 1, &readfds, NULL, NULL, &tv, &sigmask);
|
||
/* After first iteration, readfds is destroyed! */
|
||
}
|
||
```
|
||
|
||
```c
|
||
/* CORRECT */
|
||
while (running) {
|
||
fd_set rfds;
|
||
FD_ZERO(&rfds);
|
||
FD_SET(sock, &rfds);
|
||
WaitSelect(sock + 1, &rfds, NULL, NULL, &tv, &sigmask);
|
||
}
|
||
```
|
||
|
||
#### "The select() Refugee" — Using close() Instead of CloseSocket()
|
||
|
||
```c
|
||
/* BAD: Unix habit */
|
||
close(sock); /* Closes an AmigaDOS file handle, not the socket! */
|
||
|
||
/* CORRECT */
|
||
CloseSocket(sock);
|
||
```
|
||
|
||
#### "The Forgotten Timeout Reset" — Collapsing tv to Zero
|
||
|
||
```c
|
||
/* BAD */
|
||
struct timeval tv = { 1, 0 };
|
||
while (running) {
|
||
WaitSelect(..., &tv, ...); /* tv becomes {0, 0} after first timeout */
|
||
/* Now busy-polls at 100% CPU! */
|
||
}
|
||
```
|
||
|
||
```c
|
||
/* CORRECT */
|
||
while (running) {
|
||
struct timeval tv = { 1, 0 }; /* fresh each iteration */
|
||
WaitSelect(..., &tv, ...);
|
||
}
|
||
```
|
||
|
||
#### "The Phantom Hostent" — Keeping gethostbyname Result Past Next Call
|
||
|
||
```c
|
||
/* BAD */
|
||
struct hostent *he = gethostbyname("foo.com");
|
||
/* ... later ... */
|
||
struct hostent *he2 = gethostbyname("bar.com");
|
||
/* he->h_addr is now invalid — may point to bar.com's data! */
|
||
CopyMem(he->h_addr, &addr, sizeof(addr));
|
||
```
|
||
|
||
```c
|
||
/* CORRECT */
|
||
struct hostent *he = gethostbyname("foo.com");
|
||
struct in_addr addr;
|
||
CopyMem(he->h_addr, &addr, sizeof(addr)); /* copy immediately */
|
||
/* now safe to call gethostbyname again */
|
||
```
|
||
|
||
---
|
||
|
||
## Pitfalls & Common Mistakes
|
||
|
||
### 1. Per-Task SocketBase Sharing
|
||
|
||
**Symptom:** Random crashes, sockets returning invalid data, `Errno()` returning garbage.
|
||
|
||
**Cause:** Two tasks share one `SocketBase`. The stack maintains per-opener state; when two tasks interleave calls, descriptor tables get corrupted.
|
||
|
||
**Fix:** Every task that does socket I/O must call `OpenLibrary("bsdsocket.library", ...)` independently. Use message ports, not shared library bases, for inter-task communication.
|
||
|
||
### 2. WaitSelect Timeout Not Reinitialized
|
||
|
||
**Symptom:** Application starts responsive, then after a few seconds CPU usage spikes to 100%.
|
||
|
||
**Cause:** POSIX `select()` and Amiga `WaitSelect()` may modify the timeout struct to show remaining time. Reusing it causes the timeout to shrink to zero, creating a busy loop.
|
||
|
||
**Fix:** Declare `struct timeval` inside the loop or reassign before each call.
|
||
|
||
### 3. Confusing Sigmask Directionality
|
||
|
||
**Symptom:** Signals are never detected, or `WaitSelect` returns immediately with spurious signals.
|
||
|
||
**Cause:** Forgetting that `sigmask` is both input and output. If you don't reinitialize it, stale output bits from the previous call leak into the next wait set.
|
||
|
||
**Fix:**
|
||
```c
|
||
ULONG sigmask = winSig | timerSig; /* always reinitialize */
|
||
WaitSelect(..., &sigmask);
|
||
```
|
||
|
||
### 4. Big-Endian Blindness
|
||
|
||
**Symptom:** `connect()` fails with `EADDRNOTAVAIL` or binds to wrong port.
|
||
|
||
**Cause:** The 68000 is big-endian. Port numbers and IP addresses must be in network byte order. `htons(80)` is `0x0050` on Amiga (same as big-endian network order), but writing `sa.sin_port = 80` produces `0x5000` which is port 20480.
|
||
|
||
**Fix:** Always use `htons()` / `htonl()` / `ntohs()` / `ntohl()`. They compile to no-ops on big-endian but make the intent explicit and keep the code portable.
|
||
|
||
### 5. Stack-Specific Behavior Differences
|
||
|
||
**Symptom:** Code works on Miami but fails on AmiTCP, or vice versa.
|
||
|
||
**Cause:** Early AmiTCP (v3) lacks some v4 APIs, handles non-blocking TCP differently, and has a smaller fd_set size. Roadshow has the most complete implementation but may behave differently with `gethostbyname` timeouts.
|
||
|
||
**Fix:** Open the library at the API version you need (`OpenLibrary(..., 4)`). Document your minimum supported stack. Test on all target configurations.
|
||
|
||
---
|
||
|
||
## Use Cases
|
||
|
||
### Real-World Software
|
||
|
||
| Software | bsdsocket Pattern | Notes |
|
||
|---|---|---|
|
||
| **IBrowse** | `WaitSelect` + IDCMP | Single-threaded; tabs share one event loop |
|
||
| **AmiFTP** | Blocking + `WaitSelect` fallback | Data channel uses `WaitSelect`; control channel often blocking |
|
||
| **AmIRC** | Multi-socket `WaitSelect` | Server connection + DCC sends monitored together |
|
||
| **Voyager** | Non-blocking + `WaitSelect` | Heavy use of `IoctlSocket(FIONBIO)` for parallel HTTP requests |
|
||
| **Genesis** | Stack-provided utilities | `ping`, `traceroute`, `telnet` use blocking sockets |
|
||
| **MiamiDX** | `WaitSelect` + ARexx | GUI and scriptable via ARexx port in same event loop |
|
||
|
||
### Integration Patterns
|
||
|
||
- **HTTP client:** Non-blocking `connect` → `WaitSelect` → blocking send/recv for simplicity
|
||
- **Chat client:** `WaitSelect` on server socket + window port + ARexx port
|
||
- **File server:** Listener in `readfds`; accepted sockets added to master set
|
||
- **Game netcode:** UDP `recvfrom` in `WaitSelect` loop at 60 Hz with timer.device
|
||
|
||
---
|
||
|
||
## Performance
|
||
|
||
### Rough Benchmarks (68030/50 MHz, Fast RAM)
|
||
|
||
| Operation | Approximate Time | Notes |
|
||
|---|---|---|
|
||
| `socket()` + `bind()` | < 1 ms | User-space, no syscall |
|
||
| `connect()` localhost | 1–3 ms | Stack loopback |
|
||
| `connect()` LAN host | 5–20 ms | ARP + TCP handshake |
|
||
| `connect()` WAN host | 50–300 ms | RTT dependent |
|
||
| `send()` 1 KB | 0.1–0.5 ms | Memory copy into stack buffers |
|
||
| `recv()` 1 KB | 0.1–0.5 ms | Copy from stack to app buffer |
|
||
| `WaitSelect` wake | < 1 ms | Signal-based, no polling |
|
||
| `gethostbyname()` cache hit | < 1 ms | Miami/Roadshow cache |
|
||
| `gethostbyname()` cache miss | 2000–5000 ms | UDP DNS timeout on failure |
|
||
| Max throughput (68030/50) | ~300–500 KB/s | CPU-bound on copy + TCP stack processing |
|
||
| Max throughput (68060) | ~1–2 MB/s | Fast RAM, optimized stack (Roadshow) |
|
||
|
||
### Bottlenecks
|
||
|
||
1. **CPU copy overhead** — every `send()`/`recv()` copies data between app and stack buffers
|
||
2. **Stack processing** — TCP checksums, segmentation, reassembly on 680x0
|
||
3. **SANA-II driver** — Some drivers are polled; interrupt-driven drivers (X-Surf 100) perform better
|
||
4. **Chip RAM contention** — if stack or driver buffers are in Chip RAM, Blitter/Audio/DMA steal cycles
|
||
|
||
---
|
||
|
||
## FAQ
|
||
|
||
**Q: Can I use `select()` instead of `WaitSelect()`?**
|
||
A: No — Amiga stacks do not provide POSIX `select()`. `WaitSelect` is the only multiplexing primitive. It is a superset of `select()` (adds signal support).
|
||
|
||
**Q: How many sockets can I monitor?**
|
||
A: Typically 64 (`FD_SETSIZE`). Check your stack's `sys/socket.h`. For more, some stacks support larger fd_sets at compile time, or you can use multiple tasks each with their own socket set.
|
||
|
||
**Q: Do I need `htons()` on a big-endian CPU?**
|
||
A: Yes — it compiles to a no-op, but it documents intent and keeps code portable to emulators or future ports.
|
||
|
||
**Q: Can I mix `bsdsocket` I/O with DOS file I/O in the same `WaitSelect`?**
|
||
A: No — AmigaDOS file handles are not socket descriptors and cannot be passed to `WaitSelect`. Use `WaitSelect` for sockets and a separate signal-based mechanism for async DOS I/O, or use a background DOS task with a MsgPort.
|
||
|
||
**Q: Why does my non-blocking TCP `connect()` return `EINPROGRESS` then immediately fail?**
|
||
A: Some early AmiTCP versions do not support non-blocking TCP connect. Use Miami or Roadshow for reliable non-blocking behavior.
|
||
|
||
**Q: How do I set socket send/receive buffer sizes?**
|
||
A: `setsockopt(sock, SOL_SOCKET, SO_SNDBUF, &size, sizeof(size))` and `SO_RCVBUF`. Not all stacks honor large values; test your target.
|
||
|
||
**Q: Is `Errno()` thread-safe?**
|
||
A: On AmigaOS "thread-safe" is less meaningful because there are no threads in the POSIX sense — only tasks. `Errno()` returns the error for the **current task**. Setting `SBTC_ERRNOPTR` routes errors to a per-task variable, which is task-safe.
|
||
|
||
## References
|
||
|
||
- Roadshow SDK documentation: http://roadshow.apc-tcp.de/
|
||
- AmiTCP SDK: Aminet `comm/tcp/AmiTCP-SDK-4.3.lha`
|
||
- Genesis (free AmiTCP fork): Aminet `comm/tcp/Genesis.lha`
|
||
- SANA-II specification: Aminet `docs/hard/sana2.lha`
|
||
- NDK 3.9 `proto/socket.h`, `netinet/in.h`, `sys/socket.h`, `sys/time.h`
|
||
- See also: [tcp_ip_stacks.md](tcp_ip_stacks.md) — stack architecture and configuration
|
||
- See also: [protocols.md](protocols.md) — DNS, TCP, UDP working examples
|
||
- See also: [sana2.md](sana2.md) — SANA-II driver layer below the stack
|
||
- See also: [signals.md](../06_exec_os/signals.md) — Exec signal fundamentals
|
||
- See also: [message_ports.md](../06_exec_os/message_ports.md) — MsgPort and message passing
|