在学习Java其它编程模型之前,我们先看一下传统的网络编程模型是怎么做的。
Java 阻塞模型
我们可以想象一个最简单的场景,比如说有一个客户端和一个服务器,客户端每隔两秒会向服务器发一个带时间戳的PING消息,服务器收到该消息后直接输出,并丢弃该消息。
我们可以实现两个类,一个类作为服务器,一个类作为客户端。
服务器端实现
IOServer
public class IOServer {
private static final int port = 8000;
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(port);
// (1) 创建1个单独的监听线程,来监听客户端的连接
new Thread(new AcceptTask(serverSocket)).start();
}
}
服务器端首先创建一个socket ,负责监听8000端口,然后创建一个Accept任务线程,由该线程负责监听处理所有的客户端连接。
AcceptTask
class AcceptTask implements Runnable{
private ServerSocket serverSocket;
public AcceptTask(ServerSocket serverSocket) {
this.serverSocket = serverSocket;
}
@Override
public void run() {
while(true){
try{
Socket socket = serverSocket.accept();
// (2) 为接收到的每一个客户端连接,创建一个单独的工作线程
new Thread(new WorkerTask(socket)).start();
}catch (IOException e){
e.printStackTrace();
}
}
}
}
该Accept任务线程,不断调用阻塞方法accept,来阻塞接收新的客户端连接,当新的客户端连接到来,accept返回,并给每个连接创建一个新的工作者线程(WorkerTask),由该工作者线程负责每一个新到来的客户端连接。
WorkerTask
class WorkerTask implements Runnable{
private Socket socket;
public WorkerTask(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try{
byte[] data = new byte[4096];
InputStream in = socket.getInputStream();
while(true){
int len;
// (3) 以字节流的方式读取数据
while((len = in.read(data)) != -1){
String info = new String(data, 0, len);
System.out.println(info);
}
}
}catch (IOException e){
e.printStackTrace();
}
}
}
工作者线程,从客户端连接中不断接收字节流消息(带时间戳的PING消息),并输出到终端控制台。
客户端的实现
客户端的实现很简洁,连接上服务器端端口: 8000之后,每隔两秒向服务器端写一个带有时间戳的PING消息。
IOClient
public class IOClient {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("127.0.0.1", 8000);
while(true){
try{
byte[] info = (new Date() + ": PING").getBytes(Charset.forName("utf8"));
OutputStream out = socket.getOutputStream();
out.write(info);
out.flush();
TimeUnit.SECONDS.sleep(2);
}catch (Exception e){
e.printStackTrace();
}
}
}
}
服务端输出
Sat Oct 26 10:39:15 CST 2016: PING
Sat Oct 26 10:39:17 CST 2016: PING
Sat Oct 26 10:39:19 CST 2016: PING
Sat Oct 26 10:39:21 CST 2016: PING
Sat Oct 26 10:39:23 CST 2016: PING
Sat Oct 26 10:39:25 CST 2016: PING
Sat Oct 26 10:39:27 CST 2016: PING
Sat Oct 26 10:39:29 CST 2016: PING
Sat Oct 26 10:39:31 CST 2016: PING
Sat Oct 26 10:39:33 CST 2016: PING
Sat Oct 26 10:39:35 CST 2016: PING
Sat Oct 26 10:39:37 CST 2016: PING
Sat Oct 26 10:39:39 CST 2016: PING
Sat Oct 26 10:39:41 CST 2016: PING
阻塞IO模型
阻塞IO模型简单,和传统的顺序思维方式一致,如果客户端数连接不多的情况,运行良好,是一种可行的方式。但是对于客户端越来越多的场景(比如超过10K个连接),这种IO模型可能就不太合适了,我们来看一下原因。
操作系统资源限制: 线程是操作系统中很重要的资源,虽然相对进程来说线程会占用较少资源,但是如果有大量的线程,对于整个OS来说,仍然消耗太大。
线程切换效率不高:大量的线程,大部分时间处于休眠状态,需要等到OS内核的网卡驱动收到数据后,进行中断通知,将该线程唤醒,从阻塞状态切换到就绪状态;该线程开始运行接收完数据后,再次被阻塞,并挂到OS内核的阻塞队列中。这种大量线程间的频繁状态切换对于OS来说是一个沉重的负担,效率不高。
随着用Java实现高效网络编程的呼声越来越高,于是乎,为了解决这个问题,JDK在1.4之后提出了NIO模型。(见下一篇)