65.9K
CodeProject 正在变化。 阅读更多。
Home

CSPServer,基于状态的协议服务器类

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (14投票s)

2003 年 3 月 12 日

8分钟阅读

viewsIcon

161313

downloadIcon

1369

用于创建客户端/服务器协议服务器的类框架

引言

CSPServer 是一个 VC++ 类,可以相对轻松地创建健壮、多线程的客户端/服务器基于状态的协议服务器 (SPS)。  常见的现有标准客户端/服务器 SPS 系统包括 SMTP、POP3、FTP、NNTP 以及数百万人在互联网上每天使用的其他系统。 

CSPServer 为您提供了一个久经考验、设计精良的框架,用于创建您自己的标准协议服务器或适合您需求的新协议服务器。  CSPServer 用于 Santronics Software 的内网托管产品 Wildcat! Interactive Net Server ( http://www.santronics.com ),用于提供集成的多协议内网托管系统。   专有的虚拟通信技术已被移除,以创建仅基于公共套接字的 CSPServer 版本。 

本文将解释如何使用 CSPServer 类及其工作中的 SPS 示例。  这是作者的第一个 CodeProject 文章投稿,因此欢迎所有评论者和批评者。 

背景

基于状态的协议服务器或 SPS 是一种客户端/服务器方法,客户端应用程序连接到服务器应用程序以开始基于文本的“受控”对话。  这种受控对话通常称为“状态机”。  

在状态机中,已连接的客户端会发出一个命令,然后等待服务器对该命令的响应。  在设计良好的状态机中,在服务器为当前命令提供响应之前,客户端无法继续执行其他命令。 非常重要的是要理解,状态机中的所有对话都始于客户端发出命令。  除非客户端请求,否则服务器永远不会将数据或信息发送给客户端,也不会响应客户端的命令。 

CSPServer 提供了一个框架,用于为您自己的客户端/服务器应用程序创建客户端/服务器状态机对话。 

如果您理解了这个基本概念,则可以跳过下一节背景介绍,该节将使用标准的 SMTP 服务器来说明 SPS。 

示例标准 SPS - SMTP

如果您曾经连接到 SMTP、POP3、FTP、NNTP 等标准 SPS,您看到的第一个就是欢迎消息。   看到这一点最好的方法是使用标准的 TELNET 客户端,例如 Windows 自带的客户端。  例如,要使用 telnet 连接到 Microsoft SMTP 服务器 (端口 25),请键入以下内容:

Telnet maila.microsoft.com 25

如果成功,您将看到欢迎消息。  您可以键入 HELP 来查看可用命令。  大多数标准 SPS 系统都会提供有关可用命令的 HELP 信息。 

然而,SPS 系统最终是为自动化应用程序设计的,而不是为人工交互设计的。  客户端软件用于自动化过程,例如发送电子邮件。   下面说明了当您要向世界上的任何人发送电子邮件时通常会发生的情况。

示例 SMTP 客户端/服务器会话

假设电子邮件的目标地址是 gbush@whitehouse.gov  并且假设您正在使用 Outlook Express (OE) 来创建和发送电子邮件。  OE 具有内置的 SMTP 客户端组件,用于将邮件发送到 SMTP 服务器。 

以下是发送电子邮件的步骤。

  1. OE smtp 客户端获取 whitehouse.gov 域的 MX 记录。  MX 记录提供了 SMTP 服务器的 IP 地址位置。  客户端随后连接到 MX 记录定义的 IP 地址。
  2. 客户端等待欢迎响应,然后发出 HELO 或 EHLO 命令。   客户端等待正面响应。
  3. 客户端发出 MAIL FROM: <youraddress> 命令,并等待正面响应。 
  4. 客户端发出 RCPT TO: <gbush@whitehouse.gov> 命令,并等待正面响应。  负面响应意味着地址无效或存在其他错误,例如邮箱已满。
  5. 客户端发出 DATA: 命令,并等待正面响应。
  6. 客户端开始逐行发送实际电子邮件消息,并以 "." 行结束。  然后客户端等待一个正面响应,表明电子邮件已成功接收。
  7. 客户端发出 QUIT 命令,并等待正面响应。
  8. 客户端结束。

总而言之,smtp 客户端命令和 smtp 服务器响应如下发生:

SMTP 客户端命令 SMTP 服务器响应
  220 已连接到 Domain XXX,服务器就绪!
HELO 或 EHLO <客户端域名> 250 Hello Client Domain!
MAIL FROM: <您的电子邮件地址> 250 <address>.... 发件人 OK!
RCPT TO: <gbush@whitehouse.gov> 250 <address>.... 收件人 OK
DATA 354 开始输入邮件;以 <CRLF>.<CRLF> 结束
电子邮件消息  
. 250 消息已接收!
QUIT 221 关闭连接,再见!

请注意 SMTP 服务器如何使用数字响应代码来表示服务器响应。  这些代码控制客户端如何做出反应。  例如,当客户端发出 RCPT TO: 命令时,正面响应代码是 250,表示地址可接受。  然而,可能会发出负面响应代码,例如 550,表示“未知地址”。

此示例的要点在于说明基于状态的协议服务器和基于状态的协议客户端(如 SMTP)之间“紧密”的客户端/服务器状态机对话。  像 SMTP 这样的服务器有特定的 RFC 设计指南,描述了正确的状态机(命令和响应)。  FTP、NNTP 和 POP3 也是如此。

使用 CSPServer,您可以创建自己的客户端/服务器状态机对话。  您可以为您的特定客户端/服务器应用程序使用类似的响应代码概念。 

理解 CSPServer 状态机

下图 1.0 说明了 CSPServer 中的“状态机”

图 1.0
CSPServer 状态机

服务器应用程序
客户端连接监听线程

<--连接--

客户端应用程序

|
接受
|

实例化子类 CSPServer
对象线程

|

调用子类
Go() 处理程序

|

调用子类 SendWelcome() 处理程序

--响应-->

客户端等待欢迎响应

Command1() 处理程序

<-----------

command1

--响应-->

Command2() 处理程序

<-----------

command2

--响应-->

.
.

.
.

CommandN() 处理程序

<-----------

commandN

--响应-->

当客户端断开连接时,运行可选的子类 Cleanup() 处理程序

当客户端首次连接时,监听服务器将启动一个新的 CSPServer 会话,该会话将启动一个新的线程来管理客户端会话。  线程处理程序将调用子类的 Go() 处理程序。

子类的 Go() 处理程序可用于收集连接信息,但其主要目标是通过调用继承的 Go() 处理程序来启动状态机引擎。

继承的 Go() 处理程序将调用虚拟函数 "SendWelcome()" 并开始状态机。  SendWelcome() 重载为 SPS 提供了一个机会来介绍自己,并可能向客户端提供“就绪”信息。

使用 CSPServer 类

对于希望快速入门的用户,下面是“快速使用”的概述。  有关技术类或代码细节,请参阅提供的源代码和示例。

最低限度,要使用 CSPServer 类创建 SPS,您需要完成以下几项(顺序不限):

  1. 创建 CSPServer 的子类,
  2. 重载子类的构造函数,
  3. 重载子类中的 Go() 处理程序,
  4. 添加 TSPDispatch 成员变量,
  5. 创建 TSPDispatch 结构,定义状态机分派命令,
  6. 向子类添加命令分派处理程序,以及
  7. 创建监听服务器线程来处理传入的连接

您还可以重载其他虚拟函数,但构造函数和 Go() 是启动 CSPServer 引擎所需的唯一重载。 

SampleServer.cpp 源文件包含了一个完整的 SPS 工作示例。  以下是创建 SPS 的基本步骤:

步骤 1

在您的源代码中添加 #include <spserver.h>,并创建 CSPServer 的子类(例如 CMySPServer),如下所示。 

作为示例,我们将创建一个包含 5 个命令的状态机:“HELLO”、“LOGIN”、“SHOW”、“HELP”和“QUIT”。  因此,为每个命令添加一个处理程序。 

#include <spserver.h>

class CMySPServer: public CSPServer {
    typedef CSPServer inherited;
public:
    CMySPServer(CSocketIO *s);  // REQUIRED
protected:
    virtual void Go(); // REQUIRED
    virtual void SendWelcome();
private:
    static TSPDispatch Dispatch[]; // REQUIRED
    BOOL SPD_HELLO(char *args);
    BOOL SPD_LOGIN(char *args);
    BOOL SPD_SHOW(char *args);
    BOOL SPD_HELP(char *args);
    BOOL SPD_QUIT(char *args);
};

请注意,SendWelcome() 重载是可选的。  然而,在客户端首次连接到服务器时发送连接响应几乎总是必需的。 

CSocketIO 类是一个简单的套接字包装器,具有格式化函数和套接字输入循环缓冲区。  此类的文档不在本文的讨论范围内。  请参阅源文件 socketio.h/cpp 以获取用法和参考。 

第二步

为子类成员 Dispatch 创建 TSPDispatch 结构,声明命令和命令分派处理程序,如下所示:

CSPServer::TSPDispatch CMySPServer::Dispatch[] = {
  SPCMD(CMySPServer, "HELLO", SPD_HELLO),
  SPCMD(CMySPServer, "LOGIN", SPD_LOGIN),
  SPCMD(CMySPServer, "SHOW",  SPD_SHOW),
  SPCMD(CMySPServer, "HELP",  SPD_HELP),
  SPCMD(CMySPServer, "QUIT",  SPD_QUIT),
  {0}
};

对于 Dispatch 结构中的每个命令,使用以下原型在子类中声明一个分派处理程序:

BOOL dispatch_handler_name(char *args);

高级用法可以有一个单一的处理程序来调用所有命令。  在这种情况下,您可以使用 GetCurrentCommandName() 方法来返回当前发出的命令。 

步骤 3

现在开始实现重载和分派处理程序。 

//////////////////////////////////////////////////////////////
// Constructor

CMySPServer::CMySPServer(CSocketIO *s)
     : CSPServer(s, Dispatch)
{
    // Initialize all your session variables here.

    // Done is a special BOOL used to exit
    // the inherited::Go() handler. One of the
    // Dispatch commands should set Done = TRUE;
    // i.e., QUIT() command.

    Done = FALSE;

    // start thread, calls Go() handler. If you
    // wish, you can call Start() outside the constructor.

    Start();  
}

//////////////////////////////////////////////////////////////
// Go() is called by start()

void CMySPServer::Go()
{
    // By this point, we have a new thread running. 
    // This is a good point to collect client ip or
    // domain information.

    // To start the state machine, you must call
    // the inherited Go() function.  This will
    // starts the thread's socket command line
    // reader and dispatcher. Go() returns when 
    // the Done is set TRUE or if connection drops
    // or one of the dispatch handlers return FALSE.

    inherited::Go(); // REQUIRED

    // we are done, good place to do session
    // cleanup.

    delete this;  // REQUIRED
}

//////////////////////////////////////////////////////////////
// SendWelcome() is called by the inherited Go(). This is 
// a good place to provide "server readiness" information
// to the connecting client.  Standard SPS use numeric 
// response codes to provide this information. 

void CMySPServer::SendWelcome()
{
    Send("Hello! Server ready\r\n");
}

//////////////////////////////////////////////////////////////
// Dispatch handlers.  
//
// Dispatch handlers have one parameter, char *args. It will 
// contain the string, if any, passed with the command. 
// 
// Return TRUE to continue the state machine. If FALSE is 
// returned, the session ends. NOTE: Returning FALSE is not 
// a good idea in practical designs as it can put the remote 
// client in a irregular state. You should always have a 
// graceful way to complete a session.  Even if you wish to 
// show "error" conditions, you should always return TRUE.

BOOL CMySPServer::SPD_HELLO(char *args)
{
    Send("--> HELLO(%s)\r\n",args);
    return TRUE;
}

BOOL CMySPServer::SPD_LOGIN(char *args)
{
    Send("--> LOGIN(%s)\r\n",args);
    return TRUE;
}

BOOL CMySPServer::SPD_SHOW(char *args)
{
    Send("--> SHOW(%s)\r\n",args);
    return TRUE;
}

BOOL CMySPServer::SPD_HELP(char *args)
{
    Send("--- HELP commands ---\r\n");
    Send("HELLO\r\n");
    Send("LOGIN\r\n");
    Send("SHOW\r\n");
    Send("HELP\r\n");
    Send("QUIT\r\n");
    Send("--- end of help ---\r\n");
    return TRUE;
}

BOOL CMySPServer::SPD_QUIT(char *)
{
    Send("<CLICK> Bye!\r\n");
    Control->Shutdown();   // Disconnects socket
    Done = TRUE;           // Tells Go() to exit
    return TRUE;
}

步骤 4

最后,既然您已经准备好了一个 CSPServer 类,您就需要一个监听服务器线程来处理传入的套接字连接,并为每个新连接启动一个 CSPServer 实例。 

要创建监听服务器,将使用 CThread 类。

class CServerThread : public CThread {
    typedef CThread inherited;
public:
   CServerThread(const DWORD port = 4044, const DWORD flags = 0);
   virtual void Stop();
protected:
   virtual void Go();
private:
   SOCKET serverSock;
   DWORD serverPort;
};

子类 Go() 处理程序用于创建监听套接字服务器。 

当接受新连接时,会创建一个 CMySPServer 的新实例,将对等套接字句柄作为新的 CSocketIO 对象传递到 CMySPServer 构造函数中。  以下是在 CServerThread::Go() 处理程序中完成的:

  .
  .
  SOCKET t = accept(serverSock, (sockaddr *)&src, &x);
  if (serverSock == INVALID_SOCKET) break; // listening server broken
  new CMySPServer(new CSocketIO(t));       // Start new CMySPServer session
  .
  . 

您无需担心释放对象。  类本身将负责清理。 

在控制台应用程序中使用 CServerThread 的示例用法

  CServerThread server(4044);

  while (!Abort) {
    if (kbhit() && getch() == 27) break;  // Escape to Exit
    Sleep(30);
  }

  server.Stop();

关注点

请参阅 SampleServer.cpp 源文件以获取完整的可运行示例。  默认情况下,示例使用端口 4044。   要测试服务器,可以使用 telnet 如下:

    Telnet LocalHost 4044

历史

  • v1.0P - 2003 年 3 月 4 日,首次公开发布
© . All rights reserved.