二、实际应用之一:利用流制作EXE文件加密器、捆绑、自解压文件及安装程序
我们先来说一下如何制作一个EXE文件加密器吧。
EXE文件加密器的原理:建立两个文件,一个用来添加资源到另外一个EXE文件里面,称为添加程序。另外一个被添加的EXE文件称为头文件。该程序的功能是把添加到自己里面的文件读出来。Windows下的EXE文件结构比较复杂,有的程序还有校验和,当发现自己被改变后会认为自己被病毒感染而拒绝执行。所以我们把文件添加到自己的程序里面,这样就不会改变原来的文件结构了。我们先写一个添加函数,该函数的功能是把一个文件当作一个流添加到另外一个文件的尾部。函数如下:
function Cjt_AddtoFile(SourceFile, TargetFile: string): Boolean;
var
Target, Source: TFileStream;
MyFileSize: integer;
begin
try
Source := TFileStream.Create(SourceFile, fmOpenRead or fmShareExclusive);
Target := TFileStream.Create(TargetFile, fmOpenWrite or fmShareExclusive);
try
Target.Seek(0, soFromEnd); //往尾部添加资源
Target.CopyFrom(Source, 0);
MyFileSize := Source.Size + Sizeof(MyFileSize); //计算资源大小,并写入辅程尾部
Target.WriteBuffer(MyFileSize, sizeof(MyFileSize));
finally
Target.Free;
Source.Free;
end;
except
Result := False;
Exit;
end;
Result := True;
end;
有了上面的基础,我们应该很容易看得懂这个函数。其中参数SourceFile是要添加的文件, 参数TargetFile是被添加到的目标文件。比如说把a.exe添加到b.exe里面可以:Cjt_AddtoFile('a.exe', b.exe');如果添加成功就返回True否则返回假。
根据上面的函数我们可以写出相反的读出函数:
function Cjt_LoadFromFile(SourceFile, TargetFile: string): Boolean;
var
Source: TFileStream;
Target: TMemoryStream;
MyFileSize: integer;
begin
try
Target := TMemoryStream.Create;
Source := TFileStream.Create(SourceFile, fmOpenRead or fmShareDenyNone);
try
Source.Seek(-sizeof(MyFileSize), soFromEnd);
Source.ReadBuffer(MyFileSize, sizeof(MyFileSize)); //读出资源大小
Source.Seek(-MyFileSize, soFromEnd); //定位到资源位置
Target.CopyFrom(Source, MyFileSize - sizeof(MyFileSize)); //取出资源
Target.SaveToFile(TargetFile); //存放到文件
finally
Target.Free;
Source.Free;
end;
except
Result := false;
Exit;
end;
Result := true;
end;
其中参数SourceFile是已经添加了文件的文件名称, 参数TargetFile是取出文件后保存的目标文件名。比如说Cjt_LoadFromFile('b.exe', 'a.txt'); 在b.exe中取出文件保存为a.txt。如果取出成功就返回True否则返回假。
打开Delphi,新建一个工程,在窗口上放上一个Edit控件Edit1和两个Button: Button1和Button2。Button的Caption属性分别设置为“确定”和“取消”。在Button1的Click事件中写代码:
var S: string;
begin
S := ChangeFileExt(Application.ExeName, '.Cjt');
if Edit1.Text = '790617' then
begin
Cjt_LoadFromFile(Application.ExeName, S);
{取出文件保存在当前路径下并命名"原文件.Cjt"}
Winexec(pchar(S), SW_Show); {运行"原文件.Cjt"}
Application.Terminate; {退出程序}
end
else
Application.MessageBox('密码不对,请重新输入!', '密码错误', MB_ICONERROR + MB_OK);
编译这个程序,并把EXE文件改名为head.exe。新建一个文本文件head.rc, 内容为: head exefile head.exe, 然后把它们拷贝到Delphi的BIN目录下,执行Dos命令Brcc32.exe head.rc, 将产生一个head.res的文件,这个文件就是我们要的资源文件,先留着。
我们的头文件已经建立了,下面我们来建立添加程序。
新建一个工程,放上以下控件:一个Edit, 一个Opendialog, 两个Button1的Caption属性分别设置为" 选择文件" 和" 加密" 。在源程序中添加一句: {$R head.res}并把head.res文件拷贝到程序当前目录下。这样一来就把刚才的head.exe跟程序一起编译了。
在Button1的Cilck事件里面写下代码:
if OpenDialog1.Execute then Edit1.Text := OpenDialog1.FileName;
在Button2的Cilck事件里面写下代码:
var S: string;
begin
S := ExtractFilePath(Edit1.Text);
if ExtractRes('exefile', 'head', S + 'head.exe') then
if Cjt_AddtoFile(Edit1.Text, S + 'head.exe') then
if DeleteFile(Edit1.Text) then
if RenameFile(S + 'head.exe', Edit1.Text) then
Application.MessageBox('文件加密成功!', '信息', MB_ICONINFORMATION + MB_OK)
else
begin
if FileExists(S + 'head.exe') then DeleteFile(S + 'head.exe');
Application.MessageBox('文件加密失败!', '信息', MB_ICONINFORMATION + MB_OK)
end;
end;
其中ExtractRes为自定义函数,它的作用是把head.exe从资源文件中取出来。
function ExtractRes(ResType, ResName, ResNewName: string): boolean;
var
Res: TResourceStream;
begin
try
Res := TResourceStream.Create(Hinstance, Resname, Pchar(ResType));
try
Res.SavetoFile(ResNewName);
Result := true;
finally
Res.Free;
end;
except
Result := false;
end;
end;
注意:我们上面的函数只不过是简单的把一个文件添加到另一个文件的尾部。实际应用中可以改成可以添加多个文件,只要根据实际大小和个数定义好偏移地址就可以了。比如说文件捆绑机就是把两个或者多个程序添加到一个头文件里面。那些自解压程序和安装程序的原理也是一样的,不过多了压缩而已。比如说我们可以引用一个LAH单元,把流压缩后再添加,这样文件就会变的很小。读出来时先解压就可以了。另外,文中EXE加密器的例子还有很多不完善的地方,比如说密码固定为"790617",取出EXE运行后应该等它运行完毕后删除等等,读者可以自行修改。
三、实际应用之二:利用流制作可执行电子贺卡
我们经常看到一些电子贺卡之类的制作软件,可以让你自己选择图片,然后它会生成一个EXE可执行文件给你。打开贺卡时就会一边放音乐一边显示出图片来。现在学了流操作之后,我们也可以做一个了。
添加图片过程我们可以直接用前面的Cjt_AddtoFile,而现在要做的是如何把图像读出并显示。我们用前面的Cjt_LoadFromFile先把图片读出来保存为文件再调入也是可以的,但是还有更简单的方法,就是直接把文件流读出来显示,有了流这个利器,一切都变的简单了。
现在的图片比较流行的是BMP格式和JPG格式。我们现在就针对这两种图片写出读取并显示函数。
function Cjt_BmpLoad(ImgBmp: TImage; SourceFile: string): Boolean;
var
Source: TFileStream;
MyFileSize: integer;
begin
Source := TFileStream.Create(SourceFile, fmOpenRead or fmShareDenyNone);
try
try
Source.Seek(-sizeof(MyFileSize), soFromEnd);
Source.ReadBuffer(MyFileSize, sizeof(MyFileSize)); //读出资源
Source.Seek(-MyFileSize, soFromEnd); //定位到资源开始位置
ImgBmp.Picture.Bitmap.LoadFromStream(Source);
finally
Source.Free;
end;
except
Result := False;
Exit;
end;
Result := True;
end;
上面是读出BMP图片的,下面的是读出JPG图片的函数,因为要用到JPG单元,所以要在程序中添加一句: uses jpeg。
function Cjt_JpgLoad(JpgImg: Timage; SourceFile: string): Boolean;
var
Source: TFileStream;
MyFileSize: integer;
Myjpg: TJpegImage;
begin
try
Myjpg := TJpegImage.Create;
Source := TFileStream.Create(SourceFile, fmOpenRead or fmShareDenyNone);
try
Source.Seek(-sizeof(MyFileSize), soFromEnd);
Source.ReadBuffer(MyFileSize, sizeof(MyFileSize));
Source.Seek(-MyFileSize, soFromEnd);
Myjpg.LoadFromStream(Source);
JpgImg.Picture.Bitmap.Assign(Myjpg);
finally
Source.Free;
Myjpg.free;
end;
except
Result := false;
Exit;
end;
Result := true;
end;
有了这两个函数,我们就可以制作读出程序了。下面我们以BMP图片为例:运行Delphi,新建一个工程,放上一个显示图像控件Image1。在窗口的Create事件中写上一句就可以了:Cjt_BmpLoad(Image1,Application.ExeName);这个就是头文件了,然后我们用前面的方法生成一个head.res资源文件。
下面就可以开始制作我们的添加程序了。全部代码如下:
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
ExtCtrls, StdCtrls, ExtDlgs;
type
TForm1 = class(TForm)
Edit1: TEdit;
Button1: TButton;
Button2: TButton;
OpenPictureDialog1: TOpenPictureDialog;
procedure FormCreate(Sender: TObject);
procedure Button1Click(Sender: TObject);
procedure Button2Click(Sender: TObject);
private
function ExtractRes(ResType, ResName, ResNewName: string): boolean;
function Cjt_AddtoFile(SourceFile, TargetFile: string): Boolean;
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
function TForm1.ExtractRes(ResType, ResName, ResNewName: string): boolean;
var
Res: TResourceStream;
begin
try
Res := TResourceStream.Create(Hinstance, Resname, Pchar(ResType));
try
Res.SavetoFile(ResNewName);
Result := true;
finally
Res.Free;
end;
except
Result := false;
end;
end;
function TForm1.Cjt_AddtoFile(SourceFile, TargetFile: string): Boolean;
var
Target, Source: TFileStream;
MyFileSize: integer;
begin
try
Source := TFileStream.Create(SourceFile, fmOpenRead or fmShareExclusive);
Target := TFileStream.Create(TargetFile, fmOpenWrite or fmShareExclusive);
try
Target.Seek(0, soFromEnd); //往尾部添加资源
Target.CopyFrom(Source, 0);
MyFileSize := Source.Size + Sizeof(MyFileSize); //计算资源大小,并写入辅程尾部
Target.WriteBuffer(MyFileSize, sizeof(MyFileSize));
finally
Target.Free;
Source.Free;
end;
except
Result := False;
Exit;
end;
Result := True;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
Caption := 'Bmp2Exe演示程序.作者:陈经韬';
Edit1.Text := '';
OpenPictureDialog1.DefaultExt := GraphicExtension(TBitmap);
OpenPictureDialog1.Filter := GraphicFilter(TBitmap);
Button1.Caption := '选择BMP图片';
Button2.Caption := '生成EXE';
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
if OpenPictureDialog1.Execute then
Edit1.Text := OpenPictureDialog1.FileName;
end;
procedure TForm1.Button2Click(Sender: TObject);
var
HeadTemp: string;
begin
if not FileExists(Edit1.Text) then
begin
Application.MessageBox('BMP图片文件不存在,请重新选择!', '信息', MB_ICONINFORMATION + MB_OK)
Exit;
end;
HeadTemp := ChangeFileExt(Edit1.Text, '.exe');
if ExtractRes('exefile', 'head', HeadTemp) then
if Cjt_AddtoFile(Edit1.Text, HeadTemp) then
Application.MessageBox('EXE文件生成成功!', '信息', MB_ICONINFORMATION + MB_OK)
else
begin
if FileExists(HeadTemp) then DeleteFile(HeadTemp);
Application.MessageBox('EXE文件生成失败!', '信息', MB_ICONINFORMATION + MB_OK)
end;
end;
end.
怎么样?很神奇吧:)把程序界面弄的漂亮点,再添加一些功能,你会发现比起那些要注册的软件来也不会逊多少吧。
实际应用之三:利用流制作自己的OICQ
OICQ是深圳腾讯公司的一个网络实时通讯软件,在国内拥有大量的用户群。但OICQ必须连接上互联网登陆到腾讯的服务器才能使用。所以我们可以自己写一个在局部网里面使用。
OICQ使用的是UDP协议,这是一种无连接协议,即通信双方不用建立连接就可以发送信息,所以效率比较高。Delphi本身自带的FastNEt公司的NMUDP控件就是一个UDP协议的用户数据报控件。不过要注意的是如果你使用了这个控件必须退出程序才能关闭计算机,因为TNMXXX控件有BUG。所有nm控件的基础 PowerSocket用到的ThreadTimer,用到一个隐藏的窗口(类为TmrWindowClass)处理有硬伤。
出问题的地方:
Psock::TThreadTimer::WndProc(var msg:TMessage)
if msg.message=WM_TIMER then
//他自己处理
msg.result:=0
else
msg.result:=DefWindowProc(0,....)
end
问题就出在调用 DefWindowProc时,传输的HWND参数居然是常数0,这样实际上DefWindowProc是不能工作的,对任何输入的消息的调用均返回0,包括WM_QUERYENDSESSION,所以不能退出windows。由于DefWindowProc的不正常调用,实际上除WM_TIMER,其他消息由DefWindowProc处理都是无效的。
解决的办法是在 PSock.pas,在 TThreadTimer.Wndproc 内
Result := DefWindowProc( 0, Msg, WPARAM, LPARAM );
改为:
Result := DefWindowProc( FWindowHandle, Msg, WPARAM, LPARAM );
早期低版本的OICQ也有这个问题,如果不关闭OICQ的话,关闭计算机时屏幕闪了一下又返回了。
好了,废话少说,让我们编写我们的OICQ吧,这个实际上是Delphi自带的例子而已:)
新建一个工程,在FASTNET面版拖一个NMUDP控件到窗口,然后依次放上三个EDIT,名字分别为EditIP、EditPort、EditMyTxt,三个按钮BtSend、BtClear、BtSave,一个MEMOMemoReceive,一个SaveDialog和一个状态条StatusBar1。当用户点击BtSend时,建立一个内存流对象,把要发送的文字信息写进内存流,然后NMUDP把流发送出去。当NMUDP有数据接收时,触发它的DataReceived事件,我们在这里再把接收到的流转换为字符信息,然后显示出来。
注意:所有的流对象建立后使用完毕后要记得释放(Free),其实它的释构函数应该为Destroy,但如果建立流失败的话,用Destroy会产生异常,而用Free的话程序会先检查有没有成功建立了流,如果建立了才释放,所以用Free比较安全。
在这个程序中我们用到了NMUDP控件,它有几个重要的属性。RemoteHost表示远程电脑的IP或者计算机名,LocalPort是本地端口,主要监听有没有数据传入。而RemotePort是远程端口,发送数据时通过这个端口把数据发送出去。理解这些已经可以看懂我们的程序了。
全部代码如下:
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, ComCtrls, NMUDP;
type
TForm1 = class(TForm)
NMUDP1: TNMUDP;
EditIP: TEdit;
EditPort: TEdit;
EditMyTxt: TEdit;
MemoReceive: TMemo;
BtSend: TButton;
BtClear: TButton;
BtSave: TButton;
StatusBar1: TStatusBar;
SaveDialog1: TSaveDialog;
procedure BtSendClick(Sender: TObject);
procedure NMUDP1DataReceived(Sender: TComponent; NumberBytes: Integer;
FromIP: string; Port: Integer);
procedure NMUDP1InvalidHost(var handled: Boolean);
procedure NMUDP1DataSend(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure BtClearClick(Sender: TObject);
procedure BtSaveClick(Sender: TObject);
procedure EditMyTxtKeyPress(Sender: TObject; var Key: Char);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
procedure TForm1.BtSendClick(Sender: TObject);
var
MyStream: TMemoryStream;
MySendTxt: string;
Iport, icode: integer;
begin
Val(EditPort.Text, Iport, icode);
if icode< > 0 then
begin
Application.MessageBox('端口必须为数字,请重新输入!', '信息', MB_ICONINFORMATION + MB_OK);
Exit;
end;
NMUDP1.RemoteHost := EditIP.Text; {远程主机}
NMUDP1.LocalPort := Iport; {本地端口}
NMUDP1.RemotePort := Iport; {远程端口}
MySendTxt := EditMyTxt.Text;
MyStream := TMemoryStream.Create; {建立流}
try
MyStream.Write(MySendTxt[1], Length(EditMyTxt.Text)); {写数据}
NMUDP1.SendStream(MyStream); {发送流}
finally
MyStream.Free; {释放流}
end;
end;
procedure TForm1.NMUDP1DataReceived(Sender: TComponent;
NumberBytes: Integer; FromIP: string; Port: Integer);
var
MyStream: TMemoryStream;
MyReciveTxt: string;
begin
MyStream := TMemoryStream.Create; {建立流}
try
NMUDP1.ReadStream(MyStream); {接收流}
SetLength(MyReciveTxt, NumberBytes); {NumberBytes为接收到的字节数}
MyStream.Read(MyReciveTxt[1], NumberBytes); {读数据}
MemoReceive.Lines.Add('接收到来自主机' + FromIP + '的信息:' + MyReciveTxt);
finally
MyStream.Free; {释放流}
end;
end;
procedure TForm1.NMUDP1InvalidHost(var handled: Boolean);
begin
Application.MessageBox('对方IP地址不正确,请重新输入!', '信息', MB_ICONINFORMATION + MB_OK);
end;
procedure TForm1.NMUDP1DataSend(Sender: TObject);
begin
StatusBar1.SimpleText := '信息成功发出!';
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
EditIP.Text := '127.0.0.1';
EditPort.Text := '8868';
BtSend.Caption := '发送';
BtClear.Caption := '清除聊天记录';
BtSave.Caption := '保存聊天记录';
MemoReceive.ScrollBars := ssBoth;
MemoReceive.Clear;
EditMyTxt.Text := '在这里输入信息,然后点击发送.';
StatusBar1.SimplePanel := true;
end;
procedure TForm1.BtClearClick(Sender: TObject);
begin
MemoReceive.Clear;
end;
procedure TForm1.BtSaveClick(Sender: TObject);
begin
if SaveDialog1.Execute then MemoReceive.Lines.SaveToFile(SaveDialog1.FileName);
end;
procedure TForm1.EditMyTxtKeyPress(Sender: TObject; var Key: Char);
begin
if Key = #13 then BtSend.Click;
end;
end.
上面的程序跟OICQ相比当然差之甚远,因为OICQ利用的是Socket5通信方式。它上线时先从服务器取回好友信息和在线状态,发送超时还会将信息先保存在服务器,等对方下次上线后再发送然后把服务器的备份删除。你可以根据前面学的概念来完善这个程序,比如说再添加一个NMUDP控件来管理在线状态,发送的信息先转换成ASCII码进行与或运行并加上一个头信息,接收方接收信息后先判断信息头正确与否,如果正确才把信息解密显示出来,这样就提高了安全保密性。
另外,UDP协议还有一个很大的好处就是可以广播,就是说处于一个网段的都可以接收到信息而不必指定具体的IP地址。
网段一般分A、B、C三类,1 ~126.XXX.XXX.XXX(A类网): 广播地址为XXX.255.255.255
128 ~191.XXX.XXX.XXX(B类网): 广播地址为XXX.XXX.255.255
192 ~254.XXX.XXX.XXX(C类网): 广播地址为XXX.XXX.XXX.255
比如说三台计算机192.168.0.1 、192.168.0.10 、192.168.0.18 ,发送信息时只要指定IP地址为192.168.0.255 就可以实现广播了。下面给出一个转换IP为广播IP的函数,快拿去完善自己的OICQ吧^ - ^.
function Trun_ip(S: string): string;
var s1, s2, s3, ss, sss, Head: string;
n, m: integer;
begin
sss := S;
n := pos('.', s);
s1 := copy(s, 1, n);
m := length(s1);
delete(s, 1, m);
Head := copy(s1, 1, (length(s1) - 1));
n := pos('.', s);
s2 := copy(s, 1, n);
m := length(s2);
delete(s, 1, m);
n := pos('.', s);
s3 := copy(s, 1, n);
m := length(s3);
delete(s, 1, m);
ss := sss;
if strtoint(Head) in [1..126] then ss := s1 + '255.255.255'; //1~126.255.255.255 (A类网)
if strtoint(Head) in [128..191] then ss := s1 + s2 + '255.255'; //128~191.XXX.255.255(B类网)
if strtoint(Head) in [192..254] then ss := s1 + s2 + s3 + '255'; //192~254.XXX.XXX.255(C类网)
Result := ss;
end;