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

Asyncsocket 第三部分:服务器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.59/5 (9投票s)

2013年3月24日

CPOL

15分钟阅读

viewsIcon

69406

downloadIcon

1345

在TCP/IP应用程序中使用Windows类CAsynSocket

下载说明

Visual Studio 解决方案文件大小为 22 MB,但在论坛上各位的帮助下,现在只有 246 KB;

全部四篇文章

这是四篇文章的简要摘要和各自的链接。我已经完成了四篇关于异步TCP/IP和Microsoft的ASyncSocket类的文章。第一部分描述了必要的概念。第二部分描述了一个包含服务器和客户端的单个项目,都在同一个对话框中。用户可以一步一步地浏览一个事务。在第三和第四部分中,服务器和客户端被分成了两个项目,位于同一个解决方案中。服务器和客户端可以运行在不同的计算机上。该项目引入了单个解决方案中多个项目的概念。它引入了使用来自独立目录的源代码的概念。如果你不熟悉TCP/IP和ASyncSocket,前两篇文章是必读的。如果你没有处理过单个解决方案中的多个项目,或者不熟悉附加包含目录,那么第三部分是必读的。如果这句话看起来很奇怪,请阅读第三部分。

以下是每篇文章以及两个对本文至关重要的编程技巧的链接。

引言

本文展示了如何在实际应用程序中使用MFC类CAsyncSocket。它介绍了一些因异步操作而产生的问题。它演示了公共代码目录/库的使用,以及单个解决方案中多个项目的使用。

请注意,我仍然认为自己在编写TCP/IP操作和CAsyncSocket类的代码方面是一名新手。这个演示应用程序的复杂程度满足我的工作需求。我编写这个应用程序是为了学习,然后写这些文章是为了记住我做了什么以及为什么,也是因为我找不到如此简单且完整的文章。还有一些文章创建了可能被称为重量级或全面的类。我想要一个尽可能简单的。

如果您发现重大缺陷或可以使操作更简单优雅,请随时提出。请写下来并发布您的改进。

必备组件

本文是为那些对TCP/IP有基本了解但没有实际编写此类代码经验的读者准备的。读者应该能够使用Visual Studio、MFC和C++来编写程序以完成简单到中等复杂度的任务。

环境

Windows 7, Visual Studio 2008, MFC, C++

引言

在本系列四篇文章的第一篇中,我描述了ASycnSocket类的核心方法及其功能。第二篇文章在一个简化的工作环境中展示了它们。该环境进行了相当充分的检测,以显示ASyncsocket的运行方式。但是,服务器和客户端都在同一个项目中,并且所有操作都由用户启动。

与前两篇文章的代码不同,在这两篇文章中,服务器和客户端是两个独立的exe文件,并且可以运行在不同的计算机上。这比在同一主机上运行两者提供了更真实的测试环境。

第四部分讨论客户端,可在这里找到。

发送方困难

使用TCP/IP进行多台计算机之间通信的应用程序可以分为几类。那些基于用户输入和控制进行数据发送和接收的应用程序,数据速率相对较低。在TCP/IP层面很少出现带宽问题。许多应用程序,如文件传输或下载,“偏好”高带宽。这些应用程序可以在几个地方进行节流,而不会产生除完成任务所需时间之外的任何不良影响。例如,在发送大文件时,应用程序可以在发送端或接收端的慢速硬盘上轻松等待,而不会产生任何不良影响。

我的应用程序是遥测数据。数据以特定的速率到达,无论应用程序是否准备好。如果TCP/IP实用程序在数据到达时未准备好发送,则必须将数据缓冲在某个地方。这就是我开始了解返回码WSAEWOULDBLOCK的原因。当ASyncSocket方法Send()生成该错误时,应用程序必须能够缓冲数据,直到调用ASyncSocket方法OnSend()

当应用程序每毫秒只发送两三个消息时,一切正常。当速率增加时,Send()方法开始返回WSAEWOULDBLOCK值。该错误代码有两种含义。应用程序必须停止发送数据,并且,最后一次发送未成功。最后一次发送尝试的数据必须保存,以便稍后再次发送。本文介绍了一种用于缓冲数据的方法。

客户端困难

操作系统(在本文中,“操作系统”一词包括主机计算机(们)中除我们的应用程序之外的所有内容)有权将两个或多个有效载荷数据包合并成一个TCP/IP数据包,并且有权将有效载荷数据包拆分到TCP/IP数据包中。客户端必须重新组合拆分的包。一个实现该功能的实现在客户端。客户端还检查有效载荷数据包,以确保没有丢失。在有效载荷数据中添加检查不是必需的,但我强烈建议这样做。我建议程序员设计有效载荷数据包结构以提供该能力。如果此描述让您产生疑问,请放心,在描述客户端时将提供答案。

理论上TCP/IP永远不会丢失数据包。当我们隔离并仅考虑TCP/IP部分时,这可能是正确的。但当考虑整个应用程序时,有效载荷可能会丢失。创建这些类的应用程序是遥测数据。当通过返回值WSAEWOULDBLOCK暂停发送时,数据仍在不断到来。(我稍后会解释)服务器必须能够处理这种情况。

免费赠品

另外两个主题之所以包含进来,仅仅因为我需要它们。它们是如何创建和使用共享或公共代码的目录,以及如何在一个解决方案中创建和测试多个项目。

注意

我的具体应用程序永远不会在美国以外使用。我不想处理Unicode带来的额外复杂性,并禁用它用于我所做的一切。为了获得无错误编译,请禁用Unicode。如果您想要Unicode,则需要进行一些更改。如果您包含了一种启用这些类以便在有或没有Unicode的情况下进行编译的方法,并且这些方法不是太麻烦,请通知我,我可能会将其包含在内。

公共代码

解决方案包含两个项目,一个服务器和一个客户端。但是,服务器和客户端的规定项目实际上仅仅是测试文章,目的是开发和测试实际代码,这些代码将放入其他应用程序中。有一种方法可以在不复制代码到每个新项目的情况下做到这一点。这比预期的要容易。

zip文件包含两个目录,解决方案目录和另一个名为COMMON_CODE的目录。在公共目录中是类所需的* .H*和* .CPP*文件:Log_WriterClientServer_ManagerServer_Sender。将这个COMMON_CODE目录放在某个位置,然后将解决方案目录放在另一个位置。

第一步是告诉VS(Visual Studio)存在包含代码的附加目录。从解决方案资源管理器开始,右键单击您的项目。请记住,在此解决方案中有两个项目。然后选择配置属性 -> C/C++ -> 常规。在“附加包含目录”字段中,添加指向公共代码目录的路径。

注意

使用附加包含目录存在一个相当大的困难。我在这个网站上写了一篇关于此的编程技巧,位于此处。

在您理解Visual Studio的这一特性之前,该项目**将不会**成功构建。如果构建错误提到找不到包含文件,那么答案**几乎可以肯定**可以在该编程技巧中找到。

注意:对于此版本的下载zip文件,我将所有内容压缩并放在**C:\TEMP**。在完成了上述附加目录练习并获得干净构建后,我运行了批处理文件**clean.bat**(包含在zip文件中)并压缩了结果。zip文件现在缩小到246 KB。
然后删除了zip文件之外的所有内容,解压缩了zip文件,并得到了一个干净的构建。**所以**,希望读者能够将zip文件下载到**C:\TEMP**并获得良好的构建。当您将解决方案或COMMON_CODE目录移动到其他任何地方时,您必须进行编程技巧的练习,即上面提供的链接。
如果有人这样做并发布结果,我将不胜感激。

多个项目

我的下一个小任务是让单个解决方案中有多个项目。这也比预期的容易。我也写了一篇关于此主题的技巧,位于此处。
此过程比包含目录的技巧更直接,但阅读它也可以为您节省一些麻烦。

开始吧

将项目下载到目录 C:\TEMP,解压缩,然后在此处阅读该编程技巧
在Visual Studio中打开解决方案,您就可以开始了。

项目准备好后,右键单击解决方案资源管理器中的解决方案行,然后选择“设置启动项目”。该对话框控制您按F5键或单击运行按钮时如何启动每个项目。单击“操作”列以获取一些启动选项。在继续项目之前,请浏览该对话框和各种选项。四处浏览比描述所有内容更容易。服务器项目:下载整个项目,然后在解决方案资源管理器中关闭所有展开/资源管理器框,使解决方案资源管理器中只剩下三行。它看起来像这样

有两个项目,并且构建将编译并链接两者。每个项目都有自己的子目录和可执行文件(调试和发布)。当两者都设置为启动时,每个项目都有自己的对话框。TCP/IP服务器和客户端必须满足彼此的期望。在单个项目中一起开发和协调这两个项目比单独的解决方案更容易。

展开服务器项目,解决方案资源管理器应如下所示

四处浏览一下,熟悉所有文件。

日志写入器

在深入研究服务器之前,先看一下实用程序C_Log_Writer。它的目的是提供一个对整个项目通用的类,可以用来记录任何可能需要的信息。阅读时请查看.H文件。“完整”构造函数(阅读注释)接受两个参数:目录名称和前缀字符串。日志文件将打开在您在第一个参数中指定的目录中:new_directory_name。打开文件时,实用程序从操作系统获取当前时间,然后构建一个由当前年、月、日、小时、分钟和秒组成的文本字符串。该字符串以第二个参数:new_name_prefix为前缀。此解决方案包含两个同时运行的项目。当服务器创建日志写入器时,前缀是TCP_Server。在客户端,前缀是TCP_Client。如果您将此用于其他项目,请更改第二个参数的值。

接下来是更改目录和前缀字符串的两个方法。很简单。Open_Log_File()方法相当明显。Re_Open_Log_File()可以在任何您可能想重新启动日志文件并将某个事件放在文件顶部的位置使用。如果您的应用程序有GUI,请添加一个按钮来重新启动日志文件。向下滚动一点到Write_Log_File_Entry()并转到定义。该方法获取当前时间并为每个日志条目构建一个前缀,显示时间(精确到毫秒)。然后,它会将用户提供的文本添加到该字符串中,构成完整的条目并将其写入文件。再往下看,可以找到

if( ( m_log_entry_count ++ ) > LOG_FILE_RESTART_COUNT )
{
    m_log_entry_count = 0;
    Re_Open_Log_File();
}

每次写入日志文件都会使计数器递增。当计数超过**LOG_FILE_RESTART_COUNT**时,日志文件将被关闭,并打开一个新的日志文件。请随意更改该值以满足您的需求。希望该类的其余部分是显而易见的。

格式化WSA文本

查找WSAGetLastErrorGetLastError时,我发现有一个格式化程序可以获取有关各种错误代码的文本说明。我发现编写一个简单的类来提供我的应用程序可能需要获取的错误的文本更容易。该方法是C_TCP_Format_WSA_Text**.>.** 它很简单。

项目服务器对话框

该项目是解决方案服务器端的一对项目。请记住,它的目的仅仅是测试服务器的两个类C_TCP_Server_ManagerC_TCP_Server_Sender。如果您使用这些类,请使用公共代码目录中的代码,而忽略这两个对话框项目。

请注意

C_TCP_Server_Manager是此解决方案服务器部分的核心。它管理TCP连接。创建新连接时,首先创建它。我将称它为**服务器管理器**,或者仅仅是**管理器**。**类**C_TCP_Server_Sender由管理器创建,用于将数据发送到客户端。提醒读者,我的应用程序只需要将数据发送到客户端。它不需要接收数据。我将称此类为**服务器发送器**或仅仅是**发送器**。

服务器启动后,对话框将如下所示

有一个用于启动服务器的按钮和一个用于停止它的按钮。**发送一次突发**按钮发送一小段有效载荷数据包。各种文本字段从左侧开始描述。**端口号**指示服务器用于监听的端口号。它由代码中的一个常量控制。**指针**是指向**C_Server_Manager**对象的指针。它将是**未知**或**有效**。服务器有一个FSM(有限状态机)来控制其操作,**管理器状态**显示该FSM的当前状态。显示管理器状态对于开发很有帮助。

在下方有一系列文本字段,用于显示**On*()**方法被调用的次数。这些方法在前两篇文章中有描述。

C_TCP_Server_Manager由服务器项目对话框创建。它监听客户端的连接,然后创建类C_TCP_Server_Sender以继续与客户端的通信。前两篇文章讨论了该概念。使用本文顶部的链接可以找到这些文章。本文跳过了所有理论,现在我们进入本文的目的,即使用CAsyncSocket类在工作项目中所需的附加行为。

入门

启动服务器应用程序(项目)后,用户单击“**启动TCP服务器**”按钮。这会导致服务器对象被创建。启动时,服务器管理器开始监听客户端的连接。此操作独立于主应用程序。当客户端连接时,服务器管理器会创建服务器发送器。该发送器的指针必须传递给主应用程序。

在此演示项目中,主应用程序就是对话框本身。查看Project_Server_Dlg.cpp文件中的OnTimer()方法。在顶部附近有

// The client can exit at any time, causing object
// C_TCP_Server_Sender to be deleted. Always check
// to see if a new sender object has been created.
 
if( mp_C_TCP_Server_Sender == NULL && mp_C_TCP_Server_Manager != NULL )
{
  mp_C_TCP_Server_Sender =  mp_C_TCP_Server_Manager-> Get_Sender_Pointer();
  m_send_data_status = READY_TO_SEND;
}

如果发送器指针为空,则查询管理器。当发送器被创建后,会将发送器的指针返回。获得该指针后,主应用程序就可以开始发送数据了。

客户端已关闭连接

客户端可以随时关闭连接。查看OnTimer()的底部找到

if( m_send_data_status == SENDER_READY_TO_EXIT )
{
  if( mp_C_TCP_Server_Sender != NULL )
  {
     m_send_data_status = SENDER_HAS_EXITED;
     delete mp_C_TCP_Server_Sender;
     mp_C_TCP_Server_Sender = NULL;
  }
}

当发送器返回值SENDER_READY_TO_EXIT时,发送器会告诉主应用程序它必须被删除。主应用程序执行此操作,并将指针重新设置为NULL

要查看事件链是如何启动的,请查看服务器发送器的代码和OnClose()方法。

CAsyncSocket::OnClose(nErrorCode);
// main app will be notified next time it
// attempts to send data.
m_tcp_state = SENDER_MUST_EXIT;

这只是将发送器的内部状态设置为必须退出。下一次调用Class_Send()方法时

// If we must exit, don't attempt to send, just notify the main app.
if( m_tcp_state == SENDER_MUST_EXIT )
    return SENDER_READY_TO_EXIT;

它只是返回相应的值。这会通知主应用程序,然后主应用程序会删除Sender。请注意,对于Sender对象的每个状态都有一个枚举

// The state of the TCP sender function.
enum C_TCP_TD_TCP_STATE
{
   SEND_ENDIAN,
   SEND_FRAME,
   SEND_DATA,
   SENDER_MUST_EXIT
};

以及返回给主应用程序的值的单独枚举

enum C_TCP_TD_SEND_STATUS
{
   READY_TO_SEND,
   SEND_OKAY,
   SEND_FAIL,
   SEND_BLOCKED,
   SENDER_READY_TO_EXIT,
   SENDER_HAS_EXITED,
   SENDER_NOT_READY,
   OTHER_ERROR
};

在继续之前,请思考一下这些枚举。

客户端发送到服务器

此时,我们已经查看了主应用程序启动TCP管理器以及它启动Sender对象所需的代码。Sender的指针被传递给主应用程序,主应用程序可以开始发送数据。当客户端关闭连接时,Sender会通知主应用程序,然后主应用程序删除Sender。

然而,除非Sender已经在套接字上发布了读取操作,否则Sender无法检测到客户端已关闭连接。我的应用程序不需要从客户端读取数据,但为了完整起见,已创建了一个返回路径。由于不需要从客户端向服务器发送数据,因此我选择提供一种方法来控制从服务器到客户端发送的数据量。启动该事务的机制在关于客户端的文章中进行了讨论。目前,我们将在主对话框和OnTimer()方法中查看此数据的接收。

if( mp_C_TCP_Server_Sender != NULL )
{
  int received_count = 0;
  received_count = mp_C_TCP_Server_Sender->Class_Receive(
     &receive_packet.both.char_format[0],
     C_IADS_SIZE_OF_PAYLOAD_PACKET );
    
  if(received_count >= 0 )
  {
     unsigned int received_word = receive_packet.both.iads_format.header.packet_size;

     // check out the packet and if verified, set the new send count
     if( received_count = C_IADS_ONE_PARAMETER_PACKET_SIZE )
     {
        int new_count = receive_packet.both.iads_format.body[0].value.ui;
        if( new_count >= 0 && new_count < 100 )
        {
           m_packet_send_count = new_count;
        }
     }
     // now clear it out so it does not get used again
     memset( &receive_packet, 0, C_IADS_ONE_PARAMETER_PACKET_SIZE );

  }
}  // end of: if( mp_C_TCP_Server_Sender != NULL )

主应用程序向Sender请求来自客户端的任何已接收数据。当它确实接收到数据时,通过检查接收到的数据包的大小来验证接收到的有效载荷数据包。接收到的值加载到成员变量**m_packet_send_count**中。查看**OnTimer()**的顶部,可以看到这控制着。

历史

  • 2013年3月,首次提交。
  • 2013年5月26日,更新了zip文件,减小了大小,添加了公共代码,添加了编程技巧链接。
© . All rights reserved.