磨刀不误砍柴工,让我们从概念入手,逐步深入。
所谓socket通常也称作"套接字",用于描述IP地址和端口,是一个通信链的句柄。应用程序通常通过"套接字"向网络发出请求或者应答网络请求。Socket通讯是我们开发多人在线游戏中的常用通讯方式,它主要有流式Socket(SOCK_STREAM)和数据报式Socket(SOCK_DGRAM)两种类别,AS3中我们一般使用的是基于TCP的流式socket,因此本文也主要讲解这一种方式。既然这篇文章主要讲解的是流式socket,那让我们来看看什么是TCP.
TCP是一种流协议(stream protocol)。这就意味着数据是以字节流的形式传递给接收者的,没有固有的"报文"或"报文边界"的概念。
从这方面来说,读取TCP数据就像从串行端口读取数据一样--无法预先得知在一次指定的读调用中会返回多少字节( 也就是说能知道总共要读多少,但是不知道具体某一次读多少, 在AS3中的API表现为 socket. bytesAvailable. )。
为了说明这一点,让我们来看一个例子:我们假设在主机A和主机B的应用程序之间有一条TCP连接,主机A有两条报文D1,D2要发送到B主机,并两次调用send(as3中即flush)来发送,每条报文调用一次。
那么,我们自然而然的希望两条报文是作为两个独立的实体,在各自的分组中发送,如图1:
这样的话,我们无需做任何特别的处理,便能够很容易的区分每一个独立的数据,并根据需求分别做相应的处理。 但现实往往是有所偏差的,实际的数据传输过程很可能不会遵循这个模型。而是会采用以下四种方式之一进行传输。如图2:
- D1和D2数据作为两个独立的分组,分别到达主机B;
- D1和D2合为一个整体组,一起到达主机B;
- D1的部分数据先到达主机B,剩下的D1数据和D2和在一组到达主机B;
- D1和D2的部分数据先到达主机B, D2后到达主机B;
实际上,可能的情况还不止4种。 既 然是深入,那我们来看看为何会产生以上几种传输方式,可能有些开发人员感觉不需要了解这些,但还是极力推荐适当了解,我们要知其然,也要知其所以然,这其 实也为我们后面进行网络编程提供了理论依据和优化思路。言归正题,往下看,让我们来了解下TCP协议,我们知道TCP提供了全双工,可靠的传输服务。
TCP 通过以下方式来提供可靠性:
- 应用数据被分割成T C P认为最适合发送的数据块。
- 当TCP发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段。
- 当TCP收到发自TCP连接另一端的数据,它将发送一个确认。这个确认不是立即发送,通常将推迟几分之一秒(一般200毫秒左右)。
- TCP将保持它首部和数据的检验和。这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错, TCP将丢弃这个报文段和不确认收到此报文段(希望发端超时并重发)。
- 既然T C P报文段作为I P数据报来传输,而I P数据报的到达可能会失序,因此TCP报文段的到达也可能会失序。如果必要, T C P将对收到的数据进行重新排序,将收到的数据以正确的顺序交给应用层。
- 既然I P数据报会发生重复, T C P的接收端必须丢弃重复的数据。
- TCP 还能提供流量控制。TCP连接的每一方都有固定大小的缓冲空间。TCP的接收端只允许另一端发送接收端缓冲区所能接纳的数据,这将防止较快主机致使较慢主 机的缓冲区溢出,比如,发送方要发送8Kb的数据到接收端,但接收端的缓冲区已经只剩下2K的空间了,那么发送方最多只能发2Kb的数据,那么这时候不得 不把数据拆分发送。
而为了防止网络的拥塞现象,TCP提出了一系列的拥塞控制机制与算法,比如Nagle算法等,这些也可以看做是产生半包,黏包现象的其中一方面因素吧。
基于以上原因,TCP提供的是可靠的字节流服务,它不在字节流中插入记录标识符(当然这里说的是数据本身,而不是为了数据传输底层加入的一些信息,如图3), TCP 对字节流的内容也不作任何解释,它不知道传输的数据字节流是二进制数据,还是ASCII字符或者其他类型数据等,所以,对字节流的解释由TCP连接双方的应用层解释,这也是我们在写socket网络编程中所需 要做的事情。
如果想更多更深入了解网络方面的知识,可阅读这本经典书《TCP/IP详解》。 基于以上理论,我们可以来做个实验,使用AIR桌面程序做一个简单的服务端测试程序,以及建立一个AS工程客户端, 客户端和服务端连接成功之后,服务端发送200000次数据给客户端,每次发送一个整形数据后马上调用flush,代码如图4、图5:
图4
图5
当我们运行程序后发现,在200000次发送数据中,其实客户端最终的输出接收数据的次数远远少于200000次;那么这里有个结论:
当程序调用flush发送数据,只是从应用程序维护的一个缓存区拷贝到内核缓冲区,或者是说推入TCP协议栈中就返回了成功的标识,也就是说推进TCP协议栈的数据并不是马上发送的,TCP协议栈有自己的发送和控制策略。 所以这也解释了一些童鞋好奇为什么使用flush函数发送次数和数据接收事件响应次数不相等的原因以及半包、黏包现象产生的原因。
以 上讲的种种理论,最终还是为下面要讲的socket编程作铺垫的,标题写的深入,那么关于如何建立连接方面的这里不作讲解,可自行查阅相关内容,既然上面 我们说了,流式socket是以字节流的形式传输信息的,跟水流一样,没有边界,要做什么操作,传了什么内容,哪里是事情的开头,哪里又是事情的结尾,我 们完全是不知道的,所以我们一般在发送方需要对要传输的数据一定的格式,而接收方则按同样的规则解析这种格式,也就是我们常说的封包和拆包。也就是说我们 处理字节流是以包为单位进行处理的,包的结构一般如下,图6:
图6
一般是包头|Header|+包体|Body|+包尾|End|组成,有时候包尾部分也直接归入Body部分,形成|Header|+|Body|的结构。
包尾部分的信息一般用于校验包的完整性或者合法性等。
包头长度,信息字段的位置一般都是固定的,必须包含的字段一般有“包的长度(packetLen)”和“操作码(cmd)”(如图6),提供长度是为了方面我们解析数据包,而提供操作码是为了让程序知道,该条信息是要做什么操作。除以上两个字段,其他字段根据需要增减。
Socket数据处理流程:
既然上面我们人为的给数据进行了封包,那么我们处理数据就是以包为单位进行处理的。
1、 因为包头|Header|长度固定,当收到数据时,先检验缓冲区有效数据长度,是否大于等于包头长度,如果小于包头长度,则说明数据不够,则继续等待下一次数据的到来, 如果大于包头长度,则读取包头长度的数据,并按一定的格式解析包头数据,从包头中获取到包的长度(packetLen),此时包头处理基本完毕。
2、 根据包头读取到的packetLen,从而计算出包体部分的数据长度(比如bodyLen),再判断缓存区剩余数据长度是否大于等于bodyLen,如果不是,同样等待, 如果是,则从缓存区读出包体部分,进而按照一定的格式读取里面的数据(例如readInt(),readShort()…);此时包数据基本读取完毕!
3、 上面我们说过网络中的字节流没有界线的,因此我们该知道每次到达的数据,也就是缓冲区的数据,有可能不止一个数据包,因此需要循环执行 1,2步。
验证包的完整性和合法性:
之前说过我们在包尾或者是包体的最后位置会适当加上一些信息,来验证包的完整性或者合法性。
举个例子,我们通过一定的规则,把包体的数据位置顺序打乱,最后在包尾加上规则信息,以便接收端能按同样的规则解析还原数据,再通过两端比较,就知道包是否完整和合法等 其实要验证包是否合法,有很多的方法。
比如说判断包头操作码cmd的范围,传的数据长度是否跟包头声明的长度对应,或者整个包长度是否超过最大包的限制等等,不同的人有不同的做法,但目的基本是一样的。
时间有限,代码就不写了。
网络编程优化:
我们知道TCP有一系列的流量控制,网络拥塞控制的处理方案。但在AS3中,Adobe封装的太好了,导致很多底层东西AS目前都碰不了。 唯一我们能给点安慰的,就是基于TCP流量控制这一点上。
来重温一下:
TCP 还能提供流量控制。TCP 连接的每一方都有固定大小的缓冲空间。TCP的接收端只允许另一端发送接收端缓冲区所能接纳的数据,这将防止较快主机致使较慢主机的缓冲区溢出。
因此我们很自然的想到,尽量保持接收方TCP缓冲区有足够的容量,以便较快发送主机能及时发出数据,防止阻塞。
既然这样,那么AS客户端的一般保险处理方式是采取“快读取慢解析”的一种思路,也就是说当有接收到数据,我们会使用ByteArray一次性读取缓冲区的数据,然后再进行解包操作。
不过这里有个疑问,大伙可以研究探讨: Socket 类和ByteArray类同样实现了IDataInput, IdataOutput接口,那是不是socket其实已经实现了跟我们利用ByteArray存取数据的预防措施呢,也就是说内部数据存储部分跟 ByteArray具有同样的实现,如果是的话,那我们是不需要做 (&&)部分提及的把缓存区数据一次性写进 ByteArray后再进行包解析操作的方式,如果不是,那么还是实现 (&&):这一步来得保险,Adobe封装太好了,大伙可以研究下这里。