amiga-bootcamp/12_networking/bsdsocket.md
2026-04-26 14:46:18 -04:00

1057 lines
35 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

[← 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 25 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 (19921996)
| 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 100300 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 | 13 ms | Stack loopback |
| `connect()` LAN host | 520 ms | ARP + TCP handshake |
| `connect()` WAN host | 50300 ms | RTT dependent |
| `send()` 1 KB | 0.10.5 ms | Memory copy into stack buffers |
| `recv()` 1 KB | 0.10.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 | 20005000 ms | UDP DNS timeout on failure |
| Max throughput (68030/50) | ~300500 KB/s | CPU-bound on copy + TCP stack processing |
| Max throughput (68060) | ~12 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