crystal-lang / crystal

The Crystal Programming Language
https://crystal-lang.org
Apache License 2.0
19.24k stars 1.61k forks source link

Confused as to how to read binary reply from TCP socket #4154

Open sardaukar opened 7 years ago

sardaukar commented 7 years ago

I'm using Crystal v0.21.1 and trying to write a telnet server. A part of the Telnet protocol is to send short binary messages to negotiate features back and forth. Ruby's net/telnet features an extensive list.

This is my server thus far (reduced code to just have the bug):

require "socket"

class BBS
  def start!
    Signal::INT.trap { puts "SIGINT, exiting gracefully!"; exit 0 }

    server = TCPServer.new("localhost", 2023)
    puts "now listening on port 2023"
    loop do
      if socket = server.accept?
        spawn handle_connection(socket)
      end
    end
  end

  private def handle_connection(socket)
    loop do
      # IAC WILL SGA
      iac_will_sga = UInt8.slice(255, 251, 3)
      socket.send(iac_will_sga)
      puts "IAC WILL SGA sent"
      bytes_read, addr = socket.receive(3)

      puts bytes_read.inspect
      puts addr.inspect
    end
  end
end

BBS.new.start!

The error I get when a client connects on port 2023 is:

IAC WILL SGA sent
Unhandled exception in spawn:
Unsupported family type: UNSPEC (0) (Exception)
0x101ccebc5: *CallStack::unwind:Array(Pointer(Void)) at ??
0x101cceb61: *CallStack#initialize:Array(Pointer(Void)) at ??
0x101cceb38: *CallStack::new:CallStack at ??
0x101cc7ff1: *raise<Exception>:NoReturn at ??
0x101cc7fd1: *raise<String>:NoReturn at ??
0x101d04986: *Socket::Address::from<Pointer(LibC::Sockaddr), UInt32>:Socket::Address+ at ??
0x101d03cee: *TCPSocket+@Socket#receive<Int32>:Tuple(String, Socket::Address+) at ??
0x101cfd723: *BBS#handle_connection<TCPSocket+>:NoReturn at ??
0x101ccde46: ~procProc(Nil)@ex.cr:11 at ??
0x101cd5c74: *Fiber#run:(IO::FileDescriptor | Nil) at ??
0x101ccd039: ~proc2Proc(Fiber, (IO::FileDescriptor | Nil))@/usr/local/Cellar/crystal-lang/0.21.1_1/src/fiber.cr:29 at ??

What am I doing wrong? I basically want to send a 3 byte message and listen to the client's 3 byte reply.

RX14 commented 7 years ago

Looks like a bug using socket.receive on TCP connections. However, you probably don't need to use #receive there. The usual calls are IO#read and IO#write. The socket returned by TCPServer#accept extends the IO module.

iac_will_sga = UInt8.slice(255, 251, 3)
socket.write(iac_will_sga)
puts "IAC WILL SGA sent"

bytes = Slice(UInt8).new(3)
socket.read_fully(bytes)
pp bytes
sardaukar commented 7 years ago

@RX14 ok, so that works! :D thanks - should I keep the issue open?

ysbaddaden commented 7 years ago

Socket#receive relies on recvfrom(2), but apparently we should use recv(2) for SOCK_STREAM sockets. Socket#send works because it calls send(2); it calls sendto(2) when an address is specified only.

I guess we can add support for this. But the behavior of recv vs recvfrom is too different (return a message or a {message, address} tuple); maybe we should rename receive to receive_from and have receive to return message only.

serge-name commented 5 years ago

In Ruby we have this:

irb(main):014:0> r[1].inspect
=> "#<Addrinfo: empty-sockaddr SOCK_DGRAM>"

but in Crystal an exception is being raised: https://github.com/crystal-lang/crystal/blob/master/src/socket/address.cr#L19-L21 https://github.com/crystal-lang/crystal/blob/master/src/socket/address.cr#L39-L41

Would it be reasonable to have an UNSPECAddress structure or something like that?