Skip to content

Conversation

@andrewrk
Copy link
Member

So far this is DNS resolution stuff.

@andrewrk andrewrk mentioned this pull request Oct 27, 2019

pub const IPPORT_RESERVED = 1024;

pub const IPPROTO_IP = 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already have these as the misnamed PROTO_ constants see

pub const PROTO_ip = 0o000;

pub const EAI_SYSTEM = -11;
pub const EAI_OVERFLOW = -12;

pub const EAI_NODATA = -5;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These (and AI_ and NI_) are libc constants and don't belong under os/bits

}
}

const main_thread_tls_align = 32;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change should probably go straight to master and not in this PR?

Copy link
Member Author

@andrewrk andrewrk Oct 27, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, sure, I'll cherry pick it out and rebase before merging.

 * delete the std/event/net directory
 * `std.event.Loop.waitUntilFdReadable` and related functions
   no longer have possibility of failure. On Linux, they fall
   back to poll() and then fall back to sleep().
 * add some missing `noasync` decorations in `std.event.Loop`
 * redo the `std.net.Server` API. it's quite nice now, but
   shutdown does not work cleanly. There is a race condition with
   close() that I am actively working on.
 * move `std.io.OutStream` to its own file to match `std.io.InStream`.
   I started working on making `write` integrated with evented I/O,
   but it got tricky so I backed off and filed #3557. However
   I did integrate `std.os.writev` and `std.os.pwritev` with evented I/O.
 * add `std.Target.stack_align`
 * move networking tests to `lib/std/net/test.zig`
 * add `std.net.tcpConnectToHost` and `std.net.tcpConnectToAddress`.
 * rename `error.UnknownName` to `error.UnknownHostName` within the
   context of DNS resolution.
 * add `std.os.readv`, which is integrated with evented I/O.
 * `std.os.preadv`, is now integrated with evented I/O.
 * `std.os.accept4` now asserts that ENOTSOCK and EOPNOTSUPP never
    occur (misuse of API), instead of returning errors.
 * `std.os.connect` is now integrated with evented I/O.
   `std.os.connect_async` is gone. Just use `std.os.connect`.
 * fix false positive dependency loop regarding async function frames
 * add more compile notes to help when dependency loops occur
   in determining whether a function is async.
 * ir: change an assert to ir_assert to make it easier to find
   workarounds for when such an assert is triggered. In this case
   it was trying to parse an IPv4 address at comptime.
@andrewrk
Copy link
Member Author

Some notes on the progress, I refreshed the "listen on a port, send bytes, receive bytes" test.

example std.net.TcpServer API usage

(this example only works in evented I/O mode):

zig/lib/std/net/test.zig

Lines 45 to 71 in b3c8041

var server = net.TcpServer.init(net.TcpServer.Options{});
defer server.deinit();
try server.listen(localhost);
var server_frame = async testServer(&server);
var client_frame = async testClient(server.listen_address);
try await server_frame;
try await client_frame;
}
fn testClient(addr: net.Address) anyerror!void {
const socket_file = try net.tcpConnectToAddress(addr);
defer socket_file.close();
var buf: [100]u8 = undefined;
const len = try socket_file.read(&buf);
const msg = buf[0..len];
testing.expect(mem.eql(u8, msg, "hello from server\n"));
}
fn testServer(server: *net.TcpServer) anyerror!void {
var client_file = try server.accept();
const stream = &client_file.outStream().stream;
try stream.print("hello from server\n");
}

std.net.TcpServer implementation

(impl doesn't care if evented or blocking I/O, and notice there are no async or await keywords anywhere):

zig/lib/std/net.zig

Lines 1255 to 1348 in 1639724

pub const TcpServer = struct {
/// Copied from `Options` on `init`.
kernel_backlog: u32,
/// `undefined` until `listen` returns successfully.
listen_address: Address,
sockfd: ?os.fd_t,
pub const Options = struct {
/// How many connections the kernel will accept on the application's behalf.
/// If more than this many connections pool in the kernel, clients will start
/// seeing "Connection refused".
kernel_backlog: u32 = 128,
};
/// After this call succeeds, resources have been acquired and must
/// be released with `deinit`.
pub fn init(options: Options) TcpServer {
return TcpServer{
.sockfd = null,
.kernel_backlog = options.kernel_backlog,
.listen_address = undefined,
};
}
/// Release all resources. The `TcpServer` memory becomes `undefined`.
pub fn deinit(self: *TcpServer) void {
self.close();
self.* = undefined;
}
pub fn listen(self: *TcpServer, address: Address) !void {
const nonblock = if (std.io.is_async) os.SOCK_NONBLOCK else 0;
const sock_flags = os.SOCK_STREAM | os.SOCK_CLOEXEC | nonblock;
const sockfd = try os.socket(os.AF_INET, sock_flags, os.PROTO_tcp);
self.sockfd = sockfd;
errdefer {
os.close(sockfd);
self.sockfd = null;
}
var socklen = address.getOsSockLen();
try os.bind(sockfd, &address.os_addr, socklen);
try os.listen(sockfd, self.kernel_backlog);
try os.getsockname(sockfd, &self.listen_address.os_addr, &socklen);
}
/// Stop listening. It is still necessary to call `deinit` after stopping listening.
/// Calling `deinit` will automatically call `close`. It is safe to call `close` when
/// not listening.
pub fn close(self: *TcpServer) void {
if (self.sockfd) |fd| {
os.close(fd);
self.sockfd = null;
self.listen_address = undefined;
}
}
pub const AcceptError = error{
ConnectionAborted,
/// The per-process limit on the number of open file descriptors has been reached.
ProcessFdQuotaExceeded,
/// The system-wide limit on the total number of open files has been reached.
SystemFdQuotaExceeded,
/// Not enough free memory. This often means that the memory allocation is limited
/// by the socket buffer limits, not by the system memory.
SystemResources,
ProtocolFailure,
/// Firewall rules forbid connection.
BlockedByFirewall,
} || os.UnexpectedError;
/// If this function succeeds, the returned `fs.File` is a caller-managed resource.
pub fn accept(self: *TcpServer) AcceptError!fs.File {
const nonblock = if (std.io.is_async) os.SOCK_NONBLOCK else 0;
const accept_flags = nonblock | os.SOCK_CLOEXEC;
var accepted_addr: Address = undefined;
var adr_len: os.socklen_t = @sizeOf(os.sockaddr);
if (os.accept4(self.sockfd.?, &accepted_addr.os_addr, &adr_len, accept_flags)) |fd| {
return fs.File.openHandle(fd);
} else |err| switch (err) {
// We only give SOCK_NONBLOCK when I/O mode is async, in which case this error
// is handled by os.accept4.
error.WouldBlock => unreachable,
else => |e| return e,
}
}
};

strace

execve("./test", ["./test"], 0x7ffe7d999e00 /* 127 vars */) = 0
rt_sigaction(SIGSEGV, {sa_handler=0x25ba30, sa_mask=[], sa_flags=SA_RESTORER|SA_RESTART|SA_RESETHAND|SA_SIGINFO, sa_restorer=0x2075b0}, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f6f93073000
eventfd2(1, EFD_CLOEXEC|EFD_NONBLOCK)   = 3
epoll_create1(EPOLL_CLOEXEC)            = 4
eventfd2(0, EFD_CLOEXEC|EFD_NONBLOCK)   = 5
epoll_ctl(4, EPOLL_CTL_ADD, 5, {EPOLLIN, {u32=2531936, u64=2531936}}) = 0
mmap(NULL, 16785408, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f6f92071000
mprotect(0x7f6f92071000, 16785408, PROT_READ|PROT_WRITE) = 0
clone(child_stack=0x7f6f93071ff8, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID|0x400000, parent_tid=[28275], child_tidptr=0x7f6f93072000) = 28275
socket(AF_INET, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_TCP) = 6
bind(6, {sa_family=AF_INET, sin_port=htons(0), sin_addr=inet_addr("127.0.0.1")}, 16) = 0
listen(6, 128)                          = 0
getsockname(6, {sa_family=AF_INET, sin_port=htons(53527), sin_addr=inet_addr("127.0.0.1")}, [16]) = 0
accept4(6, 0x7ffe607c7ad0, [112], SOCK_CLOEXEC|SOCK_NONBLOCK) = -1 EAGAIN (Resource temporarily unavailable)
epoll_ctl(4, EPOLL_CTL_ADD, 6, {EPOLLIN|EPOLLONESHOT|EPOLLET, {u32=1618771536, u64=140730517192272}}) = 0
socket(AF_INET, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_TCP) = 7
connect(7, {sa_family=AF_INET, sin_port=htons(53527), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)
epoll_ctl(4, EPOLL_CTL_ADD, 7, {EPOLLIN|EPOLLOUT|EPOLLONESHOT|EPOLLET, {u32=1618773776, u64=140730517194512}}) = 0
epoll_pwait(4, [{EPOLLIN, {u32=1618771536, u64=140730517192272}}], 1, -1, NULL, 128) = 1
epoll_ctl(4, EPOLL_CTL_DEL, 6, NULL)    = 0
accept4(6, {sa_family=AF_INET, sin_port=htons(52202), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_CLOEXEC|SOCK_NONBLOCK) = 8
write(8, "hello from server\n", 18)     = 18
epoll_pwait(4, [{EPOLLIN|EPOLLOUT, {u32=1618773776, u64=140730517194512}}], 1, -1, NULL, 128) = 1
epoll_ctl(4, EPOLL_CTL_DEL, 7, NULL)    = 0
futex(0x26a1d8, FUTEX_WAKE, 1)          = 1
write(5, "\1\1\1\1\1\1\1\1", 8)         = 8
getsockopt(7, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
read(7, "hello from server\n", 100)     = 18
close(7)                                = 0
close(6)                                = 0
epoll_pwait(4, [{EPOLLIN, {u32=2531936, u64=2531936}}], 1, -1, NULL, 128) = 1
munmap(0x7f6f92071000, 16785408)        = 0
close(5)                                = 0
close(3)                                = 0
close(4)                                = 0
munmap(0x7f6f93073000, 32)              = 0
exit_group(0)                           = ?
+++ exited with 0 +++

next steps

I'm planning to get as far as ability to download a file to disk over a http 1.0 connection, before trying to start merging the branch. Other POSIX's I expect to be relatively easy, however I know next to nothing about Windows networking and would certainly appreciate help with that. I'll probably be looking at libuv for inspiration.

} || os.UnexpectedError;

/// If this function succeeds, the returned `fs.File` is a caller-managed resource.
pub fn accept(self: *TcpServer) AcceptError!fs.File {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's handy to know the IP address of the accepted socket for a TCP server. Not sure if you already plan to change this return type to include that information or not.

@euantorano
Copy link
Contributor

I noticed some changes to IP address parsing in this PR, and thought I'd add some thoughts in case you plan to make more changes here.

It's worth noting all of the following are valid IPv6 addresses that I don't believe will parse as it stands due to the check here:

  • ::1
  • :: (all segments are 0)
  • 2001:db8:: (last 6 segments are 0)
  • ::1234:5678 (first 6 segments are 0)
  • 2001:db8::1234:5678 (middle 4 segments are 0)

I also don't know if you intend to support IPv4 mapped or IPv4 compatible IPv6 addresses, but it would be nice to do so as they're quite common:

  • ::ffff:192.0.2.128 (IPv4 mapped IPv6 address)
  • ::192.0.2.128 (IPv4 comaptible - though this format is deprecated)

 * Delete `std.net.TmpWinAddr`. I don't think that was ever meant to
   be a real thing.
 * Delete `std.net.OsAddress`. This abstraction was not helpful.
 * Rename `std.net.Address` to `std.net.IpAddress`. It is now an extern
   union of IPv4 and IPv6 addresses.
 * Move `std.net.parseIp4` and `std.net.parseIp6` to the
   `std.net.IpAddress` namespace. They now return `IpAddress` instead of
   `u32` and `std.net.Ip6Addr`, which is deleted.
 * Add `std.net.IpAddress.parse` which accepts a port and parses either
   an IPv4 or IPv6 address.
 * Add `std.net.IpAddress.parseExpectingFamily` which additionally
   accepts a `family` parameter.
 * `std.net.IpAddress.initIp4` and `std.net.IpAddress.initIp6` are
   improved to directly take the address fields instead of a weird
   in-between type.
 * `std.net.IpAddress.port` is renamed to `std.net.IpAddress.getPort`.
 * Added `std.net.IpAddress.setPort`.
 * `os.sockaddr` struct on all targets is improved to match the
   corresponding system struct. Previously I had made it a union of
   sockaddr_in, sockaddr_in6, and sockaddr_un. The new abstraction for
   this is now `std.net.IpAddress`.
 * `os.sockaddr` and related bits are added for Windows.
 * `os.sockaddr` and related bits now have the `zero` fields default
   to zero initialization, and `len` fields default to the correct size.
   This is enough to abstract the differences across targets, and so
   no more switch on the target OS is needed in `std.net.IpAddress`.
 * Add the missing `os.sockaddr_un` on FreeBSD and NetBSD.
 * `std.net.IpAddress.initPosix` now takes a pointer to `os.sockaddr`.
@andrewrk andrewrk marked this pull request as ready for review October 31, 2019 00:25
@andrewrk andrewrk merged commit 7b7ba51 into master Oct 31, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants