1. 程式人生 > 實用技巧 >Java nio 簡易練手版 模擬使用者群聊

Java nio 簡易練手版 模擬使用者群聊

前兩天簡單瞭解了一下socket的知識,然後發現這一塊貌似與io流有不可分割的關係,於是著手複習了一下io方面的知識。到了這裡就順理成章地想到了nio,這方面的知識屬於完全陌生的狀態,之前一直沒有用到,所以也就沒有去學習。在網上花了些時間找到了一份講解nio的文件,這東西...好倒是挺好...就是看完一遍感覺好多東西看了又忘,尤其是一些方法名什麼的,算了,問題不大,反正思路大概有一些了,而且編譯器有提示。按照我一貫的學習套路,這個時候肯定是少不了一波實操,管他那麼多?幹就完事了。

首先,花了點時間構思了一下“伺服器”應該有啥功能,儲存使用者的通道必須得有,不然怎麼實現核心的群發功能呢?心跳機制搞一個?(後來覺得太麻煩,簡單的封裝了一下就沒繼續寫,而且關鍵在於我這是為學習netty打基礎,貌似沒必要在這裡花太多時間),用選擇器對通道進行監控,這個必須有。使用者的唯一標識就用LongAdder模擬一下吧,使用者名稱就用標識拼接一下,這些都從簡就好。

public class Server {

    public static int port;
    private LongAdder longAdder = new LongAdder();
    private ServerSocketChannel channel = null;
    private Selector selector = Selector.open();
    private Map<Long, NClientChannel> clients = new ConcurrentHashMap<>();
    private Charset charset = Charset.forName("utf-8");
    
private ByteBuffer recBuf = ByteBuffer.allocate(10240); private Server() throws IOException { } public void simpleInit(Integer port) throws IOException { if (port == null) init(); else init(port); listen(); } private void init() throws
IOException { init(8080); } private void init(int port) throws IOException { channel = ServerSocketChannel.open(); channel.configureBlocking(false); channel.bind(new InetSocketAddress("0.0.0.0", port)); this.port = port; channel.register(selector, SelectionKey.OP_ACCEPT); } private void listen() throws IOException { for (;;) { selector.select(); Set<SelectionKey> channel = selector.selectedKeys(); channel.forEach(selectionKey -> { try { choose(selectionKey); } catch (IOException e) { e.printStackTrace(); } }); channel.clear(); } } private void choose(SelectionKey selectionKey) throws IOException { if (selectionKey.isAcceptable()) { register(selectionKey); } else if (selectionKey.isReadable()) { chat(selectionKey); } } private void chat(SelectionKey selectionKey) throws IOException { SocketChannel client = (SocketChannel) selectionKey.channel(); long id = (long)(selectionKey.attachment()); recBuf.put((id + "號使用者:").getBytes()); if (client.read(recBuf) > 0) { recBuf.flip(); if (!clients.isEmpty()) { for (Map.Entry<Long, NClientChannel> entry : clients.entrySet()) { if (entry.getKey() != id) { NClientChannel temp = entry.getValue(); temp.getChannel().write(charset.encode(String.valueOf(charset.decode(recBuf)))); } } } recBuf.compact(); } } private void register(SelectionKey selectionKey) throws IOException { SocketChannel client = ((ServerSocketChannel)selectionKey.channel()).accept(); client.configureBlocking(false); client.register(selector, SelectionKey.OP_READ, longAdder.longValue()); clients.put(longAdder.longValue(), new NClientChannel(client, Calendar.getInstance())); longAdder.increment(); } public static void main(String[] args) throws IOException { new Server().simpleInit(8090); } }

其實本來應該做一些抽象和封裝的工作,由於是練習(主要是太懶),就從簡吧,主方法在最下面,啟動即可。

public class Client {
    private Selector selector = Selector.open();
    private ByteBuffer outBuf = ByteBuffer.allocate(10240);
    private ByteBuffer showBuf = ByteBuffer.allocate(10240);
    private Charset charset = Charset.forName("UTF-8");

    public Client() throws IOException {

    }

    public void simpleInit() throws IOException {
        init();
        listen();
    }

    private void init() throws IOException {
        SocketChannel channel = SocketChannel.open();
        channel.configureBlocking(false);
        channel.register(selector, SelectionKey.OP_CONNECT);
        channel.connect(new InetSocketAddress("0.0.0.0", 8090));
    }

    private void listen() throws IOException {
        for (;;) {
            selector.select();
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            selectionKeys.forEach(selectionKey -> {
                try {
                    choose(selectionKey);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });
            selectionKeys.clear();
        }
    }

    private void choose(SelectionKey selectionKey) throws IOException {
        if (selectionKey.isConnectable()) {
            finishReg((SocketChannel) selectionKey.channel());
        } else if (selectionKey.isReadable()) {
            show((SocketChannel) selectionKey.channel());
        }
    }

    private void show(SocketChannel channel) throws IOException {
        showBuf.clear();
        int sum = channel.read(showBuf);
        if (sum > 0) {
            String receive = new String(showBuf.array(), 0, sum, "utf-8");
            System.out.println(receive);
        }
    }

    private void finishReg(SocketChannel channel) throws IOException {
        channel.finishConnect();
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (;;) {
                    Scanner scanner = new Scanner(System.in);
                    outBuf.put(charset.encode(scanner.nextLine()));
                    outBuf.flip();
                    try {
                        channel.write(outBuf);
                        outBuf.clear();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
        channel.register(selector, SelectionKey.OP_READ);
    }
}

客戶類中單獨開的執行緒是為了讓我們可以在控制檯輸入想要傳送的內容。

public class NClientChannel {

    private SocketChannel channel;
    private long time;

    public NClientChannel(SocketChannel client, Calendar instance) {
        channel = client;
        time = instance.getTimeInMillis();
    }

    public SocketChannel getChannel() {
        return channel;
    }

    public void setChannel(SocketChannel channel) {
        this.channel = channel;
    }

    public long getTime() {
        return time;
    }

    public void setTime(long time) {
        this.time = time;
    }
}

本來是打算加上心跳機制的,所以進行了一下簡單的封裝... ...此處推薦lombok

public class ClientTwo {
    public static void main(String[] args) throws IOException {
        new Client().simpleInit();
    }
}

這個是第二個客戶端的啟動類,第一個客戶端的啟動類和它名字不同,內容一樣。

為什麼要搞兩個啟動類呢?為了可以收穫兩個控制檯...

以上的程式碼參考了網上的文章和文件,然後再加上自己的一些想法,然後就沒有然後了...