Java NIO

1.NIO概述

①.NIO: New IO / 非阻塞式IO;

②.Java NIO从Java 1.4开始引入.用来代替传统的IO;

2.Java NIO 和 Java 传统IO的区别

2.1.共同点

无论是NIO还是传统的IO他们的目的都是一样的,都是用来进行数据传输的!

2.2.不同点

1>.传统IO

①.面向流,单向的;
②.阻塞IO;

2>.NIO

①.面向缓冲区,双向的;
②.非阻塞IO;
③.选择器(Selector)

3.通道和缓冲区(Buffer)

Java NIO的核心在于:通道(Channel) 和 缓冲区(Buffer)

3.1.缓冲区
3.1.1.概述

1>.在Java NIO中负责数据的存储,缓冲区的底层是数组,用于存储不同数据类型的数据;
2>.在Java NIO中缓冲区主要用于与通道进行交互,数据是从通道读入缓冲区,从缓冲区写入通道;

如图:
在这里插入图片描述

3.1.2.实例
/**
 * 一.缓冲区(buffer) : 在Java NIO中负责数据的存取,缓冲区的底层是数组,用于存储不同数据类型的数据
 *	1.根据数据类型的不同(boolean除外),提供了对应类型的缓冲区:
 *		ByteBuffer (最常用)
 *		CharBuffer
 *		ShortBuffer
 *		IntBuffer
 *		LongBuffer
 *		FloatBuffer
 *		DoubleBuffer
 *
 *	上述缓冲区的管理方式几乎是一样的,通过 "具体类型的缓冲区.allocate()" 方法获取缓冲区
 *
 *	二.缓冲区存取数据的两个核心方法:
 *		put(): 存入数据到缓冲区
 *		get(): 获取缓冲区中的数据
 *
 *
 *	三.缓冲区中的四个核心属性:
 *		mark: 标记,表示记录当前操作的position的位置,可以通过reset()方法恢复到上一次mark标记的position的位置
 *    		position: 位置,表示缓存区中正在操作数据的位置/下标/索引,从0开始
 *   		limit: 界限,表示缓冲区中可以操作数据的大小(limit后面的数据是不能读写的)
 *   		capacity: 容量,表示缓冲区中最大存储数据的容量/最多能存储多少数据,一旦声明不能改变!!!
 *
 *   0<= mark <= position <= limit <= capacity
 */
public class TestBuffer {

    @Test
    public void test2(){
        String str="abcde";

        ByteBuffer buf=ByteBuffer.allocate(1024);

        buf.put(str.getBytes());

        buf.flip();

        byte[] dst=new byte[1024];

        buf.get(dst,0,2);

        System.out.println(new String(dst,0,2));

        System.out.println("---------------get()---------------");
        System.out.println("位置:"+buf.position()); //2
        System.out.println("界限:"+buf.limit());	 //	5
        System.out.println("容量:"+buf.capacity()); //1024

        // 标记
        buf.mark();

        buf.get(dst,2,2);
        System.out.println(new String(dst,2,2));

        System.out.println("---------------get2()---------------");
        System.out.println("位置:"+buf.position()); //4
        System.out.println("界限:"+buf.limit());	 //	5
        System.out.println("容量:"+buf.capacity()); //1024

        // 标记
        buf.mark();

        buf.get(dst,4,1);
        System.out.println(new String(dst,4,1));

        System.out.println("---------------get3()---------------");
        System.out.println("位置:"+buf.position()); //5
        System.out.println("界限:"+buf.limit());	 //	5
        System.out.println("容量:"+buf.capacity()); //1024

        // reset() 恢复到上一次mark标记的位置
        buf.reset();

        System.out.println("---------------reset()---------------");
        System.out.println("位置:"+buf.position()); //4
        System.out.println("界限:"+buf.limit());	 //	5
        System.out.println("容量:"+buf.capacity()); //1024

        // 判断缓存区中是否还有可操作的数据
        if (buf.hasRemaining()) {

            // 缓冲区中还有可以操作的数据的数量
            System.out.println("缓冲区中还有可以操作的数据的数量:"+buf.remaining()); //1
        }
    }

    @Test
    public void test1(){

        // 分配一个制定大小的byte类型的缓冲区
        ByteBuffer buf=ByteBuffer.allocate(1024);

        System.out.println("---------------allocate()---------------");
        System.out.println("位置:"+buf.position()); //0
        System.out.println("界限:"+buf.limit());	 //	1024
        System.out.println("容量:"+buf.capacity()); //1024

        //存储数据
        String str="abcd";
        buf.put(str.getBytes());

        System.out.println("---------------put()---------------");
        System.out.println("位置:"+buf.position()); //4
        System.out.println("界限:"+buf.limit());   // 1024
        System.out.println("容量:"+buf.capacity()); //1024

        // 读取数据之前切换到读取模式
        buf.flip();
        System.out.println("---------------flip()---------------");
        System.out.println("位置:"+buf.position()); //0
        System.out.println("界限:"+buf.limit());  //4
        System.out.println("容量:"+buf.capacity());  //1024

        // 读取缓冲区中的数据
        byte[] dst= new byte[buf.limit()];
        buf.get(dst);

        System.out.println(new String(dst,0,dst.length));

        System.out.println("---------------get()---------------");
        System.out.println("位置:"+buf.position()); //4
        System.out.println("界限:"+buf.limit());  //4
        System.out.println("容量:"+buf.capacity());  //1024

        // rewind()方法,重新回到读取数据的模式.可以重复读取数据
        buf.rewind();
        System.out.println("---------------rewind()---------------");
        System.out.println("位置:"+buf.position()); //0
        System.out.println("界限:"+buf.limit());  //4
        System.out.println("容量:"+buf.capacity());  //1024

        // clear() 清空缓冲区,回到最初的allocate状态/模式,但是缓冲区中的数据依然存在,只是那些数据处于'被遗忘状态'
        buf.clear();
        System.out.println("---------------clear()---------------");
        System.out.println("位置:"+buf.position()); //0
        System.out.println("界限:"+buf.limit());  //1024
        System.out.println("容量:"+buf.capacity());  //1024
    }
}
3.2.直接缓冲区与非直接缓冲区
3.2.1.直接缓冲区

①.通过allocateDirect()方法获取.缓冲区建立在操作系统的物理内存中;
注意:虽然直接缓冲区能够提升性能,但是太浪费系统的资源了,不到万不得已不要使用!

3.2.2.非直接缓冲区

①.通过allocate()方法创建,缓冲区建立在jvm的内存中

3.3.通道(Channel)
3.3.1.概述

1>.通道表示打开到IO设备(如.文件,套接字等等)的连接;
2>.如果要使用NIO,首先要获取到用于连接IO设备的通道以及用于容纳数据的缓冲区.然后操作缓冲区,对数据进行处理;
3>.通道本身并不存储数据,他需要配合缓冲区才能进行传输;

3.3.2.实例
/**
 * 一.通道(Channel): 用于源节点和目标节点的链接,在Java NIO中负责缓冲区中数据的传输
 * 		通道本身并不存储数据,他需要配合缓冲区才能进行传输
 *
 * 二.通道的主实现类:
 * 	Java.nio.channel.Channel接口:
 * 		|--FileChannel  // 本地数据传输
 * 		|--SocketChannel // 网络数据传输
 * 		|--ServerSocketChannel // 网络数据传输
 *      |--DatagramChannel // 网络数据传输
 *
 * 三.获取通道
 * 	1.Java针对支持通道的类提供了getChannel()方法:
 * 		本地IO:
 * 			FileInputStream/FileOutputStream
 *			RandomAccessFile
 *
 *		网络IO:
 *			Socket
 *			ServerSocket
 *			DatagramSocket
 *
 *	2.JDK1.7中的NIO 2 针对各个通道提供了一个静态方法open()
 *
 *  3.JDK1.7中的NIO 2 的files工具类的newByteChannel()
 *
 * 四.通道之间的数据传输:
 * 	1.filechannel.transferForm()
 * 	2.filechannel.transferTo()
 *
 * 五.分散读取和聚集写入
 * 	1.分散读取(scatter reads): 将通道中的数据分散到多个缓冲区中
 *	2.聚集写入(Gather Writes): 将多个缓冲区中的数据聚集到通道中
 */
public class TestChannel {
    // 分散和聚集
    @Test
    public void test3() throws IOException{
        // 源文件
        RandomAccessFile raf=new RandomAccessFile("1.txt", "rw");

        // 1.获取通道
        FileChannel channel=raf.getChannel();

        // 2.分配多个指定大小的缓冲区
        ByteBuffer buffer1=ByteBuffer.allocate(100);
        ByteBuffer buffer2=ByteBuffer.allocate(1024);

        // 3.分散读取到多个缓冲区中
        ByteBuffer[] buffers={buffer1,buffer2};

        channel.read(buffers);

        // 读取缓冲区中的内容
        for (ByteBuffer byteBuffer : buffers) {
            // 将每一个缓冲区变成读模式
            byteBuffer.flip();
        }
        // 读取第一个缓冲区的内容
        System.out.println(new String(buffers[0].array(),0,buffers[0].limit()));
        System.out.println("---------------------------------");
        // 读取第二个缓冲区的内容
        System.out.println(new String(buffers[1].array(),0,buffers[1].limit()));

        // 4.聚集写入
        // 目标文件
        RandomAccessFile raf1=new RandomAccessFile("2.txt", "rw");
        FileChannel channel2=raf1.getChannel();
        channel2.write(buffers);

        // 5.关闭通道和缓冲区
        channel.close();
        channel2.close();
        raf.close();
        raf1.close();
    }


    // 通道之间的数据传输(直接缓冲区)完成文件的复制
    @Test
    public void test2() throws IOException{
        // 源节点
        FileChannel inChannel=FileChannel.open(Paths.get("f:/1.jpg"), StandardOpenOption.READ);

        // 目标节点
        FileChannel outChannel=FileChannel.open(Paths.get("f:/2.jpg"), StandardOpenOption.READ,StandardOpenOption.WRITE,StandardOpenOption.CREATE);

        // 方式一(记忆方式:从源节点到目标节点)
        inChannel.transferTo(0, inChannel.size(), outChannel);

        // 方式二(记忆方式:目标节点从源节点来的)
        //outChannel.transferFrom(inChannel, 0, inChannel.size());

        // 关闭通道
        outChannel.close();
        inChannel.close();
    }


    // 利用通道完成文件的复制(非直接缓冲区)
    @Test
    public void test1(){
        FileInputStream fis=null;
        FileOutputStream fos=null;
        FileChannel inChannel=null;
        FileChannel outChannel=null;

        try {
            fis = new FileInputStream("3.jpg");

            fos = new FileOutputStream("4.jpg");

            // 获取通道
            inChannel =fis.getChannel();
            outChannel =fos.getChannel();

            // 分配一个指定大小的缓冲区
            ByteBuffer buffer=ByteBuffer.allocate(1024);

            // 将通道中的的数据存入缓冲区
            while(inChannel.read(buffer)!=-1){
                // 切换成读数据模式
                buffer.flip();

                // 将缓冲区中的数据写入通道进行数据的传输
                outChannel.write(buffer);

                // 清空缓冲区
                buffer.clear();
            }

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (outChannel!=null) {
                try {
                    outChannel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            if (inChannel!=null) {
                try {
                    inChannel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            if (fos!=null) {
                try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            if (fis!=null) {
                try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

4.阻塞和非阻塞

主要表现在网络通信/网络IO上

4.1.阻塞式
4.1.1.概述

客户端发送一个读/写请求到服务器端,如果服务器端不能判断客户端发送的这个请求中的数据真实有效时或者不能完全确定该请求中有数据的时候,那么这个时候服务器端的这个线程会一直处于阻塞状态,也就是服务器端会等待客户端什么时候发送过来有数据了,服务器端才开始工作,但是在等待这个过程中服务器端的这个线程是任何事情都做不了的;

扩展:服务器端如何判断客户端请求的数据是否有效/是否有数据?

服务端会判断内核地址空间中是否有数据,如果没有数据,那么服务端就等待,线程处于阻塞状态,等什么时候内核地址空间中有数据了,什么时候就把内核地址空间中的数据copy到用户地址空间中,然后读到程序中进行处理

4.1.2.实例
/**
 * 阻塞式
 */
public class TestBlockingNIO {

    // 客户端(先打开服务端后开客户端)
    @Test
    public void client01() throws IOException{
        // 获取通道
        SocketChannel socketChannel=SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));

        // 分配指定大小的缓冲区
        ByteBuffer byteBuffer=ByteBuffer.allocate(1024);

        // 通道
        FileChannel inChannel=FileChannel.open(Paths.get("1.txt"), StandardOpenOption.READ);

        // 读取本地文件到缓冲区中并发送到服务器端
        while(inChannel.read(byteBuffer)!=-1){
            // 缓冲区开启读模式
            byteBuffer.flip();

            // 将缓冲区中的数据写到通道中传输到服务器端
            socketChannel.write(byteBuffer);

            byteBuffer.clear();
        }

        // 关闭通道
        inChannel.close();
        socketChannel.close();
    }

    // 服务器端(先打开服务端后开客户端)
    @Test
    public void server01() throws IOException{
        // 获取通道
        ServerSocketChannel serverSocketChannel=ServerSocketChannel.open();

        // 绑定连接端口号
        serverSocketChannel.bind(new InetSocketAddress(9898));

        // 获取客户端连接的那个通道
        SocketChannel socketChannel=serverSocketChannel.accept();

        // 接受客户端的数据并保存到本地
        FileChannel targetChannel=FileChannel.open(Paths.get("2.txt"),StandardOpenOption.WRITE,StandardOpenOption.CREATE);

        ByteBuffer buffer=ByteBuffer.allocate(1024);

        while(socketChannel.read(buffer)!=-1){
            buffer.flip();

            // 将缓冲区中的数据保存到本地
            targetChannel.write(buffer);

            buffer.clear();
        }

        // 关闭通道
        socketChannel.close();
        targetChannel.close();
        serverSocketChannel.close();
    }
}
4.2.非阻塞式
4.2.1.概述

在客户端和服务器端之间加入一个Selector(选择器),该Selector会将每一个用于数据传输的通道都注册到该Selector中,该Selector会监控每一个通道的IO状况(如读,写,连接,接收数据的状况等),当某一个通道上的某一个请求的事件/数据完全准备就绪时,Selector才会将这个任务分配到服务器端的一个或者多个线程上去运行;如果客户端的请求没有准备就绪时,服务器端的线程可以做任何其他事情,依然可以完成各自的任务,这样就能更大程度地利用CPU资源;

4.2.2.实例
/**
 * 非阻塞式
 *
 * 一.使用NIO完成网络通信的三个核心:
 *  1.通道(Channel) : 负责连接
 *
 *  2.缓冲区(buffer) :  负责数据的存取
 *
 *  3.选择器(Selector) : 监控注册到该Selector中的每个用于数据传输的通道的IO状况
 */
public class TestNonBlockingNIO {

    // 客户端(可以启动多个)
    @Test
    public void client() throws IOException{
        // 获取通道
        SocketChannel socketChannel=SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));

        // 切换成非阻塞模式
        socketChannel.configureBlocking(false);

        // 分配缓冲区
        ByteBuffer buffer=ByteBuffer.allocate(1024);

        // 往缓冲区中添加内容(可以在这里变成接收用户的输入,模拟qq聊天,然后将内容添加到缓存中)
        Scanner scanner=new Scanner(System.in);

        while(scanner.hasNext()){
            String str=scanner.nextLine();

            buffer.put((new Date().toString()+"\n"+str).getBytes());

            // 发送数据给服务器端
            buffer.flip();
            socketChannel.write(buffer);
            buffer.clear();
        }

        // 关闭通道
        socketChannel.close();
    }

    // 服务端(可以将这个方法写成一个工具类)
    @Test
    public void server() throws IOException{
        // 获取通道
        ServerSocketChannel serverSocketChannel=ServerSocketChannel.open();

        // 切换成非阻塞模式
        serverSocketChannel.configureBlocking(false);

        // 绑定端口
        serverSocketChannel.bind(new InetSocketAddress(9898));

        // 获取选择器Selector
        Selector selector=Selector.open();

        // 将通道注册到Selector中,并且指定监听事件,监听接收事件
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        // 轮询式的获取Selector中已经'准备就绪'的事件
        while(selector.select()>0){
            // 获取当前Selector中所有注册的"选择键(/已就绪的监听事件)"
            Iterator<SelectionKey> iterator=selector.selectedKeys().iterator();

            while(iterator.hasNext()){
                // 获取"准备就绪"的事件
                SelectionKey selectionKey=iterator.next();

                // 判断具体是什么事件准备就绪
                if (selectionKey.isAcceptable()) {
                    // 如果是接收事件就绪,那么就获取客户端socketChannel
                    SocketChannel socketChannel=serverSocketChannel.accept();

                    // 将客户端socketchannel切换成非阻塞模式
                    socketChannel.configureBlocking(false);

                    // 将该通道注册到Selector中
                    socketChannel.register(selector, selectionKey.OP_READ);
                }else if (selectionKey.isReadable()) {
                    // 获取当前选择器上'读就绪'状态的通道
                    SocketChannel channel=(SocketChannel) selectionKey.channel();

                    // 读取客户端传递过来的数据
                    ByteBuffer buffer=ByteBuffer.allocate(1024);

                    int len=0;

                    while((len=channel.read(buffer))>0){
                        buffer.flip();

                        System.out.println(new String(buffer.array(),0,len));

                        buffer.clear();
                    }
                }

                // 取消选择键selectionKey
                iterator.remove();
            }
        }
    }
}

5.管道(Pipe)

5.1.概述

1>.Java NIO中的管道是2个线程之间的单向数据连接;
2>.Pipe有一个source通道和一个sinke通道.数据会被写到sink通道中.然后从source通道中读取;

5.2.实例
public class TestPipe {

    @Test
    public void test1() throws IOException{
        // 获取管道
        Pipe pipe=Pipe.open();

        // 将缓冲区的数据写入管道
        SinkChannel sinkChannel=pipe.sink();

        // 分配缓冲区
        ByteBuffer buffer=ByteBuffer.allocate(1024);
        buffer.put("通过单向管道发送数据".getBytes());
        buffer.flip();

        sinkChannel.write(buffer);

        // 读取缓冲区中的数据
        SourceChannel sourceChannel=pipe.source();
        buffer.flip();

        int len=sourceChannel.read(buffer);
        System.out.println(new String(buffer.array(),0,len));

        sourceChannel.close();
        sinkChannel.close();
    }
}

6.案例:基于Java NIO实现一个聊天室

6.1.需求

①.客户端通过Java NIO连接到服务端,支持多客户端的连接;

②.客户端初次连接时,服务端提示输入昵称,如果昵称已经有人使用,提示重新输入,如果昵称唯一,则登录成功,之后发送消息都需要按照规定格式带着昵称发送消息;

③.客户端登录后,发送已经设置好的欢迎信息和在线人数给客户端,并且通知其他客户端该客户端上线;

④.服务器收到已登录客户端输入内容,转发至其他登录客户端;

⑥.客户端下线检测:客户端在线的时候发送心跳,服务端用TimeCacheMap自动删除过期对象,同时通知线上用户删掉的用户下线;

6.2.代码实现
6.2.1.服务端
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.Channel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

/**
 * 网络多客户端聊天室: 服务端
 */
public class ChatRoomServer {

    private Selector selector = null;
    static final int port = 8080;
    private Charset charset = Charset.forName("UTF-8");
    //用来记录在线人数,以及昵称
    private static HashSet<String> users = new HashSet<String>();

    private static String USER_EXIST = "user exist,please change a name";
    //相当于自定义协议格式,与客户端协商好
    private static String USER_CONTENT_SPLIT = "@:\t";

    private static boolean flag = false;

    public void init() throws IOException {
        selector = Selector.open();
        ServerSocketChannel server = ServerSocketChannel.open();
        server.bind(new InetSocketAddress(port));
        //非阻塞的方式
        server.configureBlocking(false);
        //注册到选择器上,设置为监听状态
        server.register(selector, SelectionKey.OP_ACCEPT);

        System.out.println("Server is listening now...");

        while (true) {
            int readyChannels = selector.select();
            if (readyChannels == 0) continue;
            Set selectedKeys = selector.selectedKeys();  //可以通过这个方法,知道可用通道的集合
            Iterator keyIterator = selectedKeys.iterator();
            while (keyIterator.hasNext()) {
                SelectionKey sk = (SelectionKey) keyIterator.next();
                keyIterator.remove();
                dealWithSelectionKey(server, sk);
            }
        }
    }

    public void dealWithSelectionKey(ServerSocketChannel server, SelectionKey sk) throws IOException {
        if (sk.isAcceptable()) {
            SocketChannel sc = server.accept();
            //非阻塞模式
            sc.configureBlocking(false);
            //注册选择器,并设置为读取模式,收到一个连接请求,然后起一个SocketChannel,并注册到selector上,之后这个连接的数据,就由这个SocketChannel处理
            sc.register(selector, SelectionKey.OP_READ);

            //将此对应的channel设置为准备接受其他客户端请求
            sk.interestOps(SelectionKey.OP_ACCEPT);
            System.out.println("Server is listening from client :" + sc.getRemoteAddress());
            sc.write(charset.encode("客户端输入一个昵称:"));
        }
        //处理来自客户端的数据读取请求
        if (sk.isReadable()) {
            //返回该SelectionKey对应的 Channel,其中有数据需要读取
            SocketChannel sc = (SocketChannel) sk.channel();
            ByteBuffer buff = ByteBuffer.allocate(1024);
            StringBuilder content = new StringBuilder();
            try {
                while (sc.read(buff) > 0) {
                    buff.flip();
                    content.append(charset.decode(buff));

                }
                System.out.println("Server is listening from client " + sc.getRemoteAddress() + " data rev is: " + content);
                //将此对应的channel设置为准备下一次接受数据
                sk.interestOps(SelectionKey.OP_READ);
            } catch (IOException io) {
                sk.cancel();
                if (sk.channel() != null) {
                    sk.channel().close();
                }
            }
            if (content.length() > 0) {
                String[] arrayContent = content.toString().split(USER_CONTENT_SPLIT);
                //注册用户
                if (arrayContent != null && arrayContent.length == 1) {
                    String name = arrayContent[0];
                    if (users.contains(name)) {
                        sc.write(charset.encode(USER_EXIST));

                    } else {
                        users.add(name);
                        int num = OnlineNum(selector);
                        String message = "welcome {" + name + "} to chat room! Online numbers:" + num;
                        BroadCast(selector, null, message);
                    }
                }
                //注册完了,发送消息
                else if (arrayContent != null && arrayContent.length > 1) {
                    String name = arrayContent[0];
                    String message = content.substring(name.length() + USER_CONTENT_SPLIT.length());
                    message = name + " say " + message;
                    if (users.contains(name)) {
                        //不回发给发送此内容的客户端
                        BroadCast(selector, sc, message);
                    }
                }
            }

        }
    }

    public static int OnlineNum(Selector selector) {
        int res = 0;
        for (SelectionKey key : selector.keys()) {
            Channel targetchannel = key.channel();

            if (targetchannel instanceof SocketChannel) {
                res++;
            }
        }
        return res;
    }

    public void BroadCast(Selector selector, SocketChannel except, String content) throws IOException {
        //广播数据到所有的SocketChannel中
        for (SelectionKey key : selector.keys()) {
            Channel targetchannel = key.channel();
            //如果except不为空,不回发给发送此内容的客户端
            if (targetchannel instanceof SocketChannel && targetchannel != except) {
                SocketChannel dest = (SocketChannel) targetchannel;
                dest.write(charset.encode(content));
            }
        }
    }

    public static void main(String[] args) throws IOException {
        new ChatRoomServer().init();
    }
}
6.2.2.客户端
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Scanner;
import java.util.Set;

/**
 * 网络多客户端聊天室: 客户端
 */
public class ChatRoomClient {

    private Selector selector = null;
    static final int port = 8080;
    private Charset charset = Charset.forName("UTF-8");
    private SocketChannel sc = null;
    private String name = "";
    private static String USER_EXIST = "user exist";
    private static String USER_CONTENT_SPLIT = "@:\t";

    public void init() throws IOException {
        selector = Selector.open();
        //连接远程主机的IP和端口
        sc = SocketChannel.open(new InetSocketAddress("127.0.0.1", port));
        sc.configureBlocking(false);
        sc.register(selector, SelectionKey.OP_READ);
        //开辟一个新线程来读取从服务器端的数据
        new Thread(new ClientThread()).start();
        //在主线程中 从键盘读取数据输入到服务器端
        Scanner scan = new Scanner(System.in);
        while (scan.hasNextLine()) {
            String line = scan.nextLine();
            if ("".equals(line)) continue; //不允许发空消息
            if ("".equals(name)) {
                name = line;
                line = name + USER_CONTENT_SPLIT;
            } else {
                line = name + USER_CONTENT_SPLIT + line;
            }
            sc.write(charset.encode(line));//sc既能写也能读,这边是写
        }

    }

    private class ClientThread implements Runnable {
        public void run() {
            try {
                while (true) {
                    int readyChannels = selector.select();
                    if (readyChannels == 0) continue;
                    Set selectedKeys = selector.selectedKeys();  //可以通过这个方法,知道可用通道的集合
                    Iterator keyIterator = selectedKeys.iterator();
                    while (keyIterator.hasNext()) {
                        SelectionKey sk = (SelectionKey) keyIterator.next();
                        keyIterator.remove();
                        dealWithSelectionKey(sk);
                    }
                }
            } catch (IOException io) {
            }
        }

        private void dealWithSelectionKey(SelectionKey sk) throws IOException {
            if (sk.isReadable()) {
                //使用 NIO 读取 Channel中的数据,这个和全局变量sc是一样的,因为只注册了一个SocketChannel
                //sc既能写也能读,这边是读
                SocketChannel sc = (SocketChannel) sk.channel();

                ByteBuffer buff = ByteBuffer.allocate(1024);
                String content = "";
                while (sc.read(buff) > 0) {
                    buff.flip();
                    content += charset.decode(buff);
                }
                //若系统发送通知名字已经存在,则需要换个昵称
                if (USER_EXIST.equals(content)) {
                    name = "";
                }
                System.out.println(content);
                sk.interestOps(SelectionKey.OP_READ);
            }
        }
    }

    public static void main(String[] args) throws IOException {
        new ChatRoomClient().init();
    }
}
6.2.3.运行

1>.先启动服务端:
在这里插入图片描述
2>.再启动客户端:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值