在C++ Builder XE2中用Indy10开发Socket应用程序

注:本文是根据网上一篇《Indy简单教程》整理的,原文用的是Dephi7

开放源代码的Internet组件集

      开放源代码的Internet组件集——Internet DirectIndyInternet DirectIndy)是一组开放源代码的Internet组件,涵盖了几乎所有流行的Internet协议。IndyDelphi编写,被包含在 Delphi 6Kylix 1C Builder 6及以上各个版本的Borland开发环境中。Indy曾经叫做WinShoes(双关于WinSock——WindowsSocket库),是由 Chad Z. Hower领导的一群开发者构建的,可以从Indy的站点www.indyproject.org/index.en.aspx上找到更多的信息并下载其 新版本。

Indy是阻塞式(Blocking)的

     当你使用Winsock开发网络应用程序时,从Socket中读取数据或者向Socket写入数据都是异步发生的,这样就不会阻断程序中其它代码的执行。 在收到数据时,Winsock会向应用程序发送相应的消息。这种访问方式被称作非阻塞式连接,它要求你对事件作出响应,设置状态机,并通常还需要一个等待 循环。

  与通常的Winsock编程方法不同的是,Indy使用了阻塞式Socket调用方式。阻塞式访问更像是文件存取。当你读取 数 据,或是写入数据时,读取和写入函数将一直等到相应的操作完成后才返回。比如说,发起网络连接只需调用Connect方法并等待它返回,假如该方法执行成 功,在结束时就直接返回,假如未能成功执行,则会抛出相应的异常。同文件访问不同的是,Socket调用可能会需要更长的时间,因为要读写的数据可能不会 立即就能预备好(在很大程度上依靠于网络带宽)。

阻塞式Socket并非恶魔(Evil

长期以来,阻塞式Socket都遭到了毫无理由的攻击。其实阻塞式Socket并非如通常所说的那样可怕。这还要从Winsock的发展说起。

   当Socket被从Unix移植到Windows时,一个严重的问题立即就出现了。Unix支持fork,客户程序和服务器都能够fork新的进程,并 启动这些进程,从而能够很方便地使用阻塞式Socket。而Windows 3.x既不支持fork也不支持多线程,当使用阻塞式Socket时,用户界面就会被锁住而无法响应用户输入。

  为克服Windows 3.x的这一缺陷,微软在Winsock中加入了异步扩展,以使Winsock不会锁住应用程序的主线程(也是唯一的线程)。然而,这需要了一种完全不同的编程方式。于是有些人为了掩饰这一弱点,就开始强烈地诽谤阻塞式Socket

  当Win32出现的时候,它能够很好地支持线程。但是既成的观念已经很难更改,并且说出去的话也无法收回,因此对阻塞式Socket的诽谤继续存在着。

事实上,阻塞式Socket仍然是Unix实现Socket的唯一方式,并且它工作得很好。

阻塞式Socket的优点
    归结起来,在Windows上使用阻塞式Socket开发应用程序具有如下优点:

  编程简单——阻塞式Socket应用程序很轻易编写。所有的用户代码都写在同一个地方,并且顺序执行。 
轻易向Unix移植——由于Unix也使用阻塞式Socket,编写可移植的代码就变得比较轻易。Indy就是利用这一点来实现其多平台支持而又单一源代码的设计。 

  很好地利用了线程技术——阻塞式Socket是顺序执行的,其固有的封装特性使得它能够很轻易地使用到线程中。 

阻塞式Socket的缺点

事物都具有两面性,阻塞式Socket也不例外。它的一个主要的缺点就是使客户程序的用户界面冻结。当在程序的主线程中进行阻塞式Socket调用 时,由于要等待Socket调用完成并返回,这段时间就不能处理用户界面消息,使得UpdateRepaint以及其它消息得不到及时响应,从而导致用 户界面被冻结。 

 使用TIdAntiFreeze对抗冻结” 

      Indy使用一个非凡的组件TIdAntiFreeze来透明地解决客户程序用户界面冻结的问题。TIdAntiFreezeIndy内部定时 中断对栈的调用,并在中断期间调用Application.ProcessMessages方法处理消息,而外部的Indy调用继续保存阻塞状态,就似乎 TIdAntiFreeze对象不存在一样。你只要在程序中的任意地方添加一个TIdAntiFreeze对象,就能在客户程序中利用到阻塞式 Socket的所有优点而避开它的一些显著缺点。

Indy使用了线程技术

阻塞式Socekt通常都采用线程技术,Indy也是如此。从最底层开始,Indy的设计都是线程化的。因此用Indy创建服务器和客户程序跟在Unix下十分相似,并且Delphi的快速开发环境和IndyWinSock的良好封装使得应用程序创建更加轻易。

Indy服务器模型

一个典型的Unix服务器有一个或多个监听进程,它们不停地监听进入的客户连接请求。对于每一个需要服务的客户,都fork一个新进程来处理该客户的所有事务。这样一个进程只处理一个客户连接,编程就变得十分轻易。

  Indy服务器工作原理同Unix服务器十分类似,只是Windows不像Unix那样支持fork,而是支持线程,因此Indy服务器为每一个客户连接分配一个线程。

  图1显示了Indy服务器的工作原理。Indy服务器组件创建一个同应用程序主线程分离的监听线程来监听客户连接请求,对于接受的每一个客户,都创建一个新的线程来为该客户提供服务,所有与这一客户相关的事务都由该线程来处理。

 

1 Indy服务器工作原理

使用组件TIdThreadMgrPoolIndy还支持线程池。

注:Indy10IdThreadMgrPool替换为IdSchedulerOfThreadPool组件,并将IdTcpServerScheduler属性改成IdSchedulerOfThreadPool

线程与Indy客户程序

  Indy客户端组件并未使用线程。但是在一些高级的客户程序中,程序员可以在自定义的线程中使用Indy客户端组件,以使用户界面更加友好。

简单的Indy应用示例

下面将创建一个简单的TCP客户程序和一个简单的TCP服务器来演示Indy的基本使用方法。客户程序使用TCP协议同服务器连接,并向服务器发送用户所 输入数据。服务器支持两条命令:DATAQUIT。在DATA命令后跟随要发送的数据,并用空格将命令字DATA和数据分隔开。

表单布局

   建立一个项目组,添加一个客户程序项目和一个服务器项目。客户程序和服务器程序的表单布局如同2和图3所示。客户程序表单上放置了 TIdTCPClient组件,服务器程序表单上放置了TIdTCPServer组件。为防止客户程序冻结,还在其表单上放置 TIdAntiFreeze组件。

简单的TCP客户程序表单

简单的TCP服务器程序表单

客户程序和服务器程序的表单上都放置有TListBox组件,用来显示通信记录。

客户程序代码

客户程序片断如代码列表1所示。

代码列表1

 

void __fastcall TfrmMain::btnConnClick(TObject *Sender)
{
	IdTCPClient1->Host = lbledtHost->Text;
	IdTCPClient1->Port = lbledtPort->Text.ToInt();
	lstRecord->Items->Add("正在连接" + lbledtHost->Text + "...");

	try
	{
	   IdTCPClient1->Connect();

	   try
	   {
		  lstRecord->Items->Add(IdTCPClient1->IOHandler->ReadLn(TEncoding::UTF8));
		  btnConn->Enabled = false;
		  btnSend->Enabled = true;
		  btnDiscon->Enabled = true;
	   }
	   catch(...)
	   {
		   lstRecord->Items->Add("远程主机无响应!");
		   IdTCPClient1->Disconnect();
	   }
	}
	catch(...)
	{
		lstRecord->Items->Add("无法建立到" + lbledtHost->Text + "的连接!");
	}
}
//---------------------------------------------------------------------------
void __fastcall TfrmMain::btnSendClick(TObject *Sender)
{
	lstRecord->Items->Add("DATA:" + lbledtSendData->Text);

	try
	{
		IdTCPClient1->IOHandler->WriteLn("DATA:" + lbledtSendData->Text,TEncoding::UTF8);
		lstRecord->Items->Add(IdTCPClient1->IOHandler->ReadLn(TEncoding::UTF8));
	}
	catch(...)
	{
		lstRecord->Items->Add("发送数据失败!");
		IdTCPClient1->Disconnect();
		lstRecord->Items->Add("同主机 " + lbledtHost->Text + " 的连接已断开!");
		btnConn->Enabled = true;
		btnSend->Enabled = false;
		btnDiscon->Enabled = false;
    }
}
//---------------------------------------------------------------------------

void __fastcall TfrmMain::btnDisconClick(TObject *Sender)
{
	lstRecord->Items->Add("QUIT");

	try
	{
		IdTCPClient1->IOHandler->WriteLn("QUIT");
	}
	__finally
	{
		IdTCPClient1->Disconnect();
		lstRecord->Items->Add("同主机 " + lbledtHost->Text + " 的连接已断开!");
		btnConn->Enabled = true;
		btnSend->Enabled = false;
		btnDiscon->Enabled = false;
    }
}
//---------------------------------------------------------------------------

void __fastcall TfrmMain::btnExitClick(TObject *Sender)
{
	Close();
}
//---------------------------------------------------------------------------

 

 

连接按钮事件响应过程中,首先根据用户输入设置IdTCPClient的主机和端口,并调用IdTCPClientConnect方法向服务器发出连接请求。然后调用ReadLn方法读取服务器应答数据。
  在发送按钮事件响应过程中,调用WriteLn方法写DATA命令,向服务器发送数据
  在断开按钮事件响应过程中,向服务器发送QUIT命令,并调用Disconnect方法断开连接。
  程序中还包含有通信信息记录和异常处理的代码。

服务器程序代码

服务器程序片断如代码列表2所示。

代码列表2

server.h

private:	// User declarations
	String sReceived;
	String sLogEntry;
public:		// User declarations
	void __fastcall AddLogEntry();
	void __fastcall DisplayData();

 Server.cpp

void __fastcall TfrmMain::btnStartClick(TObject *Sender)
{
	IdTCPServer1->DefaultPort = lbledtPort->Text.ToInt();
	IdTCPServer1->Active = true;
	btnStart->Enabled = false;
	btnStop->Enabled = true;

	lstRecord->Items->Add("服务器已成功启动!");
}
//---------------------------------------------------------------------------

void __fastcall TfrmMain::btnStopClick(TObject *Sender)
{
	IdTCPServer1->Active = false;
	btnStart->Enabled = true;
	btnStop->Enabled = false;

	lstRecord->Items->Add("服务器已成功停止!");
}
//---------------------------------------------------------------------------

void __fastcall TfrmMain::IdTCPServer1Connect(TIdContext *AContext)
{
	lstRecord->Items->Add("来自主机" + AContext->Connection->Socket->Binding->PeerIP
		+ " 的连接请求已被接纳!");
	AContext->Connection->IOHandler->WriteLn("100: 欢迎连接到简单TCP服务器!",TEncoding::UTF8);

}
//---------------------------------------------------------------------------


void __fastcall TfrmMain::DisplayData()
{
	lbledtReceiveData->Text = sReceived;
}
//---------------------------------------------------------------------------

void __fastcall TfrmMain::AddLogEntry()
{
	lstRecord->Items->Add(sLogEntry);
}
//---------------------------------------------------------------------------

#include <idsync.hpp>
#include <StrUtils.hpp>

void __fastcall TfrmMain::IdTCPServer1Execute(TIdContext *AContext)
{
	String sCommand;

	sCommand = AContext->Connection->IOHandler->ReadLn(TEncoding::UTF8);
	sLogEntry = sCommand + "来自于主机" + AContext->Connection->Socket->Binding->PeerIP;
	TIdSync::SynchronizeMethod(AddLogEntry);

	if(AnsiStartsText("DATA",sCommand))
	{
		sReceived = AnsiRightStr(sCommand, sCommand.Length()-5);
		AContext->Connection->IOHandler->WriteLn("200: 数据接收成功!",TEncoding::UTF8);
		TIdSync::SynchronizeMethod(DisplayData);
	}
	else if(SameText(sCommand, "QUIT"))
	{
		sLogEntry = "断开同主机 " + AContext->Connection->Socket->Binding->PeerIP + "的连接";
		TIdSync::SynchronizeMethod(AddLogEntry);
		AContext->Connection->Disconnect();
	}
	else
	{
		AContext->Connection->IOHandler->WriteLn("500: 无法识别的命令!",TEncoding::UTF8);
		sLogEntry = "无法识别命令:" + sCommand;
		TIdSync::SynchronizeMethod(AddLogEntry);
    }
}
//---------------------------------------------------------------------------

void __fastcall TfrmMain::btnExitClick(TObject *Sender)
{
	Close();
}
//---------------------------------------------------------------------------

 

启动按钮设置IdTCPServer Active属性为True来启动服务器,停止按钮设置Active属性为False来关闭服务器。

   IdTCPServerConnect方法作为IdTCPServer OnCorrect事件响应过程,向客户端发送欢迎信息。OnCorrect事件在一个客户连接请求被接受时发生,为该连接创建的线程AThread被 作为参数传递给IdTCPServerConnect方法。

  IdTCPServerExecute方法是IdTCPServer OnExecute事件响应过程。OnExecute事件在TIdPeerThread对象试图执行其Run方法时发生。OnExecute事件与通常 的事件有所不同,其响应过程是在某个线程上下文中执行的,参数AThread就是调用它的线程。这一点很重要,它意味着可能有多个OnExecute事件 响应过程被同时执行。在连接被断开或中断前,OnExecute事件响应过程会被反复执行。

注:Indy10TIdPeerThread  *AThread 相关的全部代码改成TIdContext  *AContext,并将Connection下的相关I/O方法替换成Connection->IOHandler.的方法。

IdTCPServerExecute方法中,首先读入一条指令,然后对指令进行判别。假如是DATA指令,就解出数据并显示它。假如收到的是QUIT 指令,则断开连接。需要非凡指出的是,由于IdTCPServerExecute方法在某一线程上下文中执行,因此显示数据和添加事件记录都是将相应的方 法传递给Synchronize调用来完成的。

运行程序

  运行客户端和服务器程序,按如下流程进行操作:

  1. 按服务器程序的启动按钮启动服务器;

  2. 按客户程序的连接按钮,建立同服务器的连接;

  3. 在客户程序的待发送数据编辑框中输入“Hello, Indy!”,并按发送按钮发送数据;

  4. 按客户程序的断开按钮,断开同服务器的连接;

  5. 按服务器程序的停止按钮停止服务器。

  程序运行的结果如图4和图5所示。

简单的TCP客户

简单的TCP服务器



 


 

posted @ 2012-04-05 13:50  Partialsky  阅读(1248)  评论(0)    收藏  举报