-
Notifications
You must be signed in to change notification settings - Fork 67
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
More ergonomic API for constructing sockets #56
Comments
I guess this is cc @alexcrichton too — you're everywhere in Rust land! |
Thanks for the report! There's definitely quite a few constraints on this API, and I agree that we have yet to thread the needle quite well enough just yet. The original intent of the decisions in this crate were:
So given all that, I'm not personally 100% wed to the current API. I'd be totally fine moving over to Given that, what do you think? |
I agree that As for TCP/UDP, I'm a bit on the fence here. I agree that for established connections,, the methods you can run are different, but I don't know that this is true prior to calling So, in total, my preferred API would be (taking #45 into account): impl<Proto> SocketBuilder<Proto> {
fn new_v4() -> Result<Self>;
fn new_v6() -> Result<Self>;
fn ttl(&mut self, ttl: u32) -> Result<&mut Self>;
fn only_v6(&mut self, only_v6: bool) -> Result<&mut Self>;
fn reuse_address(&mut self, reuse: bool) -> Result<&mut Self>;
fn local_addr(&self) -> Result<SocketAddr>;
fn get_reuse_address(&self) -> Result<bool>;
fn take_error(&self) -> Result<Option<Error>>;
}
impl SocketBuilder<Tcp> {
fn bind(&mut self, addr: SocketAddr) -> Result<&mut Self>;
fn listen(self, backlog: i32) -> Result<TcpListener>;
fn connect<T>(&self, addr: SocketAddr) -> Result<TcpStream>;
}
impl SocketBuilder<Udp> {
fn bind(self, addr: SocketAddr) -> Result<UdpSocket>;
} I'm not entirely sure what purpose the It's sort of weird that I'm also not sure about the naming of some of these methods:
|
Oh so one point with consumption as a final step (but TcpBuilder::new_v4()?.bind("...")?.listen(n) The return value of In general we've avoided type state in the past, and I'd be hesitant to use it here. In general while it does deduplicate the implementation a little bit I've found it doesn't tend to help consumers (e.g. why would a consumer be generic over The One thing that I've personally concluded over time is that using a builder basically just isn't worth it. Instead I feel like we should just have All of my own personal previous attempts at least have ended up being unergonomic in one way or another in some situation. Having just a straight up Out of curiosity, what do you think of that? |
I think that's a great idea. In a way, that's what the API above starts to approximate (including being protocol-agnostic) if you remove the builder parts. I like it. You could arguably also get rid of the |
Heh so I actually intended on doing this a long time ago, and finally got around to it! I haven't documented much but do the APIs here seem reasonable? |
That looks pretty good! I think I'd also like to see a way to convert into |
Oh bah looks like they didn't show up in the docs but the conversions are the via |
Can I poke the authors to take a look at this again? The lack of backlog on |
@njaard I would recommend using socket2: https://docs.rs/socket2/0.3.11/socket2/ |
Thanks for the fast response! I already found it, but since I'm using tokio I also have to propagate that through mio! It's disappointing that backlog was forgotten throughout the entire ecosystem. |
You can turn a socket2 Socket into a std UnixStream and a std UnixStream into a tokio UnixStream. |
For anyone else who may stumble upon this: The following definitely changes the socket backlog to 4096:
|
I started out wanting to be able to bind a socket to a given address (for the purposes of using a particular network interface) before connecting, and quickly discovered the standard library does not provide methods for doing so. I then found
net2::TcpBuilder
, which does provide this feature, but found the API somewhat less ergonomic than I would have liked.@sfackler pointed out that RFC 1461 (after the failed RFC 1158) pulled in a couple of changes from
net2
into the standard library, and @seanmonstar that net2 is a "Desired out-of-band evaluation" in the libz blitz, so I figured I'd send some feedback in the hopes that eventually something likeTcpBuilder
might land in the standard library too.My suggestions follow below. I'd be happy to file a PR, but @sfackler suggested that given the backwards-incompatibility of these changes, filing a ticket first for discussion would be a good idea. Some of these build on the Builders enable construction of complex values (C-BUILDER) section from @brson's Rust API guidelines, whereas others are just because I like them better. Thoughts and feedback very welcome!
The methods on
TcpBuilder
that modify the socket should all take&mut self
, and return that&mut
in theirResult
. This is because, while it is technically true that a socket can be mutated safely from multiple places, it leaves the consumer of the API confused about whether there's a difference between the original builder and the returned one. For instance, it is not clear just from the API (nor from the resulting code) thattcp
andtcp2
are the same socket, bound to the same port in the following code:Using
&mut self
is also more semantically accurate, since it indicates that we are in fact modifying the underlying socket.The methods on
TcpBuilder
that listen, connect, or otherwise "initiate" the socket, should takeself
, not&self
. The current API implies that you can re-use a builder to construct a second socket after callinglisten
orconnect
on it once, but this is not true. The code will panic if you try to do this, presumably because a single socket cannot be re-used. In theory, the builder could remember the configuration, and lazily create and configure the socket when needed, which would enable this kind of API, but I'm not sure that's really better. I think it's fairly rare that you'll want to re-use a socket configuration.The change above brings us back to the first point about
&mut self
vs&self
. @brson's post says that:Which suggests that we should in fact make all the builder methods take
self
. This unfortunately combines poorly with the fact that our methods can fail, and thus returnResult<Self, ...>
. If a method fails, you'd like to still be able to recover the builder, which is tricky ifself
was consumed. Maybe @brson can provide some insight here?This is a more controversial one, but it might be that we should provide a
SocketBuilder
rather than a separateTcpBuilder
andUdpBuilder
. Under the hood, that's what the Berkeley Socket API provides, and it's unclear you want the distinction here either. Instead, you could imagineSocketBuilder
being parameterized by UDP/TCP, which would still let us only expose the TCP related methods for TCP sockets. I personally think this would make the API more readable, and it would avoid the complete duplication of methods between the implementations. Along the same lines, I'd prefer to seeV4
/V6
be an enum passed tonew
, but I feel less strongly about that particular point.@seanmonster pointed out that this is unlikely to break existing code, since current implementations must already only be calling the finalizing methods once (since any other use panics). Making the methods take
&mut self
will also likely require minor or no changes, since they are unlikely to be using the aliased&
pointer anyway. Moving toself
will require more changes to user code, as the original element will no longer be available, but if the regular one-line builder pattern is used withunwrap
(which is unfortunately common), everything will keep working as usual. Dealing with the error cases will be trickier though, and we need a story there.Here's the original discussion with @seanmonstar, @habnabit, and @sfackler from
#rust
:The text was updated successfully, but these errors were encountered: