高性能大容量SOCKET并发(四):粘包、分包、解包

粘包

使用TCP长连接就会引入粘包的问题,粘包是指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。粘包可能由发送方造成,也可能由接收方造成。TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一包数据,造成多个数据包的粘连。如果接收进程不及时接收数据,已收到的数据就放在系统接收缓冲区,用户进程读取数据时就可能同时读到多个数据包。

粘包一般的解决办法是制定通讯协议,由协议来规定如何分包解包。

分包

在IOCPDemo例子程序中,我们分包的逻辑是先发一个长度,然后紧接着是数据包内容,这样就可以把每个包分开。

应用层数据包格式如下:

应用层数据包格式 
数据包长度Len:Cardinal(4字节无符号整数)数据包内容,长度为Len
IOCPSocket分包处理主要代码,我们收到的数据都是在TSocketHandle.ProcessIOComplete方法中处理:

  1. procedure TSocketHandle.ProcessIOComplete(AIocpRecord: PIocpRecord;  
  2.   const ACount: Cardinal);  
  3. begin  
  4.   case AIocpRecord.IocpOperate of  
  5.     ioNone: Exit;  
  6.     ioRead: //收到数据   
  7.     begin  
  8.       FActiveTime := Now;  
  9.       ReceiveData(AIocpRecord.WsaBuf.buf, ACount);  
  10.       if FConnected then  
  11.         PreRecv(AIocpRecord); //投递请求   
  12.     end;  
  13.     ioWrite: //发送数据完成,需要释放AIocpRecord的指针   
  14.     begin  
  15.       FActiveTime := Now;  
  16.       FSendOverlapped.Release(AIocpRecord);  
  17.     end;  
  18.     ioStream:  
  19.     begin  
  20.       FActiveTime := Now;  
  21.       FSendOverlapped.Release(AIocpRecord);  
  22.       WriteStream; //继续发送流   
  23.     end;  
  24.   end;  
  25. end;  
procedure TSocketHandle.ProcessIOComplete(AIocpRecord: PIocpRecord;
  const ACount: Cardinal);
begin
  case AIocpRecord.IocpOperate of
    ioNone: Exit;
    ioRead: //收到数据
    begin
      FActiveTime := Now;
      ReceiveData(AIocpRecord.WsaBuf.buf, ACount);
      if FConnected then
        PreRecv(AIocpRecord); //投递请求
    end;
    ioWrite: //发送数据完成,需要释放AIocpRecord的指针
    begin
      FActiveTime := Now;
      FSendOverlapped.Release(AIocpRecord);
    end;
    ioStream:
    begin
      FActiveTime := Now;
      FSendOverlapped.Release(AIocpRecord);
      WriteStream; //继续发送流
    end;
  end;
end;
如果是收到数据,则调用ReceiveData函数,ReceiveData主要功能是把数据的写入流中,然后调用Process分包。FInputBuf是一个内存流(FInputBuf: TMemoryStream),内存流的每次写入会造成一次内存分配,如果要获得更高的效率,可以替换为内存池等更好的内存管理方式。还有一种更好的解决方案是规定每次发包的大小,如每个包最大不超过64K,哪么缓冲区的最大大小可以设置为128K(缓存两个数据包),这样就可以每次创建对象时一次分配好,减少内存分配次数,提高效率。(内存的分配和释放比内存的读写效率要低)

  1. procedure TSocketHandle.ReceiveData(AData: PAnsiChar; const ALen: Cardinal);  
  2. begin  
  3.   FInputBuf.Write(AData^, ALen);  
  4.   Process;  
  5. end;  
procedure TSocketHandle.ReceiveData(AData: PAnsiChar; const ALen: Cardinal);
begin
  FInputBuf.Write(AData^, ALen);
  Process;
end;
Process则根据收到的数据进行分包逻辑,如果不够一个包,则继续等待接收数据,如果够一个或多个包,则循环调用Execute函数进行处理,代码如下:

  1. procedure TSocketHandle.Process;  
  2. var  
  3.   AData, ALast, NewBuf: PByte;  
  4.   iLenOffset, iOffset, iReserveLen: Integer;  
  5.   
  6.   function ReadLen: Integer;  
  7.   var  
  8.     wLen: Word;  
  9.     cLen: Cardinal;  
  10.   begin  
  11.     FInputBuf.Position := iOffset;  
  12.     if FLenType = ltWord then  
  13.     begin  
  14.       FInputBuf.Read(wLen, SizeOf(wLen));  
  15.       //wLen := ntohs(wLen);   
  16.       Result := wLen;  
  17.     end  
  18.     else  
  19.     begin  
  20.       FInputBuf.Read(cLen, SizeOf(cLen));  
  21.       //cLen := ntohl(cLen);   
  22.       Result := cLen;  
  23.     end;  
  24.   end;  
  25. begin  
  26.   case FLenType of  
  27.     ltWord, ltCardinal:  
  28.     begin  
  29.       if FLenType = ltWord then  
  30.         iLenOffset := 2  
  31.       else  
  32.         iLenOffset := 4;  
  33.       iReserveLen := 0;  
  34.       FPacketLen := 0;  
  35.       iOffset := 0;  
  36.       if FPacketLen <= 0 then  
  37.       begin  
  38.         if FInputBuf.Size < iLenOffset then Exit;  
  39.         FInputBuf.Position := 0//移动到最前面   
  40.         FPacketLen := ReadLen;  
  41.         iOffset := iLenOffset;  
  42.         iReserveLen := FInputBuf.Size - iOffset;  
  43.         if FPacketLen > iReserveLen then //不够一个包的长度   
  44.         begin  
  45.           FInputBuf.Position := FInputBuf.Size; //移动到最后,以便接收后续数据   
  46.           FPacketLen := 0;  
  47.           Exit;  
  48.         end;  
  49.       end;  
  50.       while (FPacketLen > 0and (iReserveLen >= FPacketLen) do //如果数据够长,则处理   
  51.       begin //多个包循环处理   
  52.         AData := Pointer(Longint(FInputBuf.Memory) + iOffset); //取得当前的指针   
  53.         Execute(AData, FPacketLen);  
  54.         iOffset := iOffset + FPacketLen; //移到下一个点   
  55.         FPacketLen := 0;  
  56.         iReserveLen := FInputBuf.Size - iOffset;  
  57.         if iReserveLen > iLenOffset then //剩下的数据   
  58.         begin  
  59.           FPacketLen := ReadLen;  
  60.           iOffset := iOffset + iLenOffset;  
  61.           iReserveLen := FInputBuf.Size - iOffset;  
  62.           if FPacketLen > iReserveLen then //不够一个包的长度,需要把长度回退   
  63.           begin  
  64.             iOffset := iOffset - iLenOffset;  
  65.             iReserveLen := FInputBuf.Size - iOffset;  
  66.             FPacketLen := 0;  
  67.           end;  
  68.         end  
  69.         else //不够长度字节数   
  70.           FPacketLen := 0;  
  71.       end;  
  72.       if iReserveLen > 0 then //把剩下的自己缓存起来   
  73.       begin  
  74.         ALast := Pointer(Longint(FInputBuf.Memory) + iOffset);  
  75.         GetMem(NewBuf, iReserveLen);  
  76.         try  
  77.           CopyMemory(NewBuf, ALast, iReserveLen);  
  78.           FInputBuf.Clear;  
  79.           FInputBuf.Write(NewBuf^, iReserveLen);  
  80.         finally  
  81.           FreeMemory(NewBuf);  
  82.         end;  
  83.       end  
  84.       else  
  85.       begin  
  86.         FInputBuf.Clear;  
  87.       end;  
  88.     end;  
  89.   else  
  90.     begin  
  91.       FInputBuf.Position := 0;  
  92.       AData := Pointer(Longint(FInputBuf.Memory)); //取得当前的指针   
  93.       Execute(AData, FInputBuf.Size);  
  94.       FInputBuf.Clear;  
  95.     end;  
  96.   end;  
  97. end;  
procedure TSocketHandle.Process;
var
  AData, ALast, NewBuf: PByte;
  iLenOffset, iOffset, iReserveLen: Integer;

  function ReadLen: Integer;
  var
    wLen: Word;
    cLen: Cardinal;
  begin
    FInputBuf.Position := iOffset;
    if FLenType = ltWord then
    begin
      FInputBuf.Read(wLen, SizeOf(wLen));
      //wLen := ntohs(wLen);
      Result := wLen;
    end
    else
    begin
      FInputBuf.Read(cLen, SizeOf(cLen));
      //cLen := ntohl(cLen);
      Result := cLen;
    end;
  end;
begin
  case FLenType of
    ltWord, ltCardinal:
    begin
      if FLenType = ltWord then
        iLenOffset := 2
      else
        iLenOffset := 4;
      iReserveLen := 0;
      FPacketLen := 0;
      iOffset := 0;
      if FPacketLen <= 0 then
      begin
        if FInputBuf.Size < iLenOffset then Exit;
        FInputBuf.Position := 0; //移动到最前面
        FPacketLen := ReadLen;
        iOffset := iLenOffset;
        iReserveLen := FInputBuf.Size - iOffset;
        if FPacketLen > iReserveLen then //不够一个包的长度
        begin
          FInputBuf.Position := FInputBuf.Size; //移动到最后,以便接收后续数据
          FPacketLen := 0;
          Exit;
        end;
      end;
      while (FPacketLen > 0) and (iReserveLen >= FPacketLen) do //如果数据够长,则处理
      begin //多个包循环处理
        AData := Pointer(Longint(FInputBuf.Memory) + iOffset); //取得当前的指针
        Execute(AData, FPacketLen);
        iOffset := iOffset + FPacketLen; //移到下一个点
        FPacketLen := 0;
        iReserveLen := FInputBuf.Size - iOffset;
        if iReserveLen > iLenOffset then //剩下的数据
        begin
          FPacketLen := ReadLen;
          iOffset := iOffset + iLenOffset;
          iReserveLen := FInputBuf.Size - iOffset;
          if FPacketLen > iReserveLen then //不够一个包的长度,需要把长度回退
          begin
            iOffset := iOffset - iLenOffset;
            iReserveLen := FInputBuf.Size - iOffset;
            FPacketLen := 0;
          end;
        end
        else //不够长度字节数
          FPacketLen := 0;
      end;
      if iReserveLen > 0 then //把剩下的自己缓存起来
      begin
        ALast := Pointer(Longint(FInputBuf.Memory) + iOffset);
        GetMem(NewBuf, iReserveLen);
        try
          CopyMemory(NewBuf, ALast, iReserveLen);
          FInputBuf.Clear;
          FInputBuf.Write(NewBuf^, iReserveLen);
        finally
          FreeMemory(NewBuf);
        end;
      end
      else
      begin
        FInputBuf.Clear;
      end;
    end;
  else
    begin
      FInputBuf.Position := 0;
      AData := Pointer(Longint(FInputBuf.Memory)); //取得当前的指针
      Execute(AData, FInputBuf.Size);
      FInputBuf.Clear;
    end;
  end;
end;

解包

由于我们应用层数据包既可以传命令也可以传数据,因而针对每个包我们进行解包,分出命令和数据分别处理,因而每个Socket服务对象都需要解包,我们解包的逻辑是放在TBaseSocket.DecodePacket中,命令和数据的包格式为:

命令长度Len:Cardinal(4字节无符号整数)命令数据
这里和第一版公布的代码不同,这版的代码对命令进行了编码,采用UTF-8编码,代码如下:

  1. function TBaseSocket.DecodePacket(APacketData: PByte;  
  2.   const ALen: Integer): Boolean;  
  3. var  
  4.   CommandLen: Integer;  
  5.   UTF8Command: UTF8String;  
  6. begin  
  7.   if ALen > 4 then //命令长度为4字节,因而长度必须大于4   
  8.   begin  
  9.     CopyMemory(@CommandLen, APacketData, SizeOf(Cardinal)); //获取命令长度   
  10.     Inc(APacketData, SizeOf(Cardinal));  
  11.     SetLength(UTF8Command, CommandLen);  
  12.     CopyMemory(PUTF8String(UTF8Command), APacketData, CommandLen); //读取命令   
  13.     Inc(APacketData, CommandLen);  
  14.     FRequestData := APacketData; //数据   
  15.     FRequestDataLen := ALen - SizeOf(Cardinal) - CommandLen; //数据长度   
  16.     FRequest.Text := Utf8ToAnsi(UTF8Command); //把UTF8转为Ansi   
  17.     Result := True;  
  18.   end  
  19.   else  
  20.     Result := False;   
  21. end;  
function TBaseSocket.DecodePacket(APacketData: PByte;
  const ALen: Integer): Boolean;
var
  CommandLen: Integer;
  UTF8Command: UTF8String;
begin
  if ALen > 4 then //命令长度为4字节,因而长度必须大于4
  begin
    CopyMemory(@CommandLen, APacketData, SizeOf(Cardinal)); //获取命令长度
    Inc(APacketData, SizeOf(Cardinal));
    SetLength(UTF8Command, CommandLen);
    CopyMemory(PUTF8String(UTF8Command), APacketData, CommandLen); //读取命令
    Inc(APacketData, CommandLen);
    FRequestData := APacketData; //数据
    FRequestDataLen := ALen - SizeOf(Cardinal) - CommandLen; //数据长度
    FRequest.Text := Utf8ToAnsi(UTF8Command); //把UTF8转为Ansi
    Result := True;
  end
  else
    Result := False; 
end;
具体每个协议可以集成Execute方法,调用DecodePacket进行解包,然后根据命令进行协议逻辑处理,例如TSQLSocket主要代码如下:

  1. {* SQL查询SOCKET基类 *}  
  2. TSQLSocket = class(TBaseSocket)  
  3. private  
  4.   {* 开始事务创建TADOConnection,关闭事务时释放 *}  
  5.   FBeginTrans: Boolean;  
  6.   FADOConn: TADOConnection;  
  7. protected  
  8.   {* 处理数据接口 *}  
  9.   procedure Execute(AData: PByte; const ALen: Cardinal); override;  
  10.   {* 返回SQL语句执行结果 *}  
  11.   procedure DoCmdSQLOpen;  
  12.   {* 执行SQL语句 *}  
  13.   procedure DoCmdSQLExec;  
  14.   {* 开始事务 *}  
  15.   procedure DoCmdBeginTrans;  
  16.   {* 提交事务 *}  
  17.   procedure DoCmdCommitTrans;  
  18.   {* 回滚事务 *}  
  19.   procedure DoCmdRollbackTrans;  
  20. public  
  21.   procedure DoCreate; override;  
  22.   destructor Destroy; override;  
  23.   {* 获取SQL语句 *}  
  24.   function GetSQL: string;  
  25.   property BeginTrans: Boolean read FBeginTrans;  
  26. end;  
  {* SQL查询SOCKET基类 *}
  TSQLSocket = class(TBaseSocket)
  private
    {* 开始事务创建TADOConnection,关闭事务时释放 *}
    FBeginTrans: Boolean;
    FADOConn: TADOConnection;
  protected
    {* 处理数据接口 *}
    procedure Execute(AData: PByte; const ALen: Cardinal); override;
    {* 返回SQL语句执行结果 *}
    procedure DoCmdSQLOpen;
    {* 执行SQL语句 *}
    procedure DoCmdSQLExec;
    {* 开始事务 *}
    procedure DoCmdBeginTrans;
    {* 提交事务 *}
    procedure DoCmdCommitTrans;
    {* 回滚事务 *}
    procedure DoCmdRollbackTrans;
  public
    procedure DoCreate; override;
    destructor Destroy; override;
    {* 获取SQL语句 *}
    function GetSQL: string;
    property BeginTrans: Boolean read FBeginTrans;
  end;
Exceute是调用DecodePacket进行解包,然后获取命令分别调用不同的命令处理逻辑,代码如下:

  1. procedure TSQLSocket.Execute(AData: PByte; const ALen: Cardinal);  
  2. var  
  3.   sErr: string;  
  4. begin  
  5.   inherited;  
  6.   FRequest.Clear;  
  7.   FResponse.Clear;  
  8.   try  
  9.     AddResponseHeader;  
  10.     if ALen = 0 then  
  11.     begin  
  12.       DoFailure(CIPackLenError);  
  13.       DoSendResult;  
  14.       Exit;  
  15.     end;  
  16.     if DecodePacket(AData, ALen) then  
  17.     begin  
  18.       FResponse.Clear;  
  19.       AddResponseHeader;  
  20.       case StrToSQLCommand(Command) of  
  21.         scLogin:  
  22.         begin  
  23.           DoCmdLogin;  
  24.           DoSendResult;  
  25.         end;  
  26.         scActive:  
  27.         begin  
  28.           DoSuccess;  
  29.           DoSendResult;  
  30.         end;  
  31.         scSQLOpen:  
  32.         begin  
  33.           DoCmdSQLOpen;  
  34.         end;  
  35.         scSQLExec:  
  36.         begin  
  37.           DoCmdSQLExec;  
  38.           DoSendResult;  
  39.         end;  
  40.         scBeginTrans:  
  41.         begin  
  42.           DoCmdBeginTrans;  
  43.           DoSendResult;  
  44.         end;  
  45.         scCommitTrans:  
  46.         begin  
  47.           DoCmdCommitTrans;  
  48.           DoSendResult;  
  49.         end;  
  50.         scRollbackTrans:  
  51.         begin  
  52.           DoCmdRollbackTrans;  
  53.           DoSendResult;  
  54.         end;  
  55.       else  
  56.         DoFailure(CINoExistCommand, 'Unknow Command');  
  57.         DoSendResult;  
  58.       end;  
  59.     end  
  60.     else  
  61.     begin  
  62.       DoFailure(CIPackFormatError, 'Packet Must Include \r\n\r\n');  
  63.       DoSendResult;  
  64.     end;  
  65.   except  
  66.     on E: Exception do //发生未知错误,断开连接   
  67.     begin  
  68.       sErr := RemoteAddress + ':' + IntToStr(RemotePort) + CSComma + 'Unknow Error: ' + E.Message;  
  69.       WriteLogMsg(ltError, sErr);  
  70.       Disconnect;  
  71.     end;  
  72.   end;  
  73. end;  
procedure TSQLSocket.Execute(AData: PByte; const ALen: Cardinal);
var
  sErr: string;
begin
  inherited;
  FRequest.Clear;
  FResponse.Clear;
  try
    AddResponseHeader;
    if ALen = 0 then
    begin
      DoFailure(CIPackLenError);
      DoSendResult;
      Exit;
    end;
    if DecodePacket(AData, ALen) then
    begin
      FResponse.Clear;
      AddResponseHeader;
      case StrToSQLCommand(Command) of
        scLogin:
        begin
          DoCmdLogin;
          DoSendResult;
        end;
        scActive:
        begin
          DoSuccess;
          DoSendResult;
        end;
        scSQLOpen:
        begin
          DoCmdSQLOpen;
        end;
        scSQLExec:
        begin
          DoCmdSQLExec;
          DoSendResult;
        end;
        scBeginTrans:
        begin
          DoCmdBeginTrans;
          DoSendResult;
        end;
        scCommitTrans:
        begin
          DoCmdCommitTrans;
          DoSendResult;
        end;
        scRollbackTrans:
        begin
          DoCmdRollbackTrans;
          DoSendResult;
        end;
      else
        DoFailure(CINoExistCommand, 'Unknow Command');
        DoSendResult;
      end;
    end
    else
    begin
      DoFailure(CIPackFormatError, 'Packet Must Include \r\n\r\n');
      DoSendResult;
    end;
  except
    on E: Exception do //发生未知错误,断开连接
    begin
      sErr := RemoteAddress + ':' + IntToStr(RemotePort) + CSComma + 'Unknow Error: ' + E.Message;
      WriteLogMsg(ltError, sErr);
      Disconnect;
    end;
  end;
end;

更详细代码见示例代码的IOCPSocket单元。

下载地址:http://download.csdn.net/detail/sqldebug_fan/4510076

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值