Netty技术

一.概述

背景:
        由于分布式系统大行其道,并且分布式系统的根基在于网络编程,因为各个分布式系统之间要进行相互通信。而Netty是Java领域中关于网络编程领域的高性能框架。netty的底层框架是NIO,所以本文会介绍NIO的知识。

二.NIO基础

1.三大组件

        如上图所示,组成NIO的有三大组件,Channel,Buffer和Selector。
        Channel:是数据读写的双向通道,可以将数据读入到buffer中,也可以将buffer中的数据写入到Channel中。常见的Channel有:FileChannel(文件传输),DatagramChannel(UDP传输),SocketChannel(TCP传输,客户端和服务器端都能用),ServerSocketChannel(TCP传输,专用于服务端)。
        Buffer:就是用来进行缓存的组件。常见的buffer有ByteBuffer(MappedByteBuffer, DirectByteBuffer, HeapByteBuffer)使用字节来缓冲数据。
        Selector:(1)在原本的多线程服务器的设计中,系统为每一个socket都开启了一个线程,但是这样做的话内存占用高、线程上下文切换成本高、只适合连接数少的场景,相当于在餐厅里给每个客人都配备了一个服务员。(2)后来人们提出了线程池的技术。缺点就是在阻塞模式下,线程仅能处理一个Socket连接,仅适合短链接的场景。相当于一个服务员只有在当前客人吃饭结束后才能去服务别的客人。(3) 新版selector的设计就是使用一个线程来管理多个channel,获取这些channel上发生的事情,这些channel发生在非阻塞的模式下,不会让线程吊死在一个channel上。适合连接数特别多,但是流量低的场景。当selector管理的某个channel发生读写就绪事件,selector就能监听到,然后返回这个事件交给thread来进行处理。

2.ByteBuffer基本使用

快速入门

package com.guguo.test.netty;

import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class TestByteBuffer {
    public static void main(String[] args) throws IOException {
        //获得通道
        FileChannel channel =
                new FileInputStream("document\\data.txt")
                .getChannel();
        ByteBuffer buffer = ByteBuffer.allocate(10);//得到缓冲区,并命令缓冲区大小为10
        while(channel.read(buffer)!=-1){//从channel中读数据,向buffer中写
            buffer.flip();//将buffer切换成读状态
            while(buffer.hasRemaining()){
                byte b = buffer.get();
                System.out.println((char)b);
            }
            buffer.clear();将buffer切换成写状态
        }
    }
}

内部结构

        ByteBuffer内部有以下比较重要的属性:capacity,position,limit。如果说Buffer是一个数组,那么capacity表示这个数组的总大小,position表示当前读写的指针位置,limit表示读写的限制。写模式下,capacity=limit,只要不超过capacity想写多少写多少,position表示当前正在写入的位置。
        写模式切换到读模式下,position归0,limit移动到之前position的位置。
        读模式切换到写模式时,(1)如果使用clear()方法,是清空数组,从0开始写(2)compact方法是把未读完的部分向前压缩,然后切换至写模式,从上次未读完的部分末尾开始追加写。

常见方法

ByteBuffer.allocate(20);//使用java堆内存,读写效率低,会受到垃圾回收机制影响。ByteBuffer.allocateDirect(20);//使用java的直接内存,读写效率高,不会收到GC影响,但是分配内存的效率较低,可能会造成内存泄漏。

//向buffer写数据的两种方式
(1)int read=channel.read(buffer);
(2)buffer.put((byte)127);    buffer.put(new byte[]{'a','b','c'});

//从buffer读数据的两种方式
(1)int write=channel.write(buffer);
(2)byte b=buf.get(); //get()方法会让读指针向后走,如果想重复获取数据,可以使用rewind方法将position变成0。

//字符串和BateBuffer之间的转换方法
(1)ByteBuffer buffer=ByteBuffer.allocate(20);
          buffer.put("hello".getBytes());
(2)ByteBuffer buffer=StandardCharsets.UTF-8.encode("hello");
(3)ByteBuffer buffer=ByteBuffer.wrap("hello".getBytes())
(4)String str=StandardCharsets.UTF-8.decode(buffer).toString();//此时得是读状态

//特殊方法
(1)mark(); //做一个标记,记录position的位置
(2)reset(); //是将position重置到mark的位置
(3)get(i); //按照索引去读,不会导致读指针后移。

黏包半包

在数据传输的过程中,一般会把多条信息合并到一起进行传输,这种行为称为黏包。但是不是所有时候都在正好黏整数个包,如果内存不够,有可能会把一条信息拆成两个包来传输。比如:
hello\n   world\n   你好\n                               ----->                            hello\nworld\n你    好\n

3.FileChannel基本使用

获取:

不能直接打开FileChannel,必须通过FileInputStream,FileOutputStream或者RandomAccessFile来获取FileChannel,他们都有getChannel方法。
1.通过FileInputStream获取的FileChannel只能读。2.通过FileOutputStream获取的FileChannel只能写。3.通过RandomAccessFile获取的FileChannel能否读写根据构造RandomAccessFile时的读写模式决定。

读取:

会从channel读取数据填充ByteBuffer,返回值表示读到了多少字节,-1表示到达了文件的末尾。
int readBytes=channel.read(buffer);

写入:

这里使用循环是因为write方法并不能保证一次九江buffer中的内容全部写进channel。

while(buffer.hasRemaining()){
        channel.write(buffer);
)

关闭:

channel必须关闭,不过调用了FileInputStream,FileOutputStream或者RandomAccessFile中的close方法会间接调用channel的close方法。

常见方法:

size方法,获取文件的大小。
position()方法,获取当前读到的位置。
force(true)方法,可以将文件内容强制立刻写进磁盘,不会再进行数据缓存。

4.selector选择器

NIO阻塞模式

1.默认accept()方法是阻塞方法,方法执行到这里,如果没有客户端来进行连接申请,那么程序就停止在这里了。
2.read(buffer)方法也是阻塞方法,如果客户端连接了,但是没发送数据,程序就会一直停在这里。
3.如果你停在了accept()方法这里,那么即使客户端A连接过,他发送的数据也过不来

package com.guguo.test.netty;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
//服务端代码
public class selectorDemo {
    public static void main(String[] args) throws IOException {
        ByteBuffer buffer = ByteBuffer.allocate(16);
        //1.创建服务器
        ServerSocketChannel ssc = ServerSocketChannel.open();
        //2.绑定监听接口
        ssc.bind(new InetSocketAddress(8080));//客户端向这个端口发送消息,服务端就能收到
        //3.连接集合,用来存储申请连接的客户端
        ArrayList<SocketChannel> socketChannels = new ArrayList<>();

        while(true){
            //4.该方法是接受客户端发来的请求,建立连接。返回值sc用来和客户端通信
            SocketChannel sc = ssc.accept();
            socketChannels.add(sc);

            for (SocketChannel socketChannel:socketChannels) {
                //5.将socketChannel中的数据读取到buffer中
                socketChannel.read(buffer);
                buffer.flip();//换成读模式
                String string = StandardCharsets.UTF_8.decode(buffer).toString();
                System.out.println(string);
                buffer.clear();//换成写模式
            }
        }
    }

}
package com.guguo.test.netty;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
//客户端代码
public class Socket {
    public static void main(String[] args) throws IOException {
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(new InetSocketAddress("localhost",8080));
        socketChannel.write(ByteBuffer.wrap("hello".getBytes()));
    }
}

NIO非阻塞模式

sc.configureBlocking(false);将SocketChannel设置成非阻塞模式,此时read方法将不再是阻塞方法。如果没读到数据,返回值是0。
ssc.configureBlocking(false);//将ServerSocketChannel设置成非阻塞模式,此时accept方法不再是阻塞方法,如果没有客户端发起链接申请,那么返回值是null。
package com.guguo.test.netty;

import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
@Slf4j
public class selectorDemo2 {
    public static void main(String[] args) throws IOException {
        ByteBuffer buffer = ByteBuffer.allocate(16);
        //1.创建服务器
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.configureBlocking(false);//将ServerSocketChannel设置成非阻塞模式,此时accept方法不再是阻塞方法,如果没有客户端发起链接申请,那么返回值是null
        //2.绑定监听接口
        ssc.bind(new InetSocketAddress(8080));//客户端向这个端口发送消息,服务端就能收到
        //3.连接集合,用来存储申请连接的客户端
        ArrayList<SocketChannel> socketChannels = new ArrayList<>();

        while(true){
            //4.该方法是接受客户端发来的请求,建立连接。返回值sc用来和客户端通信
            SocketChannel sc = ssc.accept();
            if(sc!=null){
                sc.configureBlocking(false);将SocketChannel设置成非阻塞模式,此时read方法将不再是阻塞方法。如果没读到数据,返回值是0
                socketChannels.add(sc);
                log.debug("链接完毕。。。"+sc);
            }

            //5.将socketChannel中的数据读取到buffer中
            for (SocketChannel socketChannel:socketChannels) {
                int read=socketChannel.read(buffer);
                if(read>0){
                    buffer.flip();//换成读模式
                    String string = StandardCharsets.UTF_8.decode(buffer).toString();
                    System.out.println(string);
                    buffer.clear();//换成写模式
                }
            }
        }
    }
}
package com.guguo.test.netty;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;

public class Socket {
    public static void main(String[] args) throws IOException {
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(new InetSocketAddress("localhost",8080));
       // socketChannel.write(ByteBuffer.wrap("hello".getBytes()));

    }
}

selector强势加入

共有7个问题:

1.事件:
一共有四种事件:accept(在服务端,有连接请求时触发),connect(在客户端,连接建立后触发),read(可读事件),write(可写事件)。
2.事件处理问题:
在selector调用select()方法,得到某个事件。但是如果不对这个事件进行处理,下面的while循环就会一直循环下去。因为selector会觉得,你还没处理完,你要接着处理,然后又把这个事件加进selectedKeys()里面去。所以要么进行处理,要么使用key.cancel()方法把事件取消。
3.key值不移除导致空指针问题:
selector.selectedKeys();这个方法会得到目前有事件发生的key的集合,但是他不会主动在事件处理完之后删除key。在下次循环中,如果拿到该key,但是该key上又没有事件发生,就会返回一个null。所以在处理完事件之后记得将key移除。
4.客户端断开问题:
在客户端强制断开的时候,客户端会给服务端发送一个读事件,但是又没有真正的数据,此时就会产生一个IO异常,并且导致服务端停止运行。我们要及时捕捉到异常,然后把key取消。同样客户端正常断开时候,我们也要及时处理,处理的时机是当read方法的返回值是0时。

package com.guguo.test.netty;

import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Set;

@Slf4j
public class selectorDemo3 {
    public static void main(String[] args) throws IOException {
        //1.创建选择器,管理多个channel
        Selector selector = Selector.open();

        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.configureBlocking(false);
        ssc.bind(new InetSocketAddress(8080));

        //2.建立selector和channel的联系
        SelectionKey registerKey = ssc.register(selector, 0, null);
        //3.给这个registerKey指明它关注的事件
        registerKey.interestOps(SelectionKey.OP_ACCEPT);//关注了连接请求事件

        while(true){
            //4.如果没有事情发生,那么这里阻塞。如果有事件发生,方法执行下去。
            selector.select();
            //5.处理事件,得到不同事件触发后才能返回的key。
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
            while (keyIterator.hasNext()){
                SelectionKey key = keyIterator.next();
                keyIterator.remove();//处理事件之后,记得把key移除
                //5.1如果发生了accept事件
                if(key.isAcceptable()){
                    ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                    SocketChannel sc = channel.accept();
                    sc.configureBlocking(false);
                    SelectionKey sckey = sc.register(selector, 0, null);
                    sckey.interestOps(SelectionKey.OP_READ);
                }
                //如果发生了read事件
                if(key.isReadable()){
                    try{
                        SocketChannel channel = (SocketChannel) key.channel();
                        ByteBuffer buffer = ByteBuffer.allocate(16);
                        int read = channel.read(buffer);
                        if(read==-1){//如果客户端正常关闭,这个key要及时取消掉
                            key.cancel();
                        }
                        if(read>0){
                            buffer.flip();//换成读模式
                            String string = StandardCharsets.UTF_8.decode(buffer).toString();
                            System.out.println(string);
                            buffer.clear();//换成写模式
                        }
                    }catch(IOException e){
                        e.printStackTrace();
                        key.cancel();//在客户端断开后,及时把key取消掉
                    }

                }
            }

        }

    }

}

5.处理消息边界问题
        产生原因:比如需要传递的字符占三个字节,但是接收的时候用两个字节来接收,这样这个字就会被分成两部分,就会产生乱码。
        三种情况:第一种是ByteBuffer的长度没有传过来的信息长,这个时候需要把ByteBuffer的长度翻倍,来把整个的消息都存进去。第二种,ByteBuffer的长度相对较长,但是能存下1条信息,不一定能完整地存下第2条信息。第三种,ByteBuffer的长度过长,这个时候就会造成黏包的问题。
        解决方法:(1)以最长的信息规定一个固定的传送长度。如果规定了长度是10,那么即使传过来两个长度是5的信息,也要分两次来传输。缺点很明显,就会导致资源的浪费嘛。(2)客户端传过来的不同消息用\n进行分割,服务端检测到\n之后,就会创建一个新的buffer来接受新一条消息。缺点就是效率不高,因为要对信息中的每个字符进行判断,看看有没有\n。(3)在每条消息的前几个字节规定好该条消息是多长,服务端就知道该条消息多长了,就读取固定长度的信息。这种被叫做LTV格式,Http2.0就是这种格式。

6.ByteBuffer的大小分配问题
        (1)每个socketChannel都要一个专门属于自己的ByteBuffer来进行数据的传输,这个ByteBuffer以附件的形式注册到key中。
        (2)ByteBuffer不能太大,不然链接太多的时候,就会占用大量的内存,因此就要设计大小靠边的ByteBuffer。一种思路是首先分配一个比较小的buffer,比如4k,如果不够再给你个8k的空间。优点是消息连续容易处理,缺点是数据拷贝消耗性能。下面的方法就是这种思想的实现。另一种思路是使用多个数组组成的buffer,一个数组不够,就把多的数据写到新的数组中,和前面的区别是这种方式消息存储不连续,优点是避免了拷贝要引起的性能损耗。

//使用第二种方法解决消息边界问题:(此时buffer缓冲区的长度足够长)
private static void split(ByteBuffer buffer){
        buffer.flip();//切换成读模式
        for(int i=0;i<buffer.limit();i++){
            if(buffer.get(i)=='\n'){
                int length=i+1-buffer.position();
                ByteBuffer target = ByteBuffer.allocate(length);
                for(int j=0;j<length;j++){
                    target.put(buffer.get());
                }
                target.flip();
                String string = StandardCharsets.UTF_8.decode(target).toString();
                System.out.print(string);
            }
        }
        buffer.compact();//把读完的数据清理走,剩下的数据堆到前面去
    }
package com.guguo.test.netty;

import lombok.extern.slf4j.Slf4j;

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.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Set;

/*
* 处理消息队列问题,遇到需要扩容的问题
* */

@Slf4j
public class selectorDemo4 {

    private static void split(ByteBuffer buffer){
        buffer.flip();//切换成读模式
        for(int i=0;i<buffer.limit();i++){
            if(buffer.get(i)=='\n'){
                int length=i+1-buffer.position();
                ByteBuffer target = ByteBuffer.allocate(length);
                for(int j=0;j<length;j++){
                    target.put(buffer.get());
                }
                target.flip();
                String string = StandardCharsets.UTF_8.decode(target).toString();
                System.out.print(string);
            }
        }
        buffer.compact();//把读完的数据清理走,剩下的数据堆到前面去
    }


    public static void main(String[] args) throws IOException {
        //1.创建选择器,管理多个channel
        Selector selector = Selector.open();

        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.configureBlocking(false);
        ssc.bind(new InetSocketAddress(8080));

        //2.建立selector和channel的联系
        SelectionKey registerKey = ssc.register(selector, 0, null);
        //3.给这个registerKey指明它关注的事件
        registerKey.interestOps(SelectionKey.OP_ACCEPT);//关注了连接请求事件

        while(true){
            //4.如果没有事情发生,那么这里阻塞。如果有事件发生,方法执行下去。
            selector.select();
            //5.处理事件,得到不同事件触发后才能返回的key。
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
            while (keyIterator.hasNext()){
                SelectionKey key = keyIterator.next();
                keyIterator.remove();//处理事件之后,记得把key移除
                //5.1如果发生了accept事件
                if(key.isAcceptable()){
                    ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                    SocketChannel sc = channel.accept();
                    sc.configureBlocking(false);
                    ByteBuffer buffer = ByteBuffer.allocate(16);
                    SelectionKey sckey = sc.register(selector, 0, buffer);//同时在这个key上,注册一个只能该channel用的附件
                    sckey.interestOps(SelectionKey.OP_READ);
                }
                //如果发生了read事件
                if(key.isReadable()){
                    try{
                        SocketChannel channel = (SocketChannel) key.channel();
                        ByteBuffer buffer = (ByteBuffer)key.attachment();//获得这个附件
                        int read = channel.read(buffer);
                        if(read==-1){
                            key.cancel();
                        }
                        if(read>0){
                            split(buffer);
                            //每写一个字符,position都会+1,如果position==limit,说明写满了还没有找到\n,就需要进行扩容
                            if(buffer.position()==buffer.limit()){
                                ByteBuffer newBuffer = ByteBuffer.allocate(buffer.capacity()*2);//进行扩容
                                buffer.flip();
                                newBuffer.put(buffer);
                                key.attach(newBuffer);
                            }
                        }
                    }catch(IOException e){
                        e.printStackTrace();
                        key.cancel();
                    }

                }
            }

        }

    }

}

7.可写事件:

当写入的缓冲区满的时候,调用SocketChannel的write(buffer)方法写不进去东西的。所以我们可以在写缓冲区满的时候不要去反复去写了。可以去看看别的SocketChannel中有没有发生可读事件,去处理读事件,处理完之后再尝试去写。

package com.guguo.test.netty;

import com.google.common.base.Charsets;
import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Set;

/*
* 处理可写问题
* */

@Slf4j
public class writeServer {
    public static void main(String[] args) throws IOException {
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.configureBlocking(false);
        Selector selector = Selector.open();
        ssc.register(selector,SelectionKey.OP_ACCEPT,null);
        ssc.bind(new InetSocketAddress(8080));


        while(true){
            selector.select();
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()){
                SelectionKey key = iterator.next();
                iterator.remove();
                if(key.isAcceptable()){
                    ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                    SocketChannel sc = channel.accept();
                    sc.configureBlocking(false);
                    SelectionKey scKey = sc.register(selector, 0, null);
                    scKey.interestOps(SelectionKey.OP_READ);
                    //1.我们往客户端写点东西
                    StringBuilder stringBuilder = new StringBuilder();
                    for (int i = 0; i < 300000000; i++) {
                        stringBuilder.append("a");
                    }
                    ByteBuffer buffer=StandardCharsets.UTF_8.encode(stringBuilder.toString());
                    //2.进行一次写操作
                    int write = sc.write(buffer);
                    System.out.println(write);

                    //3.判断buffer是否有剩余内容没有写完
                    if(buffer.hasRemaining()){
                        //4.关注可写事件
                        scKey.interestOps(scKey.interestOps()+SelectionKey.OP_WRITE);//在原来关注的事件上增加事件
                        //5.把未写完的数据挂到附件上
                        scKey.attach(buffer);
                    }
                }else if(key.isWritable()){
                    ByteBuffer buffer = (ByteBuffer) key.attachment();
                    SocketChannel sc1 = (SocketChannel) key.channel();
                    int write1 = sc1.write(buffer);
                    System.out.println(write1);
                    //6.清理操作
                    if(!buffer.hasRemaining()){
                        key.attach(null);//如果内容都写完了,那么就需要清除buffer,并且取消关注可写事件
                        key.interestOps(key.interestOps()- SelectionKey.OP_WRITE);
                    }
                }
            }

        }

    }

}
package com.guguo.test.netty;

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

public class writeClient {
    public static void main(String[] args) throws IOException {
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(new InetSocketAddress("localhost",8080));
        //接收数据
        int count=0;
        while(true){
            ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024);
            count+=socketChannel.read(buffer);
            System.out.println(count);
            buffer.clear();
        }
    }
}

多线程优化

        之前的都是单线程,我们现在使用多线程对刚才的事件进行优化。前面的代码只有一个选择器,现在线程多了之后我们可以搞多个选择器。分两组选择器:单线程配一个选择器,专门处理accept事件;然后创建cpu核心数的线程,每个线程配一个选择器,轮流处理read事件。

        根据上文,我们创建选择器boss,专门处理accept事件;然后创建一个worker类,该类会额外启动一个线程,专门用来处理读写事件。

        问题1:但是我们写完多线程代码之后会出现问题:注册方法中会开启一个线程,然后这个线程中selector.select();这个方法在没有事件触发时候会阻塞,影响下面sc.register(...);导致方法无法执行。解决方法是使用任务队列把sc.register(...);方法放到worker类的线程run方法中去。修改前和修改后的代码如下:

package com.guguo.test.netty;

import com.google.common.base.Charsets;
import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Set;


@Slf4j
public class MultiThreadSever {
    static class Worker{
        private Thread thread;
        private Selector selector;
        private String name;
        private boolean start=false;//此时表示还没有进行初始化

        public Worker(String name) {
            this.name = name;
        }

        public void register() throws IOException {
            if(!start){
                //给选择器赋初值
                selector=Selector.open();
                //开启一个线程,并且规定这个线程要干什么事
                thread = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        while(true){
                            try {
                                selector.select();
                                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                                while (iterator.hasNext()){
                                    SelectionKey key = iterator.next();
                                    iterator.remove();
                                    if(key.isReadable()){//读过来数据,然后打印
                                        ByteBuffer buffer = ByteBuffer.allocate(16);
                                        SocketChannel channel = (SocketChannel) key.channel();
                                        log.info("读到了数据"+channel.getRemoteAddress());
                                        channel.read(buffer);
                                        buffer.flip();
                                        String str = Charsets.UTF_8.decode(buffer).toString();
                                        System.out.println(str);
                                        buffer.clear();
                                    }
                                }
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                });
                thread.setName(name);
                thread.start();
                start=true;
            }
        }
    }

    public static void main(String[] args) throws IOException {

        Thread.currentThread().setName("boss");//为当前进程命名
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.configureBlocking(false);
        ssc.bind(new InetSocketAddress(8080));

        Selector boss = Selector.open();
        SelectionKey sscKey = ssc.register(boss, 0, null);
        sscKey.interestOps(SelectionKey.OP_ACCEPT);

        //1.创建固定数量的worker对象,然后进行初始化
        Worker worker = new Worker("worker-0");

        while(true){
            boss.select();
            Iterator<SelectionKey> keyIterator =boss.selectedKeys().iterator();
            while (keyIterator.hasNext()){
                SelectionKey key = keyIterator.next();
                keyIterator.remove();
                if(key.isAcceptable()){
                    ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                    SocketChannel sc = channel.accept();
                    sc.configureBlocking(false);
                    log.info("建立了连接"+sc.getRemoteAddress());
                    //关联selector
                    log.info("注册前"+sc.getRemoteAddress());
                    worker.register();//这里注册方法中会开启一个多线程,然后这个多线程中selector.select();这个方法会阻塞,影响下面sc.register(...);导致该方法无法执行
                    sc.register(worker.selector,SelectionKey.OP_READ,null);
                    log.info("注册后"+sc.getRemoteAddress());
                }

            }

        }

    }

}
package com.guguo.test.netty;

import com.google.common.base.Charsets;
import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;


@Slf4j
public class MultiThreadSever {
    static class Worker{
        private Thread thread;
        private Selector selector;
        private String name;
        private boolean start=false;//此时表示还没有进行初始化
        private ConcurrentLinkedQueue<Runnable> queue=new ConcurrentLinkedQueue<>();

        public Worker(String name) {
            this.name = name;
        }

        public void register(SocketChannel sc) throws IOException {
            if(!start){
                //给选择器赋初值
                selector=Selector.open();
                //开启一个线程,并且规定这个线程要干什么事
                thread = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        while(true){
                            try {
                                selector.select();
                                //在这个位置,从队列中把任务整出来,然后执行
                                Runnable task = queue.poll();
                                if (task!=null){
                                    task.run();
                                }
                                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                                while (iterator.hasNext()){
                                    SelectionKey key = iterator.next();
                                    iterator.remove();
                                    if(key.isReadable()){//读过来数据,然后打印
                                        ByteBuffer buffer = ByteBuffer.allocate(16);
                                        SocketChannel channel = (SocketChannel) key.channel();
                                        log.info("读到了数据"+channel.getRemoteAddress());
                                        channel.read(buffer);
                                        buffer.flip();
                                        String str = Charsets.UTF_8.decode(buffer).toString();
                                        System.out.println(str);
                                        buffer.clear();
                                    }
                                }
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                });
                thread.setName(name);
                thread.start();
                start=true;
            }
            //向队列里添加了一个任务,但是此时这个任务只是添加进去了,但是此时并没有执行
            queue.add(()->{
                try {
                    sc.register(selector,SelectionKey.OP_READ,null);
                } catch (ClosedChannelException e) {
                    e.printStackTrace();
                }
            });
            selector.wakeup();

        }
    }

    public static void main(String[] args) throws IOException {

        Thread.currentThread().setName("boss");//为当前进程命名
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.configureBlocking(false);
        ssc.bind(new InetSocketAddress(8080));

        Selector boss = Selector.open();
        SelectionKey sscKey = ssc.register(boss, 0, null);
        sscKey.interestOps(SelectionKey.OP_ACCEPT);

        //1.创建固定数量的worker对象,然后进行初始化
        Worker worker = new Worker("worker-0");

        while(true){
            boss.select();
            Iterator<SelectionKey> keyIterator =boss.selectedKeys().iterator();
            while (keyIterator.hasNext()){
                SelectionKey key = keyIterator.next();
                keyIterator.remove();
                if(key.isAcceptable()){
                    ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                    SocketChannel sc = channel.accept();
                    sc.configureBlocking(false);
                    log.info("建立了连接"+sc.getRemoteAddress());
                    //关联selector
                    log.info("注册前"+sc.getRemoteAddress());
                    worker.register(sc);//这里注册方法中会开启一个多线程,然后这个多线程中selector.select();这个方法会阻塞,影响下面sc.register(...);导致该方法无法执行
                    log.info("注册后"+sc.getRemoteAddress());
                }

            }

        }

    }

}

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值