由上篇文章中知道,通道Channel和緩沖器Buffer兩者需要共同作用,但是選擇器Selector只會(huì)作用在繼承了抽象類SelectableChannel的網(wǎng)絡(luò)IO中,下面由簡(jiǎn)單的FileChannel開始了解nio包的使用。
FileChannel
之前已經(jīng)說過,在java 1.4的時(shí)候,改寫了傳統(tǒng)IO包下的三個(gè)類用以生成通道類FileChannel,這里使用緩沖器類ByteBuffer來對(duì)文件進(jìn)行操作,如下所示:
/**
**FileChannel示例 文件拷貝
**/
public static void main(String[] arg0) throws IOException{
FileChannel inputChanel = new FileInputStream("/test/test.text").getChannel();
FileChannel outputChanel = new FileOutputStream("/test/test1.text").getChannel();
ByteBuffer buf = ByteBuffer.allocate(1024);
int len = 0;
while((len = inputChanel.read(buf))!= -1){
buf.flip();
outputChanel.write(buf);
buf.clear();
}
}
如上所示,代碼會(huì)將test.text中的數(shù)據(jù)拷貝到test1.text中,其中將通道中的數(shù)據(jù)讀取到ByteBuffer后,經(jīng)過三個(gè)步驟,即flip()方法調(diào)用,調(diào)用寫通道outputChannel的write()方法,最后調(diào)用ByteBuffer的clear()方法。雖說完成了功能,但是這種方法的效率并不是很高的,因?yàn)槊恳淮巫x的時(shí)候都需要在while循環(huán)中調(diào)用了幾步。在FileChannel類中還有兩個(gè)方法transferFrom(ReadableByteChannel src,long position, long count)和transferTo(long position, long count,WritableByteChannel target),可以通過這兩個(gè)方法來做到文件拷貝,如下所示:
/**
**FileChannel示例 文件拷貝 transferTo() & transferFrom()
**/
public static void main(String[] arg0) throws IOException{
FileChannel inputChanel = new FileInputStream("/test/nio1.text").getChannel();
FileChannel outputChanel = new FileOutputStream("/test/test.text").getChannel();
// inputChanel.transferTo(0, inputChanel.size(), outputChanel);
outputChanel.transferFrom(inputChanel, 0, inputChanel.size());
}
上面的例子運(yùn)行完之后,通過將兩個(gè)通道連接也能正確的拷貝文件。需要注意的是文中的代碼為了簡(jiǎn)便全部是直接拋出異常也沒有關(guān)閉流,如果實(shí)際書寫請(qǐng)酌情處理。看了FileChannel操作普通文件,那么可以看一下怎么操作大文件的,記得之前說過這樣一個(gè)類MappedByteBuffer,使用它即可快速操作,如下:
/**
**MappedByteBuffer示例 操作文件
**/
public static void main(String[] arg0) throws IOException{
FileChannel randomAccessChannel = new RandomAccessFile("/test/nio1.text", "rw").getChannel();
FileChannel outputChannel = new FileOutputStream("/test/test.text").getChannel();
long size = randomAccessChannel.size();
MappedByteBuffer mapBuffer = randomAccessChannel.map(MapMode.READ_ONLY, 0, size);
int len = 1024;
byte[] buf = new byte[len];
long cycle = size/len;
while(mapBuffer.hasRemaining()&&cycle>=0){
if(cycle==0){
len = (int)size % len;
}
mapBuffer.get(buf,0,len);
System.out.println("--"+new String(buf,0,len));
cycle--;
}
}
如上通過類FileChannel映射文件,會(huì)打印出文件中的所有數(shù)據(jù)。在這里對(duì)于速度的提升可能無法看出,但是換一個(gè)大文件對(duì)比一下普通的ByteBuffer就可以很容易的看出來,如下所示:
/**
**MappedByteBuffer對(duì)比普通ByteBuffer
**/
public static void main(String[] arg0) throws IOException{
FileChannel randomAccessChannel = new RandomAccessFile("/test/cpicgxwx.war", "r").getChannel();
long size = randomAccessChannel.size();
long cur = System.currentTimeMillis();
MappedByteBuffer mapBuffer = randomAccessChannel.map(MapMode.READ_ONLY, 0, size);
System.out.println("MappedByteBuffer spend "+(System.currentTimeMillis()-cur));
ByteBuffer buf = ByteBuffer.allocate(1024*1024*150);
cur = System.currentTimeMillis();
randomAccessChannel.read(buf);
System.out.println("ByteBuffer spend "+(System.currentTimeMillis()-cur));
}
輸出:
MappedByteBuffer spend 4
ByteBuffer spend 259
這樣很明顯就看的出來兩者的效率,對(duì)于一個(gè)大于100M的文件,MappedByteBuffer比ByteBuffer效率高了60多倍,更不用說幾個(gè)G大小的文件了。
這里會(huì)不會(huì)有疑惑,為什么類MappedByteBuffer可以這么快?其實(shí)是因?yàn)樗⒉皇亲x取文件到內(nèi)存中而是像類名Mapped一樣映射文件。當(dāng)然這個(gè)類其實(shí)也存在一些問題,如內(nèi)存占用和文件關(guān)閉,被類MappedByteBuffer打開的文件只有在垃圾收集的時(shí)候才關(guān)閉,而這個(gè)垃圾收集的點(diǎn)是不確定的,在網(wǎng)上有人提供了這樣一個(gè)方法來關(guān)閉這個(gè)文件映射,如下所示:
/**
**MappedByteBuffer 文件映射 Clear()
**/
public static void main(String[] arg0) throws Exception{
File file = new File("/test/nio.text");
RandomAccessFile randomAccessFile = new RandomAccessFile(file,"r");
FileChannel channel = randomAccessFile.getChannel();
long size = channel.size();
long cur = System.currentTimeMillis();
MappedByteBuffer mapBuffer = channel.map(MapMode.READ_ONLY, 0, size);
System.out.println("MappedByteBuffer spend "+(System.currentTimeMillis()-cur));
ByteBuffer buf = ByteBuffer.allocate(1024*1024*150);
cur = System.currentTimeMillis();
channel.read(buf);
System.out.println("ByteBuffer spend "+(System.currentTimeMillis()-cur));
channel.close();
randomAccessFile.close();
// clean(mapBuffer);
System.out.println(file.delete());
}
public static void clean(final Object buffer) throws Exception {
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
try {
Method getCleanerMethod = buffer.getClass().getMethod("cleaner",new Class[0]);
getCleanerMethod.setAccessible(true);
sun.misc.Cleaner cleaner =(sun.misc.Cleaner)getCleanerMethod.invoke(buffer,new Object[0]);
cleaner.clean();
} catch(Exception e) {
e.printStackTrace();
}
return null;}});
}
輸出:
MappedByteBuffer spend 0
ByteBuffer spend 157
false
如上所示:在注銷掉clean(mapBuffer);時(shí),由于系統(tǒng)還持有這個(gè)文件的句柄,無法刪除文件,導(dǎo)致打印出false,加上這個(gè)方法調(diào)用后就可以了。除了反射調(diào)用sun.misc.Cleaner cleaner外,還可以直接調(diào)用,如下:
/**
**sun.misc.Cleaner調(diào)用
**/
public static void unmap(MappedByteBuffer buffer){
sun.misc.Cleaner cleaner = ((DirectBuffer) buffer).cleaner();
cleaner.clean();
}
這樣調(diào)用也是可以刪除文件的,這樣就補(bǔ)足了上面所說的問題。
在java 1.4還加入了文件加鎖機(jī)制FileLock,它允許我們同步訪問某個(gè)作為共享資源的文件,這個(gè)文件鎖直接通過映射本地操作系統(tǒng)的加鎖工具,所以對(duì)其他操作系統(tǒng)的進(jìn)程也是可見的。這個(gè)文件鎖可以通過FileChannel的tryLock()或lock()方法獲取,當(dāng)然也提供了有參數(shù)的方法來加鎖文件的一部分,如lock(long position, long size, boolean shared)和tryLock(long position, long size, boolean shared),這里參數(shù)boolean shared指是否共享鎖。這個(gè)就不放實(shí)例了,感興趣可以試試。
下面看看其他的網(wǎng)絡(luò)Channel。
SocketChannel
在上一篇就說道NIO的強(qiáng)大功能部分來自它的非阻塞特性,這一點(diǎn)在網(wǎng)絡(luò)IO中效果表現(xiàn)的更加明顯,如對(duì)ServerSocket的accept()方法會(huì)等待某一個(gè)客戶端連接而導(dǎo)致阻塞,或者InputStream的read方法阻塞到數(shù)據(jù)完全讀完。一般來說,我們?cè)谡{(diào)用一個(gè)方法之前并不知道它是不是會(huì)阻塞,但是NIO提供了這樣的方法來配置它的阻塞行為,以實(shí)現(xiàn)非阻塞信道。
/**
**nio非阻塞信道配置
**/
channel.configureBlocking(false);
非阻塞信道的優(yōu)勢(shì)在于它調(diào)用的方法都會(huì)有一個(gè)即時(shí)返回,用來指示所請(qǐng)求的操作的完成程度。下面用一個(gè)例子來演示,客戶端使用NIO非阻塞信道,服務(wù)器使用IO實(shí)現(xiàn),如下:
/**
**SocketChannel示例
**/
public static void main(String[] arg0) throws InterruptedException{
new Thread(){
public void run() {
server();
}
}.start();
new Thread(){
public void run() {
try {
client();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}.start();
}
public static void client() throws InterruptedException{
SocketChannel client = null;
ByteBuffer buf = ByteBuffer.allocate(1024);
try {
client = SocketChannel.open();
client.configureBlocking(false);
client.connect(new InetSocketAddress("192.168.191.5",8080));
if(client.finishConnect()){
int i = 0;
while(true){
Thread.sleep(3000);
String test = "test "+i+" from client";
i++;
buf.clear();
buf.put(test.getBytes());
buf.flip();
while(buf.hasRemaining()){
System.out.println(buf);
client.write(buf);
}
}
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public static void server(){
ServerSocket server = null;
InputStream in = null;
try {
server = new ServerSocket(8080);
int recvMsgSize = 0;
byte[] recvBuf = new byte[1024];
while(true){
System.out.println("mark in server 1");
Socket clntSocket = server.accept();
System.out.println("mark in server 2");
SocketAddress clientAddress = clntSocket.getRemoteSocketAddress();
System.out.println("Handling client at "+clientAddress);
in = clntSocket.getInputStream();
while((recvMsgSize=in.read(recvBuf))!=-1){
byte[] temp = new byte[recvMsgSize];
System.arraycopy(recvBuf, 0, temp, 0, recvMsgSize);
System.out.println(new String(temp));
}
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
輸出:
mark in server 1
mark in server 2
Handling client at /192.168.191.5:57982
java.nio.HeapByteBuffer[pos=0 lim=18 cap=1024]
test 0 from client
java.nio.HeapByteBuffer[pos=0 lim=18 cap=1024]
test 1 from client
...
在上面的例子中,如果將client方法先啟動(dòng)就會(huì)出現(xiàn)只打印mark in server 1,后面就不會(huì)打印了,造成這樣的情況是因?yàn)?code>client.finishConnect()方法返回false直接往程序后面跑了,并不會(huì)繼續(xù)阻塞直到連上服務(wù)端,但是換過個(gè)順序讓server先啟動(dòng)時(shí),返回true就能連接成功,因?yàn)闀?huì)在accept()方法阻塞,直到有客戶端client()連接。這里就可以看到這個(gè)非阻塞的特點(diǎn)。
當(dāng)然不僅僅只有客戶端client有這也的非阻塞特性,服務(wù)器端也是存在的。
ServerSocketChannel
類似于類SocketChannel,網(wǎng)絡(luò)IO服務(wù)器端的非阻塞特性是通過類ServerSocketChannel來實(shí)現(xiàn)的??梢詮南旅孢@個(gè)例子看出:
/**
**ServerSocketChannel示例
**/
public static void server(){
ServerSocketChannel server = null;
try {
server = ServerSocketChannel.open();
server.socket().bind(new InetSocketAddress(8080));
server.configureBlocking(false);
ByteBuffer buf = ByteBuffer.allocate(1024);
byte[] bytes = new byte[512];
System.out.println("--服務(wù)器啟動(dòng)-- ");
while(true){
SocketChannel socket = server.accept();
while(socket!=null&&socket.isConnected()){
buf.clear();
int len = socket.read(buf);
if(len == -1){
socket.close();
System.out.println("連接斷開");
}
buf.flip();
while(buf.hasRemaining()){
buf.get(bytes,0,buf.remaining());
System.out.println(new String(bytes));
}
}
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
輸出:
--服務(wù)器啟動(dòng)--
test 0 from client
test 1 from client
...
這個(gè)例子簡(jiǎn)單的將上一個(gè)例子改換了一下(其中的各種異常和資源的操作都沒有完善,實(shí)際開發(fā)請(qǐng)勿使用),服務(wù)器如果將 while(socket!=null)換成if(socket!=null),那么這個(gè)程序只會(huì)打印前面一次從客戶端發(fā)送過來的數(shù)據(jù),連接后只讀取了一次。在這里通過int len = socket.read(buf);中的len判斷后面是否還會(huì)有傳來數(shù)據(jù),如果數(shù)據(jù)長(zhǎng)度是-1,則關(guān)閉連接。這樣改過后就是非阻塞的網(wǎng)絡(luò)IO。
直到這里,都只是使用了前面兩個(gè)重要的概念Buffer和Channel,現(xiàn)在可以了解選擇器Selector并配合使用。
Selector & SelectionKey
選擇器這部分,不僅僅只有類Selector,它還有一個(gè)特別重要的類SelectionKey。在之前的文章中簡(jiǎn)單的了解過這兩個(gè)類,這里可以回顧一下:要注冊(cè)Selector則需要這個(gè)Channel繼承類SelectableChannel,這里只有通道類FileChannel沒有繼承;注冊(cè)的Selector實(shí)體可以返回一個(gè)SelectionKey集合,通過這個(gè)集合可以對(duì)不同的通道做出相應(yīng)的操作,這樣就避免了傳統(tǒng)的網(wǎng)絡(luò)IO為每一個(gè)連接創(chuàng)建一個(gè)線程而花費(fèi)大量的資源,只用一個(gè)線程就可以解決問題。Selector管理多個(gè)Channel的結(jié)構(gòu)圖如下所示:

下面可以看一下結(jié)合了選擇器類Selector后構(gòu)建的簡(jiǎn)單服務(wù)器代碼:
/**
**Selector 示例,簡(jiǎn)單服務(wù)器
**/
public static void server(){
System.out.println("--開始啟動(dòng)服務(wù)器");
Selector selector = null;
ServerSocketChannel server = null;
try {
server = ServerSocketChannel.open();
server.configureBlocking(false);
server.socket().bind(new InetSocketAddress(8080));
System.out.println("--監(jiān)聽8080端口");
selector = Selector.open();
server.register(selector,SelectionKey.OP_ACCEPT);
System.out.println("--服務(wù)器已啟動(dòng)成功");
while(true){
int num = selector.select();
if(num == 0){
continue;
}
Iterator<SelectionKey> selectionKeys = selector.selectedKeys().iterator();
while(selectionKeys.hasNext()){
SelectionKey selectionKey = selectionKeys.next();
selectionKeys.remove();
if(selectionKey.isAcceptable()){
System.out.println("-連接請(qǐng)求:");
ServerSocketChannel serverSocket = (ServerSocketChannel)selectionKey.channel();
SocketChannel socket = serverSocket.accept();
socket.configureBlocking(false);
socket.register(selector, SelectionKey.OP_READ);
}else if(selectionKey.isReadable() && selectionKey.isValid()){
System.out.println("-讀?。?);
SocketChannel channel = (SocketChannel) selectionKey.channel();
ByteBuffer buf = ByteBuffer.allocate(1024);
byte[] bytes = new byte[1024];
while(channel.isConnected()){
buf.clear();
int len = channel.read(buf);
if(len == -1){
channel.close();
selector.selectNow();
System.out.println("-連接關(guān)閉:"+channel.isConnected());
}
if(len > 0){
channel.write(ByteBuffer.wrap("收到消息啦".getBytes()));
System.out.println(buf+"-buf-length -"+len);
buf.flip();
while(buf.hasRemaining()){
buf.get(bytes, 0, len);
System.out.println(new String(bytes));
}
}
}
}
}
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
輸出:
--開始啟動(dòng)服務(wù)器
--監(jiān)聽8080端口
--服務(wù)器已啟動(dòng)成功
-連接請(qǐng)求:
-讀取:
java.nio.HeapByteBuffer[pos=18 lim=1024 cap=1024]-buf-length -18
test 0 from client
...
上面的例子中,通過selector注冊(cè)相應(yīng)的通道,通過selector獲取的selectionkey來做對(duì)應(yīng)的動(dòng)作。在這個(gè)例子一直遇到了異常如ClosedChannelException或者提示遠(yuǎn)程關(guān)閉了一個(gè)連接,導(dǎo)致一直很疑惑,因?yàn)槲颐髅髡{(diào)用了SelectableChannel.close()方法的。查過資料才知道,關(guān)閉一個(gè)已經(jīng)注冊(cè)的SelectableChannel需要兩個(gè)步驟:
1.取消注冊(cè)的
key,這個(gè)可以通過SelectionKey.cancel方法,也可以通過SelectableChannel.close方法,或者中斷阻塞在該channel上的IO操作的線程來做到。
2.后續(xù)的Selector.selectXXX()方法的調(diào)用才真正地關(guān)閉 本地Socket。
因此,如果調(diào)用了close()方法后沒有調(diào)用selectXXX()方法,那么本地socket將進(jìn)入CLOSE-WAIT 狀態(tài)。就是這個(gè)原因造成在buf.read(bytes)時(shí)發(fā)生CloseChannelException,因?yàn)樵谏厦孢@個(gè)例子中我使用了while(channel.isConnected())來進(jìn)行條件循環(huán),如果轉(zhuǎn)換一下思路,不用while循環(huán),而是把多次傳遞的信息分成多個(gè)Channel來發(fā)送,是不是就會(huì)好一點(diǎn)。每一次接收的都是新SocketChannel實(shí)例,而不在一個(gè)實(shí)例中循環(huán),造成上面那樣的不調(diào)用Selector.selectXXX()無法真正關(guān)閉連接的問題。
這里的Selector和SelectorKey還有很多細(xì)節(jié)的地方需要再細(xì)細(xì)研磨,操作。當(dāng)然現(xiàn)在也可以選擇成熟的NIO框架如Netty使用,以免進(jìn)入一些不了解的坑中。
DatagramChannel
類似于之前的類SocketChannel,類DatagramChannel處理的也是網(wǎng)絡(luò)IO,但是它對(duì)應(yīng)的是UDP連接,因?yàn)閁DP是無連接數(shù)據(jù)包的網(wǎng)絡(luò)協(xié)議,所以它并不能像其他通道一樣讀取和寫入數(shù)據(jù),但是提供了receive()方法和send()方法來使用。如下所示:
/**
**DatagramChannel示例
**/
服務(wù)器端
public static void testDatagramChannel(){
DatagramChannel datagramChannel = null;
Selector selector = null;
try {
selector = Selector.open();
datagramChannel = DatagramChannel.open();
datagramChannel.configureBlocking(false);
datagramChannel.socket().bind(new InetSocketAddress(8080));
datagramChannel.register(selector, SelectionKey.OP_READ);
System.out.println("---服務(wù)器啟動(dòng)");
while(true){
int num = selector.select();
if(num == 0)continue;
Iterator<SelectionKey> selectionKeys = selector.selectedKeys().iterator();
while(selectionKeys.hasNext()){
SelectionKey selectionKey = selectionKeys.next();
selectionKeys.remove();
if(selectionKey.isReadable()){
DatagramChannel channel = (DatagramChannel)selectionKey.channel();
ByteBuffer buf = ByteBuffer.allocate(1024);
channel.receive(buf);
buf.flip();
byte[] bytes = new byte[1024];
int len = buf.remaining();
buf.get(bytes,0,len);
String receive = new String(bytes,0,len);
System.out.println(receive);
}
if(selectionKey.isWritable()){
System.out.println("--write");
}
}
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
客戶端:
public static void testClient(){
DatagramChannel datagramChannel = null;
try {
System.out.println("--客戶端開始");
datagramChannel = DatagramChannel.open();
datagramChannel.configureBlocking(false);
ByteBuffer buf = ByteBuffer.allocate(1024);
buf.put("這是個(gè)Demo".getBytes());
buf.flip();
datagramChannel.send(buf, new InetSocketAddress("192.168.191.3", 8080));
datagramChannel.close();
System.out.println(buf);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
輸出:
--客戶端開始
java.nio.HeapByteBuffer[pos=10 lim=10 cap=1024]
---服務(wù)器啟動(dòng)
這是個(gè)Demo
在上面這個(gè)demo中有幾點(diǎn)可以說一下,類DatagramChannel是個(gè)注冊(cè)到Selector中后,這個(gè)selector.select()方法是個(gè)阻塞方法,只有等新連接進(jìn)入時(shí)才會(huì)繼續(xù)向下執(zhí)行,并不會(huì)因?yàn)橹皩?duì)DatagramChannel設(shè)置了非阻塞而使這個(gè)方法非阻塞。相對(duì)于SocketChannel類來說,變化并不大。
Pipe
看到這個(gè)名字,就已經(jīng)很眼熟了,就像之前的PipedInputStream/PipedOutputStream和PipedReader/PipedWriter,這個(gè)類也是實(shí)現(xiàn)線程間通信的功能。在上篇文章中也有提到,在這個(gè)類中是使用兩個(gè)靜態(tài)內(nèi)部類SourceChannel和SinkChannel來實(shí)現(xiàn)功能的,代碼如下所示:
/**
**Pipe 管道示例
**/
public static void testPipe() throws IOException{
final Pipe pipe = Pipe.open();
ExecutorService executor = Executors.newScheduledThreadPool(2);
executor.submit(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
Pipe.SinkChannel sinkChannel = pipe.sink();
ByteBuffer buf = ByteBuffer.allocate(1024);
int i = 0;
try {
while(i<4){
Thread.sleep(1000);
buf.clear();
String text = "Pipe test "+ i;
buf.put(text.getBytes());
buf.flip();
sinkChannel.write(buf);
i++;
}
sinkChannel.close();
} catch (InterruptedException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
executor.submit(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
Pipe.SourceChannel sourceChannel = pipe.source();
ByteBuffer buf = ByteBuffer.allocate(1024);
while(sourceChannel.isOpen()){
try {
buf.clear();
int len = sourceChannel.read(buf);
if(len == -1)sourceChannel.close();
if(len>0){
byte[] bytes = new byte[1024];
buf.flip();
buf.get(bytes, 0, len);
System.out.println(new String(bytes,0,len));
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
});
}
輸出:
Pipe test 0
Pipe test 1
Pipe test 2
Pipe test 3
如上所示,管道Pipe的操作和之前的幾個(gè)Channel的操作并沒有太大的變化,這個(gè)類完成線程間的通信靠的是它的兩個(gè)靜態(tài)內(nèi)部類,把握住著一點(diǎn),其余就需要研究書寫細(xì)節(jié)了。
總的來說,這部分其實(shí)很重要,這里也只是先打一點(diǎn)基礎(chǔ),如果想學(xué)的更深入的話,可以找找相關(guān)的框架進(jìn)行學(xué)習(xí),如Netty。有錯(cuò)誤疑惑的地方還請(qǐng)麻煩指出一起學(xué)習(xí),對(duì)于這部分我也是看了相關(guān)內(nèi)容并沒有在實(shí)際的工作中用到,很多地方可能并不深入或細(xì)節(jié)并不完善,還請(qǐng)指出,之后會(huì)一一完善。
本文參考
Java NIO 系列教程
攻破JAVA NIO技術(shù)壁壘
通俗編程——白話NIO之Selector
NIO的SelectableChannel關(guān)閉的一個(gè)問題
TCP和UDP的區(qū)別(轉(zhuǎn))