《Netty in action 中文》 第一章

1

Netty Java NIO APIs

 


 

 内容提纲:

§   Netty架构

§   为何需要 non-blocking IO (NIO)
§   阻塞与非阻塞 IO

§   JDK提供的NIO实现的相关问题,以及Netty如何加以解决

本章简单介绍一下Netty,主要的讨论点放在javaNIO API上。如果你是刚接触jvm上的网络编程,毫无疑问,本章内容是个不错的入门介绍;当然,对于有经验的java开发者而言,也算是个温故而知新的不错选择。如果,NIO对于你来说已经很熟悉了,那么请跳过本章直接阅读第2章的内容,学习使用netty的简单示例。

 

Netty是一个用于客户端 /服务端开发的socket框架,基于该框架可以方便快速的开发一些网络应用,如开发一些基于某种协议的网络客户端或者服务端。 Netty对很多复杂的网络编程内容加以抽象,屏蔽了很多底层网络的具体实现,最终为开发者提供一套简单易用的API。因此,使得基于Netty开发的网络应用,能够具有良好的易用性及可扩展性。Netty是基于NIO的,因此它的API都是异步的。

通常,无论你是基于Netty还是其他的NIO API来开发网络应用,最终都会面临可扩展性的问题。而Netty的一个重要特性就是它的异步性。本章,会通过对阻塞IO及非阻塞IO,加以详细论述,告诉您为什么后者是适用于解决可扩展性的问题。

对于网络编程的新手来说,通过学习本章内容,你将对网络应用有个整体上的认知,以及了解到Netty是如何实现这些的。同时,会讨论一下JDK提供的网络编程API的优缺点,以及Netty是如何解决这些问题的,如内存泄漏方面的问题。


 

一句话,通过学习本章,你将了解Netty是什么,Netty提供了哪些能力;同时,对于javaNIO以及异步处理方式,都会有详细的了解。具备了这些,将对学习其他章节会有很大帮助。

1.1 为什么需要Netty

David  Wheeler的话讲,计算机科学面临的所有问题,都可以通过某钟间接的方式加以解决。作为一个NIO客户端/客户端开发框架,Netty在某种程度上,则正是对这种间接解决方式的一种印证。Netty简化了TCP/UDP等网络应用的开发,因为Netty做了高度的抽象;当然,你也可以直接调用那些叫底层的JDK方法,而不使用Netty

 

1.1.1    Netty独领风骚之处

Netty的简单快速开发,并不意味着使用它开发的应用,就一定具有良好的可维护性及高性能。网络应用开发,需要使用原有的很多协议,以及基于文本或者基于二进制的协议,如ftphttpudp,这就使得Netty的开发者,必须仔细斟酌如何设计,来应对这样的需求。最终,Netty没有采用任何折衷的方式,达到了易用、高性能、高健壮性的目标。

很多著名的第三方开源或者公司,如RedHat, Twitter等,都使用了Netty。可以这么说,Netty的诞生,正是迎合了这些著名项目的需要。多年来,Netty已经广为人知并且成为jvm上使用最多的网络框架。

2011年,Netty的作者Trustin Lee,离开RedHat加入了Twitter,因此Netty也变成一个独立的项目,不归属于任何公司。Red Hat Twitter都使用Netty,因此他们也是Netty的赞助商。一句话,Netty用户越来越多,赞助者也越来越多,Netty这个项目大有前途。(不想直接翻译原文了,这里)

 

1.1.2    Netty丰富特性

本书的后续学习,你会接触并使用到Netty的很多特性。

1.1 给出了Netty支持的相关特性,以及Netty的整体架构

 

 
                                     

 

除了支持多种传输方式和协议,Netty开发应用过程中,还带来了很多其他益处。(具体见表 1.1

 

Table 1.1 Netty givesdevelopers a full set of tools. (这个就不一一翻译了)

 

Development Area      NettyFeatures

 

Design                          §  Unified API for various transport types  blocking and non-blocking socket

§  Flexible to use

§  Simple but powerfulthread-model

§  True connectionlessdatagram socket support

§  Chaining of logics tomake reuse easy

Ease of Use                  § Well-documented Javadoc and plenty of examples provided

§  No additionaldependencies except JDK 1.6 (or above). Some features are

supportedonly in Java 1.7 and above. Other features may have other

dependencies,but these are optional

Performance                 § Better throughput; lower latency than core Java APIs

§  Less resourceconsumption because of pooling and reuse

§  Minimized unnecessarymemory copy

Robustness                  § No more OutOfMemoryError due to fast, slow, or overloadedconnection.

§ No more unfair read/write ratiooften found in a NIO application in high-speed

networks

 

Security                         §  Complete SSL/TLS and StartTLS support

§  Runs in a restrictedenvironment such as Applet or OSGI

Community                   § Release early, release often

§  Active

除了上述列出的这些益处,Netty还很好的解决或者规避了Java NIO中的bug及约束。

整体上对于Netty的特性,你应该已经了然于胸。接下来,好好学习下Netty的异步处理机制,及相关理念。NIONetty,二者都浓墨重彩的使用了异步处理的代码。与异步相关的轮询机制,如果这方面没有了解的话,将很难使用好Netty或者NIO。下节内容,将告诉你为什么需要异步API

1.2 异步设计

NettyAPI整体上来说是异步的。异步处理并不是什么新东东,异步思想已经由来已久。然而,IO面临的瓶颈,使得异步处理在今天显得尤为重要。异步是如何玩转的呢?

异步处理能够使我们更加有效的使用各种资源。它启动一个任务后,这个任务并不会一直在那里“等”,直到任务处理结束(即不会阻塞在那里)。取而代之的却是,任务启动,任务执行过程中,你可以去做其他的处理。当任务处理过程结束时,你会收到任务处理结束的一个通知。

本节,将讨论实现异步API的两种常见方式,以及两种实现方式间的异同点。

1.2.1    CallBack

CallBack是异步编程常用的一种技术。CallBack被传入某个方法中,该方法执行结束,则调用CallBack。类似的技术,在javascript中被广泛使用。如何使用回调技术去获取数据,示例如下:

 

Listing 1.1 Callback example

public  interface Fetcher  {

void  fetchData(FetchCallback  callback);

}

public  interface FetchCallback  {

void  onData(Data data);

void  onError(Throwable  cause);

}

public  class Worker  {

public  void doWork()  {

Fetcher  fetcher =  ...

fetcher.fetchData(new  FetchCallback()  {
    @Override

public  void  onData(Data data)  {                          #1

System.out.println("Data received:  "  + data);

}

@Override

public  void  onError(Throwable  cause) {                    #2

System.err.println("An error  accour:  " +  cause.getMessage());

}

});

}

}

#1 Call if data is fetched without error
#2 Call if error is received during fetch

Fetcher.fetchData()方法有个FetchCallback的入参,当获取到数据或者获取过程出错的时候,就会回调FetchCallback

不同的情形,提供不同的处理方法:

   FetchCallback.onData()   获取数据不出错时候回调 (#1)

   FetchCallback.onError()  获取数据出错时候回调(#2)

这些回调方法,可以从一个调用者线程放入到其他的不同线程中。因此,无法保证究竟在什么时候会回调用到FetchCallback中的哪个方法。

回调方式面临一个这样的问题,即当具有不同回调方法的异步方法,链式的放在一起时(即),很容易导致代码的混淆,易读性很差。当然,代码易用性和可读性是两码事。例如,基于JavascriptNode.js,虽然大量的使用回调方式;但是,却能很方便的使用它去写应用,代码可读性也很好。

 

1.2.2    Futures

第二中方式就是使用FuturesFuture是一种抽象,它表示在某个条件下,这个值变得有效或者可用。Future对象要么表示某个计算结果,要么就表示计算失败的某种异常。

Javaava.util.concurrent中提供了一个Future接口,它通过使用自身的Executor,实现异步处理的效果。

如下面的例子所示,当你传入一个Runnabel对象给ExecutorService.submit()方法时,会返回一个Future。可以使用这个返回的Future去查看执行过程是否完成。Li

Listing  1.2 Future example via

ExecutorService executor  =  Executors.newCachedThreadPool();Runnable  task1 =  new  Runnable() {

@Override

public  void run()  {

doSomeHeavyWork();
}

}

Callable<Interger>  task2 =  new  Callable() {

@Override

public  Integer call()  {

return  doSomeHeavyWorkWithResul();

}

}

Future<?>  future1 =  executor.submit(task1);

Future<Integer>  future2 =  executor.submit(task2);while  (!future1.isDone()!future2.isDone())  {

 

//  do something  else

}

#A
#B

在自己的API中,你也可以使用这种技术。例如,你可以在(示例1.1)中使用Future来实现Fetcher。具体如下:

public  interface Fetcher  {

Future<Data>  fetchData();
}

 

public  class Worker  {

public  void doWork()  {

Fetcher  fetcher =  ...

Future<Data> future  =  fetcher.fetchData();
try  {

while(!fetcher.isDone())  {

 

//  do  something else

}

System.out.println("Data  received: "  +  future.get());

catch (Throwable  cause)  {

System.err.println("An  error accour:  "  +

cause.getMessage());

}

}

}

#A
#B

 

 

查看fetcher是否执行,从而进行某种相应的操作。某些时候,使用Future会显得不太优雅,因为你不得不隔一段时间就去检查一下Future的状态以观察它是否执行完成;相比之下,callback则不会这样,它实在某种执行完成后,直接去触发某种相应的操作。

了解了常用的两种实现异步执行的方式后,你可能会想想,哪种实现方式最好呢?对此,没有一个确切的答案。实际上,Netty同时使用了这两种实现方式,从而提供相对更好的异步能力。

下节内容,首先介绍如何在jvm上编写阻塞的网络应用,接着再介绍如何使用NIONIO.2。必须了解这些基本知识点,才能更好的学习本书接下来的各个章节。如果你已经很熟悉Java网络编程,可以选择快速浏览一下下节内容,达到温故而知新的母的。

1.3       BIONIO

Web的持续发展,使得对于网络应用规模处理能力方面的要求,变得越来越高。首当其冲的是,对于性能方面的要求。庆幸的是,Java自身提供了创建高效、可扩展的网络应用的相关能力(或工具类)。尽管Java的早期版本提供网络应用开发的能力。但是实际上实在1.4版本之后,Java才引入了NIO API,为我们去开发更加高效的网络应用铺平了道路。

Java 7引入的新APINIO.2),一方面,考虑到能够让开发人员写出更加高效的异步处理网络代码;另一方面,也在尝试提供一套比以往版本中更加高级的API

Java网络编程,一般会涉及如下两种处理方式:

§    阻塞IO,即BIOuse IO

§    阻塞IO,即NIO

 

New or non-blocking?

The  N in NIO  is  typically thought  to  mean  non-blocking   rather  than  new. NIO  has  beenaround for solong now that nobody calls it  new  IO anymore. Most people refer to it as  nonblocking IO

1.2 展示了阻塞IO中,线程与连接间的关系,即多少个连接就对应有多少个处理线程,两者间是1:1的关系。然而,JVM上的线程数目是有限的,这就在很大程度上限制了连接数目。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Ok,下面深入探讨一下BIONIO。我们会使用一个简单的服务端程序,揭示BIONIO间的区别。这个实例的功能,就是服务端接收客户端发送的内容,并原封不动的将收到的内容,返回给客户端。

1.3.1    基于BIOEchoServer

第一个版本的EchoServer,是基于BIO的。这是种很常见的编写网络应用方式,因为在java的很早版本中就提供了这种易用的BIO

只要不涉及网络扩展能力,BIO还是蛮不错的实现方式。EchoServer示例如下:

public  class PlainEchoServer  {

public  void serve(int  port)  throws IOException  {

final  ServerSocket socket  =  new ServerSocket(port);             #1

try  {

while  (true)  {

final  Socket clientSocket  =  socket.accept();            #2

System.out.println("Accepted connection  from  " +

clientSocket);

new  Thread(new  Runnable() {                           #3

@Override

public  void  run() {
    try  {

BufferedReader  reader  = new  BufferedReader(
       new

InputStreamReader(clientSocket.getInputStream()));

PrintWriter  writer =  new  PrintWriter(clientSocket
       .getOutputStream(),  true);

while(true)  {                            #4

writer.println(reader.readLine()); writer.flush();

}

catch (IOException  e)  {

e.printStackTrace();
try  {

clientSocket.close(); catch (IOException  ex)  {
    //  ignore on  close

}

}

}

}).start();                                        #5

}

catch (IOException  e)  {

e.printStackTrace();
}

}

}

#1 Bind server to port  
#2 Block until new client connection is accepted
#3 Create new thread to handle client connection
#4 Read data from client and write it back
#5 Start thread

写过java网络程序的话,对上述代码肯定会非常熟悉。不过,不妨思考下,上述这种方式可能会存在哪些问题呢?

   再看一下这段代码:

final  Socket clientSocket  =  socket.accept();new  Thread(new Runnable()  {

@Override

public  void run()  {

}

}).start();

每个客户端Connection都需要对应有一个处理线程。你可能会提议使用线程池来规避创建大量线程,但是这么做治标不治本。面临的问题依然存在:有限的线程数,最终导致连接数受限。如果应用程序需要处理大量连接的能力,问题则依然存在。

   下节内容,会给出一个使用NIOEchoServer,上述问题将不复存在。当然,了解NIO前,还是很有必要先学习一下NIO涉及的相关概念。

1.3.2    NIO基础

Java 7引入了新的NIO API,称之为NIO.2。你可以自行决定使用NIO还是NIO.2。尽管NIO.2依然是异步的,但是无论从API层面,还是实现层面来说,它与之前的NIO有着很大的不同。API并没有完全不同,依然使用原有的很大特性。例如,实现中依然使用ByteBUffer类抽象,作为数据的容器。

BYTEBUFFER

无论是NIO,还是Netty中,ByteBuffer都是至关重要的类。ByteBuffer可以在堆上分配也可以直接分配(直接分配,指的是分配并不是在堆上进行的)。通常,直接分配得到的buffer,能更快的将其中的内容写入channel,但是分配/回收代价高昂。ByteBufferAPI提供特有的方式去获取及管理数据。ByteBuffer允许相同的数据,能够很方便的在不同的ByteBuffer对象实现共享。ByteBuffer提供slice等操作,可以控制数据哪些内容可被读取。

 
 

 

 

 

 

 

 

 


ByteBuffer常见的操作如下:

§  ByteBuffer中写入数据

 § 调用ByteBuffer.flip()方法,在读写模式间进行切换

 § ByteBuffer中读出数据

§  调用ByteBuffer.clear()或者ByteBuffer.compact()

ByteBuffer中写入数据时,它会通过修改write下标位置,来跟踪当前已经写入了多少数据内容。

    需要写入数据时,可以调用Bytebuffer.flip(),从写模式切换到读模式。调用ByteBuffer.flip()方法,将ByteBufferlimit下标移到已经写入数据的当前位置,将position下标变为0。通过这种方式,你就可以读取ByteBuffer中的数据内容了。

   如果接着又需要往ByteBuffer中写入数据,你可以调用如下方法,切换回写模式:

§   ByteBuffer.clear()  清除整个ByteBuffer

§  Bytebuffer.compact() 清除那些已经通过内存拷贝,被读取的数据内容

ByteBuffer.compact()ByteBuffer仍未被读取的内容,移动到ByteBuffer的首部并更新position下标。下面给出ByteBuffer的典型使用方法:

   Listing  1.5 Working with a ByteBuffer

Channel  inChannel =  ....;

ByteBuffer  buf =  ByteBuffer.allocate(48);

 

int  bytesRead =  -1;

do  {

bytesRead  = inChannel.read(buf);                         #1

if  (bytesRead !=  -1)  {

buf.flip();                                             #2

while(buf.hasRemaining()){

System.out.print((char)  buf.get());                      #3

}

buf.clear();                                             #4

}

while (bytesRead  !=  -1);
inChannel.close();

#1Read data from the Channel to the ByteBuffer
#2 Make buffer ready for read
#3 Read the bytes in the ByteBuffer; every get() operation updates theposition by 1#4 Make the ByteBuffer ready for writing again

学习完ByteBuffer使用方式,我们接下来了解下selectors的相关内容。

WORKINGWITH NIO SELECTORS 使用NIO SELECTORS

新旧版本NIOAPI中,都是采用基于selector的方式来处理网络事件和相关数据的。

Channel将连接connection,关联到可以进行IO操作的实体上,如文件或socket

Selector是个NIO组件,可以检测到哪些Channel可用于读/写操作。所以说,一个selector可用于处理多个连接,不再存在BIO示例EchoServer中那种线程、连接必须11的问题。

遵循以下步骤,来使用selectors

1.  创建一或多个Selector,用于注册那些openedChannel

2.  Channel注册后,需要初始化你需要侦听哪些事件

常见的4种侦听事件有:

§   OP_ACCEPT Operation  -socket-accept操作设置bit

§   OP_CONNECT  Operation-socket-connect操作设置bit

§   OP_READ  Operation       -read操作设置bit
§   OP_WRITE Operation    -write操作设置bit

3. 完成channel注册,你可以调用Selector.select(),该方法会发生阻塞,直到监听的某个事件发生

4.  方法不再阻塞时,你可以通过各种SelectionKey的实例(该实例可以关联到注册过的channel及选中的操作项)去做某些逻辑操作。.

具体可以做什么,取决于当前哪种操作处于就绪状态(可读还是可写)。特定时刻,一个SelectedKey可能会有多个事件处于就绪态。

 

明白工作原理之后,我们不妨实现一个NIOEchoServer练练手。这样,更加有助于了解NIO工作的细节。同时,你会发现ByteBuffer真的是个不可或缺的好东东。

1.3.3    基于NIOEchoServer

如下所示,例子中的EchoServer使用了异步NIO API,具有处理数千个客户端连接的能力

As  shown in  the  following listing,  this  version of  the  EchoServer uses  the  asynchronous NIO API, which allows you to serve thousands of concurrent clients withone thread!

 

Listing 1.6 EchoServer v2: NIO

public  class PlainNioEchoServer  {

public  void serve(int  port)  throws IOException  {

System.out.println("Listening  for connections  on  port "  +  port);

ServerSocketChannel  serverChannel =  ServerSocketChannel.open();ServerSocket  ss =  serverChannel.socket();

InetSocketAddress  address =  new  InetSocketAddress(port);

ss.bind(address);                                         #1

serverChannel.configureBlocking(false); Selector   selector =  Selector.open();

serverChannel.register(selector,  SelectionKey.OP_ACCEPT);  #2

 

while  (true)  {
    try  {

selector.select();                                 #3

catch (IOException  ex)  {

ex.printStackTrace();

//  handle in  a  proper way break;

}

 

Set  readyKeys =  selector.selectedKeys();              #4

Iterator iterator  =  readyKeys.iterator(); while  (iterator.hasNext())  {

SelectionKey  key =  (SelectionKey)  iterator.next();

iterator.remove();//该步骤,不可或缺,不然这个key一直存在 #5

try  {

if  (key.isAcceptable())  {

ServerSocketChannel  server =  (ServerSocketChannel)

key.channel();

SocketChannel client  =  server.accept();     #6

System.out.println("Accepted  connection from  "  +

client);

client.configureBlocking(false);

client.register(selector,  SelectionKey.OP_WRITE

SelectionKey.OP_READ,  ByteBuffer.allocate(100));                  #7

}

if  (key.isReadable())  {                         #8

SocketChannel client  =  (SocketChannel)  key.channel();
ByteBuffer  output  = (ByteBuffer)  key.attachment();

client.read(output);                        #9

}

if  (key.isWritable())  {                        #10

SocketChannel  client =  (SocketChannel)  key.channel();

ByteBuffer  output =  (ByteBuffer)  key.attachment();

output.flip();

client.write(output);                      #11

output.compact();

}

catch  (IOException ex)  {
    key.cancel();

try  {

key.channel().close();
catch (IOException  cex)  {
}

}

}

}

}

}

 

 

#1 Bind server to port

#2 Register the channelwith the selector to be interested in new Client connections that get accepted
#3 Block until something is selected

#4 Get all SelectedKey instances

#5 Remove the SelectedKey from the iterator
#6 Accept the client connection

#7 Register connectionto selector and set ByteBuffer
#8 Check for SelectedKey for read

#9 Read data to ByteBuffer

#10 Check for SelectedKey for write

#11 Write data from ByteBuffer to channel

NIO实现的EchoServer比之前BIO实现的版本,相对复杂些。这是不得不付出的小代价,因为异步的方式编程向来就比同步方式编程要复杂。

语法上来说,NIONIO.2是很相似的,不同之处是它们的实现方式。下节内容,会描述这种不同点,同时我们会实现第三个版本的EchoServer

1.3.4    基于NIO.2EchoServer

与之前的NIO实现不同,NIO.2可以让开发者涉及到IO操作,并提供一个“操作完成”处理类(即类CompletionHandler)。当操作完全执行完成后,会自动执行“操作完成”处理类。因此,“操作完成”处理类的执行是通过底层系统调用实现的,具体实现对开发者而言是屏蔽的。同时,同一时刻的一个Channel中,NIO.2确保只有一个CompletionHandler会被执行。这种方式屏蔽了多线程执行的复杂性,从而大大简化代码。

     NIONIO.2最大的不同就是,开发者不在需要去检测channel上发生了某个事件,去触发某种处理操作。在NIO.2中,可以直接触发IO操作,并主持一个“操作完成”处理类给他,IO操作完成时会触发该处理类。这就不需要开发者自己去开发逻辑检测操作,因为这不再是必要的了。

通过下面的代码,我们来看一下如何使用NIO.2来实现EchoServer

 

Listing 1.7 EchoServer v3: NIO.2

public  class PlainNio2EchoServer  {

public  void serve(int  port)  throws IOException  {

System.out.println("Listening  for connections  on  port  "  + port);
       final AsynchronousServerSocketChannel serverChannel  =
 AsynchronousServerSocketChannel.open();

InetSocketAddress  address =  new  InetSocketAddress(port);

serverChannel.bind(address);                             #1

final  CountDownLatch  latch =  new  CountDownLatch(1);serverChannel.accept(null,  new

CompletionHandler<AsynchronousSocketChannel, Object>() {         #2

@Override

public  void completed(final AsynchronousSocketChannel channel,

Object  attachment) {

 

serverChannel.accept(null,  this);                  #3

ByteBuffer buffer  =  ByteBuffer.allocate(100);channel.read(buffer,  buffer,

new  EchoCompletionHandler(channel));       #4

}

@Override

public  void  failed(Throwable  throwable, Object  attachment)  {
    try  {

serverChannel.close();                            #5

catch (IOException  e)  {

//  ingnore  on close

finally {

latch.countDown();

}

}

});

try  {

latch.await();

catch (InterruptedException  e)  {

Thread.currentThread().interrupt();
}

}

private  final class  EchoCompletionHandler  implements

CompletionHandler<Integer,  ByteBuffer>  {

private  final AsynchronousSocketChannel channel;

EchoCompletionHandler(AsynchronousSocketChannel  channel) {
    this.channel  = channel;

}

@Override

public  void completed(Integer  result,  ByteBuffer buffer)  {
    buffer.flip();

channel.write(buffer,  buffer, new  CompletionHandler<Integer,

ByteBuffer>()  {                                                  #6

@Override

public  void  completed(Integer  result, ByteBuffer  buffer)  {
    if  (buffer.hasRemaining())  {

channel.write(buffer,  buffer, this);       #7

else  {

buffer.compact();

channel.read(buffer,  buffer,

EchoCompletionHandler.this);         #8

}

}

@Override

public  void  failed(Throwable  exc, ByteBuffer  attachment)  {
    try  {

channel.close();

catch  (IOException e)  {
    // ingnore  on  close
}

}

});

}

 

@Override

public  void failed(Throwable  exc,  ByteBuffer attachment)  {
    try  {

channel.close();

catch  (IOException e)  {
    //  ingnore on  close
}

}

}

}

#1Bind Server to port

#2 Start to accept newClient connections. Once one is accepted the CompletionHandler will get called.#3 Again accept new Client connections

#4 Trigger a readoperation on the Channel, the given CompletionHandler will be notified oncesomethingwas read

#5 Close the socket on error

#6 Trigger a writeoperation on the Channel, the given CompletionHandler will be notified oncesomethingwas written

#7 Trigger again a write operation if something is left in the ByteBuffer

#8 Trigger a readoperation on the Channel, the given CompletionHandler will be notified oncesomethingwas read

乍一看,感觉上述代码比使用NIO写的代码要多出很多。但是,你注意到没有,NIO.2替你搞定了线程方面及事件回调方面的东东。它简化了创建多线程NIO应用开发的代码量,尽管本例中看起来有点不那么如此。当应用量持续喷发时,你就会越来越体会到NIO.2带来的好处。

   下一节内容,我们将来看看JDKNIO实现,都有哪些问题。

1.4       NIO面临的问题及Netty的解决之道

本节,我们将学习一下,使用Java NIO会有哪些问题或约束,Netty是如处理决这些问题的。JDK提供NIO包,无疑是一种很大的进步。然而, JDK提供的NIO,并不易用,使用时会面临很多问题。这些问题,很大程度上是由于过往的设计层面导致的,因此难以客服。

1.4.1    跨平台的通用能力

NIO比较靠近底层,依赖于操作系统的IO处理方式。

你可能会发现在Linux上运行okNIO,在Windows上运行却出现了问题。我的建议是,无论你是否使用NIO,你都最好在你宣称支持的所有操作系统上,都能坐相关的功能验证。确保在Linux上通过的所有测试,在其他操作系统上也能运行OK。如果不这么干,到时候你肯定会后悔莫及的,O(_)O~

几乎完美的NIO.2,目前只在Java 7中支持,所有如果你的应用时在Java 6上运行的话,可能就无法使用NIO.2了。此外,NIO.2中不支持用于UDP应用的datagramchannels,换句话说,NIO.2仅仅是为TCP量身定做的。

Netty通过提供统一的API,使得无论实在Java 6还是 Java 7上,都以相同的语法方式来处理,从而上述解决这个问题。你无须担心底层究竟用的是哪个版本,因为你只要关心如何简单的使用NettyAPI就好了。

1.4.2    是否选择增强ByteBuffer  

如前所述,ByteBuffer起到数据容器的作用。不幸的是,JDK自身并没有提供用于包装ByteBuffer数组的ByteBuffer实现类。当需要减少内存拷贝的时候,这个能力就变得很有必要了。如果你想尝试自己实现的话,就别浪费宝贵时间了;因为,ByteBuffer的构造函数是private的,你无法继承实现。

  因此,Netty提供了一个自己的ByteBuffer实现类,绕开了上述问题。这个类有多重构造、使用方式,并且提供了简单的API进行某些操作处理。

1.4.3    Scatteringand gathering可能导致泄漏

大量的Channel实现都支持scattering and gathering(不好翻译,应该是散射和集聚的能力,具体意思看下文)。该特征允许同一时刻从多个ByteBuffer实例中进行读/写操作,这样做可以达到较好的性能。这里使用kernel/操作系统进行读写的操作,这往往能使性能达到最好,毕竟kernel/操作系统更靠近底层硬件,能够以最有效的方式进行相关处理。

    当你需要将数据拆分发送到多个不同ByteBuffer,并分别进行独立处理的时候,就会经常使用到Scattering/gathering能力。例如,你打算获取一个ByteBuffer钟的head内容和另一个ByteBuffer中的body内容。

   1.4展示了scattering  读是如何进行的。你将一个ByteBuffer数组传送到ScatteringByteChannel中,数据就会被分散的从channel中读出来放到buffers中。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Figure 1.4 Ascattering read from a channel

 


 

Gathering 采取类似的工作方式,但是数据是往channel中写的。你将一个ByteBuffer数组传送到GatheringByteChannel.write()方法中,数据就会从buffers中读出来一起写入Channel中去。

1.5 展示了Gathering写的过程:

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Figure 1.5 A gatheringwrite to a channel

 

然而,该特性在Java 6后续版本以及Java 7中发生问题,导致内存泄露。当使用scattering/gathering时,你得千万小心,同时要注意你使用的Java版本是否有问题。

   你也许会说,靠,为啥不通过升级Java版本来搞定这个问题呢?我同意你的意见,但是现实情况往往并不能随便升级Java的版本,比如你的公司可能会对部属的某些系统的java版本有严格控制。所以说,升级java看似容易,实际工作中确实相当蛋疼,难以操作的。

1.4.4    Squashingthe famous epoll bug

类似Linux的很多系统上,是通过epoll- IO事件通知机制来实现selector的。这是种高性能的技术,使得操作系统能够异步的处理网络栈。然而可悲的是,即使是当前也会发生epoll- bug导致selector状态不可用,最终导致cpu占用100%飙升。唯一的解决之道,就是复用老的selector,将以往注册过的channel实例传给新建的selector

也就是说,即使当前没有选中的SelectionKeys Selector.select()调用也可以迅速停止阻塞,立马返回。这和JavadocSelector.select()方法的说明,有些背道而驰了:Javadoc说,当没有选中任何内容时,Selector.select()千万不能不阻塞。

注:具体可以参见, seehttps://github.com/netty/netty/issues/327.

 

 

 

解决epoll问题的方式,并不能是否完美。Netty尝试使用自动检测的方式,来规避epoll问题发生。下面的代码,给出了一个典型的epoll-bug

 

while  (true) {

int  selected =  selector.select();                         #1

Set<SelectedKeys>  readyKeys =  selector.selectedKeys();

Iterator  iterator =  readyKeys.iterator();                 #2

while  (iterator.hasNext())  {                              #3

#4

}

}

 

#1Returns immediately and returns 0 as nothing was selected

#2 Obtains all SelectedKeys, Iterator is empty as nothing was selected

#3 Loops overSelectedKeys in Iterator, but never enters this block as nothing was selected

#4 Does the work in here

 

此处,循环的代码会“吃住”cpu

 

while  (true) {

}

 

循环会永远的不停运行,导致CPU玩命的疯转,从而耗费资源。由于可能会占用大量cpu,导致其他功能无法运转,所以这种处理方式,并不受大家认可。

 
 

 

 

 

 


 

Figure 1.4 shows a Java process that takes up all of your CPU.

 

 

 

#1The java command with the PID 58552 eats 102.2 % CPU

这些只是使用NIO肯能遇到的部分问题而已。一方面,不幸的是,即使数年之后,类似的问题可能依然存在;另一方面,值得庆幸额是,Netty已经帮忙解决了这样的问题。(不知道是否是吹牛,学习后面知识,再说)。

1.5       Summary

本章内容,从整体上大致描述了Netty的功能、设计思想以及各种优点。同时,对BIONIO做了相关描述,让读者明白需要使用NIO的原因。

学习如何使用JDKAPI,开发阻塞/非阻塞网络应用的代码。囊括了JDK 7提供的新的NIO API。使用JDKAPI开发网络应用,有太多的细节方面需要注意,如果对于这些细节理解不深入的话,会出现很多问题。而这正是Netty大受欢迎之所在:它屏蔽了这些细节内容,方便用户使用。

下一章,你将了解Netty基本的 API以及编程方式,从而使自己通过使用Netty写出高效的代码。

猜你喜欢

转载自blog.csdn.net/baogang409/article/details/38412987