java原生NIO实现客户端服务端简单通讯

Posted FreeFly辉

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java原生NIO实现客户端服务端简单通讯相关的知识,希望对你有一定的参考价值。

服务端

模拟http形式,客户端请求一次,服务端响应一次

解释以注释形式加入代码中,想要理解可看注释

public class Server {
    private static Selector selector ;
	
	public static void main(String[] args) throws IOException {
        init();
    }

    public  static void init() throws IOException{
        selector = Selector.open(); //nio是通过selector管理多个管道的,所以一定要有selector
        ServerSocketChannel ssc = ServerSocketChannel.open();//开启一个服务端
        ssc.configureBlocking(false);//设置服务socket非阻塞
        ssc.bind(new InetSocketAddress(8899));//绑定监听端口
        ssc.register(selector, SelectionKey.OP_ACCEPT);//绑定selector,此时只需在意 打开 OP_ACCEPT事件,相当于传统socket中的accept()操作
        listen(); //监听连接事件方法
    }

    public static void listen() throws IOException{
        while(true){ //死循环一直监听
            int select = selector.select();//获取检测到的连接完成,因为此时只注册了 OP_ACCEPT事件ssc.register(selector,SelectionKey.OP_ACCEPT);,所以就相当于传统socket中的accpet方法,只是这个select会一次返回所有accept状态的连接,而非传统socket中的阻塞式单个accept方法
            if(select == 0){//没有建立连接的则直接下一次循环
                continue;
            }
            Set<SelectionKey> selectedKeys = selector.selectedKeys();
            Iterator<SelectionKey> it = selectedKeys.iterator();
            while(it.hasNext()){
                SelectionKey key = it.next();
                // 由于select操作只管对selectedKeys进行添加,所以key处理后我们需要从里面把key去掉
                it.remove();
                if(key.isAcceptable()){
                    ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();//获取通道,相当于传统io的iostream作用,不同的是channel是批量传输buffer对象的,而对于buffer是可以0拷贝的,因此速度会相对有提升
                    SocketChannel channel = serverChannel.accept();// 得到与客户端的套接字通道,没啥好说的
                    channel.configureBlocking(false);//设置channel非阻塞
                    channel.register(selector, SelectionKey.OP_READ); //注册读事件,会覆盖之前注册的OP_ACCEPT事件,因为模仿http请求单次请求,单次响应,所以作为服务端肯定是先注册读事件用于处理。
                }else if(key.isReadable()){//当注册完读事件,下一次循环读就绪时就会进入此方法
                    read(key);//读取对应channel事件
                    key.interestOps(SelectionKey.OP_WRITE);// 改变自身关注事件,可以用位或操作|组合时间,读完之后注册写事件模拟http的响应,所以用写事件覆盖读事件
                }else if(key.isWritable()){//处理完读会注册写事件,下一次循环会进入此方法
                    write(key);//处理写事件
                    key.interestOps(0);// 改变自身关注事件,此处为清理所有监听事件,模拟http中相应完关闭此连接
                    key.channel().close();
                }
            }
        }
    }

    private static void write(SelectionKey key) throws IOException {
        SocketChannel channel = (SocketChannel) key.channel();
        channel.write(ByteBuffer.wrap("server write\\n".getBytes()));//此处自行搜索ByteBuffer相关使用

    }

    private static void read(SelectionKey key) throws IOException {
        SocketChannel channel = (SocketChannel) key.channel();
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        int count = channel.read(buffer);
        byte[] bytes = new byte[count];
        buffer.flip();
        buffer.get(bytes);
        System.out.println(new String(bytes));
    }
}

nio实现的客户端

模拟http形式,客户端请求一次,得到服务端响应一次,客户端就关闭

public class Client {
    private static Selector selector;
	
	public static void main(String[] args) throws IOException {
        init();
        request();
    }

    public static void init() throws IOException{
        selector = Selector.open();//客户端也用nio,事实上并不是非要用selector形式的nio,一单通过网络连接,和如何实现就无关了,后面贴了个传统socket客户端的代码
        SocketChannel sc = SocketChannel.open(); //打开客户端连接,相对服务端只是少了个Server前缀和传统Socket一样
        sc.configureBlocking(false);
        sc.connect(new InetSocketAddress(InetAddress.getLocalHost(), 8899));//绑定请求地址端口
        sc.register(selector, SelectionKey.OP_CONNECT);//注册connect事件,相对于服务端注册的OP_ACCEPT
    }

    public static void request() throws IOException{
        out:while(true){
            selector.select();
            Set<SelectionKey> selectedKeys = selector.selectedKeys();
            Iterator<SelectionKey> it = selectedKeys.iterator();
            while(it.hasNext()){
                SelectionKey key = it.next();
                it.remove();//以上代码解释同服务端
                if(key.isConnectable()){//如果selector检测到连接事件,第一次进来肯定是连接事件,因为上面只注册了连接事件
                    SocketChannel channel = (SocketChannel) key.channel();
                    if(channel.finishConnect()){ //如果完成了连接
                        channel.configureBlocking(false);
                        channel.register(selector, SelectionKey.OP_WRITE);//注册写事件,因为模拟http的形式,所以客户端是先写,再读,
                        //即先请求在得到响应数据,如果是想保持长连接则写成 channel.register(selector, SelectionKey.OP_READ|SelectionKey.OP_WRITE);形式
                    }
                }else if(key.isReadable()){//在第二次进来写数据后注册了读事件,则第三次进来就会读到 读就绪事件
                    read(key); //读取数据
                    key.interestOps(0);//清理事件监听
                    key.channel().close();//关闭连接
                    break out;//跳出循环,此处不建议这么
                }else if(key.isWritable()){//连接完成注册好写事件,第二次进来则检测到的就是写事件
                    write(key);//写出数据
                    key.interestOps(SelectionKey.OP_READ);//写完数据注册读事件,用于接受数据
                }
            }
        }
    }

    private static void write(SelectionKey key) throws IOException {
        SocketChannel channel = (SocketChannel) key.channel();
        channel.write(ByteBuffer.wrap("client write".getBytes()));
    }

    private static void read(SelectionKey key) throws IOException {
        SocketChannel channel = (SocketChannel) key.channel();
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        int count = channel.read(buffer);
        byte[] bytes = new byte[count];
        buffer.flip();
        buffer.get(bytes);
        System.out.println(new String(bytes));
    }

}

传统socket实现的客户端

模拟http形式,客户端请求一次,得到服务端响应一次,客户端就关闭

public class IOSocketClient {
    public static void main(String[] args) throws Exception{
        socketIo();
    }

    static void socketIo() throws Exception{
        Socket socket = new Socket();
        socket.connect(new InetSocketAddress(InetAddress.getLocalHost(),8899));

        socket.getOutputStream().write("io socket client write\\n".getBytes());//发起请求写数据

        BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));//读取一行响应数据

        System.out.println(reader.readLine()); //打印数据
        socket.close(); //关闭连接
    }
}

总结

1、无论是什么形式,服务端肯定要先创建服务,绑定端口。客户端创建连接 绑定请求地址+端口。

2、服务端要通过accept,客户端要通过connect,从而建立通道(即inputstream和outputstream或者channel,其实都差不多用来传数据的)。

3、nio借助 selector 去管理多个连接,通过注册的管理项,进行select()方法返回集合用于 编程人员处理从而代替了一个连接就用一个线程处理。(具体场景可自行搜索)。

4、nio传输数据通过Buffer类,而Buffer类的方法可以申请堆外内存,实现非拷贝批量传输,从而提升速率。

5、最后提一点netty是基于nio的网络框架,有兴趣的可以连接nio后去学习一下netty

以上是关于java原生NIO实现客户端服务端简单通讯的主要内容,如果未能解决你的问题,请参考以下文章

JAVA NIO 异步TCP服务端向客户端消息群发代码教程实战

JAVA NIO 异步TCP服务端向客户端消息群发代码教程实战

网络I/o编程模型7 Nio实现聊天室

golang代码片段(摘抄)

为什么不建议使用 Java 原生 NIO?

JAVA NIO原理剖析