java-----NIO总结(一)
这篇是讲解NIO的准备篇;
在JDK1.4 NIO出现之前,我们通常用到的IO都是BIO也叫同步阻塞式IO,他是面向流的,一个输入流产生一个字节的数据,一个输出流消费一个字节的数据,那么这里的阻塞主要体现在什么地方呢?比如我们从一个文件中读取数据的话,调用InputStream.read()方法,这里的read方法会一直等待直到有数据到来时才会返回,如果在网络环境中的话,阻塞现象体现在,我们使用Socket的时候,在服务端通过调用ServerSocket.accept()方法来判断有没有客户端Socket连入,这里的accept是一个阻塞的方法,同时服务端Socket想要获取到客户端Socket传来的消息也是通过InputStream的read方法进行的,同样也是阻塞方法,我们可以这样理解网络通信中的阻塞模式:(1)客户端发出请求之后会一直等待,不做其他的事情,直到服务端返回结果或者网络出现了问题;(2)服务端也是同样的,当在处理客户端A发送过来的消息时,其他客户端只能等到,直到服务端处理完客户端A的请求为止;当然你可以在服务端为每一个客户端连接创建一个单独的线程来管理该客户端,这样虽然能够做到针对单独客户端的高效处理,但是操作系统通知accept()的方式还是单一的,也就是说服务器接收到数据报文之后的"业务处理过程"是可以多线程的,但是数据报文的接收还是需要一个一个来的,这样做同时会给服务端带来很大压力,因为开启过多的线程不仅占用服务器资源,同样增加了线程之间来回切换的开销,虽然可以通过线程池的方式控制生成线程的数量,但是当用户数量过多的时候,仍然会造成BlockingQueue现象的发生;
下面我们先来看看平常使用Socket的例子:
服务端:
- public class BIOServer {
- public void initBIOServer(int port)
- {
- ServerSocket serverSocket = null;//服务端Socket
- Socket socket = null;//客户端socket
- BufferedReader reader = null;
- String inputContent;
- int count = 0;
- try {
- serverSocket = new ServerSocket(port);
- System.out.println(stringNowTime() + ": serverSocket started");
- while(true)
- {
- socket = serverSocket.accept();
- System.out.println(stringNowTime() + ": id为" + socket.hashCode()+ "的Clientsocket connected");
- reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
- while ((inputContent = reader.readLine()) != null) {
- System.out.println("收到id为" + socket.hashCode() + " "+inputContent);
- count++;
- }
- System.out.println("id为" + socket.hashCode()+ "的Clientsocket "+stringNowTime()+"读取结束");
- }
- } catch (IOException e) {
- e.printStackTrace();
- }finally{
- try {
- reader.close();
- socket.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
- public String stringNowTime()
- {
- SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
- return format.format(new Date());
- }
- public static void main(String[] args) {
- BIOServer server = new BIOServer();
- server.initBIOServer(9898);
- }
- }
客户端:
- public class BIOClient {
- public void initBIOClient(String host,int port)
- {
- BufferedReader reader = null;
- BufferedWriter writer = null;
- Socket socket = null;
- String inputContent;
- int count = 0;
- try {
- reader = new BufferedReader(new InputStreamReader(System.in));
- socket = new Socket(host, port);
- writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
- System.out.println("clientSocket started: " + stringNowTime());
- while(((inputContent = reader.readLine()) != null) && count < 2)
- {
- inputContent = stringNowTime()+": 第"+count+"条消息: "+inputContent+"\n";
- writer.write(inputContent);//将消息发送给服务端
- writer.flush();
- count++;
- }
- } catch (Exception e) {
- e.printStackTrace();
- }finally {
- try {
- socket.close();
- reader.close();
- writer.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
- public String stringNowTime()
- {
- SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
- return format.format(new Date());
- }
- public static void main(String[] args) {
- BIOClient client = new BIOClient();
- client.initBIOClient("127.0.0.1", 9898);
- }
- }
我们在客户端输入三行数据:
相应的查看服务端的输出:
注意一点就是红色框部分只有在客户端输入三行数据之后才会输出,因为我们设置客户端只能输入三行数据,这就充分证明了服务端第17行在我们的客户端有数据输入的时候始终处于阻塞状态等待输入的;
如果我们连续启动两次客户端其他什么都不做,在服务端你会发现下面输出:
也就是你只能注册一个客户端到服务端,除非这个客户端需要服务端完成的事情已经做完,这点想必是不符合大量并发的应用场景吧;
为此,我们可以将服务端代码进行修改,对于每一个客户端到来之后,都为当前客户端单独启动一个线程,这样就能解决只有一个客户端可以注册的问题了,修改后的服务端代码如下(注意客户端代码没变):
- public class BIOServer {
- public void initBIOServer(int port)
- {
- ServerSocket serverSocket = null;//服务端Socket
- Socket socket = null;//客户端socket
- ClientSocketThread thread = null;
- try {
- serverSocket = new ServerSocket(port);
- System.out.println(stringNowTime() + ": serverSocket started");
- while(true)
- {
- socket = serverSocket.accept();
- System.out.println(stringNowTime() + ": id为" + socket.hashCode()+ "的Clientsocket connected");
- thread = new ClientSocketThread(socket);
- thread.start();
- }
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- public String stringNowTime()
- {
- SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
- return format.format(new Date());
- }
- public static void main(String[] args) {
- BIOServer server = new BIOServer();
- server.initBIOServer(9898);
- }
- class ClientSocketThread extends Thread
- {
- public Socket socket;
- public ClientSocketThread(Socket socket)
- {
- this.socket = socket;
- }
- @Override
- public void run() {
- BufferedReader reader = null;
- String inputContent;
- int count = 0;
- try {
- reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
- while ((inputContent = reader.readLine()) != null) {
- System.out.println("收到id为" + socket.hashCode() + " "+inputContent);
- count++;
- }
- System.out.println("id为" + socket.hashCode()+ "的Clientsocket "+stringNowTime()+"读取结束");
- } catch (IOException e) {
- e.printStackTrace();
- }finally{
- try {
- reader.close();
- socket.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
- }
- }
此时你连续启动三个客户端,在服务端会看到如下输出:
可以发现三个客户端都已经注册到了服务端,而且各个客户端输入数据之间是不会相互影响的,我们算是解决了只能注册一个客户端的问题了,但是想想客户端不多的时候还好说,如果客户端多起来怎么办呢?为每个客户端都开启一个线程这并不是什么好主意,伴随的将是服务器性能资源的浪费;
当然我们可以通过线程池的方式来尽量较少线程的创建和销毁工作,因此我们可以将服务端代码修改如下(注意客户端代码还是不变):
- public class BIOServer {
- public void initBIOServer(int port)
- {
- ServerSocket serverSocket = null;//服务端Socket
- Socket socket = null;//客户端socket
- ExecutorService threadPool = Executors.newCachedThreadPool();
- ClientSocketThread thread = null;
- try {
- serverSocket = new ServerSocket(port);
- System.out.println(stringNowTime() + ": serverSocket started");
- while(true)
- {
- socket = serverSocket.accept();
- System.out.println(stringNowTime() + ": id为" + socket.hashCode()+ "的Clientsocket connected");
- thread = new ClientSocketThread(socket);
- threadPool.execute(thread);
- }
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- public String stringNowTime()
- {
- SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
- return format.format(new Date());
- }
- class ClientSocketThread extends Thread
- {
- public Socket socket;
- public ClientSocketThread(Socket socket)
- {
- this.socket = socket;
- }
- @Override
- public void run() {
- BufferedReader reader = null;
- String inputContent;
- int count = 0;
- try {
- reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
- while ((inputContent = reader.readLine()) != null) {
- System.out.println("收到id为" + socket.hashCode() + " "+inputContent);
- count++;
- }
- System.out.println("id为" + socket.hashCode()+ "的Clientsocket "+stringNowTime()+"读取结束");
- } catch (IOException e) {
- e.printStackTrace();
- }finally{
- try {
- reader.close();
- socket.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
- }
- public static void main(String[] args) {
- BIOServer server = new BIOServer();
- server.initBIOServer(9898);
- }
- }
可以看到我们在第6行创建了一个CachedThreadPool缓存线程池,这种线程池中默认情况下对线程的个数是没有限制的,如果新任务到来之后发现线程池中没有可用线程的话,会创建一个新的线程出来处理当前任务,这种线程池中的线程是由超时机制的,默认情况下空闲线程超过60s就会被回收掉;
我们连续启动三个客户端,查看服务端的输出如下:
我们同样也达到了将三个客户端全部注册到服务端的目的,但是这只解决了服务器接收到数据报文之后的"业务处理过程"多线程执行,而对于数据报文的接收还是需要一个一个来的,也就是说操作系统底层通知accept()的方式还是单个的;