Java NIO是相对于传统的IO操作而言的,因为提出了缓冲池等概念,使它的处理数据的效率大大提高;
多线程是并发处理的明智选择。
为减少系统开销,线程池是并发应用中是经常使用的技术。
而异步处理机制可以大大缩短每个请求的响应时间。
Mina2中就大量使用了这三项技术,使得它成为优秀的网络应用框架。(这一章并非描述Mina的实际应用,而是对它的内部处理机制做分析;我们对Mina的解析也只对服务端而言:因为无论是Mina也好,NIO也好,多线程也好,异步处理机制也好,都是解决高并发问题的;高并发却是对服务端而言的!因此,服务端才是重点。)
一.NIO分析
Mina是一个Java NIO框架,NIO的基本思想是:服务器程序只需要一个线程就能同时负责接收客户的连接、客户发送的数据,以及向各个客户发送响应数据。服务器程序的处理流程如下:
//阻塞
while(一直等待,直到有接收连接就绪事件、读就绪事件或写就绪事件发生){
if(有客户连接)
接收客户的连接; //非阻塞
if(某个Socket的输入流中有可读数据)
从输入流中读数据; //非阻塞
if(某个Socket的输出流可以写数据)
向输出流写数据; //非阻塞
}
而传统的并发型服务器则是采用多线程的模式响应用户请求的;
//阻塞
while(一直等待){
if(有客户连接)
启动新线程,与客户的通信; //可能会阻塞
}
但是,无论如何,服务端共同的结构如下:
a.Read Request; 接受请求
b.Decode Request; 请求值解码(读)
c.Process Service;请求处理
d.Encode Reply; 响应值编码(写)
e.Send Reply; 发送响应
是不是感觉太抽象了?OK,我们从传统的IO模式的并发服务器说起。
1.传统阻塞服务器
传统的服务端一次只能处理一个请求,其他请求需要排队等待;示例如下:
package com.bijian.study.mina.server;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Date;
import org.apache.log4j.Logger;
/*
* 服务端只能一次处理一个客户端的请求
* 多个请求到达后需要排队
*/
public class EchoServer01 {
private Logger logger = Logger.getLogger(EchoServer01.class);
private int PORT = 3015;
private ServerSocket serverSocket;
public EchoServer01() throws IOException {
// 请求队列最大长度为5
serverSocket = new ServerSocket(PORT,5);
logger.info("服务端启动... 端口号:" + PORT);
}
public void service() {
while (true) {
Socket socket = null;
try {
socket = serverSocket.accept();
logger.info("一个新的连接到达,地址为:" + socket.getInetAddress() + ":"
+ socket.getPort());
// 获得客户端发送信息的输入流
InputStream socketIn = socket.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(
socketIn));
// 给客户端响应信息的输出流
OutputStream socketOut = socket.getOutputStream();
PrintWriter pw = new PrintWriter(socketOut, true);
String msg = null;
while ((msg = br.readLine()) != null) {
logger.info("服务端接受到的信息为:" + msg);
pw.println("响应信息:" + new Date().toString());// 给客户端一个日期字符串
if (msg.equals("bye")) {
logger.info("客户端请求断开");
break;
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (socket != null)
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static void main(String args[]) throws IOException {
new EchoServer01().service();
}
}
代码就不解释啦,直接看注释吧,没有任何玄妙的地方。我们先用telnet做测试。
a.启动服务端
2016-02-18 23:02:22,832 INFO EchoServer01 - 服务端启动... 端口号:3015
b.启动,cmd,telnet 127.0.0.1 3015,回车
c.测试,客户端输入
服务端响应:
2016-02-18 23:04:09,984 INFO EchoServer01 - 服务端启动... 端口号:3015
2016-02-18 23:04:24,049 INFO EchoServer01 - 一个新的连接到达,地址为:/127.0.0.1:51937
2016-02-18 23:04:26,907 INFO EchoServer01 - 服务端接受到的信息为:bijian
2016-02-18 23:04:30,463 INFO EchoServer01 - 服务端接受到的信息为:test
2016-02-18 23:04:38,055 INFO EchoServer01 - 服务端接受到的信息为:123
2016-02-18 23:04:39,569 INFO EchoServer01 - 服务端接受到的信息为:sfas
2016-02-18 23:04:43,409 INFO EchoServer01 - 服务端接受到的信息为:bye
2016-02-18 23:04:43,409 INFO EchoServer01 - 客户端请求断开
2016-02-18 23:06:19,021 INFO EchoServer01 - 一个新的连接到达,地址为:/127.0.0.1:51941
d.使用客户端代码做测试;EchoClient01.java代码如下:
package com.bijian.study.mina.client;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import org.apache.log4j.Logger;
/*
* 使用Socket创建客户端请求
*/
public class EchoClient01 {
private Logger logger = Logger.getLogger(EchoClient01.class);
private String HOST = "localhost";
private int PORT = 3015;
private Socket socket;
public EchoClient01() throws IOException {
socket = new Socket(HOST, PORT);
}
public void talk() throws IOException {
try {
// 获得服务端响应信息的输入流
InputStream socketIn = socket.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(
socketIn));
// 给服务端发送信息的输出流
OutputStream socketOut = socket.getOutputStream();
PrintWriter pw = new PrintWriter(socketOut, true);
BufferedReader localReader = new BufferedReader(
new InputStreamReader(System.in));
String msg = null;
while ((msg = localReader.readLine()) != null) {
pw.println(msg);
logger.info(br.readLine());
if (msg.equals("bye"))
break;
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String args[]) throws IOException {
new EchoClient01().talk();
}
}
先启动服务端,再启动客户端,输入,客户端输出如下:
aaa
2016-02-18 23:30:46,203 INFO EchoClient01 - response info:Thu Feb 18 23:30:46 CST 2016
bbb
2016-02-18 23:30:47,689 INFO EchoClient01 - response info:Thu Feb 18 23:30:47 CST 2016
ccc
2016-02-18 23:30:48,783 INFO EchoClient01 - response info:Thu Feb 18 23:30:48 CST 2016
bye
2016-02-18 23:30:50,361 INFO EchoClient01 - response info:Thu Feb 18 23:30:50 CST 2016
"bijian" Sid: S-1-5-21-3389202862-2257394754-1792823341-1001
服务端输出如下:
2016-02-18 23:30:33,401 INFO EchoServer01 - 服务端启动... 端口号:3015
2016-02-18 23:30:39,045 INFO EchoServer01 - 一个新的连接到达,地址为:/127.0.0.1:52081
2016-02-18 23:30:46,188 INFO EchoServer01 - 服务端接受到的信息为:aaa
2016-02-18 23:30:47,689 INFO EchoServer01 - 服务端接受到的信息为:bbb
2016-02-18 23:30:48,783 INFO EchoServer01 - 服务端接受到的信息为:ccc
2016-02-18 23:30:50,345 INFO EchoServer01 - 服务端接受到的信息为:bye
2016-02-18 23:30:50,361 INFO EchoServer01 - 客户端请求断开
测试吧无疑是通过的,但是有两个问题出现了:
a. 当前服务端一下只能处理一个请求;我靠,这还是服务器吗?做web开发的人肯定有这样的想法。
b. 服务端的请求队列最多只有是5个,也就是一下能连6个请求(1个处理,5个等待),第7个请求到达后会被拒绝。
注意看,第7个客户端请求是无法成功的,异常信息如下:
Exception in thread "main" java.net.ConnectException: Connection refused: connect
at java.net.DualStackPlainSocketImpl.connect0(Native Method)
at java.net.DualStackPlainSocketImpl.socketConnect(DualStackPlainSocketImpl.java:79)
at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350)
at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206)
at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188)
at java.net.PlainSocketImpl.connect(PlainSocketImpl.java:172)
at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392)
at java.net.Socket.connect(Socket.java:589)
at java.net.Socket.connect(Socket.java:538)
at java.net.Socket.<init>(Socket.java:434)
at java.net.Socket.<init>(Socket.java:211)
at com.bijian.study.mina.client.EchoClient01.<init>(EchoClient01.java:27)
at com.bijian.study.mina.client.EchoClient01.main(EchoClient01.java:60)
"bijian" Sid: S-1-5-21-3389202862-2257394754-1792823341-1001
异常提示也很明确:拒绝连接!因为我们在服务端建立时做了请求队列最大长度的限制。
public EchoServer01() throws IOException {
// 请求队列最大长度为5
serverSocket = new ServerSocket(PORT,5);
logger.info("服务端启动... 端口号:" + PORT);
}
c.服务端容易阻塞;最显著的阻塞是IO操作,比如客户端与服务端建立连接后,向服务端发送一条消息,客户端因为人为操作很久没有输入结束;此时其他的连接只好等待在队列中。
这样的服务端我们称之为传统阻塞服务端,大凡Socket的入门示例都是这样的;但是,在实际的生产应用环境中用的非常少(注意:并不是不用哦,在特殊的环境是还是可以使用的,比如手机终端,你接听电话肯定只能一下接受一个请求,其他如短信接收,是要排队等待的)。
2.多线程阻塞服务器
在实际的应用开发中,我们更多采用的是多线程阻塞服务器,即每一个客户端请求到达,就建立一个线程单独的处理它与服务端的通信,如下图所示:
好处就是可以并发处理每一个到达的请求!服务端代码如下:
package com.bijian.study.mina.server;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import org.apache.log4j.Logger;
import com.bijian.study.mina.handler.Server02Handler;
/*
* 为每个客户端分配一个线程
* 服务器的主线程负责接收客户的连接
* 每次接收到一个客户连接,就会创建一个工作线程,由它负责与客户的通信
*/
public class EchoServer02 {
private Logger logger = Logger.getLogger(EchoServer02.class);
private int PORT = 3015;
private ServerSocket serverSocket;
public EchoServer02() throws IOException {
serverSocket = new ServerSocket(PORT);
logger.info("服务器端启动.... 端口号:" + PORT);
}
public void service() {
while (true) {
Socket socket = null;
try {
socket = serverSocket.accept(); // 请求到达
Thread workThread = new Thread(new Server02Handler(socket)); // 创建线程
workThread.start(); // 启动线程
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String args[]) throws IOException {
new EchoServer02().service();
}
}
很明显,每到达一个请求,就交给一个线程单独的处理;处理方法如下:
package com.bijian.study.mina.handler;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Date;
import org.apache.log4j.Logger;
public class Server02Handler implements Runnable {
private Logger logger = Logger.getLogger(Server02Handler.class);
private Socket socket;
public Server02Handler(Socket socket) {
this.socket = socket;
}
public void run() {
try {
logger.info("一个新的请求达到并创建 " + socket.getInetAddress() + ":"
+ socket.getPort());
InputStream socketIn = socket.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(
socketIn));
OutputStream socketOut = socket.getOutputStream();
PrintWriter pw = new PrintWriter(socketOut, true);
String msg = null;
while ((msg = br.readLine()) != null) {
logger.info("服务端受到的信息为:" + msg);
pw.println(new Date()); // 给客户端响应日期字符串
if (msg.equals("bye"))
break;
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (socket != null)
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
启动服务端,使用EchoClient01客户端测试成功!
虽然它没有了传统阻塞服务端的单处理弊端,但是却有一个致命的危险存在:大量请求到达时,不断的创建线程,很容易耗尽系统资源造成服务器崩溃;而且每个线程的创建与销毁都很浪费资源。
解决的办法就是使用线程池!(是不是很熟悉?我们经常接触的数据库连接池就是这样实现的。)
服务端代码如下:
package com.bijian.study.mina.server;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import org.apache.log4j.Logger;
import com.bijian.study.mina.handler.Server02Handler;
import com.bijian.study.mina.pool.ThreadPool;
/*
* 自定义线程池
* 多线程处理客户端请求
*/
public class EchoServer03 {
private Logger logger = Logger.getLogger(EchoServer03.class);
private int PORT = 3015;
private ServerSocket serverSocket;
private ThreadPool threadPool; // 线程池
private final int POOL_SIZE = 4; // 单个CPU时线程池中的工作线程个数
public EchoServer03() throws IOException {
serverSocket = new ServerSocket(PORT);
// 创建线程池
// Runtime的availableProcessors()方法返回当前系统的CPU格式
// 系统的CPU越多,线程池中工作线程的数目也越多
threadPool = new ThreadPool(Runtime.getRuntime().availableProcessors()
* POOL_SIZE);
logger.info("服务端启动.... 端口号:" + PORT);
}
public void service() {
while (true) {
Socket socket = null;
try {
socket = serverSocket.accept();
// 把与客户通信的任务交给线程池
threadPool.execute(new Server02Handler(socket));
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String args[]) throws IOException {
new EchoServer03().service();
}
}
服务端没有什么可解释的地方,关键是线程池的实现代码:
package com.bijian.study.mina.pool;
import java.util.LinkedList;
import org.apache.log4j.Logger;
/*
* 自定义线程池
*/
public class ThreadPool extends ThreadGroup {
private Logger logger = Logger.getLogger(ThreadPool.class);
private boolean isClosed = false; // 线程池是否关闭
// 将任务放在LinkedList中,LinkedList不支持同步,
// 所以在添加任务和获取任务的方法声明中必须使用synchronized关键字
private LinkedList<Runnable> workQueue;// 表示工作队列
private static int threadPoolID; // 表示线程池ID
private int threadID; // 表示工作线程ID
// 构建一个线程组
public ThreadPool(int poolSize) { // poolSize是指线程池中工作线程的数目
super("ThreadPool-" + (threadPoolID++)); // 线程组名
setDaemon(true);
workQueue = new LinkedList<Runnable>();// 创建工作队列
for (int i = 0; i < poolSize; i++)
new WorkThread().start(); // 创建并启动工作线程(如果工作队列为空,则所有工作线程处于阻塞状态)
}
// 向工作队列中添加一个任务,由工作线程去执行该任务
public synchronized void execute(Runnable task) {
if (isClosed) { // 线程池关闭则抛出IllegalStateException异常
throw new IllegalStateException();
}
if (task != null) {
workQueue.add(task);
notify(); // 唤醒正在getTask()方法中等待任务的工作线程
}
}
// 从工作队列中取出一个任务 ----工作线程会调用此方法
protected synchronized Runnable getTask() throws InterruptedException {
while (workQueue.size() == 0) {
if (isClosed)
return null;
wait(); // 如果工作队列没有任务,就等待任务
}
return workQueue.removeFirst();
}
// 关闭线程池
public synchronized void close() {
if (!isClosed) {
isClosed = true;
workQueue.clear(); // 清空工作队列
interrupt();// 中断所有工作线程,该方法继承自ThreadGroup类
}
}
// 等待工作线程把所有任务执行完
public void join() {
synchronized (this) {
isClosed = true;
notifyAll(); // 唤醒还在getTask()方法中等待任务的工作线程
}
// activeCount()方法是ThreadGroup类的,获得线程组中当前所有活着的工作线程数目
Thread[] threads = new Thread[activeCount()];
// enumerate方法继承自ThreadGroup类,获得线程组中当前所有活着的工作线程
int count = enumerate(threads);
for (int i = 0; i < count; i++) {// 等待所有工作线程运行结束
try {
threads[i].join(); // 等待工作线程运行结束
} catch (InterruptedException ex) {
logger.error("工作线程出错...", ex);
}
}
}
// 内部类,工作线程
private class WorkThread extends Thread {
public WorkThread() {
// 加入当前的ThreadPool线程组中
// Thread(ThreadGroup group, String name)
super(ThreadPool.this, "WorkThread-" + (threadID++));
}
public void run() {
// isInterrupted()方法继承自ThreadGroup类,判断线程是否中断
while (!isInterrupted()) {
Runnable task = null;
try {
task = getTask(); // 得到任务
} catch (InterruptedException ex) {
logger.error("获得任务异常...", ex);
}
// 如果getTask()返回null或者线程执行getTask()时被中断,则结束此线程
if (task == null)
return;
try {
// 运行任务,捕获异常
task.run(); // 直接调用task的run方法
} catch (Throwable t) {
logger.error("任务执行异常...", t);
}
}// #while end
}// #run end
}// # WorkThread class end
}
启动服务端,使用EchoClient01客户端测试成功!
很多的服务端程序的实现思想就是基于该理念!
3.使用JDK自带线程池的阻塞服务器
上面那个多线程阻塞服务器使用的是自定义的线程池,但是它的代码可能不是很健壮,在更多的实际开发应用中,我们都是使用JDK自带的线程池的。java.util.concurrent包提供了现成的线程池的实现。
a.Executor接口表示线程池,它的execute(Runnable task)方法用来执行Runnable类型的任务。Executor的子接口
b.ExecutorService中声明了管理线程池的一些方法,比如用于关闭线程池的shutdown()方法等。
c.Executors类中包含一些静态方法,它们负责生成各种类型的线程池ExecutorService实例。
我们现在使用它来实现一个多线程阻塞服务器,服务端代码如下:
package com.bijian.study.mina.server;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.apache.log4j.Logger;
import com.bijian.study.mina.handler.Server02Handler;
/*
* 使用JDK自带的线程池ExecutorService
* 多线程处理客户端请求
*/
public class EchoServer04 {
private Logger logger = Logger.getLogger(EchoServer04.class);
private int PORT = 3015;
private ServerSocket serverSocket;
private ExecutorService executorService; // 线程池
private final int POOL_SIZE = 4; // 单个CPU时线程池中的工作线程个数
public EchoServer04() throws IOException {
serverSocket = new ServerSocket(PORT);
// 创建线程池
// Runtime的availableProcessors()方法返回当前系统的CPU格式
// 系统的CPU越多,线程池中工作线程的数目也越多
executorService = Executors.newFixedThreadPool(Runtime.getRuntime()
.availableProcessors()
* POOL_SIZE);
logger.info("服务端启动.... 端口号:" + PORT);
}
public void service() {
while (true) {
Socket socket = null;
try {
socket = serverSocket.accept();
executorService.execute(new Server02Handler(socket));
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String args[]) throws IOException {
new EchoServer04().service();
}
}
怎么样,代码很简单吧!启动服务端,使用EchoClient01.java客户端测试成功!
使用线程池时需要遵循以下原则:
(1)如果任务A在执行过程中需要同步等待任务B的执行结果,那么任务A不适合加入到线程池的工作队列中。
(2)如果执行某个任务时可能会阻塞,并且是长时间的阻塞,则应该设定超时时间,避免工作线程永久的阻塞下去而导致线程泄漏。
(3)根据任务的特点,对任务进行分类,然后把不同类型的任务分别加入到不同线程池的工作队列中,这样可以根据任务的特点,分别调整每个线程池。
(4)调整线程池的大小。线程池的最佳大小主要取决于系统的可用CPU的数目以及工作队列中任务的特点。
(5)避免任务过载。
现在基本上可以解决并发处理客户端的问题啦。但是它依然存在不足:
(1)并发量激增的情况下,一台服务器很难应付海量的多并发;这就需要提高服务器并发处理能力和服务器个数;常见的解决方案是集群。
(2)服务器的好坏有两个取决因素:一个是并发能力,一个是响应速度;在并发能力有保障的情况下,每个工作线程,大部分的处理时间都浪费在IO操作上,因为CPU的处理能力比IO快太多,而IO却存在太多的局限因素,造成线程阻塞在IO操作上,大大降低了响应速度;而且会造成资源的浪费,就好比两个同学,一个负责烧水,一个负责挑水,烧水的人一直守在炉子前等待水开,一个却一直挑水;虽然烧水的人可以腾出时间帮助挑水的人,但是他却不能这样做,因为他固定的只能负责一个任务。
对于高并发,我们很有必要提高IO的操作效率,同时也应该改善我们处理每个任务的原则,提高CPU的利用率;Java NIO就是解决方案。
学习资料:http://wenku.baidu.com/link?url=VyKQnsn4b0BDJ8cQlLUu9cvpGz-Iou_499U4lJE9I0s5nPPY5kF5BDd8qo1yRMOiqsM8wxDPEL_S0koiFp8v5y36G9OGJydC2C12juo0bTW