Search code examples
rubytcptcpservertcpsocket

Sending data to TCPServer more than one time


I'm new to ruby and I'm trying to make a client to connect to a TCPServer, and it seems that in order to do so I have to call the method close_write every time I finish sending data one way, to let the client/server know that the other end is finished sending data. Whenever I do that then Im not able to write info to the server or client again because the socket is not opened for writing anymore. This is my code:

client.rb

require "socket"

socket = TCPSocket.open("localhost", 6666)

loop do
  input = gets.chomp
  socket.puts input # Send data to server
  socket.close_write
  while(line = socket.gets)
    puts line
  end # Print sever response
  break if input=="EXIT"
end
socket.close

server.rb

require "socket"

server = TCPServer.new 6666
data = Hash.new { |hash, key| hash[key] = {} }

WAITING_SET_VALUE = "1"
WAITING_NEW_COMMAND = "0"

loop do

  Thread.start(server.accept) do |session|
    thread_status ||= WAITING_NEW_COMMAND
    ....

    puts "Entering If..."
    if(thread_status == WAITING_NEW_COMMAND) #Check thread status
      puts "thread_status == WAITING_NEW_COMMAND"
      puts "CHECKING COMMAND.."
      case line.strip
      when /^set \w* \w* \d{1,7} \d{1,7}$/
        puts "COMMAND SET"
        thread_status = WAITING_SET_VALUE
        lineArr = line.strip.split(" ")
        varName = lineArr[1]
        flag = lineArr[2]
        ttl = lineArr[3]
        size = lineArr[4]
        puts "END SET EXECUTION"
        session.write "Executed"
        session.close_write
      ...

Is there a way to open the socket for writing again, or a better way to do this back and forth connection between server and client without losing context? Thanks!


Solution

  • When designing a client-server protocol, you have to decide:

    • How a client knows when a response has more lines/data.
    • How a client knows when a response is complete.
    • How a client knows when a response is invalid/valid.
    • How a client knows when there was some type of server error.

    A simple approach is for the server to return a response with the number of lines (as in the code below). However, instead, you could use END or something so that the client knows when there is no more data to read. I would strongly suggest looking into tutorials about Protocols.

    Save the below into a file called client_server.rb. First, run the server with ruby ./client_server.rb s and then in a separate terminal run the client with ruby ./client_server.rb c. Next, type in list over and over to see the different responses. I added list so that you don't have to type in set w w 1 1 over and over for testing purposes. Check it out and let me know if you have any questions.

    # frozen_string_literal: true
    
    require 'socket'
    
    
    # Use:
    #   First, run the server:  ruby ./client_server.rb s
    #   Then, run the client:   ruby ./client_server.rb c
    
    # Use "netcat localhost 6666" on the command line to test
    # without implementing a client.
    
    
    # Returns false to close this client socket.
    # Returns true to continue reading from this client socket.
    def handle_client(client_id, client_socket, command)
    
      # TODO: Define some type of client-server Protocol here.
      case command
      when /^set \w* \w* \d{1,7} \d{1,7}$/
        puts "Running command for client #{client_id}: #{command}"
    
        # This is just for testing purposes.
        case rand(3)
        when 0
          client_socket.puts 'lines 0'
        when 1
          client_socket.puts 'lines 3'
          client_socket.puts 'This is line 1.'
          client_socket.puts 'This is line 2.'
          client_socket.puts 'This is line 3.'
        when 2
          client_socket.puts 'The set command returned an error.'
        end
      when 'list'
        puts "Responding to client #{client_id} with list of messages."
    
        # This is just for testing purposes.
        case rand(3)
        when 0
          client_socket.puts 'messages 0'
        when 1
          client_socket.puts 'messages 2'
          client_socket.puts 'This is message 1.'
          client_socket.puts 'This is message 2.'
        when 2
          client_socket.puts 'Unable to retrieve messages due to error.'
        end
      when 'bye'
        puts "Killing client #{client_id}."
    
        return false # Disconnect and kill the thread.
      else
        client_socket.puts "[ERROR] Invalid command: #{command}"
      end
    
      client_socket.flush # Flush all output just in case.
    
      true # Continue connection.
    end
    
    
    case ARGV[0].to_s.downcase
    when 's' # server
      TCPServer.open(6666) do |server_socket|
        global_client_id = 1
    
        loop do
          Thread.start(global_client_id, server_socket.accept) do |client_id, client_socket|
            puts "Accepting new client #{client_id}."
    
            loop do
              command = client_socket.gets
    
              if command.nil?
                puts "Client #{client_id} disconnected manually."
                break
              end
    
              command = command.strip
              keep_alive = handle_client(client_id, client_socket, command)
    
              break unless keep_alive
            end
    
            client_socket.close
          end
    
          global_client_id += 1
        end
      end
    when 'c' # client
      TCPSocket.open('localhost', 6666) do |socket|
        puts '[Commands]'
        puts 'set <word> <word> <num> <num>    Run set command.'
        puts 'list                             Get list of messages.'
        puts 'exit, bye                        Exit the client.'
        puts
    
        loop do
          print '> '
          input = $stdin.gets.strip
    
          # TODO: Define some type of client-server Protocol here.
          case input
          when /EXIT|BYE/i
            socket.puts 'bye'
            socket.flush
            break
          when /\Aset .+\z/
            socket.puts input
            socket.flush
    
            response = socket.gets
    
            if response.nil?
              puts 'Server is not running anymore! Disconnecting.'
              break
            end
    
            response = response.strip
            match_data = response.match(/\Alines (?<lines>\d+)\z/)
    
            if match_data
              line_count = match_data[:lines].to_i
    
              puts "Received #{line_count} lines from server."
    
              1.upto(line_count) do |i|
                line = socket.gets
    
                puts ">> Resp[#{i}] #{line}"
              end
            else
              # Can check for "response == 'ERROR'" or something.
              puts "Server error or invalid response from server: #{response}"
            end
          when 'list'
            socket.puts input
            socket.flush
    
            response = socket.gets
    
            if response.nil?
              puts 'Server is not running anymore! Disconnecting.'
              break
            end
    
            response = response.strip
            match_data = response.match(/\Amessages (?<messages>\d+)\z/)
    
            if match_data
              message_count = match_data[:messages].to_i
    
              puts "Received #{message_count} messages from server."
    
              1.upto(message_count) do |i|
                line = socket.gets
    
                puts ">> Resp[#{i}] #{line}"
              end
            else
              # Can check for "response == 'ERROR'" or something.
              puts "Server error or invalid response from server: #{response}"
            end
          else
            puts "Invalid command: #{input}"
          end
        end
      end
    else
      puts "Pass in 'c' for client or 's' for server."
    end