封装网络协议

时间:2009-06-19 15:22:02

标签: c# networking

我正在开发一个接收原始二进制消息的应用程序(非常简单,第一个字节是消息类型,休息是有效负载),然后用它做一些事情。我正在努力实现的目标是确保网络服务从应用程序的其余部分抽象出来,以便现在可以修改协议,而不会过多地影响应用程序的其余部分。 应用程序的上下文是一个非常简单的客户端 - 服务器游戏,我现在正在为客户端工作。

但是,我现在有点挣扎。我需要找到一种优雅的方式来将连接抛出到某种转换器/适配器服务中,它返回漂亮的对象(我认为)。这些对象将被抛入队列,等待应用程序的其余部分消耗。我面临的问题或多或少是这个构造(伪代码):

假设每个消息都是20个字节,所以我可以处理为每20个字节调用此函数:

public Message GetMessage(byte[] buffer)
{
  switch(buffer[0])
  {
     case 1:
       return Message1(...);
     case 2:
       return Message2(...);
     .....

     case n:
       return MessageN(...);
  }
}

显然,我将使用枚举或常量来表示案例,但这并不是让我烦恼的事情。 这就是事情。我想我有大约50种消息类型,这意味着我将获得50个案例的switch语句。我真的不能想到一个合适的方法将它切成小块,这将导致一个巨大的,容易出错的方法。我想知道是否有任何模式可以让这更容易,因为我找不到任何。

感谢提前输入!

4 个答案:

答案 0 :(得分:2)

我有一些Java代码可以做到这一点。希望您可以轻松转换为C#。基本上,我有一个messageMap集合:

private final Map<Byte, Class<? extends Message>> messageMap;

这是从消息ID到其对应的Message类的映射。我为每种不同的消息类型拨打addMessage一次:

public void addMessage(int id, Class<? extends Message> messageClass) {
    messageMap.put((byte) id, messageClass);
}

然后当消息到达时,我从线路上读取消息ID,查找我需要在Message中实例化的messageMap类,然后使用反射来创建该类的实例。 / p>

Class<? extends Message> messageClass = messageMap.get(id);
Message                  message      = messageClass.newInstance();

这里newInstance()调用默认构造函数。

我在不同消息的多个应用程序中使用了这个通用的消息处理代码。每个人都有一个很好的,简单的代码块来注册不同的消息,如下所示:

// Messages that we can send to the client.
addOutgoingMessage(0, HeartbeatMessage.class);
addOutgoingMessage(1, BeginMessage    .class);
addOutgoingMessage(2, CancelMessage   .class);

// Messages that the client can send.
addIgnoredMessage (0, HeartbeatMessage.class);
addIncomingMessage(1, StatusMessage   .class, statusMessageHandler);
addIncomingMessage(2, ProgressMessage .class, progressMessageHandler);
addIncomingMessage(3, OutputMessage   .class, outputMessageHandler);
addIncomingMessage(4, FinishedMessage .class, finishedMessageHandler);
addIncomingMessage(5, CancelledMessage.class, cancelledMessageHandler);
addIncomingMessage(6, ErrorMessage    .class, errorMessageHandler);

答案 1 :(得分:1)

你可以有一个包含50个函数指针的数组(即C#委托),你可以使用第一个字节的值进行索引,或者使用其键是第一个字节值的委托字典。这只是编写switch语句的另一种方式。

它更分布式:例如,当您创建新的消息类型(可能是新源文件中的新类)时,不是编辑源代码以将新案例添加到大型switch语句中,可以调用现有方法将新委托添加到静态委托集合中。

答案 2 :(得分:1)

虽然不完全是C#解决方案,但我最近处理过类似情况。 我的解决方案是使用F#,这使它更容易。

例如,我的代码看起来像这样

member private this.processDefaultGroupMessage(m : Message) =
        try
            match m.Intro.MessageType with
            | (1us) -> this.listFirmwareVersions(m)                               //ListFirmwareVersions              0
            | (2us) -> this.startLoadingFirmwareVersion(m)                        //StartLoadingFirmwareVersion       1
            | (3us) -> this.loadFirmwareVersionBlock(m)                           //LoadFirmwareVersionBlock          2
            | (4us) -> this.removeFirmwareVersion(m)                              //RemoveFirmwareVersion             3
            | (5us) -> this.activateFirmwareVersion(m)                            //ActivateFirmwareVersion           3        
            | (12us) -> this.startLoadingBitmapLibrary(m)                         //StartLoadingBitmapLibrary         2
            | (13us) -> this.loadBitmapBlock(m)                                   //LoadBitmapLibraryBlock            2        
            | (21us) -> this.listFonts(m)                                         //ListFonts                         0
            | (22us) -> this.loadFont(m)                                          //LoadFont                          4
            | (23us) -> this.nakResponse(m, VPL_FRAMELENGTH)                      //RemoveFont                        3
            | (24us) -> this.nakResponse(m, VPL_FRAMELENGTH)                      //SetDefaultFont                    3         
            | (31us) -> this.nakResponse(m, VPL_FRAMELENGTH)                      //ListParameterSets                 0
            | (32us) -> this.nakResponse(m, VPL_FRAMELENGTH)                      //LoadParameterSets                 4
            | (33us) -> this.nakResponse(m, VPL_FRAMELENGTH)                      //RemoveParameterSet                3
            | (34us) -> this.nakResponse(m, VPL_FRAMELENGTH)                      //ActivateParameterSet              3
            | (35us) -> this.nakResponse(m, VPL_FRAMELENGTH)                      //GetParameterSet                   3        
            | (41us) -> this.nakResponse(m, VPL_FRAMELENGTH)                      //StartSelfTest                     0
            | (42us) -> this.ackResponse(m)                                       //GetStatus (reply with ACK)        0
            | (43us) -> this.getStatusDetail(m)                                   //GetStatusDetail                   0
            | (44us) -> this.resetStatus(m)                                       //ResetStatus                       5
            | (45us) -> this.setDateTime(m)                                       //SetDateTime                       6
            | (46us) -> this.nakResponse(m, VPL_FRAMELENGTH)                      //GetDateTime                       0
            | (71us) -> this.clearConfiguration(m)                                //ClearConfiguration                0
            | (72us) -> this.defineTextFields(m)                                  //DefineTextFields                  11
            | (74us) -> this.defineClockFields(m)                                 //DefineClockFields                 13
            | (80us) -> this.deleteFieldDefinitions(m)                            //DeleteFieldDefinitions            14
            | (91us) -> this.preloadTextFields(m)                                 //PreloadTextFields                 15
            | (94us) -> this.clearFields(m)                                       //ClearFields                       17
            | (95us) -> this.activate(m)                                          //Activate                          0
            | _ -> this.nakResponse(m, VPL_REQUESTNOTSUPPORTED)
        with 
            | _ -> this.nakResponse(m, VPL_INVALID)

这不是完美的解决方案,但看起来比c#中的switch语句好很多。 所以我们的整个应用程序都是用csharp编写的,但是消息解析器是用fsharp编写的。

供参考: 我们有几个接口:

IDataTransportServer - 负责通过RS232或TCP / IP接收数据

IDataProcessor - 负责解析二进制数据并将其转换为Message类的实例

IMessageProcessor - 负责处理消息(这是fsharp模块)

我不知道这对你有用,但只是想让你知道 我们如何处理这类问题。

答案 3 :(得分:1)

嗯,肯定有很多方法。标准的是将函数存储在字典中。在函数式语言中,您可以编写类似

的内容
import MyProtocol

handler  = { mListFirmware :  listFirmwareVersions,                  
             mLoadFirmware :  startLoadingFirmwareVersion,
             mLoadFirmwareBl: loadFirmwareVersionBlock, 
             ...
}

...
try {
    handler[message[0]](message[1:])
} catch (NotInTheDictionary e) {
    # complain ...
}

我不确定你的C / C ++ / C#是什么版本。如果你不能把函数放在那里,那就把指针放到函数上。如果您的某些功能非常小,在某些语言中,您可以使用lambda

放置
 ...
             mLoadFirmware :  (lambda (m): start_load(m[1:3]); do_threads()),
 ...

我会做更多的优化。请参阅,对于每条消息,您都有一个常量和一个函数名称。不过,您不必重复:

 Messages = new Array()

 def listFirmwareVersions(m):
      ...
     Messages.add(Name_Of_This_Method(), This_Method) 
     # it's possible to get name of current function in Python or C#

 ... # how to send 
 send_message(Messages.lookup(listFirmwareVersions), ...)

 ... # how to receive
 try {  
     Messages[message[0]](message[1:])
 ...

但是如果你想要在哲学上是正确的,你可以为处理程序提供单独的类:

 class MessageHandler:
      static int message_handlers = [] 
      int msg
      Array data
      void handler
      Message(a_handler):               
          msg = message_handlers.add(this)
          handler = a_handler
      write(Stream s):
          s.write(msg, data)

  listFirmwareVersions = new MessageHandler(do_firmware)
  startLoadingFirmwareVersion = new MessageHandler(load_firmware) 
  ...

 ... # how to send 
 listFirmwareVersions.send(...)

 ... # how to receive
 try {
      message_handlers[message[0]](message[1:])
 ...