NIO学习笔记(完)——简易网络多客户端聊天室实现

在掌握了基本NIO的种种操作之后,最后通过一个较大的实例来结束NIO的学习——一个简易的网络多客户端聊天室的实现。

在开始动手之前,我们需要简单普及一个基础知识,也正是上文提到的NIO三大核心组件:channel、buffer、selector中的最后一个,selector。在之前几篇博客的几个例子之中,NIO的表现其实并没有形成对传统IO的一种颠覆。无论是文件复制也好,读取也好,甚至是大文件的修改操作,整体都没有摆脱原来传统IO整体的大模型。只不过是在原有的基础上,从面向流改为了面向通道。而selector才是NIO的精髓所在,大家先通过一张图对selector的作用作简单了解:

selector在网络模型中的作用

传统IO:每一个服务端都需要创建一条线程给客户端连接。(阻塞式)
NIO:基于通道,可以为服务端注册一个selector,通过这个selector来监听接下来即将接入的一个个客户端,再反馈给serverSocketChannel。(非阻塞式)

非阻塞形式的网络模型,基于channel和buffer。打个比方,每个socket都是一根自来水管,每个自来水管下面我们都放了个桶,也就是buffer。selector作为监视者,在这里往复巡视。一旦有一个桶满了,就会告知selector,把这个桶上交给serverSocketChannel。也就是说此时,这个serverSocketChannel并不直接去管理每一个客户端,这样一来就形成了全新的网络模型。

意义在于什么呢,一条线程我就可以对付很多客户端的连接,对于线程上的开销就可以减少非常多。如果一个客户端来我就给一条线程,那么对于大型服务器,拥有成千上万的客户端连接,势必会将这个服务器拖垮。所以这种非阻塞式的多路复用模型,才能成为主流。

了解了这些之后,下面我们就利用NIO来实践网络编程——实现一个简易的网络聊天室。编写代码的过程中有不少坑,在这里不一个一个提及,下面展示全文代码,希望大家能够细细阅读并加以理解。

服务端实现:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.Channel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;

/**
 * 网络多客户端聊天室<br/>
 * 功能1:客户端通过Java NIO连接到服务端,支持多客户端的连接。<br/>
 * 功能2:客户端初次连接时,服务端提示输入昵称,之后发送消息都需要按照规定格式带着昵称发送消息。<br/>
 * 功能3:客户端登录后,发送已经设置好的欢迎信息和在线人数给客户端,并且通知其他客户端该客户端上线。<br/>
 * 功能4:服务器收到已登录客户端输入内容,转发至其他登录客户端。
 * @author 青葉
 *
 */
public class ChatRoomServer {
    ServerSocketChannel serverSocketChannel;
    Selector selector;
    Charset  charset = Charset.forName("UTF-8");
    int port = 9999;

    //初始化数据
    public void init() {
            try {
            serverSocketChannel = ServerSocketChannel.open();
            selector = Selector.open();
            serverSocketChannel.configureBlocking(false);
            serverSocketChannel.bind(new InetSocketAddress(port));
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            watching();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                serverSocketChannel.close();
                selector.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    //向其他客户端发送消息
    public void broadcast(String content, String userName) throws IOException {
        //获取当前所有的selectionKey。注意:selector.selectionKeys()方法返回的不是所有的key
        Set<SelectionKey> keys = selector.keys();
        Iterator<SelectionKey> iterator = keys.iterator();
        while(iterator.hasNext()) {
            SelectionKey selectionKey = iterator.next();
            Channel channel = selectionKey.channel();
            if(channel instanceof SocketChannel) {
                SocketChannel socketChannel = (SocketChannel) channel;
                socketChannel.write(charset.encode(userName + "对大家说" + content));
            }
        }
    }

    public void watching() throws IOException {
        System.out.println("服务器启动成功...");
        while(true) {
            int readyChannels = selector.select(); //等待所注册的事件发生
            if(0 == readyChannels) {
                continue;
            }
            //处理事件
            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = keys.iterator();
            while(iterator.hasNext()) {
                SelectionKey selectionKey = iterator.next();
                if(selectionKey.isAcceptable()) {
                    //客户端接入事件
                    SocketChannel channel = serverSocketChannel.accept();
                    channel.configureBlocking(false);
                    channel.register(selector, SelectionKey.OP_READ);
                    //写入欢迎信息
                    channel.write(charset.encode("欢迎来到聊天室,请输入姓名"));
//                  selectionKey.attach(new UserInfo());
                } else if(selectionKey.isReadable()) {
                    //获取客户端channel
                    SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
                    UserInfo userInfo = (UserInfo) selectionKey.attachment();
                    //获取channel内容
                    ByteBuffer buffer = ByteBuffer.allocate(128);
                    StringBuffer stringBuffer = new StringBuffer();
                    int flag = socketChannel.read(buffer);
                    while(flag > 0) {
                        buffer.flip();
                        stringBuffer.append(charset.decode(buffer));
                        buffer.clear();
                        flag = socketChannel.read(buffer);
                    }
                    if(null != userInfo && userInfo.init) {
                        broadcast(stringBuffer.toString(), userInfo.getName());
                    } else {
                        //接收用户名
                        UserInfo info = new UserInfo();
                        info.setName(stringBuffer.toString());
                        info.setInit(true);
                        selectionKey.attach(info);
                        //输出提示信息
                        socketChannel.write(charset.encode("您好," + info.getName() + ",现在您可以和聊天室的小伙伴聊天了。"));
                    }
                }
                iterator.remove();
            }
        }
    }

    public static void main(String[] args) {
        //创建serverSocketChannel
        try(ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();) {
            serverSocketChannel.configureBlocking(false); //声明非阻塞IO
            //声明一个选择器
            Selector selector = Selector.open();
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); //在客户端接入的事件上注册选择器
            ChatRoomServer chatRoomServer = new ChatRoomServer();
            chatRoomServer.init();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

class UserInfo {
    String name;
    boolean init = false;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public boolean isInit() {
        return init;
    }
    public void setInit(boolean init) {
        this.init = init;
    }
}

客户端实现:

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.nio.charset.Charset;
import java.util.Iterator;
import java.util.Scanner;
import java.util.Set;

public class ChatRoomClient {
    SocketChannel channel;
    Selector selector;
    int port = 9999;
    String ip = "192.168.0.10";

    public void init() {
        try {
            channel = SocketChannel.open(new InetSocketAddress(ip, port));
            channel.configureBlocking(false);
            selector = Selector.open();
            channel.register(selector, SelectionKey.OP_READ);

            //启动Selector线程
            new MySelectorThread(selector).start();

            //获取控制台输入
            while(true) {
                @SuppressWarnings("resource") 
                Scanner scanner = new Scanner(System.in);
                String content = scanner.nextLine();
                channel.write(Charset.forName("UTF-8").encode(content));
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                channel.close();
                selector.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        ChatRoomClient chatRoomClient = new ChatRoomClient();
        chatRoomClient.init();
    }
}

class MySelectorThread extends Thread {
    Selector selector;

    public MySelectorThread(Selector selector) {
        this.selector = selector;
    }

    @Override
    public void run() {
        try {
            //处理事件
            while(true) {
                int readyChannels = selector.select(); //等待所注册的事件发生
                if(0 == readyChannels) {
                    continue;
                }
                //处理事件
                Set<SelectionKey> keys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = keys.iterator();
                while(iterator.hasNext()) {
                    SelectionKey selectionKey = iterator.next();
                    if(selectionKey.isReadable()) {
                        SocketChannel channel = (SocketChannel) selectionKey.channel();
                        ByteBuffer buffer = ByteBuffer.allocate(128);
                        StringBuffer stringBuffer = new StringBuffer();
                        while(channel.read(buffer) > 0) {
                            buffer.flip();
                            stringBuffer .append(Charset.forName("UTF-8").decode(buffer));
                            buffer.clear();
                        }
                        System.out.println(stringBuffer.toString());
                    }
                    iterator.remove();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

其实还有一些小功能,例如上线通知、在线人数统计之类的并没有去实现,但这些都是基于已经完成的这些功能衍生出来的,十分简单,大家可以自己动手试试。

整体上来说,这个多路复用的好处在于:我们只需要一条selector所在的线程,去监听多个channel,而不需要每一个客户端都去创建一个线程来维护它。这个是NIO的精髓所在,同时也是现在大多数网络服务器的架构模式。

整个NIO的学习就告一段落,希望大家能够通过阅读我的学习笔记,体会到一定的其中的编程思想。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值