`
liudaoru
  • 浏览: 1558040 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

【nio】使用 ServerSocketChannel 实现的 File 服务器[z]

    博客分类:
  • java
阅读更多

From: http://www.java2000.net/p6079

 

 

package test.io;

import java.nio.channels.*;
import java.nio.charset.*;
import java.net.*;
import java.io.*;
import java.util.*;
import java.nio.*;

public class FileServer {
  private int port = 8050;

  private ServerSocketChannel serverSocketChannel;

  private Charset charset = Charset.forName("GBK");

  private Selector selector = null;

  public FileServer() throws IOException {
    selector = Selector.open();
    serverSocketChannel = ServerSocketChannel.open();
    serverSocketChannel.socket().setReuseAddress(true);
    serverSocketChannel.socket().bind(new InetSocketAddress(port));
    System.out.println("服务器启动");
  }

  /* 编码过程 */
  public ByteBuffer encode(String str) {
    return charset.encode(str);
  }

  /* 解码过程 */
  public String decode(ByteBuffer bb) {
    return charset.decode(bb).toString();
  }

  /* 服务器服务方法 */
  public void service() throws IOException {
    serverSocketChannel.configureBlocking(false);
    serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    /** 外循环,已经发生了SelectionKey数目 */
    while (selector.select() > 0) {
      /* 得到已经被捕获了的SelectionKey的集合 */
      Iterator iterator = selector.selectedKeys().iterator();
      while (iterator.hasNext()) {
        SelectionKey key = null;
        try {
          key = (SelectionKey) iterator.next();
          iterator.remove();
          if (key.isAcceptable()) {
            ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
            SocketChannel sc = ssc.accept();
            System.out.println("客户端机子的地址是 " + sc.socket().getLocalAddress() + "  客户端机机子的端口号是 " + sc.socket().getLocalPort());
            sc.configureBlocking(false);
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            sc.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE, buffer);
          }
          if (key.isReadable()) {
            reveice(key);
          }
          if (key.isWritable()) {
            send(key);
          }
        } catch (IOException e) {
          e.printStackTrace();
          try {
            if (key != null) {
              key.cancel();
              key.channel().close();
            }
          } catch (ClosedChannelException cex) {
            e.printStackTrace();
          }
        }
      }
      /* 内循环完 */
    }
    /* 外循环完 */
  }

  /* service方法完 */
  public void reveice(SelectionKey key) throws IOException {
    if (key == null)
      return;
    ByteBuffer buff = (ByteBuffer) key.attachment();
    SocketChannel sc = (SocketChannel) key.channel();
    buff.limit(buff.capacity());
    buff.position(0);
    sc.read(buff);
    buff.flip();
    String reviceData = decode(buff);
  }

  /* 发送文件 */
  public void send(SelectionKey key) {
    if (key == null)
      return;
    ByteBuffer buff = (ByteBuffer) key.attachment();
    SocketChannel sc = (SocketChannel) key.channel();
    String data = decode(buff);
    if (data.indexOf("get") == -1)
      return;
    String subStr = data.substring(data.indexOf(" "), data.length());
    System.out.println("截取之后的字符串是 " + subStr);
    FileInputStream fileInput = null;
    try {
      fileInput = new FileInputStream(subStr);
      FileChannel fileChannel = fileInput.getChannel();
      fileChannel.transferTo(0, fileChannel.size(), sc);
    } catch (IOException e) {
      e.printStackTrace();
    } finally {
      try {
        fileInput.close();
      } catch (IOException ex) {
        ex.printStackTrace();
      }
    }
  }

  public static void main(String[] args) throws IOException {
    new FileServer().service();
  }
}
 

 

 

分享到:
评论
5 楼 liudaoru 2010-06-19  
From:[url] http://dev.csdn.net/htmls/58/58120.html[/url]

在JDK 1.4以前,Java的IO操作集中在java.io这个包中,是基于流的同步(blocking)API。对于大多数应用来说,这样的API使用很方便,然而,一些对性能要求较高的应用,尤其是服务端应用,往往需要一个更为有效的方式来处理IO。从JDK 1.4起,NIO API作为一个基于缓冲区,并能提供异步(non-blocking)IO操作的API被引入。本文对其进行深入的介绍。

NIO API主要集中在java.nio和它的subpackages中:

java.nio
定义了Buffer及其数据类型相关的子类。其中被java.nio.channels中的类用来进行IO操作的ByteBuffer的作用非常重要。

java.nio.channels
定义了一系列处理IO的Channel接口以及这些接口在文件系统和网络通讯上的实现。通过Selector这个类,还提供了进行异步IO操作的办法。这个包可以说是NIO API的核心。

java.nio.channels.spi
定义了可用来实现channel和selector API的抽象类。

java.nio.charset
       定义了处理字符编码和解码的类。

java.nio.charset.spi
       定义了可用来实现charset API的抽象类。

java.nio.channels.spi和java.nio.charset.spi这两个包主要被用来对现有NIO API进行扩展,在实际的使用中,我们一般只和另外的3个包打交道。下面将对这3个包一一介绍。

Package java.nio
这个包主要定义了Buffer及其子类。Buffer定义了一个线性存放primitive type数据的容器接口。对于除boolean以外的其他primitive type,都有一个相应的Buffer子类,ByteBuffer是其中最重要的一个子类。

下面这张UML类图描述了java.nio中的类的关系:



Buffer
定义了一个可以线性存放primitive type数据的容器接口。Buffer主要包含了与类型(byte, char…)无关的功能。值得注意的是Buffer及其子类都不是线程安全的。

每个Buffer都有以下的属性:

capacity
这个Buffer最多能放多少数据。capacity一般在buffer被创建的时候指定。
limit
在Buffer上进行的读写操作都不能越过这个下标。当写数据到buffer中时,limit一般和capacity相等,当读数据时,limit代表buffer中有效数据的长度。
position
读/写操作的当前下标。当使用buffer的相对位置进行读/写操作时,读/写会从这个下标进行,并在操作完成后,buffer会更新下标的值。
mark
一个临时存放的位置下标。调用mark()会将mark设为当前的position的值,以后调用reset()会将position属性设置为mark的值。mark的值总是小于等于position的值,如果将position的值设的比mark小,当前的mark值会被抛弃掉。

这些属性总是满足以下条件:
0 <= mark <= position <= limit <= capacity

limit和position的值除了通过limit()和position()函数来设置,也可以通过下面这些函数来改变:

Buffer clear()
把position设为0,把limit设为capacity,一般在把数据写入Buffer前调用。
Buffer flip()
把limit设为当前position,把position设为0,一般在从Buffer读出数据前调用。
Buffer rewind()
把position设为0,limit不变,一般在把数据重写入Buffer前调用。

Buffer对象有可能是只读的,这时,任何对该对象的写操作都会触发一个ReadOnlyBufferException。isReadOnly()方法可以用来判断一个Buffer是否只读。

ByteBuffer
在Buffer的子类中,ByteBuffer是一个地位较为特殊的类,因为在java.io.channels中定义的各种channel的IO操作基本上都是围绕ByteBuffer展开的。

ByteBuffer定义了4个static方法来做创建工作:

ByteBuffer allocate(int capacity)
创建一个指定capacity的ByteBuffer。
ByteBuffer allocateDirect(int capacity)
创建一个direct的ByteBuffer,这样的ByteBuffer在参与IO操作时性能会更好(很有可能是在底层的实现使用了DMA技术),相应的,创建和回收direct的ByteBuffer的代价也会高一些。isDirect()方法可以检查一个buffer是否是direct的。
ByteBuffer wrap(byte [] array)
ByteBuffer wrap(byte [] array, int offset, int length)
把一个byte数组或byte数组的一部分包装成ByteBuffer。

ByteBuffer定义了一系列get和put操作来从中读写byte数据,如下面几个:

byte get()
ByteBuffer get(byte [] dst)
byte get(int index)

ByteBuffer put(byte b)
ByteBuffer put(byte [] src)
ByteBuffer put(int index, byte b)

这些操作可分为绝对定位和相对定为两种,相对定位的读写操作依靠position来定位Buffer中的位置,并在操作完成后会更新position的值。
在其它类型的buffer中,也定义了相同的函数来读写数据,唯一不同的就是一些参数和返回值的类型。

除了读写byte类型数据的函数,ByteBuffer的一个特别之处是它还定义了读写其它primitive数据的方法,如:

int getInt()
       从ByteBuffer中读出一个int值。
ByteBuffer putInt(int value)
       写入一个int值到ByteBuffer中。

读写其它类型的数据牵涉到字节序问题,ByteBuffer会按其字节序(大字节序或小字节序)写入或读出一个其它类型的数据(int,long…)。字节序可以用order方法来取得和设置:

ByteOrder order()
       返回ByteBuffer的字节序。
ByteBuffer order(ByteOrder bo)
       设置ByteBuffer的字节序。

ByteBuffer另一个特别的地方是可以在它的基础上得到其它类型的buffer。如:

CharBuffer asCharBuffer()
为当前的ByteBuffer创建一个CharBuffer的视图。在该视图buffer中的读写操作会按照ByteBuffer的字节序作用到ByteBuffer中的数据上。

用这类方法创建出来的buffer会从ByteBuffer的position位置开始到limit位置结束,可以看作是这段数据的视图。视图buffer的readOnly属性和direct属性与ByteBuffer的一致,而且也只有通过这种方法,才可以得到其他数据类型的direct buffer。

ByteOrder
用来表示ByteBuffer字节序的类,可将其看成java中的enum类型。主要定义了下面几个static方法和属性:

ByteOrder BIG_ENDIAN
       代表大字节序的ByteOrder。
ByteOrder LITTLE_ENDIAN
       代表小字节序的ByteOrder。
ByteOrder nativeOrder()
       返回当前硬件平台的字节序。

MappedByteBuffer
ByteBuffer的子类,是文件内容在内存中的映射。这个类的实例需要通过FileChannel的map()方法来创建。


接下来看看一个使用ByteBuffer的例子,这个例子从标准输入不停地读入字符,当读满一行后,将收集的字符写到标准输出:
    public static void main(String [] args)
       throws IOException
    {
       // 创建一个capacity为256的ByteBuffer
       ByteBuffer buf = ByteBuffer.allocate(256);
       while (true) {
           // 从标准输入流读入一个字符
           int c = System.in.read();
           // 当读到输入流结束时,退出循环
           if (c == -1)
              break;
         
           // 把读入的字符写入ByteBuffer中
           buf.put((byte) c);
           // 当读完一行时,输出收集的字符
           if (c == '\n') {
              // 调用flip()使limit变为当前的position的值,position变为0,
              // 为接下来从ByteBuffer读取做准备
              buf.flip();
              // 构建一个byte数组
              byte [] content = new byte[buf.limit()];
              // 从ByteBuffer中读取数据到byte数组中
              buf.get(content);
              // 把byte数组的内容写到标准输出
              System.out.print(new String(content));
              // 调用clear()使position变为0,limit变为capacity的值,
              // 为接下来写入数据到ByteBuffer中做准备
              buf.clear();
           }
       }
    }


Package java.nio.channels
这个包定义了Channel的概念,Channel表现了一个可以进行IO操作的通道(比如,通过FileChannel,我们可以对文件进行读写操作)。java.nio.channels包含了文件系统和网络通讯相关的channel类。这个包通过Selector和SelectableChannel这两个类,还定义了一个进行异步(non-blocking)IO操作的API,这对需要高性能IO的应用非常重要。

下面这张UML类图描述了java.nio.channels中interface的关系:



Channel
Channel表现了一个可以进行IO操作的通道,该interface定义了以下方法:

boolean isOpen()
       该Channel是否是打开的。
void close()
       关闭这个Channel,相关的资源会被释放。

ReadableByteChannel
定义了一个可从中读取byte数据的channel interface。

int read(ByteBuffer dst)
从channel中读取byte数据并写到ByteBuffer中。返回读取的byte数。

WritableByteChannel
定义了一个可向其写byte数据的channel interface。

int write(ByteBuffer src)
       从ByteBuffer中读取byte数据并写到channel中。返回写出的byte数。

ByteChannel
ByteChannel并没有定义新的方法,它的作用只是把ReadableByteChannel和WritableByteChannel合并在一起。

ScatteringByteChannel
继承了ReadableByteChannel并提供了同时往几个ByteBuffer中写数据的能力。

GatheringByteChannel
继承了WritableByteChannel并提供了同时从几个ByteBuffer中读数据的能力。

InterruptibleChannel
用来表现一个可以被异步关闭的Channel。这表现在两方面:
1.    当一个InterruptibleChannel的close()方法被调用时,其它block在这个InterruptibleChannel的IO操作上的线程会接收到一个AsynchronousCloseException。
2.    当一个线程block在InterruptibleChannel的IO操作上时,另一个线程调用该线程的interrupt()方法会导致channel被关闭,该线程收到一个ClosedByInterruptException,同时线程的interrupt状态会被设置。


接下来的这张UML类图描述了java.nio.channels中类的关系:



异步IO
异步IO的支持可以算是NIO API中最重要的功能,异步IO允许应用程序同时监控多个channel以提高性能,这一功能是通过Selector,SelectableChannel和SelectionKey这3个类来实现的。

SelectableChannel代表了可以支持异步IO操作的channel,可以将其注册在Selector上,这种注册的关系由SelectionKey这个类来表现(见UML图)。Selector这个类通过select()函数,给应用程序提供了一个可以同时监控多个IO channel的方法:

应用程序通过调用select()函数,让Selector监控注册在其上的多个SelectableChannel,当有channel的IO操作可以进行时,select()方法就会返回以让应用程序检查channel的状态,并作相应的处理。

下面是JDK 1.4中异步IO的一个例子,这段code使用了异步IO实现了一个time server:
    private static void acceptConnections(int port) throws Exception {
       // 打开一个Selector
       Selector acceptSelector =
           SelectorProvider.provider().openSelector();

       // 创建一个ServerSocketChannel,这是一个SelectableChannel的子类
       ServerSocketChannel ssc = ServerSocketChannel.open();
       // 将其设为non-blocking状态,这样才能进行异步IO操作
       ssc.configureBlocking(false);

       // 给ServerSocketChannel对应的socket绑定IP和端口
       InetAddress lh = InetAddress.getLocalHost();
       InetSocketAddress isa = new InetSocketAddress(lh, port);
       ssc.socket().bind(isa);

       // 将ServerSocketChannel注册到Selector上,返回对应的SelectionKey
       SelectionKey acceptKey =
           ssc.register(acceptSelector, SelectionKey.OP_ACCEPT);

       int keysAdded = 0;

       // 用select()函数来监控注册在Selector上的SelectableChannel
       // 返回值代表了有多少channel可以进行IO操作 (ready for IO)
       while ((keysAdded = acceptSelector.select()) > 0) {
           // selectedKeys()返回一个SelectionKey的集合,
           // 其中每个SelectionKey代表了一个可以进行IO操作的channel。
           // 一个ServerSocketChannel可以进行IO操作意味着有新的TCP连接连入了
           Set readyKeys = acceptSelector.selectedKeys();
           Iterator i = readyKeys.iterator();

           while (i.hasNext()) {
              SelectionKey sk = (SelectionKey) i.next();
              // 需要将处理过的key从selectedKeys这个集合中删除
              i.remove();
              // 从SelectionKey得到对应的channel
              ServerSocketChannel nextReady =
                  (ServerSocketChannel) sk.channel();
              // 接受新的TCP连接
              Socket s = nextReady.accept().socket();
              // 把当前的时间写到这个新的TCP连接中
              PrintWriter out =
                  new PrintWriter(s.getOutputStream(), true);
              Date now = new Date();
              out.println(now);
              // 关闭连接
              out.close();
           }
       }
    }
这是个纯粹用于演示的例子,因为只有一个ServerSocketChannel需要监控,所以其实并不真的需要使用到异步IO。不过正因为它的简单,可以很容易地看清楚异步IO是如何工作的。

SelectableChannel
这个抽象类是所有支持异步IO操作的channel(如DatagramChannel、SocketChannel)的父类。SelectableChannel可以注册到一个或多个Selector上以进行异步IO操作。

SelectableChannel可以是blocking和non-blocking模式(所有channel创建的时候都是blocking模式),只有non-blocking的SelectableChannel才可以参与异步IO操作。

SelectableChannel configureBlocking(boolean block)
       设置blocking模式。
boolean isBlocking()
       返回blocking模式。

通过register()方法,SelectableChannel可以注册到Selector上。

int validOps()
返回一个bit mask,表示这个channel上支持的IO操作。当前在SelectionKey中,用静态常量定义了4种IO操作的bit值:OP_ACCEPT,OP_CONNECT,OP_READ和OP_WRITE。
SelectionKey register(Selector sel, int ops)
将当前channel注册到一个Selector上并返回对应的SelectionKey。在这以后,通过调用Selector的select()函数就可以监控这个channel。ops这个参数是一个bit mask,代表了需要监控的IO操作。
SelectionKey register(Selector sel, int ops, Object att)
这个函数和上一个的意义一样,多出来的att参数会作为attachment被存放在返回的SelectionKey中,这在需要存放一些session state的时候非常有用。
boolean isRegistered()
       该channel是否已注册在一个或多个Selector上。

SelectableChannel还提供了得到对应SelectionKey的方法:

SelectionKey keyFor(Selector sel)
返回该channe在Selector上的注册关系所对应的SelectionKey。若无注册关系,返回null。

Selector
Selector可以同时监控多个SelectableChannel的IO状况,是异步IO的核心。

Selector open()
       Selector的一个静态方法,用于创建实例。

在一个Selector中,有3个SelectionKey的集合:
1. key set代表了所有注册在这个Selector上的channel,这个集合可以通过keys()方法拿到。
2. Selected-key set代表了所有通过select()方法监测到可以进行IO操作的channel,这个集合可以通过selectedKeys()拿到。
3. Cancelled-key set代表了已经cancel了注册关系的channel,在下一个select()操作中,这些channel对应的SelectionKey会从key set和cancelled-key set中移走。这个集合无法直接访问。

以下是select()相关方法的说明:

int select()
监控所有注册的channel,当其中有注册的IO操作可以进行时,该函数返回,并将对应的SelectionKey加入selected-key set。
int select(long timeout)
       可以设置超时的select()操作。
int selectNow()
       进行一个立即返回的select()操作。
Selector wakeup()
       使一个还未返回的select()操作立刻返回。

SelectionKey
代表了Selector和SelectableChannel的注册关系。

Selector定义了4个静态常量来表示4种IO操作,这些常量可以进行位操作组合成一个bit mask。

int OP_ACCEPT
有新的网络连接可以accept,ServerSocketChannel支持这一异步IO。
int OP_CONNECT
       代表连接已经建立(或出错),SocketChannel支持这一异步IO。
int OP_READ
int OP_WRITE
       代表了读、写操作。

以下是其主要方法:

Object attachment()
返回SelectionKey的attachment,attachment可以在注册channel的时候指定。
Object attach(Object ob)
       设置SelectionKey的attachment。
SelectableChannel channel()
       返回该SelectionKey对应的channel。
Selector selector()
       返回该SelectionKey对应的Selector。
void cancel()
       cancel这个SelectionKey所对应的注册关系。
int interestOps()
       返回代表需要Selector监控的IO操作的bit mask。
SelectionKey interestOps(int ops)
       设置interestOps。
int readyOps()
       返回一个bit mask,代表在相应channel上可以进行的IO操作。

ServerSocketChannel
支持异步操作,对应于java.net.ServerSocket这个类,提供了TCP协议IO接口,支持OP_ACCEPT操作。

ServerSocket socket()
       返回对应的ServerSocket对象。
SocketChannel accept()
       接受一个连接,返回代表这个连接的SocketChannel对象。

SocketChannel
支持异步操作,对应于java.net.Socket这个类,提供了TCP协议IO接口,支持OP_CONNECT,OP_READ和OP_WRITE操作。这个类还实现了ByteChannel,ScatteringByteChannel和GatheringByteChannel接口。
DatagramChannel和这个类比较相似,其对应于java.net.DatagramSocket,提供了UDP协议IO接口。

Socket socket()
       返回对应的Socket对象。
boolean connect(SocketAddress remote)
boolean finishConnect()
connect()进行一个连接操作。如果当前SocketChannel是blocking模式,这个函数会等到连接操作完成或错误发生才返回。如果当前SocketChannel是non-blocking模式,函数在连接能立刻被建立时返回true,否则函数返回false,应用程序需要在以后用finishConnect()方法来完成连接操作。

Pipe
包含了一个读和一个写的channel(Pipe.SourceChannel和Pipe.SinkChannel),这对channel可以用于进程中的通讯。

FileChannel
用于对文件的读、写、映射、锁定等操作。和映射操作相关的类有FileChannel.MapMode,和锁定操作相关的类有FileLock。值得注意的是FileChannel并不支持异步操作。

Channels
这个类提供了一系列static方法来支持stream类和channel类之间的互操作。这些方法可以将channel类包装为stream类,比如,将ReadableByteChannel包装为InputStream或Reader;也可以将stream类包装为channel类,比如,将OutputStream包装为WritableByteChannel。


Package java.nio.charset
这个包定义了Charset及相应的encoder和decoder。下面这张UML类图描述了这个包中类的关系,可以将其中Charset,CharsetDecoder和CharsetEncoder理解成一个Abstract Factory模式的实现:



Charset
代表了一个字符集,同时提供了factory method来构建相应的CharsetDecoder和CharsetEncoder。

Charset提供了以下static的方法:

SortedMap availableCharsets()
       返回当前系统支持的所有Charset对象,用charset的名字作为set的key。
boolean isSupported(String charsetName)
       判断该名字对应的字符集是否被当前系统支持。
Charset forName(String charsetName)
       返回该名字对应的Charset对象。

Charset中比较重要的方法有:

String name()
       返回该字符集的规范名。
Set aliases()
       返回该字符集的所有别名。
CharsetDecoder newDecoder()
       创建一个对应于这个Charset的decoder。
CharsetEncoder newEncoder()
       创建一个对应于这个Charset的encoder。

CharsetDecoder
将按某种字符集编码的字节流解码为unicode字符数据的引擎。

CharsetDecoder的输入是ByteBuffer,输出是CharBuffer。进行decode操作时一般按如下步骤进行:

1. 调用CharsetDecoder的reset()方法。(第一次使用时可不调用)
2. 调用decode()方法0到n次,将endOfInput参数设为false,告诉decoder有可能还有新的数据送入。
3. 调用decode()方法最后一次,将endOfInput参数设为true,告诉decoder所有数据都已经送入。
4. 调用decoder的flush()方法。让decoder有机会把一些内部状态写到输出的CharBuffer中。

CharsetDecoder reset()
       重置decoder,并清除decoder中的一些内部状态。
CoderResult decode(ByteBuffer in, CharBuffer out, boolean endOfInput)
从ByteBuffer类型的输入中decode尽可能多的字节,并将结果写到CharBuffer类型的输出中。根据decode的结果,可能返回3种CoderResult:CoderResult.UNDERFLOW表示已经没有输入可以decode;CoderResult.OVERFLOW表示输出已满;其它的CoderResult表示decode过程中有错误发生。根据返回的结果,应用程序可以采取相应的措施,比如,增加输入,清除输出等等,然后再次调用decode()方法。
CoderResult flush(CharBuffer out)
有些decoder会在decode的过程中保留一些内部状态,调用这个方法让这些decoder有机会将这些内部状态写到输出的CharBuffer中。调用成功返回CoderResult.UNDERFLOW。如果输出的空间不够,该函数返回CoderResult.OVERFLOW,这时应用程序应该扩大输出CharBuffer的空间,然后再次调用该方法。
CharBuffer decode(ByteBuffer in)
一个便捷的方法把ByteBuffer中的内容decode到一个新创建的CharBuffer中。在这个方法中包括了前面提到的4个步骤,所以不能和前3个函数一起使用。

decode过程中的错误有两种:malformed-input CoderResult表示输入中数据有误;unmappable-character CoderResult表示输入中有数据无法被解码成unicode的字符。如何处理decode过程中的错误取决于decoder的设置。对于这两种错误,decoder可以通过CodingErrorAction设置成:
1. 忽略错误
2. 报告错误。(这会导致错误发生时,decode()方法返回一个表示该错误的CoderResult。)
3. 替换错误,用decoder中的替换字串替换掉有错误的部分。

CodingErrorAction malformedInputAction()
       返回malformed-input的出错处理。
CharsetDecoder onMalformedInput(CodingErrorAction newAction)
       设置malformed-input的出错处理。
CodingErrorAction unmappableCharacterAction()
       返回unmappable-character的出错处理。
CharsetDecoder onUnmappableCharacter(CodingErrorAction newAction)
       设置unmappable-character的出错处理。
String replacement()
       返回decoder的替换字串。
CharsetDecoder replaceWith(String newReplacement)
       设置decoder的替换字串。

CharsetEncoder
将unicode字符数据编码为特定字符集的字节流的引擎。其接口和CharsetDecoder相类似。

CoderResult
描述encode/decode操作结果的类。

CodeResult包含两个static成员:

CoderResult OVERFLOW
       表示输出已满
CoderResult UNDERFLOW
       表示输入已无数据可用。

其主要的成员函数有:

boolean isError()
boolean isMalformed()
boolean isUnmappable()
boolean isOverflow()
boolean isUnderflow()
       用于判断该CoderResult描述的错误。

int length()
       返回错误的长度,比如,无法被转换成unicode的字节长度。
void throwException()
       抛出一个和这个CoderResult相对应的exception。

CodingErrorAction
表示encoder/decoder中错误处理方法的类。可将其看成一个enum类型。有以下static属性:

CodingErrorAction IGNORE
       忽略错误。
CodingErrorAction REPLACE
       用替换字串替换有错误的部分。
CodingErrorAction REPORT
报告错误,对于不同的函数,有可能是返回一个和错误有关的CoderResult,也有可能是抛出一个CharacterCodingException。


参考文献
David Flanagan – Java in a Nutshell


作者:DaiJiaLin                      
mailto:woodydai@gmail.com
http://blog.csdn.net/DaiJiaLin
4 楼 liudaoru 2010-06-19  
From:http://www.iteye.com/topic/40489

使用Java NIO编写高性能的服务器
3 楼 liudaoru 2010-06-19  
http://blog.csdn.net/haoel/archive/2008/03/27/2224055.aspx

Java NIO类库Selector机制解析(上)

一、  前言

自从J2SE 1.4版本以来,JDK发布了全新的I/O类库,简称NIO,其不但引入了全新的高效的I/O机制,同时,也引入了多路复用的异步模式。NIO的包中主要包含了这样几种抽象数据类型:

Buffer:包含数据且用于读写的线形表结构。其中还提供了一个特殊类用于内存映射文件的I/O操作。
Charset:它提供Unicode字符串影射到字节序列以及逆映射的操作。
Channels:包含socket,file和pipe三种管道,都是全双工的通道。
Selector:多个异步I/O操作集中到一个或多个线程中(可以被看成是Unix中select()函数的面向对象版本)。

我的大学同学赵锟在使用NIO类库书写相关网络程序的时候,发现了一些Java异常RuntimeException,异常的报错信息让他开始了对NIO的Selector进行了一些调查。当赵锟对我共享了Selector的一些底层机制的猜想和调查时候,我们觉得这是一件很有意思的事情,于是在伙同赵锟进行过一系列的调查后,我俩发现了很多有趣的事情,于是导致了这篇文章的产生。这也是为什么本文的作者署名为我们两人的原因。

先要说明的一点是,赵锟和我本质上都是出身于Unix/Linux/C/C++的开发人员,对于Java,这并不是我们的长处,这篇文章本质上出于对Java的Selector的好奇,因为从表面上来看Selector似乎做到了一些让我们这些C/C++出身的人比较惊奇的事情。

下面让我来为你讲述一下这段故事。

二、  故事开始 : 让C++程序员写Java程序!

没有严重内存问题,大量丰富的SDK类库,超容易的跨平台,除了在性能上有些微辞,C++出身的程序员从来都不会觉得Java是一件很困难的事情。当然,对于长期习惯于使用操作系统API(系统调用System Call)的C/C++程序来说,面对Java中的比较“另类”地操作系统资源的方法可能会略感困惑,但万变不离其宗,只需要对面向对象的设计模式有一定的了解,用不了多长时间,Java的SDK类库也能玩得随心所欲。

在使用Java进行相关网络程序的的设计时,出身C/C++的人,首先想到的框架就是多路复用,想到多路复用,Unix/Linux下马上就能让从想到select, poll, epoll系统调用。于是,在看到Java的NIO中的Selector类时必然会倍感亲切。稍加查阅一下SDK手册以及相关例程,不一会儿,一个多路复用的框架便呈现出来,随手做个单元测试,没啥问题,一切和C/C++照旧。然后告诉兄弟们,框架搞定,以后咱们就在Windows上开发及单元测试,完成后到运行环境Unix上集成测试。心中并暗自念到,跨平台就好啊,开发活动都可以跨平台了。

然而,好景不长,随着代码越来越多,逻辑越来越复杂。好好的框架居然在Windows上单元测试运行开始出现异常,看着Java运行异常出错的函数栈,异常居然由Selector.open()抛出,错误信息居然是Unable to establish loopback connection。

“Selector.open()居然报loopback connection错误,凭什么?不应该啊?open的时候又没有什么loopback的socket连接,怎么会报这个错?”

长期使用C/C++的程序当然会对操作系统的调用非常熟悉,虽然Java的虚拟机搞的什么系统调用都不见了,但C/C++的程序员必然要比Java程序敏感许多。

三、  开始调查 : 怎么Java这么“傻”!

于是,C/C++的老鸟从SystemInternals上下载Process Explorer来查看一下究竟是什么个Loopback Connection。 果然,打开java运行进程,发现有一些自己连接自己的localhost的TCP/IP链接。于是另一个问题又出现了,

“凭什么啊?为什么会有自己和自己的连接?我程序里没有自己连接自己啊,怎么可能会有这样的链接啊?而自己连接自己的端口号居然是些奇怪的端口。”

问题变得越来越蹊跷了。难道这都是Selector.open()在做怪?难道Selector.open()要创建一个自己连接自己的链接?写个程序看看:

import java.nio.channels.Selector;
import java.lang.RuntimeException;
import java.lang.Thread;
public class TestSelector {
    private static final int MAXSIZE=5;
    public static final void main( String argc[] ) {
        Selector [] sels = new Selector[ MAXSIZE];

            try{
                for( int i = 0 ;i< MAXSIZE ;++i ) {
                    sels[i] = Selector.open();
                    //sels[i].close();
                }
                Thread.sleep(30000);
            }catch( Exception ex ){
                throw new RuntimeException( ex );
            }
    }
}

这个程序什么也没有,就是做5次Selector.open(),然后休息30秒,以便我使用Process Explorer工具来查看进程。程序编译没有问题,运行起来,在Process Explorer中看到下面的对话框:(居然有10个连接,从连接端口我们可以知道,互相连接, 如:第一个连第二个,第二个又连第一个)




不由得赞叹我们的Java啊,先不说这是不是一件愚蠢的事。至少可以肯定的是,Java在消耗宝贵的系统资源方面,已经可以赶的上某些蠕虫病毒了。

如果不信,不妨把上面程序中的那个MAXSIZE的值改成65535试试,不一会你就会发现你的程序有这样的错误了:(在我的XP机器上大约运行到2000个Selector.open() 左右)

Exception in thread "main" java.lang.RuntimeException: java.io.IOException: Unable to establish loopback connection
        at Test.main(Test.java:18)
Caused by: java.io.IOException: Unable to establish loopback connection
        at sun.nio.ch.PipeImpl$Initializer.run(Unknown Source)
        at java.security.AccessController.doPrivileged(Native Method)
        at sun.nio.ch.PipeImpl.<init>(Unknown Source)
        at sun.nio.ch.SelectorProviderImpl.openPipe(Unknown Source)
        at java.nio.channels.Pipe.open(Unknown Source)
        at sun.nio.ch.WindowsSelectorImpl.<init>(Unknown Source)
        at sun.nio.ch.WindowsSelectorProvider.openSelector(Unknown Source)
        at java.nio.channels.Selector.open(Unknown Source)
        at Test.main(Test.java:15)
Caused by: java.net.SocketException: No buffer space available (maximum connections reached?): connect
        at sun.nio.ch.Net.connect(Native Method)
        at sun.nio.ch.SocketChannelImpl.connect(Unknown Source)
        at java.nio.channels.SocketChannel.open(Unknown Source)
        ... 9 more


四、  继续调查 : 如此跨平台

当然,没人像我们这么变态写出那么多的Selector.open(),但这正好可以让我们来明白Java背着大家在干什么事。上面的那些“愚蠢连接”是在Windows平台上,如果不出意外,Unix/Linux下应该也差不多吧。

于是我们把上面的程序放在Linux下跑了跑。使用netstat 命令,并没有看到自己和自己的Socket连接。貌似在Linux上使用了和Windows不一样的机制?!

如果在Linux上不建自己和自己的TCP连接的话,那么文件描述符和端口都会被省下来了,是不是也就是说我们调用65535个Selector.open()的话,应该不会出现异常了。

可惜,在实现运行过程序当中,还是一样报错:(大约在400个Selector.open()左右,还不如Windows)

Exception in thread "main" java.lang.RuntimeException: java.io.IOException: Too many open files
        at Test1.main(Test1.java:19)
Caused by: java.io.IOException: Too many open files
        at sun.nio.ch.IOUtil.initPipe(Native Method)
        at sun.nio.ch.EPollSelectorImpl.<init>(EPollSelectorImpl.java:49)
        at sun.nio.ch.EPollSelectorProvider.openSelector(EPollSelectorProvider.java:18)
        at java.nio.channels.Selector.open(Selector.java:209)
        at Test1.main(Test1.java:15)

我们发现,这个异常错误是“Too many open files”,于是我想到了使用lsof命令来查看一下打开的文件。

看到了有一些pipe文件,一共5对,10个(当然,管道从来都是成对的)。如下图所示。



可见,Selector.open()在Linux下不用TCP连接,而是用pipe管道。看来,这个pipe管道也是自己给自己的。所以,我们可以得出下面的结论:

1)Windows下,Selector.open()会自己和自己建立两条TCP链接。不但消耗了两个TCP连接和端口,同时也消耗了文件描述符。
2)Linux下,Selector.open()会自己和自己建两条管道。同样消耗了两个系统的文件描述符。

估计,在Windows下,Sun的JVM之所以选择TCP连接,而不是Pipe,要么是因为性能的问题,要么是因为资源的问题。可能,Windows下的管道的性能要慢于TCP链接,也有可能是Windows下的管道所消耗的资源会比TCP链接多。这些实现的细节还有待于更为深层次的挖掘。

但我们至少可以了解,原来Java的Selector在不同平台上的机制。
2 楼 liudaoru 2010-06-17  
From:http://11lingxian.iteye.com/blog/236062

NIO的使用
导读
J2SE1.4以上版本中发布了全新的I/O类库。本文将通过一些实例来简单介绍NIO库提供的一些新特性:非阻塞I/O,字符转换,缓冲以及通道。
一. 介绍NIO
NIO包(java.nio.*)引入了四个关键的抽象数据类型,它们共同解决传统的I/O类中的一些问题。
1. Buffer:它是包含数据且用于读写的线形表结构。其中还提供了一个特殊类用于内存映射文件的I/O操作。
2. Charset:它提供Unicode字符串影射到字节序列以及逆影射的操作。
3. Channels:包含socket,file和pipe三种管道,它实际上是双向交流的通道。
4. Selector:它将多元异步I/O操作集中到一个或多个线程中(它可以被看成是Unix中select()函数或Win32中WaitForSingleEvent()函数的面向对象版本)。
二. 回顾传统
在介绍NIO之前,有必要了解传统的I/O操作的方式。以网络应用为例,传统方式需要监听一个ServerSocket,接受请求的连接为其提供服务(服务通常包括了处理请求并发送响应)图一是服务器的生命周期图,其中标有粗黑线条的部分表明会发生I/O阻塞。
 
   图一
可以分析创建服务器的每个具体步骤。
Java代码
//首先创建ServerSocket 
ServerSocket server=new ServerSocket(10000); 
//然后接受新的连接请求 
Socket newConnection=server.accept(); 
//对于accept方法的调用将造成阻塞,直到ServerSocket接受到一个连接请求为止。一旦连接请求被接受,//服务器可以读客户socket中的请求。 
InputStream in = newConnection.getInputStream(); 
InputStreamReader reader = new InputStreamReader(in); 
BufferedReader buffer = new BufferedReader(reader); 
Request request = new Request(); 
while(!request.isComplete()) { 
  String line = buffer.readLine(); 
  request.addLine(line); 



这 样的操作有两个问题,首先BufferedReader类的readLine()方法在其缓冲区未满时会造成线程阻塞,只有一定数据填满了缓冲区或者客户 关闭了套接字,方法才会返回。其次,它回产生大量的垃圾,BufferedReader创建了缓冲区来从客户套接字读入数据,但是同样创建了一些字符串存 储这些数据。虽然BufferedReader内部提供了StringBuffer处理这一问题,但是所有的String很快变成了垃圾需要回收。
同样的问题在发送响应代码中也存在
Java代码
Response response = request.generateResponse(); 
OutputStream out = newConnection.getOutputStream(); 
InputStream in = response.getInputStream(); 
int ch; 
while(-1 != (ch = in.read())) { 
  out.write(ch); 

newConnection.close(); 


类似的,读写操作被阻塞而且向流中一次写入一个字符会造成效率低下,所以应该使用缓冲区,但是一旦使用缓冲,流又会产生更多的垃圾。
传统的解决方法
通常在Java中处理阻塞I/O要用到线程(大量的线程)。一般是实现一个线程池用来处理请求,如图二
  
       图二
线程使得服务器可以处理多个连接,但是它们也同样引发了许多问题。每个线程拥有自己的栈空间并且占用一些CPU时间,耗费很大,而且很多时间是浪费在阻塞的I/O操作上,没有有效的利用CPU。
三. 新I/O
1. Buffer
传统的I/O不断的浪费对象资源(通常是String)。新I/O通过使用Buffer读写数据避免了资源浪费。Buffer对象是线性的,有序的数据集合,它根据其类别只包含唯一的数据类型。
Java代码
java.nio.Buffer //类描述  
java.nio.ByteBuffer //包含字节类型。 可以从ReadableByteChannel中读在    WritableByteChannel中写  
java.nio.MappedByteBuffer //包含字节类型,直接在内存某一区域映射  
java.nio.CharBuffer //包含字符类型,不能写入通道  
java.nio.DoubleBuffer //包含double类型,不能写入通道  
java.nio.FloatBuffer //包含float类型  
java.nio.IntBuffer //包含int类型  
java.nio.LongBuffer //包含long类型  
java.nio.ShortBuffer //包含short类型  


可 以通过调用allocate(int capacity)方法或者allocateDirect(int capacity)方法分配一个Buffer。特别的,你可以创建MappedBytesBuffer通过调用FileChannel.map(int mode,long position,int size)。直接(direct)buffer在内存中分配一段连续的块并使用本地访问方法读写数据。非直接(nondirect)buffer通过使用 Java中的数组访问代码读写数据。有时候必须使用非直接缓冲例如使用任何的wrap方法(如ByteBuffer.wrap(byte[]))在 Java数组基础上创建buffer。
2. 字符编码
向ByteBuffer中存放数据涉及到两个问题:字节的顺序和字符转换。ByteBuffer内部通过ByteOrder类处理了字节顺序问题,但是并没有处理字符转换。事实上,ByteBuffer没有提供方法读写String。
Java.nio.charset.Charset处理了字符转换问题。它通过构造CharsetEncoder和CharsetDecoder将字符序列转换成字节和逆转换。
3. 通道(Channel)
你可能注意到现有的java.io类中没有一个能够读写Buffer类型,所以NIO中提供了Channel类来读写Buffer。通道可以认为是一种连接,可以是到特定设备,程序或者是网络的连接。通道的类等级结构图如下
 
    图三
图中ReadableByteChannel和WritableByteChannel分别用于读写。
GatheringByteChannel可以从使用一次将多个Buffer中的数据写入通道,相反的,ScatteringByteChannel则可以一次将数据从通道读入多个Buffer中。你还可以设置通道使其为阻塞或非阻塞I/O操作服务。
为了使通道能够同传统I/O类相容,Channel类提供了静态方法创建Stream或Reader
4. Selector
在 过去的阻塞I/O中,我们一般知道什么时候可以向stream中读或写,因为方法调用直到stream准备好时返回。但是使用非阻塞通道,我们需要一些方 法来知道什么时候通道准备好了。在NIO包中,设计Selector就是为了这个目的。SelectableChannel可以注册特定的事件,而不是在 事件发生时通知应用,通道跟踪事件。然后,当应用调用Selector上的任意一个selection方法时,它查看注册了的通道看是否有任何感兴趣的事 件发生。图四是selector和两个已注册的通道的例子
 
        图四
并不是所有的通道都支持所有的操作。SelectionKey类定义了所有可能的操作位,将要用两次。首先,当应用调 用SelectableChannel.register(Selector sel,int op)方法注册通道时,它将所需操作作为第二个参数传递到方法中。然后,一旦SelectionKey被选中了,SelectionKey的 readyOps()方法返回所有通道支持操作的数位的和。SelectableChannel的validOps方法返回每个通道允许的操作。注册通道 不支持的操作将引发IllegalArgumentException异常。下表列出了SelectableChannel子类所支持的操作。
Java代码
ServerSocketChannel OP_ACCEPT  
SocketChannel OP_CONNECT, OP_READ, OP_WRITE  
DatagramChannel OP_READ, OP_WRITE  
Pipe.SourceChannel OP_READ  
Pipe.SinkChannel OP_WRITE  


四. 举例说明
1. 简单网页内容下载
这个例子非常简单,类SocketChannelReader使用SocketChannel来下载特定网页的HTML内容。
Java代码
package examples.nio; 
 
import java.nio.ByteBuffer; 
import java.nio.channels.SocketChannel; 
import java.nio.charset.Charset; 
import java.net.InetSocketAddress; 
import java.io.IOException; 
 
public class SocketChannelReader{ 
    
    private Charset charset=Charset.forName("UTF-8");//创建UTF-8字符集 
    private SocketChannel channel; 
 
    public void getHTMLContent(){ 
    try{ 
       connect(); 
       sendRequest(); 
       readResponse(); 
    }catch(IOException e){ 
       System.err.println(e.toString()); 
    }finally{ 
     if(channel!=null){ 
     try{ 
      channel.close(); 
  }catch(IOException e){} 
     } 

    } 
    private void connect()throws IOException{//连接到CSDN 
InetSocketAddress socketAddress= 
     new InetSocketAddress("www.csdn.net",80); 
channel=SocketChannel.open(socketAddress); 
//使用工厂方法open创建一个channel并将它连接到指定地址上 
//相当与SocketChannel.open().connect(socketAddress);调用 

 
private void sendRequest()throws IOException{ 
channel.write(charset.encode("GET " 
        +"/document" 
        +"\r\n\r\n"));//发送GET请求到CSDN的文档中心 
//使用channel.write方法,它需要CharByte类型的参数,使用 
//Charset.encode(String)方法转换字符串。 
    } 
 
    private void readResponse()throws IOException{//读取应答 
ByteBuffer buffer=ByteBuffer.allocate(1024);//创建1024字节的缓冲 
while(channel.read(buffer)!=-1){ 
     buffer.flip();//flip方法在读缓冲区字节操作之前调用。 
     System.out.println(charset.decode(buffer)); 
//使用Charset.decode方法将字节转换为字符串 
     buffer.clear();//清空缓冲 

    } 
 
    public static void main(String [] args){ 
new SocketChannelReader().getHTMLContent(); 
    } 


2. 简单的加法服务器和客户机
服务器代码
Java代码
package examples.nio; 
 
import java.nio.ByteBuffer; 
import java.nio.IntBuffer; 
import java.nio.channels.ServerSocketChannel; 
import java.nio.channels.SocketChannel; 
import java.net.InetSocketAddress; 
import java.io.IOException; 
 
/**
* SumServer.java
*
*
* Created: Thu Nov 06 11:41:52 2003
*
* @author starchu1981
* @version 1.0
*/ 
public class SumServer { 
 
    private ByteBuffer _buffer=ByteBuffer.allocate(8); 
    private IntBuffer _intBuffer=_buffer.asIntBuffer(); 
    private SocketChannel _clientChannel=null; 
    private ServerSocketChannel _serverChannel=null; 
 
    public void start(){ 
try{ 
     openChannel(); 
     waitForConnection(); 
}catch(IOException e){ 
     System.err.println(e.toString()); 

    } 
 
    private void openChannel()throws IOException{ 
_serverChannel=ServerSocketChannel.open(); 
_serverChannel.socket().bind(new InetSocketAddress(10000)); 
System.out.println("服务器通道已经打开"); 
    } 
 
    private void waitForConnection()throws IOException{ 
while(true){ 
     _clientChannel=_serverChannel.accept(); 
     if(_clientChannel!=null){ 
System.out.println("新的连接加入"); 
processRequest(); 
_clientChannel.close(); 
     } 

    } 
 
    private void processRequest()throws IOException{ 
_buffer.clear(); 
_clientChannel.read(_buffer); 
int result=_intBuffer.get(0)+_intBuffer.get(1); 
_buffer.flip(); 
_buffer.clear(); 
_intBuffer.put(0,result); 
_clientChannel.write(_buffer); 
    } 
 
    public static void main(String [] args){ 
new SumServer().start(); 
    } 
} // SumServer 
客户代码 
package examples.nio; 
 
import java.nio.ByteBuffer; 
import java.nio.IntBuffer; 
import java.nio.channels.SocketChannel; 
import java.net.InetSocketAddress; 
import java.io.IOException; 
 
/**
* SumClient.java
*
*
* Created: Thu Nov 06 11:26:06 2003
*
* @author starchu1981
* @version 1.0
*/ 
public class SumClient { 
 
    private ByteBuffer _buffer=ByteBuffer.allocate(8); 
    private IntBuffer _intBuffer; 
    private SocketChannel _channel; 
 
    public SumClient() { 
      _intBuffer=_buffer.asIntBuffer(); 
    } // SumClient constructor 
    
    public int getSum(int first,int second){ 
int result=0; 
try{ 
     _channel=connect(); 
     sendSumRequest(first,second); 
     result=receiveResponse(); 
}catch(IOException e){System.err.println(e.toString()); 
}finally{ 
     if(_channel!=null){ 
  try{ 
      _channel.close(); 
  }catch(IOException e){} 
     } 

return result; 
    } 
 
    private SocketChannel connect()throws IOException{ 
InetSocketAddress socketAddress= 
     new InetSocketAddress("localhost",10000); 
return SocketChannel.open(socketAddress); 
    } 
    
    private void sendSumRequest(int first,int second)throws IOException{ 
_buffer.clear(); 
_intBuffer.put(0,first); 
_intBuffer.put(1,second); 
_channel.write(_buffer); 
System.out.println("发送加法请求 "+first+"+"+second); 
    } 
    
    private int receiveResponse()throws IOException{ 
_buffer.clear(); 
_channel.read(_buffer); 
return _intBuffer.get(0); 
    } 
 
    public static void main(String [] args){ 
SumClient sumClient=new SumClient(); 
System.out.println("加法结果为 :"+sumClient.getSum(100,324)); 
    } 
} // SumClient 


3. 非阻塞的加法服务器
首先在openChannel方法中加入语句
_serverChannel.configureBlocking(false);//设置成为非阻塞模式
重写WaitForConnection方法的代码如下,使用非阻塞方式
Java代码
private void waitForConnection()throws IOException{ 
Selector acceptSelector = SelectorProvider.provider().openSelector();  
 
/*在服务器套接字上注册selector并设置为接受accept方法的通知。
这就告诉Selector,套接字想要在accept操作发生时被放在ready表
上,因此,允许多元非阻塞I/O发生。*/ 
SelectionKey acceptKey = ssc.register(acceptSelector, 
           SelectionKey.OP_ACCEPT); 
int keysAdded = 0; 
  
/*select方法在任何上面注册了的操作发生时返回*/ 
while ((keysAdded = acceptSelector.select()) > 0) { 
     // 某客户已经准备好可以进行I/O操作了,获取其ready键集合 
     Set readyKeys = acceptSelector.selectedKeys(); 
     Iterator i = readyKeys.iterator(); 
 
     // 遍历ready键集合,并处理加法请求 
     while (i.hasNext()) { 
  SelectionKey sk = (SelectionKey)i.next(); 
  i.remove(); 
  ServerSocketChannel nextReady = 
      (ServerSocketChannel)sk.channel(); 
  // 接受加法请求并处理它 
  _clientSocket = nextReady.accept().socket(); 
   processRequest(); 
   _clientSocket.close(); 
     } 
  } 
    } 


参考资料
1. <Master Merlin's new I/O classes>   From <http://www.javawordl.com/ >
2. J2SE1.4.2 API Specification From <http://java.sun.com/ >
3. <Working with SocketChannels> From <http://developer.java.sun.com/developer >
4.   NIO Examples From <http://java.sun.com/ >
http://java.sun.com/javase/6/docs/technotes/guides/io/example/index.html
1 楼 liudaoru 2010-05-21  
Java高级编程-网络编程祥解 (4)

From: http://tech.ccidnet.com/art/3737/20060824/879781_1.html

使用Java NIO提高服务端程序的性能

    在前面的章节里,我们讨论了Java NIO的基本概念,在这一节里,我们将结合具体的Java Socket编程,讨论使用NIO提高服务端程序的性能的问题。

    Java NIO增加了新的SocketChannel、ServerSocketChannel等类来提供对构建高性能的服务端程序的支持。 SocketChannel、ServerSocketChannel能够在非阻塞的模式下工作,它们都是selectable的类。在构建服务器或者中间件时,推荐使用Java NIO。

    在传统的网络编程中,我们通常使用一个专用线程(Thread)来处理一个Socket连接,通过使用NIO,一个或者很少几个Socket线程就可以处理成千上万个活动的Socket连接。

    通常情况下,通过ServerSocketChannel.open()获得一个ServerSocketChannel的实例,通过SocketChannel.open或者serverSocketChannel.accept()获得一个SocketChannel实例。要使ServerSocketChannel或者SocketChannel在非阻塞的模式下操作,可以调用
    serverSocketChannel.configureBlocking (false);
    或者
    socketChannel.configureBlocking (false);

    语句来达到目的。通常情况下,服务端可以使用非阻塞的ServerSocketChannel,这样,服务端的程序就可以更容易地同时处理多个socket线程。

    下面我们来看一个综合例子,这个例子使用了ServerSocketChannel、SocketChannel开发了一个非阻塞的、能处理多线程的Echo服务端程序,见示例12-14。
    【程序源代码】


1 // ==================== Program Discription =====================
2 // 程序名称:示例12-14 : SocketChannelDemo.java
3 // 程序目的:学习Java NIO#SocketChannel
4 // ==============================================================
5
6
7 import java.nio.ByteBuffer;
8 import java.nio.channels.ServerSocketChannel;
9 import java.nio.channels.SocketChannel;
10 import java.nio.channels.Selector;
11 import java.nio.channels.SelectionKey;
12 import java.nio.channels.SelectableChannel;
13
14 import java.net.Socket;
15 import java.net.ServerSocket;
16 import java.net.InetSocketAddress;
17 import java.util.Iterator;
18
19 public class SocketChannelDemo

20
21 {
22 public static int PORT_NUMBER = 23;//监听端口
23 ServerSocketChannel serverChannel;
24 ServerSocket serverSocket ;
25 Selector selector ;
26 private ByteBuffer buffer = ByteBuffer.allocateDirect (1024);
27
28 public static void main (String [] args)
29 throws Exception
30 {
31 SocketChannelDemo server=new SocketChannelDemo();
32 server.init(args);
33 server.startWork();
34 }
35
36
37 public void init (String [] argv)throws Exception
38 {
39 int port = PORT_NUMBER;
40
41 if (argv.length > 0) {
42 port = Integer.parseInt (argv [0]);
43 }
44
45 System.out.println ("Listening on port " + port);
46
47 // 分配一个ServerSocketChannel
48 serverChannel = ServerSocketChannel.open();
49 // 从ServerSocketChannel里获得一个对应的Socket
50 serverSocket = serverChannel.socket();
51 // 生成一个Selector
52 selector = Selector.open();
53
54 // 把Socket绑定到端口上
55 serverSocket.bind (new InetSocketAddress (port));
56 //serverChannel为非bolck
57 serverChannel.configureBlocking (false);
58
59 // 通过Selector注册ServerSocetChannel
60 serverChannel.register (selector, SelectionKey.OP_ACCEPT);
61
62 }
63
64     public void startWork()throws Exception

65
66     {
67     while (true) {
68
69 int n = selector.select();//获得IO准备就绪的channel数量
70
71 if (n == 0) {
72 continue; // 没有channel准备就绪,继续执行
73 }
74
75 // 用一个iterator返回Selector的selectedkeys
76 Iterator it = selector.selectedKeys().iterator();
77
78 // 处理每一个SelectionKey
79 while (it.hasNext()) {
80 SelectionKey key = (SelectionKey) it.next();
81
82 // 判断是否有新的连接到达
83 if (key.isAcceptable()) {
84           //返回SelectionKey的ServerSocketChannel
85 ServerSocketChannel server =
(ServerSocketChannel) key.channel();
86 SocketChannel channel = server.accept();
87
88 registerChannel (selector, channel,
89 SelectionKey.OP_READ);
90
91 doWork (channel);
92 }
93
94 // 判断是否有数据在此channel里需要读取
95 if (key.isReadable()) {
96   
97 processData (key);
98
99 }
100
101 //删除 selectedkeys
102 it.remove();
103 }
104 }
105 }
106 protected void registerChannel (Selector selector,
107 SelectableChannel channel, int ops)
108 throws Exception
109 {

110 if (channel == null) {
111 return;
112 }
113
114
115 channel.configureBlocking (false);
116
117 channel.register (selector, ops);
118 }
119
120     //处理接收的数据
121 protected void processData (SelectionKey key)
122 throws Exception
123 {
124
125
126 SocketChannel socketChannel = (SocketChannel) key.channel();
127 int count;
128
129 buffer.clear(); // 清空buffer
130
131 // 读取所有的数据
132 while ((count = socketChannel.read (buffer)) > 0) {
133 buffer.flip();
134
135 // send the data, don′t assume it goes all at once
136 while (buffer.hasRemaining())
137 {
138 //如果收到回车键,则在返回的字符前增加[echo]$字样
139 if(buffer.get()==(char)13)
140 {
141 buffer.clear();
142 buffer.put("[echo]___FCKpd___0quot;.getBytes());
143 buffer.flip();
144
145 }
146 socketChannel.write (buffer);//在Socket里写数据
147 }
148
149 buffer.clear(); // 清空buffer
150 }
151
152 if (count < 0) {
153 // count<0,说明已经读取完毕
154 socketChannel.close();

155 }
156 }
157
158
159 private void doWork (SocketChannel channel)throws Exception
160 {
161 buffer.clear();
162 buffer.put ("
Hello,I am working,please input some thing,and i will echo to you!
[echo]
___FCKpd___0quot;.getBytes());
163 buffer.flip();
164 channel.write (buffer);
165 }
166
167 }


    使用:运行此程序,然后在控制台输入命令telnet localhost 23。

    【程序输出结果】如图12-1所示。






图12-1 输出结果


    【程序注解】
    关于程序的解释已经包含在程序里面了,在这里我们总结以下使用ServerSocket Channel开发服务端程序的过程:
    (1)分配一个ServerSocketChannel。
    (2)从ServerSocketChannel里获得一个对应的ServerSocket。
    (3)生成一个Selector实例。
    (4)把ServerSocket绑定到端口上。
    (5)设置ServerSocketChannel为非block模式(可选)。
    (6)在Selector里注册ServerSocetChannel。
    (7)用一个无限循环语句始终查看Selector里是否有IO准备就绪的channel。如果有,就执行对应的处理,如果没有,继续循环。

     小 结

    在本章我们主要介绍了Java中的网络编程。Java一开始就是一种网络编程语言,到后来才应用到各个方面,所以在Java中进行网络编程远比在C/C++中方便。

    我们介绍了几个在网络编程中很重要的类,如InetAddress、URL、URLConnection、Socket、 ServerSocket、DatagramSocket、DatagramPacket、MulticastSocket等。这些类包含了进行基本网络编程的所有内容。要熟练地应用这些类,关键还是要多多练习。

    基于套接字的编程基本上是客户/服务器模式,我们具体介绍了编写这种模式的步骤。在实例方面,我们给出了一个基于TCP的套接字客户/服务器程序,与此相对应,还给出了基于UDP的客户/服务器程序。两者的模式是很相似的,其实这也就是编写客户/服务器程序的一般模式。

相关推荐

Global site tag (gtag.js) - Google Analytics