网络编程的基本模型是Client/Server模型,也就是两个进程之间进行相互通信,其中服务端提供位置信息(绑定的IP地址和监听端口),客户端通过连接操作向服务器监听的地址发起连接请求,通过三次握手建立连接,如果连接建立成功,双方就可以通过网络套接字(Socket)进行通信。
在基于传统同步阻塞模型开发中,ServerSocket负责绑定IP地址,启动监听端口; Socket负责发起连接操作。连接成功之后,双方通过输入和输出流进行同步阻塞式通信。
BIO通信模型图

通过上图所示的通信模型图来熟悉下BIO的服务端通信模型:采用BIO通信模型的服务端,通常由一个独立的Acceptor线程负责监听客户端的连接,它接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回应答给客户端,线程销毁。这就是典型的——请求——应答通信模型。
该模型最大的问题就是缺乏弹性伸缩能力,当客户端并发访问量增加后,服务端的线程个数和客户端并发访问数呈1:1的正比关系,由于线程是Java虚拟机非常宝贵的系统资源,当线程数膨胀之后,系统的性能将急剧下降,随着并发访问量的继续增大,系统会发生线程堆栈溢出,创建新线程失败等问题,并最终导致进程宕机或者僵死,不能对外提供服务。
同步阻塞式I/O创建的TimeServer源码分析
1. 同步阻塞I/O的TimeServer
1package com.phei.netty.bio; 2 3import java.io.IOException; 4import java.net.ServerSocket; 5import java.net.Socket; 6 7public class TimeServer { 8 public static void main(String[] args) throws IOException{ 9 int port = 8080; 10 if(args != null && args.length > 0){ 11 try{ 12 port = Integer.valueOf(args[0]); 13 }catch (NumberFormatException e){ 14 //采用默认值 15 } 16 } 17 ServerSocket server = null; 18 try { 19 server = new ServerSocket(port); 20 System.out.println("The time server is start in port : " + port); 21 Socket socket = null; 22 while(true) { 23 socket = server.accept(); //接收客户端连接请求,没有的时候就阻塞 24 new Thread(new TimeServerHandler(socket)).start(); 25 } 26 } finally{ 27 if(server != null) { 28 System.out.println("The time server close"); 29 server.close(); 30 server = null; 31 } 32 } 33 } 34}
TimeServer根据传入的参数设置监听端口,如果没有参数,使用默认值8080,接下来通过构造函数创建ServerSocket,如果端口合法且没有被占用,服务端监听成功。然后通过一个无限循环来监听客户端的连接,如果没有客户端接入,则主线程阻塞在ServerSocket的accept操作上。
当有新的客户端接入的时候,执行代码 new Thread(new TimeServerHandler(socket)).start(),以Socket为参数构造TimeServerHandler对象,TimeServerHandler是一个Runnable,使用它为构造函数的参数创建一个新的客户端线程处理这条Socket链路。下面我们继续分析TimeServerHandler的代码:
1package com.phei.netty.bio; 2 3import java.io.BufferedReader; 4import java.io.IOException; 5import java.io.InputStreamReader; 6import java.io.PrintWriter; 7import java.net.Socket; 8 9public class TimeServerHandler implements Runnable { 10 private Socket socket; 11 public TimeServerHandler(Socket socket){ 12 this.socket = socket; 13 } 14 @Override 15 public void run() { 16 BufferedReader in = null; 17 PrintWriter out = null; 18 try { 19 in = new BufferedReader(new InputStreamReader(this.socket.getInputStream())); 20 out = new PrintWriter(this.socket.getOutputStream(),true); 21 String currentTime = null; 22 String body = null; 23 while(true) { 24 body = in.readLine(); //从客户端读 25 if(body == null) break; 26 System.out.println("The time server receive order: " + body); 27 currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ? 28 new java.util.Date(System.currentTimeMillis()).toString() : "BAD ORDER"; 29 out.print(currentTime); //向客户端写 30 } 31 } catch (Exception e) { 32 if(in != null){ 33 try { 34 in.close(); 35 } catch (IOException e1) { 36 e1.printStackTrace(); 37 } 38 } 39 if(out != null) { 40 out.close(); 41 out = null; 42 } 43 if(this.socket != null) { 44 try { 45 this.socket.close(); 46 } catch (IOException e1) { 47 e1.printStackTrace(); 48 } 49 this.socket = null; 50 } 51 } 52 } 53}
24行通过BufferedReader读取一行,如果已经读到了输入流的尾部,则返回值为null,退出循环。如果读到了非空值,则对内容进行判断,如果请求消息为查询时间的指令“QUERY TIME ORDER”则获取当前最新的系统时间,通过PrintWriter的println函数发送给客户端,最后退出循环。代码32~49行释放输入流,输出流,和Socket套接字句柄资源,最后线程自动销毁并被虚拟机回收。
同步阻塞式I/O创建的TimeClient源码分析
客户端通过Socket创建,发送查询时间服务器的“QUERY TIME ORDER”指令,然后读取服务端的响应并将结果打印出来,随后关闭连接,释放资源,程序退出执行。
同步阻塞I/O的TimeClient:
1package com.phei.netty.bio; 2 3import java.io.BufferedReader; 4import java.io.IOException; 5import java.io.InputStreamReader; 6import java.io.PrintWriter; 7import java.net.Socket; 8 9public class TimeClient { 10 public static void main(String[] args) { 11 int port = 8080; 12 if(args != null && args.length > 0){ 13 try { 14 port = Integer.valueOf(args[0]); 15 } catch (NumberFormatException e) { 16 //采用默认值 17 } 18 } 19 Socket socket = null; 20 BufferedReader in = null; 21 PrintWriter out = null; 22 try { 23 socket = new Socket("127.0.0.1",port); //创建socket 24 in = new BufferedReader(new InputStreamReader(socket.getInputStream())); 25 out = new PrintWriter(socket.getOutputStream(),true); 26 out.println("QUERY TIME ORDER"); //向服务端写出QUERY TIME ORDER 27 System.out.println("Send order 2 server succeed."); 28 String resp = in.readLine(); //从服务端读入 29 System.out.println("Now is : " + resp); 30 } catch (Exception e) { 31 //不需要处理 32 } finally { 33 if(out != null ){ 34 out.close(); 35 out = null; 36 } 37 if(in != null){ 38 try { 39 in.close(); 40 } catch (IOException e2) { 41 e2.printStackTrace(); 42 } 43 in = null; 44 } 45 if(socket != null){ 46 try { 47 socket.close(); 48 } catch (IOException e3) { 49 e3.printStackTrace(); 50 } 51 socket = null; 52 } 53 } 54 } 55}
第26行客户端通过PrintWriter向服务端发送“QUERY TIME ORDER”指令,然后通过BufferedReader的readLine读取响应并打印。
BIO主要的问题在于每当有一个新的客户端请求接入时,服务端必须创建一个新的线程处理新接入的客户端链路,一个线程只能处理一个客户端连接。在高性能服务器应用领域,往往需要面向成千上万个客户端的并发连接,这种模型显然无法满足高性能,高并发接入的场景。
为了改进一线程一连接模型,后来又演进出了一种通过线程池或者消息队列实现1个或者多个线程处理N个客户端的模型,由于它的底层通信机制依然使用同步阻塞I/O,所以被称为“伪异步”,下一节会讲到。