`

IO的阻塞与非阻塞、同步与异步以及Java网络IO交互方式(转)

 
阅读更多


最近工作中,接触到了Java网络编程方面的东西:Socket、NIO、MongoDB等,也看了tomcat的源码,也加强了线程方面的知识,也使用了MINA这样的框架。感觉获益良多,原本技术上的薄弱环节也在慢慢提高,很多想写的东西,也在慢慢规划整理。无奈最近在筹备婚礼的事情,显得有些耽搁。


想了很久,决定先写写IO中经常被提到的概念——“同步与异步、阻塞与非阻塞”以及在Java网络编程中的简单运用。


想达到的目的有两个:

  

1。深入的理解同步与异步、阻塞与非阻塞,这看似烂大街的词汇很多人已经习惯不停的说,但却说不出其中的所以然,包括我。

 

2。理解各种IO模型在Java网络IO中的运用,能够根据不同的应用场景选择合适的交互方式。了解不同的交互方式对IO性能的影响。

前提


  首先先强调上下文:下面提到了同步与异步、阻塞与非阻塞的概念都是在IO的场合下。它们在其它场合下有着不同的含义,比如操作系统中,通信技术上。

  然后借鉴下《Unix网络编程卷》中的理论:

  IO操作中涉及的2个主要对象为程序进程、系统内核。以读操作为例,当一个IO读操作发生时,通常经历两个步骤:

  1,等待数据准备

  2,将数据从系统内核拷贝到操作进程中

  例如,在socket上的读操作,步骤1会等到网络数据包到达,到达后会拷贝到系统内核的缓冲区;步骤2会将数据包从内核缓冲区拷贝到程序进程的缓冲区中。

阻塞(blocking)与非阻塞(non-blocking)IO

  IO的阻塞、非阻塞主要表现在一个IO操作过程中,如果有些操作很慢,比如读操作时需要准备数据,那么当前IO进程是否等待操作完成,还是得知暂时不能操作后先去做别的事情?一直等待下去,什么事也不做直到完成,这就是阻塞。抽空做些别的事情,这是非阻塞。

  非阻塞IO会在发出IO请求后立即得到回应,即使数据包没有准备好,也会返回一个错误标识,使得操作进程不会阻塞在那里。操作进程会通过多次请求的方式直到数据准备好,返回成功的标识。

  想象一下下面两种场景:

  A 小明和小刚两个人都很耿直内向,一天小明来找小刚借书:“小刚啊,你那本XXX借我看看”。 于是小刚就去找书,小明就等着,找了半天找到了,把书给了小明。


  B 小明和小刚两个人都很活泼外向,一天小明来找小刚借书:“嘿小刚,你那本XXX借我看看”。 小刚说:“我得找一会”,小明就去打球去了。过会又来,这次书找到了,把书给了小明。

  结论:A是阻塞的,B是非阻塞的。

  从CPU角度可以看出非阻塞明显提高了CPU的利用率,进程不会一直在那等待。但是同样也带来了线程切换的增加。增加的 CPU 使用时间能不能补偿系统的切换成本需要好好评估。

同步(synchronous)与异步(asynchronous)IO

  先来看看正式点的定义,POSIX标准将IO模型分为了两种:同步IO和异步IO,Richard Stevens在《Unix网络编程卷》中也总结道:

A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;
An asynchronous I/O operation does not cause the requesting process to be blocked;

  可以看出,判断同步和异步的标准在于:一个IO操作直到完成,是否导致程序进程的阻塞。如果阻塞就是同步的,没有阻塞就是异步的。这里的IO操作指的是真实的IO操作,也就是数据从内核拷贝到系统进程(读)的过程。

  继续前面借书的例子,异步借书是这样的:

  C 小明很懒,一天小明来找小刚借书:“嘿小刚,你那本XXX借我看看”。 小刚说:“我得找一会”,小明就出去打球了并且让小刚如果找到了就把书拿给他。小刚是个负责任的人,找到了书送到了小明手上。


  A和B的借书方式都是同步的,有人要问了B不是非阻塞嘛,怎么还是同步?

  前面说了IO操作的2个步骤:准备数据和把数据从内核中拷贝到程序进程。映射到这个例子,书即是准备的数据,小刚是内核,小明是程序进程,小刚把书给小明这是拷贝数据。在B方式中,小刚找书这段时间小明的确是没闲着,该干嘛干嘛,但是小刚找到书把书给小明的这个过程也就是拷贝数据这个步骤,小明还是得乖乖的回来候着小刚把书递手上。所以这里就阻塞了,根据上面的定义,所以是同步。



在涉及到 IO 处理时通常都会遇到一个是同步还是异步的处理方式的选择问题。同步能够保证程序的可靠性,而异步可以提升程序的性能。小明自己去取书不管等着不等着迟早拿到书,指望小刚找到了送来,万一小刚忘了或者有急事忙别的了,那书就没了。

讨论


  说实话,网上关于同步与异步、阻塞与非阻塞的文章多之又多,大部分是拷贝的,也有些写的非常好的。参考了许多,也借鉴了许多,也经过自己的思考。

  同步与异步、阻塞与非阻塞之间确实有很多相似的地方,很容易混淆。wiki更是把异步与非阻塞画上了等号,更多的人还是认为他们是不同的。原因可能有很多,每个人的知识背景不同,设定的上下文也不同。

  我的看法是:在IO中,根据上面同步异步的概念,也可以看出来同步与异步往往是通过阻塞非阻塞的形式来表达的,并且是通过一种中间处理机制来达到异步的效果。同步与异步往往是IO操作请求者和回应者之间在IO实际操作阶段的协作方式,而阻塞非阻塞更确切的说是一种自身状态,当前进程或者线程的状态。

  在发出IO读请求后,阻塞IO会一直等待有数据可读,当有数据可读时,会等待数据从内核拷贝至系统进程;而非阻塞IO都会立即返回。至于数据怎么处理是程序进程自己的事情,无关同步和异步。 

两种方式的组合

  组合的方式当然有四种,分别是:同步阻塞、同步非阻塞、异步阻塞、异步非阻塞。

Java网络IO实现和IO模型


  不同的操作系统上有不同的IO模型,《Unix网络编程卷》将unix上的IO模型分为5类:blocking I/O、nonblocking I/O、I/O multiplexing (select and poll)、signal driven I/O (SIGIO)以及asynchronous I/O (the POSIX aio_functions)。具体可参考《Unix网络编程卷1》6.2章节。

  在windows上IO模型也是有5种:select 、WSAAsyncSelect、WSAEventSelect、Overlapped I/O 事件通知以及IOCP。具体可参考windows五种IO模型。
  Java是平台无关的语言,在不同的平台上会调用底层操作系统的不同的IO实现,下面就来说一下Java提供的网络IO的工具和实现,为了扩大阻塞非阻塞的直观感受,我都使用了长连接。

长连接: 在短信开发中,用到,象cmpp中提到过,就是不间断的发送测试连通性的包,以确认是否连接中断 如果中断,则继续连接;

  短连接:连接完成,发送完消息后,就断开连接,下次在发消息的时候在次连接;)


阻塞IO


  同步阻塞最常用的一种用法,使用也是最简单的,但是 I/O 性能一般很差,CPU 大部分在空闲状态。下面是一个简单的基于TCP的同步阻塞的Socket服务端例子:


1 @Test
2 public void testJIoSocket() throws Exception
3 {
4 ServerSocket serverSocket = new ServerSocket(10002);
5 Socket socket = null;
6 try
7 {
8 while (true)
9 {
10 socket = serverSocket.accept();
11 System.out.println("socket连接:" + socket.getRemoteSocketAddress().toString());
12 BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
13 while(true)14 {
15 String readLine = in.readLine();
16 System.out.println("收到消息" + readLine);
17 if("end".equals(readLine))
18 {
19 break;20 }
21 //客户端断开连接22 socket.sendUrgentData(0xFF);
23 }
24 }
25 }
26 catch (SocketException se)
27 {28 System.out.println("客户端断开连接");
29 }
30 catch (IOException e)
31 {
32 e.printStackTrace();
33 }
34 finally
35 {
36 System.out.println("socket关闭:" + socket.getRemoteSocketAddress().toString());
37 socket.close();
38 }
39 }
  使用SocketTest作为客户端工具进行测试,同时开启2个客户端连接Server端并发送消息,如下图:





再看下后台的打印

socket连接:/127.0.0.1:54080收到消息hello!收到消息my name is client1

由于服务器端是单线程的,在第一个连接的客户端阻塞了线程后,第二个客户端必须等待第一个断开后才能连接。当输入“end”字符串断开客户端1,这时候看到后台继续打印:

socket连接:/127.0.0.1:54080收到消息hello!收到消息my name is client1收到消息endsocket关闭:/127.0.0.1:54080socket连接:/127.0.0.1:54091收到消息hello!收到消息my name is client2

所有的客户端连接在请求服务端时都会阻塞住,等待前面的完成。即使是使用短连接,数据在写入 OutputStream 或者从 InputStream 读取时都有可能会阻塞。这在大规模的访问量或者系统对性能有要求的时候是不能接受的。

阻塞IO + 每个请求创建线程/线程池

通常解决这个问题的方法是使用多线程技术,一个客户端一个处理线程,出现阻塞时只是一个线程阻塞而不会影响其它线程工作;为了减少系统线程的开销,采用线程池的办法来减少线程创建和回收的成本,模式如下图:

简单的实现例子如下,使用一个线程(Accptor)接收客户端请求,为每个客户端新建线程进行处理(Processor),线程池的我就不弄了:

public class MultithreadJIoSocketTest{

@Test

public void testMultithreadJIoSocket() throws Exception

{

ServerSocket serverSocket = new ServerSocket(10002);

Thread thread = new Thread(new Accptor(serverSocket));

thread.start();

Scanner scanner = new Scanner(System.in);

scanner.next();

}

public class Accptor implements Runnable

{

private ServerSocket serverSocket;

public Accptor(ServerSocket serverSocket)

{

this.serverSocket = serverSocket;

}

public void run()

{

while (true)

{

Socket socket = null;

try

{

socket = serverSocket.accept();

if(socket != null)

{

System.out.println(“收到了socket:” + socket.getRemoteSocketAddress()。toString());

Thread thread = new Thread(new Processor(socket));

thread.start();

}

}

catch (IOException e)

{

e.printStackTrace();

}

}

}

}

public class Processor implements Runnable

{

private Socket socket;

public Processor(Socket socket)

{

this.socket = socket;

}

@Override

public void run()

{

try

{

BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));

String readLine;

while(true)

{

readLine = in.readLine();

System.out.println(“收到消息” + readLine);

if(“end”.equals(readLine))

{

break;

}





//客户端断开连接

socket.sendUrgentData(0xFF);

Thread.sleep(5000);

}

}

catch (InterruptedException e)

{

e.printStackTrace();

}

catch (SocketException se)

{

System.out.println(“客户端断开连接”);

}

catch (IOException e)

{

e.printStackTrace();

}

finally {

try

{

socket.close();

}

catch (IOException e)

{

e.printStackTrace();

}

}

}

}}

使用2个客户端连接,这次没有阻塞,成功的收到了2个客户端的消息。

收到了socket:/127.0.0.1:55707收到了socket:/127.0.0.1:55708收到消息hello!收到消息hello!

在单个线程处理中,我人为的使单个线程read后阻塞5秒,就像前面说的,出现阻塞也只是在单个线程中,没有影响到另一个客户端的处理。

这种阻塞IO的解决方案在大部分情况下是适用的,在出现NIO之前是最通常的解决方案,Tomcat里阻塞IO的实现就是这种方式。但是如果是大量的长连接请求呢?不可能创建几百万个线程保持连接。再退一步,就算线程数不是问题,如果这些线程都需要访问服务端的某些竞争资源,势必需要进行同步操作,这本身就是得不偿失的。

非阻塞IO + IO multiplexing

Java从1.4开始提供了NIO工具包,这是一种不同于传统流IO的新的IO方式,使得Java开始对非阻塞IO支持;NIO并不等同于非阻塞IO,只要设置Blocking属性就可以控制阻塞非阻塞。至于NIO的工作方式特点原理这里一概不说,以后会写。模式如下图:

下面是简单的实现:

public class NioNonBlockingSelectorTest{

Selector selector;

private ByteBuffer receivebuffer = ByteBuffer.allocate(1024);

@Test

public void testNioNonBlockingSelector()

throws Exception

{

selector = Selector.open();

SocketAddress address = new InetSocketAddress(10002);

ServerSocketChannel channel = ServerSocketChannel.open();

channel.socket()。bind(address);

channel.configureBlocking(false);

channel.register(selector, SelectionKey.OP_ACCEPT);

while(true)

{

selector.select();

Iterator iterator = selector.selectedKeys()。iterator();

while (iterator.hasNext()) {

SelectionKey selectionKey = iterator.next();

iterator.remove();

handleKey(selectionKey);

}

}

}

private void handleKey(SelectionKey selectionKey) throws IOException

{

ServerSocketChannel server = null;

SocketChannel client = null;

if(selectionKey.isAcceptable())

{

server = (ServerSocketChannel)selectionKey.channel();

client = server.accept();

System.out.println(“客户端: ” + client.socket()。getRemoteSocketAddress()。toString());

client.configureBlocking(false);

client.register(selector, SelectionKey.OP_READ);

}

if(selectionKey.isReadable())

{

client = (SocketChannel)selectionKey.channel();

receivebuffer.clear();

int count = client.read(receivebuffer);

if (count > 0) {

String receiveText = new String( receivebuffer.array(),0,count);

System.out.println(“服务器端接受客户端数据--:” + receiveText);

client.register(selector, SelectionKey.OP_READ);

}

}

}

}

Java NIO提供的非阻塞IO并不是单纯的非阻塞IO模式,而是建立在Reactor模式上的IO复用模型;在IO multiplexing Model中,对于每一个socket,一般都设置成为non-blocking,但是整个用户进程其实是一直被阻塞的。只不过进程是被select这个函数阻塞,而不是被socket IO给阻塞,所以还是属于非阻塞的IO.

这篇文章中把这种模式归为了异步阻塞,我其实是认为这是同步非阻塞的,可能看的角度不一样。

异步IO

Java1.7中提供了异步IO的支持,暂时还没有看过,所以以后再讨论。

网络IO优化

对于网络IO有一些基本的处理规则如下:

1.减少交互的次数。比如增加缓存,合并请求。

2.减少传输数据大小。比如压缩后传输、约定合理的数据协议

3.减少编码。比如提前将字符转化为字节再传输。

4.根据应用场景选择合适的交互方式,同步阻塞,同步非阻塞,异步阻塞,异步非阻塞。

就说到这里吧,感觉有点乱,有些地方还是找不到更贴切的语言来描述。


 

分享到:
评论

相关推荐

    Socket高性能IO模型浅析

    (1)同步阻塞IO(BlockingIO):即传统的IO模型。(2)同步非阻塞IO(Non-blockingIO):默认创建的socket都是阻塞的,非阻塞IO要求socket被设置为...同步和异步的概念描述的是用户线程与内核的交互方式:同步是指用

    java-simple-reactor:java简单的reactor的模型

    同步和异步是针对应用程序和内核的交互而言的,同步指的是用户进程触发IO操作并等待或者轮询的去查看IO操作是否就绪,而异步是指用户进程触发IO操作以后便开始做自己的事情,而当IO操作已经完成的时候会得到IO完成的...

    JAVA上百实例源码以及开源项目源代码

     Java二进制IO类与文件复制操作实例,好像是一本书的例子,源代码有的是独立运行的,与同目录下的其它代码文件互不联系,这些代码面向初级、中级Java程序员。 Java访问权限控制源代码 1个目标文件 摘要:Java源码,...

    JAVA上百实例源码以及开源项目

     Java二进制IO类与文件复制操作实例,好像是一本书的例子,源代码有的是独立运行的,与同目录下的其它代码文件互不联系,这些代码面向初级、中级Java程序员。 Java访问权限控制源代码 1个目标文件 摘要:Java源码,...

    Java BIO、NIO、AIO、Netty知识详解(值得珍藏)

    简单来说就是java通过io流方式和外部设备进行交互。 在Java类库中,IO部分的内容是很庞大的,因为它涉及的领域很广泛:标准输入输出,文件的操作,网络上 的数据传输流,字符串流,对象流等等等。 比如程序从服务器...

    reactphp:使用PHP进行事件驱动的非阻塞IO

    事件循环的核心是事件循环,它在其之上提供了低级实用程序,例如:流抽象,异步DNS解析器,网络客户端/服务器,HTTP客户端/服务器以及与进程的交互。 第三方库可以使用这些组件来创建异步网络客户端/服务器等。 ...

    字节大佬总结的Java面试资料

    字节大佬总结的Java面试资料 ...TCP、UDP 和文件传输的支持,作为一个异步 NIO 框架,Netty 的所有 IO 操作都是异步非阻塞 的,通过 Future-Listener 机制,用户可以方便的主动获取或者通过通知机制获得 IO 操作结果。

    java开源包11

    WebSocket4J 是一个用 Java 实现的 WebSocket 协议的类库,可使用 Java 来构建交互式 Web 应用。WebSocket4J 并未实现客户端通讯协议,所以不能用它来连接 WebSocket 服务器。 Struts验证码插件 JCaptcha4Struts2 ...

    java开源包6

    WebSocket4J 是一个用 Java 实现的 WebSocket 协议的类库,可使用 Java 来构建交互式 Web 应用。WebSocket4J 并未实现客户端通讯协议,所以不能用它来连接 WebSocket 服务器。 Struts验证码插件 JCaptcha4Struts2 ...

    java开源包9

    WebSocket4J 是一个用 Java 实现的 WebSocket 协议的类库,可使用 Java 来构建交互式 Web 应用。WebSocket4J 并未实现客户端通讯协议,所以不能用它来连接 WebSocket 服务器。 Struts验证码插件 JCaptcha4Struts2 ...

    java开源包4

    WebSocket4J 是一个用 Java 实现的 WebSocket 协议的类库,可使用 Java 来构建交互式 Web 应用。WebSocket4J 并未实现客户端通讯协议,所以不能用它来连接 WebSocket 服务器。 Struts验证码插件 JCaptcha4Struts2 ...

    java开源包101

    WebSocket4J 是一个用 Java 实现的 WebSocket 协议的类库,可使用 Java 来构建交互式 Web 应用。WebSocket4J 并未实现客户端通讯协议,所以不能用它来连接 WebSocket 服务器。 Struts验证码插件 JCaptcha4Struts2 ...

    java开源包5

    WebSocket4J 是一个用 Java 实现的 WebSocket 协议的类库,可使用 Java 来构建交互式 Web 应用。WebSocket4J 并未实现客户端通讯协议,所以不能用它来连接 WebSocket 服务器。 Struts验证码插件 JCaptcha4Struts2 ...

    java开源包8

    WebSocket4J 是一个用 Java 实现的 WebSocket 协议的类库,可使用 Java 来构建交互式 Web 应用。WebSocket4J 并未实现客户端通讯协议,所以不能用它来连接 WebSocket 服务器。 Struts验证码插件 JCaptcha4Struts2 ...

    java开源包10

    WebSocket4J 是一个用 Java 实现的 WebSocket 协议的类库,可使用 Java 来构建交互式 Web 应用。WebSocket4J 并未实现客户端通讯协议,所以不能用它来连接 WebSocket 服务器。 Struts验证码插件 JCaptcha4Struts2 ...

    java开源包1

    WebSocket4J 是一个用 Java 实现的 WebSocket 协议的类库,可使用 Java 来构建交互式 Web 应用。WebSocket4J 并未实现客户端通讯协议,所以不能用它来连接 WebSocket 服务器。 Struts验证码插件 JCaptcha4Struts2 ...

    java开源包3

    WebSocket4J 是一个用 Java 实现的 WebSocket 协议的类库,可使用 Java 来构建交互式 Web 应用。WebSocket4J 并未实现客户端通讯协议,所以不能用它来连接 WebSocket 服务器。 Struts验证码插件 JCaptcha4Struts2 ...

    Java资源包01

    WebSocket4J 是一个用 Java 实现的 WebSocket 协议的类库,可使用 Java 来构建交互式 Web 应用。WebSocket4J 并未实现客户端通讯协议,所以不能用它来连接 WebSocket 服务器。 Struts验证码插件 JCaptcha4Struts2 ...

Global site tag (gtag.js) - Google Analytics