Networking
Everything a module does over the network lives in SharpProspero.Platform: read the connection’s
status, download over HTTP, run a small HTTP server, or drive raw TCP and UDP sockets directly. The
sockets are IPv4, with a poller for serving many connections from one thread and a resolver for
connecting by host name.
graph LR
subgraph status [Status]
NI[NetworkInfo]
end
subgraph traffic [Sockets and HTTP]
HR[HostResolver] --> SA[SocketAddress]
SA --> TCP[TcpConnection / TcpListener]
SA --> UDP[UdpSocket]
TCP --> SP[SocketPoller]
TCP --> HS[HttpServer]
HC[HttpClient]
end
Two layers sit here. The high-level types — NetworkInfo, HttpClient, HttpServer, and
DownloadService — cover the common jobs on their own. Under them, the raw sockets and the poller are
for a protocol the high-level types do not speak.
| To | Use |
|---|---|
| Read the connection’s status | NetworkInfo |
| Fetch a file or a package from a URL | HttpClient |
| Serve a page or an API | HttpServer |
| Speak your own protocol over TCP | TcpConnection, TcpListener |
| Send and receive datagrams | UdpSocket |
| Serve many connections from one thread | SocketPoller |
| Connect to a host by name | HostResolver |
| Control the transfers the system is running | DownloadService |
On this page
Network information
NetworkInfo reports the connection, the same panel a system-information screen shows. Open it, read
the fields, dispose it. Each read reflects the connection at the moment it is called.
using var net = NetworkInfo.Open();
if (net.IsConnected)
{
Show(net.IpAddress); // "192.168.1.20"
Show(net.Ssid); // wireless network name, empty when wired
Show(net.MacAddress); // "00:1a:2b:c0:ff:ee"
Show(net.SignalStrength); // 0 to 100 on wireless
}
State reports where the connection is as a NetCtlState (Disconnected, Connecting,
IpObtaining, IpObtained); IsConnected is the shorthand for having an address. Device reports
Wired or Wireless. Both enums live in SharpProspero.Interop.Net, so add that using directive to
branch on them. IpAddress, SubnetMask, DefaultGateway, PrimaryDns, Ssid, MacAddress and
Mtu fill in the rest. Opening needs no socket pool; the status service is the only network call it
makes.
Addresses
A SocketAddress is an IPv4 endpoint: four address octets and a port.
var any = SocketAddress.Any(8080); // every interface, port 8080
var local = SocketAddress.Loopback(9000); // 127.0.0.1:9000
var server = SocketAddress.Parse("192.168.1.10", 21);
IpString gives the address as text without the port, ToString gives address:port, and TryParse
returns false instead of throwing on a malformed address.
A TCP client
Connect, send a request, read the reply, and dispose the connection.
using var conn = TcpConnection.Connect(SocketAddress.Parse("192.168.1.10", 80));
conn.SendAll("GET / HTTP/1.0\r\n\r\n"u8);
Span<byte> buffer = stackalloc byte[2048];
int read = conn.Receive(buffer); // 0 means the peer closed the connection
SendAll repeats until every byte is accepted. Send sends once and reports how many bytes went, for
callers that manage their own buffering. SetReceiveTimeout bounds a blocking receive, in microseconds,
with zero meaning wait forever; RemoteAddress reports the peer, and Shutdown stops sends, receives,
or both without closing the socket.
A TCP server
Bind a listener, accept a client, serve it. This shape handles one client at a time; the next section handles many at once.
using var listener = TcpListener.Listen(SocketAddress.Any(8080));
while (running)
{
using TcpConnection client = listener.Accept();
Span<byte> request = stackalloc byte[1024];
int read = client.Receive(request);
client.SendAll(response);
}
Listen takes an optional backlog, the number of pending connections the system may queue.
LocalAddress reports the bound endpoint, resolving a zero port to the one the system assigned.
Serving many clients from one thread
To serve several connections at once without threads, set the sockets to non-blocking and drive them
from a SocketPoller. The poller reports which sockets are ready; register each with a token the
caller chooses, usually an index into its own table of connections.
using var listener = TcpListener.Listen(SocketAddress.Any(8080));
listener.Blocking = false;
using var poller = SocketPoller.Create();
poller.Add(listener.Handle, PollEvents.Read, token: 0);
Span<PollReady> ready = stackalloc PollReady[32];
while (running)
{
int count = poller.Wait(ready, timeoutMicroseconds: -1); // -1 waits until something is ready
for (int i = 0; i < count; i++)
{
if (ready[i].Token == 0)
{
TcpConnection client = listener.Accept();
client.Blocking = false;
// register client.Handle with its own token and keep it in a table
}
else if (ready[i].IsReadable)
{
// read from the connection the token maps to
}
}
}
PollEvents is a flags enum: watch for Read and Write; Error and HangUp are reported, never
watched for. Each PollReady carries the Token that was registered and helpers over what fired —
IsReadable, IsWritable, and IsClosed for an error or a hang-up. Add, Modify and Remove
change what is watched. SocketPoller.Abort unblocks a thread waiting in Wait, so a server loop can
be told to stop.
Downloading over HTTP
HttpClient downloads over HTTP and HTTPS, to fetch a file or a package from a URL. Create it, make as
many requests as needed, dispose it. Creating it brings up the network pool, the TLS context, and the
HTTP service in the order they depend on each other.
using var http = HttpClient.Create();
HttpResponse response = http.Get("https://example.com/homebrew.pkg");
if (response.IsSuccess)
FileSystem.WriteAllBytes("/data/homebrew.pkg", response.Body);
Get resolves the host in the URL itself and returns an HttpResponse with the StatusCode, the
Body and the Headers; IsSuccess is true for a 2xx code, and Header("Content-Type") picks one
header out without regard to case. Create accepts an optional user-agent string. FileSystem is
covered in Files and storage; combined with the package installer in
Packages and devices, this downloads and installs a package from the network.
Post sends a body and names what it is. Send is the general form: it takes any HttpMethod, an
optional body, and header lines of the form Name: value.
HttpResponse created = http.Send(
HttpMethod.Post,
"https://example.com/api/items",
Encoding.UTF8.GetBytes("""{"name":"one"}"""),
["Content-Type: application/json", "Authorization: Bearer " + token]);
string? location = created.Header("Location");
The body’s length is declared on the request before it is sent, so a server that refuses a request
without one accepts these. A Head request reads the headers and no body.
An HTTP server
HttpServer builds a small HTTP/1.1 server on these sockets, so a module can serve a page or an API to
a phone or a computer on the same network — a remote control panel, a status page, or a file browser.
Start it on a port and call PollOnce each frame so it never blocks the loop; it answers one waiting
request and returns. A handler maps a request to a response.
using var server = HttpServer.Start(8080);
// In the frame loop, once per frame:
server.PollOnce(request => request.Path switch
{
"/" => HttpServerResponse.Html("<h1>Hello from C#</h1>"),
"/status" => HttpServerResponse.Json("{\"ok\":true}"),
_ => HttpServerResponse.NotFound(),
});
HttpServerRequest gives the Method, Path (percent-decoded), Query, Headers and Body, plus
Header(name) for one header and BodyText() for the body as UTF-8. HttpServerResponse has Text,
Html, Json, Bytes, NotFound and Redirect builders, and its StatusCode, ContentType,
Headers and Body are settable for anything else. Each request is answered and its connection
closed, which keeps it simple and robust. PollOnce returns whether it served a request, and takes an
optional wait in microseconds — zero, the default, returns at once when no client is waiting.
HttpServerRequest also carries Target, the path and query exactly as sent;
HttpServerResponse.ReasonPhrase sets the text after the status code; and HttpServer.Port reports
the bound port. To dedicate the loop to serving instead of polling, call
Run(handler, keepRunning). Bind to the loopback address only, with Start(port, loopbackOnly: true),
for a server just this console reaches.
The server reads the whole request into memory, so it caps the header block (
MaxHeaderBytes, a fixed 64 KiB) and the body (MaxBodyBytes, default 8 MiB). A request over either cap is answered with 400 Bad Request and its connection closed.
URL encoding and query strings
WebEncoding handles the URL text a client builds and a server reads: percent-encode a value so it is
safe in a URL, decode one back, build a query string from name/value pairs, and parse a query string or
an application/x-www-form-urlencoded request body into pairs.
using SharpProspero.Platform;
string url = "https://host/search?" + WebEncoding.BuildQuery([new("q", "hello world")]);
// In an HTTP handler, read the posted form or the query:
foreach ((string name, string value) in WebEncoding.ParseQuery(request.BodyText()))
Apply(name, value);
PercentEncode keeps the unreserved characters and escapes the rest (a space becomes %20, or + in
form style); PercentDecode reverses it. Text is handled as UTF-8, and a malformed escape raises a
FormatException. ParseQuery returns a list rather than a map, because a name may repeat.
UDP
Datagrams need no connection. Bind to receive, send to an explicit destination.
using var udp = UdpSocket.Bind(SocketAddress.Any(9000));
Span<byte> buffer = stackalloc byte[1500];
int read = udp.ReceiveFrom(buffer, out SocketAddress sender);
udp.SendTo(reply, sender);
UdpSocket.Create makes a send-only socket that is not bound to a local port. Each SendTo is one
whole datagram and each ReceiveFrom returns one, truncated to the buffer if it is larger.
EnableBroadcast allows sending to the broadcast address.
Connecting by host name
HttpClient resolves the host in a URL on its own, so this step is only for the raw sockets: when a
TcpConnection or a UdpSocket targets a name rather than an address, resolve it first. A
HostResolver owns a small network pool for its lifetime.
using var dns = HostResolver.Create();
SocketAddress address = dns.Resolve("example.com", 80);
using var conn = TcpConnection.Connect(address);
Resolve takes a per-attempt timeout in seconds (5 by default, 2000 at most) and a retry count (2 by
default). Unlike the poller and the socket timeouts, this one is in seconds.
Background transfers
DownloadService controls the transfers the system is already running: find the task carrying a piece
of content, then hold it back, let it carry on, or stop it. A tool that reports what the console is
downloading, or that pauses a transfer while something else runs, works through this. The service is
loaded at run time and needs a block of memory the object reserves and releases, so reaching it depends
on what the running build is permitted to do — TryOpen reports a refusal rather than raising, while
Open is the same call that raises.
using SharpProspero.Platform;
if (DownloadService.TryOpen(out DownloadService? transfers))
{
using (transfers)
{
if (transfers!.TryFindTaskByContentId(contentId, kind, out int task))
{
transfers.Pause(task);
// ... later
transfers.Resume(task);
}
}
}
| Call | What it does |
|---|---|
TryOpen(out service, memorySize) |
Load and start the service, reporting whether it could be reached. |
TryFindTaskByContentId(contentId, kind, out taskId) |
Turn a content identifier into a task identifier. kind is one of the values in FindKinds. |
Start / Stop / Pause / Resume (taskId) |
Control one transfer. |
TryGetProgress(taskId, out progress) |
Read how far a transfer has gone as named fields. |
TryGetProgressRecord(taskId, destination) |
Read the whole progress record for the fields that are not named. destination must hold at least DownloadService.ProgressSize (88) bytes, or the call raises ArgumentException. |
TryGetProgress returns a TransferProgress: TotalBytes, TransferredBytes, a PercentComplete
derived from them, an ErrorCode (negative on failure, exposed as HasError), IsComplete, and the
raw State word that last flag reads. The service controls transfers that already exist; creating one
is not offered.
The service asks for at least
DownloadService.MinimumMemorySize(1 MiB), which is the default the object passes.
Errors
A failed socket call raises a ProsperoException whose Code carries the network error, so a caller
can branch on a specific failure such as a refused connection or a timeout. The address helpers throw
the usual argument exceptions instead — SocketAddress.Parse raises FormatException on a bad
address, where TryParse returns false.