aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoruser <user@clank>2026-09-22 21:54:03 +0200
committeruser <user@clank>2026-09-22 21:54:03 +0200
commitdcd1f1320c102f9cc4cff7eee387ec538304f7d1 (patch)
treee0b48e345c8d34a5f110378e795b8690120ebd55
parentbuild: rebuild every object when include/*.inc changes (diff)
downloadgbos-dcd1f1320c102f9cc4cff7eee387ec538304f7d1.tar.gz
gbos-dcd1f1320c102f9cc4cff7eee387ec538304f7d1.tar.xz
gbos-dcd1f1320c102f9cc4cff7eee387ec538304f7d1.zip
net: one socket per client - four-tuple demux, spawning accept, bounded close
A server could only ever hold ONE connection, which makes an ircd pointless: listen() turned the listener into the connection, and net_find_tcp demuxed on local port alone, so two clients on 6667 were indistinguishable. - net_find_tcp now matches an established socket on the full four-tuple (local port + peer IP + peer port) and only falls back to a LISTENing socket when none matches. - A SYN at a listener no longer consumes it: tcp_spawn_conn clones a new socket (local port + owning pid, so exit/kill still reclaims it) and the listener keeps listening. accept() returns that socket; closing it leaves the listener alone. A full table drops the SYN, and the client's retransmit is taken once a slot frees. - tcp_peer_is_current preserves BC: net_find_tcp scans with the socket index there, and the compare needs BC for the rx buffer base. Without this the scan died at the first mismatching socket, so a second client's handshake ACK never reached its socket and it hung in SYNRCVD - tcpdump showed our SYN+ACK going out and the client's NICK retransmitted five times into silence. - listen() only conflicts with another LISTENER on the port; connections share it by design now. - MAX_SOCKS 4 -> 6 (listener + 3 clients + 2 spare; 537 bytes of WRAM0 still free), and SK_ACC marks a spawned connection as not yet accepted. tcp_close's settle is also time-boxed to ~250 ms of real time. It counted PUMPS (8 x 8000), which is ~30s of wall clock when the peer never answers - invisible with well-behaved peers, but a daemon refusing a client that stayed connected froze itself, and every other client with it, for half a minute. httpd keeps its listener open across requests now, so the next client's SYN is accepted immediately instead of being dropped between close and re-listen: three parallel clients go 0.1/1.1/2.1s -> 0.4/1.0/0.1s.
-rw-r--r--docs/internals.md36
-rw-r--r--include/gbos.inc2
-rw-r--r--src/socket.asm359
-rw-r--r--src/syscall.asm2
-rw-r--r--usr/httpd.c31
-rw-r--r--usr/sock.h47
6 files changed, 376 insertions, 101 deletions
diff --git a/docs/internals.md b/docs/internals.md
index 62167f4..3e27bd2 100644
--- a/docs/internals.md
+++ b/docs/internals.md
@@ -149,16 +149,21 @@ Userland networking is plain C on top of that: `dhcp` (DISCOVER→ACK, then
pump stdin and print the reply), `irc` (a full client: channels, /join /msg
/me, nick colors, flood-safe rendering) and `chat`.
-**Listening** (`net_op_listen` / `net_op_accept`): sockets demux on the local
-port *alone*, so a listener and its connection are the same socket - the SYN
-fills in the peer and the socket becomes the connection. That means one
-connection at a time, re-armed by listening again after close; a second client
-knocking is dropped in silence and its SYN retransmit is taken when we are
-free again (~1s later), which is why `httpd` serves three parallel clients in
-0.1/1.1/2.1s. `accept` never blocks: one RX pump per call, like `recv_nb`.
-Since SYNs reach the same socket as the live connection, `tcp_peer_is_current`
-is what tells a peer's retransmitted SYN apart from a newcomer's - without it
-a second client silently *hijacks* the connection mid-request.
+**Listening** (`net_op_listen` / `net_op_accept`): a listener stays a listener.
+When a SYN arrives, `tcp_spawn_conn` clones a *new* socket from it (local port
++ owning pid), and `accept` hands that socket to the application - so a server
+holds several clients at once, which is the whole point of `ircd`. Closing a
+client socket leaves the listener untouched. `accept` never blocks: one RX pump
+per call, like `recv_nb`. If the socket table is full the SYN is dropped, and
+the client's retransmit is taken once a slot frees.
+
+That makes **demux a four-tuple problem**: one local port now hosts a listener
+plus a socket per client. `net_find_tcp` scans for an established socket
+matching (local port, peer IP, peer port) and only falls back to a LISTENing
+socket if none matches. Matching on local port alone - what it used to do -
+hands every client's segments to whichever socket sits earliest in the table.
+`tcp_peer_is_current` does the peer comparison and **preserves BC**, because
+the scan keeps its socket index there.
**Receive-path invariants** (all three were bugs once, and each one is
invisible until a peer sends more than a toy reply):
@@ -177,10 +182,13 @@ invisible until a peer sends more than a toy reply):
EOF at the hole — truncating the transfer just before the last segments.
`tcp_close` FINs and then pumps for the peer's FIN/ACK, but **stops as soon as
-it arrives** instead of burning its whole budget: the socket otherwise sits in
-`TCP_FINWAIT` for about a second, and a server that closes and re-listens drops
-the next client's SYN in that window, so every request after the first paid a
-SYN retransmit (1.1s → 0.1s per request).
+it arrives**, and in any case after **~250 ms of real time**. Both halves were
+bugs. Lingering in `TCP_FINWAIT` made a server drop the next client's SYN, so
+every request after the first paid a SYN retransmit (1.1s → 0.1s per request).
+And the budget used to be counted in *pumps* (8 × 8000 ≈ 30s of wall clock when
+the peer never answers): a well-behaved peer closes in milliseconds so nobody
+noticed, until `ircd` refused a client that stayed connected and `close()` froze
+the daemon - and every other client with it - for half a minute.
A server must also **drain the request it was sent**. Reading only the request
line leaves the rest unacknowledged: the client retransmits headers for a
diff --git a/include/gbos.inc b/include/gbos.inc
index 589c33c..7f4431b 100644
--- a/include/gbos.inc
+++ b/include/gbos.inc
@@ -194,7 +194,7 @@ DEF NET_RECVNB EQU 8 ; like NET_RECV but never blocks: pump once, then
DEF SOCK_ICMP EQU 1
DEF SOCK_UDP EQU 2
DEF SOCK_TCP EQU 3
-DEF MAX_SOCKS EQU 4
+DEF MAX_SOCKS EQU 6 ; a server holds one listener + a socket per client
; SLIP framing (RFC 1055)
DEF SLIP_END EQU $C0
DEF SLIP_ESC EQU $DB
diff --git a/src/socket.asm b/src/socket.asm
index c719948..9079b7e 100644
--- a/src/socket.asm
+++ b/src/socket.asm
@@ -31,8 +31,10 @@ DEF SK_STATE EQU 17 ; 1 TCP state (see TCP_* below)
DEF SK_SND EQU 18 ; 4 snd_nxt (our next sequence number)
DEF SK_RCV EQU 22 ; 4 rcv_nxt (next sequence we expect from peer)
DEF SK_OWNER EQU 26 ; 1 pid that created this socket (freed when it dies)
-DEF SK_RXBUF EQU 27 ; 288 payload (holds a DHCP reply, or a max segment)
-DEF SK_SIZE EQU 315
+DEF SK_ACC EQU 27 ; 1 connection spawned by a listener, not yet handed
+ ; to the app by accept()
+DEF SK_RXBUF EQU 28 ; 288 payload (holds a DHCP reply, or a max segment)
+DEF SK_SIZE EQU 316
DEF TCP_MSS EQU 200
; TCP connection states
@@ -80,6 +82,12 @@ wNetTO:: DS 3 ; recv timeout counter (net_pump clobbers registers)
wNetRxRem:: DS 1 ; recv: bytes left in RXBUF after a clamped delivery
wNetRxOff:: DS 1 ; tcp_buffer_data: append offset (unread bytes)
wNetTcpInOrd:: DS 1 ; tcp_in: did this segment start exactly at rcv_nxt?
+wNetListen:: DS 2 ; net_find_tcp: listener seen while scanning for an
+ ; exact four-tuple match (used only if none is found)
+wNetAccPort:: DS 2 ; net_op_accept: the listening socket's local port
+wNetSpawnPort::DS 2 ; tcp_spawn_conn: listener's port, cloned to the child
+wNetSpawnOwner::DS 1 ; tcp_spawn_conn: listener's owning pid
+wNetCloseT0:: DS 2 ; tcp_close: tick when the settle wait started
wNetTcpFlags:: DS 1 ; TCP flags for the segment being built
wNetTcpDlen:: DS 1 ; TCP payload length for the segment being built
wNetTcpIn:: DS 1 ; incoming TCP flags
@@ -381,8 +389,9 @@ net_op_listen:
xor a
ret
-; listen_port_free -> CF set if a socket OTHER than this one is already bound
-; to the requested local port.
+; listen_port_free -> CF set if another socket is already LISTENING on the
+; requested local port. Established connections share that port by design (one
+; per client), so only a second listener is a conflict.
listen_port_free:
ld c, 0
.l
@@ -394,6 +403,17 @@ listen_port_free:
ld a, [hl]
or a
jr z, .nx ; free slot
+ push hl
+ ld a, l
+ add SK_STATE
+ ld l, a
+ ld a, h
+ adc 0
+ ld h, a
+ ld a, [hl]
+ pop hl
+ cp TCP_LISTEN
+ jr nz, .nx ; a connection on this port, not a listener
ld a, l
add SK_LPORT
ld l, a
@@ -417,10 +437,15 @@ listen_port_free:
or a
ret
-; ---- accept(sock) -> A = 0 connected / $FE not yet ---------------------------
-; Never blocks: one pump per call, exactly like recv_nb. A server that owns its
-; main loop (httpd) keeps polling the keyboard and stays killable; a kernel-side
-; blocking accept would wedge the process forever on a quiet port.
+; ---- accept(lsock) -> A = new socket id / $FE if nobody is waiting ----------
+; Never blocks: one pump per call, exactly like recv_nb, so a server owns its
+; own main loop and stays killable.
+;
+; The listener KEEPS listening; each connection gets its own socket (spawned by
+; tcp_in when the SYN arrives), which is what lets a server hold several
+; clients at once - an ircd is pointless otherwise. The caller talks to the
+; returned socket with the ordinary send/recv/close, and closing it leaves the
+; listener untouched.
net_op_accept:
ld a, [wNetReq + NR_SOCK]
call net_sock_ptr
@@ -429,13 +454,60 @@ net_op_accept:
ld a, h
ld [wNetSockPtr+1], a
call net_pump
- ld a, [wNetReq + NR_SOCK] ; tcp_in moves wNetSockPtr; restore ours
+ ; hand over the first connection on our port that the app has not seen yet
+ ld a, [wNetReq + NR_SOCK]
call net_sock_ptr
ld a, l
- ld [wNetSockPtr], a
+ add SK_LPORT
+ ld l, a
ld a, h
- ld [wNetSockPtr+1], a
- call sock_ptr_hl
+ adc 0
+ ld h, a
+ ld a, [hl+]
+ ld [wNetAccPort], a ; the listener's local port
+ ld a, [hl]
+ ld [wNetAccPort+1], a
+ ld c, 0
+.scan
+ ld a, c
+ cp MAX_SOCKS
+ jr nc, .none
+ ld a, [wNetReq + NR_SOCK]
+ cp c
+ jr z, .nx ; skip the listener itself
+ ld a, c
+ call net_sock_ptr ; HL = &sock[c]
+ ld a, [hl]
+ cp SOCK_TCP
+ jr nz, .nx
+ push hl
+ ld a, l
+ add SK_ACC
+ ld l, a
+ ld a, h
+ adc 0
+ ld h, a
+ ld a, [hl]
+ or a ; SK_ACC: spawned and not yet accepted?
+ pop hl
+ jr z, .nx
+ push hl
+ ld a, l
+ add SK_LPORT
+ ld l, a
+ ld a, h
+ adc 0
+ ld h, a
+ ld a, [wNetAccPort]
+ cp [hl]
+ jr nz, .nomatch
+ inc hl
+ ld a, [wNetAccPort+1]
+ cp [hl]
+.nomatch
+ pop hl
+ jr nz, .nx
+ push hl
ld a, l
add SK_STATE
ld l, a
@@ -443,14 +515,27 @@ net_op_accept:
adc 0
ld h, a
ld a, [hl]
+ pop hl
cp TCP_ESTAB
- jr z, .up
- cp TCP_DONE ; opened and closed between two polls: still a
- jr z, .up ; connection - let the app read its buffer/EOF
- ld a, $FE
- ret
-.up
+ jr z, .give
+ cp TCP_DONE ; connected and gone between two polls: still
+ jr nz, .nx ; a connection - let the app read its EOF
+.give
+ ld a, l
+ add SK_ACC
+ ld l, a
+ ld a, h
+ adc 0
+ ld h, a
xor a
+ ld [hl], a ; handed over; never offered twice
+ ld a, c ; -> the new socket id
+ ret
+.nx
+ inc c
+ jr .scan
+.none
+ ld a, $FE
ret
; ---- bind(sock, port) - set local port -----------------------------------
@@ -1664,7 +1749,19 @@ sk_set_rcv_from_seq:
ret
; net_find_tcp -> HL = &TCP socket bound to wNetUdpPort, CF set if none
+; net_find_tcp -> HL = &socket for the segment in wNetRxBuf, CF if none.
+;
+; Two passes, because one local port can now host a listener AND several
+; connections at once (that is what lets a server take more than one client):
+; 1. an established socket matching the FULL four-tuple (local port + the
+; peer's IP and port)
+; 2. failing that, a socket LISTENing on the local port - a new connection
+; Matching on local port alone (what this used to do) hands every client's
+; segments to whichever socket sits earliest in the table.
net_find_tcp:
+ xor a
+ ld [wNetListen], a ; no listener seen yet this scan
+ ld [wNetListen+1], a
ld c, 0
.l
ld a, c
@@ -1672,6 +1769,48 @@ net_find_tcp:
ld a, [hl]
cp SOCK_TCP
jr nz, .nx
+ call tcp_lport_matches
+ jr nz, .nx
+ ; local port matches; is this the connection or the listener?
+ push hl
+ ld a, l
+ ld [wNetSockPtr], a
+ ld a, h
+ ld [wNetSockPtr+1], a
+ call sock_state_get
+ cp TCP_LISTEN
+ jr z, .islisten
+ call tcp_peer_is_current ; Z = the four-tuple matches
+ pop hl
+ jr nz, .nx
+ or a ; exact connection wins immediately
+ ret
+.islisten
+ pop hl
+ ld a, l
+ ld [wNetListen], a ; remember it; keep scanning for an exact match
+ ld a, h
+ ld [wNetListen+1], a
+.nx
+ inc c
+ ld a, c
+ cp MAX_SOCKS
+ jr c, .l
+ ld a, [wNetListen] ; no connection matched: use the listener
+ ld l, a
+ ld a, [wNetListen+1]
+ ld h, a
+ or l ; both bytes zero -> nothing for us
+ jr z, .none
+ or a ; CF = 0
+ ret
+.none
+ scf
+ ret
+
+; tcp_lport_matches(HL = &sock) -> Z if SK_LPORT == the segment's dest port.
+; Preserves HL.
+tcp_lport_matches:
push hl
ld a, l
add SK_LPORT
@@ -1681,29 +1820,118 @@ net_find_tcp:
ld h, a
ld a, [wNetUdpPort]
cp [hl]
- jr nz, .nomatch
+ jr nz, .no
inc hl
ld a, [wNetUdpPort+1]
cp [hl]
- jr nz, .nomatch
+.no
pop hl
- or a
ret
-.nomatch
- pop hl
-.nx
- inc c
+
+; sock_state_get -> A = SK_STATE of the current socket (wNetSockPtr)
+sock_state_get:
+ call sock_ptr_hl
+ ld a, l
+ add SK_STATE
+ ld l, a
+ ld a, h
+ adc 0
+ ld h, a
+ ld a, [hl]
+ ret
+
+; tcp_spawn_conn - give an inbound connection its own socket, cloned from the
+; listening socket in wNetSockPtr (local port + owning pid) and marked pending
+; accept(). Points wNetSockPtr at the new socket. CF set if the table is full.
+tcp_spawn_conn:
+ call sock_ptr_hl ; save the listener's local port + owner
+ ld a, l
+ add SK_LPORT
+ ld l, a
+ ld a, h
+ adc 0
+ ld h, a
+ ld a, [hl+]
+ ld [wNetSpawnPort], a
+ ld a, [hl]
+ ld [wNetSpawnPort+1], a
+ call sock_ptr_hl
+ ld a, l
+ add SK_OWNER
+ ld l, a
+ ld a, h
+ adc 0
+ ld h, a
+ ld a, [hl]
+ ld [wNetSpawnOwner], a
+ ld c, 0
+.find
ld a, c
cp MAX_SOCKS
- jr c, .l
+ jr nc, .full
+ ld a, c
+ call net_sock_ptr
+ ld a, [hl]
+ or a
+ jr z, .got ; SK_TYPE == 0: free slot
+ inc c
+ jr .find
+.full
scf
ret
+.got
+ ld a, l
+ ld [wNetSockPtr], a ; everything below runs on the NEW socket
+ ld a, h
+ ld [wNetSockPtr+1], a
+ ld a, SOCK_TCP
+ ld [hl], a ; SK_TYPE
+ call sock_ptr_hl
+ ld a, l
+ add SK_LPORT
+ ld l, a
+ ld a, h
+ adc 0
+ ld h, a
+ ld a, [wNetSpawnPort]
+ ld [hl+], a
+ ld a, [wNetSpawnPort+1]
+ ld [hl], a
+ call sock_ptr_hl
+ ld a, l
+ add SK_HASRX
+ ld l, a
+ ld a, h
+ adc 0
+ ld h, a
+ xor a
+ ld [hl+], a ; HASRX = 0
+ ld [hl], a ; RXLEN = 0 (SK_RXLEN = SK_HASRX + 1)
+ call sock_ptr_hl
+ ld a, l
+ add SK_OWNER
+ ld l, a
+ ld a, h
+ adc 0
+ ld h, a
+ ld a, [wNetSpawnOwner]
+ ld [hl+], a ; inherit the listener's owner, so exit/kill
+ ld a, 1 ; reclaims client sockets too
+ ld [hl], a ; SK_ACC = 1 (waiting for accept)
+ or a ; CF = 0
+ ret
; tcp_peer_is_current -> Z if the segment in wNetRxBuf came from the peer this
-; socket is already talking to (same source IP *and* source port). Sockets
-; demux on local port alone, so a second client's SYN reaches the same socket
-; as the connection in progress; only this tells them apart.
+; socket is already talking to (same source IP *and* source port). One local
+; port now hosts a listener plus several connections, so this is what tells a
+; peer's own segment from another client's.
tcp_peer_is_current:
+ push bc ; net_find_tcp scans with the socket index in
+ call .body ; C, and the compare below needs BC for the
+ pop bc ; rx buffer base - without this the scan dies
+ ret ; after the first mismatching socket. (pop
+ ; leaves the Z flag from .body alone.)
+.body
call sock_ptr_hl
ld a, l
add SK_RIP
@@ -2034,32 +2262,42 @@ tcp_close:
call sk_add_snd
ld a, TCP_FINWAIT
call sock_state_set
- ; Settle: pump for the peer's FIN/ACK - but STOP as soon as it arrives.
- ; Always burning the whole budget keeps the socket in FINWAIT for ~a
- ; second, and a server that closes and re-listens (httpd) drops the next
- ; client's SYN in that window, so every request after the first paid a
- ; TCP SYN retransmit (~1s) before it was even accepted.
- ld b, 8
-.settle
- ld de, 8000
-.s2
- call net_pump
- call sock_ptr_hl
+ ; Settle: pump for the peer's FIN/ACK, stopping the moment it arrives -
+ ; and in any case after ~250 ms of REAL time.
+ ;
+ ; Two lessons are baked in here. Stopping early matters because a server
+ ; that lingers in FINWAIT drops the next client's SYN, which cost httpd a
+ ; ~1s SYN retransmit per request. Time-boxing matters because the budget
+ ; used to be counted in PUMPS (8 x 8000) - about 30 seconds of wall clock
+ ; when the peer never answers. Well-behaved peers close in milliseconds so
+ ; it went unnoticed, until an ircd refused a client that stayed connected:
+ ; close() then froze the whole daemon, and every other client with it, for
+ ; half a minute. One rude peer must not hold a server hostage.
+ call ticks16
ld a, l
- add SK_STATE
- ld l, a
+ ld [wNetCloseT0], a
ld a, h
- adc 0
- ld h, a
- ld a, [hl]
+ ld [wNetCloseT0+1], a
+.settle
+ call net_pump
+ call sock_state_get
cp TCP_DONE
jr z, .free ; peer closed too: nothing left to wait for
- dec de
- ld a, d
- or e
- jr nz, .s2
- dec b
- jr nz, .settle
+ call ticks16 ; HL = now, in 1/64 s ticks
+ ld a, [wNetCloseT0]
+ ld e, a
+ ld a, [wNetCloseT0+1]
+ ld d, a
+ ld a, l
+ sub e
+ ld b, a
+ ld a, h
+ sbc d ; B = elapsed ticks (low byte)
+ or a
+ jr nz, .free ; >= 256 ticks: long past
+ ld a, b
+ cp 16 ; ~250 ms
+ jr c, .settle
.free
call sock_ptr_hl
xor a
@@ -2216,13 +2454,20 @@ tcp_in:
call sock_state_set
ret
.listen
- ; A SYN arriving at a listening socket: learn the peer from the packet,
- ; answer SYN+ACK. Reached again if the peer retransmits its SYN (our
- ; SYN+ACK was lost), which is why SND is re-seeded here rather than
- ; advanced - the reply must always carry the same ISN.
+ ; A SYN at a LISTENING socket: give the connection a socket of its own and
+ ; leave the listener listening, so the next client can arrive while this
+ ; one is being served.
ld a, [wNetTcpIn]
and TF_SYN
ret z ; nothing else opens a connection
+ call tcp_spawn_conn
+ ret c ; table full: drop the SYN. The client
+ ; retransmits, and by then a slot may have
+ ; freed - better than evicting a live peer.
+.handshake
+ ; Learn the peer from the packet and answer SYN+ACK. Re-entered when a
+ ; peer retransmits its SYN (our SYN+ACK was lost), which is why SND is
+ ; re-seeded here rather than advanced: the reply must carry the same ISN.
call sock_ptr_hl ; peer IP <- the IP header's source address
ld a, l
add SK_RIP
@@ -2286,7 +2531,9 @@ tcp_in:
and TF_SYN
jr z, .sr_ack
call tcp_peer_is_current
- jr z, .listen ; our peer resent its SYN: redo the SYN+ACK
+ jp z, .handshake ; our peer resent its SYN: redo the SYN+ACK on
+ ; THIS socket (spawning another would leak one
+ ; socket per retransmission)
ret ; a *different* client knocking while we are
; mid-handshake: drop it. It retransmits, and
; we take it once we are listening again -
diff --git a/src/syscall.asm b/src/syscall.asm
index e0961bc..0ed9f85 100644
--- a/src/syscall.asm
+++ b/src/syscall.asm
@@ -282,7 +282,7 @@ ENDR
; (TimerISR can carry between the two byte reads, which would otherwise read
; $01FF out of a counter going $00FF -> $0100).
; -----------------------------------------------------------------------------
-ticks16:
+ticks16::
ld a, [wTicks+1]
ld h, a
ld a, [wTicks]
diff --git a/usr/httpd.c b/usr/httpd.c
index 335e824..e704e8c 100644
--- a/usr/httpd.c
+++ b/usr/httpd.c
@@ -33,7 +33,8 @@
#define REQSZ 120 /* the request line is all we parse */
#define PATHSZ 48
-static unsigned char sock;
+static unsigned char lsock; /* the listener, held for our lifetime */
+static unsigned char sock; /* the client being served right now */
static unsigned char rbuf[BUFSZ];
static char obuf[OBUFSZ];
static unsigned char olen;
@@ -256,22 +257,22 @@ void main(void) {
puts(" (run it with & and kill it to stop)");
nl();
+ lsock = net_socket(SOCK_TCP);
+ if (lsock == 0xFF) {
+ puts("httpd: no socket");
+ nl();
+ sexit(1);
+ }
+ if (net_listen(lsock, port) == 0xFF) {
+ puts("httpd: listen failed (port in use?)");
+ nl();
+ net_close(lsock);
+ sexit(1);
+ }
for (;;) {
- sock = net_socket(SOCK_TCP);
- if (sock == 0xFF) {
- puts("httpd: no socket");
- nl();
- sexit(1);
- }
- if (net_listen(sock, port) == 0xFF) {
- puts("httpd: listen failed");
- nl();
- net_close(sock);
- sexit(1);
- }
- while (net_accept(sock) != 0) /* one RX pump per call */
+ while ((sock = net_accept(lsock)) == 0xFE) /* one RX pump per call */
msleep(20);
handle();
- net_close(sock); /* FIN: this is what ends the body */
+ net_close(sock); /* FIN: this is what ends the body; listener stays */
}
}
diff --git a/usr/sock.h b/usr/sock.h
index efe7547..c27b0f4 100644
--- a/usr/sock.h
+++ b/usr/sock.h
@@ -21,35 +21,47 @@ static struct netreq _nr;
/* -> socket id, or 0xFF if the table is full */
static unsigned char net_socket(unsigned char type) {
- _nr.op = 0; _nr.sock = type;
+ _nr.op = 0;
+ _nr.sock = type;
return sys_net(&_nr);
}
/* set the remote address/port for send()/recv() */
static unsigned char net_connect(unsigned char s, const unsigned char *ip, unsigned int port) {
unsigned char i;
- _nr.op = 1; _nr.sock = s;
+ _nr.op = 1;
+ _nr.sock = s;
for (i = 0; i < 4; i++) _nr.ip[i] = ip[i];
- _nr.port_hi = (unsigned char)(port >> 8); _nr.port_lo = (unsigned char)port;
+ _nr.port_hi = (unsigned char)(port >> 8);
+ _nr.port_lo = (unsigned char)port;
return sys_net(&_nr);
}
/* send 'len' payload bytes to the connected peer -> 0 ok / 0xFF error */
static unsigned char net_send(unsigned char s, void *buf, unsigned char len) {
- _nr.op = 2; _nr.sock = s; _nr.buf = (unsigned char *)buf; _nr.len = len;
+ _nr.op = 2;
+ _nr.sock = s;
+ _nr.buf = (unsigned char *)buf;
+ _nr.len = len;
return sys_net(&_nr);
}
/* receive up to 'max' payload bytes -> length, or 0xFF on timeout */
static unsigned char net_recv(unsigned char s, void *buf, unsigned char max) {
- _nr.op = 3; _nr.sock = s; _nr.buf = (unsigned char *)buf; _nr.len = max;
+ _nr.op = 3;
+ _nr.sock = s;
+ _nr.buf = (unsigned char *)buf;
+ _nr.len = max;
return sys_net(&_nr);
}
/* non-blocking recv: one RX pump -> length, 0xFE if nothing buffered yet,
0 on TCP EOF. Loop it with msleep() to own your timeout (see ping). */
static unsigned char net_recv_nb(unsigned char s, void *buf, unsigned char max) {
- _nr.op = 8; _nr.sock = s; _nr.buf = (unsigned char *)buf; _nr.len = max;
+ _nr.op = 8;
+ _nr.sock = s;
+ _nr.buf = (unsigned char *)buf;
+ _nr.len = max;
return sys_net(&_nr);
}
-/* passive open: take connections on a local port (TCP). The listener and the
- connection are the same socket, so serve one, close, then listen again. */
+/* passive open: take connections on a local port (TCP). The listener stays a
+ listener - accept() hands back a separate socket for each client. */
static unsigned char net_listen(unsigned char s, unsigned int port) {
_nr.op = 9;
_nr.sock = s;
@@ -57,8 +69,10 @@ static unsigned char net_listen(unsigned char s, unsigned int port) {
_nr.port_lo = (unsigned char)port;
return sys_net(&_nr);
}
-/* -> 0 once a peer has connected, 0xFE if none yet. Never blocks: one RX pump
- per call, so loop it with yield()/msleep() and poll the keyboard too. */
+/* -> the new client's socket id, or 0xFE if nobody is waiting. Never blocks:
+ one RX pump per call, so loop it with yield()/msleep(). Talk to the returned
+ socket with the ordinary send/recv/close; closing it leaves the listener
+ alone, so a server can hold several clients at once (see usr/ircd.c). */
static unsigned char net_accept(unsigned char s) {
_nr.op = 10;
_nr.sock = s;
@@ -66,16 +80,21 @@ static unsigned char net_accept(unsigned char s) {
}
/* set the local (source) port - needed so UDP replies demux back to us */
static unsigned char net_bind(unsigned char s, unsigned int port) {
- _nr.op = 5; _nr.sock = s;
- _nr.port_hi = (unsigned char)(port >> 8); _nr.port_lo = (unsigned char)port;
+ _nr.op = 5;
+ _nr.sock = s;
+ _nr.port_hi = (unsigned char)(port >> 8);
+ _nr.port_lo = (unsigned char)port;
return sys_net(&_nr);
}
static void net_close(unsigned char s) {
- _nr.op = 4; _nr.sock = s; sys_net(&_nr);
+ _nr.op = 4;
+ _nr.sock = s;
+ sys_net(&_nr);
}
/* pump the RX path once (answers inbound pings) without blocking */
static void net_poll(void) {
- _nr.op = 6; sys_net(&_nr);
+ _nr.op = 6;
+ sys_net(&_nr);
}
/* set our IPv4 address (the DHCP client calls this once it has a lease) */
static void net_setip(const unsigned char *ip) {