基于TCP/IP的多线程通信及其在远程监控系统中的应用
2009-05-19
作者:王金廉 谢剑英 贾 青
摘 要: 提出了一种在Windows NT下基于TCP/IP协议的多线程通信的设计与实现方法,在此基础上给出了多线程通信在蓄电池远程监控系统中的应用实例。
关键词: 多线程 实时性 TCP/IP协议 远程监控系统
传统的应用程序都是单线程的,即在程序运行期间,由单个线程独占CPU的控制权,负责执行所有任务。在这种情况下,程序在执行一些比较费时的任务时,就无法及时响应用户的操作,影响了应用程序的实时性能。在监控系统,特别是远程监控系统中,应用程序往往不但要及时把监控对象的最新信息反馈给监视客户(通过图形显示),还要处理本地机与远程机之间的通信以及对控制对象的实时控制等任务,这时 ,仅仅由单个线程来完成所有任务,显然无法满足监控系统的实时性要求。在DOS系统下,这些工作可以由中断来完成。而在Windows NT下,中断机制对用户是不透明的。为此,可引进多线程机制,主线程专门负责消息的响应,使程序能够响应命令和其他事件。辅助线程可以用于完成其他比较费时的工作,如通信、图形显示和后台打印等,这样就不至于影响主线程的运行。
1 Windows NT 多线程概述
Windows NT是一个真正的抢占式多任务操作系统。在Windows NT中,启动一个应用程序就是启动该应用程序的一个实例,即进程。进程由一个或多个线程构成,拥有内存和资源,但自己不能执行自己,而是进程中的线程被调度执行。进程至少要有一个线程,当创建一个进程时,就创建了一个线程,即主线程。主线程可以创建其他辅助线程,由主线程创建的线程又可创建线程。每个线程都可指定优先级,操作系统根据线程的优先级调度线程的执行。
Windows NT中使用多线程的方法有三种:
· 使用C多线程库函数;
· 使用CreateThread() 等Win32函数;
· 使用MFC类。
本文采用第三种方法。在Visual C++5.0 中,MFC应用程序用CWinThread 对象表示线程。基本操作如下:
· 创建新线程:调用MFC全局函数AfxBeginThread( )创建新线程。AfxBeginThread( )启动新线程并返回控制,然后,新线程和调用AfxBeginThread( )的线程同时运行。它的返回值为指向CWinThread对象的指针;
· 暂停/恢复线程:调用CWinThread类成员函数SuspendThread( )暂停线程的运行,调用ResumeThread( )成员函数恢复线程的运行;
· 终止线程:在线程内部可调用全局函数AfxEndThread( )终止线程的运行,否则,线程执行结束后,线程自动从线程函数返回并释放线程占有的资源。
2 基于TCP/IP 的多线程编程
TCP/IP是Internet上广泛使用的一种协议,可用于异种机之间的互联。TCP/IP协议本身是非常复杂的,然而在网络编程中,程序员不必考虑TCP/IP的实现细节,只需利用协议的网络编程接口Socket(亦称套接字)即可。在Windows中,网络编程接口是Windows Sockets,它包含标准的Berkley Sockets的功能调用的集合,以及为Windows 所做的一些扩展。TCP/IP协议的应用一般采用客户/服务器模式,面向连接的应用调用如图1所示。
根据上述顺序调用函数建立连接后,通信双方便可交换数据[1]。然而,在调用带*号的函数时,操作常会阻塞,特别是当套接字工作在同步阻塞模式(Blocking Mode)时。这时,程序无法响应任何消息。为了避免出现这种情况,本文引进辅助线程。在执行含有可能阻塞的函数的任务时,动态创建新的线程,专门处理该任务。主线程把任务交给辅助线程后,不再对辅助线程加以控制与调度。本文分别针对connect()、accept()、receive()、send()等可能阻塞的函数创建了相应的线程,如表1所示。
多线程编程常常还要考虑线程间的通信。线程间的通信可以采用全局变量、指针参数和文件映射等方式。本文采用指针参数方式。在调用AfxBeginThread()函数时,通过传递指针参数的方式在主线程与辅助线程间通信。
AfxBeiginThread( )函数的用法如下:
CWinThread* AfxBeginThread( AFX_THREADPROC pfnThreadProc,
LPVOID pParam,
int nPriority=THREAD_PRIORITY_NORMAL,
UINT nStackSize=0,
DWORD dwCreateFlags=0,
LPSECURITY_ATTRIBUTES lpSecurityAttrs=NULL );
参数pfnThreadProc指定线程函数,必须如下定义:
UINT MyControllingFunction( LPVOID pParam );
参数pParam 是调用线程传递给线程函数pfnThreadProc的参数;
其他参数一般只需采用缺省值。
指针参数通信方式就是通过参数pParam在线程间通信的,它可为指向任何数据类型的指针。本文中,定义了一个名叫EXCHANGE_INFO的结构如下:
typedef struct
{ SOCKET sServerSocket;
SOCKET *psClientSocket;
SOCKADDR_IN *pClientAddr;
BOOL *pbConnected;
unsigned char *pucBuffer;
int *pnMessageLen;
} EXCHANGE_INFO;
在需要通信时,先声明一个结构变量,再把变量的指针作为pParam参数,调用AfxBeginThread((AFX_THREADPROC) CSocketThread::WaitForConnectThread, (LPVOID)& m_ExchangeInfo)函数即可。
为了利用面向对象技术编程所具有的模块性强、便于修改、可移植性好等优点,本文还把表1中的线程封装为父类为CWinThread的自定义类CSocketThread中。还自定义了一个叫CSocketComm的新类,封装了一些函数,如CreateSocket、ConnectToServer、WaitForClient、ReadMessage、SendMessage等,这些函数屏蔽了面向连接的通信程序的实现细节,如创建、连接、发送和接收等,在这些函数里,动态创建辅助线程。
下面以CSocketComm类中的等待客户连接请求的函数WaitForClient()为例,注释说明多线程编程的具体细节。
BOOL CSocketComm::WaitForClient()
{
if( m_bConnected ) return( TRUE );
//配置bind函数的参数,即服务器的套接字地址结构
SOCKADDR_IN Addr;
memset( &Addr, 0, sizeof( SOCKADDR_IN ) );
Addr.sin_family = AF_INET;
Addr.sin_port = htons( m_nPort );
Addr.sin_addr.s_addr = htonl( INADDR_ANY );
//将套接字地址结构赋予套接字(绑定),以指定本地半相关
int nReturnValue;
nReturnValue = ::bind( m_sServerSocket, (LPSOCKADDR) &Addr,sizeof( SOCKADDR_IN ) );
if( nReturnValue == SOCKET_ERROR ) return( FALSE );
//配置传给WaitForConnectThread线程函数的参数m_ExchangeInfo
m_ExchangeInfo.sServerSocket = m_sServerSocket;
m_ExchangeInfo.psClientSocket = &m_sClientSocket;
m_ExchangeInfo.pClientAddr = &m_ClientAddr;
m_ExchangeInfo.pbConnected = &m_bConnected;
//以m_ExchangeInfo的指针为参数调用WaitforConnectThread线程等待客户端连接
AfxBeginThread((AFX_THREADPROC)CSocketThread::
WaitForConnectThread, (LPVOID) & m_ExchangeInfo );
return( TRUE );
}
//等待连接线程
UINT CSocketThread::WaitForConnectThread(LPVOIDpParam)
{
EXCHANGE_INFO*pExchangeInfo=(EXCHANGE_INFO *) pParam;
int nReturnValue, nClientAddrSize = sizeof( SOCKADDR_IN );
//侦听连接
nReturnValue = ::listen(pExchangeInfo ->sServerSocket, 1 );
if( nReturnValue == SOCKET_ERROR ) return( 0 );
//阻塞调用accept,直至有客户连接请求
*pExchangeInfo->psClientSocket = ::accept(pExchangeInfo->sServerSocket, (LPSOCKADDR) pExchangeInfo ->pClientAddr, &nClientAddrSize );
if( (*pExchangeInfo ->psClientSocket) != INVALID_SOCKET )
//通过pExchangeInfo的指针在线程间通信
*pExchangeInfo->pbConnected = TRUE;
return( 0 );
}
3 应用实例-高层协议的设计
在电厂和电站中,为了保证安全工作,保护系统必不可少。保护系统的电源供应通常使用两种方式。一般情况下,使用交流电系统对保护系统进行供电;当交流电系统出现故障时,立即使用后备的蓄电池系统对保护系统进行供电。为了对蓄电池系统进行监控和管理,以保证蓄电池在关键时刻能正常工作,设计了在Windows NT环境下具有远程通讯功能和动态人机界面的智能蓄电池远程监控系统 。该系统由蓄电池智能管理、充电机控制、母线绝缘在线检测、声光报警、系统组态、远程通信等子系统组成,实现对蓄电池/充电机智能化远程管理和控制,对整个系统的运行状态进行实时监控,具有多媒体报警、事件处理、动态数据库、趋势画面和动态画面显示、操作提前提醒等功能。系统框图如图2所示。在远程通信模块中,远程监控机需把监控客户的操作命令及时传给本地机,本地机根据命令控制充电机,使之按照一定的方式工作,而本地机需定时向远程监控机反馈实时的充电机状态信息。它们之间的通信是基于TCP/IP的广域网通信,而且,我们引进了多线程机制以保证系统具有良好的实时性。
下面以其中的充电机控制系统为例谈谈如何使用CSocketComm类进行远程通信。为简单起见,假定本地机与远程监控机之间通信的信息仅有下面三种类型:
·本地机接收到该命令后,控制充电机按照稳压模式运行,输出电压为电压给定值;
·本地机接收到该命令后,控制充电机按照稳流定时模式运行,输出电流为电流给定值;
·本地机向远程监控机发送充电机的实时状态数据(包括输出电压、输出电流、状态指示和故障类型指示)。
在基于TCP/IP的面向连接的网络通信中,客户与服务器之间传送的是有序可靠的字节流(Byte Stream),所以程序员有必要在传输层TCP上定义自己的高层协议,设计帧结构,将字节流变成有意义的信息。在CSocketComm类中由AssembleMessage( )函数把数据组合成一定的帧结构。帧结构为:
其中@为帧起始标志,#为帧终结标志
对应的结构定义如下:
typedef struct
{ int MessageType; //信息类型
int ChargerNo; //充电机编号
int DataNo; //数据类型
float Data; //数据
} MessageStruct;
需要通信时,先声明一个MessageStruct变量,根据信息内容对各成员变量赋值,传给 AssembleMessage()函数组合成帧,再调用SendMessage()函数发送给接受方。接受方接到数据后,对数据内容的解释,是由CSocketComm类中的AnalyzeMessage()函数完成的。AnalyzeMessage()函数返回一个MessageStruct变量。应用程序就可根据它的各成员变量控制充电机或动态显示充电机的状态。
总之,把多线程机制引进通信,有利于提高应用程序的实时性,充分利用系统资源。对于大型的工程应用来说,不同的线程完成不同的任务,也有利于提高程序的模块化,便于维护和扩展。本文给出了一种在Windows NT下基于TCP/IP协议的多线程通信的基本方法,根据该方法进行修改和扩充,便可设计出符合具体应用的高质量的多线程通信程序。
参考文献
1 蒋东兴,林鄂华.Windows Socket 网络程序设计指南.北京:清华大学出版社,1995
2 Rajagopal Raj, Monica Subodh P.Windows NT4高级程序设计.北京:机械工业出版社,1998