1. Socket 定义:
网络上两个程序为了相互通讯运行,构成服务端客户端结构,连接的每一端可称为一个Socket
(或者套接字)。
客户程序可以向服务端Socket 发送请求,服务端收到后处理此请求,然后将处理结果发送给客户端Socket ,从而形成一次应答。如此重复必要次数,就完成了一次通讯
2. 属性
Port: 在哪个端口侦听。
Service: 服务的描述。一般情况下可以设为空;如果是“FTP ”、 “HTTP”、“ Finger ”、“ Time”等公开的协议名,实际侦听 端口会被自动指定为这些公开协议默认的端口。
ServerType: 其中:TServerType = (stNonBlocking, stThreadBlocking); 用于指定线程模式。
stNonBlocking表示单线程执行
stThreadBlocking 表示多线程执行
Address用IP 地址表示,
Host 用计算机名表示。
实现服务端
公用库文件(定义了服务端和客户端使用的令牌,客户端也要使用此文件):
unit FunAndProc; interface uses Windows, Classes, SysUtils; const DefaultPort = 5643 ; { 服务器缺省侦听端口} KEY_Clt: Array[1..4] of String = { 从客户端发出以下令牌} (’AskForFilesName’ , { 请求文件名} ’AskForFilesLengt h’, { 请求文件长度} ’AskForFilesData’ , { 请求发送文件} ’WanttoDisConnect ’); { 文件发送完成,告知服务端连接可以关闭了} KEY_Srv: Array[1..2] of String = { 从服务端发出以下令牌:} (’Return1’ , { 后面跟的是所有文件名,文件名之间用FilesNameSepStr分隔} ’Return2’) ; { 后面跟的是所有文件长度,文件长度之间用FilesLengthSepStr 分隔} FilesNameSepStr = ’| ’; FilesLengthSepStr = ’,’; {StringToStrings 将一个字符串转化为字符串列表,转化方法由字符串中的分隔符SepStr 决 定} function StringToStrings(SepStr: String; S: String): TStrings; { 将字符串列表转化为字符串,由SepStr 分隔} function StringsToString(SepStr: String; Strs: TStrings; GetFileName: Bo olean = False): String; { 返回本机的名字} function Get_ComputerName: String; implementation function StringToStrings(SepStr: String; S: String): TStrings; var P: Integer ; begin Result := TStringLis t.Create; P := Pos(SepStr, S); while P <> 0 do begin Result.Add(Copy(S, 1, P-1)); Delete(S, 1, P-1+L ength(SepStr)); P := Pos(SepStr,S) ; end ; Result.Add(S); end ; function StringsToString(SepStr: String; Strs: TStrings; GetFileName: Bo olean = False): String; var I: Integer; begin Result := ’’; for I := 0 to Strs.Count-1 do if not GetFileName then Result := Result + SepStr + Strs[I] else Result := Result + SepStr + ExtractFileName(Strs[I]); Delete(Result, 1, Le ngth(SepStr)); end ; function Get_ComputerName: String; var iSize: LongWord; ComputerName: PChar; begin iSize := MAX_COMPUTE RNAME_LENGTH + 1; GetMem(ComputerName, iSize); GetComputerName(Comp uterName,iSize); Result := ComputerNa me; FreeMem(ComputerName ); end ; end .
服务端主界面程序:
unit UT_DL_SRV; interface uses Windows, Messages, S ysUtils, Classes, Controls, Forms, ScktComp, StdCtrls, Com Ctrls ; type TFM_DL_SRV = class(TForm) SrvSocket: TServer Socket; sbSRV: TStatusBar; pcSRV: TPageContro l; TabSheet1: TTabShe et; UserInfo: TListVie w; procedure SrvSocketGetThread(Sender: TObject; ClientSocket: TS erverClientWinSocket; var SocketThread : TServerClientThread); procedure FormCreate(Sender: TObject); procedure FormDestroy(Sender: TObject); private FilesName: TString s; public ActiveThreadsCount , BufferSize{ 以KB为单位}: Integer; end ; var FM_DL_SRV: TFM_DL_SR V; implementation {$R *.dfm} uses UT_SRVTHRD, FunAndPr oc; procedure TFM_DL_SRV.FormCreate(Sender: TObject); var Path: String; begin FilesName := TString List.Create; Path := ExtractFileP ath(ParamStr(0)); FilesName.Add(Path + ’\’ + ’ 待传输文件1.txt’); FilesName.Add(Path + ’\’ + ’ 待传输文件2.txt’); ActiveThreadsCount : = 0; { 设定数据缓冲区大小为3K} BufferSize := 3; { 初始化SrvSocket的参数并开始侦听} with SrvSocket do begin Port := DefaultPor t; ServerType := stTh readBlocking; Open; end ; end ; procedure TFM_DL_SRV.FormDestroy(Sender: TObject); begin FreeAndNil(FilesName ); end ; procedure TFM_DL_SRV.SrvSocketGetThread(Sender: TObject; C lientSocket: TServerClientWinSocket; var SocketThread: TServerClientThread); begin { 建立服务端线程ServerThread,并传给参数SocketThread} SocketThread := TSer verThread.Create( True,ClientSoc ket, FilesName, BufferSize); { 设定该线程结束时自动析构} SocketThread.FreeOnT erminate := True; { 启动线程} SocketThread.Resume; Inc(ActiveThreadsCou nt); sbSRV.Panels.Items[0 ].Text := ’当前线程数:’ + IntToStr(ActiveT hreadsCount);; end ; end .
以下是线程TServerThread的实现代码:
unit UT_SRVTHRD; interface uses Classes, ScktComp, ComCtrls; type TServerThread = class(TServerClientThread) private WriteSizes { 以字节为单位}: Integer; { 向客户端发送文件数据时使用的缓冲区大小} FilesName: TString s; { 文件名列表} FilesStrm: Array of TFileStream; { 文件流数组} FilesLength: Array of Integer; { 文件长度数组} AllFilesLength, Fi leCurrLength: Integer; { 所有文件长度;已经对某个文件读取了多少长度的数据;当该长度等于该文件的长度时, 应该开始读下一个文件} Fileth: Integer ; { 当前正在读第几个文件} ListItem: TListIte m; ErrorRaise: Boolea n; procedure ListItemAdd; procedure ListItemEnd; procedure ListItemErr; procedure ThreadCountDec; protected { TServerClientThr ead 类的执行过程,相当于普通线程的TThread.Execute} procedure ClientExecute; override ; public { 重载构造函数,增加两个参数:AFilesName表示要传输的文件名,AWriteSize表示向 客户端写数据时使用的缓冲区大小} constructor Create(CreateSuspended: Boolean; ASocket: TServer ClientWinSocket; AFilesName: TStrings; AWriteSize: I nteger); overload ; destructor Destroy ; override ; end ; implementation uses UT_DL_SRV, SysUtils, FunAndProc; { ServerThread } constructor TServerThread.Create( CreateSuspended : Boolean; ASocket: TServerClientWinSocket; AFilesName: TSt rings; AWriteSize: Integer); var I: Integer; begin inherited Create(CreateSuspended, ASocket); FilesName := TString List.Create; FilesName.Assign(AFi lesName); WriteSizes := AWrite Size*1024; { 向客户端写数据时使用的缓冲区大小} { 初始化所有变量} Fileth := 0 ; FileCurrLength := 0; SetLength(FilesStrm, FilesName.Count); SetLength(FilesLengt h, FilesName.Count); AllFilesLength := 0; { 创建对应个数的文件流对象} for I := 0 to FilesName.Count-1 do begin FilesStrm[I] := TF ileStream.Create( FilesName[I] , fmOpenRead or fmShareDenyNone); FilesLength[I] := FilesStrm[I].Size; Inc(AllFilesLength , FilesLength[I]); end ; ErrorRaise := False; end ; destructor TServerThread.Destroy; var I: Integer; begin for I := Low(FilesStrm) to High(FilesStrm) do FreeAndNil(FilesSt rm[I]); FreeAndNil(FilesName ); if ErrorRaise then { 在一个子线程中对主线程的对象操作时,应该将这些操作定义在一个过程中,并使用 Synchronize 来调用这个过程,以保证操作安全} Synchronize(ListIt emErr) else Synchronize(ListIt emEnd); Synchronize(ThreadCo untDec); inherited; end ; procedure TServerThread.ClientExecute; var pStream: TWinSocketS tream; Buffer: Pointer; ReadText, SendText: String; I: Integer; const {读客户端令牌时使用的缓冲区大小,因为它们都是一些字符串,所以定义为1024Byte 足够了} ReadLen = 1024; begin { 创建连接流对象,以便和客户端交流} pStream := TWinSocke tStream.Create(ClientSocket, 60000); try {ClientSocket 是TServerClient Thread类内置的一个对象,它是和客户端连接的套接字} while (not Termina ted) and ClientSocket.Connected do begin try { 分配读数据缓冲区} Buffer := Alloc Mem(ReadLen); if pStream.Wait ForData(6000) then begin pStream.Read( Buffer^, ReadLen); ReadText := P Char(Buffer); FreeMem(Buffe r); { 客户端请求文件名} if ReadText = KEY_Clt[1] then begin Synchronize (ListItemAdd); SendText := KEY_Srv[1] + StringsToString( FilesNameSepStr, FilesName, True); { 特别注意SendText 后应该加上索引1 ,指定Write方法从SendText 第一个字符 开始读,否则默认从0 开始。那样的话就错了} pStream.Wri te(SendText[1], Length(SendText)+1); end { 客户端请求文件长度} else if ReadText = KEY_Clt[2] then begin SendText := ’’; for I := Lo w(FilesStrm) to High(FilesStrm) do SendText : = SendText + FilesLengthSepStr + IntToS tr(FilesStrm[I].Size); Delete(Send Text, 1, 1); SendText := KEY_Srv[2] + SendText; pStream.Wri te(SendText[1], Length(SendText)+1); end { 客户端请求发送文件} else if ReadText = KEY_Clt[3] then begin { 如果当前文件读取完毕,应该开始读取下一个文件} if FileCurrLength >= FilesLength[Fileth] then begin Inc(Fileth ); FileCurrLe ngth := 0; end ; { 分配写入数据缓冲区} Buffer := A llocMem(WriteSizes); { 从文件流中读取WriteSizes字节的数据并写入连接流,最后累加 FileCurrLength} Inc(FileCur rLength, pStream.Write(Buffer^, FilesStr m[Fileth].Read(Buffer^, WriteSizes))); FreeMem(Buf fer); { 客户端完成了所有文件的接收,请求关闭连接} end else if ReadText = KEY_Clt[4] then Terminate; end ; { 如果发生错误,则结束线程} except ErrorRaise := T rue; Terminate; end ; end ; finally pStream.Free; CltSocket.Close; end ; end ; procedure TServerThread.ListItemAdd; begin ListItem := FM_DL_SR V.UserInfo.Items.Add; ListItem.Caption := DateTimeToStr(Now); with ListItem.SubItems do begin Add(ClientSocket.R emoteHost); Add(ClientSocket.R emoteAddress); Add(IntToStr(Clien tSocket.RemotePort)); Add(StringsToStrin g(’;’, FilesName)); Add(IntToStr(Files Name.Count)); Add(’ 传送文件’); end ; end ; procedure TServerThread.ListItemEnd; begin if ListItem <> nil then with ListItem.SubItems do Strings[Count-1] : = ’ 传送完毕’; end ; procedure TServerThread.ListItemErr; begin if ListItem <> nil then with ListItem.SubItems do Strings[Count-1] : = ’ 传送错误’; end ; procedure TServerThread.ThreadCountDec; begin with FM_DL_SRV do begin Dec(ActiveThreadsC ount); sbSRV.Panels.Items [0].Text := ’ 当前线程数:’ + IntToStr(Active ThreadsCount); end ; end ; end .
小结
本节重点:
(1 )如何建立线程。
(2 )使用线程类的Synchronize 方法来保证资源访问安全。
(3 )如何使用辅助类TWinSocketStream 来实现Socket 通讯。
========================================================================
实现客户端
主界面:
unit UT_DL_CLT; interface uses Windows, Classes, Fo rms, ScktComp, StdCtrls, Controls, ComCtrls , Gauges, ExtCtrls, UT_CLTTHRD; type TFM_DL_CLT = class(TForm) CltSocket: TClient Socket; lbNote: TLabel; btCommand: TButton ; ggCopy: TGauge; procedure btCommandClick(Sender: TObject); procedure FormCreate(Sender: TObject); private { 客户端线程} ClientThread: TCli entThread; public procedure InitUI(Mode: Word); end ; var FM_DL_CLT: TFM_DL_CL T; implementation uses SysUtils, FunAndProc; {$R *.dfm} { TFM_DL_CLT } procedure TFM_DL_CLT.FormCreate(Sender: TObject); begin InitUI(0); end ; { InitUI 用来改变主界面组件状态} procedure TFM_DL_CLT.InitUI(Mode: Word); begin btCommand.Tag := Mod e; case Mode of 0: begin { 初始化CltSocket} with CltSocket do begin Host := Get_Com puterName; Port := Default Port; end ; lbNote.Caption : = ’ 请按"开始"开始下载。’; ggCopy.Progress := 0; btCommand.Captio n := ’开始’; end ; 1: begin lbNote.Caption : = ’ 正在下载,请等待……’; btCommand.Captio n := ’取消’; end ; 2: begin lbNote.Caption : = ’ 下载完毕。’; ggCopy.Progress := ggCopy.MaxValue; btCommand.Captio n := ’确定’; end ; end ; end ; procedure TFM_DL_CLT.btCommandClick(Sender: TObject); begin case TComponent(Sender).Tag of 0: begin InitUI(1); { 创建读写线程} ClientThread := TClientThread.Create( True, CltSock et, ExtractFilePath(ParamStr(0))+’\’); { 线程ClientThread结束时自动销毁} ClientThread.Fre eOnTerminate := True; { 建立连接} CltSocket.Open; { 线程开始运行} ClientThread.Res ume; end ; 1: begin ClientThread.Ter minate; Close; end ; 2: Close; end ; end ; end . 以下是线程TClientThread 的实现代码: unit UT_CLTTHRD; interface uses Classes, ScktComp; type TClientThread = class(TThread) private CltSocket: TClient Socket; { 客户端套接字对象} FilesName: TString s; { 服务端传来的文件名} FilesStrm: Array of TFileStream; {下载时用来保存文件的流数组} FilesLength: Array of Integer; {服务端传来的文件长度} CurrReadSize{ 以字节为单位}: Integer; { 某次读数据操作实际读到的数据} Fileth, AllFilesLe ngth, FileCurrLength: Integer; {当前正在接收第几个文件,总文件长度,对当前文件已经接收的数据量} GaugeStepRate: Dou ble; { 进度条每次增加量的百分率} ParentDir: String; { 下载的文件保存到哪个目录} procedure Init(LengthText: String); { 接收到文件名和文件长度后初始化一系列变量,为接收文件内容作准备} procedure StepProgressToEnd; { 所有文件下载完毕} procedure StepProgress; { 接收一段文件数据后,增加进度条的进度} protected procedure Execute; override ; public { 重在构造函数,使它可以传入客户端套接字对象和保存文件的目录} constructor Create(CreateSuspended: Boolean; ClientSocket: T ClientSocket; AParentDir: String);overload ; destructor Destroy ; override ; end ; implementation uses SysUtils, FunAndProc, UT_DL_CLT; constructor TClientThread.Create(CreateSuspended: Boolean; ClientSocket: T ClientSocket; AParentDir: String); begin ParentDir := AParent Dir; inherited Create(CreateSuspended); CltSocket := ClientS ocket; end ; destructor TClientThread.Destroy; var I: Integer; begin for I := Low(FilesStrm) to High(FilesStrm) do FreeAndNil(FilesSt rm[I]); FreeAndNil(FilesName ); inherited; end ; procedure TClientThread.Execute; var pStream: TWinSocketS tream; ReadBuffer: Pointer; ReadText, TaskName, SendText: String; Start, FileReading: Boolean; { 是否已经向服务端发出第一个请求"AskForFilesName";是否已经准备好开始接收文件数据} const ReadLen = 4*1024; begin Start := False; FileReading := False ; { 建立一个套接字连接流对象} pStream := TWinSocke tStream.Create(CltSocket.Socket, 60000); try while (not Termina ted) and CltSocket.Active do begin if not Start then begin { 发出请求"AskForFilesName"} SendText := KEY _Clt[1]; pStream.Write(S endText[1], Length(SendText)); Start := True; end ; { 分配读取数据缓冲区,缓冲区大小设置为Windows默认缓冲区大小4*1024Byte} ReadBuffer := Al locMem(ReadLen); if pStream.WaitForData(6000) then begin CurrReadSize := pStream.Read(ReadBuffer^, ReadLen); if FileReading then begin { 读取数据流并保存到文件流} Inc(FileCurrL ength, FilesStrm[Fileth].Write( ReadBuffer ^, CurrReadSize)); { 增加进度} Synchronize(S tepProgress); { 如果当前文件的数据已经接收完毕,则开始接收下一个文件} if FileCurrLength >= FilesLength[Fileth] then begin Inc(Fileth) ; FileCurrLen gth := 0; end ; { 如果所有文件接收完毕,则向服务端发送"WanttoDisConnect" 并结束线程} if Fileth = FilesName.Count then begin SendText := KEY_Clt[4]; pStream.Wri te(SendText[1], Length(SendText)); Synchronize (StepProgressToEnd); Terminate; end else { 接收完第n 条数据后向服务端请求第n+1 条数据} begin SendText := KEY_Clt[3]; pStream.Wri te(SendText[1], Length(SendText)); end ; end else begin ReadText := P Char(ReadBuffer); TaskName := C opy(ReadText, 1, Length(KEY_Srv[1])); { 如果服务端发来文件名} if TaskName = KEY_Srv[1] then begin Delete(Read Text, 1, Length(KEY_Srv[1])); FilesName : = TStringList.Create; FilesName.A ssign( StringToStrings(FilesNameSepStr, ReadText)); SendText := KEY_Clt[2]; { 向服务端请求文件长度} pStream.Wri te(SendText[1], Length(SendText)); { 如果服务端发来文件长度} end else if TaskName = KEY_Srv[2] then begin Delete(Read Text, 1, Length(KEY_Srv[1])); Init(ReadTe xt); SendText := KEY_Clt[3]; { 向服务端请求发送第一条数据} pStream.Wri te(SendText[1], Length(SendText)); { 可以开始读文件数据了} FileReading := True; end ; end ; end ; FreeMem(ReadBuff er); end ; finally pStream.Free; CltSocket.Close; end ; end ; procedure TClientThread.Init(LengthText: String); var I: Integer; Lengths: TStrings; begin SetLength(FilesStrm, FilesName.Count); SetLength(FilesLengt h, FilesName.Count); Lengths := StringToS trings(FilesLengthSepStr, LengthText); Fileth := 0; FileCurrLength := 0; AllFilesLength := 0; { 创建对应个数的文件流对象} for I := 0 to FilesName.Count-1 do begin FilesName[I] := Pa rentDir + ’\’ + FilesName[I]; FilesStrm[I] := TF ileStream.Create(FilesName[I], fmCreate); FilesLength[I] := StrToInt(Lengths[I]); Inc(AllFilesLength , FilesLength[I]); end ; GaugeStepRate := FM_ DL_CLT.ggCopy.MaxValue / AllFilesLength; FreeAndNil(Lengths); end ; procedure TClientThread.StepProgress; begin with FM_DL_CLT.ggCopy do Progress := Progre ss + Round(GaugeStepRate*CurrReadSize); end ; procedure TClientThread.StepProgressToEnd; begin FM_DL_CLT.InitUI(2); end ; end .
小结
本节重点:
(1 )如何建立线程。
(2 )使用线程类的Synchronize 方法来保证资源访问安全。
(3 )如何使用辅助类TWinSocketStream 来实现Socket 通讯。