一步一個腳印,從BIO到NIO
上次寫了一篇BIO筆記 (查看),聊了BIO。這次接着聊NIO。
NIO三大元素
Channel 通道
個人感覺,通道有點類似於BIO的Stream。只不過stream只是單向的要麼input,要麼output。而Channel則是雙向的
並且支持非阻塞
。具體實現包括 FileChannel,DatagramChannel,SocketChannel,ServerSocketChannel等。本文重點用到後面兩個。
Buffer 緩存
用於保存數據的緩存,包括以下現實: ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer。具有非常靈添的操作。
Selector 多路複用器
可以通過多路複用器來管理多個通道
,以及獲取通道觸發事件
。
通道 通過註冊的方式,添加偵聽事件,通過select()
方法獲取可用的事件(比如可連接,可讀,可寫等操作)。
一步一個腳印,實例演示
1:實現多人聊天的工具,包括服務端,客戶端。
引用上一篇的 Soket IO的通信工具的代碼,如下:
服務端
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
public class ServerMain {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(4700);
Socket socket = ss.accept();
PrintWriter os=new PrintWriter(socket.getOutputStream());
BufferedReader br =new BufferedReader(new InputStreamReader(System.in) );
BufferedReader is =new BufferedReader(new InputStreamReader(socket.getInputStream()) );
new Thread( ()->{
while (true) {
try {
System.out.println( is.readLine());
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
while (true) {
String str=br.readLine();
os.println(str);
os.flush();
}
}
}
客戶端
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
public class ClientMain {
public static void main (String[] args) throws IOException {
Socket socket =new Socket("127.0.0.1",4700);
PrintWriter os=new PrintWriter(socket.getOutputStream());
BufferedReader br =new BufferedReader(new InputStreamReader(System.in) );
BufferedReader is =new BufferedReader(new InputStreamReader(socket.getInputStream()) );
new Thread( ()->{
while (true) {
try {
System.out.println( is.readLine());
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
while (true) {
String str=br.readLine();
os.println(str);
os.flush();
}
}
}
分碼分析:
1:上面實例的服務端與客戶端都是使用的BIO實現的。
2 :客戶端與服務端是一對一的。
3:服務端在 Socket socket = ss.accept();
的時候產生blocking,等待連接客戶端連接。
4:服務端、客戶端 在is.readLine()
的時候產生blocking。
總結:
聊天工具那肯定是要支持多個客戶端的,不然跟服務端聊天多沒意思。
服務端可以只有一個,必須支持同時響應多個人,那就必須是非blocking的。
使用NIO改造 :
服務端
import sun.nio.ch.ThreadPool;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.HashMap;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ArrayBlockingQueue;
public class ServerMain {
static volatile HashMap<String, SocketChannel> clients = new HashMap<>();
static Queue<Msg> queue = new ArrayBlockingQueue<Msg>(1000);
static ByteBuffer bfWrite = ByteBuffer.allocate(4000);
static ByteBuffer bfRead = ByteBuffer.allocate(4000);
public static void main(String[] args) throws IOException {
new Thread(() -> {
try {
connect();
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "connection").start();
new Thread(() -> {
try {
read();
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "read").start();
new Thread(() -> {
try {
trans();
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "trans").start();
}
public static void connect() throws IOException, InterruptedException {
System.out.println("--------鏈接監聽開始-------");
ServerSocketChannel channel = ServerSocketChannel.open();
//偵聽 127.0.0.1 :11111
channel.bind(new InetSocketAddress("127.0.0.1", 11111));
//設置爲 非阻塞
channel.configureBlocking(false);
//循環執行
while (true) {
//等待連接,非阻塞,如果沒有連接,clien=null;
SocketChannel client = channel.accept();
//連接不爲空時
if (client != null) {
//連接設置 非阻塞
client.configureBlocking(false);
//把鏈接添加到 鏈接列表
synchronized (clients) {
clients.put(String.valueOf(client.socket().getPort()), client);
}
System.out.println("添加鏈接:" + client.socket().getPort());
}
Thread.sleep(500);
}
}
//讀取
public static void read() throws IOException, InterruptedException {
System.out.println("--------信息監聽開始-------");
while (true) {
synchronized (clients) {
for (Map.Entry<String, SocketChannel> it : clients.entrySet()) {
//斷開連接的去掉
SocketChannel sc=it.getValue();
if (sc.isConnected() == false) {
clients.remove(it.getKey());
System.out.println("刪除連接:" + sc);
continue;
}
//嘗試讀取數據
if (sc.read(bfRead) > 0) {
bfRead.flip();
byte[] arr = new byte[bfRead.limit()];
bfRead.get(arr);
String str = new String(arr);
String [] msgArr =str.split(":");
if (msgArr == null || msgArr.length < 2) {
System.out.println("忽略信息:" + str);
}
else
{
Msg msg= new Msg( String.valueOf(sc.socket().getPort()),msgArr[0],msgArr[1]);
queue.add(msg);
}
bfRead.clear();
}
}
}
java.lang.Thread.sleep(500);
}
}
//信息轉發
public static void trans() throws IOException, InterruptedException {
System.out.println("--------信息轉發開始-------");
while (true) {
Msg msg = queue.poll();
if (msg != null) {
SocketChannel c = clients.get(msg.to);
bfWrite.clear();
bfWrite.put( String.format("%s:%s",msg.from,msg.msg) .getBytes());
bfWrite.flip();
c.write(bfWrite);
}
Thread.sleep(500);
}
}
public static class Msg {
public String to;
public String from;
public String msg;
public Msg( String from,String to, String msg) {
this.to = to;
this.from = from;
this.msg = msg;
}
public String getTo() {
return to;
}
public void setTo(String to) {
this.to = to;
}
public String getFrom() {
return from;
}
public void setFrom(String from) {
this.from = from;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
}
客戶端
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class ClientMain {
static SocketChannel sc;
static ByteBuffer bfWrite = ByteBuffer.allocate(4000);
static ByteBuffer bfRead = ByteBuffer.allocate(4000);
public static void main(String[] args) throws IOException, InterruptedException {
sc = SocketChannel.open();
sc.configureBlocking(false); //設置爲非阻塞
sc.connect(new InetSocketAddress("127.0.01", 11111));
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
try {
if (sc.finishConnect()) {
//啓動讀線程
new Thread(() -> {
try {
read();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}, "readThread").start();
//寫入
while (true) {
String msg = br.readLine();
bfWrite.clear();
bfWrite.put(msg.getBytes());
bfWrite.flip();
System.out.println("發送信息:" + msg);
sc.write(bfWrite);
Thread.sleep(200);
}
}
} finally {
sc.finishConnect();
}
}
public static void read() throws InterruptedException, IOException {
while (true) {
if (sc.read(bfRead) > 0) {
bfRead.flip();
byte[] arr = new byte[bfRead.limit()];
bfRead.get(arr);
String str = new String(arr);
String[] msgArr = str.split(":");
//不符合格式的數據
if (msgArr == null || msgArr.length < 2) {
continue;
}
System.out.println(String.format("來自%s的信息:%s", msgArr[0], msgArr[1]));
bfRead.clear();
}
Thread.sleep(500);
}
}
}
把它放到服務器上。並運行起來,開啓一個服務器實例,開啓兩個客戶端實例。運行效果如下:
服務器:
客戶端1:
額戶端2:
代碼分析:
服務端
1:
使用了NIO 的channel,並設置爲 非阻塞。
ServerSocketChannel channel = ServerSocketChannel.open();
//偵聽 127.0.0.1 :11111
channel.bind(new InetSocketAddress("127.0.0.1", 11111));
//設置爲 非阻塞
channel.configureBlocking(false);
2:
因爲是非阻塞的,資源的調用需要考慮多線程。多處引入synchronize
static volatile HashMap<String, SocketChannel> clients = new HashMap<>();
3:
服務端包括分爲三個線程,分別是,連接,讀取信息,轉發信息。
連接:如果有新的連接進來,則把連接放入clients
。
讀取:循環嘗試去讀取各個連接的數據,如有數據則寫入轉發隊列
。
轉發:循環讀取隊列
的數據,根據 消息to
找到目標連接,並把數據發送。
4:
代碼優化,爲了更好的應對業務,引入隊列、消息封裝等。
客戶端
1:同樣使用了NIO 的channel,並設置爲 非阻塞。
sc = SocketChannel.open();
sc.configureBlocking(false); //設置爲非阻塞
sc.connect(new InetSocketAddress("127.0.01", 11111));
總結:
1:聊天的功能基本能實現。
2:但是能效方面偏低,比如服務器的信息讀取。假如說有10000個連接,那就得遍歷10000次。如果能做到哪個連接有信息就讀哪個連接,做到目標精確就好了。
引入多路複用器 selector
服務端
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.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ArrayBlockingQueue;
public class ServerMain2 {
static volatile HashMap<String, SocketChannel> clients = new HashMap<>();
static Queue<Msg> queue = new ArrayBlockingQueue<Msg>(1000);
static Selector selector;
public static void main(String[] args) throws IOException, InterruptedException {
selector = Selector.open();
Selector selector = Selector.open();
ServerSocketChannel channel = ServerSocketChannel.open();
//偵聽 127.0.0.1 :11111
channel.bind(new InetSocketAddress("127.0.0.1", 11111));
//設置爲 非阻塞
channel.configureBlocking(false);
//通道註冊到selector, 類型爲連接
channel.register(selector, SelectionKey.OP_ACCEPT);
new Thread(()->{
try {
trans();
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"trans").start();
while (true) {
if (selector.select(1000) == 0) {
continue;
}
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove();
if (key.isAcceptable()) {
handleAccept(key);
}
if (key.isReadable()) {
handleRead(key);
}
}
Thread.sleep(500);
}
}
public static void handleAccept(SelectionKey key) throws IOException {
System.out.println("-------handleAccept---------");
ServerSocketChannel ssChannel = (ServerSocketChannel) key.channel();
SocketChannel sc = ssChannel.accept();
sc.configureBlocking(false);
sc.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocateDirect(4000));
//把鏈接添加到 鏈接列表
synchronized (clients) {
clients.put(String.valueOf(sc.socket().getPort()), sc);
}
System.out.println("添加鏈接:" + sc.socket().getPort());
}
public static void handleRead(SelectionKey key) throws IOException {
System.out.println("-------handleRead---------");
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer buf = (ByteBuffer) key.attachment();
long bytesRead = sc.read(buf);
while (bytesRead > 0) {
buf.flip();
while (buf.hasRemaining()) {
byte[] arr = new byte[buf.limit()];
buf.get(arr);
String str = new String(arr);
String[] msgArr = str.split(":");
if (msgArr == null || msgArr.length < 2) {
System.out.println("忽略信息:" + str);
} else {
Msg msg = new Msg(String.valueOf(sc.socket().getPort()), msgArr[0], msgArr[1]);
queue.add(msg);
}
}
System.out.println();
buf.clear();
bytesRead = sc.read(buf);
}
if (bytesRead == -1) {
sc.close();
}
}
//信息轉發
public static void trans() throws IOException, InterruptedException {
ByteBuffer bfWrite =ByteBuffer.allocate(4000);
while (true) {
Msg msg = queue.poll();
if (msg != null) {
SocketChannel c = clients.get(msg.to);
bfWrite.clear();
bfWrite.put(String.format("%s:%s", msg.from, msg.msg).getBytes());
bfWrite.flip();
c.write(bfWrite);
}
Thread.sleep(500);
}
}
public static class Msg {
public String to;
public String from;
public String msg;
public Msg(String from, String to, String msg) {
this.to = to;
this.from = from;
this.msg = msg;
}
public String getTo() {
return to;
}
public void setTo(String to) {
this.to = to;
}
public String getFrom() {
return from;
}
public void setFrom(String from) {
this.from = from;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
}
代碼分析:
大概流程如下圖所示:
相對之前的版本,做了以下調整。
1:使用多路複用器。
selector = Selector.open();
2:通道註冊到多路複用器
ServerSocketChannel channel = ServerSocketChannel.open();
//偵聽 127.0.0.1 :11111
channel.bind(new InetSocketAddress("127.0.0.1", 11111));
//設置爲 非阻塞
channel.configureBlocking(false);
//通道註冊到selector, 類型爲連接
channel.register(selector, SelectionKey.OP_ACCEPT);
3:偵聽 連接以及可讀事件。取代了原先開始兩個線程(連接線程,讀取線程)遍歷的方式。
while (true) {
if (selector.select(1000) == 0) {
continue;
}
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove();
if (key.isAcceptable()) {
handleAccept(key);
}
else if (key.isReadable()) {
handleRead(key);
}
}
Thread.sleep(500);
}
演示效果:
服務端:
客戶端1:
客戶端2:
總結:
1:NIO的三大要素在本例子已經使用到了。
2:再次總結一下使用的好處。
1):channel ,支持not blocking。SeverSocketChanel 支持一個通道連接多個客戶端。
2):Buffer,可以定義固定的一塊內存,支持clear, flip等操作。可以重複利用,減少GC。
3):selector, 管理channel,偵聽channel的事件。減少白忙添,增加能效。從以前的循環嘗試讀數據,到有數據纔去讀。同樣連接也是。減少用戶態/核生態的切換。
寫在最後。
感覺NIO有點複雜,代碼量有點多。。是不是應該有更好的封裝呢?
未完,待續。。。。