Java知識點——NIO實現網絡聊天室

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();
            }
        }


    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章