首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >用于处理FTP服务器的Telnet序列的状态机

用于处理FTP服务器的Telnet序列的状态机
EN

Code Review用户
提问于 2013-10-02 16:08:51
回答 4查看 792关注 0票数 6

我已经请代码气候生成度量那里买ftpd红宝石了。它正确地识别了上帝类,我知道该怎么做。但其中一个较小的班级让我很困惑。这是telnet.rb:

代码语言:javascript
复制
# -*- ruby encoding: us-ascii -*-

module Ftpd

  # Handle the limited processing of Telnet sequences required by the
  # FTP RFCs.
  #
  # Telnet option processing is quite complex, but we need do only a
  # simple subset of it, since we can disagree with any request by the
  # client to turn on an option (RFC-1123 4.1.2.12).  Adhering to
  # RFC-1143 ("The Q Method of Implementing TELNET Option Negiation"),
  # and supporting only what's needed to keep all options turned off:
  #
  # * Reply to WILL sequence with DONT sequence
  # * Reply to DO sequence with WONT sequence
  # * Ignore WONT sequence
  # * Ignore DONT sequence
  #
  # We also handle the "interrupt process" and "data mark" sequences,
  # which the client sends before the ABORT command, by ignoring them.
  #
  # All Telnet sequence start with an IAC, followed by at least one
  # character.  Here are the sequences we care about:
  #
  #     SEQUENCE             CODES
  #     -----------------    --------------------
  #     WILL                 IAC WILL option-code
  #     WONT                 IAC WONT option-code
  #     DO                   IAC DO option-code
  #     DONT                 IAC DONT option-code
  #     escaped 255          IAC IAC
  #     interrupt process    IAC IP
  #     data mark            IAC DM
  #
  # Any pathalogical sequence (e.g. IAC + \x01), or any sequence we
  # don't recognize, we pass through.

  class Telnet

    # The command with recognized Telnet sequences removed

    attr_reader :plain

    # Any Telnet sequences to send

    attr_reader :reply

    # Create a new instance with a command that may contain Telnet
    # sequences.
    # @param command [String]

    def initialize(command)
      telnet_state_machine command
    end

    private

    module Codes
      IAC  = 255.chr    # 0xff
      DONT = 254.chr    # 0xfe
      DO   = 253.chr    # 0xfd
      WONT = 252.chr    # 0xfc
      WILL = 251.chr    # 0xfb
      IP   = 244.chr    # 0xf4
      DM   = 242.chr    # 0xf2
    end
    include Codes

    def telnet_state_machine (command)
      @plain = ''
      @reply = ''
      state = :idle
      command.each_char do |c|
        case state
        when :idle
          if c == IAC
            state = :iac
          else
            @plain << c
          end
        when :iac
          case c
          when IAC
            @plain << c
            state = :idle
          when WILL
            state = :will
          when WONT
            state = :wont
          when DO
            state = :do
          when DONT
            state = :dont
          when IP
            state = :idle
          when DM
            state = :idle
          else
            @plain << IAC + c
            state = :idle
          end
        when :will
          @reply << IAC + DONT + c
          state = :idle
        when :wont
          state = :idle
        when :do
          @reply << IAC + WONT + c
          state = :idle
        when :dont
          state = :idle
        else
          raise "Unknown state #{state.inspect}"
        end
      end
    end

  end
end

Code不喜欢#telnet_state_machine的复杂性。我同意,但我不知道如何降低复杂性,同时也使状态机更难跟踪。状态机似乎从来都不是那么可读的。你有什么建议?

注意:如果您想尝试一些重构,这个类具有rspec测试覆盖率。只需对ftpd项目执行"git克隆“即可。

EN

回答 4

Code Review用户

回答已采纳

发布于 2013-10-02 20:17:10

编辑

我越多地思考这个问题,就越明显解析器比状态机更能适应这个问题。不幸的是,我对写解析器知之甚少。

原始答案

也许您可以尝试在每个州创建一个类:

代码语言:javascript
复制
class TelnetState

  include Telnet::Codes
  attr_reader :plain, :reply

  def initialize( plain = '', reply ='' )
    @plain = plain
    @reply = reply
  end

  def accept_char( char )
    raise "Abstract Method - not implemented"
  end

end

class TelnetIDLEState < TelnetState

  def accept_char( char )
    if char == IAC 
      TelnetIACState.new( plain, reply )
    else
      @plain << char
      self        
    end
  end

end

class TelnetIACState < TelnetState

   def accept_char( char )
     update_plain!( char )
     next_state( char )
   end

   private

   def update_plain!( char )
     return if [WILL, WONT, DO, DONT, IP,DM].include? char
     @plain << char == IAC ? char : IAC + char
   end

   def next_state( char ) 
     next = case char
              when WILL then TelnetWILLState
              when WONT then TelnetWONTState
              when DO   then TelnetDOState
              when WONT then TelnetDONTState
            end
     next ? next.new( plain, reply ) : self
   end

 end

..。等等,然后:

代码语言:javascript
复制
class Telnet

  def telnet_state_machine( command )
    state = command.each_char.inject( TelnetIDLEState.new ) do |state, char|
              state.accept_char( char )
            end
    @plain, @reply = state.plain, state.reply
  end

end

当然,肯定有办法改善.只是个主意。(我对telnet协议也不太熟悉,所以我做了一些蒙住眼睛的工作)

票数 3
EN

Code Review用户

发布于 2013-10-05 03:13:16

正如@m_x所建议的那样,这个解决方案使用了一个由StringScanner内置类驱动的解析器。这非常紧凑、可读性很强,并且完全摆脱了状态机:

一些处理操作的方法:

代码语言:javascript
复制
def accept(scanner)   
  @plain << scanner[1]
end

def reply_dont(scanner)
  @reply << IAC + DONT + scanner[1]
end

def reply_wont(scanner)
  @reply << IAC + WONT + scanner[1]
end

def ignore(scanner)
end

telnet序列的列表:

代码语言:javascript
复制
# Telnet sequences to handle, and how to handle them

SEQUENCES = [
  [/#{IAC}(#{IAC})/, :accept],
  [/#{IAC}#{WILL}(.)/m, :reply_dont],
  [/#{IAC}#{WONT}(.)/m, :ignore],
  [/#{IAC}#{DO}(.)/m, :reply_wont],
  [/#{IAC}#{DONT}(.)/m, :ignore],
  [/#{IAC}#{IP}/, :ignore],
  [/#{IAC}#{DM}/, :ignore],
  [/(.)/m, :accept],
]

以及使用它们的解析器:

代码语言:javascript
复制
# Parse the the command.  Sets @plain and @reply

def parse_command(command)
  @plain = ''
  @reply = ''
  scanner = StringScanner.new(command)
  while !scanner.eos?
    SEQUENCES.each do |regexp, method|
      if scanner.scan(regexp)
        send method, scanner
        break
      end
    end
  end
end
票数 4
EN

Code Review用户

发布于 2013-10-05 02:29:51

正如建议的那样,我尝试使用类似于状态_机器的gem。嗯,不是像state_machine这样的宝石,而是特别的宝石。状态机定义是:

代码语言:javascript
复制
    state_machine :state, :initial => :idle do

      event :iac do
        transition :idle => :iac
        transition :iac => :idle
        transition [:will, :wont, :do, :dont] => :idle
      end

      event :will do
        transition :iac => :will
        transition :will => :idle
      end

      event :dm do
        transition :iac => :idle
      end

      event :ip do
        transition :iac => :idle
      end

      event :dont do
        transition :iac => :dont
      end

      event :other do
        transition all => :idle
      end

      event :do do
        transition :iac => :do
      end

      event :wont do
        transition :iac => :wont
      end

      before_transition :from => :do, :to => :idle, :do => :send_wont
      before_transition :from => :will, :to => :idle, :do => :send_dont
      before_transition :from => :idle, :to => :idle, :do => :accept_plain
      before_transition :from => :iac, :to => :idle, :on => :other, :do => :accept_unknown_iac
      before_transition :from => :iac, :to => :idle, :on => :iac, :do => :accept_plain

    end

state_machine DSL是干净和相对紧凑的。不幸的是,它是围绕导致转换的事件来组织的,而不是以具有转换的状态为中心的。这还不错,但这完全超出了我对状态机的看法。它还使您可以将操作与转换分开定义。最终的结果是,我无法从这个定义中看到状态机。

但是,state_machine gem有一个rake任务,可以直接从代码生成状态图。下面是它为上述代码生成的内容:

除了缺少操作之外,state_machine生成的状态图非常好。

票数 3
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/32144

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档