Netty学习笔记(一)——nio基础

Netty简单认识:

  1) Netty 是由JBOSS 提供的一个Java 开源框架。

  2) Netty 是一个异步的、基于事件驱动的网络应用框架,用以快速开发高性能、高可靠性的网络I0 程序。

  3) Netty 主要针对在TCP协议下的使用

  4)Netty本质是- 个NIO框架,适用于服务器通讯相关的多种应用场景

 

Netty应用:

  https://netty.io/wiki/related-projects.html这里面是和netty有关的框架

  Netty应用于网络间的通信,如阿里的dubbo框架,应用于服务之间的调用;谷歌的grpc框架;

 

I/O模型:

  netty是基于nio开发,所以我们需要了解java中的I/O模型:

  • Bio:同步阻塞io,服务器实现模式为一 一个连接-一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理

  如果这个连接不做任何事情会造成不必要的线程开销,适用于连接数小的和固定的,编程简单。

 

 

 

  • Nio:同步非阻塞,JavaNIO :同步非阻塞, 服务器实现模式为- -个线程处理多个请求(连接),即客户端发送的连接请求

  都会注册到多路复用器上,多路复用器轮询到连接有I/O请求就进行处理,适用于连接数多的连接时间短的,编程复杂。(1.4以后版本)

 

 

 

  • Aio:异步非阻塞,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,

  一般适用于连接数较多且连接时间较长的应用,适用于连接数多的和连接时间长的,编程复杂(1.7以后版本)

 

bio:

public class ServerClient {
    public static void main(String[] args) throws IOException {
        //创建自定义线程池
        ExecutorService executorService = new ThreadPoolExecutor(
                3,
                7,
                2000,
                TimeUnit.MILLISECONDS,
                new LinkedBlockingDeque<>(3),
                new ThreadPoolExecutor.CallerRunsPolicy());

        //启动服务端
        ServerSocket serverSocket = new ServerSocket(8086);
        System.out.println("服务端启动,等待连接。。。。。。。");

        while(true){
            //等待连接
            final Socket socket = serverSocket.accept();
            System.out.println("连接上一个客户。。。。。。"+socket.getPort());
            executorService.execute(()->{
                handel(socket);
            });
        }
    }

    //处理读取到的数据
    public static void handel(Socket socket){
        try{
            byte[] bytes = new byte[1024];
            InputStream inputStream = socket.getInputStream();
             while (true){
                 //等待读取数据
                 int length = inputStream.read(bytes);
                 if(length != -1){
                     System.out.println("读取到数据"+new String(bytes,0,length));
                 }else{
                     break;
                 }
           }
        }catch (Exception e){
           e.printStackTrace();
        }finally {
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

通过这个demo我们可以看到,每个客户端需要独立的线程,使用bio会在等待连接和读取数据两处进行阻塞,造成线程资源的浪费。

 

nio:

  1》 NIO 有三大核心部分: Channel(通道), Buffer(缓冲区), Selector(选择器)

  2》NIO是面向缓冲区,或者面向块编程的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后

  移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络

  3》Java NIO的非阻塞模式,使-一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,

  如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可

  以继续做其他的事情。非阻塞写 也是如此,-一个线程请求写入- -些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。

  通俗理解: NIO是可以做到用-一个线程来处理多个操作的。假设有10000 个请求过来,根据实际情况,可以分配50或者100个线程来处理。

  不像之前的阻塞I0那样,非得分配10000个。

  4》HTTP2.0采用多路复用技术

  5》nio中buffer的demo:

 

public class NioBaise {

    public static void main(String[] args) {
        //创建容量为5的IntBuffer
        IntBuffer intBuffer = IntBuffer.allocate(5);
        for (int i = 0; i <5 ; i++) {
            intBuffer.put(i);
        }
        //切换,有写变读
        intBuffer.flip();
        //判断是否还有数据
        while (intBuffer.hasRemaining()){
            //获取数据,get()获取数据后,就会移动下标
            System.out.println(intBuffer.get());
        }
    }
}

 

NIO 和BIO的比较:

  1》BIO 是阻塞的,NIO则是非阻塞的      

  2》 BIO 以流的方式处理数据,而NIO以块的方式处理数据,块I/O 的效率比流I0高很多

  3》 BIO 基于字节流和字符流进行操作,而NIO基于Channel(通道)和Buffer(缓冲区 )进行操作,数据总是从通道

    读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择 器)用于监听多个通道的事件(比如:连接请求,

    数据到达等),因此使用单个线程就可以监听多个客户端通道

 

nio中的buffer:

有四个重要属性

  Capacity:容量,即可以容纳的最大数据量;在缓冲区创建时被设定并且不能改变

  Limit:表示缓冲区的当前终点,不能对缓冲区超过极限的位置进行读写操作。且极限是可以修改的

  Position:位置,下一个要被读或写的元素的索引,每次读写缓冲区数据时都会改变改值,每次读写缓冲区数据时都会改变改值,

  Mark:标记

Buffer中常用方法:

public abstract class Buffer {
    //JDK1.4时,引入的api
    public final int capacity( )//返回此缓冲区的容量
    public final int position( )//返回此缓冲区的位置
    public final Buffer position (int newPositio)//设置此缓冲区的位置
    public final int limit( )//返回此缓冲区的限制
    public final Buffer limit (int newLimit)//设置此缓冲区的限制
    public final Buffer mark( )//在此缓冲区的位置设置标记
    public final Buffer reset( )//将此缓冲区的位置重置为以前标记的位置
    public final Buffer clear( )//清除此缓冲区, 即将各个标记恢复到初始状态,但是数据并没有真正擦除, 后面操作会覆盖
    public final Buffer flip( )//反转此缓冲区
    public final Buffer rewind( )//重绕此缓冲区
    public final int remaining( )//返回当前位置与限制之间的元素数
    public final boolean hasRemaining( )//告知在当前位置和限制之间是否有元素
    public abstract boolean isReadOnly( );//告知此缓冲区是否为只读缓冲区
 
    //JDK1.6时引入的api
    public abstract boolean hasArray();//告知此缓冲区是否具有可访问的底层实现数组
    public abstract Object array();//返回此缓冲区的底层实现数组
    public abstract int arrayOffset();//返回此缓冲区的底层实现数组中第一个缓冲区元素的偏移量
    public abstract boolean isDirect();//告知此缓冲区是否为直接缓冲区
}

 

 在Buffer中ByteBuffer是最常用的(二进制数据),主要方法如下:

public abstract class ByteBuffer {
    //缓冲区创建相关api
    public static ByteBuffer allocateDirect(int capacity)//创建直接缓冲区
    public static ByteBuffer allocate(int capacity)//设置缓冲区的初始容量
    public static ByteBuffer wrap(byte[] array)//把一个数组放到缓冲区中使用
    //构造初始化位置offset和上界length的缓冲区
    public static ByteBuffer wrap(byte[] array,int offset, int length)
     //缓存区存取相关API
    public abstract byte get( );//从当前位置position上get,get之后,position会自动+1
    public abstract byte get (int index);//从绝对位置get
    public abstract ByteBuffer put (byte b);//从当前位置上添加,put之后,position会自动+1
    public abstract ByteBuffer put (int index, byte b);//从绝对位置上put
 }

 

nio中的channel:

1) NIO的通道类似于流,但有些区别如下:

  ●通道可以同时进行读写, 而流只能读或者只能写

  ●通道可以实现异步读写 数据

    ●通道可以从缓冲读数据, 也可以写数据到缓冲:

2) BIO 中的stream 是单向的,例如FilelnputStream 对象只能进行读取数据的操作,而NIO中的通道(Channel)是双向的,可以读操作,也可以写操作。

3) Channel 在NIO中是一个接口 public interface Channel extends Closeable{}

4)常用的Channel 类有: FileChannel、DatagramChannel、ServerSocketChannel 和SocketChannel。

5) FileChannel 用于文件的数据读写,DatagramChannel 用于UDP 的数据读写,ServerSocketChannel 和SocketChannel用于TCP的数据读写。

 

FileChannel主要用来对本地文件进行 IO 操作,常见的方法有

public int read(ByteBuffer dst) ,从通道读取数据并放到缓冲区中
public int write(ByteBuffer src) ,把缓冲区的数据写到通道中
public long transferFrom(ReadableByteChannel src, long position, long count),从目标通道中复制数据到当前通道
public long transferTo(long position, long count, WritableByteChannel target),把数据从当前通道复制给目标通道

 

FileChannel的demo1:向文件中写入字符串

public class FileChannelDemo {
    public static void main(String[] args) throws IOException {
        String str = "hello";

        //创建文件流
        FileOutputStream fileOutputStream = new FileOutputStream("e:\\hello.txt");

        //将流转为通道
        FileChannel fileChannel = fileOutputStream.getChannel();

        //创建换缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        byteBuffer.put(str.getBytes());

        //将byteBuffer进行反转
        byteBuffer.flip();
       
        //将缓冲区数据写入到fileChannel通道里
        fileChannel.write(byteBuffer);

        //关闭管道
        fileChannel.close();

    }
}

 FileChannel的demo2:向文件中读取字符串

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


        //创建文件输入流
        FileInputStream fileInputStream = new FileInputStream("e:\\hello.txt");

        //将流转为通道
        FileChannel fileChannel = fileInputStream.getChannel();

        //创建换缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate(fileInputStream.available());

        //将管道里面的数据写入到byteBuffer中
        fileChannel.read(byteBuffer);

        //将byteBuffer进行反转
        byteBuffer.flip();

        System.out.println(new String(byteBuffer.array()));

        //关闭管道
        fileChannel.close();

    }
}

FileChannel的demo3:文件复制:

public class FileChannelDemo3 {
    public static void main(String[] args) throws IOException {
        //创建文件输入流
        FileInputStream fileInputStream = new FileInputStream("e:\\hello.txt");
        FileChannel fileChannel = fileInputStream.getChannel();

        //创建文件输出流
        FileOutputStream fileOutputStream = new FileOutputStream("e:\\hello2.txt");
        FileChannel channel = fileOutputStream.getChannel();

        //创建换缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

        while (true){
            //复位
            byteBuffer.clear();
            //将管道里面的数据写入到byteBuffer中
            int read = fileChannel.read(byteBuffer);
            if(read == -1){
                break;
            }
            //将byteBuffer进行反转
            byteBuffer.flip();
            //将byteBuffer里面的数据写入到通道中
            channel.write(byteBuffer);
        }
        System.out.println("写入完成");
        //关闭管道
        fileChannel.close();
        fileOutputStream.close();
        fileInputStream.close();
    }
}

 

FileChannel的demo4:文件复制,直接使用transferFrom()方法;

public class FileChannelDemo4 {
    public static void main(String[] args) throws IOException {
        //创建文件输入流
        FileInputStream fileInputStream = new FileInputStream("e:\\hello.txt");
        FileChannel sourceChannel = fileInputStream.getChannel();

        //创建文件输出流
        FileOutputStream fileOutputStream = new FileOutputStream("e:\\hello2.txt");
        FileChannel targetChannel = fileOutputStream.getChannel();

        //复制
       targetChannel.transferFrom(sourceChannel,0,sourceChannel.size());

       sourceChannel.close();
       targetChannel.close();
       fileInputStream.close();
       fileOutputStream.close();
    }
}

 

Buffer和channel的注意点:

1) ByteBuffer 支持类型化的put和get, put放入的是什么数据类型,get 就应该使用相应的数据类型来取出,否

   则可能有BufferUnderflowException 异常。

2)可以将-一个普通Buffer转成只读Buffer,使用asReadOnlyBuffer()返回ByteBuffer

  public abstract ByteBuffer asReadOnlyBuffer()

3) NIO 还提供了MappedByteBuffer, 可以让文件直接在内存 (堆外的内存)中进行修改,而 如何同步到文件由nio完成。

demo:

public class MapedByteBufferDemo {
    public static void main(String[] args) throws Exception{
        RandomAccessFile randomAccessFile = new RandomAccessFile("e:\\hello.txt","rw");

        FileChannel channel = randomAccessFile.getChannel();

        /**
         * 参数一:读写模式
         * 参数二:开始读取的位值
         * 参数三:是映射到内存的大小,而不是索引的大小
         */
        MappedByteBuffer map = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);

        map.put(0,(byte)'H');
        map.put(2,(byte)'y');

        //如果超过大小会发生异常:java.lang.IndexOutOfBoundsException
        //map.put(5,(byte)'0');

        randomAccessFile.close();
        System.out.println("修改成功");
    }
}

 

nio中的Selector:

示意图及特点:

 说明:

1》Netty 的 IO 线程 NioEventLoop 聚合了 Selector(选择器,也叫多路复用器),可以同时并发处理成百上千个客户端连接。
2》当线程从某客户端 Socket 通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。
3》线程通常将非阻塞 IO 的空闲时间用于在其他通道上执行 IO 操作,所以单独的线程可以管理多个输入和输出通道。
4》由于读写操作都是非阻塞的,这就可以充分提升 IO 线程的运行效率,避免由于频繁 I/O 阻塞导致的线程挂起。
5》一个 I/O 线程可以并发处理 N 个客户端连接和读写操作,这从根本上解决了传统同步阻塞 I/O 一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。

 

   常用方法:

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


方法说明:
selector.select()//阻塞
selector.select(1000);//阻塞1000毫秒,在1000毫秒后返回
selector.wakeup();//唤醒selector
selector.selectNow();//不阻塞,立马返还

 

 过程:

1)当客户端连接时, 会通过ServerSocketChannel得到SocketChannel

2) Selector 进行监听select 方法,返回有事件发生的通道的个数.

3)将socketChannel注册到Selector上, register(Selector sel, int ops),一个selector上可以注册多个SocketChannel

4)注册后返回一个SelectionKey, 会和该Selector关联(集合)

5) 进-步得到各个SelectionKey (有事件发生)

6)在通过SelectionKey 反向 获取SocketChannel ,方法channel()

7)可以通过得到的 channel, 完成业务 处理

 

demo:NioServer(服务端)

public class NioServer {
    public static void main(String[] args) throws IOException {
        //创建ServerSocketChannel
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

        //绑定网络端口
        serverSocketChannel.socket().bind(new InetSocketAddress(666));

        //得到selector对象
        Selector selector = Selector.open();

        //设置为非阻塞的
        serverSocketChannel.configureBlocking(false);

        //将serverSocketChannel注册到selector中,selector只关心OP_ACCEPT事件
        SelectionKey selectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        //循环等待客户连接
        while (true){
            //没有事件就返回
            if(selector.select(1000)==0){
                System.out.println("服务端等待连接。。。。。。");
                continue;
            }
            //获取到相关SelectionKey集合,表示获取到关注的事件
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            //使用迭代器遍历
            Iterator<SelectionKey> keyIterator = selectionKeys.iterator();

            if(keyIterator.hasNext()){
                //获取到SelectionKey
                SelectionKey key = keyIterator.next();
                //根据key对应的事件作相应的处理
                if(key.isAcceptable()){//表示有新客户端连接
                    //每次连接一个客户端都会产生新的SocketChannel
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    System.out.println("每次连接一个客户端都会产生新的SocketChannel的hashcode"+socketChannel.hashCode());
                    //设置客户端为非阻塞的
                    socketChannel.configureBlocking(false);
                    //将socketChannel注册到selector,关注事件为OP_READ,同时给socketChannel关联一个buffer
                    socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                    System.out.println("连接上一个客户端....");
                }
                if(key.isReadable()){//发生OP_READ事件
                    //通过key反向获取到channel
                    SocketChannel socketChannel = (SocketChannel)key.channel();
                    //获取到该channer关联的buffer
                    ByteBuffer byteBuffer =(ByteBuffer) key.attachment();
                    socketChannel.read(byteBuffer);
                    System.out.println("读取到数据。。。"+new String(byteBuffer.array()));
                }
                //移除seletorKey,防止重复操作。
                keyIterator.remove();
            }

        }
    }
}

客户端:NioClient

public class NioClient {
    public static void main(String[] args) throws IOException {
        //连接服务端的ip和端口
        InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 8888);

        //得到一个网络通道
        SocketChannel socketChannel = SocketChannel.open();
        //设置为非阻塞的
        socketChannel.configureBlocking(false);

        //连接服务器
        if (!socketChannel.connect(inetSocketAddress)) {
            while (!socketChannel.finishConnect()) {
                System.out.println("因为连接需要时间,客户端不会阻塞,可以做其他工作");
            }
        }
        String str = "hello";
        //创建buffer
        ByteBuffer byteBuffer = ByteBuffer.wrap(str.getBytes());
        //将buffer数据写入到channel中
        socketChannel.write(byteBuffer);
        System.in.read();
    }
}

 

SelectKey介绍:

1) SelectionKey,表示Selector 和网络通道的注册关系,共四种:

int OP_ ACCEPT:有新的网络连接可以accept,值为16

int OP_ CONNECT:代表连接已经建立,值为8

intOP_ READ:代表读操作,值为1

intOP_ WRITE:代表写操作,值为4

源码是采用位操作符:

public static final int OP_READ= 1 << 0;

public static final int OP_WRITE= 1 << 2;

public static final int OP_CONNECT= 1 << 3;

public static final int OP_ACCEPT= 1 <<4;

 相关方法:

public abstract Selector selecto/(;/得到与之关联的Selector对象)

public abstract SelectableChannel chanel)://得到与之关联的通道

public final Object ttachment():/得到与之关联的共享数据

public abstract SelectionKey interestOps(int ops//设置或改变监听事件

public final boolean isAcceptable(;//是否可以accept

public final boolean isReadablel(;//是否可以读

public final boolean isWritable(;//是否可以写

 

ServerSocketChannel:在服务端监听客户端的连接

public static ServerSocketChannel open(),得到-一个ServerSocketChannel通道

public final ServerSocketChannel bind(SocketAddress local),设置服务器端端口号

public final SelectableChannel configureBlocking(boolean block),设置阻塞或非阻塞模式,取值false表示采用非阻塞模式

public SocketChannel accept),接受一个连接, 返回代表这个连接的通道对象

public final SelectionKey register(Selectorsel, intops),注册一个选择器并设置监听事件

 

SocketChannel:网络io操作,负责数据的读写操作。Nio把数据从缓存中写入通道或把通道里的数据读取到缓存中。

public static SocketChannel open/://得到一个SocketChannel通道

public final SelectableChannel configureBlocking(boolean blok;//设置阻塞或非阻塞模式,取值false表示采用非阻塞模式

public boolean connect(SocketAddress remote://连接服务器

public boolean finishConnet://如果上面的方法连接失败,接下来就要通过该方法完成连接操作

public int write(ByteBuffer sc);//通道里写数据

public int read(ByteBuffer dst);//从通道里读数据

public final SelectionKey register(Selector sel, int ops, Object a://注册个选择器并设置监听事件,最后一个参数可以设置共享数据

public final void close(;//关闭通道

SocketChannel和ServerSocketChannel区别:SocketChannel更注重于数据上的操作,ServerSocketChannel注重于连接上。

 群聊demo:服务端

package com.netty.groupChat;

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

public class GroupChatServer {
    private  int port = 9999;
    private  ServerSocketChannel listenChannel;
    private  Selector selector;

    //初始化
    public GroupChatServer(){
        try{
            //创建选择器
             selector = Selector.open();
             //创建ServerSocketChannel
            listenChannel = ServerSocketChannel.open();
            //绑定端口
            listenChannel.socket().bind(new InetSocketAddress(port));
            //设置非阻塞模式
            listenChannel.configureBlocking(false);
            //将ServerSocketChannel注册到Secletor中,接受连接事件
            listenChannel.register(selector,SelectionKey.OP_ACCEPT);
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    //监听连接
    public  void listenEvent() {
        try {
            while (true) {
                //有事件才处理
               if(selector.select() > 0){
                   //遍历循环得到SelectionKey
                   Set<SelectionKey> selectionKeys = selector.selectedKeys();
                   Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
                   while (keyIterator.hasNext()) {
                       //得到SelectKey
                       SelectionKey key = keyIterator.next();

                       //监听对应的事件,这里监听连接事件
                       if (key.isAcceptable()) {
                           //创建SocketChannel
                           SocketChannel sc =  listenChannel.accept();
                           //设置非阻塞
                           sc.configureBlocking(false);
                           //将SocketChannel注册到Selector中,处理读事件
                           sc.register(selector, SelectionKey.OP_READ);
                           //提示上线
                           System.out.println(sc.getRemoteAddress() + "上线");
                       }
                       //处理读事件
                       if (key.isReadable()) {
                               readData(key);
                       }
                       //删除当前的key,避免重复处理
                       keyIterator.remove();
                   }
               }else{
                   System.out.println("等待。。。。");
               }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        finally {

        }
    }

    //读取数据
    public  void readData(SelectionKey key){
        //通过key获取绑定的SocketChannel
        SocketChannel socketChannel = (SocketChannel)key.channel();
        //创建缓存区
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        try{
            //从通道的数据读取到缓冲区中
            int read = socketChannel.read(byteBuffer);
            //当有数据时
            if(read>0){
                //获取缓冲区数据
                String msg = new String(byteBuffer.array());
                //虎丘客户端信息
                System.out.println("客户端信息。。。。。"+msg);
                //向其他用户分发信息
                forwardMsg(msg,socketChannel);
            }
        }catch (Exception e){
            try{
                //取消注册
                key.cancel();
                //关闭通道
                socketChannel.close();
                System.out.println(socketChannel.getRemoteAddress()+"下线");
            }catch (Exception ex){
                ex.printStackTrace();
            }
        }
    }
    
    //转发信息
    public   void forwardMsg(String msg,SocketChannel self) throws IOException {
        System.out.println("消息转发。。。。。");
        //获取到所有注册到selector中的SocketChannel
        Set<SelectionKey> keys = selector.keys();
        for(SelectionKey key : keys){
            Channel targetChannel = key.channel();
            //不向自己发消息
            if(targetChannel instanceof SocketChannel && targetChannel != self){
                SocketChannel dest = (SocketChannel) targetChannel;
                ByteBuffer byteBuffer =  ByteBuffer.wrap(msg.getBytes());
                //将数据从缓冲区中读到通道中
                dest.write(byteBuffer);
            }
        }
    }
    
    public static void main(String[] args) {
        GroupChatServer groupChatServer = new GroupChatServer();
        System.out.println("服务器启动。。。");
        groupChatServer.listenEvent();

    }
}

 客户端:

package com.netty.groupChat;

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.util.Iterator;
import java.util.Scanner;
import java.util.Set;

public class GroupChatClient {
    private String host = "127.0.0.1";
    private int port = 9999;
    private SocketChannel socketChannel;
    private  Selector selector;
    private String user;

    public GroupChatClient(){
        try {
            //创建选择器
            selector = Selector.open();
            //连接服务器
            socketChannel = socketChannel.open(new InetSocketAddress(host,port));
            //设置非阻塞
            socketChannel.configureBlocking(false);
            //将socketChannel注册到selector,处理OP_READ事件
            socketChannel.register(selector, SelectionKey.OP_READ);
            //得到用户
            user = socketChannel.getRemoteAddress().toString();
            System.out.println("ok ............");

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


    //发送消息
    public void sendInfo(String info) {
        try{
            info =  user+"说:"+info;
            ByteBuffer byteBuffer = ByteBuffer.wrap(info.getBytes());
            socketChannel.write(byteBuffer);
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    //读取数据
    public void readInfo() {
        try{
            int readChannel = selector.select();
            //有事件
            if(readChannel > 0){
                //遍历循环,得到SelectionKey
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()){
                    SelectionKey key = iterator.next();
                    //处理读取事件
                    if(key.isReadable()){
                        //通过key获取绑定的SocketChannel
                        SocketChannel socketChannel =(SocketChannel) key.channel();
                        //创建缓冲区
                        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                        //将通道里的数据写出到byteBuffer
                        socketChannel.read(byteBuffer);
                        //获取信息
                        System.out.println(new String(byteBuffer.array()));
                    }

                    //删除当前的SelectionKey,防止重复
                    iterator.remove();
                }
            }else{

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

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

        GroupChatClient groupChatClient = new GroupChatClient();

            new Thread(() -> {
               while (true) {
                   groupChatClient.readInfo();
                   try {
                       Thread.currentThread().sleep(3000);
                   } catch (Exception e) {
                       e.printStackTrace();
                   }
               }
            }).start();

            Scanner scanner = new Scanner(System.in);
            while (scanner.hasNextLine()) {
                groupChatClient.sendInfo(scanner.nextLine());
            }
        };
    }

NIO与零拷贝.

1)零拷贝是网络编程的关键,很多性能优化都离不开。

2)在Java 程序中,常用的零拷贝有mmap(内存映射)和sendFile. 那么,他们在OS里,到底是怎么样的一个

的设计?我们分析mmap和sendFile 这两个零拷贝

 

传统IO模型:

DMA: directmemory access直接内存拷则(不使用CPU)

 

mmap优化:

1) mmap通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据.这样,在进行网

络传输时,就可以减少内核空间到用户空间的拷贝次数。如下图

 

sendFile优化:

Linux2.1版本提供了sendFile 函数,其基本原理如下:数据根本不经过用户态,直接从内核缓冲区进入到

Socket Buffer,同时,由于和用户态完全无关,就减少了一次上下文切换。

 

零拷贝理解:

1) 我们说零拷贝,是从操作系统的角度来说的。因为内核缓冲区之间,没有数据是重复的(只有kermel buffer 有

一份数据),

2)零拷贝不仅仅带来更少的数据复制,还能带来其他的性能优势,例如更少的上下文切换,更少的CPU缓存伪共享

以及无CPU校验和计算。

 

mmap和sendFile的区别:

1》mmap适合小数据量读写,sendFile 适合大文件传输。

2》mmap需要4次上下文切换,3次数据拷贝: sendFile 需要3次上下文切换,最少2次数据拷贝。

3》sendFile可以利用DMA方式,减少CPU拷贝,mmap则不能(必须从内核拷贝到Socket 缓冲区)。

 

在nio中FileChannel中的transformTo()方法底层原理就是零拷贝。

demo:

public class NioCopyServer {
    public static void main(String[] args) throws IOException {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.socket().bind(new InetSocketAddress(9998));
        while (true){
            SocketChannel socketChannel = serverSocketChannel.accept();
            ByteBuffer byteBuffer = ByteBuffer.allocate(4096);
            int readCount = socketChannel.read(byteBuffer);
            while (readCount == -1){
                break;
            }
            byteBuffer.rewind();
        }
        
    }
}

客户端

public interface NioCopyClient {
    public static void main(String[] args) throws IOException {
        SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",9999));

        FileInputStream fileInputStream = new FileInputStream(new File("e:\\hello.txt"));


        FileChannel fileChannel = fileInputStream.getChannel();

        long startTimeMillis = System.currentTimeMillis();


        long transferTo = fileChannel.transferTo(0, fileChannel.size(), socketChannel);

        long endTimeMillis = System.currentTimeMillis();

        System.out.println("文件大小:"+transferTo+",耗时:"+(endTimeMillis-startTimeMillis));

    }
}

 

 参考:

尚硅谷韩顺平Netty视频教程(2019最新netty教程)_哔哩哔哩 (゜-゜)つロ 干杯~-bilibili

 

 

posted @ 2019-11-25 22:03  来一杯可乐  阅读(534)  评论(1编辑  收藏  举报