MFC:一个简单的多线程传送文件的实现 client端(2)

// NewFtpClientDlg.h : header file
//

#if !defined(AFX_NEWFTPCLIENTDLG_H__7DE54983_D072_4771_946E_4AD8CCBE4BC6__INCLUDED_)
#define AFX_NEWFTPCLIENTDLG_H__7DE54983_D072_4771_946E_4AD8CCBE4BC6__INCLUDED_

#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000

/
// CNewFtpClientDlg dialog

#include "Download.h"


class CNewFtpClientDlg : public CDialog
{
// Construction
public:

 LRESULT OnUpdateFileStatus( WPARAM wParam, LPARAM lParam);

 
 LRESULT OnAddServerFile( WPARAM wParam, LPARAM lParam);

 //保存服务器ip的字符串
 CString m_ServerIPStr;
 //保存当前下载的文件在服务器端的路径的链表
    CStringList m_ServerPathList;
    //负责启动下载进程的类
 CDownLoad m_DownLoad;

 CNewFtpClientDlg(CWnd* pParent = NULL); // standard constructor

// Dialog Data
 //{{AFX_DATA(CNewFtpClientDlg)
 enum { IDD = IDD_NEWFTPCLIENT_DIALOG };
 CListCtrl m_ServerFileList;
 //}}AFX_DATA

 // ClassWizard generated virtual function overrides
 //{{AFX_VIRTUAL(CNewFtpClientDlg)
 protected:
 virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support
 //}}AFX_VIRTUAL

// Implementation
protected:
 HICON m_hIcon;

 // Generated message map functions
 //{{AFX_MSG(CNewFtpClientDlg)
 virtual BOOL OnInitDialog();
 afx_msg void OnPaint();
 afx_msg HCURSOR OnQueryDragIcon();
 afx_msg void OnConnectbutton();
 afx_msg void OnDblclkList1(NMHDR* pNMHDR, LRESULT* pResult);
 //}}AFX_MSG
 DECLARE_MESSAGE_MAP()
};

//{{AFX_INSERT_LOCATION}}
// Microsoft Visual C++ will insert additional declarations immediately before the previous line.

#endif // !defined(AFX_NEWFTPCLIENTDLG_H__7DE54983_D072_4771_946E_4AD8CCBE4BC6__INCLUDED_)


// NewFtpClientDlg.cpp : implementation file
//

#include "stdafx.h"
#include "NewFtpClient.h"
#include "NewFtpClientDlg.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

/
// CNewFtpClientDlg dialog

CNewFtpClientDlg::CNewFtpClientDlg(CWnd* pParent /*=NULL*/)
: CDialog(CNewFtpClientDlg::IDD, pParent)
{
 //{{AFX_DATA_INIT(CNewFtpClientDlg)
 // NOTE: the ClassWizard will add member initialization here
 //}}AFX_DATA_INIT
 // Note that LoadIcon does not require a subsequent DestroyIcon in Win32
 m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
}

void CNewFtpClientDlg::DoDataExchange(CDataExchange* pDX)
{
 CDialog::DoDataExchange(pDX);
 //{{AFX_DATA_MAP(CNewFtpClientDlg)
 DDX_Control(pDX, IDC_LIST1, m_ServerFileList);
 //}}AFX_DATA_MAP
}

BEGIN_MESSAGE_MAP(CNewFtpClientDlg, CDialog)
//{{AFX_MSG_MAP(CNewFtpClientDlg)
ON_WM_PAINT()
ON_WM_QUERYDRAGICON()
ON_BN_CLICKED(IDC_CONNECTBUTTON, OnConnectbutton)
ON_MESSAGE( WM_ADDSERVERFILE, OnAddServerFile )
ON_NOTIFY(NM_DBLCLK, IDC_LIST1, OnDblclkList1)
ON_MESSAGE( WM_UPDATEFILESTATUS, OnUpdateFileStatus )
//}}AFX_MSG_MAP
END_MESSAGE_MAP()

/
// CNewFtpClientDlg message handlers

BOOL CNewFtpClientDlg::OnInitDialog()
{
 CDialog::OnInitDialog();
 
 // Set the icon for this dialog.  The framework does this automatically
 //  when the application's main window is not a dialog
 SetIcon(m_hIcon, TRUE);   // Set big icon
 SetIcon(m_hIcon, FALSE);  // Set small icon
 
 // TODO: Add extra initialization here
 
 //注意设置listcontrol的属性: styles 选项卡设置为Report!!!!!!
 int iColumnWidth = 50;
 m_ServerFileList.InsertColumn( 0, "文件名", LVCFMT_LEFT, 3 * iColumnWidth, -1 );
 m_ServerFileList.InsertColumn( 1, "文件大小(字节)", LVCFMT_LEFT, 2 * iColumnWidth, -1 );
    m_ServerFileList.InsertColumn( 2, "完成百分比", LVCFMT_LEFT,  2 * iColumnWidth, -1 );
 m_ServerFileList.InsertColumn( 3, "下载速率", LVCFMT_LEFT,  2 * iColumnWidth, -1 );
 m_ServerFileList.InsertColumn( 4, "下载状态", LVCFMT_LEFT, 2 * iColumnWidth, -1 );
    
    m_ServerFileList.SetExtendedStyle( LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES );
 
 m_ServerPathList.RemoveAll();
 return TRUE;  // return TRUE  unless you set the focus to a control
}

// If you add a minimize button to your dialog, you will need the code below
//  to draw the icon.  For MFC applications using the document/view model,
//  this is automatically done for you by the framework.

void CNewFtpClientDlg::OnPaint() 
{
 if (IsIconic())
 {
  CPaintDC dc(this); // device context for painting
  
  SendMessage(WM_ICONERASEBKGND, (WPARAM) dc.GetSafeHdc(), 0);
  
  // Center icon in client rectangle
  int cxIcon = GetSystemMetrics(SM_CXICON);
  int cyIcon = GetSystemMetrics(SM_CYICON);
  CRect rect;
  GetClientRect(&rect);
  int x = (rect.Width() - cxIcon + 1) / 2;
  int y = (rect.Height() - cyIcon + 1) / 2;
  
  // Draw the icon
  dc.DrawIcon(x, y, m_hIcon);
 }
 else
 {
  CDialog::OnPaint();
 }
}

// The system calls this to obtain the cursor to display while the user drags
//  the minimized window.
HCURSOR CNewFtpClientDlg::OnQueryDragIcon()
{
 return (HCURSOR) m_hIcon;
}

void CNewFtpClientDlg::OnConnectbutton() 
{
 //获得用户输入的ip地址
    GetDlgItem (IDC_IPADDRESS)->GetWindowText( m_ServerIPStr ); 
 //发送连接请求
 m_DownLoad.SendListRequest( m_ServerIPStr ); 
}

//将wParam中指明的FileInfo中的文件加入到客户端的list控件中
LRESULT CNewFtpClientDlg::OnAddServerFile( WPARAM wParam, LPARAM lParam )
{
 FileInfo* ReceivedFileInfo = (FileInfo*)wParam;
 
 CString strPathName ( ReceivedFileInfo->filepath );
 m_ServerPathList.AddTail ( strPathName );
 
 CString strFileName ( ReceivedFileInfo->filename );
 
 long lFileLength = ReceivedFileInfo->len;
 
 //否则将文件加入到文件列表中
 CString strFileSize;
    _ltoa( lFileLength , strFileSize.GetBuffer(0), 10 );

 int iItem = m_ServerFileList.GetItemCount();
 LV_ITEM lvi;
 lvi.mask = LVIF_TEXT|LVIF_PARAM;
 lvi.iItem = iItem;
 lvi.iSubItem = 0;
 lvi.lParam = lFileLength;
 lvi.pszText = strFileName.GetBuffer(0);
 m_ServerFileList.InsertItem( &lvi );
 //将用户选择的文件加入到文件链表控件中,设置文件大小和文件路径等信息
 m_ServerFileList.SetItemText( iItem, 1, strFileSize.GetBuffer(0) );
 //m_FileListCtrl.SetItemText( iItem, 2, strPathName.GetBuffer(0) );
 
 return 0;
}

//双击一项后出现“另存为”的文件对话框
void CNewFtpClientDlg::OnDblclkList1(NMHDR* pNMHDR, LRESULT* pResult) 
{
 NMLISTVIEW* pListView = (NMLISTVIEW*)pNMHDR;
 int iSel = pListView->iItem;
 
 if( iSel == -1 )
 {
  return;
 }

    //获取文件名
 CString strFileName = m_ServerFileList.GetItemText(iSel, 0);
 CString LengthStr = m_ServerFileList.GetItemText(iSel, 1);
 //TRACE(" 获得的文件长度是: %s \n", LengthStr);
 long FileLength = atol ( LengthStr );

 //找到用户选择的文件在链表中的位置,获取该文件对应的服务器端路径
 POSITION position = m_ServerPathList.GetHeadPosition();
 CString currentServerPath = "";

 while ( position != NULL )
 {
  currentServerPath = m_ServerPathList.GetAt( position );
  if ( currentServerPath.Find( strFileName ) != -1  )
  {
            break;
  }
  m_ServerPathList.GetNext( position );
 }

 if ( currentServerPath == "" )
 {
  AfxMessageBox("错误!服务器路径链表中找不到您选中的文件!");
  return;
 }

 //这里的currentServerPath保存了用户选中的文件在server端的物理路径
    //出现一个另存为对话框,让用户选择文件保存路径
    CFileDialog dlg ( FALSE, NULL, strFileName.GetBuffer(0), 
  OFN_HIDEREADONLY|OFN_OVERWRITEPROMPT, "All Files(*.*)|*.*||", this);
 if ( dlg.DoModal() != IDOK )
 {
  return;
 }
 
 //这里的strClientPath保存了用户要保存到本地的路径
 CString strClientPath = dlg.GetPathName();
 //创建该文件,初始大小为0
 CFile file;
 BOOL bOpen = file.Open(strClientPath, CFile::modeCreate|CFile::modeWrite, NULL);
 if ( !bOpen )
 {
  MessageBox("文件创建或打开失败!请确认路径再重试!");
  return;
 }
 file.Close();

 //开4个线程下载用户选择的文件
 m_DownLoad.StartDownLoad( currentServerPath, FileLength, strClientPath );

 *pResult = 0;
}

//更新wParam参数中的
LRESULT CNewFtpClientDlg::OnUpdateFileStatus(WPARAM wParam, LPARAM lParam)
{
 UpdateParam *up = (UpdateParam*)wParam;

 CString ServerPathName ( up->ServerFilePath);

 //根据wParam参数中的ServerPathName域,在list控件中找到对应的项
 POSITION position = m_ServerPathList.GetHeadPosition();
 CString currentServerPath = "";
 int i = 0;

 while ( position != NULL )
 {
  currentServerPath = m_ServerPathList.GetAt( position );
  if ( currentServerPath ==  ServerPathName )
  {
            break;
  }
  m_ServerPathList.GetNext( position );
  ++i;
 }
    
 if ( i >= m_ServerFileList.GetItemCount() )
 {
  AfxMessageBox("错误!没有找到需要下载的文件!!");
  return -1;
 }

    //TRACE(" 找到的list控件中的 i = %d \n", i );

 //下载百分比
 CString PercentStr;
 PercentStr.Format( "%d %%", up->Percent );
    //下载速率
 CString VelocityStr;
 VelocityStr.Format("%d k/s", up->Velocity );

 //状态字符串
 CString DownLoadStatusStr = "下载中......";
 if ( up->Percent >= 100 )
 {
  DownLoadStatusStr = "下载完毕!";
 }
    //更新list控件
 m_ServerFileList.SetItemText( i, 2, PercentStr );
    m_ServerFileList.SetItemText( i, 3, VelocityStr );
 m_ServerFileList.SetItemText( i, 4, DownLoadStatusStr );

 return 0;
}



#ifndef CDOWNLOAD_H
#define CDOWNLOAD_H

//下载进程用到的数据结构
struct DownLoadThreadParam
{
 FileInfo fileinfo;
 //workno指示了当前是第几个任务
 int workno;
};

//保存用户下载的任务的一个数据结构
struct WorkItem
{
 int workno;
 long finished[ DOWNLOAD_THREAD_NUMBER + 1 ];
};

//更新list控件时用到的数据结构
struct UpdateParam
{
 char ServerFilePath[MAX_PATH];
 int Percent;
 int Velocity;
};

class CDownLoad
{
public:

 CDownLoad();
 ~CDownLoad();
    //向服务器发送文件列表的请求
 int SendListRequest( CString IPStr );
 int StartDownLoad( CString ServerPathName, long FileLength, CString LocalPathName);


private:

 static int readn( SOCKET ClientSideSocket, char* buf, int len);
 static int sendn ( SOCKET ClientSideSocket, char* bp, int len);

 //使用类的函数创建thread是可以的,只要把这个函数按如下声明:
 //static dword winapi funcname (LPVOID lparam )
 static DWORD WINAPI RecvFileInfoThread( LPVOID lParam );

 static DWORD WINAPI RecvFileThread( LPVOID lParam );

 static DWORD WINAPI UpdateListThread( LPVOID lParam );

private:
 //保存了最后下载的任务在WorkList中的位置
 int TotalWorks;


};

#endif 


#include "stdafx.h"
#include "newftpclient.h"
#include "Download.h"


//全局变量,保存当前下载的文件任务
WorkItem WorkList[ MAX_DOWNLOAD_FILES];

//服务器ip字符串
CString ServerIPStr;

CDownLoad::CDownLoad()
{
 memset ( WorkList, 0, MAX_DOWNLOAD_FILES * sizeof( WorkItem) );
 TotalWorks = 0;
}

CDownLoad::~CDownLoad()
{

 
}

int CDownLoad::SendListRequest ( CString IPStr )
{
 sockaddr_in local;
 
 local.sin_family = AF_INET;
 local.sin_port = htons ( SERVERPORT );
 local.sin_addr.S_un.S_addr = inet_addr( IPStr );
 
 SOCKET ClientSideSocket = socket ( AF_INET, SOCK_STREAM, 0 );
 
 int ret = connect ( ClientSideSocket, (LPSOCKADDR)&local, sizeof(local) );
 if ( ret < 0 )
 {
  AfxMessageBox("服务器连接不上!");
  return -1;
 }
 ServerIPStr = IPStr;
 FileInfo ListRequest;
 ListRequest.type = LIST_REQUEST;
 //发送文件列表请求
 sendn ( ClientSideSocket, (char*)&ListRequest, sizeof(FileInfo) );
 
 //创建一个接受线程来接受服务器发来的文件名和文件大小
 DWORD dwthread;
 ::CreateThread( NULL, 0, RecvFileInfoThread, (LPVOID)ClientSideSocket, 0, &dwthread ); 
 return 0;
}

//接收服务器发来的文件的文件名和大小等信息,并在列表框中显示出来
DWORD WINAPI CDownLoad::RecvFileInfoThread( LPVOID lParam )
{
 FileInfo ReceivedFileInfo;
 SOCKET ClientSideSocket = (SOCKET)lParam;
 int ret = 0;
    for ( ; ; )
 {
  ret = recv( ClientSideSocket, (char*)&ReceivedFileInfo, sizeof(FileInfo), 0 );
  if ( ret > 0 )
  {
   //在list控件中显示
   ::SendMessage( ::AfxGetMainWnd()->GetSafeHwnd(), 
    WM_ADDSERVERFILE, (WPARAM)&ReceivedFileInfo, NULL );
  }
  else if ( ret <= 0 )
  {
   return -1;
  }
 }
 
 return 0;
}

//开4个线程下载文件
int CDownLoad::StartDownLoad( CString ServerPathName,  long FileLength,  CString LocalPathName)
{
 FileInfo FileRequest; 
    FileRequest.type = FILE_REQUEST;
 strcpy ( FileRequest.filepath, ServerPathName );
 strcpy ( FileRequest.filename, LocalPathName );
 
 DWORD dwthread;

 //最大只能开50个任务,如果在开多了,就把原先的内容给清除了!!!!!!!!
 if ( TotalWorks > MAX_DOWNLOAD_FILES )
 {
  TotalWorks = 0;
 }

 //将新下载任务加入到WorkList中
 WorkItem wi;
 wi.workno = TotalWorks;
 memset ( wi.finished, 0 ,  DOWNLOAD_THREAD_NUMBER * sizeof( long ) );
 wi.finished[ DOWNLOAD_THREAD_NUMBER ] = FileLength;
 WorkList[TotalWorks] = wi;

 // 如果文件长度小于1M,则开一个线程,否则开 DOWNLOAD_THREAD_NUMBER 个线程
 if ( FileLength < FILE_SIZE_THRESHOLD ) 
 {
  FileRequest.seek = 0;
  FileRequest.len = FileLength;

     DownLoadThreadParam * pdp = new DownLoadThreadParam;
  pdp->fileinfo = FileRequest;
  pdp->workno = TotalWorks;

  //创建一个接受线程来接受服务器发来的文件名和文件大小
     ::CreateThread( NULL, 0, RecvFileThread, 
   pdp, 0, &dwthread ); 
 }
    else // 如果文件长度大于1M
 {
  //将文件长度分成4份,每个线程下载 1 / 4
  //SeekStartPos数组保存了每个线程请求的文件起始位置
  //和剩余的大小
  long SeekStartPos[ DOWNLOAD_THREAD_NUMBER * 2 ];
  long FileSection = FileLength / DOWNLOAD_THREAD_NUMBER;
  for ( int i = 0; i <  DOWNLOAD_THREAD_NUMBER ; ++i )
  {
   SeekStartPos[ 2 * i] = FileSection * i;
   SeekStartPos[ 2 * i + 1] = FileSection;
  }
  //最后一个的seeklength应该是剩余的所有长度
  SeekStartPos[ DOWNLOAD_THREAD_NUMBER * 2  - 1 ]
   = FileLength - FileSection * (  DOWNLOAD_THREAD_NUMBER - 1 );
 
        //开DOWNLOAD_THREAD_NUMBER个线程
        for ( i = 0; i < DOWNLOAD_THREAD_NUMBER; ++i )
  {
   FileRequest.seek = SeekStartPos[ 2 * i];
       FileRequest.len = SeekStartPos[ 2 * i + 1];

            DownLoadThreadParam * pdp = new DownLoadThreadParam;
   pdp->fileinfo = FileRequest;
   pdp->workno = TotalWorks;
            //TRACE("创建线程第i个: i = %d \n", i );
         ::CreateThread( NULL, 0, RecvFileThread, 
         pdp, 0, &dwthread );

  } // end of for

 } // end of  文件长度大于1M

 //创建更新list控件线程
 DownLoadThreadParam * pUpdatedp = new DownLoadThreadParam;
 strcpy( pUpdatedp->fileinfo.filepath, ServerPathName );

 pUpdatedp->workno = TotalWorks;
 ::CreateThread( NULL, 0, UpdateListThread, 
         pUpdatedp, 0, &dwthread );
    ++TotalWorks;
 return 0;
}

//接收服务器发来的文件
DWORD WINAPI CDownLoad::RecvFileThread( LPVOID lParam )
{
 sockaddr_in local;
 
 local.sin_family = AF_INET;
 local.sin_port = htons ( SERVERPORT );
 local.sin_addr.S_un.S_addr = inet_addr( ServerIPStr );
 
 SOCKET ClientSideSocket = socket ( AF_INET, SOCK_STREAM, 0 );
 
 int ret = connect ( ClientSideSocket, (LPSOCKADDR)&local, sizeof(local) );
 if ( ret < 0 )
 {
  AfxMessageBox("服务器连接不上!");
  return -1;
 }
 
 DownLoadThreadParam* dp = (DownLoadThreadParam*)lParam;

 FileInfo RecvFileInfo = dp->fileinfo;

 //发送文件下载的请求
 sendn ( ClientSideSocket, (char*)&RecvFileInfo, sizeof(FileInfo) );

 int workno = dp->workno;

 CFile RecvFile;
 RecvFile.Open( RecvFileInfo.filename, 
  CFile::modeWrite | CFile::typeBinary | CFile::shareDenyNone );

 RecvFile.Seek( RecvFileInfo.seek , CFile::begin );

 int section = WorkList[workno].finished[DOWNLOAD_THREAD_NUMBER]
  / DOWNLOAD_THREAD_NUMBER;

 int sectionno = RecvFileInfo.seek / section ;
 //TRACE("线程 %d 开始运行.\n", sectionno );

 char* buffer = new char [BUFFER_SIZE];

 int CharLeft = RecvFileInfo.len;
 int CurrentCount = 0;
 int rc = 0;

 //一直读取数据,直到把1/4个文件大小都读取完了
 while ( CharLeft > 0 )
 {
  CurrentCount = CharLeft > BUFFER_SIZE ? BUFFER_SIZE : CharLeft;
        rc = readn ( ClientSideSocket, buffer, CurrentCount );
  if ( rc < 0 )
  {
   closesocket( ClientSideSocket );
   break;
  }
        //TRACE("线程 %d 写入文件%d个字符.\n", sectionno , rc );
  RecvFile.Write( buffer, rc );
        WorkList[workno].finished[sectionno] += rc;
  CharLeft -= rc;
 }

 RecvFile.Close();
 delete []buffer;
 delete dp;
 return 0;
}

//更新list控件中的下载百分比
DWORD WINAPI CDownLoad::UpdateListThread( LPVOID lParam )
{
    DownLoadThreadParam* dp = (DownLoadThreadParam*)lParam;
 int workno = dp->workno;

 //DownLoadCount保存了已经下载的文件的大小
 long DownLoadCount = 0;
 //precount保存了1秒之前下载的文件的大小
 long PreCount = 0;
 //当前时间
 long CurrentTick = 0;
 //之前的时间,用来算是否超过了一秒钟
 long PreTick = 0;
 //完成百分比
 double Percent = 0.0;
 //下载速率
 int Velocity = 0;
 //一直更新直至文件读取完毕
 for ( ; ; )
 {
  CurrentTick = GetTickCount();
  //如果过了一秒钟,更新一下list控件
  if ( CurrentTick - PreTick >= 1000 )
  {
   PreTick = CurrentTick;
   DownLoadCount = 0;
   for ( int i = 0; i < DOWNLOAD_THREAD_NUMBER; ++i )
   {
    //4个线程下载的数据之和
    DownLoadCount += WorkList[workno].finished[i];
   }
            //速率以k为单位
   Velocity = ( DownLoadCount - PreCount ) / 1024;

   PreCount = DownLoadCount;
   
   Percent = (double)DownLoadCount / WorkList[workno].finished[DOWNLOAD_THREAD_NUMBER];

   //将上面算好的所有数据保存到UpdateParam中的相应数据里面
   UpdateParam up;
   up.Percent = (int) ( Percent * 100 )  ;
   strcpy ( up.ServerFilePath, dp->fileinfo.filepath );
          
   up.Velocity = Velocity;
           
   //发送更新list控件消息,更新控件
   ::SendMessage( ::AfxGetMainWnd()->GetSafeHwnd(), 
    WM_UPDATEFILESTATUS, (WPARAM)&up, NULL );
            //如果已经下载完了,则跳出循环
   if ( DownLoadCount >= WorkList[workno].finished[DOWNLOAD_THREAD_NUMBER] )
   {
    break;
   }
  }
  Sleep( 1000 );
 }
 
 delete dp;
 return 0;
}

int CDownLoad::sendn(SOCKET ClientSideSocket, char *bp, int len)
{
    int CharLeft = len;
 int rc = 0;
 while ( CharLeft > 0 )
 {
  rc = send ( ClientSideSocket, bp, CharLeft, 0 );
  if ( rc < 0 )
  {
   AfxMessageBox("客户端发送数据失败!");
   return -1;
  }
  if ( rc == 0 )
  {
   return len - CharLeft;
  }
  bp += rc;
  CharLeft -= rc;
 }
 
 return len;
}

int CDownLoad::readn( SOCKET ClientSideSocket, char *buf, int len )
{
    int CharLeft = len;
 int rc = 0;
 while ( CharLeft > 0 )
 {
  rc = recv ( ClientSideSocket,  buf, CharLeft, 0  );
  if ( rc < 0 )
  {
   AfxMessageBox("客户端接收数据失败!");
   return -1;
  }
  if ( rc == 0 )
  {
   return len - CharLeft;
  }
  buf += rc;
  CharLeft -= rc;
 }
 
 return len;
 
}



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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值