What a Socket Is
A socket is a file descriptor with an address. That is almost the full definition. When two sockets have a connection, all the work uses the reader and writer interface from Readers and Writers. A file uses the same interface.
const std = @import("std");
pub fn main(init: std.process.Init) !void {
const io = init.io;
var buf: [512]u8 = undefined;
var file_writer = std.Io.File.stdout().writerStreaming(io, &buf);
const out = &file_writer.interface;
// An address is only data. The parse does not use the network and
// opens nothing. It fails only if the text is not an address.
const wanted = try std.Io.net.IpAddress.parse("127.0.0.1", 0);
try out.print("asked for port: {d}\n", .{wanted.getPort()});
// Port 0 tells the kernel to select a free port. The kernel gives the
// port when it binds the socket, and the socket records the port. A
// fixed port makes a test suite fail when two copies run at one time.
var server = try wanted.listen(io, .{});
defer server.deinit(io);
const bound = server.socket.address;
try out.print("kernel gave me: {s}\n", .{if (bound.getPort() == 0) "port 0" else "a real port"});
try out.print("still loopback: {}\n\n", .{bound.getPort() != wanted.getPort()});
// `.stream` is TCP: an ordered and reliable byte stream on a
// connection. `.dgram` is UDP, which has none of these three properties.
var client = try bound.connect(io, .{ .mode = .stream });
defer client.close(io);
var accepted = try server.accept(io);
defer accepted.close(io);
try out.writeAll("connected. two handles now refer to one conversation.\n\n");
// After this point, the code does not use sockets. The reader and
// writer interfaces that a file uses move the bytes. Thus the next
// protocol chapters can run without a network.
var write_buf: [64]u8 = undefined;
var writer = client.writer(io, &write_buf);
try writer.interface.writeAll("ping\n");
try writer.interface.flush();
var read_buf: [64]u8 = undefined;
var reader = accepted.reader(io, &read_buf);
const line = try reader.interface.takeDelimiterExclusive('\n');
try out.print("server read: \"{s}\"\n", .{line});
// No other code closes a socket for you. You must close each socket.
// Each socket is a file descriptor. If a server does not close its
// sockets, it cannot accept connections at the process limit. This
// occurs a long time before the server has no more memory.
try out.writeAll("closing: the defers above release three descriptors\n");
try out.flush();
}Run it yourself:zig run what-is-a-socket.zig
An address is data
IpAddress.parse does not use the network. It opens nothing and sends
nothing. It fails only if the text is not an address. No network action
occurs until you call listen, bind or connect.
const wanted = try std.Io.net.IpAddress.parse("127.0.0.1", 0);
Port 0 tells the kernel to select a port
If you ask for port 0, the kernel gives the socket a free port when it binds
the socket. The socket records the port in server.socket.address.
A test that uses a fixed port such as 8080 can fail in three conditions:
- Two copies of the test run at the same time.
- The server from the last run is still active.
- A different program on the computer already uses port 8080.
Each snippet in this section binds port 0 and reads the port that it gets. Thus all of the snippets can run on the same CI computer at the same time.
Stream or datagram
var client = try bound.connect(io, .{ .mode = .stream });
.stream is TCP. TCP keeps the bytes in order, is reliable, and uses a
connection. .dgram is UDP,
and UDP has none of these three properties.
UDP Datagrams, at the end of this section,
gives more information about UDP. You select the mode one time, on the socket.
Each difference between the two protocols comes from that mode.
After the connection, the code does not use sockets
var writer = client.writer(io, &write_buf);
try writer.interface.writeAll("ping\n");
try writer.interface.flush();
When the connection exists, no part of the code is specific to networks.
Thus five of the chapters between this chapter and the UDP chapter run in your
browser, without sockets. A protocol parser that uses std.Io.Reader works
on a socket, on a file and on a byte array in a test. You write the parser
one time only.
You must close each socket
Each socket is a file descriptor, and each process has a limit on file
descriptors. If a server does not close one socket for each connection, the
server cannot accept new connections when it reaches that limit. This occurs a long time before the server has no
more memory. The symptom is an error from accept, and the error does not
tell you about the leak. Put a defer immediately after the line that makes
the socket. Then the program always closes the socket.
Next: A TCP Round Trip. That chapter adds an accept loop to this program.