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

这个是第二个客户端的启动类,第一个客户端的启动类和它名字不同,内容一样。

为什么要搞两个启动类呢?为了可以收获两个控制台...

以上的代码参考了网上的文章和文档,然后再加上自己的一些想法,然后就没有然后了...

posted @ 2020-08-11 10:51  无心大魔王  阅读(244)  评论(0编辑  收藏  举报