《Java TCP/IP Socket编程》读书笔记(16)

第五章 NIO


5.1我们为什么需要NIO

多线程环境下对共享状态进行同步访问,增加了系统调度和切换上下文的开销,程序员对这些开销无法控制。

阻塞等待。

我们需要一种可以一次轮询一组客户端,以查找那个客户端需要服务。在NIO中,一个Channel代表一个可以轮询的I/O目标,Channel能够注册一个Selector实例。Selectorselect可以查找“在当前一组信道中,哪一个需要服务”。

Buffer提供了比Stream抽象更高效和可预测的I/OStream抽象好的方面是隐藏了底层缓冲区的有限性,提供了一个能够容纳任意长度数据的容器的假象,要么会产生大量的内存开销,要么会引入大量的上下文切换。使用线程的过程中,这些开销都隐藏在具体实现中,也失去了对其的可控性和可预测性。这种方法使得编写程序变得容易,但调整他们的性能则变得困难。不幸的是,使用JavaSocket抽象,流是唯一的选择

Buffer抽象代表了一个有限容量的数据容器——其本质是一个数组,由指针指示了在哪存放数据和在哪读取数据。使用Buffer有两个好处,第一、与读写缓冲区数据相关联的系统开销暴露给了程序员,第二、一些对Java对象特殊的Buffer映射能够直接操作底层的平台的资源,这样操作节省了在不同的地址空间复制数据的开销。


5.2 Buffer一起使用Channel

Channel使用的不是流而是缓冲区来发送或者读取数据。Buffer类或者其任何子类的实例都可以看做是一个定长的Java基本数据类型元素序列。与流不同,缓冲区由固定的、有限的容量,并由内部状态记录了由多少数据放入或者取出,就像是有限容量的队列一样。在Channel中使用的Buffer通常不是构造函数创建的,而是通过调用allocate()方法创建指定容量的Buffer实例:

ByteBuffer buffer = ByteBuffer.allocate(CAPACITY)

或者使用包装一个已有数据来实现

ByteBuffer buffer = ByteBuffer.wrap(byteArray)

NIO的强大来自于channel的非阻塞特性。

下面是一个字符回显的非阻塞客户端

package com.suifeng.tcpip.chapter5;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

/**
 * 非阻塞客户端
 * 
 * @author Suifeng
 * 
 */
public class TCPEchoClientNonBlocking
{

	public static void main(String[] args) throws IOException
	{
		if (args.length < 2 || args.length > 3)
		{
			throw new IllegalArgumentException("Parameters:<Server> <Word> [<Port>]");
		}

		String server = args[0];

		byte[] msg = args[1].getBytes();

		int serverPort = (args.length == 3) ? Integer.parseInt(args[2]) : 7;
		
		SocketChannel channel = SocketChannel.open();
		
		// 将信道置为非阻塞方式
		channel.configureBlocking(false);
		
		if(!channel.connect(new InetSocketAddress(server,serverPort)))
		{
			System.out.print("Trying connected server");
			// 轮询连接的状态,知道建立连接,这样忙等比较浪费资源,
			while(!channel.finishConnect())
			{
				System.out.print(".");
			}
		}
		
		System.out.println("\nClient has connected to server successfully");
		
		// 写缓冲区
		ByteBuffer writeBuffer = ByteBuffer.wrap(msg);
		// 读缓冲区
		ByteBuffer readBuffer = ByteBuffer.allocate(msg.length);
		
		int totalBytesReceived = 0;
		int bytesReceived = -1;
		
		System.out.print("Waiting for server Response");
		
		while(totalBytesReceived < msg.length)
		{
			// 向服务器发送数据
			if(writeBuffer.hasRemaining())
			{
				channel.write(writeBuffer);
			}
			
			// 等待服务器返回数据
			if((bytesReceived = channel.read(readBuffer)) == -1)
			{
				throw new SocketException("Connection closed prematurely");
			}
			
			totalBytesReceived += bytesReceived;
			System.out.print(".");
		}
		
		System.out.println("");
		
		System.out.println("Received: "+new String(readBuffer.array(),0,totalBytesReceived));
		channel.close();
	}
}

启动服务器端,监听39393端口


启动客户端


再次查看服务器端



5.3 Selector

一个Selector实例可以检查一组信道的I/O状态。

下面使用信道和选择器实现一个回显服务器,并且不适用多线程和忙等。

协议接口

package com.suifeng.tcpip.chapter5;

import java.io.IOException;
import java.nio.channels.SelectionKey;

/**
 * 回显服务器协议接口
 * @author Suifeng
 *
 */
public interface TCPProtocol
{
	/**
	 * 接收请求
	 * @param key
	 * @throws IOException
	 */
	void handleAccept(SelectionKey key) throws IOException;
	
	/**
	 * 读取数据
	 * @param key
	 * @throws IOException
	 */
	void handleRead(SelectionKey key) throws IOException;
	
	/**
	 * 接收数据
	 * @param key
	 * @throws IOException
	 */
	void handleWrite(SelectionKey key) throws IOException;
}

回显协议的实现


package com.suifeng.tcpip.chapter5;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;

/**
 * 使用选择器和信道实现的回显协议
 * @author Administrator
 *
 */
public class EchoSelectorProtocol implements TCPProtocol
{
	private int bufferSize;

	public EchoSelectorProtocol(int bufferSize) {
		super();
		this.bufferSize = bufferSize;
	}

	@Override
	public void handleAccept(SelectionKey key) throws IOException
	{
		System.out.println("Handle Accepting Now...");
		SocketChannel channel = ((ServerSocketChannel) key.channel()).accept();
		// 设置为阻塞方式
		channel.configureBlocking(false);
		// 信道可读
		channel.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocate(bufferSize));
	}

	@Override
	public void handleRead(SelectionKey key) throws IOException
	{
		System.out.println("Handle Reading Now...");
		SocketChannel channel = (SocketChannel) key.channel();
		ByteBuffer buf = (ByteBuffer) key.attachment();

		long bytesRead = channel.read(buf);
			
		System.out.println("Receiving from client:" + channel.socket().getRemoteSocketAddress()+"\nReceived:"+new String(buf.array()));

		if (bytesRead == -1)
		{
			channel.close();
		}
		else if(bytesRead > 0)
		{
			// 信道可读、可写
			key.interestOps(SelectionKey.OP_WRITE | SelectionKey.OP_READ);
		}
	}

	@Override
	public void handleWrite(SelectionKey key) throws IOException
	{
		System.out.println("Handling Writing Now....");
		ByteBuffer buf = (ByteBuffer) key.attachment();
		buf.flip();
		SocketChannel channel = (SocketChannel) key.channel();
		
		// 向客户端写入数据
		channel.write(buf);

		if (!buf.hasRemaining())
		{
			// 信道可读
			key.interestOps(SelectionKey.OP_READ);
		}

		buf.compact();
	}

}

服务器端

package com.suifeng.tcpip.chapter5;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.util.Iterator;

/**
 * 非阻塞处理服务器端
 * 
 * @author Suifeng
 * 
 */
public class TCPServerSelector
{
	private static final int BUFFER_SIZE = 32;
	private static final int TIMEOUT = 3000;

	public static void main(String[] args) throws IOException
	{
		if (args.length < 1)
		{
			throw new IllegalArgumentException("Parameter(s):<Port> ...");
		}
		
		// 创建选择器实例
		Selector selector = Selector.open();

		// 可以同时监听来自多个信道的数据,使用不同的端口
		for (String arg : args)
		{
			// 创建信道
			ServerSocketChannel serverChannel = ServerSocketChannel.open();
			// 侦听指定的端口
			serverChannel.socket().bind(new InetSocketAddress(Integer.parseInt(arg)));
			// 将信道设置为非阻塞方式
			serverChannel.configureBlocking(false);
			
			// 该信道可以进行accept操作
			serverChannel.register(selector, SelectionKey.OP_ACCEPT);
		}
		
		TCPProtocol protocol = new EchoSelectorProtocol(BUFFER_SIZE);
		
		System.out.println("Server is Running.");
		
		while(true)
		{
			// 阻塞等待直到超时
			if(selector.select(TIMEOUT) == 0)
			{
				System.out.println("Waiting data from client.");
				continue;
			}
			
			// 获取选择器下的键集
			Iterator<SelectionKey> keys = selector.selectedKeys().iterator();
			
			while(keys.hasNext())
			{
				SelectionKey key = keys.next();
				
				if(key.isAcceptable())					// accept操作
				{
					protocol.handleAccept(key);
				}
					
				if(key.isReadable())					// 可读
				{
					protocol.handleRead(key);
				}
				
				if(key.isValid() && key.isWritable())	// 可写
				{
					protocol.handleWrite(key);
				}
				
				 keys.remove();
			}
		}
	}
}

启动服务器端,侦听39393和39395端口


启动客户端,一次使用39393端口和39395端口发送数据


再次查看服务器端


  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值