Java网络编程 - 同步阻塞IO模型

由于项目需要使用Java开发后台服务器程序,C/C++程序员就要学学Java了。博客内容用来记录我的学习过程。Unix/Linux下的几种网络IO模型在之前的博客中已经提及到,但是使用的大多数都是Unix/Linux下的系统调用
博客内容大多数来自网络资料以及书籍《Netty权威指南》,转载请注明出处http://blog.csdn.net/Robin__Chou/article/details/54378934,谢谢!

如果想了解Unix/Linux下网络IO模型的可以看如下的几篇相关博客,里面大多都是代码段,原理性的讲解比较少。
1.Unix/Linux下5种I/O模型
2.多进程并发服务器
3.多线程并发服务器
4.IO多路复用之select
5.IO多路复用之poll
6.IO多路复用之epoll
Java作为一门跨平台的语言,当然也会支持上述的几种IO模型。在JDK1.4之前的版本中,Java对IO的支持并没有那么好。直到JDK1.4发布,Java才开始支持非阻塞IO,也是这个版本开始逐渐使用Java开发高性能服务器。更是到现在有了像Netty,Mina这样优秀的开源框架。

1.同步阻塞式IO编程

同步阻塞式IO模型中Server端始终有一个“前置”的连接负责和接入进来的客户端。当有客户端接入进来时,就创建一个线程用于和该客户端进行交互,主线程则继续回到accept阻塞状态。
相比使用C/C++开发,Java程序的Socket编程则方便了许多。下面实例一个时间服务器的程序,示例程序设计思路上来源《Netty权威指南》一书。时间服务器的运行流程就是客户端连接到服务器之后,向服务器发送TIME QUERY命令,Server接受到命令后回送服务器当前时间给客户端。程序如下:

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/*
 * BIO模型的问题在于每一个客户端连接进来,服务器端就需要创建一个线程来处理客户端链路
 * 一个线程只能处理一个客户端连接,对于高性能,高并发接入场景很难满足。
 * */
public class TimeServer {
    public static void main(String[] args){
        int port = 8080;                                            //监听端口号为8080

        ServerSocket server = null;
        try {
            server = new ServerSocket(port);
            System.out.println("时间服务器监听端口:" + port);
            Socket socket = null;

            while (true) {
                socket = server.accept();                           //accecpt阻塞等待连接

                //有客户端连接进来则创建处理线程,负责和客户端交互
                new Thread(new TimeServerHandler(socket)).start();
            }
        }catch(Exception e){
            e.printStackTrace();
        }finally {
            if (server != null) {
                System.out.println("服务器端关闭。");
                try {
                    server.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                server = null;
            }
        }
    }
}

处理客户端的线程则可以写成:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

public class TimeServerHandler implements Runnable {
    private Socket socket;                      //与客户端交互Socket

    public TimeServerHandler(Socket socket) {   //构造函数,传入连接进来的socket
        this.socket = socket;
    }

    @Override
    public void run() {
        BufferedReader in = null;
        PrintWriter out = null;
        try {
            in = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
            out = new PrintWriter(this.socket.getOutputStream(), true);
            String currentTime = null;
            String body = null;
            while (true) {
                body = in.readLine();           //读取客户端发送的消息
                if (body == null)               //空消息,则关闭Server端线程
                    break;
                System.out.println("服务器端收到消息:" + body);

                /*
                 * 此处处理客户端请求
                */

                if("QUERY TIME".equalsIgnoreCase(body)){
                    currentTime = new java.util.Date(System.currentTimeMillis()).toString();
                }else{
                    currentTime = "未识别的命令";
                }

                out.println(currentTime);
            }

        } catch (Exception e) {
            //关闭接受流
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e1) {
                    e1.printStackTrace();
                }
            }
            //关闭发送流
            if (out != null) {
                out.close();
                out = null;
            }
            //关闭socket
            if (this.socket != null) {
                try {
                    this.socket.close();
                } catch (IOException e1) {
                    e1.printStackTrace();
                }
                this.socket = null;
            }
        }
    }
}

客户端程序则比较简单就是建立连接后发送一条QUERY TIME命令即可。

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;


public class TimeClient {
    public static void main(String[] args) {
        int port = 8080;
        Socket socket = null;
        BufferedReader in = null;
        PrintWriter out = null;
        try {
            socket = new Socket("127.0.0.1", port); 
            in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            out = new PrintWriter(socket.getOutputStream(), true);
            out.println("QUERY TIME");
            System.out.println("成功发送命令到服务器端。");     
            String resp = in.readLine();                    //读取服务器端返回信息
            System.out.println("当前时间:" + resp);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //关闭输出流
            if (out != null) {
                out.close();
                out = null;
            }

            //关闭输入数据流
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                in = null;
            }

            //关闭socket连接
            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                socket = null;
            }
        }
    }
}

程序分析

同步阻塞式IO在每个客户端连接进来时就创建一个线程来对客户端进行交互,我们知道每个线程是会占资源的,而且创建线程的过程也是要占据资源的。所以对于大量的用户连接使用同步阻塞IO进行服务器端程序设计并不合理。这种模型也难以满足高性能多并发接入的应用场合。

2.使用线程池优化的同步阻塞式IO编程

为了解决同步阻塞模型中一个客户端需要创建一个线程进行交互的问题,人们对其进行优化,后台通过一个线程池来处理多个客户端的求解接入。
和上面不同的就是在客户端连接进来之后,不是去创建一个线程来进行交互,而是直接将客户端的交互任务丢给线程池去完成。JDK的线程池维护一个消息队列和一些活跃的线程来完成消息队列中的任务。线程池是可以设置消息队列的大小和最大的线程数量的,所以,相比之前的情况,这种方式的资源是可控的,不会导致客户端连接过多而导致服务器端资源耗尽。
同样时间服务器作为案例,从代码中可以看出,其实流程上大致相同,不同的就是一个在创建线程进行客户端交互,而另一个是创建一个线程池,客户端连接进来之后,直接将客户端连接丢给线程池去处理。

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

import com.phei.netty.bio.TimeServerHandler;


public class TimeServer {
    public static void main(String[] args){
        //监听端口号为8080
        int port = 8080;                                                
        ServerSocket server = null;
        try {
            server = new ServerSocket(port);
            System.out.println("时间服务器监听端口:" + port);
            Socket socket = null;
            TimeServerHandlerExecutePool singleExecutor 
                = new TimeServerHandlerExecutePool(50, 10000);          // 创建IO任务线程池
            while (true) {
                socket = server.accept();
                singleExecutor.execute(new TimeServerHandler(socket));
            }
        }catch(Exception e){
            e.printStackTrace();
        }finally {
            if (server != null) {
                System.out.println("服务器端关闭。");
                try {
                    server.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                server = null;
            }
        }
    }
}
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;


public class TimeServerHandlerExecutePool {

    private ExecutorService executor;

    public TimeServerHandlerExecutePool(int maxPoolSize, int queueSize) {
        executor = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(),
                maxPoolSize, 120L, TimeUnit.SECONDS,
                new ArrayBlockingQueue(queueSize));
    }

    public void execute(Runnable task) {
        executor.execute(task);
    }
}

线程池中单个线程的处理还是和同步阻塞式IO模型的Handler相同。客户端没有任何不同。使用线程池优化的同步阻塞式IO编程虽然解决了上述中的创建线程问题,由于引入了一个消息队列,所以如果连接数量一旦很大的时候肯定会导致消息的延迟,如果某些客户端的网络不畅,其他的客户端没办法得到及时的响应。而且在队列满了之后的客户端消息都会被直接拒绝。

3.后记

根据近段时间翻阅的论坛,博客等等地方来看,使用Java来开发后台服务器的案例不在少数,甚至是实时性要求较高的游戏行业也有使用Java开发后台服务器的案例。手机端APP应用后台使用Java开发服务器的就更多了。此外物联网技术中也有使用的案例,特别是常常使用自定义的通信协议的智能家居等。分析使用的原理无外乎有两点:

  • 使用方便,开发简单
  • 容易维护,可靠性高

尤其使用Netty,Mina这样的开源框架,让开发关注于业务,而不被繁琐的技术细节所困扰。

你可能感兴趣的:(【Unix/Linux】)