掌握网络编程的基本原理
掌握网络控制的使用方法
具备通过使用网络控制开发简单网络应用程序的能力
Delphi 的Socket 编程概述
Socket 是建立在传输层协议(主要是TCP 和UDP)上的一种套接字规范,它定义两台计算机间进行通信的规范(即一种编程规范),如果说两台计算机是利用一个“通道”进行通信,那么这个“通道”的两端就是两个套接字。套接字屏蔽了底层通信软件和具体操作系统的差异,使得任何两台安装了TCP 协议软件和实现了套接字规范的计算机之间的通信成为可能。
在Delphi 中,其底层的Socket 也应该是Windows的Socket。Socket减轻了编写计算机间通信软件的难度。Inprise在Delphi中对Windows Socket进行了有效的封装,使用户可以很方便地编写网络通信程序。
用户端程序 |
客户端程序 |
Windows Sockets |
网络通信协议服务界面(如:TCP/IP) |
Windows |
物理层通信介质 |
TCP/IP 协议及特点
1. TCP/IP体系结构
TCP/IP 协议实际上就是在物理网上的一组完整的网络协议。其中TCP 是提供传输层服务,而IP 则是提供网络层服务。TCP/IP协议簇(如图1所示)包括协议如下。
(1) IP:网间协议(Internet Protocol)。此协议负责主机间数据的路由和网络上数据的存储。同时为ICMP,TCP,UDP提供分组发送服务,用户进程通常不需要涉及这一层。
(2) ARP:地址解析协议(Address Resolution Protocol)。此协议将网络地址映射到硬件地址。
(3) RARP:反向地址解析协议(Reverse Address Resolution Protocol)。此协议将硬件地址映射到网络地址
(4) ICMP:网间报文控制协议(Internet Control Message Protocol)。此协议处理信关和主机的差错和传送控制。
(5) TCP:传送控制协议(Transmission Control Protocol)。这是一种提供给用户进程的可靠的全双工字节流面向连接的协议。它要为用户进程提供虚电路服务,并为数据可靠传输建立检查(注:大多数网络用户程序使用TCP)。
(6) UDP:用户数据报协议(User Datagram Protocol)。这是提供给用户进程的无连接协议,用于传送数据而不执行正确性检查。
(7) FTP:文件传输协议(File Transfer Protocol)。此协议允许用户以文件操作的方式(文件的增、删、改、查、传送等)与另一主机相互通信。
(8) SMTP:简单邮件传送协议(Simple Mail Transfer Protocol)。SMTP协议的作用是为系统之间传送电子邮件。
(9) Telnet:终端协议(Telnet Terminal Procotol)。允许用户以虚终端方式访问远程主机
(10) HTTP:超文本传输协议(Hypertext Transfer Procotol)
(11) TFTP:简单文件传输协议(Trivial File Transfer Protocol)
FTP |
SMTP |
Telnet |
HTTP |
TFTP |
FTP |
FTP |
ICMP |
IP |
ARP |
RARP |
硬件接口 |
如图1
2. TCP/IP 特点
TCP/IP 协议的核心部分是传输层协议(TCP、UDP),网络层协议(IP)和物理接口层,这三层通常是在操作系统内核中实现。因此用户一般不涉及。编程时的编程界面有两种形式:其一是由内核心直接提供的系统调用;其二是使用以库函数方式提供的各种函数。前者为核内实现,后者为核外实现。用户服务要通过核外的应用程序才能实现,所以网络编程要使用套接字(Socket)来实现,如图2所示是TCP/IP协议核心与应用程序关系图。
应用程序1 |
应用程序2 |
应用程序3 |
网络应用程序界面(Socket) |
TCP/IP核心协议 |
物理介质 |
如图2
Socket 套接字
1. Socket
Socket 是网络的基本构件,它是可以被命名和寻址的通信端点,使用中的每一个套接字都有其类型和一个与之相连接的进程。
2. 基本套接字方法
在 Socket 编程规范中定义了许多套接字方法,为了更好地理解套接字编程原理,下面介绍几个基本的套接字方法。
1) 创建套接字——socket( )
功能:使用前创建一个新的套接字
格式:SOCKETPASCALFARsocket(intaf,inttype,intprocotol);
参数:af:通信发生的区域
type:要建立的套接字类型
procotol:使用的特定协议
2) 指定本地地址——bind( )
功能:将套接字地址与所创建的套接字号联系起来。
格式:intPASCALFARbind(SOCKETs,conststructsockaddrFAR*name,intnamelen);
参数:s:是由socket( )调用返回的并且未作连接的套接字描述符(套接字号)。
其他:如果没有错误,bind( )返回0,否则返回SOCKET_ERROR
3) 建立套接字连接——connect( )和accept( )
功能:共同完成连接工作
格式:intPASCALFARconnect(SOCKETs,conststructsockaddrFAR*name,intnamelen);
SOCKETPASCALFARaccept(SOCKETs,structsockaddrFAR*name,intFAR*addrlen);
参数:同上
4) 监听连接——listen( )
功能:用于面向连接服务器,表明它愿意接收连接。
格式:intPASCALFARlisten(SOCKETs,intbacklog);
5) 数据传输——send( )与recv( )
功能:数据的发送与接收
格式:intPASCALFARsend(SOCKETs,constcharFAR*buf,intlen,intflags);
intPASCALFARrecv(SOCKETs,constcharFAR*buf,intlen,intflags);
参数:buf:指向存有传输数据的缓冲区的指针。
6) 多路复用——select( )
功能:用来检测一个或多个套接字状态。
格式:intPASCALFARselect(intnfds,fd_setFAR*readfds,fd_setFAR*writefds,
fd_setFAR*exceptfds,conststructtimevalFAR*timeout);
参数:readfds:指向要做读检测的指针
writefds:指向要做写检测的指针
exceptfds:指向要检测是否出错的指针
timeout:最大等待时间
7) 关闭套接字——closesocket( )
功能:关闭套接字
格式:BOOLPASCALFARclosesocket(SOCKETs);
Socket 工作过程
在 TCP/IP 网络中两个进程间的相互作用的主机模式是客户机/服务器模式(Client/Servermodel),客户机/服务器模式在操作过程中采取的是主动请示方式:
服务器方的工作过程如下。
(1) 打开一通信通道并告知本地主机,它愿意在某一个公认地址上接收客户请求。
(2) 等待客户请求到达该端口。
(3) 接收到客户端服务请求,处理该请求并发送应答信号,等待客户端连接。
(4) 返回第二步,等待另一客户请求。
(5) 接受客户端连接,建立新的套接字,并在该套接字上传输数据。
(6) 关闭套接字。
(7) 关闭服务器。
客户方工作过程如下。
(1) 打开一通信通道,并连接到服务器所在主机的特定端口。
(2) 向服务器发送服务请求报文,等待并接收应答;继续提出请求。
(3) 请求结束后关闭通信通道并终止。
上述客户机/服务器模式工作过程用套接字方法描述如下。
服务器(Server)方首先用Socket( )方法新建一个Socket 连接,然后用Bind( )方法使该连接同服务器地址绑定,Listen( )方法使该连接处于监听状态,一旦监听到客户请求便调用Accept( )方法处理连接请求并等待客户建立连接,连接建立成功后通过send( )与recv( )在该连接上交换数据,最后通过closesocket( )方法关闭套接字。
客户机(Client)方首先用Socket( )方法新建一个Socket 连接,然后通过Connect( )方法连接到特定的服务器,并与该服务器建立连接,通过send( )与recv( )在该连接上交换数据,最后通过closesocket( )方法关闭套接字。
(1) 面向连接的套接字的系统调用时序。
服务器端 |
Socket()方法建立套接字 |
Bind()方法绑定本机IP |
Listen( )监听客户端 |
Accept( )方法接受连接 |
Accept( )方法建立连接 |
Recv( )/Send( )交换数据 |
Closesocket( )关闭该连接 |
Closesocket( )关闭服 |
客户湍 |
Closesocket( )关闭该连接 |
Recv( )/Send( )交换数据 |
Connect( )方法建立连接 |
Socket( )方法请求连接 |
(2) 无连接协议的套接字调用时序。
服务器端 |
Secket( )方法建立套接字 |
Bind( )方法绑定本机IP |
Recv( )/Send( )交换数据 |
Closesocket( )关闭该连接 |
客户端 |
Socket( )方法请求连接 |
Bind( )方法绑定服务端IP |
Recv( )/Send( )交换数据 |
Closesocket( )关闭该连接 |
使用Delphi 网络控件
TClientSocket 控件和TServerSocket 控件
TServerSocket 和TClientSocket 涵盖了基本的WinSocket 编程,其中TServerSocket 作为服务器方使用,TClientSocket作为客户端使用,这两个控件本身并不提供Socket 连接,但是它们都有一个Socket 属性,这个属性才提供了Socket 连接。
由于这两个控件在 Delphi 7 中只以类的形式出现,所以在应用之前需要让用户自己创建该类的实例并初始化,方法如下。
ClientSocket:TClientSocket; //声明客户端套接字控件
ServerSocket:TServerSocket; //声明服务器端套接字控件
ClientSocket:=TClientSocket.Create(Self);
//创建客户端套接字控件实例
//客户端套接字控件实例属性设置
ClientSocket.Active:=False;
ClientSocket.Address:='127.0.0.1';
ClientSocket.ClientType:=ctNonBlocking;
//非堵塞式工作方式
ClientSocket.Port:=1024;
…
//客户端套接字控件实例指定事件
ClientSocket.OnDisconnect:=ClientSocketDisconnect;
ClientSocket.OnRead:=ClientSocketRead;
ClientSocket.OnError:=ClientSocketError;
…
ServerSocket:=TServerSocket.Create(Self);
//创建服务器端套接字控件实例
//创建服务器端套接字控件实例属性设置
ServerSocket.Active:=False;
ServerSocket.Port:=1024;
ServerSocket.ServerType:=stNonBlocking;
//非堵塞式工作方式
…
//创建服务器端套接字控件实例指定事件处理
ServerSocket.OnClientConnect:=ServerSocketClientConnect;
ServerSocket.OnClientDisconnect:=ServerSocketClientDisconnect;
ServerSocket.OnClientRead:=ServerSocketClientRead;
ServerSocket.OnClientError:=ServerSocketClientError;
…
下面就介绍一下这两个控件常用的方法、属性及其含义。
1. TServerSocket和TClientSocket 组件的常用方法与属性
1) TServerSocket组件的常用属性及说明见表1。
表1:
名称 | 类型 | 说明 |
Socket | TServerWinSocket | 最重要的属性,提供Socket连接,事实上发送/接收数据都要靠这个属性 |
Port | int | 要监听的端口,如果在Serverice属性中指定了服务类型,此属性将被忽略 |
Service | AnsiString | 提供的服务,如HTTP、FTP等,如果在这里指定了服务类型,Port将被忽略,因为各种服务都有特定的端口,如FTP:21、HTTP:80 |
ServerType | TServerType | 设置与客户连接的方式,取值为两个枚举常量stNonBlocking和stThreadBlocking,stNonBloking表示用非阻塞方式连接每一个客户,每个连接都在一个单独的线程中处理。并用OnClienRead()和OnClientWrite()通知服务器端的Socket进行读写。stThreadBlocking表示以阻塞方式连接客户,即以主动查询的方式和客户连接。 |
Active | bool | 激活服务,相当于调用Open()方法 |
2) TServerSocket控件的常用事件
(1) OnAccept事件:当有客户请求连接时触发。
(2) OnClientRead 事件:通知服务器去读取有关信息。
(3) OnClientWrite 事件:通知服务器去写有关信息。
3) TClientSocket 组件的常用属性
TClientSocket控件的常用属性及说明见表2。
表2:
名称 | 类型 | 说明 |
Socket | TClientWinSocket | 同TServerSocket |
Active | bool | 同TServerSocket |
Address | AnsiString | 服务器的IP地址,如202.98.35.14 |
ClientTyp | TCLientType | 与服务器连接的方式,取值为两个枚举常量ctNonBlocking,tBlocking。ctNonBlocking表示非阻塞方式,ctBlocking表示阻塞方式。 |
Host | AnsiString | 要连接的主机名,如www.pup6.com |
Port | Int | 同TServerSocket |
Serverice | AnsiString | 同TServerSocket |
4) TClientSocket 控件的常用事件
(1) OnConnect 事件:当连接成功时发生。
(2) OnConnecting事件:当连接时发生。
(3) OnDisConnect事件:当连接关闭时发生。
(4) OnRead 事件:通知客户机去读取有关信息。
(5) OnWrite 事件:通知客户机去写有关信息。
2. TServerWinSocket类和TClientWinSocket 类的常用属性和方法
上面介绍的TServerSocket 控件和TClientSocket 控件只提供基本的服务器/客户机的连接,真正提供数据传输的是它们都有的Socket 属性,即服务器端接受了客户机方面的连接请求后,在客户机和服务器之间就拥有了一个Socket,通过此Socket 双方实现通信。所以Socket 属性很重要,它又拥有很多的方法,用其中的几个简单的方法,就可以实现数据的发送和接收。Socket 的类型分别是TServerWinSocket 和ClientWinSocket,而TServerWinSocket 和TClientWinSocket 的父类都是TCustomWinSocket,下面就来看看TServerWinSocket 和TClientWinSocket 常用的属性和方法。
(1) TServerWinSocket类和TClientWinSocket类共同的属性和方法,即TcustomWinSocket类的属性和方法见表3 和表4。
表3:
名称 | 类型 | 说明 |
Connected | Bool | 检查是否连接成功 |
LocalAddress | AnsiString | 本地IP地址,与此类似。LocalHost:本机域名,LocalPort:本机端口 |
RemoteAddres | AnsiString | 另一端的IP地址,与此类似。RemoteHost:另一端域名,RemotePort:另一端端口 |
SocketHandle | int | 只读,返回Socket对象的Windows句柄,调用WinSocketAPI函数会用到它 |
Handle | HWND | Socket能够接受到的异步事件都是以Windows消息的形式发送给此句柄的 |
表4:
名称 | 说明 |
Close() | 作为服务器,关闭所有连接:作为客户机,关闭自己与服务器的连接 |
SendText() | 发送一个字符串 |
SendBuf() | 发送缓冲区buff中的count个字节,返回实际发送的字节数 |
SendStream() | 改善一个流到Socket中 |
ReceiveText() | 从Socket中读取并返回一个字符串 |
ReceiveLength() | 从Socket读取数据需多少字节的缓冲区 |
ReceiveBuf() | 从Socket中读取count字节的数据到buff |
(2) TClientWinSocket 控件。TClientWinSocket 类只比TCustomWinSocket 类增加了一个ClientType 属性,用于决定与服务器的连接类型(参见表1)。
(3) TServerWinSocket控件。TServerWinSocket 类比TCustomWinSocket 类增加的属性见表5。
表5:
名称 | 类型 | 说明 |
ServerType | 服务类型 | 参见表2 |
ActiveConne | Int只读 | 返回当前活动的连接数 |
Connection | TCustomWinSocket | 数组,索引n表示第n+1个连接,如Connection[0]表示第一个连接 |
3. 编写WinSock 程序的步骤
因为有了Delphi 的封装,使WinSocket 编程不需要直接和WinSock 的API 打交道,即使不了解WinSock 的API 函数,通过上面介绍的这些知识,我们就可以利用Delphi 完成一些基本的WinSocket 编程,下面将介绍TClientSocket 控件和TCerverSocket 控件编程的一般步骤。
在 Delphi 中,编写WinSock 程序分为以下4 个步骤。
1) TClientSocket 控件和TCerverSocket 控件的属性设置
在 Delphi 中,将WinSocket 封装为两个类:ClientSocket 和ServerSocket,它们分别作为客户端和服务器端的控件(注:在Delphi 7 中,ClientSocket 类和ServerSocket 类在使用前需要创建其实例并初始化,下文将其称为控件)。通过这两种控件之间的通信,再加上辅助的应用程序代码,就可以实现一个简单的通信程序。当然,如果想在客户端程序中再引入ServerSocket 的话,那么客户端程序就可以充当服务器了,可以对其他的客户端程序的请求进行响应。
如果正在编写服务器端程序,就必须设置ServerSocket 控件的Port 属性。设置此参数主要是因为在同一台计算机上可能运行着多个服务器程序,而它们可能总在不停地接受来自于远程客户端程序的连接请求。也可以设置Service 属性,它指示了ServerSocket 所提供的服务类型,比如:FTP、HTTP 等,然后设置Active 属性为True。如果正在编写客户端程序,则设置ClientServer 控件的属性就多一些。其中Port 属性应设置成和服务器端的Port 属性值一致;另外Host 的属性必须正确设置,它是一个只读属性,在设计时不可用;Host 指示了客户程序所要连接的远程服务器的主机名。也可以设置Address 属性,也就是远程主机的IP地址。
2) 建立与远程计算机的连接
要在远程计算机系统之间进行数据传输,首先必须在通信的两台主机之间建立连接。
服务器端的 ServerSocket 控件调用Open 方法初始化Socket 连接,同时也就设置了Active 属性为True,将ServerSocket 控件设置成监听模式,随时侦测是否有连接请求。
如果服务器接受了客户程序的连接请求,则触发OnAccept 事件,如下代码就是处理接受连接后服务器程序所要做的工作。
procedure Myform..ServerSocketAccept(Sender:TObject,Socket:TCustomWinSocket);
begin
IsServer := True;
end;
在客户端程序中,ClientSocket 控件设置Port、Host 等必须的属性,然后设置Active属性为True,提出连接请求。
3) 计算机之间数据传输
一旦服务器端接受了客户机方面的连接请求,客户机就可以发送数据。这时在客户机和服务器之间就拥有了一个Socket,通过此Socket 实现双方通信。所以Socket 属性很重要,它又拥有很多的方法(见表10-4),用其中的几个简单的方法,就可以实现数据的发送和接收。
客户机端用如下形式发送数据:ClientSocket1.socket.sendtext('string you want to send');
服务器端采用如下形式接收数据:ServerSocket1.socket.recievetext( str: string);
此函数返回接收到的字符串的长度,将字符串存储在变量str 中。上述是数据传输的最简单的例子,还可以采用Socket 属性所提供的其他方法来实现复杂的数据传输。
4) 关闭与远程计算机的连接
数据传输完毕,最后要关闭Socket 连接,方法如下。
关闭某一个Socket 连接:ClientSocket1.close 或ClientSocket1.active:=false;
关闭所有的Socket 连接:ServerSocket1.close 或ServerSocket1.active:=false;
Indy 系列控件介绍
Indy的全名是Internet Direct(也叫Winshoes),它是一套开放源代码的Internet控件集,它支持大部分流行的Internet 协议,包括TCP、UDP、DNS、ICMP、FINGER、FTP、GOPHER、HTTP、POP3、SMTP、Telnet、WHOIS 等,支持BASE64、MD2、MD4、MD5等编解码,提供Internet 流行协议的客户端和服务器控件。Indy 控件集的客户端和服务器控件都有完整、详细的源代码例程和帮助文件。
在Windows 上使用阻塞式Socket 开发应用程序具有如下优点。
_ 编程简单——阻塞式Socket 应用程序很容易编写。所有的用户代码都写在同一个地方,并且顺序执行。
_ 容易向Unix 移植——由于Unix 也使用阻塞式Socket,编写可移植的代码就变得比较容易。Indy 就是利用这一点来实现其多平台支持而又单一源代码的设计。
_ 很好地利用了线程技术——阻塞式Socket 是顺序执行的,其固有的封装特性使得它能够很容易地使用到线程中。
Indy服务器的工作原理为:Indy 服务器控件创建一个同应用程序主线程分离的监听线程来监听客户连接请求,对于接受的每一个客户,都创建一个新的线程来为该客户提供服务,所有与这一客户相关的事务都由该线程来处理。
简单的Indy 应用实例
下面将创建一个简单的TCP 客户程序和一个简单的TCP 服务器来演示Indy 的基本使用方法。客户程序使用TCP 协议与服务器连接,并向服务器发送用户所输入的数据。服务器支持两条命令:DATA 和QUIT,所谓的DATA 命令和QUIT 命令是人为定义的,当然也可以用其他的词来代替。在DATA 命令后跟随要发送的数据,并用空格将命令字DATA和数据分隔开。
【例】建立两个应用程序,分别是客户端程序和服务器端程序。客户端和服务器端的界面如图所示:客户程序表单上放置4个TButton按钮、4个Label标签、3 个Edit 文本框、3 个Memo1 控件和1个TIdTCPClient 控件,为防止客户程序“冻结”,还在其表单上放置TIdAntiFreeze 控件。服务器程序表单上放置3个TButton 按钮、3 个Label 标签、2 个Edit 文本框和1 个TIdTCPServer 控件。
图:简单的TCP客户程序表单 图:简单的TCP服务器程序表单
下面的代码节选自客户端程序。
procedure TForm1.btnconnectClick(Sender: TObject);
begin
IdTCPClient1.Host:=edit1.Text;
IdTCPClient1.Port:=StrToInt(edit2.Text);
memo1.lines.Add('正在连接'+edit1.Text+'...');
with IdTCPClient1 do
begin
try
Connect(5000);
try
memo1.lines.Add(ReadLn());
BtnConnect.Enabled:=False;
BtnSend.Enabled:=True;
BtnDisconnect.Enabled:=True;
except
memo1.lines.Add('远程主机无响应!');
IdTCPClient1.Disconnect();
end; //endtry
except
memo1.lines.Add('无法建立到'+edit1.Text+'的连接!');
end; //endtry
end; //endwith
end;
procedure TForm1.btnsendClick(Sender: TObject);
begin
memo1.lines.Add('DATA'+Edit3.Text);
with IdTCPClient1 do
begin
try
WriteLn('DATA'+Edit3.Text);
memo1.lines.Add(ReadLn())
except
memo1.lines.Add('发送数据失败!');
IdTCPClient1.Disconnect();
memo1.lines.Add('同主机'+edit1.Text+'的连接已断开!');
BtnConnect.Enabled:=True;
BtnSend.Enabled:=False;
BtnDisconnect.Enabled:=False;
end; //endtry
end; //endwith
end;
procedure TForm1.btndisconnectClick(Sender: TObject);
var
Received:string;
begin
memo1.lines.Add('QUIT');
try
IdTCPClient1.WriteLn('QUIT');
finally
IdTCPClient1.Disconnect();
memo1.lines.Add('同主机'+edit1.Text+'的连接已断开!');
BtnConnect.Enabled:=True;
BtnSend.Enabled:=False;
BtnDisconnect.Enabled:=False;
end; //endtry
end;
在【连接】按钮事件响应过程中,首先根据用户的输入设置IdTCPClient 的主机和端口,并调用IdTCPClient 的Connect 方法向服务器发出连接请求。然后调用ReadIn 方法读取服务器应答数据。
在【发送】按钮事件响应过程中,调用WriteIn 方法写DATA 命令,向服务器发送数据。
在【断开】按钮事件响应过程中,向服务器发送QUIT 命令,并调用Disconnect 方法断开连接。
程序中还包含有通信信息记录和异常处理的代码。
下面的代码节选自服务器程序。
procedure TForm1.btnstartClick(Sender: TObject);
begin
IdTCPServer.DefaultPort:=StrToInt(Edit1.Text);
IdTCPServer.Active:=True;
BtnStart.Enabled:=False;
BtnStop.Enabled:=True;
memo1.lines.Add('服务器已成功启动!');
end;
procedure TForm1.btnstopClick(Sender: TObject);
begin
IdTCPServer.Active:=False;
BtnStart.Enabled:=True;
BtnStop.Enabled:=False;
memo1.lines.Add('服务器已成功停止!');
end;
procedure TForm1.IdTCPServerConnect(AThread: TIdPeerThread);
begin
memo1.lines.Add('来自主机' + AThread.Connection.Socket.Binding.PeerIP
+ '的连接_请求已被接纳!');
AThread.Connection.WriteLn('100:欢迎连接到简单TCP服务器!');
end;
procedure TForm1.IdTCPServerExecute(AThread: TIdPeerThread);
var
tempstr, AcceptStr,AcceptData:string;
begin
with AThread.Connection do
begin
AcceptStr:=ReadLn();
TempStr:=AcceptStr+'来自于主机' +
AThread.Connection.Socket.Binding.PeerIP;
Memo1.Lines.Add(Tempstr);
if AnsiStartsText('DATA',AcceptStr) then
begin
AcceptData:=RightStr(AcceptStr,Length(AcceptStr)-5);
WriteLn('200:数据接收成功!');
Edit2.Text :=AcceptData;
end
else if SameText(AcceptStr,'QUIT') then
begin
TempStr:='断开同主机' +AThread.Connection.Socket.Binding.PeerIP +'
的连接!';
Memo1.Lines.Add(Tempstr);
Disconnect;
end
else begin
WriteLn('500:无法识别的命令!');
TempStr:='无法识别命令:'+AcceptStr;
Memo1.Lines.Add(Tempstr);
end;
end; //结束With语句
end;
单击【启动】按钮设置IdTCPServer 的Active 属性为True 来启动服务器,单击【停止】按钮设置Active 属性为False 来关闭服务器。
IdTCPServerConnect 方法作为IdTCPServer 的OnCorrect 事件响应过程,向客户端发送欢迎信息。OnCorrect 事件在一个客户连接请求被接受时发生,为该连接创建的线程AThread 被作为参数传递给dTCPServerConnect 方法。
IdTCPServerExecute方法是IdTCPServer 的OnExecute 事件响应过程。OnExecute 事件在TIdPeerThread对象试图执行其Run方法时发生。OnExecute事件与通常的事件有所不同,其响应过程是在某个线程上下文中执行的,参数AThread 就是调用它的线程。这一点很重要,它意味着可能有多个OnExecute 事件响应过程被同时执行。在连接被断开或中断前,
OnExecute 事件响应过程会被反复执行。在 IdTCPServerExecute 方法中,首先读入一条指令,然后对指令进行判别。如果是DATA 指令,就解出数据并显示它;如果收到的是QUIT 指令,则断开连接。程序运行的结果。
图:客户端运行结果 图:服务器端运行结果
网络编程应用实例
前面介绍了Delphi 网络编程的基本思想和方法,本节通过一个文件传输程序实例来讲解网络编程的具体方法。
【例】本例由两部分组成,分别是客户端和服务器端,客户端可以将本机的文件传输到服务器端所在的主机上。本例使用TSlientSocket 类和TServerSocket 类来完成数据传输。
1. 客户端设计
客户端的功能是与服务器建立/断开连接,选择本机上的文件并发送文件到服务器。客户端表单设计。
图:客户端表单设计
客户端窗体上放置了5个Label 标签、4个TButton 按钮、3 个TEdit 文本框、1 个Open对话框和1个TMemo控件。在Delphi 7中不存在TSlientSocket控件,而是以TSlientSocket类的形式存在,因此本例中应该创建该类的实例并初始化,方法如下。
uses
……,ScktComp ; //在uses中添加Tclient
//Socket类所在的头文件
//ScktComp
type //声明TclientSocket类
//变量及其需要的过程
TForm1 = class(TForm)
procedure ClientSocketRead(Sender: TObject; Socket:
TCustomWinSocket);
procedure ClientSocketError(Sender: TObject; Socket:
TCustomWinSocket;ErrorEvent:_ TErrorEvent; var ErrorCode: Integer);
public
ClientSocket: TClientSocket; //声明TclientSocket类
//变量
end;
//在过程中创建Tclient
//Socket实例并设定属性
procedure TForm1.FormCreate(Sender: TObject);
begin
ClientSocket:=TClientSocket.Create(Self); //创建TclientSocket实例
ClientSocket.Active:=False; //设定属性
ClientSocket.Address:= EServerIP.Text ; //指定服务器地址
ClientSocket.ClientType:=ctNonBlocking; //非堵塞式工作方式
ClientSocket.Port:=strtoint(EServerPort.text); //指定服务器端口号
ClientSocket.OnDisconnect:=ClientSocketDisconnect;
//指定事件处理
ClientSocket.OnRead:=ClientSocketRead;
ClientSocket.OnError:=ClientSocketError;
end;
客户端其他过程定义如下。
//当断开连接时提示关闭连接
procedure TForm1.ClientSocketDisconnect(Sender: TObject; Socket:
TCustomWinSocket);
begin
Label1.Caption:='连接已经断开';
end;
procedure TForm1.ClientSocketError(Sender: TObject; Socket:
TCustomWinSocket; ErrorEvent: TErrorEvent; var ErrorCode: Integer);
//客户端连接错误
begin
ErrorCode := 0;
end;
//客户端读取服务器消息
procedure TForm1.ClientSocketRead(Sender: TObject; Socket:
TCustomWinSocket);
begin
label3.Caption :='连接成功';
Memo1.Lines.add(socket.ReceiveText);
if not btnOpenFile.Enabled then
btnOpenFile.Enabled:=True //准备选择文件
else btnUploadFile.Enabled:=True; //准备上传文件
end;
procedure TForm1.FormCreate(Sender: TObject); //初始化窗体和全局变量
begin
Count := 0;
btnOpenFile.Enabled:=False;
btnUploadFile.Enabled:=False;
abort:=False;
uploadfname:='';
uploadfsize:=0;
ClientSocket:=TClientSocket.Create(Self); //创建客户端的Client
//Socket并设定属性
ClientSocket.Active:=False;
ClientSocket.Address:= EServerIP.Text ; //指定服务器地址
ClientSocket.ClientType:=ctNonBlocking; //非堵塞式工作方式
ClientSocket.Port:=strtoint(EServerPort.text); //指定服务器端口号
ClientSocket.OnDisconnect:=ClientSocketDisconnect;
//指定事件处理
ClientSocket.OnRead:=ClientSocketRead;
ClientSocket.OnError:=ClientSocketError;
end;
function GetFileSize(const FileName: string):integer;
//获取文件大小
var
f : TFileStream;
begin
f := TFileStream.Create(FileName,fmOpenRead or fmShareDenyNone);
Result :=f.Size;
F.Free;
end;
//关闭窗体时关闭连接
procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
ClientSocket.Active := False;
end;
//连接或断开与服务器的
//连接
procedure TForm1.btnConnectClick(Sender: TObject);
begin
if btnConnect.Caption='连接' then
begin
ClientSocket.Port:=strtoint(EServerPort.text);
ClientSocket.Active := not ClientSocket.Active;
btnConnect.Caption:='断开连接';
Label3.Caption:='正在连接...';
end
else
begin
ClientSocket.Active := not ClientSocket.Active;
btnConnect.Caption:='连接';
Label3.Caption:='正在断开连接...';
end;
end;
procedure TForm1.btnOpenFileClick(Sender: TObject);
//选择上传文件
begin
if abort then
begin
btnUploadFileClick(Self); //如果未按上传
//文件按钮,视为
//放弃,强制执行
end;
with OpenDialog1 do
begin
Execute;
if FileName <> '' then
begin
uploadfname:=FileName;
uploadfsize:=GetFileSize(FileName);
Edit3.Text := 'UPLOAD '+ ExtractFileName(FileName) +'
'+Inttostr(uploadfsize);
Label3.Caption :='选择文件'+uploadfname+',大小为
'+inttostr(uploadfsize)+'字节';
ClientSocket.Socket.SendText(edit3.Text); //提交文件信息
end;
end;
abort:=True;
end;
procedure TForm1.btnUploadFileClick(Sender: TObject); //上传文件
var
fs : TFileStream;
Buf : pointer;
begin
abort:=False;
fs := TFileStream.Create(uploadfname,fmOpenRead or fmShareDenyNone);
GetMem(Buf,uploadfsize); //分配内存空间
fs.Seek(0,soFromBeginning);
fs.ReadBuffer(Buf^,uploadfsize); //把文件从文件
//流读入内存
memo1.Lines.Add('上传文件'+uploadfname+',已经发送 : '
+ inttostr(ClientSocket.Socket.SendBuf(Buf^,uploadfsize))+'字节
'#13#10);
//发送该内存内
//容到socket
end;
2. 服务器端设计
服务器端的功能是监听客户端的连接,接受符合要求的连接请求,并接受、存储收到
的文件。服务器端表单设计。
图:服务器端表单的设计
服务器端表单上放置了2 个Label 标签、1 个TButton 按钮、1 个TEdit 文本框、1 个Save 对话框和1 个TMemo 控件。在Delphi 7 中不存在TServerSocket 控件,而是以TServerSocket 类的形式存在,因此本例中应该创建该类的实例并初始化,创建方法如同TClientSocket类实例的创建方法。
服务器端过程的定义如下。
type
TCon = record //定义记录类型
FileName : String;
TotalSize : Integer;
Status : Integer;
end;
PCON = ^TCON;
End;
procedure TForm1.FormCreate(Sender: TObject); //初始化窗体和
//全局变量
begin
Count := 0;
abort:=False;
uploadfname:='';
uploadfsize:=0;
ServerSocket:=TServerSocket.Create(Self); //创建服务器端
//的Client
//Socket并设定
//属性
ServerSocket.Active:=False;
ServerSocket.Port:=strtoint(edit1.Text);
ServerSocket.ServerType:=stNonBlocking; //非堵塞式工作
//方式
ServerSocket.OnClientConnect:=ServerSocketClientConnect;
ServerSocket.OnClientDisconnect:=ServerSocketClientDisconnect;
ServerSocket.OnClientRead:=ServerSocketClientRead; //指定事件处理
ServerSocket.OnClientError:=ServerSocketClientError;
end;
procedure TForm1.btnServerActiveClick(Sender: TObject); //开启或断开FTP
//服务
begin
if btnServerActive.Caption='启动服务' then
begin
ServerSocket.Port:=strtoint(edit1.Text);
ServerSocket.Active := not ServerSocket.Active;
btnServerActive.Caption:='终止服务';
Label1.Caption:='服务已经开启';
end
else
begin
ServerSocket.Active := not ServerSocket.Active;
btnServerActive.Caption:='启动服务';
Label1.Caption:='服务已经终止';
end;
end;
//关闭窗体时关闭服务器连接
procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
ServerSocket.Active := False;
end;
//连接服务器成功
procedure TForm1.ServerSocketClientConnect(Sender: TObject; Socket:
TCustomWinSocket);
var
c : pcon;
begin
c :=new(pcon);
c.FileName := '';
c.TotalSize := 0 ;
c.Status := 0;
Socket.Data := c;
Socket.SendText('已经连接,请上传文件,格式:UPLOAD FILENAME SIZE'#13#10);
Label1.Caption:='已经连接';
end;
//当连接断开时显示的信息
procedure TForm1.ServerSocketClientDisconnect(Sender: TObject; Socket:
TCustomWinSocket);
begin
Label1.Caption:='断开连接成功!';
end;
//服务器错误
procedure TForm1.ServerSocketClientError(Sender: TObject; Socket:
TCustomWinSocket; ErrorEvent: TErrorEvent; var ErrorCode: Integer);
begin
ErrorCode := 0;
end;
//服务器读取客户端消息
procedure TForm1.ServerSocketClientRead(Sender: TObject; Socket:
TCustomWinSocket);
var
C : PCON;
cmd:String;
Buffer : pointer;
nRetr : integer;
fs : TFileStream;
const
bufferSize = 1024*1024 ; //文件大小上限
begin
C:= Socket.Data ;
case c.Status of
0 : //已经连接,并提交文件信息
begin
cmd := trim(Socket.ReceiveText) ;
if Pos('UPLOAD ',uppercase(cmd)) > 0 then
begin
c.FileName := trim(Copy(cmd,Pos(' ',cmd)+1,Length(cmd)));
try
c.TotalSize := StrToInt(Copy(c.FileName,Pos(' ',c.FileName)
+1,Length(c.FileName)
-Pos(' ',c.FileName)));
except
c.Status:=3;
Socket.SendText('文件名不可以有空格!'#13#10);
Socket.Data:=c;
exit;
end;
c.FileName := trim(Copy(c.FileName,1,Pos(' ',c.FileName)));
if c.TotalSize<=buffersize then
begin
c.Status := 1;
Socket.SendText('可以上传文件!'#13#10);
end
else
begin
c.status:=2;
Socket.SendText('文件大小超过100K!不可以上传文件!'#13#10);
end;
Socket.Data := C;
end;
end;
1 : //可以上传文件
begin
Count := count + 1;
GetMem(Buffer,BufferSize);
nRetr := Socket.ReceiveBuf(Buffer^,BufferSize);
//接收socket端的内存变量
Memo1.Lines.Add('接收第'+IntToStr(Count) + '个文件,大小为 ' +
IntToStr(nRetr)+'字节'#13#10);
SaveDialog1.Title:='请选择或输入接收到的数据保存到的文件名:';
SaveDialog1.FileName:=c.FileName;
memo1.Lines.Add ('开始接收!');
if SaveDialog1.Execute then
begin
fs :=TFileStream.Create(SaveDialog1.FileName,fmCreate or
fmShareDenyNone);
fs.Seek(0,soFromBeginning);
//文件不存在,则创建该文件
end
else
begin
fs :=TFileStream.Create(SaveDialog1.FileName,fmOpenWrite or
fmShareDenyNone);
fs.Seek(0,soFromEnd); //文件存在,则在文件末尾添加上传内容。
end;
fs.WriteBuffer(Buffer^,nRetr); //把内存变量写入文件流
memo1.Lines.Add ('传输完毕!');
fs.Destroy;
FreeMem(Buffer); //释放变量和内存空间,为下一次文件传送做
//准备
c.Status:=0;
Socket.Data:=c;
end;
2: //文件大小超限
begin
Memo1.Lines.Add('文件大小超过100K!不能上传!'#13#10);
GetMem(Buffer,BufferSize);
nRetr := Socket.ReceiveBuf(Buffer^,BufferSize);
FreeMem(Buffer); //接收socket端口内容,释放
//为下一次文件传送做准备
c.Status:=0;
Socket.Data:=c;
end;
3: //文件名错误
begin
Memo1.Lines.Add('文件名错误!'#13#10);
GetMem(Buffer,BufferSize);
nRetr := Socket.ReceiveBuf(Buffer^,BufferSize);
FreeMem(Buffer); //接收socket端口内容释放,为下一次文件
//传送做准备
c.Status:=0;
Socket.Data:=c;
end;
end;
end;
运行客户端和服务器程序,按如下流程进行操作。
(1) 首先设置服务器的端口号如2121,然后单击服务器程序的【启动服务】按钮,启动服务器。
(2) 在客户程序上设置服务器程序的IP 地址和端口号,单击【连接】按钮,建立和服务器的连接。
(3) 在客户程序上单击【选择文件】按钮,选择要发送的文件,并单击【上传文件】按钮上传文件。
(4) 文件发送完毕,单击客户程序的【断开连接】按钮,断开同服务器的连接。
(5) 按下服务器程序的【停止服务】按钮,停止服务器的服务。