Please help with TCP Chat Server example

Hello, I have just started to learn Crystal some days ago:

I just started to write a small TCP chat program
-Is it possible to download the whole Crystal Ref and API as a book in HTML or PDF ?
-This would be useful for searching and grepping the docs

I wrote a small TCP chat server which passes messages between:
STDIN <ā€“> TCP Server <-> TCP Clients
-TCP Clients are added/deleted to a class array ā€œ@@acā€ when they come and go.

I a using spawn to handle messaging for the clients and stdin/stdout.
How could i introduce channels to improve this small program, from my point
of view it runs just fine. Is it already using fibers behind the scene ?
For me its not clear what the top level fiber is.
When I start introducing fibers i get this error most of the time, even when the var
looks ok for me:
Error: undefined local variable or method ā€˜channelā€™ for top-level

BR,
Peter

require "socket"
PORT=9090
puts "Welcome, server listening on TCP port #{PORT}"
puts "Enter message to be sent to all TCP clients"
spawn handle_stdin
Myclients.print_state

server = TCPServer.new("localhost", PORT)
while client = server.accept?
  p! client
  p! client.remote_address
  spawn handle_client(client)
end

class Myclients 
   @@ac = Array(TCPSocket).new
   def Myclients.add_client(str)
       @@ac << str
       Myclients.print_state
   end

   def Myclients.del_client(str)
       @@ac.delete str
       Myclients.print_state
   end

   def Myclients.get_clients
       @@ac
   end    

   def Myclients.count
       @@ac.size
   end    

   def Myclients.print_state
    p! @@ac
    puts "Number of Clients Online: #{Myclients.count}"
  end
end   

def handle_client(client)
    Myclients.add_client(client)
    while message = client.gets
      puts client.inspect + message
      #send echo to all other clients
      Myclients.get_clients.each do |client_from_array|  
        if client_from_array != client
            client_from_array.puts (message)
        end 
      end
    end
    #Client closes session
    Myclients.del_client(client)  
end

def handle_stdin
    while line = STDIN.gets
      Myclients.get_clients.each do |client|    
        client.puts (line) 
      end  
    end
end

You can grab the HTML files off releases assets section. https://github.com/crystal-lang/crystal/releases/download/0.34.0/crystal-0.34.0-docs.tar.gz

To be clear you would need to instantiate a new channel, itā€™s not some method on the top level.

channel = Channel(String).new

...

Iā€™d suggest reading thru Concurrency - Crystal if you havenā€™t already.

1 Like

Thank you, I defined channel exactly like this. I think it has something to do with the
visibility or scope of this var.
BR,
Peter

In tcp_echo2.cr:13:5

 13 | channel.send(line)
      ^------
Error: undefined local variable or method 'channel' for top-level
require "socket"
channel = Channel(String).new
server = TCPServer.new("localhost", 9090)
p! server
while client = server.accept?
  p! client
  spawn handle_client(client)
end

def handle_client(client)
    while message = client.gets
    client.puts "echo: " + message
    channel.send(message)  #### ???
    end
end

loop do 
  puts channel.receive
end

You have to pass it in, spawn handle_client(client, channel) and def handle_client(client, channel)

Thank You - we got one step further.
Currently nothing is happening on the fiber receive side.
I even do not see the ā€œloopā€ on stdout.
On the tcp client I see one time the echo message, but the program does not continue.
Spawning several tcp clients works, also only for one echo message.

require "socket"
channel = Channel(String).new
server = TCPServer.new("localhost", 9090)
p! server
while client = server.accept?
  p! client
  spawn handle_client(client,channel)
end

def handle_client(client,channel)
    while message = client.gets
    client.puts "echo: " + message
    channel.send(message)  
    end
end

loop do
  puts "loop" 
  puts channel.receive
end

your program never gets to this part.

loop do
  puts "loop" 
  puts channel.receive
end

The execution stops inside the while loop, waiting on server.accept?

you need to wrap that loop in something like spawn:

spawn do
  while client = server.accept?
    p! client
    spawn handle_client(client,channel) 
  end
end

then your program reaches the loop do at the end and will work.

1 Like

Thank you, that works now, also with several TCP clients.
I got some nice logging to stdout:
Welcome: Listening on 9090
server # => #<TCPServer:fd 9>
client # => #<TCPSocket:fd 22>
127.0.0.1:56518 Got: hello from client1
127.0.0.1:56518 Got: line2
client # => #<TCPSocket:fd 23>
127.0.0.1:56522 Got: hello from client2
127.0.0.1:56522 Got: test
BR,
Peter

require "socket"
PORT=9090
puts "Welcome: Listening on #{PORT}" 
channel = Channel(String).new
server = TCPServer.new("localhost", PORT)
p! server
spawn do
    while client = server.accept?
      p! client
      spawn handle_client(client,channel)
    end
end

def handle_client(client,channel)
    loop do
    while message = client.gets
     client.puts "echo: " + message
     channel.send(client.remote_address.to_s + " Got: " + message)  
    end
  end
end

loop do
  puts channel.receive
end

This example includes:
-store tcp sessions in array
-send messages from any to any tcp client and stdin/stdout
-use a channel for logging client messages in main fiber to stdout
So I am happy that my blog article is finished for now and Crystal just works.

require "socket"
PORT=9090
puts "Welcome, server listening on TCP port #{PORT}"
puts "Enter message in terminal to be sent to all TCP clients"
channel = Channel(String).new
spawn handle_stdin
Myclients.print_state

server = TCPServer.new("localhost", PORT)
p! server
spawn do
  while client = server.accept?
    p! client
    p! client.remote_address
    spawn handle_client(client,channel)
  end
end  

class Myclients 
   @@ac = Array(TCPSocket).new  #array for client connections
   def Myclients.add_client(str)
       @@ac << str
       Myclients.print_state
   end
   def Myclients.del_client(str)
       @@ac.delete str
       Myclients.print_state
   end
   def Myclients.get_clients
       @@ac
   end    
   def Myclients.count
       @@ac.size
   end    
   def Myclients.print_state
    p! @@ac
    puts "Number of Clients Online: #{Myclients.count}"
  end
end   

def handle_client(client,channel)
    Myclients.add_client(client)
    while message = client.gets
      channel.send (client.remote_address.to_s + " Got: " + message)
      #send echo to all other clients
      Myclients.get_clients.each do |client_from_array|  
        if client_from_array != client
            client_from_array.puts (message)
        end 
      end
    end
    #Client closes session
    Myclients.del_client(client)  
end

def handle_stdin
    while line = STDIN.gets
      Myclients.get_clients.each do |client|    
        client.puts (line) 
      end  
    end
end

#logging to stdout in main fiber
loop do
  puts Time.local.to_s + " IP:" + channel.receive
end

Crystal 0.34.0 does not support multi-threading yet

It does! You just need to build with -Dpreview_mt.

MyClients is never instantiated, I would recommend to make it a module instead of a class.

I would recommend to use def self. over def MyClients..

I also would recommend to run crystal tool format on your code :)

Thank you for your help and infos. Myclients just works as a Singelton. Am I wong to use it without explicit instantiating ?
Maybe I need to refresh my Ruby.

Itā€™s a pure style suggestion. You never intend to make an instance of it and thereā€™s no use to doing so ever, so why even allow it. It uses zero features of class, only those of module, so why not make it a module.

I tried to change Myclients into a module. How can I define a array var there now ?
@@ac = Array(TCPSocket).new #array for client connections

30 | @@ac
^
Error: canā€™t use class variables at the top level

To be clear, you are defining a module right? Like:

module Foo
  @@array = [1,2,3]
end

A constant would also work if youā€™re only reading the value of the array.

See https://crystal-lang.org/reference/syntax_and_semantics/modules.html.

Yes, I would like to have a module which replaces this Singleton.
I have read the modules documentation, but this does not help me in the moment.
I would like to use extend self.

class Myclients 
   @@ac = Array(TCPSocket).new  #array for client connections
   def Myclients.add_client(str)
       @@ac << str
       Myclients.print_state
   end
   def Myclients.del_client(str)
       @@ac.delete str
       Myclients.print_state
   end
   def Myclients.get_clients
       @@ac
   end    
   def Myclients.count
       @@ac.size
   end    
   def Myclients.print_state
    p! @@ac
    puts "Number of Clients Online: #{Myclients.count}"
  end
end
module Myclients 
  class_getter ac : Array(String) = [] of String
  
  def self.add_client(str : String) : Nil
    @@ac << str
     puts self
   end

   def self.del_client(str : String) : Nil
     @@ac.delete str
     puts self
   end
  
   def self.count : Int32
     @@ac.size
   end  
  
  def self.to_s(io : IO)
    io.puts @@ac
    io.puts "Number of Clients Online: #{self.count}"
  end
end

Myclients.add_client "foo"
# ["foo"]
# Number of Clients Online: 1

Something like this is what you want iā€™d imagine.

Very tricky, if I start using the class getter ac thing and try to combine it with interesting module features like: extend self and include Myclients - I end up with the following error again:
34 | @@ac.size
^
Error: canā€™t use class variables at the top level
The main reason for using a Singleton was to use it as a container for the ac array which should act as a global variable. Is there another way of defining a sort of ā€œglobal varā€ in a module, without making ac a sort of instance or class var ?
Why cant a class var not be used at the top level ?

What do you even need those for?

Thats already what you have with my example I provided. Maybe provide the code youā€™re having trouble with?

I ended with having two moduls, one with just a Gloval Var, the other with the methods now nicely integrating without using a namespace.

module Gvar
  @@ac = Array(TCPSocket).new  #array for client connections
  def self.ac; @@ac; end
end   

module Myclients
   extend self
   def add_client(str)
       Gvar.ac << str
       clients_state
   end
   def del_client(str)
       Gvar.ac.delete str
       clients_state
   end
   def get_clients
       Gvar.ac
   end    
   def clients_count : Int32
       Gvar.ac.size
   end    
   def clients_state
       puts Gvar.ac
       puts "Number of Clients Online: #{clients_count}"
   end
end   
include Myclients