通用的 C 语言 TCP 客户端应用程序
用于编写简单 TCP 客户端应用程序的库。
引言
本文介绍了一个用于编写简单的 TCP 客户端应用程序的库,该库使用 C 语言和面向对象的思想编写,利用标准的 Berkeley 套接字接口,并旨在实现跨平台。开发是在安装了 Ubuntu Linux 发行版的系统上进行的,之后在 Windows 2003 上使用 Visual Studio Express 2008 进行了移植和少量测试。
背景
当我为我的 CodeProject 文章“一个小型 C 语言 TCP 服务器框架”(请参阅此处)编写示例程序时,我注意到一个奇怪的现象:编写服务器示例比编写客户端示例更容易!因此,我决定深入研究这个问题,结果是原始服务器项目的精简版本,现在只包含用于编写简单客户端应用程序的功能,并且简单直接。
描述
首先,我将介绍一些术语。
通用客户端是使用本文所述库编写的简单 TCP 客户端应用程序。
这个通用客户端遵循一个由三个概念构建的特定模式:状态、事件和命令。
状态是以下之一
CONNECTED_IDLE
NOT_CONNECTED
命令是以下之一
CONNECT
SEND
RECV
CLOSE
事件是以下之一
CONNECTION_CREATED
CONNECTION_DESTROYED
CONNECT_ERROR
RECV_COMPLETE
RECV_TIMEOUT
SEND_COMPLETE
还有另外两个实体参与其中:由库提供的 GenericClient
类的一个实例,以及由库用户编写的应用程序代码。GenericClient
类是从 Client
类非正式派生而来的,Client
类提供了编写客户端应用程序的基本方法。
使用此库编写的应用程序中的一般控制流如下所示:
- 应用程序代码将一个命令传递给
GenericClient
实例。 GenericClient
实例处理接收到的命令,并将一个事件传递给应用程序代码。- 然后,应用程序代码处理从
GenericClient
实例接收到的事件,然后循环再次重复。
GenericClient
实例由一个状态机驱动,如下面的伪代码所示
1. if state is NOT_CONNECTED
// only accepts the CONNECT command
do connect
if result is OK
set state to CONNECTED_IDLE
return event CONNECTION_CREATED
else
// remains in the state NOT_CONNECTED
return event CONNECT_ERROR
2. if state is CONNECTED_IDLE
2.1 if command is SEND
do send
if result is OK
// remains in the state CONNECTED_IDLE
return event SEND_COMPLETE
else
set state to NOT_CONNECTED
return event CONNECTION_DESTROYED
2.2 if command is RECV
do recv
if result is OK
// remains in the state CONNECTED_IDLE
return event RECV_COMPLETE
else if occurred TIMEOUT
// remains in the state CONNECTED_IDLE
return event RECV_TIMEOUT
else // error
set state to NOT_CONNECTED
return event CONNECTION_DESTROYED
2.3 if command is CLOSE
do close
set state to NOT_CONNECTED
return event CONNECTION_DESTROYED
应用程序代码不必关心 GenericClient
实例如何工作;它只关心如何处理从 GenericClient
实例接收到的事件。
应用程序代码的伪代码是开放的,但通常如下所示
1. if event is CONNECTION_CREATED
prepare message to send to the server
pass the command SEND to the generic client instance
2. if event is SEND_COMPLETE
update whatever controls needed by the application
pass the command RECV to the generic client instance
3. if event is RECV_COMPLETE
process reply received from the server
prepare another message to send to the server
pass the command SEND to the generic client instance
4. if event is RECV_TIMEOUT (*** optional ***)
do whatever the application needs in case of timeout
if business rules says try again
prepare another message to send to the server
pass the command SEND to the generic client instance
else
pass the command CLOSE to the generic client instance
5. if event is CONNECTION_DESTROYED
// can be the result of SEND, RECV or CLOSE commands
// (most errors will automatically destroy the connection)
do whatever cleanup the application needs
pass the command CONNECT to the generic client instance
6. if event is CONNECT_ERROR
do whatever the application needs in this case
if business rules says try again
pass the command CONNECT to the generic client instance
else
end application
利用上述模式,编写简单的 TCP 客户端应用程序非常容易,因为看不到 TCP/IP 网络代码。实际上,只需要编写应用程序代码。
这里的目标不是编写生产级别的应用程序,而是用于测试场景、原型开发新功能、查找服务器应用程序中的错误等的简单实用程序。在这些情况下,开发速度至关重要,因为创建的程序通常是可丢弃的、一次性的实用程序,其范围和功能有限,不值得花费太多时间进行开发。
实现上述伪代码的 C 代码如下所示
switch (genCli_waitEvent())
{
case CLI_EVT_CONNECTION_CREATED:
prepareFirstMessage();
genCli_send();
break;
case CLI_EVT_RECV_COMPLETE:
processServerReply();
prepareAnotherMessage();
genCli_send();
break;
case CLI_EVT_SEND_COMPLETE:
genCli_recv();
break;
case CLI_EVT_RECV_TIMEOUT:
prepareAnotherMessage();
genCli_send();
break;
case CLI_EVT_CONNECT_ERROR:
printConnectionError();
genCli_connect();
break;
case CLI_EVT_CONNECTION_DESTROYED:
printOperationError();
genCli_connect();
break;
default:
printf("*invalid event %d\n", genCli_event());
abort();
break;
} // switch
上面的 genCli_xxx
方法由 GenericClient
类提供。应用程序程序员不必担心通常与 TCP/IP 编程相关的所有样板代码。有关更多详细信息,请参阅 gen_client_1
示例项目。
该库还提供了其他功能,由 Client
类呈现,但实际上是由其他类实现的(这里的 Client
类充当外观)。
这些附加功能中最重要的是与 Message
类相关。它提供了在客户端和服务器之间交换消息时使用的缓冲区的封装,还提供了在网络上传输消息的定界框架。请参阅 Message
类、Client
类以及此项目附带的 gen_client_1
示例的文档。顺便说一下,Message
类为应用程序提供的更重要的方法(由 Client
类封装)是:client_messageBuffer
、client_messageSize
和 client_setMessageSize
。
还有一个 Log
类,由库内部使用,但也可以由应用程序使用。其主要方法是(同样,由 Client
类封装)
client_logInfo
client_logWarn
client_logDebug
client_logTrace
client_logError
client_logFatal
库内部还有一些其他类,在编写客户端应用程序时可能有用,也可能无用:Mutex
、Thread
、Time
和 Timeout
。
该库的编写旨在与使用此 CodeProject 文章中描述的库编写的服务器进行互操作。具体来说,此项目附带的 gen_client_1
示例可与上述文章附带的 server_2
和 server_3
示例一起使用。
要使该库能够与不同的服务器一起工作,很可能需要更改 MessageImpl.h
文件中声明的 Message
结构。具体来说,请参阅客户端和服务器之间交换的消息在网络上的格式。
结论
TCP 客户端应用程序通常遵循一个通用模式:打开与服务器的连接,发送请求,然后接收并处理响应。在通信出错的情况下,最安全的做法几乎总是关闭连接并重新开始。在该库中,我试图整合这种通用行为,以便将繁琐且重复的工作只做一次,而应用程序程序员只需关注与应用程序相关的业务规则。当然,这是利用了软件工程中一个非常著名的原则:DRY(Don't Repeat Yourself,不要重复你自己)原则。
历史
这是本文的第一个版本,涵盖了库的 1.00 版本。