AlgoMaster Logo

Networking Basics

Last Updated: May 22, 2026

Medium Priority
15 min read

Almost every real C# application talks to other machines: a web app sends HTTP requests to a payment service, a desktop client streams updates from a server, a backend pulls product data from a partner's API. Before you can write code that does any of that confidently, it helps to understand what's actually happening underneath. This lesson covers the pieces every networked program relies on: hosts, IP addresses, ports, DNS, the difference between TCP and UDP, the client-server model, where HTTP sits, and what .NET puts in your hand for talking over a network.

What a Network Is, From an App Developer's Angle

A network, at the level a C# developer needs to care about, is a way for two programs on two machines to exchange bytes. One side writes bytes into a connection, the other side reads them out. Both sides agree on a format ahead of time so the bytes mean something, and both sides agree on an address so the bytes go to the right place. Everything else (cables, routers, packet headers, congestion control) is the operating system's problem.

The two programs don't have to live on different physical machines. Two processes on the same laptop can talk over a network connection too, using the loopback address 127.0.0.1. That's how you usually run a local web server during development, and the code is identical to the production case where the two programs live on different continents.

The picture you want in your head looks like this.

The diagram makes one important point: C# code never touches the wire directly. It hands bytes to the OS through a small set of APIs (Socket, HttpClient, TcpClient, and so on), and the OS does the work of getting those bytes across. The same C# code works the same way over Wi-Fi, ethernet, a cellular link, or loopback.

Hosts, IP Addresses, and Ports

Three pieces of identity show up in almost every networked call.

A host is any machine on the network that can send or receive data. Your laptop is a host. The server running an online store's API is a host. A phone, a virtual machine, a container, a smart TV, each one is a host. The job of an address is to point at a specific host.

An IP address is the numeric address of a host. Two versions are in active use. IPv4 addresses look like 192.168.1.10 or 203.0.113.5: four numbers from 0 to 255, separated by dots, holding 32 bits total. IPv6 addresses look like 2001:db8::1: eight groups of four hex digits separated by colons, holding 128 bits total. IPv4 ran out of unique addresses years ago, so IPv6 is the long-term direction, but most everyday traffic still flows over IPv4 thanks to address-sharing tricks like NAT. In C#, an IP address is represented by the IPAddress type in System.Net.

A port is a 16-bit number from 0 to 65535 that identifies a specific program on a host. A single machine can host many networked programs at once: a web server, an SSH daemon, a database, a chat app, each one binds to its own port so the OS knows where to route incoming bytes. Ports below 1024 are "well-known" and usually require admin rights to bind (port 80 for HTTP, port 443 for HTTPS, port 22 for SSH). Anything from 1024 upward is fair game for application use, with 1024-49151 being "registered" ports and 49152-65535 being the ephemeral range the OS picks from automatically for outgoing connections.

Put together, the pair (IP address, port) is called an endpoint, and it's how every TCP or UDP connection is addressed. C# models this with the IPEndPoint type.

IPAddress.Parse builds an IPAddress from a string and works for both IPv4 and IPv6. IPEndPoint pairs an address with a port, and its ToString formats them as address:port. The same types show up across every networking API in the BCL, so the values you build here are exactly what you'd hand to a TcpClient, a Socket, or a UdpClient later in this section.

DNS and Hostname Resolution

People rarely use raw IP addresses. They type algomaster.io into a browser, not 104.21.34.56. The system that turns names into addresses is DNS, the Domain Name System. DNS is itself a network of servers, scattered around the world, that answer the question "what's the IP address for this hostname?"

When your code talks to a named host, a DNS lookup runs first. The OS asks a configured DNS server for the hostname's address, the DNS server either knows the answer or asks another DNS server, and eventually one or more IPAddress values come back. After that, the actual connection is opened to the resolved address, exactly as if you'd typed the address directly.

.NET exposes DNS through the System.Net.Dns class. The most common entry point is Dns.GetHostEntryAsync, which returns an IPHostEntry with the resolved addresses.

Output (sample, addresses change over time):

A single hostname can map to multiple addresses, which is how big sites spread traffic across many servers. The AddressFamily property indicates whether each entry is IPv4 (InterNetwork) or IPv6 (InterNetworkV6). A higher-level API like HttpClient picks one of these addresses automatically. At the socket level, the choice is explicit.

DNS lookups are network round trips and can take anywhere from a few milliseconds (cached) to a few hundred milliseconds (cold lookup on a slow network). The OS and HttpClient both cache results, but calling Dns.GetHostEntryAsync directly in a tight loop pays the cost on every call. Cache results in memory when the hostname doesn't change.

Dns.GetHostAddressesAsync skips the canonical-name lookup and returns just the IPAddress[]. For most application code, either method is fine. GetHostEntryAsync is the more general one and matches what most diagnostic tools (nslookup, dig) show.

The hostname localhost is always defined to mean "this machine," and the OS resolves it from a local hosts file without a network round trip. That's why the loopback addresses come back. The same call against a public hostname goes out to a real DNS server.

TCP vs UDP

Once you've found the address of a host, you need a way to actually move bytes to it. Two protocols cover almost every case: TCP (Transmission Control Protocol) and UDP (User Datagram Protocol). They sit at the same level of the stack but offer very different guarantees.

TCP is connection-oriented and reliable. Before any data flows, the two sides perform a handshake to establish a connection. After that, both sides can send a continuous stream of bytes, and TCP guarantees three things: every byte arrives, the bytes arrive in the order they were sent, and duplicates are removed. If a packet gets lost in transit, TCP retransmits it. If packets arrive out of order, TCP reorders them before handing them to the application. The price for those guarantees is overhead: the handshake adds latency, retransmissions slow things down when the network is lossy, and the protocol carries extra bytes for tracking.

UDP is connectionless and unreliable. There's no handshake, no ordering guarantee, no automatic retransmission. You hand the OS a chunk of bytes (a datagram) and an address, and the OS sends it. The datagram might arrive, might arrive twice, might arrive out of order with respect to other datagrams you sent, or might never arrive at all. In exchange for giving up those guarantees, UDP is faster, has less overhead, and lets the application decide what to do about loss.

The two protocols suit different jobs.

AspectTCPUDP
Connection modelConnection-oriented (handshake first)Connectionless (fire and forget)
ReliabilityGuaranteed delivery, retransmits on lossBest-effort, packets may be lost
OrderingBytes arrive in send orderNo ordering guarantee
Duplicate handlingDuplicates removedDuplicates possible
Unit of transferContinuous byte streamIndependent datagrams
OverheadHigher (handshake, ACKs, headers)Lower (just send the datagram)
Typical usesHTTP, gRPC, SSH, database protocols, file transferDNS queries, real-time video/voice, gaming, telemetry
C# entry pointsTcpClient, TcpListener, SocketUdpClient, Socket

The "stream of bytes" framing for TCP is worth pausing on. TCP doesn't preserve message boundaries. If the sender calls "send these 100 bytes" three times in a row, the receiver might see one read of 300 bytes, or three reads of 100 bytes each, or one read of 250 followed by one of 50. The application has to define its own message format on top (length prefixes, delimiters, or a higher-level protocol like HTTP) if it cares about boundaries.

UDP, in contrast, preserves boundaries exactly. If the sender sends a 100-byte datagram, the receiver gets exactly that 100-byte datagram in one read, or doesn't get it at all.

The choice between TCP and UDP comes down to what your application can tolerate. Anything where every byte matters (a bank transfer, a chat message, a product purchase) uses TCP. Anything where freshness beats completeness (a video frame, a voice packet, a position update in a game) often uses UDP, because retransmitting a packet from 200 ms ago is worse than just skipping it and moving on.

The decision tree is small because the question is binary at this level. The actual choice rarely needs more nuance: if dropping data is fine, UDP is on the table; otherwise, use TCP. The next two lessons in this section (TCP Sockets and UDP Sockets) cover the C# APIs for each in detail.

The Client-Server Model

Most networked C# apps follow the client-server model. One side, the server, runs continuously, listens on a known address and port, and waits for incoming connections. The other side, the client, opens a connection when it has something to ask for, sends a request, waits for a response, and closes the connection (or reuses it). The server doesn't reach out first; it responds.

This shape shows up everywhere. A web browser is a client; the web server is, well, the server. A C# HttpClient is a client; an ASP.NET Core app is the server. A database driver in your code is a client; PostgreSQL is the server. The roles are about behavior, not about the kind of machine.

That five-step pattern is the heart of every client-server interaction. The transport (TCP here) and the message format (whatever sits on top) can vary, but the shape stays the same: the client initiates, the server responds, both sides agree on when to close.

A server typically handles many clients concurrently. The standard pattern is: one socket that listens for incoming connections, and a separate handler (a task, a thread, or an async continuation) for each accepted connection. C#'s async/await is a particularly good fit here because most of a connection's lifetime is spent waiting for the next byte from the client, and async releases the thread during the wait. We won't dig into the listener side here; lesson 04 (TCP Sockets) covers TcpListener in detail.

Some systems break this client-server symmetry, most notably peer-to-peer networks, where every node acts as both a client and a server, and modern WebSockets and gRPC streaming, which keep a single connection open and let either side push data at any time. But even those typically start with a client-side request that opens the connection.

HTTP on Top of TCP

The most common protocol on top of TCP is HTTP, the Hypertext Transfer Protocol. When you open a website, call a REST API, or invoke a webhook, you're almost certainly using HTTP. HTTPS is the same protocol with TLS encryption layered underneath, which is what the s stands for. HTTP/1.1, HTTP/2, and HTTP/3 are the three versions in use today; the application-level semantics are the same across them, with each new version mainly changing how bytes are framed on the wire for better performance.

At its core, HTTP is a request-response protocol. The client sends a request with a method (GET, POST, PUT, DELETE, and a few others), a path (/products/42), a set of headers (key-value metadata like Authorization: Bearer ...), and an optional body. The server sends back a response with a status code (200, 404, 500, and so on), a set of headers, and an optional body. That's the whole shape. Everything else (REST, GraphQL, webhooks, server-sent events) is convention on top of those primitives.

For an online store's catalog, a single product fetch looks roughly like this in spirit.

You almost never write HTTP bytes by hand in C#. The framework does it through HttpClient. HTTP is an application-level protocol that runs on top of TCP, and it's what almost every backend service speaks today.

A Light Tour of the Layers

Networking is usually described in terms of layers, where each layer handles a specific concern and uses the layer below it without caring how that layer works internally. Two layered models get cited often: the seven-layer OSI model (a teaching framework from the 1980s) and the simpler four-layer TCP/IP model that matches how the internet actually works.

For an application developer, the simplified version is enough.

LayerConcernExamplesWhere C# code typically lives
ApplicationThe protocol two apps speakHTTP, HTTPS, gRPC, WebSockets, SMTP, DNSHttpClient, ClientWebSocket, Grpc.Net.Client, SmtpClient
TransportGetting bytes between two programs reliably (or not)TCP, UDPTcpClient, TcpListener, UdpClient, Socket
InternetRouting packets between hosts across networksIP (v4 and v6)IPAddress, IPEndPoint, mostly hidden
LinkMoving bits over a physical or virtual linkEthernet, Wi-Fi, cellularNot exposed to C# at all in normal apps

The vast majority of C# networking code lives at the Application layer. A call to HttpClient.GetAsync makes the framework pick an HTTP version, open or reuse a TCP connection, perform the TLS handshake, send the request bytes, read the response, and return an HttpResponseMessage. Transport-layer work (TcpClient, UdpClient) fits a custom protocol or integration with a non-HTTP system. The Internet and Link layers are invisible to C# application code, which is the intended design.

Each layer wraps the data from the layer above with its own header before handing it down, and each layer strips its own header off on the way back up. By the time bytes leave your network card, an HTTP request body has been wrapped in HTTP headers, then in a TCP segment, then in an IP packet, then in an ethernet frame, and the receiving side undoes those wrappings in reverse on arrival. None of that is something you write code for; the OS and framework handle it.

The reason this layered view matters for a C# developer is debugging. When HttpClient.GetAsync throws, the error might come from any layer. A SocketException means the transport layer failed (no route to host, connection refused, timeout). An HttpRequestException wrapping a 404 means the application layer succeeded in talking to the server but the server returned an error. A AuthenticationException means the TLS handshake failed between the transport and application layers. Knowing which layer is complaining helps you find the fix faster.

What .NET Gives You for Networking

The Base Class Library ships with a layered set of networking namespaces, each one matching a different layer of the stack. You almost never pick "the networking API" in .NET; you pick the one that fits the protocol you're using.

Namespace / PackageWhat it coversWhen to use it
System.NetShared types: IPAddress, IPEndPoint, Dns, HttpStatusCode, cookies, credentialsEverywhere. These types appear across every networking API.
System.Net.HttpHttpClient and friends. The standard way to make HTTP requests.Calling REST APIs, fetching JSON, calling webhooks, anything HTTP.
System.Net.SocketsTcpClient, TcpListener, UdpClient, Socket. Direct transport-layer access.Custom protocols, integrating with non-HTTP servers, building servers from scratch.
System.Net.WebSocketsClientWebSocket and the server-side WebSocket plumbing.Persistent bidirectional connections: chat, live dashboards, multiplayer game state.
Grpc.Net.Client (NuGet)gRPC client API on top of HTTP/2.Strongly-typed service-to-service calls with schema-defined messages, common in microservices.

A short tour, one line each.

`System.Net` is the foundation. The types here (IPAddress, IPEndPoint, Dns, status code enums, credentials) show up as parameters and return values across every other networking API in the BCL.

`System.Net.Http` is the home of HttpClient. If your app talks to a REST API, calls a webhook, or fetches data from a URL, you almost certainly use this.

`System.Net.Sockets` is the transport-layer API. TcpClient and TcpListener wrap a Socket for TCP work, and UdpClient does the same for UDP. Drop down here for raw byte streams or non-HTTP protocols.

`System.Net.WebSockets` handles WebSockets, a protocol that starts as an HTTP request and upgrades to a persistent bidirectional channel. Either side can push messages without waiting for a request, which fits chat applications, live order-status updates, or any real-time push scenario.

`Grpc.Net.Client` is a NuGet package, not part of the BCL itself. gRPC is a service-to-service RPC framework built on HTTP/2 with schema-defined messages (Protocol Buffers). It's a common choice for backend-to-backend traffic in microservices because the message shapes are typed and the wire format is compact.

The decision tree for picking an API is straightforward.

The dominant case is "calling a REST API," and the answer there is HttpClient. Most app code never needs anything else. The other branches exist for narrower needs, and the lessons that follow walk through each one.

Two of these APIs are built on top of others. HttpClient ultimately uses TCP sockets through SocketsHttpHandler. Grpc.Net.Client uses HttpClient with HTTP/2, which uses TCP. ClientWebSocket starts as an HTTP request and upgrades into a persistent connection on the same TCP socket. The BCL really is layered: the higher-level APIs save the lower-level coding, but they're other APIs from the BCL composed together.

Using a lower-level API doesn't make code faster. It makes the caller responsible for more of the work. HttpClient already handles connection pooling, DNS caching, retries (if configured), TLS, and HTTP/2 multiplexing. Re-implementing all of that on TcpClient rarely pays off. Drop down to sockets only when the protocol isn't HTTP.

Making It Concrete

Two short examples tie the pieces together.

The first one resolves a hostname into addresses, then builds endpoints from them. This is the kind of code you'd write in a custom diagnostic tool or an integration test that wants to confirm a target service is reachable before it tries the real network call.

The IPv6 address is wrapped in square brackets in the endpoint format to keep the colons in the address separate from the colon before the port. That's the standard text representation and what every C# networking API both produces and accepts. The AddressFamily enum tells you which kind of address you got back, which matters when you're choosing a socket type later (AddressFamily.InterNetwork for IPv4, AddressFamily.InterNetworkV6 for IPv6).

The second example checks whether a single string is a valid IP address or a hostname, using IPAddress.TryParse. This is a common pattern when an app accepts either form from a configuration file or user input.

TryParse follows the standard TryParse pattern across the BCL: it returns true and fills the out parameter when the input is valid, false otherwise. No exception is thrown on bad input, which fits classification of untrusted strings. For inputs that come back as "not an IP address," the next step is usually a DNS lookup, which will either succeed (returning real addresses) or fail with a clear error.

IPAddress.Parse (without Try) does the same parsing but throws FormatException on bad input. Use TryParse when the input might be invalid, Parse when the input is known to be well-formed (such as a hard-coded address in a test).

IPAddress.TryParse does pure string parsing, no network calls. It's fast and safe to call on any string. Dns.GetHostAddressesAsync, by contrast, can take hundreds of milliseconds on a cold lookup. The "cheap parse first, network resolution second" pattern skips the network call when the input is already an address.

Exception Shapes

Networking code has its own family of exception types. Three of them cover most of what comes up early on.

  • SocketException is the transport-layer error: "Connection refused," "No such host is known," "Connection timed out." It has a SocketErrorCode property that maps to a SocketError enum (HostNotFound, ConnectionRefused, TimedOut, and so on). When you see a SocketException, the problem is somewhere between DNS and TCP.
  • HttpRequestException is the HttpClient wrapper. It can carry a SocketException as its InnerException (when the network failed), or it can carry an HTTP-specific failure (when the server returned a non-success status code and you called EnsureSuccessStatusCode).
  • OperationCanceledException is the cancellation case, raised when a CancellationToken you passed in gets cancelled, typically because of a timeout. Both HttpClient and the socket APIs surface cancellation this way.

The Exception Handling section covers the general try/catch mechanics; this section's later lessons cover the specific catch patterns each API needs. The one rule worth stating now: don't catch Exception broadly in networking code. Catch the specific type you can actually handle (SocketException, HttpRequestException, OperationCanceledException) and let anything else bubble up.