Netty学习---基本介绍和IO模型

Netty学习–基本介绍和IO模型

介绍和应用场景

介绍:

  • netty是由JBOSS提供的一个Java开源框架,现为github上的独立项目。
  • netty是一个异步的,基于事件驱动的网络应用框架,用于快速开发高性能,高可靠的网络IO程序
  • netty主要针对TCP协议下,面向Client端的高并发应用,或者peer-to-peer场景下的大量数据持续传输的应用
  • netty本质是一个NIO框架,使用于服务器通讯相关的多种应用场景

应用场景:

  • 互联网行业,在分布式系统中,各个节点之间需要远程服务调用,高性能的RPC框架必不可少,netty作为异步高性能的通信框架,往往作为基础通信组件被这些RPC框架使用
  • 游戏行业
  • 大数据领域

I/O模型

  • Java BIO:同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销。
    • 适用场景:连接数小且固定的架构,这种方式对服务器资源的要求较高,并发局限于应用中
  • Java NIO:同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求就进行处理
    • 适用场景:连接数多且连接比较短的架构,比如聊天服务器,弹服务器
  • Java AIO:异步非阻塞,AIO引进异步通道的概念,采用proactor模式,简化了程序编写,有效的请求才会启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。
    • 适用场景:连接数多且连接比较长的架构,比如相册服务器

BIO模型示例

package com.fish;

import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

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

        //创建线程池
        ExecutorService threadPool = Executors.newCachedThreadPool();
        //创建服务端
        ServerSocket serverSocket = new ServerSocket(8888);

        //启动服务端
        while (true) {
            //等待客户端连接
            System.out.println("正在等待客户端连接...");
            final Socket socket = serverSocket.accept();
            System.out.println("客户端已经连接...");
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println("|启动的线程id==>" + Thread.currentThread().getId());
                    try {
                        socketHandler(socket);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
    public static void socketHandler(Socket socket) throws IOException {
        try {
            byte[] bytes = new byte[1024];
            InputStream inputStream = socket.getInputStream();
            while (true){
                int read = inputStream.read(bytes);
                if (read!=-1){
                    System.out.println(new String(bytes, 0, read));
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            socket.close();
        }
    }

}

测试:

命令行输入telnet ip port,然后ctrl+]发送数据send data

NIO模型

NIO是面向缓冲区或者块编程的,数据读取到一个它稍后处理的缓存区,需要时可在缓冲区中前后移动,这就是增加了处理过程的灵活性,使用它可以提供非阻塞式的高伸缩性网络。

Java NIO的非阻塞模式,使得一个线程从某通道发送请求或者读取数据,但是他仅能得到目前可用的数据,如果目前没有可用的数据,就什么都不会做,而不是保持线程阻塞,所以直至数据变得可以被读取到之前,该线程可以继续做其他的事情

核心部件:

  • 通道(channel)
  • 缓冲区(buffer)
  • 选择器(selector)

NIO和BIO的区别

  • BIO以流的方式处理数据,而NIO以块的方式处理数据,块的效率比流的效率高很多
  • BIO是阻塞的,NIO是非阻塞的
  • BIO基于字节流和字符流进行操作,而NIO基于channel和buffer进行操作,数据总是从通道读取到缓冲区,或者从缓冲区中写入到通道中。selector用于监听多个通道的事件(比如,连接请求,数据到达等),因此使用单个线程就能监听到多个客户端通道

Buffer的使用

package com.fish.NIO;
import java.nio.IntBuffer;

public class BufferTest {
    public static void main(String[] args) {

        //创建buffer,容量为5,可以存放5个int类型的数据
        IntBuffer intBuffer=IntBuffer.allocate(5);
        //向buffer中存储数据
        for (int i=0;i<intBuffer.capacity();i++){
            intBuffer.put(i+1);
        }
        //切换读写状态
        intBuffer.flip();
        for (int i=0;i<intBuffer.capacity();i++){
            System.out.println(intBuffer.get());
        }
    }
}

三大核心的关系

  • 每个channel都会对应一个buffer
  • 一个selector对应一个线程,一个线程对应多个channel(连接)
  • 上图反应了三个channel注册到了selector
  • 程序切换到哪个channel是由事件决定的
  • selector会根据不同的事件,在各个通道上切换
  • buffer就是一个内存块,底层是有一个数组的
  • 数据的读取写入是通过buffer,是可读可写的,但是需要切换
  • channel是双向的,可以返回底层操作系统的情况。
缓冲区(Buffer)

​ 缓冲区本质上是一个可以读写数据的内存块,可以理解为一个容器对象(含数组),该对象提供了一组方法,可以更加轻松的使用内存块。缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。channel提供从文件,网络读取数据的渠道,但是读取或写入的数据都必须经由buffer

buffer的四个属性
属性描述
capacity容量,即可以容纳的最大数量,在缓冲区创建时被设定,不可改变
limit表示缓冲区当前的终点,不能对缓冲区超过极限的位置进行读写操作,且极限是可以修改的
position位置,下一个要被读或写的元素的索引,每次读写缓冲区时数据都会改变值,为下次读写做准备
mark标记
注意事项:
  • ByteBuffer支持类型化的put和get,put放入的什么类型,get就应该使用相应的数据类型来取出,否则可能有异常

  • 可以将一个普通buffer转化为只读buffer

    • 使用asReadOnlyBuffe方法转化
  • MapperByteBuffer:可以让文件直接在内存中修改,操作系统不需要再拷贝一次

    • public class MapperBufferTest {
          public static void main(String[] args) throws IOException {
              //创建随机访问文件流,以读取和(可选)写入具有指定名称的文件。
              //将创建一个新的FileDescriptor对象来表示与该文件的连接。
              RandomAccessFile randomAccessFile = new RandomAccessFile("d://1.tx5t","rw");
              FileChannel channel = randomAccessFile.getChannel();
              /*
              *参数1:使用的模式
              *参数2:可以直接修改的起始位置
              *参数3:映射到内存的大小,就是可以直接修改的范围是0~5
              */
              MappedByteBuffer map = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
              map.put(0,(byte)'H');
              randomAccessFile.close();
          }
      }
      
      
  • NIO还支持通过多个Buffer(即buffer数组)完成读写操作,即Scattering和Gathering

    • Scattering:将数据写入到buffer时,可以采用buffer数组,依次写入(分散)

    • Gathering:从buffer读取数据时,可以采用buffer数组以此读取(聚集)

    • public class ScatteringAndGatheringTest {
          public static void main(String[] args) throws IOException {
              //创建一个serverSocketChannel
              ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
              //创建端口
              InetSocketAddress inetSocketAddress = new InetSocketAddress(8888);
              //绑定端口到serverSocketChannel
              serverSocketChannel.bind(inetSocketAddress);
              //创建buffer数组
              ByteBuffer[] byteBuffers=new ByteBuffer[2];
              byteBuffers[0]=ByteBuffer.allocate(5);
              byteBuffers[1]=ByteBuffer.allocate(3);
      
              //等待客户端连接
              SocketChannel socketChannel = serverSocketChannel.accept();
              //最大数据读取长度
              int messageLength=8;
              //循环读取数据
              while (true){
                  int read=0;
                  while (read<messageLength){
                      //将数据从通道读取到缓冲区中
                      long l = socketChannel.read(byteBuffers);
                      read+=l;
                      //打印当前已经读取数据的长度
                      System.out.println("read already==>" + read);
                      //查看当前buffer的position和limit
                      Arrays.asList(byteBuffers).stream().map(buffer -> "position==>"+buffer.position()+",limit==>"+buffer.limit()).forEach(System.out::println);
                  }
                  //将bytebuffer数组中的buffer状态进行读写切换
                  Arrays.asList(byteBuffers).forEach(buffer -> buffer.flip());
                  //从缓冲区读取数据
                  int write=0;
                  while (write<messageLength){
                      long l = socketChannel.write(byteBuffers);
                      write+=l;
                  }
                  //清理buffer
                  Arrays.asList(byteBuffers).forEach(buffer ->{buffer.clear();});
                  System.out.println("read==>" + read + "====write==>" + write);
              }
          }
      }
      
      
通道(Channel):

​ NIO的通道类似流,但有些区别

  • 通道可以同时读写,而流只能读或者写
  • 通道可以实现异步读写数据
  • 通道可以从缓冲读数据,也可以写数据到缓冲

常用的channel类有:

  • FileChannel:用于文件的读写
  • DatagramChannel:用于UDP的数据读写
  • ServerSocketChannel
  • SocketChannel

常用方法:

  • read(ByteBuffer dst):从通道读取数据到缓冲区
  • writer(ByteBuffer src):从缓冲区读取数据到通道

示例:

public class NIOFileChannel {
    public static void main(String[] args) throws IOException {
        String str="hello,fish";
        //创建输出流,
        FileOutputStream outputStream = new FileOutputStream("d://1.txt");
        //通过输出流获取fileChannel
        FileChannel channel = outputStream.getChannel();
        //创建一个byteBuffer
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        //将数据写入到buffer中
        buffer.put(str.getBytes());
        //切换buffer读写状态
        buffer.flip();
        //通道从buffer中读取数据
        channel.write(buffer);
        //关闭输出流
        outputStream.close();
    }
}

示例图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pjfwWpoj-1619252627168)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20210411195404996.png)]

**示例2:**拷贝文件

public class NIOFileChannel02 {
    public static void main(String[] args) throws IOException {
        FileInputStream fileInputStream = new FileInputStream("d://1.txt");

        FileOutputStream fileOutputStream = new FileOutputStream("d://2.txt");
        //创建缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(512);
        FileChannel inputStreamChannel = fileInputStream.getChannel();
        FileChannel outputStreamChannel = fileOutputStream.getChannel();
        while (true){
            //clear是必须的,清空buffer
            buffer.clear();
            int read = inputStreamChannel.read(buffer);
            if (read==-1){
                break;
            }
            //读写反转
            buffer.flip();
            outputStreamChannel.write(buffer);
        }
        fileOutputStream.close();
        fileInputStream.close();

    }
}

选择器(Selector):

selector能够检测多个注册的通道上是否有事件发生(注意,多个channel以事件的方式可以注册到同一个selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。

只有在连接真正有读写事件发生时,才会进行读写,就大大的减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程

避免了多线程之间的上下文切换导致的开销。

示例:

服务端:

public class NIOServer {
    public static void main(String[] args) throws IOException {
        //创建serverSocketChannel
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //创建selector
        Selector selector = Selector.open();
        //绑定端口
        serverSocketChannel.bind(new InetSocketAddress(8888));
        //设置非阻塞模式
        serverSocketChannel.configureBlocking(false);
        //serverSocketChannel注册到selector,并设置关注事件为 连接事件(SelectionKey.OP_ACCEPT)
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        //循环等待客户端连接
        while (true){
            //等待一秒,如果没有事件发生
            if (selector.select(1000)==0){
                System.out.println("没有客户端连接到服务器");
            }
            //如果有事件对其进行响应,获取到相关的selectionKey集合
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()){
                //获取到selectorKey
                SelectionKey selectionKey = iterator.next();
                //根据key找到对应的通道进行处理
                if (selectionKey.isAcceptable()){
                    //发生的事件为连接事件
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    //将连接到服务器端的客户端设置为非阻塞
                    socketChannel.configureBlocking(false);
                    //打印
                    System.out.println("已经有一个客户端连接到服务端" + selectionKey.hashCode());
                    //将socketChannel注册到selector
                    socketChannel.register(selector,SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                }
                if (selectionKey.isReadable()){
                    //发生的事件为读取事件
                    SocketChannel channel = (SocketChannel) selectionKey.channel();
                    //获取到和SocketChannel关联的buffer
                    ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();
                    //从客户端读取数据到缓冲
                    channel.read(buffer);
                    //打印数据
                    System.out.println(new String(buffer.array()));
                }
                //手动从集合中移除当前的selectionKey,防止重复操作
                iterator.remove();
            }
        }
    }
}

客户端:

public class NIOClient {
    public static void main(String[] args) throws IOException {
        //得到一个网络通道
        SocketChannel socketChannel = SocketChannel.open();
        //设置非阻塞
        socketChannel.configureBlocking(false);
        //提供服务端ip和端口
        InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 8888);
        //连接服务器
        if (!socketChannel.connect(inetSocketAddress)){
            //如果没有成功连接到服务器,客户端不会阻塞,可以进行其他的工作
            while (!socketChannel.finishConnect()){
                System.out.println("客户端未阻塞");
            }
        }

        //成功连接到服务器后
        String str="hello";
        //wrap方法,将字符串包装到buffer
        ByteBuffer buffer = ByteBuffer.wrap(str.getBytes());
        //将数据传输到服务去端
        socketChannel.write(buffer);
        //将客户端保持运行
        System.in.read();
    }
}

SelectorKey,表示selector和网络通道注册的关系

  • OP_ACCEPT = 1 << 4			//新的网络连接可以accept
    OP_CONNECT = 1 << 3			//连接已经建立
    OP_WRITE = 1 << 2			//写操作
    OP_READ = 1 << 0			//读操作
    
  • channel():获取与之关联的通道

  • attachmen():获取与之关联的共享数据

  • seletor():获取与之关联的选择器

Selector API中keys()和selectedKey()的区别

  • keys:代表当前注册到seletor中的通道
  • selectedKeys:代表当前响应事件的通道

群聊示例:

服务端:

public class GroupChatServer {
    private Selector selector;
    private ServerSocketChannel serverSocketChannel;
    private static final int PORT=8888;
    public GroupChatServer(){
        try {
            selector=Selector.open();
            serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.socket().bind(new InetSocketAddress(PORT));
            serverSocketChannel.configureBlocking(false);
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

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

    public void listen(){
        //循环监听事件
        try {
            while (true){

                int count = selector.select(200);
                if (count>0){
                    Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
                    while (keyIterator.hasNext()){
                        SelectionKey key = keyIterator.next();
                        if (key.isAcceptable()){
                            SocketChannel socketChannel = serverSocketChannel.accept();
                            socketChannel.configureBlocking(false);
                            socketChannel.register(selector,SelectionKey.OP_READ);
                            //提示上线
                            System.out.println(socketChannel.getRemoteAddress() + ": already online");
                        }
                        if (key.isReadable()){
                            readData(key);
                        }
                        keyIterator.remove();
                    }
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    public void readData(SelectionKey selectionKey){
        SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        try {
            int read = socketChannel.read(buffer);
            if (read>0){
                String msg = new String(buffer.array(), 0, read);
                System.out.println("读取到的数据为: "+msg);
                sendInfoToOthers(msg,socketChannel);
            }
        } catch (IOException e) {
            try {
                System.out.println(socketChannel.getRemoteAddress() + " already outline");
                selectionKey.cancel();
                socketChannel.close();
            } catch (IOException ioException) {
                ioException.printStackTrace();
            }
        }
    }

    public void sendInfoToOthers(String msg,SocketChannel self) throws IOException {

        for (SelectionKey key:selector.keys()){
            Channel channel=key.channel();
            if (channel instanceof  SocketChannel && channel!=self){
                SocketChannel dest=(SocketChannel) channel;
                ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
                dest.write(buffer);
            }
        }
    }

    public static void main(String[] args) {
        GroupChatServer groupChatServer = new GroupChatServer();
        groupChatServer.listen();
    }
}

客户端:

public class GroupChatClient {
    private static final String HOST="127.0.0.1";
    private static final int PORT=8888;
    private Selector selector;
    private SocketChannel socketChannel;
    private String userName;

    public GroupChatClient() throws IOException {
        selector = Selector.open();
        socketChannel=SocketChannel.open(new InetSocketAddress(HOST,PORT));
        socketChannel.configureBlocking(false);
        socketChannel.register(selector, SelectionKey.OP_READ);
        userName=socketChannel.getLocalAddress().toString().substring(1);
        System.out.println(userName + " is ok ...");
    }

    public void sendInfo(String info){
        info=userName+" say :" +info;
        try {
            socketChannel.write(ByteBuffer.wrap(info.getBytes()));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void readInfo(){
        try {
            int select = selector.select();
            if (select>0){
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    if (key.isReadable()){
                        SocketChannel socketChannel = (SocketChannel) key.channel();
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        socketChannel.read(buffer);
                        String msg = new String(buffer.array());
                        System.out.println(msg);
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws IOException {
        GroupChatClient groupChatClient = new GroupChatClient();

        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    groupChatClient.readInfo();
                }
            }
        }).start();

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

开始的疑惑和解答:

服务端和客户端都含有selector,两者是否有关系?

  • 没有关系,两者之间的关系类似于:
    -

零拷贝原理

DMA:direct memory access ,直接内存拷贝,不使用CPU

mmap优化:

通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。这样,在进行网络传输时,就可以减少内核空间到用户空间的拷贝次数

适合小数据量读写

sendFile优化:

linux2.1版本提供了sendFile函数,其基本原理为,数据根本不经过用户态,直接从内存和缓冲区进入到SocketBuffer,同时,优于和用户态完全不玩,就减少了一次上下文切换。

linux2.4版本做了一些修改,避免了从内核缓冲区拷贝到Socket buffer的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝。

适合大文件传输

示例:

server:

public class IOServer {
    public static void main(String[] args) throws IOException {
        InetSocketAddress address = new InetSocketAddress(8888);
        ServerSocketChannel socketChannel = ServerSocketChannel.open();
        socketChannel.socket().bind(address);

        ByteBuffer buffer = ByteBuffer.allocate(1024);
        while (true){
            SocketChannel accept = socketChannel.accept();
            int read=0;
            if (read!=-1){
                read= accept.read(buffer);
            }
            buffer.rewind();//position置为0,mark作废
        }
    }
}

client:

public class IOClient {
    public static void main(String[] args) throws IOException {
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(new InetSocketAddress("127.0.0.1",8888));
        String filename="d:/1.txt";
        FileChannel fileChannel = new FileInputStream(filename).getChannel();
        /*
        在linux下一个transferTo方法就可以完成传输
        在window下一次调用transferTo只能传输8M文件,就需要分段传输文件
        * */
        long start = System.currentTimeMillis();
        fileChannel.transferTo(0,fileChannel.size(),socketChannel);
        System.out.println("传输文件大小: " + fileChannel.size() + "使用时间: " + (System.currentTimeMillis() - start));
        fileChannel.close();
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值