64
京東網上商城
在Java中使用NIO進行網絡編程
在JDK中,有一個非常有意思的庫:NIO(New I/O)。這個庫中有3個重要的類,分別是java.nio.channels中Selector和Channel,以及java.nio中的Buffer。
本篇文章我們首先了解一下為什麼需要NIO來進行網絡編程,然後看看一步一步來講解如何在網絡編程中使用NIO。
為什麼需要NIO
使用Java編寫過Socket程序的同學一定都知道Socket和SocketServer。當調用某個調用的時候,調用的地方就會阻塞,等待響應。這種方式對於小規模的程序非常方便,但是對於大型的程序就有點力不從心了,當有大量的連接的時候,我們可以為每一個連接建立一個線程來操作。但是這種做法帶來的缺陷也是顯而易見的:
-
硬件能夠支持大量的並發。
-
並發的數量始終有一個上限。
-
各個線程之間的優先級不好控製。
-
各個Client之間的交互與同步困難。
我們也可以使用一個線程來處理所有的請求,使用不阻塞的IO,輪詢查詢所有的Client。這種做法同樣也有缺陷:無法迅速響應Client端,同時會消耗大量輪詢查詢的時間。
所以,我們需要一種poll的模式來處理這種情況,從大量的網絡連接中找出來真正需要服務的Client。這正是NIO誕生的原因:提供一種Poll的模式,在所有的Client中找到需要服務的Client。
回到我們剛剛說到的3個最最重要的Class:java.nio.channels中Selector和Channel,以及java.nio中的Buffer。
Channel代表一個可以被用於Poll操作的對象(可以是文件流也可以使網絡流),Channel能夠被注冊到一個Selector中。通過調用Selector的select方法可以從所有的Channel中找到需要服務的實例(Accept,read ..)。Buffer對象提供讀寫數據的緩存。相對於我們熟悉的Stream對象,Buffer提供更好的性能以及更好的編程透明性(人為控製緩存的大小以及具體的操作)。
配合Buffer使用Channel
與傳統模式的編程不用,Channel不使用Stream,而是Buffer。我們來實現一個簡單的非阻塞Echo Client:
- package com.cnblogs.gpcuster;
- import java.net.InetSocketAddress;
- import java.net.SocketException;
- import java.nio.ByteBuffer;
- import java.nio.channels.SocketChannel;
- public class TCPEchoClientNonblocking {
- public static void main(String args[]) throws Exception {
- if ((args.length < 2) || (args.length > 3))// Testforcorrect#ofargs
- throw new IllegalArgumentException(
- "Parameter(s): <Server> <Word> [<Port>]");
- String server = args[0];// ServernameorIPaddress
- // ConvertinputStringtobytesusingthedefaultcharset
- byte[] argument = args[1].getBytes();
- int servPort = (args.length == 3) ? Integer.parseInt(args[2]) : 7;
- // Createchannelandsettononblocking
- SocketChannel clntChan = SocketChannel.open();
- clntChan.configureBlocking(false);
- // Initiateconnectiontoserverandrepeatedlypolluntilcomplete
- if (!clntChan.connect(new InetSocketAddress(server, servPort))) {
- while (!clntChan.finishConnect()) {
- System.out.print(".");// Dosomethingelse
- }
- }
- ByteBuffer writeBuf = ByteBuffer.wrap(argument);
- ByteBuffer readBuf = ByteBuffer.allocate(argument.length);
- int totalBytesRcvd = 0;// Totalbytesreceivedsofar
- int bytesRcvd;// Bytesreceivedinlastread
- while (totalBytesRcvd < argument.length) {
- if (writeBuf.hasRemaining()) {
- clntChan.write(writeBuf);
- }
- if ((bytesRcvd = clntChan.read(readBuf)) == -1) {
- throw new SocketException("Connection closed prematurely");
- }
- totalBytesRcvd += bytesRcvd;
- System.out.print(".");// Dosomethingelse
- }
- System.out.println("Received:" + // converttoStringperdefaultcharset
- new String(readBuf.array(), 0, totalBytesRcvd));
- clntChan.close();
- }
- }
這段代碼使用ByteBuffer來保存讀寫的數據。通過clntChan.configureBlocking(false
); 設置後,其中的connect,read,write操作都不回阻塞,而是立刻放回結果。
使用Selector
Selector的可以從所有的被注冊到自己Channel中找到需要服務的實例。
我們來實現Echo Server。
首先,定義一個接口:
- package com.cnblogs.gpcuster;
- import java.nio.channels.SelectionKey;
- import java.io.IOException;
- public interface TCPProtocol {
- void handleAccept(SelectionKey key) throws IOException;
- void handleRead(SelectionKey key) throws IOException;
- void handleWrite(SelectionKey key) throws IOException;
- }
我們通過listnChannel.register(selector, SelectionKey.OP_ACCEPT); 注冊了一個我們感興趣的事件,然後調用selector.select(TIMEOUT)等待訂閱的時間發生,然後再采取相應的處理措施。
我們的Echo Server將使用這個接口。然後我們實現Echo Server:
- import java.io.IOException;
- import java.net.InetSocketAddress;
- import java.nio.channels.SelectionKey;
- import java.nio.channels.Selector;
- import java.nio.channels.ServerSocketChannel;
- import java.util.Iterator;
- public class TCPServerSelector {
- private static final int BUFSIZE = 256;// Buffersize(bytes)
- private static final int TIMEOUT = 3000;// Waittimeout(milliseconds)
- public static void main(String[] args) throws IOException {
- if (args.length < 1) {// Testforcorrect#ofargs
- throw new IllegalArgumentException("Parameter(s):<Port>...");
- }
- // Createaselectortomultiplexlisteningsocketsandconnections
- Selector selector = Selector.open();
- // Createlisteningsocketchannelforeachportandregisterselector
- for (String arg : args) {
- ServerSocketChannel listnChannel = ServerSocketChannel.open();
- listnChannel.socket().bind(
- new InetSocketAddress(Integer.parseInt(arg)));
- listnChannel.configureBlocking(false);// mustbenonblockingtoregister
- // Registerselectorwithchannel.Thereturnedkeyisignored
- listnChannel.register(selector, SelectionKey.OP_ACCEPT);
- }
- // Createahandlerthatwillimplementtheprotocol
- TCPProtocol protocol = new EchoSelectorProtocol(BUFSIZE);
- while (true) {// Runforever,processingavailableI/Ooperations
- // Waitforsomechanneltobeready(ortimeout)
- if (selector.select(TIMEOUT) == 0) {// returns#ofreadychans
- System.out.print(".");
- continue;
- }
- // GetiteratoronsetofkeyswithI/Otoprocess
- Iterator<SelectionKey> keyIter = selector.selectedKeys().iterator();
- while (keyIter.hasNext()) {
- SelectionKey key = keyIter.next();// Keyisbitmask
- // Serversocketchannelhaspendingconnectionrequests?
- if (key.isAcceptable()) {
- protocol.handleAccept(key);
- }
- // Clientsocketchannelhaspendingdata?
- if (key.isReadable()) {
- protocol.handleRead(key);
- }
- // Clientsocketchannelisavailableforwritingand
- // keyisvalid(i.e.,channelnotclosed)?
- if (key.isValid() && key.isWritable()) {
- protocol.handleWrite(key);
- }
- keyIter.remove();// removefromsetofselectedkeys
- }
- }
- }
- }
- package com.cnblogs.gpcuster;
- import java.nio.channels.SelectionKey;
- import java.nio.channels.SocketChannel;
- import java.nio.channels.ServerSocketChannel;
- import java.nio.ByteBuffer;
- import java.io.IOException;
- public class EchoSelectorProtocol implements TCPProtocol {
- private int bufSize;// SizeofI/Obuffer
- public EchoSelectorProtocol(int bufSize) {
- this.bufSize = bufSize;
- }
- public void handleAccept(SelectionKey key) throws IOException {
- SocketChannel clntChan = ((ServerSocketChannel) key.channel()).accept();
- clntChan.configureBlocking(false);// Mustbenonblockingtoregister
- // Registertheselectorwithnewchannelforreadandattachbytebuffer
- clntChan.register(key.selector(), SelectionKey.OP_READ, ByteBuffer
- .allocate(bufSize));
- }
- public void handleRead(SelectionKey key) throws IOException {
- // Clientsocketchannelhaspendingdata
- SocketChannel clntChan = (SocketChannel) key.channel();
- ByteBuffer buf = (ByteBuffer) key.attachment();
- long bytesRead = clntChan.read(buf);
- if (bytesRead == -1) {// Didtheotherendclose?
- clntChan.close();
- } else if (bytesRead > 0) {
- // Indicateviakeythatreading/writingarebothofinterestnow.
- key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
- }
- }
- public void handleWrite(SelectionKey key) throws IOException {
- /*
- * Channelisavailableforwriting,andkeyisvalid(i.e.,clientchannel
- * notclosed).
- */
- // Retrievedatareadearlier
- ByteBuffer buf = (ByteBuffer) key.attachment();
- buf.flip();// Preparebufferforwriting
- SocketChannel clntChan = (SocketChannel) key.channel();
- clntChan.write(buf);
- if (!buf.hasRemaining()) {// Buffercompletelywritten?
- // Nothingleft,sonolongerinterestedinwrites
- key.interestOps(SelectionKey.OP_READ);
- }
- buf.compact();// Makeroomformoredatatobereadin
- }
- }
在這裏,我們又進一步對Selector注冊了相關的事件:key.interestOps(SelectionKey.OP_READ);
這樣,我們就實現了基於NIO的Echo 係統。
最後更新:2017-04-03 14:54:18