现在的位置: 首页 > 综合 > 正文

Delphi Socket 实现编程(3)

2018年05月27日 ⁄ 综合 ⁄ 共 12710字 ⁄ 字号 评论关闭

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 通讯。

抱歉!评论已关闭.