java网络编程之传统IO与伪异步IO(一)

概念理解

      Socket又称“套接字”,应用程序通常通过”套接向网络发出请求或者应答网络请求、Socket和ServerSocket类库位于java.net包中。ServerSocket用于服务器端,Socket是建立网络连接时使用的。在连接成功时,应用程序两端都会产生一个Socket实例,操作这个实例,完成所需的会话。对于一个网络许接来说,套接字是平等的,不因为在服务器端或在客户端而产生小同级别。不管是Socket还是ServerSocket它们的工作都是通过SocketImpl类及其子类完成的。

      套接字之间的连接过程可以分为四个步骤:服务器监听,客户端请求服务器,服务器确认,客户端确认,进行通信。

(1) 服务器监听:是服务器端套接字并不位具体的客户端套接字,面是处于等待连接的状态,实时监控网络状态。

(2) 客户端请求,是指由客户端的套接字提出连接请求,要连接的目标是服务器端的接字。为此,客户端的套接字必须首先描述它要接的服务器的套接字,指出服务器端套接字的地址和端囗号,然后就向服务器端套接字提出连接请求。

(3) 服务器端连接确认,是指当服务器端套接字监听到或者说接收到客户端套接字的连接请求,它就晌应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发给客户端。

(4) 客户端接确认:一旦客户端确认了此描述,诈接就津立好了。双方开始进通信。而服务器端套接字处于监听状态,继续接收其他客户端套接字的连接请求。

IO (BIO)和NIO的区别:其本质就是阻塞和非阻塞的区别

阻塞概念:应用程序在获取网络数据的时候,如果网络传输数据很慢,那么程序就一直等着,直到传输完毕为止。

非阻塞概念;应用程序直接可以获取己经准备就绪好的数据,无需等待。

IO为同步阻塞形式,NIO为同步非阻塞形式·。NI并没有实现异步,在JDK1.7之后。升级了NIO库包,支持异步非阻塞通信模型即NIC2.0 (AIO)。

同步和异步:同步和异步一般是面向操作系统与用程序对操作的层面上来区别的。

同步时,应用程序会直接参与IO读写操作,并且我们的应用程序会直接阻塞到某一个方法上,直到数据准备就绪;或者采用轮询的策略实时检查数据的就绪状态,如果就绪获取数据。

异步时,则所有IO读写操作交给操作系统处理。与我们的应用程序没有直接关系,我们程序不需要关心IO读写。当作系统完成了IO读写操作时,会给我们应用程序发送通知,我们的应用程序直接拿走数据即可。

同步说的是你的server服务器端的执行方式

阻塞说的是具体的技术,接收数据的方式、状态(IO、NIO)

传统的BIO编程

      网络编程的基本模型是Client/Server模型,也就是两个进程直接进行相互通信,其中服务端提供配置信息(绑定的IP地址和监听端囗),客户端通过连接操作向服务端端监听的地址发起连接请求,通过三次握手建立连接,如果连接成功,则双方即可以进行通信(网络套接字socket)。

BIO通信模型图

      我们通过下面的通信模型图来熟悉下BIO的服务端通信模型:采用BIO通信模型的服务端,通常由一个独立的Acceptor线程负责监听客户端的连接,它接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回应答给客户端,线程销毁。这就是典型的一请求一应答通信模型。
这里写图片描述

      该模型最大的问题就是缺乏弹性伸缩能力,当客户端并发访问量增加后,服务端的线程个数和客户端并发访问数呈1:1的正比关系,由于线程是JAVA虚拟机非常宝贵的系统资源,当线程数膨胀之后,系统的性能将急剧下降,随着并发访问量的继续增大,系统会发生线程堆栈溢出、创建新线程失败等问题,并最终导致进程宕机或者僵死,不能对外提供服务。

下面通过一个案例来说明BIO弊端。

案例

Server.java


public class Server {

    final static int PROT = 8765;

    public static void main(String[] args) {

        ServerSocket server = null;
        try {
            //通过构造函数创建ServerSocket
            server = new ServerSocket(PROT);
            System.out.println(" server start .. ");
            //进行阻塞
            Socket socket = server.accept();
            //新建一个线程执行客户端的任务
            new Thread(new ServerHandler(socket)).start();

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if(server != null){
                try {
                    server.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            server = null;
        }
      }
}

ServerHandler.java

public class ServerHandler implements Runnable{

    private Socket socket ;

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

    @Override
    public void run() {
        BufferedReader in = null;
        PrintWriter out = null;
        try {
            in = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
            out = new PrintWriter(this.socket.getOutputStream(), true);
            String body = null;
            while(true){
                //通过BufferedReader读取一行,如果已经读到了输入流的尾部,则返回值为null,退出循环
                body = in.readLine();
                if(body == null) break;
                System.out.println("Server :" + body);
                //通过PrintWriter的println函数发送给客户端,最后退出循环
                out.println("服务器端回送响的应数据.");
            }

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if(in != null){
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(out != null){
                try {
                    out.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            if(socket != null){
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            socket = null;
        }


    }

}

Client.java

public class Client {

    final static String ADDRESS = "127.0.0.1";
    final static int PORT = 8765;

    public static void main(String[] args) {

        Socket socket = null;
        BufferedReader in = null;
        PrintWriter out = null;

        try {
            socket = new Socket(ADDRESS, PORT);
            in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            out = new PrintWriter(socket.getOutputStream(), true);

            //向服务器端发送数据
            out.println("接收到客户端的请求数据...");
            String response = in.readLine();
            System.out.println("Client: " + response);

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if(in != null){
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(out != null){
                try {
                    out.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            if(socket != null){
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            socket = null;
        }
    }
}

我相信上面的代码大家应该都能读懂,重要的部分我都做了注释。

先启动服务端,再启动客户端,如果你注意看当执行完客户端代码后所有的程序都结束了。

服务端打印:
这里写图片描述

客户端打印:

这里写图片描述

在idea中Process finished with exit code 0 代表程序运行结束

客户端启动后服务端的打印:

这里写图片描述

这时候服务端接收到客户端的响应,随之服务端程序也结束,这也就是我们的传统BIO一对一。

我们发现,BIO主要的问题在于每当有一个新的客户端请求接入时,服务端必须创建一个新的线程处理新接入的客户端链路,一个线程只能处理一个客户端连接。在高性能服务器应用领域,往往需要面向成千上万个客户端的并发连接,这种模型显然无法满足高性能、高并发接入的场景。

为了改进一线程一连接模型,后来又演进出了一种通过线程池或者消息队列实现1个或者多个线程处理N个客户端的模型,由于它的底层通信机制依然使用同步阻塞IO,所以被称为 “伪异步”,

伪异步IO

采用线程池和任务队列可以实现一种伪异步的IO通信框架。

其实就是将客户端的Socket封装成一个task任务(实现runnable接囗的类)然后投递到线程池中去,配置相应的队列进行实现。

伪异步IO模型图

采用线程池和任务队列可以实现一种叫做伪异步的IO通信框架,它的模型图如下:

这里写图片描述

其实跟传统BIO案例差不多,就是多了一个线程池,下面通过一个案例来讲解。

案例

server.java

public class Server {

    final static int PORT = 8765;

    public static void main(String[] args) {
        ServerSocket server = null;
        BufferedReader in = null;
        PrintWriter out = null;
        try {
            server = new ServerSocket(PORT);
            System.out.println("server start");
            Socket socket = null;
            //创建线程池
            HandlerExecutorPool executorPool = new HandlerExecutorPool(50, 1000);
            while(true){
                //阻塞
                socket = server.accept();
                //执行客户端任务
                executorPool.execute(new ServerHandler(socket));
            }

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if(in != null){
                try {
                    in.close();
                } catch (Exception e1) {
                    e1.printStackTrace();
                }
            }
            if(out != null){
                try {
                    out.close();
                } catch (Exception e2) {
                    e2.printStackTrace();
                }
            }
            if(server != null){
                try {
                    server.close();
                } catch (Exception e3) {
                    e3.printStackTrace();
                }
            }
            server = null;              
        }



    }


}

HandlerExecutorPool.java

public class HandlerExecutorPool {

    private ExecutorService executor;
    public HandlerExecutorPool(int maxPoolSize, int queueSize){
        this.executor = new ThreadPoolExecutor(
                Runtime.getRuntime().availableProcessors(),
                maxPoolSize, 
                120L, 
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<Runnable>(queueSize));
    }

    public void execute(Runnable task){
        this.executor.execute(task);
    }



}

在这有什么线程池不懂的可以看我之前的文章。

由于线程池和消息队列都是有界的,因此,无论客户端并发连接数多大,它都不会导致线程个数过于膨胀或者内存溢出,相比于传统的一连接一线程模型,是一种改良。

ServerHandler.java

public class ServerHandler implements Runnable {

    private Socket socket;
    public ServerHandler (Socket socket){
        this.socket = socket;
    }

    @Override
    public void run() {
        BufferedReader in = null;
        PrintWriter out = null;
        try {
            in = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
            out = new PrintWriter(this.socket.getOutputStream(), true);
            String body = null;
            while(true){
                body = in.readLine();
                if(body == null) break;
                System.out.println("Server:" + body);
                out.println("Server response");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if(in != null){
                try {
                    in.close();
                } catch (Exception e1) {
                    e1.printStackTrace();
                }
            }
            if(out != null){
                try {
                    out.close();
                } catch (Exception e2) {
                    e2.printStackTrace();
                }
            }
            if(socket != null){
                try {
                    socket.close();
                } catch (Exception e3) {
                    e3.printStackTrace();
                }
            }
            socket = null;          
        }


    }

}

Client.java

public class Client {

    final static String ADDRESS = "127.0.0.1";
    final static int PORT =8765;

    public static void main(String[] args) {
        Socket socket = null;
        BufferedReader in = null;
        PrintWriter out = null;
        try {
            socket = new Socket(ADDRESS, PORT);
            in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            out = new PrintWriter(socket.getOutputStream(), true);

            out.println("Client request");

            String response = in.readLine();
            System.out.println("Client:" + response);


        }  catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } finally {
            if(in != null){
                try {
                    in.close();
                } catch (Exception e1) {
                    e1.printStackTrace();
                }
            }
            if(out != null){
                try {
                    out.close();
                } catch (Exception e2) {
                    e2.printStackTrace();
                }
            }
            if(socket != null){
                try {
                    socket.close();
                } catch (Exception e3) {
                    e3.printStackTrace();
                }
            }
            socket = null;              
        }



    }

}

服务端执行结果如下:
这里写图片描述

客户端执行结果如下:
这里写图片描述

客户端启动后服务端的打印:
这里写图片描述

这时候并没有 Process finished with exit code 0 (在idea代表程序结束)这句话,证明服务端还在启动着,客户端还可以继续启动多个。每启动一个客户端服务端就会打印这句话“Server:Client request”也就是客户端启动回传服务端的内容。

伪异步IO通信框架采用了线程池实现,因此避免了为每个请求都创建一个独立线程造成的线程资源耗尽问题。但是由于它底层的通信依然采用同步阻塞模型,因此无法从根本上解决问题。

下面说一下伪异步IO的弊端:

伪异步IO实际上仅仅只是对之前IO线程模型的一个简单优化,它无法从根本上解决同步IO导致的通信线程阻塞问题。

如果通信对方返回应答时间过长引起的级联故障:

1、服务端处理缓慢,返回应答消息耗费60S,平时只需要10MS;

2、采用伪异步IO的线程正在读取故障服务节点的响应,由于读取输入流是阻塞的,因此,它将会被同步阻塞60S;

3、假如所有的可用线程都被故障服务器阻塞,那后续所有的IO消息都将在队列中排队;

4、由于线程池采用阻塞队列实现,当队列积满之后,后续入队列的操作将被阻塞;

5、由于前端只有一个Accptor线程接收客户端接入,它被阻塞在线程池的同步阻塞队列之后,新的客户端请求消息将被拒绝,客户端会发生大量的连接超时;

6、由于几乎所有的连接都超时,调用者会认为系统已经崩溃,无法接收新的请求消息。

看到了说弊端,我想大家应该都知道还有下一章,如何解决这种难题。下个章节的NIO将给出答案。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值