优秀的编程知识分享平台

网站首页 > 技术文章 正文

「12.网络编程」5.Udp Socket 编程

nanyue 2024-12-16 15:09:56 技术文章 9 ℃

5.Udp Socket 编程

在采用 TCP/IP 网络协议的应用中,网络应用程序之间的主要通信方式时客户/服务器(C/S)模式。即服务器在某端口监听客户端的请求,而客户端向服务器发出服务请求,服务器在收到客户端请求后,提供相应的服务。在前面几节的内容中,我们实现了客户/服务器模式的应用程序开发,本节我们介绍基于数据报的 UDP 通信模式,在 Udp Socket 编程中,主要通过 Indy 组件的 TIdUDPServer 和 TI的UDPClient 组件进行开发。

5.1 TIdUDPServer 组件

该组件位于 Indy Server 页,用于实现基于 UDP 的服务器通信。

5.1.1 TIdUDPServer 组件的主要属性

  • Bindings
  • 服务器分配的 Socket 句柄,通过 TIdUDPListenerThread 来访问 Socket 句柄和协议栈提供的底层方法。
  • DefaultPort

用来标识服务器创建的新的 Socket 绑定的端口,新的连接用该端口号来进行监听。

  • Active

激活并使服务端启动监听

  • Binding

用于发送和接收数据的 Socket 绑定。

  • BroadcastEnabled

是否允许对网络上的所有计算机广播数据报

  • BufferSize

指定能通过 Binding 发送和接收的最大 UDP 包,默认数据包的最大值为 8192

  • ReceiveTimeout

用于标识 ReceiveString 方法等待的最长时间,单位为毫秒

  • LocalName

用户计算机的系统名称

5.1.2 TIdUDPServer 组件的主要方法

  • Broadcast

原型格式:

procedure Broadcast(
    const AData: string; 
    const APort: integer
);

向网络中所有的计算机广播数据,其中 AData 为数据,APort指定计算机的端口号

  • ReceiveBuffer

原型格式:

function ReceiveBuffer(
    var ABuffer: TIdBytes; 
    var VPeerIP: string; 
    var VPeerPort: integer; 
    AMSec: Integer = IdTimeoutDefault
): integer;

从 VPeerIP 和 VPeerPort 指定的计算机中读取数据到 ABuffer 中。AMSec 参数指定超时时长,默认为 IdTimeoutDefault ,

IdTimeoutDefault = -1;
  • Send

原型格式:

procedure Send(
    AHost: string; 
    const APort: Integer; 
    const AData: string
);

将 AData 中的数据发送到 AHost 和 APort 指定的计算机

  • SendBuffer

原型格式:

procedure SendBuffer(
    AHost: string; 
    const APort: Integer; 
    const ABuffer: TIdBytes
); 

将 ABuffer 中的数据发送到 AHost 和 APort 指定的计算机

  • BeginWork

原型格式:

procedure BeginWork(
    AWorkMode: TWorkMode; 
    const ASize: Int64 = 0
); 

用于触发 OnBeginWork 事件,可以被嵌套调用,但只在第一次调用时触发事件。

其中:AWorkMode 参数表示连接的工作模式,取值为:wmRead | wmWrite ; ASize 参数表示读或写的字节数。

  • DoWork

原型格式:

procedure DoWork(
    AWorkMode: TWorkMode; 
    const ACount: Int64
);

用于触发 OnWork 事件,在调用 DoWork 过程之前必须先调用 BeginWork 过程,否则 DoWork 过程将不会产生任何效果。

  • EndWork

原型格式:

procedure EndWork(
    AWorkMode: TWorkMode
);

用于触发 OnEndWork 事件,该方法可以嵌套调用,但只有在第一次调用时会触发事件。

5.1.3 TIdUDPServer 组件的主要事件

  • OnUDPRead

当数据从 Scoket 中读取出来可以被服务器使用时触发

  • OnStatus

当前连接状态发生改变时触发

5.2 TIdUDPClient 组件

该组件位于 Indy Clients 页,用于实现基于 UDP 的客户端通信。

5.2.1 TIdUDPClient 组件的主要属性

  • Host

远程计算机的地址

  • Port

远程计算机的端口

  • ReceiveTimeOut

表示接收数据的最大时长,单位为毫秒数

  • Active

表示 Socket 绑定是否已分配

  • Binding

用于发送和接收数据的 Socket 绑定

  • BroadcastEnabled

指定 Socket 绑定是否可用执行广播传输

  • BufferSize

表示传输的 UDP 数据包的最大字节数,默认为8192

  • LocalName

本地计算机名

5.2.2 TIdUDPClient 组件的主要方法

  • Send

原型格式:

procedure Send(
    AData: string
);

将 AData 中的数据传输给远程计算机。

  • SendBuffer

原型格式:

procedure SendBuffer(
    AHost: string; 
    const APort: Integer; 
    const ABuffer: TIdBytes
);

传输数据给远程计算机

  • Broadcast

原型格式:

procedure Broadcast(
    const AData: string; 
    const APort: integer
);

向网络中的所有计算机广播数据

  • ReceiveBuffer

原型格式:

function ReceiveBuffer(
    var ABuffer: TIdBytes; 
    var VPeerIP: string; 
    var VPeerPort: integer; 
    AMSec: Integer = IdTimeoutDefault
): integer;

从 VPeerIP 和 VPeerPort 参数指定的远程计算机读取数据到 ABuffer 缓冲区

  • ReceiveString

原型格式:

function ReceiveString(
    var VPeerIP: string; 
    var VPeerPort: integer; 
    const AMSec: Integer = IdTimeoutDefault
): string;

从 VPeerIP 和 VPeerPort 参数指定的远程计算机读取字符串数据

  • BeginWork

原型格式:

procedure BeginWork(
    AWorkMode: TWorkMode; 
    const ASize: Int64 = 0
);

用于触发 OnBeginWork 事件,同时维护读写堵塞操作的数量,以及初始读写操作的大小。

  • DoWork

原型格式:

procedure DoWork(
    AWorkMode: TWorkMode; 
    const ACount: Int64
); 

用于触发 OnWork 事件,在调用该方法之前必须调用 BeginWork 过程,否则该过程将不会产生任何效果。

  • EndWork

原型格式:

procedure EndWork(
    AWorkMode: TWorkMode
);

用于触发 OnEndWork 事件,该方法可以嵌套调用,但是 OnEndWork 事件仅在第一次调用时触发。

5.2.3 TIdUDPClient 组件的主要事件

  • OnStatus

当前连接状态改变时触发

5.3 Indy UDP Socket 编程示例

本节采用 Indy 10 提供的组件 TIdUDPServer 和 TIdUDPClient 来演示 TCP Socket 编程。示例仍然采用前面的,只是使用 UDP Socket 来实现。

示例:客户端定时实时检测所在机器的屏幕分辨率上行到服务端,服务端接收到数据后,根据其屏幕分辨率随机生成一个坐标并下发给客户端,客户端将应用程序的窗体位置放置到相应的坐标上。

5.3.1 服务器端

界面设计如下图:

界面比较简单,组件属性也基本上不需要设置,主要设置各个组件的 Name 属性,在此不做说明。服务器端代码相对比较简单,只需要实现 TIdUDPServer 组件的 OnUDPRead 和 OnUDPException 事件即可。

首先确定传输的数据结构:

TCommBlock = Record
    // 客户端上传: W-屏幕宽度, H-屏幕高度, E-结束;
    // 服务端下发: X-水平坐标, Y-垂直坐标, E-结束;
    Part: String[1];
    Desc: String[16];   // 描述
    Value: Integer; // 数据值
  end;    

“启动”按钮的单击事件:

procedure TForm1.StartButtonClick(Sender: TObject);
begin
  IdUDPServe.DefaultPort:=PortSpinEdit.Value;
  IdUDPServe.Active:=True;

  PortSpinEdit.Enabled:=False;
  StartButton.Enabled:=False;
end;  

实现 TIdUDPServer 组件的 OnUDPException 事件:

procedure TForm1.IdUDPServeUDPException(AThread: TIdUDPListenerThread;
  ABinding: TIdSocketHandle; const AMessage: String;
  const AExceptionClass: TClass);
begin
  LogMemo.Lines.Add(AMessage);
end;   

实现 TIdUDPServer 组件的 OnUDPRead 事件:

procedure TForm1.IdUDPServeUDPRead(AThread: TIdUDPListenerThread;
  const AData: TIdBytes; ABinding: TIdSocketHandle);
var
  Ip: String;
  Port: Integer;
  CommBlock: TCommBlock;
  W, H, X, Y: Integer;
begin
  Ip:=AThread.Binding.PeerIP;
  Port:=AThread.Binding.PeerPort;

  BytesToRaw(AData, CommBlock, SizeOf(CommBlock));

  LogMemo.Lines.Add(Ip + ':' + inttostr(Port) + ' - ' + CommBlock.Desc + ': ' + inttostr(CommBlock.Value));
  if CommBlock.Part = 'W' then W:=CommBlock.Value;
  if CommBlock.Part = 'H' then H:=CommBlock.Value;

  if CommBlock.Part = 'E' then
  begin
    Randomize;
    X:=Random(W);
    Y:=Random(H);

    // 发送水平坐标
    CommBlock.Part:='X';
    CommBlock.Desc:='水平坐标';
    CommBlock.Value:=X;
    LogMemo.Lines.Add(Ip + ':' + inttostr(Port) + ' - ' + CommBlock.Desc + ': ' + inttostr(CommBlock.Value));

     AThread.Server.Sendbuffer(Ip, Port, RawToBytes(CommBlock, SizeOf(CommBlock)));
    // 发送垂直坐标
    CommBlock.Part:='Y';
    CommBlock.Desc:='垂直坐标';
    CommBlock.Value:=Y;
    LogMemo.Lines.Add(Ip + ':' + inttostr(Port) + ' - ' + CommBlock.Desc + ': ' + inttostr(CommBlock.Value));

    AThread.Server.Sendbuffer(Ip, Port, RawToBytes(CommBlock, SizeOf(CommBlock)));
    // 发送结束标志
    CommBlock.Part:='E';
    CommBlock.Desc:='结束';
    CommBlock.Value:=0;
    LogMemo.Lines.Add(Ip + ':' + inttostr(Port) + ' - ' + CommBlock.Desc + ': ' + inttostr(CommBlock.Value));

    AThread.Server.Sendbuffer(Ip, Port, RawToBytes(CommBlock, SizeOf(CommBlock)));
  end;
end; 

在上面的代码中,使用 AThread.Server.Sendbuffer 方法来将返回的数据发送给客户端。

5.3.2 客户端

客户端界面设计如下图所示:

客户端主要实现 TTimer 组件的 OnTimer 事件,用于发送数据,接收数据仍然采用启动一个单独的接收数据线程来实现。

声明与服务端通信的数据结构:

TCommBlock = Record
    // 客户端上传: W-屏幕宽度, H-屏幕高度, E-结束;
    // 服务端下发: X-水平坐标, Y-垂直坐标, E-结束;
    Part: String[1];
    Desc: String[16];   // 描述
    Value: Integer; // 数据值
  end;   

声明数据读取线程:

TClientHandleThread = class(TTHread)
  private
    Logs: String;
    procedure HandleLog;
    procedure HandlePos;
  protected
    procedure Execute; Override;
  end;             

声明应用程序使用的变量:

var
  Form1: TForm1;
  X, Y: Integer;
  Ip: String;
  Port: Integer;
  ClientHandleThread: TClientHandleThread; 

“开始”按钮的单击事件:

procedure TForm1.StartButtonClick(Sender: TObject);
begin
  if HostEdit.Text = '' then
  begin
    Application.MessageBox('请设置地址!', '提示');
    Exit;
  end;

  HostEdit.Enabled:=False;
  PortSpinEdit.Enabled:=False;
  StartButton.Enabled:=False;

  Ip:=HostEdit.Text;
  Port:=PortSpinEdit.Value;

  IdUDPClien.Host:=Ip;
  IdUDPClien.Port:=Port;

  SendTimer.Interval:=1000*10;
  SendTimer.Enabled:=True;

  // 启动读取线程
  ClientHandleThread:=TClientHandleThread.Create(True);
  ClientHandleThread.FreeOnTerminate:=True;
  ClientHandleThread.Start;
end;

数据读取线程的具体实现我们在后面介绍。

通过 TTimer 的 OnTimer 事件实现数据发送:

procedure TForm1.SendTimerTimer(Sender: TObject);
var
  CommBlock: TCommBlock;
  w, h: Integer;
begin
  // 定时器
  w:=Screen.Width;
  h:=Screen.Height;

  LogMemo.Lines.Add('分辨率: ' + inttostr(w) + ' * ' + inttostr(h));

  // 发送宽度
  CommBlock.Part:='W';
  CommBlock.Desc:='宽度';
  CommBlock.Value:=w;
  IdUDPClien.SendBuffer(RawToBytes(CommBlock, SizeOf(CommBlock)));


  // 发送高度
  CommBlock.Part:='H';
  CommBlock.Desc:='高度';
  CommBlock.Value:=h;
  IdUDPClien.SendBuffer(RawToBytes(CommBlock, SizeOf(CommBlock)));

  // 发送结束标志
  CommBlock.Part:='E';
  CommBlock.Desc:='结束';
  CommBlock.Value:=0;
  IdUDPClien.SendBuffer(RawToBytes(CommBlock, SizeOf(CommBlock)));
end;          

数据读取线程的实现:

procedure TClientHandleThread.HandleLog;
begin
  Form1.LogMemo.Lines.Add(Logs);
  Logs:='';
end;

procedure TClientHandleThread.HandlePos;
begin
  Form1.Left:=X;
  Form1.Top:=Y;
end;

procedure TClientHandleThread.Execute;
var
  CommBlock: TCommBlock;
  bytes: TIdBytes;
begin
  while not Self.Terminated do
  begin
    SetLength(bytes, Form1.IdUDPClien.BufferSize);
    Form1.IdUDPClien.ReceiveBuffer(bytes);

    BytesToRaw(bytes, CommBlock, SizeOf(CommBlock));
    Logs := Logs + CommBlock.Desc + ': ' + inttostr(CommBlock.Value);
    Synchronize(@HandleLog);

    if CommBlock.Part = 'X' then X:=CommBlock.Value;
    if CommBlock.Part = 'Y' then Y:=CommBlock.Value;
    if CommBlock.Part = 'E' then Synchronize(@HandlePos);
  end;
end;

在上面的代码中,通过 TIdUDPClient 的 ReceiveBuffer 方法读取服务端返回的数据,在使用该方法读取之前,必须对 TIdBytes 进行初始化,这个是与 TIdTCPClient 组件不同的地方,我们来看一下 TIdBytes 类型:

TIdBytes = TBytes;

再来看一下 TBytes 类型:

TBytes = array of Byte;

也就是说,TIdBytes 是一个动态字节数组,所以,在读取数据之前,要先初始化该变量,代码如下:

SetLength(bytes, Form1.IdUDPClien.BufferSize);

以上内容就是使用 Indy 组件进行 UDP Socket 编程的实现过程,在代码中,我没有进行异常处理,在实践开发中,必须对数据发送和接收进行异常处理。

最近发表
标签列表