Java NIO 总结与示例

Java NIO是New IO的简称,它是一种可以替代Java IO的一套新的IO机制。它提供了一套不同于Java标准IO的操作机制。

Java NIO中涉及的基础内容有通道(Channel)和缓冲区(Buffer)、文件IO和网络IO。有关通道、缓冲区以及文件IO在这里不打算进行详细的介绍。这里参考《实战Java高并发程序设计》利用NIO实现一个Echo服务器的服务端与客户端。


在看完Echo服务器实现之后,发现使用NIO进行网络编程跟Linux中的epoll模型是非常类似的。同样是将Channel注册到Selector上,并且说明感兴趣的事件。注册之后调用selector.select()进行阻塞等待。而对于epoll来说,需要使用

epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)

进行事件的注册,使用

epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)

来进行事件阻塞等待。epoll在Linux中是一种IO复用技术,感觉NIO对于Java的作用是类似的,一个Selector可以监听多个Channel上的事件,有一个事件(可能有多个)触发时,则进行相应。


对于NIO中的socket来说,感兴趣的事件有以下几类,在SelectionKey中定义:

    public static final int OP_READ = 1 << 0;

    public static final int OP_WRITE= 1 << 2;

    public static final int OP_CONNECT= 1 << 3;

    public static final int OP_ACCEPT =1 << 4;


在Java中使用Socket进行编程的过程跟在Linux上类似,这里大体总结一下:

服务端:


1、使用静态工厂产生一个Selector实例,

    private Selectorselector =null;

    selector = SelectorProvider.provider().openSelector();


2、使用静态工厂产生一个SerSocketChannel,也就是一个服务端的通道

       ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

       serverSocketChannel.configureBlocking(false);


3、将上面的通道(Channel)绑定(bind)到服务器的地址。

       InetSocketAddressinet SocketAddress new InetSocketAddress("127.0.0.1",8001);

       serverSocketChannel.socket().bind(inetSocketAddress);


4、将服务端Channel注册到Selector,并说明感兴趣的事件。这里的SelectionKey就关联了对应的Channel和Selector。

       SelectionKey acceptKeyserverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);


5、服务端等待事件发生(这个过程在一个无限while循环中),这个过程是阻塞的。

      int readyEventNum =selector.select();


6、有事件发生了,获得所有的通道的SelectionKey,进行遍历检查,检查主要是检查这个SelectionKey是对什么事件感兴趣,不同事件有不同的处理。

           Set<SelectionKey> readyKeys selector.selectedKeys();

           Iterator<SelectionKey> iterator readyKeys.iterator();

           while(iterator.hasNext()){

              SelectionKeyreadyKey=iterator.next();

              //将正在处理的实例移除,否则就会重复处理相同的SelectionKey

              iterator.remove();

               //TODO 检查并处理

               ……

           } //endwhile

客户端编程类似,大家直接看代码吧。


服务端代码:

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.channels.spi.SelectorProvider;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class NioEchoServer {

	//selector用于处理每一个网络连接,一个服务端程序使用一个selector实例就够了
	//这个实例使用静态工厂生成
	private Selector selector = null;
	
	//用于对每一个客户端进行相应的处理,每一个请求都会委托给线程池中的线程进行实际处理
	private ExecutorService pool = Executors.newCachedThreadPool();
	
	//统计服务器线程在一个客户端花费了多少时间
	public static Map<Socket,Long> time_stat = new HashMap<Socket,Long>(10240);
	
	
	private void startServer() throws IOException{

		//服务套接字通道
		ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
		serverSocketChannel.configureBlocking(false);

		//一个socket地址。
		InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1",8001);
		
		//UnknownHostException
		serverSocketChannel.socket().bind(inetSocketAddress);
		
		//通过工厂方法获得一个Selector对象的实例
		selector = SelectorProvider.provider().openSelector();
		
		/**
		 * 注意每向selector注册一个channel就返回一个SelectionKey实例
		 * 一个SelectionKey表示一个SelectableChannel到一个Selector的注册
		 * 一个Selector可以管理多个SelectableChannel, SocketChannel是SelectableChannel的一个子类
		 */
		//这里注册感兴趣的事件为 Accept
		SelectionKey acceptKey =  serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
		
		for(;;){
			
			//这个select方法是一个阻塞方法,如果没有任何数据准备好,他就会等待,一旦有数据可读
			//他就会返回,它的返回值是已经准备就绪的SelectionKey数量。
			int readyEventNum = selector.select();
			
			//获取那些准备好的SelectionKey。因为可能多个Chennel已经准备好
			Set<SelectionKey> readyKeys = selector.selectedKeys();
			
			Iterator<SelectionKey> iterator = readyKeys.iterator();
			
			while(iterator.hasNext()){
				
				SelectionKey readyKey = iterator.next();
				
				//将正在处理的实例移除,否则就会重复处理相同的SelectionKey
				iterator.remove();
				
				if(readyKey.isAcceptable()){
					doAccept(readyKey);
				}else if(readyKey.isValid() && readyKey.isReadable()){
					if(!time_stat.containsKey(((SocketChannel)readyKey.channel()).socket())){
						time_stat.put(((SocketChannel)readyKey.channel()).socket(), System.currentTimeMillis());
						doRead(readyKey);
					}
				}else if(readyKey.isValid() && readyKey.isWritable()){
					doWrite(readyKey);
					long end = System.currentTimeMillis();
					long begin = time_stat.remove(((SocketChannel)readyKey.channel()).socket());
					System.out.println("spend: "+(end-begin)+" ms");
				}
			}//end while
			
		}//end for
	}
	
	private void doAccept(SelectionKey selectionKey){
		ServerSocketChannel server = (ServerSocketChannel) selectionKey.channel();
		SocketChannel clientChannel = null;
		
		try{
			//生成的clientChannel表示和新的客户端通信的通道
			clientChannel = server.accept();
			clientChannel.configureBlocking(false);
			
			//Register this channel for reading,将新生成的Channel注册到selector选择器上,并告诉Selector,
			//我现在对读OP_READ操作很感兴趣。这样当Selector发现这个Channel已经准备好读时,就能给线程一个通知
			SelectionKey clientKey = clientChannel.register(selector, SelectionKey.OP_READ);
			
			//Allocate an EchoClient instance and attach it to this selection key.
			//将一个客户端实例作为附件,附加到这个连接的SelectionKey上,这样在连接的处理过程中
			//我们都可以共享这个EchoClient实例
			EchoClient echoClient = new EchoClient();
			clientKey.attach(echoClient);
			
			InetAddress clientAddress = clientChannel.socket().getInetAddress();
			System.out.println("accepted connection from "+ clientAddress.getHostAddress());
		}catch(Exception e){
			System.out.println("failed to accept new client.");
			e.printStackTrace();
		}
	}
	
	//读完了要
	private void doRead(SelectionKey selectionKey){
		SocketChannel channel = (SocketChannel)selectionKey.channel();
		ByteBuffer bb = ByteBuffer.allocate(8192);
		
		int len;
		try{
			len = channel.read(bb);
			if(len<0){
				disconnect(selectionKey);
				return;
			}
		}catch(Exception e){
			System.out.println("Failed to read from client.");
			e.printStackTrace();
			disconnect(selectionKey);
			return;
		}
		bb.flip();
		pool.execute(new HandleMsg(selectionKey,bb));
	}
	
	private void doWrite(SelectionKey sk){
		SocketChannel channel = (SocketChannel) sk.channel();
		EchoClient echoClient = (EchoClient) sk.attachment();
		
		LinkedList<ByteBuffer> outq = echoClient.getOutputQueue();
		ByteBuffer byteBuffer = outq.getLast();
		
		try{
			int len = channel.write(byteBuffer);
			
			if(len==-1){
				disconnect(sk);
				return;
			}
			if(byteBuffer.remaining()==0){
				outq.removeLast();
			}
		}catch(Exception e){
			System.out.println("Failed to write to client");
			e.printStackTrace();
			disconnect(sk);
		}
		
		if(outq.size()==0){
			sk.interestOps(SelectionKey.OP_READ);
		}
		
	}
	
	private void disconnect(SelectionKey sk){
		try {
			sk.channel().close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	//启动一个新的线程任务来处理消息
	class HandleMsg implements Runnable{
		SelectionKey selectionKey;
		ByteBuffer byteBuffer;
		public HandleMsg(SelectionKey sk,ByteBuffer bb){
			this.byteBuffer = bb;
			this.selectionKey = sk;
		}
		@Override
		public void run(){
			EchoClient echoClient = (EchoClient)selectionKey.attachment();
			echoClient.enqueue(byteBuffer);
			
			//从此对写也很感兴趣,那这个通道空了就可以写了?
			selectionKey.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
			
			//强迫selector立即返回
			selector.wakeup();
		}
	}
	
	public static void main(String[] args) throws IOException {
		NioEchoServer server = new NioEchoServer();
		server.startServer();
	}

	/**
	 * 一个EchoClient代表一个客户端,保存所有收到的ByteBuffer缓冲
	 * @version 创建时间:2016年8月14日 下午7:27:51
	 */
	class EchoClient {

		private LinkedList<ByteBuffer> outq = null;
		
		EchoClient(){
			outq = new LinkedList<ByteBuffer>();
		}
		public LinkedList<ByteBuffer> getOutputQueue(){
			return outq;
		}
		public void enqueue(ByteBuffer bb){
			outq.addFirst(bb);
		}
	}
	
}

客户端代码:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.channels.spi.SelectorProvider;
import java.util.Iterator;

/**
 * 用Nio实现EchoServer的客户端
 * @version 创建时间:2016年8月14日 下午5:17:26
 */
public class ClientNio {
	
	//同样是一个客户端使用一个selector实例,这个实例也是使用静态工厂生成的
	private Selector selector = null;
	
	public void init(String ip,int port) throws IOException{
		
		//使用静态工厂产生一个channel
		SocketChannel channel = SocketChannel.open();
		channel.configureBlocking(false);
		this.selector = SelectorProvider.provider().openSelector();
		
		//将这个通道连接到服务端
		channel.connect(new InetSocketAddress(ip,port));
		
		//这个通道暂时只对 "读" 感兴趣
		channel.register(selector, SelectionKey.OP_CONNECT);
	}
	
	public void working()throws IOException{
		while(true){
			if(!selector.isOpen())
				break;
			
			//阻塞,直到有事件发生
			selector.select();

			//获取所有发生的时间,并进行遍历
			Iterator<SelectionKey> iterator = this.selector.selectedKeys().iterator();
			while(iterator.hasNext()){
				SelectionKey key = iterator.next();
				iterator.remove();
				
				if(key.isConnectable()){  //连接事件
					connect(key);
				}else if(key.isReadable()){  //读事件
					read(key);
				}
			}
		}
	}
	
	public void connect(SelectionKey selectionKey) throws IOException{
		SocketChannel channel = (SocketChannel)selectionKey.channel();
		
		//如果正在连接,则完成连接
		if(channel.isConnectionPending()){
			channel.finishConnect();
		}
		
		channel.configureBlocking(false);
		
		//这里进行了一系列转换:String -> byte[] -> ByteBuffer
		//调用的方法依次是 String.getBytes() ByteBuffer.wrap()
		channel.write(ByteBuffer.wrap(new String("hello server.\r\n").getBytes()));
		
		//从此这个channel对读感兴趣
		channel.register(this.selector, SelectionKey.OP_READ);
	}
	
	public void read(SelectionKey key) throws IOException{
		SocketChannel channel = (SocketChannel)key.channel();
		ByteBuffer buffer = ByteBuffer.allocate(100);
		channel.read(buffer);
	
		byte[] data = buffer.array();
		String msg = new String(data).trim();
		
		System.out.println("client receive: "+msg);
		channel.close();
		key.selector().close();
	}
	
	public static void main(String[] args) {
		ClientNio client = new ClientNio();
		try {
			client.init("127.0.0.1", 8001);
			client.working();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}


参考《实战Java高并发程序设计》

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是一个使用 Java NIO(New I/O)进行网络编程的简单示例: ```java import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class NIOExample { public static void main(String[] args) throws IOException { // 创建一个线程池用于处理客户端连接 ExecutorService executor = Executors.newFixedThreadPool(10); // 创建 ServerSocketChannel 并绑定端口 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.bind(new InetSocketAddress("localhost", 8080)); System.out.println("Server started on port 8080"); while (true) { // 接受客户端连接 SocketChannel socketChannel = serverSocketChannel.accept(); // 使用线程池处理客户端连接 executor.execute(() -> handleClient(socketChannel)); } } private static void handleClient(SocketChannel socketChannel) { try { ByteBuffer buffer = ByteBuffer.allocate(1024); // 读取客户端发送的数据 int bytesRead = socketChannel.read(buffer); while (bytesRead != -1) { buffer.flip(); while (buffer.hasRemaining()) { System.out.print((char) buffer.get()); } buffer.clear(); bytesRead = socketChannel.read(buffer); } // 响应客户端 String response = "Hello from server"; ByteBuffer responseBuffer = ByteBuffer.wrap(response.getBytes()); socketChannel.write(responseBuffer); // 关闭连接 socketChannel.close(); } catch (IOException e) { e.printStackTrace(); } } } ``` 这个示例创建了一个简单的服务器,监听本地的 8080 端口。当客户端连接时,会使用线程池处理连接,并读取客户端发送的数据。然后,服务器会向客户端发送 "Hello from server" 的响应,并关闭连接。 请注意,这只是一个简单的示例,实际的网络编程可能涉及更复杂的逻辑和处理。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值