异步 TCP - 第 2 部分
目标:
必备组件
阅读第一部分。这是大写并带有强调的。这已经够长了,所以我必须假设读者已经阅读了第一部分。
读者应了解如何创建 MFC 项目以及 TCP/IP 操作的基础知识。(什么是 TCP/IP 及其目的,客户端/服务器角色是什么,IP 地址、端口等。)读者应该是一名有经验的 Visual Studio 初学者,或者可能是中级的 VS 用户或更高级的用户,对对话框及其控件充满信心,但应是异步 TCP/IP 操作的新手。
全部四篇文章
晚些补充:这是所有四篇文章的简短摘要以及每篇文章的链接。
我已完成四篇关于异步 TCP/IP 和 Microsoft 类 ASyncSocket
的文章。第一部分描述了必要概念。第二部分描述了一个在单个项目中包含服务器和客户端以及单个对话框的项目。用户可以一步一步地完成一个事务。在第三部分和第四部分中,Server
和 Client
被分离成一个解决方案中的两个项目。Server
和 Client
可以在不同的计算机上运行。该项目引入了单个解决方案中多个项目的概念。它引入了从单独目录使用源代码的概念。如果您不熟悉 TCP/IP 和 ASyncSocket
,前两篇文章是必读的。如果您没有处理过一个解决方案中的多个项目,或者没有处理过**附加包含目录**,那么第三部分是必读的。如果这句话看起来很奇怪,请阅读第三部分。
以下是每篇文章的链接
引言
这是两部分教程文章的第二部分。第一部分描述了如何使用 Windows 类 CAsyncSocket
来实现异步 TCP/IP 接口的概念。它尽可能地避免了代码。在阅读本文之前,必须阅读那篇文章。它可以在这里找到。
本文介绍了 CAsyncSocket
类的使用。该应用程序的目的是提供一个应用程序来容纳和演练 CAsyncSocket
类。为了能够看到对话框中的所有交互,它最终有六十多个控件,还可以有更多。描述如此多的控件以及驱动它们的代码对于一篇关于 CAsyncSocket
的文章来说太多了。本文描述了三个工作类,并省略了支持代码。
使用 Windows 和 CAsyncSocket
发起和进行 TCP/IP 对话需要三个类。在此应用程序中,它们是 C_Server
、C_Client
和 C_Server_Send_Time_Socket
。类 C_Server
包含一个 Accept()
方法、一个 CAsyncSocket::Accept()
方法、一个 OnAccept
方法和一个 CAsyncSocket::OnAccept()
方法。为了确保名称与基类的名称完全清晰,C_Server
中可能与基类方法混淆的方法都带有前缀 Class_
。其他两个类也同样如此。
在阅读了第一部分之后,我们直接进入 C_Server
类。
Class C_Server::Class_Initialize()
C_Server
类中有三个关键方法。从 initialize
方法开始。
bool C_Server::Class_Initialize()
{
m_winsock_status = AfxSocketInit();
m_winsock_status == 0 ? m_method_status = false : m_method_status = true;
if( m_method_status )
{
m_winsock_status = CAsyncSocket::Create(
m_port_number,
SOCK_STREAM,
FD_READ | FD_WRITE | FD_OOB | FD_ACCEPT | FD_CONNECT | FD_CLOSE,
DEFAULT_IP_ADDRESS );
m_wsa_error = WSAGetLastError();
m_winsock_status == 0 ? m_method_status = false : m_method_status = true;
}
return m_method_status;
}
此方法中只有两行工作代码,其余均为支持代码。
m_winsock_status = AfxSocketInit();
m_winsock_status = CAsyncSocket::Create(
m_port_number,
SOCK_STREAM,
FD_READ | FD_WRITE | FD_OOB | FD_ACCEPT | FD_CONNECT | FD_CLOSE,
DEFAULT_IP_ADDRESS );
第一行告诉 Windows 执行大量用于套接字操作的初始化。第二行创建套接字。几乎所有工作都由基类完成,只留下高级调用。
请注意对 WSAGetLastError()
的调用,以检查问题。这必须在调用基类后立即完成。如果您是 Windows 中 TCP/IP 的新手,WSA 错误代码值得花一些时间在 Google 上搜索。在以下讨论中,将不描述支持代码。
Class C_Server::Class_Listen()
在初始化套接字操作后,下一步是告诉 Windows 开始侦听将请求 TCP/IP 连接的客户端。
m_winsock_status = Listen();
这是此方法中唯一的实际代码。返回后,Windows 已完成 TCP/IP 初始化,并准备好接收客户端。
这是基类 CAsyncSocket
使我们能够从简单的阻塞 TCP/IP 操作中脱颖而出的地方。在同步操作中,Listen()
的调用要等到 Windows 收到客户端请求后才会返回。在异步操作中,Listen()
会返回一个状态码,允许应用程序在 Windows 等待客户端连接时执行其他操作。
Class C_Server::Class_Accept()
这是关键方法。同样,只有两行实际代码。
mp_C_Server_Send_Time_Socket = new C_Server_Send_Time_Socket;
m_winsock_status = CAsyncSocket::Accept( *mp_C_Server_Send_Time_Socket );
首先是创建一个 C_Server_Send_Time_Socket
实例。这是将与客户端进行所有通信的对象。它为每个特定应用程序定制。
非常重要的一段
接下来,告诉 Windows 此对象用于与客户端通信。为此,我们调用基类的 Accept()
方法,并将新对象作为唯一参数传递。基类和 Windows 管理所有详细信息。处理与客户端的所有通信的责任 hereby 分配给新对象。第二行代码如此简单,但又是此过程的关键,您应该花一些时间仔细考虑这一点,然后再继续。
如果您的应用程序期望单个客户端,这将对您有效。**然而**,这是一个很大的“但是”,如果您将接受多个客户端连接,C_Server
类将需要对它创建的新对象的指针做一些事情,并且需要代码来回收并准备好接受下一个连接。为简单起见,此应用程序将只处理一个客户端。
就是这样。比我预期的要简单得多。
Class C_Server::Class_Close()
Close()
操作相当直接。
Class_C_Server::On*()
在第一部分引用的 CodeProject 文章中以及其他地方,我找到了基类中需要重写的方法列表。这是它们在 .h 文件中的声明。
virtual void OnAccept( int nErrorCode );
virtual void OnClose( int nErrorCode );
virtual void OnConnect( int nErrorCode );
virtual void OnOutOfBandData( int nErrorCode );
virtual void OnReceive( int nErrorCode );
virtual void OnSend( int nErrorCode );
对于这个非常简单的演示应用程序,定义非常简单。这个是典型的。
void C_Server::OnAccept(int nErrorCode)
{
m_server_on_accept_count ++;
mp_main_dialog->Set_Server_On_Call_Counts( m_server_on_accept_count,
m_server_on_close_count,
m_server_on_connect_count,
m_server_on_out_of_band_count,
m_server_on_receive_count,
m_server_on_send_count );
if(nErrorCode==0)
{
CAsyncSocket::OnAccept(nErrorCode);
}
}
每个类中的所有 On*()
方法都有一个计数器,该计数器在每次进入时增加,并调用主对话框的方法,以便立即显示调用次数。所有类都有相同的声明和基本相同的定义。
读者可以从这个演示应用程序开始,然后添加复杂性,看看每个方法何时被调用,或者是否被调用。
C_Server_Send_Time_Socket::Class_Send()
如前所述,该类由 C_Server::Class_Accept()
方法创建。Windows 和基类管理所有详细信息。在将对象作为参数交给基类后,该对象就可以处理与客户端的所有通信。
int C_Server_Send_Time_Socket::Class_Send( )
{
int chars_sent = 0;
int size = sizeof( m_current_time );
GetSystemTime( &m_current_time );
chars_sent = CAsyncSocket::Send( (const void *) &m_current_time, size, 0 );
return chars_sent;
}
此方法获取当前系统时间,然后告诉基类发送它。发送完整的结构给客户端使其变得简单。
查看该类的其余方法,您会发现所有剩余的代码都是演示应用程序的支持代码。核心发送数据功能不需要这些。
同样,在这个简单的应用程序中,就只有这些了。
C_Client::Class_Initialize()
客户端初始化相当简单。
m_winsock_status = AfxSocketInit( NULL );
m_winsock_status = Create();
同样,只有两行实际代码。
C_Client::Class_Connect()
这是我们的“工蜂”。
<span style="font-size: 10pt; font-family: "Courier New";">m_winsock_status = CAsyncSocket::Connect( m_ip_address, m_port_number );</span>
当调用此方法时,基类会访问 Windows 及其 API,并发出一个搜索信号来查找服务器。这会引发几个事件。在 C_Server
中,会调用 OnAccept()
方法。这会告诉 C_Server
对象它现在有一个活动的客户端。服务器通过网络响应,C_Client
会收到服务器已找到的消息。MFC 应用程序检测到此消息,并知道要调用客户端方法 OnConnect()
。当您运行应用程序时,该计数器将增加到 1
。您可以在此方法中编写代码来响应 connect
事件。
所有这些在 Windows 级别的交互都会导致调用客户端的 OnSend()
。虽然客户端方法 OnConnect()
表示我们已连接,但 OnSend()
表示我们现在可以向服务器发送数据了。
如前所述,此应用程序足够简单,除了计数之外,所有 On*()
方法调用都被忽略了。在此应用程序中,用户和键盘闭合了回路。在实际应用程序中,将会有代码来闭合回路并使其全部正常工作。
在标准代码中,这是一个阻塞调用,应用程序会等待直到找到服务器。CAsyncSocket
类允许客户端应用程序在等待响应时处理其他事务。
C_Client::Class_Receive()
同样,这几乎太简单了。
int chars_received = 0;
int size = sizeof( m_current_time );
chars_received = CAsyncSocket::Receive( (void *) &m_current_time, size, 0 );
客户端有一个与服务器完全相同的时间结构,并简单地将数据读取到该结构中。为简单起见,显示时间的代码故意留在此方法之外。请查看方法 SYSTEMTIME
C_Client::Class_Get_Time()
及其调用者,了解如何处理此问题。
结论
考虑到首次将这些内容组合在一起的长度和难度,实际操作相当简单。(认识到在拥有一个可工作的 TCP/IP 真实应用程序之前还有很多工作要做。)
快速回顾一下。
- 初始化
C_Server
。 - 启动服务器**侦听**模式,指示 Windows 侦听客户端。
- 初始化
C_Client
。 - 启动
connect
方法,指示 Windows 查找并连接到服务器。 - 在
Server
中,**接受**连接并创建C_Server_Send_Time_Socket
类与客户端通信。 - 使用
C_Server_Send_Time_Socket
向客户端发送信息。 - 在
Client
中,接收信息。
请注意,这里有七个步骤,应用程序中有七个按钮。它们是一一对应的。我建议您在每个类的每个方法中设置一个断点,然后从**1:Initialize** 开始,看看您到达了哪里。(返回第一部分进行演练。)到达时删除每个断点,然后单步执行代码,直到返回到 Windows MFC 应用程序代码(您未编写的部分)。然后点击调试器中的 Continue,检查结果,然后继续下一个按钮。返回第一部分以获得更好的这些活动的演练。
重申一下早些时候的评论,当您将应用程序对话框放在一个显示器上,同时在另一个显示器上通过 Visual Studio 单步调试时,效果会好得多。