Java知識點——NIO完成一個TCP聊天室

1. NIO完成一個TCP聊天室

1.1 NIO TCP聊天室客戶端完成
package com.qfedu.b_niochat;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

/**
 * NIO 非阻塞狀態的TCP聊天室客戶端核心代碼
 *
 * @author Anonymous 2020/3/16 16:20
 */
public class ChatClient {
    /**
     * 服務器IP地址
     */
    private static final String HOST = "192.168.31.154";

    /**
     * 服務器連接對應的端口號
     */
    private static final int PORT = 8848;

    /**
     * 返回NIO要求是ScoketChannel對象
     */
    private SocketChannel socket;

    /**
     * 用戶名
     */
    private String userName;

    /**
     * 客戶端構造方法,創建客戶端對象
     *
     * @param userName 指定的用戶名
     */
    public ChatClient(String userName) throws IOException, InterruptedException {
        // 1. 打開SocketChannel
        socket = SocketChannel.open();

        // 2. 設置非阻塞狀態
        socket.configureBlocking(false);

        // 3. 根據指定的HOST IP地址和對應PORT端口號創建對應的 InetSocketAddress
        InetSocketAddress address = new InetSocketAddress(HOST, PORT);

        // 4. 連接服務器
        if (!socket.connect(address)) {
            // 如果沒有連接到服務器,保持請求連接的狀態
            while (!socket.finishConnect()) {
                System.out.println("服務器請求連接失敗,等待2s繼續請求連接...");
                Thread.sleep(2000);
            }
        }

        this.userName = userName;

        System.out.println("客戶端 " + userName + " 準備就緒");
    }

    /*
    這裏需要完成兩個方法,一個是發送數據到服務器,一個是接受服務器發送的數據
     */

    /**
     * 發送數據到服務器,用於廣播消息,羣聊
     *
     * @param message 指定的消息
     */
    public void sendMsg(String message) throws IOException {
        // 斷開服務器連接 close
        if ("close".equals(message)) {
            socket.close();
            return;
        }
        /*
        StringBuffer
            線程安全,效率低
        StringBuilder
            線程不安全,效率高
         */
        message = userName + ":" + message;
        ByteBuffer buffer = ByteBuffer.wrap(message.getBytes());
        socket.write(buffer);
    }

    public void receiveMsg() throws IOException {
        // 準備ByteBuffer
        ByteBuffer buffer = ByteBuffer.allocate(1024);

        int length = socket.read(buffer);
        if (length > 0) {
            System.out.println(new String(buffer.array()));
        }
    }
}
1.2 NIO TCP聊天室服務端完成
package com.qfedu.b_niochat;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;

/**
 * NIO 非阻塞狀態的TCP聊天室服務端核心代碼
 *
 * @author Anonymous 2020/3/16 16:59
 */
public class ChatServer {

    /**
     * 服務器核心模塊,ServerSocketChannel
     */
    private ServerSocketChannel serverSocket;

    /**
     * 服務端NIO Selector選擇器
     */
    private Selector selector;

    /**
     * 服務端監聽服務指定端口號
     */
    private static final int PORT = 8848;

    /*
    1. 構造方法
    2. 接收方法
    3. 發送方法(廣播)
    4. 同時啓動接收和發送功能 start
     */

    /**
     * 服務器構造方法,開啓ServerSocketChannel,同時開啓Selector,註冊操作
     *
     * @throws IOException 異常
     */
    public ChatServer() throws IOException {
        // 1. 啓動服務器SocketServer NIO服務器
        serverSocket = ServerSocketChannel.open();

        // 2. 啓動選擇器
        selector = Selector.open();

        // 3. 端口綁定
        serverSocket.bind(new InetSocketAddress(PORT));

        // 4. 選擇NIO方式爲非阻塞狀態
        serverSocket.configureBlocking(false);

        // 5. 註冊SeverSocket,確定當前監聽的狀態是OP_ACCEPT
        serverSocket.register(selector, SelectionKey.OP_ACCEPT);
    }

    /**
     * 服務器幹活方法,指定客戶端綁定,數據接收和轉發
     */
    public void start(){
        try {
            while (true) {
                if (0 == selector.select(2000)) {
                    System.out.println("服務器默默的等待連接,無人訪問...");
                    continue;
                }

                /*
                selectedKeys:
                    獲取當前所有發生事件操作的對應SelectionKey Set集合
                 */
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();

                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    // 1. 連接
                    if (key.isAcceptable()) {
                        // 連接客戶端,獲取對應的SocketChannel對象
                        SocketChannel socket = serverSocket.accept();
                        socket.configureBlocking(false);
                        socket.register(selector, SelectionKey.OP_READ);

                        // 廣播上線
                        broadcast(socket, socket.getRemoteAddress().toString() + "上線了");
                    }

                    // 2. 接收數據轉發
                    if (key.isReadable()) {
                        readMsg(key);
                    }

                    iterator.remove();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 從指定的SelectionKey中讀取數據
     *
     * @param key 符合OP_READ 要求的SelectionKey
     */
    public void readMsg(SelectionKey key) throws IOException {
        // 根據指定SelectionKey獲取對應的SocketChannel對象
        SocketChannel socket = (SocketChannel) key.channel();

        // 創建緩衝區
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        // 從緩衝區中讀取數據,返回值類型是讀取到的字節數
        int length = socket.read(buffer);

        // 因爲讀取的數據可能存在0的情況
        if (length > 0) {
            String message = new String(buffer.array());

            // 廣播數據
            broadcast(socket, message);
        }
    }

    /**
     * 廣播方法,該方法是羣發消息,但是不要發給自己
     *
     * @param self 當前發送數據的客戶端
     * @param message 消息
     */
    public void broadcast(SocketChannel self, String message) throws IOException {
        /*
         獲取當前Selector選擇器中所有的SelectionKey
         Selector中註冊的內容有SocketChannel對應的SelectionKey
         已經ServerSocketChannel對應的SelectionKey
        */
        Set<SelectionKey> keys = selector.keys();

        // 遍歷整個SelectionKey Set集合
        for (SelectionKey key : keys) {
            // 獲取對應SelectionKey的Channel對象
            SelectableChannel channel = key.channel();

            // 第一: channel對應的是一個SocketChannel對象,並且不是當前發送消息的SocketChannel對象
            if (channel instanceof SocketChannel && !channel.equals(self)) {
                SocketChannel socketChannel = (SocketChannel) channel;
                // 根據指定的Byte類型數組,創建對應的ByteBuffer緩衝區
                ByteBuffer buffer = ByteBuffer.wrap(message.getBytes());

                // 發送數據
                socketChannel.write(buffer);
            }
        }
    }

    public static void main(String[] args) throws IOException {
        new ChatServer().start();
    }

}
1.3 NIO TCP聊天室客戶端線程代碼實現
package com.qfedu.b_niochat;

import java.io.IOException;
import java.util.Scanner;

/**
 * 客戶端線程代碼
 *
 * @author Anonymous 2020/3/16 17:37
 */
public class ChatClientThread {
    public static void main(String[] args) throws IOException, InterruptedException {
        Scanner scanner = new Scanner(System.in);

        System.out.println("請輸入用戶名:");
        String userName = scanner.nextLine();

        if (0 == userName.length()) {
            return;
        }

        ChatClient chatClient = new ChatClient(userName);

        // 接收消息
        new Thread(() -> {
            while (true) {
                try {
                    chatClient.receiveMsg();
                    Thread.sleep(2000);
                } catch (IOException | InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        // 發送消息
        while (scanner.hasNextLine()) {
            String msg = scanner.nextLine();

            chatClient.sendMsg(msg);
        }
    }
}

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