65.9K
CodeProject 正在变化。 阅读更多。
Home

二进制数据封送

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.63/5 (12投票s)

Jan 13, 2005

5分钟阅读

viewsIcon

62196

downloadIcon

1894

使用简单的CMarshal类实现快速二进制数据封送。

Sample Image - Marshal.gif

引言

数据封送是将数据对象转换为与网络传输协议的包结构对应的数据流的过程。或者将数据对象表示为可以由网络协议发送和接收,并在另一端重新翻译的标准格式。

数据封送使用了许多思想,但它们之间的共同点是都试图在任何数据对象前面加上其类型,并以多种格式表示。

  1. XML:使用XML标签和属性表示数据信息,并使用XML文本保存值,XML可以以文本格式(字符数组)发送,并在外部进行解析以重新构建数据对象。
  2. 二进制:在每个数据对象之前使用二进制头来标识其类型和长度(如果需要)。(图1)
  3. 文本:仅使用文本表示数据,例如:(string:plapla,int:3232,short:43,...),并在另一端进行简单的解析。

第一种和第三种方式都很好,并且是可表示的、人类可读的,但它们在需要高性能的应用程序中速度较慢,因此我在我的类中采用了第二种方式来提高速度。在本文中,我将尝试通过引入一个简单快速的封送类来简化这个想法,该类可以收集多种格式的数据,并通过套接字连接将其发送到另一个封送对象。

二进制封送

二进制封送意味着将数据对象以二进制格式放入,每个数据对象前面都带有其类型,如图1所示。

Sample Image

73:    's' character means the current element is a string
0d 00: 2 bytes to keep string length
...:   string ASCII bytes
69:    'i' character means the current element is a short

and so on,  'type',' value',...

这里的优点是,类型始终占用一个字节,并且每种类型都占用其允许的最大字节数,如果类型是可变长度的,如string,则长度占用2个字节。因此,解析过程将非常快,只需直接访问即可。但是,在处理一些特殊情况时,需要注意以下几点:

  1. 封送对象:要封送类和结构等对象,需要继承自一个简单的类CMarshalObject,该类有两个用于序列化和反序列化对象数据的功能,因此需要封送的类必须实现这两个函数,因为CMarshal类在封送和反封送过程中会内部调用它们。在封送缓冲区中,对象的类型是字符'o'。
  2. 封送向量:要封送任何类型的向量,只需在类型前面加上'v'字符,表示向量,因此封送的缓冲区将像这样来封送字符数组:

    Text: vcHatem Mostafa
    Binary: 76 63 48 61 74 65 6d 20 4d 6f 73 74 61 66 61
  3. 封送对象向量:您可以通过在对象类型'o'前面加上'v'字符来封送对象向量,如前一点所述。

请记住,您不必自己完成所有这些工作,我已经为我的类引入了有用的函数来完成所有这些工作。

类函数

高级函数

封送 使用可选参数函数,一次调用即可封送任意数量的数据类型。
解封 使用可选参数函数,一次调用即可解封任意数量的数据类型。
发送 通过已连接的套接字发送封送数据。
接收 通过已连接的套接字接收封送数据。
bool Marshal(LPCSTR lpcsFormat, ...);
bool Unmarshal(LPCSTR lpcsFormat, ...);

Ex:
Client side:
    char c;
    int n;
    vector<string> vs;
    ...
    CMarshal obj;
    obj.Marshal("%c%vs%d", c, &vs, n);
    obj.Send(socket);
Server side:
    CMarshal obj;
    obj.Recv(socket);
    obj.Unmarshal("%c%vs%d", c, &vs, n);

这些函数的情况很简单,在客户端只需<封送,发送>,在服务器端只需<接收,解封>。

注意:您可以从任何一方发送和接收。

低级函数

PopType 弹出封送缓冲区中当前索引处的类型。
Pop Pop current data at current index in the marshaled buffer.
PopObject 弹出封送缓冲区中当前索引处的对象。
PopVector 弹出封送缓冲区中当前索引处的向量。
PopObjectVector 弹出封送缓冲区中当前索引处的对象向量。
推送 (Push) 将数据推送到封送缓冲区中的索引处。
PushVector 将向量推送到封送缓冲区中的索引处。
PushObjectVector 将对象向量推送到封送缓冲区中的索引处。

所有这些函数都直接处理封送对象的内部缓冲区,以根据图1调整缓冲区,或者在解封过程中解析缓冲区以填充数据对象。

关注点

  1. 封送对象使用String类来处理所有内部缓冲区,我为String类提供了一些有用的运算符,例如:
    const String & operator+=(const String & string);
    const String & operator+=(LPCTSTR lpsz);
    const String & operator+=(LPTSTR lpsz);
    const String & operator+=(const unsigned char* lpsz);
    const String & operator+=(int n);
    const String & operator+=(short s);
    const String & operator+=(double d);
    const String & operator+=(float f);
    const String & operator+=(char c);

    这有助于我将任何数据类型推送到封送对象的堆栈中。

  2. 我在此代码中使用的String类类似于MFC的CString类,并带有一些附加运算符,如前一点所示。
  3. 套接字同步是本文中最棒的功能。
    1. 封送对象中的SendRecv函数可以从客户端或服务器端使用,但如果一个客户端在两个线程中使用封送对象,并希望使用同一个socket同时发送,会发生什么情况?

      根据套接字库的文档,套接字不是线程安全的。因此,在客户端,您应该注意避免从多个线程使用同一个socket调用Send。您应该使用同步对象来序列化对Send函数的调用。

    2. 如果客户端从多个线程(使用同步对象)调用Send,并且每个线程都为同一个套接字调用Recv,那么它们如何正确地获得回复?拥有当前时间片的线程将首先接收到回复!!!因此,我在这里采用了一种好的技术来解决这个问题。
      1. 每个线程都应该在Send函数中将自己的唯一ID发送给服务器。
      2. 客户端应该在一个地方(线程)接收此套接字的回复。
      3. 所有线程都应该在Recv函数中挂起,等待来自公共地方(线程)的回复。
      4. 服务器应该在每个客户端回复前面加上客户端ID。

      这就是我在代码中所做的。

      1. Send函数中
        // create event to be used at the Recv
        m_hEvent = ::CreateEvent(NULL, FALSE, FALSE, NULL);
        // insert marshal pointer as a unique ID
        m_data.Insert(0, (int)this);
      2. 在客户端,我正在使用一个公共线程来从该套接字接收数据。
        void ClientRecv(void *lpv)
        {
            SOCKET sock = (SOCKET)lpv;
            CMarshal* pMarshal;
            try
            {    
                while(true)
                {
                    // recv client marshal pointer
                    if(recv(sock, (char*) & pMarshal, 
                             sizeof(int), 0) != sizeof(int))
                        break;
                    // check for version
                    if(pMarshal->m_fVer != 1)
                        continue;
                    // recv data using recieved marshal
                    if(pMarshal->RecvData(sock) > 0)
                        // set the marshal event to 
                        //let its thread continue execution
                        if(::SetEvent(pMarshal->m_hEvent) == false)
                            continue;// just to put breakpoint
                                      // for debuging
                }
            }
            catch(...)
            {
            }
        }
      3. 在客户端线程的Recv中,我使用了在Send函数中创建的事件来挂起。
        // check if m_hEvent initialized in the Send
        if(m_hEvent)
        {    // wait tell the recv thread fire my event
            if(::WaitForSingleObject(m_hEvent, 60000) == WAIT_TIMEOUT)
                return 0;
            ::CloseHandle(m_hEvent);
            m_hEvent = 0;
            return GetLength();
        }

源代码文件

  1. Marshal.cpp, Marshal.h
  2. Socket.cpp, Socket.h
  3. String.cpp, String.h
  4. mem.cpp, mem.h

感谢...

我非常感谢我的同事们在实现和测试这个模块时给予我的帮助。(JAK)

© . All rights reserved.