Java 网络编程(NIO)

32 阅读4分钟

Java 网络编程(NIO)

前提

  • 熟悉IO
  • 了解BIO

了解NIO

NIO(即非阻塞IO,JDK1.4), 与BIO完全不同,服务端不会阻塞当前线程去等待新的客户端连接,也不会阻塞当前线程去等待客户端发送数据。

image.png 基础NIO,服务端源码

白话:服务端一直循环去获取与客户端的连接,获取到了就存到一个集合中,然后一直遍历这个集合来处理每一个客户端连接,当客户端连接与服务端断开后,就将集合中的连接channel移除掉。

package org.net;

import javax.swing.text.html.HTMLDocument;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * 基础NIO 服务端示例
 */
public class NIOSocketServer {
    public static void main(String[] args) {

        try {
            //创建NIOSocket通道
            ServerSocketChannel serverSocket = ServerSocketChannel.open();
            //绑定监听的地址端口
            serverSocket.socket().bind(new InetSocketAddress("127.0.0.1", 9090));;
            //设置为非阻塞(如果是true,就跟BIO一个鸟样了)
            serverSocket.configureBlocking(false);

            //创建一个List用于放客户端连接过来的socketChannel(遍历处理)
            List<SocketChannel> socketChannels = new ArrayList<>();
            //开始监听客户端连接
            while (true) {
                //非阻塞,这里其实和BIO差不多,就是不会一直阻塞等待客户端连接,有没有客户端连接进来都会往下走
                SocketChannel socketChannel = serverSocket.accept();

                //有客户端连接上来了,socketChannel不为null,反之
                //将客户端连接配置为非阻塞,并且将其放到一个List中存着
                if (socketChannel != null) {
                    System.out.println("有新的客户端连接上来了");
                    socketChannel.configureBlocking(false);
                    socketChannels.add(socketChannel);
                }

                //遍历List,处理每个客户端连接
                Iterator<SocketChannel> iterator = socketChannels.iterator();
                while (iterator.hasNext()) {
                    //从List中取出要处理客户端连接
                    SocketChannel socket_channel = iterator.next();
                    //读写取缓存
                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

                    int read = socket_channel.read(byteBuffer);
                    if (read > 0) {
                        //将缓存区准备成读模式
                        byteBuffer.flip();
                        //将缓存中的内容转换成字符串
                        String msg = new String(byteBuffer.array(), 0, read);
                        System.out.println("收到客户端消息:" + msg);
                        //将缓冲区清空
                        byteBuffer.clear();
                        //向客户端写入消息
                        socket_channel.write(ByteBuffer.wrap("服务端:收到消息,谢谢".getBytes()));
                        if (msg.equals("exit")){
                            //客户端退出连接
                            socket_channel.close();
                            iterator.remove();
                        }
                    } else if (read == 0) {
                        //将缓存区清空
                        byteBuffer.clear();
                        try {
                            TimeUnit.MILLISECONDS.sleep(1500);
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                        //向客户端写入消息
                        socket_channel.write(ByteBuffer.wrap("服务端:你处于空闲状态".getBytes()));
                    }
                }
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

测试

客户端程序代码查看BIO文章

  • 先启动服务端程序,然后直接启动两个客户端程序

image.png

image.png

image.png 可以发现,服务端一直遍历socketChannel集合,处理连接上来的每一个客户端

  • 两个客户端都给服务端发送一条数据

image.png

可以发现,服务端无需开启新的线程去处理客户端连接.

发现问题

  • 因为serverSocketChannel.accept(),不会阻塞,所以一直while---true死循环,cpu↑
  • 每个客户端socketChannel都放入List,当客户端并发很多的时候,List↑

解决问题,多路复用(Selector)

多路复用,指多个socket连接,使用一个线程来检测多个文件描述符(Socket)的状态,(Redis单线程模型,也是使用的多路复用)

监听客户端连接,有连接才处理,监听客户端发送数据,只处理发送数据的客户端。

白话:使用Selector,就像一个监听器,可以将服务端的serverSocketChannel以及客户端连接服务端成功后的socketChannel注册到Selector里面,,只要将serverSocketChannel和SocketChannel注册到Selector中就会产生一个SelectionKey,这个SelectKey就是对应serverSocketChannel和SocketChannel注册到Selector中时所设置的事件,只要对应的serverSocketChannel或SocketChannel触发了对应的注册进Selector时设置的事件,Selector.select()监听到事件发生就会放行,Selector.selectedKeys()就可以获取触发了的SelectionKey(以Set集合方式返回,集合中不包含没有触发的SelectionKey),通过SelectionKey.channel()就可以获取对应事件的Channel(serverSocketChannel或SocketChannel)然后做相应的处理。

image.png


/**
 * 基础NIO 服务端示例
 */
public class NIOSocketServer {
    public static void main(String[] args) {

        try {
            //创建NIOSocket通道
            ServerSocketChannel serverSocket = ServerSocketChannel.open();
            //绑定监听的地址端口
            serverSocket.socket().bind(new InetSocketAddress("127.0.0.1", 9090));;
            //设置为非阻塞(如果是true,就跟BIO一个鸟样了)
            serverSocket.configureBlocking(false);
            //创建一个选择器,用于监听服务端与客户端之间发生的事件
            Selector selector = Selector.open();
            //将服务端通道注册到选择器上,并监听客户端连接事件
            serverSocket.register(selector, SelectionKey.OP_ACCEPT);

            while (true) {
                //开始监听客户端与服务端发生的事件(会阻塞,只要客户端和服务器存在事件发生才会放开)
                selector.select();

                //获取客户端与服务端发生的事件集合
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                //只处理发生的
                while (iterator.hasNext()) {
                    //取出事件
                    SelectionKey selectionKey = iterator.next();
                    //如果是连接事件
                    if (selectionKey.isAcceptable()) {
                        //事件中获取到这个与客户端的socketChannel,因为是ServerSocketChannel发生accept事件所以
                        //selectionKey.channel()获取的是因为是ServerSocketChannel发生accept事件所以
                        ServerSocketChannel channel = (ServerSocketChannel) selectionKey.channel();
                        SocketChannel socketChannel = channel.accept();
                        socketChannel.configureBlocking(false);
                        //将这个socketChannel放到选中器上,监听这个socketChannel读事件
                        socketChannel.register(selector, SelectionKey.OP_READ);
                        System.out.println("有新的客户端连接");
                        //写数据给客户端
                        socketChannel.write(ByteBuffer.wrap("连接服务器成功".getBytes("UTF-8")));
                        //如果是读事件,也就是客户端往服务器发送数据
                    } else if (selectionKey.isReadable()) {
                        //因为是SocketChannel发生读事件,所以selectionKey.channel()获取SocketChannel
                        SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
                        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                        int read = socketChannel.read(byteBuffer);
                        if (read > 0) {
                            //将byteBuffer为读
                            byteBuffer.flip();
                            String msg = new String(byteBuffer.array(), 0, read);
                            System.out.println("服务器收到消息:" + msg);
                            //清空byteBuffer
                            byteBuffer.clear();
                            //写数据给客户端
                            socketChannel.write(ByteBuffer.wrap("服务器收到消息:".getBytes("UTF-8")));
                            if (msg.equals("exit")){
                                //断开对应客户端连接
                                socketChannel.close();
                            }
                        }
                    }
                    //每次处理完一个事件就集合中清除这个事件
                    iterator.remove();
                }

            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

测试结果

image.png

image.png

image.png

epoll_create、epoll_ctl、epoll_wait(linux内核函数)

参考 IO多路复用——深入浅出理解select、poll、epoll的实现 - 知乎 (zhihu.com)