长链接NIO

长链接NIO

推送

所有需要客户端被动接收信息的功能模块,都可以用推送实现,比如A想给B发消息,A调服务器接口,服务器只是存数据,它调推送的接口,推送去去找B。推送用的是xmpp协议, 是一种基于TCP/IP的协议, 这种协议更适合消息发送

  1. socket 套接字, 发送和接收网络请求
  2. 长连接 keep-alive, 服务器基于长连接找到设备,发送消息
  3. 心跳包 , 客户端会定时(30秒一次)向服务器发送一段极短的数据,作为心跳包, 服务器定时收到心跳,证明客户端或者,才会发消息.否则将消息保存起来,等客户端活了之后(重新连接),重新发送.
  4. 网络状态变化
    手机网络和WIFI网络切换、网络断开和连上等情况有网络状态的变化,也会使长连接变为无效连接,需要监听响应的网络状态变化事件,重新建立Push长连接。

关于心跳

  1. 客户端网络空闲5秒没有进行写操作是,进行发送一次ping心跳给服务端;
  2. 客户端如果在下一个发送ping心跳周期来临时,还没有收到服务端ping的心跳应答,则失败心跳计数器加1;
  3. 每当客户端收到服务端的ping心跳应答后,失败心跳计数器清零;
  4. 如果连续超过3次没有收到服务端的心跳回复,则断开当前连接,在5秒后进行重连操作,直到重连成功,否则每隔5秒又会进行重连;(不对吧)。死链check 时间应该是心跳间隔 + 心跳消息超时

长连接会不会耗流量?
维持长连接并不是连接在那儿就一直消耗流量,只是隔段时间进行“心跳”来保持连接,而一次心跳流量是可以做得很小的。总之,长连接的方式一方面实时性好,另一方面,比轮询更少的消耗流量。项目是5秒发一次

心跳包和轮询的区别
轮询是为了获取数据,而心跳是为了保活TCP连接。
轮询得越频繁,获取数据就越及时,心跳的频繁与否和数据是否及时没有直接关系。
轮询比心跳能耗更高,因为一次轮询需要经过TCP三次握手,四次挥手,单次心跳不需要建立和断开TCP连接。

I/O 是什么?

程序内部和外部进⾏行数据交互的过程,就叫输入输出。
程序内部是谁?内存
程序外部是谁?
⼀般来说是两类:本地文件和网络。
也有别的情况,比如你和别的程序做交互,和你交互的程序也属于外部,但一般来说,就是文件和网络这么两种。

从文件里或者从网络上读数据到内存里,就叫输入;
从内存里写到文件里或者发送到网络上,就叫输出
Java I/O 作⽤用只有一个:和外界做数据交互

常见的IO

答:字节流和字符流。字节流继承于InputStream、OutputStream,字符流继承于Reader、Writer,如果字符流就得有刷新动作。

  1. 打印流:将各种数据类型的数据原样打印,PrintStream  、PrintWriter
  2. 对象流:它的写方法是ObjectOutputStream,读方法是ObjectInputStream。它主要操作的是对象,而对象中也能封装数据,所以它也具备操作基本数据类型的方法。被它操作的对象必须是实现了序列化的对象也就是Serializable接口,但是输入流还多支持一种Externalizable 接口的对象。持久化
  3. DataStream:可以用于操作基本数据类型的数据的流对象:DataInputStream与DataOutputStream,它主要的特点就是操作基本数据类型,特色方法writeUTF、readUTF
  4. 字节数组流:ByteArrayInputStream 、ByteArrayOutputStream,因为这两个流对象都操作的数组,并没有使用系统资源。所以,不用进行close关闭,而且关闭也是无效的。
  5. 文件流:FileReader
    字符流
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
      FileReader fr = new FileReader("buf.txt");
BufferedReader bufr = new BufferedReader(fr);

FileWriter fw = new FileWriter("buf_copy.txt");
BufferedWriter bufw = new BufferedWriter(fw);

String line = null;//第一种
while ((line = bufr.readLine()) != null) {
bufw.write(line);
bufw.newLine();
bufw.flush();
}

字节流
FileInputSteam fis = new FileInputSteam(1.jpg);
FileOnputSteam fos = new FileOnputSteam(2.jpg);
btey[] b = new byte[1024];//第二种
int len=0;
while((len=fis.read(b))!=-1){
fos.writer(b,0,len);
}

第三种
FileInputStream fis = new FileInputStream("c:\\0.mp3");
BufferedInputStream bufis = new BufferedInputStream(fis);
FileOutputStream fos = new FileOutputStream("c:\\2.mp3");
BufferedOutputStream bufos = new BufferedOutputStream(fos);

int ch = 0;
while((ch=bufis.read())!=-1){
bufos.write(ch);
}

NIO

NIO vs IO区别

NIO vs IO之间的理念上面的区别(NIO将阻塞交给了后台线程执行)

  1. IO是面向流的,NIO是面向缓冲区的
    Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方;NIO则能前后移动流中的数据,因为是面向缓冲区的
  2. IO流是阻塞的 NIO流是不阻塞的
    Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了
    Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。NIO可让您只使用一个(或几个)单线程管理多个通道(网络连接或文件),但付出的代价是解析数据可能会比从一个阻塞流中读取数据更复杂。

IO是阻塞式的,NIO是非阻塞式的,所有数据都是用缓冲区进行处理的。在读取数据时,它是直接读到缓冲区中;在写入数据时,它也是写入到缓冲区中。任何时候访问 NIO 中的数据,我们都是通过缓冲区进行读写操作。

  • 看到Content-Encoding: gzip时,需要将Body数据先解压缩,才能得到真正的数据。压缩的目的在于减少Body的大小,加快网络传输。
  • 同步和异步的区别就在于是否等待IO执行的结果。好比你去麦当劳点餐,你说“来个汉堡”,服务员告诉你,对不起,汉堡要现做,需要等5分钟,于是你站在收银台前面等了5分钟,拿到汉堡再去逛商场,这是同步IO。
    你说“来个汉堡”,服务员告诉你,汉堡需要等5分钟,你可以先去逛商场,等做好了,我们再通知你,这样你可以立刻去干别的事情(逛商场),这是异步IO。
    很明显,使用异步IO来编写程序性能会远远高于同步IO,但是异步IO的缺点是编程模型复杂。想想看,你得知道什么时候通知你“汉堡做好了”,而通知你的方法也各不相同。如果是服务员跑过来找到你,这是回调模式,如果服务员发短信通知你,你就得不停地检查手机,这是轮询模式。总之,异步IO的复杂度远远高于同步IO。

unknown_filename


Netty

有一个接口监听。监听里有建立连接,收到消息,完成等各种回调,收到后解析

  • Netty是一个异步非阻塞的事件驱动型的网络应用程序框架。是一个高性能、方便开发的NIO框架,使用它可以简单快速地开发网络应用程序,比如客户端和服务端的协议。Netty大大简化了网络编程比如TCP和UDP的 Socket的开发。
  • IO是阻塞式的,NIO是非阻塞式的,所有数据都是用缓冲区进行处理的。在读取数据时,它是直接读到缓冲区中;在写入数据时,它也是写入到缓冲区中。任何时候访问 NIO 中的数据,我们都是通过缓冲区进行读写操作。
  • 当客户端与服务器建立起连接后,ChannelHandler的方法是被网络event(这里的event是广义的)触发的,由ChannelHandler直接处理输入输出数据,并传递到管道中的下一个ChannelHandler中。
  • 通过Channel或者ChannelHandlerContext发生的请求/响应event 就是在管道中ChannelHandler传递。

ChannelInboundHandler

  • ChannelInboundHandler对从客户端发往服务器的报文进行处理,一般用来执行解码、读取客户端数据、进行业务处理等;ChannelOutboundHandler对从服务器发往客户端的报文进行处理,一般用来进行编码、发送报文到客户端。
  • 一般就是继承ChannelInboundHandlerAdapter和ChannelOutboundHandlerAdapter,因为Adapter把定制(custom) ChannelHandler的麻烦减小到了最低,Adapter本身已经实现了基础的数据处理逻辑(例如将event转发到下一个handler),你可以只重写那些你想要特别实现的方法。
  • 和NIO一样,读取数据时,它是直接读到缓冲区中;在写入数据时,它也是写入到缓冲区中。在TCP/IP中,NETTY会把读到的数据放到ByteBuf的数据结构中。所以这里读取在ByteBuf的信息,得到服务器返回的内容。
  • ChannelPipeline作为放置ChannelHandler的容器,采用了J2EE的 拦截过滤模式,用户可以定义管道中的ChannelHandler以哪种规则去拦截并处理事件以及在管道中的ChannelHandler之间如何通信。每个Channel都有它自己的Pipeline,当一个新的Channel被创建时会自动被分配到一个Pipeline中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
private final String TAG = "Netty";
private INettyClient.OnConnectStatusListener statusListener;
private List<INettyClient.OnDataReceiveListener> listeners = new ArrayList<>();

@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//channelActive()方法将会在连接被建立并且准备进行通信时被调用。
Log.d(TAG, "channel active");
super.channelActive(ctx);
}

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg)
throws Exception {
//channelRead()方法是在数据被接收的时候调用。
ByteBuf buf = (ByteBuf) msg;
byte[] req = new byte[buf.readableBytes()];
buf.readBytes(req);
String body = new String(req, "UTF-8");
//verify(String body)方法对服务器返回的数据进行校验,并取出数据部分。
//具体校验的方法需要与后台同事进行协议。
body = verify(body);

Log.d(TAG, "verify : " + body);
if (null != body)
parseJson(body);
}


@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
//exceptionCaught()事件处理方法是当出现Throwable对象才会被调用,
//即当Netty由于IO错误或者处理器在处理事件时抛出的异常时。
//在大部分情况下,捕获的异常应该被记录下来并且把关联的channel给关闭掉。
ctx.close();
Log.e(TAG, "Unexpected exception from downstream : "
+ cause.getMessage());
if (statusListener != null)//连接异常时触发onDisconnected()
statusListener.onDisconnected();
}

@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.fireChannelReadComplete();
LogUtils.d(TAG, "channelReadComplete");
}

//对数据进行解析,拿出区分不同请求的 flag字段,再根据不同的flag字段去触发相对应的监听器
private void parseJson(String json) {
try {
JSONObject jObject = new JSONObject(json);
int msgType = jObject.getInt(Constant.FLAG_MT);
Log.d(TAG, "parseJson message type: " + msgType + " json: " + json);
callListeners(msgType, json);
} catch (Exception e) {
LogUtils.e(TAG, "parseJson exception: " + e.getMessage());
e.printStackTrace();
}
}

//遍历监听器List,触发拥有正确msgType 的OnDataReceiveListener,
//回调 void onDataReceive(int mt, String json);方法
private void callListeners(final int msgType , final String json) {
for (final INettyClient.OnDataReceiveListener listener : listeners)
if (listener != null)
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {//主线程中进行
listener.onDataReceive(mt, json);
}
});
}

//绑定OnDataReceiveListener
public void addDataReceiveListener(INettyClient.OnDataReceiveListener listener) {
if (!listeners.contains(listener))
listeners.add(listener);
}
//绑定OnConnectStatusListener
public void setConnectStatusListener(INettyClient.OnConnectStatusListener listener) {
statusListener = listener;
}
}

以上,就是一个供Android客户端使用的

ChannelHandler,可以通过实现具体的OnDataReceiveListener来异步地获得服务器返回的 数据。NettyClient的实现 以上仅仅是展示了如何处理服务器返回的数据。建立连接、发送消息以及心跳包的功能还没进行封装。
在2.接口的定义 里面已经定义好了NettyClient应该具备哪些行为,现在进行具体的实现。 主要的实现思路是:

  1. 构建Bootstrap,其中包括设置好ChannelHandler来处理将来接收到的数据(详见Android开发之使用Netty进行Socket编程(二) )。由Boostrap建立连接。通过channel.writeAndFlush(constructMessage(sendMsg)).sync()发送消息。这些工作都在子线程完成。
  2. 在子线程 建立连接并向服务器发送请求,这里采用了HanlderThread+Handler的方案。通过Looper依次从Handler的队列中获取信息,逐个进行处理,保证安全,不会出现混乱。
  3. 心跳包的发送通过handleMessage(Message msg)中的死循环进行不间断地发送。
  4. NettyClientHandler的实现中我们已经知道,当Netty异常时会触发statusListener.onDisconnected();,NettyClient中,onDisconnected()方法会进行重连操作。 接收到服务器返回的消息时,会在主线程中触发onDataReceiveListener .onDataReceive(mt, json);
     5. 外部通过单例模式进行调用。

长链接NIO
http://peiniwan.github.io/2024/04/60d6aa7b5dec.html
作者
六月的雨
发布于
2024年4月6日
许可协议