Closed kylegoetz closed 6 months ago
Awesome, thanks for tracking here
After struggling mightily with why I couldn't get the UDP library's accept
to destructure the Socket
from ListenSocket
work by hooking into Network.Socket.accept
(the built-in UDP lib's accept
has as one of its args a given ClientSockAddr
which is an impossibility for listening for any client connection to accept, I discovered this talking about the same error I was getting in the Haskell:
You are using a udp socket, SOCK_DGRAM, and udp does not listen for connections, it receives each message on its own Use recvfrom to receive udp messages
recvFrom :: ListenSocket -> IO (ByteString, ClientSockAddr)
works, and I suppose then accept :: ListenSocket -> Socket
only exists to prepare a UDP server to also be a client that can send via sendTo :: ListenSocket -> Bytes -> ClientSockAddr -> IO ()
.
So now I'm leaning toward ClientSockAddr being exposed to Unison, but nothing to modify it. Maybe a toText, not no way to construct it since it seems for UDP purposes, its only relevance is to re-feed it into a function like sendTo
so a server can send to a client after a client has already send data (??).
@kylegoetz This is awesome. Paging @dolio to help w/ any runtime questions, and @runarorama to help w/ any design questions. We can create a Discord thread as well.
Yeah it looks like the only way to get a ClientSockAddr
is via recvFrom
. It extracts the peer address from the datagram. So I think you're right that accept
prepares a UDPSocket
so the server can respond. Seems like this library is trying to provide a TCP-like API so that e.g. you don't have to specify the address each time and buffers can be reused by hanging them on the simulated "connection".
Yeah given that accept
doesn't have good documentation, is seemingly just sugar, and when I use it, my Haskell hangs at accept
without advancing to the next line of code, I think it's not worth including as a built-in.
I didn't want to edit someone else's issue, so I've created this one to track my thoughts/progress on UDP builtins (plus related additions to
base
)The existing (TCP)
Socket
(et al.) currently has the following functionality:Socket
ListeningSocket
BoundServerSocket
UnboundServerSocket
(seemingly not used anywhere inbase
)Socket.accept : ListeningServerSocket ->{IO, Exception} Socket
(with underlying implSocket.client : HostName -> Port ->{IO, Exception} Socket
Socket.close : Socket ->{IO, Exception} ()
Socket.listen : BoundServerSocket ->{IO, Exception} ListeningServerSocket
Socket.port : Socket ->{IO, Exception} Nat
Socket.receive : Socket ->{IO, Exception} Bytes
Socket.receiveAtMost : Socket ->Nat ->{IO, Exception} Bytes
Socket.send : Socket -> Bytes ->{IO, Exception} ()
Socket.toText : Socket -> Text
"Simple" stuff
base
raw
functions in there that are thin wrappers around the built-in, and "cooked" versions that are safe. Users are encouraged not to use the raw versions.Use cases
Creating a client
Socket.client
yields aSocket
that is connected to a given(HostName, Port)
. From there, a client cansend
andreceive[AtMost]
as operations onSocket
.Creating a server
Socket.server
yields aBoundServerSocket
, not yet listening but it has acquired a socket resource. From there, onelisten
s, which yields aListeningServerSocket
and it listens for incoming connections. From there, oneaccept
s an incoming connection (the function blocks until one arrives), which converts theListeningServerSocket
into aSocket
. At this point, like with a client, the server cansend
andreceive
.Closing
Socket
can beclose
d when no longer in use.UDP
The leading UDP library for Haskell seems to be
Network.UDP
. It largely works the same asTCP.Simple
(which is used for the existingSocket
functionality). However, there are a couple differences I'll highlight, and I'll use the Haskell sigs instead of potential Unison ones here:-
UDPSocket
(equivalent toSocket
)ListenSocket
(equivalent toListeningSocket
)ClientSockAddr
(no analogue in TCP, but used to represent the Socket address of a connected (or attempting-to-connect) clientBoundServerSocket
UnboundServerSocket
accept : ListenSocket -> ClientSockAddr -> IO UDPSocket (equiv. to
Socket.accept : ListeningServerSocket ->{IO, Exception} Socketbut requires the
ClientSockAddr` to be known)clientSocket :: HostName -> ServiceName -> Bool -> IO UDPSocket (
Socket.client : HostName -> Port ->{IO, Exception} Socket;
ServiceNamewraps
Port,
Boolparam connects to the host if
true`)close :: UDPSocket -> IO ()
andstop :: ListenSocket -> IO ()
(Socket.close : Socket ->{IO, Exception} ()
)Socket.listen : BoundServerSocket ->{IO, Exception} ListeningServerSocket
since the UDP library initializes the server socket in listening stateSocket.port : Socket ->{IO, Exception} Nat
recv :: UDPSocket -> IO ByteStream
(equivalent toSocket.receive : Socket ->{IO, Exception} Bytes
)recvFrom :: ListenSocket -> IO (ByteStream, ClientSockAddr) (no equiv in
base; this lets you receive from a ListenSocket that hasn't
accept`ed a connection and will give you the data plus info about the sending client)Socket.receiveAtMost : Socket ->Nat ->{IO, Exception} Bytes
send :: UDPSocket -> (ByteStream -> IO ()) (equiv to
Socket.send : Socket -> Bytes ->{IO, Exception} ()`)sendTo :: UDPSocket -> ByteStream 0> ClientSockAddr -> IO ()
(no equivalent, similar toUDP.send
, analogous in purpose toUDP.recvFrom
)serverSocket :: (IP, PortNumber) -> IO ListenSocket
(equiv to `Socket.server : Optional HostName -> Port ->{IO, Exception} BoundServerSocket except it skips Bound and proceeds right to Listen state)Socket.toText : Socket -> Text
Ideas
If we want to keep UDP as close to TCP as possible for UX (similar terms, types), we can do this, but eschew the Unbound and Bound socket states/types by building in:
##UDPSocket
##ListenSocket
##ClientSockAddr
(maybe?)##IO.UDP.ListenSocket.accept.impl.v1 : ListenSocket -> ClientSockAddr ->{IO} Either Failure UDPSocket
##IO.UDP.clientSocket.impl.v1 : HostName -> Port -> {IO} Either Failure UDPSocket
##IO.UDP.UDPSocket.close.impl.v1 : UDPSocket ->{IO} EIther Failure ()
##IO.UDP.ListenSocket.close.impl.v1 : ListenSocket ->{IO} EIther Failure ()
##IO.UDP.recv.impl.v1 : UDPSocket ->{IO} EIther Failure Bytes
##IO.UDP.recvFrom.impl.v1 : ListenSocket ->{IO} EIther Failure (Bytes, ClientSockAddr)
##IO.UDP.serverSocket.impl.v1: Optional Text -> Text ->{IO} Either Failure ListenSocket
(non-impl will beOptional HostName -> Port -> ...
)##IO.UDP.UDPSocket.toText.impl.v1 : UDPSocket -> Text
##IO.UDP.UDPSocket.port.impl.v1 : UDPSocket -> Nat
(non-impl should cast toPort
instead ofNat
)##IO.UDP.sendTo.impl.v2 : ListenSocket -> Bytes -> (Text, Text) ->{IO} Either Failure ()
I'm not sure about the utility of
sendTo
andsend
andaccept
. I need to read more about how UDP sockets work and do testing. I find it strange that a server would need to accept aClientSockAddr
connection, but require you to already know it before accepting, but I don't see a function to detect an attempted connection.More testing needed.
current state https://github.com/unisonweb/unison/commit/354ced3ef8e4be18ea0235d8b225d3b5b01396cd