NIO与Netty编程(二)之BIO和NIO编程

1、BIO(Blocking I/O)编程

       BIO 有的称之为basic(基本) IO,有的称之为block(阻塞) IO,主要应用于文件IO 和网络IO,这里不再说文件IO, 大家对此都非常熟悉,本次课程主要讲解网络IO。
       在JDK1.4 之前,我们建立网络连接的时候只能采用BIO,需要先在服务端启动一个ServerSocket,然后在客户端启动Socket 来对服务端进行通信,默认情况下服务端需要对每个请求建立一个线程等待请求,而客户端发送请求后,先咨询服务端是否有线程响应,如果没有则会一直等待或者遭到拒绝,如果有的话,客户端线程会等待请求结束后才继续执行,这就是阻塞式IO。


接下来通过一个例子复习回顾一下BIO 的基本用法(基于TCP)。

1.1 BIO服务端

package com.zdw.bio;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * Create By zdw on 2019/7/19
 * 该类是BIO的服务端
 */
public class BIOServer {
    public static void main(String[] args) throws Exception {
        //创建一个ServerSocket来监听9999端口
        ServerSocket serverSocket = new ServerSocket(9999);
        while (true){
            //监听客户端,得到连接请求
            Socket socket = serverSocket.accept();//阻塞
            //从连接中取出输入流来获取客户端消息
            InputStream inputStream = socket.getInputStream();//阻塞
            //构建缓冲区
            byte[] bytes = new byte[1024];
            //把输入流中的数据读到缓冲区中
            inputStream.read(bytes);
            //获取客户端的IP地址
            String clientIp = socket.getInetAddress().getHostAddress();
            System.out.println(clientIp+":客户端发送了消息:"+new String(bytes));

            //从连接中取出输出流,并给客户端回应消息
            OutputStream outputStream = socket.getOutputStream();
            outputStream.write("我也是啊".getBytes());

            //关闭连接
            socket.close();
        }
    }
}

上述代码编写了一个服务器端程序,绑定端口号9999,accept 方法用来监听客户端连接,如果没有客户端连接,就一直等待,程序会阻塞到这里。 

1.2 BIO客户端

package com.zdw.bio;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;

/**
 * Create By zdw on 2019/7/19
 * BIO编程的客户端
 */
public class BIOClient {
    public static void main(String[] args) throws IOException {
        while(true){
            //创建连接对象Socket
            Socket socket = new Socket("127.0.0.1",9999);
            //从连接中获取输出流,并且像服务端写消息
            OutputStream outputStream = socket.getOutputStream();
            System.out.println("------------------请输入你的想说的话:");
            Scanner scanner = new Scanner(System.in);
            String msg = scanner.nextLine();//得到输入的消息
            //往连接中写入消息
            outputStream.write(msg.getBytes());

            //从连接中取出输入流,并接收服务端消息
            InputStream inputStream = socket.getInputStream();//阻塞
            //构建缓冲区
            byte[] bytes = new byte[1024];
            //把服务器的回应消息写入到缓冲中
            inputStream.read(bytes);
            System.out.println("对方的回应是:"+new String(bytes));

            //关闭连接
            socket.close();
        }
    }
}

上述代码编写了一个客户端程序,通过9999 端口连接服务器端,getInputStream 方法用来等待服务器端返回数据,如果没有返回,就一直等待,程序会阻塞到这里。

运行效果如下图所示:

从上述可以看出,BIO编程的效率是很低下,它会阻塞,一直等待服务器进行处理,如果服务端的资源没有准备好,那就会一直处于等待中,浪费了资源。

 

2、NIO编程

2.1 概述

       java.nio 全称java non-blocking IO,是指JDK 提供的新API。从JDK1.4 开始,Java 提供了一系列改进的输入/输出的新特性,被统称为NIO(也称为New IO)。新增了许多用于处理输入输出的类,这些类都被放在java.nio 包及子包下,并且对原java.io 包中的很多类进行改写,新增了满足NIO 的功能。

       NIO 和BIO 有着相同的目的和作用,但是它们的实现方式完全不同,BIO 以流的方式处理数据,而NIO 以块的方式处理数据,块I/O 的效率比流I/O 高很多。另外,NIO 是非阻塞式的,这一点跟BIO 也很不相同,使用它可以提供非阻塞式的高伸缩性网络。

       NIO 主要有三大核心部分:Channel(通道),Buffer(缓冲区),,Selector(选择器)。传统的BIO基于字节流和字符流进行操作,而NIO 基于Channel(通道)和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择区)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道。

2.2 文件IO

2.2.1 概念和核心API

       Buffer(缓冲区):实际上一个容器,是一个特殊的数组。缓冲区对象内置了一些机制,能够记录和跟踪缓冲区的状态变化情况。Channel提供从文件、网络读取数据的通道,但是读取和写入的数据都必须经过Buffer,如下图所示:

在NIO 中,Buffer 是一个顶层父类,它是一个抽象类,常用的Buffer 子类有:
        ByteBuffer,          存储字节数据到缓冲区
        ShortBuffer,        存储字符串数据到缓冲区                                                                                                                                  CharBuffer,          存储字符数据到缓冲区
        IntBuffer,              存储整数数据到缓冲区
        LongBuffer,          存储长整型数据到缓冲区
        DoubleBuffer,       存储小数到缓冲区
        FloatBuffer,           存储小数到缓冲区

        对于Java 中的基本数据类型,都有一个Buffer 类型与之相对应,最常用的自然是ByteBuffer 类(二进制数据),该类的主要方法如下所示:
        public abstract ByteBuffer put(byte[] b);                   存储字节数据到缓冲区
        public abstract byte[] get();                                         从缓冲区获得字节数据
        public final byte[] array();                                            把缓冲区数据转换成字节数组
        public static ByteBuffer allocate(int capacity);         设置缓冲区的初始容量
        public static ByteBuffer wrap(byte[] array);              把一个现成的字节数组放到缓冲区中使用
        public final Buffer flip();                                               翻转缓冲区,重置位置到初始位置

 

       通道(Channel):类似于BIO 中的stream(流),例如FileInputStream 对象,用来建立到目标(文件,网络套接字,硬件设备等)的一个连接,但是需要注意:BIO 中的stream 是单向的,例如FileInputStream 对象只能进行读取数据的操作,而NIO 中的通道(Channel)是双向的,既可以用来进行读操作,也可以用来进行写操作。

       常用的Channel 类有:FileChannel、DatagramChannel、ServerSocketChannel 和SocketChannel。FileChannel 用于文件的数据读写,DatagramChannel 用于UDP 的数据读写,ServerSocketChannel 和SocketChannel 用于TCP 的数据读写。

2.2.2 FileChannel

这里我们先讲解FileChannel 类,该类主要用来对本地文件进行IO 操作,主要方法如下所示:
         public int read(ByteBuffer dst) ,从通道读取数据并放到缓冲区中
         public int write(ByteBuffer src) ,把缓冲区的数据写到通道中
         public long transferFrom(ReadableByteChannel src, long position, long count),从目标通道中复制数据到当前通道
         public long transferTo(long position, long count, WritableByteChannel target),把数据从当前通道复制给目标通道

案例:接下来我们通过NIO 实现几个案例,分别演示一下本地文件的读、写和复制操作,并和BIO 做个对比。

1、往本地文件中写数据

package com.zdw.nio;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

/**
 * Create By zdw on 2019/7/19
 */
public class FileChannelTest {
    public static void main(String[] args) {
        writeToLocalFile();
    }

    //把数据写到本地文件中
    public static void writeToLocalFile(){
        String msg = "嗨,你好啊,今天是周五了";
        try {
            //创建本地目录
            String fileFloader = "d:/word/test/";
            File floader = new File(fileFloader);
            if(!floader.exists()){
                floader.mkdirs();
            }
            //创建本地文件
            String filePath = fileFloader+"hello.txt";
            File file = new File(filePath);
            //判断本地文件是否存在
            if(!file.exists()){
                file.createNewFile();//不存在就创建
            }
            //构建本地文件的文件输出流
            FileOutputStream fileOutputStream = new FileOutputStream(file);
            //得到该文件输出流的通道
            FileChannel fileChannel = fileOutputStream.getChannel();
            //构建缓冲区
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            //把消息存储到缓冲区中
            buffer.put(msg.getBytes());
            //翻转缓冲区,重置位置到初始位置
            buffer.flip();
            //把缓冲区的数据写到文件通道中
            fileChannel.write(buffer);
            //释放资源
            fileOutputStream.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

        NIO中的通道是从输出流对象里通过getChannel 方法获取到的,该通道是双向的,既可以读,又可以写。在往通道里写数据之前,必须通过put 方法把数据存到ByteBuffer 中,然后通过通道的write 方法写数据。在write 之前,需要调用flip 方法翻转缓冲区,把内部重置到初始位置,这样在接下来写数据时才能把所有数据写到通道里。

2、从本地文件中读取数据

//把本地文件中的数据读出来
    public static void readFromLocalFile(){
        try {
            //本地文件路径
            String localFilePath = "D:/word/test/hello.txt";
            //得到文件的输入流
            FileInputStream fileInputStream = new FileInputStream(localFilePath);
            //得到本地文件通道
            FileChannel fileChannel = fileInputStream.getChannel();
            //构建缓冲区
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
            //把输入流中数据读到缓冲区中
            fileChannel.read(byteBuffer);
            //直接输出缓冲区中的数据
            System.out.println(new String(byteBuffer.array()));
            //释放资源
            fileInputStream.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

3、复制文件

//复制文件
    public static void copyFile() throws Exception {
        //输出到本地文件路径
        String localFilePath_out = "D:/word/test/out.txt";
        FileOutputStream fileOutputStream = new FileOutputStream(localFilePath_out);

        //要复制的文件路径
        String localFilePath_in = "D:/word/test/hello.txt";
        FileInputStream fileInputStream = new FileInputStream(localFilePath_in);
        //从两个流中取出对应的通道
        FileChannel outputStreamChannel = fileOutputStream.getChannel();//输出文件的通道
        FileChannel inputStreamChannel = fileInputStream.getChannel();//输入文件的通道
        //通过api直接把输入流中的 数据  复制到输出流的文件中
        //outputStreamChannel.transferFrom(inputStreamChannel,0,inputStreamChannel.size());//从哪个通道里面复制对应的数据到目标文件中
        inputStreamChannel.transferTo(0,inputStreamChannel.size(),outputStreamChannel);//把该通道中的数据复制到对应的目录通道中
        //关闭连接
        outputStreamChannel.close();
        inputStreamChannel.close();
    }

 

2.3 网络IO

2.3.1 概述和核心API

       前面在进行文件IO 时用到的FileChannel 并不支持非阻塞操作,学习NIO 主要就是进行网络IO,Java NIO 中的网络通道是非阻塞IO 的实现,基于事件驱动,非常适用于服务器需要维持大量连接,但是数据交换量不大的情况,例如一些即时通信的服务等等....
       在Java 中编写Socket 服务器,通常有以下几种模式:
               一个客户端连接用一个线程,优点:程序编写简单;缺点:如果连接非常多,分配的线程也会非常多,服务器可能会因为资源耗尽而崩溃。
               把每一个客户端连接交给一个拥有固定数量线程的连接池,优点:程序编写相对简单,可以处理大量的连接。缺点:线程的开销非常大,连接如果非常多,排队现象会比较严重。                                                                                                                         使用Java 的NIO,用非阻塞的IO 方式处理。这种模式可以用一个线程,处理大量的客户端连接。

NIO中的核心类和相关API:

1. Selector(选择器),能够检测多个注册的通道上是否有事件发生,如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接。这样使得只有在连接真正有读写事件发生时,才会调用函数来进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程,并且避免了多线程之间的上下文切换导致的开销。

该类的常用方法如下所示:
        public static Selector open(),得到一个选择器对象
        public int select(long timeout),监控所有注册的通道,当其中有IO 操作可以进行时,将对应的SelectionKey 加入到内部集合中并返回,参数用来设置超时时间
        public Set<SelectionKey> selectedKeys(),从内部集合中得到所有的SelectionKey

 

2. SelectionKey,代表了Selector 和网络通道的注册关系,一共四种:
        int OP_ACCEPT:有新的网络连接可以accept,值为16
        int OP_CONNECT:代表连接已经建立,值为8
        int OP_READ 和int OP_WRITE:代表了读、写操作,值为1 和4

该类的常用方法如下所示:
        public abstract Selector selector(),得到与之关联的Selector 对象
        public abstract SelectableChannel channel(),得到与之关联的通道
        public final Object attachment(),得到与之关联的共享数据
        public abstract SelectionKey interestOps(int ops),设置或改变监听事件
        public final boolean isAcceptable(),是否可以accept
        public final boolean isReadable(),是否可以读
        public final boolean isWritable(),是否可以写

3. ServerSocketChannel,用来在服务器端监听新的客户端Socket 连接,常用方法如下所示:
        public static ServerSocketChannel open(),得到一个ServerSocketChannel 通道
        public final ServerSocketChannel bind(SocketAddress local),设置服务器端端口号                                                                  public final SelectableChannel configureBlocking(boolean block),设置阻塞或非阻塞模式,取值false 表示采用非阻塞模式
        public SocketChannel accept(),接受一个连接,返回代表这个连接的通道对象
        public final SelectionKey register(Selector sel, int ops),注册一个选择器并设置监听事件

 

4. SocketChannel,网络IO 通道,具体负责进行读写操作。

NIO 总是把缓冲区的数据写入通道,或者把通道里的数据读到缓冲区。常用方法如下所示:
        public static SocketChannel open(),得到一个SocketChannel 通道
        public final SelectableChannel configureBlocking(boolean block),设置阻塞或非阻塞模式,取值false 表示采用非阻塞模式
        public boolean connect(SocketAddress remote),连接服务器
        public boolean finishConnect(),如果上面的方法连接失败,接下来就要通过该方法完成连接操作
        public int write(ByteBuffer src),往通道里写数据
        public int read(ByteBuffer dst),从通道里读数据
        public final SelectionKey register(Selector sel, int ops, Object att),注册一个选择器并设置监听事件,最后一个参数可以设置共享数据
        public final void close(),关闭通道

 

流程示意图

 

2.3.2 入门案例

API 学习完毕后,接下来我们使用NIO 开发一个入门案例,实现服务器端和客户端之间的数据通信(非阻塞)。

 

NIO的服务端

package com.zdw.nio;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

/**
 * Create By zdw on 2019/7/22
 * NIO的网络服务端
 */
public class NIOServer {
    public static void main(String[] args) throws Exception{
        //1\创建用来在服务器端监听新的客户端Socket 连接
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //2\创建一个监听事件的选择器
        Selector selector = Selector.open();

        //3\绑定一个端口号
        serverSocketChannel.bind(new InetSocketAddress(9999));
        //4\设置为非阻塞方式
        serverSocketChannel.configureBlocking(false);
        //5\把ServerSocketChannel对象注册给Selector,并设置监听事件为 连接事件
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        //6\业务操作
        while(true){
            //6.1\监控所有注册的通道,当其中有IO 操作可以进行时,将对应的SelectionKey 加入到内部集合中并返回,参数用来设置超时时间
            int select = selector.select(2000);//该方法的返回值是监控到的客户端的个数
            if(select==0){//说明没有客户端连接,这个时候nio的优势体现出来了,我们可以干点别的事
                System.out.println("Server:没有客户端来连接我,我没事干,就可以在这里做点想做的事情了");
                continue;//继续下一次循环
            }
            //6.2\得到所有的SelectionKey,得到通道里面的事件
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
            while (keyIterator.hasNext()){
                //遍历每一个SelectionKey。判断通道里面的事件
                SelectionKey selectionKey = keyIterator.next();
                if(selectionKey.isAcceptable()){//判断是不是连接事件
                    System.out.println("OP_ACCEPT");
                    //接受客户端连接,得到通道
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    //设置客户端通道为非阻塞方式
                    socketChannel.configureBlocking(false);
                    //把客户端通道注册到选择器,并设置监听事件为 读数据 事件,设置共享的数据(只是一个空的ByteBuffer)
                    socketChannel.register(selector,SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                }
                if(selectionKey.isReadable()){//判断是不是读取客户端数据事件
                    System.out.println("OP_READ");
                    SocketChannel socketChannel = (SocketChannel) selectionKey.channel();//得到通道
                    //得到客户端的共享数据
                    ByteBuffer byteBuffer = (ByteBuffer) selectionKey.attachment();
                    //把共享数据读取到通道中
                    socketChannel.read(byteBuffer);
                    System.out.println("客户端发来数据:"+new String(byteBuffer.array()));
                }
            }
            //6.3\手动从当前的集合移除key,避免重复处理
            keyIterator.remove();
        }
    }
}

上面代码用NIO 实现了一个服务器端程序,能不断接受客户端连接并读取客户端发过来的数据。

 

NIO客户端

package com.zdw.nio;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

/**
 * Create By zdw on 2019/7/22
 * NIO的网络客户端
 */
public class NIOClient {
    public static void main(String[] args) throws Exception {
        //1、得到一个网络通道
        SocketChannel socketChannel = SocketChannel.open();
        //2、设置为非阻塞方式
        socketChannel.configureBlocking(false);
        //3、得到服务端的IP地址和端口号
        InetSocketAddress address = new InetSocketAddress("127.0.0.1",9999);
        //4、连接服务器端
        if(!socketChannel.connect(address)){//表示没有连接上
            while(!socketChannel.finishConnect()){//finishConnect表示再次连接,如果没有连接上,那么就可以在这里处理一些其他的业务逻辑
                System.out.println("Client连接服务端的同时,我还可以处理一些其他的事情");
            }
        }
        //向缓冲区中存入要发送的数据
        String msg = "Hello,NIOServer";
        ByteBuffer byteBuffer = ByteBuffer.wrap(msg.getBytes());
        //发送数据
        socketChannel.write(byteBuffer);
        //这是为了客户端连接不断开
        System.in.read();
    }
}

上面代码通过NIO 实现了一个客户端程序,连接上服务器端后发送了一条数据。演示效果如下:

 

如果我们只启动客户端,也可以看到客户端的非阻塞效果,不过由于我们没有进行异常处理,所以会出现异常,效果如下:

 

2.3.3 网络聊天案例

       刚才我们通过NIO 实现了一个入门案例,基本了解了NIO 的工作方式和运行流程,接下来我们用NIO 实现一个多人聊天案例,具体代码如下所示:

服务端ChatServer

package com.zdw.chat;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Iterator;

/**
 * Create By zdw on 2019/7/22
 * 网络聊天案例的服务端
 */
public class ChatServer {
    private ServerSocketChannel serverSocketChannel;//监听通道,老大
    private Selector selector;//选择器对象
    private static final int PORT=9999;//监听的端口号

    public ChatServer(){//构造方法,得到一些必需的对象
        try{
            //1、得到监听通道对象
            serverSocketChannel = ServerSocketChannel.open();
            //2、得到选择器对象
            selector = Selector.open();
            //3、设置为非阻塞方式
            serverSocketChannel.configureBlocking(false);
            //4、绑定端口
            serverSocketChannel.bind(new InetSocketAddress(PORT));
            //5、将监听通道注册到选择器,并设置为监听连接事件
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            //调用方法,打印日志信息
            printInfo("Chat Server is Ready.........");
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    //6、业务逻辑处理的方法
    public void start() throws Exception{
        while (true){
            //6.1、不停的监听客户端通道,看是否有连接,如果有连接就进行业务处理,否则就干点别的事情
            int select = selector.select(2000);//2秒钟就监听一次,得到的是连接个数
            if(select==0){
                System.out.println("Server:没有客户端找我, 我就干别的事情");
                continue;
            }
            //6.2、得到所有的SelectionKey,得到通道里面的事件
            Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
            while (keyIterator.hasNext()){
                //得到下一个连接关系,进行业务处理
                SelectionKey selectionKey = keyIterator.next();
                if(selectionKey.isAcceptable()){//如果是连接请求事件
                    //接收连接请求,得到通道
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    //设置为非阻塞
                    socketChannel.configureBlocking(false);
                    //把通道注册到选择器中,并设置监听事件为 读数据 事件
                    socketChannel.register(selector,SelectionKey.OP_READ);
                    //打印提示信息,参照QQ的好友上线功能,提示的是客户端的IP和端口号
                    System.out.println(socketChannel.getRemoteAddress().toString().substring(1)+":上线了");
                }
                if(selectionKey.isReadable()){//如果是读数据事件
                    //读取客户端发送来的消息,并把接收到的消息广播到其他客户端,其他客户端都能收到消息
                    SocketChannel socketChannel = (SocketChannel)selectionKey.channel();//得到客户端通道
                    //定义缓冲区
                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    //把客户端消息读到缓冲区中
                    int count = socketChannel.read(byteBuffer);//返回的是读到数据的长度
                    if(count>0){//读到了数据
                        //把数据转成字符串
                        String msg = new String(byteBuffer.array());
                        //控制台打印消息
                        printInfo(msg);

                        //广播消息到其他客户端
                        System.out.println("服务器端广播了消息.........");
                        //得到客户端所有的通道,判断客户端的通道不是发送消息给服务端的通道
                        for(SelectionKey key : selector.keys()){
                            Channel targetChannel = key.channel();//得到客户端的通道
                            //判断该客户端通道类型是不是SocketChannel,并且该客户端不是发送消息的客户端
                            if(targetChannel instanceof SocketChannel && targetChannel!=socketChannel){
                                SocketChannel destChannel = (SocketChannel) targetChannel;
                                //把消息放到缓冲区中
                                ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
                                //把缓冲区数据写进通道中
                                destChannel.write(buffer);
                            }
                        }
                    }
                }
                //6.3处理完本次的连接关系之后,一定要把key从集合中移除,避免重复处理
                keyIterator.remove();
            }
        }
    }

    private void printInfo(String str) { //往控制台打印消息
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println("[" + sdf.format(new Date()) + "] -> " + str);
    }

    //main方法用来启动服务器端
    public static void main(String[] args) throws Exception{
        new ChatServer().start();
    }
}

上述代码通过NIO 编写了一个聊天程序的服务端,可以接受客户端发来的数据,并能把数据广播给其他客户端。

客户端ChatClient

package com.zdw.chat;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

/**
 * Create By zdw on 2019/7/22
 * 网络聊天客户端程序
 */
public class ChatClient {
    private SocketChannel socketChannel;//客户端网络通道
    private static final String ADDRESS = "127.0.0.1";//服务器端IP
    private static final int PORT=9999;//服务器端端口号
    private String username;//客户端的聊天用户名

    public ChatClient() throws Exception {//构造方法中得到一些必须的对象
        //1、得到客户端通道
        socketChannel = SocketChannel.open();
        //2、设置为非阻塞方法
        socketChannel.configureBlocking(false);
        //3、提供服务器端的IP地址和端口号
        InetSocketAddress address = new InetSocketAddress(ADDRESS,PORT);
        //4、连接服务器端,这里会不停的请求服务器端,是非阻塞的,当连接不上的时候可以做点其他的事情
        if(!socketChannel.connect(address)){
            //如果没有连接上,就需要不断的重新调用finishConnect进行连接
            while(!socketChannel.finishConnect()){
                System.out.println("Client客户端在连接过程中,我可以干点别的事情");
            }
        }
        //5、得到客户端的IP地址和端口号,作为聊天用户名
        username = socketChannel.getLocalAddress().toString().substring(1);
        System.out.println("----------Client("+username+") is Ready-----------");
    }

    //向服务器端发送数据
    public void sendMsgToServer(String msg) throws Exception{
        //假设当客户端发送的消息是bye的时候,就关闭当前的客户端连接
        if("bye".equalsIgnoreCase(msg)){
            socketChannel.close();
            return;
        }
        //向服务器端发送消息,拼装消息
        msg = username+"说了:"+msg;
        //发送数据
        socketChannel.write(ByteBuffer.wrap(msg.getBytes()));
    }

    //从服务器端接收收取
    public void receiveMsgFromServer() throws Exception {
        //得到缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        //把服务器端返回的数据读到缓冲区
        int read = socketChannel.read(byteBuffer);
        if(read>0){
            //打印消息到控制台
            System.out.println(new String(byteBuffer.array()).trim());
        }
    }
}

上述代码通过NIO 编写了一个聊天程序的客户端,可以向服务器端发送数据,并能接收服务器广播的数据。 

客户端启动类ChatClientStart

package com.zdw.chat;

import java.util.Scanner;

/**
 * Create By zdw on 2019/7/22
 * 聊天程序客户端启动类,启用多线程进行接收服务端消息
 */
public class ChatClientStart {
    public static void main(String[] args) throws Exception {
        ChatClient chatClient = new ChatClient();
        //开启线程,接收客户端消息
        new Thread(){
            @Override
            public void run() {
                while(true){
                    try{
                        chatClient.receiveMsgFromServer();
                        Thread.sleep(2000);
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                }
            }
        }.start();

        //控制台录入消息
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNextLine()){
            String msg=scanner.nextLine();
            chatClient.sendMsgToServer(msg);//发送消息到服务器端
        }
    }
}

上述代码运行了聊天程序的客户端,并在主线程中发送数据,在另一个线程中不断接收服务器端的广播数据,该代码运行一次就是一个聊天客户端,可以同时运行多个聊天客户端,聊天效果如下图所示:

服务端:

客户端01: 

客户端02:

 

客户端03:

2.4 AIO编程

       JDK 7 引入了Asynchronous I/O,即AIO。在进行I/O 编程中,常用到两种模式:Reactor和Proactor。Java 的NIO 就是Reactor,当有事件触发时,服务器端得到通知,进行相应的处理。
       AIO 即NIO2.0,叫做异步不阻塞的IO。AIO 引入异步通道的概念,采用了Proactor 模式,简化了程序编写,一个有效的请求才启动一个线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。                                                                                                                                                                                                   目前AIO 还没有广泛应用,并且也不是本课程的重点内容,这里暂不做讲解。

 

2.5 IO 对比总结

IO 的方式通常分为几种:同步阻塞的BIO、同步非阻塞的NIO、异步非阻塞的AIO。
        BIO 方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4 以前的唯一选择,但程序直观简单易理解。
        NIO 方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4 开始支持。
        AIO 方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS 参与并发操作,编程比较复杂,JDK7 开始支持。


举个例子再理解一下:
        同步阻塞:你到饭馆点餐,然后在那等着,啥都干不了,饭馆没做好,你就必须等着!
        同步非阻塞:你在饭馆点完餐,就去玩儿了。不过玩一会儿,就回饭馆问一声:好了没啊!
        异步非阻塞:饭馆打电话说,我们知道您的位置,一会给你送过来,安心玩儿就可以了,类似于现在的外卖。

对比总结BIONIOAIO
IO方式同步阻塞同步非阻塞(多路复用)异步非阻塞
API使用难度简单复杂复杂
可靠性
吞吐量
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值