多次尝试的学习,终于搞懂了NIO!

NIO—NonBlocking IO(new IO)

  1. io面向流编程,只能作为输入或者输出流的一种,是同步阻塞的,每一个连接过来都要创建一个线程去处理,线程上下文切换开销很大,造成了很大的瓶颈
  2. 于是有了线程池实现的伪阻塞IO,一定程度解决了线程创建过多的问题,但是没有从根本上解决阻塞的问题,并且线程过多而线程池过小时也会造成很大的瓶颈
  3. 既然根本瓶颈原因是线程数和阻塞IO,那么我们有没有办法只用1个线程去处理多个客户端连接呢?这就是NIO出现的原因

NIO主要有三个核心部分组成

  • buffer缓冲区
  • Channel管道
  • Selector选择器

nio面向block块,buffer缓冲区编程,底层是数组,buffer提供数据访问,channel读写到buffer,buffer读写到channel,从buffer读取到程序channel是双向的

理解NIO需要理解事件编程模型

NIO核心:

NIO由原来的阻塞读写(占用线程)变成了单线程轮询事件,找到可以进行读写的网络描述符进行读写。除了事件的轮询是阻塞的(没有可干的事情必须要阻塞),剩余的I/O操作都是纯CPU操作,没有必要开启多线程。

单线程处理I/O的效率确实非常高,没有线程切换,只是拼命的读、写、选择事件。

NIO带个我们:

  1. 事件驱动模型—异步编程都离不开事件
  2. 单线程处理多连接—多路复用使得处理更加高效
  3. 非阻塞IO,只阻塞获取可操作事件
  4. 基于block传输比基于流传输更加高效
  5. 零拷贝—DirectBuffer

缺点:

NIO并没有完全屏蔽平台差异,它仍然是基于各个操作系统的I/O系统实现的,差异仍然存在。使用NIO做网络编程构建事件驱动模型并不容易,陷阱重重。

推荐使用NIO成熟框架Netty

Buffer

缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。

Capacity、Position、Limit

<=< code=""> mark <=< code=""> position <=< code=""> limit <=< code=""> capacity

  • capacity

作为一个内存块,Buffer有一个固定的大小值,也叫“capacity”.你只能往里写capacity个byte、long,char等类型。一旦Buffer满了,需要将其清空(通过读数据或者清除数据)才能继续写数据往里写数据。

  • position

当你写数据到Buffer中时,position表示当前的位置。初始的position值为0.当一个byte、long等数据写到Buffer后, position会向前移动到下一个可插入数据的Buffer单元。position最大可为capacity – 1.

当读取数据时,也是从某个特定位置读。当将Buffer从写模式切换到读模式,position会被重置为0. 当从Buffer的position处读取数据时,position向前移动到下一个可读的位置。

  • limit

在写模式下,Buffer的limit表示你最多能往Buffer里写多少数据。 写模式下,limit等于Buffer的capacity。

当切换Buffer到读模式时, limit表示你最多能读到多少数据。因此,当切换Buffer到读模式时,limit会被设置成写模式下的position值。换句话说,你能读到之前写入的所有数据(limit被设置成已写数据的数量,这个值在写模式下就是position)

同一个buffer可以存储不同数据类型的数据,但是获取的时候要指定类型获取

ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.putInt(1);
buffer.putLong(387524875628742L);
buffer.putChar('s');
buffer.flip();
System.out.println(buffer.getInt());
System.out.println(buffer.getLong());
System.out.println(buffer.getChar());复制代码

put方法只能放入byte型,不能放入int

flip、clear、rewind、mark

  • flip

flip方法将Buffer从写模式切换到读模式。调用flip()方法会将position设回0,并将limit设置成之前position的值。

    public final Buffer flip() {
        this.limit = this.position;
        this.position = 0;
        this.mark = -1;
        return this;
    }复制代码

  • clear

position将被设回0,limit被设置成 capacity的值。换句话说,Buffer 被清空了。Buffer中的数据并未清除,只是这些标记告诉我们可以从哪里开始往Buffer里写数据。

    public final Buffer clear() {
        this.position = 0;
        this.limit = this.capacity;
        this.mark = -1;
        return this;
    }复制代码

  • rewind

Buffer.rewind()将position设回0,所以你可以重读Buffer中的所有数据。limit保持不变,仍然表示能从Buffer中读取多少个元素

    public final Buffer rewind() {
        this.position = 0;
        this.mark = -1;
        return this;
    }复制代码

  • mark

可以标记Buffer中的一个特定position。之后可以通过调用Buffer.reset()方法恢复到这个position。

    public final Buffer mark() {
        this.mark = this.position;
        return this;
    }复制代码

  • slice分片

将buffer根据设置的position和limit分片一个buffer,有自己的position、limit和capacity,数据共用一个内存地址的buffer数据

public static void test2(){
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        for(int i=0;i<buffer.capacity();i++){
            buffer.put((byte)i);
        }
        buffer.position(10);
        buffer.limit(20);
        ByteBuffer buffer1 = buffer.slice();//buffer分片
        for(int m=0;m<buffer1.capacity();m++){
            byte b = buffer1.get();
            System.out.print(b+" ");
        }
    }

输出:
10 11 12 13 14 15 16 17 18 19复制代码

ReadOnlyBuffer

普通的Buffer(可读可写)可以随时转换为只读Buffer,但是只读Buffer不可以转换为普通Buffer

ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer();复制代码

转换后的Buffer是一个新的只读Buffer,拥有独立的position、limit和capacity

DirectBuffer

堆外内存buffer,本地JNI非JVM堆内存buffer,允许直接访问

普通ByteBuffer由JVM管理,在JVM堆上分配内存

ByteBuffer buf = ByteBuffer.allocate(1024);复制代码

DirectBuffer会在本地内存中分配,脱离JVM的堆管理

ByteBuffer buf = ByteBuffer.allocateDirect(1024);复制代码

为什么要这样做呢?

----------又是GC--------

我们都知道JVM 在堆上的老年代中,GC时会采取标记-整理策略,会使得对象在堆内存中的地址发生变化,整理时会buffer太大时会很难gc整理

所以出现了DirectBuffer,它使用unsafe.allocateMemory分配内存,是一个native方法,由buffer的address变量记录这个内存的地址来提供访问

比较

  • DirectBuffer:本地方法分配内存显然没有JVM堆分配快,但是涉及IO网络IO的话就是DirectBuffer比较快了

DirectByteBuffer继承了MappedByteBuffer

缓存的使用可以使用DirectByteBuffer和HeapByteBuffer。如果使用了DirectByteBuffer,一般来说可以减少一次系统空间到用户空间的拷贝。

数据量比较小的中小应用情况下,可以考虑使用heapBuffer;反之可以用directBuffer

MappedByteBuffer

映射到堆外内存的ByteBuffer,DirectByteBuffer继承此类实现堆外内存的分配

通过下面方式映射buffer到堆外内存

MappedByteBuffer mappedByteBuffer = channel.map(MapMode.READ_WRITE, 0, channel.size());复制代码

使用拷贝文件:

RandomAccessFile in = new RandomAccessFile("nio/1.txt", "rw");
RandomAccessFile out = new RandomAccessFile("nio/2.txt", "rw");
FileChannel inChannel = in.getChannel();
FileChannel outChannel = out.getChannel();
MappedByteBuffer inputData = inChannel.map(FileChannel.MapMode.READ_ONLY,0,new File("nio/1.txt").length());
Charset charset = Charset.forName("utf-8");//编码
CharsetDecoder decoder = charset.newDecoder();
CharsetEncoder encoder = charset.newEncoder();
CharBuffer charBuffer = decoder.decode(inputData);
ByteBuffer buffer = encoder.encode(charBuffer);
outChannel.write(buffer);
in.close();out.close();复制代码

Channel—通道

FileChannel

NIO提供的一种连接到文件的通道,用于文件的读写

在使用FileChannel时,需要从输入输出流或者RandomAccessFile中获取FIleChannel

  • 如果要向FileChannel中读取数据,需要申请一个ByteBuffer,将数据从FileChannel中读取到缓冲区ByteBuffer,read()返回多少个字节被读取,如果返回-1说明文件已经到达末尾
  • 如果要向FileChannel中写入数据,需要先将数据写入到ByteBuffer中,在从ByteBuffer中写入到FileChannel中,调用write()方法

注意读写之间需要Buffer.flip();

例子:

1.读取文件数据并打印

FileInputStream fileInputStream = new FileInputStream("1.log");
FileChannel channel = fileInputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(512);;
channel.read(byteBuffer);
byteBuffer.flip();
while(byteBuffer.remaining()>0){
    byte b = byteBuffer.get();
    System.out.println((char) b);
}
fileInputStream.close();复制代码

2.把1.txt数据写入2.txt

FileInputStream inputStream = new FileInputStream("1.txt");
FileChannel in = inputStream.getChannel();
FileOutputStream outputStream = new FileOutputStream("2.txt");
FileChannel out = outputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
while(true){
    byteBuffer.clear();//没有的话会一直读取
    int read = in.read(byteBuffer);
    System.out.println("read:"+read);
    if(read==-1){
        break;//为-1表示文件结束 返回
    }
    byteBuffer.flip();
    out.write(byteBuffer);
}
inputStream.close();
outputStream.close();复制代码

ServerSockerChannel

NIO提供了一种可以监听新进入的TCP连接的通道,就是ServerSocketChannel,对应IO中ServerSocket

  • 打开监听通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(9999));
while(true){
    SocketChannel socketChannel = serverSocketChannel.accept();
    //do something with socketChannel...
}复制代码

SocketChannel

NIO提供的一种连接到TCP套接字的通道,就是SocketChannel,对应IO中Socket

  • 打开一个SocketChannel
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost", 8080));复制代码

Channel读写

ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = socketChannel.read(buf);复制代码

ByteBuffer writeBuffer = ByteBuffer.allocate(48);
String msg = "hello";
writeBuffer.put(msg.getBytes());
writeBuffer.flip();
channel.write(writeBuffer);复制代码

  • 读完写
ByteBuffer buffer = ByteBuffer.allocate(1024);
int byteRead = channel.read(buffer);
if(byteRead<=0){
    channel.close();
    break;
}
buffer.flip();
channel.write(buffer);
read += byteRead;
buffer.clear();复制代码

每次写完buffer,如果buffer数据不需要再使用,建议clear清空buffer,准备下一次写操作

Selector—多路复用器(选择器)

多路复用器,这个名字很形象,使用一个线程去处理多个channel,从而管理多个channel

为什么要使用一个线程管理多个channel?

线程上下文切换开销很大,线程越少处理channel更高效

创建Selector—创建比赛

Selector selector = Selector.open();复制代码

注册channel—购买入场卷

channel通过注册到selector上来把channel的事件交给Selector管理,并且返回一个SelectionKey

SelectionKey key = channel.register(selector, SelectionKey.OP_READ);复制代码

  • 与 Selector 一起使用时,Channel 必须处于非阻塞模式下。这意味着不能将 FileChannel 与 Selector 一起使用,因为 FileChannel 不能切换到非阻塞模式
channel.configureBlocking(false);复制代码

  • 通过SelectionKey获取channel和selector以及准备好的事件
Channel  channel  = selectionKey.channel();
Selector selector = selectionKey.selector();复制代码

Selector执行选择—拿着入场卷入场

把channel注册到Selector后,我们可以使用Selector.select();方法获取准备就绪的通道,返回一个int型整数,表示准备好的channel数

通过selector.selectedKeys();方法获取准备就绪的SelectionKey,再通过SelectionKey获取channel和selector,一般使用迭代器遍历这些准备好的channel

在每一次处理完一个SelectionKey,必须把它从迭代器中删除,处理完,这个SelectionKey就没有用了,就像一个入场卷,你可以通过它进入赛场并且它上面有入场人和座位对应信息,比赛结束后你无法再通过它执行任何有效的操作。

  • 看完比赛,举办者不会回收所有的票据,需要你们自己处理,不能乱丢在场地中,需要自己丢到垃圾桶中或者带回家
iterator.remove();复制代码

  • wakeUp()方法

某个线程调用 select() 方法后阻塞了,即使没有通道已经就绪,也无法返回,wakeUp方法使得立马返回。

Scatter、Gather

scatter / gather 经常用于需要将传输的数据分开处理的场合,例如传输一个由消息头和消息体组成的消息,你可能会将消息体和消息头分散到不同的 buffer 中,这样你可以方便的处理消息头和消息体。

Scatter

分散(scatter)从 Channel 中读取是指在读操作时将读取的数据写入多个 buffer 中。因此,Channel 将从 Channel 中读取的数据 “分散(scatter)” 到多个 Buffer 中。

Gather

聚集(gather)写入 Channel 是指在写操作时将多个 buffer 的数据写入同一个 Channel,因此,Channel 将多个 Buffer 中的数据 “聚集(gather)” 后发送到 Channel。

例子:用三个长度分别为3,4,5的buffer存储输入的字符串,前3个字符存储在第一个buffer,4-7字符存储在第二个buffer,长度为4,8-12存储在第三个buffer,长度为5

ServerSocketChannel serverSocketChannel =  ServerSocketChannel.open();
        InetSocketAddress inetSocketAddress = new InetSocketAddress(8899);
        serverSocketChannel.socket().bind(inetSocketAddress);
        int messageLength = 3 + 4 + 5;
        ByteBuffer[] byteBuffer = new ByteBuffer[3];
        byteBuffer[0] = ByteBuffer.allocate(3);
        byteBuffer[1] = ByteBuffer.allocate(4);
        byteBuffer[2] = ByteBuffer.allocate(5);
        SocketChannel socketChannel = serverSocketChannel.accept();
        while (true){
            int byteRead = 0;
            while (byteRead<messageLength){
                long r = socketChannel.read(byteBuffer);
                byteRead += r;
                System.out.println("byteread:"+byteRead);
                Arrays.stream(byteBuffer).map(o->"position:"+o.position()+",limit:"+o.limit()).forEach(System.out::println);
            }

            Arrays.stream(byteBuffer).forEach(Buffer::flip);

            int byteWrite = 0;
            while(byteWrite<messageLength){
                long r = socketChannel.write(byteBuffer);
                byteWrite += r;
                System.out.println("bytewrite:"+byteWrite);
                Arrays.stream(byteBuffer).map(o->"position:"+o.position()+",limit:"+o.limit()).forEach(System.out::println);
            }

            Arrays.stream(byteBuffer).forEach(Buffer::clear);
        }

测试:使用linux nc localhost 8899测试
输入:helloworld回车 
输出:
byteread:11
position:3,limit:3
position:4,limit:4
position:4,limit:5
解释:
回车算一个字符一共11个字符,前三个存储到第一个buffer了,存满了;中间四个存储到第二个buffer,存满了;剩下多余的存储到第三个buffer,没有存满复制代码

NIO服务端客户端

这个程序演示使用NIO创建一个聊天室,服务端和多个客户端连接,客户端可以互发消息

  • server服务端
/**
 * 可以直接使用 linux nc命令当做客户端
 * nc localhost 端口
 */
public class Server {
    private static Map<SocketChannel,String> clientMap = new HashMap<>();
    public static void main(String[] args) throws IOException {
        //打开服务器channel
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //设置非阻塞 即将使用selector
        serverSocketChannel.configureBlocking(false);
        //获取服务器的socket
        ServerSocket serverSocket = serverSocketChannel.socket();
        //绑定端口
        serverSocket.bind(new InetSocketAddress(8089));
        //打开一个多路复用器,使用一条线程处理客户端channel
        Selector selector = Selector.open();
        //注册服务器channel到
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        while (true){
            //阻塞获取channel事件
            //一旦调用了 select() 方法,并且返回值表明有一个或更多个通道就绪了
            int num = selector.select();
            /**
             * 获取到后 拿到多路复用器的SelectionKey 核心方法channel获取注册在起上的channel
             * SelectionKey 每次注册一个channel都会创建一个SelectionKey 其中常量定义channel状态
            **/
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            //对其中每一个SelectionKey进行操作
            selectionKeys.forEach(selectionKey->{
                    try {
                        //如果该服务器SelectionKey被接收
                        if(selectionKey.isAcceptable()){
                            //拿到服务器channel
                            ServerSocketChannel server = (ServerSocketChannel) selectionKey.channel();
                            SocketChannel client = null;
                            //拿到本次连接上服务器的客户端
                            client = server.accept();
                            client.configureBlocking(false);
                            //把客户端注册到多路复用器,监听客户端的可读事件
                            client.register(selector,SelectionKey.OP_READ);
                            //为每个客户端分配id
                            String key = "["+ UUID.randomUUID()+"]";
                            clientMap.put(client,key);
                            //如果SelectionKey读就绪,执行读操作
                        }else if(selectionKey.isReadable()){
                            //拿到channel
                            SocketChannel channel = (SocketChannel) selectionKey.channel();
                            //创建读buffer
                            ByteBuffer readBuffer = ByteBuffer.allocate(1024);
                            //读取channel中数据到读buffer
                            int read = channel.read(readBuffer);
                            String reMsg = "";
                            //如果有数据
                            if(read>0){
                                //翻转进行写操作
                                readBuffer.flip();
                                //制定解码集utf-8,对读buffer解码打印
                                Charset charset = Charset.forName("utf-8");
                                reMsg = String.valueOf(charset.decode(readBuffer).array());
                                System.out.println(clientMap.get(channel)+" receive: "+reMsg);
                            }else if(read==-1) channel.close();//如果客户端关闭就关闭客户端channel
                            //群发:发送数据到其他客户端channel
                            for(SocketChannel ch:clientMap.keySet()){
                                if(ch!=channel) {
                                    String key = clientMap.get(ch);
                                    ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
                                    writeBuffer.put(("来自"+key + ":" + reMsg).getBytes());
                                    writeBuffer.flip();
                                    ch.write(writeBuffer);
                                }
                            }
                        }

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

                }
            });
            selectionKeys.clear();//每次处理完一个SelectionKey的事件,把该SelectionKey删除
        }
    }
}复制代码

  • 客户端
public class Client {
    public static void main(String[] args) throws IOException {
        //打开客户端channel
        SocketChannel socketChannel = SocketChannel.open();
        //设置为非阻塞模式,可以配合selector使用
        socketChannel.configureBlocking(false);
        //打开selector
        Selector selector = Selector.open();
        //注册客户端channel到多路复用器,监听连接事件
        socketChannel.register(selector, SelectionKey.OP_CONNECT);
        //连接到指定地址
        socketChannel.connect(new InetSocketAddress("localhost",8089));
        while (true){
            try{
                    //执行selector方法,阻塞获取channel事件的触发
                    int num = selector.select();
                    //获取注册到多路复用器上的SelectionKey
                    Set<SelectionKey> selectionKeys = selector.selectedKeys();
                    //通过迭代器遍历SelectionKey
                    Iterator<SelectionKey> iterator = selectionKeys.iterator();
                    while (iterator.hasNext()) {
                        SelectionKey selectionKey = iterator.next();
                        //如果SelectionKey触发的事件为连接就绪
                        if(selectionKey.isConnectable()){
                            //拿到SelectionKey的客户端channel
                            SocketChannel client = (SocketChannel) selectionKey.channel();
                            if(client.isConnectionPending()){
                                //完成连接
                                client.finishConnect();
                                //新建一个写buffer
                                ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
                                //写入客户端连接成功消息
                                writeBuffer.put((client.toString()+":连接成功!").getBytes());
                                //翻转读写操作 执行写操作
                                writeBuffer.flip();
                                //写入buffer数据刅客户端
                                client.write(writeBuffer);
                                //开辟一个线程写,因为标准输入是阻塞的,当前线程不能阻塞写
                                ExecutorService executorService = Executors.newSingleThreadExecutor();
                                executorService.submit(()->{
                                    while (true){
                                        writeBuffer.clear();
                                        InputStreamReader reader = new InputStreamReader(System.in);
                                        BufferedReader br = new BufferedReader(reader);
                                        String msg = br.readLine();
                                        //每次读入一行,写入数据到buffer并且写入客户端channel
                                        writeBuffer.put(msg.getBytes());
                                        writeBuffer.flip();
                                        client.write(writeBuffer);
                                    }
                                });
                            }
                            //注册客户端可读事件到多路复用器
                            client.register(selector,SelectionKey.OP_READ);
                            //如果多路复用器上的SelectionKey处于读就绪状态
                        }else if(selectionKey.isReadable()){
                            //拿到SelectionKey触发相应事件对应的客户端channel,执行读操作
                            SocketChannel client = (SocketChannel) selectionKey.channel();
                            //创建一个新的读buffer,
                            ByteBuffer readBuffer = ByteBuffer.allocate(1024);
                            //从准备好读操作的channel中读取数据
                            int count = client.read(readBuffer);
                            if (count>0){
                                //转码并数据使用String保存且打印
                                String reMsg = new String(readBuffer.array(),0,count);
                                System.out.println(reMsg);
                            }else if(count==-1) client.close();//关闭客户端
                        }
                    }
                    selectionKeys.clear();//每次处理完一个SelectionKey的事件,把该SelectionKey删除
                }
            catch (Exception e){
                e.printStackTrace();
            }
        }
    }
}复制代码

  • 测试

1.创建一个服务端和三个客户端

2.客户端1,2,3分别发送数据

服务端拿到连接信息和三个客户端发送信息

客户端1先创建,拿到2,3连接信息和2,3发送信息

客户端2先于3创建,拿到3连接信息和1,3发送信息

客户端3最后创建,只能拿到1,2发送信息

3.此时再使用nc命令创建一个客户端

发送信息,客户端可以收到

客户端2发送信息,该终端客户端也可以收到

NIO案例—跨端口传输数据—MultiServer

实现目标:服务端监听两个端口,一个8089,一个8090,8089只有唯一的一个主客户端A连接,8090有多个客户端B连接,客户端A接收多个客户端B连接的发送的消息,实现跨端口的消息转发

  • 服务端

我们先看服务端,服务端首先需要监听两个端口,我们创建两个服务端channel;服务端接收到连接后监听客户端B的发送数据事件(也就是客户端writable服务端readable事件);拿到客户端B的消息后,把它发送到客户端A

服务端怎么发送数据到客户端A?

保存一个客户端channel集合,为不同端口客户端分配不同的id的结尾部分,客户端A分配为wxq],客户端B分配为gzh],在他们channel创建的时候保存到HashMap中,channel作为key,id作为值保存

下面说一下服务端流程:

  1. 创建两个服务端channel,绑定不同端口
  2. 创建一个多路复用器selector,把两个服务端注册到selector上,并监听acceptable事件
  3. 执行selector.select()方法,拿到SelectionKey集合,对不同事件做不停处理
    1. 如果事件为接收就绪,通过SelectionKey.channel()方法拿到服务端channel,根据端口不同注册不同的监听事件,如果是8090的,说明是客户端B的连接完成,拿到客户端B的channel,监听它的可读事件,并且分配id后缀为gzh]并且保存;如果是8089端口的服务端channel,说明是客户端A的连接完成,客户端客户端A的channel,监听它的可写事件,并且分配id后缀为wxq],保存到hashmap
    2. 如果事件是读就绪,说明客户端B已经完后数据的写操作,可以读取客户端B的数据,执行读取;首先把数据读取并写入到readBuffer,使用new String(readBuffer.array()创建即将发送的msg,遍历客户端channel的key,如果后缀为wxq],说明是客户端A,则把数据写入writeBuffer中,并把数据写入客户端A的channel中
  4. 每次SelectionKey的事件执行完毕,把该SelectionKey删除

代码:

public class Server {
    private static int CAPACITY = 1024;
    private static ByteBuffer readBuffer = ByteBuffer.allocate(CAPACITY);
    private static ByteBuffer writeBuffer = ByteBuffer.allocate(CAPACITY);
    private static Map<SocketChannel,String> clientMap = new HashMap<>();

    public static void main(String[] args) throws IOException {
        ServerSocketChannel serverSocketChannelWxq = ServerSocketChannel.open();
        ServerSocketChannel serverSocketChannelGzh = ServerSocketChannel.open();
        serverSocketChannelGzh.configureBlocking(false);
        serverSocketChannelWxq.configureBlocking(false);
        ServerSocket serverSocketWxq = serverSocketChannelWxq.socket();
        ServerSocket serverSocketGzh = serverSocketChannelGzh.socket();
        serverSocketWxq.bind(new InetSocketAddress(8089));
        System.out.println("监听8089:微信墙服务端口");
        serverSocketGzh.bind(new InetSocketAddress(8090));
        System.out.println("监听8090:公众号服务端口");
        Selector selector = Selector.open();
        serverSocketChannelWxq.register(selector, SelectionKey.OP_ACCEPT);
        serverSocketChannelGzh.register(selector, SelectionKey.OP_ACCEPT);
        while (true){
            int num = selector.select();
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            selectionKeys.forEach(selectionKey->{
                try {
                    if(selectionKey.isAcceptable()){
                        ServerSocketChannel server = (ServerSocketChannel) selectionKey.channel();
                        SocketChannel client = null;
                        client = server.accept();
                        client.configureBlocking(false);
                        String key = "";
                        if(server==serverSocketChannelGzh) {//如果是公众号server,注册其客户端的可读事件
                            client.register(selector, SelectionKey.OP_READ);
                            key = "["+ UUID.randomUUID()+":gzh]";
                        }else if(server==serverSocketChannelWxq){//如果是
                            client.register(selector,SelectionKey.OP_WRITE);
                            key = "["+ UUID.randomUUID()+":wxq]";
                        }
                        System.out.println(key+":连接成功!");
                        clientMap.put(client,key);
                    }else if(selectionKey.isReadable()){
                        SocketChannel channel = (SocketChannel) selectionKey.channel();
                        readBuffer.clear();
                        int read = 0;
                        while(true){
                            int byteRead = channel.read(readBuffer);
                            if(byteRead<=0){
                                break;
                            }
                            readBuffer.flip();
                            channel.write(readBuffer);
                            read += byteRead;
                            readBuffer.clear();
                        }
                        String reMsg = new String(readBuffer.array(),0,read);
                        System.out.println(clientMap.get(channel)+" send to wxq: "+reMsg);
                        //写入微信墙服务
                        for(SocketChannel ch:clientMap.keySet()){
                            if(ch!=channel) {
                                String key = clientMap.get(ch);
                                if(key.endsWith("wxq]")) {
                                    writeBuffer.clear();
                                    writeBuffer.put(("来自" + clientMap.get(channel) + ":" + reMsg).getBytes(StandardCharsets.UTF_8));
                                    writeBuffer.flip();
                                    ch.write(writeBuffer);
                                }
                            }
                        }
                    }

                } catch (IOException e) {
                    e.printStackTrace();
                }
            });
            selectionKeys.clear();//每次处理完一个SelectionKey的事件,把该SelectionKey删除
        }
    }
}复制代码

到此,服务端写完了,你就可以使用linux或者win下的nc命令连接到服务端,模拟客户端A和客户端B发送消息

客户端发送消息后会会写一条是因为我在接收到消息后把消息写入客户端B的buffer中了

  • 客户端B—发送消息

客户端B负责发送消息,主要事件就是负责写数据

流程:

  1. 创建一个客户端channelSocketChannel,打开一个多留复用器selector,绑定可连接事件,连接到服务端监听的8090端口
  2. 执行selector.select()方法,处理连接就绪写就绪两个事件
    1. 如果事件为连接就绪,只需要拿到channel,执行finishConnect方法完成连接,并且注册监听事件为可写事件
    2. 如果事件为写就绪,执行写操作,使用标准输入从控制台读取输入并且写入writebuffer中,通过channel.write()方法写入数据到客户端
  3. 清理事件的SelectionKey

代码:

public class GzhClient {
    public static void main(String[] args) throws IOException {
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.configureBlocking(false);
        Selector selector = Selector.open();
        socketChannel.register(selector, SelectionKey.OP_CONNECT);
        socketChannel.connect(new InetSocketAddress("localhost",8090));
        while (true){
            try{
                int num = selector.select();
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey selectionKey = iterator.next();
                    if(selectionKey.isConnectable()){
                        SocketChannel client = (SocketChannel) selectionKey.channel();
                        if(client.isConnectionPending()){
                            client.finishConnect();
                        }
                        client.register(selector,SelectionKey.OP_WRITE);
                    }else if(selectionKey.isWritable()){
                        SocketChannel client = (SocketChannel) selectionKey.channel();
                        ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
                        writeBuffer.clear();
                        InputStreamReader reader = new InputStreamReader(System.in);
                        BufferedReader br = new BufferedReader(reader);
                        String msg = br.readLine();
                        //每次读入一行,写入数据到buffer并且写入客户端channel
                        writeBuffer.put(msg.getBytes());
                        writeBuffer.flip();
                        client.write(writeBuffer);
                    }
                }
                selectionKeys.clear();//每次处理完一个SelectionKey的事件,把该SelectionKey删除
            }
            catch (Exception e){
                e.printStackTrace();
            }
        }
    }
}
复制代码

  • 客户端A—接收服务端转发消息

客户端A负责发送消息,主要事件就是负责读数据

流程:

  1. 创建一个客户端channelSocketChannel,打开一个多留复用器selector,绑定可连接事件,连接到服务端监听的8089端口
  2. 执行selector.select()方法,处理连接就绪读就绪两个事件
    1. 如果事件为连接就绪,只需要拿到channel,执行finishConnect方法完成连接,并且注册监听事件为可写事件
    2. 如果事件为读就绪,执行读操作,把channel中数据使用read()方法读取到readBuffer中,通过new String(readBuffer.array()方法接收String类型数据,并且打印到控制台
  3. 清理事件的SelectionKey

代码:

public class WxQClient {
    public static void main(String[] args) throws IOException {
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.configureBlocking(false);
        Selector selector = Selector.open();
        socketChannel.register(selector, SelectionKey.OP_CONNECT);
        socketChannel.connect(new InetSocketAddress("localhost",8089));
        while (true){
            try{
                int num = selector.select();
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey selectionKey = iterator.next();
                    if(selectionKey.isConnectable()){
                        SocketChannel client = (SocketChannel) selectionKey.channel();
                        if(client.isConnectionPending()){
                            client.finishConnect();
                        }
                        client.register(selector,SelectionKey.OP_READ);
                    }else if(selectionKey.isReadable()){
                        SocketChannel client = (SocketChannel) selectionKey.channel();
                        ByteBuffer readBuffer = ByteBuffer.allocate(1024);
                        int count = client.read(readBuffer);
                        if (count>0){
                            String reMsg = new String(readBuffer.array(),0,count);
                            System.out.println(reMsg);
                        }else if(count==-1) client.close();//关闭客户端
                    }
                }
                selectionKeys.clear();//每次处理完一个SelectionKey的事件,把该SelectionKey删除
            }
            catch (Exception e){
                e.printStackTrace();
            }
        }
    }
}复制代码

至此,我们服务端和客户端AB都已经完后,现在我们测试一下

  1. 启动服务端,启动一个WxQClient也就是ClientA,启动两个GzhClient,也就是ClientB

服务端显示连接成功

  1. 客户端B发送消息

服务端接收到消息并打印,并转发到客户端A,客户端A打印消息

猜你喜欢

转载自juejin.im/post/5dce325251882510793d47ad