HTTP Range

2009-3-29 17:01 | by 2ndboy

  工作关系,两周来一直在研究 HTTP 和 RTSP 等协议,并且实现了一个小型的 HTTP server。其实写 HTTP server 这种事,03 年就干过。那时为了给前公司的某产品配一个 web 方式的管理界面,曾经实现过一个可以完善支持 CGI 的跨平台 mini HTTP server。那时的精力主要集中在跨平台和支持 CGI 上,这一次主要是为了支持多媒体文件的传输,并且要支持 Range。

  所谓 Range,是在 HTTP/1.1(http://www.w3.org/Protocols/rfc2616/rfc2616.html)里新增的一个 header field,也是现在众多号称多线程下载工具(如 FlashGet、迅雷等)实现多线程下载的核心所在。

Range 的规范定义如下:
ranges-specifier = byte-ranges-specifier
byte-ranges-specifier = bytes-unit “=” byte-range-set
byte-range-set = 1#( byte-range-spec | suffix-byte-range-spec )
byte-range-spec = first-byte-pos “-” [last-byte-pos]
first-byte-pos = 1*DIGIT
last-byte-pos = 1*DIGIT
(RFC2616 里充斥着这种形式的定义,一开始觉得挺乱,后来习惯了就发现其实挺科学,挺好理解的:))

值得注意的就是 byte-range-set 是一个 byte-range 的集合,所以在实际请求中完全可能会出现如下这种形式:
Range: bytes=123-567,789-
这个数据区间是个闭合区间,起始值是 0,所以“Range: bytes=0-1”这样一个请求实际上是在请求开头的 2 个字节。

byte-range-spec 里的 last-byte-pos 可以省略,代表从 first-byte-pos 一直请求到结束位置。其实 first-byte-pos 也是可以省略的,只不过就不叫 byte-range-spec 了,而是叫 suffix-byte-range-spec,规范如下:
suffix-byte-range-spec = “-” suffix-length
suffix-length = 1*DIGIT
比如“Range: bytes=-200”,它不是表示请求文件开始位置的 201 个字节,而是表示要请求文件结尾处的 200 个字节。

如果 byte-range-spec 的 last-byte-pos 小于 first-byte-pos,那么这个 Range 请求就是无效请求,server 需要忽略这个 Range 请求,然后回应一个 200 OK,把整个文件发给 client。
如果 byte-range-spec 里的 first-byte-pos 大于文件长度,或者 suffix-byte-range-spec 里的 suffix-length 等于 0,那么这个 Range 请求被认为是不能满足的,server 需要回应一个 416 Requested range not satisfiable。

server 除了要能理解 Range 请求之外,在回应 client 时还要使用 Content-Range 来告诉 client 他到底发送了多少数据,Content-Range 的规范定义如下:
Content-Range = “Content-Range” “:” content-range-spec
content-range-spec = byte-content-range-spec
byte-content-range-spec = bytes-unit SP byte-range-resp-spec “/” ( instance-length | “*” )
byte-range-resp-spec = (first-byte-pos “-” last-byte-pos) | “*”
instance-length = 1*DIGIT

详尽的细枝末节就不在这里记述了,举个例子说明一下,比如某文件的大小是 1000 字节,client 请求这个文件时用了 “Range: bytes=0-500”,那么 server 应该把这个文件开头的 501 个字节发回给 client,同时回应头要有如下内容:
Content-Range: bytes 0-500/1000

Range 请求的一些注意事项:
1) 不支持 Range 请求的 server 要用“Accept-Ranges: none”对 client 表明心意;server 也可以主动告诉 client “Accept-Ranges: bytes”,但是 client 也可以在没有收到这个指示的前提下向 server 发 Range 请求。
2) byte-range-set 中的区间可以是“有洞”的,也可以是部分重叠的
3) 单区间的 byte-range-set 正常回应就可以了,但是多区间 byte-range-set 需要 server 使用 multipart/byterange 来回应

UCenter 创始人的密码修改

2009-3-26 19:48 | by 2ndboy

  UCenter 创始人密码不是写在数据库里的,而是根据放在 UCenter 安装目录下的 data/config.inc.php 里。基本上,创始人密码是这么产生的:

加密后的密码 = md5( md5( 创始人密码 ) + 加扰串 )

下面是一段重新产生创始人密码的 PHP 代码:

  1. <?php
  2.  $UC_FOUNDERSALT = '加扰串';
  3.  $password = '密码';
  4.  
  5.  $UC_FOUNDERPW = md5( md5( $password ) . $UC_FOUNDERSALT );
  6.  
  7.  echo "define( 'UC_FOUNDERSALT', '$UC_FOUNDERSALT' );\n";
  8.  echo "define( 'UC_FOUNDERPW', '$UC_FOUNDERPW' );";
  9.  ?>

用程序运行后产生的结果替换 config.inc.php 里相应的内容就 ok 了。

基于最近好好学习 Python 才能天天向上的原则,再给出一段多余的 Python 实现:

  1. import hashlib
  2.  
  3.  def md5( data ):
  4.      m = hashlib.md5()
  5.      m.update( bytes( data, 'GB2312' ) )
  6.      return( m.hexdigest() )
  7.  
  8.  UC_FOUNDERSALT = '加扰串'
  9.  password = '密码'
  10.  
  11.  print( "define( 'UC_FOUNDERSALT', '%s' );" % UC_FOUNDERSALT )
  12.  print( "define( 'UC_FOUNDERPW', '%s' );" % md5( md5( password ) + UC_FOUNDERSALT ) )

FTP 的“断点续传”

2009-3-22 15:33 | by 2ndboy

  最近写了个 FTP 上传程序,考虑到网络断线等因素造成的上传中断,所以打算给这个程序加个“断点续传”的功能,说是断点续传,其实跟现在 FlashGet 或者迅雷等下载工具的断点续传还是不一样的,我这里断点续传指的是断线以后只上传刚才还没有被上传的和没有完整上传的文件。比如总共有 100 个文件需要上传,只完整上传了 49 个文件,在第 50 个文件只传了一半的时候断线了,那么再次连接之后只需要重新上传第 50 个文件和后面 50 个文件就可以啦。

  为了达到这个目的,只要等取到 FTP 上的文件列表就 OK。初次使用 Python 的 ftplib,尝试了几种方法,FTP.nlst() 只能拿到文件名,拿不到文件大小。最后决定用 FTP.retrlines() 搞定,但是这个接口的麻烦之处是需要一个 callback 来接收从 server 返回的每一行,对 C/C++ 这类语言来说,由于涉及两个函数调用过程,所以上下文关联稍稍有些麻烦,但是 Python 的 lambda 用在这里真是方便(话又说回来,FTP.retrlines() 直接返回一个 tuple 或者 list 不是更干脆?)。

  1. def GetFileInfo( ftp ):
  2.      file_list = []
  3.      try:
  4.          ftp.retrlines( 'LIST', lambda item: file_list.append( item ) )
  5.      except:
  6.          return( {} )
  7.  
  8.      ro = re.compile( '(.).{9}\s+\d+\s+(?:[^ ]+\s+){2}(\d+)\s+(?:[^ ]+\s+){3}(.+)' )
  9.      file_info = {}
  10.  
  11.      for item in file_list:
  12.          m = ro.match( item )
  13.          if m:
  14.              file_info[m.group(3)] = [ m.group(1), m.group(2) ]
  15.  
  16.      return( file_info )

这个函数返回一个 dictionary,然后对于一个本地文件,用 if file in file_info 就能判断 server 上是否已经有了这个文件,有的话,file_info[file][1] 就是 server 上这个文件的大小,这个大小跟本地文件大小不符就重新上传。

用 Python 遍历目录、清空临时文件

2009-3-12 22:01 | by 2ndboy

  前几天由于调试时需要频繁的清空临时文件夹,同事给了我一个他用 Python 写的脚本,目录及文件遍历用的是 os.walk(),后来在查文档的时候又发现了一个 os.path.walk(),Google 了一下,据说 os.walk() 是 Python 2.3 中才增加的,之前只有 os.path.walk()。但是从 Python 3.0 的文档来看,os.path.walk() 已经被废止了,只剩下 os.walk()。

  但是 os.walk() 用起来比较不顺手,不能像 Windows API 那样递归的进行目录树遍历,而是一次性的扔给你某路径下的所有文件和子目录。查了查文档,用 os.listdir() 实现了递归式层层深入的目录和文件遍历:

  1. import os, stat
  2.  
  3.  def WalkDir( dir, dir_callback = None, file_callback = None ):
  4.      for item in os.listdir( dir ):
  5.          fullpath = dir + os.sep + item
  6.          if os.path.isdir( fullpath ):
  7.              WalkDir( fullpath, dir_callback, file_callback )
  8.              if dir_callback: dir_callback( fullpath )
  9.          else:
  10.              if file_callback: file_callback( fullpath )

由于发现目录时会首先递归调用 WalkDir() 自己,然后再给目录 callback 一个执行机会,所以这算是一个深度优先的遍历实现,调用 WalkDir() 时传入相应的目录回调和文件回调就可以实现特定的功能,我利用刚刚实现的基础设施重新实现了一个清空临时文件夹的脚本:

  1. def DeleteDir( dir ):
  2.      try:
  3.          os.rmdir( dir )
  4.      except WindowsError, e:
  5.          print( "Can't delete directory " + dir + ", err = " + str( e.winerror ) )
  6.  
  7.  def DeleteFile( file ):
  8.      try:
  9.          os.unlink( file )
  10.      except WindowsError, e:
  11.          if 32 == e.winerror:
  12.              print( "Can't delete " + file + ", err = " + str( e.winerror ) )
  13.          elif 5 == e.winerror:
  14.              os.chmod( file, stat.S_IWRITE )  # Try to remove readonly attribute
  15.              try:
  16.                  os.unlink( file )  # Try again
  17.              except WindowsError, e:
  18.                  print( "Can't delete " + file + ", err = " + str( e.winerror ) )
  19.  
  20.  WalkDir( os.environ['TEMP'], DeleteDir, DeleteFile )

  最近抽时间看了些 Python 的电子文档,经常写些练手的小东西,以后打算拿这个做日常使用的脚本语言。至于 PHP,还是在 Web 领域比较专精和适用。

[2011/09/14 Update]
“del /f /s /q %temp%”可以部分替代上述 Python 脚本,缺点是不能删除空目录。

ThinkPad Hard Drive Active Protection System

2009-3-7 16:13 | by 2ndboy

  数年前,在我还没有买现在用的这台 T60 之前,曾经在网上看过一条技术新闻,说是国外 Linux 玩家给 ThinkPad 开发了一种软件,它可以响应对显示器拍打的动作,从而执行特定的操作,比如拍拍显示器的边框就运行 Vim 等等。据说这是因为 ThinkPad 内置了特殊的硬件,在其它品牌的本本上,这个软件是没有作用的。

  今天偶然想起这件事来就研究了一把,原来 ThinkPad 内置了一个陀螺仪,这个东西可以监测到笔记本的振动和倾斜,用来在笔记本跌落和受到撞击的时候避免硬盘磁头打到盘面从而使硬盘受伤。IBM 管这套东西叫 Hard Drive Active Protection System,简称 HDAPS。

  网上有大把关于如何在 Linux 下利用 APS 的信息,但是关于如何在 Windows 下利用 APS 的资源就比较少了。经过几次三番的 Google,终于搞明白了如何在 Windows 下获取 ThinkPad APS 信息的方法。

  首先,可以在 ftp://ftp.software.ibm.com/pc/pccbbs/mobiles_pdf/aps2mst.pdf 下载到 IBM Active Protection System Whitepaper,在其中的 User interface and task tray applet 小节下可以找到如下信息:
ShockPrf.sys: kernel mode device driver for prediction algorithm and hard disk drive control
Shockmgr.sys: kernel mode driver for miscellaneous operation
Sensor.dll: application interface dll

可见我们可以利用 C:\Windows\system32 下的 Sensor.dll 来获取 APS 数据,用 eXeScope 看了一下,Sensor.dll 导出了 21 个函数:
00000001:ShockproofCallSMAPIBIOS
00000002:ShockproofControl
00000003:ShockproofEnableDisableSnooze
00000004:ShockproofGetAccelerometerData
00000005:ShockproofGetAccelerometerDataEx
00000006:ShockproofGetAccelerometerMutex
00000007:ShockproofGetAutoDisable
00000008:ShockproofGetShockStatus
00000009:ShockproofGetSlaveCPUinfo
0000000A:ShockproofGetUnloadCnt
0000000B:ShockproofGetVersion
0000000C:ShockproofInformMouseDevChange
0000000D:ShockproofInformPENevent
0000000E:ShockproofInformPMevent
0000000F:ShockproofInformTabletLIDstate
00000010:ShockproofInvokeSnooze
00000011:ShockproofManualSensitivitySetting
00000012:ShockproofReleaseAccelerometerMutex
00000013:ShockproofSetAutoDisable
00000014:ShockproofSnoozeSetting
00000015:ShockproofTaskComplete

但是要利用其中的哪个函数呢?继续搜索,在 CodeProject 找到一篇有用的东西:http://www.codeproject.com/KB/system/LenovoAPS.aspx。不过里面的实现是 C# 的,被我改写成 C 代码如下:

  1. typedef struct
  2.  {
  3.      INT       PresentState;
  4.      USHORT    LatestRawAccelDataX;
  5.      USHORT    LatestRawAccelDataY;
  6.      USHORT    LatestAccelDataX;
  7.      USHORT    LatestAccelDataY;
  8.      CHAR      Temperature;
  9.      USHORT    LatestZeroG_X;
  10.      USHORT    LatestZeroG_Y;
  11.  } AccelerometerData;
  12.  
  13.  typedef void (__stdcall * funcShockproofGetAccelerometerData)( AccelerometerData * );

AccelerometerData 里除了倾斜数据之外还有单位是摄氏度的温度数据:)
最后两项 LatestZeroG_X & LatestZeroG_Y 是最后一次稳定时的数据,这两个值在把本本放在水平桌面上静止一会儿后取出来可以作为水平的参考值;
LatestRawAccelDataX & LatestRawAccelDataY 是陀螺仪输出的原始数据;
LatestAccelDataX & LatestAccelDataY 是 40ms 内由原始数据得出的平均数据;
PresentState 是内部状态值,具体的取值和本本的状态对应关系我没有仔细研究过,但基本上 0 是稳定状态,10 是禁用状态。

一开始应该把本本放在水平桌面上取水平参考值:

  1. USHORT cX = 0;
  2.  USHORT cY = 0;
  3.  
  4.  HMODULE hDll = LoadLibrary( _T("Sensor.dll") );
  5.  if( hDll )
  6.  {
  7.      ShockproofGetAccelerometerData = (funcShockproofGetAccelerometerData)GetProcAddress( hDll, "ShockproofGetAccelerometerData" );
  8.      if( ShockproofGetAccelerometerData )
  9.      {
  10.          AccelerometerData ad = { 0 };
  11.          ShockproofGetAccelerometerData( &ad );
  12.          cX = ad.LatestZeroG_X;
  13.          cY = ad.LatestZeroG_Y;
  14.      }
  15.  }

然后可以不断的调用(通过 timer 等)如下代码获取实时的本本倾斜状态:

  1. AccelerometerData ad = { 0 };
  2.  ShockproofGetAccelerometerData( &ad );
  3.  
  4.  int pitch = (int)( 90 * ( ad.LatestRawAccelDataX - cY ) / ( cX - cY ) );
  5.  int roll  = (int)( 90 * ( ad.LatestRawAccelDataY - cX ) / ( cY - cX ) );

pitch 是前后的倾斜角度,roll 是左右的倾斜角度。

  其实有了以上知识就足以开发出一些有意思的程序了,网上已经有网友利用 APS 写了震动监测(测地震?)、浏览 StreetView、模拟游戏摇杆、模拟鼠标移动、移动窗口等小玩意。所以完全有可能把 iPhone 上一些需要陀螺仪的游戏移植到 PC 上,当然,只能使用有类似 APS 的本本来玩:) 据说 Apple 的本本上就有类似的东西,叫做 Sudden Motion Sensor。

  不过,在利用 APS 特性来玩的时候会触发硬盘保护,所以你会听到硬盘磁头不断复位的咔咔声,所以,一定不要玩太过激烈的玩意:D