NIO剖析

Java NIO基本介绍

  • Java NIO(New IO)也有人称之为java non-blocking IO是从java 1.4版本开始引入的一个新的IO API,可以替换标准的java IOAPI。NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓冲区的、基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作。NIO可以理解为非阻塞IO,传统的IO的read和write只能阻塞执行,线程在读写IO期间不能干其他事情,比如调用socket.read()时,如果服务器一直没有数据传输过来,线程就一直阻塞,而NIO中可以配置socket为非阻塞模式
  • NIO 相关类都被放在java.nio包下及子包,并且对原java.io包中的很多类进行改写
  • NIO 有三大核心部分:Channel(通道),Buffer(缓冲区),Selector(选择器)
  • Java NIO的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他事情。非阻塞写也是如此,一个线程请求写入一个写数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情
  • 通俗理解:NIO时可以做到用一个线程来处理多个操作的。假设有1000个请求过来,根据实际情况,可以分配20个或80个线程来处理。不像之前的阻塞IO那样,非的分配1000个

NIO和BIO的比较

  • BIO以流的方式处理数据,而NIO以块的方式处理数据,块I/O的效率比流I/O高的多
  • BIO是阻塞的,NIO是非阻塞的
  • BIO基于字节流和字符流进行操作,而NIO基于Channel通道和Buffer缓冲区进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道
NIOColumn 2
面向缓冲区(Buffer)面向流(Stream)
非阻塞(Non Blocking IO)阻塞(Blocking IO)
选择器(Selectors)

NIO三大核心原理示意图

NIO三大部分组成:Channel(通道)、Buffer(缓冲区)、Selector(选择器)

  • Buffer缓冲区
    缓冲区本质上是一个块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。相比较直接对数组操作,Buffer API更加容易操作和管理
  • Channel通道
    Java NIO的通道类似流,但又有些不同:既可以从通道中读取数据,又可以写数据到通道。但流的(input或output)读写通常是单向的。通道可以非阻塞读取和写入通道,通道可以支持读取或写入缓冲区,也支持异步地读写
  • Selector选择器
    Selector 是一个Java NIO组件,可以能够检查一个或多个NIO通道,并确定哪些通道已准备好进行读写或写入。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接,提高效率
    请添加图片描述
  1. 每个channel都会对应一个Buffer
  2. 一个线程对应一个Selector,一个Selector对应多个channel(连接)
  3. 程序切换到哪个channel是由事件决定的
  4. Selector会根据不同的事件,在各个通道上切换
  5. Buffer就是一个内存块,底层是一个数组
  6. 数据的读取写入是通过Buffer完成的,BIO中要么是输入流,或者是输出流,不能双向,但是NIO的Buffer可以读也可以写
  7. Java NIO系统的核心在于:通道(Channel)和缓冲区(Buffer)。通道表示打开到IO设备(例如:文件,套接字)的连接。若需要使用NIO系统,需要获取连接IO设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区,对数据进行处理。简而言之,Channel负责传输,Buffer负责存取数据

NIO核心一:缓冲区(Buffer)

缓冲区(Buffer)

一个用于特定基本数据类型的容器。由java.nio包定义,所有的缓冲区都是Buffer抽象类的子类。Java NIO中的Buffer主要用于与NIO通道进行交互,数据从通道读入缓冲区,从缓冲区写入通道

请添加图片描述

Buffer 类及其子类
Buffer就像一个数组,可以保存多个相同类型的数据。根据数据类型不同,有以下Buffer常用子类

ByteBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer
上述Buffer类 它们都采用相似的方法进行管理数据,只是各自管理的数据类型不同而已。都是通过如下方法获取一个Buffer对象:
static XxxBuffer allocate(int capacity) // 创建一个容量为capacity的 XxxBuffer对象

缓冲区的基本属性

缓冲区的重要概念

把缓冲区理解成一个数组

  • 容量capacity:作为一个内存块,Buffer具有一定的固定大小,也称为“容量”,缓冲区容量不能为负,并且创建后不能更改
  • 限制limit:表示缓冲区可以操作数据的大小(limit后数据不能进行读写)。缓冲区的限制不能为负,并且不能大于其他容量。写入模式,限制等于buffer的容量,读取模式,limit等于写入数据量
  • 位置position:下一个要读取或写入的数据的索引,通过Buffer中的mark()方法 指定 Buffer 中一个特定的position,之后可以通过调用reset() 方法恢复到这个position
    标记、位置、限制、容量遵守以下不变式:
    0 <= mark <= position <= limit <= capacity
    请添加图片描述

Buffer常见方法

方法说明
Buffer clear()清空缓冲区并返回缓冲区的引用
Buffer flip()为将缓冲区的界限设置为当前位置,并将当前位置充值为0
int capacity()返回Buffer的capacity大小
boolean hasRemaining()判断缓冲区是否还有元素
int limit()返回Buffer的界限limit的位置
Buffer limit(int n)将设置缓冲区界限为n,并返回一个具有新的limit的缓冲区对象
Buffer mark()对缓冲区设置标记
int position()返回缓冲区当前位置position
Buffer position(int n)将设置缓冲区当前位置n,并返回修改后的Buffer对象
int remaining()返回position和limit之间的元素个数
Buffer reset()将position转到以前设置的mark所在的位置
Buffer rewind()将位置设置为0,取消设置的mark

缓冲区的数据操作

Buffer 所有的子类提供了两个用于数据操作的方法:get() put() 方法
获取Buffer的数据
get(); 读取单个字节
get(byte[] dst):批量读取多个字节到dst中
get(int index):读取指定索引位置的字节(不会移动 position)

放入数据到Buffer中
put(byte b):将给定单个字节写入缓冲区的当前位置
put(byte[] src):将src中的字节写入缓冲区的当前位置
put(int index, byte b):将指定字节写入缓冲区的索引位置(不会移动position)

public class BufferTest {
    public static void main(String[] args) {
        /**
         Buffer clear()	清空缓冲区并返回缓冲区的引用
         Buffer flip()	为将缓冲区的界限设置为当前位置,并将当前位置充值为0
         int capacity()	返回Buffer的capacity大小
         boolean hasRemaining()	判断缓冲区是否还有元素
         int limit()	返回Buffer的界限limit的位置
         Buffer limit(int n)	将设置缓冲区界限为n,并返回一个具有新的limit的缓冲区对象
         Buffer mark()	对缓冲区设置标记
         int position()	返回缓冲区当前位置position
         Buffer position(int n)	将设置缓冲区当前位置n,并返回修改后的Buffer对象
         int remaining()	返回position和limit之间的元素个数
         Buffer reset()	将position转到以前设置的mark所在的位置
         Buffer rewind()	将位置设置为0,取消设置的mark

         */
        // 1. 创建一个Buffer
        ByteBuffer buffer = ByteBuffer.allocate(20);
        System.out.println(buffer.position());
        System.out.println(buffer.limit());
        System.out.println(buffer.capacity());
        System.out.println("---------");

        // 2. put
        buffer.put("hello".getBytes());
        System.out.println(buffer.position());
        System.out.println(buffer.limit());
        System.out.println(buffer.capacity());
        System.out.println("---------");

        // 3. flip 读模式  limit变为当前元素个数
        buffer.flip();
        System.out.println(buffer.position());
        System.out.println(buffer.limit());
        System.out.println(buffer.capacity());
        System.out.println("---------");

        // 4. get limit 改变
        System.out.println(buffer.get());
        System.out.println(buffer.position());
        System.out.println(buffer.limit());
        System.out.println(buffer.capacity());
        System.out.println("---------");


        // 5. clear 清空之后数据还在,只是position变为0,新put覆盖
        buffer.clear();
        System.out.println(buffer.position());
        System.out.println(buffer.limit());
        System.out.println(buffer.capacity());
        System.out.println("---------");

        // 6.get 读取 position = 5
        buffer.put("helloworld".getBytes());
        buffer.flip();
        byte[] bytes = new byte[5];
        buffer.get(bytes);
        String s = new String(bytes);
        System.out.println(s);
        System.out.println(buffer.position());
        System.out.println(buffer.limit());
        System.out.println(buffer.capacity());
        System.out.println("---------");

        // 7. mark标记
        buffer.mark();
        byte[] b1 = new byte[5];
        buffer.get(b1);
        String s1 = new String(b1);
        System.out.println(s1);
        System.out.println(buffer.position());
        System.out.println(buffer.limit());
        System.out.println(buffer.capacity());
        System.out.println("---------");
        buffer.reset();
        byte[] b2 = new byte[4];
        buffer.get(b2);
        String s2 = new String(b2);
        System.out.println(s2);
        System.out.println(buffer.position());
        System.out.println(buffer.limit());
        System.out.println(buffer.capacity());
        System.out.println("---------");

        // 8 是否还有元素,如果有元素个数
        if (buffer.hasRemaining()) {
            System.out.println(buffer.remaining());
        }
    }
}

使用Buffer读写数据一般遵循以下四个步骤

  1. 写入数据到Buffer
  2. 调用flip()方法,转换为读取模式
  3. 从Buffer中读取数据
  4. 调用buffer.clear()方法或者buffer.compact()方法清除缓冲区

直接与非直接缓冲区

什么是直接内存与非直接内存
根据官方文档描述:

byte buffer可以是两种类型,一种是基于直接内存(也就是非堆内存);另一种是非直接内存(也就是堆内存)。对于直接内存来说,JVM将会在IO操作上具有更高的性能,因为他直接作用于本地系统的IO操作。而非直接操作内存,也就是堆内存中的数据。如果要做IO操作,会先从本进程内复制到直接内存,再利用本地IO处理

从数据流的角度,非直接内存是下面这样的作用链:

本地IO–>直接内存–>非直接内存–>直接内存–>本地IO

而直接内存:

本地IO–>直接内粗–>本地IO
跟明显,在做IO处理时,比如网络发送大量数据时,直接内存具有更高的效率。直接内存使用allocateDirect创建,但是它比申请普通的堆内存需要消耗更高的性。不过,这部分数据是在jvm之外的,因此他不会占用应用的内存。所以呢,当你有很大的数据要缓存,并且他的生命周期很长时,那么就比较适合使用直接内存。只是一般来说,如果不是能带来很明星性能提升,还是推荐使用堆内存。字节缓冲区是直接缓冲区还是非直接缓冲区可以调用isDirect()方法来确定

使用场景

  1. 有很大的数据需要存储,他的生命周期有很长
  2. 适合频繁的IO操作,比如网络并发场景

NIO核心二:通道(Channel)

通道channel概述

通道(Channel):由java.nio.channels包定义的。Channel表示IO源与目标打开的连接。Channel类似于传统的“流”。只不过Channel本身不能直接访问数据,Channel只能与Buffer进行交互

  1. NIO的通道类似于流,但有些区别
  • 通道可以同时及进行读写,而流只能读或者只能写
  • 通道可以实现异步读写数据
  • 通道可以从缓冲读数据,也可以写数据到缓冲
  1. BIO中的stream是单向的,例如FileInputStream对象只能进行读取数据的操作,而NIO中的通道(Channel)是双向的,可以读操作,也可以写操作
    public interface Channel extends Closeable{}

常用的Channel实现类

  • FileChannel:用于读取、写入、映射和操作文件的通道
  • DatagramChannel:通过UDP读写网络中的数据通道
  • SocketChannel:通过TCP读写网络中的数据
  • ServerSocketChannel:可以监听新进来的TCP连接,对每一个新进来的连接都会创建一个SocketChannel【ServerSocketChannel类似于ServerSocket,SocketChannel类似于Socket】

FileChannel类

获取通道的一种方式是对支持通道的对象调用getChannel()方法,支持通道的类如下:

  • FileOutputStream
  • RandomAccessFile
  • DatagramSocket
  • Socket
  • ServerSocket
    获取通道的其他方式是使用Files类的静态方法 newByteChannel() 获取字节通道。或者通过通道的静态方法open()打开并返回指定通道

FileChannel的常用方法

方法说明
int read(ByteBuffer dst)从Channel中读取数据到ByteBuffer
long read(ByteBuffer[] dsts)将Channel中的数据分散到ByteBuffer
int write[ByteBuffer src]将ByteBuffer中的数据写入到Channel
long write(ByteBuffer[] srcs)将ByteBuffer[] 中的数据聚集到Channel
long position()返回此通道的文件位置
FileChannel position(long p)设置此通道的文件位置
long size()返回此通道的文件的当前大小
FileChannel truncate(long s)将此通道的文件截取为给定大小
void force(boolean metaData)强制将所有对此通道的文件更新写入到存储设备

向本地文件写数据

public class ChannelTest {
    public static void main(String[] args) {
        try {
            // 1. 字节输出流通向目标文件
            FileOutputStream fos = new FileOutputStream("file.txt");
            // 2. 得到字节输出流对应的通道Channel
            FileChannel channel = fos.getChannel();
            // 3. 分配缓冲区
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            buffer.put("helloworld".getBytes());
            // 4. 把缓冲区切换成写的模式
            buffer.flip();
            channel.write(buffer);
            fos.close();
            System.out.println("写入数据完毕");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


}

从本地文件读取数据

public static void main(String[] args) {
        try {
            // 1 字节文件输入流通向文件
            FileInputStream fis = new FileInputStream("file.txt");
            // 2 得到字节文件输入流对应的channel
            FileChannel channel = fis.getChannel();
            // 3 分配缓冲区
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            channel.read(buffer);
            // 4 转换写模式
            buffer.flip();
            String s = new String(buffer.array(), 0, buffer.remaining());
            System.out.println(s);
            
            fis.close();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

文件复制

public static void main(String[] args) {
        try {
            // 源文件
            File srcFile = new File("/Users/mac/Desktop/111.png");
            File desFile = new File("newPic.png");
            // 分别得到输入、输出流
            FileInputStream fis = new FileInputStream(srcFile);
            FileOutputStream fos = new FileOutputStream(desFile);
            // 分别获取到通道
            FileChannel fisChannel = fis.getChannel();
            FileChannel fosChannel = fos.getChannel();
            // 分配缓冲区
            ByteBuffer buffer =ByteBuffer.allocate(1024);
            while (true) {
                // 必须先清空缓冲区然后在写入到缓冲区
                buffer.clear();
                // 开始读取一次数据
                int flag = fisChannel.read(buffer);
                if (flag == -1) {
                    break;
                }
                // 已经读取到了数据,把缓冲区切换成可读模式
                buffer.flip();
                // 把数据写出
                fosChannel.write(buffer);
            }
            fisChannel.close();
            fosChannel.close();
            System.out.println("文件复制完毕");

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

分散(Scatter)和聚集(Gather)

分散读取(Scatter):指把Channel通道的数据读入到多个缓冲区
聚集写入(Gathering):指将多个Buffer中的数据聚集到Channel

public static void main(String[] args) {
        try {
            // 文件输入流
            FileInputStream fis = new FileInputStream("file.txt");
            FileChannel fisChannel = fis.getChannel();
            // 文件输出流
            FileOutputStream fos = new FileOutputStream("file_copy.txt");
            FileChannel fosChannel = fos.getChannel();

            // 分散
            ByteBuffer buffer1 = ByteBuffer.allocate(4);
            ByteBuffer buffer2 = ByteBuffer.allocate(1024);
            ByteBuffer[] byteBuffers = {buffer1,buffer2};
            fisChannel.read(byteBuffers);

            for (ByteBuffer byteBuffer : byteBuffers) {
                byteBuffer.flip();
                String s = new String(byteBuffer.array(), 0, byteBuffer.remaining());
                System.out.println(s);
            }
            // 聚集
            fosChannel.write(byteBuffers);
            fis.close();
            fos.close();


        } catch (  Exception e) {
            e.printStackTrace();
        }
    }

高级复制(transferTo & transferFrom)

  public static void main(String[] args) {
        try {
            FileInputStream fis = new FileInputStream("file.txt");
            FileChannel fisChannel = fis.getChannel();

            FileOutputStream fos = new FileOutputStream("file3.txt");
            FileChannel fosChannel = fos.getChannel();

//            fisChannel.transferTo(fosChannel.position(), fisChannel.size(),fosChannel);
            fosChannel.transferFrom(fisChannel,fosChannel.position(),fisChannel.size());

            System.out.println("文件复制完毕");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

NIO核心三:选择器(Selector)

选择器(Selector)概述

选择器(Selector)是SelectableChannel对象的多路复用器,Selector可以同时监听多个SelectableChannel的IO状况,也就说,利用Selector可使一个单独的线程管理多个Channel。Selector是非阻塞IO的核心请添加图片描述

  • Java的NIO,用非阻塞的IO方式,可以用一个线程,处理多个客户端连接,就会使用到Selector(选择器)
  • Selector能够检测多个注册的通道上是否有事件发生(注意:多个Channel以事件的方式可以注册到同一个Selector),如果有事件发生,便获取时间后针对每个事件进行相应处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求
  • 只有在 连接/通道 真正有读写事件发生时,才会进行读写,就大大的减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程
  • 避免了多线程之间的上下文切换

Selector应用

创建 Selector:通过调用Selector.open()方法创建一个Selector
Selector selector = Selector.open()
向选择器注册通道:SelectableChannel.register(Selector sel,int ops)

// 1 获取通道
ServerSocketChannel ssChannel = ServerSocketChannel.open();
// 2 切换非阻塞模式
ssChannel.configureBlocking(false)
// 3 绑定端口
ssChannel.bind(new InetSocketAddress(9898))
// 4 获取选择器
Selector selector = selector.open();
// 5 将通道注册到选择器,并且指定“监听接收事件”
ssChannel.register(selector,selectionKey.OP_ACCEPT)

当调用register(Selector sel,int ops)将通道注册到选择器时,选择器对通道的监听事件,需要通过第二个参数ops指定,可以监听的时间类型(用可使用SelectionKey的四个常量表示):

  • 读:SelectionKey.OP_READ(1)
  • 写:SelectionKey.OP_WRITE(4)
  • 连接:SelectionKey.OP_CONNECT(8)
  • 接收:SelectionKey.OP_ACCEPT(16)
  • 若注册时不止监听一个事件,则可以使用“位或操作符”连接
    int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE

NIO非阻塞时网络通信原理分析

Selector可以实现:一个IO线程可以并发处理N个客户端连接和读写操作,这从根本上解决了传统同步阻塞IO一连接一线程模型,架构性能、弹性伸缩能力和可靠性得到了极大的提高
请添加图片描述

  • Server
public class Server {
    public static void main(String[] args) {
        try {
            // 1 获取通信
            ServerSocketChannel ssChannel = ServerSocketChannel.open();
            // 2 切换为非阻塞模式
            ssChannel.configureBlocking(false);
            // 3 绑定连接端口
            ssChannel.bind(new InetSocketAddress(9999));
            // 4 获取选择器
            Selector selector = Selector.open();
            // 5 将通信都注册到选择器上,并且开始指定监听接收事件
            ssChannel.register(selector, SelectionKey.OP_ACCEPT);
            // 6 使用Selector选择器轮询已经就绪的事件
            while (selector.select() > 0) {
                // 7 获取选择器中的所有注册的通道中已经就绪的事件
                Iterator<SelectionKey> it = selector.selectedKeys().iterator();
                // 8 遍历事件
                while (it.hasNext()) {
                    SelectionKey sk = it.next();
                    // 9 判断这个事件具体是什么
                    if (sk.isAcceptable()) {
                        // 10 获取当前介入客户端的通道
                        SocketChannel sChannel = ssChannel.accept();
                        // 11 切换成非阻塞模式
                        sChannel.configureBlocking(false);
                        // 12 注册到选择器
                        sChannel.register(selector,SelectionKey.OP_READ);
                    }else if(sk.isReadable()){
                        // 13 获取当前选择器的就绪事件
                        SocketChannel sChannel = (SocketChannel) sk.channel();
                        // 14 读取数据
                        ByteBuffer buf = ByteBuffer.allocate(1024);
                        int len = 0;
                        while ((len = sChannel.read(buf)) > 0) {
                            buf.flip();
                            System.out.println(new String(buf.array(), 0, len));
                            buf.clear();
                        }
                    }
                    it.remove();// 移除当前事件
                }
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

  • Client

public class Client {
    public static void main(String[] args) {
        try {
            // 1 获取通道
            SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9999));
            // 2 切换成非阻塞模式
            sChannel.configureBlocking(false);
            // 3 分配指定缓冲区大小
            ByteBuffer buf = ByteBuffer.allocate(1024);
            // 4 发送数据
            Scanner sc = new Scanner(System.in);
            while (true) {
                System.out.println("请说:");
                String msg = sc.nextLine();
                buf.put(("波仔: "+msg).getBytes());
                buf.flip();
                sChannel.write(buf);
                buf.clear();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

通讯

server

public class Server {
    //定义属性
    private Selector selector;
    private ServerSocketChannel ssChannel;
    private static final int PORT = 9999;
    //构造器
    //初始化工作
    public Server() {
        try {
            // 1、获取通道
            ssChannel = ServerSocketChannel.open();
            // 2、切换为非阻塞模式
            ssChannel.configureBlocking(false);
            // 3、绑定连接的端口
            ssChannel.bind(new InetSocketAddress(PORT));
            // 4、获取选择器Selector
            selector = Selector.open();
            // 5、将通道都注册到选择器上去,并且开始指定监听接收事件
            ssChannel.register(selector , SelectionKey.OP_ACCEPT);
        }catch (IOException e) {
            e.printStackTrace();
        }
    }
    //监听
    public void listen() {
        System.out.println("监听线程: " + Thread.currentThread().getName());
        try {
            while (selector.select() > 0){
                System.out.println("开始一轮事件处理~~~");
                // 7、获取选择器中的所有注册的通道中已经就绪好的事件
                Iterator<SelectionKey> it = selector.selectedKeys().iterator();
                // 8、开始遍历这些准备好的事件
                while (it.hasNext()){
                    // 提取当前这个事件
                    SelectionKey sk = it.next();
                    // 9、判断这个事件具体是什么
                    if(sk.isAcceptable()){
                        // 10、直接获取当前接入的客户端通道
                        SocketChannel schannel = ssChannel.accept();
                        // 11 、切换成非阻塞模式
                        schannel.configureBlocking(false);
                        // 12、将本客户端通道注册到选择器
                        System.out.println(schannel.getRemoteAddress() + " 上线 ");
                        schannel.register(selector , SelectionKey.OP_READ);
                        //提示
                    }else if(sk.isReadable()){
                        //处理读 (专门写方法..)
                        readData(sk);
                    }

                    it.remove(); // 处理完毕之后需要移除当前事件
                }
            }
        }catch (Exception e) {
            e.printStackTrace();
        }finally {
            //发生异常处理....

        }
    }

    //读取客户端消息
    private void readData(SelectionKey key) {
        //取到关联的channle
        SocketChannel channel = null;
        try {
            //得到channel
            channel = (SocketChannel) key.channel();
            //创建buffer
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            int count = channel.read(buffer);
            //根据count的值做处理
            if(count > 0) {
                //把缓存区的数据转成字符串
                String msg = new String(buffer.array());
                //输出该消息
                System.out.println("form 客户端: " + msg);
                //向其它的客户端转发消息(去掉自己), 专门写一个方法来处理
                sendInfoToOtherClients(msg, channel);
            }
        }catch (IOException e) {
            try {
                System.out.println(channel.getRemoteAddress() + " 离线了..");
                e.printStackTrace();
                //取消注册
                key.cancel();
                //关闭通道
                channel.close();
            }catch (IOException e2) {
                e2.printStackTrace();;
            }
        }
    }

    //转发消息给其它客户(通道)
    private void sendInfoToOtherClients(String msg, SocketChannel self ) throws  IOException{
        System.out.println("服务器转发消息中...");
        System.out.println("服务器转发数据给客户端线程: " + Thread.currentThread().getName());
        //遍历 所有注册到selector 上的 SocketChannel,并排除 self
        for(SelectionKey key: selector.keys()) {
            //通过 key  取出对应的 SocketChannel
            Channel targetChannel = key.channel();
            //排除自己
            if(targetChannel instanceof  SocketChannel && targetChannel != self) {
                //转型
                SocketChannel dest = (SocketChannel)targetChannel;
                //将msg 存储到buffer
                ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
                //将buffer 的数据写入 通道
                dest.write(buffer);
            }
        }
    }

    public static void main(String[] args) {
        //创建服务器对象
        Server groupChatServer = new Server();
        groupChatServer.listen();
    }
}

client

public class Client {
    //定义相关的属性
    private final String HOST = "127.0.0.1"; // 服务器的ip
    private final int PORT = 9999; //服务器端口
    private Selector selector;
    private SocketChannel socketChannel;
    private String username;

    //构造器, 完成初始化工作
    public Client() throws IOException {

        selector = Selector.open();
        //连接服务器
        socketChannel = socketChannel.open(new InetSocketAddress("127.0.0.1", PORT));
        //设置非阻塞
        socketChannel.configureBlocking(false);
        //将channel 注册到selector
        socketChannel.register(selector, SelectionKey.OP_READ);
        //得到username
        username = socketChannel.getLocalAddress().toString().substring(1);
        System.out.println(username + " is ok...");

    }

    //向服务器发送消息
    public void sendInfo(String info) {
        info = username + " 说:" + info;
        try {
            socketChannel.write(ByteBuffer.wrap(info.getBytes()));
        }catch (IOException e) {
            e.printStackTrace();
        }
    }

    //读取从服务器端回复的消息
    public void readInfo() {
        try {

            int readChannels = selector.select();
            if(readChannels > 0) {//有可以用的通道

                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()) {

                    SelectionKey key = iterator.next();
                    if(key.isReadable()) {
                        //得到相关的通道
                        SocketChannel sc = (SocketChannel) key.channel();
                        //得到一个Buffer
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        //读取
                        sc.read(buffer);
                        //把读到的缓冲区的数据转成字符串
                        String msg = new String(buffer.array());
                        System.out.println(msg.trim());
                    }
                }
                iterator.remove(); //删除当前的selectionKey, 防止重复操作
            } else {
                //System.out.println("没有可以用的通道...");

            }

        }catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws Exception {
        //启动我们客户端
        Client chatClient = new Client();
        //启动一个线程, 每个3秒,读取从服务器发送数据
        new Thread() {
            public void run() {

                while (true) {
                    chatClient.readInfo();
                    try {
                        Thread.currentThread().sleep(3000);
                    }catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();

        //发送数据给服务器端
        Scanner scanner = new Scanner(System.in);

        while (scanner.hasNextLine()) {
            String s = scanner.nextLine();
            chatClient.sendInfo(s);
        }
    }
}

`	``

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值