查看: 4121|回复: 13

[原创发布] 独立线程文件下载脚本 支持进度查询

[复制链接]
梦石
0
星屑
13562
在线时间
2753 小时
注册时间
2014-10-4
回帖
705

R考场第七期纪念奖

发表于 2017-10-17 23:43:11 | 显示全部楼层 |阅读模式

加入我们,或者,欢迎回来。

您需要 登录 才可以下载或查看,没有账号?注册会员

×
本帖最后由 SixRC 于 2017-10-21 01:44 编辑

正式版请参照五楼@hyrious 的完善版本
暂时我绝对写不出那么漂亮的代码啦

原正文:
额 先上代码好了 写了两天终于写满意了

[fold=code][pre lang="ruby"]#------------------------------------------------------------------------#
#                                 ----SixRC   于   2017.10.17            
#       说明                                                            
#  文件下载类 对每个下载的文件会创建新的线程                             
#  随时可以获取下载状态  下载每个文件都应该新建一个实例                  
#  初始化需传入文件源地址、保存地址                       
#  如 test=FileDownload.new("www.../xxxxx.rar","data/x.rar")      
#  调用 Start_download 方法开始创建线程下载                              
#  调用 Get_status 获取下载信息 是一个含两个元素的数组                  
#  第一位为已经下载的量 第二位为目标大小    字节单位                     
#  第二位开始未获取时是0  初次确定后也可能会随着下载改变                 
#  若第二位始终为0 则表明未知                                            
#  调用 abort 终止下载                                                   
#  调用 check 查看线程是否结束 或下载结束 或中断 结束返true              
#  在第33行url后加了随机数是为了防止从缓存中读取                        
#  若需要从缓存中读取 比方接着上次下载的功能 注释掉随机数就好            
#------------------------------------------------------------------------#
class FileDownload
  Urlmon=Win32API.new('kernel32','LoadLibrary','p','l').call("urlmon.dll")
  URLDownloadToFile=Win32API.new('kernel32','GetProcAddress','lp','l').call(Urlmon,"URLDownloadToFileA")
  NewThread=Win32API.new('kernel32','CreateThread','llppll','l')
  CloseHandle=Win32API.new('kernel32','CloseHandle','l','l')
  CodeH1=[184,1,64,0,128,194,8,0].pack("C*")
  CodeH2=[184,1,64,0,128,194,12,0].pack("C*")
  CodeH4=[184,1,64,0,128,194,20,0].pack("C*")
  CodeU0=[49,192,194,4,0].pack("C*")
  CodeT=[139,68,36,4,255,112,12,106,0,255,112,8,255,112,4,106,0,255,16,139,68,36,4,139,64,16,199,0,1,0,0,0,195].pack("C*")
  OnProgress=[86,87,139,116,36,12,139,126,8,139,68,36,16,137,7,139,68,36,20,137,71,4,139,118,4,139,6,95,94,194,20,0].pack("C*")
  VTBL=[CodeH2,CodeU0,CodeU0,CodeH2,CodeH1,CodeH1,OnProgress,CodeH2,CodeH2,CodeH4,CodeH2].pack("p*")
  CWP = Win32API.new('user32.dll','CallWindowProc','ppiii','i')
  Abort=[139,68,36,4,199,0,4,64,0,128,194,16,0].pack("C*")
  def initialize(url,dest)
    @url=url+"?"+rand(2333333).to_s
    @dest=dest
    @temp="\0"*8
    @state="\0"*4
    @data=[VTBL,@state,@temp].pack("p*")
    @para=[URLDownloadToFile,@url,@dest,@data,@state].pack("lp*")
    @disposed=0
  end
  def Start_download
    @handle=NewThread.call(0,0,CodeT,@para,0,0)
  end
  def Get_status
    @temp.unpack("ll")
  end
  def check
    if @state.unpack("l")[0]==1
      if @disposed==0
        CloseHandle.call(@handle)
        @disposed=1
      end
      return true
    end
    false
  end
  def abort
    return if check
    CWP.call(Abort,@state,0,0,0)
    loop{
    break if check
    sleep 0.001 #Graphics.update
    }
  end
end[/pre][/fold]
很随便的测试代码:
[fold=测试代码][pre lang="ruby"]u="http://zt.bdinfo.net/speedtest/wo3G.rar"
d="tx.rar"
tt=FileDownload.new(u,d)
tt.Start_download
loop{
Graphics.update
p tt.Get_status
break if tt.check
}[/pre][/fold]

你可以同时下载多个文件 线程是独立的 对游戏没有影响 check过为true会关闭线程
可以随时查看进度 应该很方便了吧

关于作用 我想可以利用github等个人空间站固定一个txt的下载地址 里面保存一些可以下载的文件信息和地址
然后每天可以自动换标题画面啊
可以检查更新啊
只要文件不大于4G都可以下载 因为保存文件大小的那个内存量只有32位 (不过我也没试过)
暂时 这样吧 突然不知道该写点什么了

脚本的原理:
[fold=原理及写代码的经过]原理、写代码的经过
一切从URLDownloadToFile开始
这个下载文件的api需要五个参数 1.控件接口的指针 2.url地址 3.保存的文件名 4.保留字段 5.IBindStatusCallback接口指针
1、4直接传0就好 重要的是2、3、5  2、3好理解 重要的在于5
什么是IBindStatusCallback接口?
简单说 URLDownloadToFile 在下载过程中会调用这个接口中的一些函数
其中一个OnProgress  URLDownloadToFile会传入一些下载信息然后这个函数可以告诉调用者继不继续等等
我们就从这里得到下载进等信息
那怎么传这个参数?IBindStatusCallback接口指针本来是c++的东西 在ruby里怎么表示呢
其实不管什么东西 到了内存里都是差不多的
写一个相同结构的东西就好了
我查了一下这个接口 它有八个函数
然后我接着查c++的类的内存结构 得到其指针指向的首位是虚函数表 然后是一些类数据
而虚函数表储存着函数的地址  排列顺序以定义时为准
于是接着翻IBindStatusCallback定义的代码
等等等等
这个类还继承自 IUnknown 于是还需要知道那个类的定义
都知晓后 就可以开始了
对于不重要的函数 直接返回零或者需要的值
mov eax,xxxxxxxxh
retn xxh
具体每个函数的参数次序多少、平堆栈的过程通过动态调试解决
唯一需要仔细定义的是OnProgress
就用他把传入的值拿来修改@temp就好
然后把@state作为返回值
或者别的功能
然后这张函数表就写好了
传入后就能得到下载信息
这之后还有一个问题就是URLDownloadToFile的下载会阻塞线程
最初的想法是让OnProgress调用ruby代码  (dll的RGSSEval)
然后显示下载进度  可是线程本质上依旧是阻塞的  除了显示下载进度其他什么都不能做
这个方案明显不行
于是想到新建线程
不过ruby自带的线程还是会阻塞  于是还是api
CreateThread需要的参数有六个
重要的只有两个
一个是新线程执行的代码 一个是参数结构体
很好解决
于是让新线程去call URLDownloadToFile
而因为线程不同 主线程随时可以查询进度即@tmep
而需要人为结束下载时只需要改掉@state
线程结束时让它把@state改成1
易于检查是否完成
大致如上
若需深究细节 还是自己多查多试吧
有需要请自行改造脚本[/fold]




评分

参与人数 3星屑 +100 赞 +3 收起 理由
hyrious + 1 塞糖
fux2 + 1 我很赞同
guoxiaomi + 100 + 1 精品文章

查看全部评分

梦石
0
星屑
785
在线时间
59 小时
注册时间
2017-9-30
回帖
118
发表于 2017-10-18 09:00:38 | 显示全部楼层
建议不要全部读入内存,用分段写入,每下载1MB就写入文件
还有,建议加两个回调函数,一个是下载中不断被触发的下载进度回调,一个是下载完成或错误后的完成回调

点评

经测试URLDownloadToFile只占用很少的内存  发表于 2017-10-18 11:21
关于第一点 URLDownloadToFile并不支持分段下载 第二点我已经写了  发表于 2017-10-18 09:21
回复

使用道具 举报

梦石
1
星屑
24088
在线时间
3339 小时
注册时间
2011-7-8
回帖
3862

开拓者

发表于 2017-10-18 09:48:59 | 显示全部楼层
本帖最后由 guoxiaomi 于 2017-10-18 09:52 编辑

我之前写了个get请求的:
http://rpg.blue/forum.php?mod=re ... &fromuid=210672
不过这个http访问是异步的,但写入应该不是异步的……因为我试过了在Thread里执行的代码会异常的缓慢,所以只是读取到了变量里,然后在主线程处理文件写入~
此外,还是很好奇怎么知道下载了多少内容~

点评

学习了(๑•̀ㅂ•́)و✧  发表于 2017-10-18 15:16
已经更新了原理  发表于 2017-10-18 12:01
对了 对象的变量名是不需要的..开始是另一个方案 后来改掉了忘记把它也改掉了 ...晚上果然比较傻  发表于 2017-10-18 10:31
上完课就写原理给你 对于下载的具体过程我不太清楚 请求啊协议啊 只是用了urldownloadtofile  发表于 2017-10-18 10:15
熟悉rgss和ruby,xp区版主~
正在填坑:《膜拜组传奇》讲述膜拜组和学霸们的故事。
已上steam:与TXBD合作的Reformers《变革者》
* 战斗调用公共事件 *
* RGSOS 网络脚本 *
回复

使用道具 举报

老黄鸡

梦石
4
星屑
45692
在线时间
7947 小时
注册时间
2009-7-6
回帖
13335

MZ评测员RM创作大赛01组委会开拓者贵宾

发表于 2017-10-20 10:14:46 | 显示全部楼层
这楼主怕是丧心病狂了,不过还是精品文章

点评

远不及你呀 : )  发表于 2017-10-20 11:02
RGDirect - DirectX驱动的RGSS,点我了解.
(排满,暂停)RM全系列成套系统定制请联系QQ1213237796
不接受对其他插件维护的委托
回复

使用道具 举报

梦石
0
星屑
9862
在线时间
580 小时
注册时间
2017-9-28
回帖
203
发表于 2017-10-20 23:25:20 | 显示全部楼层
本帖最后由 hyrious 于 2017-11-1 22:50 编辑

触瞎
想学口算虚函数表(不

[fold=看代码的时候顺手封装了一个整齐点的(][pre lang="ruby"]
#==============================================================================
# </> Downloader                                            SixRC / 2017.10.17
#------------------------------------------------------------------------------
# Usage
#   x = Downloader.download(url[, filename=url.basename[, cache=true]])
#   x.ok?                   # true if downloaded or stopped
#   x.info, [x.now, x.max]  # progress information
#   x.stop!                 # terminate downloading process
#
# * files using `stop!` stopped can be resumed by downloading it again.
#
# Example
#   x = Downloader.download "http://zt.bdinfo.net/speedtest/wo3G.rar"
#   loop do
#     Input.update
#     puts "#{x.now}/#{x.max}"
#     break if x.ok?
#     x.stop! if Input.trigger? Input::C
#     Graphics.update
#   end
#   puts "Done!"
#
#==============================================================================

module Downloader
  LoadLibrary       = Win32API.new 'kernel32', 'LoadLibrary'   , 'p'     , 'l'
  GetProcAddress    = Win32API.new 'kernel32', 'GetProcAddress', 'lp'    , 'l'
  Urlmon            = LoadLibrary.call 'urlmon.dll'
  URLDownloadToFile = GetProcAddress.call Urlmon, 'URLDownloadToFileA'
  CreateThread      = Win32API.new 'kernel32', 'CreateThread'  , 'llppll', 'l'
  CloseHandle       = Win32API.new 'kernel32', 'CloseHandle'   , 'l'     , 'l'
  CallWindowProc    = Win32API.new 'user32'  , 'CallWindowProc', 'ppLLL' , 'i'
  HRESULT_E_NOTIMPL = 0x80004001
  EAT1              = [0xb8, HRESULT_E_NOTIMPL, 0xc2, 8].pack('CLCS')
  EAT2              = [0xb8, HRESULT_E_NOTIMPL, 0xc2, 12].pack('CLCS')
  EAT4              = [0xb8, HRESULT_E_NOTIMPL, 0xc2, 20].pack('CLCS')
  RET0              = [0x31, 0300, 0xc2, 4].pack('CCCS')
  PROC = [
    # a0 = [URLDownloadToFile, url, dest, data, state]
    0x8b, 0104,0044, 4, # mov    4(%esp), %eax  |
    0xff, 0160,     12, # pushl 12(%eax)        |
    0x6a,            0, # push   0              |
    0xff, 0160,      8, # pushl  8(%eax)        |
    0xff, 0160,      4, # pushl  4(%eax)        |
    0x6a,            0, # push   0              |
    0xff, 0020,         # call   *(%eax)        |  a0_0(0, a0_1, a0_2, 0, a0_3)
    0x8b, 0104,0044, 4, # mov    4(%esp), %eax  |
    0x8b, 0100,0020,    # mov   16(%eax), %eax  |
    0xc7, 0000,1,0,0,0, # movl   1      ,(%eax) |  arg[0][4] = 1 # state
    0xc3,               # ret
  ].pack('C*')
  OnProgress = [
    # (ulProgress, ulProgressMax, ulStatusCode, szStatusText)
    0x56, 0x57,         # push  %esi;push  %edi  |
    0x8b, 0164,0044,12, # mov   12(%esp),  %esi  |
    0x8b, 0176,      8, # mov    8(%esi),  %edi  |
    0x8b, 0104,0044,16, # mov   16(%esp),  %eax  |
    0x89, 0007,         # mov      %eax , (%edi) | arg[2][2][0] = arg[3]
    0x8b, 0104,0044,20, # mov   20(%esp),  %eax  |
    0x89, 0107,      4, # mov      %eax ,4(%edi) | arg[2][2][1] = arg[4]
    0x8b, 0166,      4, # mov    4(%esi),  %esi  |
    0x8b, 0006,         # mov     (%esi),  %eax  | return arg[2][1][0]
    0x5f, 0x5e,         # pop   %edi;pop   %esi  |
    0xc2,         20,0, # ret   20
  ].pack('C*')
  HRESULT_E_ABORT   = 0x80004004
  Abort = [
    0x8b, 0104,0044, 4,
    0xc7, 0,HRESULT_E_ABORT,                     # arg[0] = HRESULT_E_ABORT
    0xc2, 16,
  ].pack('CCCCCCLCS')
  VTBL = [
    EAT2,RET0,RET0,
    # IUnknown { QueryInterface, AddRef, Release }
    EAT2,EAT1,EAT1,OnProgress,EAT2,EAT2,EAT4,EAT2,
    # IBindStatusCallback
  ].pack('p*')
  class Downloading
    def initialize url, filename
      @url, @filename = url, filename
      @state = [0].pack('L')
      @info = [0, 0].pack('LL')
      @data = [VTBL, @state, @info].pack('p*')
      @param = [URLDownloadToFile, url, filename, @data, @state].pack('Lp*')
      @handle = CreateThread.call(0, 0, PROC, @param, 0, 0)
      @disposed = false
    end
    def info
      @info.unpack('LL')
    end
    def now
      @info.unpack('LL')[0]
    end
    def max
      @info.unpack('LL')[1]
    end
    def ok?
      return true if @disposed
      CloseHandle.call @handle if @disposed = @state.unpack('L')[0] == 1
      @disposed
    end
    def stop
      return if ok?
      CallWindowProc.call(Abort, @state, 0, 0, 0)
    end
    def stop!
      return if ok?
      CallWindowProc.call(Abort, @state, 0, 0, 0)
      until ok?
        yield if block_given?
        Graphics.update
      end
    end
  end
  Pool = []
  def self.download url, filename = File.basename(url), cache = true
    url += "?#{rand 2333333}" unless cache
    x = Downloading.new url, filename
    Pool << x
    x
  end
  def self.clear
    Pool.each { |x| x.stop }
    Graphics.update until Pool.all? { |x| x.ok? }
    Pool.clear
  end
end
[/pre][/fold]







评分

参与人数 1星屑 +35 收起 理由
RyanBern + 35 不懂漂亮的代码

查看全部评分

喵喵喵
回复

使用道具 举报

梦石
0
星屑
13562
在线时间
2753 小时
注册时间
2014-10-4
回帖
705

R考场第七期纪念奖

 楼主| 发表于 2017-10-21 01:34:35 | 显示全部楼层
hyrious 发表于 2017-10-20 23:25
触瞎
想学口算虚函数表(不

谢谢!学到了很多
没学过C++ 虚函数表的继承排列方式都是边查边试的啦
假如稍微复杂点就没那么好运气了
倒是 想学手写机器码
第一次知道还能在模块里建类 还有其他许多写法思路
真的很棒耶

刚刚突然想到
在终止下载后需要多久真正停止返回 用update好还是sleep好
测试了一下 估计因为请求回应期的不同
耗时在0-50毫秒之间 大概sleep 0.001比较好吧 update固定25毫秒一刷新
期间想到两个问题
一是self.clear 假如下载任务很多
每个都需要等待结束会比较久
可以先统一call abort
虽然即使十个任务也顶多延迟0.5s...没差了啦
二是人为结束任务前需要检查一下是否完成下载了
否则就比较好玩了
-->下载完成
-->state置1
-->终止下载
-->state置结束符
-->ok?永远不成立
当然也是因为我懒没有多加一个变量..
hhh去改啦

点评

你说的很有道理,我把 `stop!` 改了  发表于 2017-10-21 10:41

评分

参与人数 1星屑 +20 收起 理由
RyanBern + 20 赞认真的作者回复

查看全部评分

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册会员

本版积分规则

Powered by Discuz! X5.0 © 2001-2026 Discuz! Team.

在本版发帖返回顶部