Network programming
In short: Network programming is two programs talking over the network: the server waits on an IP address and port, the client connects, and sockets send the data over TCP or UDP.
Problem
The checkout and the warehouse
The checkout in the shop has to ask the warehouse how much coffee is left, and the warehouse program runs on another computer. A file does not help: the two programs cannot see each other's disks. One of them has to ask and the other one has to answer, right away, over the network.
That is how almost everything around you works: the browser asks the server for a page, the bank app asks the bank for your balance, a game sends your moves to the other players.
The idea
Address, port and two ways to send
For data to reach a program on another computer, you need two things:
- An IP address: the number of the computer on the network, for example
192.168.1.20or142.250.185.78. Your own computer is always127.0.0.1, with the namelocalhost. - A port: a number from 0 to 65,535 for the program on that computer. The address is the building, the port is the apartment. HTTPS uses 443, and we will use 5601 and up.
The program that waits for someone to reach it is the server. The one that does the reaching is the client. How the data travels is decided by the protocol:
| TCP | UDP | |
|---|---|---|
| Connection | they connect first, then they talk | no connection, every packet is a separate letter |
| Delivery | everything arrives, in the same order, or you find out it did not | a packet can get lost or come in a different order |
| Speed | slower | faster |
| Used for | the web, email, payments, files | live voice and video, games, sensors |
System.Net.Sockets.More precisely, if you are curious
- The network is described in layers. The OSI model has seven: physical (bits over the cable or the air), data link (the connection to the next device), network (IP addresses and routes), transport (TCP, UDP), session, presentation and application (HTTP, email). In practice the internet uses the simpler TCP/IP model with four layers. Your program works on the top one: .NET and the operating system take care of everything below.
- IP delivers separate packets to an address, but it does not promise that they will arrive. TCP is built on top of IP and numbers the packets, so it can put them in order and ask again for the lost ones.
- Protocols you will come across: HTTP and HTTPS (the web; HTTPS is HTTP encrypted with TLS, whose older name is SSL), WebSocket (a two-way connection for chat and notifications in the browser), FTP (files), SSH (secure access to a remote computer).
See it
The server
The warehouse server gets the name of a product and answers with its price:
static readonly Dictionary<string, double> Prices = new Dictionary<string, double> { ["coffee"] = 7.50, ["tea"] = 3.20 };
static void Serve(TcpListener server){ try { using (TcpClient client = server.AcceptTcpClient()) using (StreamReader reader = new StreamReader(client.GetStream())) using (StreamWriter writer = new StreamWriter(client.GetStream()) { AutoFlush = true }) { string product = reader.ReadLine(); Console.WriteLine("Server received: " + product); writer.WriteLine(Prices.TryGetValue(product, out double price) ? product + ": €" + price : "no such product"); } } catch (IOException e) { Console.WriteLine("Server: " + e.Message); }}TcpListenerlistens on a port.server.AcceptTcpClient()waits until a client connects and returns aTcpClientfor the connection with it: the same type the client uses to connect.client.GetStream()gives you the stream of the connection: bytes flow over it in both directions. We wrap it in two classes:StreamReaderreads text line by line withReadLine(), andStreamWriterwrites withWriteLine(). The same classes read and write files too.AutoFlush = truemeans "send after everyWriteLine". You will see why that matters.using (…)closes the connection and the streams at the end of the block, even if an exception happens in the middle: as if afinallycalledDispose(). Severalusinglines one under another share one block and are closed in reverse order.TryGetValuelooks up the product and gives you the price throughout.
See it
The client
new TcpClient(address, port) and uses the same StreamReader and StreamWriter. Here the server and the client are in one program: the server runs in its own thread, and Main is the client. That way you can try them without a second computer:TcpListener server = new TcpListener(IPAddress.Loopback, 5601);server.Start();new Thread(() => Serve(server)).Start();
using (TcpClient client = new TcpClient("localhost", 5601))using (StreamReader reader = new StreamReader(client.GetStream()))using (StreamWriter writer = new StreamWriter(client.GetStream()) { AutoFlush = true }){ writer.WriteLine("coffee"); Console.WriteLine("Client received: " + reader.ReadLine());}server.Stop();Server received: coffeeClient received: coffee: €7.5new TcpListener(IPAddress.Loopback, 5601) does not open the port yet. server.Start() opens it, in Main, before the thread starts. That way, when the client connects, the server is already listening. IPAddress.Loopback is 127.0.0.1: the server accepts only programs from the same computer.The two sides take turns: the client writes a line and waits for an answer with ReadLine(), and the server reads the line and answers. If they swap places and both wait to read, they will wait forever.
TcpListener server = new TcpListener(IPAddress.Loopback, 5601);server.Start();new Thread(() => Serve(server)).Start();
using (TcpClient client = new TcpClient("localhost", 5601))using (StreamReader reader = new StreamReader(client.GetStream()))using (StreamWriter writer = new StreamWriter(client.GetStream()) { AutoFlush = true }){ writer.WriteLine("coffee"); Console.WriteLine("Client received: " + reader.ReadLine());}server.Stop();Step by step, in words
- The server opens port 5601 and starts listening.
- In its own thread, the server calls AcceptTcpClient() and waits for a client.
- The client connects. AcceptTcpClient() returns a TcpClient, and each side has a writer for writing and a reader for reading.
- The client writes a line: "coffee". It travels over the connection to the server.
- The client waits in ReadLine(). The server reads the line and prints it.
- The server finds the price and writes back: "coffee: €7.5".
- The client's ReadLine() returns the answer, and the client prints it.
- using closes the connection, and server.Stop() closes the port.
More precisely, if you are curious
- The whole program, with the
imports:
using System.Net;using System.Net.Sockets;
class Program{ static readonly Dictionary<string, double> Prices = new Dictionary<string, double> { ["coffee"] = 7.50, ["tea"] = 3.20 };
static void Serve(TcpListener server) { try { using (TcpClient client = server.AcceptTcpClient()) using (StreamReader reader = new StreamReader(client.GetStream())) using (StreamWriter writer = new StreamWriter(client.GetStream()) { AutoFlush = true }) { string product = reader.ReadLine(); Console.WriteLine("Server received: " + product); writer.WriteLine(Prices.TryGetValue(product, out double price) ? product + ": €" + price : "no such product"); } } catch (IOException e) { Console.WriteLine("Server: " + e.Message); } }
static void Main() { TcpListener server = new TcpListener(IPAddress.Loopback, 5601); server.Start(); new Thread(() => Serve(server)).Start();
using (TcpClient client = new TcpClient("localhost", 5601)) using (StreamReader reader = new StreamReader(client.GetStream())) using (StreamWriter writer = new StreamWriter(client.GetStream()) { AutoFlush = true }) { writer.WriteLine("coffee"); Console.WriteLine("Client received: " + reader.ReadLine()); } server.Stop(); }}- In two separate files and two terminals it is the same: you start the server first, then the client. If there is no server on the port,
new TcpClientthrows aSocketException(Connection refused). If the port is already taken by another program,server.Start()throws aSocketException(Address already in use). In .NET aSocketExceptionis not anIOException, socatch (IOException)does not catch it. - Text travels over the network as bytes.
StreamReaderandStreamWriteruse UTF-8 by default, so non-ASCII characters such as € arrive intact. usingalso has a short form, with no parentheses and no block:using TcpClient client = new TcpClient("localhost", 5601);closes the connection at the end of the block that the line is in.
Try it
Wait for a client
Which method waits until a client connects?
TcpListener server = new TcpListener(IPAddress.Loopback, 5601);
server.Start();
TcpClient client = server.();AcceptTcpClient() blocks until someone connects, and returns a TcpClient for the connection with them. The server can call it many times, for many clients.The idea
Many clients at once
AcceptTcpClient() is not called and the others wait. So a real server runs an endless loop: it accepts a client, gives it to a new thread and goes right back to AcceptTcpClient():static void AcceptAll(TcpListener server){ int number = 0; while (true) { try { TcpClient client = server.AcceptTcpClient(); number++; int n = number; new Thread(() => Handle(client, n)).Start(); } catch (SocketException) { return; } }}
static void Handle(TcpClient client, int number){ try { using (client) using (StreamReader reader = new StreamReader(client.GetStream())) using (StreamWriter writer = new StreamWriter(client.GetStream()) { AutoFlush = true }) { string line; while ((line = reader.ReadLine()) != null) { Console.WriteLine("Client " + number + ": " + line); writer.WriteLine("Echo: " + line); } } } catch (IOException e) { Console.WriteLine("Client " + number + " disconnected: " + e.Message); }}Handle answers every line until the client closes the connection: then ReadLine() returns null. Two clients, one after the other:TcpListener server = new TcpListener(IPAddress.Loopback, 5602);server.Start();Thread acceptor = new Thread(() => AcceptAll(server));acceptor.IsBackground = true;acceptor.Start();
foreach (string order in new string[] { "table 4: two coffees", "table 7: the check" }){ using (TcpClient client = new TcpClient("localhost", 5602)) using (StreamReader reader = new StreamReader(client.GetStream())) using (StreamWriter writer = new StreamWriter(client.GetStream()) { AutoFlush = true }) { writer.WriteLine(order); Console.WriteLine(reader.ReadLine()); }}server.Stop();Client 1: table 4: two coffeesEcho: table 4: two coffeesClient 2: table 7: the checkEcho: table 7: the checkMore precisely, if you are curious
IsBackground = truemakes the thread a "background" thread: the program ends whenMainends, without waiting for the endless loop. A thread fromnew Threadis not a background thread by default. A real server does not make it a background thread: it has to run until you stop it.server.Stop()closes the port. The waitingAcceptTcpClient()throws aSocketException, and the loop ends withreturn.using (client)puts theTcpClientthat already exists into ausing, so it is closed at the end.int n = number;is needed: the lambda does not remember the value ofnumber, it remembers the variable itself. Without the copy, the first client's thread could readnumberafter the loop has already increased it for the second one.- With thousands of clients, a thread for each one is expensive. Then the accept loop gives the clients to the thread pool from the previous lesson:
Task.Run(() => Handle(client, n)).
Try it
Both of them wait
The client sends "coffee" and waits for an answer. The server is the same, but the program hangs and prints nothing. Which line is the bug on?
Pick the line with the bug.
StreamWriter without AutoFlush = true does not send right away: it keeps the text in a buffer until more of it builds up. WriteLine("coffee") stays in the client's memory, the server waits for a line, and the client waits for an answer. The fix is new StreamWriter(client.GetStream()) { AutoFlush = true } or writer.Flush() after WriteLine.The idea
UDP: letters without a connection
UdpClient:UdpClient receiver = new UdpClient(new IPEndPoint(IPAddress.Loopback, 5603));
using (UdpClient sensor = new UdpClient()){ foreach (string reading in new string[] { "kitchen 21.5", "storeroom 4.0", "fridge 3.2" }) { byte[] data = Encoding.UTF8.GetBytes(reading); sensor.Send(data, data.Length, "localhost", 5603); }}
IPEndPoint sender = new IPEndPoint(IPAddress.Any, 0);for (int i = 0; i < 3; i++){ byte[] data = receiver.Receive(ref sender); string text = Encoding.UTF8.GetString(data); Console.WriteLine("Temperature: " + text + " °C");}receiver.Close();Temperature: kitchen 21.5 °CTemperature: storeroom 4.0 °CTemperature: fridge 3.2 °C- The receiver listens on port 5603:
new UdpClient(new IPEndPoint(IPAddress.Loopback, 5603)). AnIPEndPointis an address plus a port. The sender isnew UdpClient()without a port: the system gives it a free one. - Bytes travel over the network.
Encoding.UTF8.GetBytesturns the text into bytes, andEncoding.UTF8.GetStringturns them back (Encodingis inSystem.Text). Send(bytes, count, address, port)sends one packet.Receive(ref sender)waits for a packet, returns its bytes and writes intosenderwhere it came from: that is why it isref.- Nobody confirms that the packet arrived. On
localhostthey do not get lost, but on a real network they can.
More precisely, if you are curious
If the receiver has to answer, it sends a new packet to the address that Receive wrote into sender: receiver.Send(answer, answer.Length, sender).
A packet has a size limit (under 64 KB, and over the internet it is sensible to stay at about 1,400 bytes), so UDP is no good for files.
The idea
HTTP: the language of the web
When you open a page, the browser makes a TCP connection to the server and sends a text HTTP request: "give me /". The server sends back a response: a status, headers and a body (HTML, JSON, an image). In C# this is done by HttpClient, but it works with async and await, which come in the "Web and desktop" module. Here the request is written by hand and sent over a TcpClient. That way you can see that HTTP is just text over a TCP connection:
using System.Net.Sockets;
using (TcpClient client = new TcpClient("example.com", 80))using (StreamReader reader = new StreamReader(client.GetStream()))using (StreamWriter writer = new StreamWriter(client.GetStream()) { AutoFlush = true }){ writer.Write("GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n");
string status = reader.ReadLine(); Console.WriteLine("Status: " + status.Split(' ')[1]); string line; while ((line = reader.ReadLine()) != "") { if (line.StartsWith("Content-Type: ")) { Console.WriteLine("Type: " + line.Substring(14)); } } string body = reader.ReadToEnd(); int start = body.IndexOf("<title>") + 7; Console.WriteLine("Title: " + body.Substring(start, body.IndexOf("</title>") - start));}Status: 200Type: text/html; charset=utf-8Title: Example Domain- The request is a few lines, and each one ends with
\r\n:GET / HTTP/1.1("give me/"),Host:(which site) andConnection: close("close after the response"). The empty line at the end says the request is over. - The response starts with a line like
HTTP/1.1 200 OK. Its second part is the status. The headers (Content-Type: …) follow until an empty line, and then comes the body.ReadToEnd()reads it until the server closes the connection. - The status tells you how it went:
200is success,404means there is no such page,500is an error on the server. - Port 80 is HTTP without encryption. Most sites today want HTTPS on port 443, where the text travels encrypted (TLS).
HttpClientdoes that for you.
The site's code check runs without internet, so this program is shown with the output from a run with a network. Without a network, new TcpClient throws a SocketException. In the "Web and desktop" module the request and the response are taken apart line by line, and then HttpClient does all of this for you.
More precisely, if you are curious
HttpListener). That way you can try the same request without internet, on localhost:static void Answer(HttpListener server){ try { while (true) { HttpListenerContext context = server.GetContext(); bool found = context.Request.Url.AbsolutePath == "/stock"; byte[] answer = Encoding.UTF8.GetBytes(found ? "coffee: 12 in stock" : "no such address"); context.Response.StatusCode = found ? 200 : 404; context.Response.ContentLength64 = answer.Length; context.Response.OutputStream.Write(answer, 0, answer.Length); context.Response.Close(); } } catch (HttpListenerException) { return; }}
HttpListener server = new HttpListener();server.Prefixes.Add("http://localhost:5604/");server.Start();Thread answering = new Thread(() => Answer(server));answering.IsBackground = true;answering.Start();
foreach (string path in new string[] { "/stock", "/prices" }){ using (TcpClient client = new TcpClient("localhost", 5604)) using (StreamReader reader = new StreamReader(client.GetStream())) using (StreamWriter writer = new StreamWriter(client.GetStream()) { AutoFlush = true }) { writer.Write("GET " + path + " HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n"); string status = reader.ReadLine().Split(' ')[1]; while (reader.ReadLine() != "") { // the response headers are not needed here } Console.WriteLine(path + " -> " + status + " " + reader.ReadToEnd()); }}server.Stop();/stock -> 200 coffee: 12 in stock/prices -> 404 no such addressGetContext() waits for a request, the way AcceptTcpClient() waits for a client. For /prices the server sets the status 404 itself. ContentLength64 says how many bytes the body has, so the client knows where it ends. server.Stop() interrupts the waiting GetContext() with an HttpListenerException. In real projects, web servers are written with ASP.NET Core, in the "Web and desktop" module. It is built on the same sockets and threads.Exercise
Servers on localhost
1. Echo server. The server sends back every line with "Echo: " in front, until it gets "end". Then it answers "Goodbye" and closes the connection. The client sends "hello", "how are you" and "end", and prints the answers:
Echo: helloEcho: how are youGoodbye2. A server with commands. UPCASE text returns the text in uppercase, TIME returns the current time as 14:05 (DateTime.Now.ToString("HH:mm")), and anything else returns "Unknown command: " and the command. The time changes, so the client checks only its shape with TimeOnly.TryParseExact(time, "HH:mm", out _):
ORDER 42 IS READYThe time looks like HH:MM: TrueUnknown command: HOURBoris received: Hi, Boris! Are you free at 6 PM?Ana received: Yes, I will be there.Boris received: Great, see you soon.Ana received: Bye!Test
Check yourself
Related lessons
Section titled "Related lessons"- Builds on: Lists · Dictionary and HashSet · Lambdas and delegates · Threads
- Continues in: Untrusted input · Sessions, cookies and CSRF · JWT and its traps · API design: contract, versions, retried requests
- Tasks on this topic: Real-world programs