实现长连接心跳服务
在类似RPC(远程过程调用)场景中为了保证传输的效率,通常情况下会采用长链接,而长链接的保持即通过定时心跳实现。类似场景在消息推送服务中也是非常常见。所以心跳服务是网络编程中的基础且普遍的应用。
Dubbo和zookpper都有心跳饱和机制
心跳服务需求说明
由客户端定时发送给服务端,服务端作出响应。因为一直要发送,所以发送的消息和返回的消息的体量必须足够小,只要能标识心跳事件即可。具体消息格式设计要根据所使用的应用协议而定。
心跳服务设计
在客户端我们采用SocketChannel来连接服务并注册到选择器,由选择器来监听管道的状态并触发相对应的事件处理。并通过线程休眠来实现,定时发送。其整个流程如下图
- 先初始化初始化管道,建立连接()。
连接远程服务,在非阻塞模式,connect()是异步完成的,当服务端Accept链接,客户端会触发OP_CONNECT事件,然后必须调用 finishConnect() 才能真正完成调用。
SocketChannel channel = SocketChannel.open();// 打开管道
channel.configureBlocking(false); //设置非阻塞模型
Selector selector = Selector.open();//打开选择器
channel.register(selector, SelectionKey.OP_CONNECT);//监听连接事件
- 注册选择器。
- 选择器进行循环遍历(基于对择器的轮询,就可以获得选择集,并触发相对应事件)。
while (true) {//轮询选择器
selector.select(100);// 刷新选择集
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {//遍历选择集
SelectionKey key = iterator.next();
iterator.remove();
if (!key.isValid()) {
continue;
}
}
}
循序操作首先调用select()刷新键,刷新键的时候会有三个事件
当服务端Accept连接后,客户端就会触发OP_CONNECT事件。此时并不代表连接已完成,这时往管道中写数据是会报NotYetConnectedException异常的。必须调用 finishConnect()才会真正建立好连接。当建立连接后,就不在需要监听 OP_CONNECT,而是OP_WRITE事件,以将心跳事件数据写入管道。
if (key.isConnectable()) {
//返回false ,因为还没有真正建立链接
System.out.println("是否连接:"+channel.isConnected());
channel.finishConnect();
key.interestOps(SelectionKey.OP_WRITE);// 监听写数据
}
OP_write 监听可写 一旦写入心跳就是把转态切换为 op_read
什么时候会触发OP_WRITE 事件?当建立连接后管道就是一个可写状态,所以直接就能触发OP_WRITE事件。并且在关闭连接前OP_WRITE事件,一直会被触发。所以写入心跳后必须移除对OP_WRITE事件的监听,改为监听OP_READ事件。
if (key.isConnectable()) {
//返回false ,因为还没有真正建立链接
System.out.println("是否连接:"+channel.isConnected());
channel.finishConnect();
key.interestOps(SelectionKey.OP_WRITE);// 监听写数据
}
op_read 监听可读 等待读取结果等待两秒钟就会切换为OP_write
当服务端消息返回客户后就会触发客户端的OP_READ事件,此时直接读取即可。然后在将线程休眠2s后切换监听到OP_WRITE。如果不休眠会立马触发OP_WRITE事件。
channel.read(ByteBuffer.allocate(64));
key.interestOps(SelectionKey.OP_WRITE);
Thread.sleep(2000);// 休眠2秒防止立马进行写入
因为是客户端不是服务端没有accpet
2.心跳服务端设计
服务端流程说明
- 初始化管道
与客户端类似 也是NIO中常规操作,打开管道与选择器,设置阻塞然后注册到选择器。
这里服务端使用ServerSocketChannel ,而客户端使用SocketChannel。两者区别是ServerSocketChannel仅用于接受连接,不支持读写。而SocketChannel用于连接和读写。
ServerSocketChannel serverListener=ServerSocketChannel.open();
serverListener.bind(new InetSocketAddress(8080));// 绑定TCP端口
serverListener.configureBlocking(false);
Selector selector = Selector.open();
// 注册ACCEPT 事件,用于同意客户端连接
serverListener.register(selector,SelectionKey.OP_ACCEPT);
- 轮询选择器
请参照上面选择器轮询 - 触发OP_ACCEPT事件
OP_ACCEPT指有新连接到达,通过ServerSocketChannel.accept() 即可获取一个新管道SocketChannel。 基于它就可以与客户端进行读写。这种读写同样基于非阻塞方式执行,并注册到选择器。
if(key.isAcceptable()){
SocketChannel socketChannel = serverListener.accept();
socketChannel.configureBlocking(false);
// 将新管道注册到选择器
socketChannel.register(selector, SelectionKey.OP_READ);
}
注:假设服务端接收的连接到达极限,是否可以直接勿略客户端连接,不执行accept()方法?这是不行的,在TCP中所有事件都必须进行处理,否则会一直触发该事件,造成死循环。
4. 触发OP_READ事件
当客户端发送数据过来,即会触发读取事件,这时直接读取即可,然后在写回响应数据即可。
if(key.isReadable()){
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer=ByteBuffer.allocate(64);
channel.read(buffer);// 读取数据到缓冲区
if (buffer.get(0)==4) {//客户端主动关闭连接, 传输结束
channel.close();
System.out.println("关闭管道:"+channel);
break;
}
buffer.put(String.valueOf(System.currentTimeMillis()).getBytes());
buffer.flip();
channel.write(buffer);// 写回数据到管道
}
字节’4’ 在ASCII中 表示EOT (end of transmission )传输结束,所以在管道中读取到4这个字节,即可手动的去关闭连接。