NIO Socket 编程实现tcp通信入门(三)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qinshi965273101/article/details/81676164

1、概念介绍

  • 选择器(Selector)中的三种键集

  • 键集中每一个键(SelectionKey)的构造

  • 四种操作详情
操作 定义 二进制 就绪条件
read int OP_READ = 1 << 0; 0000 0001 OS的读缓冲区中有数据可读
write int OP_WRITE = 1 << 2; 0000 0100 底层缓冲区有空闲空间
connect int OP_CONNECT = 1 << 3; 0000 1000 客户端调用connect(),客户端才用
accept int OP_ACCEPT = 1 << 4; 0001 0000 收到一个客户端的连接请求
  • 操作配合逻辑运算符使用
| (或)
& (与)
^ (异或)相同为真,不同为假
~ (非)取反

操作是用一个int类型的变量表示,例如 0000 0101(OP_READ | OP_WRITE) 则表示 读和写两种操作。

2、代码实现

看代码时请结合后面的‘代码重点解释’,更利于理解。

package day03.nio.selector;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

public class NioSelectorServer {
	public static void main(String[] args) throws Exception {
		//1、创建一个选择器
		Selector selc = Selector.open();
		//2、创建一个服务端
		ServerSocketChannel ssc = ServerSocketChannel.open();
		//3、绑定端口
		ssc.bind(new InetSocketAddress(12345));
		//4、服务端设置为非阻塞模式
		ssc.configureBlocking(false);
		//5、把 ServerSocketChannel 注册到选择器中,并绑定对应的操作
		ssc.register(selc, SelectionKey.OP_ACCEPT);
		//6、一直选择,一直处理
		while(true) {
			//7、唯一一个会阻塞的方法,直到选择器中有准备好的通道
			selc.select();
			//8、获取准备好的键集
			Set<SelectionKey> selectedKeys = selc.selectedKeys();
			if(selectedKeys.size()>0) {
				Iterator<SelectionKey> iterator = selectedKeys.iterator();
				//9、根据键集的操作类型,做出相应的处理
				while(iterator.hasNext()) {
					SelectionKey key = iterator.next();
					if(key.isAcceptable()) {
						ServerSocketChannel ssChannel = (ServerSocketChannel) key.channel();
						SocketChannel sc = null;
						while(sc == null) {
							sc = ssChannel.accept();
						}
						
						sc.configureBlocking(false);
						sc.register(selc, SelectionKey.OP_READ);
								
					}else if(key.isConnectable()) {
						
					}else if(key.isReadable()) {
						SocketChannel sc = (SocketChannel) key.channel();
						
						ByteBuffer lenBuf = ByteBuffer.allocate(4);
						//hasRemaining 就是position-limit
						while(lenBuf.hasRemaining()) {
							sc.read(lenBuf);
						}
						lenBuf.flip();
						int len = lenBuf.getInt();
						ByteBuffer buf = ByteBuffer.allocate(len);
						while(buf.hasRemaining()) {
							sc.read(buf);
						}
						System.out.println(new String(buf.array()));
						
						sc.register(selc, key.interestOps() | SelectionKey.OP_WRITE);
					}else if(key.isWritable()) {
						SocketChannel sc = (SocketChannel) key.channel();
						String str = "我是服务端,已经收到客户端请求";
						
						ByteBuffer lenBuf = ByteBuffer.allocate(4);
						lenBuf.putInt(str.getBytes().length);
						lenBuf.flip();
						while(lenBuf.hasRemaining()) {
							sc.write(lenBuf);
						}
						ByteBuffer dataBuf = ByteBuffer.wrap(str.getBytes());
						while(dataBuf.hasRemaining()) {
							sc.write(dataBuf);
						}
						//删除写的操作,并重新注册读操作
						sc.register(selc, key.interestOps()& ~SelectionKey.OP_WRITE);
					}else {
						throw  new  Exception("对不起,这是一个未知的键!");
					}
				}
				
				iterator.remove();
			}
		}
	}
}

package day03.nio.selector;

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.util.Iterator;
import java.util.Set;

import sun.security.krb5.SCDynamicStoreConfig;

public class NioSelectorClient {
	public static void main(String[] args) throws Exception {
		//1、创建一个选择器
		Selector selc = Selector.open();
		//2、创建一个客户端
		SocketChannel sc = SocketChannel.open();
		//3、设置为非阻塞模式
		sc.configureBlocking(false);
		//4、连接服务端
		sc.connect(new InetSocketAddress(12345));
		//5、把 SocketChannel 注册到选择器中,并绑定对应的操作
		sc.register(selc, SelectionKey.OP_CONNECT);
		//6、一直选择,一直处理
		while(true) {
			//7、唯一一个会阻塞的方法,直到选择器中有准备好的通道
			selc.select();
			//8、获取准备好的键集
			Set<SelectionKey> selectedKeys = selc.selectedKeys();
			if(selectedKeys.size() > 0) {
				Iterator<SelectionKey> iterator = selectedKeys.iterator();
				//9、根据键集的操作类型,做出相应的处理
				while(iterator.hasNext()) {
					SelectionKey key = iterator.next();
					if(key.isAcceptable()) {
						
					}else if(key.isConnectable()) {
						SocketChannel sChannel = (SocketChannel) key.channel();
						while(!sChannel.isConnected()) {
							sChannel.finishConnect();
						}
						
						sChannel.register(selc, SelectionKey.OP_WRITE);
					}else if(key.isReadable()) {
						SocketChannel sChannel = (SocketChannel) key.channel();
						ByteBuffer lenBuf = ByteBuffer.allocate(4);
						while(lenBuf.hasRemaining()) {
							sChannel.read(lenBuf);
						}
						lenBuf.flip();
						int len = lenBuf.getInt();
						ByteBuffer dataBuf = ByteBuffer.allocate(len);
						while(dataBuf.hasRemaining()) {
							sChannel.read(dataBuf);
						}
						System.out.println(new String(dataBuf.array()));
					}else if(key.isWritable()) {
						SocketChannel sChannel = (SocketChannel) key.channel();
						String str = "我是客户端,正在发送信息给服务端!";
						ByteBuffer lenBuf = ByteBuffer.allocate(4);
						lenBuf.putInt(str.getBytes().length);
						lenBuf.flip();
						while(lenBuf.hasRemaining()) {
							sChannel.write(lenBuf);
						}
						ByteBuffer dataBuf = ByteBuffer.wrap(str.getBytes());
						while(dataBuf.hasRemaining()) {
							sChannel.write(dataBuf);
						}
						//删除写的操作,并重新注册读操作
						//sChannel.register(selc, key.interestOps()& ~SelectionKey.OP_WRITE | SelectionKey.OP_READ);
						sChannel.register(selc, key.interestOps() & ~SelectionKey.OP_WRITE);
						sChannel.register(selc, SelectionKey.OP_READ);
					}else {
						throw new Exception("对不起,这是一个未知的键!");
					}
				}
				
				iterator.remove();
			}
		}
		
	}
}

3、代码重点解释

3.1、调用select()方法的执行逻辑:

  • 把‘已取消键集合’中的键,从‘已选择键集合’中移除
  • 清空‘已取消键集合’
  • 判断‘注册键集合’中的各个键,是否有键准备就绪,即通道对应的 interestOps(兴趣操作) 是否就绪。若没有,则会阻塞。
  • 当‘注册键集合’中有键就绪,则判断‘已选择键的集合’中是否存在这个键。若不存在,则清空该键的 readOps,并写上对应的就绪操作。若存在,则在原有的 readOps 的基础上,加上就绪操作。

3.2、register方法的执行逻辑

  • 如果这个通道在这个选择器上已经注册过,则直接覆盖该键的 interestOps (兴趣操作),若不存在,则重新添加一个键。
  • 源码如下:

3.3、注册write操作须知:

  • 写操作的就绪条件为底层缓冲区有空闲空间,所以注册写操作后,一旦写完就立马注销掉,否则写操作一直就绪。
  • 注销写操作,只需要用 register方法覆盖 interestOps(兴趣操作)。

3.4、数据传输的协议

  • ByteBuffer中首部用一个整型来存放字节个数,表示内容的到校
  • 紧跟着整型的是真正的传输数据内容。

3.5、socket变成实现UDP协议通信,与这个类似。

猜你喜欢

转载自blog.csdn.net/qinshi965273101/article/details/81676164
今日推荐