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.
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.
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