Skip to content

Commit 0e21251

Browse files
committed
Add TCP library code.
1 parent c95c334 commit 0e21251

File tree

9 files changed

+374
-3
lines changed

9 files changed

+374
-3
lines changed

LICENSE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
Copyright 2018 Joe Eli McIlvain
1+
Copyright 2021 Joe Eli McIlvain
22

33
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
44

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
A base repository for Savi language libraries, with common CI actions configured.
1+
# TCP
22

3-
See the [Guide](https://github.com/savi-lang/base-standard-library/wiki/Guide) for details on how it works and how to use it for your own libraries.
3+
TCP networking implementation for the Savi standard library.

manifest.savi

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
:manifest lib TCP
2+
:sources "src/*.savi"
3+
4+
:dependency ByteStream v0
5+
:from "github:savi-lang/ByteStream"
6+
7+
:dependency IO v0
8+
:from "github:savi-lang/IO"
9+
:depends on ByteStream
10+
:depends on OSError
11+
12+
:dependency OSError v0
13+
14+
:manifest bin "spec"
15+
:copies TCP
16+
:sources "spec/*.savi"
17+
18+
:dependency Spec v0
19+
:from "github:savi-lang/Spec"
20+
:depends on Map
21+
22+
:transitive dependency Map v0
23+
:from "github:savi-lang/Map"

spec/Main.savi

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
:actor Main
2+
:new (env)
3+
Spec.Process.run(env, [
4+
Spec.Run(TCP.Spec).new(env)
5+
])

spec/TCP.Spec.savi

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
:class iso TCP.Spec.Listener.Notify
2+
:is TCP.Listener.Notify
3+
:let env Env
4+
:new (@env)
5+
6+
:fun ref listening(listen TCP.Listener'ref)
7+
TCP.Spec.EchoClient.new(@env, Inspect[listen.local_port])
8+
@env.err.print("[Listener] Listening")
9+
10+
:fun ref not_listening(listen TCP.Listener'ref) None
11+
@env.err.print("[Listener] Not listening:")
12+
@env.err.print(listen.listen_error.name)
13+
14+
:fun ref closed(listen TCP.Listener'ref): None
15+
@env.err.print("[Listener] Stopped listening")
16+
17+
:fun ref connected!(listen TCP.Listener'ref, ticket TCP.Listener.AcceptTicket)
18+
TCP.Spec.Echoer.new(@env, listen, --ticket)
19+
20+
:actor TCP.Spec.Echoer
21+
:is IO.Actor(IO.Action)
22+
:let env Env
23+
:let io TCP.ConnectionEngine
24+
:new (@env, listen, ticket)
25+
@io = TCP.ConnectionEngine.accept(@, listen, --ticket)
26+
@env.err.print("[Echoer] Accepted")
27+
28+
:fun ref _io_react(action IO.Action)
29+
case action == (
30+
| IO.Action.Read |
31+
@io.pending_reads -> (bytes_available |
32+
@io.read_stream.advance_to_end
33+
bytes val = @io.read_stream.extract_token
34+
@env.err.print("[Echoer] Received:")
35+
@env.err.print(bytes.as_string)
36+
@io.write_stream << bytes.clone // TODO: is clone still needed?
37+
try @io.flush! // TODO: should we flush automatically on close below?
38+
@io.close
39+
)
40+
| IO.Action.Closed |
41+
@env.err.print("[Echoer] Closed")
42+
try @io.listen.as!(TCP.Listener).dispose
43+
)
44+
@
45+
46+
:actor TCP.Spec.EchoClient
47+
:is IO.Actor(IO.Action)
48+
:let env Env
49+
:let io TCP.ConnectionEngine
50+
:new (@env, service)
51+
@io = TCP.ConnectionEngine.connect(@, "localhost", service)
52+
53+
// TODO: Can we make this trigger _io_react with IO.Action.OpenFailed
54+
// automatically via the same mechanism we will use for queuing later
55+
// pending reads, instead of checking for this error case here?
56+
if (@io.connect_error != OSError.None) (
57+
@env.err.print("[EchoClient] Failed to connect:")
58+
@env.err.print(@io.connect_error.name)
59+
)
60+
61+
:fun ref _io_react(action IO.Action)
62+
case action == (
63+
| IO.Action.Opened |
64+
@env.err.print("[EchoClient] Connected")
65+
@io.write_stream << b"Hello, World!"
66+
try @io.flush!
67+
68+
| IO.Action.OpenFailed |
69+
@env.err.print("[EchoClient] Failed to connect:")
70+
@env.err.print(@io.connect_error.name)
71+
72+
| IO.Action.Read |
73+
@io.pending_reads -> (bytes_available |
74+
if (bytes_available >= b"Hello, World!".size) (
75+
@io.read_stream.advance_to_end
76+
@env.err.print("[EchoClient] Received:")
77+
@env.err.print(@io.read_stream.extract_token.as_string)
78+
@io.close
79+
)
80+
)
81+
82+
| IO.Action.Closed |
83+
@env.err.print("[EchoClient] Closed")
84+
try @io.listen.as!(TCP.Listener).dispose
85+
)
86+
@
87+
88+
:class TCP.Spec
89+
:is Spec
90+
:const describes: "TCP"
91+
92+
:it "can listen, connect, send, respond, disconnect, and stop listening"
93+
TCP.Listener.new(TCP.Spec.Listener.Notify.new(@env))

src/TCP.ConnectionEngine.savi

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
:class TCP.ConnectionEngine
2+
:is IO.Engine(IO.Action)
3+
:var io IO.CoreEngine
4+
:var listen (TCP.Listener | None): None
5+
:var connect_error OSError: OSError.None
6+
:let read_stream: ByteStream.Reader.new
7+
:let write_stream ByteStream.Writer
8+
9+
:fun non connect(
10+
// TODO: TCPConnectionAuth, rather than ambient authority.
11+
actor AsioEventNotify
12+
host String
13+
service String
14+
from String = ""
15+
)
16+
try (
17+
@_new_with_io(IO.CoreEngine.new_tcp_connect!(actor, host, service, from))
18+
|
19+
invalid = @_new_with_io(IO.CoreEngine.new)
20+
invalid.connect_error = OSError.EINVAL
21+
invalid
22+
)
23+
24+
:fun non accept(
25+
actor AsioEventNotify
26+
listen TCP.Listener
27+
ticket TCP.Listener.AcceptTicket
28+
)
29+
io = IO.CoreEngine.new_from_fd_rw(actor, ticket._fd)
30+
new = @_new_with_io(io)
31+
new.listen = listen
32+
new
33+
34+
:new _new_with_io(@io)
35+
@write_stream = ByteStream.Writer.new(@io)
36+
37+
:fun ref deferred_actions
38+
:yields IO.Action for None
39+
// TODO
40+
@
41+
42+
:fun ref react(event CPointer(AsioEvent), flags U32, arg U32) @
43+
:yields IO.Action
44+
@io.react(event, flags, arg) -> (action |
45+
case action == (
46+
| IO.Action.Closed |
47+
try @listen.as!(TCP.Listener)._conn_closed
48+
49+
// TODO: windows complete writes, flush-after-mute (pending writes logic from Pony)
50+
// | IO.Action.Write |
51+
// ...
52+
)
53+
yield action
54+
)
55+
@
56+
57+
:fun ref close
58+
@io.close
59+
@
60+
61+
:fun ref flush!
62+
@write_stream.flush!
63+
64+
:fun ref pending_reads
65+
:yields USize for None
66+
if Platform.windows (
67+
None // TODO: @_windows_complete_reads(arg)
68+
|
69+
@_pending_reads_unix -> (bytes_available | yield bytes_available)
70+
)
71+
@
72+
73+
:fun ref _pending_reads_unix None
74+
:yields USize for None
75+
while @io.is_readable (
76+
try (
77+
bytes_read = @read_stream.receive_from!(@io)
78+
if (bytes_read > 0) (yield @read_stream.bytes_ahead_of_marker)
79+
)
80+
)

src/TCP.Listener.savi

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
:trait TCP.Listener.Notify
2+
:fun ref listening(listen TCP.Listener'ref): None
3+
:fun ref not_listening(listen TCP.Listener'ref) None
4+
:fun ref closed(listen TCP.Listener'ref): None
5+
:fun ref connected!(
6+
listen TCP.Listener'ref
7+
ticket TCP.Listener.AcceptTicket
8+
) IO.Actor(IO.Action)
9+
10+
// TODO: Is there another way to protect the fd by making it non-forgeable,
11+
// while avoiding the overhead of an allocation and pointer indirection?
12+
:class iso TCP.Listener.AcceptTicket
13+
:var _fd U32
14+
:new iso _new(@_fd)
15+
16+
:actor TCP.Listener
17+
:let notify TCP.Listener.Notify
18+
:var listen_error OSError: OSError.None
19+
20+
:var _fd U32: -1
21+
:var _event CPointer(AsioEvent): CPointer(AsioEvent).null
22+
23+
:var _count USize: 0
24+
:var _limit USize
25+
:var _read_buffer_size USize
26+
:var _yield_after_reading USize
27+
:var _yield_after_writing USize
28+
29+
:var _closed Bool: False
30+
:var _paused Bool: False
31+
32+
:fun local_port: _NetAddress._for_fd(@_fd).port
33+
34+
:new (
35+
// TODO: TCP.Listener.Auth, rather than ambient authority.
36+
notify TCP.Listener.Notify'iso
37+
host String = ""
38+
service String = "0"
39+
@_limit = 0
40+
@_read_buffer_size = 16384
41+
@_yield_after_reading = 16384
42+
@_yield_after_writing = 16384
43+
)
44+
new_notify TCP.Listener.Notify'ref = --notify // TODO: should not be needed
45+
@notify = new_notify
46+
47+
event = _LibPonyOS.pony_os_listen_tcp(@, host.cstring, service.cstring)
48+
if event.is_not_null (
49+
@_event = event
50+
@_fd = AsioEvent.fd(@_event)
51+
error = _LibPonyOS.pony_os_errno
52+
new_notify.listening(@)
53+
|
54+
@listen_error = _LibPonyOS.pony_os_errno
55+
@_closed = True
56+
new_notify.not_listening(@)
57+
)
58+
59+
:: This is a special behaviour that hooks into the AsioEventNotify runtime,
60+
:: called whenever an event handle we're subscribed to receives an event.
61+
:be _event_notify(event CPointer(AsioEvent), flags U32, arg U32)
62+
if (@_event === event) (
63+
if AsioEvent.is_readable(flags) (
64+
@_accept(arg)
65+
)
66+
if AsioEvent.is_disposable(flags) (
67+
AsioEvent.destroy(@_event)
68+
@_event = CPointer(AsioEvent).null
69+
)
70+
)
71+
72+
:be _accept(ns U32 = 0)
73+
if Platform.windows (
74+
None // TODO
75+
|
76+
if @_closed.not (
77+
try (
78+
while (@_limit == 0 || @_count < @_limit) (
79+
conn_fd = _LibPonyOS.pony_os_accept(@_event)
80+
case conn_fd == (
81+
| 0 | error! // EWOULDBLOCK, don't try again
82+
| -1 | None // Some other error, so we can try again
83+
| @_spawn(conn_fd)
84+
)
85+
)
86+
@_paused = True
87+
)
88+
)
89+
)
90+
91+
:fun ref _spawn(fd U32)
92+
try (
93+
@notify.connected!(@, TCP.Listener.AcceptTicket._new(fd))
94+
@_count += 1
95+
|
96+
_LibPonyOS.pony_os_socket_close(fd)
97+
)
98+
99+
:be _conn_closed
100+
@_count -= 1
101+
102+
// If releasing this connection takes us below the limit,
103+
// unpause acceptance and try to accept more connections.
104+
if (@_paused && @_count < @_limit) (
105+
@_paused = False
106+
@_accept
107+
)
108+
109+
:be dispose: @close
110+
:fun ref close
111+
if (@_closed.not && @_event.is_not_null) (
112+
// When not on windows, unsubscribe immediately here instead of later.
113+
if Platform.windows.not AsioEvent.unsubscribe(@_event)
114+
115+
_LibPonyOS.pony_os_socket_close(@_fd)
116+
@_fd = -1
117+
118+
@notify.closed(@)
119+
)

src/_Lib.savi

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
:ffi _LibC
2+
:fun ntohs(network_short U16) U16
3+
:fun ntohl(network_long U32) U32
4+
5+
:ffi _LibPonyOS
6+
:fun pony_os_listen_tcp(owner AsioEventNotify, host CPointer(U8), service CPointer(U8)) CPointer(AsioEvent)
7+
:fun pony_os_accept(event CPointer(AsioEvent)) U32
8+
:fun pony_os_socket_close(fd U32) None
9+
:fun pony_os_errno OSError
10+
:fun pony_os_sockname(fd U32, net_addr _NetAddress'ref) None
11+
:fun pony_os_ipv4(net_addr _NetAddress'box) Bool
12+
:fun pony_os_ipv6(net_addr _NetAddress'box) Bool

src/_NetAddress.savi

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
2+
3+
:class val _NetAddress
4+
:is Equatable(_NetAddress)
5+
6+
:let _family U16: 0
7+
:let _port U16: 0 :: Port number in network byte order.
8+
:let _ipv4 U32: 0 :: Bits for an IPv4 address in network byte order.
9+
:let _ipv6a U32: 0 :: Bits 0-32 of an IPv6 address in network byte order.
10+
:let _ipv6b U32: 0 :: Bits 33-64 of an IPv6 address in network byte order.
11+
:let _ipv6c U32: 0 :: Bits 65-96 of an IPv6 address in network byte order.
12+
:let _ipv6d U32: 0 :: Bits 97-128 of an IPv6 address in network byte order.
13+
:let _scope U32: 0 :: IPv6 scope (unicast, anycast, multicast, etc...).
14+
15+
:new _for_fd(fd): _LibPonyOS.pony_os_sockname(fd, @)
16+
17+
:fun is_ipv4: _LibPonyOS.pony_os_ipv4(@)
18+
:fun is_ipv6: _LibPonyOS.pony_os_ipv6(@)
19+
20+
:fun port: _LibC.ntohs(@_port) // (converted to host byte order)
21+
:fun scope: _LibC.ntohl(@_scope) // (converted to host byte order)
22+
:fun ipv4_addr: _LibC.ntohl(@_ipv4) // (converted to host byte order)
23+
// TODO: ipv6_addr (needs tuple return value)
24+
// TODO: family (needs Platform.big_endian)
25+
26+
:fun "=="(other _NetAddress'box)
27+
@_family == other._family
28+
&& @_port == other._port
29+
&& (
30+
if @is_ipv4 (
31+
@_ipv4 == other._ipv4
32+
|
33+
@_ipv6a == other._ipv6a
34+
&& @_ipv6b == other._ipv6b
35+
&& @_ipv6c == other._ipv6c
36+
&& @_ipv6d == other._ipv6d
37+
)
38+
)
39+
&& @_scope == other._scope

0 commit comments

Comments
 (0)