NIO 网络编程 群聊案例

前言

最近在学习B站尚硅谷的netty的网络编程课程,非常感谢硅谷,硅谷威武!感兴趣的朋友,也可以去看一下。
学习netty之前,学习了BIO ,NIO,给后续netty课程打下扎实基础,现分享一下NIO 的群聊的一个案例 ,案例仅作为自己笔记
需要对NIO 的 三大组件有些了解哦,通道,选择器,buffer(缓冲区)

服务端代码流程:
客户端发起连接,服务端进行监听,当有事件发生时,服务器根据不同事件做不同处理
连接事件:服务端会为每个连接的客户端创建对应的通道,并注册到选择器,
读取事件:服务端会将通道中的数据读到buffer中,然后再进行转发,转发排除自己
客户端代码流程:

客户端发起连接,并开启一个线程循环去读取服务端转发过来的数据,主线程则阻塞,等待用户输入数据,然后发送,将数据写入通道,服务器端再从通道内获取数据

下面我贴上代码

服务端

package com.bio.demo;

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

/**
 * @author yanjun.liu
 * @date 2021/4/29--10:42
 * <p>
 * GroupChatServer 服务端的职责:
 * 1.初始化  selector ,serverSocketChannel 并且进行绑定ip 端口,并且将自己注册到selector上,指定监听的事件(连接事件)
 * 2.开启监听,并且处理selector 上,发送的事件(根据不同事件做不同处理)
 * 如果是客户端连接事件  需要通过连接获取客户端socketChannel,然后设置非阻塞,并且将这个客户端socketChannel注册在selector,并指定监听的事件
 * 如果发送事件的户端是发送数据,那么就是读事件,serverSocketChannel需要读取客户端的数据
 */
public class GroupChatServer {

    private Selector selector;

    private ServerSocketChannel serverSocketChannel;

    private static final int PORT = 8888;

    /**
     * 构造器初始化工作
     *
     * @throws Exception
     */
    public GroupChatServer() throws Exception {
        //创建初始化selector
        selector = Selector.open();
        //创建初始化serverSocketChannel
        serverSocketChannel = ServerSocketChannel.open();
        //设置非阻塞
        serverSocketChannel.configureBlocking(false);
        //绑定ip端口
        serverSocketChannel.bind(new InetSocketAddress(PORT));
        //将serverSocketChannel 注册到 selector上,SelectionKey.OP_ACCEPT 客户端连接事件
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    }


    /**
     * 服务端需要监听selector 上发送的事件,根据不同事件,做出不同处理
     */
    public void listen() throws Exception {

        while (true) {
            //Selector 进行监听 select 方法,返回有事件发生的通道的个数,不传毫秒值会阻塞,直到拿到至少一个通道返回
            // 重载方法,此方法还可以传入一个毫秒值,表示等待多少毫秒之后返回
            int select = selector.select();
            //说明有事件发送,进行处理
            if (select > 0) {
                //获取selector 上发送事件的SelectionKey,这里注意!SelectionKey 和通道是有绑定关系的,
                //也可以说 获取selector 上发送事件的通道 socketChannel
                Set<SelectionKey> selectionKeys = selector.selectedKeys();

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

                while (iterator.hasNext()) {
                    //取出每个有事件发送的  SelectionKey
                    SelectionKey key = iterator.next();
                    //如果是客户端连接事件,监听建立连接,并且拿到对应的socketChannel,
                    // 设置非阻塞,并且将此 socketChannel注册在selector 上并设置监听事件
                    if (key.isAcceptable()) {
                        //serverSocketChannel.accept(); 此方法本身是阻塞的,但是代码进入if,就说明有客户端连接,因此这个方法必定会往下执行,不阻塞
                        SocketChannel socketChannel = serverSocketChannel.accept();
                        System.out.println("获取到客户端的socketChannel:" + socketChannel.hashCode());
                        socketChannel.configureBlocking(false);
                        //设置读事件,
                        socketChannel.register(selector, SelectionKey.OP_READ);

                        System.out.println(socketChannel.getRemoteAddress() + ":上线了");
                    }
                    //读事件,将数据从通道读取到buffer中
                    if (key.isReadable()) {
                        redDate(key);
                    }
                    //进入while,处理完这个事件之后,需要将这个事件从selectedKeys 数组中,将当前selectedKey 删除
                    iterator.remove();
                }

            } else {
                System.out.println("客户端没有事件发生,继续循环等待");
            }

        }

    }


    public void redDate(SelectionKey key) throws IOException {
        //通过SelectionKey 反向获取 SocketChannel
        SocketChannel socketChannel = (SocketChannel) key.channel();
        //创建buffer
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        int count = 0;
        try {
            //读取 从通道读取到buffer,返回读取到的长度
            count = socketChannel.read(buffer);
            if (count > 0) {
                String msg = new String(buffer.array());
                System.out.println("服务器收到客户端->" + socketChannel.getRemoteAddress() + "-发送的消息msg:" + msg);
                System.out.println("服务器开始转发消息===============================");
                //重点!服务端接收到消息了,还需要将消息转发给其客户端,当然要排除发消息的这个客户端哦
                //获取所有注册在此selector 上通道 对应的SelectionKey
                Set<SelectionKey> keys = selector.keys();
                Iterator<SelectionKey> it = keys.iterator();
                while (it.hasNext()) {
                    SelectionKey next = it.next();
                    Channel channel = (Channel) next.channel();
                    //不是他自己,服务端就转发消息给客户端  socketChannel 为发消息的客户端
                    if (channel instanceof SocketChannel && channel != socketChannel) {
                        ByteBuffer wrap = ByteBuffer.wrap(msg.getBytes());
                        ((SocketChannel) channel).write(wrap);
                    }
                }
            }
        } catch (IOException e) {
            System.out.println(socketChannel.getRemoteAddress() + ": 离线了===============================");
            //取消注册关闭通道
            key.cancel();
            socketChannel.close();
        }
    }

    public static void main(String[] args) throws Exception {
        GroupChatServer groupChatServer = new GroupChatServer();

        groupChatServer.listen();
    }

}

客户端

package com.bio.demo;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Scanner;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author yanjun.liu
 * @date 2021/4/29--17:10
 */
public class GroupChatClient {

    private SocketChannel socketChannel;
    private Selector selector;
    private String userName;

    /**
     * 客户端类 初始化
     *
     * @throws IOException
     */
    public GroupChatClient() throws IOException {
        //客户端进行连接服务端
        socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 8888));
        //创建selector
        selector = Selector.open();
        //设置为非阻塞
        socketChannel.configureBlocking(false);
        //将客户端此通道 注册在 selector上,并指定监听的事件为读 read
        socketChannel.register(selector, SelectionKey.OP_READ);
        //初始化客户端名称
        userName = socketChannel.getRemoteAddress().toString().substring(1);
        System.out.println("客户端 is  ok");
    }

    /**
     * 客户端发送消息
     *
     * @param massage
     * @throws Exception
     */
    public void sendMessage(String massage) throws Exception {
        String msg = userName + "说: " + massage;
        ByteBuffer wrap = ByteBuffer.wrap(msg.getBytes());
        //通过buff 中的数据 ,写入客户端通道,(服务端会监听,得到此通道,并读取此通道内的数据)
        socketChannel.write(wrap);
    }

    /**
     * 客户端也要读取服务端 转发  过来的数据,并显示
     *
     * @throws IOException
     */
    public void readMassage() throws IOException {
        //获取此 selector 有事件发生的 通道个数,如果不删除执行过的key,这里不会阻塞,而是返回0
        int select = selector.select();

        if (select > 0) {
            //获取此selector 发生事件的  SelectionKey
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                //处理读事件
                if (key.isReadable()) {
                    //通过SelectionKey 反现获取对应的通道
                    SocketChannel channel = (SocketChannel) key.channel();
                    System.out.println("客户端的通道对象为:" + channel);
                    ByteBuffer buff = ByteBuffer.allocate(1024);
                    //将通道里面的数据读取到buff中
                    channel.read(buff);
                    String msg = new String(buff.array());
                    System.out.println(msg);
                    //删除当前事件
                    iterator.remove();
                }
            }
        } else {
            System.out.println("没有可用的通道");
        }
    }


    public static void main(String[] args) throws Exception {

        GroupChatClient groupChatClient = new GroupChatClient();

        //启动一个线程去读取数据
        ExecutorService executorService = Executors.newFixedThreadPool(1);
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                //一直尝试读取服务端发送过来的数据,每割两秒
                while (true) {
                    try {
                        groupChatClient.readMassage();
                        Thread.currentThread().sleep(2000);
                    } catch (Exception e) {
                        e.getStackTrace();
                    }
                }
            }
        });

        //主线程等待用户输入(scanner 阻塞)
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNextLine()) {
            String s = scanner.nextLine();
            groupChatClient.sendMessage(s);
        }

    }
}

运行

依次启动服务端,然后启动客户端即可,进行测试

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值