`
marlonyao
  • 浏览: 249109 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

java nio网络编程的一点心得

    博客分类:
  • java
阅读更多
前几日用java nio写了一个tcp端口转发小工具,还颇费周折,其中一个原因在于网上资料很混乱,不少还是错误的。这篇文章中我会以一个EchoServer作为例子。先看《Java网络编程》中的写法,这也是在网上颇为常见的一个写法。

public class EchoServer {
	public static int DEFAULT_PORT = 7777;

	public static void main(String[] args) throws IOException {
		System.out.println("Listening for connection on port " + DEFAULT_PORT);

		Selector selector = Selector.open();
		initServer(selector);

		while (true) {
			selector.select();

			for (Iterator<SelectionKey> itor = selector.selectedKeys().iterator(); itor.hasNext();) {
				SelectionKey key = (SelectionKey) itor.next();
				itor.remove();
				try {
					if (key.isAcceptable()) {
						ServerSocketChannel server = (ServerSocketChannel) key.channel();
						SocketChannel client = server.accept();
						System.out.println("Accepted connection from " + client);
						client.configureBlocking(false);
						SelectionKey clientKey = client.register(selector, SelectionKey.OP_WRITE|SelectionKey.OP_READ);
						ByteBuffer buffer = ByteBuffer.allocate(100);
						clientKey.attach(buffer);
					}
					if (key.isReadable()) {
						SocketChannel client = (SocketChannel) key.channel();
						ByteBuffer buffer = (ByteBuffer) key.attachment();
						client.read(buffer);
					}
					if (key.isWritable()) {
						// System.out.println("is writable...");
						SocketChannel client = (SocketChannel) key.channel();
						ByteBuffer buffer = (ByteBuffer) key.attachment();
						buffer.flip();
						client.write(buffer);
						buffer.compact();
					}
				} catch (IOException e) {
					key.cancel();
					try { key.channel().close(); } catch (IOException ioe) { }
				}
			}
		}
	}

	private static void initServer(Selector selector) throws IOException,
			ClosedChannelException {
		ServerSocketChannel serverChannel = ServerSocketChannel.open();
		ServerSocket ss = serverChannel.socket();
		ss.bind(new InetSocketAddress(DEFAULT_PORT));
		serverChannel.configureBlocking(false);
		serverChannel.register(selector, SelectionKey.OP_ACCEPT);
	}
}


上面的代码很典型,运行结果似乎也是正确的。
marlon$ java EchoServer&
--> Listening for connection on port 7777
marlon$ telnet localhost 7777
--> Accepted connection from java.nio.channels.SocketChannel[connected local=/127.0.0.1:7777 remote=/127.0.0.1:65030]
hello
--> hello
world
-->world

但是如果你这时top用看一下发现服务器进程CPU占用到95%以上,如果取消掉32行的注释,服务器会不断地输出"is writable...",这是为什么呢?让我们来分析当第一个客户端连接上时发生什么情况。
  1. 在连接之前,服务器第11行:selector.select()处阻塞。当阻塞时,内核会将这个进程调度至休眠状态,此时基本不耗CPU。
  2. 当客户端发起一个连接时,服务器检测到客户端连接,selector.select()返回。selector.selectedKeys()返回已就绪的SelectionKey的集合,在这种情况下,它只包含一个key,也就是53行注册的acceptable key。服务器开始运行17-25行的代码,server.accept()返回代码客户端连接的socket,第22行在socket上注册OP_READ和OP_WRITE,表示当socket可读或者可写时就会通知selector。
  3. 接着服务器又回到第11行,尽管这时客户端还没有任何输入,但这时selector.select()不会阻塞,因为22行在socket注册了写操作,而socket只要send buffer不满就可以写,刚开始send buffer为空,socket总是可以写,于是server.select()立即返回,包含在22行注册的key。由于这个key可写,所以服务器会运行31-38行的代码,但是这时buffer为空,client.write(buffer)没有向socket写任何东西,立即返回0。
  4. 接着服务器又回到第11行,由于客户端连接socket可以写,这时selector.select()会立即返回,然后运行31-38行的代码,像步骤3一样,由于buffer为空,服务器没有干任何事又返回到第11行,这样不断循环,服务器却实际没有干事情,却耗大量的CPU。


从上面的分析可以看出问题在于我们在没有数据可写时就在socket上注册了OP_WRITE,导致服务器浪费大量CPU资源,解决办法是只有数据可以写时才注册OP_WRITE操作。上面的版本还不只浪费CPU那么简单,它还可能导致潜在的死锁。虽然死锁在我的机器上没有发生,对于这个简单的例子似乎也不大可能发生在别的机器上,但是在对于复杂的情况,比如我写的端口转发工具中就发生了,这还依赖于jdk的实现。对于上面的EchoServer,出现死锁的场景是这样的:
  1. 假设服务器已经启动,并且已经有一个客户端与它相连,此时正如上面的分析,服务器在不断地循环做无用功。这时用户在客户端输入"hello"。
  2. 当服务器运行到第11行:selector.select()时,这时selector.selectedKeys()会返回一个代表客户端连接的key,显然这时客户端socket是既可读又可写,但jdk却并不保证能够检测到两种状态。如果它检测到key既可读又可写,那么服务器会执行26-38行的代码。如果只检测到可读,那么服务器会执行26-30行的代码。如果只检测到可写,那么会执行31-38行的代码。对于前两种情况,不会造成死锁,因为当执行完29行,buffer会读到用户输入的内容,下次再运行到36行就可以将用户输入内容echo回。但是对最后一种情况,服务器完全忽略了客户端发过来的内容,如果每次selector.select()都只能检测到socket可写,那么服务器永远不能将echo回客户端输入的内容。


避免死锁的一个简单方法就是不要在同一个socket同时注册多个操作。对于上面的EchoServer来说就是不要同时注册OP_READ和OP_WRITE,要么只注册OP_READ,要么只注册OP_WRITE。下面的EchoServer修正了以上的错误:
	public static void main(String[] args) throws IOException {
		System.out.println("Listening for connection on port " + DEFAULT_PORT);

		Selector selector = Selector.open();
		initServer(selector);

		while (true) {
			selector.select();

			for (Iterator<SelectionKey> itor = selector.selectedKeys().iterator(); itor.hasNext();) {
				SelectionKey key = (SelectionKey) itor.next();
				itor.remove();
				try {
					if (key.isAcceptable()) {
						ServerSocketChannel server = (ServerSocketChannel) key.channel();
						SocketChannel client = server.accept();
						System.out.println("Accepted connection from " + client);
						client.configureBlocking(false);
						SelectionKey clientKey = client.register(selector, SelectionKey.OP_READ);
						ByteBuffer buffer = ByteBuffer.allocate(100);
						clientKey.attach(buffer);
					} else if (key.isReadable()) {
						SocketChannel client = (SocketChannel) key.channel();
						ByteBuffer buffer = (ByteBuffer) key.attachment();
						int n = client.read(buffer);
						if (n > 0) {
							buffer.flip();
							key.interestOps(SelectionKey.OP_WRITE);		// switch to OP_WRITE
						}
					} else if (key.isWritable()) {
						System.out.println("is writable...");
						SocketChannel client = (SocketChannel) key.channel();
						ByteBuffer buffer = (ByteBuffer) key.attachment();
						client.write(buffer);
						if (buffer.remaining() == 0) {	// write finished, switch to OP_READ
							buffer.clear();
							key.interestOps(SelectionKey.OP_READ);
						}
					}
				} catch (IOException e) {
					key.cancel();
					try { key.channel().close(); } catch (IOException ioe) { }
				}
			}
		}
	}


主要变化,在第19行接受客户端连接时只注册OP_READ操作,第28行当读到数据时才切换到OP_WRITE操作,第35-38行,当写操作完成时再切换到OP_READ操作。由于一个key同时只能执行一个操作,我将原来三个并行if换成了if...else。

上面的代码不够优雅,它将处理服务器Socket和客户连接Socket的代码搅在一起,对于简单的EchoServer这样做没什么问题,当服务器变得复杂,使用命令模式将它们分开变显得非常必要。首先创建一个接口来抽象对SelectionKey的处理。
	interface Handler {
		void execute(Selector selector, SelectionKey key);
	}

再来看main函数:
	public static void main(String[] args) throws IOException {
		System.out.println("Listening for connection on port " + DEFAULT_PORT);

		Selector selector = Selector.open();
		initServer(selector);

		while (true) {
			selector.select();

			for (Iterator<SelectionKey> itor = selector.selectedKeys().iterator(); itor.hasNext();) {
				SelectionKey key = (SelectionKey) itor.next();
				itor.remove();
				Handler handler = (Handler) key.attachment();
				handler.execute(selector, key);
			}
		}
	}

	private static void initServer(Selector selector) throws IOException,
			ClosedChannelException {
		ServerSocketChannel serverChannel = ServerSocketChannel.open();
		ServerSocket ss = serverChannel.socket();
		ss.bind(new InetSocketAddress(DEFAULT_PORT));
		serverChannel.configureBlocking(false);
		SelectionKey serverKey = serverChannel.register(selector, SelectionKey.OP_ACCEPT);
		serverKey.attach(new ServerHandler());
	}

main函数非常简单,迭代SelectionKey,对每个key的attachment为Handler,调用它的execute的方法,不用管它是服务器Socket还是客户Socket。注意initServer方法将serverKey附加了一个ServerHandler。下面是ServerHandler的代码:
	class ServerHandler implements Handler {
		public void execute(Selector selector, SelectionKey key) {
			ServerSocketChannel server = (ServerSocketChannel) key.channel();
			SocketChannel client = null;
			try {
				client = server.accept();
				System.out.println("Accepted connection from " + client);
			} catch (IOException e) {
				e.printStackTrace();
				return;
			}
			
			SelectionKey clientKey = null;
			try {
				client.configureBlocking(false);
				clientKey = client.register(selector, SelectionKey.OP_READ);
				clientKey.attach(new ClientHandler());
			} catch (IOException e) {
				if (clientKey != null)
					clientKey.cancel();
				try { client.close(); } catch (IOException ioe) { }
			}
		}
	}

ServerHandler接收连接,为每个客户Socket注册OP_READ操作,返回的clientKey附加上ClientHandler。
	class ClientHandler implements Handler {
		private ByteBuffer buffer;
		
		public ClientHandler() {
			buffer = ByteBuffer.allocate(100);
		}
		
		public void execute(Selector selector, SelectionKey key) {
			try {
				if (key.isReadable()) {
					readKey(selector, key);
				} else if (key.isWritable()) {
					writeKey(selector, key);
				}
			} catch (IOException e) {
				key.cancel();
				try { key.channel().close(); } catch (IOException ioe) { }
			}
		}
		
		private void readKey(Selector selector, SelectionKey key) throws IOException {
			SocketChannel client = (SocketChannel) key.channel();
			int n = client.read(buffer);
			if (n > 0) {
				buffer.flip();
				key.interestOps(SelectionKey.OP_WRITE);		// switch to OP_WRITE
			}
		}
		
		private void writeKey(Selector selector, SelectionKey key) throws IOException {
			// System.out.println("is writable...");
			SocketChannel client = (SocketChannel) key.channel();
			client.write(buffer);
			if (buffer.remaining() == 0) {	// write finished, switch to OP_READ
				buffer.clear();
				key.interestOps(SelectionKey.OP_READ);
			}
		}
	}

这个代码没有什么新内容,只是将根据key是可读还可写拆分为两个方法,代码结构显得更清晰。对于EchoServer,这么做确实有些过度工程,对于稍微复杂一点的服务器这么做是很值得的。

代码:EchoServer.java, EchoServer2.java, EchoServer3.java

参考:
  1. The Rox Java NIO Tutorial
  2. Architecture of a Highly Scalable NIO-Based Server
分享到:
评论
10 楼 runjia1987 2017-03-06  
cpu 100%,是因为读=-1时,没有解注册OP_READ,应改成:
key.interestOps(key.interestOps() & ~SelectionKey.OP_READ);
注册OP_WRITE,应该为:
key.interestOps(key.interestOps() | SelectionKey.OP_WRITE);
9 楼 two_plus 2017-02-27  
按照你的例子 服务端变成读循环了...
8 楼 ilovemyyang 2016-05-31  
我也遇到同样的空循环问题,也是百度才找到了的,楼主给力哦
7 楼 xiaoduanayu 2015-09-02  
再次过来感谢博主,看完茅塞顿开,结尾处的两个参考链接也十分给力!  
6 楼 xiaoduanayu 2015-09-02  
好文一定要顶!d=====( ̄▽ ̄*)b
我也发现了这个问题,我参照的是孙卫琴写的那本java网络编程精解,发现写事件一直在空轮询,在网上搜了个遍,搜到了这篇文章,博主好样的!
5 楼 zxywithal 2014-05-03  
4 楼 hardPass 2013-08-12  
这篇文章写的赞,
3 楼 cfyme 2013-03-24  
学习了,学习了NIO
2 楼 lvshuding 2012-10-11  
学习了,以我很有意义,谢谢分享。
1 楼 youjianbo_han_87 2012-09-25  
呵呵,不错。。。

相关推荐

    java NIO和java并发编程的书籍

    java NIO和java并发编程的书籍java NIO和java并发编程的书籍java NIO和java并发编程的书籍java NIO和java并发编程的书籍java NIO和java并发编程的书籍java NIO和java并发编程的书籍java NIO和java并发编程的书籍java...

    java nio 网络编程指南

    基于java nio的服务器与客户端的开发指南

    Java NIO 编程指引

    Java,NIO,教程 非堵塞IO编程

    JavaNIO chm帮助文档

    Java NIO系列教程(一) Java NIO 概述 Java NIO系列教程(二) Channel Java NIO系列教程(三) Buffer Java NIO系列教程(四) Scatter/Gather Java NIO系列教程(五) 通道之间的数据传输 Java NIO系列教程(六)...

    Java NIO 网络编程初探

    Java NIO 网络编程初探 1. Java NIO Java 1.4 版本添加了一个新的IO API,称为NIO(New IO)。NIO拥有所有IO的功能,但是操作方法却完全不一样。NIO支持面向缓冲区的、基于通道的IO操作。能够更加高效的进行IO操作。...

    Java NIO英文高清原版

    Java NIO英文高清原版

    java网络编程NIO视频教程

    01-Java NIO-课程简介.mp4 02-Java NIO-概述.mp4 03-Java NIO-Channel-概述.mp4 04-Java NIO-Channel-FileChannel(介绍和示例).mp4 05-Java NIO-Channel-FileChannel详解(一).mp4 06-Java NIO-Channel-FileChannel...

    java nio 包读取超大数据文件

    Java nio 超大数据文件 超大数据文件Java nio 超大数据文件 超大数据文件Java nio 超大数据文件 超大数据文件Java nio 超大数据文件 超大数据文件Java nio 超大数据文件 超大数据文件Java nio 超大数据文件 超大数据...

    java NIO.zip

    java NIO.zip

    java NIO 中文版

    讲解了 JavaIO 与 JAVA NIO区别,JAVA NIO设计理念,以及JDK中java NIO中语法的使用

    Java NIO 中文 Java NIO 中文 Java NIO 中文文档

    Java NIO 深入探讨了 1.4 版的 I/O 新特性,并告诉您如何使用这些特性来极大地提升您所写的 Java 代码的执行效率。这本小册子就程序员所面临的有代表性的 I/O 问题作了详尽阐述,并讲解了 如何才能充分利用新的 I/O ...

    java nio 实现socket

    java nio 实现socketjava nio 实现socketjava nio 实现socketjava nio 实现socketjava nio 实现socket

    java NIO 视频教程

    Java NIO(New IO)是一个可以替代标准Java IO API的IO API(从Java 1.4开始),Java NIO提供了与标准IO不同的IO工作方式。 Java NIO: Channels and Buffers(通道和缓冲区) 标准的IO基于字节流和字符流进行操作的,...

    java nio 异步编程源码

    java bio nio aio socket

    java nio中文版

    java NIO是 java New IO 的简称,在 jdk1.4 里提供的新 api 。 Sun 官方标榜的特性如下: – 为所有的原始类型提供 (Buffer) 缓存支持。 – 字符集编码解码解决方案。 – Channel :一个新的原始 I/O 抽象。 – 支持...

    Java NIO.pdf

    java nio编程 非阻塞模式的通信 电子书 带目录标签

    Java Nio selector例程

    java侧起server(NioUdpServer1.java),基于Java Nio的selector 阻塞等候,一个android app(NioUdpClient1文件夹)和一个java程序(UI.java)作为两个client分别向该server发数据,server收到后分别打印收到的消息...

    java nio 读文件

    java nio 读文件,java nio 读文件

    java NIO技巧及原理

    java NIO技巧及原理解析,java IO原理,NIO框架分析,性能比较

Global site tag (gtag.js) - Google Analytics