`
marlonyao
  • 浏览: 253214 次
  • 性别: 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网络程序设计学习实例

    这个学习实例主要关注如何利用Java进行网络编程,通过实际的代码示例帮助理解概念和应用。以下是一些关键的知识点: 1. **Java网络编程基础**: - **Socket编程**:Java中的Socket类和ServerSocket类是实现TCP/IP...

    Java相关技术的源码学习心得

    在深入探讨Java源码学习心得之前,我们先要理解Java作为一种多用途、面向对象的编程语言,其广泛应用于企业级应用、移动应用、云计算、大数据等领域。Java的源码是理解其工作原理的关键,这对于提升编程技能、解决...

    Java技术_开发心得_两年开发经验_(文章汇总...热)

    这个压缩包文件名为"Java技术文章",显然包含了作者对于Java编程语言在实际应用中的深入理解和心得体会。以下是基于这些信息提炼出的一些Java技术相关的知识点: 1. **Java基础知识**:作为开发者,对Java的基础...

    Thinking In Java 4th<Java编程思想4>

    ### 《Java编程思想4》核心知识点概览 #### 一、书籍基本信息 - **书名**:《Thinking In Java 4th》(Java编程思想第四版) - **作者**:Bruce Eckel - **出版社**:MindView, Inc. #### 二、读者评论概述 1. *...

    java的一些学习心得

    6. 网络编程:Java提供了Socket和ServerSocket类进行网络编程,可以实现客户端-服务器通信,是构建分布式系统的基础。 7. JDBC数据库连接:Java Database Connectivity(JDBC)允许Java程序与各种数据库进行交互,...

    201年最新学习java心得

    Java编程语言作为一款广泛应用的开源语言,自1995年发布以来,一直是软件开发领域的热门选择。201年(假设此处为笔误,实际应为2021年或2022年)的学习Java心得,反映出这个语言持续的重要性以及其在技术更新迭代中...

    500份优秀JAVA学习文档

    7. **网络编程**:Java提供Socket编程接口,可用于创建客户端和服务器应用程序。学习文档可能会涉及TCP/IP通信、HTTP协议以及套接字编程的基本概念。 8. **反射与动态代理**:反射机制允许程序在运行时检查和操作类...

    java自学课件

    Java是一种广泛使用的面向对象的编程语言,以其跨平台、高性能、丰富的类库和强大的社区支持而闻名。"java自学课件"提供了丰富的学习资源,帮助初学者或有经验的开发者深入理解和掌握Java语言。 首先,从标题“java...

    java 开发教程 java 开发教程 java 开发教程

    Java开发还涉及网络编程,如Socket通信,以及数据库连接和操作,JDBC(Java Database Connectivity)是实现这一目标的关键API。对于Web开发,Servlet和JSP(JavaServer Pages)是构建动态网站的基础,Spring框架则...

    IO、文件、NIO 最佳阅读资料与实践

    在IT领域,输入/输出(IO)和文件操作是编程中的基础部分,而NIO(New Input/Output)则是Java平台提供的一种高级IO机制,它为开发者提供了更强大的数据传输能力。这篇博客“IO、文件、NIO最佳阅读资料与实践”可能...

    Core Java心得笔记

    【Core Java心得笔记】主要涵盖了Java编程的基础及进阶知识,包括对象导向编程、类与对象、封装、继承、多态、接口、异常处理、集合框架、IO流、线程等核心概念。以下是对这些知识点的详细阐述: 1. **对象导向编程...

    java--夜未眠<过来人的心得>

    "Java--夜未眠&lt;过来人的心得&gt;"这个标题暗示了一位经验丰富的Java开发者在深夜编程时积累的心得体会,可能涵盖了他在Java学习和实践过程中的各种经验和教训。这可能包括了错误调试、性能优化、设计模式、并发编程等多...

    java开发手册文档

    10. **Java标准库**:包括各种内置类库,如IO、NIO、网络编程、数据库连接(JDBC)等,这些都极大地丰富了Java的功能。 压缩包中的`prototype.js开发手记.doc`和`prototype.js开发者手册1.4.doc`文件,虽然不是直接...

    TCPIP.Sockets.in.Java.Second.Edition

    本书是Morgan Kaufmann出版社出版的系列之一,主要介绍了如何使用Java语言进行网络编程,并特别聚焦于TCP/IP协议栈中的Socket编程技术。本书适合那些希望深入理解网络编程并能够在实际项目中应用这些知识的专业...

    java 学习资料

    4. **IO流**和**NIO**: Java的IO流用于读写文件,网络通信等,而NIO(非阻塞I/O)提供了更高效的数据传输方式。学习资料可能包含了对这两个模块的详细讲解,以及如何在实际项目中应用。 5. **多线程**: Java内置对...

    2018年新版Java程序员面试宝典

    网络编程基础,如TCP/IP协议、HTTP协议,以及Socket编程等,都是Java程序员需要掌握的知识。 十、算法与数据结构 最后,书中还可能会包含一些基本的算法和数据结构,如排序、搜索、图论等,这些是解决问题的关键...

    传智JAVA 12月

    5. **输入/输出(I/O)**:包括文件操作、网络通信,以及NIO(非阻塞I/O)等相关内容。 6. **多线程编程**:理解并发编程的基本概念,如线程同步、互斥、死锁等,并能编写多线程程序。 7. **反射机制**:掌握Java反射...

    跑步社区(java)

    这个项目可能是为了帮助开发者了解如何在实际环境中运用Java,包括网络编程、数据库交互、用户界面设计等多个方面。 【描述】提到的“运用了java相关的一些知识点”暗示了此项目可能涵盖了Java的基础到高级特性。这...

    我的JAVA学习心德

    在深入探讨Java编程的学习心得之前,我们先了解一下Java语言的基础知识。Java是一种广泛使用的面向对象的编程语言,由Sun Microsystems(现为Oracle公司)于1995年发布。它的设计目标是“简单、通用、面向对象、健壮...

    JAVA源码笔记

    这份"JAVA源码笔记"集合了作者从零开始学习Java时,通过观看网络视频并结合实践所整理的一系列源码和学习心得。这不仅有助于加深对Java语言的理解,也能帮助初学者建立良好的编程思维。 首先,我们要明白Java是一种...

Global site tag (gtag.js) - Google Analytics