http://qs321.pair.com?node_id=11122630

pudda has asked for the wisdom of the Perl Monks concerning the following question:

Hello guys,

I'm trying to do a tcp socket server which will send a message to all clients connected every 10s, like a heartbeat.

I managed to do it using IO::Socket::INET only, but the code only handle a single connection, so when the client dies, the server dies too.

Searching, I found a way to do it with IO::Select, but I can't make it work too.

It doesn't send the 'HeartBeat' and still when I stop the a client the server stops too. Can you guys help me to find what I'm doing wrong?

Here the codes:

server.pl

#!/usr/bin/perl -w use strict; use warnings; use IO::Socket; use IO::Select; # auto-flush on socket $| = 1; my $server_ip; my $server_port; ($server_ip,$server_port)=('127.0.0.1', 5000); # creating a listening socket my $tcp_socket = new IO::Socket::INET( LocalHost => $server_ip, LocalPort => $server_port, Proto => 'tcp', Listen => 5, Reuse => 1 ); die "cannot create socket $!\n" unless $tcp_socket; print "server waiting for client connection on port $server_port\n"; my $read_select = IO::Select->new(); $read_select->add($tcp_socket); my $hello ="Hello <VERSION>\n"; while(1){ my $new_readable; my $nick; ($new_readable)=IO::Select->select($read_select,undef,undef,0); foreach my $read(@$new_readable){ if($read==$tcp_socket){ my $new_connection = $read->accept(); $new_connection->send($hello); $read_select->add($new_connection); }else{ my $buf; my $msg; $buf='HeartBeat'; if($buf){ my @sockets = $read_select->can_write(); foreach my $sck(@sockets){ $sck->send("$buf\n"); } }else{ $read_select->remove($read); } } } } $tcp_socket->close();

client.pl

#!/usr/bin/perl #tcpclient.pl use IO::Socket::INET; use CGI; use CGI::Carp qw(fatalsToBrowser warningsToBrowser); $| = 1; my ($socket,$client_socket); $socket = new IO::Socket::INET ( PeerAddr => '127.0.0.1', PeerPort => '5000', Proto => 'tcp' ) or die "ERROR in Socket Creation : $! $@\n"; print "TCP Connection Success.\n"; while(1){ $socket->recv($data,1024); print "Received from Server : $data\n"; } $socket->close();

Replies are listed 'Best First'.
Re: How do I make a tcp socket heartbeat?
by haukex (Archbishop) on Oct 09, 2020 at 14:45 UTC

    I see two issues with your code: First, you're not giving a timeout to the select (or rather, IO::Select) call, which is what you would need to do to implement the 10-second interval. Second, you've got the code for the heartbeat inside the loop over @$new_readable, however, if the select call times out, that variable will be undefined - please see those two documentation links for details. You will need to move the heartbeat code out of the loop, and detect timeout by setting and checking $! as described in the IO::Select docs.

    Note that at the moment, your code does not cover a lot of the cases of select-based server code (e.g. errors or even the clients sending data). If you are still getting to that, great, I think that writing a complete server with select is a great learning experience for how things work on a relatively low level. OTOH, IMHO nowadays using an event loop is my personal favorite way to go, and at the moment, my favorite event loop is Mojo::IOLoop. I even wrote an example server and client and posted them here. A heartbeat can be implemented using Mojo::IOLoop->recurring.

      Thanks for the help!

      In fact, I'm pretty new to both perl and TCP connections. I used IO :: Socket because I found the most examples out there, but I found it very difficult to understand how everything works with it. Maybe in the future I will study more and try to do the script again, writing the complete server.

      I tested your example using Mojo and it worked very well, exactly as I wanted. Thanks very much!

        I used IO :: Socket because I found the most examples out there, but I found it very difficult to understand how everything works with it.

        Yes, the select(2) system call is (one of) the classic ways to do it. "Classic" both in the good and bad sense: good because understanding it will help you understand how lots of servers work and how things work on a lower level, not so good because nowadays I'd consider it fairly low level, so for just getting stuff done, as I said IMHO event loops are a good approach.

      Taking the opportunity, Could you tell me how I do if I wanted my client, when the server goes down, to try to reconnect?

      Like, in the $stream->on(close => ...) how do try to I connect again to the server?

        One way to do that is to place the Mojo::IOLoop->client call in a subroutine, as in sub reconnect { Mojo::IOLoop->client( ... ) }, make sure to call that sub once before starting the loop to set up the initial connection, and then, in the handler for the close event, do something like Mojo::IOLoop->timer( 2 => \&reconnect ); to call that subroutine to reestablish the connection. I very much suggest the timer call so as to not flood the server with connection attempts, and I would also recommend adding a counter variable so you can stop attempting to reconnect after a certain number of failed attempts.

Re: How do I make a tcp socket heartbeat?
by tybalt89 (Monsignor) on Oct 09, 2020 at 21:00 UTC

    I'm glad you have a working solution using one of the async libs, because that's the way it should be done. However, here's a semi-tested version using just IO::Socket and IO::Select as an example of one way it could have been done.

    #!/usr/bin/perl use strict; # https://perlmonks.org/?node_id=11122630 use warnings; use IO::Socket; use IO::Select; my $port = 5000; my $hello = "Hello <VERSION>\n"; my $interval = 10; my $heartbeat = "HeartBeat\n"; my $listen = IO::Socket::INET->new( LocalPort => $port, Listen => 10, Reuse => 1, ) or die $@; my $sel = IO::Select->new($listen); my %clients; while( 1 ) { for my $fh ( $sel->can_read(1) ) { if( $fh == $listen ) { my $client = $listen->accept; $clients{$client} = { client => $client, heartbeat => time + $in +terval }; $sel->add( $client ); print $client $hello; } elsif( sysread $fh, my $buf, 4096 ) { print "got: $buf"; } else { $sel->remove( $fh ); delete $clients{ $fh }; } } for my $ref ( values %clients ) { if( time >= $ref->{heartbeat} ) { print { $ref->{client} } $heartbeat; $ref->{heartbeat} = time + $interval; } } }

    I tested it with multiple telnets as clients and it happily heartbeats away, even with clients coming and going...

Re: How do I make a tcp socket heartbeat?
by haukex (Archbishop) on Oct 09, 2020 at 20:13 UTC

    I just noticed that your previous question from last week was in regards to AnyEvent. Note that it is an event loop framework much like Mojo::IOLoop, so if you're already using one of them, it certainly can't hurt to learn about the others, but for your production code, you probably don't want to mix the two. See AnyEvent::Socket for TCP servers and clients. (Personally I still prefer Mojo, and it can do Websockets too, but TIMTOWTDI.)

      Hi, thanks for the response!

      In my previous question I was trying to resolve another problem. I had a trouble but with a Websocket, this one it's for a tcp server.

      I saw that AnyEvent can handle TCP connections too, but I've found it more complicated, at least for me, how to understand and use it, I'm actually changing that code to use Mojo too.

      Anyways, thanks for the advice!

Re: How do I make a tcp socket heartbeat?
by eyepopslikeamosquito (Archbishop) on Oct 09, 2020 at 21:33 UTC
Re: How do I make a tcp socket heartbeat?
by Anonymous Monk on Oct 09, 2020 at 15:20 UTC
    As Haukex suggests ... it should always be a mantra: "am I now trying to do 'Actum Ne Agas: A Thing Already Done?'" In this case the answer is definitely "yes." There are plenty of both high-level and low-level frameworks on CPAN which have already thoroughly solved this problem. The code has been very thoroughly tested and it works.
      I agree, I used IO::Socket because it was in almost every topic I searched. But I'm using Mojo as suggested by Haukex. Thanks for the advice.