NIO實現網絡聊天室
1. NIO完成網絡編程
1.1 Selector選擇器老大
Selector
選擇器,網絡編程使用NIO的大哥!!!
服務器可以執行一個線程,運行Selector程序,進行監聽操作。
新連接, 已經連接, 讀取數據,寫入數據
Selector常用方法:
public static Selector Open();
得到一個選擇器對象
public int select(long timeout);
監聽所有註冊通道,存在IO流操作是,會將對應的信息SelectionKey存入到內部的集
閤中,參數是一個超時時間
public Set<SelectionKey> selectionKeys();
返回當前Selector內部集合中保存的所有SelectionKey
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Yp9zg3qj-1584977796519)(img/Selector大哥.png)]
1.2 SelectionKey
SelectionKey
表示Selector和網絡通道直接的關係
int OP_ACCEPT; 16 需要連接
int OP_CONNECT; 8 已經連接
int OP_READ; 1 讀取操作
int OP_WRITE; 4 寫入操作
SelectionKey
public abstract Selector selector();
得到與之關聯的 Selector 對象
public abstract SelectableChannel channel();
得到與之關聯的通道
public final Object attachment();
得到與之關聯的共享數據
public abstract SelectionKey interestOps(int ops);
設置或改變監聽事件
public final boolean isAcceptable();
是否可以 accept
public final boolean isReadable();
是否可以讀
public final boolean isWritable();
是否可以寫
1.3 ServerSocketChannel
ServerSocketChannel
服務端Socket程序對應的Channel通道
常用方法:
public static ServerSocketChannel open();
開啓服務器ServerSocketChannel通道,等於開始服務器程序
public final ServerSocketChannel bind(SocketAddress local);
設置服務器端端口號
public final SelectableChannel configureBlocking(boolean block);
設置阻塞或非阻塞模式, 取值 false 表示採用非阻塞模式
public SocketChannel accept();
[非阻塞]
獲取一個客戶端連接,並且得到對應的操作通道
public final SelectionKey register(Selector sel, int ops);
[重點方法]
註冊當前選擇器,並且選擇監聽什麼事件
1.4 SocketChannel
SocketChannel
客戶端Socket對應的Channel對象
常用方法:
public static SocketChannel open();
打卡一個Socket客戶端Channel對象
public final SelectableChannel configureBlocking(boolean block)
這裏可以設置是阻塞狀態,還是非阻塞狀態
false,表示非阻塞
public boolean connect(SocketAddress remote);
連接服務器
public boolean finishConnect();
如果connect連接失敗,可以通過finishConnect繼續連接
public int write(ByteBuffer buf);
寫入數據到緩衝流中
public int read(ByteBuffer buf); 、
從緩衝流中讀取數據
public final SelectionKey register(Selector sel, int ops, Object attechment);
註冊當前SocketChannel,選擇對應的監聽操作,並且可以帶有Object attachment參數
public final void close();
關閉SocketChannel
1.5 使用NIO完成一個客戶端和服務器
1.5.1 首先完成客戶端
package com.qfedu.a_tcpnio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.Scanner;
import java.util.concurrent.ThreadPoolExecutor;
/**
* 符合TCP協議,非阻塞IO NIO完成對應的客戶端代碼
*
* @author Anonymous 2020/3/16 15:10
*/
public class TcpNioClient {
public static void main(String[] args) throws IOException, InterruptedException {
// 1. 得到一個網絡通道
SocketChannel socket = SocketChannel.open();
// 2. 設置當前NIO採用的方式爲非阻塞方式
socket.configureBlocking(false);
// 3. 確定服務器IP地址和對應程序端口號,創建一個InetSocketAddress對象
InetSocketAddress address = new InetSocketAddress("192.168.31.154", 8848);
// 4. 連接服務器
if (!socket.connect(address)) {
// 如果是false,表示連接失敗,保持申請連接的狀態
while (!socket.finishConnect()) {
// 因爲採用NIO非阻塞方式,在獲取等待連接的狀態下,可以去做當前程序的其他操作。
System.out.println("保持呼叫服務器狀態,但是我還能做點別的事情~~~ 等待2s繼續申請連接~~~");
Thread.sleep(2000);
}
}
// 5. 準備一個數據存入到緩衝區
ByteBuffer buffer = ByteBuffer.wrap("你好,服務器,我在等你...".getBytes());
// 6. 通過SocketChannel 符合TCP協議Socket要求Channel對象發送
socket.write(buffer);
new Scanner(System.in).nextLine();
}
}
1.5.2 再來完成服務端
1. 開啓服務器
ServerScoketChannel
2. 開啓Selector 大哥
Selector對象
3. 服務器ServerSocketChannel bind監聽端口號
8848端口
4. 設置非阻塞狀態
configureBlocking(false)
5. Selector 註冊--> ServerSocketChannel
register(selector, OP_ACCEPT);
6. Selector大哥開始忙活
6.1 獲取連接,註冊對應的Socket
6.2 監聽讀寫事件
6.2.1 讀數據。客戶端發送數據到服務器
6.2.2 寫數據。發送數據數據給客戶端
package com.qfedu.a_tcpnio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
/**
* 使用ServerSocketChannel NIO非阻塞方式,完成服務端代碼
*
* 1. 開啓服務器
* ServerScoketChannel
* 2. 開啓Selector 大哥
* Selector對象
* 3. 服務器ServerSocketChannel bind監聽端口號
* 8848端口
* 4. 設置非阻塞狀態
* configureBlocking(false)
* 5. ServerSocketChannel 註冊--> Selector
* register(selector, OP_ACCEPT);
*
* 6. Selector大哥開始忙活
* 6.1 獲取連接,註冊對應的Socket
* 6.2 監聽讀寫事件
* 6.2.1 讀數據。客戶端發送數據到服務器
* 6.2.2 寫數據。發送數據數據給客戶端
*
*
* @author Anonymous 2020/3/16 15:44
*/
public class TcpNioServer {
public static void main(String[] args) throws IOException {
// 1. 開啓服務器
ServerSocketChannel serverSocket = ServerSocketChannel.open();
// 2. 開啓Selector 大哥
Selector selector = Selector.open();
// 3. 服務端代碼綁定端口號
serverSocket.bind(new InetSocketAddress(8848));
// 4. 設置非阻塞狀態
serverSocket.configureBlocking(false);
// 5. ServerSocketChannel 註冊--> Selector 返回值是一個SelectionKey
// 並且明確當前Selector監聽SelectionKey.OP_ACCEPT,監聽連接服務器
serverSocket.register(selector, SelectionKey.OP_ACCEPT);
// 6. 大哥幹活
while (true) {
// 6.1 獲取連接,註冊對應的Socket
if (0 == selector.select(1000)) {
// 0 == selector.select(1000) 表示沒有連接到客戶端
System.out.println("ServerSocket提示,當前沒有客戶端搭理我,我自己默默的畫圈圈~~~");
continue;
}
// 6.2 監聽讀寫事件
// 得到當前Selector中所有的SelectionKey
Iterator<SelectionKey> selectionKeys = selector.selectedKeys().iterator();
while (selectionKeys.hasNext()) {
SelectionKey selectionKey = selectionKeys.next();
// 6.2.1 判斷客戶端是一個連接請求 OP_ACCEPT
if (selectionKey.isAcceptable()) {
System.out.println("客戶端請求連接!!!");
// 獲取對應的Socket,只不過這裏是獲取對應的SocketChannel
SocketChannel socket = serverSocket.accept();
// 設置當前對應客戶端的SocketChannel對象是一個非阻塞狀態
socket.configureBlocking(false);
/*
註冊當前Socket對象
selector 註冊到當前的Selector【核心】
SelectionKey.OP_READ 選擇當前Socket監聽的操作內容是OP_READ 從當前Socket中讀取數據
ByteBuffer.allocate(1024 * 4) attachment 補充參數,這裏是給予當前Socket對象一個4KB 字節緩衝區對象
*/
socket.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024 * 4));
}
// 6.2.2 判斷客戶端目前是可讀狀態,獲取客戶端發送給服務器的數據
if (selectionKey.isReadable()) {
// 從SelectionKey 中獲取對應的SocketChannel對象
SocketChannel socket = (SocketChannel) selectionKey.channel();
// 因爲使用的是NIO,涉及到Channel和ByteBuffer,數據在緩衝區中
ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();
// 讀取數據。利用SocketChannel從緩衝中ByteBuffer中讀取數據。
socket.read(buffer);
System.out.println("客戶端發送數據:" + new String(buffer.array()));
}
// 處理完進行一個移除當前SelectionKey操作
selectionKeys.remove();
}
}
}
}