【程序说明】
P2P(点对点)的流行产生了大批网络传输软件,这里我们要介绍的就是自己写一个简单的P2P文件传输,一方发送文件,一方接受,直到传输完整个文件。
程序运行效果如图所示。
服务器端:
图8.5.1
客户端:
图8.5.2
由服务器端负责发送文件,客户端接受。
【编程思路】
利用TCP协议连接双方,服务器端建立文件流读入待发送文件进入监听状态,客户端发送信号开始传输,服务器根据客户端发送的当前文件流传输位置按接收缓冲区大小一块一块的发送给客户端,客户端接受后再保存到接收文件流,直到整个文件流发送完毕,这里服务器端使用TIdTCPServer组件,客户端对应的使用TIdTCPClient组件。
【编程步骤】
1.启动Delphi7,建立一个标准的Application,首先我们来做服务器端,。
2.按图放置如下组件:
图8.5.3
将项目保存Server目录下,取名为Server.dpr,单元取名为U_Server.pas。
3.然后我们再来看看客户端,这里由于我们实际上是做了两个程序(服务器端和客户端),因而引入一个新的概念:项目组(ProjectGroup),使用项目组我们很方便的同时调试两个以上的程序,也因为如此,我们上面才需要更改项目名,单元名以区分服务端和客户端,使用项目组功能首先我们找到DELPHI的IDE菜单的View项,打开ProjectManager(Ctrl+Alt+F11)即可看到一个项目组管理窗口,其中已经有我们刚才建立的Server.exe了,现在我们New新建一个标准的Application项目,按照下图放置组件:
图8.5.4
将项目保存在Client目录下,取名Client.dpr,单元名U_Client.pas,现在我们可以看到,项目组窗口中多了一个Client.exe项目,其中项目名黑色加粗表示当前激活的项目。以上组件除了Tlabel组件、Tbutton组件修改标题和StatusBar1修改SimplePanel为True外全部使用默认属性,属性列表我们这里就省略了。
4.编写代码:
首先来看服务器端,浏览文件将文件名传给Edit1:
procedureTfrm_Server.Button1Click(Sender:TObject);
begin
ifOpenDialog1.Executethen
Edit1.Text:=OpenDialog1.FileName;
end;
然后进入传输状态:
procedureTfrm_Server.Button2Click(Sender:TObject);
begin
ifnotFileExists(Edit1.Text)then//检测文件是否存在
begin
Showmessage(''文件不存在,请选择文件!'');
exit;
end;
//建立文件流
AFileStream:=TFileStream.Create(Edit1.Text,fmOpenRead);
ProgressBar1.Max:=AFileStream.Size;//初始化进度条的最大值
ProgressBar1.Position:=0;
ButtonBegin;//VCL开始状态设置
//服务器准备好连接
IdTCPServer1.DefaultPort:=StrToIntDef(Edit2.Text,9925);
ifnotIdTCPServer1.ActivethenIdTCPServer1.Active:=True;
end;
其中IdTCPServer1.Active:=True即让服务器端听入监听状态,结束后取消此状态,实际上我们也可以程序一运行就让他开始监听,在进入监听状态前我们首先要设置服务器监听使用的端口:
IdTCPServer1.DefaultPort:=StrToIntDef(Edit2.Text,9925);
这句就设置了端口为Edit2的值,转换文本到数字失败就使用默认9925端口。
ButtonBegin是我们自己写的一个过程,目的是改变一些按钮的可操作状态,例如传输过程中不允许在选择文件等:
procedureTfrm_Server.ButtonBegin;
begin//VCL开始状态设置
Button1.Enabled:=False;//不可浏览
Button2.Enabled:=False;//不可发送
Button3.Enabled:=True;//可以取消
Button4.Enabled:=False;//不可退出
end;
这种方法在组件很多,需要限定很多组件的状态时可以使程序可读性更好,也方便多次调用,同样后面用到的ButtonEnd过程也是一样,在取消按钮代码中:
procedureTfrm_Server.Button3Click(Sender:TObject);
begin
StatusBar1.SimpleText:=''传输取消...'';
AFileStream.Free;//释放文件流
ButtonEnd;//VCL结束状态设置
end;
程序的重点就是如何进行传输了,IdTCPServer1的OnExecute事件捕获到任何一个连接到服务器的进程Athread就激发此事件,这里我将在此代码注释中详细解释传输过程:
procedureTfrm_Server.IdTCPServer1Execute(AThread:TIdPeerThread);
var
cmd:string;//接收到客户端的字符串信息
ASize:Integer;//需要传输的流大小
begin
withAThread.Connectiondo//已经连街上的一个进程
begin
cmd:=UpperCase(ReadLn);//客户端发送的命令字符串
ifcmd=''BEGIN''then//开始传输
begin
//告诉远程传输文件的大小和文件名
WriteLn(Format(''%d|%s'',[AFileStream.Size,ExtractFileName(Edit1.Text)]));
StatusBar1.SimpleText:=''准备传输...'';
Exit;
end;
ifcmd=''END''then
begin//传输完成
Button3.Click;
StatusBar1.SimpleText:=''传输完成...'';
Exit;
end;
ifcmd=''CANCEL''then
begin//传输取消
StatusBar1.SimpleText:=''传输取消...'';
//保持传输状态
Exit;
end;
//按照指定位置传输文件
AFileStream.Seek(StrToInT(cmd),soFromBeginning);//转到文件流传输的位置
ASize:=Min(AFileStream.Size-AFileStream.Position,RecvBufferSize);
//计算需要发送的大小,Min()函数在Math单元
OpenWriteBuffer;//准备发送缓冲
WriteStream(AFileStream,false,false,ASize);
//注意这个函数的参数。
CloseWriteBuffer;//结束发送缓冲
StatusBar1.SimpleText:=Format(''当前传输位置%s/大小%d'',[cmd,AFileStream.Size]);
ProgressBar1.Position:=ProgressBar1.Position+ASize;
end;
end;
其中主要是WriteStream()函数的使用注意参数:
原型WriteStream(AStream:TStream;
constAAll:Boolean=True;
const:Boolean=False;
const:Integer=0);virtual;
参数AAll表示是否一次全部发送;参数AwriteByteCount表示是否在发送的信息中包含大小信息;参数ASize表示发送的大小,这里可以用F1查看Delphi帮助获取帮助信息,也可以按住Ctrl键鼠标点击WriteStream直接察看他的实现代码获取参考信息,而上面代码中的With…do语法也是经常用到的,例如:
withbutton1do
begin
caption:=’电脑报’;
end;
他表示在这个语法begin…end中的代码如果有属性如A:=C属于with…do中的组件B,则相当于B.A:=C;上面的例子实际上就是Button1.Caption:=’电脑报’。
当然要深入理解上面的代码最好配合一下客户端的接收按钮的代码:
procedureTfrm_Client.Button1Click(Sender:TObject);
var
cmd:string;
ASize,TotalSize:Int64;
AFileStream:TFileStream;
begin
IdTCPClient1.Host:=Edit1.Text;//连接主机
IdTCPClient1.Port:=StrToIntDef(Edit2.Text,9925);//端口
IdTCPClient1.Connect;//连接
try
IdTCPClient1.WriteLn(''BEGIN'');//提示服务器开始接收
cmd:=IdTCPClient1.ReadLn;
//以“|”符号分离文件名
SaveDialog1.FileName:=Copy(cmd,Pos(''|'',cmd)+1,Length(cmd));
ifnotSaveDialog1.Executethen
begin
IdTCPClient1.WriteLn(''CANCEL'');//告诉服务器取消
IdTCPClient1.Disconnect;//断开连接
exit;
end;
TotalSize:=StrToInt(Copy(cmd,0,Pos(''|'',cmd)-1));//分离文件大小
//建立文件流准备接收
AFileStream:=TFileStream.Create(SaveDialog1.FileName,fmCreate);
try//循环开始接受
repeat
IdTCPClient1.WriteLn(IntToStr(AFileStream.Size));//发送当前传输的位置
ASize:=Min(TotalSize-AFileStream.Size,IdTCPClient1.RecvBufferSize);
//选择剩余大小和缓冲区大小小的一个作为传输的大小
IdTCPClient1.ReadStream(AFileStream,ASize);//接收流
StatusBar1.SimpleText:=Format(''当前传输位置%d/大小%d'',[AFileStream.Size,TotalSize]);
Application.ProcessMessages;
untilAFileStream.Size=TotalSize;//大小一致了表示结束
finally
AFileStream.Free;//释放文件流
end;
IdTCPClient1.WriteLn(''END'');//提示服务器传输完成
StatusBar1.SimpleText:=''传输完成...'';
except
StatusBar1.SimpleText:=''连接服务器失败或者对方已经中断传输!'';
end;
IdTCPClient1.Disconnect;
end;
5.主要代码这里就介绍完了,F9分别运行一下服务器端和客户端,编译好exe文件后运行两个程序再传输一个文件看看效果。
【程序小结】
本程序主要的功能由TIdTCPServer和TIdTCPClient组件完成,通过本例我们主要掌握文件流的分段传输,C/S模式的通信,with…do结构的用法,项目组的使用等。
这里只是一个演示,因而我们只做了单向传输,也就是只能服务器端发文件给客户端,程序稍加修改即可做成双向的,留给读者思考吧。
【作者后话】
TIdTCPServer和TIdTCPClient组件本身就具有自动分割大文件流传输的功能,察看TIdTCPConnection.WriteStream的代码可以看到它的分段方式和我们上面介绍的方法相似,假设我们的服务端发送代码改为:
withAThread.Connectiondo
begin
OpenWriteBuffer;
WriteStream(AFileStream,true,false,AFileStream.Size);
//注意上面的参数,参数二发送全部数据,参数三不发送大小,参数四发送的大小为文件流大小
CloseWriteBuffer;
Disconnect;
end;
接收端接收代码改为:
IdTCPClient1.ReadStream(AFileStream,-1,true);//读入全部直到结束服务端Disconnect的。
这种接收方式不需要提前知道文件流大小,它将自动一直接收数据直到断开连接,使用这种方法代码将更简单。
|
|