Java NIO网络编程之群聊系统

概述

对ServerSocketChannel、SocketChannel、SelectionKey有一定的理解和了解对应API。

NIO非阻塞网络编程相关关系梳理:

在这里插入图片描述
以下概念:
ServerSocketChannel:可以类比BIO网络编程的ServerSocket,属于服务端的Socket,用于监听接收客户端连接。
SocketChannel:类比Socket,用于连接两台计算机的套接字,只是支持非阻塞。
Selector:多路复用器。
SelectionKey:与Channel进行绑定,Selector监听事件发生会返回SelectionKey,然后根据相应的SelectionKey获得对应的Channel进行操作。

流程:

  1. 服务端创建ServerSocketChanel,用于监听客户端连接,此时ServerSocketChannel也要注册到Selector中。
  2. Selector进行监听各种事件发生。
  3. 当有客户端连接进来时,发产生ServerSocketChannel的ACCEPT事件,然后可以获取连接进来的SelectionKey。
  4. 根据SelectionKey获取对应SocketChannel,然后注册到Selector,可以注册成读事件或者写事件,如果读写完成,则触发该事件。
  5. 完成业务处理。
群聊系统

要求:

  1. 编写一个NIO群聊系统,实现服务器端与客户端之间的简单数据通讯。
  2. 实现多人群聊。
  3. 服务器端可以检测用户上线、下线、并实现消息转发功能。
  4. 客户端通过channel可以无阻塞发送消息给其他所有在线用户,同时接受其他用户发送的消息。

目的:通过一个小demo熟悉相关NIO非阻塞网络编程API和机制的应用。

//服务端

public class GroupChatServer {
    private  int port;

    private Selector selector;

    private ServerSocketChannel serverSocketChannel;

    public GroupChatServer(int port){
        this.port = port;
    }

    public void start() throws IOException {
        serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.bind(new InetSocketAddress(port));
        //设置非阻塞
        serverSocketChannel.configureBlocking(false);

        selector = Selector.open();

        //把serverSocketChannel自身注册到selector上
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("启动成功");

        try {
            while (true){
                selector.select();

                //获得发生了事件的SelectionKey集合
                Set<SelectionKey> selectionKeys = selector.selectedKeys();

                Iterator<SelectionKey> iterator = selectionKeys.iterator();

                while (iterator.hasNext()){
                    //遍历处理事件
                    SelectionKey next = iterator.next();
                    if (next.isAcceptable()){
                        //处理发生了OP_ACCEPT事件的SelectionKey
                        handleAccept(next);
                    }else if (next.isReadable()){
                        //处理发生了OP_READ事件的SelectionKey
                        handleRead(next);
                    }
                    else if (next.isWritable()){
                        System.out.println("转发给一个用户完成");
                    }
                    //迭代完要删除,不然下次触发事件获取的SelectionKey会有他在里面
                    iterator.remove();
                }
            }
        }catch ( Exception e){
            e.printStackTrace();
        }

    }

    private void handleRead(SelectionKey next) throws IOException {

        //获取触发事件的SocketChannel
        SocketChannel channel = (SocketChannel) next.channel();

        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        //读取事件,这里不会阻塞,因为触发了读取完成事件才进入这里,数据必定是准备好的
        try {
            //不循环读完了 ,直接就假设信息小于1024,一个buffer一次能读完。
            int read = channel.read(byteBuffer);
            if (read>0){
                //转发到除了自身以外的其他客户端,以实现群聊功能
                forwardOthersClient(next, byteBuffer.array());
            }
        }catch (Exception e){
            System.out.println("用户:" + next.attachment() + "离线了。");
            next.cancel();
            try {
                channel.close();
            } catch (IOException e1) {
                e1.printStackTrace();
            }
        }

    }

    private void forwardOthersClient(SelectionKey next,byte[] msg) throws IOException {
        //获取所有注册到selector的SelectionKey
        Set<SelectionKey> keys = selector.keys();
        //遍历
        for (SelectionKey selectionKey:keys){
            if (selectionKey == next || selectionKey.channel() == serverSocketChannel){
                //排除当前客户端和ServerSocketchannel不转发,
                continue;
            }
            SocketChannel channel = (SocketChannel) selectionKey.channel();
            channel.configureBlocking(false);
            try {
                channel.write(ByteBuffer.wrap(msg));
            }catch (Exception e){
                System.out.println("用户:" + next.attachment() + "离线了。");
                next.cancel();
                try {
                    channel.close();
                } catch (IOException e1) {
                    e1.printStackTrace();
                }
            }

        }
    }

    private void handleAccept(SelectionKey next) throws IOException {
        //因为ACCEPT事件只能是serverSocketChannel,所以这里就直接用ServerSocketchannel了
        //监听客户端进来,因为已经发生了ACCEPT事件,必定有了客户端进来,所以accept方法不会阻塞
        SocketChannel socketChannel = serverSocketChannel.accept();
        //设置为非阻塞
        socketChannel.configureBlocking(false);
        //将该channel注册到selector,事件为读事件,因为连接后,要读取客户端传来的数据,如果读取完成,就会触发该事件,而不用阻塞
        socketChannel.register(selector,SelectionKey.OP_READ);
        System.out.println("客户端 : " + socketChannel.getRemoteAddress() + "连接服务器");
    }

    //启动服务端
    public static void main(String[] args) throws IOException {
        GroupChatServer groupChatServer = new GroupChatServer(5555);
        groupChatServer.start();
    }
}

客户端:

public class GroupChatClient {

    private Selector selector;

    private String host = "127.0.0.1";

    private int port;

    private SocketChannel socketChannel;

    private String clientName;




    public GroupChatClient(int port,String clientName){
        this.port = port;
        this.clientName = clientName;
    }

    public void start() throws IOException {
        socketChannel = SocketChannel.open(new InetSocketAddress(host,port));
        socketChannel.configureBlocking(false);

        //其实这里用不用selector都可以,因为就只有一个SocketChannel
        selector = selector.open();
        socketChannel.register(selector, SelectionKey.OP_READ,clientName);

        try {
            while (true){
                selector.select();
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()){
                    SelectionKey next = iterator.next();
                    if (next.isReadable()){
                        SocketChannel channel = (SocketChannel) next.channel();
                        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                        int read = channel.read(byteBuffer);
                        if (read>0)
                            System.out.println(new String(byteBuffer.array()));
                    }
                    if (next.isWritable()){
                        System.out.println("消息发送完成");
                    }
                    //迭代完要删除,不然下次触发事件获取的SelectionKey会有他在里面
                    iterator.remove();
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }

    }

    public void sendMsg(String msg){
        if (!StringUtils.isEmpty(msg)) {
            try {
                socketChannel.write(ByteBuffer.wrap(msg.getBytes()));
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {

        GroupChatClient groupChatClient = new GroupChatClient(5555,"salar");
        try {
            //发送消息要新开线程,并且在start方法前面使用,然后睡眠一定时间确保start方法执行完成,也可以用countdownlatch来确保。
            //要新开线程发消息的原因是到时主线程会在start方法的while死循环里面不出来,执行不到后面
            new Thread(()->{
                while (true){
                    try {
                        TimeUnit.SECONDS.sleep(3);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    groupChatClient.sendMsg("Hi, I am salar");

                }
            }).start();
            groupChatClient.start();

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

先启动服务端再启动客户端,启动三个客户端,每个发送的消息都不一样,以做区别。

结果:
服务端:
在这里插入图片描述

客户端:能够收到其他客户端发送的消息。
在这里插入图片描述

在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
以下是一个使用 Java NIO(New I/O)进行网络编程的简单示例: ```java import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class NIOExample { public static void main(String[] args) throws IOException { // 创建一个线程池用于处理客户端连接 ExecutorService executor = Executors.newFixedThreadPool(10); // 创建 ServerSocketChannel 并绑定端口 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.bind(new InetSocketAddress("localhost", 8080)); System.out.println("Server started on port 8080"); while (true) { // 接受客户端连接 SocketChannel socketChannel = serverSocketChannel.accept(); // 使用线程池处理客户端连接 executor.execute(() -> handleClient(socketChannel)); } } private static void handleClient(SocketChannel socketChannel) { try { ByteBuffer buffer = ByteBuffer.allocate(1024); // 读取客户端发送的数据 int bytesRead = socketChannel.read(buffer); while (bytesRead != -1) { buffer.flip(); while (buffer.hasRemaining()) { System.out.print((char) buffer.get()); } buffer.clear(); bytesRead = socketChannel.read(buffer); } // 响应客户端 String response = "Hello from server"; ByteBuffer responseBuffer = ByteBuffer.wrap(response.getBytes()); socketChannel.write(responseBuffer); // 关闭连接 socketChannel.close(); } catch (IOException e) { e.printStackTrace(); } } } ``` 这个示例创建了一个简单的服务,监听本地的 8080 端口。当客户端连接时,会使用线程池处理连接,并读取客户端发送的数据。然后,服务会向客户端发送 "Hello from server" 的响应,并关闭连接。 请注意,这只是一个简单的示例,实际的网络编程可能涉及更复杂的逻辑和处理。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值