NIO,IO 保证数据丢失问题分析

底层和基础的清晰得理解,是解决网络开发所必须得

两个程序系统进行通信,底层是Tcp/Ip协议 ,我们开发应用程序是在这个Tcp/Ip协议之上,制定自己Application Protocol ,具体实现是有个encode 与 decode 的过程,最重要的是通信协议! 

                                    关于粘包和半包问题的讨论                                          
之所以出现粘包和半包现象,是因为TCP当中,只有流的概念,没有包的概念. 可以使用UDP协议. 这样可以就可以区分每个包了.但是要确保包的丢失处理 .为了提到效率,可以考虑写一个滑动窗口进行收发包. 若采用TCP协议进行传输,就要将每个包区分开来.可以有三种方式.因为TCP是面向流的.流只有打开和关闭,你要用一个流传输多个包 ,那就要向办法区分出每个包.

一:: 可以每次发送同样大小的包,过大的包不予发送,过小的包,后面部分用固定的字符'\0'进行填充.

二:: 将流按字符处理,抽出一个字符做转义字符(通常Java用'\'来做转义字符,比如"\n"表示换行).假如就设'\'为转义字符,发送方如果流当中出现'\',就在后面在追加一个'\',如果包结束,则用'\'做包的结束符.这样,在接收方,若读取一个单独的'\'或者流结束,就标示前面的内容构成一个包,如果连续读取两个'\',就将两个'\'用一个'\'进行替换.这样,就可以保证原来包中的信息不变,同时也能区分出每个包了.

三:: 在发送方发送一个包的时候,先将这个包的长度发送给对方(一般是4个字节表示包长),然后再将包的内容发送过去.接收方先接收4个字节,看看包的长度,然后按照长度来接收包,这样就不会出错了.

以上三种方法,是网络传输中经常用到的方法.后两种很常见.最后一种,在TCP长连接传输中应用最多.
综合以上的说法,就是要在TCP协议以上再封装一层协议,用来做分包的信息交换.


一些处理方法:

一个BUFFER,用于保存当前连接的读缓存
有数据时,Buffer = Buffer + DataIn,不停的接收
收完成后,开始解析Buffer,
根据包的协议,不停的解析Buffer,并形成一个个包进行处理,处理后,Buffer = Buffer - Data,并继续解包
完成。

JDK里面.BufferedReader是用来处理字符流的.在网络通信当中,一般不用这个类.
而用这个类来处理的.一般是我讲到的第二种处理方式.只不过,是用换行符作为包的分隔符(讲的第二方式比较通用,特例的情况下如果传输的是ACSII字符流,可以指定换行符为包的分隔符).接收端使用BufferedReader的readLine()方法.发送端在发送字符串之后,再追发一个换行符'\n',用于进行包的分隔.
可以试一下,这个效率会很高,而且,不会有粘包,半包的现象.

由于网络传输数据的不确定性(也就是说有可能传输的是图像,文件什么的),所以,一般直接使用InputStream这个类来操作.它的read方法都是阻塞读的.参数为byte数组的情况下,如果流没有结束,该方法直到byte数组填满才会返回.可以试一下,直接用InputStream这个类来处理.就不会出现使用BufferedReader读出半包的情况了.当然也不用再去递归补读了.
直接使用InputStream一般效率还是很高的,如果还要提高效率,那就要自己编写一个缓冲区了.使用多线程(线程数量3个就可以)并发处理.性能会显著提高的.

                                                    NIO 保证数据完整

       IO与NIO最重要的区别是数据打包和传输的方式 。原来的 I/O 以流的方式处理数据,而 NIO 以块的方式处理数据。

      面向流 的 I/O 系统一次一个字节地处理数据。一个输入流产生一个字节的数据,一个输出流消费一个字节的数据。为流式数据创建过滤器非常容易。链接几个过滤器,以便每个过滤器只负责单个复杂处理机制的一部分,这样也是相对简单的。不利的一面是,面向流的 I/O 通常相当慢。

      面向块 的 I/O 系统以块的形式处理数据。每一个操作都在一步中产生或者消费一个数据块。按块处理数据比按(流式的)字节处理数据要快得多。但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性。

       通道 是对原 I/O 包中的流的模拟。到任何目的地(或来自任何地方)的所有数据都必须通过一个 Channel 对象。

       Buffer 实质上是一个容器对象。发送给一个通道的所有对象都必须首先放到缓冲区中;同样地,从通道中读取的任何数据都要读到缓冲区中。

      Channel 是一个对象,可以通过它读取和写入数据。拿 NIO 与原来的 I/O 做个比较,通道就像是流。

     通道类型 通道与流的不同之处在于通道是双向的。而流只是在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类), 而 通道 可以用于读、写或者同时用于读写。 因为它们是双向的,所以通道可以比流更好地反映底层操作系统的真实情况。特别是在 UNIX 模型中,底层操作系统通道是双向的。

 

                                          异常情况

*比如说客户端发过来1024个字节.这样的话正好 requestLineBuffer 一次读到1024个字节.这时 responseSize==requestLineBuffer里面数据的大小. 这正是你需要的完整数据.

 

*如果客户端发送过来100个字节.这时responseSize<requestLineBuffer里面数据的大小.这时你就需要判断当读到100个字节的时候就不要再往下读了,即使再读的话也是空数据.

 

前两种情况正是你所希望的,responseSize<=requestLineBuffer里面数据的大小.

 

*如果客户端发送过来2048个字节,第一次最多也就读1024个字节的数据.这时responseSize>requestLineBuffer,你就要判断如果第一次没有把数据读完整的话,还要再去把剩下的1024个字节的数读过来.

 

这种情况也许你已经想到了.但下面还有2种诡异的情况.

 

*如果客户端发送过来 2048 个字节,但是分2次或多次发送过来,比如说前两次读到 1548 个字节,这时你判断发现还没有接收到完整数据但再去用int count = socketChannel.read(requestLineBuffer);读,就是读不到数据.这样的话你就需要把这次读到的数据保存下来,再注册一个读的事件去读,等到下次把剩下的 500 个字节数据读过来后再去处理逻辑.这样的情况就是socketChannel.read的数据并不是一次把数据读到的,而是分多次.

 

*最后一种情况就是 如果客户端发送过来 5000 个字节,但是每次请求的数据只有 1000 个字节,这说明客户端发送太快,或是服务器处理太慢,系统把这5次的请求的数据加到一起了.这时你就要把这5个请求一个一个的分别取出来一个一个的去处理相应的逻辑.

你可能感兴趣的:(jdk,多线程,应用服务器,网络协议,网络应用)