【网络编程】回顾BIO

最近两三年一直使用的网络编程框架是Netty,今天就来聊聊为什么使用Netty框架,为什么不使用Java原生的类库。说到这,就需要把BIO、NIO、AIO梳理一遍。

网络编程的基本模型是Client/Server模型,也就是两个进程之间进行相互通信,服务器端提供位置信息,包括绑定的IP地址和监听端口,客户端通过连接向服务器端监听地址发起连接请求,通过三次握手建立连接,如果连接成功,双方就可以通过网络套接字Socket进行通信。

BIO是Blocking IO的缩写,又称同步阻塞IO。BIO使用的是传统的java.io包,它是基于流模型实现的,服务器端和客户端交互的方式是同步、阻塞。

在BIO同步阻塞模型开发中,服务器端的ServerSocket负责绑定IP地址,启动监听端口;客户端Socket负责发起连接;连接成功之后,双方通过输入和输出流进行同步阻塞式通信。

采用BIO通信模型的服务器端,通常由一个Acceptor线程负责监听客户端的连接,服务器端接收到客户端的连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回应答给客户端。

BIO模型最大的问题,就是缺乏弹性伸缩能力,当客户端并发访问量增加以后,服务器端的线程池和客户端并发访问数呈1:1正比关系。线程是java虚拟机比较宝贵的资源,当线程数膨胀之后,虚拟机的性能就会急剧下降,随着并发访问量的继续增大,系统就会发生线程堆栈溢出,创建线程失败,最终导致系统宕机或假死,不能对外提供服务。

下面看服务器端代码:

package com.test.bio;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
/**
 * BIO的服务器端
 * @author 程就人生
 * @Date
 */
public class HelloServer {

  public static void main( String[] args ){
    int port = 8080;
        ServerSocket serverSocket = null;
        try {
      serverSocket = new ServerSocket(port);
      Socket socket = null;
      // 通过无限循环监听客户端连接
      while(true){
        // 如果没有客户端连接,则阻塞在Accept操作上
        socket = serverSocket.accept();
        // 如果有客户端连接,则创建一个线程,使用这个线程处理这个链路
        new Thread(new HelloHandler(socket)).start();
      }
    } catch (IOException e) {
      e.printStackTrace();
    }
    }
}
/**
 * hello处理类
 * @author 程就人生
 * @Date
 */
class HelloHandler implements Runnable{

  private Socket socket;

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

  public void run() {
    BufferedReader reader = null;
    PrintWriter writer = null;
    try {
      reader = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
      // 返回给客户端,是否刷新设置为true
      writer = new PrintWriter(this.socket.getOutputStream(), true);
      String body = null;
      while(true){
        body = reader.readLine();
        if(body == null){
          break;
        }
        System.out.println("服务器端接收到的:" + body);
        writer.println("来自服务器端的响应!");        
      }
    } catch (IOException e) {
      e.printStackTrace();
      // 出现异常时,对资源的释放
      if(reader != null){
        try {
          reader.close();
        } catch (IOException e1) {
          e1.printStackTrace();
        }
      }
      if(writer != null){
        writer.close();
        writer = null;
      }
      if(socket != null){
        try {
          socket.close();
        } catch (IOException e1) {
          e1.printStackTrace();
        }
      }
    }
  }  
}

HelloServer在20行通过构造函数创建了一个端口号为8080的ServerSocket。在第23行通过无限循环监听客户端的连接。在第25行,如果没有客户端连接则阻塞在accept操作上。

运行服务端代码。在cmd控制台,通过jps -l 获取进程ID。

通过 jstack pid 打印堆栈信息,main方法阻塞在accept操作上。

当有新的客户端连接时,执行27行代码,以socket为参数构造一个线程HelloHandler来处理新加入的socket链接。

在HelloHandler中,通过51行的BufferedReader读取一行,如果已经读到了输入流的末尾,则退出循环。如果读到了非空值则进行打印,并使用PrintWriter 响应客户端。

客户端代码:

package com.test.bio;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
/**
 * BIO的客户端
 * @author 程就人生
 * @Date
 */
public class HelloClient {

  public static void main( String[] args ){
    int port = 8080;
    Socket socket = null;
    BufferedReader reader = null;
    PrintWriter writer = null;
    try {
      socket = new Socket("127.0.0.1", port);
      reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
      writer = new PrintWriter(socket.getOutputStream(), true);
      writer.println("来自客户端的hello!");
      System.out.println(reader.readLine());
    } catch (UnknownHostException e) {
      e.printStackTrace();
    } catch (IOException e) {
      e.printStackTrace();
    }finally{
      // 释放资源
      if(reader != null){
        try {
          reader.close();
        } catch (IOException e1) {
          e1.printStackTrace();
        }
      }
      if(writer != null){
        writer.close();
        writer = null;
      }
      if(socket != null){
        try {
          socket.close();
        } catch (IOException e1) {
          e1.printStackTrace();
        }
      }
    }
  }
}

客户端代码通过Socket进行创建,通过BufferedReader读取服务器端的响应,通过PrintWriter对服务器进行响应。响应完毕后,释放资源。

运行客户端代码,查看客户端控制台输出。客户端响应完毕,释放资源,关闭连接。

查看服务器端控制台输出。服务器端控制台接收到客户端的请求,打印输出,并等待新的客户端链接。

以上便是BIO开发的简单示例,在网上也有不少类似的demo。有面试NIO的地方,就少不了BIO。通过这个简单的demo,也可对BIO有个深刻的了解。

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