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

安卓与串口通信-modbus篇 Modbus 是一种应用层报文传输协议,由 Modicon 公司在 1979 年发布,是为了解决 PLC 通信而研发的协议。因为 Modbus 是开源的且无著作权要求、易于部署维护、可靠性强的特性,所以 Modbus 已经成为工业领域通信协议事实上的业界标准,并且现在是工业电子设备之间常用的连接方式。由于 Modbus 定义的只是应用层的报文协议,所以它可以使用串口(RS232、RS485)、以太网作为物理层接口。Modbus 分为三种传输模式:RTU、ASII、TCP。 阅读详情

粘包

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

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

分包

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

应用层数据包格式如下:

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

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(缓存两个数据包),这样就可以每次创建对象时一次分配好,减少内存分配次数,提高效率。(内存的分配和释放比内存的读写效率要低)

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

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编码,代码如下:

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主要代码如下:

  {* 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进行解包,然后获取命令分别调用不同的命令处理逻辑,代码如下:

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单元。

V1版下载地址:http://download.youkuaiyun.com/detail/sqldebug_fan/4510076,需要资源10分,有稳定性问题,可以作为研究稳定性用;

V2版下载地址:http://download.youkuaiyun.com/detail/sqldebug_fan/5560185,不需要资源分,解决了稳定性问题和提高性能;免责声明:此代码只是为了演示IOCP编程,仅用于学习和研究,切勿用于商业用途。水平有限,错误在所难免,欢迎指正和指导。邮箱地址:fansheng_hx@163.com。
Delphi的Socket编程步骤说明 比较详细介绍 ClientSocket 和ServerSocket 重要的属性: 1.client和server都有port属性,需要一致才能互相通信 2.client有Address属性,使用时填写对方(server)的IP地址 几个重要的事件: client: OnRead事件,当client受到冲击消息时在OnRead事件中可以获得server发送过来消息。 Server: O... 阅读详情

相关推荐

C#高性能大容量SOCKET并发(一):IOCP完成端口例子介绍

例子主要包括SocketAsyncEventArgs通讯封装、服务端实现日志查看、SCOKET列表、上传、下载、远程文件流、吞吐量协议,用于测试SocketAsyncEventArgs的性能和压力,最大连接数支持65535个长连接,最高命令交互速度达到250MB/S(使用的是127.0.0.1的方式,相当于千兆网卡1Gb=125MB/S两倍的吞吐量)。服务端用C#编写,并使用log4net作为日志模块。

SQLDebug_Fan的专栏 6万+

STM32实现基于RS485的简单的Modbus协议

这里用STM32实现,其实可以搬移到其他MCU,之前有项目使用STM32实现Modbus协议这个场景比较正常,很多时候都能碰到这里主要是Modbus和变频器通信最常见的是使用Modbus实现传感器数据的采集,我记得之前用过一些传感器都是Modbus协议这就需要MCU实现Modbus协议,不过实际使用的Modbus协议往往都是简化版本的可能只是几条Modbus协议格式的指令而已初学者,网上一搜Modubus协议,往往越看越糊涂。

大牛攻城狮的专栏 1万+

DELPHI高性能大容量SOCKET并发(九):稳定性问题解决

IOCP接收缓存导致的内存错乱 在用IOCP控件写了一个ERP服务器后,服务器会发生运行3天后,出现莫名的内存错误,用FastMM检测,是本没有内存错误的地方,而且内存错误出现的地方也不固定。这是一个不可重现的Bug,后续通过打日志把错误范围缩小后发现,每次出现内存错误之前都是由于有链接断开释放,因此就加了日志逐步定位到是TSocketHandle释放引起的,具体原因是:在IOCP中,每个S

SQLDebug_Fan的专栏 1万+

delphi 下一种对socket异步模式接收数据分包与粘包的处理方式原理及其代码

delphi 下一种对socket异步模式接收数据分包与粘包的处理方式原理及其代码 前言:     网络上传输的数据总是一份一份的,每一份数据称为一个完整的数据包。数据包由包头和包体(数据)构成。包头描述数据的性质,大小等相关属性。当我们用socket的recv()异步模式接收数据时,由于网络的差异性,复杂性,第一次的recv()接收往往不是一...

byc6352的专栏 1112

Delphi高性能大容量SOCKET并发完成端口例子完整实例源码(根据SQLDebug_Fan的源码移植)

例子主要包括IOCP通讯封装、服务端实现日志查看、SCOKET列表、上传、下载、远程文件流、吞吐量协议,用于测试IOCP的性能和压力,最大连接数支持65535个长连接,最高命令交互速度达到250MB/S(使用的是127.0.0.1的方式,相当于千兆网卡1Gb=125MB/S两倍的吞吐量)。使用log4net作为日志模块;同时支持65536个连接,网络吞吐量可以达到400M。完成端口 IOCP SOCKET socket 高性能。SQLDebug_Fan的源码客户端用的是indy9,我把它移植到indy10,方便学习和研究

json粘包处理 [Delphi]

本demo演示了 多个json包粘在一起的处理方法,用到了第三方库 QDAC

DELPHI高性能大容量SOCKET并发(二):IOCP完成端口控件封装

IOCP完成端口介绍: 完成端口模型是Windows平台下SOCKET端口模型最为复杂的一种I/O模型。如果一个应用程序需要同时管理为数众多的套接字,而且希望随着系统内安装的CPU数量的增多,应用程序的性能也可以线性提升,采用完成端口模型,往往可以达到最佳的系统性能。 完成端口可以管理成千上万的连接,长连接传文件可以支持5000个以上,长连接命令交互可以支持20000个以上。这么大并发的连

SQLDebug_Fan的专栏 1万+

DELPHI高性能大容量SOCKET并发完成端口例子完整实例源码

Delphi高性能大容量SOCKET并发完成端口例子完整实例源码

DELPHI高性能大容量SOCKET并发(一):IOCP完成端口例子介绍

例子主要包括IOCP控件封装、服务端实现、传输协议和日志、控制、SQL查询、上传、下载等协议实现,并包括一些初步的性能测试结果。 服务端: 界面截图如下: 提供服务和桌面方式运行,桌面方式可直接打开程序,方便日常调试,可以使用命令行注册或卸载服务,在CMD中输入D:\DEMO\IOCPDemo\Bin\IOCPDemoSvr.exe -install来注册服务,在CMD输入D:\DE

SQLDebug_Fan的专栏 3万+

基于STM32F407VET6的智能巡检小车灰度循迹系统优化设计

本文详细介绍了基于STM32F407VET6主控芯片的智能巡检小车灰度循迹系统优化设计。通过将传感器输出从数字量升级为模拟量以获取更丰富的路面信息,并采用PD控制算法结合速度管理策略,有效提升了小车在复杂弯道上的循迹稳定性与适应性。文章分享了硬件电路抗干扰设计、软件滤波及实战调优经验,为嵌入式开发者提供了从“能跑”到“跑得稳”的系统性解决方案。

weixin_28273593的博客 443

C# HttpClient 实战:3步绕过Cloudflare指纹拦截,获取完整网页内容

本文详细介绍了如何使用C# HttpClient绕过Cloudflare指纹拦截,获取完整网页内容的3步实战方案。通过完美复制浏览器请求头、智能管理Cookie容器以及处理动态挑战页面,开发者可以有效突破Cloudflare的反爬机制,适用于爬虫开发和数据采集项目。

weixin_31856057的博客 320

DELPHI高性能大容量SOCKET并发(三):接收、发送、缓存

接收 完成端口是结合重叠端口一起使用的,在接收数据之前,我们需要投递一个接收请求,使用function WSARecv(s: TSocket; lpBuffers: LPWSABUF; dwBufferCount: DWORD;  var lpNumberOfBytesRecvd, lpFlags: DWORD; lpOverlapped: LPWSAOVERLAPPED;  lpCompl

SQLDebug_Fan的专栏 1万+

Crossbar RRAM技术解析:从1TnR架构到存储革命

非易失性存储器(NVM)是数据存储的核心技术,其原理基于物理状态的稳定改变以实现数据持久化。阻变存储器(RRAM)作为一种新兴NVM,利用导电细丝在介质层中的形成与断裂实现数据存储,具有高速、低功耗和结构简单的优势。在工程实践中,RRAM面临潜行路径电流等挑战,而Crossbar提出的1TnR架构通过引入共享晶体管有效解决了这一问题,在保证性能的同时提升了集成密度。该技术与CMOS工艺兼容,支持后端集成与多层堆叠,为高密度存储和嵌入式应用提供了新的解决方案。结合当前行业对高能效、高密度存储的需求,RRAM在

weixin_42523104的博客 319

DELPHI高性能大容量SOCKET并发(五):锁和对象分离

锁和对象一起封装的危险 在多线程编写中,绝大多数编码风格喜欢把锁和要访问的对象封装在同一个对象,释放对象的时候也释放锁,这样会造成死锁。我们写一个测试例子,我们创建一个锁,把锁锁住,然后再创建一个线程,一直不停的等待锁返回,然后我们把锁释放,这时线程就死锁,代码如下: 定义接口: type TLockObject = class; TLockTestThread = clas

SQLDebug_Fan的专栏 7064

《Ventoy 制作系统安装 U 盘的具体步骤》

然后您可以在电脑的磁盘管理中对 U 盘进行快速格式化,选择 FAT32 或 NTFS 文件系统。至此,Ventoy 系统安装 U 盘就制作完成了。您可以在需要安装系统的电脑上,从 U 盘启动,然后选择相应的系统镜像进行安装。安装完成后,将您需要的操作系统镜像文件(如 Windows、Linux 等的 ISO 格式镜像)复制到 U 盘的根目录。在安装程序中,选择您插入的 U 盘设备,确保选择正确,以免误操作。2. 确认选择的 U 盘盘符正确,防止误操作导致其他磁盘数据丢失。点击“安装”按钮,等待安装完成。

jiyiwangluokeji的博客 2666

三菱PLC通信库大比拼:手把手教你用C#实现最高效的McProtocol数据读写

本文深入比较了三菱PLC通信库的性能,重点介绍了如何使用C#高效实现McProtocol数据读写。通过实测数据对比HslCommunication、McProtocol-next和自定义方案的优劣,提供优化技巧和代码示例,帮助开发者在工业自动化场景中提升通信效率。

m0n1o2p的博客 255

DELPHI高性能大容量SOCKET并发(六):协议字符集

UTF-8 UTF-8是UNICODE的一种变长字符编码又称万国码,由Ken Thompson于1992年创建。现在已经标准化为RFC 3629。UTF-8用1到6个字节编码UNICODE字符。用在网页上可以同一页面显示中文简体繁体及其它语言(如日文,韩文)。 使用UTF-8的好处是现在一些手机平台都是使用UTF-8,另外在一些嵌入式平台,如果不支持中文,只支持英文,可以不转换,UTF-8就可

SQLDebug_Fan的专栏 6017

SAPFiori Elements Analytical List Page 概述

ALP(Analytical List Page)是SAP Fiori提供的一种复合页面类型,结合了分析(图表/KPI)与事务(表格/操作)功能,适用于需要分析后立即执行处理的业务场景(如采购价格监控、销售逾期处理等)。其核心价值在于让用户在同一页面完成从数据洞察到事务处理的闭环,减少应用切换带来的效率损耗。 页面结构包含页头KPI标签、交互式图表和明细表格,通过联动过滤、选择同步等机制实现数据钻取与即时操作。支持标准过滤和可视化过滤,并依赖OData注解(SelectionVariant/Presenta

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 268

DELPHI高性能大容量SOCKET并发(十):IOCP完成端口性能优化

IOCP性能优化主要是集中在每个处理接收数据和发送数据的对象锁,如果能降低锁的调用次数和提高锁的效率,对IOCP的整理效率和吞吐量都非常有帮助。有很多开发人员在优化IOCP的时候,对于如何提高锁的效率,有使用原子操作来加锁,这样做的效率比较调用Windows的锁效率,因而吞吐率也更高。我们不讨论提高锁的效率,我们这次的优化是降低锁的调用次数。 服务端优化 IOCP是异步线程通知方式,有

SQLDebug_Fan的专栏 9931

Vue3 - Element Plus 全局修改更换主题色配置详细教程,element-plus自定义主题色全局替换主色调强力解决方案,支持动态修改主题颜色/动态皮肤,将默认蓝色主题色更换为其他颜色!

vue3,nuxt3,element-plus,组件库,主题色,修改主题颜色,更换主题色,如何修改组件默认主题蓝色,项目主题色定制,修改失败,主题色覆盖不了,:root直接修改,hover颜色没改,自定义主题色,vue3全局设置elementPlus主题色最新详细修改教程,怎么修改全局的颜色,scss变量,覆盖原来的主题色改为自己的颜色,统一替换,有没有好办法,颜色不变,失败,无效,动态主题切换,主题颜色怎么在项目里直接改,plus主题色更换,覆盖默认主题色,最强最好用,设置主题色,换肤,皮肤,动态更换

🏆 前端领域优质创作者 1万+

某验SVG图标验证码识别思路(非训练)

本文提出了一种无需训练的某验SVG图标验证码识别方法。通过分析SVG文件结构,使用正则表达式提取静态图标数据,并借助CairoSVG库转换为PNG格式。随后采用坐标定位方式裁剪出12个固定位置的图标,最后利用ResNet50提取特征,通过余弦相似度匹配目标图标。该方法避免了训练目标检测模型,实现了高效准确的验证码识别。关键步骤包括SVG文件预处理、图标定位裁剪和基于预训练模型的相似度比对。

九月镇灵将的博客 417

DELPHI高性能大容量SOCKET并发(七):通讯协议

协议种类 开发Socket程序有两种协议类型,一种是用文本描述的,类似HTTP协议,定义字符集,好处是兼容性和调试方便,缺点是解析文本会损耗一些性能;一种是用Code加结构体,定义字节顺序,好处是性能高,缺点是兼容性和调试不方便。这个可以根据应用场景灵活选择,如果您的应用相对稳定,需求变化少,性能要求高,则可以使用Code加结构体的方式。如果您的应用需要不停的扩充功能,但是对性能要求不苛刻,

SQLDebug_Fan的专栏 6730

Probability and Statistics - 4th edition

Probability and Statistics - 4th edition

DELPHI高性能大容量SOCKET并发(八):断点续传

断点续传 断点续传主要是用在上传或下载文件,一般做法是开始上传的时候,服务器返回上次已经上传的大小,如果上传完成,则返回-1;下载开始的时候,由客户端上报本地已经下载大小,服务器根据位置信息下发数据,因此上传下载协议都需要带Size大小,例如我们协议格式。 上传开始: 客户端->服务器 { [Request] Command=Upload Dir=Dir

SQLDebug_Fan的专栏 5744

复分析 阿尔福斯(Ahlfors) 第三版答案 英文

答案分为十章,有部分习题没有答案,全英文复分析阿尔福斯(Ahlfors) 第三版答案

精雕软件(JDpaint) 5.5 绿色版

精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版

上一篇: DELPHI高性能大容量SOCKET并发(三):接收、发送、缓存
下一篇: DELPHI高性能大容量SOCKET并发(五):锁和对象分离
SQLDebug_Fan
博客等级 码龄17年 710粉丝 26原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值