Netty入门笔记-BIO编程

导语
  对于网络编程来说最为典型的就是基于客户端、服务器的C/S模型。也就是说客户端有一个线程,服务器端有一个线程,两个线程之间进行相互的通信。其中服务器段提供的是数据的信息,例如IP端口以及数据等。而客户端进程就是请求获取服务器端的数据,通过TCP的三次握手协议建立连接,连接建立之后通过Socket进行通信。
  在Java的传统操作中ServerSocket作为服务端进行IP和端口的绑定和监听操作;客户端Socket就是发起一次TCP的请求。连接成功之后就采用输入输出流的方式进行同步阻塞式的通信。

下面就来通过简单的BIO通行模型来了解一下BIO的过程。对于BIO来说就是同步阻塞,对于同步来说就是没一个请求都会等到对应的响应完成之后才会继续执行,而对于阻塞来说就是会不会进行等待接收请求而占用资源。这里的资源是指线程资源或者是I/O流。
  首先,服务器端和客户端是一对一的对应关系,也就是说有一个服务器端的请求就有一个客户端的请求,在服务端通过一个独立的Acceptor线程负责监听客户端的请求连接,当它收到客户端的请求之后为每个客户端建立一个新的线程作为处理链路,链路建立之后通过输出流返回应答给客户端,线程销毁。也就是说有多少客户端就建立多少服务器端线程。
在这里插入图片描述
  从上面图中可以看到这个模型最大的问题就是弹性伸缩能力。当客户端的并发访问量增加之后,服务端的线程数也会增加,由于在JVM中每个线程都有自己独享的虚拟机栈、本地方法栈、程序计数器等等资源,所以说会导致虚拟机的性能急剧下降。随着并发访问量的增加,JVM就会出现分配内存不够的问题。会发生堆栈溢出、创建新线程失败等问题,并且最终导致宕机或者虚拟机假死,导致服务器不能向外提供服务。
下面通过一个小例子来看一下关于传统BIO编程的操作。
同步阻塞式I/O创建的TimeServer源码分析

public class TimeServer {

    public static void main(String[] args) throws IOException {
        int port = 8080;
        //传入的参数不为空且长度大于零
        if (args != null && args.length > 0) {
            port = Integer.valueOf(args[0]);
        }

        ServerSocket serverSocket = null;

        try {
            serverSocket = new ServerSocket(port);
            System.out.println("The time server is start in port " + port);
            Socket socket = null;
            while (true){
                socket = serverSocket.accept();
                new Thread(new TimeServerHandler(socket)).start();
            }

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

        }

    }
}

  TimeServer会根据传入的参数来控制监听的端口,如果没有传入对应的参数,默认使用8080端口进行提供服务,会通过一个无限的循环操作来保证服务端始终向外提供服务。如果没有新的客户端接入主线程就会被组塞在accept()操作上。那么首先我们来观察一下服务端启动之后整个虚拟机的变化。
在这里插入图片描述
  在这里会发现,主线程被阻塞到了accept方法上,当有新的客户端计入的时候就会有新的变化,这里看一下关于客户端的代码。

public class TimeClinet {
    public static void main(String[] args) {
        int port = 8080;
        if (args!=null&&args.length>0){
            port = Integer.valueOf(args[0]);
        }

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

        try{
            socket = new Socket("127.0.0.1",port);

            in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            out = new PrintWriter(socket.getOutputStream(),true);

            out.println("QUERY TIME OVER");

            System.out.println("Send order  2 server succeed.");

            String resp = in.readLine();
            System.out.println("Now is "+ resp);

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

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

  客户端通过Socket创建,发送当前查询到了服务器时间,然后读取服务器端的结果并且进行输出,随后关闭对应的连接释放连接资源,客户端进程进行退出。这里提供了一个Handler。

public class TimeServerHandler implements Runnable {

    private Socket socket;

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

    @Override
    public void run() {

        BufferedReader in = null;
        PrintWriter out = null;

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

            String currentTime = null;
            String body = null;
            while (true){
                body = in.readLine();
                if (body==null){
                    break;
                }
                System.out.println("The time server receive order : "+body);
                currentTime = "QUERY TIME OVER".equalsIgnoreCase(body)? new java.util.Date(System.currentTimeMillis()).toString():"BAD ORDER";
                out.println(currentTime);
            }
        }catch (Exception e){
            if (in!=null){
                try {
                    in.close();
                } catch (IOException e1) {
                    e1.printStackTrace();
                }
            }
            if (out!=null){
                out.close();
                out = null;
            }
            if (socket!=null){
                try {
                    socket.close();
                } catch (IOException e1) {
                    e1.printStackTrace();
                }
                socket = null;

            }
        }
    }
}

  通过上面的代码我们可以知道,同步阻塞式的I/O,最大的问题就是在于每个客户端请求服务器都会有一个新的线程区处理。同时一个线程只能接受到一个客户端的连接,这里我们来做一个测试可以让客户端进程多次请求服务器端。我们会发现在在服务器端会出现很多的线程并且导致宕机。那么为什么会出现这种情况,我们知道,如果出现每个客户端线程进行请求操作,对于服务器来说就会创建多个线程进行处理。如果没有一个合理的线程管理机制,任凭线程无限被创建,就会导致服务器的内存溢出,从而创建不了新的线程,无法向外提供服务。
在这里插入图片描述
  为了改进这个一对一的线程模型,后来又提出了一个模式,通过线程池的操作来实现一个或者多个线程处理N个客户端的模型,但是尽管实现了这个模式但是它的底层还是BIO的同步阻塞方式。所以被称为是"伪异步"。下一次的博客中就会分享关于伪异步的知识。

总结

  对于BIO来说,理解起来是很简单的,但是真正的使用到实际的场景中,是有一定的难度,在平时做开发的时候并没有注意到这些问题,那是因为在使用的时候并没有像这样详细的分析过,通过上面的一个小例子,可以看出来所有的BIO编程都是离不开这样的一个思路。怎么能很好的设计出高效的代码作为开发是比较重要的能力的培养。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

nihui123

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

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

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

打赏作者

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

抵扣说明:

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

余额充值