网络编程基础笔记

85 阅读6分钟

网络编程

JAVA IO模型

什么是IO?

I/O就是计算机内存与外部设备之间拷贝数据的过程

image.png

I/O中的两组概念

注意:需要和线程中的同步线程和异步线程区分开,这里指的是同步IO/异步IO

  • 同步IO/异步IO:数据就绪后需要自己去读就是同步,数据就绪后系统直接读好再回调给程序就是异步。
  • 阻塞/非阻塞:没有数据传过来时,读会阻塞直到有数据;缓冲区满时,写操作也会阻塞。非阻塞遇到这些情况,都是直接返回。

常见IO模型

  • 同步阻塞 image.png
  • 同步非阻塞 image.png
  • I/O多路复用 image.png
  • 异步 image.png

JAVA BIO模型

什么是BIO?

BIO是blocking I/O的简称,它是同步阻塞IO,其相关的类和接口在java.io下

BIO模型简单来讲,就是服务端为每一个请求都分配一个线程进行处理,I/O操作都是基于流Stream的操作

image.png

代码实现

  • 客户端
public class BioClient {
    public static void main(String[] args) {
        Socket socket = null;
        BufferedReader inputStream = null;
        BufferedWriter outputStream = null;
        try {
            // Socket客户端对象,绑定本机 8989端口
            socket = new Socket("127.0.0.1", 8989);
            inputStream = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            outputStream = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
            System.out.println("开始向服务端发送数据!");
            // \n 不能忘因为是 readLine
            outputStream.write("hello sercer,i am client! \n");
            outputStream.flush();

            String line = inputStream.readLine();
            System.out.println("接收服务端返回的数据:" + line);
        } catch (IOException e) {
            if (inputStream != null){
                try {
                    inputStream.close();
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }
            if (outputStream != null){
                try {
                    outputStream.close();
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }
            if (socket != null){
                try {
                    socket.close();
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }
        }
    }
}
  • 服务端
public class BioServer {
    public static void main(String[] args) throws IOException {
        // 基于ServerSocket
        ServerSocket serverSocket = new ServerSocket(8989);
        System.out.println("开开始服务端监听!");
        // socket是一个客户端对象,accept是阻塞的
        Socket socket = serverSocket.accept();
        // 执行业务
        new Thread(new ServerHandle(socket)).start();
    }

    static class ServerHandle implements Runnable {

        private final Socket socket;

        public ServerHandle(Socket socket) {
            this.socket = socket;
        }

        @Override
        public void run() {
            try {
                BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
                while (true){
                    // 接收来自客户端的数据
                    String line = bufferedReader.readLine();
                    System.out.println("接收到一个socket链接:" + socket);
                    System.out.println("客户端发来数据:"+ line);
                    // bus
                    // 向客户端发数据
                    bufferedWriter.write("hello client,i am server \n");
                    bufferedWriter.flush();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

BIO 的弊端

image.png

  • 线程开销

客户端的并发数与后端的线程数成1:1的比列,线程的创建、销毁是非常消耗系统资源的,随着并发量增大,服务端性能将显著下降,甚至会发生线程堆栈溢出等错误

  • 线程阻塞

当连接创建后,如果该线程没有操作时,会进行阻塞操作,这样极大的浪费了服务器资源

JAVA NIO模型

什么是NIO?

NIO,称之为New IO 或是 non-block IO(非阻塞IO),这两种说法都可以,其实称之为非阻塞IO更恰当一些。

NIO的三大核心组件

  • Buffer(缓冲区)

Buffer是一个对象,包含一些要写入或者写出的数据,体现了与原I/O的一个重要区别,在面向流的I/O中,数据读写是直接进入到Stream中,而在NIO中,所有数据都是用缓冲区处理的,读数据直接从缓冲区读,写数据直接写入缓冲区。

缓冲区的本质是一个数组,通常是一个字节数组(ByteBuffer),也可以使用其他类型,但缓冲区又不仅仅是一个数组,它还提供了对数据结构化访问以及维护读写位置等操作。

image.png

  • Channel(通道) Channel是一个通道,管道,网络数据通过Channel读取和写入,Channel和流Stream的不同之处在于Channel是双向的流只在一个方向上移动(InputStream/OutputStream),而Channel可以用于读写同时进行,即Channel是全双工的.

image.png Channel继承结构

image.png

ServerSocketChannel和SocketChannel

客户端的SocketChannel向服务端ServerSocketChannel发起连接,服务端会生成一个SocketChannel 这个SocketChannel就是客户端的SocketChannel

image.png

-Selector(选择器/多路复用器)

Selector会不断轮询注册在其上的Channel,如果某个Channel上面发生读或者写事件,即该Channel处于就绪状态,他就会被Selector轮询出来,然后通过selectedKeys可以获取就绪Channel的集合,进行后续的I/O操作。

image.png

代码实现

  • 客户端
public class NioClient {
    public static void main(String[] args) {
        try {
            // 1、窗口客户端SocketChannel,绑定客户端本地地址(不选默认随机分配一个可用地址)
            SocketChannel socketChannel = SocketChannel.open();
            // 2、设置非阻塞模式
            socketChannel.configureBlocking(false);
            // 3、创建Selector
            Selector selector = Selector.open();
            // 4、创建Reactor模型
            new Thread(new SingleReactorClient(socketChannel,selector)).start();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
class SingleReactorClient implements Runnable {

    private final SocketChannel socketChannel;

    private final Selector selector;

    public SingleReactorClient(SocketChannel socketChannel, Selector selector) {
       this.socketChannel = socketChannel;
       this.selector = selector;
    }

    @Override
    public void run() {
        try {
            // 连接服务器
            doConnect(socketChannel,selector);
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 7、多路复用器执行多路复用程序
        while (true){
            try {
                selector.select(1000);
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()){
                    SelectionKey selectionKey = iterator.next();
                    processKey(selectionKey);
                    iterator.remove();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private void processKey(SelectionKey selectionKey) throws IOException {
        if (selectionKey.isValid()){
            // 8、根据准备就绪的事件类型分别处理
            if (selectionKey.isConnectable()){
                // 服务端可连接事件准备就绪
                SocketChannel sc = (SocketChannel) selectionKey.channel();
                if (sc.finishConnect()){
                    // 8.1、向selector注册可读事件(接收来自服务端的数据)
                    sc.register(selector,SelectionKey.OP_READ);
                    // 8.2、处理业务,向服务端发送数据
                    doService(sc);
                }else{
                    // 连接失败,退出
                    System.exit(1);
                }
            }
            // 读事件准备就绪
            if (selectionKey.isReadable()){
                // 9、读服务端返回的数据
                SocketChannel sc = (SocketChannel) selectionKey.channel();
                ByteBuffer readBuffer = ByteBuffer.allocate(1024);
                int readBytes = sc.read(readBuffer);
                // 前面设置过socketChannel是非阻塞的,故要通过返回值判断读取的字节数
                if (readBytes > 0){
                    readBuffer.flip();// 读写模式切换
                    byte[] bytes = new byte[readBuffer.remaining()];
                    readBuffer.get(bytes);
                    String msg = new String(bytes, "utf-8");
                    // 接收到服务端返回的数据后进行相关操作
                    doService(msg);
                }else if (readBytes < 0){
                    // 值为-1表示链路通道已经关闭
                    selectionKey.cancel();
                    sc.close();
                }else{
                    // 没读取到数据,忽略
                }
            }
        }
    }

    private String doService(String msg) {
        System.out.println("成功接收来自服务端响应的数据:"+msg);
        return "";
    }

    private void doConnect(SocketChannel socketChannel, Selector selector) throws IOException {
        System.out.println("客户端启动成功,开始连接服务器");
        // 5、连接服务器
        boolean connect = socketChannel.connect(new InetSocketAddress("127.0.0.1", 8989));
        // 6、将socketChannel注册到selector并判断是否连接成功,连接成功监听读事件,没有继续监听连接事件
        System.out.println("connect="+connect);
        if (connect){
            socketChannel.register(selector, SelectionKey.OP_READ);
            System.out.println("客户端成功连上服务器,准备发送数据");
            // 开始进行业务处理,向服务端发送数据
            doService(socketChannel);
        }else{
            socketChannel.register(selector,SelectionKey.OP_CONNECT);
        }
    }

    private void doService(SocketChannel socketChannel) throws IOException {
        System.out.println("客户端开始向服务端发送数据:");
        // 向服务器发送数据
        byte[] bytes = "hello nioServer,i am nioClient!".getBytes();
        ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
        writeBuffer.put(bytes);
        writeBuffer.flip();
        socketChannel.write(writeBuffer);
    }
}
  • 服务端
public class NioServer {
    public static void main(String[] args) throws IOException {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        // 开启端口监听
        serverSocketChannel.socket().bind(new InetSocketAddress(8989));

        Selector selector = Selector.open();
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        // 开启selector程序即可
        new Thread(new SingleReactor(selector)).start();
    }

    static class SingleReactor implements Runnable {

        private final Selector selector;

        public SingleReactor(Selector selector) {
            this.selector = selector;
        }

        @Override
        public void run() {
            // 开启多路复用程序
            while (true){
                // 检测
                try {
                    int select = selector.select();
                    Set<SelectionKey> selectionKeys = selector.selectedKeys();
                    Iterator<SelectionKey> iterator = selectionKeys.iterator();
                    while (iterator.hasNext()){
                        SelectionKey selectionKey = iterator.next();
                        iterator.remove();
                        processSelectedKey(selectionKey);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        private void processSelectedKey(SelectionKey selectionKey) throws IOException {
            if (selectionKey.isValid()){
                // 按照事件类型进行区分
                if (selectionKey.isAcceptable()){
                    ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector,SelectionKey.OP_READ);
                }
            }
            // 有数据
            if (selectionKey.isReadable()){
                SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                socketChannel.read(buffer);

                // 从buffer中拿到数据
                buffer.flip();
                byte[] bytes = new byte[buffer.remaining()];
                buffer.get(bytes);
                String msg = new String(bytes, Charset.defaultCharset());
                System.out.println("收到了来自客户端的数据:"+msg);

                buffer.clear();
                buffer.put("hello client,i am server".getBytes(StandardCharsets.UTF_8));
                buffer.flip();
                socketChannel.write(buffer);
            }
        }
    }
}

JAVA AIO模型

什么是AIO

在NIO中,Selector多路复用器在做轮询时,如果没有事件发生,也会进行阻塞,如何优化?

AIO是asynchronous I/O的简称,是异步非阻塞IO,该异步IO是需要依赖于操作系统底层的异步IO实现。

AIO的基本流程

用户线程通过系统调用,告知kernel内核启动某个IO操作,用户线程返回。kernel内核在整个IO操作(包括数据准备、数据复制)完成后,通知用户程序,用户执行后续的业务操作。 image.png

AIO不足之处

  • windows下是实现成熟,但很少作为百万级以上或者说高并发应用的服务器操作系统来使用。
  • Linux系统下,异步IO模型在2.6版本才引入,目前并不完善。所以Linux下,实现高并发编程时都是以NIO多路复用模型模式为主。

image.png