📜  红宝石 |案例陈述(1)

📅  最后修改于: 2023-12-03 14:56:48.593000             🧑  作者: Mango

红宝石 | 案例陈述

背景

红宝石是一门动态编程语言,它的设计目标是追求简洁高效,同时保持代码的可读性。它既可以面向对象编程,也可以函数式编程,因此受到了许多程序员的喜爱。本文将介绍红宝石实践案例,希望能给程序员提供参考和启发。

案例

我们希望开发一个聊天室,需要实现以下功能:

  • 用户注册、登录、注销
  • 用户间私聊、群聊
  • 聊天记录保存和查询
  • 系统广播
  • 私聊、群聊使用不同的消息类型(如文本、图片、语音等)

我们使用 Sinatra 框架实现了基本的 Web 服务器,使用 Redis 存储聊天记录和用户信息。

用户注册、登录、注销

用户的信息包括用户名、密码、邮箱等,存储在 Redis 中。用户注册时,需要判断用户名是否已存在,密码需要加密后存储。用户登录时,需要校验用户名和密码是否匹配,匹配成功后,生成一个 token 作为登录凭证,存储在 Redis 中,并设置有效期。用户注销时,需要删除 token。

以下是注册、登录、注销的代码片段:

class User
  def self.add(username, password, email)
    raise '用户名已存在' if exists?(username)
    password = Digest::MD5.hexdigest(password)
    redis.hmset(user_key(username), 'username', username, 'password', password, 'email', email)
  end

  def self.login(username, password)
    password = Digest::MD5.hexdigest(password)
    user = redis.hgetall(user_key(username))
    raise '用户名或密码错误' unless user['password'] == password
    token = SecureRandom.hex
    redis.setex(token_key(token), TOKEN_EXPIRES_IN, user['username'])
    token
  end

  def self.logout(token)
    redis.del(token_key(token))
  end

  private

  def self.exists?(username)
    redis.exists(user_key(username))
  end

  def self.user_key(username)
    "user:#{username}"
  end

  def self.token_key(token)
    "token:#{token}"
  end

  def self.redis
    @redis ||= Redis.new
  end
end
用户间私聊、群聊

用户间的聊天使用 WebSocket 实现,私聊和群聊使用不同的命名空间。客户端发送消息到服务端时,需要指定目标用户或群组,服务端接收到消息后,将消息保存到 Redis 中,并广播消息到指定的用户或群组中。

以下是私聊和群聊的代码片段:

class Chat
  def initialize(app)
    @app = app
    @redis = Redis.new
    @sockets = []
    @clients = {}
  end

  def call(env)
    if Faye::WebSocket.websocket?(env)
      socket = Faye::WebSocket.new(env)

      socket.on :open do |event|
        @sockets << socket
      end

      socket.on :message do |event|
        data = JSON.parse(event.data)
        case data['type']
        when 'join'
          username = User.valid_token?(data['token'])
          if username
            @clients[username] ||= []
            @clients[username] << socket
            emit_all('join', username)
          else
            socket.close
          end
        when 'leave'
          username = User.valid_token?(data['token'])
          if username
            remove_client(username, socket)
            emit_all('leave', username)
          else
            socket.close
          end
        when 'message'
          username = User.valid_token?(data['token'])
          if username
            save_message(username, data['to'], data['content'])
            send_message(username, data['to'], data['content'])
          else
            socket.close
          end
        end
      end

      socket.on :close do |event|
        @sockets.delete(socket)
        @clients.each do |username, sockets|
          remove_client(username, socket) if sockets.include?(socket)
        end
      end

      # return async Rack response
      socket.rack_response
    else
      @app.call(env)
    end
  end

  private

  def save_message(from, to, content)
    message = {
      'from' => from,
      'to' => to,
      'content' => content,
      'timestamp' => Time.now.to_i
    }
    key = message_key(from, to)
    @redis.lpush(key, message.to_json)
    @redis.ltrim(key, 0, MESSAGE_LIMIT - 1)
  end

  def send_message(from, to, content)
    message = {
      'from' => from,
      'to' => to,
      'content' => content,
      'timestamp' => Time.now.to_i
    }
    if User.group?(to)
      emit_group(to, 'message', message)
    else
      emit_user(to, 'message', message)
      emit_user(from, 'message', message)
    end
  end

  def emit_all(type, data)
    @sockets.each do |socket|
      socket.send(JSON.generate({ 'type' => type, 'data' => data }))
    end
  end

  def emit_user(username, type, data)
    if @clients[username]
      @clients[username].each do |socket|
        socket.send(JSON.generate({ 'type' => type, 'data' => data }))
      end
    end
  end

  def emit_group(group, type, data)
    redis.smembers(User.group_key(group)).each do |username|
      emit_user(username, type, data)
    end
  end

  def remove_client(username, socket)
    @clients[username].delete(socket)
    @clients.delete(username) if @clients[username].empty?
  end

  def message_key(from, to)
    "message:#{User.group?(to) ? "group:#{to}" : "#{from}/#{to}"}"
  end
end
聊天记录保存和查询

聊天记录存储在 Redis 中,保存最近的 MESSAGE_LIMIT 条记录。查询聊天记录时,可以指定起止时间、发送方、接收方等条件,从 Redis 中读取记录并返回给客户端。

以下是聊天记录查询的代码片段:

class Message
  def self.query(params)
    from = params['from']
    to = params['to']
    start_time = params['start_time'] ? Time.parse(params['start_time']).to_i : 0
    end_time = params['end_time'] ? Time.parse(params['end_time']).to_i : Time.now.to_i

    results = []
    if User.group?(to)
      redis.smembers(User.group_key(to)).each do |username|
        key = message_key(from, username)
        results.concat(read_messages(key, start_time, end_time))
      end
    else
      key = message_key(from, to)
      results.concat(read_messages(key, start_time, end_time))
    end
    results
  end

  private

  def self.read_messages(key, start_time, end_time)
    messages = redis.lrange(key, 0, MESSAGE_LIMIT - 1)
    messages = messages.map { |msg| JSON.parse(msg) }
    messages.select { |msg| msg['timestamp'] >= start_time && msg['timestamp'] <= end_time }
  end

  def self.message_key(from, to)
    "message:#{User.group?(to) ? "group:#{to}" : "#{from}/#{to}"}"
  end

  def self.redis
    @redis ||= Redis.new
  end
end
系统广播

系统广播使用类似于消息的方式,向所有在线用户发送公告、通知等信息。客户端可以订阅系统广播,接收到新的广播时,进行页面自动提示。

以下是系统广播的代码片段:

class Broadcast
  def self.publish(content)
    message = {
      'from' => 'system',
      'to' => 'broadcast',
      'content' => content,
      'timestamp' => Time.now.to_i
    }
    redis.publish(BROADCAST_CHANNEL, message.to_json)
  end

  def self.subscribe(socket)
    @subscriber ||= start_subscriber
    @subscriber.subscribe(BROADCAST_CHANNEL) do |on|
      on.message do |_channel, message|
        socket.send(JSON.generate({ 'type' => 'broadcast', 'data' => JSON.parse(message) }))
      end
    end
  end

  private

  BROADCAST_CHANNEL = 'broadcast'

  def self.start_subscriber
    redis = Redis.new
    subscriber = Redis.new
    Thread.new do
      subscriber.subscribe(BROADCAST_CHANNEL) do |on|
        on.message do |_channel, message|
          redis.lpush(BROADCAST_LOG_KEY, message)
          redis.ltrim(BROADCAST_LOG_KEY, 0, MESSAGE_LIMIT - 1)
        end
      end
    end
    subscriber
  end

  def self.redis
    @redis ||= Redis.new
  end

  def self.broadcast_log
    redis.lrange(BROADCAST_LOG_KEY, 0, MESSAGE_LIMIT - 1).map { |msg| JSON.parse(msg) }
  end
end
结语

红宝石的简洁高效和灵活性,使得它在 Web 开发和数据处理等领域都得到了广泛的应用。同时,大量同类的现成代码库和优秀的教程,也为程序员提供了较为友好的学习和使用体验。希望本文介绍的案例能为红宝石开发者提供参考和启发。