Asyncsocket - 第 4 部分: 客户端
Microsoft 类 ASyncSocket。
引言
第四部分讨论了 ASyncSocket
系列文章的客户端部分。我的应用程序将数据发送给供应商的应用程序。我们只需要服务器端。然而,当服务器与供应商应用程序配合不佳时,就必须编写一个客户端来了解服务器端出了什么问题。本文描述了这个客户端。服务器在第 3 部分介绍。
全部四篇文章
这是对所有四篇文章的简短总结以及每篇文章的链接。
我完成了四篇关于异步 TCP/IP 和 Microsoft 的 ASyncSocket
类的文章。第一部分描述了必要的概念。第二部分描述了一个包含服务器和客户端的单个项目,以及单个对话框。用户可以一步一步地进行事务处理。在第 3 部分和第 4 部分中,Server
和 Client
被分成两个项目,位于一个解决方案中。Server
和 Client
可以在不同的计算机上运行。该项目介绍了单个解决方案中多个项目的概念。它介绍了使用来自不同目录的源代码的概念。如果您不熟悉 TCP/IP 和 ASyncSocket
,那么前两篇文章是必读的。如果您没有处理过一个解决方案中的多个项目,或者没有使用过附加包含目录,那么第 3 部分是必读的。如果这句话看起来很奇怪,请阅读第 3 部分。下面是每篇文章的链接:
背景
在我最初接触 TCP/IP 代码和 Microsoft 的 CAsyncSocket
类时,我写了两篇文章并将它们上传到了 Code Project。它们描述了如何使用 CAsyncSocket
的基本知识。您可以使用这些链接找到第 1 部分和第 2 部分。此链接指向第 3 部分。
注意:服务器运行不需要 IP 地址。客户端需要服务器的地址。此应用程序是在我的台式机上在家开发的。LAN 由无线路由器和交换机组合、台式计算机和笔记本计算机组成。台式机和笔记本计算机的 IP 地址由我的 ISP 设置。通常台式机获得 IP 地址 192.168.2.2,笔记本电脑获得 .4。有时不是。查看 C_TCP_Constants.h 以找到以下行:
// Default IP address
//const char C_TCP_DEFAULT_IP_ADDRESS[] = ("127.0.0.1");
const char C_TCP_DEFAULT_IP_ADDRESS[] = ("192.168.2.3");
127 地址是环回地址,工作良好。但是,它不会强调网络,也不会像有时那样引起阻塞错误。使用 192 地址时,您可能会发现您的代码昨天还能工作,今天却不行。您的计算机可能启动时地址为 192.168.2.2 或 .4,而不是 .3。只需更改常量即可继续。随意在对话框中添加一些代码来简化此过程。如果您喜欢冒险,可以编写代码来查找地址并在服务器和客户端的对话框中显示它。我已经达到了并超过了这个项目的最大需求,并且正在继续前进。
注意:如果您不确定 127 和 198 地址的意义,在继续阅读本文之前,您将受益于一些关于网络总体知识的背景阅读。
Using the Code
以 Release 模式构建解决方案。将其中一个 EXE 文件复制到另一台计算机并启动两者。先启动服务器,然后启动客户端。在客户端,选择连接。然后选择每个服务器计时器周期要发送的消息数量,然后选择发送计数。服务器将开始向客户端发送消息。观察各种状态字段,以了解这两个项目是如何运行的。
如果无法正常工作,请检查服务器的 IP 地址,并确保客户端与之匹配。
项目客户端
这是客户端项目的顶层。它的唯一目的是驱动和测试 C_TCP_Client
类。此解决方案中的此项目是您项目的替身。使用它来测试和改进客户端实用程序,然后自信地将客户端合并到您的实际项目中。
对话框
按钮启动客户端会创建 C_TCP_Client
类的对象,但不会指示它执行任何操作。各种状态值将填充,它会等待您准备好连接。按钮连接是激动人心的部分。当然,服务器必须已经启动并正在监听客户端连接。
端口号字段应该很明显。当客户端启动后,客户端指针字段将仅仅表明指针有效。
在此下方是每个 On*()
方法的计数,即每个方法被调用的次数。
向右移动到复选框验证有效负载数据包。当未选中时,此客户端仅仅是服务器的数据接收器。它会吞噬有效负载数据包,允许服务器发送数据。选中时,客户端会检查有效负载数据包。它会检查它们的格式和序列号是否正确。在此下方还有几个用于记录数据的复选框。日志文件在开发和测试代码时可以提供极大的帮助。
既然已经提到了验证选项,请查看左下角的三个字段。此解决方案中的有效负载数据包有一个序列计数器,每个有效负载数据包递增一。序列计数器可以包含预期值,也可以不包含。如果不包含,则表示有问题,某个有效负载数据包已丢失。有可能用过多的数据使服务器应用程序过载并导致数据包丢失。但这又是另一个话题了。
服务器端的操作系统可以将两个或多个有效负载数据包合并到一个 TCP/IP 数据包中。并且,它可以将一个有效负载数据包拆分成两个或多个 TCP/IP 数据包。有效负载拆分计数字段显示了有多少个有效负载数据包被拆分到了两个 TCP/IP 数据包中。
移动到右侧的列表框。启动时,服务器的发送计数为零。它执行所有正确的步骤,但每个计时器中断发送零个数据包。在列表框中选择一个值,然后单击发送计数。这会告诉服务器每个计时器中断发送多少个有效负载数据包。提供的服务器以 200 毫秒的中断速率运行。当您在列表框中选择 10 时,结果是每 200 毫秒爆发十个数据包。它们不是均匀分布的,而是成批发送的。
客户端项目
这个客户端有点不寻常。由于我的需求性质,这个演示客户端从未需要主动执行读取操作。当服务器发送数据包,并且客户端的操作系统接收到它时,操作系统会调用 OnReceive()
方法。这就是有数据要从操作系统获取的指示。看看 C_TCP_Client
中的 OnReceive()
方法。
m_on_receive_count ++;
m_receive_byte_count = CAsyncSocket::Receive(
m_receive_buffer.char_array,
C_TCP_MAX_RECEIVE_SIZE, 0 );
m_wsa_error_code = WSAGetLastError( );
第二行本身足以用于服务器的基本测试实用程序。它将作为数据接收器,并允许服务器继续发送数据。在查看此短方法底部之前,请对此进行一些思考。
if( m_data_valadation )
{
Validate_Received_Data( );
}
如果对话框中设置了验证数据的复选框,则会调用 validate
方法。如果没有,数据将被丢弃,OnReceive()
方法退出。客户端代码可以非常简单。所有对话框所做的就是启动客户端,然后轮询其状态。
验证收到的数据
另一方面,validate
方法有一项艰巨的任务。
如果您希望将其制成一个功能齐全的客户端实用程序,那么您必须理解并修改以下关键部分以适应您的需求。对我来说,这只是验证了服务器数据包,并证明了所有数据包都已完好发送。对于一个功能齐全的客户端,这应该被修改为从 TCP/IP 数据包中提取单个有效负载数据包,在流量大的情况下,这些数据包可能包含多个有效负载数据包。我建议您修改此部分,将有效负载数据包放入缓冲区,并告知您的应用程序有多少个数据包等待处理。然后添加一个方法,以便您的应用程序可以一次获取一个有效负载数据包。将所有复杂性保留在此类中,以便您的应用程序只需请求下一个有效负载数据包。
继续描述
考虑这些问题。任何有效负载数据包都可能被拆分成两个或多个 TCP 数据包。任何 TCP 数据包都可能包含两个或多个有效负载数据包。事实上,根据大小,一个 TCP/IP 数据包可能包含两个半有效负载数据包。最后,请记住用于启用验证的复选框?当启用验证时,第一个 TCP/IP 数据包可能以有效负载数据包的第一个字节、第二个字节或任何位置开始,直到有效负载数据包的最后一个字节。实际上,它不会只获得一两个字节的有效负载数据包,而是介于两者之间。
void C_TCP_Client::Set_Data_Validation_State( bool new_value )
{
m_data_valadation = new_value;
m_carry_over_bytes = 0;
m_payload_packet_count = 0
}
去除冗余部分后,我们只剩下这些。第一行将标志设置为 true
或 false
以启用或禁用验证。清除 m_carry_over_bytes
,因为我们从头开始,没有剩余的。当禁用验证时剩余的所有内容都丢失了。重置计数器。现在 m_data_validation
被设置为 true
,请回顾上面覆盖 OnReceive()
的地方,看看 validate 方法将被调用。
转到方法 Validate_Received_Data()
的 426 行,找到此处的顶部附近:
if( m_carry_over_bytes > 0 )
{ ... }
请注意,我们刚刚描述了启用的数据包验证确保第一个 IF
不会被执行。暂时跳过此部分,然后向下跳转到从 474 行开始的 DO
循环。
do
{
mp_search_pointer = (RECEIVE_TYPE * ) &m_receive_buffer.char_array[ start_of_payload_packet ];
...
}
成员变量 m_receive_buffer
从 ASyncSocket
类获取/接收数据。成员变量 mp_search_pointer
用于访问该缓冲区并提供结构,以便数据可以被视为原始预期的结构。追溯声明以理解这一点。当我们查看接收缓冲区中的数据包时,此指针指向每个数据包的开头。UNION
然后允许代码直接访问有效负载数据包的各个字段。
首次进入此方法时,mp_search_pointer
指向接收缓冲区的第一个字节。start_of_payload_packet
是一个方法局部变量,在进入时设置为零。它在刚刚跳过的进位部分中设置,因此在处理了部分有效负载数据包后,它指向刚接收的 TCP/IP 数据包中的第一个完整有效负载数据包。
检查代码中的注释。数据包通过检查标题中的两个字来“验证”。如果不存在,代码会重新开始,并快速返回。最终,我们将找到一个以有效有效负载数据包开头的新接收缓冲区。
在对有效数据包进行这种相对宽松的检查之后,会检查序列计数器,并将其计为好或坏。服务器每次输出一个有效负载数据包时会将其加一。这只是一个简单的检查,客户端可以显示它来显示事情的进展情况。在实际项目中,它可以非常有助于验证所有数据是否都已接收。
在检查当前有效负载数据包后,有三种可能的情况来确定下一步操作。检查 532、538 和 546 行上的注释。这三种情况之一总是会满足,从而决定此实用程序的流程。
进位
检查 549 行处的代码。
else if( end_of_payload_packet > m_receive_byte_count )
{
m_payload_split_count ++;
get_another = false;
// m_carry_over_bytes is critical in the next entry
m_carry_over_bytes = m_receive_byte_count - start_of_payload_packet;
memcpy( &m_carry_over_buffer,
mp_search_pointer,
m_carry_over_bytes );
}
如果有效负载数据包的结束位置超出已接收的数据,则该有效负载数据包已拆分成两个 TCP/IP 数据包。计算成员变量 m_carry_over_bytes
,并将这些字节数复制到进位缓冲区。当它非零时,将执行此方法中的第一个 IF
,并捕获有效负载数据包的剩余部分。返回到此方法的顶部查找
if( m_carry_over_bytes > 0 )
{
true_packet_size = m_carry_over_buffer.iads_structure.header.packet_size + 4;
// Move sufficient data into m_carry_over_buffer to complete the payload packet
// make this a memcpy later
for( ;
m_carry_over_bytes < true_packet_size;
m_carry_over_bytes ++, start_of_payload_packet ++ )
{
m_carry_over_buffer.char_array[ m_carry_over_bytes] =
m_receive_buffer.char_array[ start_of_payload_packet ];
}
真正的包大小从进位缓冲区捕获,然后一个有些不寻常的 FOR
循环用于将有效负载包的其余部分复制到进位缓冲区。
这有点不寻常,因为启动条件在进入时已经设置好,并且有两个循环结束操作而不是一个。请注意,start_of_payload_packet
在 FOR
循环中被推进。这使得 DO
循环可以从 TCP/IP 数据包的第一个完整有效负载数据包开始。
希望本节的其余部分相对简单。
第 3 部分和第 4 部分回顾
在第 3 部分,我们讨论了服务器的活动。发送数据并不总是那么简单。服务器类以及应用程序必须能够在操作系统繁忙时缓冲一定量的数据。请注意,缓冲区的最大大小肯定存在限制。包装类 CAsyncSocket
是一个很好的类,但对于高数据速率,它可能会不堪重负。监控发送缓冲区的深度。如果它增长到极限并溢出,那么最大吞吐量可能已经超过了。
在第 4 部分,我们讨论了客户端的活动。它必须能够处理突发到达的接收数据。
附注:搜索纳格尔算法。它描述了一种常用的提高 TCP/IP 效率的方法,通过合并有效负载数据包。
一个 TCP/IP 数据包可能包含多个有效负载数据包,它们首尾相连,中间没有标记。有效负载数据包的设计必须包含有效负载大小,以便客户端代码可以确定每个有效负载数据包的结束位置和下一个数据包的开始位置。
这里省略的是允许应用程序一次从该实用程序中获取有效负载数据包的代码。我目前有其他紧迫的需求,而忽略了这一点。如果您编写了这部分代码,请写一篇文章。
历史
- 2013 年 3 月 23 日:提交第一个版本
- 2013 年 5 月 26 日:更新了 zip 文件,添加了公共代码,减小了尺寸