网络编程之每天学习一点点[day4]-----nio实现单向通信
Posted talk.push
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了网络编程之每天学习一点点[day4]-----nio实现单向通信相关的知识,希望对你有一定的参考价值。
重要观点
客户端的通道注册和事件监听是在服务器端实现的!NIO的本质是避免原始的TCP连接建立时使用的三次握手操作,减少连接的开销!因为Channel非直连,而是注册到了多路复用器上。一个TCP连接对应多个channel。
NIO概念
我们使用nio来实现客户端发送数据,服务端接收数据这样一个例子。
先来看下NIO的几个概念:
Channel
通道,它就像自来水管道一样,网络数据通过Channel读取和写入,通道与流的不同之处在于通道是双向的,流是单向的,且是OutPutStream和InputStream的子类。通道可以用于读、写或者二者同时进行。
而最关键的是通道可以与多路复用器selector结合起来,有多种的状态位,方便多路复用器去识别。
事实上通道分为两大类:一类是网络读写的SelectableChannel;一类是用于文件操作的FileChannel。
我们使用的ServerSocketChannel和SocketChannel都是SelectableChannel的子类。
它们是对Socket和ServerScoket的向上的抽象,Channel是一个TCP连接之间的抽象,一个Tcp连接可以对应多个Channel,而不是以前的方式只有一个通信信道,这个时候就减少TCP了连接的次数。,然后将SocketChannel的相应事件注册到selector多路复用器上,监听对应的事件。
private void accept(SelectionKey key)
try
//1 获取服务通道
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
//2 执行阻塞方法
SocketChannel sc = ssc.accept();
//3 设置阻塞模式
sc.configureBlocking(false);
//4 注册到多路复用器上,并设置读取标识
sc.register(this.seletor, SelectionKey.OP_READ);
catch (IOException e)
e.printStackTrace();
Selector
多路复用器,它是NIO编程的基础,非常重要,多路复用器选择已经就绪的任务的能力。
简单说,就是Selector会不断轮询注册在其上的通道Channel,如果某个通道发生了读写操作,这个通道就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以取得就绪的channel集合,从而进行后续的IO操作。
一个多路复用器Selector可以负责成千上万的Channel通道,没有上限,这也是JDK采用了epoll替代了传统的select实现,获得连接句柄没有限制,这也就意味着我们只需要一个线程thread负责selector的轮询,就可以接入成千上万的客户端,这是JDK NIO库的巨大进步。Selector线程类似于一个管理者master,管理成千上万的通道,然后轮询哪个通道数据已经准备好,通知CPU执行IO的读写操作。
selector模式
当网络IO事件(即Channel)注册到选择器以后,selector会分配给每个管道一个key值,相当于标签。selector选择器是以轮询的方式进行查找在其上注册的所有网络IO事件(channel)。当我们的网络IO事件(Channel)准备就绪,selector就会识别,会通过key来找到相应的管道channel,进行数据的相关操作,从这个 角度看,其主动轮询的方式 仍然是同步模式!
事件状态
每个管道都会对selector选择器注册不同的事件状态,以便选择器查找。
SelectionKey.OP_CONNECT
SelectionKey.OP_ACCEPT
SelectionKey.OP_READ
SelectionKey.OP_WRITE
我们来看一幅图:
图示分析:
public Server(int port)
try
//1 打开路复用器
this.seletor = Selector.open();
//2 打开服务器通道
ServerSocketChannel ssc = ServerSocketChannel.open();
//3 设置服务器通道为非阻塞模式
ssc.configureBlocking(false);
//4 绑定地址
ssc.bind(new InetSocketAddress(port));
//5 把服务器通道注册到多路复用器上,并且监听阻塞事件
ssc.register(this.seletor, SelectionKey.OP_ACCEPT);
System.out.println("Server start, port :" + port);
catch (IOException e)
e.printStackTrace();
NIO服务端实例化流程
构造方法中的步骤描述了服务器端的实例化流程:
1 打开多路复用器selector。
2 打开ServerSocket服务器通道
3设置非阻塞模式,才能使得channel和selector结合使用。
4 绑定了监听端口
5将serverSocketChannel注册到selector上,监听阻塞事件。
看下sever.java的run方法:
@Override
public void run()
while(true)
try
//1 必须要让多路复用器开始监听 select方法阻塞 直到有新的key进来
this.seletor.select();
//2 返回多路复用器已经选择的结果集
Iterator<SelectionKey> keys = this.seletor.selectedKeys().iterator();
//3 进行遍历
while(keys.hasNext())
//4 获取一个选择的元素
SelectionKey key = keys.next();
//5 直接从容器中移除就可以了
keys.remove();
//6 如果是有效的
if(key.isValid())
//7 如果为阻塞状态
if(key.isAcceptable())
this.accept(key);
//8 如果为可读状态
if(key.isReadable())
this.read(key);
//9 写数据
if(key.isWritable())
//this.write(key); //ssc
catch (IOException e)
e.printStackTrace();
server.java启动时,进入run方法执行到this.selector.select()阻塞,直到有新的SocketChannel注册key到selector。
注意:实例化server'时注册的通道不会向下执行,this.selector.select()直接阻塞,监听客户端的通道注册。
run方法分析:
1 在run方法中服务器端迭代selectionKey,每次迭代拿出来后执行keys.remove();删除这个key。
2 判断key的状态,当客户端启动以后(只要执行sc.connect(address);),客户端的SocketChannel就被连接到了selector上,这个时候:
Iterator<SelectionKey> keys = this.seletor.selectedKeys().iterator();
开始执行,得到了多路复用器上的所有key,其中有一个是服务器端的,监听的阻塞事件,所以当客户端connect方法执行后(只是启动,不发送数据),相应服务器端拿到了所有key进行迭代和判断:
if(key.isAcceptable())
this.accept(key);
服务器端的那个key必然是服务器端的阻塞状态的key,代码一定会执行到上边代码进入accept方法:
private void accept(SelectionKey key)
try
//1 获取服务通道
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
//2 执行阻塞方法
SocketChannel sc = ssc.accept();
//3 设置阻塞模式
sc.configureBlocking(false);
//4 注册到多路复用器上,并设置读取标识
sc.register(this.seletor, SelectionKey.OP_READ);
catch (IOException e)
e.printStackTrace();
通过这个key就得到了ServerSocketChannel,并从ssc.accept方法获得了SocketChannel,设置其为非阻塞模式,并注册到多路复用器上监听“读就绪事件”,由于这个客户端的SocketChannel已经被注册到了selctor多路复用器上,那么server的run方法一定会继续将this.selector.select()阻塞方法放开,毕竟有新的通道注册上去了。由于刚才的SocketChannel注册了读就绪事件,那么:
if(key.isReadable())
this.read(key);
就会执行。
private void read(SelectionKey key)
try
//1 清空缓冲区旧的数据
this.readBuf.clear();
//2 获取之前注册的socket通道对象
SocketChannel sc = (SocketChannel) key.channel();
//3 读取数据
int count = sc.read(this.readBuf);
//4 如果没有数据
if(count == -1)
key.channel().close();
key.cancel();
return;
//5 有数据则进行读取 读取之前需要进行复位方法(把position 和limit进行复位)
this.readBuf.flip();
//6 根据缓冲区的数据长度创建相应大小的byte数组,接收缓冲区的数据
byte[] bytes = new byte[this.readBuf.remaining()];
//7 接收缓冲区数据
this.readBuf.get(bytes);
//8 打印结果
String body = new String(bytes).trim();
System.out.println("Server : " + body);
// 9..可以写回给客户端数据
catch (IOException e)
e.printStackTrace();
读取通道serverSocket的数据打印。在读取前清空缓冲区readBuffer。
接下来就可以进行客户端和服务器端的通信了!!!
需要注意的是,服务器端的accept方法只会在客户端启动的时候执行一次用来获得ServerSocket通道,并将其注册到多路复用器上,监听对应的事件。通道注册后,当前客户端就不需要再次注册通道了,除非通道断开!后续的客户端发送buffer和服务端读取buffer不需要注册通道,只是事件的监听而已。
看下客户端的代码,比较简单:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class Client
//需要一个Selector
public static void main(String[] args)
//创建连接的地址
InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8765);
//声明连接通道
SocketChannel sc = null;
//建立缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
try
//打开通道
sc = SocketChannel.open();
//进行连接
sc.connect(address);
while(true)
//定义一个字节数组,然后使用系统录入功能:
byte[] bytes = new byte[1024];
System.in.read(bytes);
//把数据放到缓冲区中
buf.put(bytes);
//对缓冲区进行复位
buf.flip();
//写出数据
sc.write(buf);
//清空缓冲区数据
buf.clear();
catch (IOException e)
e.printStackTrace();
finally
if(sc != null)
try
sc.close();
catch (IOException e)
e.printStackTrace();
这个例子中我们实现的是客户端发送数据--->服务器端读取数据,对于双向通信可以自行研究实现!
通俗理解
客户端(socketChannel)就像一群乞丐,服务端就像一个富有的大善人(serverSocketChannel)
大善人给乞丐们提供食物救济,于是派了一个大管家(selector)去负责登记并查看(register)
这些乞丐们,只有登记的才能领到食物,每个乞丐只需要登记一次就可以一直领(跟selector建立长连接)。
每当新来一个乞丐(客户端),都会登记(register)到管家(selector)那里。
当乞丐们饿了的时候就到管家那里领取(客户端发送buffer数据)食物,管家拿出登记册看到了乞丐已经登记的名字(selectionkey),且看到乞丐饿的张着嘴(读就绪),于是把食物给了乞丐(服务端发送数据)。食物每次管够(非阻塞),不会让乞丐半饱离开。乞丐只要饿了就来,不会不饿也来领事务(非阻塞)。即便来了一群乞丐,只要有人不太饿,这个乞丐也不会领食物(一个TCP连接对应多了通道),算是比较良心的,食物也不会被浪费(节省了TCP连接数)。大善人的善举传到了千里之外,大家都知道去他那里就可以得到事务,只要他活着(服务端阻塞等待乞丐到来),乞丐们就不会饿死。
以上是关于网络编程之每天学习一点点[day4]-----nio实现单向通信的主要内容,如果未能解决你的问题,请参考以下文章
软件测试自动化测试之——接口测试从入门到精通,每天学习一点点