Netty入门_N/O模型(一)

一、介绍

Java共支持3种网络编程模型I/O模式:BIO、NIO、AIO
简单来说就是:用什么样的通道进行数据的发送和接收,很大程度上决定了程序通信的性能。

二、BIO 同步并阻塞模型

1.介绍BIO

服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就会需要启动一个线程进行处理,会造成不必要的线程开销。
适用场景:连接数目较小,且固定的架构。

2.BIO编程流程

  1. 服务器端启动一个ServerSocket
  2. 客户端启动Socket对服务器端进行通讯,默认情况下服务器端需要对每个客户简历一个线程与之通讯。
  3. 客户端发出请求后,先咨询服务器是否有线程响应,如果没有则会等待,或者被拒绝。
  4. 如果有响应,客户端线程会等待请求结束后,在继续执行。

3.代码实现

package com.lsh;

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;

/**
 * @author :LiuShihao
 * @date :Created in 2020/9/1 4:58 下午
 * @desc :BIO  同步阻塞模型
 */
public class bio {
    
    
    public static void main(String[] args) throws IOException {
    
    
        /**
         * 思路:
         * 1.创建一个线程池
         * 2.如果有客户端,就创建一个线程,与之通讯
         *
         */
        ExecutorService newexecutorService = Executors.newCachedThreadPool();
        //创建一个ServerSocket
        ServerSocket serverSocket = new ServerSocket(6666);
        System.out.println("服务器已经启动了");
        System.out.println("等待连接....");

        while(true){
    
    
            //监听,等待客户端连接
            final Socket accept = serverSocket.accept();
            System.out.println("连接到一个客户端");
            //创建一个线程
            newexecutorService.execute(new Runnable() {
    
    
                public void run() {
    
    
                    handler(accept);
                }
            });
        }
    }

    public static void handler(Socket socket){
    
    
        try{
    
    
            byte[] bytes = new byte[1024];
            //通过socket获取输入流
            InputStream inputStream = socket.getInputStream();
            //循环的读取客户端发送的数据
            while(true){
    
    
                //将数据读取到bytes字节数组中去
                System.out.println("read....");
                int read = inputStream.read(bytes);
                if (read != -1){
    
    
                    //输出客户端发送的数据 将bytes字节数组中的数据从0开始到read(数据长度)读取到字符串中去
                    System.out.println("当前线程:"+ Thread.currentThread().getId()+"-"+Thread.currentThread().getName());
                    System.out.println(new String(bytes,0,read));
                }else {
    
    
                    break;
                }
            }
        }catch (Exception e){
    
    
            e.printStackTrace();
        }finally {
    
    
            try {
    
    
                socket.close();
                System.out.println("关闭client连接");
            } catch (IOException e) {
    
    
                e.printStackTrace();
            }
        }
    }
}

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

三、NIO 同步非阻塞模型

1.介绍NIO

服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求就进行处理。
适用场景:连接数目较多且连接时间较短的架构,比如聊天服务器、弹幕系统等。

1).Selector选择器

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

2).SelectionKey

SelectionKey表示Selector和网络通道的注册关系,共四种:
OP_ACCEPT:表示有新的网络连接 值为16
OP_CONNECT:表示连接已经建立 值为8
OP_READ:代表读操作 值为1
OP_WRITE:代表写操作,值为4
在这里插入图片描述

3).ServerSocketChannel

ServerSocketChannel在服务器端监听新的客户端Scoket连接。
在这里插入图片描述

# 得到一个ServerScoketChannel通道
 public static ServerSocketChannel open() {
    
    }
# 设置服务器端端口号
public abstract ServerSocketChannel bind(SocketAddress local, int backlog){
    
    }
       
# 设置阻塞或非阻塞模式  false为非阻塞 NIO编程需要设置为false
public final SelectableChannel configureBlocking(boolean block){
    
    }
# 接收一个连接 返回这个连接的通道对象
public abstract SocketChannel accept(){
    
    }
# 注册一个选择器 并设置监听事件
public final SelectionKey register(Selector sel, int ops,
                                       Object att){
    
    }

4).ScoketChannel

ScoketChannel 网络IO通道,具体负责进行读写操作,NIO把缓冲区的数据写到通道,或者吧通道中的数据写入到缓冲区。
在这里插入图片描述

# 往通道中写数据
 public abstract int write(ByteBuffer src) {
    
    }
# 从通道中读数据
 public abstract int read(ByteBuffer dst) {
    
    }
 

2.NIO和BIO的比较

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

3.NIO应用实例-群聊系统

服务器端:
在这里插入图片描述
客户端:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

需求:
1)编写一个NIO群聊系统,实现服务器
端和客户端之间的数据简单通讯(非
阻塞)
2)实现多人群聊
3) 服务器端:可以监测用户上线,离线,
并实现消息转发功能
4)客户端:通过channel可以无阻塞发送
消息给其它所有用户,同时可以接受
其它用户发送的消息(有服务器转发得
到)
5) 目的:进一步理解NIO非阻塞网络编程
机制

服务器代码:

package com.lsh.wechat;

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

/**
 * @author :LiuShihao
 * @date :Created in 2020/9/17 10:45 上午
 * @desc :
 */
public class WeChatServer {
    
    
    //定义属性
    private Selector selector;
    private ServerSocketChannel listenChannel;
    private static final int PORT = 6667;

    //初始化  构造器
    public WeChatServer(){
    
    
        try{
    
    
            //得到选择器
            selector = Selector.open();
            //获得ServerSocketChannel
            listenChannel = ServerSocketChannel.open();
            //绑定端口
            listenChannel.socket().bind(new InetSocketAddress(PORT));
            //设置非阻塞
            listenChannel.configureBlocking(false);
            //将ServerSocketChannel注册到selector
            listenChannel.register(selector, SelectionKey.OP_ACCEPT);
        }catch(IOException e){
    
    
            e.printStackTrace();
        }
    }

    //监听
    public void listen(){
    
    
        try{
    
    
            //循环处理
            while(true){
    
    
//                int count = selector.select(2000);
                int count = selector.select();
                if (count>0){
    
    //有事件要处理
                    Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                    while(iterator.hasNext()){
    
    
                        SelectionKey key = iterator.next();
                        //监听accept事件
                        if (key.isAcceptable()){
    
    
                            SocketChannel accept = listenChannel.accept();
                            //设置非阻塞
                            accept.configureBlocking(false);
                            //将accept注册到Selector
                            accept.register(selector,SelectionKey.OP_READ);
                            // 上线提示
                            System.out.println(accept.getRemoteAddress()+" 上线了");
                        }
                        if (key.isReadable()){
    
    
                            //通道发生 READ事件
                            //从通道中读取数据到BUffer中
                            readData(key);
                        }
                        //当前的key删除,防止重复处理
                        iterator.remove();
                    }
                }else {
    
    
                    System.out.println("等待中...");
                }
            }
        }catch (IOException e){
    
    
            e.printStackTrace();
        }
    }

    private void  readData (SelectionKey key)  {
    
    
            //定义一个SocketChannel
            //得到关联的channel
            SocketChannel channel= (SocketChannel)key.channel();
            //创建Buffer
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        try {
    
    
             int count = channel.read(byteBuffer);
            if (count>0){
    
    
                String msg = new String(byteBuffer.array());
                System.out.println("客户端消息: "+msg);
                //向其他客户端转发消息
                sendInfoOther(msg,channel);
            }
        } catch (IOException e) {
    
    
            try {
    
    
                System.out.println(channel.getRemoteAddress()+" 离线了...");
                //取消注册
                key.cancel();
                //关闭通道
                channel.close();
            } catch (IOException ex) {
    
    
                ex.printStackTrace();
            }
        }
    }

    private void sendInfoOther(String msg,SocketChannel self) throws IOException {
    
    
        System.out.println("服务器转发消息");
        //遍历所有注册到selectorng上的SocketorChannel  并排除自己
        for (SelectionKey key : selector.keys()) {
    
    
            //通过key 取出对应的SocketChannel
            Channel targetChannel = key.channel();
            //排除自己
            if (targetChannel instanceof SocketChannel && targetChannel!= self){
    
    
                SocketChannel dest =  (SocketChannel)targetChannel;
                //将msg存储到buffer
                ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
                //将buffer中的数据写入到通道中
                dest.write(buffer);
            }
        }
    }
    public static void main(String[] args) {
    
    
        WeChatServer weChatServer = new WeChatServer();
        weChatServer.listen();
    }
}

客户端代码:

package com.lsh.wechat;

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;

/**
 * @author :LiuShihao
 * @date :Created in 2020/9/17 2:02 下午
 * @desc :
 */
public class WeChatClient1 {
    
    

    private static final String HOST ="127.0.0.1";
    private static final int PORT = 6667;
    private SocketChannel socketChannel;
    private Selector selector;
    private String username;

    public WeChatClient1() throws IOException {
    
    
        selector = Selector.open();
        socketChannel = socketChannel.open(new InetSocketAddress(HOST,PORT));
        socketChannel.configureBlocking(false);
        //将channle注册到selector
        socketChannel.register(selector, SelectionKey.OP_READ);
        username = socketChannel.getLocalAddress().toString().substring(1);
        System.out.println(username+"is OK...");

    }

    /**
     * 向服务器发送消息
     */
    public void snedInfo(String info){
    
    
        info = username + "说:"+info;
        try {
    
    
            socketChannel.write(ByteBuffer.wrap(info.getBytes()));
        }catch (Exception e){
    
    
            e.printStackTrace();
        }
    }
    public void readInfo(){
    
    
        try {
    
    
            int select = selector.select(2000);
            if (select>0){
    
    
                //有可以用的通道
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while(iterator.hasNext()){
    
    
                    SelectionKey key = iterator.next();
                    if (key.isReadable()){
    
    
                        //得到相关通道
                        SocketChannel channel = (SocketChannel)key.channel();
                        //得到一个buffer
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        //将通道中的数据读取到缓冲区
                        channel.read(buffer);
                        //把读到缓冲区的数据 转成字符串
                        String msg = new String(buffer.array());
                        System.out.println(msg.trim());
                    }
                    iterator.remove();
                }
            }else {
    
    
//                System.out.println("没有可用的通道");

            }

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

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

        WeChatClient1 weChatClient = new WeChatClient1();

        new Thread(()->{
    
    
            while (true){
    
    
                weChatClient.readInfo();
                    try {
    
    
                        Thread.currentThread(). sleep(3000);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
            }
        }).start();
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNextLine()){
    
    
            String s = scanner.nextLine();
            weChatClient.snedInfo(s.trim());
        }

    }
}

Bug:
这里发生了一个bug,暂时还未解决,就是客户端下线的时候,服务器端并没有打印下线提醒。

3.NIO零拷贝

零拷贝是网络编程的关键,很多性能优化都离不开,在Java中常用的零拷贝技术有mmap(内存映射)和sendFile。

零拷贝从操作系统角度,是没有CPU拷贝。
DMA拷贝:直接内存拷贝不使用CPU
在这里插入图片描述

四、AIO异步非阻塞模型

1.介绍AIO

AIO引入异步通道的概念,采用了Rroactor模式,简化了程序编写,对于有效的请求才启动线程,他的特点是先由操作系统完成后才通知服务端启动线程去处理,一般适用于连接数较多且连接时间较长的应用。
(目前应用不广泛)
使用场景:连接数目较多且连接时间较长的架构。

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/DreamsArchitects/article/details/108350238