1. 程式人生 > 程式設計 >初識網路程式設計NIO實現多人聊天室

初識網路程式設計NIO實現多人聊天室

New I/O

回顧BIO程式設計模型

服務端對於每個到達的客戶端都重新開啟一個執行緒專門處理它們之間的互動。 這種互動在邏輯上是客戶端與服務端直接進行通訊。 隨著高併發的場景到來,伺服器處理上下文切換,建立和銷燬執行緒的代價,將會讓伺服器不堪重負。

NIO程式設計模型構想

NIO in JAVA

Channel --> 區別於單向的InputStream/OutputStream,它是雙向的。 Selector --> 主要的控制器 Buffer --> 讀寫兩種模式可以通過flip切換。

多人聊天室實戰

Server 服務端

啟動方法

ServerSocketChannel先建立,後繫結一個埠。 設定為非阻塞模式。 將channel註冊到selector上,監聽連線事件。 開始迴圈等待新接入的連線。

迴圈內: 每次呼叫selector.select()將會阻塞地等待至少一個channel準備就緒,返回準備就緒地channel數量。 如果數量為零,開始下一輪select(); 數量不為零,則將這些準備就緒地channel取出來。 根據這些channel對應的當初向selector註冊的型別(accept/read),執行對應的業務邏輯。

public void start() throws IOException {

    Selector selector = Selector.open();
    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    serverSocketChannel.bind(new
InetSocketAddress(8000)); serverSocketChannel.configureBlocking(false); serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT); System.out.println("伺服器啟動成功!"); for (;;) { int readyChannels = selector.select(); if (readyChannels == 0) continue; Set<SelectionKey> selectionKeys = selector.selectedKeys(); Iterator iterator = selectionKeys.iterator(); while
(iterator.hasNext()) { SelectionKey selectionKey = (SelectionKey) iterator.next(); if (selectionKey.isAcceptable()) { acceptHandler(serverSocketChannel,selector); } if (selectionKey.isReadable()) { readHandler(selectionKey,selector); } } } } 複製程式碼

連線建立處理

建立連線成功後,設定非阻塞模式,並且將這個剛剛建立的channel,註冊到服務端的Selector。

private void acceptHandler(ServerSocketChannel serverSocketChannel,Selector selector)
        throws IOException {
    SocketChannel socketChannel = serverSocketChannel.accept();
    socketChannel.configureBlocking(false);
    socketChannel.register(selector,SelectionKey.OP_READ);
    socketChannel.write(Charset.forName("UTF-8")
            .encode("你與聊天室裡其他人都不是朋友關係,請注意隱私安全"));
}
複製程式碼

訊息讀取處理

private void readHandler(SelectionKey selectionKey,Selector selector)
        throws IOException {
    SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

    String request = "";
    while (socketChannel.read(byteBuffer) > 0) {
        byteBuffer.flip();
        request += Charset.forName("UTF-8").decode(byteBuffer);
    }

	// 將channel再次註冊到selector上,監聽他的可讀事件
    socketChannel.register(selector,SelectionKey.OP_READ);

    if (request.length() > 0) {
        broadCast(selector,socketChannel,request);
    }
}
複製程式碼

訊息廣播

private void broadCast(Selector selector,SocketChannel sourceChannel,String request) {
	
	// 獲取到所有已經接入的客戶端channel
	Set<SelectionKey> selectionKeySet = selector.keys();

	selectionKeySet.forEach(selectionKey -> {
	    Channel targetChannel = selectionKey.channel();

	    // 剔除發訊息的客戶端
	    if (targetChannel instanceof SocketChannel
	            && targetChannel != sourceChannel) {
	        try {
	            // 將資訊傳送到targetChannel客戶端
	            ((SocketChannel) targetChannel).write(
	                    Charset.forName("UTF-8").encode(request));
	        } catch (IOException e) {
	            e.printStackTrace();
	        }
	    }
	});
}

複製程式碼

Client 客戶端

啟動方法

public void start(String nickname) throws IOException {

    SocketChannel socketChannel = SocketChannel.open(
            new InetSocketAddress("127.0.0.1",8000));

    Selector selector = Selector.open();
    socketChannel.configureBlocking(false);
    socketChannel.register(selector,SelectionKey.OP_READ);
    //開啟一個執行緒專門處理服務端發來的訊息
    new Thread(new NioClientHandler(selector)).start();

    //向服務端傳送訊息
    Scanner scanner = new Scanner(System.in);
    while (scanner.hasNextLine()) {
        String request = scanner.nextLine();
        if (request != null && request.length() > 0) {
            socketChannel.write(
                    Charset.forName("UTF-8")
                            .encode(nickname + " : " + request));
        }
    }
}
複製程式碼

處理服務端訊息

與服務端的start方法類似。 但客戶端的Selector只註冊了一個讀事件的SocketChannel。 因此該Selector,實際上就只是不斷地監聽服務端有沒有訊息傳過來。 如果有訊息傳來那麼該Selector中繫結的這個唯一的channel就會程式設計已經就緒的狀態,將會執行它的readHadler()。 所以客戶端使用NIO和BIO的效能影響差別不大。

@Override
public void run() {
    try {
        for (;;) {
            int readyChannels = selector.select();
            if (readyChannels == 0) continue;

            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey selectionKey = (SelectionKey) iterator.next();

                if (selectionKey.isReadable()) {
                    readHandler(selectionKey,selector);
                }
            }
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}
複製程式碼

readHandler

private void readHandler(SelectionKey selectionKey,Selector selector)
        throws IOException {
    SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

    String response = "";
    while (socketChannel.read(byteBuffer) > 0) {
        byteBuffer.flip();
        response += Charset.forName("UTF-8").decode(byteBuffer);
    }

    socketChannel.register(selector,SelectionKey.OP_READ);

    if (response.length() > 0) {
        System.out.println(response);
    }
}
複製程式碼