Mina解析(一)

        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,回车

Mina解析(一)_第1张图片

c.测试,客户端输入

Mina解析(一)_第2张图片

        服务端响应:

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个请求到达后会被拒绝。
Mina解析(一)_第3张图片
        注意看,第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.多线程阻塞服务器
        在实际的应用开发中,我们更多采用的是多线程阻塞服务器,即每一个客户端请求到达,就建立一个线程单独的处理它与服务端的通信,如下图所示:
Mina解析(一)_第4张图片
        好处就是可以并发处理每一个到达的请求!服务端代码如下:
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包提供了现成的线程池的实现。

Mina解析(一)_第5张图片
        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

你可能感兴趣的:(java,Mina)