﻿<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:trackback="http://madskills.com/public/xml/rss/module/trackback/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/"><channel><title>C++博客-jack-wang-随笔分类-网络通讯</title><link>http://www.cppblog.com/jack-wang/category/10924.html</link><description>小王</description><language>zh-cn</language><lastBuildDate>Tue, 28 Mar 2023 17:59:32 GMT</lastBuildDate><pubDate>Tue, 28 Mar 2023 17:59:32 GMT</pubDate><ttl>60</ttl><item><title>非boost版本的asio无法识别asio::placeholders::error和boost::system::error_code</title><link>http://www.cppblog.com/jack-wang/archive/2023/03/28/229784.html</link><dc:creator>小王</dc:creator><author>小王</author><pubDate>Tue, 28 Mar 2023 03:01:00 GMT</pubDate><guid>http://www.cppblog.com/jack-wang/archive/2023/03/28/229784.html</guid><wfw:comment>http://www.cppblog.com/jack-wang/comments/229784.html</wfw:comment><comments>http://www.cppblog.com/jack-wang/archive/2023/03/28/229784.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/jack-wang/comments/commentRss/229784.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/jack-wang/services/trackbacks/229784.html</trackback:ping><description><![CDATA[asio::placeholders::error<br />改用<br />std::placeholders::_1<br /><br /><div>boost::system::error_code<br />改用<br />std::error_code</div><br />O了<img src ="http://www.cppblog.com/jack-wang/aggbug/229784.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/jack-wang/" target="_blank">小王</a> 2023-03-28 11:01 <a href="http://www.cppblog.com/jack-wang/archive/2023/03/28/229784.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>libevent 无法解析的外部符号 __imp__WSASend</title><link>http://www.cppblog.com/jack-wang/archive/2016/05/04/213417.html</link><dc:creator>小王</dc:creator><author>小王</author><pubDate>Wed, 04 May 2016 08:27:00 GMT</pubDate><guid>http://www.cppblog.com/jack-wang/archive/2016/05/04/213417.html</guid><wfw:comment>http://www.cppblog.com/jack-wang/comments/213417.html</wfw:comment><comments>http://www.cppblog.com/jack-wang/archive/2016/05/04/213417.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/jack-wang/comments/commentRss/213417.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/jack-wang/services/trackbacks/213417.html</trackback:ping><description><![CDATA[导入ws2_32.lib，O了！<img src ="http://www.cppblog.com/jack-wang/aggbug/213417.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/jack-wang/" target="_blank">小王</a> 2016-05-04 16:27 <a href="http://www.cppblog.com/jack-wang/archive/2016/05/04/213417.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>UDT协议-基于UDP的可靠数据传输协议 </title><link>http://www.cppblog.com/jack-wang/archive/2010/04/13/112470.html</link><dc:creator>小王</dc:creator><author>小王</author><pubDate>Tue, 13 Apr 2010 09:14:00 GMT</pubDate><guid>http://www.cppblog.com/jack-wang/archive/2010/04/13/112470.html</guid><wfw:comment>http://www.cppblog.com/jack-wang/comments/112470.html</wfw:comment><comments>http://www.cppblog.com/jack-wang/archive/2010/04/13/112470.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/jack-wang/comments/commentRss/112470.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/jack-wang/services/trackbacks/112470.html</trackback:ping><description><![CDATA[<p style="FONT-SIZE: 10pt">转：<a href="http://blog.csdn.net/windcsn/archive/2006/01/04/570242.aspx">http://blog.csdn.net/windcsn/archive/2006/01/04/570242.aspx</a><br><br>1.&nbsp;&nbsp; 介绍<br>随着网络带宽时延产品(BDP)的增加，通常的TCP协议开始变的低效。这是因为它的AIMD（additive increase multiplicative decrease）算法彻底减少了TCP拥塞窗口，但不能快速的恢复可用带宽。理论上的流量分析表明TCP在BDP增加到很高的时候比较容易受包损失攻击。</p>
<p style="FONT-SIZE: 10pt">另外，继承自TCP拥塞控制的不公平的RTT也成为在分布式数据密集程序中的严重问题。拥有不同RTT的并发TCP流将不公平地分享带宽。尽管在小的BDP网络中使用通常的TCP实现来相对平等的共享带宽，但在拥有大量BDP的网络中，通常的基于TCP的程序就必须承受严重的不公平的问题。这个RTT基于的算法严重的限制了其在广域网分布式计算的效率，例如：internet上的网格计算。</p>
<p style="FONT-SIZE: 10pt">一直到今天，对标准的TCP的提高一直都不能在高BDP环境中效率和公平性方面达到满意的程度（特别是基于RTT的问题）。例如：TCP的修改，RFC1423（高性能扩展），RFC2018（SACK）、RFC2582（New Reno）、RFC2883（D-SACK）、和RFC2988（RTO计算）都或多或少的提高了点效率，但最根本的AIMD算法没有解决。HS TCP（RFC 3649）通过根本上改变TCP拥塞控制算法来在高BDP网络中获得高带宽利用率，但公平性问题仍然存在。</p>
<p style="FONT-SIZE: 10pt">考虑到上面的背景，需要一种在高BDP网络支持高性能数据传输的传输协议。我们推荐一个应用程序级别的传输协议，叫UDT或基于UDP的数据传输协议并拥有用塞控制算法。</p>
<p style="FONT-SIZE: 10pt">本文描述两个正交的部分，UDP协议和UDT拥塞控制算法。一个应用层级别的协议，位于UDP之上，使用其他的拥塞算法，然而这些本文中描述的算法也可以在其他协议中实现，例如：TCP。</p>
<p style="FONT-SIZE: 10pt">一个协议的参考实现叫[UDT]；详细的拥塞控制算法的性能分析在[GHG04]中可以找到。</p>
<p style="FONT-SIZE: 10pt">2.&nbsp;&nbsp; 设计目标<br>UDT主要用在小数量的bulk源共享富裕带宽的情况下，最典型的例子就是建立在光纤广域网上的网格计算，一些研究所在这样的网络上运行他们的分布式的数据密集程序，例如，远程访问仪器、分布式数据挖掘和高分辨率的多媒体流。</p>
<p style="FONT-SIZE: 10pt">UDT的主要目标是效率、公平、稳定。单个的或少量的UDT流应该利用所有高速连接提供的可用带宽，即使带宽变化的很剧烈。同时，所有并发的流必须公平地共享带宽，不依赖于不同的带宽瓶劲、起始时间、RTT。稳定性要求包发送速率应该一直会聚可用带宽非常快，并且必须避免拥塞碰撞。</p>
<p style="FONT-SIZE: 10pt">UDT并不是在瓶劲带宽相对较小的和大量多元短文件流的情况下用来取代TCP的。</p>
<p style="FONT-SIZE: 10pt">UDT主要作为TCP的朋友，和TCP并存，UDT分配的带宽不应该超过根据MAX-MIN规则的最大最小公平共享原则。（备注，最大最小规则允许UDT在高BDP连接下分配TCP不能使用的可用带宽）。我们</p>
<p style="FONT-SIZE: 10pt">3.&nbsp;&nbsp; 协议说明<br>3.1. 概述<br>UDT是双工的，每个UDT实体有两个部分：发送和接收。发送者根据流量控制和速率控制来发送（和重传）应用程序数据。接收者接收数据包和控制包，并根据接收到的包发送控制包。发送和接收程序共享同一个UDP端口来发送和接收。</p>
<p style="FONT-SIZE: 10pt">接收者也负责触发和处理所有的控制事件，包括拥塞控制和可靠性控制和他们的相对机制，例如RTT估计、带宽估计、应答和重传。</p>
<p style="FONT-SIZE: 10pt">UDT总是试着将应用层数据打包成固定的大小，除非数据不够这么大。和TCP相似的是，这个固定的包大小叫做MSS（最大包大小）。由于期望UDT用来传输大块数据流，我们假定只有很小的一部分不规则的大小的包在UDT session中。MSS可以通过应用程序来安装，MTU是其最优值（包括所有包头）。</p>
<p style="FONT-SIZE: 10pt">UDT拥塞控制算法将速率控制和窗口（流量控制）合并起来，前者调整包的发送周期，后者限制最大的位被应答的包。在速率控制中使用的参数通过带宽估计技术来更新，它继承来自基于接收的包方法。同时，速率控制周期是估计RTT的常量，流控制参数依赖于对方的数据到达速度，另外接收端释放的缓冲区的大小。</p>
<p style="FONT-SIZE: 10pt"><br>&nbsp;</p>
<p style="FONT-SIZE: 10pt"><br>&nbsp;</p>
<p style="FONT-SIZE: 10pt"><br>3.2. 包结构<br>UDT有两种包：数据包和控制包。他们通过包头的第一位来区分（标志位）。如果是0，表示是数据包，1表示是控制包。</p>
<p style="FONT-SIZE: 10pt">3.2.1.&nbsp;&nbsp;&nbsp; 数据包</p>
<p style="FONT-SIZE: 10pt">&nbsp;</p>
<p style="FONT-SIZE: 10pt"><br>&nbsp;</p>
<p style="FONT-SIZE: 10pt">&nbsp;</p>
<p style="FONT-SIZE: 10pt">&nbsp;</p>
<p style="FONT-SIZE: 10pt"><br>&nbsp;</p>
<p style="FONT-SIZE: 10pt"><br>数据包结构如下显示：</p>
<p style="FONT-SIZE: 10pt">0&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 1&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 3&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 4</p>
<p style="FONT-SIZE: 10pt">0&nbsp; 1&nbsp; 2 3 4&nbsp; 5&nbsp; 6&nbsp; 7 8&nbsp; 9 0&nbsp; 1&nbsp; 2&nbsp; 3&nbsp; 4 5&nbsp; 6 7&nbsp; 8&nbsp; 9&nbsp; 0 1&nbsp; 2&nbsp; 3&nbsp; 4 5&nbsp; 6 7&nbsp; 8&nbsp; 9 0&nbsp; 1</p>
<p style="FONT-SIZE: 10pt">0<br>&nbsp;包序号<br>&nbsp;<br>应用数据<br>&nbsp;</p>
<p style="FONT-SIZE: 10pt">包序号是UDT数据包头中唯一的内容。它是一个无符号整数，使用标志位后的31位，UDT使用包基础的需要，例如，每个非重传的包都增加序号1。序号在到达最大值2^31-1的时候覆盖。紧跟在这些数据后面的是应用程序数据。</p>
<p style="FONT-SIZE: 10pt">3.2.2.&nbsp;&nbsp;&nbsp; 控制包<br>控制包结构如下：</p>
<p style="FONT-SIZE: 10pt">0&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 1&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 3&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 4</p>
<p style="FONT-SIZE: 10pt">0&nbsp; 1&nbsp; 2 3 4&nbsp; 5&nbsp; 6&nbsp; 7 8&nbsp; 9 0&nbsp; 1&nbsp; 2&nbsp; 3&nbsp; 4 5&nbsp; 6 7&nbsp; 8&nbsp; 9&nbsp; 0 1&nbsp; 2&nbsp; 3&nbsp; 4 5&nbsp; 6 7&nbsp; 8&nbsp; 9 0&nbsp; 1</p>
<p style="FONT-SIZE: 10pt">1<br>&nbsp;类型<br>&nbsp;保留<br>&nbsp;ACK序号<br>&nbsp;<br>控制信息字段<br>&nbsp;</p>
<p style="FONT-SIZE: 10pt">有6种类型的控制包在UDT中，bit1-3表示这些信息。前32位在包头中必须存在。控制信息字段包括0（例如，它不存在）或者多个32位无符号整数，这由包类型决定。</p>
<p style="FONT-SIZE: 10pt">UDT使用应答子序号的方法。每个ACK/ACK2包有一个无符号的16位序号，它独立于数据包需要。它使用位16-31。应答需要从0到（2^16-1）。位16-31在其他控制包中没有定义。</p>
<p style="FONT-SIZE: 10pt">类型<br>&nbsp;说明<br>&nbsp;控制信息<br>&nbsp;<br>000<br>&nbsp;协议连接握手<br>&nbsp;1．32位 UDT版本</p>
<p style="FONT-SIZE: 10pt">2．32位 内部顺序号</p>
<p style="FONT-SIZE: 10pt">3．32位 MSS（字节）</p>
<p style="FONT-SIZE: 10pt">4．32位 最大流量窗口大小（字节）</p>
<p style="FONT-SIZE: 10pt"><br>&nbsp;</p>
<p style="FONT-SIZE: 10pt"><br>&nbsp;</p>
<p style="FONT-SIZE: 10pt">&nbsp;<br>001<br>&nbsp;保活<br>&nbsp;没有<br>&nbsp;<br>010<br>&nbsp;应答，位16-31是应答序号<br>&nbsp;1．32位包序号，先前接收到的包序号</p>
<p style="FONT-SIZE: 10pt">2．32位，RTT（微秒）</p>
<p style="FONT-SIZE: 10pt">3．32位，RTT 变量或者RTTVar (微秒)</p>
<p style="FONT-SIZE: 10pt">4．32位，流量窗口大小（包的数量）</p>
<p style="FONT-SIZE: 10pt">5．32位，连接容量估计（每秒包的数量）<br>&nbsp;<br>011<br>&nbsp;Negative应答（NAK）<br>&nbsp;丢失信息的32位整数数组，见3.9节<br>&nbsp;<br>100<br>&nbsp;保留<br>&nbsp;这种类型的控制信息保留作为拥塞警告使用，从接收到发送端。一个拥塞警告能被ECN或包延迟增加趋势的度量方法触发。<br>&nbsp;<br>101<br>&nbsp;关闭<br>&nbsp;<br>&nbsp;</p>
<p style="FONT-SIZE: 10pt"><br>&nbsp;</p>
<p style="FONT-SIZE: 10pt">&nbsp;<br>110<br>&nbsp;应答一个应答（ACK2）<br>&nbsp;16-31位，应答序号。<br>&nbsp;<br>111<br>&nbsp;4-15的解释<br>&nbsp;保留将来使用<br>&nbsp;</p>
<p style="FONT-SIZE: 10pt">注意，对于数据和控制包来说，可以从UDP协议头中得到实际的包大小。包大小信息能被用来得到有效的数据负载和NAK包中的控制信息字段大小。</p>
<p style="FONT-SIZE: 10pt">3.3. 定时器<br>UDT在接收端使用4个定时器来触发不同的周期事件，包括速率控制、应答、丢失报告（negative应答）和重传/连接维护。</p>
<p style="FONT-SIZE: 10pt">UDT中的定时器使用系统时间作为源。UDT接收端主动查询系统时间来检查一个定时器是否过期。对于某个定时器T来说，其拥有周期TP，将定变量t用来记录最近T被设置或复位的时间。如果T在系统时间t0（t= t0）被复位，那么任何t1（t1-t&gt;=TP）是T过期的条件。</p>
<p style="FONT-SIZE: 10pt">四个定时器是：RC定时器、ACK定时器、NAK定时器、EXP定时器。他们的周期分别是：RCTP、ATP、NTP、ETP。</p>
<p style="FONT-SIZE: 10pt">RC定时器用来触发周期性的速率控制。ACK定时器用来触发周期性的有选择的应答（应答包）。RCTP和ATP是常量值，值为：RCTP=ATP=0.01秒。</p>
<p style="FONT-SIZE: 10pt">NAK被用来触发negative应答（NAK包）。重传定时器被用来触发一个数据包的重传和维护连接状态。他们周期依赖于对于RTT的估计。ETP值也依赖于连续EXP时间溢出的次数。推荐的RTT初始值是0.1秒，而NTP和ETP的初始值是：NTP=3*RTT，ETP=3*RTT+ATP。</p>
<p style="FONT-SIZE: 10pt">在每次bounded UDP接收操作（如果收到一个UDP包，一些额外的必须的数据处理时间）时查询系统时间来检查四个定时器是否已经过期。推荐的周期粒度是微秒。UDP接收时间溢出值是实现的一个选择，这依赖于循环查询的负担和事件周期精确度之间的权衡。</p>
<p style="FONT-SIZE: 10pt">速率控制事件更新包发送周期，UDT发送端使用STP来安排数据包的发送。假定一个在时间t0被发送，那么下一次包发送时间是（t0+ STP）。换句话说，如果前面的包发送花费了t&#8217;时间，发送端将等待（STP-t&#8217;）来发送下一个数据包（如果STP-t&#8217; &lt;0，就不需要等待了）。这个等待间隔需要一个高精确度的实现，推荐使用CPU时钟周期粒度。</p>
<p style="FONT-SIZE: 10pt">3.4. 发送端算法<br>3.4.1.&nbsp;&nbsp;&nbsp; 数据结构和变量<br>A． SND PKT历史窗口：一个循环数组记录每个数据包的开始时间</p>
<p style="FONT-SIZE: 10pt">B． 发送端丢失链表：发送段丢失列表是一个连接链表，用来存储被接收方NAK包中返回的丢失包序号。这些数字以增加的顺序存储。</p>
<p style="FONT-SIZE: 10pt">3.4.2.&nbsp;&nbsp;&nbsp; 数据发送算法<br>A． 如果发送端的丢失链表是非空的，重传第一个在list中的包，并删除该成员，到5。</p>
<p style="FONT-SIZE: 10pt">B． 等待有应用程序数据需要发送</p>
<p style="FONT-SIZE: 10pt">C． 如果未应答的包数量超过了两量窗口的大小，转到1。如果不是包装一个新的包并发送它。</p>
<p style="FONT-SIZE: 10pt">D．如果当前包的序号是16n，n是一个整数，转第2步。</p>
<p style="FONT-SIZE: 10pt">E． 在SND PKT历史窗口中记录包的发送时间</p>
<p style="FONT-SIZE: 10pt">F． 如果这是自上次发送速率降低之后的第一个包，等外SYN时间。</p>
<p style="FONT-SIZE: 10pt">G．等外（STP &#8211; t）时间，t是第1到第4步之间的总时间，然后转到1。</p>
<p style="FONT-SIZE: 10pt">3.5. 接收端算法<br>3.5.1.&nbsp;&nbsp;&nbsp; 数据结构和变量<br>A． 接收端丢失链表：是一个duple连接链表，元素的值包括：丢失数据包的序号、最近丢失包的反馈时间和包已经被反馈的次数。值以包序号增序的方式存储。</p>
<p style="FONT-SIZE: 10pt">B． 应答历史窗口：每个发送ACK的和时间一个循环数组；由于其循环的特性，意味着如果数组中没有更多空间的时候新的值将覆盖老的值。</p>
<p style="FONT-SIZE: 10pt">C． RCV PKT历史窗口：一个用来记录每个包到达时间的循环数组。</p>
<p style="FONT-SIZE: 10pt">D．对包窗口：一个用来记录每个探测包对之间的时间间隔。</p>
<p style="FONT-SIZE: 10pt">E． LRSN：一个用来记录最大接收数据包需要的变量。LRSN被初始化为初始序号减1。</p>
<p style="FONT-SIZE: 10pt">3.5.2.&nbsp;&nbsp;&nbsp; 数据接收算法<br>A． 查询系统时间来检查RC、ACK、NAK、或EXP定时器是否过期。如果任一定时器过期，处理事件（本节下面介绍）并复位过期的定时器。</p>
<p style="FONT-SIZE: 10pt">B． 启动一个时间bounded UDP接收。如果每个包到，转1。</p>
<p style="FONT-SIZE: 10pt">C． 设置exp-count为1，并更新ETP为：ETP=RTT+4*RTTVar + ATP。</p>
<p style="FONT-SIZE: 10pt">D．如果所有的发送数据包已经被应答，复位EXP时间变量。</p>
<p style="FONT-SIZE: 10pt">E． 检查包头的标志位。如果是一个控制包，根据类型处理它，然后转1。</p>
<p style="FONT-SIZE: 10pt">F． 如果当前数据包的需要是16n+1，n是一个整数，记录当前包和上个在对包窗口中数据包的时间间隔。</p>
<p style="FONT-SIZE: 10pt">G．在PKT历史窗口中记录包到达时间</p>
<p style="FONT-SIZE: 10pt">H． 如果当前数据包的序号大于LRSN+1，将所有在（但不包括）这两个值之间的序号放入接收丢失链表，并在一个NAK包中将这些序号发送给发送端。如果序号小于LRSN，从接收丢失链表中删除它。</p>
<p style="FONT-SIZE: 10pt">I．&nbsp;&nbsp; 更新LRSN，转1。</p>
<p style="FONT-SIZE: 10pt">3.5.3.&nbsp;&nbsp;&nbsp; RC定时器到<br>通过速率控制算法来更新STP（见3.6节）。</p>
<p style="FONT-SIZE: 10pt">过程如下：</p>
<p style="FONT-SIZE: 10pt">A． 按照下面的原则查找接收端所接收到的所有包之前的序号：如果接收者丢失链表是空的，ACK号码是LRSN+1，否则是在接收丢失队列中的最小序号。</p>
<p style="FONT-SIZE: 10pt">B． 如果应答号不大于曾经被ACK2应答的最大应答号，或等于上次应答的应答号并且两次应答之间的时间间隔小于RTT+4*RTTVar，停止（不发送应答）。</p>
<p style="FONT-SIZE: 10pt">C． 分配这个应答一个唯一增加的ACK序列号，推荐采用ACK序列号按步骤1增加，并且重叠在达到最大值之后。</p>
<p style="FONT-SIZE: 10pt">D．根据下面的算法来计算包的抵达速度：使用PKT历史窗口中的值计算最近16个包抵达间隔（AI）中值。在这16个值中，删除那些大于AI*8或小于AI*8的包，如果最后剩余8个值，计算他们的平均值(AI&#8217;)，包抵达速度是1/AI&#8217;（每秒包的数量），否则是0。</p>
<p style="FONT-SIZE: 10pt">E． 根据3.7节中的内容为每端（W）计算流量窗口。然后计算有效的流量窗口大小为：最大（W，可用接收方缓冲大小），2）。</p>
<p style="FONT-SIZE: 10pt">F． 根据下面的算法来计算连接容量估计。如果流量控制快启动阶段（3.7）一直继续，返回0，否则计算最近16个对包间隔（PI），这些值在对包窗口中，那么连接容量就是1/PI（每秒包的数量）。</p>
<p style="FONT-SIZE: 10pt">G．打包应答序列号，应答号，RTT，RTT 变量，有效的流量窗口大小并估计连接，将他们放入ACK包中，然后发送出去。</p>
<p style="FONT-SIZE: 10pt">H． 记录ACK序列号，应答号和这个应答的开始时间，并放入历史窗口中。</p>
<p style="FONT-SIZE: 10pt">3.5.4.&nbsp;&nbsp;&nbsp; 处理NAK定时器到时<br>&#216;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 查找接受方的丢失链表，找到所有上次反馈时间是（k*（RTT+4*RTTVar ) ）前的包，k当前这个包的反馈次数加1，如果没有反馈丢失，停止。</p>
<p style="FONT-SIZE: 10pt">&#216;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 压缩第一步中得到的序号（见3.9），然后在一个NAK包中发送他们到发送方。</p>
<p style="FONT-SIZE: 10pt">&#216;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 如果不是停止流量控制快启动阶段。</p>
<p style="FONT-SIZE: 10pt">3.5.5.&nbsp;&nbsp;&nbsp; 处理EXP定时器<br>A．&nbsp;&nbsp; 如果发送端的丢失链表不是空的，停止</p>
<p style="FONT-SIZE: 10pt">B．&nbsp;&nbsp; 将所有未应答的包放到发送端的丢失链表中</p>
<p style="FONT-SIZE: 10pt">C． 如果(exp-count&gt;16)并且自上次从对方接收到一个包以来的总时间超过3秒，或者这个时间已经超过3分钟了，这被认为是连接已经断开，关闭UDT连接。</p>
<p style="FONT-SIZE: 10pt">D．如果没有数据，也就没有应答，发送一个保活包给对端，否则将所有未应答包的序号放入发送丢失列表中。</p>
<p style="FONT-SIZE: 10pt">E．&nbsp;&nbsp; 更新exp-count为：exp-count= exp-count+1</p>
<p style="FONT-SIZE: 10pt">F．&nbsp;&nbsp; 更新ETP为：ETP=exp-count*（RTT+4*RTTVar）+ATP。</p>
<p style="FONT-SIZE: 10pt">3.5.6.&nbsp;&nbsp;&nbsp; 收到应答包<br>A．&nbsp;&nbsp; 更新最大的应答序号</p>
<p style="FONT-SIZE: 10pt">B． 更新RTT和RTTVar为：RTT = rtt， RTTVar = rv；rtt和rv是ACK包中的RTT和RTTVar值。</p>
<p style="FONT-SIZE: 10pt">C．&nbsp;&nbsp; 更新NTP和ETP为：NTP=RTT+4*RTTVar；ETP=exp-count*（RTT+4*RTTVar）+ATP。</p>
<p style="FONT-SIZE: 10pt">D．&nbsp; 更新连接容量估计：B=（B*7+b）/8，b是ACK包带的值。</p>
<p style="FONT-SIZE: 10pt">E．&nbsp;&nbsp; 更新流量窗口大小为ACK中的值。</p>
<p style="FONT-SIZE: 10pt">F．&nbsp;&nbsp; 发送ACK2包，并设置与ACK序号相同的应答号到对端</p>
<p style="FONT-SIZE: 10pt">G．&nbsp; 复位EXP定时器</p>
<p style="FONT-SIZE: 10pt">3.5.7.&nbsp;&nbsp;&nbsp; 当收到NAK包的时候<br>A． 将所有NAK包中带的序号放入发送方的丢失列表中</p>
<p style="FONT-SIZE: 10pt">B． 通过速率控制来更新STP（见3.6）</p>
<p style="FONT-SIZE: 10pt">C． 复位EXP定时器</p>
<p style="FONT-SIZE: 10pt">3.5.8.&nbsp;&nbsp;&nbsp; 当收到ACK2包<br>&#216;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 在ACK历史窗口中根据接收到的ACK2序列号查找行营的ACK包。</p>
<p style="FONT-SIZE: 10pt">&#216;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 更新曾经被应答的最大应答号</p>
<p style="FONT-SIZE: 10pt">&#216;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 根据ACK2的到达时间和ACK离开时间计算新的rtt值，并且更新RTT和RTTVar值为：</p>
<p style="FONT-SIZE: 10pt">RTTVar = (RTTVar *3 +abs(rtt-RTT)/4</p>
<p style="FONT-SIZE: 10pt">RTT = (RTT *7+rtt)/8</p>
<p style="FONT-SIZE: 10pt">RTT和RTTVar的初始值是0.1秒和0.05秒。</p>
<p style="FONT-SIZE: 10pt">&#216;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 更新NTP和ETP为：</p>
<p style="FONT-SIZE: 10pt">NTP = RTT；</p>
<p style="FONT-SIZE: 10pt">ETP = (exp-count +1)* RTT+ATP</p>
<p style="FONT-SIZE: 10pt">3.5.9.&nbsp;&nbsp;&nbsp; 当收到保活包的时候<br>什么也不做</p>
<p style="FONT-SIZE: 10pt">3.5.10.&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 当收到连接握手和关闭包的时候<br>见3.8节</p>
<p style="FONT-SIZE: 10pt">3.6. 速度控制算法<br>3.6.1.&nbsp;&nbsp;&nbsp; 速率控制快启动<br>STP被初始为最小的时间精度（1个CPU周期或1毫秒）。这是在快启动阶段，一般收到一个ACK包其携带的估计带宽大于0这个阶段就停止了。包的发送周期被设置为1/W，W是ACK携带的流量窗口的大小。</p>
<p style="FONT-SIZE: 10pt">快启动阶段仅仅在开始一个UDT连接的时候发生，且不会在UDT连接的以后再出现。在快启动阶段之后，下面的算法就要工作了。</p>
<p style="FONT-SIZE: 10pt">3.6.2.&nbsp;&nbsp;&nbsp; 当RC定时器时间到<br>1．&nbsp; 如果在上一个RCTP时间内，没有收到一个ACK，停止</p>
<p style="FONT-SIZE: 10pt">2．&nbsp; 计算在上个RCTP时间内的丢失率，计算方法是根据总共发送的包与NAK反馈中总共丢失包的数量。如果丢失率大于0.1%，停止。</p>
<p style="FONT-SIZE: 10pt">3．&nbsp; 下个RCTP时间内发送包的增加数量如下计算：(inc)</p>
<p style="FONT-SIZE: 10pt">If (B&lt;=C) inc = 1/MSS</p>
<p style="FONT-SIZE: 10pt">Else inc = max (10^(ceil(log10((B-C)*MSS*8)))*Beta/MSS,1/MSS)</p>
<p style="FONT-SIZE: 10pt">B是连接容量估计，C是当前的发送速度。两个都计算为每秒多少个包。MSS是以字节计算的；Beta是值为0.0000015的常量。</p>
<p style="FONT-SIZE: 10pt">4．&nbsp; 更新STP：STP=（STP*RCTP）/（STP*inc + RCTP）</p>
<p style="FONT-SIZE: 10pt">5．&nbsp; 计算真正的数据发送周期（rsp），从SND PKT历史窗口中得到，如果（STP&lt;0.5 *rsp）设置STP为（0.5 * rsp）。</p>
<p style="FONT-SIZE: 10pt">6．&nbsp; 如果（STP&lt;1.0），设置STP为1.0。</p>
<p style="FONT-SIZE: 10pt">3.6.3.&nbsp;&nbsp;&nbsp; 当收到NAK包时<br>3.6.3.1.&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 数据结构和变量<br>1．&nbsp; LSD：自上次速率降低后发送的最大序号</p>
<p style="FONT-SIZE: 10pt">2．&nbsp; NumNAK：自上次LSD更新以后的NAK数量</p>
<p style="FONT-SIZE: 10pt">3．&nbsp; AvgNAK：当最大序号大于LSD时两次事件之间的NAK移动的平均数。</p>
<p style="FONT-SIZE: 10pt">4．&nbsp; DR：在1到AvgNAK之间的随机平均数。</p>
<p style="FONT-SIZE: 10pt">3.6.3.2.&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 算法<br>1．&nbsp; 如果NAK中最大的丢失序列号大于LSD：</p>
<p style="FONT-SIZE: 10pt">增加STP为：STP=STP*（1+1/8）</p>
<p style="FONT-SIZE: 10pt">更新AvgNAK为：AvgNAK = （AvgNAK *7 +NumNAK）/8</p>
<p style="FONT-SIZE: 10pt">更新DR</p>
<p style="FONT-SIZE: 10pt">复位 NumNAK = 0</p>
<p style="FONT-SIZE: 10pt">记录LSD</p>
<p style="FONT-SIZE: 10pt">2．&nbsp; 否则，增加NumNAK按照1个步骤增加；如果NumNAK % DR = 0；增加STP为：STP=STP*（1+1/8）；记录LSD。</p>
<p style="FONT-SIZE: 10pt">3.7. 流量控制算法<br>流量控制窗口大小（W）初始值是16。</p>
<p style="FONT-SIZE: 10pt">3.7.1.&nbsp;&nbsp;&nbsp; 当ACK定时器到的时候<br>1．&nbsp; 流量控制快启动：如果没有NAK产生或者W没有到达或超过15个包，并且AS&gt;0，流量窗口大小更新为应答包的总数量。</p>
<p style="FONT-SIZE: 10pt">2．&nbsp; 否则，如果（AS&gt;0），W更新为：（AS是包的到达速度）</p>
<p style="FONT-SIZE: 10pt">W= ceil (W *0.875+AS* (RTT +ATP) *0.125)</p>
<p style="FONT-SIZE: 10pt">3．&nbsp; 限制W到对方最大流量窗口大小。</p>
<p style="FONT-SIZE: 10pt">3.8. 连接建立和关闭<br>一个UDT实体首先作为一个SERVER启动，当一个客户端需要连接的时候其发送握手包。客户端在从服务端接收到一个握手响应包或时间溢出之前，应该每隔一段时间发送一个握手包（时间间隔由响应时间和系统overhead来权衡）。</p>
<p style="FONT-SIZE: 10pt">握手包有如下信息：</p>
<p style="FONT-SIZE: 10pt">1．&nbsp; UDT版本：这个值是兼容的目的。当前的版本是2</p>
<p style="FONT-SIZE: 10pt">2．&nbsp; 初始序号：这是发送这个UDT实体将来用于发送数据包的起始序号。它必须是一个在1到（2^31-1）之间的随机值。另外，建议这个值在合理的时间历史窗口中不应该重复。</p>
<p style="FONT-SIZE: 10pt">3．&nbsp; MSS：数据包的大小（通过IP有效负载来度量）</p>
<p style="FONT-SIZE: 10pt">4．&nbsp; 最大的流量窗口大小：这是接收到握手信息的UDT实体允许的最大流量窗口大小，窗口大小通常限制为接收端的数据结构大小。</p>
<p style="FONT-SIZE: 10pt">服务器接收到一个握手包之后，比较MSS值和他自己的值并设置它自己的值为较小的值。结果值也在握手响应中被发送到客户端，另外还有服务器的版本信息，初始序列号，最大流量窗口大小。</p>
<p style="FONT-SIZE: 10pt">版本字段用来检查两端的兼容性。初始序列号和最大流量窗口大小用于初始化接收到这个握手包的UDT实体参数。</p>
<p style="FONT-SIZE: 10pt">服务器在第一步完成以后就准备发送或接收数据。然而，只要从同一个客户端接收任何握手包，其应该发送响应包。</p>
<p style="FONT-SIZE: 10pt">客户端一旦得到服务器的一个握手响应其就进入发送和接收数据状态。设置它自己的MSS为握手响应包中的值并初始化相应的参数为包中的值（序列号、最大流量窗口）。如果收到任何其他的握手信息，丢掉它。</p>
<p style="FONT-SIZE: 10pt">如果其中的UDT实体要关闭，它将发送一个关闭信息到对端；对方收到这个信息以后将自己关闭。这个关闭信息通过UDP传输，仅仅发送一次，并不保证一定收到。如果消息没有收到，对方将根据时间溢出机制来关闭连接。</p>
<p style="FONT-SIZE: 10pt">3.9. 丢失信息的压缩方案<br>NAK包中携带的丢失信息是一个32-bit整数的数组。如果数组的中数字是一个正常的序号（第1位是0），这意味着这个序号的包丢失了，如果第1位是1，意味着从这个号码开始（包括该号码）到下一个数组中的元素（包括这个元素值）之间的包（它的第1位必须是0）都丢失。</p>
<p style="FONT-SIZE: 10pt">例如，下面的NAK中携带的信息：</p>
<p style="FONT-SIZE: 10pt">0x00000002, 0x80000006, 0x0000000B, 0x0000000E</p>
<p style="FONT-SIZE: 10pt">上面的信息表明序号为：2，6，7，8，9，10，11，14的包都丢了。</p>
<p style="FONT-SIZE: 10pt">4.&nbsp;&nbsp; 效率和公平性<br>UDT能够充分利用当前有线网络的独立于连接容量的可用带宽 、RTT、后台共存流、给定的连接比特错误率。UDT在没有数据包丢失的情况下从0bits/s到90%带宽需要一个常量时间，这个时间是7.5秒。UDT并不适合无线网络。</p>
<p style="FONT-SIZE: 10pt">UDT的确满足单瓶劲网络拓扑的最大-最小公平性。在多个瓶劲情况下，根据最大最小原则它能保证较小瓶劲连接或者至少一半的平等共享(it guarantees that flows over smaller bottleneck links obtain at least half of their fair share according to max-min rule)。RTT对公平性都一点影响。</p>
<p style="FONT-SIZE: 10pt">当和大块的TCP流共存的时候，TCP能占用比UDT更多的带宽，除了三种情况：</p>
<p style="FONT-SIZE: 10pt">1．&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 网络BDP非常大，TCP不能利用他们的公平共享带宽。这种情况下，UDT将占用TCP不能利用的带宽。</p>
<p style="FONT-SIZE: 10pt">2．&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 连接容量是如此的小，从而导致UDT的带宽估计技术不能最有的工作；模拟显示这个极限连接容量大约是100kb/s。</p>
<p style="FONT-SIZE: 10pt">3．&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 在使用FIFO队列作为网络路径的网络中，如果队列大小大于BDP，TCP的共享带宽随着队列大小的增加而降低。然而，抵达UDT的共享带宽是，队列大小通常超过实际路由器/交换机提供的数量。</p>
<p style="FONT-SIZE: 10pt">当短（timewise）类似web的TCP流和小的并发UDT流共存的时候，UDT在TCP流上的效果非常小。</p>
<p style="FONT-SIZE: 10pt">更多的分析在[GHG03]。</p>
<p style="FONT-SIZE: 10pt">5.&nbsp;&nbsp; 安全考虑<br>UDT并没有使用特定的安全机制，相反，它依赖于应用程序提供的授权和底层提供的安全机制。</p>
<p style="FONT-SIZE: 10pt">然而，由于UDP是无连接的，UDT实现应该检查所有达到的包是否是预期的来源。这是从socket的API连接概念中继承而来，其连接只是接收指定来源的数据。</p>
<p style="FONT-SIZE: 10pt"><br>6.UDT SOURCE CODE LINK<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <a href="http://sourceforge.net/projects/dataspace">http://sourceforge.net/projects/dataspace</a></p>
<p style="FONT-SIZE: 10pt"><br>本文来自CSDN博客，转载请标明出处：<a href="http://blog.csdn.net/windcsn/archive/2006/01/04/570242.aspx">http://blog.csdn.net/windcsn/archive/2006/01/04/570242.aspx</a></p>
<img src ="http://www.cppblog.com/jack-wang/aggbug/112470.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/jack-wang/" target="_blank">小王</a> 2010-04-13 17:14 <a href="http://www.cppblog.com/jack-wang/archive/2010/04/13/112470.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>(TCP-over-UDP library)基于UDP协议之上实现通用、可靠、高效的TCP协议</title><link>http://www.cppblog.com/jack-wang/archive/2010/04/12/112342.html</link><dc:creator>小王</dc:creator><author>小王</author><pubDate>Mon, 12 Apr 2010 08:35:00 GMT</pubDate><guid>http://www.cppblog.com/jack-wang/archive/2010/04/12/112342.html</guid><wfw:comment>http://www.cppblog.com/jack-wang/comments/112342.html</wfw:comment><comments>http://www.cppblog.com/jack-wang/archive/2010/04/12/112342.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/jack-wang/comments/commentRss/112342.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/jack-wang/services/trackbacks/112342.html</trackback:ping><description><![CDATA[<span style="FONT-SIZE: 10pt">转：</span><a href="http://www.cppblog.com/fwxjj/archive/2009/03/17/76923.html"><span style="FONT-SIZE: 10pt">http://www.cppblog.com/fwxjj/archive/2009/03/17/76923.html</span></a><br><br>随着互联网应用广泛推广，出现了越来越多的网络应用，其中基于p2p思想的各种网络技术的产品也越来越多的出现在我们的视野当中。从最早闻名的Napster到现在的Bittorrent、eMule、skype等产品，P2P这种网络应用模式已经从各个方面深入人心。这些产品在各自的网络实现技术上，都以各自的方法解决着同样面临的一个问题，如何让他们的软件产品在各异的网络拓扑结构中顺利的进行P2P通信。<br>&nbsp;众所周知，在当今的网络拓扑结构中，普遍存在使用NAT设备来进行网络地址转换，而让应用程序能跨越这些NAT设备进行全双工的通信，就成为非常重要的一个问题。对于实现跨越NAT通信可以采取很多种办法（对于能够直接连接、反向连接的情况不在此列）：首先是通过服务器进行转发，这是比较粗暴的方法，而且在用户量大的时候，转发服务器需要付出相当大的代价；第二，可以使用NAT穿透技术。而大家知道关于NAT穿透中，UDP穿透的成功率比起TCP穿透要高出许多，这一点这里将不做多述，可以参考Bryan Ford的文章《Peer-to-Peer Communication Across Network Address Translators》（<a href="http://www.brynosaurus.com/pub/net/p2pnat/"><font color=#0000ff><u>http://www.brynosaurus.com/pub/net/p2pnat/</u></font></a>）。因此在UDP协议上构建一些大型的网络应用程序可能会成为很多人的需求。<br>&nbsp;当然也可能基于更多的原因，会有很多人希望能在UDP协议上进行大型应用程序的构建。然而UDP协议本身存在着不通信不可靠的缺点，于是对于基于UDP进行可靠通信的需求就浮现出来了。目前在网络上有许多人正做着这一工作，UDT、RakNet、eNet等都是构建在UDP之后网络可靠通信开发库。然后这些库开发时都针对了一些特殊应用来进行设计的，不具备通用性。比如RakNet是为游戏应用而设计，对于实时性等游戏相关的网络需求有很好的支持，对于大批量数据传输却有点力所不及。而UDT基于一种基于带宽速率控制的拥塞控制算法进行设计（<a href="http://udt.sourceforge.net/doc/draft-gg-udt-01.txt"><font color=#0000ff><u>http://udt.sourceforge.net/doc/draft-gg-udt-01.txt</u></font></a>），主要用在小数量的bulk源共享富裕带宽的情况下，最典型的例子就是建立在光纤广域网上的网格计算，而在ISP提供带宽有限的情况下运行却显得消耗资源并性能不足。甚至可能被防火墙，或ISP服务商判断为恶意带宽使用攻击。这些都使用得他们不能被广泛地用于各种网络应用程序。另外大家也陆续发现目前的UDT实现版本存在的一些问题。比如UDT做服务端接收连接时，总是新开一个端口与客户端进行连接，这样会带来几个问题：1）较多客户端连接上来时，服务端新打开的众多端口中可能有的端口会被防火墙拦截而导致通信失败，2）如果客户端处于Symmetric NAT和Port-Restricted Cone NAT后面时，将导致服务器端与客户端连接无法成功建立，3）由于udp端口数最大值有限，所以UDT服务器端可接收的连接数也因些受限。再有就是不仅仅是UDT库，基本上所有的UDP-based可靠通信库，都未提供穿越proxy代理的功能（socks5）；再有就是对UDP打洞技术有的支持得不完善或并不支持。<br>&nbsp;基于这些原因，使得我需要开发一个基于UDP协议之上实现一个可靠、高效、通用的通信库，来满足我目前所开发的项目的需要。TCP协议算法已经是经过多方面及多年的验证，是最具通用性，且可靠高效的。虽然UDT等各种库指出TCP在这样或那样的网络环境下存在不足，但众多实现当中他仍然是最通用、可靠、高效的。相信有许多人跟我一样，需要这么一个开发库，所以我打算在开发过程中，陆续公开相关的文档及这个开发库。<br>&nbsp;<br>二、设计目标<br>&nbsp;<br>&nbsp;TDP主要的目标就是在UDP层之上实现TCP的协议算法，使得应用程序能够在UDP层之上获得通用、可靠、高效的通信能力。<br>&nbsp;TDP网络开发库所实现的算法，都来自久经考验的TCP协议算法，网上有着非常多的参考资料。在实现当中，参考最多的是Richard Stevens的《TCP/IP详解》。<br>&nbsp;TDP提供的用于开发的应用程序接口与Socket API非常相像，姑且称之为TDP Socket API，基本上的函数名与参数等都与Socket API相一致，但是TDP Socket API的API接口都位于命名空间TDP当中。只要使用过Socket API进行开发过的朋友，将都会使用TDP库进行开发。下图为TDP及TDP Socket API所处在的协议栈应用中的位置，以及与TCP协议栈应用的对比。<br>&nbsp;<img alt="" src="http://p.blog.csdn.net/images/p_blog_csdn_net/huanghongbo/tdp%20api.jpg"><br>&nbsp;<br>三、协议说明<br>&nbsp;<br>&nbsp;1．协议格式<br>&nbsp;<br>&nbsp;TDP的实现的算法虽然与TCP实现的算法是大致相同的,但TDP的协议格式只是从TCP协议格式获得参考，但并不完全与他相同。TDP的协议格式如下：<br>&nbsp;<br>&nbsp;<img alt="" src="http://p.blog.csdn.net/images/p_blog_csdn_net/huanghongbo/format2.jpg"><br>&nbsp;<br>&nbsp;接下来介绍一下协议格式的各个字段含义。<br>&nbsp;4位首部长度：表示用户数据在数据包中的起始位置。<br>&nbsp;LIV：连接保活标志，用于表示TDP连接通路存活状态。<br>&nbsp;ACK：确认序号有效。<br>&nbsp;PSH：接收方应该尽快将这个报文段交给应用层。<br>&nbsp;RST：重建连接。<br>&nbsp;SYN：同步序号用来发起一个连接。<br>&nbsp;FIN：发端完成发送任务。<br>&nbsp;16位窗口大小：接收端可接收数据的窗口大小。<br>&nbsp;选项：只有一个选项字段，为最长报文大小，即MSS。TDP选项格式与TCP选项格式一致，kind=0时表示选项结束，kind=1时表示无操作，kind=2时表示最大报文段长度。如下图：<br>&nbsp;<br>&nbsp;<img alt="" src="http://p.blog.csdn.net/images/p_blog_csdn_net/huanghongbo/mss.jpg"><br>&nbsp;<br>&nbsp;数据：用户通过TDP传输的数据。<br>&nbsp;<br>&nbsp;2．TDP连接建立与终止<br>&nbsp;<br>&nbsp;TDP的连接建立与终止可以参考TCP的状态变迁图(此图的详细解释请参考《TCP/IP详解 卷一》第18章)，如下：<br>&nbsp;<img alt="" src="http://p.blog.csdn.net/images/p_blog_csdn_net/huanghongbo/tcpstatus.gif"><br>&nbsp;<br>&nbsp;2.1连接建立<br>&nbsp;<br>&nbsp;2.1.1三次握手<br>&nbsp;连接建立分要经过三次握手过程：1)客户端发送一个SYN段到指明客户打算连接的服务器的端口，报文段中要设置客户端初始序号。2）服务器发回包含服务器的初始序号的SYN报文段作为应答。同时，将确认序号设置为客户的初始序号加1,并设置ACK位标志报文段为确认报文段。3）客户端必须将确认序号设置为服务器初始序号加1,对服务器的SYN报文段进行确认。<br>&nbsp;TDP在全局维护一个初始序号种子，这个初始序号为随时产生的32位整数。<br>&nbsp;连接建立的超时和重传初始值为3秒，超时采用指数退避算法，3秒超时后超时值为6秒，然后是12秒，24秒&#8230;&#8230;。连接建立最长时间限制为75秒。<br>&nbsp;<br>&nbsp;2.1.2 NAT UDP PUNCH模式<br>&nbsp;当TDP工作模式是NAT UDP PUNCH时，在三次握手之前，向对端NAT端口及预测端口间隔默认2ms发送默认为10个LIV报文段，一来用于打开自已的NAT端口，二来是用于进入对端NAT端口。默认值可以由用户程序设置。这时的LIV报文段中初始序号及确认序号都为0。<br>&nbsp;当接收到对端LIV报文段后，立即停止LIV报文段发送，发出SYN报文段进行连接建立。这时有两种可能：其一是另一端直到接收到该SYN报文段之前，都没有接收到LIV报文段，或是刚接收到但没有来得及发送SYN报文段，此时将会如上文描述的正常模式下连接建立的过程一致，将经历三次握手。基二是另一端在接收到该SYN报文段之前，也已经发送出SYN报文段，此时双方都需要对SYN报文段进行确认，可以称之为四次握手。
<p style="FONT-SIZE: 12pt">&nbsp;2.1.3 最大传输报文大小（MSS）<br>&nbsp;TCP报文段在连接建立时需要通报MSS，在TDP的实现中也进行通报，默认通报为1460字节（符合以太网标准，这个默认值允许20字节的IP首部、8字节的UDP首部和12字节的TDP首部，以适合 1500字节的IP数据报）默认值可以由用户程序设置。<br>&nbsp;TCP在对端地址为非本地IP时，默认通报为536字节。TDP之所以默认通报为1460，是因为TDP在数据传输过程中，实现了路径MTU发现技术，通过实际发现的MTU，进行MSS的动态调整，以尽量避免报文段在网络中的传输产生分片的情况。路径MTU发现技术在传输数据流一节中进行描述。<br>&nbsp;<br>&nbsp;2.1.4 半打开连接及连接保活<br>&nbsp;半打开连接是指对端异常关闭，如网线拔掉、突然断电等情况将引发一端导演关闭，而另一端的连接却仍然认为连接处于打开当中，这种情况称之为半打开连接。TDP中的一个TDP SOCKET描述符由本地IP、本地端口、远端IP、远端端口唯一确定。当远端客户端连接请求到来时，服务端将接收到一个新的TDP SOCKET描述符，当这一个描述符唯一确定信息已经存在时，对新的连接请求发送RST报文段，通知其重置连接请求。对于旧的连接，由保活机制自动发现是否为半打开连接，如果是半打开连接，则自动关闭该连接。这里RST报文段与TCP中的RST报文段有些不一样，TCP的RST报文段工作描述请参考《TCP/IP详解 卷一》。<br>&nbsp;连接建立之后，TDP连接需要启动保活机制。TCP连接在没有数据通信的情况下也能保持连接，但TDP连接不行。TDP连接在一定时间段内如果没有数据交互的话，将主动发送保活LIV报文段。这个时间段根据TDP连接工作模块不同有所差异，在NAT UDP PUNCH模式下，这个时间段默认值为1分钟（大多数的NAT中，UDP会话超时时间为2－5分钟左右）；而在常规模块下这个时间段默认值为5分钟。默认值可以由用户程序设置，用户程序需要指明两种模块下的保活时间周期。这里TDP的保活机制与TCP中的保活机制完全不一样，TCP的保活机制描述请参考《TCP/IP详解 卷一》。<br>&nbsp;<br>&nbsp;2.2连接关闭<br>&nbsp;<br>&nbsp;TDP连接与TCP连接一样是全双工的，因此每个方向必须单独地进行关闭。客户机给服务器一个FIN报文段，然后服务器返回给客户端一个确认ACK报文，并且发送一个FIN报文段，当客户机回复ACK报文后（四次握手），连接就结束了。<br>&nbsp;TDP连接的一端接收到FIN报文段时，如果还有数据要发送，需要继续将数据进行发送完成，然后才发出FIN报文段；如果还有数据未从缓存中取出，将取出数据，并进行确认，直到所有确认完成之后，然后才发出FIN报文段（此时如果有乱序的报文段情况不进行处理）。上面的描述也表现出，TDP是支持半关闭的，当一端发出FIN报文段时，仍然允许接收另一端数据。但是半关闭可能导致连接永远停留在状态图中FIN_WAIT_2状态中，此时保活机制仍然在工作当中，如果对端已经关闭，那么保活机制将在检测到时立即关闭这一连接。<br>&nbsp;<br>&nbsp;下图是一个典型的连接建立与连接关闭的示意图，此图摘自《TCP/IP详解 卷一》。<br>&nbsp;<br>&nbsp;<img alt="" src="http://p.blog.csdn.net/images/p_blog_csdn_net/huanghongbo/synfin.JPG"><br>&nbsp;<br>四、TDP传输数据流<br>&nbsp;<br>&nbsp;1．传输的报文段<br>&nbsp;<br>&nbsp;在TDP工作过程中传输的所有报文段，只有SYN报文段、FIN报文段、数据报文段是可靠的之外，其它报文段如ACK报文段、LIV报文段、RST报文段等都不是可靠的。SYN报文段与FIN报文段传输中都占用一个序号，数据报文段在传输中根据传输的数据字节数占用相应的序号，其它报文段不占用传输序号。<br>&nbsp;成功接收数据报文段，应当将按序对下一个期望的数据报文段的序号作为确认序号发送ACK报文段进行确认。当出现接收到乱序的数据报文段时，将乱序数据报文段按序缓存，并发送期望报文段的ACK报文段进行确认。ACK报文段的发送并非即时的，也并非是对应接收数据报进行一对一确认发送。ACK报文段由200ms定时触发发送，也就是说ACK报文段要经受最多200ms的时延进行发送。ACK报文段对此时期望的数据序号进行确认，因此并不是与接收数据报相对应。ACK报文段是不可靠的，当丢失时对端将无法了解接收情况，因此发送方将会有一个超时机制，如果发现确认的ACK报文段超时，发送方将重发该数据报，这一点在第五节进行详细描述。<br>&nbsp;<br>&nbsp;2．路径MTU发现及MSS通告<br>&nbsp;<br>&nbsp;前面已经提到要在连接建立过程中会通告初始MSS，这个值可以由用户程序进行设置。但这个初始值是一个静态的。当通信的两个端点之间跨越多个网络时，使用设置的MSS进行报文段发送时，可能导致传输的IP报文分片情况的产生。为了避免分片情况的产生，TDP在数据传输过程中进行动态的路径MTU发现，并进行MSS的更新及通告。<br>&nbsp;TDP创建UDP SOCKET时，即将描述符设置IP选项为不允许进行分片（setsockopt (clientSock, IPPROTO_IP, IP_DONTFRAGMENT,(char*)&amp;dwFlags, sizeof(dwFlags))）。在发送数据时以当前MSS大小值进行数据发送，如果返回值为错误码WSAEMSGSIZE（10040）表示为报文段尽寸大于MTU，需要进行IP分片传输。此时，缩减MSS大小再次进行报文段发送，直至不再返回错误码WSAEMSGSIZE（10040）。当MSS变更并能成功发送报文段后，需要向对端通报新的MSS值。每次MSS缩小后，默认隔30秒，TDP将默认扩大MSS大小，以检查是否路径MTU增大了（默认值可以由用户程序设置），之后隔30*2秒、30*2*2秒进行检测，如果三次都未发现MTU增大则停止进行检测。见RFC1191描述，网络中MTU值的个数是有限的，如下图描述（摘自RFC1191）。因此MSS的扩大及缩减，可依据一些由近似值按序构成的表，依照此表索引进行MSS值的扩大与缩减计算。<br>&nbsp;<img alt="" src="http://p.blog.csdn.net/images/p_blog_csdn_net/huanghongbo/mtulist.JPG"><br>&nbsp;TDP中MSS与MTU之间关系的计算公式如下：<br>&nbsp;MSS = MTU &#8211; 20(IP首部) &#8211; 8(UDP首部) &#8211; 12(TDP首部)。<br>&nbsp;<br>&nbsp;3．Nagle算法<br>&nbsp;<br>&nbsp;有些人误认为经受时延的捎带ACK发送是Nagle算法，其实不是。经受时延的捎带ACK发送是TCP的通常实现，在TDP中也是如此。而Nagle算法是要求一个TCP（TDP也是如此）连接上最多只能有一个未被确认的未完成的报文段，在该报文段的确认到达之前不能发送其他的报文段。相反，TCP（TDP也是如此）在这个时候收集这些报文段，关在确认到来时合并作为一个报文段发送出去。Nagle算法对于处理应用程序产生大量小报文段的情况，有利于避免网络中由于发送太多的包而过载（这便是发送端的糊涂窗口综合症，关于糊涂窗口综合症在下文将做更详细描述）。<br>&nbsp;Nagle算法适用于产生大量小报文段的情况，但有时我们需要关闭Nagle算法。一个典型的例子是X窗口系统服务器：小消息（鼠标移动）必须无时延地发送，以便为进行某种操作的交互用户提供实时的反馈。<br>&nbsp;默认的TDP实现中Nagle算法是关闭的，用户程序可以设置打开它。<br>&nbsp;<br>&nbsp;4．窗口大小通告与滑动窗口<br>&nbsp;<br>&nbsp;双方接收模块需要依据各自的缓冲区大小，相互通告还能接受对方数据的尺寸。双方发送模块则必须根据对方通告的接收窗口大小，进行数据发送。这种机制称之谓滑动窗口，它是TDP接收方的流量控制方法。它允许发送方在停止并等待确认前可以连续发送多个分组（依据滑动窗口的大小），由于发送方不必每发一个分组就停下来等待确认，因此可以加速数据的传输。<br>&nbsp;参照《TCP/IP详解 卷一 20.3滑动窗口》一节，滑动窗口在排序数据流上不时的向右移动，窗口两个边沿的相对运动增加或减少了窗口的大小，关于窗口边沿的运动有三个术语：窗口合拢（当左边沿向右边沿靠近）、窗口张开（当右边沿向右移动）、窗口收缩（当右边沿向左移动）。RFC文档强烈建议不要在实现当中出现窗口收缩的情况出现，在我们的实现中也将不会出现。<br>&nbsp;当遇到快的发送方与慢的接收方的情况时，接收方的窗口会很快被发送方的数据填满，此时接收方将通告窗口大小为0,发送方则停止发送数据。直到接收方用户程序取走数据后更新窗口大小，发送方可以继续发送数据；另外，因为ACK报文段有可能丢失，发送方可能没有成功接收到更新的窗口大小，因此发送方将启动一个坚持定时器，当坚持定时器超时，发送方将发送一个字节的数据到接收方，尝试检查窗口大小的更新。<br>&nbsp;在Nagle算法中接到过糊涂窗口综合症，在这里要进一步进行描述。糊涂窗口综合症是指众多少量数据的报文段将通过连接进行交换，而不是满长度的报文段，这将导致连接占用过多带宽，降低传输速率。糊涂窗口综合症产生是分两端的，接收方可以通告一个小的窗口（而不是一直等到有大的窗口时才通告），发送方也可以发送少量的数据（而不是等待其他的数据以便发送一个大的报文段）。要以采用如下方法避免这一现象：<br>&nbsp;1）接收方不通告小窗口。通常的算法是接收方不通告一个比当前窗口大的窗口（可以为0），除非窗口可以增加一个报文段大小(也就是将要接收的MSS)或者可以增加缓存空间的一半，不论实际有多少。<br>&nbsp;2）发送方避免出现糊涂窗口综合症的措施是只有以下条件之一满足时才发送数据：(a)可以发送一个满长度的报文段；(b)可以发送至少是接收方通告窗口大小一半的报文段；(c)可以发送任何数据并且不希望接收ACK（也就是说，我们没有还未被确认的数据）或者该连接上不能使用Nagle算法。<br>&nbsp;<br>&nbsp;5．PUSH标志<br>&nbsp;<br>&nbsp;PSUH标志的作用是发送方使用PUSH标志通知接收方将所收到的数据全部提交给接收进程。在TDP实现中，用户程序并不需要关心PUSH标志。因为TDP实现从不将接收到的数据推迟交付给用户程序，因此这个标志在TDP的实现中是被忽略的。<br>&nbsp;<br>五、TDP超时与重传<br>&nbsp;<br>&nbsp;1．带宽时延乘积与拥塞<br>&nbsp;<br>&nbsp;每个网络通道都有一定的容量，可以计算通道的容量大小：<br>&nbsp;Capacity(bit) = bandwidth(b/s) * round-trip time(s)<br>&nbsp;这个值一般称之为带宽时延乘积。这个值依赖于网络速度和两端的RTT，可以有很大的变动。不论是带宽还是时延均会影响发送方与接收方之间通路的容量。<br>&nbsp;当数据到达一个大的网络通道并向一个小的网络通道发送，将发生拥塞现象。另外当多个输入流到达一个路由器，而路由器的输出流小于这些输入流的总和时也会发生拥塞。TDP超时与重传机制刚采用TCP的拥塞控制算法来进行发送端的流量控制。<br>&nbsp;<br>&nbsp;2．往返时间与重传超时时间测量<br>&nbsp;<br>&nbsp;超时与重传中最重要的部分就是对一个给定连接的往返时间（RTT）的测量。由于路由器和网络流量均会发生变化，因此一般认为RTT可能经常会发生变化，TDP应该跟踪这些变化并相应地改变相应的超时时间。<br>&nbsp;首先是必须测量在发送一个带有特别序号的字节和接收到包含字节的确认之间的RTT。由于数据报文段与ACK之间通常没有一一对应的关系，如下图（摘自《TCP/IP详解 卷一》图20.1）中，这意味着发送方可以测量到的一个RTT，是在发送报文段4和接收报文段7之间的时间，用M表示所测量到的RTT。<br>&nbsp;根据[Jacobson 1988]描述（见《TCP/IP详解 卷一》参考文献），用A表示被平滑的RTT（均值估计器），用D表示被平滑的均值偏差，用Err表示刚得到的测量结果M与当前RTT估计器之差，则可以计算下一个超时重传时间（用RTO表示下一个超时重传时间）。<br>&nbsp;A = 0 （未进行测量往返时间之前，A的初始值）<br>&nbsp;D = 3 （未进行测量往返时间之前，D的初始值）<br>&nbsp;RTO = A + 2D = 6 （未进行测量往返时间之前，RTO的初始值）<br>&nbsp;A = M + 0.5 (第一次测量到往返时间结果，对RTT估计器计算初始值)<br>&nbsp;D = A / 2 （第一次测量到往返时间结果，对均值偏差D计算初始值）<br>&nbsp;RTO = A + 4D （第一次测量到往返时间结果，对均值偏差RTO计算初始值）<br>&nbsp;之后的计算方法如下：<br>&nbsp;Err = M &#8211; A<br>&nbsp;A &lt;- A + gErr<br>&nbsp;D &lt;- D + h(|Err| - D)<br>&nbsp;RTO = A + 4D<br>&nbsp;其中g是常量增量，取值为1/8(0.125)；h也是常量增量，取值为1/4(0.25)。<br>&nbsp;<br>&nbsp;<img alt="" src="http://p.blog.csdn.net/images/p_blog_csdn_net/huanghongbo/RTT.JPG"><br>&nbsp;<br>&nbsp;Karn算法：Karn算法是解决所谓的重传多义性问题的。[Karn and Partridge 1987]规定（见《TCP/IP详解 卷一》参考文献），当一个超时和重传发生时，在重传数据的确认最后到达之前，不能更新RTT估计器，因为我们并不知道ACK对应哪次传输（也许第一次传输被延迟而并没有被丢弃，也有可能是第一次传输的ACK被延迟丢弃）。并且，由于数据被重传，RTO已经得到了一个指数退避，我们在下一次传输时使用这个退避后的RTO。对一个没有被重传的报文段而言，除非收到了一个确认，否则不要计算新的RTO。<br>&nbsp;在任何时候对每个连接并行仅测量一次RTT值，在发送一个报文段时，如果给定连接的定时器已经被使用，则该报文段不被计时，反之如果给定连接的定时器未被使用，则开始计时以测量RTT值。即并非每个发出报文段都进行测量RTT值，同一时间段里只能有一个RTT值测量行为进行，不会并行进行多个RTT值测量。<br>&nbsp;<br>&nbsp;3．慢启动<br>&nbsp;<br>&nbsp;如果发送方一开始便向网络发送多个报文段，直至达到接收方通告窗口大小为止。当发送方与接收方在同一局域网时，这种方式是可以的。但如果在发送方与接收方之间存在多个路由器和速率较慢的链路时，就可能出现问题。一些中间路由器必须缓存分组，并有可能耗尽存储器的空间，将来得降低TCP连接的吞吐量。于是需要一种叫&#8220;慢启动&#8221;的拥塞控制算法。<br>&nbsp;慢启动为发送方增加一个拥塞窗口，记为cwnd，当与另一个网络的主机建立连接时，拥塞窗口被初始化为1个报文段。每收到一个ACK，拥塞窗口就增加一个报文段（cwnd以字节为单位，但慢启动以报文段大小为单位进行增加）。发送方取拥塞窗口与通告窗口中的最小值作为发送上限。拥塞窗口是发送方使用的流量控制，而通告窗口是接收方使用的流量控制。<br>&nbsp;发送方开始时发送一个报文段，然后等待ACK。当收到该ACK时，拥塞窗口从1增加到2,即可以发送两个报文段。当收到这两个报文段的ACK时，拥塞窗口就增加为4。这是一种指数增加的关系。<br>&nbsp;<br>&nbsp;4．拥塞避免<br>&nbsp;<br>&nbsp;慢启动算法增加拥塞窗口大小到某些点上可能达到了互联网的容量，于是中间路由器开始丢弃分组。这就通知发送方它的拥塞窗口开得太大。拥塞避免算法是一种处理丢失分组的方法。该算法假定由于分组受到损坏引起的丢失是非常少的（远小于1％），因此分组丢失就意味着在源主机和目标主机之间的某处网络上发生了拥塞。有两种分组丢失的指示：发生超时和接收到重复的确认。拥塞避免算法与慢启动算法是两个独立的算法，但实际中这两个算法通常在一起实现。<br>&nbsp;拥塞避免算法和慢启动算法需要对每个连接维持两个变量：一个拥塞窗口cwnd和一个慢启动门限ssthresh。算法的工作过程如下：<br>&nbsp;1) 对一个给定的连接，初始化cwnd为1个报文段，ssthresh为65535个字节。<br>&nbsp;2) TCP输出例程的输出不能超过cwnd和接收方通告窗口的大小。拥塞避免是发送方使用的流量控制，而通告窗口则是接收方进行的流量控制。前者是发送方感受到的网络拥塞的估计，而后者则与接收方在该连接上的可用缓存大小有关。<br>&nbsp;3) 当拥塞发生时（超时或收到重复确认），ssthresh被设置为当前窗口大小的一半（cwnd和接收方通告窗口大小的最小值，但最少为2个报文段）。此外，如果是超时引起了拥塞，则cwnd被设置为1个报文段（这就是慢启动）。<br>&nbsp;4) 当新的数据被对方确认时，就增加cwnd，但增加的方法依赖于我们是否正在进行慢启动或拥塞避免。如果cwnd小于或等于ssthresh，则正在进行慢启动，否则正在进行拥塞避免。慢启动一直持续到我们回到当拥塞发生时所处位置的半时候才停止（因为我们记录了在步骤2中给我们制造麻烦的窗口大小的一半），然后转为执行拥塞避免。<br>&nbsp;慢启动算法初始设置cwnd为1个报文段，此后每收到一个确认就加1。这会使窗口按指数方式增长：发送1个报文段，然后是2个，接着是4个&#8230;&#8230;。拥塞避免算法要求每次收到一个确认时将cwnd增加1/cwnd。与慢启动的指数增加比起来，这是一种加性增长。我们希望在一个往返时间内最多为cwnd增加1个报文段（不管在这个RT T中收到了多少个ACK），然而慢启动将根据这个往返时间中所收到的确认的个数增加cwnd。<br>&nbsp;处于拥塞避免状态时，拥塞窗口的计算公式如下（引公式参照BSD的实现，segsize/8的值是一个匹配补充量，不在算法描述当中）：<br>&nbsp;cwnd &lt;- cwnd + segsize * segsize / cwnd + segsize / 8<br>&nbsp;<br>&nbsp;5．快速重传与快速恢复<br>&nbsp;<br>&nbsp;由于我们不知道一个重复的ACK是由一个丢失的报文段引起的，还是由于仅仅出现了几个报文段的重新排序，因此我们等待少量重复的ACK到来。假如这只是一些报文段的重新排序，则在重新排序的报文段被处理并产生一个新的ACK之前，只可能产生1 ~ 2个重复的ACK。如果一连串收到3个或3个以上的重复ACK，就非常可能是一个报文段丢失了。于是我们就重传丢失的数据报文段，而无需等待超时定时器溢出。这就是快速重传算法。接下来执行的不是慢启动算法而是拥塞避免算法。这就是快速恢复算法。<br>&nbsp;这个算法通常按如下过程进行实现：<br>&nbsp;1) 当收到第3个重复的ACK时，将ssthresh设置为当前拥塞窗口cwnd的一半。重传丢失的报文段。设置cwnd为ssthresh加上3倍的报文段大小。<br>&nbsp;2) 每次收到另一个重复的ACK时，cwnd增加1个报文段大小并发送1个分组（如果新的cwnd允许发送）。<br>&nbsp;3) 当下一个确认新数据的ACK到达时，设置cwnd为ssthresh（在第1步中设置的值）。这个ACK应该是在进行重传后的一个往返时间内对步骤1中重传的确认。另外，这个ACK也应该是对丢失的分组和收到的第1个重复的A C K之间的所有中间报文段的确认。这一步采用的是拥塞避免，因为当分组丢失时我们将当前的速率减半。<br>&nbsp;<br>六、代理socks5支持<br>&nbsp;<br>&nbsp;参照RFC1928、RFC1929，在TDP实现中，支持匿名通过socks5代理以及用户名/密码验证方式通过socks5代理。<br>&nbsp;由于socks5代理是工作于运输层上，因此连接当中对IP层选项的设置都将没有效果。socks5代理起到的作用只是应用数据的转发，但这已经基本上能支持大部分用户程序的应用需求。在使用socks5代理进行工作中，路径MTU的发现机制，将无法有效工作，此时MSS默认为536（MTU默认为576）,用户程序可以修改使用的MSS值。<br>&nbsp;<br>七、安全考虑<br>&nbsp;<br>&nbsp;TDP协议及算法方面并不对数据的安全性做任何考虑，用户程序在传输数据时如果对安全性有要求，可以自行在应用数据层做相应的工作。但TDP实现中，会提供一个简单的AES256位加解密方法，提供给用户程序使用。用户程序可以调用该加解密方法，对数据进行加密然后再通过网络进行发送，接收时将加密数据流进行解密再将会用户程序数据逻辑处理模块进行处理。<br>&nbsp;<br>八、定时器<br>&nbsp;<br>&nbsp;如BSD的TCP实现类似，TDP也为每条连接建立了六个定时器，简要介绍如下：<br>&nbsp;1）&#8220;连接建立&#8221;定时器，在发送SYN报文段建立一条新的连接时启动。如果没有在75秒内收到响应，连接建立将中止。<br>&nbsp;2）&#8220;重传&#8221;定时器，在发送数据时设定。如果定时器已超时而对端的确认还未到达，将重传数据。重传定时器的值是动态计算的，取决来RTT与该报文段被重传的次数。<br>&nbsp;3）&#8220;延迟ACK&#8221;定时器，收到必须确认但无需马上发出确认的数据时设定。等待200ms后发送确认响应。如果，在这200ms内，有数据要在该连接上发送，延迟的ACK响应就可随数据一起发送回对端，称为捎带确认。<br>&nbsp;4）&#8220;坚持&#8221;定时器，在连接对端通告接收窗口为0,阻止继续发送数据时设定。坚持定时器在超时后向对端发送1字节的数据，判定对端接收窗口是否已经打开。坚持定时器的值是动态的计算的，取决于RTT值，在5秒与60秒之间取值。<br>&nbsp;5）&#8220;保活&#8221;定时器。TDP连接在一定时间段内如果没有数据交互的话，将主动发送保活LIV报文段。即当&#8220;保活&#8221;定时器超时，说明没有数据交互，则发送保活数据包。保活定时器默认时间为2分钟，用户程序可以进行设置。<br>&nbsp;6）TIME_WAIT定时器,也可称为2MSL定时器（实现中，一个MSL为1分钟）。当连接状态转移到TIME_WAIT时，即连接主动关闭时，定时器启动。<br>&nbsp;<br>九、开发接口<br>&nbsp;<br>&nbsp;使用TDP进行网络程序开发是非常容易的，它的开发接口（API）与socket API是非常相似的，尤其是对应功能的函数名称都是一致的，需要注意的是TDP的所有API都处于名称空间TDP之下。开发接口见下表：<br>&nbsp;<br>函数&nbsp;描述&nbsp;<br>TDP::accept&nbsp;接受一个链接&nbsp;<br>TDP::bind&nbsp;绑定本地地址到一个TDP::SOCKET句柄&nbsp;<br>TDP::cleanup&nbsp;清除TDP全局资源，一个进程中只需要调用一次&nbsp;<br>TDP::close&nbsp;关闭已打开的TDP::SOCKET句柄，并关闭连接&nbsp;<br>TDP::connect&nbsp;连接到服务器端&nbsp;<br>TDP::getlasterror&nbsp;获得TDP最后的一个错误&nbsp;<br>TDP::getpeername&nbsp;读取连接的对端的地址信息&nbsp;<br>TDP::getsockname&nbsp;读取连接的本地的地址信息&nbsp;<br>TDP::getsockopt&nbsp;读取TDP的选项信息&nbsp;<br>TDP::listen&nbsp;等待客户端来连接&nbsp;<br>TDP::recv&nbsp;接收数据&nbsp;<br>TDP::select&nbsp;等待集合中的TDP SOCKET改变状态&nbsp;<br>TDP::send&nbsp;发送数据&nbsp;<br>TDP::setsockopt&nbsp;修改TDP的选项信息&nbsp;<br>TDP::shutdown&nbsp;指定关闭连接上双工通信的部分或全部&nbsp;<br>TDP::socket&nbsp;创建一个TDP SOCKET&nbsp;<br>TDP::startup&nbsp;初始化TDP全局信息，一个进程中只需要调用一次&nbsp;<br>&nbsp;</p>
<img src ="http://www.cppblog.com/jack-wang/aggbug/112342.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/jack-wang/" target="_blank">小王</a> 2010-04-12 16:35 <a href="http://www.cppblog.com/jack-wang/archive/2010/04/12/112342.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>TCP短连接与长连接</title><link>http://www.cppblog.com/jack-wang/archive/2010/03/24/110419.html</link><dc:creator>小王</dc:creator><author>小王</author><pubDate>Wed, 24 Mar 2010 03:32:00 GMT</pubDate><guid>http://www.cppblog.com/jack-wang/archive/2010/03/24/110419.html</guid><wfw:comment>http://www.cppblog.com/jack-wang/comments/110419.html</wfw:comment><comments>http://www.cppblog.com/jack-wang/archive/2010/03/24/110419.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/jack-wang/comments/commentRss/110419.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/jack-wang/services/trackbacks/110419.html</trackback:ping><description><![CDATA[<p style="FONT-SIZE: 10pt">&nbsp;&nbsp;<br>&nbsp;&nbsp;</p>
<p style="FONT-SIZE: 10pt">刚接触TCP/IP通信设计的人根据范例可以很快编出一个通信程 <br>序，据此一些人可能会认为TCP/IP编程很简单。其实不然， <br>TCP/IP编程具有较为丰富的内容。其编程的丰富性主要体现在 <br>通信方式和报文格式的多样性上。 </p>
<p style="FONT-SIZE: 10pt"><br>一。通信方式 </p>
<p style="FONT-SIZE: 10pt">主要有以下三大类: </p>
<p style="FONT-SIZE: 10pt">(一)SERVER/CLIENT方式 </p>
<p style="FONT-SIZE: 10pt">1.一个Client方连接一个Server方，或称点对点(peer to peer)： <br>2.多个Client方连接一个Server方，这也是通常的并发服务器方式。 <br>3.一个Client方连接多个Server方，这种方式很少见，主要 <br>用于一个客户向多个服务器发送请求情况。 </p>
<p style="FONT-SIZE: 10pt"><br>(二)连接方式 </p>
<p style="FONT-SIZE: 10pt">1.长连接 </p>
<p style="FONT-SIZE: 10pt">Client方与Server方先建立通讯连接，连接建立后不断开， <br>然后再进行报文发送和接收。这种方式下由于通讯连接一直 <br>存在，可以用下面命令查看连接是否建立： </p>
<p style="FONT-SIZE: 10pt">netstat &#8211;f inet|grep 端口号(如5678)。 </p>
<p style="FONT-SIZE: 10pt">此种方式常用于点对点通讯。 </p>
<p style="FONT-SIZE: 10pt"><br>2.短连接 </p>
<p style="FONT-SIZE: 10pt">Client方与Server每进行一次报文收发交易时才进行通讯连 <br>接，交易完毕后立即断开连接。此种方式常用于一点对多点 <br>通讯，比如多个Client连接一个Server. </p>
<p style="FONT-SIZE: 10pt"><br>(三)发送接收方式 </p>
<p style="FONT-SIZE: 10pt">1.异步 </p>
<p style="FONT-SIZE: 10pt">报文发送和接收是分开的，相互独立的，互不影响。这种方 <br>式又分两种情况： </p>
<p style="FONT-SIZE: 10pt">(1)异步双工：接收和发送在同一个程序中，有两个不同的 <br>子进程分别负责发送和接收 <br>(2)异步单工：接收和发送是用两个不同的程序来完成。 </p>
<p style="FONT-SIZE: 10pt">2.同步 </p>
<p style="FONT-SIZE: 10pt">报文发送和接收是同步进行，既报文发送后等待接收返回报文。 <br>同步方式一般需要考虑超时问题，即报文发上去后不能无限等 <br>待，需要设定超时时间，超过该时间发送方不再等待读返回报 <br>文，直接通知超时返回。&nbsp; </p>
<p style="FONT-SIZE: 10pt"><br>实际通信方式是这三类通信方式的组合。比如一般书上提供的 <br>TCP/IP范例程序大都是同步短连接的SERVER/CLIENT程序。有的 <br>组合是基本不用的，比较常用的有价值的组合是以下几种： </p>
<p style="FONT-SIZE: 10pt">同步短连接Server/Client <br>同步长连接Server/Client <br>异步短连接Server/Client <br>异步长连接双工Server/Client <br>异步长连接单工Server/Client </p>
<p style="FONT-SIZE: 10pt">其中异步长连接双工是最为复杂的一种通信方式，有时候经 <br>常会出现在不同银行或不同城市之间的两套系统之间的通信。 <br>比如金卡工程。由于这几种通信方式比较固定，所以可以预 <br>先编制这几种通信方式的模板程序。 </p>
<p style="FONT-SIZE: 10pt"><br>二.报文格式 </p>
<p style="FONT-SIZE: 10pt">通信报文格式多样性更多，相应地就必须设计对应的读写报文的接 <br>收和发送报文函数。 </p>
<p style="FONT-SIZE: 10pt">(一)阻塞与非阻塞方式　 </p>
<p style="FONT-SIZE: 10pt">1.非阻塞方式 </p>
<p style="FONT-SIZE: 10pt">读函数不停地进行读动作，如果没有报文接收到，等待一段时间后 <br>超时返回，这种情况一般需要指定超时时间。 </p>
<p style="FONT-SIZE: 10pt">2.阻塞方式 </p>
<p style="FONT-SIZE: 10pt">如果没有报文接收到，则读函数一直处于等待状态，直到有报文到达。 </p>
<p style="FONT-SIZE: 10pt">&nbsp;</p>
<p style="FONT-SIZE: 10pt">(二)循环读写方式 <br>&nbsp; </p>
<p style="FONT-SIZE: 10pt">1.一次直接读写报文 </p>
<p style="FONT-SIZE: 10pt">在一次接收或发送报文动作中一次性不加分别地全部读取或全部 <br>发送报文字节。 </p>
<p style="FONT-SIZE: 10pt">2.不指定长度循环读写 </p>
<p style="FONT-SIZE: 10pt">这一般发生在短连接进程中，受网络路由等限制，一次较长的报 <br>文可能在网络传输过程中被分解成了好几个包。一次读取可能不 <br>能全部读完一次报文，这就需要循环读报文，直到读完为止。 </p>
<p style="FONT-SIZE: 10pt">3.带长度报文头循环读写 </p>
<p style="FONT-SIZE: 10pt">这种情况一般是在长连接进程中，由于在长连接中没有条件能够 <br>判断循环读写什么时候结束，所以必须要加长度报文头。读函数 <br>先是读取报文头的长度，再根据这个长度去读报文.实际情况中， <br>报头的码制格式还经常不一样，如果是非ASCII码的报文头，还必须 <br>转换成ASCII,常见的报文头码制有： <br>(1)n个字节的ASCII码 <br>(2)n个字节的BCD码 <br>(3)n个字节的网络整型码 </p>
<p style="FONT-SIZE: 10pt">&nbsp;</p>
<p style="FONT-SIZE: 10pt">以上是几种比较典型的读写报文方式，可以与通信方式模板一起 <br>预先提供一些典型的API读写函数。当然在实际问题中，可能还 <br>必须编写与对方报文格式配套的读写API. </p>
<p style="FONT-SIZE: 10pt">在实际情况中，往往需要把我们自己的系统与别人的系统进行连接， <br>有了以上模板与API,可以说连接任何方式的通信程序都不存在问题。 </p>
<p style="FONT-SIZE: 10pt"><br>本文来自CSDN博客，转载请标明出处：<a href="http://blog.csdn.net/wgl_suc102/archive/2008/01/23/2060828.aspx">http://blog.csdn.net/wgl_suc102/archive/2008/01/23/2060828.aspx</a></p>
<img src ="http://www.cppblog.com/jack-wang/aggbug/110419.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/jack-wang/" target="_blank">小王</a> 2010-03-24 11:32 <a href="http://www.cppblog.com/jack-wang/archive/2010/03/24/110419.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>拼包函数及网络封包的异常处理</title><link>http://www.cppblog.com/jack-wang/archive/2010/02/18/107993.html</link><dc:creator>小王</dc:creator><author>小王</author><pubDate>Wed, 17 Feb 2010 18:27:00 GMT</pubDate><guid>http://www.cppblog.com/jack-wang/archive/2010/02/18/107993.html</guid><wfw:comment>http://www.cppblog.com/jack-wang/comments/107993.html</wfw:comment><comments>http://www.cppblog.com/jack-wang/archive/2010/02/18/107993.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/jack-wang/comments/commentRss/107993.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/jack-wang/services/trackbacks/107993.html</trackback:ping><description><![CDATA[<p style="FONT-SIZE: 10pt">拼包函数及网络封包的异常处理(含代码) 收藏 <br>本文作者：sodme<br>本文出处：<a href="http://blog.csdn.net/sodme">http://blog.csdn.net/sodme</a><br>声明：本文可以不经作者同意任意转载、复制、传播，但任何对本文的引用都请保留作者、出处及本声明信息。谢谢！</p>
<p style="FONT-SIZE: 10pt">　　常见的网络服务器，基本上是7*24小时运转的，对于网游来说，至少要求服务器要能连续工作一周以上的时间并保证不出现服务器崩溃这样的灾难性事件。事实上，要求一个服务器在连续的满负荷运转下不出任何异常，要求它设计的近乎完美，这几乎是不太现实的。服务器本身可以出异常（但要尽可能少得出），但是，服务器本身应该被设计得足以健壮，&#8220;小病小灾&#8221;打不垮它，这就要求服务器在异常处理方面要下很多功夫。</p>
<p style="FONT-SIZE: 10pt">　　服务器的异常处理包括的内容非常广泛，本文仅就在网络封包方面出现的异常作一讨论，希望能对正从事相关工作的朋友有所帮助。</p>
<p style="FONT-SIZE: 10pt">　　关于网络封包方面的异常，总体来说，可以分为两大类：一是封包格式出现异常；二是封包内容（即封包数据）出现异常。在封包格式的异常处理方面，我们在最底端的网络数据包接收模块便可以加以处理。而对于封包数据内容出现的异常，只有依靠游戏本身的逻辑去加以判定和检验。游戏逻辑方面的异常处理，是随每个游戏的不同而不同的，所以，本文随后的内容将重点阐述在网络数据包接收模块中的异常处理。</p>
<p style="FONT-SIZE: 10pt">　　为方便以下的讨论，先明确两个概念（这两个概念是为了叙述方面，笔者自行取的，并无标准可言）：<br>　　1、逻辑包：指的是在应用层提交的数据包，一个完整的逻辑包可以表示一个确切的逻辑意义。比如登录包，它里面就可以含有用户名字段和密码字段。尽管它看上去也是一段缓冲区数据，但这个缓冲区里的各个区间是代表一定的逻辑意义的。<br>　　2、物理包：指的是使用recv(recvfrom)或wsarecv(wsarecvfrom)从网络底层接收到的数据包，这样收到的一个数据包，能不能表示一个完整的逻辑意义，要取决于它是通过UDP类的&#8220;数据报协议&#8221;发的包还是通过TCP类的&#8220;流协议&#8221;发的包。</p>
<p style="FONT-SIZE: 10pt">　　我们知道，TCP是流协议，&#8220;流协议&#8221;与&#8220;数据报协议&#8221;的不同点在于：&#8220;数据报协议&#8221;中的一个网络包本身就是一个完整的逻辑包，也就是说，在应用层使用sendto发送了一个逻辑包之后，在接收端通过recvfrom接收到的就是刚才使用sendto发送的那个逻辑包，这个包不会被分开发送，也不会与其它的包放在一起发送。但对于TCP而言，TCP会根据网络状况和neagle算法，或者将一个逻辑包单独发送，或者将一个逻辑包分成若干次发送，或者会将若干个逻辑包合在一起发送出去。正因为TCP在逻辑包处理方面的这种粘合性，要求我们在作基于TCP的应用时，一般都要编写相应的拼包、解包代码。</p>
<p style="FONT-SIZE: 10pt">　　因此，基于TCP的上层应用，一般都要定义自己的包格式。TCP的封包定义中，除了具体的数据内容所代表的逻辑意义之外，第一步就是要确定以何种方式表示当前包的开始和结束。通常情况下，表示一个TCP逻辑包的开始和结束有两种方式：<br>　　1、以特殊的开始和结束标志表示，比如FF00表示开始，00FF表示结束。<br>　　2、直接以包长度来表示。比如可以用第一个字节表示包总长度，如果觉得这样的话包比较小，也可以用两个字节表示包长度。</p>
<p style="FONT-SIZE: 10pt">　　下面将要给出的代码是以第2种方式定义的数据包，包长度以每个封包的前两个字节表示。我将结合着代码给出相关的解释和说明。</p>
<p style="FONT-SIZE: 10pt">　　函数中用到的变量说明：</p>
<p style="FONT-SIZE: 10pt">　　CLIENT_BUFFER_SIZE：缓冲区的长度，定义为：Const int CLIENT_BUFFER_SIZE=4096。<br>　　m_ClientDataBuf：数据整理缓冲区，每次收到的数据，都会先被复制到这个缓冲区的末尾，然后由下面的整理函数对这个缓冲区进行整理。它的定义是：char m_ClientDataBuf[2* CLIENT_BUFFER_SIZE]。<br>　　m_DataBufByteCount：数据整理缓冲区中当前剩余的未整理字节数。<br>　　GetPacketLen(const char*)：函数，可以根据传入的缓冲区首址按照应用层协议取出当前逻辑包的长度。<br>　　GetGamePacket(const char*, int)：函数，可以根据传入的缓冲区生成相应的游戏逻辑数据包。<br>　　AddToExeList(PBaseGamePacket)：函数，将指定的游戏逻辑数据包加入待处理的游戏逻辑数据包队列中，等待逻辑处理线程对其进行处理。<br>　　DATA_POS：指的是除了包长度、包类型等这些标志型字段之外，真正的数据包内容的起始位置。</p>
<p style="FONT-SIZE: 10pt">Bool SplitFun(const char* pData,const int &amp;len)<br>{<br>&nbsp;&nbsp;&nbsp; PBaseGamePacket pGamePacket=NULL;<br>&nbsp;&nbsp;&nbsp; __int64 startPos=0, prePos=0, i=0;<br>&nbsp;&nbsp;&nbsp; int packetLen=0;</p>
<p style="FONT-SIZE: 10pt">&nbsp;　//先将本次收到的数据复制到整理缓冲区尾部<br>&nbsp;&nbsp;&nbsp; startPos = m_DataBufByteCount;&nbsp; <br>&nbsp;&nbsp;&nbsp; memcpy( m_ClientDataBuf+startPos, pData, len );<br>&nbsp;&nbsp;&nbsp; m_DataBufByteCount += len;&nbsp;&nbsp;&nbsp; </p>
<p style="FONT-SIZE: 10pt">&nbsp;&nbsp;&nbsp; //当整理缓冲区内的字节数少于DATA_POS字节时，取不到长度信息则退出<br>　//注意：退出时并不置m_DataBufByteCount为0<br>&nbsp;&nbsp;&nbsp; if (m_DataBufByteCount &lt; DATA_POS+1)<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return false; </p>
<p style="FONT-SIZE: 10pt">&nbsp;&nbsp;&nbsp; //根据正常逻辑，下面的情况不可能出现，为稳妥起见，还是加上<br>&nbsp;&nbsp;&nbsp; if (m_DataBufByteCount &gt;&nbsp; 2*CLIENT_BUFFER_SIZE)<br>&nbsp;&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; //设置m_DataBufByteCount为0，意味着丢弃缓冲区中的现有数据<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; m_DataBufByteCount = 0;</p>
<p style="FONT-SIZE: 10pt">　　//可以考虑开放错误格式数据包的处理接口，处理逻辑交给上层<br>　　//OnPacketError()<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return false;<br>&nbsp;&nbsp;&nbsp; }</p>
<p style="FONT-SIZE: 10pt">&nbsp;&nbsp;&nbsp;&nbsp; //还原起始指针<br>&nbsp;&nbsp;&nbsp;&nbsp; startPos = 0;</p>
<p style="FONT-SIZE: 10pt">&nbsp;&nbsp;&nbsp;&nbsp; //只有当m_ClientDataBuf中的字节个数大于最小包长度时才能执行此语句<br>&nbsp;&nbsp;&nbsp; packetLen = GetPacketLen( pIOCPClient-&gt;m_ClientDataBuf );</p>
<p style="FONT-SIZE: 10pt">&nbsp;&nbsp;&nbsp; //当逻辑层的包长度不合法时，则直接丢弃该包<br>&nbsp;&nbsp;&nbsp; if ((packetLen &lt; DATA_POS+1) || (packetLen &gt; 2*CLIENT_BUFFER_SIZE))<br>&nbsp;&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; m_DataBufByteCount = 0;</p>
<p style="FONT-SIZE: 10pt">　　//OnPacketError()<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return false;<br>&nbsp;&nbsp;&nbsp; }</p>
<p style="FONT-SIZE: 10pt">&nbsp;&nbsp;&nbsp; //保留整理缓冲区的末尾指针<br>&nbsp;&nbsp;&nbsp; __int64 oldlen = m_DataBufByteCount; </p>
<p style="FONT-SIZE: 10pt">&nbsp;&nbsp;&nbsp; while ((packetLen &lt;= m_DataBufByteCount) &amp;&amp; (m_DataBufByteCount&gt;0))<br>&nbsp;&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; //调用拼包逻辑，获取该缓冲区数据对应的数据包<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; pGamePacket = GetGamePacket(m_ClientDataBuf+startPos, packetLen); </p>
<p style="FONT-SIZE: 10pt">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (pGamePacket!=NULL)<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; //将数据包加入执行队列<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; AddToExeList(pGamePacket);<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</p>
<p style="FONT-SIZE: 10pt">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; pGamePacket = NULL;<br>&nbsp;<br>　　//整理缓冲区的剩余字节数和新逻辑包的起始位置进行调整<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; m_DataBufByteCount -= packetLen;<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; startPos += packetLen; </p>
<p style="FONT-SIZE: 10pt">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; //残留缓冲区的字节数少于一个正常包大小时，只向前复制该包随后退出<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (m_DataBufByteCount &lt; DATA_POS+1)<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; for(i=startPos; i&lt;startPos+m_DataBufByteCount; ++i)<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; m_ClientDataBuf[i-startPos] = m_ClientDataBuf[i];</p>
<p style="FONT-SIZE: 10pt">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return true;<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</p>
<p style="FONT-SIZE: 10pt">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; packetLen = GetPacketLen(m_ClientDataBuf + startPos );</p>
<p style="FONT-SIZE: 10pt">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; //当逻辑层的包长度不合法时，丢弃该包及缓冲区以后的包<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if ((packetLen&lt;DATA_POS+1) || (packetLen&gt;2*CLIENT_BUFFER_SIZE))<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; m_DataBufByteCount = 0;</p>
<p style="FONT-SIZE: 10pt">&nbsp;&nbsp;&nbsp; 　　//OnPacketError()<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return false;<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</p>
<p style="FONT-SIZE: 10pt">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (startPos+packetLen&gt;=oldlen)<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; for(i=startPos; i&lt;startPos+m_DataBufByteCount; ++i)<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; m_ClientDataBuf[i-startPos] = m_ClientDataBuf[i];&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </p>
<p style="FONT-SIZE: 10pt">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return true;<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br>&nbsp;&nbsp;&nbsp;&nbsp; }//取所有完整的包</p>
<p style="FONT-SIZE: 10pt">&nbsp;&nbsp;&nbsp;&nbsp; return true;<br>}</p>
<p style="FONT-SIZE: 10pt">　　以上便是数据接收模块的处理函数，下面是几点简要说明：</p>
<p style="FONT-SIZE: 10pt">　　1、用于拼包整理的缓冲区(m_ClientDataBuf)应该比recv中指定的接收缓冲区(pData)长度(CLIENT_BUFFER_SIZE)要大，通常前者是后者的2倍(2*CLIENT_BUFFER_SIZE)或更大。</p>
<p style="FONT-SIZE: 10pt">　　2、为避免因为剩余数据前移而导致的额外开销，建议m_ClientDataBuf使用环形缓冲区实现。</p>
<p style="FONT-SIZE: 10pt">　　3、为了避免出现无法拼装的包，我们约定每次发送的逻辑包，其单个逻辑包最大长度不可以超过CLIENT_BUFFER_SIZE的2倍。因为我们的整理缓冲区只有2*CLIENT_BUFFER_SIZE这么长，更长的数据，我们将无法整理。这就要求在协议的设计上以及最终的发送函数的处理上要加上这样的异常处理机制。</p>
<p style="FONT-SIZE: 10pt"><br>　　4、对于数据包过短或过长的包，我们通常的情况是置m_DataBufByteCount为0，即舍弃当前包的处理。如果此处不设置m_DataBufByteCount为0也可，但该客户端只要发了一次格式错误的包，则其后继发过来的包则也将连带着产生格式错误，如果设置m_DataBufByteCount为0，则可以比较好的避免后继的包受此包的格式错误影响。更好的作法是，在此处开放一个封包格式异常的处理接口(OnPacketError)，由上层逻辑决定对这种异常如何处置。比如上层逻辑可以对封包格式方面出现的异常进行计数，如果错误的次数超过一定的值，则可以断开该客户端的连接。</p>
<p style="FONT-SIZE: 10pt">　　5、建议不要在recv或wsarecv的函数后，就紧接着作以上的处理。当recv收到一段数据后，生成一个结构体或对象(它主要含有data和len两个内容，前者是数据缓冲区，后者是数据长度)，将这样的一个结构体或对象放到一个队列中由后面的线程对其使用SplitFun函数进行整理。这样，可以最大限度地提高网络数据的接收速度，不至因为数据整理的原因而在此处浪费时间。</p>
<p style="FONT-SIZE: 10pt">　　代码中，我已经作了比较详细的注释，可以作为拼包函数的参考，代码是从偶的应用中提取、修改而来，本身只为演示之用，所以未作调试，应用时需要你自己去完善。如有疑问，可以我的blog上留言提出。</p>
<p style="FONT-SIZE: 10pt"><br>本文来自CSDN博客，转载请标明出处：<a href="http://blog.csdn.net/clever101/archive/2008/10/12/3061679.aspx">http://blog.csdn.net/clever101/archive/2008/10/12/3061679.aspx</a></p>
<img src ="http://www.cppblog.com/jack-wang/aggbug/107993.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/jack-wang/" target="_blank">小王</a> 2010-02-18 02:27 <a href="http://www.cppblog.com/jack-wang/archive/2010/02/18/107993.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>常用的setsockopt</title><link>http://www.cppblog.com/jack-wang/archive/2010/02/13/107802.html</link><dc:creator>小王</dc:creator><author>小王</author><pubDate>Sat, 13 Feb 2010 07:39:00 GMT</pubDate><guid>http://www.cppblog.com/jack-wang/archive/2010/02/13/107802.html</guid><wfw:comment>http://www.cppblog.com/jack-wang/comments/107802.html</wfw:comment><comments>http://www.cppblog.com/jack-wang/archive/2010/02/13/107802.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/jack-wang/comments/commentRss/107802.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/jack-wang/services/trackbacks/107802.html</trackback:ping><description><![CDATA[a
<img src ="http://www.cppblog.com/jack-wang/aggbug/107802.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/jack-wang/" target="_blank">小王</a> 2010-02-13 15:39 <a href="http://www.cppblog.com/jack-wang/archive/2010/02/13/107802.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>使用VC9+PlatformSDK编写WinSock程序时，#include WinSock2.h文件导致类型重定义问题（麻辣隔壁的，微软又想搞什么花样）</title><link>http://www.cppblog.com/jack-wang/archive/2010/02/13/107800.html</link><dc:creator>小王</dc:creator><author>小王</author><pubDate>Sat, 13 Feb 2010 07:14:00 GMT</pubDate><guid>http://www.cppblog.com/jack-wang/archive/2010/02/13/107800.html</guid><wfw:comment>http://www.cppblog.com/jack-wang/comments/107800.html</wfw:comment><comments>http://www.cppblog.com/jack-wang/archive/2010/02/13/107800.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/jack-wang/comments/commentRss/107800.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/jack-wang/services/trackbacks/107800.html</trackback:ping><description><![CDATA[<div style="FONT-SIZE: 10pt" class=tit>&nbsp;</div>
<div style="FONT-SIZE: 10pt" class=date>&nbsp;</div>
<table style="WIDTH: 100%; TABLE-LAYOUT: fixed">
    <tbody>
        <tr>
            <td style="FONT-SIZE: 10pt">
            <div style="FONT-SIZE: 10pt" id=blog_text class=cnt>
            <p style="FONT-SIZE: 10pt">编写WinSock程序时，如果不包含WinSock2.h文件很多系统类型无法识别。可是如果包含了WinSock2.h文件则报N多系统类型重定义的错误。<br>例如&nbsp;：<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<font style="COLOR: #000000" color=#ff6600>mswsock.h(69) : error C2065: 'SOCKET' : undeclared identifier<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;winsock2.h(99) : error C2011: 'fd_set' : 'struct' type redefinition<br><br>&nbsp;<br><font color=#3366ff>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;多亏了网上诸多网友的帖子给了我提示，问题解决了。跪谢了。<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br></font></font></p>
            <p style="FONT-SIZE: 10pt">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Windows网络编程至少需要两个头文件：winsock2.h和windows.h，而在WinSock2.0之前还存在一个老版本的winsock.h。正是这三个头文件的包含顺序，导致了上述问题的出现。</p>
            <p style="FONT-SIZE: 10pt">先看看winsock2.h的内容，在文件开头有如下宏定义：</p>
            <div style="FONT-SIZE: 10pt" class=postCode><font color=#3366ff>#ifndef _WINSOCK2API_<br>#define _WINSOCK2API_<br>#define _WINSOCKAPI_&nbsp;&nbsp; /* Prevent inclusion of winsock.h in windows.h */</font></div>
            <p style="FONT-SIZE: 10pt">_WINSOCK2API_很容易理解，这是最常见的防止头文件重复包含的保护措施。_WINSOCKAPI_的定义则是为了阻止对老文件winsock.h的包含，即是说，如果用户先包含了winsock2.h就不允许再包含winsock.h了，否则会导致类型重复定义。这是怎样做到的呢？很简单，因为winsock.h的头部同样存在如下的保护措施：</p>
            <div style="FONT-SIZE: 10pt" class=postCode><font color=#3366ff>#ifndef _WINSOCKAPI_<br>#define _WINSOCKAPI_</font></div>
            <p style="FONT-SIZE: 10pt">再回过头来看winsock2.h，在上述内容之后紧跟着如下宏指令：</p>
            <div style="FONT-SIZE: 10pt" class=postCode><font color=#3366ff>/*<br>* Pull in WINDOWS.H if necessary<br>*/<br>#ifndef _INC_WINDOWS<br>#include &lt;windows.h&gt;<br>#endif /* _INC_WINDOWS */</font></div>
            <p style="FONT-SIZE: 10pt">其作用是如果用户没有包含windows.h（_INC_WINDOWS在windows.h中定义）就自动包含它，以定义WinSock2.0所需的类型和常量等。</p>
            <p style="FONT-SIZE: 10pt">现在切换到windows.h，查找winsock，我们会惊奇的发现以下内容：</p>
            <div style="FONT-SIZE: 10pt" class=postCode><font color=#3366ff>#ifndef WIN32_LEAN_AND_MEAN<br>#include &lt;cderr.h&gt;<br>#include &lt;dde.h&gt;<br>#include &lt;ddeml.h&gt;<br>#include &lt;dlgs.h&gt;<br>#ifndef _MAC<br>#include &lt;lzexpand.h&gt;<br>#include &lt;mmsystem.h&gt;<br>#include &lt;nb30.h&gt;<br>#include &lt;rpc.h&gt;<br>#endif<br>#include &lt;shellapi.h&gt;<br>#ifndef _MAC<br>#include &lt;winperf.h&gt; </font>
            <p style="FONT-SIZE: 10pt"><font color=#3366ff></font></p>
            <p style="FONT-SIZE: 10pt"><font color=#3366ff>#if(_WIN32_WINNT &gt;= 0x0400)<br>#include &lt;winsock2.h&gt;<br>#include &lt;mswsock.h&gt;<br>#else<br>#include &lt;winsock.h&gt;<br>#endif /* _WIN32_WINNT &gt;= 0x0400 */</font></p>
            <p style="FONT-SIZE: 10pt"><font color=#3366ff>#endif<br>// 这里省略掉一部分内容<br>#endif /* WIN32_LEAN_AND_MEAN */</font></p>
            </div>
            <p style="FONT-SIZE: 10pt">看到没？windows.h会反向包含winsock2.h或者winsock.h！相互间的包含便是万恶之源！</p>
            <p style="FONT-SIZE: 10pt">下面具体分析一下问题是怎么发生的。</p>
            <p style="FONT-SIZE: 10pt"><strong>错误情形1：</strong>我们在自己的工程中先包含winsock2.h再包含windows.h，如果WIN32_LEAN_AND_MEAN未定义且_WIN32_WINNT大于或等于0x400，那么windows.h会在winsock2.h开头被自动引入，而windows.h又会自动引入mswsock.h，此时，mswsock.h里所用的socket类型还尚未定义，因此会出现类型未定义错误。</p>
            <p style="FONT-SIZE: 10pt"><strong>错误情形2：</strong>先包含windows.h再包含winsock2.h，如果WIN32_LEAN_AND_MEAN未定义且_WIN32_WINNT未定义或者其版本号小于0x400，那么windows.h会自动导入旧有的winsock.h，这样再当winsock2.h被包含时便会引起重定义。</p>
            <p style="FONT-SIZE: 10pt">&nbsp;&nbsp;&nbsp; 这里要说明的是，宏WIN32_LEAN_AND_MEAN的作用是减小win32头文件尺寸以加快编译速度，一般由AppWizard在stdafx.h中自动定义。_WIN32_WINNT的作用是开启高版本操作系统下的特殊函数，比如要使用可等待定时器（WaitableTimer），就得要求_WIN32_WINNT的值大于或等于0x400。因此，如果你没有遇到上述两个问题，很可能是你没有在这些条件下进行网络编程。</p>
            <p style="FONT-SIZE: 10pt">&nbsp;&nbsp;&nbsp; 问题还没有结束，要知道除了VC自带windows库文件外，MS的Platform SDK也含有这些头文件。我们很可能发现在之前能够好好编译的程序在改变了windows头文件包含路径后又出了问题。原因很简单，Platform SDK中的windows.h与VC自带的文件存在差异，其相同位置的代码如下：</p>
            <div style="FONT-SIZE: 10pt" class=postCode><font color=#3366ff>#ifndef WIN32_LEAN_AND_MEAN<br>#include &lt;cderr.h&gt;<br>#include &lt;dde.h&gt;<br>#include &lt;ddeml.h&gt;<br>#include &lt;dlgs.h&gt;<br>#ifndef _MAC<br>#include &lt;lzexpand.h&gt;<br>#include &lt;mmsystem.h&gt;<br>#include &lt;nb30.h&gt;<br>#include &lt;rpc.h&gt;<br>#endif<br>#include &lt;shellapi.h&gt;<br>#ifndef _MAC<br>#include &lt;winperf.h&gt;<br>#include &lt;winsock.h&gt; // 这里直接包含winsock.h<br>#endif<br>#ifndef NOCRYPT<br>#include &lt;wincrypt.h&gt;<br>#include &lt;winefs.h&gt;<br>#include &lt;winscard.h&gt;<br>#endif </font>
            <p style="FONT-SIZE: 10pt"><font color=#3366ff></font></p>
            <p style="FONT-SIZE: 10pt"><font color=#3366ff>#ifndef NOGDI<br>#ifndef _MAC<br>#include &lt;winspool.h&gt;<br>#ifdef INC_OLE1<br>#include &lt;ole.h&gt;<br>#else<br>#include &lt;ole2.h&gt;<br>#endif /* !INC_OLE1 */<br>#endif /* !MAC */<br>#include &lt;commdlg.h&gt;<br>#endif /* !NOGDI */<br>#endif /* WIN32_LEAN_AND_MEAN */</font></p>
            </div>
            <p style="FONT-SIZE: 10pt">唉，我们不禁要问MS为什么要搞这么多花样，更让人气愤的是，既然代码不一样，windows.h里却没有任何一个宏定义能够帮助程序辨别当前使用的文件是VC自带的还是PSDK里的。</p>
            <p style="FONT-SIZE: 10pt">&nbsp;&nbsp;&nbsp; 后来，我写了一个头文件专门处理winsock2.h的包含问题，名为winsock2i.h，只需在要使用WinSock2.0的源文件里第一个包含此文件即可，不过由于前面提到的问题，当使用PSDK时，需要手工定义一下USING_WIN_PSDK，源码如下：</p>
            <div style="FONT-SIZE: 10pt" class=postCode><font color=#3366ff>//<br>// winsock2i.h - Include winsock2.h safely.<br>//<br>// Copyleft 02/24/2005 by freefalcon<br>//<br>//<br>// When WIN32_LEAN_AND_MEAN is not defined and _WIN32_WINNT is LESS THAN 0x400,<br>// if we include winsock2.h AFTER windows.h or winsock.h, we get some compiling<br>// errors as following:<br>//&nbsp;&nbsp; winsock2.h(99) : error C2011: 'fd_set' : 'struct' type redefinition<br>// <br>// When WIN32_LEAN_AND_MEAN is not defined and _WIN32_WINNT is NOT LESS THAN 0x400,<br>// if we include winsock2.h BEFORE windows.h, we get some other compiling errors:<br>//&nbsp;&nbsp; mswsock.h(69) : error C2065: 'SOCKET' : undeclared identifier<br>// <br>// So, this file is used to help us to include winsock2.h safely, it should be <br>// placed before any other header files.<br>// </font>
            <p style="FONT-SIZE: 10pt"><font color=#3366ff></font></p>
            <p style="FONT-SIZE: 10pt"><font color=#3366ff>#ifndef _WINSOCK2API_</font></p>
            <p style="FONT-SIZE: 10pt"><font color=#3366ff>// Prevent inclusion of winsock.h<br>#ifdef _WINSOCKAPI_<br>#error Header winsock.h is included unexpectedly.<br>#endif</font></p>
            <p style="FONT-SIZE: 10pt"><font color=#3366ff>// NOTE: If you use Windows Platform SDK, you should enable following definition:<br>// #define USING_WIN_PSDK</font></p>
            <p style="FONT-SIZE: 10pt"><font color=#3366ff>#if !defined(WIN32_LEAN_AND_MEAN) &amp;&amp; (_WIN32_WINNT &gt;= 0x0400) &amp;&amp; !defined(USING_WIN_PSDK)<br>#include &lt;windows.h&gt;<br>#else<br>#include &lt;winsock2.<br><br><br><br></font></p>
            </div>
            </div>
            </td>
        </tr>
    </tbody>
</table>
<img src ="http://www.cppblog.com/jack-wang/aggbug/107800.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/jack-wang/" target="_blank">小王</a> 2010-02-13 15:14 <a href="http://www.cppblog.com/jack-wang/archive/2010/02/13/107800.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>隧道技术及其应用（架设服务器集群的理想选择）</title><link>http://www.cppblog.com/jack-wang/archive/2010/02/08/107501.html</link><dc:creator>小王</dc:creator><author>小王</author><pubDate>Mon, 08 Feb 2010 09:13:00 GMT</pubDate><guid>http://www.cppblog.com/jack-wang/archive/2010/02/08/107501.html</guid><wfw:comment>http://www.cppblog.com/jack-wang/comments/107501.html</wfw:comment><comments>http://www.cppblog.com/jack-wang/archive/2010/02/08/107501.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/jack-wang/comments/commentRss/107501.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/jack-wang/services/trackbacks/107501.html</trackback:ping><description><![CDATA[<h1>隧道技术</h1>
<div style="FONT-SIZE: 10pt" id=lemmaContent>　　隧道技术及其应用<br>
<div style="FONT-SIZE: 10pt" class=spctrl></div>
　　隧道技术（Tunneling）是一种通过使用互联网络的基础设施在网络之间传递数据的方式。使用隧道传递的数据（或负载）可以是不同协议的数据帧或包。隧道协议将其它协议的数据帧或包重新封装然后通过隧道发送。新的帧头提供路由信息，以便通过互联网传递被封装的负载数据。<br>
<div style="FONT-SIZE: 10pt" class=spctrl></div>
　　这里所说的隧道类似于点到点的连接。这种方式能够使来自许多信息源的网络业务在同一个基础设施中通过不同的隧道进行传输。隧道技术使用点对点通信协议代替了交换连接，通过路由网络来连接数据地址。隧道技术允许授权移动用户或已授权的用户在任何时间、任何地点访问企业网络。<br>
<div style="FONT-SIZE: 10pt" class=spctrl></div>
　　通过隧道的建立，可实现：<br>
<div style="FONT-SIZE: 10pt" class=spctrl></div>
　　* 将数据流强制送到特定的地址<br>
<div style="FONT-SIZE: 10pt" class=spctrl></div>
　　* 隐藏私有的网络地址<br>
<div style="FONT-SIZE: 10pt" class=spctrl></div>
　　* 在IP网上传递非IP数据包<br>
<div style="FONT-SIZE: 10pt" class=spctrl></div>
　　* 提供数据安全支持<br>
<div style="FONT-SIZE: 10pt" class=spctrl></div>
　　近来出现了一些新的隧道技术，并在不同的系统中得到运用和拓展。<br>
<div style="FONT-SIZE: 10pt" class=spctrl></div>
　　隧道技术<br>
<div style="FONT-SIZE: 10pt" class=spctrl></div>
　　为创建隧道，隧道的客户机和服务器双方必须使用相同的隧道协议。隧道技术可分别以第2层或第3层隧道协议为基础。第2层隧道协议对应于OSI模型的数据链路层，使用帧作为数据交换单位。PPTP（点对点隧道协议）、L2TP（第二层隧道协议）和L2F（第2层转发协议）都属于第2层隧道协议，是将用户数据封装在点对点协议（PPP）帧中通过互联网发送。第3层隧道协议对应于OSI模型的网络层，使用包作为数据交换单位。IPIP（IP over IP）以及IPSec隧道模式属于第3层隧道协议，是将IP包封装在附加的IP包头中，通过IP网络传送。无论哪种隧道协议都是由传输的载体、不同的封装格式以及用户数据包组成的。它们的本质区别在于，用户的数据包是被封装在哪种数据包中在隧道中传输。<br>
<div style="FONT-SIZE: 10pt" class=spctrl></div>
　　点对点隧道协议<br>
<div style="FONT-SIZE: 10pt" class=spctrl></div>
　　PPTP（Point to Point Tunneling Protocol）提供PPTP客户机和PPTP服务器之间的加密通信。PPTP客户机是指运行了该协议的PC机，如启动该协议的Windows95/98；PPTP服务器是指运行该协议的服务器，如启动该协议的WindowsNT服务器。PPTP是PPP协议的一种扩展。它提供了一种在互联网上建立多协议的安全虚拟专用网（VPN）的通信方式。远端用户能够透过任何支持PPTP的ISP访问公司的专用网。<br>
<div style="FONT-SIZE: 10pt" class=spctrl></div>
　　通过PPTP，客户可采用拨号方式接入公用IP网。拨号用户首先按常规方式拨到ISP的接入服务器（NAS），建立PPP连接；在此基础上，用户进行二次拨号建立到PPTP服务器的连接，该连接称为PPTP隧道，实质上是基于IP协议的另一个PPP连接，其中的IP包可以封装多种协议数据，包括TCP／IP、IPX和NetBEUI。PPTP采用了基于RSA公司RC4的数据加密方法，保证了虚拟连接通道的安全。对于直接连到互联网的用户则不需要PPP的拨号连接，可以直接与PPTP服务器建立虚拟通道。PPTP把建立隧道的主动权交给了用户，但用户需要在其PC机上配置PPTP，这样做既增加了用户的工作量，又会给网络带来隐患。另外，PPTP只支持IP作为传输协议。<br>
<div style="FONT-SIZE: 10pt" class=spctrl></div>
　　第二层转发协议<br>
<div style="FONT-SIZE: 10pt" class=spctrl></div>
　　L2F(Layer Two Forwarding protocol )是由Cisco公司提出的可以在多种介质，如ATM、帧中继、IP网上建立多协议的安全虚拟专用网的通信。远端用户能通过任何拨号方式接入公用IP网，首先按常规方式拨到ISP的接入服务器（NAS），建立PPP连接；NAS根据用户名等信息，建立直达HGW服务器的第二重连接。在这种情况下，隧道的配置和建立对用户是完全透明的。其体系结构见图1。<br>
<div style="FONT-SIZE: 10pt" class=spctrl></div>
　　第二层隧道协议<br>
<div style="FONT-SIZE: 10pt" class=spctrl></div>
　　L2TP（Layer Two Tunneling Protocol）结合了L2F和PPTP的优点，允许用户从客户端或访问服务器端建立VPN连接。L2TP是把链路层的PPP帧装入公用网络设施，如IP、ATM、帧中继中进行隧道传输的封装协议。其体系结构见图1。<br>
<div style="FONT-SIZE: 10pt" class=spctrl></div>
　　Cisco、Ascend、Microsoft和RedBack公司的专家们在修改了十几个版本后，终于在1999年8月公布了L2TP的标准RFC2661。目前用户拨号访问Internet时，必须使用IP协议，并且其动态得到的IP地址也是合法的。L2TP的好处在于支持多种协议，用户可以保留原有的IPX、Appletalk等协议或公司原有的IP地址。L2TP还解决了多个PPP链路的捆绑问题，PPP链路捆绑要求其成员均指向同一个NAS，L2TP则允许在物理上连接到不同NAS的PPP链路，在逻辑上的终点为同一个物理设备。L2TP扩展了PPP连接，在传统的方式中用户通过模拟电话线或ISDN/ADSL与网络访问服务器建立一个第2层的连接，并在其上运行PPP，第2层连接的终点和PPP会话的终点均设在同一个设备上(如NAS)。L2TP作为PPP的扩充提供了更强大的功能，包括允许第2层连接的终点和PPP会话的终点分别设在不同的设备上。<br>
<div style="FONT-SIZE: 10pt" class=spctrl></div>
　　L2TP主要由LAC(L2TP Access Concentrator)和LNS(L2TP Network Server)构成。LAC支持客户端的L2TP，发起呼叫，接收呼叫和建立隧道；LNS是所有隧道的终点。在传统的PPP连接中，用户拨号连接的终点是LAC，而L2TP能把PPP协议的终点延伸到LNS。<br>
<div style="FONT-SIZE: 10pt" class=spctrl></div>
　　L2TP的建立过程如图2。<br>
<div style="FONT-SIZE: 10pt" class=spctrl></div>
　　1.用户通过公用电话网或ISDN拨号呼叫本地接入服务器LAC；LAC接受呼叫并进行基本的识别过程，这一过程可以采用几种标准，如域名、呼叫线路识别(CLID)或拨号ID业务(DNIS)等。<br>
<div style="FONT-SIZE: 10pt" class=spctrl></div>
　　2.当用户被确认为合法企业用户时，就建立一个通向LNS的拨号VPN隧道。<br>
<div style="FONT-SIZE: 10pt" class=spctrl></div>
　　3.企业内部的安全服务器如TACACS+、RADIUS对拨号用户进行验证。<br>
<div style="FONT-SIZE: 10pt" class=spctrl></div>
　　4.LNS与远程用户交换PPP信息，分配IP地址。LNS可采用企业专用地址(未注册的IP地址)或服务提供商提供的地址空间分配IP地址。因为内部源IP地址与目的地IP地址实际上都通过服务提供商的IP网络在PPP信息包内传送，企业专用地址对提供者的网络是透明的。<br>
<div style="FONT-SIZE: 10pt" class=spctrl></div>
　　5.端到端的数据从拨号用户传到LNS。<br>
<div style="FONT-SIZE: 10pt" class=spctrl></div>
　　在实际应用中，LAC将拨号用户的PPP帧封装后，传送到LNS，后者去掉封装包头，取出PPP帧，再去掉PPP帧头，最后获得网络层数据包。<br>
<div style="FONT-SIZE: 10pt" class=spctrl></div>
　　L2TP方式给服务提供商和用户带来了许多方便。用户不需要在PC板上安装专门的客户端软件，企业网可以使用未注册的IP地址，并在本地管理认证数据库，从而降低了应用成本和培训维护费用。<br>
<div style="FONT-SIZE: 10pt" class=spctrl></div>
　　与PPTP和L2F相比，L2TP的优点在于提供了差错和流量控制；L2TP使用UDP封装和传送PPP帧。面向无连接的UDP无法保证网络数据的可靠传输，L2TP使用Nr（下一个希望接受的信息序列号）和Ns（当前发送的数据包序列号）字段进行流量和差错控制。双方通过序列号来确定数据包的顺序和缓冲区，一旦丢失数据，根据序列号可以进行重发。<br>
<div style="FONT-SIZE: 10pt" class=spctrl></div>
　　作为PPP的扩展协议，L2TP支持标准的安全特性CHAP和PAP，可以进行用户身份认证。L2TP定义了控制包的加密传输，每个被建立的隧道分别生成一个独一无二的随机钥匙，以便对付欺骗性的攻击，但是它对传输中的数据并不加密。<br>
<div style="FONT-SIZE: 10pt" class=spctrl></div>
　　通用路由封装<br>
<div style="FONT-SIZE: 10pt" class=spctrl></div>
　　通用路由封装（GRE：Generic Routing Encapsulation）在RFC1701/RFC1702中定义，它规定了怎样用一种网络层协议去封装另一种网络层协议的方法。GRE的隧道由两端的源IP地址和目的IP地址来定义，它允许用户使用IP封装IP、IPX、AppleTalk，并支持全部的路由协议，如RIP、OSPF、IGRP、EIGRP。通过GRE，用户可以利用公用IP网络连接IPX网络和AppleTalk网络，还可以使用保留地址进行网络互联，或对公网隐藏企业网的IP地址。<br>
<div style="FONT-SIZE: 10pt" class=spctrl></div>
　　GRE的包头包含了协议类型（用于标明乘客协议的类型）；校验和包括了GRE的包头和完整的乘客协议与数据；密钥（用于接收端验证接收的数据）；序列号（用于接收端数据包的排序和差错控制）和路由信息（用于本数据包的路由）。<br>
<div style="FONT-SIZE: 10pt" class=spctrl></div>
　　GRE只提供了数据包的封装，它没有防止网络侦听和攻击的加密功能。所以在实际环境中它常和IPsec一起使用，由IPsec为用户数据的加密，给用户提供更好的安全服务。<br>
<div style="FONT-SIZE: 10pt" class=spctrl></div>
　　IP安全协议<br>
<div style="FONT-SIZE: 10pt" class=spctrl></div>
　　IP安全协议（IPSec：IP Security）实际上是一套协议包而不是一个独立的协议，这一点对于我们认识IPSec是很重要的。从1995年开始IPSec的研究以来，IETF IPSec工作组在它的主页上发布了几十个Internet草案文献和12个RFC文件。其中，比较重要的有RFC2409 IKE（互连网密钥交换）、RFC2401 IPSec协议、RFC2402 AH验证包头、RFC2406 ESP加密数据等文件。<br>
<div style="FONT-SIZE: 10pt" class=spctrl></div>
　　IPSec安全体系包括3个基本协议：AH协议为IP包提供信息源验证和完整性保证；ESP协议提供加密机制；密钥管理协议(ISAKMP)提供双方交流时的共享安全信息。ESP和AH协议都有相关的一系列支持文件，规定了加密和认证的算法。最后，解释域（DOI）通过一系列命令、算法、属性和参数连接所有的IPSec组文件。<br>
<div style="FONT-SIZE: 10pt" class=spctrl></div>
　　隧道技术应用<br>
<div style="FONT-SIZE: 10pt" class=spctrl></div>
　　<a href="http://www.cppblog.com/view/480950.htm" target=_blank><u><font color=#0000ff>虚拟专用网络</font></u></a><br>
<div style="FONT-SIZE: 10pt" class=spctrl><u><font color=#0000ff></font></u></div>
　　VPN是Internet技术迅速发展的产物，其简单的定义是，在公用数据网上建立属于自己的专用数据网。也就是说不再使用长途专线建立专用数据网，而是充分利用完善的公用数据网建立自己的专用网。它的优点是，既可连到公网所能达到的任何地点，享受其保密性、安全性和可管理性，又降低网络的使用成本。<br>
<div style="FONT-SIZE: 10pt" class=spctrl></div>
　　VPN依靠Internet服务提供商（ISP）和其他的网络服务提供商（NSP）在公用网中建立自己的专用&#8220;隧道&#8221;，不同的信息来源，可分别使用不同的&#8220;隧道&#8221;进行传输。<br>
<div style="FONT-SIZE: 10pt" class=spctrl></div>
　　新出台的标准ISE CHEIP6版保证用户数据的安全加密。由于用户对企业网传输个人数据很敏感，因此集成度更高的<a href="http://www.cppblog.com/view/105152.htm" target=_blank><u><font color=#0000ff>VPN技术</font></u></a>不久将会流行起来。<br>
<div style="FONT-SIZE: 10pt" class=spctrl></div>
　　Linux 中的IP隧道<br>
<div style="FONT-SIZE: 10pt" class=spctrl></div>
　　为了在TCP/IP网络中传输其他协议的数据包，Linux采用了一种IP隧道技术。在已经使用多年的桥接技术中就是通过在源协议数据包上再套上一个IP协议帽来实现。<br>
<div style="FONT-SIZE: 10pt" class=spctrl></div>
　　利用IP隧道传送的协议包也包括IP数据包，Linux的IPIP包封指的就是这种情况。<a href="http://www.cppblog.com/view/65888.htm" target=_blank><u><font color=#0000ff>移动IP</font></u></a>（Mobile-IP）和IP多点广播（IP-Multicast）是两个流行的例子。目前，IP隧道技术在VPN中也显示出极大的魅力。<br>
<div style="FONT-SIZE: 10pt" class=spctrl></div>
　　移动IP是在全球Internet上提供移动功能的一种服务，它允许节点在切换链路时仍可保持正在进行的通信。它提供了一种IP路由机制，使移动节点以一个永久的IP地址连接到任何链路上。与特定主机路由技术和数据链路层方案不同，移动IP还要解决安全性和可靠性问题，并与传输媒介无关。移动IP的可扩展性使其可以在整个互联网上应用。<br>
<div style="FONT-SIZE: 10pt" class=spctrl></div>
　　GPRS隧道协议<br>
<div style="FONT-SIZE: 10pt" class=spctrl></div>
　　随着隧道技术的发展，各种业务已经开始根据本业务的特点制定相应的隧道协议。GPRS（General Packet Radio Service）中的隧道协议GTP（GPRS Tunnel Protocol）就是一例。<br>
<div style="FONT-SIZE: 10pt" class=spctrl></div>
　　GPRS是GSM提供的分组交换和分组传输方式的新的承载业务，可以应用在PLMN（Public Land Mobile Network）内部或应用在GPRS网与外部互联分组数据网（IP、X.25）之间的分组数据传送，GPRS能提供到现有数据业务的无缝连接。它在GSM网络中增加了两个节点：服务GPRS支持节点（SGSN─serving GPRS support node）和网关GPRS支持节点（GGSN─Gateway GPRS support node）。<br>
<div style="FONT-SIZE: 10pt" class=spctrl></div>
　　SGSN是GPRS骨干网与无线接入网之间的接口，它将分组交换到正确的基站子系统（BSS）。其任务包括提供对移动台的加密、认证、会话（session）管理、移动性管理和逻辑链路管理。它也提供到HLR等数据库的连接。<br>
<div style="FONT-SIZE: 10pt" class=spctrl></div>
　　通过GPRS隧道协议可为多种协议的数据分组通过GPRS骨干网提供隧道。GTP根据所运载的协议需求，利用TCP或UDP协议来分别提供可靠的连接（如支持X.25的分组传输）和无连接服务（如IP分组）。</div>
<img src ="http://www.cppblog.com/jack-wang/aggbug/107501.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/jack-wang/" target="_blank">小王</a> 2010-02-08 17:13 <a href="http://www.cppblog.com/jack-wang/archive/2010/02/08/107501.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>Socket程序开发，发送端写入数据成功，接收端收不到数据的现象分析</title><link>http://www.cppblog.com/jack-wang/archive/2010/01/28/106660.html</link><dc:creator>小王</dc:creator><author>小王</author><pubDate>Thu, 28 Jan 2010 10:47:00 GMT</pubDate><guid>http://www.cppblog.com/jack-wang/archive/2010/01/28/106660.html</guid><wfw:comment>http://www.cppblog.com/jack-wang/comments/106660.html</wfw:comment><comments>http://www.cppblog.com/jack-wang/archive/2010/01/28/106660.html#Feedback</comments><slash:comments>1</slash:comments><wfw:commentRss>http://www.cppblog.com/jack-wang/comments/commentRss/106660.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/jack-wang/services/trackbacks/106660.html</trackback:ping><description><![CDATA[<p><span style="FONT-SIZE: medium"><span style="FONT-WEIGHT: bold"><font size=4>Problem:</font></span></span></p>
<ul>
    <li>1 client 1 server, connected with non-block tcp socket. Linux 2.6.*+. </li>
    <li>Client 写入大概 3k 数据到 socket。 </li>
    <li>Write()正确返回实际写入字节数。 </li>
    <li>Server 什么也收不到。 </li>
</ul>
<p><span style="FONT-SIZE: medium"><span style="FONT-WEIGHT: bold"><font size=4>Causes:</font></span></span></p>
<ul>
    <li>发送端 MTU稍大于路由器上的MTU设置 </li>
    <li>通知发送端需要拆包的ICMP在某处被杀掉了 </li>
    <li>发送端不停的重发包 </li>
</ul>
<p>设置了DF标志的ip包当遇到路由器的MTU比包小的时候，不会被路由器拆包。而路由器发送icmp消息到发送端，通知它应该拆包。</p>
<p>但icmp消息被防火墙拦截下来。</p>
<p><font size=3><span style="FONT-SIZE: medium"><span style="FONT-WEIGHT: bold">环境和现象：</span></span><br>这个例子中，MTU在client和server都是1500.</font></p>
<p>dump出来的包如下:</p>
<p><span style="FONT-WEIGHT: bold">客户端看到的:</span><br>发送了2个包，后1个包成功，第1个过大而不停的被发送:<span style="FONT-FAMILY: monospace"><br></span></p>
<blockquote>
<p><span style="FONT-FAMILY: monospace"><font face=新宋体>17:23:06.933574 IP (tos 0&#215;0, ttl 64, <span style="BACKGROUND-COLOR: #ffb3ff"><span>id 57558</span></span>, offset 0, flags [<span style="BACKGROUND-COLOR: #8ccbea"><span>DF</span></span>], proto: TCP (6), <span style="BACKGROUND-COLOR: #ffdb72"><span>length: 1500</span></span>) <span style="BACKGROUND-COLOR: #ff7272"><span>10.54.40.43.43145</span></span> &gt; <span style="BACKGROUND-COLOR: #a4e57e"><span>10.29.14.74.http</span></span>: ., cksum 0&#215;5096 (incorrect (-&gt; 0&#215;5c4e), <span style="BACKGROUND-COLOR: #9999ff"><span>0:1448(1448)</span></span> ack 1 win 46</font></span></p>
<p>17:23:06.933580 IP (tos 0&#215;0, ttl 64, <span style="BACKGROUND-COLOR: #ffb3ff"><span>id 57559</span></span>, offset 0, flags [<span style="BACKGROUND-COLOR: #8ccbea"><span>DF</span></span>], proto: TCP (6), <span style="BACKGROUND-COLOR: #ffdb72"><span>length: 730</span></span>) <span style="BACKGROUND-COLOR: #ff7272"><span>10.54.40.43.43145</span></span> &gt; <span style="BACKGROUND-COLOR: #a4e57e"><span>10.29.14.74.http</span></span>: P, cksum 0&#215;4d94 (incorrect (-&gt; 0&#215;3933), <span style="BACKGROUND-COLOR: #9999ff"><span>1448:2126(678)</span></span> ack 1 win 46</p>
<p>17:23:07.167049 IP (tos 0&#215;0, ttl 64, <span style="BACKGROUND-COLOR: #ffb3ff"><span>id 57560</span></span>, offset 0, flags [<span style="BACKGROUND-COLOR: #8ccbea"><span>DF</span></span>], proto: TCP (6), <span style="BACKGROUND-COLOR: #ffdb72"><span>length: 1500</span></span>) <span style="BACKGROUND-COLOR: #ff7272"><span>10.54.40.43.43145</span></span> &gt; <span style="BACKGROUND-COLOR: #a4e57e"><span>10.29.14.74.http</span></span>: ., cksum 0&#215;5096 (incorrect (-&gt; 0&#215;5b5b), <span style="BACKGROUND-COLOR: #9999ff"><span>0:1448(1448)</span></span> ack 1 win 46</p>
<p>17:23:07.634922 IP (tos 0&#215;0, ttl 64, <span style="BACKGROUND-COLOR: #ffb3ff"><span>id 57561</span></span>, offset 0, flags [<span style="BACKGROUND-COLOR: #8ccbea"><span>DF</span></span>], proto: TCP (6), <span style="BACKGROUND-COLOR: #ffdb72"><span>length: 1500</span></span>) <span style="BACKGROUND-COLOR: #ff7272"><span>10.54.40.43.43145</span></span> &gt; <span style="BACKGROUND-COLOR: #a4e57e"><span>10.29.14.74.http</span></span>: ., cksum 0&#215;5096 (incorrect (-&gt; 0&#215;5987), <span style="BACKGROUND-COLOR: #9999ff"><span>0:1448(1448)</span></span> ack 1 win 46</p>
</blockquote>
<p><span style="FONT-WEIGHT: bold">接受端看到的:</span><br>只有730大小的包接受成功</p>
<blockquote>
<p>17:23:08.605622 IP (tos 0&#215;0, ttl 59, <span style="BACKGROUND-COLOR: #ffb3ff"><span>id 57559</span></span>, offset 0, flags [<span style="BACKGROUND-COLOR: #8ccbea"><span>DF</span></span>], proto: TCP (6), <span style="BACKGROUND-COLOR: #ffdb72"><span>length: 730</span></span>) 202.108.3.204.43145 &gt; <span style="BACKGROUND-COLOR: #a4e57e"><span>10.29.14.74.http</span></span>: P, cksum 0&#215;9d5b (correct), <span style="BACKGROUND-COLOR: #9999ff"><span>1448:2126(678)</span></span> ack 1 win 46</p>
</blockquote>
<p><font size=3><span style="FONT-SIZE: medium"><span style="FONT-WEIGHT: bold">解决方法:</span></span><br>调整发送端机器的配置:（任选1个）</font></p>
<p><span style="FONT-WEIGHT: bold">在网络层上:</span><br>Decrease mtu on network adapter:</p>
<blockquote>
<p>ifconfig eth* mtu 1400</p>
</blockquote>
<p><span style="FONT-WEIGHT: bold">操作系统配置:</span><br>Clear the default &#8216;MTU discovery&#8217; flag with sysctl:</p>
<blockquote>
<p>net.ipv4.ip_no_pmtu_disc = 1</p>
</blockquote>
<p><span style="FONT-WEIGHT: bold">或在应用程序里:</span><br>Set socket option &#8216;<strong>IP_MTU_DISCOVER&#8217;</strong> with <strong><a href="http://linux.die.net/man/2/setsockopt"><u><font color=#0000ff>setsockopt</font></u></a></strong>(2) to clear &#8216;DF&#8217; flag of IP package.</p>
<p><span style="FONT-SIZE: medium"><span style="FONT-WEIGHT: bold"><font size=4>Reference:<br></font></span></span></p>
<ol>
    <li><span style="FONT-SIZE: medium"><span style="FONT-SIZE: small"><font size=2>DF flag of</font></span><span style="FONT-WEIGHT: bold"><font size=4> </font></span><a href="http://en.wikipedia.org/wiki/IPv4#Header"><span style="FONT-SIZE: small"><u><font color=#0000ff size=2>IP package Header<br></font></u></span></a></span></li>
    <li><a href="http://en.wikipedia.org/wiki/Internet_Control_Message_Protocol"><span style="FONT-SIZE: small"><u><font color=#0000ff size=2>Internet Control Message Protocol</font></u></span></a> </li>
    <li><span style="FONT-SIZE: medium"><span style="FONT-SIZE: small"><a href="http://en.wikipedia.org/wiki/IP_fragmentation"><u><font color=#0000ff size=2>IP fragmentation</font></u></a></span></span> </li>
    <li><a href="http://en.wikipedia.org/wiki/MTU_%28networking%29"><u><font color=#0000ff>MTU or</font></u></a><a href="http://en.wikipedia.org/wiki/MTU_%28networking%29"><span style="FONT-SIZE: small"><u><font color=#0000ff size=2> Maximum transmission unit</font></u></span></a> </li>
    <li><a href="http://linux.die.net/man/7/ip"><u><font color=#0000ff>IP programming</font></u></a> </li>
    <li><a href="http://en.wikipedia.org/wiki/Path_MTU_discovery"><u><font color=#0000ff>Path MTU Discovery</font></u></a> </li>
    <li><a href="http://linux.die.net/man/8/sysctl"><u><font color=#0000ff>sysctl</font></u></a> </li>
</ol>
<p><strong>Thanks：</strong></p>
<p>esx kobe steve</p>
<p>来自：<a href="http://blog.developers.api.sina.com.cn/?p=672"><u><font color=#0000ff>http://blog.developers.api.sina.com.cn/?p=672</font></u></a><br>原文：<a href="http://drdr-xp-tech.blogspot.com/2009/04/black-hole-socket-problem.html"><u><font color=#0000ff>http://drdr-xp-tech.blogspot.com/2009/04/black-hole-socket-problem.html</font></u></a></p>
<img src ="http://www.cppblog.com/jack-wang/aggbug/106660.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/jack-wang/" target="_blank">小王</a> 2010-01-28 18:47 <a href="http://www.cppblog.com/jack-wang/archive/2010/01/28/106660.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>asio异步方式</title><link>http://www.cppblog.com/jack-wang/archive/2009/08/19/93756.html</link><dc:creator>小王</dc:creator><author>小王</author><pubDate>Tue, 18 Aug 2009 16:36:00 GMT</pubDate><guid>http://www.cppblog.com/jack-wang/archive/2009/08/19/93756.html</guid><wfw:comment>http://www.cppblog.com/jack-wang/comments/93756.html</wfw:comment><comments>http://www.cppblog.com/jack-wang/archive/2009/08/19/93756.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/jack-wang/comments/commentRss/93756.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/jack-wang/services/trackbacks/93756.html</trackback:ping><description><![CDATA[ASIO的异步方式 <br />异步方式 和同步方式不同，从来不花时间去等那些龟速的IO操作，只是向系统说一声要做什么，然后就可以做其它事去了。<br />如果系统完成了操作， 系统就会通过我之前给它的回调对象来通知我。 <br />在ASIO库中，异步方式的函数或方法名称前面都有“async_” 前缀，函数参数里会要求放一个回调函数（或仿函数）。<br />异步操作执行 后不管有没有完成都会立即返回，这时可以做一些其它事，直到回调函数（或仿函数）被调用，说明异步操作已经完成。<br />在ASIO中很多回调函数都只接受一个boost::system::error_code参数，在实际使用时肯定是不够的，所以一般 使用仿函数携带一堆相关数据作为回调，或者使用boost::bind来绑定一堆数据。 <br />另外要注意的是，只有io_service类的run()方法运行之后回调对象才会被调用，否则即使系统已经完成了异步操作也不会有任 务动作。<br />好了，就介绍到这里，下面是我带来的异步方式TCP Helloworld服务器端: <br />1.#include <br /><iostream>2.#include <br /><string>3.#include <br /><boost asio.hpp="">4.#include <boost bind.hpp=""><br />5.#include <br /><boost smart_ptr.hpp="">6. <br />7.using namespace boost::asio; <br />8.using boost::system::error_code; <br />9.using ip::tcp; <br />10. <br />11.struct CHelloWorld_Service{ <br />12. CHelloWorld_Service(io_service &amp;iosev) <br />13. :m_iosev(iosev),m_acceptor(iosev, tcp::endpoint(tcp::v4(), 1000)) <br />14. { <br />15. } <br />16. <br />17. void start() <br />18. { <br />19. // 开始等待连接（非阻塞） <br />20. boost::shared_ptr<?xml:namespace prefix = tcp /?><tcp::socket> psocket(new tcp::socket(m_iosev)); <br />21. // 触发的事件只有error_code参数，所以用boost::bind把socket绑定进去 <br />22. m_acceptor.async_accept(*psocket, <br />23. boost::bind(&amp;CHelloWorld_Service::accept_handler, this, psocket, _1) <br />24. ); <br />25. } <br />26.<br /> 27. // 有客户端连接时accept_handler触发 <br />28. void accept_handler(boost::shared_ptr<tcp::socket> psocket, error_code ec) <br />29. { <br />30. if(ec) return; <br />31. // 继续等待连接 <br />32. start();<br /> 33. // 显示远程IP<br /> 34. std::cout &lt;&lt; psocket-&gt;remote_endpoint().address() &lt;&lt; std::endl; <br />35. // 发送信息(非阻塞) <br />36. boost::shared_ptr<?xml:namespace prefix = std /?><std::string> pstr(new std::string("hello async world!")); <br />37. psocket-&gt;async_write_some(buffer(*pstr), <br />38. boost::bind(&amp;CHelloWorld_Service::write_handler, this, pstr, _1, _2) <br />39. );<br /> 40. }<br /> 41. <br />42. // 异步写操作完成后write_handler触发 <br />43. void write_handler(boost::shared_ptr<std::string> pstr, <br />44. error_code ec, size_t bytes_transferred) <br />45. { <br />46. if(ec) <br />47. std::cout&lt;&lt; "发送失败!" &lt;&lt; std::endl; <br />48. else <br />49. std::cout&lt;&lt; *pstr &lt;&lt; " 已发送" &lt;&lt; std::endl; <br />50. } <br />51. <br />52.private: <br />53. io_service &amp;m_iosev; <br />54. ip::tcp::acceptor m_acceptor; <br />55.}; <br />56. <br />57.int main(int argc, char* argv[]) <br />58.{ <br />59. io_service iosev; <br />60. CHelloWorld_Service sev(iosev);<br /> 61. // 开始等待连接 <br />62. sev.start(); <br />63. iosev.run(); <br />64. <br />65. return 0; <br />66.} <br />在这个例子中，首先调用sev.start()开 始接受客户端连接。<br />由于async_accept调 用后立即返回，start()方 法 也就马上完成了。 <br />sev.start()在 瞬间返回后iosev.run()开 始执行，iosev.run()方法是一个循环，负责分发异步回调事件，<br />只 有所有异步操作全部完成才会返回。<br />这里有个问题，就是要保证start()方法中m_acceptor.async_accept操 作所用的tcp::socket对象在整个异步操作期间保持有效<br />(不 然系统底层异步操作了一半突然发现tcp::socket没了，不是拿人家开涮嘛-_-!!!)，<br />而且客户端连接进来后这个tcp::socket对象还 有用呢。<br />这里的解决办法是使用一个带计数的智能指针boost::shared_ptr<tcp:: socket="">，并把这个指针作为参数绑定到回调函数上。<br />一旦有客户连接，我们在start()里给的回调函数accept_handler就会被 调用，<br />首先调用start()继续异步等待其 它客户端的连接，然后使用绑定进来的tcp::socket对象与当前客户端通信。<br />发送数据也使用了异步方式(async_write_some)， 同样要保证在整个异步发送期间缓冲区的有效性，<br />所以也用boost::bind绑定了boost::shared_ptr<std:: string="">。<br />对于客户端也一样，在connect和read_some方法前加一个async_前缀，然后加入回调即可，大家自己练习写一写。 </std::></tcp::></std::string></std::string></tcp::socket></tcp::socket></boost></boost></boost></string></iostream><img src ="http://www.cppblog.com/jack-wang/aggbug/93756.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/jack-wang/" target="_blank">小王</a> 2009-08-19 00:36 <a href="http://www.cppblog.com/jack-wang/archive/2009/08/19/93756.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>asio同步方式</title><link>http://www.cppblog.com/jack-wang/archive/2009/08/19/93755.html</link><dc:creator>小王</dc:creator><author>小王</author><pubDate>Tue, 18 Aug 2009 16:29:00 GMT</pubDate><guid>http://www.cppblog.com/jack-wang/archive/2009/08/19/93755.html</guid><wfw:comment>http://www.cppblog.com/jack-wang/comments/93755.html</wfw:comment><comments>http://www.cppblog.com/jack-wang/archive/2009/08/19/93755.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/jack-wang/comments/commentRss/93755.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/jack-wang/services/trackbacks/93755.html</trackback:ping><description><![CDATA[ASIO的同步方式<br>&nbsp;Boost.Asio是一个跨平台的网络及底层IO的C++编程库，它使用现代C++手法实现了统一的异步调用模型。<br>头文件 #include <br><boost asio.hpp>名空间 using namespace boost::asio; <br>ASIO库能够使用TCP、UDP、ICMP、串口来发送/接收数据，<br>下面先介绍TCP协议的读写操作对于读写方式，ASIO支持同步和异步两种方式，<br>首先登场的是同步方式，<br>下面请同步方式自我介绍一下：大家好！我是同步方式！我的主要特点就是执着！<br>所有的操作都要完成或出错才会返回，不过偶的执着被大家称之为阻塞，实在是郁闷~~（场下一片嘘声），<br>其实这样 也是有好处的，比如逻辑清晰，编程比较容易。<br>在服务器端，我会做个socket交给acceptor对象，让它一直等客户端连进来，连上以后再通过这个socket与客户端通信， <br>而所有的通信都是以阻塞方式进行的，读完或写完才会返回。在客户端也一样，<br>这时我会拿着socket去连接服务器，当然也是连上或出错了才返回，最后也是以阻塞的方式和服务器通信。<br>有人认为同步方式没有异步方式高效，其实这是片面的理解。<br>在单线程的情况下可能确实如此，我不能利用耗时的网络操作这段时间做别的事 情，不是好的统筹方法。<br>不过这个问题可以通过多线程来避免，比如在服务器端让其中一个线程负责等待客户端连接，连接进来后把socket交给另外的线程去 和客户端通信，这样与一个客户端通信的同时也能接受其它客户端的连接，主线程也完全被解放了出来。<br>我的介绍就有这里，谢谢大家！好，感谢同步方式的自我介绍，<br>现在放出同步方式的演示代码(起立鼓掌!): <br>服务器端 <br>1.#include <br><iostream>2.#include <boost asio.hpp><br>3. <br>4.using namespace boost::asio;<br>&nbsp;5.<br>&nbsp;6.int main(int argc, char* argv[]) <br>7.{ <br>8. // 所有asio类都需要io_service对象 <br>9. io_service iosev; <br>10. ip::tcp::acceptor acceptor(iosev, <br>11. ip::tcp::endpoint(ip::tcp::v4(), 1000)); <br>12. for(;;) <br>13. { <br>14. // socket对象 <br>15. ip::tcp::socket socket(iosev);<br>&nbsp;16. // 等待直到客户端连接进来 <br>17. acceptor.accept(socket); <br>18. // 显示连接进来的客户端 <br>19. std::cout &lt;&lt; socket.remote_endpoint().address() &lt;&lt; std::endl; <br>20. // 向客户端发送hello world! <br>21. boost::system::error_code ec; <br>22. socket.write_some(buffer("hello world!"), ec); <br>23. <br>24. // 如果出错，打印出错信息 <br>25. if(ec) <br>26. { <br>27. std::cout &lt;&lt; <br>28. boost::system::system_error(ec).what() &lt;&lt; std::endl; <br>29. break; <br>30. } <br>31. // 与当前客户交互完成后循环继续等待下一客户连接 <br>32. } <br>33. return 0; <br>34.} <br><br>客户端 <br>1.#include <iostream><br>2.#include <br><boost asio.hpp>3. <br>4.using namespace boost::asio; <br>5. <br>6.int main(int argc, char* argv[]) <br>7.{ <br>8. // 所有asio类都需要io_service对象 <br>9. io_service iosev; <br>10. // socket对象 <br>11. ip::tcp::socket socket(iosev); <br>12. // 连接端点，这里使用了本机连接，可以修改IP地址测试远程连接 <br>13. ip::tcp::endpoint ep(ip::address_v4::from_string("127.0.0.1"), 1000); <br>14. // 连接服务器 <br>15. boost::system::error_code ec; <br>16. socket.connect(ep,ec); <br>17. // 如果出错，打印出错信息 <br>18. if(ec) <br>19. { <br>20. std::cout &lt;&lt; boost::system::system_error(ec).what() &lt;&lt; std::endl; <br>21. return -1; 22. } <br>23. // 接收数据 <br>24. char buf[100]; <br>25. size_t len=socket.read_some(buffer(buf), ec); <br>26. std::cout.write(buf, len); <br>27. <br>28. return 0; <br>29.} <br><br>从演示代码可以得知 <br>&#8226;ASIO的TCP协议通过boost::asio::ip名 空间下的tcp类进行通信。 <br>&#8226;IP地址（address,address_v4,address_v6）、 端口号和协议版本组成一个端点（tcp:: endpoint）。<br>用于在服务器端生成tcp::acceptor对 象，并在指定端口上等待连接；或者在客户端连接到指定地址的服务器上。 <br>&#8226;socket是 服务器与客户端通信的桥梁，连接成功后所有的读写都是通过socket对 象实现的，当socket析 构后，连接自动断 开。 <br>&#8226;ASIO读写所用的缓冲区用buffer函 数生成，这个函数生成的是一个ASIO内部使用的缓冲区类，它能把数组、指针（同时指定大 小）、std::vector、std::string、boost::array包装成缓冲区类。<br>&nbsp;&#8226;ASIO中的函数、类方法都接受一个boost::system::error_code类 型的数据，用于提供出错码。<br>它可以转换成bool测试是否出错，并通过boost::system::system_error类 获得详细的出错信息。<br>另外，也可以不向ASIO的函数或方法提供 boost::system::error_code，这时如果出错的话就会直 接抛出异常，异常类型就是boost::system:: system_error(它是从std::runtime_error继承的)。 
<img src ="http://www.cppblog.com/jack-wang/aggbug/93755.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/jack-wang/" target="_blank">小王</a> 2009-08-19 00:29 <a href="http://www.cppblog.com/jack-wang/archive/2009/08/19/93755.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>boost::asio 客户端</title><link>http://www.cppblog.com/jack-wang/archive/2009/08/19/93754.html</link><dc:creator>小王</dc:creator><author>小王</author><pubDate>Tue, 18 Aug 2009 16:24:00 GMT</pubDate><guid>http://www.cppblog.com/jack-wang/archive/2009/08/19/93754.html</guid><wfw:comment>http://www.cppblog.com/jack-wang/comments/93754.html</wfw:comment><comments>http://www.cppblog.com/jack-wang/archive/2009/08/19/93754.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/jack-wang/comments/commentRss/93754.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/jack-wang/services/trackbacks/93754.html</trackback:ping><description><![CDATA[<div>#pragma once</div><div>#include &lt;iostream&gt;</div><div>#include &lt;boost/asio.hpp&gt;</div><div>#include &lt;boost/bind.hpp&gt;</div><div>#include &lt;boost/shared_ptr.hpp&gt;</div><div></div><div></div><div>using namespace boost::asio::ip;</div><div>using namespace boost::asio;</div><div></div><div>class Client</div><div>{</div><div>public:</div><div><span style="white-space:pre">	</span>//boost::shared_ptr&lt;Client&gt;<span style="white-space:pre">	</span>ClientPtr;</div><div></div><div>public:</div><div><span style="white-space:pre">	</span>Client(boost::asio::io_service&amp; io_service, tcp::endpoint&amp; endpoint);</div><div></div><div><span style="white-space:pre">	</span>~Client();</div><div></div><div>private:</div><div><span style="white-space:pre">	</span>void handle_connect(const boost::system::error_code&amp; error);</div><div></div><div><span style="white-space:pre">	</span>void handle_read(const boost::system::error_code&amp; error);</div><div></div><div><span style="white-space:pre">	</span>void handle_write(const boost::system::error_code&amp; error);</div><div></div><div>private:</div><div><span style="white-space:pre">	</span>tcp::socket socket_;&nbsp;</div><div><span style="white-space:pre">	</span>char getBuffer[1024];</div><div>};<br /><br /><br /><div>#include "stdafx.h"</div><div>#include "Client.h"</div><div></div><div></div><div>Client::Client(boost::asio::io_service&amp; io_service, tcp::endpoint&amp; endpoint):</div><div>socket_(io_service)</div><div>{</div><div><span style="white-space:pre">	</span>socket_.async_connect(endpoint, boost::bind(&amp;Client::handle_connect, this, boost::asio::placeholders::error));</div><div><span style="white-space:pre">	</span>::memset(getBuffer, '\0', 1024);</div><div>}</div><div></div><div>Client::~Client()</div><div>{</div><div></div><div>}</div><div></div><div>void Client::handle_connect(const boost::system::error_code&amp; error)</div><div>{</div><div><span style="white-space:pre">	</span>if (!error)</div><div><span style="white-space:pre">	</span>{</div><div><span style="white-space:pre">		</span>// 一连上，就向服务端发送信息&nbsp;</div><div><span style="white-space:pre">		</span>boost::asio::async_write(socket_, boost::asio::buffer("hello,server!"),</div><div><span style="white-space:pre">			</span>boost::bind(&amp;Client::handle_write, this, boost::asio::placeholders::error));</div><div></div><div><span style="white-space:pre">		</span>// boost::asio::async_read(...)读取的字节长度不能大于数据流的长度，否则就会进入&nbsp;</div><div><span style="white-space:pre">		</span>// ioservice.run()线程等待，read后面的就不执行了。&nbsp;</div><div><span style="white-space:pre">		</span>//boost::asio::async_read(socket,&nbsp;</div><div><span style="white-space:pre">		</span>// boost::asio::buffer(getBuffer,1024),</div><div><span style="white-space:pre">		</span>// boost::bind(&amp;client::handle_read,this,boost::asio::placeholders::error)&nbsp;</div><div><span style="white-space:pre">		</span>// );&nbsp;</div><div><span style="white-space:pre">		</span>socket_.async_read_some(boost::asio::buffer(getBuffer, 1024),&nbsp;</div><div><span style="white-space:pre">			</span>boost::bind(&amp;Client::handle_read, this, boost::asio::placeholders::error));</div><div><span style="white-space:pre">	</span>}</div><div><span style="white-space:pre">	</span>else</div><div><span style="white-space:pre">	</span>{</div><div><span style="white-space:pre">		</span>socket_.close();</div><div><span style="white-space:pre">	</span>}</div><div>}</div><div></div><div>void Client::handle_read(const boost::system::error_code&amp; error)</div><div>{</div><div><span style="white-space:pre">	</span>if (!error)</div><div><span style="white-space:pre">	</span>{</div><div><span style="white-space:pre">		</span>std::cout &lt;&lt; getBuffer &lt;&lt; std::endl;</div><div></div><div><span style="white-space:pre">		</span>//boost::asio::async_read(socket,&nbsp;</div><div><span style="white-space:pre">		</span>// boost::asio::buffer(getBuffer,1024),&nbsp;</div><div><span style="white-space:pre">		</span>// boost::bind(&amp;client::handle_read,this,boost::asio::placeholders::error)&nbsp;</div><div><span style="white-space:pre">		</span>// );&nbsp;</div><div><span style="white-space:pre">		</span>//这样就可以实现循环读取了，相当于while（1）&nbsp;</div><div><span style="white-space:pre">		</span>//当然，到了这里，做过网络的朋友就应该相当熟悉了，一些逻辑就可以自行扩展了&nbsp;</div><div><span style="white-space:pre">		</span>//想做聊天室的朋友可以用多线程来实现&nbsp;</div><div><span style="white-space:pre">		</span>socket_.async_read_some(boost::asio::buffer(getBuffer, 1024),</div><div><span style="white-space:pre">			</span>boost::bind(&amp;Client::handle_read, this, boost::asio::placeholders::error));</div><div><span style="white-space:pre">	</span>}</div><div><span style="white-space:pre">	</span>else</div><div><span style="white-space:pre">	</span>{</div><div><span style="white-space:pre">		</span>socket_.close();</div><div><span style="white-space:pre">	</span>}</div><div>}</div><div></div><div>void Client::handle_write(const boost::system::error_code&amp; error)</div><div>{</div><div></div><div>}<br /><br /><br />////////////////////////////////////////////////////////////////////////////////////////////////////////////////////<br /><div>#include "stdafx.h"</div><div>#include "Client.h"</div><div></div><div></div><div>using namespace boost::asio::ip;</div><div>using namespace boost::asio;</div><div></div><div>int _tmain(int argc, _TCHAR* argv[])</div><div>{</div><div><span style="white-space:pre">	</span>io_service ioservice;</div><div></div><div><span style="white-space:pre">	</span>tcp::endpoint endpoint(address_v4::from_string("127.0.0.1"), 8100);</div><div><span style="white-space:pre">	</span>//ClientPtr client_ptr(new Client(io_service, endpoint));</div><div><span style="white-space:pre">	</span>Client client(ioservice, endpoint);</div><div></div><div><span style="white-space:pre">	</span>ioservice.run();</div><div></div><div><span style="white-space:pre">	</span>return 0;</div><div>}</div><div></div></div></div><img src ="http://www.cppblog.com/jack-wang/aggbug/93754.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/jack-wang/" target="_blank">小王</a> 2009-08-19 00:24 <a href="http://www.cppblog.com/jack-wang/archive/2009/08/19/93754.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>boost::asio 服务器端</title><link>http://www.cppblog.com/jack-wang/archive/2009/08/19/93753.html</link><dc:creator>小王</dc:creator><author>小王</author><pubDate>Tue, 18 Aug 2009 16:21:00 GMT</pubDate><guid>http://www.cppblog.com/jack-wang/archive/2009/08/19/93753.html</guid><wfw:comment>http://www.cppblog.com/jack-wang/comments/93753.html</wfw:comment><comments>http://www.cppblog.com/jack-wang/archive/2009/08/19/93753.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/jack-wang/comments/commentRss/93753.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/jack-wang/services/trackbacks/93753.html</trackback:ping><description><![CDATA[<div>#include &lt;iostream&gt;</div><div>#include &lt;boost/asio.hpp&gt;</div><div>#include &lt;boost/bind.hpp&gt;</div><div>#include &lt;boost/shared_ptr.hpp&gt;</div><div>#include "Session.h"</div><div></div><div></div><div>using namespace boost::asio::ip;</div><div>using namespace boost::asio;</div><div></div><div>class Acceptor&nbsp;</div><div>{</div><div>public:</div><div><span style="white-space:pre">	</span>typedef boost::shared_ptr&lt;Session&gt;<span style="white-space:pre">	</span>SessionPtr;</div><div></div><div>public:&nbsp;</div><div><span style="white-space:pre">	</span>Acceptor(io_service&amp; ioservice, tcp::endpoint&amp; endpoint);</div><div><span style="white-space:pre">	</span></div><div><span style="white-space:pre">	</span>~Acceptor();</div><div></div><div>private:&nbsp;</div><div><span style="white-space:pre">	</span>void handle_accept(const boost::system::error_code&amp; error, SessionPtr session);</div><div></div><div>private:&nbsp;</div><div><span style="white-space:pre">	</span>io_service&amp; ioservice_;</div><div><span style="white-space:pre">	</span>tcp::acceptor acceptor_;&nbsp;</div><div>};<br /><br /><br /><div>#include "stdafx.h"</div><div>#include "Acceptor.h"</div><div></div><div>Acceptor::Acceptor(boost::asio::io_service&amp; ioservice, tcp::endpoint&amp; endpoint) :&nbsp;</div><div>ioservice_(ioservice),&nbsp;</div><div>acceptor_(ioservice, endpoint)</div><div>{</div><div><span style="white-space:pre">	</span>SessionPtr new_session(new Session(ioservice));</div><div></div><div><span style="white-space:pre">	</span>acceptor_.async_accept(new_session-&gt;socket(),</div><div><span style="white-space:pre">		</span>boost::bind(&amp;Acceptor::handle_accept, this, boost::asio::placeholders::error, new_session));</div><div>}</div><div></div><div>Acceptor::~Acceptor()</div><div>{</div><div>}</div><div></div><div>void Acceptor::handle_accept(const boost::system::error_code&amp; error, SessionPtr session)</div><div>{</div><div><span style="white-space:pre">	</span>if (!error)</div><div><span style="white-space:pre">	</span>{</div><div><span style="white-space:pre">		</span>std::cout &lt;&lt; "get a new client!" &lt;&lt; std::endl; //实现对每个客户端的数据处理&nbsp;</div><div><span style="white-space:pre">		</span>session-&gt;start();</div><div><span style="white-space:pre">		</span>SessionPtr new_session(new Session(ioservice_));</div><div><span style="white-space:pre">		</span>acceptor_.async_accept(new_session-&gt;socket(),</div><div><span style="white-space:pre">			</span>boost::bind(&amp;Acceptor::handle_accept, this, boost::asio::placeholders::error, new_session));</div><div><span style="white-space:pre">	</span>}</div><div>}<br /><br />////////////////////////////////////////////////////////////////////////////////////////////////////////////////<br /><div>#pragma once</div><div>#include &lt;iostream&gt;</div><div>#include &lt;boost/asio.hpp&gt;</div><div>#include &lt;boost/bind.hpp&gt;</div><div>#include &lt;boost/smart_ptr.hpp&gt;</div><div></div><div>#define max_len 1024</div><div></div><div>using namespace boost::asio::ip;</div><div></div><div>class Session : public boost::enable_shared_from_this&lt;Session&gt;</div><div>{</div><div>public:</div><div><span style="white-space:pre">	</span>Session(boost::asio::io_service&amp; ioservice);</div><div></div><div><span style="white-space:pre">	</span>~Session();</div><div></div><div><span style="white-space:pre">	</span>tcp::socket&amp; socket();</div><div></div><div><span style="white-space:pre">	</span>void start();</div><div></div><div>private:</div><div><span style="white-space:pre">	</span>void handle_write(const boost::system::error_code&amp; error);</div><div></div><div><span style="white-space:pre">	</span>void handle_read(const boost::system::error_code&amp; error);</div><div></div><div>private:</div><div><span style="white-space:pre">	</span>tcp::socket m_socket;</div><div><span style="white-space:pre">	</span>char data_[max_len];</div><div>};</div></div></div><img src ="http://www.cppblog.com/jack-wang/aggbug/93753.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/jack-wang/" target="_blank">小王</a> 2009-08-19 00:21 <a href="http://www.cppblog.com/jack-wang/archive/2009/08/19/93753.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>网络游戏服务器设计</title><link>http://www.cppblog.com/jack-wang/archive/2009/01/02/70968.html</link><dc:creator>小王</dc:creator><author>小王</author><pubDate>Thu, 01 Jan 2009 19:54:00 GMT</pubDate><guid>http://www.cppblog.com/jack-wang/archive/2009/01/02/70968.html</guid><wfw:comment>http://www.cppblog.com/jack-wang/comments/70968.html</wfw:comment><comments>http://www.cppblog.com/jack-wang/archive/2009/01/02/70968.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/jack-wang/comments/commentRss/70968.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/jack-wang/services/trackbacks/70968.html</trackback:ping><description><![CDATA[<h1><img height=14 alt=转载 src="http://blog.csdn.net/images/turnship.gif" width=15 border=0>&nbsp;<a href="http://blog.csdn.net/staryy/archive/2008/11/29/3410388.aspx"><font color=#3468a4>网络游戏服务器设计</font></a><cite class=fav_csdnstylebykimi><font face=Tahoma color=#3468a4 size=2>收藏</font></cite><span id=Post.ascx_ViewPost_PreviousAndNextEntriesUp></h1>
<div class=blogstory>原文：<br><a href="http://blog.csdn.net/staryy/archive/2008/11/29/3410388.aspx">http://blog.csdn.net/staryy/archive/2008/11/29/3410388.aspx</a><br></span>
<script>function StorePage(){d=document;t=d.selection?(d.selection.type!='None'?d.selection.createRange().text:''):(d.getSelection?d.getSelection():'');void(keyit=window.open('http://www.365key.com/storeit.aspx?t='+escape(d.title)+'&u='+escape(d.location.href)+'&c='+escape(t),'keyit','scrollbars=no,width=475,height=575,left=75,top=20,status=no,resizable=yes'));keyit.focus();}</script>
<a href="http://www.yq8.cn/html/15/215-47957.html"><font color=#006bad>http://www.yq8.cn/html/15/215-47957.html</font></a>
<div class=xspace-itemmessage id=xspace-showmessage>
<p><font face=Tahoma size=2>&nbsp;</p>
<div id=show>
<h1 class=xspace-title>网络游戏服务器设计</h1>
<p class=xspace-smalltxt><a href="http://www.yq8.cn/batch.common.php?action=viewspace&amp;op=up&amp;itemid=47957&amp;uid=215"><font color=#3399cc>上一篇</font></a> / <a href="http://www.yq8.cn/batch.common.php?action=viewspace&amp;op=next&amp;itemid=47957&amp;uid=215"><font color=#3399cc>下一篇</font></a> &nbsp;2008-02-22 17:34:18 / 个人分类：<a href="http://www.yq8.cn/?uid-215-action-spacelist-type-blog-itemtypeid-12"><font color=#3399cc>回收站</font></a> </p>
</div>
</font>
<p><font face=Tahoma size=2></font>&nbsp;</p>
<p><font face=Tahoma size=2>　谈这个话题之前，首先要让大家知道，什么是<a onclick="javascript:tagshow(event, '%B7%FE%CE%F1%C6%F7');" href="javascript:;" target=_self><u><strong><font color=#0066cc>服务器</font></strong></u></a>。在<a onclick="javascript:tagshow(event, '%CD%F8%C2%E7%D3%CE%CF%B7');" href="javascript:;" target=_self><u><strong><a onclick="javascript:tagshow(event, '%CD%F8%C2%E7');" href="javascript:;" target=_self><u><strong><font color=#0066cc>网络</font></strong></u></a>游戏</strong></u></a>中，服务器所扮演的角色是同步，广播和服务器主动的一些行为，比如说天气，NPC AI之类的，之所以现在的很多网络游戏服务器都需要负担一些游戏逻辑上的运算是因为为了防止客户端的<a onclick="javascript:tagshow(event, '%D7%F7%B1%D7');" href="javascript:;" target=_self><u><strong><font color=#0066cc>作弊</font></strong></u></a>行为。了解到这一点，那么本系列的文章将分为两部分来谈谈网络游戏服务器的<a onclick="javascript:tagshow(event, '%C9%E8%BC%C6');" href="javascript:;" target=_self><u><strong><font color=#0066cc>设计</font></strong></u></a>，一部分是讲如何做好服务器的网络连接，同步，广播以及NPC的设置，另一部分则将着重谈谈哪些逻辑放在服务器比较合适，并且用什么样的结构来安排这些逻辑。<br><br><br><strong>服务器的网络连接</strong><br><br>　　大多数的网络游戏的服务器都会选择非阻塞select这种结构，为什么呢？因为网络游戏的服务器需要处理的连接非常之多，并且大部分会选择在Linux/Unix下运行，那么为每个用户开一个线程实际上是很不划算的，一方面因为在Linux/Unix下的线程是用进程这么一个概念模拟出来的，比较消耗<a onclick="javascript:tagshow(event, '%CF%B5%CD%B3');" href="javascript:;" target=_self><u><strong><font color=#0066cc>系统</font></strong></u></a>资源，另外除了I/O之外，每个线程基本上没有什么多余的需要并行的<a onclick="javascript:tagshow(event, '%C8%CE%CE%F1');" href="javascript:;" target=_self><u><strong><font color=#0066cc>任务</font></strong></u></a>，而且网络游戏是互交性非常强的，所以线程间的同步会成为很麻烦的问题。由此一来，对于这种含有大量网络连接的单线程服务器，用阻塞显然是不现实的。对于网络连接，需要用一个结构来储存，其中需要包含一个向客户端写消息的缓冲，还需要一个从客户端读消息的缓冲，具体的大小根据具体的消息结构来定了。另外对于同步，需要一些时间校对的值，还需要一些各种不同的值来记录当前状态，下面给出一个初步的连接的结构：<br><br></font><font face=Fixedsys size=2>typedef connection_s {<br><br>&nbsp;&nbsp;&nbsp; user_t *ob; /* 指向处理服务器端逻辑的结构 */<br><br>&nbsp;&nbsp;&nbsp; int fd; /* socket连接 */<br><br>&nbsp;&nbsp;&nbsp; struct sockaddr_in addr; /* 连接的地址信息 */<br><br>&nbsp;&nbsp;&nbsp; char text[MAX_TEXT]; /* 接收的消息缓冲 */<br><br>&nbsp;&nbsp;&nbsp; int text_end; /* 接收消息缓冲的尾指针 */<br><br>&nbsp;&nbsp;&nbsp; int text_start; /* 接收消息缓冲的头指针 */<br><br>&nbsp;&nbsp;&nbsp; int last_time; /* 上一条消息是什么时候接收到的 */<br><br>&nbsp;&nbsp;&nbsp; struct timeval latency; /* 客户端本地时间和服务器本地时间的差值 */<br><br>&nbsp;&nbsp;&nbsp; struct timeval last_confirm_time; /* 上一次验证的时间 */<br><br>&nbsp;&nbsp;&nbsp; short is_confirmed; /* 该连接是否通过验证过 */<br><br>&nbsp;&nbsp;&nbsp; int ping_num; /* 该客户端到服务器端的ping值 */<br><br>&nbsp;&nbsp;&nbsp; int ping_ticker; /* 多少个IO周期处理更新一次ping值 */<br><br>&nbsp;&nbsp;&nbsp; int message_length; /* 发送缓冲消息长度 */<br><br>&nbsp;&nbsp;&nbsp; char message_buf[MAX_TEXT]; /* 发送缓冲区 */<br><br>&nbsp;&nbsp;&nbsp; int iflags; /* 该连接的状态 */<br><br>} connection_t;</font><font face=Tahoma><br><br><font size=2>　　服务器循环的处理所有连接，是一个死循环过程，每次循环都用select检查是否有新连接到达，然后循环所有连接，看哪个连接可以写或者可以读，就处理该连接的读写。由于所有的处理都是非阻塞的，所以所有的Socket IO都可以用一个线程来完成。<br><br>　　由于网络传输的关系，每次recv()到的数据可能不止包含一条消息，或者不到一条消息，那么怎么处理呢？所以对于接收消息缓冲用了两个指针，每次接收都从text_start开始读起，因为里面残留的可能是上次接收到的多余的半条消息，然后text_end指向消息缓冲的结尾。这样用两个指针就可以很方便的处理这种情况，另外有一点值得注意的是：<a onclick="javascript:tagshow(event, '%BD%E2%CE%F6');" href="javascript:;" target=_self><u><strong><font color=#0066cc>解析</font></strong></u></a>消息的过程是一个循环的过程，可能一次接收到两条以上的消息在消息缓冲里面，这个时候就应该执行到消息缓冲里面只有一条都不到的消息为止，大体<a onclick="javascript:tagshow(event, '%C1%F7%B3%CC');" href="javascript:;" target=_self><u><strong><font color=#0066cc>流程</font></strong></u></a>如下：<br><br></font></font><font face=Fixedsys size=2>while ( text_end &#8211; text_start &gt; 一条完整的消息长度 )<br><br>{<br><br>&nbsp;&nbsp;&nbsp; 从text_start处开始处理;<br><br>&nbsp;&nbsp;&nbsp; text_start += 该消息长度;<br><br>}<br><br>memcpy ( text, text + text_start, text_end &#8211; text_start );</font><font face=Tahoma><br><br><font size=2>　　对于消息的处理，这里首先就需要知道你的游戏总共有哪些消息，所有的消息都有哪些，才能设计出比较合理的消息头。一般来说，消息大概可分为主角消息，场景消息，同步消息和界面消息四个部分。其中主角消息包括客户端所控制的角色的所有动作，包括走路，跑步，战斗之类的。场景消息包括天气变化，一定的时间在场景里出现一些东西等等之类的，这类消息的特点是所有消息的发起者都是服务器，广播对象则是场景里的所有玩家。而同步消息则是针对发起对象是某个玩家，经过服务器广播给所有看得见他的玩家，该消息也是包括所有的动作，和主角消息不同的是该种消息是服务器广播给客户端的，而主角消息一般是客户端主动发给服务器的。最后是界面消息，界面消息包括是服务器发给客户端的聊天消息和各种<a onclick="javascript:tagshow(event, '%CA%F4%D0%D4');" href="javascript:;" target=_self><u><strong><font color=#0066cc>属性</font></strong></u></a>及状态信息。<br><br>　　下面来谈谈消息的组成。一般来说，一个消息由消息头和消息体两部分组成，其中消息头的长度是不变的，而消息体的长度是可变的，在消息体中需要保存消息体的长度。由于要给每条消息一个很明显的区分，所以需要定义一个消息头特有的标志，然后需要消息的类型以及消息ID。消息头大体结构如下：<br><br></font></font><font face=Fixedsys size=2>type struct message_s {<br><br>&nbsp;&nbsp;&nbsp; unsigned short message_sign;<br><br>&nbsp;&nbsp;&nbsp; unsigned char message_type;<br><br>&nbsp;&nbsp;&nbsp; unsigned short message_id<br><br>&nbsp;&nbsp;&nbsp; unsigned char message_len<br><br>}message_t;<br><br></font><font face=Tahoma><br><font size=2><strong>服务器的广播</strong><br><br>　　服务器的广播的重点就在于如何计算出广播的对象。很显然，在一张很大的地图里面，某个玩家在最东边的一个动作，一个在最西边的玩家是应该看不到的，那么怎么来计算广播的对象呢？最简单的<a onclick="javascript:tagshow(event, '%B0%EC%B7%A8');" href="javascript:;" target=_self><u><strong><font color=#0066cc>办法</font></strong></u></a>，就是把地图分块，分成大小合适的小块，然后每次只象周围几个小块的玩家进行广播。那么究竟切到多大比较合适呢？一般来说，切得块大了，<a onclick="javascript:tagshow(event, '%C4%DA%B4%E6');" href="javascript:;" target=_self><u><strong><font color=#0066cc>内存</font></strong></u></a>的消耗会增大，切得块小了，CPU的消耗会增大（原因会在后面提到）。个人觉得切成一屏左右的小块比较合适，每次广播广播周围九个小块的玩家，由于广播的操作非常频繁，那么遍利周围九块的操作就会变得相当的频繁，所以如果块分得小了，那么遍利的范围就会扩大，CPU的资源会很快的被吃完。<br><br>　　切好块以后，怎么让玩家在各个块之间走来走去呢？让我们来想想在切换一次块的时候要做哪些工作。首先，要算出下个块的周围九块的玩家有哪些是现在当前块没有的，把自己的信息广播给那些玩家，同时也要算出下个块周围九块里面有哪些物件是现在没有的，把那些物件的信息广播给自己，然后把下个块的周围九快里没有的，而现在的块周围九块里面有的物件的消失信息广播给自己，同时也把自己消失的消息广播给那些物件。这个操作不仅烦琐而且会吃掉不少CPU资源，那么有什么办法可以很快的算出这些物件呢？一个个做比较？显然看起来就不是个好办法，这里可以参照二维矩阵碰撞检测的一些思路，以自己周围九块为一个矩阵，目标块周围九块为另一个矩阵，检测这两个矩阵是否碰撞，如果两个矩阵相交，那么没相交的那些块怎么算。这里可以把相交的块的坐标转换成内部坐标，然后再进行运算。<br><br>　　对于广播还有另外一种<a onclick="javascript:tagshow(event, '%BD%E2%BE%F6');" href="javascript:;" target=_self><u><strong><font color=#0066cc>解决</font></strong></u></a>方法，实施起来不如切块来的简单，这种方法需要客户端来协助进行运算。首先在服务器端的连接结构里面需要增加一个广播对象的队列，该队列在客户端登陆服务器的时候由服务器传给客户端，然后客户端自己来维护这个队列，当有人走出客户端视野的时候，由客户端主动要求服务器给那个物件发送消失的消息。而对于有人总进视野的情况，则比较麻烦了。<br><br>　　首先需要客户端在每次给服务器发送update position的消息的时候，服务器都给该连接算出一个视野范围，然后在需要广播的时候，循环整张地图上的玩家，找到坐标在其视野范围内的玩家。使用这种方法的好处在于不存在转换块的时候需要一次性广播大量的消息，缺点就是在计算广播对象的时候需要遍历整个地图上的玩家，如果当一个地图上的玩家多得比较离谱的时候，该操作就会比较的慢。<br><br><br><strong>服务器的同步</strong><br><br>　　同步在网络游戏中是非常重要的，它保证了每个玩家在屏幕上看到的东西大体是一样的。其实呢，解决同步问题的最简单的方法就是把每个玩家的动作都向其他玩家广播一遍，这里其实就存在两个问题：1，向哪些玩家广播，广播哪些消息。2，如果网络延迟怎么办。事实上呢，第一个问题是个非常简单的问题，不过之所以我提出这个问题来，是提醒大家在设计自己的消息结构的时候，需要把这个因素考虑进去。而对于第二个问题，则是一个挺麻烦的问题，大家可以来看这么个例子：<br><br>　　比如有一个玩家A向服务器发了条<a onclick="javascript:tagshow(event, '%D6%B8%C1%EE');" href="javascript:;" target=_self><u><strong><font color=#0066cc>指令</font></strong></u></a>，说我现在在P1点，要去P2点。指令发出的时间是T0，服务器收到指令的时间是T1，然后向周围的玩家广播这条消息，消息的内容是&#8220;玩家A从P1到P2&#8221;有一个在A附近的玩家B，收到服务器的这则广播的消息的时间是T2，然后开始在客户端上画图，A从P1到P2点。这个时候就存在一个不同步的问题，玩家A和玩家B的屏幕上显示的画面相差了T2-T1的时间。这个时候怎么办呢？<br><br><br><br>　　有个解决方案，我给它取名叫 预测拉扯，虽然有些怪异了点，不过基本上大家也能从字面上来理解它的意思。要解决这个问题，首先要定义一个值叫：预测误差。然后需要在服务器端每个玩家连接的类里面加一项属性，叫latency，然后在玩家登陆的时候，对客户端的时间和服务器的时间进行比较，得出来的差值保存在latency里面。还是上面的那个例子，服务器广播消息的时候，就根据要广播对象的latency，计算出一个客户端的CurrentTime，然后在消息头里面包含这个CurrentTime，然后再进行广播。并且同时在玩家A的客户端本地建立一个队列，保存该条消息，只到获得服务器验证就从未被验证的消息队列里面将该消息删除，如果验证失败，则会被拉扯回P1点。然后当玩家B收到了服务器发过来的消息&#8220;玩家A从P1到P2&#8221;这个时候就检查消息里面服务器发出的时间和本地时间做比较，如果大于定义的预测误差，就算出在T2这个时间，玩家A的屏幕上走到的地点P3，然后把玩家B屏幕上的玩家A直接拉扯到P3，再继续走下去，这样就能保证同步。更进一步，为了保证客户端运行起来更加smooth，我并不推荐直接把玩家拉扯过去，而是算出P3偏后的一点P4，然后用(P4-P1)/T(P4-P3)来算出一个很快的速度S，然后让玩家A用速度S快速移动到P4，这样的处理方法是比较合理的，这种解决方案的原形在国际上被称为（Full plesiochronous），当然，该原形被我篡改了很多来适应网络游戏的同步，所以而变成所谓的：预测拉扯。<br><br>　　另外一个解决方案，我给它取名叫 验证同步，听名字也知道，大体的意思就是每条指令在经过服务器验证通过了以后再执行动作。具体的思路如下：首先也需要在每个玩家连接类型里面定义一个latency，然后在客户端响应玩家鼠标行走的同时，客户端并不会先行走动，而是发一条走路的指令给服务器，然后等待服务器的验证。服务器接受到这条消息以后，进行逻辑层的验证，然后计算出需要广播的范围，包括玩家A在内，根据各个客户端不同的latency生成不同的消息头，开始广播，这个时候这个玩家的走路信息就是完全同步的了。这个方法的优点是能保证各个客户端之间绝对的同步，缺点是当网络延迟比较大的时候，玩家的客户端的行为会变得比较不流畅，给玩家带来很不爽的感觉。该种解决方案的原形在国际上被称为（Hierarchical master-slave synchronization），80年代以后被广泛应用于网络的各个领域。<br><br>　　最后一种解决方案是一种理想化的解决方案，在国际上被称为Mutual synchronization，是一种对未来网络的前景的良好预测出来的解决方案。这里之所以要提这个方案，并不是说我们已经完全的实现了这种方案，而只是在网络游戏领域的某些方面应用到这种方案的某些思想。我对该种方案取名为：半服务器同步。大体的设计思路如下：<br><br>　　首先客户端需要在登陆世界的时候建立很多张广播列表，这些列表在客户端后台和服务器要进行不及时同步，之所以要建立多张列表，是因为要广播的类型是不止一种的，比如说有local message,有remote message,还有global message 等等，这些列表都需要在客户端登陆的时候根据服务器发过来的消息建立好。在建立列表的同时，还需要获得每个列表中广播对象的latency，并且要维护一张完整的用户状态列表在后台，也是不及时的和服务器进行同步，根据本地的用户状态表，可以做到一部分决策由客户端自己来决定，当客户端发送这部分决策的时候，则直接将最终决策发送到各个广播列表里面的客户端，并对其时间进行校对，保证每个客户端在收到的消息的时间是和根据本地时间进行校对过的。那么再采用预测拉扯中提到过的计算提前量，提高速度行走过去的方法，将会使同步变得非常的smooth。该方案的优点是不通过服务器，客户端自己之间进行同步，大大的降低了由于网络延迟而带来的误差，并且由于大部分决策都可以由客户端来做，也大大的降低了服务器的资源。由此带来的弊端就是由于消息和决策权都放在客户端本地，所以给外挂提供了很大的可乘之机。</font></font></p>
<p><font face=Tahoma size=2>　下面我想来谈谈关于服务器上NPC的设计以及NPC智能等一些方面涉及到的问题。首先，我们需要知道什么是NPC，NPC需要做什么。NPC的全称是（Non-Player Character），很显然，他是一个character，但不是玩家，那么从这点上可以知道，NPC的某些行为是和玩家类似的，他可以行走，可以战斗，可以呼吸（这点将在后面的NPC智能里面提到），另外一点和玩家物件不同的是，NPC可以复生（即NPC被打死以后在一定时间内可以重新出来）。其实还有最重要的一点，就是玩家物件的所有决策都是玩家做出来的，而NPC的决策则是由计算机做出来的，所以在对NPC做何种决策的时候，需要所谓的NPC智能来进行决策。<br><br>　　下面我将分两个部分来谈谈NPC，首先是NPC智能，其次是服务器如何对NPC进行组织。之所以要先谈NPC智能是因为只有当我们了解清楚我们需要NPC做什么之后，才好开始设计服务器来对NPC进行组织。<br><br><br><strong>NPC智能</strong><br><br>　　NPC智能分为两种，一种是被动触发的事件，一种是主动触发的事件。对于被动触发的事件，处理起来相对来说简单一些，可以由事件本身来呼叫NPC身上的函数，比如说NPC的死亡，实际上是在NPC的HP小于一定值的时候，来主动呼叫NPC身上的OnDie() 函数，这种由事件来触发NPC行为的NPC智能，我称为被动触发。这种类型的触发往往分为两种：<br><br>一种是由别的物件导致的NPC的属性变化，然后属性变化的同时会导致NPC产生一些行为。由此一来，NPC物件里面至少包含以下几种函数：<br><br></font><font face=Fixedsys size=2>class NPC {<br><br>public:<br><br>&nbsp;&nbsp;&nbsp; // 是谁在什么地方导致了我哪项属性改变了多少。<br><br>&nbsp;&nbsp;&nbsp; OnChangeAttribute(object_t *who, int which, int how, int where);<br><br>Private:<br><br>&nbsp;&nbsp;&nbsp; OnDie();<br><br>&nbsp;&nbsp;&nbsp; OnEscape();<br><br>&nbsp;&nbsp;&nbsp; OnFollow();<br><br>&nbsp;&nbsp;&nbsp; OnSleep();<br><br>&nbsp;&nbsp;&nbsp; // 一系列的事件。<br><br>}<br></font><font face=Tahoma><br><font size=2>　　这是一个基本的NPC的结构，这种被动的触发NPC的事件，我称它为NPC的反射。但是，这样的结构只能让NPC被动的接收一些信息来做出决策，这样的NPC是愚蠢的。那么，怎么样让一个NPC能够主动的做出一些决策呢？这里有一种方法：呼吸。那么怎么样让NPC有呼吸呢？<br><br>　　一种很简单的方法，用一个计时器，定时的触发所有NPC的呼吸，这样就可以让一个NPC有呼吸起来。这样的话会有一个问题，当NPC太多的时候，上一次NPC的呼吸还没有呼吸完，下一次呼吸又来了，那么怎么解决这个问题呢。这里有一种方法，让NPC异步的进行呼吸，即每个NPC的呼吸周期是根据NPC出生的时间来定的，这个时候计时器需要做的就是隔一段时间检查一下，哪些NPC到时间该呼吸了，就来触发这些NPC的呼吸。<br><br>　　上面提到的是系统如何来触发NPC的呼吸，那么NPC本身的呼吸频率该如何设定呢？这个就好象现实中的人一样，睡觉的时候和进行激烈运动的时候，呼吸频率是不一样的。同样，NPC在战斗的时候，和平常的时候，呼吸频率也不一样。那么就需要一个Breath_Ticker来设置NPC当前的呼吸频率。<br><br>　　那么在NPC的呼吸事件里面，我们怎么样来设置NPC的智能呢？大体可以概括为检查环境和做出决策两个部分。首先，需要对当前环境进行数字上的统计，比如说是否在战斗中，战斗有几个敌人，自己的HP还剩多少，以及附近有没有敌人等等之类的统计。统计出来的数据传入本身的决策模块，决策模块则根据NPC自身的性格取向来做出一些决策，比如说野蛮型的NPC会在HP比较少的时候仍然猛扑猛打，又比如说智慧型的NPC则会在HP比较少的时候选择逃跑。等等之类的。<br><br>　　至此，一个可以呼吸，反射的NPC的结构已经基本构成了，那么接下来我们就来谈谈系统如何组织让一个NPC出现在世界里面。<br><br><br><strong>NPC的组织</strong><br><br>　　这里有两种方案可供选择，其一：NPC的位置信息保存在场景里面，载入场景的时候载入NPC。其二，NPC的位置信息保存在NPC身上，有专门的事件让所有的NPC登陆场景。这两种方法有什么区别呢？又各有什么好坏呢？<br><br>　　前一种方法好处在于场景载入的时候同时载入了NPC，场景就可以对NPC进行管理，不需要多余的处理，而弊端则在于在刷新的时候是同步刷新的，也就是说一个场景里面的NPC可能会在同一时间内长出来。而对于第二种方法呢，设计起来会稍微麻烦一些，需要一个统一的机制让NPC登陆到场景，还需要一些比较麻烦的设计，但是这种方案可以实现NPC异步的刷新，是目前网络游戏普遍采用的方法，下面我们就来着重谈谈这种方法的实现：<br><br>　　首先我们要引入一个&#8220;灵魂&#8221;的概念，即一个NPC在死后，消失的只是他的肉体，他的灵魂仍然在世界中存在着，没有呼吸，在死亡的附近漂浮，等着到时间投胎，投胎的时候把之前的所有属性清零，重新在场景上构建其肉体。那么，我们怎么来设计这样一个结构呢？首先把一个场景里面要出现的NPC制作成图量表，给每个NPC一个独一无二的标识符，在载入场景之后，根据图量表来载入属于该场景的NPC。在NPC的OnDie() 事件里面不直接把该物件destroy 掉，而是关闭NPC的呼吸，然后打开一个重生的计时器，最后把该物件设置为invisable。这样的设计，可以实现NPC的异步刷新，在节省服务器资源的同时也让玩家觉得更加的真实。<br><br>（这一章节已经牵扯到一些服务器脚本相关的东西，所以下一章节将谈谈服务器脚本相关的一些设计）<br><br>补充的谈谈启发式搜索（heuristic searching）在NPC智能中的应用。<br><br>　　其主要思路是在广度优先搜索的同时，将下一层的所有节点经过一个启发函数进行过滤，一定范围内缩小搜索范围。众所周知的寻路A*算法就是典型的启发式搜索的应用，其原理是一开始设计一个Judge(point_t* point)函数，来获得point这个一点的代价，然后每次搜索的时候把下一步可能到达的所有点都经过Judge()函数评价一下，获取两到三个代价比较小的点，继续搜索，那些没被选上的点就不会在继续搜索下去了，这样带来的后果的是可能求出来的不是最优路径，这也是为什么A*算法在寻路的时候会走到障碍物前面再绕过去，而不是预先就走斜线来绕过该障碍物。如果要寻出最优化的路径的话，是不能用A*算法的，而是要用动态规划的方法，其消耗是远大于A*的。<br><br>　　那么，除了在寻路之外，还有哪些地方可以应用到启发式搜索呢？其实说得大一点，NPC的任何决策都可以用启发式搜索来做，比如说逃跑吧，如果是一个2D的网络游戏，有八个方向，NPC选择哪个方向逃跑呢？就可以设置一个Judge(int direction)来给定每个点的代价，在Judge里面算上该点的敌人的强弱，或者该敌人的敏捷如何等等，最后选择代价最小的地方逃跑。下面，我们就来谈谈对于几种NPC常见的智能的启发式搜索法的设计：<br><br>Target select （选择目标）：<br><br>　　首先获得地图上离该NPC附近的敌人列表。设计Judge() 函数，根据敌人的强弱，敌人的远近，算出代价。然后选择代价最小的敌人进行主动攻击。<br><br>Escape（逃跑）：<br><br>　　在呼吸事件里面检查自己的HP，如果HP低于某个值的时候，或者如果你是远程兵种，而敌人近身的话，则触发逃跑函数，在逃跑函数里面也是对周围的所有的敌人组织成列表，然后设计Judge() 函数，先选择出对你构成威胁最大的敌人，该Judge() 函数需要判断敌人的速度，战斗力强弱，最后得出一个主要敌人，然后针对该主要敌人进行路径的Judge() 的函数的设计，搜索的范围只可能是和主要敌人相反的方向，然后再根据该几个方向的敌人的强弱来计算代价，做出最后的选择。<br><br>Random walk（随机走路）：<br><br>　　这个我并不推荐用A*算法，因为NPC一旦多起来，那么这个对CPU的消耗是很恐怖的，而且NPC大多不需要长距离的寻路，只需要在附近走走即可，那么，就在附近随机的给几个点，然后让NPC走过去，如果碰到障碍物就停下来，这样几乎无任何负担。<br><br>Follow Target（追随目标）：<br><br>　　这里有两种方法，一种方法NPC看上去比较愚蠢，一种方法看上去NPC比较聪明，第一种方法就是让NPC跟着目标的路点走即可，几乎没有资源消耗。而后一种则是让NPC在跟随的时候，在呼吸事件里面判断对方的当前位置，然后走直线，碰上障碍物了用A*绕过去，该种设计会消耗一定量的系统资源，所以不推荐NPC大量的追随目标，如果需要大量的NPC追随目标的话，还有一个比较简单的方法：让NPC和目标同步移动，即让他们的速度统一，移动的时候走同样的路点，当然，这种设计只适合NPC所跟随的目标不是追杀的关系，只是跟随着玩家走而已了。</font></font></p>
<p><font size=2>　在这一章节，我想谈谈关于服务器端的脚本的相关设计。因为在上一章节里面，谈NPC智能相关的时候已经接触到一些脚本相关的东东了。还是先来谈谈脚本的作用吧。<br>　　在基于编译的服务器端程序中，是无法在程序的运行过程中构建一些东西的，那么这个时候就需要脚本语言的支持了，由于脚本语言涉及到逻辑判断，所以光提供一些函数接口是没用的，还需要提供一些简单的语法和文法解析的功能。其实说到底，任何的事件都可以看成两个部分：第一是对自身，或者别的物件的数值的改变，另外一个就是将该事件以文字或者图形的方式广播出去。那么，这里牵扯到一个很重要的话题，就是对某一物件进行寻址。恩，谈到这，我想将本章节分为三个部分来谈，首先是服务器如何来管理动态创建出来的物件（服务器内存管理），第二是如何对某一物件进行寻址，第三则是脚本语言的组织和解释。其实之所以到第四章再来谈服务器的内存管理是因为在前几章谈这个的话，大家对其没有一个感性的认识，可能不知道服务器的内存管理究竟有什么用。<br><br><strong>4.1、服务器内存管理</strong><br>　　对于服务器内存管理我们将采用内存池的方法，也称为静态内存管理。其概念为在服务器初始化的时候，申请一块非常大的内存，称为内存池（<span class=English>Memory pool</span>），同时也申请一小块内存空间，称为垃圾回收站（<span class=English>Garbage recollecting station</span>）。其大体思路如下：当程序需要申请内存的时候，首先检查垃圾回收站是否为空，如果不为空的话，则从垃圾回收站中找一块可用的内存地址，在内存池中根据地址找到相应的空间，分配给程序用，如果垃圾回收站是空的话，则直接从内存池的当前指针位置申请一块内存；当程序释放空间的时候，给那块内存打上已经释放掉的标记，然后把那块内存的地址放入垃圾回收站。<br>　　下面具体谈谈该方法的详细设计，首先，我们将采用类似于操作系统的段页式系统来管理内存，这样的好处是可以充分的利用内存池，其缺点是管理起来比较麻烦。嗯，下面来具体看看我们怎么样来定义页和段的结构：<br><br></font><font face=Fixedsys>　　<span class=ColorCode>typedef struct m_segment_s<br>　　{<br>　　　　struct m_segment_s *next;</span>　<font color=#808080><span class=ColorCatchword>/* 双线链表 + 静态内存可以达到随机访问和顺序访问的目的，<br>　　　　　　　　　　　　　　　　　　　真正的想怎么访问，就怎么访问。 */</span></font><br>　　　　<span class=ColorCode>struct m_segment_s *pre; int flags;</span>　　<span class=ColorCatchword><font color=#808080>// 该段的一些标记。</font><br></span>　　　　<span class=ColorCode>int start;</span>　　　　　　　　　　　　　　<span class=ColorCatchword><font color=#808080>// 相对于该页的首地址。</font><br></span>　　　　<span class=ColorCode>int size;</span>　　　　　　　　　　　　　　　<font color=#808080><span class=ColorCatchword>// 长度。</span></font><br>　　　　<span class=ColorCode>struct m_page_s *my_owner;</span>　　　　　　<font color=#808080><span class=ColorCatchword>// 我是属于哪一页的。</span></font><br>　　　　<span class=ColorCode>char *data;</span>　　　　　　　　　　　　　　<font color=#808080><span class=ColorCatchword>// 内容指针。</span></font><br>　　<span class=ColorCode>}m_segment_t;<br><br>　　typedef struct m_page_s<br>　　{<br>　　　　unsigned int flags;</span>　　　<span class=ColorCatchword><font color=#808080>/* 使用标记，是否完全使用，是否还有空余 */</font><br></span>　　　　<span class=ColorCode>int size;</span>　　　　　　　　<font color=#808080><span class=ColorCatchword>/* 该页的大小，一般都是统一的，最后一页除外 */</span></font><br>　　　　<span class=ColorCode>int end;</span>　　　　　　　 　<span class=ColorCatchword><font color=#808080>/* 使用到什么地方了 */</font><br></span>　　　　<span class=ColorCode>int my_index;</span>　　　　　　<span class=ColorCatchword><font color=#808080>/* 提供随机访问的索引 */</font><br></span>　　　　<span class=ColorCode>m_segment_t *segments;</span>　　<span class=ColorCatchword><font color=#808080>// 页内段的头指针。</font><br></span>　<span class=ColorCode>　}m_page_t;<br></span></font><font face=Arial size=2><br>　　那么内存池和垃圾回收站怎么构建呢？下面也给出一些构建相关的伪<a onclick="javascript:tagshow(event, '%B4%FA%C2%EB');" href="javascript:;" target=_self><u><strong><font color=#0066cc>代码</font></strong></u></a>：<br><br></font><font face=Fixedsys>　　<span class=ColorCode>static m_page_t *all_pages;<br></span>　　<span class=ColorCatchword><font color=#808080>// total_size是总共要申请的内存数，num_pages是总共打算创建多少个页面。</font><br></span>　　<span class=ColorCode>void initialize_memory_pool( int total_size, int num_pages )<br>　　{<br>　　　　int i, page_size, last_size;</span>　　　　<span class=ColorCatchword><font color=#808080>// 算出每个页面的大小。</font><br></span>　　　　<span class=ColorCode>page_size = total_size / num_pages;</span>　<span class=ColorCatchword><font color=#808080>// 分配足够的页面。</font><br></span>　　　　<span class=ColorCode>all_pages = (m_page_t*) calloc( num_pages, sizeof(m_page_t*) );<br>　　　　for ( i = 0; i &lt; num_pages; i ++ )<br>　　　　{<br></span>　　　　　　<span class=ColorCatchword><font color=#808080>// 初始化每个页面的段指针。</font><br></span>　　　　　　<span class=ColorCode>all_pages[i].m_segment_t = (m_segment_t*) malloc( page_size );<br></span>　　　　　　<span class=ColorCatchword><font color=#808080>// 初始化该页面的标记。</font><br></span>　　　　　　<span class=ColorCode>all_pages[i].flags |= NEVER_USED;<br></span>　　　　　　<span class=ColorCatchword><font color=#808080>// 除了最后一个页面，其他的大小都是page_size 大小。</font><br></span>　　　　　　<span class=ColorCode>all_pages[i].size = page_size;<br></span>　　　　　　<span class=ColorCatchword><font color=#808080>// 初始化随机访问的索引。</font><br></span>　　　　　　<span class=ColorCode>all_pages[i].my_index = i;<br></span>　　　　　　<span class=ColorCatchword><font color=#808080>// 由于没有用过，所以大小都是0</font><br></span>　　　　　　<span class=ColorCode>all_pages[i].end = 0;<br>　　　　}<br></span><br>　　　　<font color=#808080><span class=ColorCatchword>// 设置最后一个页面的大小。</span></font><br>　　　　<span class=ColorCode>if ( (last_size = total_size % num_pages) != 0 )<br>　　　　　　all_pages[i].size = last_size;<br>　　}<br></span></font><font face=Arial size=2><br>　　下面看看垃圾回收站怎么设计：<br><br></font><font face=Fixedsys>　　<span class=ColorCode>int **garbage_station;<br>　　void init_garbage_station( int num_pages, int page_size )<br>　　{<br>　　　　int i;<br>　　　　garbage_station = (int**) calloc( num_pages, sizeof( int* ) );<br>　　　　for ( i = 0; i &lt; num_pages; i ++)<br>　　　　{<br></span>　　　　　　<span class=ColorCatchword><font color=#808080>// 这里用unsigned short的高8位来储存首相对地址，低8位来储存长度。</font><br></span>　　　　　　</font><span class=ColorCode><font face=Fixedsys>garbage_station[i] = (int*) calloc( page_size, sizeof( unsigned short ));<br>　　　　　　memset( garbage_station[i], 0, sizeof( garbage_station[i] ));<br>　　　　}<br>　　}</font><font face=Arial><br></font></span><font face=Arial size=2><br>　　也许这样的贴代码会让大家觉得很不明白，嗯，我的代码水平确实不怎么样，那么下面我来用文字方式来叙说一下大体的概念吧。对于段页式内存管理，首先分成N个页面，这个是固定的，而对于每个页面内的段则是动态的，段的大小事先是不知道的，那么我们需要回收的不仅仅是页面的内存，还包括段的内存，那么我们就需要一个二维数组来保存是哪个页面的那块段的地址被释放了。然后对于申请内存的时候，则首先检查需要申请内存的大小，如果不够一个页面大小的话，则在垃圾回收站里面寻找可用的段空间分配，如果找不到，则申请一个新的页面空间。<br>　　这样用内存池的方法来管理整个游戏世界的内存可以有效的减少内存碎片，一定程度的提高游戏运行的稳定性和效率。<br><br><strong>4.2、游戏中物件的寻址<br></strong>　　第一个问题，我们为什么要寻址？加入了脚本语言的概念之后，游戏中的一些逻辑物件，比如说NPC，某个ITEM之类的都是由脚本语言在游戏运行的过程中动态生成的，那么我们通过什么样的方法来对这些物件进行索引呢？说得简单一点，就是如何找到他们呢？有个很简单的方法，全部遍历一次。当然，这是个简单而有效的方法，但是效率上的消耗是任何一台服务器都吃不消的，特别是在游戏的规模比较大之后。<br>　　那么，我们怎么来在游戏世界中很快的寻找这些物件呢？我想在谈这个之前，说一下Hash Table这个数据结构，它叫哈希表，也有人叫它散列表，其工作原理是不是顺序访问，也不是随机访问，而是通过一个散列函数对其key进行计算，算出在内存中这个key对应的value的地址，而对其进行访问。好处是不管面对多大的数据，只需要一次计算就能找到其地址，非常的快捷，那么弊端是什么呢？当两个key通过散列函数计算出来的地址是同一个地址的时候，麻烦就来了，会产生碰撞，其的解决方法非常的麻烦，这里就不详细谈其解决方法了，否则估计再写个四，五章也未必谈得清楚，不过如果大家对其感兴趣的话，欢迎讨论。<br>　　嗯，我们将用散列表来对游戏中的物件进行索引，具体怎么做呢？首先，在内存池中申请一块两倍大于游戏中物件总数的内存，为什么是两倍大呢？防止散列表碰撞。然后我们选用物件的名称作为散列表的索引key，然后就可以开始设计散列函数了。下面来看个例子：<br><br></font></font><font face=Fixedsys size=2>　　<span class=ColorCode>static int T[] =<br>　　{<br>　　　　1, 87, 49, 12, 176, 178, 102, 166, 121, 193, 6, 84, 249, 230, 44, 163,<br>　　　　14, 197, 213, 181, 161, 85, 218, 80, 64, 239, 24, 226, 236, 142, 38, 200,<br>　　　　110, 177, 104, 103, 141, 253, 255, 50, 77, 101, 81, 18, 45, 96, 31, 222,<br>　　　　25, 107, 190, 70, 86, 237, 240, 34, 72, 242, 20, 214, 244, 227, 149, 235,<br>　　　　97, 234, 57, 22, 60, 250, 82, 175, 208, 5, 127, 199, 111, 62, 135, 248,<br>　　　　174, 169, 211, 58, 66, 154, 106, 195, 245, 171, 17, 187, 182, 179, 0, 243,<br>　　　　132, 56, 148, 75, 128, 133, 158, 100, 130, 126, 91, 13, 153, 246, 216, 219,<br>　　　　119, 68, 223, 78, 83, 88, 201, 99, 122, 11, 92, 32, 136, 114, 52, 10,<br>　　　　138, 30, 48, 183, 156, 35, 61, 26, 143, 74, 251, 94, 129, 162, 63, 152,<br>　　　　170, 7, 115, 167, 241, 206, 3, 150, 55, 59, 151, 220, 90, 53, 23, 131,<br>　　　　125, 173, 15, 238, 79, 95, 89, 16, 105, 137, 225, 224, 217, 160, 37, 123,<br>　　　　118, 73, 2, 157, 46, 116, 9, 145, 134, 228, 207, 212, 202, 215, 69, 229,<br>　　　　27, 188, 67, 124, 168, 252, 42, 4, 29, 108, 21, 247, 19, 205, 39, 203,<br>　　　　233, 40, 186, 147, 198, 192, 155, 33, 164, 191, 98, 204, 165, 180, 117, 76,<br>　　　　140, 36, 210, 172, 41, 54, 159, 8, 185, 232, 113, 196, 231, 47, 146, 120,<br>　　　　51, 65, 28, 144, 254, 221, 93, 189, 194, 139, 112, 43, 71, 109, 184, 209,<br>　　};<br><br></span>　　<span class=ColorCatchword><font color=#808080>// s是需要进行索引的字符串指针，maxn是字符串可能的最大长度，返回值是相对地址。</font><br></span>　　</font><span class=ColorCode><font face=Fixedsys size=2>inline int whashstr(char *s, int maxn)<br>　　{<br>　　　　register unsigned char oh, h;<br>　　　　register unsigned char *p;<br>　　　　register int i;<br><br>　　　　if (!*s)<br>　　　　　　return 0;<br>　　　　p = (unsigned char *) s;<br>　　　　oh = T[*p]; h = (*(p++) + 1) &amp; 0xff;<br>　　　　for (i = maxn - 1; *p &amp;&amp; --i &gt;= 0; )<br>　　　　{<br>　　　　　　oh = T[oh ^ *p]; h = T[h ^ *(p++)];<br>　　　　}<br>　　　　return (oh &lt;&lt; 8) + h;<br>　　}</font><font face=Arial size=2><br></font></span><font face=Arial><font face=Arial size=2><br>　　具体的算法就不说了，上面的那一大段东西不要问我为什么，这个算法的出处是CACM 33-6中的一个叫Peter K.Pearson的鬼子写的论文中介绍的算法，据说速度非常的快。有了这个散列函数，我们就可以通过它来对世界里面的任意物件进行非常快的寻址了。<br><br><strong>4.3、脚本语言解释</strong><br>　　在设计脚本语言之前，我们首先需要明白，我们的脚本语言要实现什么样的功能？否则随心所欲的做下去写出个C的解释器之类的也说不定。我们要实现的功能只是简单的逻辑判断和循环，其他所有的功能都可以由事先提供好的函数来完成。嗯，这样我们就可以列出一张工作量的表单：设计物件在底层的保存结构，提供脚本和底层间的访问接口，设计支持逻辑判断和循环的解释器。<br>　　下面先来谈谈物件在底层的保存结构。具体到每种不同属性的物件，需要采用不同的结构，当然，如果你愿意的话，你可以所有的物件都采同同样的结构，然后在结构里面设计一个散列表来保存各种不同的属性。但这并不是一个好方法，过分的依赖散列表会让你的游戏的逻辑变得繁杂不清。所以，尽量的区分每种不同的物件采用不同的结构来设计。但是有一点值得注意的是，不管是什么结构，有一些东西是统一的，就是我们所说的物件头，那么我们怎么来设计这样一个物件头呢？<br><br></font></font><font face=Fixedsys size=2>　　</font><span class=ColorCode><font face=Fixedsys size=2>typedef struct object_head_s<br>　　{<br>　　　　char* name;<br>　　　　char* prog;<br>　　}object_head_t;</font><font face=Arial size=2><br></font></span><font face=Arial><font face=Arial size=2><br>　　其中name是在散列表中这个物件的索引号，prog则是脚本解释器需要解释的程序内容。下面我们就以NPC为例来设计一个结构：<br><br></font></font><font face=Fixedsys size=2>　　<span class=ColorCode>typedef struct npc_s<br>　　{<br>　　　　object_head_t header;</span>　　　　<span class=ColorCatchword><font color=#808080>// 物件头</font><br></span>　　　　<span class=ColorCode>int hp;</span>　　　　　　　　　　　<span class=ColorCatchword><font color=#808080>// NPC的hp值。</font><br></span>　　　　<span class=ColorCode>int level;</span>　　　　　　　 　　<span class=ColorCatchword><font color=#808080>// NPC的等级。</font><br></span>　　　　<span class=ColorCode>struct position_s position;</span>　<span class=ColorCatchword><font color=#808080>// 当前的位置信息。</font><br></span>　　　　<span class=ColorCode>unsigned int personality;</span>　　<font color=#808080><span class=ColorCatchword>// NPC的个性，一个unsigned int可以保存24种个性。</span></font><br>　　<span class=ColorCode>}npc_t;</span></font><font face=Arial><font face=Arial size=2><br><br>　　OK，结构设计完成，那么我们怎么来设计脚本解释器呢？这里有两种法，一种是用虚拟机的模式来解析脚本语言，另外一中则是用类似汇编语言的那种结构来设计，设置一些条件跳转和循环就可以实现逻辑判断和循环了，比如：<br><br></font></font><font face=Fixedsys size=2>　　<span class=ColorCode>set name, "路人甲";<br>　　CHOOSE: random_choose_personality;</span>　　<span class=ColorCatchword><font color=#808080>// 随机选择NPC的个性</font><br></span>　　<span class=ColorCode>compare hp, 100;</span>　　　　　　　　　　　<span class=ColorCatchword><font color=#808080>// 比较气血，比较出的值可以放在一个固定的变量里面</font><br></span>　　<span class=ColorCode>ifless LESS;</span>　　　　　　　　　　　　　<span class=ColorCatchword><font color=#808080>// hp &lt; 100的话，则返回。</font><br></span>　　<span class=ColorCode>jump CHOOSE;</span>　　　　　　　　　　　　　<font color=#808080><span class=ColorCatchword>// 否则继续选择，只到选到一个hp &lt; 100的。</span></font><br>　</font><span class=ColorCode><font face=Fixedsys size=2>　LESS: return success;</font><font face=Arial size=2><br></font></span><font face=Arial><font face=Arial size=2><br>　　这种脚本结构就类似CPU的指令的结构，一条一条指令按照顺序执行，对于脚本程序员（<span class=English>Script. Programmer</span>）也可以培养他们汇编能力的说。<br>　　那么怎么来模仿这种结构呢？我们拿CPU的指令做参照，首先得设置一些寄存器，CPU的寄存器的大小和数量是受硬件影响的，但我们是用内存来模拟寄存器，所以想要多大，就可以有多大。然后提供一些指令，包括四则运算，寻址，判断，循环等等。接下来针对不同的脚本用不同的解析方法，比如说对NPC就用NPC固定的脚本，对ITEM就用ITEM固定的脚本，解析完以后就把结果生成底层该物件的结构用于使用。<br>　　而如果要用虚拟机来实现脚本语言的话呢，则会将工程变得无比之巨大，强烈不推荐使用，不过如果你想做一个通用的网络游戏底层的话，则可以考虑设计一个虚拟机。虚拟机大体的解释过程就是进行两次编译，第一次对关键字进行编译，第二次生成汇编语言，然后虚拟机在根据编译生成的汇编语言进行逐行解释，如果大家对这个感兴趣的话，可以去<a class=cLink href="http://www.mudos.org/" target=_blank><u><font color=#0066cc>www.mudos.org</font></u></a>上<a onclick="javascript:tagshow(event, '%CF%C2%D4%D8');" href="javascript:;" target=_self><u><strong><font color=#0066cc>下载</font></strong></u></a>一份MudOS的原码来研究研究。</font></font></p>
</div>
</div>
<img src ="http://www.cppblog.com/jack-wang/aggbug/70968.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/jack-wang/" target="_blank">小王</a> 2009-01-02 03:54 <a href="http://www.cppblog.com/jack-wang/archive/2009/01/02/70968.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>高效网游服务器实现探讨</title><link>http://www.cppblog.com/jack-wang/archive/2009/01/02/70966.html</link><dc:creator>小王</dc:creator><author>小王</author><pubDate>Thu, 01 Jan 2009 19:49:00 GMT</pubDate><guid>http://www.cppblog.com/jack-wang/archive/2009/01/02/70966.html</guid><wfw:comment>http://www.cppblog.com/jack-wang/comments/70966.html</wfw:comment><comments>http://www.cppblog.com/jack-wang/archive/2009/01/02/70966.html#Feedback</comments><slash:comments>1</slash:comments><wfw:commentRss>http://www.cppblog.com/jack-wang/comments/commentRss/70966.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/jack-wang/services/trackbacks/70966.html</trackback:ping><description><![CDATA[<p>随着多核处理器的普及,如何充分利用多核并行工作就成为高性能程序设计的一个重点。本系列文章将围绕高性能网游服务器的实现,探讨这方面的技术。<br><br>网游服务器的特点是:<br><br>具有大量客户端连接(数百至数千个),每个客户端都以一定的速率不断发送和接收数据;<br>服务器端的数据流量通常在几个至几十个Mbps之间;<br>数据需要实时处理;<br>数据包具有时序关系,往往需要按照严格的先后顺序予以处理。<br><br>网游服务器实际上代表了一类典型的新兴流数据处理服务器。这里只是为了讨论方便而限定于网游服务器,但是所讨论的原理和技术应该是普适的。<br><br>同步多线程技术肯定是无法满足要求的。由于每个客户端都在持续和服务器交换数据,系统将无法有效管理太多的线程;即使使用线程池技术,所能服务的客户连接也是很有限的。至于数据处理的实时性和数据的时序都无法顾及。<br><br>异步技术有好几种方式,这里只讨论IOCP和轮询模式。IOCP是微软推动的技术。对非常大量的连接(数千至数万)很有效。但是由于使用了多线程,这些线程需要把所需读写的数据通过共享的FIFO与主线程解耦(否则无法保持时序)。这就造成频繁的线程切换,无法满足大数据量的实时处理要求。另外,由于网卡只有一块(就一个网络地址而言),多线程并不能增加读写的速率。在另外一些时序要求不那么严格的场合,这些线程可以各自独立完成所有的处理任务,只需要在线程内部保持数据的时序。这就是向同步多线程技术退化了。<br><br>轮询是常用的模式。程序员把需要处理的Socket连接注册到一个数据结构中,然后提交给系统检查它们的读写状态。系统返回可供操作的Socket连接列表供程序员逐个处理。如果有数据可读就读入并处理,如果可写则把相应的数据写出去。为了提高效率和程序结构的清晰起见,Socket服务器通常单独使用一个线程,并且通过FIFO数据结构和主线程解耦。<br><br>在单核处理器上,上面这种轮询的模式是没有问题的。但是在多核平台上,用于解耦的FIFO将会变成并发瓶颈。这是因为传统的实现技术必须对FIFO加锁。虽然网络线程和主线程分别跑在不同的核上,理论上可以物理同时地运行(如果分别操作不同的数据项),但是同步锁却强行迫使其中的一个线程必须等待另外一个线程退出临界段,即使另外一个核空闲着。<br><br>这时候就需要一种支持并发的数据结构,下面称之为ConcurrentFIFO。<br><br>public&nbsp;interface&nbsp;ConcurrentFIFO&nbsp;{<br>&nbsp;&nbsp;&nbsp;&nbsp;public&nbsp;Object&nbsp;remove();<br>&nbsp;&nbsp;&nbsp;&nbsp;public&nbsp;void&nbsp;put(Object&nbsp;o);<br>}<br><br>put方法把一个数据对象推进FIFO,而remove方法从FIFO删除并返回一个数据对象。通过精心设计,ConcurrentFIFO的实现是线程安全的,两个线程可以安全而同时地访问FIFO。这样在多核平台上就能达到极高的性能。<br><br>通用的ConcurrentFIFO是非常难于实现的。基本的技术是使用原子的CAS操作来实现。CAS即CompareAndSet。现代处理器基本上都能支持这一类指令。但是这种数据结构的实现的一个很大的障碍就是垃圾回收。在多线程并发运行的情况下,被原子替换下来的数据无法得知其是否是其它线程所需要的,也就无法决定是否回收这块内存。除非有垃圾回收器,否则ConcurrentFIFO是很难实现的。(鼓吹手工管理内存效率最高的朋友们请瞪大眼睛看清楚)<br><br>其实,即使是对于有垃圾回收和内建线程支持的Java语言,要想构造一个支持并发的数据结构,也是极端困难的。java.util.concurrent包是经过并发领域的专家(Doug&nbsp;Lea,同时也是早期lig++的主要作者,以及DLmalloc的作者。我后面讨论内存管理的时候还要提到他)精心编写,并且由java社区的许多专家仔细评审测试之后才发布的。</p>
<p>现在来讨论上次提到的并发FIFO,其实现需要一些特殊的技巧。我上次说要实现单线程读单线程写的FIFO,但是这里我们先来讨论一般的并发FIFO。<br><br>我们知道,传统的生产者——消费者问题,通常是使用一个共享的缓冲区来交换数据的,生产者和消费者各自有对应的指针,在生产或者消费的时候相应地移动。如果达到了缓冲区的边界则回绕。如果生产者指针追上消费者指针,则表明缓冲区满了;如果消费者指针追上生产者指针,则表明缓冲区空了。问题在于,为了防止在缓冲区满的时候插入数据,或者在缓冲区空的时候删除数据,生产者或者消费者的每一次插入或者删除数据操作,都必须同时访问这两个指针,这就带来了不必要的同步。<br><br>在单核处理器上,共享缓冲区方式非常高效,并且具有固定的空间开销(有时候你需要保守地估计一个比较大的数值)。但是在多核处理器上(或者SMP系统中),如果要实现并发的FIFO,就必须摒弃这种方式。使用单链表而不是共享缓冲区就可以避开这个问题,这是第一个技巧。<br><br>第二个技巧关系到链表的使用方向。一般使用链表,其插入或者删除节点的位置是任意的。但是把链表作为FIFO使用,则只能也只需要在两端操作。需要注意的是这时候必须从尾部TAIL插入新的节点,而从头部HEAD删除节点。否则从尾部删除节点之后,无从得知新的尾部在哪里,除非从头部遍历。这样做的好处是,插入或者删除都只涉及到一个节点。插入的时候,只要让新创建的节点包含所需要插入的数据,并且其后继(下一个节点)为NULL;再让当前尾部的节点的后继从NULL变成这个新节点,这个新节点也就变成了新的尾部节点(这里的操作顺序很关键)。删除的时候,则检查当前头部节点的后继NEXT是否NULL。若是,表明FIFO是空的;否则,取NEXT所包含的数据来使用(是的,是NEXT而不是当前头部节点所包含的数据,参看下一个技巧和不变式),并把该数据从NEXT中删除,而NEXT也成为新的头部节点。(没有配图,各位请自己想象一下)<br><br>最后一个技巧:为了隔离对头部和尾部的访问,我们需要一个空节点N(不包含数据的有效节点),其下一个节点为NULL;并且引入HEAD和TAIL。在开始的时候,HEAD和TAIL都等于N。插入和删除数据的过程上面已经讲过了,这里讲一下不变式。<br><br>第一个不变式:头部节点总是空的(不包含数据)。在FIFO初始化的时候这是成立的。之后的插入操作不改变头部节点,因此对不变式没有影响。而对于删除操作,则每一个新头部节点的数据都已经在它成为新的头部节点的时候被删除(取用)了。<br><br>第二个不变式:插入和删除操作没有数据冲突,也就是说,插入线程和删除线程不会同时读写同一项数据(不是节点)。我们只需要考虑FIFO为空,即相当于刚刚完成初始化之后的情况。对于空节点N,插入操作改变其后继,删除操作则检查其后继。只要插入线程保证先让新节点包含数据再把新节点插入链表(也就是不能先插入空节点,再往节点中填入数据),那么删除线程就不会拿到空的节点。我们看到,唯一可能发生争用的地方就是N的后继指针,插入线程只要在更新N的后继指针之前准备好其它相关数据和设置即可。<br><br>这意味着,如果能够做到:1)一个线程对数据的更新能够被另外一个线程即刻看到;2)对数据的读或者写(更新和读取N的后继指针)都是原子的;3)指令没有被乱序执行。那么在单线程读单线程写的情况下,甚至不需要使用锁就可以安全地完成并发FIFO;如果有多个生产者线程,则增加一个生产者锁;如果有多个消费者线程,则可以增加一个消费者锁。也就是说,可以有四种组合。<br><br>但是实际情况远非如此。对于2)是容易满足的,因为现代通用处理器上32位数据的读或者写通常都是原子的。对于1),则取决于系统的内存模型:在强内存模型如C/C++中是满足的,在弱内存模型如Java中则不然。但是主要的问题还在于3)。由于指令的乱序执行,第二个不变式所需要的保证很可能被破坏,即使代码确实是那样写的。因此锁是必不可少的,因为加锁的同时还会插入内存屏障。<br><br>这样看来,上次说的SRSW并发FIFO就没有特别的意义了。干脆就用两个锁分别对应生产者和消费者,而并不限制生产者或者消费者的数量:T_LOCK和H_LOCK。在插入新建节点到链表尾部的时候使用T_LOCK,而在对头部操作的时候使用H_LOCK。<br><br><br>具体的代码这里先不给了。这里的算法不是我发明的,而是来自Maged&nbsp;M.&nbsp;Michael&nbsp;和&nbsp;Michael&nbsp;L.&nbsp;Scott的Simple,&nbsp;Fast,&nbsp;and&nbsp;Practical&nbsp;Non-Blocking&nbsp;and&nbsp;Blocking&nbsp;Concurrent&nbsp;Queue&nbsp;Algorithms。请参考其双锁算法的伪码。<br></p>
<p>现在来讨论游戏消息的传送。在一个网游的运营成本中,带宽费用应该是很大的一块。因此如何高效编码以及收发消息就成为节省运营成本的关键。这里面能做很多文章。<br><br>首先是一个基本的判断:随着处理器的计算能力不断提高,以及多核的日益普及,在消息的编码以及收发环节,CPU资源将不会成为瓶颈。相对的,应该千方百计考虑如何在保证游戏正常运行的前提下,降低不必要的通信开销。也就是说,可以对游戏中的消息进行一些比较复杂的编码。<br><br>那么游戏中都有哪些消息?我们知道聊天和语音消息优先级比较低,而且可以通过专门的服务器来处理。真正比较关键、能够影响玩家的游戏体验的,是那些状态变更、动作、玩家之间或者玩家和服务器/NPC之间的实时交互的消息。尤其是,这些消息的传送有严格的时序要求。如果一个玩家先看到自己的角色被砍死,然后才看到对方发出来的攻击动作,甚至根本没有看到对方有什么动作,他/她肯定会愤愤不平。因此,消息系统必须保证每一条消息的及时传递,并且不能打乱它们之间的顺序。<br><br>这意味着,每一条消息必须有明确的边界。也就是说,收到一条消息之后,接收方必须能够明确这条消息有多少个字节。这是一条显而易见的要求。但是大概是出于惯性,在实践中它常常变为消息编码中的长度字段。<br><br>这无疑是一种浪费。很多消息的长度是固定的,仅仅靠检查其消息类型就可以了解其边界。变长消息的处理后面会讨论。我这里并不是说要把具体的游戏逻辑与网络代码混在一起。通过使用元数据就可以有效的把网络代码跟具体的游戏逻辑有效隔离开来。关于元数据的使用后面也会详加探讨。今天时间不多了,下面讨论消息类型的编码作为结束。<br><br>通常一个字节会被用来编码消息的类型,以方便接收方的解码。但是我们知道,游戏中并不是每种类型的消息的传送频率都是一样的。事实上,我们知道哪些消息会被大量发送,哪些消息的频率会低很多,而另外一些消息,一天也不会有几条。明乎此,就可以采用非对称的编码方式来编码消息的类型。这就是Huffman编码。对于占据了绝大部分通信量的状态变更消息而言,即使每条消息节省下半个字节,也是非常划算的。以我的经验,一台普通PC可以作为服务器支持2000人同时在线的实时动作类游戏,消息通量是每秒10000条;如果一个服务集群有5台处理器,那么就相当于节省了200kbps的带宽。这还仅仅是从消息类型编码方面榨取的。当然,Huffman编码的解码是比较麻烦的,效率也会低一些。但是正如前面所指出的,这部分的运行开销并不会造成性能瓶颈。<br></p>
<img src ="http://www.cppblog.com/jack-wang/aggbug/70966.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/jack-wang/" target="_blank">小王</a> 2009-01-02 03:49 <a href="http://www.cppblog.com/jack-wang/archive/2009/01/02/70966.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>网游服务器通信架构的设计</title><link>http://www.cppblog.com/jack-wang/archive/2009/01/02/70963.html</link><dc:creator>小王</dc:creator><author>小王</author><pubDate>Thu, 01 Jan 2009 18:23:00 GMT</pubDate><guid>http://www.cppblog.com/jack-wang/archive/2009/01/02/70963.html</guid><wfw:comment>http://www.cppblog.com/jack-wang/comments/70963.html</wfw:comment><comments>http://www.cppblog.com/jack-wang/archive/2009/01/02/70963.html#Feedback</comments><slash:comments>3</slash:comments><wfw:commentRss>http://www.cppblog.com/jack-wang/comments/commentRss/70963.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/jack-wang/services/trackbacks/70963.html</trackback:ping><description><![CDATA[随着网游从业者的规模和需求不断扩大，越来越多的朋友进入了网游开发这个领域，使得市场中网游开发技术相关的需求量迅猛增长。目前，网游行业比较紧缺的是具有较深技术功底的&#8220;专家型&#8221;开发者，这主要包括两个方面：服务器端设计人员以及客户端设计人员。对于网络游戏而言，由于其主要的游戏逻辑计算是在服务器端完成的，数据同步与广播信息的传递也是通过服务器完成的，所以，是否拥有一个有经验的服务器端设计人员已经成为一款网游产品能否成功的关键之一。鉴于此，本文将试图就网游服务器设计的一系列问题展开讨论和总结，笔者将结合自己的开发经验和体会，将其中各方面内容逐一呈现。希望能够对以下三类人员有所帮助：<br>　　有一定网络编程基础、准备进入网游行业作服务器端设计的人员；<br>　　正在从事网游服务器设计的人员；<br>　　网游项目的技术负责人。<br>　　<br>　　由于网游服务器的设计牵涉到太多内容，比如：网络通信方面、人工智能、数据库设计等等，所以本文将重点从网络通信方面的内容展开论述。谈到网络通信，就不能不涉及如下五个问题：<br>[attach]1264[/attach]<br><br>[attach]1265[/attach]<br><br>[attach]1266[/attach]<br><br>[attach]1267[/attach]<br>1、 常见的网游服务通信器架构概述<br>2、 网游服务器设计的基本原则<br>3、 网游服务器通信架构设计所需的基本技术<br>4、 网游服务器通信架构的测试<br>5、 网游服务器通信架构设计的常见问题<br><br>下面我们就从第一个问题说起：<br><br>常见的网游服务器通信架构概述<br>　　目前，国内的网游市场中大体存在两种类型的网游游戏：MMORPG（如：魔兽世界）和休闲网游（如：QQ休闲游戏和联众游戏，而如泡泡堂一类的游戏与QQ休闲游戏有很多相同点，因此也归为此类）。由于二者在游戏风格上的截然不同，导致了他们在通信架构设计思路上的较大差别。下面笔者将分别描述这两种网游的通信架构。<br><br>1．MMORPG类网游的通信架构<br>　　网游的通信架构，通常是根据几个方面来确定的：游戏的功能组成、游戏的预计上线人数以及游戏的可扩展性。<br>　　目前比较通用的MMORPG游戏流程是这样的：<br><br>a. 玩家到游戏官方网站注册用户名和密码。<br>b. 注册完成后，玩家选择在某一个区激活游戏账号。<br>c. 玩家在游戏客户端中登录进入已经被激活的游戏分区，建立游戏角色进行游戏。<br><br>　　通常，在这样的模式下，玩家的角色数据是不能跨区使用的，即：在A区建立的游戏角色在B区是无法使用的，各区之间的数据保持各自独立性。我们将这样独立的A区或B区称为一个独立的服务器组，一个独立的服务器组就是一个相对完整的游戏世界。而网游服务器的通信架构设计，则包括了基于服务器组之上的整个游戏世界的通信架构，以及在一个服务器组之内的服务器通信架构。<br><br>　　我们先来看看单独的服务器组内部的通信是如何设计的。<br>　　一个服务器组内的各服务器组成，要依据游戏功能进行划分。不同的游戏内容策划会对服务器的组成造成不同的影响。一般地，我们可以将一个组内的服务器简单地分成两类：场景相关的（如：行走、战斗等）以及场景不相关的（如：公会聊天、不受区域限制的贸易等）。为了保证游戏的流畅性，可以将这两类不同的功能分别交由不同的服务器去各自完成。另外，对于那些在服务器运行中进行的比较耗时的计算，一般也会将其单独提炼出来，交由单独的线程或单独的进程去完成。<br><br>　　各个网游项目会根据游戏特点的不同，而灵活选择自己的服务器组成方案。经常可以见到的一种方案是：场景服务器、非场景服务器、服务器管理器、AI服务器以及数据库代理服务器。<br>　　以上各服务器的主要功能是：<br><br>　　场景服务器：它负责完成主要的游戏逻辑，这些逻辑包括：角色在游戏场景中的进入与退出、角色的行走与跑动、角色战斗（包括打怪）、任务的认领等。场景服务器设计的好坏是整个游戏世界服务器性能差异的主要体现，它的设计难度不仅仅在于通信模型方面，更主要的是整个服务器的体系架构和同步机制的设计。<br><br>　　非场景服务器：它主要负责完成与游戏场景不相关的游戏逻辑，这些逻辑不依靠游戏的地图系统也能正常进行，比如公会聊天或世界聊天，之所以把它从场景服务器中独立出来，是为了节省场景服务器的CPU和带宽资源，让场景服务器能够尽可能快地处理那些对游戏流畅性影响较大的游戏逻辑。<br><br>　　服务器管理器：为了实现众多的场景服务器之间以及场景服务器与非场景服务器之间的数据同步，我们必须建立一个统一的管理者，这个管理者就是服务器组中的服务器管理器。它的任务主要是在各服务器之间作数据同步，比如玩家上下线信息的同步。其最主要的功能还是完成场景切换时的数据同步。当玩家需要从一个场景A切换到另一个场景B时，服务器管理器负责将玩家的数据从场景A转移到场景B，并通过协议通知这两个场景数据同步的开始与结束。所以，为了实现这些内容繁杂的数据同步任务，服务器管理器通常会与所有的场景服务器和非场景服务器保持socket连接。<br><br>　　AI（人工智能）服务器：由于怪物的人工智能计算非常消耗系统资源，所以我们把它独立成单独的服务器。AI服务器的主要作用是负责计算怪物的AI，并将计算结果返回给场景服务器，也就是说，AI服务器是单独为场景服务器服务的，它完成从场景服务器交过来的计算任务，并将计算结果返回给场景服务器。所以，从网络通信方面来说，AI服务器只与众多场景服务器保持socket连接。<br><br>　　数据库代理服务器：在网游的数据库读写方面，通常有两种作法，一种是在应用服务器中直接加进数据库访问的代码进行数据库访问，还有一种方式是将数据库读写独立出来，单独作成数据库代理，由它统一进行数据库访问并返回访问结果。<br><br>　　其中，非场景服务器在不同的游戏项目中可能会被设计成不同的功能，比如以组队、公会或全频道聊天为特色的游戏，很可能为了满足玩家的聊天需求而设立单独的聊天服务器；而如果是以物品贸易（如拍卖等）为特色的游戏，很可能为了满足拍卖的需求而单独设立拍卖服务器。到底是不是有必要将某一项游戏功能独立处理成一个服务器，要视该功能对游戏的主场景逻辑（指行走、战斗等玩家日常游戏行为）的影响程度而定。如果该功能对主场景逻辑的影响比较大，可能对主场景逻辑的运行造成比较严重的性能和效率损失，那么应考虑将其从主场景逻辑中剥离，但能否剥离还有另一个前提：此功能是否与游戏场景（即地图坐标系统）相关。如果此功能与场景相关又确实影响到了主场景逻辑的执行效率，则可能需要在场景服务器上设立专门的线程来处理而不是将它独立成一个单独的服务器。<br><br>　　以上是一个服务器组内的各服务器组成情况介绍，那么，各服务器之间是如何通信的呢？它的基本通信构架有哪些呢？<br>　　MMORPG的单组服务器架构通常可以分为两种：第一种是带网关的服务器架构；第二种是不带网关的服务器架构。两种方案各有利弊。<br><br>　　就带网关的服务器架构而言，由于它对外只向玩家提供唯一的一个通信端口，所以在玩家一侧会有比较流畅的游戏体验，这通常也是那些超大规模无缝地图网游所采用的方案，但这种方案的缺点是服务器组内的通信架构设计相对复杂、调试不方便、网关的通信压力过大、对网关的通信模型设计要求较高等。第二种方案会同时向玩家开放多个游戏服务器端口，除了游戏场景服务器的通信端口外，同时还可能提供诸如聊天服务器等的通信端口。这种方案的主要缺点是在进行场景服务器的切换时，玩家客户端的表现中通常会有一个诸如场景调入的界面出现，影响了游戏的流畅感。基于这种方案的游戏在客户端的界面处理方面，比较典型的表现是：当要进行场景切换时，只能通过相应的&#8220;传送功能&#8221;传送到另外的场景去，或者需要进入新的场景时，客户端会有比较长时间的等待进入新场景的等待界面(Loading界面)。<br><br>　　从技术角度而言，笔者更倾向于将独立的服务器组设计成带网关的模型，虽然这加大了服务器的设计难度，但却增强了游戏的流畅感和安全性，这种花费还是值得的。<br>　　笔者在下面附上了带网关的MMORPG通信架构图，希望能给业内的朋友们一点有益的启迪。
<img src ="http://www.cppblog.com/jack-wang/aggbug/70963.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/jack-wang/" target="_blank">小王</a> 2009-01-02 02:23 <a href="http://www.cppblog.com/jack-wang/archive/2009/01/02/70963.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item></channel></rss>