伪异步 I/O 编程

本文导读

  •  传统 BIO 编程》同步阻塞 I/O 一个链接需要一个线程处理,而在线程方面仍有优化的余地,Java JDK1.5 开始引入线程池,也叫 Executor 框架 或 Java 并发框架,使用线程池来替代单个的线程,其优点不言而喻。
  • 线程池可以参考《 线程池理论 之 线程池饱和策略 与 工作队列排队策略》、《线程池(ThreadPoolExecutor) 创建与使用
  • 后台通过一个线程池来处理多个客户端的请求接入,形成 客户端个数 M : 线程池最大线程数 N 的比例关系,其中 M 可以远远大于 N。通过线程池可以灵活的调配线程资源,设置线程的最大数,以及每个线程的工作任务队列数,防止由于海量并发接入导致线程耗尽。

  • 采用线程池和任务队列可以实现如上所示的伪异步 I/O 通信框架,当新的客户端接入时,将客户端的 Socket 封装成一个 Task(实现 java.lang.Runnable 接口的任务)传递到线程池中进行处理,JDK 线程池维护一个消息队列和 N 个活跃线程,对消息队列中的任务进行处理。
  • 由于线程池可以设置消息队列的大小和最大线程数,因此它的资源时可控的,无论多少客户端并发访问,都不会导致资源的耗尽和宕机。

伪异步I/O

  • 仍然以《传统 BIO 编程》中的示例进行改写,客户端往服务器发送数据,服务器回复数据。

·服务端·

  • 如下示例中线程池中的核心线程数、最大线程数、以及工作队列大小只是为了测试结果更加清晰,所以故意设置比较小,实际中应该根据服务器性能与需求适当调大一点,如 最大线程数 30,任务队列1000 等等。
package com.lct.bio;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * Created by Administrator on 2018/10/14 0014.
 * 时间服务器
 */
public class TimeServer {
    public static void main(String[] args) {
        tcpAccept();
    }

    public static void tcpAccept() {
        ServerSocket serverSocket = null;

        /**
         * 构造线程池
         * ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,
         * TimeUnit unit,BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler)
         * 参数含义:2个核心线程数,最大线程数为5,空闲线程最大等待时间为 120秒,任务队列大小2个,线程饱和策略采用抛弃旧任务策略
         * 应该根据实际场景进行合适调整
         */
        ExecutorService executor = new ThreadPoolExecutor(2, 5,
                120L, TimeUnit.SECONDS,
                new ArrayBlockingQueue<Runnable>(2),
                new ThreadPoolExecutor.DiscardOldestPolicy());

        try {
            /**Tcp 服务器监听端口,ip 默认为本机地址*/
            serverSocket = new ServerSocket(8080);
            /**循环监听客户端的连接请求
             * accept 方法会一直阻塞,直到 客户端连接成功,主线程才继续往后执行*/
            Socket socket = null;
            while (true) {
                System.out.println("我是线程 " + Thread.currentThread().getName() + ",等待客户端连接..........");
                socket = serverSocket.accept();
                System.out.println("我是线程 " + Thread.currentThread().getName() + ",客户端连接成功..........");
                /**线程池调度线程执行任务
                 * 为每一个客户端连接都新开线程进行处理
                 */
                executor.execute(new TimeServerHandler(socket));
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            /**发生意外时,关闭服务端*/
            if (serverSocket != null && !serverSocket.isClosed()) {
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
  • 对每个客户端连接新开线程单独处理:
package com.lct.bio;

import java.io.*;
import java.net.Socket;
import java.util.Date;

/**
 * Created by Administrator on 2018/10/14 0014.
 * 为每个 TCP 客户端新开线程进行处理
 */
public class TimeServerHandler implements Runnable {
    private Socket socket = null;

    /**
     * 将每个 TCP 连接的 Socket 通过构造器传入
     *
     * @param socket
     */
    public TimeServerHandler(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        DataInputStream dataInputStream = null;
        DataOutputStream dataOutputStream = null;
        try {
            /**读客户端数据*/
            InputStream inputStream = socket.getInputStream();
            dataInputStream = new DataInputStream(inputStream);
            String message = dataInputStream.readUTF();
            System.out.println("我是线程 " + Thread.currentThread().getName() + " ,收到客户端消息:" + message);

            /**往客户端写数据*/
            OutputStream outputStream = socket.getOutputStream();
            dataOutputStream = new DataOutputStream(outputStream);
            dataOutputStream.writeUTF(new Date().toString());
            dataOutputStream.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            /**操作完成,关闭流*/
            if (dataOutputStream != null) {
                try {
                    dataOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (dataInputStream != null) {
                try {
                    dataInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            /**操作完成,关闭连接,线程自动销毁*/
            if (socket != null && !socket.isClosed()) {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

·客户端·

package com.lct.bio;

import java.io.*;
import java.net.Socket;

/**
 * Created by Administrator on 2018/10/14 0014.
 * 时间 客户端
 */
public class TtimeClient {
    public static void main(String[] args) {
        /**
         * 5个线程模拟5个客户端
         */
        for (int i = 0; i < 5; i++) {
            new Thread() {
                @Override
                public void run() {
                    tcpSendMessage();
                }
            }.start();
        }
    }

    /**
     * Tcp 客户端连接服务器并发送消息
     */
    public static void tcpSendMessage() {
        Socket socket = null;
        DataOutputStream dataOutputStream = null;
        DataInputStream dataInputStream = null;
        try {
            /**
             * Socket(String host, int port):
             *      host)被连接的服务器 IP 地址
             *      port)被连接的服务器监听的端口
             * Socket(InetAddress address, int port)
             *      address)用于设置 ip 地址的对象
             * 此时如果 TCP 服务器未开放,或者其它原因导致连接失败,则抛出异常:
             * java.net.ConnectException: Connection refused: connect
             */
            socket = new Socket("127.0.0.1", 8080);
            System.out.println("连接成功.........." + Thread.currentThread().getName());

            /**往服务端写数据*/
            OutputStream outputStream = socket.getOutputStream();
            dataOutputStream = new DataOutputStream(outputStream);
            dataOutputStream.writeUTF("我是长城" + Thread.currentThread().getName());
            dataOutputStream.flush();

            /**读服务端数据*/
            InputStream inputStream = socket.getInputStream();
            dataInputStream = new DataInputStream(inputStream);
            String message = dataInputStream.readUTF();
            System.out.println("收到服务器消息:" + message);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            /**关闭流,释放资源*/
            if (dataOutputStream != null) {
                try {
                    dataOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (dataInputStream != null) {
                try {
                    dataInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            /** 操作完毕关闭 socket*/
            if (socket != null && !socket.isClosed()) {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

服务端控制台输出:

我是线程 main,等待客户端连接..........
我是线程 main,客户端连接成功..........
我是线程 main,等待客户端连接..........
我是线程 main,客户端连接成功..........
我是线程 main,等待客户端连接..........
我是线程 main,客户端连接成功..........
我是线程 main,等待客户端连接..........
我是线程 main,客户端连接成功..........
我是线程 main,等待客户端连接..........
我是线程 main,客户端连接成功..........
我是线程 main,等待客户端连接..........
我是线程 pool-1-thread-3 ,收到客户端消息:我是长城Thread-1
我是线程 pool-1-thread-1 ,收到客户端消息:我是长城Thread-3
我是线程 pool-1-thread-2 ,收到客户端消息:我是长城Thread-0
我是线程 pool-1-thread-2 ,收到客户端消息:我是长城Thread-4
我是线程 pool-1-thread-3 ,收到客户端消息:我是长城Thread-2

客户端控制台输出:

连接成功..........Thread-0
连接成功..........Thread-4
连接成功..........Thread-3
连接成功..........Thread-2
连接成功..........Thread-1
收到服务器消息:Mon Oct 15 17:30:11 CST 2018
收到服务器消息:Mon Oct 15 17:30:11 CST 2018
收到服务器消息:Mon Oct 15 17:30:11 CST 2018
收到服务器消息:Mon Oct 15 17:30:11 CST 2018
收到服务器消息:Mon Oct 15 17:30:11 CST 2018

总 结

  • 熟悉基础 TCP 编程(TCP 理论详解)的应该知道,当对 Socket 的输入流进行读取操作时,它会一直阻塞下去,直到发生以下事件:

1)有数据可读

2)可用数据已经读取完毕

3)发生空指针或者 I/O 异常

4)socket.setSoTimeout  数据读取超时

扫描二维码关注公众号,回复: 3662981 查看本文章
  • 这意味着当对方发送请求或者应答消息比较缓慢,或者网络传输较慢时,读取输入流一方的通信线程将被长时间阻塞,如果对方要 60 秒才能将数据发送完成,则读取一方的 I/O 线程也将会被阻塞 60 秒,在此期间,其它接入消息只能在任务队列中排队。
  • 同时 Socket 的输出流 OutputStream 写操作时,也是会阻塞的,直到所有要发送的字节全部写入完毕,或者发生异常。、
  • 伪异步 I/O 实际上仅仅是对 BIO 线程模型的简单优化,无法从根本上解决同步 I/O 导致的通信线程阻塞问题。

·····下一篇《 NIO 理论 与 编程

猜你喜欢

转载自blog.csdn.net/wangmx1993328/article/details/83061564