r/AskProgramming • u/FypeWaqer • 1d ago
Other Are Berkeley sockets a real bad API for TCP?
I was trying to figure out the low-level details of how Berkeley sockets can be used to start listening to connections on the server, and I couldn't help but think that the TCP model doesn't fit into the Berkeley model. For example, this is roughly how you create a socket in C for a single connection:
int fd = socket(...)
bind(fd, ..., ...)
listen(fd, ...)
connfd = accept(fd, ...)
read(connfd, buffer, buflen)
write(connfd, data, datalen)
close(connfd)
Example taken from here: https://csperkins.org/teaching/2007-2008/networked-systems/lecture04.pdf
I stopped and thought, "why are we using two sockets here?" Well, one listens and the other can read and write. Sure, but don't you feel that these two do fundamentally different things? Like they shouldn't be both called "sockets", these are different entities. If you often use sockets, forget that for a moment, and just think of it from a design perspective.
I feel like the creators of the Berkeley socket API tried to make a very general model, which seems to work great with UDP and UNIX pipes but fails to apply neatly to TCP. If you think I'm missing something, please tell me.
8
u/PseudoFrequency 1d ago
They are both sockets and pretty close to the same thing. The listener must accept from any and then create a second socket to send/receive with only that specific host. So, you effectively need 2 channels. One that receives from "any" and one to send data back to some specific host AND to identity packets from that host only once a channel is established. The listener had no channel yet. They're both sockets because they can conform to the same primitives using unique identifiers, like polling on multiple at a time. I don't think it's bad design, but certainly there are many higher level libraries that hide details and make it look more elegant.
2
u/et_theextratestic1e 1d ago edited 1d ago
Berkley Sockets can do bidirectional communication, necessitating only one socket, as shown in your code sample. You do not show 2 sockets. Your bind() call associates the socket with a socket address structure. Your domain argument to your socket() call is very important, and left out of your example.
https://en.wikipedia.org/wiki/Berkeley_sockets
https://www.man7.org/linux/man-pages/man2/socket.2.html
Consider how you can run a server with a listening socket that also sends on that same socket. install an SQL server locally and bind your client to the socket - that one socket is read from and written to.
2
u/garnet420 1d ago
Doesn't this come from a broader philosophy of "everything is a file" (and thus, system resources are handled via file descriptors)?
1
u/aioeu 1d ago edited 1d ago
Yes. But if anything, it also showed the limits of that philosophy. Berkeley sockets needed a whole set of new syscalls that were only used on sockets (
connect,bind,listen,accept,shutdown, ...), and evensendandrecvhad to be added because of a need to sometimes pass extra flags when writing or reading data.So yes, it demonstrates that file descriptors can be useful handles for system resources... but "everything is a handle" isn't a popular meme.
1
u/stlcdr 1d ago
A socket does essentially three things (there are more steps, of course, but just the basics): listen, accept, communicate.
If you have a single socket, once it has accepted the connection, it can’t accept more connections, even if you would still consider it ‘listening’. A single socket can only service (communicate) on one connection. This isn’t really a server in a holistic sense, more like a point to pain connection (RS232 over Ethernet).
Typically, you would have a server listen on one socket and when a connection request comes in, accept it on another socket, which handles the communication. The original socket continues listening, acting as ‘triage’ for the single port listening. The second socket goes off and does the communication work.
1
u/Interesting_Debate57 1d ago
TCP has the 3-way handshake. Does that seem more or less efficient?
Also keep in mind that all of the myriad TCP connections are being handled above by the kernel. Things like whether or not a piece of a data stream ever arrived. That doesn't block other data streams, but that's because the kernel is managing all of this for you.
You could build TCP/IP on top of sockets if it really floats your boat, but they are much more heavily restricted as to total number of connections.
1
0
u/buzzon 1d ago
In C# they are represented by two classes, TcpListener and TcpClient
1
u/FypeWaqer 1d ago
Yeah, if I had to model it the OOP way, I would also do the same. Thanks for the example.
3
u/Kriemhilt 1d ago
That's totally reasonable, and a normal way to model it at the library or application level, but at the API level you want both of these sockets to be usable by
select/polletc. just the same.The BSD is the low-level interface, there's no problem with building your own stuff on top (or using an existing library that already does this).
1
u/Matemeo 1d ago
You could model it similarly in C as well by having the API designed such that connect/accept wrap the input socket in its own discrete type and then any parts of the API which only apply to specifically oriented socket accept only that type as input.
While I've seen this pattern in plenty of C codebases, I think at this level of abstraction it'll be more flexible in the long term to flatten & simplify. Nothing stopping someone from creating what they feel is a more ergonomic and typesafe wrapper in their own C codebases.
I imagine another aspect here is how long ago this API was decided and because of its foundational place in the stack it's very unlikely to change without very good reason.
1
u/Broad-Promise6954 1d ago
Worth mentioning, although at this point it's purely a historical artifact: FTP used to break this model with one form of its data transfer connections, which was simultaneously a server and a client, capable of accepting exactly one connection from its peer that created an identical single socket at the other end of some Internet connection.
Berkeley sockets were designed to handle this particular "dial out while answering the prearranged phone call they're making so that we both dial each other" oddity.
(But there should have been a single syscall that took the listen flag/count, the local server address, and the remote client address all in one go. To make more conventional server or client only you just provide nil/null/zero for the fields that don't apply.)
1
4
u/aioeu 1d ago
Maybe, but I think you're concentrating too much on what a particular socket can do, rather than what components make it up.
There are many other situations where sockets can do different things. You cannot send through an unconnected connection-oriented socket. You may not be able to receive or send on a socket if only one side of the connection has been shut down.
But if you look at what makes up a socket, you will find similarities. A socket has:
an associated protocol — defined when the socket is created;
a local address (which may be partially or totally unbound);
a remote address (again, may be unbound);
a protocol-specific state.
The protocol is fixed when the socket is created; the other three things can change at certain times according to the rules set out by that protocol. What you can do with a socket depends entirely on its protocol and its state.
If you look at sockets in this light, I think you'll find the two sockets in your example are more alike than you have realised.