一、为什么要做UDP可靠性传输?

UDP与TCP的区别:

TCP是为流量设计的(每秒可以传输多少KB的数据),因此在刚启动的时候会尽量少带宽,讲究的时候充分利用带宽。

可靠性UDP设计目的是解决:延迟问题。

二、如何做到可靠性传输

做到可靠性传输,可以通过以下机制实现:

  • ACK机制
  • 重传机制
  • 序号机制
  • 重排机制
  • 窗口机制

1)ACK机制
发送端将数据发送出去后,等待对端的确认应答。
如果有确认应答, 说明数据已经成功到达对端。
如果没有收到应答,不能说数据一定丢失了,但数据丢失的可能性比较大。

2)重传机制
数据重传,是发送端发送数据后没有收到对端ACK,继续重发数据。
数据重传,常用方式是在重发数据前,等待确认应答到来指定超时时间,如果超过了该时间还没收到确认应答,发送端将进行重传。

3)序号机制
序号机制,可以保证传送数据包的顺序。
序号既包含在数据发送包,也包含在响应包内。

a)接收端:接收端在收到发送包后,可以根据序号对数据进行重组。
b)发送端:在传送一个数据包时,会把该数据包放入重传队列中,同时启动计时器,如果收到对端收到数据包的确认应答,则通过序号识别,将该数据包从重传队列中删除;如果超时仍未收到对端的确认应答,则重发数据。

4)重排机制
重排机制,指的是对数据段进行排序重组。
可借助于序号机制,使用序号对数据段进行排序。

5)窗口机制
窗口机制主要用于流量控制和拥塞控制。

》》以TCP进行说明

a)利用窗口控制提高速度
在建立TCP连接的同时,也可以确定发送数据包的单位,称其为“最大消息长度”(MSS)。
TCP在传输大量数据时,是以MSS的大小将数据进行分割发送。进行重发时也是以MSS为单位。

TCP以一个数据段为单位,每发一个数据段进行一次确认应答处理。这种传输方式的缺点:包的往返时间越长,通信性能越低。TCP引入窗口来解决这个问题。

图1

图1所示,确认应答不再是以每个分段,而是以更大的单位进行确认时,转发时间将被大幅度缩短。发送端主机在发送了一个段以后不必一直等待确认应答,而是继续发送。
窗口大小就是指无需等待确认应答而可以继续发送数据的最大值。图1窗口大小为3个段。

如下图所示,黄色部分表示窗口。这个窗口中的数据即便没有收到应答确认也可以被发送出去。不过,在整个窗口的确认应答没有到达之前,如果其中部分数据出现丢包,那么发送端仍然需要重传。在收到确认应答的情况下,将窗口滑动到确认应答中的序列号的位置。这样可以顺序得将多个段同时发送提高通信性能。这种机制也被称为窗口滑动机制。

图2

b)流量控制(窗口大小)
流量控制是对发送端和接收端之间的处理速度进行的平衡。

比如,发送端一直在发送数据,接收端可能由于本身业务原因没能够及时接收数据,一些本该接收的数据被丢弃,从而触发发送端的重发机制,导致网络流量的浪费。

TCP通过流量控制可以让发送端根据接收端的实际接收能力控制发送数据的数量。
接收端主机向发送端主机通知自己可以接收数据的大小,于是发送端会发送不超过这个限度的数据。该大小限度就被称作窗口大小。

TCP首部中,专门有一个字段用来通知窗口大小。接收主机将自己可以接收的缓冲区大小放入到这个字段中通知给发送端。这个字段的值越大,说明网络的吞吐量越大。当接收端的这个缓冲区一旦面临数据溢出时,窗口大小的值也会随之被设置成一个更小的值发送给发送端,从而控制数据发送量。

图3

c)拥塞控制
有了窗口控制,发送端和接收端能够连续发送大量数据包。不过,如果在通信刚开始的时候就发送大量数据,也可能会引发其他问题。

一般来说,计算机网络都处在一个共享的环境。因此也有可能会因为其他主机之间的通信使得网络拥堵。在网络出现拥堵时,如果突然发送一个较大量的数据,极有可能导致整个网络的瘫痪。

解决方案:
在慢启动的时候,将这个拥塞窗口的大小设置为1个数据段(1MSS)发送数据。之后每收到一次确认应答,拥塞窗口的值就加1。在发送数据包时,将拥塞窗口的大小与接收端主机通知的窗口大小做比较,然后按照它们当中较小的那个值,发送比其还小的数据量。

问题:在使用窗口控制中,如果出现段丢失该怎么办?

① 情况1
丢失描述:发送端没有收到接收端的确认应答。
解决方案:这种情况,数据已经给到达对端,不需要进行重发。使用了窗口机制,某些确认应答即使丢失也无需重发,可以通过下一个确认应答进行确认。

②情况2
丢失描述:发送端会重复的收到接收端的同一序号应答。
解决方案:发送端主机如果连续三次接收到同一个确认应答,就会将其对应的数据进行重发。

三、UDP可靠性传输KCP协议原理

3.1 KCP简介

KCP是一个快速可靠传输ARQ(Automatic Repeat-reQuest)协议,相比于TCP,KCP以浪费10%-20%的带宽代价,换取比TCP快30%-40%的传输速度,并且最大延迟降低三倍的传输效果。

KCP为纯算法实现,不负责底层协议的收发,需要使用者自己定义下层数据包的发送方式,以callback的方式提供给KCP。KCP内部不会有任何的系统调用,就连时钟都需要外部传递进来。

KCP整个协议只有ikcp.h,ikcp.c两个源文件,使用者可以很方便的集成到自己的协议栈中。比如,你实现了一个P2P或某个基于UDP的协议,而缺乏一套完善的ARQ可靠协议,那么简单的拷贝这两个文件到已有项目中,稍微修改下,即可使用。

3.2 KCP特性

相比于TCP,KCP具有以下特性:

1)RTO翻倍 vs 不翻倍

  • TCP超时计算是RTOx2,这样连续丢三次包就变成RTOx8了,十分恐怖;
  • KCP启动快速模式后不x2,只是x1.5(实验证明1.5这个值相对比较好),提高了传输速度。

2)选择性重传 vs 全部重传

  • TCP丢包时会全部重传从丢的那个包开始以后的数据;
  • KCP是选择性重传,只重传真正丢失的数据包。

3)快速重传

  • 机制:跳过多少个包马上重传(若使用过了快速重传,可以不考虑RTO)。
  • 示例:发送端发送了1,2,3,4,5几个包,然后收到远端的ACK: 1, 3, 4, 5,当收到ACK3时, KCP知道2被跳过1次,收到ACK4时,知道2被跳过了2次,此时可以认为2号丢失,不用等超时,直接重传2号包,大大改善了丢包时的传输速度。

4)延迟ACK vs 非延迟ACK

  • TCP为了充分利用带宽,延迟发送ACK( NODELAY都没用),这样超时计算会算出较大 RTT时间,延长了丢包时的判断过程;
  • KCP的ACK是否延迟发送可以调节。

5)UNA vs ACK+UNA

  • ARQ模型响应有两种, UNA(此编号前所有包已收到,如TCP)和ACK(该编号包已收到),光用UNA将导致全部重传,光用ACK则丢失成本太高,以往协议都是二选其一;
  • KCP协议中,除去单独的 ACK包外,所有包都有UNA信息。

6)非退让流

  • KCP正常模式同TCP一样使用公平退让法则,即发送窗口大小由:发送缓存大小、接收端剩余接收缓存大小、丢包退让及慢启动这四要素决定。
  • KCP传送及时性要求很高的小数据时,可选择通过配置跳过后两步,仅用前两项来控制发送频率。以牺牲部分公平性及带宽利用率之代价,换取了开着BT都能流畅传输的效果。

3.3 KCP一些名词说明

  • 用户数据:应用层发送的数据,比如一张图片2KB的数据。

  • MTU:最大传输单元,即每次发送的最大数据。

  • RTO:Retransmission TimeOut,重传超时时间。

  • cwnd:ongestion window,拥塞窗口,表示发送方可发送多少个KCP数据包。与接收方窗口有关,与网络状况(拥塞控制)有关,与发送窗口大小有关。

  • rwnd:receiver window,接收方窗口大小,表示接收方还可接收多少个KCP数据包。

  • snd_queue:待发送KCP数据包队列。

  • snd_nxt:下一个即将发送的kcp数据包序列号。

  • snd_una:下一个待确认的序列号。

3.4 KCP协议格式

图4

  • conv:连接号。UDP是无连接的,conv用于表示来自哪个客户端。对连接的一种替代,因为有conv,因此KCP也是支持多路复用
  • cmd:命令类型,如下表1所示
  • frg:分片,用户数据可能会被分成多个KCP包,发送出去
  • wnd:接收窗口大小,发送方的发送窗口不能超过接收方给出的数值, (其实是接收窗口的剩余大小,这个大小是动态变化的)
  • ts:时间序列
  • sn:序列号
  • una:下一个可接收的序列号。其实就是确认号,收到sn=10的包,una为11
  • len:数据长度(DATA的长度)
  • data:用户数据

表1

IKCP_CMD_PUSH 和 IKCP_CMD_ACK 关联;IKCP_CMD_WASK 和 IKCP_CMD_WINS 关联。

3.5 KCP发送数据过程

KCP收发数据流程简图

图6

检测对方接收窗口rmt_wnd是否为0,如果是0则要发送探测包。
发送数据,根据对方的接收窗口能力去发送。
1)snd_buf:解决重传的问题,只有被应答的包才能从buf删除。
2)对方窗口大小,snd_buf不能超过对方窗口大小。

KCP会不停的进行update更新最新情况,数据的实际发送在update时进行。发送过程如图7所示:

图7

在KCP中,数据被拆分成Segment后:

  • 步骤1:待发送队列移至发送队列
    KCP会把snd_queue待发送队列中的kcp包,移至snd_buf发送队列。
    移动的包的数量不会超过snd_una+cwnd-snd_nxt,确保发送的数据不会让接收方的接收队列溢出。该功能类似于TCP协议中的滑动窗口。
    cwnd=min(snd_wnd,rmt_wnd,kcp->cwnd)的最小值决定,snd_wnd,rmt_wnd比较好理解可发送的数据,可发送的数据最大值,应该是发送方可以发送的数据和接收方可以接收的数据的最小值。
    kcp->cwnd是拥塞控制的一个值,跟网络状况相关,网络状况差的时候,KCP认为应该降低发送的数据。

    如上图中,snd_queue待发送队列中有4个KCP包等待发送,这个时候snd_nxt下一个发送的kcp包序列号为11,snd_una下一个确认的KCP包为9(8已经确认,9,10已经发送但是还没得到接收方的确认)。因为cwnd=5,发送队列中还有2个发送了但是还未得到确认,所以可以从待发送队列中取前面的3个KCP包放入到发送队列中,序列号分别设置为11,12,13。

  • 步骤2:发送发送对接的数据
    发送队列中包含两种数据:已发送但是尚未被接收方确认的数据 和 没被发送过的数据。
    没发送过的数据直接发送即可。
    已发送但是尚未被接收方确认的数据,该部分的策略直接决定着协议快速、高效与否。KCP主要使用两种策略来决定是否需要重传KCP数据包,超时重传、快速重传、选择重传。

    1)超时重传
    TCP超时计算是RTOx2,这样连续丢三次包就变成RTOx8了,而KCP非快速模式下每次+RTO,急速模式下+0.5RTO(实验证明1.5这个值相对比较好),提高了传输速度。
    【RTO算法对比 图8】

    2)快速重传
    发送端发送了1,2,3,4,5几个包,然后收到远端的ACK: 1, 3, 4, 5,当收到ACK3时,KCP知道2被跳过1次,收到ACK4时,知道2被跳过了2次,此时可以认为2号丢失,不用等超时,直接重传2号包,大大改善了丢包时的传输速度。TCP有快速重传算法,TCP包被跳过3次之后会进行重传。
    注:可以通过统计错误重传(重传的包实际没丢,仅乱序),优化该设置。

    3)选择重传

  • 步骤3:数据发送
    老的TCP丢包时会全部重传从丢的那个包开始以后的数据,KCP是选择性重传,只重传真正丢失的数据包。但是,目前大部分的操作系统,linux与android手机均是支持SACK选择重传的。

3.6 KCP数据接收过程

KCP的接收过程是将UDP收到的数据进行解包,重新组装顺序的、可靠的数据后交付给用户。

【接收处理流程 图9】

kcp_input输入UDP收到的数据包。kcp包对前面的24个字节进行解压,包括conv、 frg、 cmd、 wnd、 ts、 sn、 una、 len。根据una,会删除snd_buf中,所有una之前的kcp数据包,因为这些数据包接收者已经确认。根据wnd更新接收端接收窗口大小。根据不同的命令字进行分别处理。

1)IKCP_CMD_PUSH数据发送命令

  • ① KCP会把收到的数据包的sn及ts放置在acklist中,两个相邻的节点为一组,分别存储sn和ts。update时会读取acklist,并以IKCP_CMD_ACK的命令返回确认包。如下图中,收到了两个kpc包,acklist中会分别存放10,123,11,124。
  • ② KCP数据包放置rcv_buf队列。丢弃接收窗口之外的和重复的包。然后将rcv_buf中的包,移至rcv_queue。原来的rcv_buf中已经有sn=10和sn=13的包了,sn=10的kcp包已经在rcv_buf中了,因此新收到的包会直接丢弃掉,sn=11的包放置至rcv_buf中。
  • ③ 把rcv_buf中前面连续的数据sn=11,12,13全部移动至rcv_queue,rcv_nxt也变成14。rcv_queue的数据是连续的,rcv_buf可能是间隔的。
  • ④ kcp_recv函数,用户获取接收到数据(去除kcp头的用户数据)。该函数根据frg,把kcp包数据进行组合返回给用户。

    【PUSH处理 图10】

2)IKCP_CMD_ACK数据确认包
两个使命:RTO更新 和 确认发送包接收方已接收到。

正常情况:收到的sn为11,una为12。表示sn为11的已经确认,下一个等待接收的为12。发送队列中,待确认的一个包为11,这个时候snd_una向后移动一位,序列号为11的包从发送队列中删除。

【数据确认包处理流程 图11】

异常情况:如下图所示,sn!=11的情况均为异常情况。sn<11表示,收到重复确认的包,如本来以为丢失的包重新又收到了,所以产生重复确认的包;sn>17,收到没发送过的序列号,概率极低,可能是conv没变重启程序导致的;112,则启动快速重传。

【KCP快速确认 图12】

确认包发送,接收到的包会全部放在acklist中,以IKCP_CMD_ACK包发送出去。

3.7 KCP流量控制和拥塞控制

1)RTO计算
KCP的RTO计算与TCP计算方式一样。
RTT:一个报文段发送出去,到收到对应确认包的时间差。
SRTT(kcp->rx_srtt):RTT的一个加权RTT平均值,平滑值。
RTTVAR(kcp->rx_rttval):RTT的平均偏差,用来衡量RTT的抖动。

2)流量控制
KCP的发送机制采用TCP的滑动窗口方式,可以非常容易的控制流量。KCP的头中包含wnd,即接收方目前可以接收的大小。能够发送的数据即为snd_una与snd_una+wnd之间的数据。接收方每次都会告诉发送方我还能接收多少,发送方就控制下,确保自己发送的数据不多于接收端可以接收的大小。

KCP默认为32,即可以接收最大为32*MTU=43.75kB。KCP采用update的方式,更新间隔为10ms,那么KCP限定了你最大传输速率为4375kB/s,在高网速传输大内容的情况下需要调用ikcp_wndsize调整接收与发送窗口。

KCP的主要特色在于实时性高,对于实时性高的应用,如果发生数据堆积会造成延迟的持续增大。建议从应用侧更好的控制发送流量与网络速度持平,避免缓存堆积延迟。

3)拥塞控制
KCP的优势在于可以完全关闭拥塞控制,非常自私的进行发送。KCP采用的拥塞控制策略为TCP最古老的策略,无任何优势。完全关闭拥塞控制,也不是一个最优策略,它全是会造成更为拥堵的情况。

网络中链路的带宽,与整条网络中的交换节点(路由器、交换机、基站等)有关。如果,所有使用该链路的流量超出了,该链路所能提供的能力,就会发生拥塞。车多路窄,就会堵车,车越多堵的越厉害。因此,TCP作为一个大公无私的协议,当网络上发送拥堵的时候会降低自身发送数据的速度。拥塞控制是整个网络的事情,流量控制是发送和接收两方的事情。

当发送方没有按时接收到确认包,就认为网络发生了拥堵行为。TCP拥塞控制的方式,归结为慢开始、拥塞避免,如图13所示:

【拥塞控制算法 图13】

KCP发生丢包的情况下的拥塞控制策略与TCP Tahoe版本的策略一致。TCP Reno版本已经使用快恢复策略。因此,丢包的情况下,其实KCP拥塞控制策略比TCP更为苛刻。

KCP在发生快速重传,数据包乱序时,采用的是TCP快恢复的策略。控制窗口调整为已经发送没有接收到ack的数据包数目的一半+resent。

四、KCP应用

4.1 KCP快速安装

kcp可以使用vcpkg库管理器下载并安装。
vcpkg中的kcp库由Microsoft团队成员和社区贡献者保持最新状态。

$ git clone https://github.com/Microsoft/vcpkg.git
$ cd vcpkg
$ ./bootstrap-vcpkg.sh
$  ./vcpkg integrate install
$  ./vcpkg install kcp

4.2 KCP协议配置

协议默认模式是一个标准的 ARQ,需要通过配置打开各项加速开关:

1)工作模式

int ikcp_nodelay(ikcpcb *kcp, int nodelay, int interval, int resend, int nc)
  • nodelay :是否启用 nodelay模式,0不启用;1启用。
  • interval :协议内部工作的 interval,单位毫秒,比如 10ms或者 20ms。
  • resend :快速重传模式,默认0关闭,可以设置2(2次ACK跨越将会直接重传)。
  • nc :是否关闭流控,默认是0代表不关闭,1代表关闭。

普通模式: ikcp_nodelay(kcp, 0, 40, 0, 0);
极速模式: ikcp_nodelay(kcp, 1, 10, 2, 1);

2)最大窗口

int ikcp_wndsize(ikcpcb *kcp, int sndwnd, int rcvwnd);

该调用将会设置协议的最大发送窗口和最大接收窗口大小,默认为32,单位为包。
这个可以理解为 TCP的SND_BUF 和 RCV_BUF,只不过单位不一样 SND/RCV_BUF 单位是字节,这个单位是包。

3)最大传输单元

int ikcp_setmtu(ikcpcb *kcp, int mtu);

kcp协议并不负责探测 MTU,默认 mtu是1400字节。该值将会影响数据包归并及分片时候的最大传输单元。

4)最小RTO
不管是 TCP还是 KCP计算 RTO时都有最小 RTO的限制,即便计算出来RTO为40ms,由于默认的 RTO是100ms,协议只有在100ms后才能检测到丢包,快速模式下为30ms,可以手动更改该值: kcp->rx_minrto = 10。

4.3 KCP函数说明

1)创建KCP对象

ikcpcb *kcp = ikcp_create(conv, user);

初始化 kcp对象,conv为一个表示会话编号的整数,和tcp的 conv一样,通信双方需保证 conv相同,相互的数据包才能够被认可;user是一个给回调函数的指针。

2)设置回调函数

//回调函数
int udp_output(const char *buf, int len, ikcpcb *kcp, void *user)
{....
}//设置回调
kcp->output = udp_output;

设置传输回调函数,如UDP的send函数,真正发送数据需要调用sendto。
KCP的下层协议输出函数,KCP需要发送数据时会调用它;buf/len 表示缓存和长度,user指针为 kcp对象创建时传入的值,用于区别多个 KCP对象。

3)循环调用update函数

ikcp_update(kcp, millisec);

以一定频率调用ikcp_update来更新 kcp状态,并且传入当前时钟(毫秒单位);如 10ms调用一次,或用ikcp_check确定下次调用 update的时间不必每次调用。

4)输入一个应用层数据包

ikcp_input(kcp, received_udp_packet, received_udp_size);

收到一个下层数据包(比如UDP包)时需要调用该函数。我们要使用recvfrom接收,然后扔到kcp里面做解析。

5)发送数据

ikcp_send(kcp1, buffer, 8);

用户层接口。

6)接收数据

hr = ikcp_recv(kcp2, buffer, 10);

4.4 KCP源码调用流程

【应用调用KCP流程 图14】

参考来源:《TCP的可靠性传输》 《可靠UDP,KCP协议快在哪?》

UDP可靠性传输KCP实现原理和应用相关推荐

  1. android 判断byte值_Android开发之UDP可靠性传输

    轩羽:Android开发之UDP​zhuanlan.zhihu.com 在这一篇文章里,小编说到UDP是不可靠的,故,我们要自己写一套协议,来使UDP实现可靠性传输,这里,小编和小编的小伙伴一起,写了 ...

  2. UDP可靠性传输-QUIC

    一.QUIC协议 QUIC ,即 快速UDP网络连接 ( Quick UDP Internet Connections ), 是由 Google 提出的实验性网络传输协议 ,位于 OSI 模型传输层. ...

  3. 【KCP】UDP可靠性传输

    1 如何做到可靠性传输 ◼ ACK机制 ◼ 重传机制 ◼ 序号机制 3 2 1 ->2 3 1 ◼ 重排机制 2 3 1 ->3 2 1 ◼ 窗口机制 Tcp不用我们管 可靠性udp 5种 ...

  4. UDP可靠性传输协议(QUIC)

    目录 UDP与TCP对比 可靠性机制 ACK机制 重传机制 流控控制 序号机制 重排机制 窗口机制 UDP可靠性设计 UDP窗口流控 KCP(出于实时性考虑) QUIC 简述 优点 缺点 报文格式 建 ...

  5. tcp重复的确认_TCP如何实现可靠性传输

    1.UDP与TCP的区别 TCP(TransmissionControl Protocol 传输控制协议)是一种面向连接的.可靠的.基于字节流的传输层通信协议. UDP是User Datagram P ...

  6. TCP如何实现可靠性传输

    1.UDP与TCP的区别 TCP(TransmissionControl Protocol 传输控制协议)是一种面向连接的.可靠的.基于字节流的传输层通信协议. UDP是User Datagram P ...

  7. UDP与TCP对比,TCP保证可靠性传输的详细说明

    目录 1 UDP 2 TCP 2.1 TCP协议如何保证可靠传输 2.2 流量控制 2.2.1 TCP的滑动窗口 2.3 拥塞控制 2.3.1 慢开始 2.3.2 拥塞避免 2.3.3 快重传 2.3 ...

  8. KCP协议:从TCP到UDP家族QUIC/KCP/ENET

    行文前先安利下<再深谈TCP/IP三步握手&四步挥手原理及衍生问题-长文解剖IP >.<再谈UDP协议-浅入理解深度记忆> KCP协议科普 KCP是一个快速可靠协议,能 ...

  9. C++TCP和UDP属于传输层协议

    TCP和UDP属于传输层协议.其中TCP提供IP环境下的数据可靠传输,它事先为要发送的数据开辟好连接通道(三次握手),然后再进行数据发送:而UDP则不为IP提供可靠性,一般用于实时的视频流传输,像rt ...

最新文章

  1. 一个电商供应链系统的DDD实战
  2. 130.被围绕的区域
  3. 配置网络(静态ip)
  4. MySQL如何找到表与表之间的关系?
  5. 【控制】《最优控制理论与系统》-胡寿松老师-第1章-导论
  6. 如何用web3.js在以太坊区块链上保存数据?
  7. 脱胎换骨 XIV Gen3摆脱最后的“羸弱点”
  8. 10 个实战及面试常用 Shell 脚本编写
  9. Atitit.输入法配置说明v1 q229
  10. AJAX跨域请求的理解,JAVA
  11. Python模块的导入方法1
  12. 基于深度学习的自然场景文字识别系统研究 faster-RCNN + CRNN (二)
  13. 2017ccpc哈尔滨站部分简要题解
  14. html框架之间空隙,Span之间空隙原因和解决办法
  15. c语言中变量属性,C语言学习笔记--C语言中变量的属性关键字
  16. Exception evaluating SpringEL expression:
  17. 安卓之父安迪·鲁宾:让乔布斯羡慕嫉妒恨的人
  18. linux cuda安装目录,ubuntu16.04上的cuda安装、卸载以及替换
  19. 螃蟹RTS5733DL固态硬盘,曲线救盘案例……
  20. 如何选择GPS定位器

热门文章

  1. ALLHiC: 辅助组装简单的二倍体基因组
  2. 什么是UDS诊断协议
  3. MathType转Word公式(OMML)
  4. Unity的多人游戏与网络
  5. 代正通_iPhone忘记密码怎么办?苹果手机忘记密码怎么办?iPhone已停用怎么办?
  6. Arduino使用 单路继电器
  7. 截图密报Windows 8 Beta测试版windows8测试版主
  8. 软件项目管理的流程控制分析
  9. Html5-画布(canvas)之常用绘图方法介绍
  10. 亚商投资顾问 早餐FM/1026维护股市、债市、楼市健康发展