NIO是jdk1.4引入的java.nio包,它提供了高速的、面向塊的IO。通過定義包含數據的類,以及通過塊的形式處理這些數據。NIO類庫包含緩衝區Buffer、多路複用選擇器Selector、通道Channel等新的抽象,可以構建多路複用、同步非阻塞的IO程序,同時提供了更接近操作系統底層高性能的數據操作方式。
緩衝區Buffer,包含一些要寫入或者要讀出的數據。在面向流的IO中,可以將數據直接寫入或者將數據直接讀到Stream對象中。緩衝區提供了對數據的結構化訪問以及維護讀寫位置等信息。
緩衝區Buffer是一個數組,通常是一個字節數組ByteBuffer,還有其他類型的數組字符緩衝區CharBuffer、短整型緩衝區ShortBuffer、整型緩衝區IntBuffer、長整形緩衝區LongBuffer、浮點型整型區FloatBuffer、雙精度浮點型緩衝區。每一種Buffer的類都是Buffer接口的一個子實例。所以它們有完全一樣的操作,只是操作的數據類型不一樣。
通道Channel,Channel好比自來水管,網絡數據通過Channel讀取和寫入。通道與流的不同之處在於,通道是雙向的,而流是單向的,流只能在一個方向上移動,一個流必須是InputStream或者OutputStream的子類,通道可以用於讀寫或者兩者同時進行。通道Channel是全雙工的,所以它可以比流更好地映射底層操作系統的API。Channel可以分爲兩大類,一類用於網絡對象,一類用於文件操作。
多路複用選擇器Selector,Selector會不斷輪詢註冊在其上的Channel,如果某個Channel上面發生讀或者寫事件,這個Channel就處於就緒狀態,會被Selector輪詢出來,通過SelectionKey可以獲得就緒Channel的集合,進行後續的IO操作。
一個多路複用器Selector可以同時輪詢多個Channel,由於JDK使用epoll()代替傳統的select實現,所以它並沒有最大連接句柄1024/2048的限制。只要一個線程負責Selector的輪詢,就可以接入成千上萬的客戶端。
下面來看服務器端代碼:
package com.test.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
/**
* NIO服務器端
* @author 程就人生
* @Date
*/
public class HelloServer {
public static void main( String[] args ){
int port = 8080;
// 多路複用服務類
MultiplexerHelloServer helloServer = new MultiplexerHelloServer(port);
new Thread(helloServer,"多路複用服務類").start();
}
}
class MultiplexerHelloServer implements Runnable{
private Selector selector;
private ServerSocketChannel serverChannel;
private volatile boolean stop;
/**
* 初始化多路複用,綁定監聽端口
* @param port
*/
MultiplexerHelloServer(int port){
try {
// 初始化多路複用器,創建Selector
selector = Selector.open();
// 打開ServerSocketChannel
serverChannel = ServerSocketChannel.open();
// 設置爲非堵塞模式
serverChannel.configureBlocking(false);
// 綁定監聽端口
serverChannel.socket().bind(new InetSocketAddress(port), 1024);
// 將ServerSocketChannel註冊到Selector上去,監聽accept事件
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("服務器端已啓動,啓動端口爲:" + port);
} catch (IOException e) {
e.printStackTrace();
}
}
public void stop(){
this.stop = true;
}
public void run() {
while(!stop){
try {
SelectionKey key = null;
// 每隔一秒被喚醒一次
selector.select(1000);
// 獲取就緒狀態的SelectionKey
Set<SelectionKey> selectedKeys = selector.selectedKeys();
// 對就緒狀態的SelectionKey進行迭代
Iterator<SelectionKey> it = selectedKeys.iterator();
while(it.hasNext()){
key = it.next();
it.remove();
try{
// 對網絡事件進行操作(連接和讀寫操作)
handleInput(key);
}catch(Exception e){
if(key != null){
key.cancel();
if(key.channel() != null){
try {
key.channel().close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
// 多路複用器關閉後,所有註冊在上面的channel和pipe等資源都會被自動去註冊並關閉,所以不需要重複釋放資源
if(selector != null){
try {
selector.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void handleInput(SelectionKey key) throws IOException{
if(key.isValid()){
// 處理新接入的客戶端請求信息
if(key.isAcceptable()){
// 接入新的連接
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
SocketChannel sc = ssc.accept();
// 設置爲異步非阻塞
sc.configureBlocking(false);
// 監聽讀操作
sc.register(selector, SelectionKey.OP_READ);
}
// 處理客戶端發來的信息,讀取操作
if(key.isReadable()){
SocketChannel sc = (SocketChannel) key.channel();
// 開闢一個1KB的緩衝區
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int readBytes = sc.read(readBuffer);
// 讀取到了字節
if(readBytes > 0){
readBuffer.flip();
byte[] bytes = new byte[readBuffer.remaining()];
readBuffer.get(bytes);
// 對字節碼進行解碼
String body = new String(bytes, "UTF-8");
System.out.println("服務器端收到的:" + body);
// 回寫客戶端
doWrite(sc);
}
}
}
}
// 回寫客戶端
private void doWrite(SocketChannel sc) throws IOException{
byte[] bytes = "服務器端的響應來了".getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
writeBuffer.put(bytes);
writeBuffer.flip();
sc.write(writeBuffer);
}
}
在40-56行,在構造方法中對資源進行初始化。創建多路複用選擇器Selector、ServerSocketChannel,對Channel和TCP參數進行配置。
在63行代碼,對網絡事件進行輪詢監聽;在67行代碼中,每隔1s喚醒一次,監聽多路複用選擇器中是否有就緒的SelectionKey,如果有則進行遍歷。
在105行代碼中,在handleInput方法中,對SelectionKey進行判斷。判斷SelectionKey目前所處的狀態,是接入的新連接,還是處於網絡讀狀態。如果是新連接,則監聽網絡讀操作。如果是網絡讀操作,在通過doWrite方法回寫客戶端。
客戶端代碼:
package com.test.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
/**
* NIO客戶端
* @author 程就人生
* @Date
*/
public class HelloClient {
public static void main( String[] args ){
int port = 8080;
new Thread(new HelloClientHandle("127.0.0.1", port)).start();
}
}
/**
* 客戶端處理器
* @author 程就人生
* @Date
*/
class HelloClientHandle implements Runnable{
private Selector selector;
private SocketChannel socketChannel;
private volatile boolean stop;
private String host;
private int port;
public HelloClientHandle(String host, int port) {
try {
this.host = host;
this.port = port;
// 創建多路複用選擇器
selector = Selector.open();
// 打開SocketChannel
socketChannel = SocketChannel.open();
// 設置爲非阻塞
socketChannel.configureBlocking(false);
} catch (IOException e) {
e.printStackTrace();
}
}
public void run() {
try {
// 連接服務器端判斷
if(!socketChannel.connect(new InetSocketAddress(host,port))){
// 將socketChannel註冊到多路複用器,並監聽連接操作
socketChannel.register(selector, SelectionKey.OP_CONNECT);
}
} catch (IOException e) {
e.printStackTrace();
}
while(!stop){
try {
selector.select(1000);
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> it = selectedKeys.iterator();
SelectionKey key = null;
while(it.hasNext()){
key = it.next();
it.remove();
handleInput(key);
}
} catch (IOException e) {
e.printStackTrace();
}
}
if(selector != null){
try {
selector.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void handleInput(SelectionKey key) throws IOException{
if(key.isValid()){
SocketChannel sc = (SocketChannel) key.channel();
if(key.isConnectable()){
// 完成連接
if(sc.finishConnect()){
// 監聽讀時間
sc.register(selector, SelectionKey.OP_READ);
// 給服務器端發送消息
doWrite(sc);
}else{
// 退出
System.exit(1);
}
}
// 是否爲可讀的
if(key.isReadable()){
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int readBytes =sc.read(readBuffer);
// 讀到了字節
if(readBytes > 0){
readBuffer.flip();
byte[] bytes = new byte[readBuffer.remaining()];
readBuffer.get(bytes);
String body = new String(bytes, "utf-8");
System.out.println("客戶端收到的信息是:" + body);
this.stop = true;
// 沒讀到字節
} else if(readBytes < 0){
// 取消
key.cancel();
// 關閉連接
sc.close();
}
}
}
}
/**
* 網絡寫操作
* @param sc
* @throws IOException
*/
private void doWrite(SocketChannel sc) throws IOException{
byte[] req = "來自客戶端的消息".getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(req.length);
writeBuffer.put(req);
writeBuffer.flip();
sc.write(writeBuffer);
if(!writeBuffer.hasRemaining()){
System.out.println("已發送給服務器端~!");
}
}
}
客戶端的連接步驟:先創建多路複用選擇器,打開SocketChannel,綁定本地端地址,設置SocketChannel爲非阻塞,異步連接服務器。將SocketChannel註冊到多路選擇器中,註冊監聽事件。
SocketChannel連接服務器連接成功後,對SelectionKey進行輪詢監聽,每隔10s喚醒一次。在97行,連接成功後,監聽網絡讀事件,並給服務器端發送消息。
在第106行,堅挺到網絡讀事件後,將字節讀出,並打印出來。如果讀取完畢,則關閉通道,關閉連接。
客戶端發起的連接操作是異步的,通過在多路複用器註冊OP_CONNECT等待後續結果,不需要之前那樣被同步阻塞。SocketChannel的讀寫操作都是異步的。如果沒有可讀寫的數據不會同步等待。
以上便是來自java.nio包的非阻塞服務器端、客戶端編碼的簡單演示。