資料接收中粘包及半包的處理

工程師WWW發表於2013-11-04

在使用TCP協議的網路應用中,不可避免需要處理的一個問題就是半包和粘包的情況。
    一種做法是在接收端設一個比較大的緩衝區,先將收到的資料包都放到緩衝區中,然後從該緩衝區中選取完整的資料包出來。該緩衝區的實現可以使用環形緩衝區進行優化,避免頻繁的資料移動。使用該方法的一個描述見http://www.vckbase.com/document/viewdoc/?id=1203


    另外一種做法就是在接收的時候就只接收完整包。這要求資料包有固定的包頭結構體,其中還要包含資料包的長度資訊。在服務端接收的時候,先接收該包頭資料,然後再接收指定長度的資料體。

    在ACE中,用於儲存訊息的ACE_Message_Block有一個重要的特性:複合。即將多條訊息連線在一起,形成一個單連結串列。這樣便可以將先收到的包頭和後收到的包體連成一個複合體,而不用建一個大的資料包,將兩個Message_Block拷貝進去。


    下面的示例採用ACE的Proactor框架完成,實現了伺服器端半包及粘包的處理,以及ACE_Message_Block的複合,網路IO與邏輯處理的分離。

 

    在Proactor框架中,接收新連線後,會初始化一個讀請求,此時只要求讀包頭長度的資料:
void init_read_stream()
{
    ACE_NEW_NORETURN (recv_data_, ACE_Message_Block (sizeof(PacketHeader), MB_NORMAL_PACKET));
    ACE_HANDLE handle = this->handle ();
    this->recv_data_->copy ((const char *)&handle, sizeof(ACE_HANDLE));
    this->reader_.read (*recv_data_, recv_data_->space ());
}
    這裡由於使用了網路IO與邏輯處理分執行緒處理的方式,遞交給邏輯執行緒的資料包前面還加上了標識網路連線的handle,用以告訴邏輯執行緒該資料包是哪個客戶端連線發上來的。

遞交給邏輯執行緒的資料包頭結構為:
struct PacketHeader
{
 ACE_HANDLE handler;
 int data_length;
};
其中data_length就是從接收到的資料包中獲取到的。


    對於粘包的情況比較容易處理,
先收了包頭後再接收指定長度的資料包,多餘的資料由下次再讀取。
    半包情況稍微複雜一點,每個資料包是分兩次接收的,兩次接收的時候都有可能接收不完全。

當接收包頭不完全時所做的處理是繼續提交讀請求,讀的資料長度為剩餘的包頭長度
if (this->recv_data_->length() < sizeof(PacketHeader))
{
    // 資料包長度資訊還未接收完
    this->reader_.read (*recv_data_, recv_data_->space ());
    return;
}

當包頭接收完後,新建一個Message_Block,長度為需要接收的資料體長度,並將該Message_Block連結到包頭後
PacketHeader * hdr = reinterpret_cast<PacketHeader *> (this->recv_data_->rd_ptr());
ACE_Message_Block * data_mb = this->recv_data_->cont();
if (!data_mb)
{
    // 剛剛接收完長度資訊
    ACE_NEW (data_mb, ACE_Message_Block(hdr->data_length));
    this->recv_data_->cont (data_mb);
}

如果該資料包的包體接收完全,則將該完整的資料包傳送到邏輯執行緒的訊息佇列,然後初始化一個新的接收請求
if (data_mb->length () == hdr->data_length)
{
    // 資料已接收完
    // 再繼續接收下一個資料包
    logic_thread->putq (recv_data_);
    this->init_read_stream();
    return;
}

否則表示資料體還未接收完全,處理方法也是繼續提交剩餘資料的讀請求
this->reader_.read (*data_mb, data_mb->space ());
直接該資料包讀取完全。


資料包接收處理函式的完整實現為:
virtual void handle_read_stream (const ACE_Asynch_Read_Stream::Result &result)
{
 ACE_Message_Block &mb = result.message_block ();
 if (!result.success () || result.bytes_transferred () == 0)
 {
  mb.release ();
  delete this;
 }
 else
 {
  if (this->recv_data_->length() < sizeof(PacketHeader))
  {
   // 資料包長度資訊還未接收完
   this->reader_.read (*recv_data_, recv_data_->space ());
   return;
  }

  PacketHeader * hdr = reinterpret_cast<PacketHeader *> (this->recv_data_->rd_ptr());
  ACE_Message_Block * data_mb = this->recv_data_->cont();
  if (!data_mb)
  {
   // 剛剛接收完長度資訊
   ACE_NEW (data_mb, ACE_Message_Block(hdr->data_length));
   this->recv_data_->cont (data_mb);
  }

  if (data_mb->length () == hdr->data_length)
  {
   // 資料已接收完
   // 再繼續接收下一個資料包
   logic_thread->putq (recv_data_);
   this->init_read_stream();
   return;
  }

  // 否則繼續接收該資料包
  this->reader_.read (*data_mb, data_mb->space ());
 }
}


完整的伺服器實現和模擬半包及粘包情況的客戶端程式碼見
http://helloqinglan.googlepages.com/repack.rar
伺服器僅實現了最簡單的資料接收功能,為精減程式碼,未做錯誤檢查 

相關文章