12/08/2018, 15:08

ChatRoom TCP console with Ruby Socket

Hôm nay, mình muốn giới thiệu với các bạn 1 cách cơ bản nhất để tạo 1 ứng dụng chat TCP sử dụng thư viện Socket của ngôn ngữ Ruby. Mình sử dụng Ruby 2.3.1 và Ubuntu 14.04 Đầu tiên chúng ta cùng ôn lại 1 tí về TCP (Transmission Control Protocol): TCP (Transmission Control Protocol - "Giao thức ...

Hôm nay, mình muốn giới thiệu với các bạn 1 cách cơ bản nhất để tạo 1 ứng dụng chat TCP sử dụng thư viện Socket của ngôn ngữ Ruby. Mình sử dụng Ruby 2.3.1 và Ubuntu 14.04 Đầu tiên chúng ta cùng ôn lại 1 tí về TCP (Transmission Control Protocol):

TCP (Transmission Control Protocol - "Giao thức điều khiển truyền vận") là một trong các giao thức cốt lõi của bộ giao thức TCP/IP. Sử dụng TCP, các ứng dụng trên các máy chủ được nối mạng có thể tạo các "kết nối" với nhau, mà qua đó chúng có thể trao đổi dữ liệu hoặc các gói tin. Giao thức này đảm bảo chuyển giao dữ liệu tới nơi nhận một cách đáng tin cậy và đúng thứ tự. TCP còn phân biệt giữa dữ liệu của nhiều ứng dụng (chẳng hạn, dịch vụ Web và dịch vụ thư điện tử) đồng thời chạy trên cùng một máy chủ. (Theo Wikipedia)

Đây là cách mà ứng dụng Chat TCP sẽ hoạt động:

Xây dựng chương trình

Chúng ta sẽ xây dựng một server để lắng nghe và nhận kết nối từ phía client và lưu trữ chúng trong 1 data dictionaries. Dictionaries này sẽ theo dõi các client nào đang kết nối, nhận message từ client nào và chuyển message đó đến cho các client còn lại. Mỗi user sẽ có 1 username khác nhau vì dictoinaries sẽ thực hiện tìm kiếm kết nối dựa trên username. Bây giờ chúng ta sẽ tạp 2 file có tên là "server.rb" và "client.rb" Trong server.rb và client.rb, chúng ta phải require thư viện Socket của Ruby

# in client.rb and server.rb
require "socket"

Sau đó chúng ta xây dựng class Client tương ứng để thực hiện việc kết nối đến Server, cho phép người dùng gửi và nhận message. Trong hàm khởi tạo sẽ có 3 thuộc tính:

  • @server: một instance server để client có thể kết nối đến
  • @request: gửi message đến server
  • @response: nhận message từ server

Ban đầu @request và @response nhận giá trị null, nhưng sau đó chúng ta sẽ xây dựng 2 Thread và assign vào chúng để có thể gửi và nhận dữ liệu.

# in client.rb
require "socket"
class Client
  def initialize(server)
    @server = server
    @request = nil
    @response = nil
  end
end

Tiếp theo chúng ta tiến hành cài đặt Server. Để khởi tạo một Server thì chúng ta phải có địa chỉ ip và port. Thông qua chúng để client có thể xác định được kết nối đến địa chỉ nào thông qua port nào để server có thể lắng nghe kết nối và thực hiện việc trao đổ dữ liệu giữa client và server. Ngoài ra, khi khởi tạo Server thì chúng ta còn có 3 thuộc tính:

  • @connections (Hash): lưu trữ các kết nối đến server
  • @rooms (Hash): có key là tên của room chat và chứa các user đang ở trong room
  • @clients (Hash): chứa các client đang kết nối

Bây giờ chúng ta đã có thể biết được người dùng nào đang ở phòng nào. Điều đặc biệt quan trọng ở đây là tên của người dùng phải là duy nhất. Cùng preview lại các hash mà chúng ta dùng để lưu trữ

# hash Connections preview
connections: {
  clients: { client_name: {attributes}, ... },
  rooms: { room_name: [clients_names], ... }
}

Tiếp đến, chúng ta cần khởi tạo 2 Thread để client có thể gửi và nhận message ở cũng 1 thời điểm.

# ( @request, @response ) objects preview and descriptions
# The request and response objects implementation may look like this

# in client.rb
def send
  @request = Thread.new do
    loop { # write as much as you want
      # read from the console
      # with the enter key, send the message to the server
    }
  end
end

def listen 
  @response = Thread.new do
    loop { # listen for ever
      # listen the server responses
      # show them in the console
    }
  end
end

File đầy đủ client side:

require "socket"
class Client
  def initialize( server )
    @server = server
    @request = nil
    @response = nil
    listen
    send
    @request.join
    @response.join
  end

  def listen
    @response = Thread.new do
      loop {
        msg = @server.gets.chomp
        puts "#{msg}"
      }
    end
  end

  def send
    puts "Enter the username:"
    @request = Thread.new do
      loop {
        msg = $stdin.gets.chomp
        @server.puts( msg )
      }
    end
  end
end

server = TCPSocket.open( "localhost", 3000 )
Client.new( server )

Ở phía Server, chúng ta cũng cần 1 cái gì đó tương tự, về cơ bản chúng ta sẽ sử dụng 1 thread cho 1 connection của người dùng. Với cách này chúng ta có thể xử lý cùng lúc nhiều client kết nối đến.

# in server.rb
def run
  loop {
    Thread.start do |client| # each client thread
    end
  }
end

Sau khi đã có hàm run, chúng ta sẽ xử lí unique username trong hàm run, nếu tên đó đã tồn tại, thì thông báo error message cho người dùng và kill connection đó, ngươc lại thì chấp nhận kết nối.

# server.rb ( server side )
class Server
  def  initialize(port,ip)
    @server = TCPServer.open(ip, port)
    ...
  end

  def run
    loop {
      # for each user connected and accepted by server, it will create a new thread object
      # and which pass the connected client as an instance to the block
      Thread.start(@server.accept) do | client |
        nick_name = client.gets.chomp.to_sym
        @connections[:clients].each do |other_name, other_client|
          if nick_name == other_name || client == other_client
            client.puts "This username already exist"
            Thread.kill self
          end
        end
        puts "#{nick_name} #{client}"
        @connections[:clients][nick_name] = client
        client.puts "Connection established, Thank you for joining! Happy chatting"
      end
    }
  end
end
server = Server.new("localhost", 3000) # (ip, port) in each machine "localhost" = 127.0.0.1
server.run

Sau khi đã khởi tạ kết nốt, chúng ta cần một hàm để có thể nhận message và gửi đến các client khác:

# in server.rb
def listen_user_messages(username, client)
  loop {
    # get client messages
    msg = client.gets.chomp
    # send a broadcast message, a message for all connected users, but not to its self
    @connections[:clients].each do |other_name, other_client|
      unless other_name == username
        other_client.puts "#{username.to_s}: #{msg}"
      end
    end
  }
end

Gọi hàm listen_user_messages trong hàm run để có thể thực hiện viêc truyền và gửi dữ liệu:

# in server.rb
def run
  loop {
    Thread.start(@server.accept) do | client |
      ...
      listen_user_messages(nick_name, client)
    end
  }
end

File server.rb đầy đủ:

require "socket"
class Server
  def initialize( port, ip )
    @server = TCPServer.open( ip, port )
    @connections = Hash.new
    @rooms = Hash.new
    @clients = Hash.new
    @connections[:server] = @server
    @connections[:rooms] = @rooms
    @connections[:clients] = @clients
    run
  end

  def run
    loop {
      Thread.start(@server.accept) do | client |
        nick_name = client.gets.chomp.to_sym
        @connections[:clients].each do |other_name, other_client|
          if nick_name == other_name || client == other_client
            client.puts "This username already exist"
            Thread.kill self
          end
        end
        puts "#{nick_name} #{client}"
        @connections[:clients][nick_name] = client
        client.puts "Connection established, Thank you for joining! Happy chatting"
        listen_user_messages( nick_name, client )
      end
    }.join
  end

  def listen_user_messages( username, client )
    loop {
      msg = client.gets.chomp
      @connections[:clients].each do |other_name, other_client|
        unless other_name == username
          other_client.puts "#{username.to_s}: #{msg}"
        end
      end
    }
  end
end

Server.new( 3000, "localhost" )

OK, vậy là chúng ta đã hòan thành ứng dụng chat. Bây giờ mình sẽ chạy demo ứng dụng.

0