Netty原理详解系列(三)---NIO实战之心跳服务

1.概述

Netty原理详解系列的前两篇文章介绍了NIO中的三个重要的组件,有了理论的基础,这篇博客就带着大家实现一个简单的心跳服务。

2.心跳服务

在类似RPC(远程过程调用)场景中为了保证传输的效率,通常情况下会采用长链接,而长链接的保持即通过定时心跳实现。类似场景在消息推送服务中也是非常常见。所以心跳服务是网络编程中的基础且普遍的应用。

3.服务端实现

服务端主要用于接收客户端的请求,并且接收客户端的心跳请求,进行响应。

服务端流程说明:

  • 创建serverSocketChannel
  • 设置阻塞模型为false
  • 绑定端口
  • 创建选择器Selector
  • 将第一步创建的channel注册到选择器Selector中,并设置监听模式是ACCEPT,也就是用于接收客户端的连接消息。
  • 轮询选择键集
    • 若当前选择键是可接收连接的状态。则接收客户端连接,构建新的管道socketChannel,设置阻塞模型为false。然后将该管道注册到选择器中,并设置监听模式是Read,用来监听客户端发送来的消息。
    • 若当前选择键是可写模式,则基于缓冲区,将内容写入管道中,写入后将监听模式修改为Read。
    • 若当前选择键是可读模式,则将内容从管道中读到缓冲区。也就是写入缓冲区。然后再从缓冲区中拿出消息。最后设置监听模式为Write。注意写入缓冲区后,要记得对缓冲区执行flip操作。才可以正确的读取缓冲区。

代码实现如下:

/**
 * @author gongsenlin
 * @version 1.0
 * @date 2020-10-12 20:32
 */
public class echoServer {
    public static void main(String[] args) throws IOException {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();//创建serverSocketChannel
        serverSocketChannel.configureBlocking(false);//设置阻塞模型为false
        Selector selector = Selector.open();//创建选择器Selector
        serverSocketChannel.bind(new InetSocketAddress(8080));//绑定端口
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);//将第一步创建的channel注册到选择器Selector中,并设置监听模式是ACCEPT,也就是用于接收客户端的连接消息。
        for (; ; ) {// 轮询选择键集
            selector.select();
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                iterator.remove();
                if (!key.isValid()) {
                    continue;
                }
                if (key.isAcceptable()) {
                    System.out.println("接收客户端连接");
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector, SelectionKey.OP_READ);
                } else if (key.isWritable()) {
                    String str = "心跳 pong";
                    SocketChannel channel = (SocketChannel) key.channel();
                    channel.write(ByteBuffer.wrap(str.getBytes()));
                    key.interestOps(SelectionKey.OP_READ);
                } else if (key.isReadable()) {
                    SocketChannel channel = (SocketChannel) key.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    channel.read(buffer);
                    buffer.flip();
                    System.out.println(new String(buffer.array(), 0, buffer.limit()));
                    key.interestOps(SelectionKey.OP_WRITE);
                }
            }
        }
    }
}

4.客户端实现

和服务端建立连接,建立可发送可接收的消息通道

客户端的流程如下:

  • 创建socketChannel 这里和服务端的不同。原因是socketChannel是SelectableChannel的子类 可以设置CONNECT、WRITE、READ模式,但是serverSocketChannel仅可以设置ACCEPT模式
  • 设置阻塞模式为false
  • 创建选择器Selector
  • 将socketChannel注册到选择器中,并设置监听模型为CONNECT
  • 执行连接服务端 connect()是异步完成的,当服务端Accept链接,客户端会触发OP_CONNECT事件,然后必须调用 finishConnect() 才能真正完成调用。
  • 轮询选择键集
    • 若当前选择键是连接状态,那么要执行socketChannel.finishConnect完成连接。然后将选择键的监听模式改为WRITE
    • 若当前选择键是可写状态,那么就基于buffer写入到channel中,将“心跳 ping”发送到服务端,并修改选择键的监听模式为READ
    • 若当前选择键是可读模式,则将内容从管道中读到缓冲区。也就是写入缓冲区。然后再从缓冲区中拿出消息,得到客户端发送的“心跳 pong”。最后设置监听模式为Write。注意写入缓冲区后,要记得对缓冲区执行flip操作。才可以正确的读取缓冲区。

代码实现如下:

public static void main(String[] args) throws IOException, InterruptedException {
    SocketChannel socketChannel = SocketChannel.open();//创建socketChannel
    socketChannel.configureBlocking(false);//设置阻塞模式为false
    Selector selector = Selector.open();//创建选择器Selector
    socketChannel.register(selector, SelectionKey.OP_CONNECT);//将socketChannel注册到选择器中,并设置监听模型为CONNECT
    socketChannel.connect(new InetSocketAddress("127.0.0.1", 8080));//连接服务端
    for (; ; ) {// 轮询选择键集
        selector.select();
        Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
        while (iterator.hasNext()) {
            SelectionKey key = iterator.next();
            iterator.remove();
            if (!key.isValid()) {
                continue;
            }
            if (key.isConnectable()) {
                System.out.println("是否连接:" + socketChannel.isConnected());
                socketChannel.finishConnect();//完成与服务端的连接
                System.out.println("是否连接:" + socketChannel.isConnected());
                key.interestOps(SelectionKey.OP_WRITE);
            } else if (key.isWritable()) {
                String str = "心跳 ping";
                socketChannel.write(ByteBuffer.wrap(str.getBytes()));
                key.interestOps(SelectionKey.OP_READ);
            } else if (key.isReadable()) {
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                socketChannel.read(buffer);
                buffer.flip();
                System.out.println(new String(buffer.array(), 0, buffer.limit()));
                key.interestOps(SelectionKey.OP_WRITE);
                Thread.sleep(2_000);
            }
        }
    }
}

5.结果展示

先启动服务端,然后启动客户端。控制台的输出如下:
在这里插入图片描述
在这里插入图片描述

6.总结

客户端与服务端的异同

  • 服务端创建的是ServerSocketChannel,用于接收客户端的连接, 该通道仅能设置监听模式为ACCEPT
  • 客户端创建的是SocketChannel,用于发送连接,发送消息,读取消息,可设置的监听模式有三种WRITE,READ,CONNECT
  • 服务端在接收了连接后,要多创建一个用于收发消息的socketChannel,有多少个客户端连接,就会创建多少个socketChannel,上限是多少个连接不是很清楚,可能和电脑的额配置有关系吧,清楚的大佬可以评论区分享一下。我的电脑上模拟了1000个连接是可行的,1W个连接就报错了。
  • 服务端与客户端用于收发消息的channel 对应的键的监听模式在WRITE和READ之间来回切换。
  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值