手把手教你玩转SOCKET模型之重叠I/O篇(下)

四。     实现重叠模型的步骤

作了这么多的准备工作,费了这么多的笔墨,我们终于可以开始着手编码了。其实慢慢的你就会明白,要想透析重叠结构的内部原理也许是要费点功夫,但是只是学会如何来使用它,却是真的不难,唯一需要理清思路的地方就是和大量的客户端交互的情况下,我们得到事件通知以后,如何得知是哪一个重叠操作完成了,继而知道究竟该对哪一个套接字进行处理,应该去哪个缓冲区中的取得数据,everything will be OK^_^

下面我们配合代码,来一步步的讲解如何亲手完成一个重叠模型。

第一步定义变量…………

#define DATA_BUFSIZE     4096          // 接收缓冲区大小
   
   
SOCKET         ListenSocket,             // 监听套接字
   
   
AcceptSocket;             // 与客户端通信的套接字
   
   
WSAOVERLAPPED  AcceptOverlapped;     // 重叠结构一个
   
   
WSAEVENT  EventArray[WSA_MAXIMUM_WAIT_EVENTS];  
   
   
// 用来通知重叠操作完成的事件句柄数组
   
   
WSABUF     DataBuf[DATA_BUFSIZE] ;      
   
   
DWORD     dwEventTotal = 0,            // 程序中事件的总数
   
   
             dwRecvBytes = 0,            // 接收到的字符长度
   
   
                   Flags = 0;                    // WSARecv的参数
   
   

   
   
    
     
   
   

 

【第二步】创建一个套接字,开始在指定的端口上监听连接请求

和其他的SOCKET初始化全无二致,直接照搬即可,在此也不多费唇舌了,需要注意的是为了一目了然,我去掉了错误处理,平常可不要这样啊,尽管这里出错的几率比较小。

WSADATA wsaData;
   
   
WSAStartup(MAKEWORD(2,2),&wsaData);
   
   

   
   
    
     
   
   
ListenSocket = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);  //创建TCP套接字
   
   

   
   
    
     
   
   
SOCKADDR_IN ServerAddr;                           //分配端口及协议族并绑定
   
   
ServerAddr.sin_family=AF_INET;                                
   
   
ServerAddr.sin_addr.S_un.S_addr  =htonl(INADDR_ANY);          
   
   
ServerAddr.sin_port=htons(11111);
   
   

   
   
    
     
   
   
bind(ListenSocket,(LPSOCKADDR)&ServerAddr, sizeof(ServerAddr)); // 绑定套接字
   
   

   
   
    
     
   
   
listen(ListenSocket, 5);                                   //开始监听
   
   

 

【第三步】接受一个入站的连接请求

  一个accept就完了,都是一样一样一样一样的啊~~~~~~~~~~

 至于AcceptEx的使用,在完成端口中我会讲到,这里就先不一次灌输这么多了,不消化啊^_^

 AcceptSocket = accept (ListenSocket, NULL,NULL) ; 
   
   

当然,这里是我偷懒,如果想要获得连入客户端的信息(记得论坛上也常有人问到),accept的后两个参数就不要用NULL,而是这样

SOCKADDR_IN ClientAddr;                   // 定义一个客户端得地址结构作为参数
   
   
int addr_length=sizeof(ClientAddr);
   
   
AcceptSocket = accept(ListenSocket,(SOCKADDR*)&ClientAddr, &addr_length);
   
   
// 于是乎,我们就可以轻松得知连入客户端的信息了
   
   
LPCTSTR lpIP =  inet_ntoa(ClientAddr.sin_addr);      // IP
   
   
UINT nPort = ClientAddr.sin_port;                      // Port
   
   

 

【第四步】建立并初始化重叠结构

为连入的这个套接字新建立一个WSAOVERLAPPED重叠结构,并且象前面讲到的那样,为这个重叠结构从事件句柄数组里挑出一个空闲的对象句柄“绑定”上去。

// 创建一个事件
   
   
// dwEventTotal可以暂时先作为Event数组的索引
   
   
EventArray[dwEventTotal] = WSACreateEvent();      
   
   

   
   
    
     
   
   
ZeroMemory(&AcceptOverlapped, sizeof(WSAOVERLAPPED));      // 置零
   
   
AcceptOverlapped.hEvent = EventArray[dwEventTotal];            // 关联事件
   
   

   
   
    
     
   
   
char buffer[DATA_BUFSIZE];
   
   
ZeroMemory(buffer, DATA_BUFSIZE);
   
   
DataBuf.len = DATA_BUFSIZE;
   
   
DataBuf.buf = buffer;                          // 初始化一个WSABUF结构
   
   
dwEventTotal ++;                              // 总数加一
   
   

 

【第五步】以WSAOVERLAPPED结构为参数,在套接字上投递WSARecv请求

各个变量都已经初始化OK以后,我们就可以开始Socket操作了,然后让WSAOVERLAPPED结构来替我们管理I/O 请求,我们只用等待事件的触发就OK了。

if(WSARecv(AcceptSocket ,&DataBuf,1,&dwRecvBytes,&Flags,
   
   
                                        & AcceptOverlapped, NULL) == SOCKET_ERROR)
   
   
{ 
   
   
   // 返回WSA_IO_PENDING是正常情况,表示IO操作正在进行,不能立即完成
   
   
   // 如果不是WSA_IO_PENDING错误,就大事不好了~~~~~~!!!
   
   
      if(WSAGetLastError() != WSA_IO_PENDING)    
   
   
      {
   
   
                 // 那就只能关闭大吉了
   
   
                         closesocket(AcceptSocket);
   
   
                         WSACloseEvent(EventArray[dwEventTotal]);
   
   
         }
   
   
}
   
   

 

【第六步】 WSAWaitForMultipleEvents函数等待重叠操作返回的结果

  我们前面已经给WSARecv关联的重叠结构赋了一个事件对象句柄,所以我们这里要等待事件对象的触发与之配合,而且需要根据WSAWaitForMultipleEvents函数的返回值来确定究竟事件数组中的哪一个事件被触发了,这个函数的用法及返回值请参考前面的基础知识部分。

DWORD dwIndex;
   
   
// 等候重叠I/O调用结束
   
   
// 因为我们把事件和Overlapped绑定在一起,重叠操作完成后我们会接到事件通知
   
   
dwIndex = WSAWaitForMultipleEvents(dwEventTotal, 
   
   
EventArray ,FALSE ,WSA_INFINITE,FALSE);
   
   
// 注意这里返回的Index并非是事件在数组里的Index,而是需要减去WSA_WAIT_EVENT_0
   
   
dwIndex = dwIndex – WSA_WAIT_EVENT_0;
   
   

 

【第七步】使用WSAResetEvent函数重设当前这个用完的事件对象

事件已经被触发了之后,它对于我们来说已经没有利用价值了,所以要将它重置一下留待下一次使用,很简单,就一步,连返回值都不用考虑

WSAResetEvent(EventArray[dwIndex]);
   
   

 

【第八步】使用WSAGetOverlappedResult函数取得重叠调用的返回状态

  这是我们最关心的事情,费了那么大劲投递的这个重叠操作究竟是个什么结果呢?其实对于本模型来说,唯一需要检查一下的就是对方的Socket连接是否已经关闭了

DWORD dwBytesTransferred;
   
   
WSAGetOverlappedResult( AcceptSocket, AcceptOverlapped ,
   
   
&dwBytesTransferred, FALSE, &Flags);
   
   
// 先检查通信对方是否已经关闭连接
   
   
// 如果==0则表示连接已经,则关闭套接字
   
   
if(dwBytesTransferred == 0)
   
   
{
   
   
         closesocket(AcceptSocket);
   
   
      WSACloseEvent(EventArray[dwIndex]);    // 关闭事件
   
   
         return;
   
   
}
   
   

 

【第九步】“享受”接收到的数据

如果程序执行到了这里,那么就说明一切正常,WSABUF结构里面就存有我们WSARecv来的数据了,终于到了尽情享用成果的时候了!喝杯茶,休息一下吧~~~^_^

DataBuf.buf就是一个char*字符串指针,听凭你的处理吧,我就不多说了

 

【第十步】同第五步一样,在套接字上继续投递WSARecv请求,重复步骤 6 ~ 9

 这样一路作下来,我们终于可以从客户端接收到数据了,但是回想起来,呀~~~~~,这样岂不是只能收到一次数据,然后程序不就Over了?…….-_-b  所以我们接下来不得不重复一遍第四步和第五步的工作,再次在这个套接字上投递另一个WSARecv请求,并且使整个过程循环起来,are u clear??

     大家可以参考我的代码,在这里就先不写了,因为各位都一定比我smart,领悟了关键所在以后,稍作思考就可以灵活变通了。

 

 

五。         多客户端情况的注意事项

     完成了上面的循环以后,重叠模型就已经基本上搭建好了80%了,为什么不是100%呢?因为仔细一回想起来,呀~~~~~~~,这样岂不是只能连接一个客户端??是的,如果只处理一个客户端,那重叠模型就半点优势也没有了,我们正是要使用重叠模型来处理多个客户端。

      所以我们不得不再对结构作一些改动。

1. 首先,肯定是需要一个SOCKET数组 ,分别用来和每一个SOCKET通信

其次,因为重叠模型中每一个SOCKET操作都是要“绑定”一个重叠结构的,所以需要为每一个SOCKET操作搭配一个WSAOVERLAPPED结构,但是这样说并不严格,因为如果每一个SOCKET同时只有一个操作,比如WSARecv,那么一个SOCKET就可以对应一个WSAOVERLAPPED结构,但是如果一个SOCKET上会有WSARecv WSASend两个操作,那么一个SOCKET肯定就要对应两个WSAOVERLAPPED结构,所以有多少个SOCKET操作就会有多少个WSAOVERLAPPED结构。

然后,同样是为每一个WSAOVERLAPPED结构都要搭配一个WSAEVENT事件,所以说有多少个SOCKET操作就应该有多少个WSAOVERLAPPED结构,有多少个WSAOVERLAPPED结构就应该有多少个WSAEVENT事件,最好把SOCKET – WSAOVERLAPPED – WSAEVENT三者的关联起来,到了关键时刻才会临危不乱:)

 

2. 不得不分作两个线程:

一个用来循环监听端口,接收请求的连接,然后给在这个套接字上配合一个WSAOVERLAPPED结构投递第一个WSARecv请求,然后进入第二个线程中等待操作完成。

第二个线程用来不停的对WSAEVENT数组WSAWaitForMultipleEvents,等待任何一个重叠操作的完成,然后根据返回的索引值进行处理,处理完毕以后再继续投递另一个WSARecv请求。

这里需要注意一点的是,前面我是把WSAWaitForMultipleEvents函数的参数设置为WSA_

INFINITE的,但是在多客户端的时候这样就不OK了,需要设定一个超时时间,如果等待超时了再重新WSAWaitForMultipleEvents,因为WSAWaitForMultipleEvents函数在没有触发的时候是阻塞在那里的,我们可以设想一下,这时如果监听线程忠接入了新的连接,自然也会为这个连接增加一个Event,但是WSAWaitForMultipleEvents还是阻塞在那里就不会处理这个新连接的Event了。也不知道说明白了没有。。。。。。-_-b 可能在这里你也体会不到,真正编码的时候就会明白了。

 

其他还有不明白的地方可以参考我的代码,代码里也有比较详尽的注释,  Enjoy~~~

不过可惜是为了照顾大多数人,使用的是MFC的代码,显得代码有些杂乱。

 

六.    已知问题

    这个已知问题是说我的代码中的已知问题,可不是重叠结构的已知问题:)

这个示例代码已经写好了很久了,这两天做最后测试的时候才发现竟然有两个Bug,而且还不是每次都会出现,5555,我最近是实在没有精力去改了,如果有心的朋友能修改掉这两个Bug,那真是造福大家了,这篇文章都险些流产,我更没有经历去修改都快要淡忘了的代码的Bug了,我写在这里提醒一下大家了,反正这个代码也仅仅是抛砖引玉而已,而且我觉得比起代码来还是文字比较珍贵^_^,因为重叠模型的代码网上也还是有不少的。两个Bug是这样的:

1.  多个客户端在连续退出的时候,有时会出现异常;

2.  有时多个客户端的接收缓冲区竟然会重叠到一起,就是说A客户端发送的数据后面会根有B客户端上次发来的数据。。。。。-_-b

改进算法:其实代码中的算法还有很多可以改进的地方,limin朋友就向我提及过几个非常好的改进算法,比如如何在socket数组中寻找空闲的socket用来通信,但是我并没有加到这份代码里面来,因为本来重叠模型的代码就比较杂,再加上这些东西恐怕反而会给初学者带来困难。但是非常欢迎各位和我讨论重叠模型的改进算法以及我代码中存在问题!^_^

 

就说这么多吧,但愿你能通过这篇文章熟练的玩转重叠IO模型,就没有枉费我这番功夫了。^_^

敬请期待本系列下一篇拙作《手把手教你玩转SOCKET模型之完成例程篇》

                                              

               ------    Finished in DLUT | DIP

                                                                                     ------    2004-09-22



Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=114908

<script src="http://writeblog.csdn.net/PromoteIcon.aspx?Id=114908" type="text/javascript"></script> [ 推荐本文] [ 点击此处收藏本文]   发表于 2004年09月23日 11:07 PM
href="http://blog.csdn.net/PiggyXP/Services/Pingback.aspx" rel="pingback" /> <script type="text/javascript">function hide(){showComment();}</script>
<script type="text/javascript">document.write(" ");</script>

 

 
shootingstars 发表于2004-09-26 10:09 PM  IP: 219.237.60.*
呵呵,小猪辛苦了。。。一定花了不少功夫来排版吧 ^_^

 
PiggyXP(小猪) 发表于2004-10-17 1:19 PM  IP: 218.56.172.*
没想到把我blog美化以后都没有办法留言了,现在才注意到,终于弄好了呵呵呵^_^

 
Tony 发表于2004-10-19 9:44 AM  IP: 220.244.224.*
小猪老乡辛苦了.

我有个Server,里面有个CList,里面有不断收到的设备数据. 如何把这些数据转发给每一个client呢?

比如, 在上面的代码中, WSASend加在什么地方呢?

 
PiggyXP(小猪) 发表于2004-10-20 10:45 AM  IP: 218.56.172.*
WSASend可以和WSARecv放在一起的,总是就是投递给系统去完成就可以了,系统完成以后我们会接到它的通知,一个socket对应两个重叠操作的时候注意对应关系哦~~:)

为什么是老乡呢?都是大工的兄弟吗?

 
张尚建 发表于2004-10-25 8:53 AM  IP: 218.88.66.*
怎么不来blog了?
给你个代码,为blog加入weather。
在 选项 » 配置 的 静态新闻/声明中加入:
<p><b>今日天气</b><br>
<center><iframe width=157 height=240 frameborder=0 scrolling=NO src='http://appnews.qq.com/cgi-bin/news_qq_search?city=%B3%C9%B6%BC&hl'></iframe></center>

上面的%B3%C9%B6%BC是成都的编码,可以使用php中的urlencode函数转换,如urlencode("成都")。
上海-%C9%CF%BA%A3
北京-%B1%B1%BE%A9
青岛-%C7%E0%B5%BA
济南-%BC%C3%C4%CF
武汉-%BC%C3%C4%CF
大连-%B4%F3%C1%AC

 
Tony 发表于2004-10-26 10:50 AM  IP: 203.29.131.*
发送已经搞定:)

都是武汉的呀.

IOCP即使出笼呀?

 
PiggyXP(小猪) 发表于2004-11-01 5:20 PM  IP: 218.56.172.*
呵呵呵,谢谢张尚建啊,你怎么知道我最近没来blog?

 
Mega.C 发表于2004-11-02 2:52 PM  IP: 222.33.84.*
小猪你是大连理工的么?

 
sql_fly 发表于2004-11-06 9:20 AM  IP: 202.118.74.*
原来斑竹是我们大工的阿

 
PiggyXP(小猪) 发表于2004-11-06 10:52 AM  IP: 218.56.172.*
这么多大工的兄弟啊,献丑了......-_-b

 
Mega.C 发表于2004-11-08 12:56 PM  IP: 222.33.84.*
我不是大工的,不过总去你们大工电信学院计算机系研究生那边。不知道小猪是哪一位,呵呵

 
PiggyXP(小猪)  发表于2004-11-08 5:29 PM  IP: 218.56.172.*
啊??呵呵,那你可能已经见过我了^_^

 
coolcol 发表于2004-11-13 1:43 PM  IP: 219.144.170.*
能具体讲讲多客户端时的处理吗,WSAWaitForMultipleEvents最多能处理64个事件,而客户端会远远大于这个数字,怎么处理?

 
xingboy 发表于2004-12-10 6:17 PM  IP: 218.246.233.*
小猪,我看了你的文章受益非浅,谢谢你这篇文章,由于我以前用完成端口写socket,所以98一直是个头痛的问题,现在看了你的文章后我将我的代码修改后能在98下运行了^^,先谢谢你,不过现在遇见一个问题就是最大只支持64个事件,我想了想,如果让大家都利用一个时间句柄,那么只要一个事件就可以了,这样也解决了最大事件问题,主要是得分清楚是哪个socket发送过来的数据,我在这点卡住了,请问你在传送事件的时候可不可以自己加一些标识信息?

 
zwz 发表于2005-01-05 12:29 PM  IP: 218.11.191.*
请教小猪,使用io重叠方式接收数据是否还应该在调用WSAGetOverlappedResult获取实际接收的数据实际数值后判断是否是完整信息?是否会出现一次接收数据不完全的情况?如是,那么该如何处理这种情况?象阻塞模式那样调整接收缓冲区指针,直到所有数据都接收完毕再退出外层循环吗?

 
guest 发表于2005-01-10 5:15 PM  IP: 61.149.254.*
唉,和书讲的基本上一样,没什么新意,相应的完成例程比那个WAITFOR***高效多了,而且不必受限于64。既然是版主,希望来一些点晴之作。 一些想法,别见怪.

 
BAMBOO 发表于2005-01-17 7:55 PM  IP: 218.12.100.*
服务器和客户端建立了一个SOCKET,服务器WSARECV客户的查询请求,然后把客户需要的数据WSASEND出去,这样既有接受,又有发送的情况怎么处理呢,同时服务器在这个SOCKET上还可以接受客户的操作信息,既是2个WSARECV,以及和其中一个相对应的WSASEND,请问怎么处理合适,用线程吗?盼回复!

 
BAMBOO 发表于2005-01-18 7:07 PM  IP: 218.12.100.*
高手在吗?好着急啊!

 
LEABER 发表于2005-01-20 4:26 PM  IP: 221.218.59.*
可以考虑用两个线程来做不同的事,一个处理接收相关,一个处理发送相关。

呵呵,交流一下
MSN:LEABERSOCKET@HOTMAIL.COM

 
abc 发表于2005-01-20 4:41 PM  IP: 61.49.228.*
如果WSARecv之后要循环发送信息怎么办啊?在for里面加入
WaitForMutipleEvents()来控制WSASend?

 
enoloo 发表于2005-01-26 9:41 AM  IP: 222.171.23.*

最近老是没看到你, 寒假玩的爽把。 呵呵~~

你的两个 bug 可能和下面的原因有关:

1. 多个客户端在连续退出的时候,有时会出现异常;
〉 events 数组中某些项可能会因为某个客户的退出而被 WSACloseEvent 掉, 所以这个数组中的 events 不是连续有效的 -- 如果有某些客户断开套接字的话。 这样可能引起 Wait 函数异常。

2. 有时多个客户端的接收缓冲区竟然会重叠到一起,就是说A客户端发送的数据后面会根有B客户端上次发来的数据。
〉 可能是 DataBuf 引起的问题, 因为 listen 线程和 work 线程都调用 Recv 动作来操作 DataBuf。 去掉某个或者线程同步 DataBuf 可能会解决问题。

偶没有仔细研究代码, 初步想法, 斟酌~~

 
enoloo 发表于2005-01-26 10:03 AM  IP: 222.171.23.*

偶感觉不同平台的 tcp/udp 网络的处理模块的性能都相差无几。 最影响性能的是, 程序对 tcp/udp 模型的 i/o 处理。

考虑暴多用户的情况, 当操作系统将网络事件提交到应用程序的时候, 程序需要维护套接字表, 检查哪个套接字上有操作等等。 不管是 select, WSA*** 都需要在接受到事件之后处理繁琐的查表定位套接字操作, 如果网络事件和这些操作时顺序执行的关系, 那么这种操作在很多用户的情况下会严重影响网络处理效率和加大错误出现的可能。 完成端口的提供的解决方案缓解了这种情形。

如果是提供特定服务类型的服务器,提高网络性能可能从硬件底层和操作系统网络内核来提速。 这是偶的想法, 没有研究过。

还有就是多服务器信息同步和负载平衡的问题, 偶没什么经验。 有时间真的应该好好交流一下~~

 
哈哈 发表于2005-03-03 8:39 PM  IP: 211.82.98.*
多客户时是不是有几个客户就得有几个线程啊?但他们采用同一线程函数?

 
Delphityro 发表于2005-04-22 1:07 PM  IP: 211.162.70.*
enoloo说的对。
1 多个客户端同时退出会因为WSACloseEvent使得事件数组不连续有效,不知道能不能在这儿使用链表。
2 DataBuf是个数组啊。就算两个线程同时使用。那dwIndex不同两个线程使用的DataBuf[dwIndex]肯定不一样。不应该是这儿的问题。。
期待高人解决。。

 
泥巴 发表于2005-12-09 9:30 PM  IP: 60.12.1.*
“这两天做最后测试的时候才发现竟然有两个Bug,而且还不是每次都会出现”
——对于多线程网络应用程序来说这是最危险的事,也整件事的关键。所以个人认为你需要完美解决好这件事,再写文章。不可轻视。一个实用的服务器器有这样的问题是不可想象的。
——BTW:你为什么不用GetQueuedCompletionStatus?是因为想用EVENT机制么?
houstond@163.com 董浩

 
风逐云 发表于2006-02-17 11:20 AM  IP: 60.176.80.*
首先,肯定是需要一个SOCKET数组 ,分别用来和每一个SOCKET通信

其次,因为重叠模型中每一个SOCKET操作都是要“绑定”一个重叠结构的,所以需要为每一个SOCKET操作搭配一个WSAOVERLAPPED结构,但是这样说并不严格,因为如果每一个SOCKET同时只有一个操作,比如WSARecv,那么一个SOCKET就可以对应一个WSAOVERLAPPED结构,但是如果一个SOCKET上会有WSARecv 和WSASend两个操作,那么一个SOCKET肯定就要对应两个WSAOVERLAPPED结构,所以有多少个SOCKET操作就会有多少个WSAOVERLAPPED结构。

然后,同样是为每一个WSAOVERLAPPED结构都要搭配一个WSAEVENT事件,所以说有多少个SOCKET操作就应该有多少个WSAOVERLAPPED结构,有多少个WSAOVERLAPPED结构就应该有多少个WSAEVENT事件,最好把SOCKET – WSAOVERLAPPED – WSAEVENT三者的关联起来,到了关键时刻才会临危不乱:)



这句话说得不对,socket-event-overlapped三者是严格的一一对应关系!而对于socket所产生的accept,send,recv三种操作,event中通过设置掩码已经把对应三种时间囊括在内!

 
风逐云 发表于2006-02-23 11:42 AM  IP: 60.176.83.*
这句话说得不对,socket-event-overlapped三者是严格的一一对应关系!而对于socket所产生的accept,send,recv三种操作,event中通过设置掩码已经把对应三种事件囊括在内!

请看:
WSARecv(s, &DataBuf, dwBufferCount, &dwRecvBytes,
&Flags, &AcceptOverlapped, NULL);


BOOL WSAGetOverlappedResult(
SOCKET s, // SOCKET,不用说了
LPWSAOVERLAPPED lpOverlapped, // 这里是我们想要查询结果的那个重叠结构的指针
LPDWORD lpcbTransfer, // 本次重叠操作的实际接收(或发送)的字节数
BOOL fWait, // 设置为TRUE,除非重叠操作完成,否则函数不会返回
// 设置FALSE,而且操作仍处于挂起状态,那么函数就会返回FALSE
// 错误为WSA_IO_INCOMPLETE
// 不过因为我们是等待事件传信来通知我们操作完成,所以我们这里设
// 置成什么都没有作用…..-_-b 别仍鸡蛋啊,我也想说得清楚一些…
LPDWORD lpdwFlags // 指向DWORD的指针,负责接收结果标志
);

发送Buffer:SendBuff[len]
接收Buffer:RecvBuff[len]
发送/接收行为发生后,都可以把
所传回的处理长度赋给WSAGetOverlappedResult()中的lpcbTransfer中,这是通用的
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值