2009年5月1日
我维护的某个程序的某个httphandler需要作修改了,就是在出错三次后,需要增加图形验证码,图验正确后才继续往后台的服务发送验证信息。因为里面的逻辑比较复杂,所以我就增加了两个变量,一个是是否有图验,另一个是图验是否正确。三分钟就修改完了代码,自测了一把,一切OK,于是提交更新。过一分儿测试组的就传来消息,现在图验只出一次,以后再怎么错,就不再出图验了。
我首先怀疑是不是程序的代码不一致引起的,于是到环境上把DLL取下来,反编译,查看逻辑,一切都是一样的。
最后发现上面的IsReUseable 被置成了true.原来问题是出在这里。当这个属性被置成true后,所有的对象被创建的时候,就会放在一个对象池里,有新的请求的时候,就从池里面去取一个现成的来处理,只有当发现对象池的对象不够用的时候,才会继续创建新的对象。
系统又要升级了,编码,测试一切好像都很正常,正当准备要上线的时候,突然发现有一个功能不能用了。这个功能就是使用HTTPS去调别的服务的WebServices。根据程序记下来的日志发现了这样的一个错误:
Catched Exception: [System.Net.WebException]: "基础连接已经关闭: 接收时发生错误。" in <System.Web.Services>
System.Net.WebException: 基础连接已经关闭: 接收时发生错误。 ---> System.EntryPointNotFoundException: 无法在 DLL“security.dll”中找到名为“EnumerateSecurityPackagesW”的入口点。
在 System.Net.UnsafeNclNativeMethods.SafeNetHandles_SECURITY.EnumerateSecurityPackagesW(Int32& pkgnum, SafeFreeContextBuffer_SECURITY& handle)
在 System.Net.SafeFreeContextBuffer.EnumeratePackages(SecurDll Dll, Int32& pkgnum, SafeFreeContextBuffer& pkgArray)
在 System.Net.SSPISecureChannelType.EnumerateSecurityPackages(Int32& pkgnum, SafeFreeContextBuffer& pkgArray)
在 System.Net.SSPIWrapper.EnumerateSecurityPackages(SSPIInterface SecModule)
在 System.Net.SSPIWrapper.GetVerifyPackageInfo(SSPIInterface secModule, String packageName, Boolean throwIfMissing)
在 System.Net.Security.SecureChannel..ctor(String hostname, Boolean serverMode, SchProtocols protocolFlags, X509Certificate serverCertificate, X509CertificateCollection clientCertificates, Boolean remoteCertRequired, Boolean checkCertName, Boolean checkCertRevocationStatus, LocalCertSelectionCallback certSelectionDelegate)
在 System.Net.Security.SslState.ValidateCreateContext(Boolean isServer, String targetHost, SslProtocols enabledSslProtocols, X509Certificate serverCertificate, X509CertificateCollection clientCertificates, Boolean remoteCertRequired, Boolean checkCertRevocationStatus, Boolean checkCertName)
在 System.Net.TlsStream.ProcessAuthentication(LazyAsyncResult result)
在 System.Net.TlsStream.Write(Byte[] buffer, Int32 offset, Int32 size)
在 System.Net.PooledStream.Write(Byte[] buffer, Int32 offset, Int32 size)
在 System.Net.ConnectStream.WriteHeaders(Boolean async)
--- 内部异常堆栈跟踪的结尾 ---
在同一台机器上的旧版程序,相同的代码,确可以正常使用。也是用同一个应用程序池(asp.net的程序),可以排除权限的问题。最后突然想到系统加载security.dll的时候,会不会不是加载的是系统的那个?于是再看一下程序的bin目录下,发现有一个同名的DLL,把它改名后,发现问题依旧,于是到%windir%/microsoft.net/framework/下面的asp.net cache目录下面把临时文件清除后,解决问题。
让我们顺路看看程序加载一个DLL的查找顺序吧。我用程序去引用一个根本不存在的dll,然后用filemon去看文件句柄的查找顺序:
D:\PersonWork\ForTest\ConsoleApplication1\bin\Debug\ironsoftEncryp.dll
D:\PersonWork\ForTest\ConsoleApplication1\bin\Debug\ironsoftEncryp.dll
D:\PersonWork\ForTest\ConsoleApplication1\bin\Debug\ironsoftEncryp.dll
C:\Windows\System32\ironsoftEncryp.dll
C:\Windows\system\ironsoftEncryp.dll
C:\Windows\ironsoftEncryp.dll
D:\PersonWork\ForTest\ConsoleApplication1\bin\Debug\ironsoftEncryp.dll
C:\Program Files\Borland\Delphi7\Bin\ironsoftEncryp.dll
C:\Program Files\Borland\Delphi7\Projects\Bpl\ironsoftEncryp.dll
C:\Windows\System32\ironsoftEncryp.dll
C:\Windows\ironsoftEncryp.dll
C:\Windows\System32\wbem\ironsoftEncryp.dll
C:\Program Files\Microsoft SQL Server\80\Tools\Binn\ironsoftEncryp.dll
C:\Program Files\Microsoft SQL Server\80\Tools\Binn\ironsoftEncryp.dll
C:\Program Files\Microsoft SQL Server\90\Tools\Binn\ironsoftEncryp.dll
C:\Program Files\Microsoft SQL Server\90\DTS\Binn\ironsoftEncryp.dll
C:\Program Files\Microsoft SQL Server\90\Tools\Binn\VSShell\Common7\IDE\ironsoftEncryp.dll
可以总结一下查找顺序:
1. 可执行文件所在目录;
2.进程当前目录;
3.系统目录,%SystemRoot%/system32
4.16位系统目录;,%SystemRoot%/system
5.Windows目录,%SystemRoot%/
6.环境变量PATH中的目录;
2009年3月9日
以前,一个朋友说,网络真的有价值的是电子邮件,这个改变了传统的通讯方式。后来又加了IM,因为它相对于邮件来说更快,更加高效。
现在我觉得网络真的渗透到生活的每个角落:电话费,给手机充值,水费等等这些正常的市政相关的费用都可以在网上来交,省去了去银行排队的痛苦和麻烦。现在网上可信任的网店也越来越多了,想买书,如果不知道哪本书好,可以去图书大厦转转,看看哪本书是想要的,记下名字,然后到网上的书店去下单,很实惠。甚至各种电器也可以在网上下单,不但便宜,还省去了和店里的销售磨嘴皮子的功夫。
网络真的是一个好东西。
2009年1月12日
有时候会碰到一些比较BT的需求,比如说想要实现avi的在线播放。有时候会发现一些工具能实现,但是发现总是不够稳定。原因在于协议,如果播放器写得好,可以一边下载,一边分析下载的流,来实现播放。但是想要实现快进,或是拖动这些的就不容易了。
要解决这些问题,只有一点,那就是自己来实现这套流协议,然后作播放器来播放。为了实现这些东西,偶作一下分析,可以简单地把技术点分为三个:
1.流服务器的实现
2.流播放器的实现
3.注册客户端,来实现播放器的关联(这样点击网页上的链接时,可以自动弹出我们的播放器来播放)
接下来让我们详细地看看具体的实现分析吧。
一、流服务器的实现
我们可以通过分析AVI的文件格格式,以及播放时的需要,可以弄出来一个很简单的流协议。并且为了方便使用,我还是基于标准的HTTP协议上来扩展实现。在这里,我把这个协议分成了两大块:媒体摘要和媒体块。
媒体的摘要:根据我们在播放AVI的时候的要求,我们的摘要里面要含有这些信息:宽,高,帧率,解码器类型,流的数量(音频和视频),推荐的缓冲大小,总帧数,如果有音频流,那还得指定音频的采样频率,位数等信息,以及每一帧的索引。
媒体块:在这里面,我们就是真正的下载数据了,每次请求的帧数,这个可以根据我们的应用环境综合考虑了,主要要考虑以下几点:下载尽量快,请求数尽量少。
有了服务器的这些支持,我们就可以实现我们的客户端了。
二、流播放器的实现
具体的播放实现我在这里就不多写了,对于媒体播放器来说,无非就是拿到一帧数据,然后调用对应的解码器解成位图数据,然后把它画在界面上。
在这里主要讲流播放器数据的请求和快进的实现:
我把程序划成两个线程:一个线程负责下载数据,另一个线程负责把内容绘在界面上。
第一步获得媒体的摘要。得到这个摘要后,我们就可以得到媒体的大概信息了。然后再顺序地把索引表发送到服务器上去请求数据。为了更好的播放性能,我们可以把下载下面的媒体块保存在临时文件里面,然后再加一个索引位置来标明每一帧的起始位,以及长度。当播放时,如果发现该帧还未下载。就给那个下载线程发个请求,让它下次下载的时候,就从这个位置开始。所以快进这些的实现就变得很简单了,如果没有下载,只要很短时间的buffer就可以继续播放了。
三、注册客户端
为了更好的体验,偶们可以注册新的协议,这样用户在点击浏览器上的链接的时候,就可以自动打开我们的播放器来播放,偶暂把这个协议定为:ivp(Ironsoft Video Protrol).注册协议很简单,只要把以下的注册导入就可以了,当然我们也可以编程实现生成这些注册表:
Windows Registry Editor Version 5.00
[HKEY_CLASSES_ROOT\IVP]
@="IVPProtocol"
"URL Protocol"="\"C:\\Program Files\\IronSoftPlayer\\player.exe\""
[HKEY_CLASSES_ROOT\IVP\DefaultIcon]
@="\"C:\\Program Files\\IronSoftPlayer\\player.exe\",1"
[HKEY_CLASSES_ROOT\IVP\shell]
[HKEY_CLASSES_ROOT\IVP\shell\open]
[HKEY_CLASSES_ROOT\IVP\shell\open\command]
@="\"C:\\Program Files\\IronSoftPlayer\\player.exe\" \"%1\""
?
好了,avi的在线流播放就可以很轻松地实现了。嘿嘿,偶一共是用三个工作日左右就全部实现了。
2009年1月8日
IronSoft的FlashCapture可以说是当前已知的一个唯一的可以实现flash抓图的组件,可惜它是在asp才能使用。现在跟着我来看看,如何在asp.net下面去调用它。
首先,在asp.net下添加FlashCapture的引用:
选中网站的项目,然后右键,选择“添加引用”,在弹出来的框里面选择”COM”这个选项卡,往下拉,找到”Ironsoft Library”,然后点击确定,这时候可以看到在bin目录下面添加了:Interop.IronSoft.dll.
其实就是添加代码:
FlashCaptureClass fc = new FlashCaptureClass();
fc.FileName = Server.MapPath("test.swf");
fc.CaptureFrame = 1;
fc.Save(Server.MapPath("test.jpg"));
这时候你会发现图片是抓出来了,可是是一片白的。这是因为asp和asp.net的线程模型不一样了,所以出现了这样的问题,这时候我们要设置asp.net的这个页面的Asp兼容模式:AspCompat 为"true"。也就是写在asp页面的顶上,如:
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="FlashCapture.aspx.cs" Inherits="FlashCapture" AspCompat="true" %>
这下子抓出来的图片里面就有内容了(IronSoft的其它组件调用如果有类似的问题,也都可以这样处理)。
最后,如果你用VS自带的服务器功能来调试的时候,会发现系统的状态栏上多了一个图标。这说明这时候FlashCapture组件并没有被释放掉。可以用以下代码来实现释放:
System.Runtime.InteropServices.Marshal.ReleaseComObject(fc);
GC.Collect();
这样就轻松实现了在asp.net下面实现Flash的抓图功能了。
2009年1月7日
有时候开发一些应用,需要用到Office的一些组件,比如说在线生成Excel,或是Word文档。在自己的机器上跑得满好的,可是部署的时候,发现报错,如:“未能加载文件或程序集“Microsoft.Office.Interop.Excel, Version=11.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c”或它的某一个依赖项。系统找不到指定的文件。 ”。这个最常规的办法就是在服务器上安装一个Office。不过这太大了,而且有时候并不一定需要真的Office在上面运行。为了解决这个问题,微软发布了Office的一些更新包:Redistributable Primary Interop Assemblies.
下载地址是:
Office2003: http://www.microsoft.com/downloads/details.aspx?FamilyId=3C9A983A-AC14-4125-8BA0-D36D67E0F4AD&displaylang=en
Office2007:http://www.microsoft.com/downloads/details.aspx?FamilyId=3C9A983A-AC14-4125-8BA0-D36D67E0F4AD&displaylang=en
不过微软不建议我们这样来用Office。上面写了,如果把它放在asp.net或是windows服务中,Office会变得不稳定或是没有响应。这也就是为啥有时候服务器上会多很多Office相关的进程,因为没有办法正常地关闭它。
2008年12月22日
当我们使用ProcessExplorer的时候,发现它可以得到各个进程的启动参数(也就是在查看一个进程的属性里,在commandline里面显示的内容)。但是翻遍了MSDN也没有对应的API可以去作这样的事情,最开始的时候很无奈,只好先用内存查看器,看一下commmandline在内存啥位置,然后再用ReadProcessMemory去读一个尽可能大的块出来,虽然简单,但是不可靠,而且有时候读出来的东西后面是一堆无用的数据,影响美观。
经常不断的搜索,终于找到了方法:
1.先用OpenProcess 打开目标进程的进程空间,得到句柄
2.使用NtQueryInformationProcess这个API去读取进程里面的PE块的基地址也就是:PebBaseAddress
3.继续使用ReadProcessMemory,从这个PebBaseAddress,开始读取PEB(PE Block),这时候可以得到ProcessParameters,进程的参数地址
4.继续使用ReadProcessMemory,从这个ProcessParameters,开始读取PROCESS_PARAMETERS,这时候可以得到CommandLine.Length和CommandLine.Buffer,也就是启动参数的长度和启动参数的地址。
5.最后再使用ReadProcessMemory,根据记动参数的地址和长度去读取启动参数。特别要注意的事情是,如果在unicode的系统中,这时候读到的启动参数也是unicode的,所以得定义对应的字串类型去读取,不然打印出来的字串只有第一个字母(比如说参数是:abc,如果用ansi的字串,结果就是:a\0b\0c\0,\0这个就表示字串的结束了)。
最后,如果发现在第2步的时候出现读取错误,这时候应该是程序没有debug的权限了,可以用以下方法来提升程序的权限:
1.先用LookupPrivilegeValue来查看能否拥有:SeDebugPrivilege这个权限
2.如果可以,就用以下代码来提升权限:
Privileges.Privileges[0].Luid:=DebugNameValue;
Privileges.Privileges[0].Attributes:=SE_PRIVILEGE_ENABLED;
Result:=AdjustTokenPrivileges(TokenHandle,False,Privileges,SizeOf(Privileges),nil,RetLen);
2008年12月12日
最近突然发现招行的网银不好使了,输入密码后,提示:无效用户,请重新输入#21。于是以为是恢复的证书坏掉了,把用户删掉重新恢复。经过几天痛苦的尝试,终于把恢复问题的答案给答对了。重建了帐户,再次登录还是这样的错误。
最后没有招了,google一下,发现原来这是招行网银的一个防盗策略:如果里面的密码不是本机的键盘输入的话,就作出这样的提示。偶在本本的外挂键盘下输入的密码,所以不成功。改用本本自己的键盘输入就没有问题了。
2008年12月8日
前一阵子,项目中的一个页面每秒只能处理300次,而这个页面的逻辑也不复杂,就是根据条件拼出一个字串然后输出。开始以为这里面逻辑太复杂,所以有问题。不过后面发现了vs里面带了性能分析工具,于是抱着试试看的想法,作了一下性能分析。最后的结果让人大吃一惊:string.format这个操作竟然用掉了一半的时间,为啥它会这么费时间呢?为了真相,我用.net reflector查看了string的实现:
public static string Format(IFormatProvider provider, string format, params object[] args)
{
if ((format == null) || (args == null))
{
throw new ArgumentNullException((format == null) ? "format" : "args");
}
StringBuilder builder = new StringBuilder(format.Length + (args.Length * 8));
builder.AppendFormat(provider, format, args);
return builder.ToString();
}
很让人吃惊,string.format竟然是调用了StringBuilder的AppendFormat来实现的。再继续根下去(这个源码只是通过IL得来的,可能和原始的不太一样,但是差不多了),注意里面的红色的那句:
public StringBuilder AppendFormat(IFormatProvider provider, string format, params object[] args)
{
int num3;
if ((format == null) || (args == null))
{
throw new ArgumentNullException((format == null) ? "format" : "args");
}
char[] chArray = format.ToCharArray(0, format.Length);
int index = 0;
int length = chArray.Length;
char ch = '\0';
ICustomFormatter formatter = null;
if (provider != null)
{
formatter = (ICustomFormatter) provider.GetFormat(typeof(ICustomFormatter));
}
Label_004E:
num3 = index;
int num4 = index;
while (index < length)
{
ch = chArray[index];
index++;
if (ch == '}')
{
if ((index < length) && (chArray[index] == '}'))
{
index++;
}
else
{
FormatError();
}
}
if (ch == '{')
{
if ((index < length) && (chArray[index] == '{'))
{
index++;
}
else
{
index--;
break;
}
}
chArray[num4++] = ch;
}
if (num4 > num3)
{
this.Append(chArray, num3, num4 - num3);
}
if (index == length)
{
return this;
}
index++;
if (((index == length) || ((ch = chArray[index]) < '0')) || (ch > '9'))
{
FormatError();
}
int num5 = 0;
do
{
num5 = ((num5 * 10) + ch) - 0x30;
index++;
if (index == length)
{
FormatError();
}
ch = chArray[index];
}
while (((ch >= '0') && (ch <= '9')) && (num5 < 0xf4240));
if (num5 >= args.Length)
{
throw new FormatException(Environment.GetResourceString("Format_IndexOutOfRange"));
}
while ((index < length) && ((ch = chArray[index]) == ' '))
{
index++;
}
bool flag = false;
int num6 = 0;
if (ch == ',')
{
index++;
while ((index < length) && (chArray[index] == ' '))
{
index++;
}
if (index == length)
{
FormatError();
}
ch = chArray[index];
if (ch == '-')
{
flag = true;
index++;
if (index == length)
{
FormatError();
}
ch = chArray[index];
}
if ((ch < '0') || (ch > '9'))
{
FormatError();
}
do
{
num6 = ((num6 * 10) + ch) - 0x30;
index++;
if (index == length)
{
FormatError();
}
ch = chArray[index];
}
while (((ch >= '0') && (ch <= '9')) && (num6 < 0xf4240));
}
while ((index < length) && ((ch = chArray[index]) == ' '))
{
index++;
}
object arg = args[num5];
string str = null;
if (ch == ':')
{
index++;
num3 = index;
num4 = index;
while (true)
{
if (index == length)
{
FormatError();
}
ch = chArray[index];
index++;
switch (ch)
{
case '{':
if ((index < length) && (chArray[index] == '{'))
{
index++;
}
else
{
FormatError();
}
break;
case '}':
if ((index < length) && (chArray[index] == '}'))
{
index++;
}
else
{
index--;
if (num4 > num3)
{
str = new string(chArray, num3, num4 - num3);
}
goto Label_0253;
}
break;
}
chArray[num4++] = ch;
}
}
Label_0253:
if (ch != '}')
{
FormatError();
}
index++;
string str2 = null;
if (formatter != null)
{
str2 = formatter.Format(str, arg, provider);
}
if (str2 == null)
{
if (arg is IFormattable)
{
str2 = ((IFormattable) arg).ToString(str, provider);
}
else if (arg != null)
{
str2 = arg.ToString();
}
}
if (str2 == null)
{
str2 = string.Empty;
}
int repeatCount = num6 - str2.Length;
if (!flag && (repeatCount > 0))
{
this.Append(' ', repeatCount);
}
this.Append(str2);
if (flag && (repeatCount > 0))
{
this.Append(' ', repeatCount);
}
goto Label_004E;
}
发现里面会有new string,这时候会有新的内存分配出现,也就是说string.format会产生很多临时的string对象,这个会费时间,同时也会使GC的工作量增加.既然这里面调用了stringbuilder来实现的,那为啥不直接调用stringbuilder.append来实现。于是我就把原来的实现改成了stringbuilder的append,同时设置它初始容量为我们预期的大小,通过测试,这部分的性能提高了十倍。于是性能问题解决了。
最后,我觉得如果程序的性能很重要,而在这里面又经常有string.format的时候,还是改用stringbuilder.append来实现,虽然麻烦一些,代码也不好看,但是效果还是会很明显的。
2008年12月3日
虽然AVI的文件格式很熟悉了,根据RIFF file refence里面的说明生成的AVI拿mpc一类的播放器可以正常播放,但是windows media player却死活也播放不了。用偶的程序生成的AVI文件在目录里面也没有预览,也读不出文件里面的信息,搞得偶好郁闷。有时候甚至想把自己写的这个上千行的avibuilder删了,换成系统自带的avi相关的API来实现,不过看着那么多的API自己感觉很复杂,也没有办法支持压缩的格式,所以只好放弃。
今天无聊了,又拿起了visual dub来看avi的格式,突然我发现我生成的avi文件和正常的avi文件的文件头的差别了(左边是错误的,右边是正确的,黄颜色背景的那个):
一个极小的失误,费了好长的工夫。只怪当时看文档的时候没有多想想,怎么可能会把header的标识放到了stream上面。
修改完了后,偶的avibuidler生成的AVI终于和正常的一样了。