思維導圖
學如逆水行舟,不進則退
1 NIO概述
1.1 定義
java.nio全稱java non-blocking IO,是指JDK1.4 及以上版本里提供的新api(New IO) ,爲所有的原始類型(boolean類型除外)提供緩存支持的數據容器,使用它可以提供非阻塞式的高伸縮性網絡(來源於百度百科)。
1.2 爲什麼使用NIO
在上面的描述中提到,是在JDK1.4以上的版本才提供NIO,那在之前使用的是什麼呢?答案很簡單,就是BIO(阻塞式IO),也就是我們常用的IO流。
BIO的問題其實不用多說了,因爲在使用BIO時,主線程會進入阻塞狀態,這就非常影響程序的性能,不能充分利用機器資源。但是這樣就會有人提出疑問了,那我使用多線程不就可以了嗎?
但是在高併發的情況下,會創建很多線程,線程會佔用內存,線程之間的切換也會浪費資源開銷。
而NIO只有在連接/通道真正有讀寫事件發生時(事件驅動),纔會進行讀寫,就大大地減少了系統的開銷。不必爲每一個連接都創建一個線程,也不必去維護多個線程。
避免了多個線程之間的上下文切換,導致資源的浪費。
2 NIO的三大核心
NIO的核心 對應的類或接口 應用 作用 緩衝區 java.nio.Buffer 文件IO/網絡IO 存儲數據 通道 java.nio.channels.Channel 文件IO/網絡IO 運輸 選擇器
java.nio.channels.Selector 網絡IO 控制器
2.1緩衝區(Buffer)
2.1.1 什麼是緩衝區
我們先看以下這張類圖,可以看到Buffer有七種類型。
Buffer是一個內存塊。在NIO中,所有的數據都是用Buffer處理,有讀寫兩種模式。所以NIO和傳統的IO的區別就體現在這裏。傳統IO是面向Stream流,NIO而是面向緩衝區(Buffer)。
2.1.2 常用的類型ByteBuffer
一般我們常用的類型是ByteBuffer,把數據轉成字節進行處理。實質上是一個byte[]數組。
public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer>{
//存儲數據的數組
final byte[] hb;
//構造器方法
ByteBuffer(int mark, int pos, int lim, int cap, byte[] hb, int offset) {
super(mark, pos, lim, cap);
//初始化數組
this.hb = hb;
this.offset = offset;
}
}
複製代碼
2.1.3 創建Buffer的方式
主要分成兩種:JVM堆內內存塊Buffer、堆外內存塊Buffer。
創建堆內內存塊(非直接緩衝區)的方法是:
//創建堆內內存塊HeapByteBuffer
ByteBuffer byteBuffer1 = ByteBuffer.allocate(1024);
String msg = "java技術愛好者";
//包裝一個byte[]數組獲得一個Buffer,實際類型是HeapByteBuffer
ByteBuffer byteBuffer2 = ByteBuffer.wrap(msg.getBytes());
複製代碼
創建堆外內存塊(直接緩衝區)的方法:
//創建堆外內存塊DirectByteBuffer
ByteBuffer byteBuffer3 = ByteBuffer.allocateDirect(1024);
複製代碼
2.1.3.1 HeapByteBuffer與DirectByteBuffer的區別
其實根據類名就可以看出,HeapByteBuffer所創建的字節緩衝區就是在JVM堆中的,即JVM內部所維護的字節數組。而DirectByteBuffer是直接操作操作系統本地代碼創建的內存緩衝數組。
DirectByteBuffer的使用場景:
- java程序與本地磁盤、socket傳輸數據
- 大文件對象,可以使用。不會受到堆內存大小的限制。
- 不需要頻繁創建,生命週期較長的情況,能重複使用的情況。
HeapByteBuffer的使用場景:
除了以上的場景外,其他情況還是建議使用HeapByteBuffer,沒有達到一定的量級,實際上使用DirectByteBuffer是體現不出優勢的。
2.1.3.2 Buffer的初體驗
接下來,使用ByteBuffer做一個小例子,熟悉一下:
public static void main(String[] args) throws Exception {
String msg = "java技術愛好者,起飛!";
//創建一個固定大小的buffer(返回的是HeapByteBuffer)
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byte[] bytes = msg.getBytes();
//寫入數據到Buffer中
byteBuffer.put(bytes);
//切換成讀模式,關鍵一步
byteBuffer.flip();
//創建一個臨時數組,用於存儲獲取到的數據
byte[] tempByte = new byte[bytes.length];
int i = 0;
//如果還有數據,就循環。循環判斷條件
while (byteBuffer.hasRemaining()) {
//獲取byteBuffer中的數據
byte b = byteBuffer.get();
//放到臨時數組中
tempByte[i] = b;
i++;
}
//打印結果
System.out.println(new String(tempByte));//java技術愛好者,起飛!
}
複製代碼
這上面有一個flip()方法是很重要的。意思是切換到讀模式。上面已經提到緩存區是雙向的,既可以往緩衝區寫入數據,也可以從緩衝區讀取數據。但是不能同時進行,需要切換。那麼這個切換模式的本質是什麼呢?
2.1.4 三個重要參數
//位置,默認是從第一個開始
private int position = 0;
//限制,不能讀取或者寫入的位置索引
private int limit;
//容量,緩衝區所包含的元素的數量
private int capacity;
複製代碼
那麼我們以上面的例子,一句一句代碼進行分析:
String msg = "java技術愛好者,起飛!";
//創建一個固定大小的buffer(返回的是HeapByteBuffer)
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
複製代碼
當創建一個緩衝區時,參數的值是這樣的:
當執行到byteBuffer.put(bytes),當put()進入多少數據,position就會增加多少,參數就會發生變化:
接下來關鍵一步byteBuffer.flip(),會發生如下變化:
flip()方法的源碼如下:
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
複製代碼
爲什麼要這樣賦值呢?因爲下面有一句循環條件判斷:
byteBuffer.hasRemaining();
public final boolean hasRemaining() {
//判斷position的索引是否小於limit。
//所以可以看出limit的作用就是記錄寫入數據的位置,那麼當讀取數據時,就知道讀到哪個位置
return position < limit;
}
複製代碼
接下來就是在while循環中get()讀取數據,讀取完之後。
最後當position等於limit時,循環判斷條件不成立,就跳出循環,讀取完畢。
所以可以看出實質上capacity容量大小是不變的,實際上是通過控制position和limit的值來控制讀寫的數據。
2.2 管道(Channel)
首先我們看一下Channel有哪些子類:
常用的Channel有這四種:
FileChannel,讀寫文件中的數據。 SocketChannel,通過TCP讀寫網絡中的數據。 ServerSockectChannel,監聽新進來的TCP連接,像Web服務器那樣。對每一個新進來的連接都會創建一個SocketChannel。 DatagramChannel,通過UDP讀寫網絡中的數據。
Channel本身並不存儲數據,只是負責數據的運輸。必須要和Buffer一起使用。
2.2.1 獲取通道的方式
2.2.1.1 FileChannel
FileChannel的獲取方式,下面舉個文件複製拷貝的例子進行說明:
首先準備一個"1.txt"放在項目的根目錄下,然後編寫一個main方法:
public static void main(String[] args) throws Exception {
//獲取文件輸入流
File file = new File("1.txt");
FileInputStream inputStream = new FileInputStream(file);
//從文件輸入流獲取通道
FileChannel inputStreamChannel = inputStream.getChannel();
//獲取文件輸出流
FileOutputStream outputStream = new FileOutputStream(new File("2.txt"));
//從文件輸出流獲取通道
FileChannel outputStreamChannel = outputStream.getChannel();
//創建一個byteBuffer,小文件所以就直接一次讀取,不分多次循環了
ByteBuffer byteBuffer = ByteBuffer.allocate((int)file.length());
//把輸入流通道的數據讀取到緩衝區
inputStreamChannel.read(byteBuffer);
//切換成讀模式
byteBuffer.flip();
//把數據從緩衝區寫入到輸出流通道
outputStreamChannel.write(byteBuffer);
//關閉通道
outputStream.close();
inputStream.close();
outputStreamChannel.close();
inputStreamChannel.close();
}
複製代碼
執行後,我們就獲得一個"2.txt"。執行成功。
以上的例子,可以用一張示意圖表示,是這樣的:
2.2.1.2 SocketChannel
接下來我們學習獲取SocketChannel的方式。
還是一樣,我們通過一個例子來快速上手:
public static void main(String[] args) throws Exception {
//獲取ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
InetSocketAddress address = new InetSocketAddress("127.0.0.1", 6666);
//綁定地址,端口號
serverSocketChannel.bind(address);
//創建一個緩衝區
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
while (true) {
//獲取SocketChannel
SocketChannel socketChannel = serverSocketChannel.accept();
while (socketChannel.read(byteBuffer) != -1){
//打印結果
System.out.println(new String(byteBuffer.array()));
//清空緩衝區
byteBuffer.clear();
}
}
}
複製代碼
然後運行main()方法,我們可以通過telnet命令進行連接測試:
通過上面的例子可以知道,通過ServerSocketChannel.open()方法可以獲取服務器的通道,然後綁定一個地址端口號,接着accept()方法可獲得一個SocketChannel通道,也就是客戶端的連接通道。
最後配合使用Buffer進行讀寫即可。
這就是一個簡單的例子,實際上上面的例子是阻塞式的。要做到非阻塞還需要使用選擇器Selector。
2.3 選擇器(Selector)
Selector翻譯成選擇器,有些人也會翻譯成多路複用器,實際上指的是同一樣東西。
只有網絡IO纔會使用選擇器,文件IO是不需要使用的。
選擇器可以說是NIO的核心組件,它可以監聽通道的狀態,來實現異步非阻塞的IO。換句話說,也就是事件驅動。以此實現單線程管理多個Channel的目的。
2.3.1 核心API
API方法名 作用 Selector.open() 打開一個選擇器。 select() 選擇一組鍵,其相應的通道已爲 I/O 操作準備就緒。 selectedKeys() 返回此選擇器的已選擇鍵集。
以上的API會在後面的例子用到,先有個印象。
3 NIO快速入門
3.1 文件IO
3.1.1 通道間的數據傳輸
這裏主要介紹兩個通道與通道之間數據傳輸的方式:
transferTo():把源通道的數據傳輸到目的通道中。
public static void main(String[] args) throws Exception {
//獲取文件輸入流
File file = new File("1.txt");
FileInputStream inputStream = new FileInputStream(file);
//從文件輸入流獲取通道
FileChannel inputStreamChannel = inputStream.getChannel();
//獲取文件輸出流
FileOutputStream outputStream = new FileOutputStream(new File("2.txt"));
//從文件輸出流獲取通道
FileChannel outputStreamChannel = outputStream.getChannel();
//創建一個byteBuffer,小文件所以就直接一次讀取,不分多次循環了
ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length());
//把輸入流通道的數據讀取到輸出流的通道
inputStreamChannel.transferTo(0, byteBuffer.limit(), outputStreamChannel);
//關閉通道
outputStream.close();
inputStream.close();
outputStreamChannel.close();
inputStreamChannel.close();
}
複製代碼
transferFrom():把來自源通道的數據傳輸到目的通道。
public static void main(String[] args) throws Exception {
//獲取文件輸入流
File file = new File("1.txt");
FileInputStream inputStream = new FileInputStream(file);
//從文件輸入流獲取通道
FileChannel inputStreamChannel = inputStream.getChannel();
//獲取文件輸出流
FileOutputStream outputStream = new FileOutputStream(new File("2.txt"));
//從文件輸出流獲取通道
FileChannel outputStreamChannel = outputStream.getChannel();
//創建一個byteBuffer,小文件所以就直接一次讀取,不分多次循環了
ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length());
//把輸入流通道的數據讀取到輸出流的通道
outputStreamChannel.transferFrom(inputStreamChannel,0,byteBuffer.limit());
//關閉通道
outputStream.close();
inputStream.close();
outputStreamChannel.close();
inputStreamChannel.close();
}
複製代碼
3.1.2 分散讀取和聚合寫入
我們先看一下FileChannel的源碼:
public abstract class FileChannel extends AbstractInterruptibleChannel
implements SeekableByteChannel, GatheringByteChannel, ScatteringByteChannel {
}
複製代碼
從源碼中可以看出實現了GatheringByteChannel, ScatteringByteChannel接口。也就是支持分散讀取和聚合寫入的操作。怎麼使用呢,請看以下例子:
我們寫一個main方法來實現複製1.txt文件,文件內容是:
abcdefghijklmnopqrstuvwxyz//26個字母
複製代碼
代碼如下:
public static void main(String[] args) throws Exception {
//獲取文件輸入流
File file = new File("1.txt");
FileInputStream inputStream = new FileInputStream(file);
//從文件輸入流獲取通道
FileChannel inputStreamChannel = inputStream.getChannel();
//獲取文件輸出流
FileOutputStream outputStream = new FileOutputStream(new File("2.txt"));
//從文件輸出流獲取通道
FileChannel outputStreamChannel = outputStream.getChannel();
//創建三個緩衝區,分別都是5
ByteBuffer byteBuffer1 = ByteBuffer.allocate(5);
ByteBuffer byteBuffer2 = ByteBuffer.allocate(5);
ByteBuffer byteBuffer3 = ByteBuffer.allocate(5);
//創建一個緩衝區數組
ByteBuffer[] buffers = new ByteBuffer[]{byteBuffer1, byteBuffer2, byteBuffer3};
//循環寫入到buffers緩衝區數組中,分散讀取
long read;
long sumLength = 0;
while ((read = inputStreamChannel.read(buffers)) != -1) {
sumLength += read;
Arrays.stream(buffers)
.map(buffer -> "posstion=" + buffer.position() + ",limit=" + buffer.limit())
.forEach(System.out::println);
//切換模式
Arrays.stream(buffers).forEach(Buffer::flip);
//聚合寫入到文件輸出通道
outputStreamChannel.write(buffers);
//清空緩衝區
Arrays.stream(buffers).forEach(Buffer::clear);
}
System.out.println("總長度:" + sumLength);
//關閉通道
outputStream.close();
inputStream.close();
outputStreamChannel.close();
inputStreamChannel.close();
}
複製代碼
打印結果:
posstion=5,limit=5
posstion=5,limit=5
posstion=5,limit=5
posstion=5,limit=5
posstion=5,limit=5
posstion=1,limit=5
總長度:26
複製代碼
可以看到循環了兩次。第一次循環時,三個緩衝區都讀取了5個字節,總共讀取了15,也就是讀滿了。還剩下11個字節,於是第二次循環時,前兩個緩衝區分配了5個字節,最後一個緩衝區給他分配了1個字節,剛好讀完。總共就是26個字節。
這就是分散讀取,聚合寫入的過程。
使用場景就是可以使用一個緩衝區數組,自動地根據需要去分配緩衝區的大小。可以減少內存消耗。網絡IO也可以使用,這裏就不寫例子演示了。
3.1.3 非直接/直接緩衝區
非直接緩衝區的創建方式:
static ByteBuffer allocate(int capacity)
複製代碼
直接緩衝區的創建方式:
static ByteBuffer allocateDirect(int capacity)
複製代碼
非直接/直接緩衝區的區別示意圖:
從示意圖中我們可以發現,最大的不同在於直接緩衝區不需要再把文件內容copy到物理內存中。這就大大地提高了性能。其實在介紹Buffer時,我們就有接觸到這個概念。直接緩衝區是堆外內存,在本地文件IO效率會更高一點。
接下來我們來對比一下效率,以一個136 MB的視頻文件爲例:
public static void main(String[] args) throws Exception {
long starTime = System.currentTimeMillis();
//獲取文件輸入流
File file = new File("D:\\小電影.mp4");//文件大小136 MB
FileInputStream inputStream = new FileInputStream(file);
//從文件輸入流獲取通道
FileChannel inputStreamChannel = inputStream.getChannel();
//獲取文件輸出流
FileOutputStream outputStream = new FileOutputStream(new File("D:\\test.mp4"));
//從文件輸出流獲取通道
FileChannel outputStreamChannel = outputStream.getChannel();
//創建一個直接緩衝區
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(5 * 1024 * 1024);
//創建一個非直接緩衝區
//ByteBuffer byteBuffer = ByteBuffer.allocate(5 * 1024 * 1024);
//寫入到緩衝區
while (inputStreamChannel.read(byteBuffer) != -1) {
//切換讀模式
byteBuffer.flip();
outputStreamChannel.write(byteBuffer);
byteBuffer.clear();
}
//關閉通道
outputStream.close();
inputStream.close();
outputStreamChannel.close();
inputStreamChannel.close();
long endTime = System.currentTimeMillis();
System.out.println("消耗時間:" + (endTime - starTime) + "毫秒");
}
複製代碼
結果:
直接緩衝區的消耗時間:283毫秒
非直接緩衝區的消耗時間:487毫秒
3.2 網絡IO
其實NIO的主要用途是網絡IO,在NIO之前java要使用網絡編程就只有用Socket。而Socket是阻塞的,顯然對於高併發的場景是不適用的。所以NIO的出現就是解決了這個痛點。
主要思想是把Channel通道註冊到Selector中,通過Selector去監聽Channel中的事件狀態,這樣就不需要阻塞等待客戶端的連接,從主動等待客戶端的連接,變成了通過事件驅動。沒有監聽的事件,服務器可以做自己的事情。
3.2.1 使用Selector的小例子
接下來趁熱打鐵,我們來做一個服務器接受客戶端消息的例子:
首先服務端代碼:
public class NIOServer {
public static void main(String[] args) throws Exception {
//打開一個ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
InetSocketAddress address = new InetSocketAddress("127.0.0.1", 6666);
//綁定地址
serverSocketChannel.bind(address);
//設置爲非阻塞
serverSocketChannel.configureBlocking(false);
//打開一個選擇器
Selector selector = Selector.open();
//serverSocketChannel註冊到選擇器中,監聽連接事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//循環等待客戶端的連接
while (true) {
//等待3秒,(返回0相當於沒有事件)如果沒有事件,則跳過
if (selector.select(3000) == 0) {
System.out.println("服務器等待3秒,沒有連接");
continue;
}
//如果有事件selector.select(3000)>0的情況,獲取事件
Set<SelectionKey> selectionKeys = selector.selectedKeys();
//獲取迭代器遍歷
Iterator<SelectionKey> it = selectionKeys.iterator();
while (it.hasNext()) {
//獲取到事件
SelectionKey selectionKey = it.next();
//判斷如果是連接事件
if (selectionKey.isAcceptable()) {
//服務器與客戶端建立連接,獲取socketChannel
SocketChannel socketChannel = serverSocketChannel.accept();
//設置成非阻塞
socketChannel.configureBlocking(false);
//把socketChannel註冊到selector中,監聽讀事件,並綁定一個緩衝區
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
//如果是讀事件
if (selectionKey.isReadable()) {
//獲取通道
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
//獲取關聯的ByteBuffer
ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();
//打印從客戶端獲取到的數據
socketChannel.read(buffer);
System.out.println("from 客戶端:" + new String(buffer.array()));
}
//從事件集合中刪除已處理的事件,防止重複處理
it.remove();
}
}
}
}
複製代碼
客戶端代碼:
public class NIOClient {
public static void main(String[] args) throws Exception {
SocketChannel socketChannel = SocketChannel.open();
InetSocketAddress address = new InetSocketAddress("127.0.0.1", 6666);
socketChannel.configureBlocking(false);
//連接服務器
boolean connect = socketChannel.connect(address);
//判斷是否連接成功
if(!connect){
//等待連接的過程中
while (!socketChannel.finishConnect()){
System.out.println("連接服務器需要時間,期間可以做其他事情...");
}
}
String msg = "hello java技術愛好者!";
ByteBuffer byteBuffer = ByteBuffer.wrap(msg.getBytes());
//把byteBuffer數據寫入到通道中
socketChannel.write(byteBuffer);
//讓程序卡在這個位置,不關閉連接
System.in.read();
}
}
複製代碼
接下來啓動服務端,然後再啓動客戶端,我們可以看到控制檯打印以下信息:
服務器等待3秒,沒有連接
服務器等待3秒,沒有連接
from 客戶端:hello java技術愛好者!
服務器等待3秒,沒有連接
服務器等待3秒,沒有連接
複製代碼
通過這個例子我們引出以下知識點。
3.2.2 SelectionKey
在SelectionKey類中有四個常量表示四種事件,來看源碼:
public abstract class SelectionKey {
//讀事件
public static final int OP_READ = 1 << 0; //2^0=1
//寫事件
public static final int OP_WRITE = 1 << 2; // 2^2=4
//連接操作,Client端支持的一種操作
public static final int OP_CONNECT = 1 << 3; // 2^3=8
//連接可接受操作,僅ServerSocketChannel支持
public static final int OP_ACCEPT = 1 << 4; // 2^4=16
}
複製代碼
附加的對象(可選),把通道註冊到選擇器中時可以附加一個對象。
public final SelectionKey register(Selector sel, int ops, Object att)
複製代碼
從selectionKey中獲取附件對象可以使用attachment()方法
public final Object attachment() {
return attachment;
}
複製代碼
4 使用NIO實現多人聊天室
接下來進行一個實戰例子,用NIO實現一個多人運動版本的聊天室。
服務端代碼:
public class GroupChatServer {
private Selector selector;
private ServerSocketChannel serverSocketChannel;
public static final int PORT = 6667;
//構造器初始化成員變量
public GroupChatServer() {
try {
//打開一個選擇器
this.selector = Selector.open();
//打開serverSocketChannel
this.serverSocketChannel = ServerSocketChannel.open();
//綁定地址,端口號
this.serverSocketChannel.bind(new InetSocketAddress("127.0.0.1", PORT));
//設置爲非阻塞
serverSocketChannel.configureBlocking(false);
//把通道註冊到選擇器中
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 監聽,並且接受客戶端消息,轉發到其他客戶端
*/
public void listen() {
try {
while (true) {
//獲取監聽的事件總數
int count = selector.select(2000);
if (count > 0) {
Set<SelectionKey> selectionKeys = selector.selectedKeys();
//獲取SelectionKey集合
Iterator<SelectionKey> it = selectionKeys.iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
//如果是獲取連接事件
if (key.isAcceptable()) {
SocketChannel socketChannel = serverSocketChannel.accept();
//設置爲非阻塞
socketChannel.configureBlocking(false);
//註冊到選擇器中
socketChannel.register(selector, SelectionKey.OP_READ);
System.out.println(socketChannel.getRemoteAddress() + "上線了~");
}
//如果是讀就緒事件
if (key.isReadable()) {
//讀取消息,並且轉發到其他客戶端
readData(key);
}
it.remove();
}
} else {
System.out.println("等待...");
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
//獲取客戶端發送過來的消息
private void readData(SelectionKey selectionKey) {
SocketChannel socketChannel = null;
try {
//從selectionKey中獲取channel
socketChannel = (SocketChannel) selectionKey.channel();
//創建一個緩衝區
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//把通道的數據寫入到緩衝區
int count = socketChannel.read(byteBuffer);
//判斷返回的count是否大於0,大於0表示讀取到了數據
if (count > 0) {
//把緩衝區的byte[]轉成字符串
String msg = new String(byteBuffer.array());
//輸出該消息到控制檯
System.out.println("from 客戶端:" + msg);
//轉發到其他客戶端
notifyAllClient(msg, socketChannel);
}
} catch (Exception e) {
try {
//打印離線的通知
System.out.println(socketChannel.getRemoteAddress() + "離線了...");
//取消註冊
selectionKey.cancel();
//關閉流
socketChannel.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
/**
* 轉發消息到其他客戶端
* msg 消息
* noNotifyChannel 不需要通知的Channel
*/
private void notifyAllClient(String msg, SocketChannel noNotifyChannel) throws Exception {
System.out.println("服務器轉發消息~");
for (SelectionKey selectionKey : selector.keys()) {
Channel channel = selectionKey.channel();
//channel的類型實際類型是SocketChannel,並且排除不需要通知的通道
if (channel instanceof SocketChannel && channel != noNotifyChannel) {
//強轉成SocketChannel類型
SocketChannel socketChannel = (SocketChannel) channel;
//通過消息,包裹獲取一個緩衝區
ByteBuffer byteBuffer = ByteBuffer.wrap(msg.getBytes());
socketChannel.write(byteBuffer);
}
}
}
public static void main(String[] args) throws Exception {
GroupChatServer chatServer = new GroupChatServer();
//啓動服務器,監聽
chatServer.listen();
}
}
複製代碼
客戶端代碼:
public class GroupChatClinet {
private Selector selector;
private SocketChannel socketChannel;
private String userName;
public GroupChatClinet() {
try {
//打開選擇器
this.selector = Selector.open();
//連接服務器
socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", GroupChatServer.PORT));
//設置爲非阻塞
socketChannel.configureBlocking(false);
//註冊到選擇器中
socketChannel.register(selector, SelectionKey.OP_READ);
//獲取用戶名
userName = socketChannel.getLocalAddress().toString().substring(1);
System.out.println(userName + " is ok~");
} catch (Exception e) {
e.printStackTrace();
}
}
//發送消息到服務端
private void sendMsg(String msg) {
msg = userName + "說:" + msg;
try {
socketChannel.write(ByteBuffer.wrap(msg.getBytes()));
} catch (Exception e) {
e.printStackTrace();
}
}
//讀取服務端發送過來的消息
private void readMsg() {
try {
int count = selector.select();
if (count > 0) {
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
//判斷是讀就緒事件
if (selectionKey.isReadable()) {
SocketChannel channel = (SocketChannel) selectionKey.channel();
//創建一個緩衝區
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//從服務器的通道中讀取數據到緩衝區
channel.read(byteBuffer);
//緩衝區的數據,轉成字符串,並打印
System.out.println(new String(byteBuffer.array()));
}
iterator.remove();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws Exception {
GroupChatClinet chatClinet = new GroupChatClinet();
//啓動線程,讀取服務器轉發過來的消息
new Thread(() -> {
while (true) {
chatClinet.readMsg();
try {
Thread.sleep(3000);
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
//主線程發送消息到服務器
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()) {
String msg = scanner.nextLine();
chatClinet.sendMsg(msg);
}
}
}
複製代碼
先啓動服務端的main方法,再啓動兩個客戶端的main方法:
然後使用兩個客戶端開始聊天了~
以上就是使用NIO實現多人聊天室的例子,同學們可以看着我這個例子自己完成一下。要多寫代碼纔好理解這些概念。