java NIO及NIO聊天室

参考链接:

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> writeCache = new HashMap>();
	
	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 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);
				}
				if(key.isWritable()){
					SocketChannel client = (SocketChannel) key.channel();
					//TODO 将缓存中对应SocketChannel的数据取出,输出到特定的channel
					//List 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 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 onlineUsers = new HashMap();//将用户对应的channel对应起来
	private Selector selector;
	private ServerSocketChannel server;
	
	
	public void startServer() throws IOException{
		//NIO server初始化固定流程:5步
		selector = Selector.open();					//1.selector open
		server = ServerSocketChannel.open();		//2.ServerSocketChannel open
		server.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 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取得客户端channel
					StringBuilder 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();			//关闭channel
						key.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){	//排除ServerSocketChannel
				count++;
			}
		}
		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{
		@Override
		public void run(){
			Iterator 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管理多个连接的。只不过要求这些连接的实时性不高,否则阻塞会很严重。

你可能感兴趣的:(java,NIO,聊天室)