Polling on two TCP sockets and the STDIN at the same time?

I’m trying to create a simple app in Crystal that is basically a TCP proxy (opens a TCP port and forwards connections to another port), and the proxy also accepts some simple commands on the STDIN while running, like:

  • drop the current connection and don’t accept any other while otherwise told,
  • start listening for incoming connections again,
  • change the listen port to N,
  • change the connect port to M, etc.

I know how to parse the STDIN commands, how to listen for TCP connections (TCPServer listen, accept) and how to open TCP connections to others (TCPSocket), but cannot figure out how to organize my code in order to be able to wait for events from any of the input channgels: STDIN, the accepted TCP connection (it’s fine to handle only 1 accepted connection on the input side at the same time), and the forward TCP connection. Is there a way to select/poll/epoll on these 3 at the same time? The select ... end construct is a nice one, but only works for channels, right?

Putting these things in separate Fibers seemd like a good idea (that brings select .. end back to the game), but then the fibers have to communicate over channels. Isn’t that going to be slow?

If I go with Fibers: let’s say the incoming TCP connection handler Fiber is sitting in an accept indefinitely. What if a command arrives that asks to change the listen port? How can I stop the acceptor Fiber in this case? As far as I know there’s no way to kill a Fiber from outside, but also impossible to wake a Fiber up when it is in a TCPServer accept and no client connect to the TCP port.

Do you have any suggestions on how to to this?

By the way, do you think that Crystal is not the right tool for this problem?

I don’t think so. Channels are pretty efficient. You don’t seem to be expecting high capacity workloads with only a single TCP connection at a time. So there’s really no reason to worry about channel performance.

Yes, fibers are the solution to your problem. I’d probably place the console in the main fiber and spawn a fiber for every TCPServer (where you do accept; apparently just one in your case) and for every client handler (the latter could use a worker pool, but that’s doesn’t seem necessary).

Close the TCPServer and the accept will unblock. That either exits the fiber, or you keep the fiber running and waiting for the new server to listen.
Then you can start a new server on a new port and accept again.

On the contrary. Crystal is a very good tool for this kind of software.

3 Likes

Not that it’s neccessary, but I wanted to reinforce this.
Crystal might even be the best language to implement this usecase.

I wrote a thing for work, that has 2 http servers on different ports, listens on one unix socket, listents to unix signals and even a gpio pin (its running on raspberryPIs).

I am 100% sure I could not have done this in any other language as a single developer with a comparable degree of bug-free-ness .

5 Likes