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<Runnable>(queueSize));
    }

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

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

3.后记

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

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

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: IO(输入输出)是指操作系统与硬件之间进行数据传输的过程。常见的IO操作包括读取和写入文件、网络数据传输等。 NIO (非阻塞IO)是Java中的一种IO编程方式,它支持非阻塞的数据传输,即程序在等待IO操作完成时不会阻塞。这样可以避免程序因等待IO而卡住的问题。NIO提供了异步通道、缓冲区等功能来支持高效的IO操作。 AIO (异步IO)是操作系统提供的另一种IO编程方式,它支持异步的数据传输。AIO允许程序在发起IO请求后立即返回,而不是等待IO操作完成。这样程序可以继续执行其他任务,避免因等待IO阻塞。AIO在操作系统层面支持异步IO,而NIO是在Java语言层面支持异步IO. ### 回答2: IO,即Input/Output,是指计算机与外部设备之间进行数据传输的过程。传统的IO操作是同步的,即在进行IO操作时,程序会阻塞等待数据的读写完成。这种方式效率较低,因为程序在等待IO操作完成时无法做其他事情。 NIO,即New Input/Output,是Java在JDK1.4版本引入的一种新的IO模型。NIO可以实现非阻塞IO操作,其核心是通过通道(Channel)和缓冲区(Buffer)来实现数据的读写。NIO通过选择器(Selector)实现了多路复用,一个线程可以同时处理多个通道的IO操作,实现了更高的效率。NIO适用于连接数较多且连接时间较短的场景,例如网络编程中的ServerSocketChannel和SocketChannel。 AIO,即Asynchronous Input/Output,是Java在JDK1.7版本引入的一种新的IO模型。AIO通过回调机制实现异步的IO操作,当IO操作完成后会触发回调函数的执行。AIO适用于连接数较少且连接时间较长的场景,例如网络编程中的AsynchronousServerSocketChannel和AsynchronousSocketChannel。相比于NIO,AIO的优势是在IO操作完成前不需要阻塞线程,可以充分利用线程资源,并且编程模型更简单。 综上所述,IO是传统的同步IO模型,效率较低;NIO是一种非阻塞IO模型,通过选择器实现了多路复用,效率较高;AIO是一种异步的IO模型,通过回调机制实现了非阻塞IO操作,适用于连接时间较长的场景。在实际开发中,可以根据具体需求选择适合的IO模型来提高程序的性能和效率。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

空空的司马

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值