JAVA的BIO、NIO、AIO模式精解(一)

1. BIO、NIO、AIO介绍

在不同系统或进程间数据交互,或高并发场景下都选哟网络通信。早期是基于性能低下的同步阻塞IO(BIO)实现。后支持非阻塞IO(NIO)。
前置须知:javsse,java多线程,javaIO,java网络模型
目的:局域网内通信,多系统间底层消息传递机制,高并发下大数据通信,游戏应用。

2 .java的io演进

2.1 IO模型基本说明

IO模型:性能取决于用什么通信模式或架构进行数据传输和接收。java共支持3种网络编程的IO,见标题

2.2 IO模型

javaBIO

同步并阻塞。服务器一个链接一个线程,即客户端请求服务器只启动一个线程处理,如果链接空闲则浪费线程开销。

javaNIO

同步非阻塞。服务器一个线程处理多个链接(请求)。即客户端请求都会注册到多路复用上。轮询到有IO请求就进行处理。
java NIO

javaAIO(NIO2.0版本)

异步非阻塞。服务器一个有效请求一个线程,客户端IO请求都由OS先完成了在通知服务器创建线程处理,一般用于连接数较多且链接时间较长应用。

2.3 BIO、NIO、AIO使用场景

  1. BIO:连接小且固定架构。JDK1.4前唯一选择,简单。
  2. NIO:连接多且较短架构。比如聊天,弹幕,服务间通讯等,JDK1.4后支持,复杂。
  3. AIO:连接多且较长家都。比如相册服务,充分调用OS参与并发,JDK1.7后支持,复杂。

3. Java BIO

3.1 BIO介绍

相关类接口间java.io。一个链接创建一个线程。可通过线程池优化成多客户端链接。

3.2 BIO机制

BIO

3.3 传统的BIO编程实例

网络编程CS架构实现两个进程间通信,服务端提供IP+PORT,客户端通过链接操作向服务端监听的端口地址发起请求。基于TCP三次握手建立链接,通过套接字Socket进行通信。
同步阻塞种服务端serverSocket负责绑定IP地址,启动监听端口。客户端Socket负责发起请求。通过输入输出流进行同步阻塞通信。
特点:C/S完全同步,耦合。

public class Client {
    public static void main(String[] args) throws IOException {
        //1.创建socket对象请求服务端的链接
        Socket socket = new Socket("127.0.0.1", 9999);
        //2.从socket对象中获取一个字节输出流
        OutputStream os = socket.getOutputStream();
        //3.把字节输出流包装成一个打印流
        PrintStream ps = new PrintStream(os);
        ps.println("hello world!服务端");
        ps.flush();
    }
}
/**
 * 目标:客户端发送消息,服务端接受消息
 */
public class Server {
    public static void main(String[] args) {
        try {
            System.out.println("服务端启动");
            //1.定义ServerSocket对象惊醒服务器端口注册
            ServerSocket serverSocket = new ServerSocket(9999);
            //2.监听客户端的Socket链接请求
            Socket socket = serverSocket.accept();
            //3.从socket管道中得到一个字节输入流对象
            InputStream is = socket.getInputStream();
            //4.把字节输入流包装成一个缓冲字符输入流 要以行为单位读取
            BufferedReader br = new BufferedReader(new InputStreamReader(is));
            String msg;
            if ((msg = br.readLine()) != null) {
                System.out.println("服务端接收到:" + msg);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

小结:

  • 在以上通信中,服务端会一直等待客户端的消息,如果客户端没有进行消息的发送,服务端将一直进入阻塞状态
  • 同时服务端是按照行 获取信息的,客户端也必须按照行 发送。否则服务端进入等待消息的阻塞态。

3.4 BIO实现多发和多收消息

在3中,只能客户端发送消息,服务端接收消息,并不能反复的接收和发送消息。改进:

Client:
        PrintStream ps = new PrintStream(os);
        Scanner sc = new Scanner(System.in);
        while(true) {
            System.out.println("input:");
            String msg = sc.nextLine();
            ps.println(msg);
            ps.flush();
        }
Server:
       while ((msg = br.readLine()) != null) {
           System.out.println("服务端接收到:" + msg);
       }

小结:

  • 服务端只能处理一个客户端请求,因为单线程,一次只能与一个客户端进行消息通信。

3.5 BIO下接收多个客户端

需在服务端引入多线程解决多客户端请求。这样就实现了一个客户端一个线程模型。

/**
 * 目标:实现服务端同时处理多个客户端的socket连接
 * 实现:服务端每接收一个客户端socket请求对象后都交给一个独立线程处理客户端的数据交互。
 */
public class Server {
    public static void main(String[] args) {
        try{
            //1.注册端口
            ServerSocket ss = new ServerSocket(9999);
            //2.定义死循环,不断接收客户端的Socket链接
            while(true) {
                Socket socket = ss.accept();
                //3.创建一独立的线程来处理与这个客户端的socket通信
                new ThreadServerReader(socket).start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
public class ThreadServerReader extends Thread {
    private Socket socket;

    public ThreadServerReader(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
            //从socket对象中得到一个字节输入流
            InputStream is = socket.getInputStream();
            //使用缓冲字符输入流包装字节输入流
            BufferedReader br = new BufferedReader(new InputStreamReader(is));
            String msg;
            while ((msg = br.readLine()) != null) {
                System.out.println(msg);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

小结:

  1. 每个Socket接收都会创建线程,线程竞争,切换上下文会影响性能。
  2. 每个线程都会占用栈空间和CPU资源
  3. 并不是每个Socket都进行IO操作,无意义的线程处理
  4. 客户端的并发访问增加时。服务端将呈现1:1的线程开销,访问量越大,系统将发生线程栈溢出,线程创建失败,最终导致进程宕机或僵死。

3.6 伪异步IO编程

采用线程池和任务队列实现,当客户端接入,将Socket封装成一个Task交由后端的线程池进行处理。JDK线程池维护一个消息队列和N个活跃线程。对消息队列中socket任务进行处理,由于线程池可设置消息队列的大小和最大线程数,因此,资源可控,无论多少客户端并发访问,都不会资源不够。

/**
 * 目标:伪异步通信架构
 */
public class Server {
    public static void main(String[] args) {
        try {
            ServerSocket ss = new ServerSocket(9999);
            //初始化线程池对象
            HandlerSocketServerPool pool = new HandlerSocketServerPool(6, 10);
            while(true) {
                Socket socket = ss.accept();
                //把socket封装成任务对象交给一个线程池来处理
                Runnable target = new ServerRunnableTarget(socket);
                pool.execute(target);
            }
        } catch (Exception e){
            e.printStackTrace();
        }
    }
}
//线程池
public class HandlerSocketServerPool {
    //1.创建一个线程池
    private ExecutorService excutorService;

    //2.初始化
    public HandlerSocketServerPool(int maxThreadNum, int queueSize) {
        excutorService = new ThreadPoolExecutor(3, maxThreadNum, 120, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(queueSize));
    }

    //3.提供方法提交任务给线程池的任务来暂存,等待线程池来执行
    public void execute(Runnable target){
        excutorService.execute(target);
    }
}
//功能
public class ServerRunnableTarget implements Runnable {
    private Socket socket;

    public ServerRunnableTarget(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        //处理客户端socket请求
        try {
            InputStream is = socket.getInputStream();
            BufferedReader br = new BufferedReader(new InputStreamReader(is));
            String msg;
            if ((msg = br.readLine()) != null) {
                System.out.println(msg);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

小结:

  • 伪异步io采用线程池实现,因此避免了为每个请求都创建独立线程造成资源耗尽的问题,由于底层还是同步阻塞,没解决根本问题。
  • 如果单个消息处理缓慢,或服务器全部阻塞。那么后面的socket的io消息都将在队列中排队,新的socket将被拒绝,客户端会大量链接超时。

3.7 基于BIO形势下的文件上传

/**
 * 实现客户端任意类型文件给服务端保存
 */
public class Client {
    public static void main(String[] args) {
        try {
            Socket socket = new Socket("127.0.0.1", 8888);
            //把字节输出流包装成一个数据输出流
            DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
            //发送文件后缀给服务端
            dos.writeUTF(".png");
            //把文件数据发送给服务端
            InputStream is = new FileInputStream("//Path");
            byte[] buffer = new byte[1024];
            int len;
            while ((len = is.read(buffer)) > 0) {
                dos.write(buffer, 0, len);
            }
            dos.flush();
            //通知服务端接收完毕
            socket.shutdownOutput();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
/**
 * 接收客户端任意类型文件并保存
 */
public class Server {
    public static void main(String[] args) {
        try {
            ServerSocket socket = new ServerSocket(8888);
            while (true) {
                Socket accept = socket.accept();
                //交给独立线程来处理与这个客户端的文件通信需求
                new ServerReaderThread(accept).start();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
public class ServerReaderThread extends Thread {
    private Socket socket;

    public ServerReaderThread(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
            //得到数据输入流读取客户端发送过来的数据
            DataInputStream dis = new DataInputStream(socket.getInputStream());
            //读取客户端发送的文件类型
            String suffix = dis.readUTF();
            System.out.println("收到文件,类型:" + suffix);
            //定义字节输出管道,负责把客户端发过来的数据写出
            FileOutputStream os = new FileOutputStream("C:\\path\\" + UUID.randomUUID().toString() + suffix);
            //从数据输入流中读取文件数据,写出到字节输出流中
            byte[] buffer = new byte[1024];
            int len;
            while ((len = dis.read(buffer)) > 0) {
                os.write(buffer, 0, len);
            }
            os.close();
            System.out.println("保存文件成功");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

3.8 javaBIO下的端口转发

需求:一个客户端消息可发送所有的客户端接收(群聊)
BIO端口转发

/**
 * BIO下服务端端口转发
 * 服务端需求:
 *      1.注册端口
 *      2.收到客户端的socket链接,交给独立的线程来处理
 *      3.把当前连接的客户端socket存入到一个所谓的在线socket集合中保存
 *      4.接收客户端消息,然后推送给当前所有在线的socket接收
 */
public class Server {
    //定义静态集合
    public static List<Socket> allSocketOnLine = new ArrayList<>();

    public static void main(String[] args) {
        try {
            ServerSocket ss = new ServerSocket(9999);
            while(true){
                Socket socket = ss.accept();
                //把登陆的客户端socket存入到一个在线集合中去
                allSocketOnLine.add(socket);
                //为当前登录成功的socket分配一个独立的线程来处理与之通信
                new ServerReaderThread(socket).start();
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}
public class ServerReaderThread extends Thread {
    private Socket socket;
    public ServerReaderThread(Socket socket){
        this.socket = socket;
    }
    @Override
    public void run(){
        try {
            BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            String msg;
            while ((msg = br.readLine())!=null){
                //服务端接收到客户端的消息推送给当前所有在线socket
                sendMsgToAllClient(msg);
            }
        }catch (Exception e){
            System.out.println("当前有人下线!");
            //从在线socket中移除本socket
            Server.allSocketOnLine.remove(socket);
            e.printStackTrace();
        }
    }

    //把当前客户端发来的消息推送全部在线socket
    private void sendMsgToAllClient(String msg) throws IOException {
        for (Socket sk : Server.allSocketOnLine) {
            PrintStream ps = new PrintStream(sk.getOutputStream());
            ps.println(msg);
            ps.flush();
        }
    }
}

3.9 基于BIO下即时通信

项目功能

需要解决客户端到客户端的通信,即实现客户端间的端口消息转发。
功能说明:
1.客户端登陆:输入用户名和服务端ip
2.在线人数实时更新:用户登录同步更新客户端联系人列表
3.离线人数更新:下线同步
4.群聊:任一客户端消息转发所有客户端接收
5.私聊:选择某一对象发送消息
6.@消息:可@该用户,所有人可见
7.消息用户和时间点:服务端记录用户消息时间点,然后进行多路转发或选择。

服务端设计

服务端接收多个客户端

服务端需要接收多个客户端的接入。

  • 1.服务端需要接收多个客户端,目前我们采取的策略是一个客户端对应一个服务端线程。
  • 2.服务端除了要注册端口以外,还需要为每个客户端分配一个独立线程处理与之通信。

服务端主体代码,主要进行端口注册,和接收客户端,分配线程处理该客户端请求

服务端接收登录消息及检测离线

接收客户端的登陆消息。

实现步骤
  • 需要在服务端处理客户端的线程的登陆消息。
  • 需要注意的是,服务端需要接收客户端的消息可能有很多种。
    • 分别是登陆消息,群聊消息,私聊消息 和@消息。
    • 这里需要约定如果客户端发送消息之前需要先发送消息的类型,类型我们使用信号值标志(1,2,3)。
      • 1代表接收的是登陆消息
      • 2代表群发| @消息
      • 3代表了私聊消息
  • 服务端的线程中有异常校验机制,一旦发现客户端下线会在异常机制中处理,然后移除当前客户端用户,把最新的用户列表发回给全部客户端进行在线人数更新。
服务端接收群聊

接收客户端发来的群聊消息推送给当前在线的所有客户端

实现步骤
  • 接下来要接收客户端发来的群聊消息。
  • 需要注意的是,服务端需要接收客户端的消息可能有很多种。
    • 分别是登陆消息,群聊消息,私聊消息 和@消息。
    • 这里需要约定如果客户端发送消息之前需要先发送消息的类型,类型我们使用信号值标志(1,2,3)。
      • 1代表接收的是登陆消息
      • 2代表群发| @消息
      • 3代表了私聊消息
服务端接收私聊

私聊消息的推送逻辑.

实现步骤
  • 解决私聊消息的推送逻辑,私聊消息需要知道推送给某个具体的客户端
  • 我们可以接收到客户端发来的私聊用户名称,根据用户名称定位该用户的Socket管道,然后单独推送消息给该Socket管道。
  • 需要注意的是,服务端需要接收客户端的消息可能有很多种。
    • 分别是登陆消息,群聊消息,私聊消息 和@消息。
    • 这里需要约定如果客户端发送消息之前需要先发送消息的类型,类型我们使用信号值标志(1,2,3)。
      • 1代表接收的是登陆消息
      • 2代表群发| @消息
      • 3代表了私聊消息
    小结
demo地址:https://gitee.com/xuyu294636185/JAVA_IO_DEMO.git
  • 本节我们解决了私聊消息的推送逻辑,私聊消息需要知道推送给某个具体的客户端Socket管道
  • 我们可以接收到客户端发来的私聊用户名称,根据用户名称定位该用户的Socket管道,然后单独推送消息给该Socket管道。

客户端设计

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值