- 浏览: 1576157 次
- 性别:
- 来自: 北京
文章分类
最新评论
-
nich002:
原网站失效了。撸主简单粗暴的复制过来,可读性极差!差评!
Apache配置详解(最好的APACHE配置教程) -
107x:
不错,谢谢!
LINUX下查看文件夹下的文件个数! -
Hypereo:
好你妹,连个格式都没有!
Apache配置详解(最好的APACHE配置教程) -
resteater:
代码排版感觉有点乱!收发信息代码可读性不强!请问第一次发服务器 ...
java socket例子 -
resteater:
代码排版感觉有点乱!收发信息代码可读性不强!请问第一次发服务器 ...
java socket例子
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
在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
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在不同平台上的机制。
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
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的客户/服务器程序。两者的模式是很相似的,其实这也就是编写客户/服务器程序的一般模式。
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的客户/服务器程序。两者的模式是很相似的,其实这也就是编写客户/服务器程序的一般模式。
发表评论
-
JVM问题追查与调优
2012-03-27 14:44 1144JDK的几种分析工具 http://liudaoru ... -
NodeJs相关资料
2011-08-18 14:55 3010NodeJs获取参数: proces ... -
jprofiler追踪问题
2011-08-12 18:20 1048Jprofiler下载页: http://www.ej ... -
Linux服务器性能评估与优化【z】
2011-07-01 10:05 1549来自:http://www.itlearner.com/ ... -
Java 理论与实践: 非阻塞算法简介【z】
2011-03-26 20:39 1284From: http://www.ibm.com/develo ... -
Java Crash问题分析[z]
2011-03-23 14:41 5973参考: http://www.ibm.com/develop ... -
Berkeley DB相关
2010-09-25 22:17 1054为什么要使用Berkeley DB,它适合什么场合应用?Ber ... -
熟悉系统方法总结
2010-07-06 14:26 816了解一个陌生的系统是我们经常碰到的事情,下面总结一下自己的一些 ... -
Java缓存框架 EhCache
2010-07-06 14:09 4728From: http://www.oschina.net/p/ ... -
Memcached命令行管理
2010-03-15 11:18 4488From: http://www.exp2up.com/2 ... -
(转)Resin服务器配置指南
2010-01-21 15:35 3463From:http://blog.21cn.com/super ... -
Flickr架构
2010-01-11 09:52 1267From: http://www.cyask.com/ques ... -
JDK的几种分析工具
2009-12-04 12:13 10903From: http://blog.csdn.net/hant ... -
XMemcached——一个新的开源Java memcached客户端
2009-10-23 09:27 1893From: http://www.infoq.com/cn/ ... -
多线程任务调度学习
2009-10-16 13:58 2302昨天找到一套多线程任务调度的代码,相当的不错,先把思路总结一下 ... -
用HSCALE实现MySQL的数据分布式存储
2009-10-15 12:47 3017From:http://www.ningoo.net/ht ... -
马化腾:搜索、电子商务硬仗一定要坚持打
2009-10-15 12:09 1717From:http://www.techweb.com.c ... -
MySQL分表实现上百万上千万记录分布存储的批量查询设计模式【z】
2009-10-15 09:56 3172From:http://hi.baidu.com/jabber ... -
nginx负载均衡和lvs负载均衡的比较分析【z】
2009-10-13 20:02 1472From:http://www.shouker.com/u ... -
新型的大型bbs架构(squid+nginx)【z】
2009-10-13 19:53 1623From:http://www.fovweb.com/opti ...
相关推荐
总的来说,本教程将围绕ServerSocketChannel和SocketChannel展开,讲解如何在Java NIO框架下创建服务器并接受客户端连接,以及如何通过非阻塞的方式进行数据交换,这对于我们理解和掌握Java NIO编程至关重要。...
本篇将基于给定的标题“采用NIO实现一个Socket服务器”来详细阐述如何使用NIO进行Socket服务端的开发。 首先,理解NIO的核心概念: 1. **通道(Channel)**:通道是连接到I/O设备的途径,可以读取或写入数据。常见的...
在这个场景下,Java NIO能够帮助服务器有效地处理多个客户端连接,并实现客户端之间消息的互发。 首先,我们要理解Java NIO的基本组件。主要包括通道(Channels)、缓冲区(Buffers)和选择器(Selectors)。通道是...
2. 使用NioSocket实现HTTP服务器的步骤:注册ServerSocketChannel到Selector、等待客户端的请求、处理请求 3. HttpHandler的实现:多线程的实现、处理来自HttpServer的不加工信息 4. 使用Selector的select()方法来...
### 使用Java NIO编写高性能服务器的关键知识点 #### 一、NIO概述 - **NIO简介**:NIO(New IO)自JDK 1.4引入以来,为Java提供了非阻塞IO的支持,这对于提高服务端应用的性能至关重要。NIO的核心特性包括缓冲区...
Java NIO(New IO)是Java 1.4版本引入的一个新特性,它为Java程序提供了非阻塞I/O操作的能力,极大地提升了Java在处理网络通信和文件...通过合理设计和使用NIO,开发者可以构建出能够处理大规模并发连接的高效服务器。
总结一下,NIO服务器/客户端模式通过通道、缓冲区和选择器实现了非阻塞的I/O通信,有效提升了服务器处理并发连接的能力。理解并熟练掌握这些概念和实践,是成为一名优秀的Java网络编程开发者的关键。在实际开发中,...
NIO的核心在于反应器模式,它允许服务器使用单个或者少量线程处理多个客户端连接。当客户端发起连接或数据传输时,服务器通过Selector(选择器)监控多个通道,一旦有事件发生,如连接请求、数据到达等,Selector会...
在探讨如何使用Java NIO实现Socket通信之前,我们需要先理解NIO(Non-blocking I/O,非阻塞I/O)与传统阻塞I/O之间的区别。 **传统阻塞I/O模型**:在传统的Java IO编程中,当我们调用`read()`或`write()`方法时,...
标题中的“java nio服务器”指的是使用Java NIO API构建的网络服务器,这种服务器设计模式通常被称为多路复用或非阻塞I/O服务器。NIO的核心组件包括通道(Channels)、缓冲区(Buffers)和选择器(Selectors)。下面...
在"Using NIO to copy Java file fast"的例子中,开发者可能使用了以下步骤来快速复制文件: 1. 打开源文件和目标文件的FileChannel。 2. 创建一个ByteBuffer作为数据传输的中介。 3. 使用FileChannel的read()方法...
用java编写的nio通信的例子,nio是io编程的新版本,比io较流行。同时本例子是适用socket通信的。可以在此基础上,添加您的个人应用。本例子适用于:java通信的学习者,android平台通信的学习者。
关键类包括:`java.nio.channels.Selector`、`java.nio.channels.ServerSocketChannel`、`java.nio.channels.SocketChannel`,以及缓冲区类如`ByteBuffer`。 **3. 埼于Java的AIO(Asynchronous I/O)** AIO,也...
标题中的“NIO实现telnet”指的是使用Java的非阻塞I/O(New Input/Output,简称NIO)来构建一个telnet服务器。在Java中,NIO是一种替代传统阻塞I/O(BIO)的I/O模型,它提供了更高效的数据传输方式,尤其适合处理...
在`DownloadServer.java`这个文件中,我们可以预见到它实现了一个使用Selector的服务器端程序。可能包含了以下部分: 1. 初始化ServerSocketChannel和Selector。 2. 绑定服务器的端口,并将ServerSocketChannel设置...
这个简单的例子展示了如何使用Java NIO实现服务器和客户端的基本通信流程,但实际应用中可能还需要处理异常、数据解析、心跳检测等复杂逻辑。理解NIO的核心概念和API,对于构建高效、可扩展的网络应用至关重要。
java基于NIO实现Reactor模型源码java基于NIO实现Reactor模型源码java基于NIO实现Reactor模型源码java基于NIO实现Reactor模型源码java基于NIO实现Reactor模型源码java基于NIO实现Reactor模型源码java基于NIO实现...
本资源主要讲解了Java-NIO非阻塞服务器的示例,通过使用Java-NIO包来实现非阻塞的服务器端模式。下面是从代码中提取的知识点: 一、Java-NIO包简介 Java-NIO(New I/O)包是Java 1.4版本中引入的新IO处理机制,...
使用NIO设计并发型服务器程序的过程主要包括以下几个步骤: 1. **初始化Selector**:创建一个Selector实例作为监控通道的状态变化的控制器。 2. **设置ServerSocketChannel**:配置 `ServerSocketChannel` 为非阻塞...