Socket TCP 协议实现服务端和客户端的简单通信 ——使用Java 的 NIO

版权声明:本文为博主原创文章,如有错误劳烦指正。转载请声明出处,便于读取最新内容。——Bestcxx https://blog.csdn.net/bestcxx/article/details/83447699

前言

在网络通信中,服务器端需要接收客户端传递的信息并进行处理,如果在接收信息的同时服务器端只能等待IO,显然会造成服务器资源的浪费,也可能造成服务的阻塞。所以本文介绍NIO 并在 Socket 通信中加以实践运用。

阻塞和非阻塞

本段摘自 《Java 高并发程序设计》
“阻塞和非阻塞通常用来形容多线程间对相互影响。比如一个线程占用了临界区资源,那么其他所有需要这个资源的线程就必须在这个临界区中进行等待。等待会导致线程挂起,这种情况就是阻塞。此时,如果占用资源的线程一直不愿意释放资源,那么其他所有阻塞在这个临界区上的线程都不能工作。”
非阻塞就是相反的意思
NIO 既可以设置为阻塞的,也可以设置为非阻塞的

Java 的 IO 和 NIO

Java 的 IO 是阻塞的,简单说就是一个线程在处理一个信息的时候,从输入、处理、成功返回或者报错,期间处理端的资源会被一直占用,而 IO 本身是一个十分耗费时间的过程,从而造成了资源的浪费和效率的下降。而NIO,又被成为 New IO 或者 Non-block I/O,其允许数据传输存储到缓存中,处理端通过轮询缓存的方式对已准备好对队列进行处理,I/O传输和数据处理分离了,从而提高了 IO 效率。
(更信息的内容请移步 https://www.cnblogs.com/diegodu/p/6823855.html ,但是建议先别看了)

NIO 中的几个概念 Channel 、Buffer 和 Selector

NIO 通过一组机制将数据传输和处理进行了分离,从而可以实现非阻塞的效果。
Channel:字面意思就是通道,比如服务端要获取客户端的信息,就要先获取通道,然后通过通道来进行信息的获取
Buffer:数据并非直接从客户端到服务端,而是先被放到缓存区中,然后服务端从通道中读取或者写入缓存,这个缓存就是 Buffer (描述的不是很书面化,但是我相信你有明白我的意思)
Selector:服务端并非直接获取到了 Channel ,而是需要先获取 Selector ,可以认为 Selector 是 Channel 的管理者,因为服务肯定不止一个,所以 Channel 也肯定不止一个。
由于Buffer的存在,Channel 可以判断是否有数据需要处理,这样有则处理,没有则不处理,数据来来就先存在 Buffer 中,然后通过 Channel 去读或写。IO 部分的操作和业务流程操作解耦了。

在这里插入图片描述

ServerSocketChannel 和 SocketChannel 是 Socket 场景下对 Channel 的一种具体实现

类似于标准和实现, Channel 是一个抽象,参考下图可以看到具体的类(或接口)关系,
ServerSocketChannel 和 SocketChannel 是 对 Channel 的一种具体实现,而且其适用于 Socket 场景

将 ServerSocketChannel 转化为 SocketChannel 的方法 accept()
ServerSocketChannel serverSocketChannel;
SocketChannel clientChannel=serverSocketChannel.accept();
SelectionKey 是 Channel 和 Selector 的关联凭证

Channel 需要被注册到 Selector 进行"托管"
每次向选择器(Selector)注册通道(Channel)时就会创建一个选择键。
通过调用某个键(SelectionKey)的 cancel 方法关闭其通道,或者通过关闭其选择器来取消该键之前,它一直保持有效。取消某个键(SelectionKey)不会立即从其选择器中移除它;相反,会将该键(SelectionKey)添加到选择器(Selector)的已取消键集,以便在下一次进行选择操作时移除它。
可通过调用某个键(SelectionKey)的 isValid 方法来测试其有效性。

SelectionKey 的四个状态

OP_READ: 可读
OP_WRITE:可写
OP_CONNECT:可连接
OP_ACCEPT:可以接收

SelectionKey 预期状态的设定和更新-感兴趣的事件

有两种方式可以设定或者更新
第一种:Channel 在注册到 Selector 时可以指定一个状态,表示这个 Channel 接下来可以做什么,状态有四种 OP_READOP_WRITEOP_CONNECTOP_ACCEPT
比如下面到代码向 selector 注册 ServerSocketChannel ,状态为 OP_ACCEPT,表示 selector 可以为 ServerSocketChannel 准备接收其他请求到相关工作了
第二种是直接调用 SelectionKey 的成员方法
需要注意的是,可以一次绑定多个类型,如下方式二的事例

//方式一:绑定时设定感兴趣事件
		SelectionKey acceptKey=serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//方式二:调用成员方法设定感兴趣事件
selectionKey.interestOps(SelectionKey.OP_READ|SelectionKey.OP_WRITE);//重新注册感兴趣的消息事件
SelectionKey 实际状态的判断

在将 Channel 在注册到 Selector 时 我们指定了这个 Channel 需要做到准备工作,这之后还需要判断通道是否满足了我们到预期状态
比如初始化时预期状态为 OP_READ,那么就要判断现在是否可以读了,可读状态满足后才可以读取或者设置为 OP_WRITE ,可写状态满足后才可以写

 // boolean 判断 selectionKey 是否可读
 selectionKey.isReadable()
可以通过 SelectionKey 获取 Channel,强制转化为具体类型

可以通过 SelectionKey 获取 Channel,强制转化为具体类型

//通过 SelectionKey 获取 ServerSocketChannel 对象
ServerSocketChannel server=(ServerSocketChannel) selectionKey对象.channel()

//通过 SelectionKey 获取 SocketChannel 对象
SocketChannel server=(SocketChannel) selectionKey对象.channel()
SelectionKey 可以携带附加对象

SelectionKey 本身是可以携带附加对象的,添加的方式有两种
第一种是在 Channel 绑定到 Selector 到时候
第二种是调用 SelectionKey 的成员方法 .attach();

// 方式一:Channel 绑定到 Selector 时
SelectionKey clientKey=clientChannel.register(selector, SelectionKey.OP_READ,"附加内容,可以是Object");

//方式二:调用 SelectionKey 成员方法
SelectionKey clientKey=clientChannel.register(selector, SelectionKey.OP_READ);
clientKey.attach("附加内容,可以是Object");		
SelectionKey 携带附加对象的获取

直接看代码

//你具体是啥类型就强制转化为啥类型
Object object=(Object) selectionKey.attachment();
代码
NIO 实现 Socket 服务端

相比 IO 实现 Socker 通信,NIO 很繁琐,要有充分的思想准备。
基本思路是,获取 selector ,注册 channel 且初始化状态为 OP_ACCEPT ,然后依次轮训 channel ,对于满足可接受的,修改状态为 OP_READ,并从新注册到 selector,对于满足可读到,读取数据

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;


/**
 * 使用线程池实现 Socket 服务端 处理 Socket 请求
 * NIO 
 * @author jie.wu
 */
public class ConcurentSocketNioServer {
	
	private Selector selector;//NIO 的 Selector
	private ExecutorService pools=Executors.newCachedThreadPool();//声明一个线程池
	
	/**
	 * <p>统计某一个Socket 花费的时间<p>
	 * key:Socket<br>
	 * value:开始的时间戳<br>
	 */
	public static Map<Socket,Long> time_start=new HashMap<Socket,Long>(10240);
	
	@SuppressWarnings("unused")
	private void startServer() throws IOException{
		selector=SelectorProvider.provider().openSelector();//通过工厂方法获得一个 Selector 实例
		ServerSocketChannel ssc=ServerSocketChannel.open();//获得表示服务端的 ServerSocketChannel 实例
		ssc.configureBlocking(false);//设置为非阻塞模式,默认是阻塞模式
		
		InetSocketAddress socketAddress=new InetSocketAddress(10000);//设置 Socket 端口
		ssc.socket().bind(socketAddress);//将 Socket 服务地址和端口绑定到 ServerSocketChannel
		
		//将 ServerSocketChannel 注册 到 selector,并将感兴趣到事件设置为 Accept
		//每向selector 注册一个ServerSocketChannel就会返回一个 SelectionKey 作为凭证
		
		
		//这个凭证是有状态的,比如接收、读、写、销毁等,这里我们并没有下一步的操作
		SelectionKey acceptKey=ssc.register(selector, SelectionKey.OP_ACCEPT);
		
		//截止到目前,我们一共只向 selector 注册了一个 SelectionKey ,感兴趣状态为 Accept
		
		//等待-分发 网络消息
		for(;;){
			//The number of keys, possibly zero, whose ready-operation sets were updated
			//这是一个阻塞方法,如果当前没有任何数据准备好,它就会等待,一旦有数据可读,它就会返回,返回值是准备就绪的 SelectionKey 的数量 
			int keyNumbers=selector.select();
			//System.out.println("准备就绪的 SelectionKey 数量="+keyNumbers);
			//获取 selector 中的所有 SelectionKey 的集合
			Set<SelectionKey> readyKeys=selector.selectedKeys();
			
			//遍历集合
			Iterator<SelectionKey> i=readyKeys.iterator();
			long endTime=0;//服务器端处理结束时间
			while(i.hasNext()){
				SelectionKey sk=(SelectionKey) i.next();
				i.remove();//将待处理的 SelectionKey 从集合中移除
				
				//SelectionKey 是否可以接收
				if(sk.isAcceptable()){
					//如果是,重新初始化一个 channel,感兴趣事件为 accept 进行客户端的接收
					doAccept(sk);
				}
				//SelectionKey 是否可读
				else if(sk.isValid()&&sk.isReadable()){
					
					//如果时间记录Map 还不包含这个 Socket
					if(!time_start.containsKey(((SocketChannel)sk.channel()).socket())){
						time_start.put(((SocketChannel)sk.channel()).socket(), System.currentTimeMillis());
					}
					//如果可读,就进行读取
					doRead(sk);
				}
				//SelectionKey 是否可写
				else if(sk.isValid()&&sk.isWritable()){
					//如果可写,则进行写操作
					doWrite(sk);
					endTime=System.currentTimeMillis();
					long beginTime=time_start.remove(((SocketChannel)sk.channel()).socket());
					System.out.println("spend:"+(endTime-beginTime)+"ms");
				}else{
					System.out.println("没有预期状态");
				}
				
			}
		}
		
	}
	
	/**
	 * 处理 Accept 状态的 SelectionKey
	 * @param sk
	 */
	private void doAccept(SelectionKey sk){
		
		//获取 SocketChannel 方式有两种,第一种 ServerSocketChannel 返回 SocketChannel
		ServerSocketChannel server=(ServerSocketChannel) sk.channel();
		SocketChannel clientChannel;
		
		//第二种 是强制转化-不可行
		//SocketChannel clientChannel=(SocketChannel) sk.channel();
		try {
			clientChannel=server.accept();
			clientChannel.configureBlocking(false);//非阻塞模式,IO结束后再通知线程来处理
			
			//向 selector 注册了一个 SelectionKey ,感兴趣状态为 Read
			SelectionKey clientKey=clientChannel.register(selector, SelectionKey.OP_READ);
			
			EchoClient echoClient=new EchoClient();
			clientKey.attach(echoClient);
			InetAddress clientAddress=clientChannel.socket().getInetAddress();
			System.out.println("NIO Accept from :"+clientAddress.getHostAddress()+".");
			
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	private class EchoClient{
		private LinkedList<ByteBuffer> outq;
		EchoClient(){
			outq=new LinkedList<ByteBuffer>();
		}
		public LinkedList<ByteBuffer> getOutputQueue(){
			return outq;
		}
		public void enqueue(ByteBuffer bb){
			outq.addFirst(bb);
		}
	}
	
	@SuppressWarnings("unused")
	private void doRead(SelectionKey sk){
		SocketChannel channel=(SocketChannel) sk.channel();
		ByteBuffer bb=ByteBuffer.allocate(8192);//准备 8k 的缓冲区
		int len;
		try {
			
			len=channel.read(bb);
			if(len<0){
				disconnect(sk);
				return;
			}
			
//			byte b[] = new byte[1024];
//			StringBuffer sbf = new StringBuffer();
//			for (int n; (n = channel.read(bb)) != -1;) {
//				sbf.append(new String(b, 0, n));
//			}
//			if(sbf.length()==0){
//				disconnect(sk);
//				return;
//			}else{
//				System.out.println("服务器端收到客户端消息:"+sbf.toString());
//			}
			
		} catch (IOException e) {
			// TODO Auto-generated catch block
			System.out.println("Failed to read from client.");
			e.printStackTrace();
			disconnect(sk);
			return;
		}
		
		bb.flip();//调用flip之后,读写指针指到缓存头部,并且设置了最多只能读出之前写入的数据长度(而不是整个缓存的容量大小)。
		pools.execute(new HandMsg(sk,bb));
		
	}
	
	@SuppressWarnings("unused")
	private void disconnect(SelectionKey sk){
        SocketChannel sc=(SocketChannel)sk.channel();
        try {
            sc.finishConnect();
        }catch (IOException e){
        	e.printStackTrace();
        	System.out.println("Fail to finish SocketChannel");
        }
    }
	
	class HandMsg implements Runnable{
		private SelectionKey sk;
		private ByteBuffer bb;
		public HandMsg(SelectionKey sk,ByteBuffer bb){
			this.sk=sk;
			this.bb=bb;
		}
		@Override
		public void run() {
			EchoClient echoClient=(EchoClient) sk.attachment();
			echoClient.enqueue(bb);//将数据压入队列
			sk.interestOps(SelectionKey.OP_READ|SelectionKey.OP_WRITE);//重新注册感兴趣的消息事件
			
			//强迫 selector 立即返回,selector.wakeup 主要是为了唤醒阻塞在selector.select上的线程,
			//让该线程及时去处理其他事情,例如注册channel,改变interestOps、判断超时等等。
			selector.wakeup(); 
		}
	}
	
	@SuppressWarnings("unused")
	private void doWrite(SelectionKey sk){
		SocketChannel channel=(SocketChannel) sk.channel();
		//获取该 SelectionKey 携带的附属信息
		EchoClient echoClient=(EchoClient) sk.attachment();
		LinkedList<ByteBuffer> outq=echoClient.getOutputQueue();
		
		ByteBuffer bb=outq.getLast();
		int len;
		try {
			len = channel.write(bb);
			if(len==-1){
				disconnect(sk);
				return;
			}
			if(bb.remaining()==0){
				//The Buffer was complately written,remove it
				outq.removeLast();
			}
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		if(outq.size()==0){
			sk.interestOps(SelectionKey.OP_READ);
		}
	}
	public static void main(String[] args) {
		ConcurentSocketNioServer c=new ConcurentSocketNioServer();
		try {
			c.startServer();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

}
参考资料

[1]、https://blog.csdn.net/zjy15203167987/article/details/82084093
[2]、《Java 高并发程序设计》
[3]、https://www.cnblogs.com/diegodu/p/6823855.html
[4]、https://www.cnblogs.com/Free-Thinker/p/6231743.html
[5]、https://blog.csdn.net/iteye_11587/article/details/82681312
[6]、https://www.cnblogs.com/sally-zhou/p/6624901.html

猜你喜欢

转载自blog.csdn.net/bestcxx/article/details/83447699