3、Java网络编程之深入理解BIO原理和实现

章节概览

Netty源码分析章节概览


1、概述

关于网络方面的知识,这里不再赘述。可以看七层网络模型,TCP/IP协议,三次握手,四次挥手等网络编程方面的知识。本章节主要结合Java BIO 讲解BIO编程的原理和过程。

1.1、七层网络协议

在这里插入图片描述

1.2、 五层网络协议

在这里插入图片描述


2、socket发送和接受数据过程

在这里插入图片描述

  • 发送过程:
  1. 应用程序调用系统调用,将数据发送给socket
  2. socket检查数据类型,调用相应的send函数
  3. send函数检查socket状态、协议类型,传给传输层
  4. tcp/udp(传输层协议)为这些数据创建数据结构,加入协议头部,比如端口号、检验和。传给下层(网络层)ip(网络层协议)添加ip头,比如ip地址、检验和如果数据包大小超过了mtu(最大数据包大小),则分片;ip将这些数据包传给链路层链路层写到网卡队列
  5. 网卡调用响应中断驱动程序,发送到网络
  • 接收过程:
  1. 数据包从网络到达网卡,网卡接收帧,放入网卡buffer,在向系统发送中断请求
  2. cpu调用相应中断函数,这些中断处理程序在网卡驱动中
  3. 中断处理函数从网卡读入内存,交给链路层
  4. 链路层将包放入自己的队列,置软中断标志位
  5. 进程调度器看到了标志位,调度相应进程
  6. 该进程将包从队列取出,与相应协议匹配,一般为ip协议,再将包传递给该协议接收函数
    ip层对包进行错误检测,无错,路由。路由结果,packet被转发或者继续向上层传递
    如果发往本机,进入链路层
  7. 链路层再进行错误侦测,查找相应端口关联socket,包被放入相应socket接收队列
  8. socket唤醒拥有该socket的进程,进程从系统调用read中返回,将数据拷贝到自己的buffer,返回用户态。

Socket是在传输层tcp/udp 之上抽象出来的,为了满足于传出层和应用层之间的通信。每个请求对应一个socket连接,socket连接都会放入到一个接受队列里面,等待服务端进行消费。默认的socket连接的队列的长度:backlog是50。


3、Java BIO 原理和实现

在这里插入图片描述
早期的jdk中,采用BIO通信模式。通常有一个acceptor(消费者) 去负责监听客户端的连接,它接收到客户端的连接请求之后为每个客户端创建一个线程进行链路处理,处理完成之后,线程销毁。从其通信结构图中,我们可以清晰的看到,一个客户端连接,对应赢处理线程。他们之间的对应关系是 1:1。

由于客户端连接和服务端的处理之间的对应关系是1:1,如果遇到任务比较大,处理比较慢。或者并发量比较大的情况下,系统会创建大量的线程。从而导致服务器线程暴增,性能急剧下降,甚至宕机。

3.1 客户端代码
public class TimeClient {
    public static void main(String[] args) {
        int port = 8080;
        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 Order");
            System.out.println("Send order 2 server succeed.");
            String resp = in.readLine();
            System.out.println("Now is : " + resp);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }

            try {
                in.close();
            } catch (IOException e) {
                e.printStackTrace();
            }

            out.close();
            out = null;
        }
    }
}
3.2、服务端代码
public class TimeServer {
    public static void main(String[] args) {
        int port = 8080;
        ServerSocket serverSocket = null;

        try {
            serverSocket = new ServerSocket(port);
            Socket socket = null;
            while (true){
                // 从socket的队列中获取socket的连接
                // 相当于一个消费者
                socket = serverSocket.accept();
                // 获得到socket连接之后,分配线程任务进行处理
                new Thread(new TimeServerHandler(socket)).start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if(serverSocket != null){
                System.out.println("The time server close");
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                serverSocket = null;
            }
        }
    }
}
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(this.socket.getInputStream()));
            out = new PrintWriter(this.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 Order".equalsIgnoreCase(body) ?
                        new Date(System.currentTimeMillis()).toString() : "Bad Order";
                // 输出数据
                out.println(currentTime);

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

            if(out != null){
                out.close();
                out = null;
            }
        }
    }
}

我们实现了了一个简单的BIO程序,获取服务端的时间案例。从这个简单demo里面,我们分析下,bio
主要阻塞在哪里?!

  1. 当前的线程阻塞在accept 方法上面。该方法一直阻塞,除非获取到socket连接返回。
  2. 读取io流产生阻塞,从上一章节中:大白话分析BIO,NIO,AIO中,我们分析了阻塞IO流。那么在这个案例里面,主要的阻塞IO流阻塞的点有以下的几个方面:
    2.1、 内核程序从网卡中把数据读取到内核空间中,是一直阻塞的。
    2.2、用户程序把内核空间的数据复制到用户空间的过程是阻塞的。
    这两个过程中,对应的程序部分就是read方法的阻塞。我们看下jdk源码中关于read方法的描述信息:
/**
     * Reads some number of bytes from the input stream and stores them into
     * the buffer array <code>b</code>. The number of bytes actually read is
     * returned as an integer.  This method blocks until input data is
     * available, end of file is detected, or an exception is thrown.
     *
     * <p> If the length of <code>b</code> is zero, then no bytes are read and
     * <code>0</code> is returned; otherwise, there is an attempt to read at
     * least one byte. If no byte is available because the stream is at the
     * end of the file, the value <code>-1</code> is returned; otherwise, at
     * least one byte is read and stored into <code>b</code>.
     *
     * <p> The first byte read is stored into element <code>b[0]</code>, the
     * next one into <code>b[1]</code>, and so on. The number of bytes read is,
     * at most, equal to the length of <code>b</code>. Let <i>k</i> be the
     * number of bytes actually read; these bytes will be stored in elements
     * <code>b[0]</code> through <code>b[</code><i>k</i><code>-1]</code>,
     * leaving elements <code>b[</code><i>k</i><code>]</code> through
     * <code>b[b.length-1]</code> unaffected.
     *
     * <p> The <code>read(b)</code> method for class <code>InputStream</code>
     * has the same effect as: <pre><code> read(b, 0, b.length) </code></pre>
     *
     * @param      b   the buffer into which the data is read.
     * @return     the total number of bytes read into the buffer, or
     *             <code>-1</code> if there is no more data because the end of
     *             the stream has been reached.
     * @exception  IOException  If the first byte cannot be read for any reason
     * other than the end of the file, if the input stream has been closed, or
     * if some other I/O error occurs.
     * @exception  NullPointerException  if <code>b</code> is <code>null</code>.
     * @see        java.io.InputStream#read(byte[], int, int)
     */
    public int read(byte b[]) throws IOException {
        return read(b, 0, b.length);
    }

当对Socket的输入流进行读取操作的时候,它会一直阻塞下去,直到发生以下三种事情:

  1. 有数据可读,直接返回,读取至少一个字节。
  2. 可用的数据已经读取完成,这里的完成可用是返回-1,socket连接关闭。
  3. 发生空指针或者 I/O异常。

假设存在这么一个场景,由于网络延迟,导致数据发送缓慢。而由于使用的是阻塞IO,那么read方法一直处于阻塞状态,要等到数据传送完成才结束(返回-1)。那么这种场景下,在高并发。直接导致线程暴增、服务器宕机。


4、 小结

从上面的原理分析,最后的案例。理解了BIO阻塞的本质,如果有不懂的,欢迎博客留言。本人才疏学浅,如有错误的地方,欢迎指正。立刻修改。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章