简体   繁体   English

封装网络协议

[英]Encapsulating a network protocol

I'm working on an app which takes in a raw binary message (very simple, first byte is the message type, rest is payload), and then does something with it. 我正在开发一个应用程序,该应用程序接收原始的二进制消息(非常简单,第一个字节是消息类型,其余是有效负载),然后对其进行处理。 The thing I'm trying to accomplish is making sure that the networking service is abstracted away from the rest of the app, to allow for modifying the protocol now and then without affecting the rest of the application too much. 我要完成的事情是确保将网络服务从应用程序的其余部分中抽象出来,以允许不时修改协议,而又不会对应用程序的其余部分造成太大影响。 The context of the application is a very simple client-server game, for which I am doing the client work now. 该应用程序的上下文是一个非常简单的客户端-服务器游戏,我现在正在为此做客户端工作。

I'm kinda struggling now though. 我现在有点挣扎。 I need to find an elegant way to just throw a connection into some sort of translator/adapter service, which returns pretty objects (I think). 我需要找到一种优雅的方法来将连接放入某种翻译器/适配器服务,该服务会返回漂亮的对象(我认为)。 Those objects will be thrown in a queue awaiting consumption by the rest of the app. 这些对象将被放入队列中,以等待其余的应用程序使用。 The problem I'm facing is more or less this construct (pseudo code): 我面临的问题或多或少是这种结构(伪代码):

Let's assume each message is the 20 bytes, so I can deal with calling this function for each 20 bytes: 假设每个消息是20个字节,那么我可以处理每个20个字节的函数:

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

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

Obviously, I'll use an enum or constants for the case, but that's not the thing that's bugging me. 显然,在这种情况下,我将使用枚举或常量,但这并不是困扰我的事情。 Here's the thing. 就是这个 I think I've got about 50 message types, which would mean I'll get a switch statement with 50 cases. 我认为我有大约50种消息类型,这意味着我将获得50种情况的switch语句。 I can't really think of a proper way to cut this up into smaller pieces, which will result in a huge, error prone method. 我真的想不出将其切成小块的正确方法,这将导致产生巨大的,容易出错的方法。 I was wondering if there's any patterns to make this easier, as I couldn't find any. 我想知道是否有任何模式可以简化此过程,因为我找不到任何模式。

Thanks for the input in advance! 感谢您的预先输入!

I have some Java code which does this. 我有一些执行此操作的Java代码。 Hopefully you can easily translate to C#. 希望您可以轻松地转换为C#。 Essentially, I have a messageMap collection: 本质上,我有一个messageMap集合:

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

This is a map from message IDs to their corresponding Message classes. 这是从消息ID到其对应的Message类的映射。 I call addMessage once for each different message type: 我为每种不同的消息类型调用一次addMessage

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

Then when a message arrives I read the message ID off the wire, look up the Message class I need to instantiate in messageMap , and then use reflection to create an instance of that class. 然后,当一条消息到达时,我从线下读取消息ID,查找我需要在messageMap实例化的Message类,然后使用反射来创建该类的实例。

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

Here newInstance() calls the default constructor. 在这里, newInstance()调用默认的构造函数。

I've used this generic message-handling code across multiple applications with different messages. 我已经在具有不同消息的多个应用程序中使用了这种通用的消息处理代码。 Each one just has a nice, simple block of code registering the different messages like so: 每个人只有一个不错的,简单的代码块,用于注册不同的消息,如下所示:

// 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);

You could have an array of 50 function pointers (ie C# delegates) which you index using the value of first byte, or a dictionary of delegates whose key is the the value of first byte. 您可能有一个由50个函数指针(即C#委托)组成的数组,您可以使用第一个字节的值对其进行索引,也可以拥有代表其键为第一个字节的值的代表字典。 That's merely another way of writing a switch statement though. 不过,这只是编写switch语句的另一种方式。

It's more distributed though: for example, when you create a new message type (which may be a new class in a new source file), then instead of editing source code to add a new case to a big switch statement, you can call an existing method to add a new delegate to the static collection of delegates. 不过,它分布更广:例如,当您创建新的消息类型(可能是新的源文件中的新类)时,您可以调用一个将新委托添加到委托的静态集合的现有方法。

While not exactly a C# solution, I have dealt with a similar situation recently. 虽然不是完全C#解决方案,但最近我也遇到了类似情况。 My solution was to use F# which makes it a lot easier. 我的解决方案是使用F#,这使它变得容易得多。

For example, my code looks like this 例如,我的代码如下所示

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)

It's not the perfect solution, but looks a lot better than the switch statement in c#. 这不是完美的解决方案,但是比c#中的switch语句好得多。 So our entire application is written in csharp, but the message parser is written in fsharp. 因此,我们的整个应用程序都是用csharp编写的,但是消息解析器是用fsharp编写的。

FYI: We have several interfaces: 仅供参考:我们有几个接口:

IDataTransportServer - Responsible for receiving data via RS232 or TCP/IP IDataTransportServer-负责通过RS232或TCP / IP接收数据

IDataProcessor - Responsible for parsing binary data and turning it into instances of the Message class IDataProcessor-负责解析二进制数据并将其转换为Message类的实例

IMessageProcessor - Responsible for processing messages (this is the fsharp module) IMessageProcessor-负责处理消息(这是fsharp模块)

I have no idea if this is useful for you, but just wanted to let you know how we deal with this kind of problem. 我不知道这对您是否有用,只是想让您知道我们如何处理此类问题。

Well, there are certainly many ways. 好吧,当然有很多方法。 The standard one is to store functions in a dictionary. 标准的一种是将功能存储在字典中。 In functional languages you would write something like 在功能语言中,您可能会写类似

import MyProtocol

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

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

I'm not sure what's your version of C/C++/C#. 我不确定您的C / C ++ / C#版本是什么。 If you can't put functions there, then put pointers to functions. 如果您不能在其中放置函数,则将指针指向函数。 If some of your functions are very small, in some languages you can put then right there with lambda : 如果您的某些功能非常小,则可以使用lambda在某些语言中将其放在此处:

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

There are more optimizations that I would do. 我会做更多的优化。 See, for every message you have a constant and a function name. 请参阅,对于每条消息,您都有一个常量和一个函数名。 You don't have to repeat, though: 不过,您不必重复:

 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:])
 ...

But if you want to be philosophically correct, you can have separate classes for handlers: 但是,如果您想在哲学上是正确的,则可以为处理程序提供单独的类:

 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:])
 ...

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM