赞
踩
Android NIO 系列教程(一) NIO概述
Android NIO 系列教程(二) – Channel
Android NIO 系列教程(三) – Buffer
Android NIO 系列教程(四) – Selector
Android NIO 系列教程(五) – FileChannel
Android NIO 系列教程(六) – SocketChannel
Android NIO 系列教程(七) – ServerSocketChannel
Android NIO 系列教程(八) --NIO简易聊天室
从上面几章,我们已经知道了 NIO 的 SocketChannel ,ServerSocketChannel,Selector 等知识,这章我们来做个总结,实现一个简易的聊天室。
今天要实现的效果如下:
首先,先构建服务器,从上面几章的理解,我们总结出以下步骤
所以服务端的代码如下:
首先是初始化:
//1.创建 selector
Selector selector = Selector.open();
//2.创建 ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//3.绑定端口
serverSocketChannel.bind(new InetSocketAddress(Constants.PORT));
//4.设置为非阻塞模式
serverSocketChannel.configureBlocking(false);
//5.将channel注册到selector中
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("服务端启动成功,开始监听...");
接着监听感兴趣的事件,比如客户端接入和客户端的数据:
// 7.通过selectedKeys() 拿到 selectedKeys 集合 Set<SelectionKey> selectionKeys = selector.selectedKeys(); Iterator<SelectionKey> iterator = selectionKeys.iterator(); while (iterator.hasNext()){ //拿到 selectedKeys 实例 SelectionKey selectionKey = iterator.next(); //移除 selectedKeys 实例 iterator.remove(); /** * 如果是接入事件 */ if (selectionKey.isAcceptable()){ handleAccept(serverSocketChannel,selector); } /** * 如果是可读事件 */ if (selectionKey.isReadable()){ handleRead(selectionKey,selector); } }
如果是 accept 事件,我们处理的比较简单,就打印个提示即可:
/** * 处理接受事件 * @param serverSocketChannel * @param selector * @throws IOException */ private static void handleAccept(ServerSocketChannel serverSocketChannel,Selector selector) throws IOException { //拿到 SocketChannel 客户端 SocketChannel socketChannel = serverSocketChannel.accept(); System.out.println("新客户端连接:"+socketChannel.getRemoteAddress().toString()); //设置 socketchannel 为非阻塞模式 socketChannel.configureBlocking(false); //客户端注册读事件,这样我们才能接收到客户端的信息 socketChannel.register(selector,SelectionKey.OP_READ); //发送 conected 提示服务端已经接收到 // ByteBuffer buf = Charset.forName("utf-8").encode(Constants.CLIENT_CONNECTED); // socketChannel.write(buf); }
注意到对 socketChannel 注册成 read 模式,这样,客户端的数据我们才能接收到。
接着是监听:
/** * 处理客户端读事件,并广播出去 * @param selectionKey * @param selector * @throws IOException */ private static void handleRead(SelectionKey selectionKey,Selector selector) throws IOException { //拿到已经就绪的 channel SocketChannel channel = (SocketChannel) selectionKey.channel(); if (channel != null) { //读取channel的数据 ByteBuffer buf = ByteBuffer.allocate(512); StringBuilder sb = new StringBuilder(); int readByte = channel.read(buf); while (readByte > 0) { //切换为读模式 buf.flip(); String msg = String.valueOf(Charset.forName("utf-8").decode(buf)); sb.append(msg); readByte = channel.read(buf); } buf.clear(); //将 channel 继续注册为可读事件 channel.register(selector, SelectionKey.OP_READ); if (sb.length() > 0) { System.out.println(channel.getRemoteAddress().toString()+" : " + sb.toString()); //返回数据 //String responeMsg = sb.length(); //channel.write(Charset.forName("utf-8").encode(responeMsg)); //广播 broadcastMsg(selector,channel,sb.toString()); } } }
注释已经很清楚了,这里就不过多解释了。然后广播的代码为:
/** * g * @param selector * @param targetChannel * @param msg * @throws IOException */ private static void broadcastMsg(Selector selector,SocketChannel targetChannel,String msg) throws IOException { //拿到已连接的客户端个数 Set<SelectionKey> keys = selector.keys(); for (SelectionKey selectionKey : keys) { Channel channel = selectionKey.channel(); //不是自己本身,其他通道才需要拿到信息 if (channel instanceof SocketChannel){ if (targetChannel != null && channel == targetChannel ){ continue; } ((SocketChannel) channel).write(Charset.forName("utf-8").encode(msg)); } } }
接着是客户端,我们也总结了以下步骤:
首先,首先初始化为:
//1.创建 selector
Selector selector = Selector.open();
//2.创建SocketChannel
SocketChannel socketChannel = SocketChannel.open();
//3.设置为非阻塞模式
socketChannel.configureBlocking(false);
//4.连接服务器
socketChannel.connect(new InetSocketAddress("localhost", Constants.PORT));
//注册读事件,读取服户端信息
socketChannel.register(selector, SelectionKey.OP_READ);
接着,读取终端的信息,并发送给服务端:
if (socketChannel.finishConnect()) { while (!isFinish) { String msg = br.readLine(); if ("bye".equals(msg)) { isFinish = true; sendData(socketChannel,"客户端A退出了"); readerThread.exit(); socketChannel.close(); System.out.println("服务端已退出"); break; } else { sendData(socketChannel,msg); } } }
其中 sendData 如下:
/**
* 给服务器发送数据
* @param channel
* @param msg
* @throws IOException
*/
private static void sendData(SocketChannel channel,String msg) throws IOException {
if (channel.isConnected()){
channel.write(Charset.forName("utf-8").encode(msg));
}
}
而读取服务端的信息,则另开一个线程去监听数据即可:
static class ReaderThread extends Thread{ private Selector selector; private boolean isFinish = false; public ReaderThread(Selector selector) { this.selector = selector; } @Override public void run() { super.run(); try { while (!isFinish){ //调用 select 方法,拿到 channel int channels = selector.select(); if (channels == 0){ continue; } //如果能拿到,则通过 selectedKeys() 方法,拿到 selectedKeys 的集合 Set<SelectionKey> selectionKeys = selector.selectedKeys(); Iterator<SelectionKey> iterator = selectionKeys.iterator(); while (iterator.hasNext()){ //拿到 selectedKeys 实例 SelectionKey selectionKey = iterator.next(); //如果是可读事件 if (selectionKey.isReadable()){ //拿到 就绪的 SocketChannel SocketChannel channel = (SocketChannel) selectionKey.channel(); if (channel != null) { //读取channel的数据 ByteBuffer buf = ByteBuffer.allocate(1024); StringBuilder sb = new StringBuilder(); while (channel.read(buf) > 0) { //切换为读模式 buf.flip(); String msg = String.valueOf(Charset.forName("utf-8").decode(buf)); sb.append(msg); } //将 channel 继续注册为可读事件 channel.register(selector, SelectionKey.OP_READ); if (Constants.CLIENT_CONNECTED.equals(sb.toString())){ sendData(channel,"我是客户端A"); }else { System.out.println(sb.toString()); } } } iterator.remove(); } } } catch (IOException e) { // e.printStackTrace(); }finally { exit(); } } public void exit(){ isFinish = true; try { selector.close(); } catch (IOException e) { e.printStackTrace(); } } }
这样,我们一个简易的聊天室就完成了。
可能你会问到,这样并没有体现 NIO 的单个 selector 和 多个channel 的优势啊,还有其他等问题,这些,等我们后面再去优化。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。