Java常见的IO模型

  • 最基本的就是基于Socket的通信,统称为BIO,意味着同步阻塞通信。有消息也好,没消息也好,都会等待接收。通信的A、B两方,一直连接着,当发出请求后,持续等待另一方返回响应,不返回就一直等待,性能较差。当一方请求下线,另一方也是,体现了同步的特点,socket是两方紧紧耦合在一起的
  • 进阶的通信技术:NIO通信模式。同步非阻塞通信。当发出请求后,不需要持续等待另一方返回响应,可以过一会来查看是否有返回信息,不会一直等待阻塞。
  • 进阶的通信技术:NIO2.0(AIO通信)模式。异步非阻塞通信。有数据的时候通知,不需要请求方去访问,等待,性能较好

BIO通信模式下的案例介绍

案例1:一个客户端发消息,一个服务端收消息(这个案例只能发一次消息)

public class ServerDemo {
    public static void main(String[] args) throws IOException {
        System.out.println("服务器启动");
        //服务器注册一个端口
        ServerSocket serverSocket = new ServerSocket(8888);
        //暂停接收客户端请求,得到一个端到端的socket管道
        Socket accept = serverSocket.accept();
        //从socket管道得到字节输入流
        InputStream inputStream = accept.getInputStream();
        //将字节输入流包装成自己想要的流读取
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
        //读取数据
        String len;
        while ((len = bufferedReader.readLine()) != null) {
            System.out.println("服务端收到消息:" + len);
        }
    }
}
public class ClientDemo {
    public static void main(String[] args) throws IOException {
        System.out.println("客户端启动");
        Socket socket = new Socket(InetAddress.getLocalHost(), 8888);
        OutputStream outputStream = socket.getOutputStream();
        PrintStream printStream = new PrintStream(outputStream);
        printStream.println("客户端:我们开始交流吧");
        printStream.flush();
    }
}

        上述的服务端代码是有问题的,当客户端发送一次消息就结束了,socket是耦合的,客户端断开,服务器也断开了,那么当客户端再次发送的时候,服务端没有启动,会抛出Connection refused (Connection refused)异常,只支持一次发送。

案例2:客户端可以反复的发送消息,服务端可以反复的接收消息

public class ServerDemo {
    public static void main(String[] args) throws IOException {
        System.out.println("服务器启动===");
        //注册端口
        ServerSocket serverSocket = new ServerSocket(8888);
        //这里暂停等待客户端的连接,得到一个端到端的socket管道
        Socket socket = serverSocket.accept();
        //从Socket中得到一个字节输入流
        InputStream inputStream = socket.getInputStream();
        //将流封装成需要的流对象,进去读取操作
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
        //读取操作
        String line;
        while ((line = bufferedReader.readLine()) != null) {
            System.out.println("服务器收到:" + line);
        }
    }
}
public class ClientDemo {
    public static void main(String[] args) throws IOException {
        System.out.println("客户端启动");
        Socket socket = new Socket("127.0.0.1", 8888);
        OutputStream outputStream = socket.getOutputStream();
        PrintStream printStream = new PrintStream(outputStream);
        Scanner scanner = new Scanner(System.in);
        while (true) {
            System.out.println("请说");
            String msg = scanner.nextLine();
            printStream.println(msg);
            //这里不能写printStream.close();如果将流关闭了,socket是端到端的,服务器也就关闭,无法接收消息了
            printStream.flush();
        }
    }
}

        本案例可以实现多发多收,但是只能处理一个客户端的请求,当多个客户端连接时,只接收第一个客户端的请求,服务端是单线程的,一次只能和一个客户端进行通信。

案例3:一个服务端可以接收无数个客户端的连接,并且可以接收他们反复发送消息

public class ServerReadThread extends Thread {
    private Socket socket;
    public ServerReadThread(Socket socket) {
        this.socket = socket;
    }
    @Override
    public void run() {
        try {
            InputStream inputStream = socket.getInputStream();
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
            String len;
            while ((len = bufferedReader.readLine()) != null) {
                System.out.println("服务端收到:" + socket.getRemoteSocketAddress() + ":" + len);
            }
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println(socket.getRemoteSocketAddress() + "下线了");
        }
    }
}
public class ServerDemo {
    /**
     * 思路:接收多个客户端,那么使用多个服务器线程来接收
     *
     * @param args
     * @throws IOException
     */
    public static void main(String[] args) throws IOException {
        System.out.println("服务器启动===");
        //注册端口
        ServerSocket serverSocket = new ServerSocket(8888);
        while (true) {
            Socket socket = serverSocket.accept();
            //创建多个客户端连接线程来接收消息
            new ServerReadThread(socket).start();
            System.out.println(socket.getRemoteSocketAddress() + "上线了");
        }
    }
}
public class ClientDemo {
    public static void main(String[] args) throws IOException {
        System.out.println("客户端启动");
        Socket socket = new Socket("127.0.0.1", 8888);
        OutputStream outputStream = socket.getOutputStream();
        PrintStream printStream = new PrintStream(outputStream);
        Scanner scanner = new Scanner(System.in);
        while (true) {
            System.out.println("请说");
            String msg = scanner.nextLine();
            printStream.println(msg);
            //这里不能写printStream.close();如果将流关闭了,socket是端到端的,服务器也就关闭,无法接收消息了
            printStream.flush();
        }
    }
}
客户端1输出:
客户端启动
请说
1
请说
2
=========
客户端2输出:
客户端启动
客户端启动
请说
3
请说
4
=========
服务端输出:
服务器启动===
/127.0.0.1:59226上线了
服务端收到:/127.0.0.1:59226:1
服务端收到:/127.0.0.1:59226:2
/127.0.0.1:59231上线了
服务端收到:/127.0.0.1:59231:3
服务端收到:/127.0.0.1:59231:4

总结

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

案例4:伪异步I/O通信方案

       提供一个线程池里面放固定的线程数,处理若干个客户端。将客户端的Socket封装成一个Task,交给后端线程池处理,JDK的线程池维护消息队列和N个活跃的线程,对消息队列中Socket任务进行处理。

  • 优点:无论有多少个客户端进行连接,线程数量是可控的,不会引起服务器的宕机和僵死
  • 缺点:虽然线程池可以固定线程数量,但是同时也只能处理这么多客户端,在满负荷状态下必须某个客户端断开连接,其他的客户端才可以接入
//线程池处理类
public class HandlerSocketThreadPool {
    //线程池
    private ExecutorService executorService;
    //线程池,最大运行3个,队列数最多为100个
    public HandlerSocketThreadPool(int maxPoolSize, int queueSize) {
        this.executorService = new ThreadPoolExecutor(3, maxPoolSize,
                120L, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(queueSize));
    }
    public void execute(Runnable task) {
        this.executorService.execute(task);
    }
}
//读取客户端消息类
public class ReaderClientRunnable implements Runnable {
    private Socket socket;
    public ReaderClientRunnable(Socket socket) {
        this.socket = socket;
    }
    @Override
    public void run() {
        try {
            InputStream inputStream = socket.getInputStream();
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
            String len;
            while ((len = bufferedReader.readLine()) != null) {
                System.out.println("服务器端收到了:" + len);
            }
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("有人下线了");
        }
    }
}
//模拟客户端发请求
public class ClientDemo {
    public static void main(String[] args) throws IOException {
        System.out.println("客户端启动");
        //建立一个socket连接
        Socket socket = new Socket("127.0.0.1", 8888);
        //从socket管道中获取一个输出流,写数据到服务端
        OutputStream outputStream = socket.getOutputStream();
        //将输出流包装成打印流
        PrintWriter printStream = new PrintWriter(outputStream);
        //接收用户的输入
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
        String len;
        //写入消息
        while ((len = bufferedReader.readLine()) != null) {
            //注意,这里得使用println(),因为服务器端是readLine()读取的
            printStream.println(len);
            //为了将内存中的数据全部强制写出
            printStream.flush();
        }
    }
}
//服务器接收多个客户端的请求
public class ServerDemo {
    /**
     * 思路:接收多个客户端,那么使用多个服务器线程来接收
     *
     * @param args
     * @throws IOException
     */
    public static void main(String[] args) throws IOException {
        System.out.println("服务器启动===");
        try {
            //注册端口
            ServerSocket serverSocket = new ServerSocket(8888);
            //只需要一个服务端对应线程池,不需要每次都创建一个线程处理客户端的请求
            HandlerSocketThreadPool threadPool = new HandlerSocketThreadPool(3, 100);
            //客户端可能有多个
            while (true) {
                //阻塞式请求
                Socket socket = serverSocket.accept();
                System.out.println("有人上线了");
                //每次收到一个客户端的socket请求,都需求为该客户端分配一个线程,处理其请求
                threadPool.execute(new ReaderClientRunnable(socket));
            }
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("有人下线了");
        }
    }
}

案例5:实现客户端与客户端通信消息(端口转发)

场景:适合固定通信个数的应用,不会引起宕机!伪异步实现的端口转发

思路:服务端要记录当前全部在线的客户端通道,一旦有一个客户端发来消息,服务端就把这个消息全部推送给全部在线的客户端通道

总结BIO思想

  • 同步阻塞式通信,一个线程处理一个客户端,而且是等着消息回复。
  • 伪异步I/O可以固定服务端的线程个数,不会引起高并发下宕机,但是同时只能处理若干个客户端,性能极差,只适合低并发,低负载的业务场景

NIO通信模式

       同步非阻塞式通信,性能较好,在没有消息的情况下,服务端可以去看其他通道是否有消息,服务端只提供一个线程去轮询所有的客户端,有就处理,没有就继续轮询。但是编程很复杂,调试也很麻烦,优点是性能较好,可以支持并发通信


说到Java通信,就不得不说开源框架Netty。Netty具备以下优点

  • API使用简单,开发门槛较低
  • 功能强大,预置了多种编码功能,支持多种主流协议
  • 定义能力强,可以通过ChannelHandler对通信框架进行灵活地扩展
  • 与其他NIO框架相比,Netty的综合性能最优
  • 成熟,稳定,Netty修复了已经发现的所有JDK NIO的Bug
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

芦蒿炒香干

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值