java NIO及NIO聊天室

来源:互联网 发布:linux查看cpu使用率sar 编辑:程序博客网 时间:2024/04/30 06:50

参考链接:

java NIO实例1:http://blog.chinaunix.net/uid-25808509-id-3346228.html

java NIO教程之selector(一系列):http://ifeve.com/selectors/
java NIO与IO的比较:http://blog.csdn.net/keda8997110/article/details/19549493
JAVA NIO实例2:http://weixiaolu.iteye.com/blog/1479656
JAVA NIO教程(主要参考NIO和IO如何选择):http://www.iteye.com/magazines/132-Java-NIO


多人聊天室 IO:http://blog.csdn.net/baolong47/article/details/6735853

多人聊天室 NIO:http://www.cnblogs.com/yanghuahui/p/3686054.html


NIO重点摘要:

1.为了将Channel和Selector配合使用,必须将channel注册到selector上。
与Selector一起使用时,Channel必须处于非阻塞模式下。
这意味着不能将FileChannel与Selector一起使用,因为FileChannel不能切换到非阻塞模式。而套接字通道都可以。

2.将ServerSocketChannel注册到上面的Selector上,需要什么事件?(下面服务端Channel是ServerSocketChannel类,客户端Channel是SocketChannel类)
2.1)服务端Channel只负责监听连接。所以服务端channel不用注册读写,注册了也会报错IllegalArgumentException。如:ssChannel.register(selector, SelectionKey.OP_ACCEPT|SelecionKey.OP_READ);
需要注册读写事件的是客户端channel;
2.2)客户端负责主动连接服务端,而双方的通信都是获取客户端channel完成的;
2.3)客户端channel如何在服务端注册呢?
在服务端监听连接处理块内注册,也就是循环处理块的if(selectKey.isAcceptable())块内。

3.每次处理完一个事件,很多代码还会调用selectionkey.interestOps()方法来设置下一次检测感兴趣的事件。但如果事件类型不变化,可以不用重复注册?
不用重复注册。socketChannel.register(selector, SelectionKey.OP_READ);语句到最后调用的是SelectionKey.interestOps(SelectionKey.OP_READ);语句。也就是说如果我们下次需要扫描的事件不变化,可以不用重复写SelectionKey.interestOps(SelectionKey.OP_READ)这句代码。显示的书写后面这句代码,是为了修改下次的扫描事件。

4.NIO的限制之一:NIO是基于缓存的,所以对通信双方每次的每次通信量有限制,要能提请预判。
如果一个channel需要重复使用一个Buffer,记得每次使用前用clear方法清空,否则会出现问题。至于为什么会出现问题,没细研究。

5.对于NIO,一般可用一个线程管理服务端,一个线程管理客户端。如果客户端数量非常多,NIO的瓶颈在哪里?
未解决……

6.NIO通道读循环的问题如何解决?
如果一个客户端断开了链接,那么服务端的监听channel会一直可读,但是又读不出数据。
所以服务端读,需要做处理,将channel.read(buffer)放在try-catch块中,如果读出长度==-1,作相应的掉线处理。(参考NIO聊天室代码)
  客户端链接异常也一样,客户端的读事件会一直发生,同样channel.read(buffer)会抛异常:java.io.IOException: 远程主机强迫关闭了一个现有的连接。
  与服务端做同样处理即可。


7.NIO通道写循环的问题如何解决?
ServerSocketChannel不用注册读、写事件,一般只关注accept()事件;SocketChannel一般不用注册write()事件,因为写事件在大部分情况下是可用的,只有特殊情况下需要考虑注册写事件:数据量大,写数据可能会出现长时间等待的情况。
因为一般情况下,在读事件发生后,先获取读连接,读取信息,然后直接通过读连接给出响应。不需要等待写事件,因为写事件大部分时间是可用的,等待无必要。

8.需要注册写事件的代码示例:
public class OpWriteRegister {private Selector selector;//注册写事件,需要借助一个缓存,缓存需要写到某个特定通道(key)private Map<SocketChannel,List<byte[]>> writeCache = new HashMap<SocketChannel,List<byte[]>>();public void init() throws IOException{selector = Selector.open();ServerSocketChannel server = ServerSocketChannel.open();server.bind(new InetSocketAddress(8899));server.configureBlocking(false);server.register(selector, SelectionKey.OP_ACCEPT);while(true){selector.select();Iterator<SelectionKey> ikeys = selector.selectedKeys().iterator();while(ikeys.hasNext()){SelectionKey key = ikeys.next();if(key.isAcceptable()){SocketChannel client = server.accept();client.configureBlocking(false);client.register(selector, SelectionKey.OP_READ|SelectionKey.OP_WRITE);}if(key.isReadable()){SocketChannel client = (SocketChannel) key.channel();//TODO 将待写出的数据先存如缓存//writeCache.put(client, List<byte[]>);}if(key.isWritable()){SocketChannel client = (SocketChannel) key.channel();//TODO 将缓存中对应SocketChannel的数据取出,输出到特定的channel//List<byte[]> data = writeCache.get(client);//client.write(data);}}}}}

9.如何给服务端的建立的连接添加自定义的名称或ID?
一个Selector可以注册多个通道,每个通道在Selector中用一个SelectionKey来代表。那么selectionKey.attache()就可以将一个对象或者更多信息附着到SelectionKey上,这样就能方便的识别某个给定的通道。例如,可以附加与通道一起使用的Buffer,或是包含聚集数据的某个对象。使用方法如下: 
selectionKey.attach(theObject);
Object attachedObj = selectionKey.attachment();
将名称附加到SelectionKey示例,参考NIO聊天室代码。


10.如果N个客户端,共享一个selector,代码如何处理?
public class MultiClient {private Selector selector;public void init() throws IOException{selector = Selector.open();//下面是一个selector管理三个客户端连接,实际使用时,三个待连接服务端IP或端口是不同的SocketChannel client1 = SocketChannel.open(new InetSocketAddress("127.0.0.1",8897));client1.configureBlocking(false);client1.register(selector, SelectionKey.OP_READ);SocketChannel client2 = SocketChannel.open(new InetSocketAddress("127.0.0.1",8898));client2.configureBlocking(false);client2.register(selector, SelectionKey.OP_READ);SocketChannel client3 = SocketChannel.open(new InetSocketAddress("127.0.0.1",8899));client3.configureBlocking(false);client3.register(selector, SelectionKey.OP_READ);//数据处理伪代码while(true){selector.select();Iterator<SelectionKey> ikeys = selector.selectedKeys().iterator();while(ikeys.hasNext()){SelectionKey key = ikeys.next();if(key.isReadable()){//TODO 数据处理}}}}}

NIO聊天室

11.1)参考链接:http://www.cnblogs.com/yanghuahui/p/3686054.html
11.2)需求说明:
功能1:客户端通过Java NIO连接到服务端,支持多客户端的连接
功能2:客户端初次连接时,服务端提示输入昵称,如果昵称已经有人使用,提示重新输入,如果昵称唯一,则登录成功,之后发送消息都需要按照规定格式带着昵称发送消息
功能3:客户端登录后,发送已经设置好的欢迎信息和在线人数给客户端,并且通知其他客户端该客户端上线
功能4:服务器收到已登录客户端输入内容,转发至其他登录客户端。
功能5:如果客户端断线,要能在服务端做相应的处理。
11.3)注意考虑:
因为在while(true)循环中,if(selectKey.isWritable())中,等待用户输入信息时,会阻塞整个while(true)循环,即便此时有新消息到达,也不能获取。
上面的情况只能适合:一问一答的模式。
而对于聊天室:连续发问,连续回答的形式则会阻塞。所以客户端新开一个线程来处理异步读。
11.4)如何检查客户端掉线,参考第6点。
11.5)代码示例:
###服务端###public class ChatServer {private final int port = 8899;private final String seperator = "[|]";//消息分隔符private final Charset charset = Charset.forName("UTF-8");//字符集private ByteBuffer buffer = ByteBuffer.allocate(1024);//缓存private Map<String,SocketChannel> onlineUsers = new HashMap<String,SocketChannel>();//将用户对应的channel对应起来private Selector selector;private ServerSocketChannel server;public void startServer() throws IOException{//NIO server初始化固定流程:5步selector = Selector.open();//1.selector openserver = ServerSocketChannel.open();//2.ServerSocketChannel openserver.bind(new InetSocketAddress(port));//3.serverChannel绑定端口server.configureBlocking(false);//4.设置NIO为非阻塞模式server.register(selector, SelectionKey.OP_ACCEPT);//5.将channel注册在选择器上//NIO server处理数据固定流程:5步SocketChannel client;SelectionKey key;Iterator<SelectionKey> iKeys;while(true){selector.select();//1.用select()方法阻塞,一直到有可用连接加入iKeys = selector.selectedKeys().iterator();//2.到了这步,说明有可用连接到底,取出所有可用连接while(iKeys.hasNext()){key = iKeys.next();//3.遍历if(key.isAcceptable()){//4.对每个连接感兴趣的事做不同的处理//对于客户端连接,注册到服务端client = server.accept();//获取客户端首次连接client.configureBlocking(false);//不用注册写,只有当写入量大,或写需要争用时,才考虑注册写事件client.register(selector, SelectionKey.OP_READ);System.out.println("+++++客户端:"+client.getRemoteAddress()+",建立连接+++++");client.write(charset.encode("请输入自定义用户名:"));}if(key.isReadable()){client = (SocketChannel) key.channel();//通过key取得客户端channelStringBuilder msg = new StringBuilder();buffer.clear();//多次使用的缓存,用前要先清空try{while(client.read(buffer) > 0){buffer.flip();//将写模式转换为读模式msg.append(charset.decode(buffer));}}catch(IOException e){//如果client.read(buffer)抛出异常,说明此客户端主动断开连接,需做下面处理client.close();//关闭channelkey.cancel();//将channel对应的key置为不可用onlineUsers.values().remove(client);//将问题连接从map中删除System.out.println("-----用户'"+key.attachment().toString()+"'退出连接,当前用户列表:"+onlineUsers.keySet().toString()+"-----");continue;//跳出循环}if(msg.length() > 0) this.processMsg(msg.toString(),client,key);//处理消息体}iKeys.remove();//5.处理完一次事件后,要显示的移除}}}/** * 处理客户端传来的消息 * @param msg 格式:user_to|body|user_from * @Key 这里主要用attach()方法,给通道定义一个表示符 * @throws IOException  */private void processMsg(String msg, SocketChannel client,SelectionKey key) throws IOException{String[] ms = msg.split(seperator);if(ms.length == 1){String user = ms[0];//输入的是自定义用户名if(onlineUsers.containsKey(user)){client.write(charset.encode("当前用户已存在,请重新输入用户名:"));}else{onlineUsers.put(user, client);key.attach(user);//给通道定义一个表示符String welCome = "\t欢迎'"+user+"'上线,当前在线人数"+this.getOnLineNum()+"人。用户列表:"+onlineUsers.keySet().toString();this.broadCast(welCome);//给所用用户推送上线信息,包括自己}}else if(ms.length == 3){String user_to = ms[0];String msg_body = ms[1];String user_from = ms[2];SocketChannel channel_to = onlineUsers.get(user_to);if(channel_to == null){client.write(charset.encode("用户'"+user_to+"'不存在,当前用户列表:"+onlineUsers.keySet().toString()));}else{channel_to.write(charset.encode("来自'"+user_from+"'的消息:"+msg_body));}}}//map中的有效数量已被很好的控制,可以从map中获取,也可以用下面的方法取private int getOnLineNum(){int count = 0;Channel channel;for(SelectionKey k:selector.keys()){channel = k.channel();if(channel instanceof SocketChannel){//排除ServerSocketChannelcount++;}}return count;}//广播上线消息private void broadCast(String msg) throws IOException{Channel channel;for(SelectionKey k:selector.keys()){channel = k.channel();if(channel instanceof SocketChannel){SocketChannel client = (SocketChannel) channel;client.write(charset.encode(msg));}}}public static void main(String[] args){try {new ChatServer().startServer();} catch (IOException e) {e.printStackTrace();}}}###客户端###public class ChatClient1 {private final int port = 8899;private final String seperator = "|";private final Charset charset = Charset.forName("UTF-8");//字符集private ByteBuffer buffer = ByteBuffer.allocate(1024);private SocketChannel _self;private Selector selector;private String name = "";private boolean flag = true;//服务端断开,客户端的读事件不会一直发生(与服务端不一样)Scanner scanner = new Scanner(System.in);public void startClient() throws IOException{//客户端初始化固定流程:4步selector = Selector.open();//1.打开Selector_self = SocketChannel.open(new InetSocketAddress(port));//2.连接服务端,这里默认本机的IP_self.configureBlocking(false);//3.配置此channel非阻塞_self.register(selector, SelectionKey.OP_READ);//4.将channel的读事件注册到选择器/* * 因为等待用户输入会导致主线程阻塞 * 所以用主线程处理输入,新开一个线程处理读数据 */new Thread(new ClientReadThread()).start();//开一个异步线程处理读String input = "";while(flag){input = scanner.nextLine();if("".equals(input)){System.out.println("不允许输入空串!");continue;}else if("".equals(name) && input.split("[|]").length == 1){name = input;selector.keys().iterator().next().attach(name);//给通道添加名称}else if(!"".equals(name) && input.split("[|]").length == 2){input = input + seperator + name;}else{System.out.println("输入不合法,请重新输入:");continue;}try{_self.write(charset.encode(input));}catch(Exception e){System.out.println(e.getMessage()+"客户端主线程退出连接!!");}}}private class ClientReadThread implements Runnable{@Overridepublic void run(){Iterator<SelectionKey> ikeys;SelectionKey key;SocketChannel client;try {while(flag){selector.select();//调用此方法一直阻塞,直到有channel可用ikeys = selector.selectedKeys().iterator();while(ikeys.hasNext()){key = ikeys.next();if(key.isReadable()){//处理读事件client = (SocketChannel) key.channel();//这里的输出是true,从selector的key中获取的客户端channel,是同一个//System.out.println("client == _self:"+ (client == _self));buffer.clear();StringBuilder msg = new StringBuilder();try{while(client.read(buffer) > 0){buffer.flip();//将写模式转换为读模式msg.append(charset.decode(buffer));}}catch(IOException en){System.out.println(en.getMessage()+",客户端'"+key.attachment().toString()+"'读线程退出!!");stopMainThread();}System.out.println(msg.toString());}}}} catch (Exception e) {e.printStackTrace();}}}private void stopMainThread(){flag = false;}public static void main(String[] args){try {new ChatClient1().startClient();} catch (IOException e) {e.printStackTrace();}}}

12.对于IO聊天室和NIO聊天室,线程数到底有没有减少?有没有优化?
上面的服务端,一个服务端管理多个客户端,只用了一个线程;而不像传统的IO套接字,服务端接受到客户端连接后,此连接一定要以线程的方式,阻塞等待客户端的突发请求,因为这样的话,有几个客户端连接,就会有几个线程。很明显NIO比IO在线程上是有大量的减少。
而对于客户端,就是两个线程,一个写线程,一个读线程。理论上已经不能精简了,差别不大。
如果说我们的系统作为客户端,主动去连多个服务端,也可以用一个selector管理多个连接的。只不过要求这些连接的实时性不高,否则阻塞会很严重。

0 0
原创粉丝点击