NIO知识
NIO也被大家称为NewIO或者是Non-Block IO,顾名思义,非阻塞就是采取了一系列的措施解决了传统IO阻塞的问题,提高了资源利用率。NIO中通过什么组件来实现这一功能呢?那就是选择器。
NIO和BIO的不同
BIO原理图如下
NIO的原理图如下
可以从上面很明显的看到它们的区别,NIO是通过选择器监听/轮询事件,而BIO每个线程只能做单一的一件事。下面从不同的层面看看BIO和NIO的不同。
- 从IO技术来说:NIO基于多路复用技术,IO非阻塞,而BIO是阻塞的IO
- 从数据传输来说:NIO是面向缓冲区(Buffer,基本类型中也只有boolean没有对应的buffer),通过Channel进行数据传输,BIO是面向流的数据传输
- 从性能上说:NIO减少了很多线程的创建和销毁,相对于BIO性能有了明显提升
NIO的几个核心组件
在NIO中几个核心组件(载体+编码+传输通道+选择复用器)可以组成一套NIO数据传输系统。
Buffer
位于java.nio包下,所谓Buffer就是缓冲区的意思,在NIO体系中充当的是载体这一角色,对于基本的数据类型,它都有相对于的Buffer(除了boolean类型),除过这几种缓冲区还有一种缓冲区就是MappedByteBuffer(是ByteBuffer的子类,能够将缓冲区或文件的某个区域直接映射,从而可以减少内存拷贝次数)
其中最基本的Buffer就是ByteBuffer,它有两种分配Buffer方式
- allocate(int capacity):分配一个capacity大小的缓冲区
- allocateDirect(int capacity):分配一个capacity大小的直接缓冲区
其余的不同类型Buffer(如IntBuffer等等)不能分配直接内存。
缓冲区存在的意义
- 可以减少读写次数(可以按缓冲区大小一块一块读写)
- 可以减少内存分配和垃圾回收的次数,因为缓冲区提前分配并可以重用
缓冲区的几个重要属性
具体的关系如下图所示
- position:下一次读/写单元的位置
- limit:读/写的极限位置
- capicity:Buffer的容量
- mark:用来标记位置,可以通过reset()返回到此位置
因此它们三者的关系也就很明显了:capicity>=limit>=position,也就是它三个指针的组合就可以有Buffer复用的效果
几个重要方法
- rewind():把position设置为0,不改变极限位置,可以实现重读的效果
public final Buffer rewind() {
position = 0;
mark = -1;
return this;
}
- clear():将极限值设置为容量,将position设置为0,逻辑上清空Buffer
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
- flip():将limit=position,position=0(position本是上一次写结束的位置,切换为读的话需要将limit设置为当前的position),可以实现读写Buffer的反转
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
- mark():标记当前位置
public final Buffer mark() {
mark = position;
return this;
}
Charset
Charset类提供了对字符集的统一管理,可以通过Charset charset = Charset.forName(“UTF-8”)来选取指定的字符集,并可以进行编码或者解码等操作,也可以获得编码解码器,如下图
Channel
Channel接口
Channel是缓冲区与目的地之间的通道,Buffer可以在Channel中进行数据传输。
Channel.java,它是所有Channel的最顶级接口
public interface Channel extends Closeable {
// 如果状态为open就返回true
public boolean isOpen();
// 关闭通道
public void close() throws IOException;
}
分散读和聚集写Channel
它有两个最重要的子接口,那就是ReadableByteChannel
和WritableByteChannel
public interface ReadableByteChannel extends Channel {
// 从Channel中读出一个字节序列放入到给定的Buffer中
public int read(ByteBuffer dst) throws IOException;
}
public interface WritableByteChannel
extends Channel
{
// 从给定缓冲区给Channel中写入一个字节序列
public int write(ByteBuffer src) throws IOException;
}
它们和Buffer的关系如下图所示(以数据节点为中心)
当然在ReadableByteChannel和WritableByteChannel下面也有它的比较重要的扩展类那就是ScatteringByteChannel(分散读)和GatheringByteChannel(聚集写)
// 分散读
public interface ScatteringByteChannel
extends ReadableByteChannel
{
// 从channel中读出多个ByteBuffer,并依次放入到dsts中
public long read(ByteBuffer[] dsts, int offset, int length)
throws IOException;
public long read(ByteBuffer[] dsts) throws IOException;
}
// 聚集写
public interface GatheringByteChannel
extends WritableByteChannel
{
// 把srcs中的多个ByteBuffer依次写入到Channel
public long write(ByteBuffer[] srcs, int offset, int length)
throws IOException;
public long write(ByteBuffer[] srcs) throws IOException;
}
FileChannel
然后往下比较重要的就是FileChannel
,它主要是用于文件操作的Channel,没有构造方法,可以从FileInputStream
,FileOutputStream
,RandomAccessFile
类中的getChannel()获取对应该文件的通道。它也实现了分散读和聚集写的接口,因此它也具有此功能。
SelectableChannel
除了上面这些Channel,还有一种Channel,它是一个抽象类,实现了Channel接口,它就是SelectableChannel
,这个类也是比较重要的,尤其是在NIO网络编程中。可以看到在网络编程中用到的几个Channel都是这个类的子类。它是一个不仅支持阻塞IO也支持非阻塞IO的通道。
- 非阻塞模式下:数据的读写操作不会阻塞,可以向Selector中注册读就绪和写就绪等事件,等待这些事件发生就可以进行相应的读写操作了
- 阻塞模式:和传统的阻塞IO相同,不能去Selector中注册事件
因为它在网络编程中必不可少,因此简单介绍一下它的一些重要方法(这些方法大多都需要子类根据自身情况来进行实现)
- public final SelectableChannel configureBlocking(boolean block):可以用来配置阻塞或非阻塞
- public final boolean isBlocking():用来判断是否是阻塞的
- public final SelectionKey register(Selector sel, int ops):向指定的Selector注册事件(如读就绪/写就绪)
- public abstract SelectionKey register(Selector sel, int ops, Object att):上个方法是这个方法的一个特例(att为null的情况),此方法可以给SelectionKey关联一个附件,当触发该事件时,可以从SelectionKey中获取这个附件(一般用于Handler,触发此事件时进行相应的handler处理),也可以使用SelectionKey的attach方法进行关联
ServerSocketChannel
ServerSocketChannel
类是上面介绍到的SelectableChannel
的子类,是用于服务端的SocketChannel,是传统的ServerSocket
的替代类,可以配置阻塞和非阻塞没有构造方法,只能通过它的open()方法获得ServerSocketChannel
实例。
端口绑定:需要先得到ServerSocket,然后调用它的bind方法实现端口绑定和backlog的配置
accept():类似于ServerSocket的accept(),此类的accept()可返回SocketChannel对象
public abstract SocketChannel accept() throws IOException;
socket():返回与ServerSocketChannel关联的ServerSocket对象(每一个ServerSocketChannel都和一个ServerSocket对象关联)
SocketChannel
SocketChannel可以看作为是Socket的替代,可以配置阻塞和非阻塞,也是需要open()方法获得一个SocketChannel实例。它也支持聚集写入,分散读取。
它有两个open()方法,如下,一个是无参的一个是可以与远程服务器建立连接(选择无参后也可以使用connect(remote)方法进行远程连接),它返回的这个SocketChannel是一个阻塞的,需要对它进行非阻塞的配置
SocketChannel能产生的事件:读,写,连接
public final int validOps() {
return (SelectionKey.OP_READ
| SelectionKey.OP_WRITE
| SelectionKey.OP_CONNECT);
}
read()方法,返回-1说明读取结束
write()方法
Selector和Selectionkey
Selector可以接受ServerSocketChannel和SocketChannel的注册事件(通过SelectableChannel的注册方法,返回一个SelectionKey对象),那么Selector就会对这些事件进行监控。
SelectionKey用于跟踪这些被注册事件的句柄
Selector
Selector对象会包含有三种SelectionKey集合
// 在Selector中注册的所有SelectionKey
public abstract Set<SelectionKey> keys();
// 放的是已经被捕获的SelectionKey
public abstract Set<SelectionKey> selectedKeys();
第三种Selector中是没有对它访问的途径的,只能从官方注释中得出,意思就是cancelled-key是一个已经被取消的SelectionKey但是没有进行注销的一个Set,不能直接访问,它是all-keys的子集。
详细如下图
Selector中的一些重要方法
- public static Selector open() :得到一个Selector对象
- public abstract Set keys():得道all-keys集合
- public abstract Set selectedKeys():得到selected-key集合
- public abstract int selectNow():返回now 发生SelectionKey对象的数目,如果没有就立即返回0(非阻塞工作方式)
- public abstract int select(long timeout):返回相关事件已发生的SelectionKey对象数目,如果一个都没有就阻塞,知道出现才返回,不带参数的select()就表明永不超时(阻塞工作方式)
- public abstract Selector wakeup():唤醒,就是当线程A调用了Selector的wakeup,如果此时B线程正在select()或将要select(),B线程当前无论有没有SelectionKey都会立即返回,不会阻塞(只有一次有效期)
- public abstract void close() :关闭这个Selector,如果有线程在执行select()会立即返回,所有与Selector关联的SelectionKey都会取消
SelectionKey
SelectionKey的失效
- 调用SelectionKey的cancell方法
- SelectionKey对应的Channel关闭
- 注册到指定的Selector,Selector关闭
SelectionKey的几种触发条件,都有特定的int值来表示
// 读就绪事件,表示Channel中有数据可读了(1)
public static final int OP_READ = 1 << 0;
// 写就绪事件,表示可以向通道写数据了(4)
public static final int OP_WRITE = 1 << 2;
// 连接就绪事件,表示客户端与服务器已经建立了连接(8)
public static final int OP_CONNECT = 1 << 3;
// 接收就绪事件,表示服务端接受到了客户端的连接请求,可以进行接受连接了(16)
public static final int OP_ACCEPT = 1 << 4;
重要方法
对于SelectionKey的合法校验,或者是对上面几种状态的校验
- public abstract SelectionKey interestOps(int ops):添加一个感兴趣事件
- public abstract int interestOps():返回感兴趣事件的集合(因为1 4 8 16 不会有歧义)
- public final Object attach(Object ob):可以关联一个对象(多次关联,取最后一次对象)
NIO网络编程
阻塞型的服务端和客户端
Server
package NIO.blocking;
/*
* @Author Wrial
* @Date Created in 11:01 2020/4/11
* @Description 使用nio包下的类创建阻塞式的Server
*/
import java.io.*;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class BlockingServer {
public static void main(String[] args) throws IOException {
new BlockingServer(8090).service();
}
private static final int THREADS = 5;
private int port;
private ServerSocketChannel sSC;
private ExecutorService executorService;
public BlockingServer(int port) throws IOException {
init(port);
}
// 服务端的初始化
private void init(int port) throws IOException {
this.port = port;
sSC = ServerSocketChannel.open();
// 设置端口复用 一般只有在多播时有用到
sSC.socket().setReuseAddress(true);
// 绑定本地端口
sSC.socket().bind(new InetSocketAddress(8090));
// 根据处理器情况创建固定的线程池大小
executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * THREADS);
System.out.println("Blocking Server init in port " + port);
}
public void service() throws IOException {
while (true) {
SocketChannel socketChannel;
socketChannel = sSC.accept();
executorService.submit(new HandleTask(socketChannel));
}
}
// 因为采用的是阻塞方式,因此每个任务都需要一个线程来完成
class HandleTask implements Runnable {
// 服务端accept一定会获取到客户端的socketChannel
private SocketChannel socketChannel;
public HandleTask(SocketChannel socketChannel) {
this.socketChannel = socketChannel;
}
public void run() {
try {
handle(socketChannel);
} catch (IOException e) {
e.printStackTrace();
}
}
private void handle(SocketChannel socketChannel) throws IOException {
Socket socket = socketChannel.socket();
System.out.println("receive from " + socket.getInetAddress());
BufferedReader reader = getBufferReader(socket);
PrintWriter writer = getPrintWriter(socket);
String msg;
while ((msg = reader.readLine()) != null) {
writer.println(receiveAndEchoMsg(msg,socket.getInetAddress()));
if (msg.equals("end")) {
System.out.println("结束会话");
break;
}
}
if (socketChannel != null) socketChannel.close();
}
private String receiveAndEchoMsg(String msg, InetAddress inetAddress) {
System.out.println("receive msg from client address : "+inetAddress.getHostName() +" msg : "+ msg);
return "echo from server : " + msg;
}
private PrintWriter getPrintWriter(Socket socket) throws IOException {
OutputStream outputStream = socket.getOutputStream();
return new PrintWriter(outputStream,true);
}
private BufferedReader getBufferReader(Socket socket) throws IOException {
InputStream inputStream = socket.getInputStream();
return new BufferedReader(new InputStreamReader(inputStream));
}
}
}
基于Channel的阻塞式客户端
package NIO.blocking;
/*
* @Author Wrial
* @Date Created in 13:04 2020/4/11
* @Description 本质还是Stream
*/
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.channels.SocketChannel;
public class ChannelClient {
public static void main(String[] args) throws IOException {
new ChannelClient(8090).sendMsg();
}
private SocketChannel socketChannel;
public ChannelClient(int port) throws IOException {
init(port);
}
private void init(int port) throws IOException {
socketChannel = SocketChannel.open(new InetSocketAddress("localhost", 8090));
System.out.println("client init ");
}
private PrintWriter getWriter(Socket socket) throws IOException {
// true是自动刷新
return new PrintWriter(socket.getOutputStream(), true);
}
private BufferedReader getReader(Socket socket) throws IOException {
return new BufferedReader(new InputStreamReader(socket.getInputStream()));
}
private void sendMsg() throws IOException {
BufferedReader bufferedReader = getReader(socketChannel.socket());
PrintWriter printWriter = getWriter(socketChannel.socket());
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
String sendMsg;
while ((sendMsg = reader.readLine()) != null) {
printWriter.println(sendMsg);
System.out.println(bufferedReader.readLine());
if (sendMsg.equals("end")) {
System.out.println("end");
break;
}
}
if (socketChannel != null) socketChannel.close();
}
}
基于Stream的阻塞式客户端
package NIO.blocking;
/*
* @Author Wrial
* @Date Created in 23:19 2020/4/8
* @Description
*/
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class StreamClient {
public static void main(String[] args) throws IOException {
new StreamClient("localhost", 8090).sendMsg();
}
private String ip;
private int port;
Socket socket;
public StreamClient(String ip, int port) throws IOException {
this.ip = ip;
this.port = port;
init();
}
private void init() throws IOException {
socket = new Socket(ip, port);
System.out.println("client init - ->" + port);
}
private PrintWriter getWriter(Socket socket) throws IOException {
// true是自动刷新
return new PrintWriter(socket.getOutputStream(),true);
}
private BufferedReader getReader(Socket socket) throws IOException {
return new BufferedReader(new InputStreamReader(socket.getInputStream()));
}
private void sendMsg() throws IOException {
BufferedReader bufferedReader = getReader(socket);
PrintWriter printWriter = getWriter(socket);
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
String sendMsg;
while ((sendMsg = reader.readLine())!=null){
printWriter.println(sendMsg);
System.out.println(bufferedReader.readLine());
if (sendMsg.equals("end")){
System.out.println("end");
break;
}
}
if (socket!=null) socket.close();
}
}
非阻塞的服务端和客户端
这里需要注意的是,在非阻塞的读取过程中有可能一次能读完所有客户端发过来的数据也有可能读不完也有可能读出的数据比我们想要的多一些其他数据,因此需要使用一个特定的分隔符(如换行)来确定数据的独立性,然后读完后进行compact将读过的数据进行释放。而且在每一次读写的过程中要控制好绑定的buffer(要足够大)指针的移动。
Server
package NIO.none_blocking;
/*
* @Author Wrial
* @Date Created in 11:09 2020/4/11
* @Description 使用nio包下类创建非阻塞的Server
*/
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;
public class NoneBlockingServer {
public static void main(String[] args) throws IOException {
new NoneBlockingServer(8090).service();
}
private ServerSocketChannel serverSocketChannel = null;
private int port;
private Selector selector;
private Charset charset;
// 初始化ServerSocketChannel和选择器,并配置非阻塞
public NoneBlockingServer(int port) throws IOException {
this.port = port;
charset = Charset.forName("UTF-8");
serverSocketChannel = ServerSocketChannel.open();
selector = Selector.open();
serverSocketChannel.socket().bind(new InetSocketAddress(8090));
serverSocketChannel.configureBlocking(false);
serverSocketChannel.socket().setReuseAddress(true);
// 在选择器进行事件的注册
System.out.println("server init ");
}
public void service() throws IOException {
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (selector.select() > 0) {
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> it = selectionKeys.iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
it.remove();
// 对各种状态进行处理
try {
handle(key);
} catch (IOException e) {
e.printStackTrace();
// 只有在出现异常的时候进行关闭
try {
if (key != null) {
key.cancel();
key.channel().close();
}
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
}
}
private void handle(SelectionKey key) throws IOException {
if (key.isAcceptable()) {
handleAccept(key);
}
if (key.isReadable()) {
receive(key);
}
if (key.isWritable()) {
send(key);
}
}
private void send(SelectionKey key) throws IOException {
// System.out.println("writable send");
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
// limit = position position = 0 因为读已经到了limit了,需要进行覆盖写
buffer.flip();
String data = decode(buffer);
// 未到一行 就是无效数据
if (data.indexOf("\r\n") == -1) {
return;
}
// 截取一行
String outputData = data.substring(0, data.indexOf("\n") + 1);
System.out.println("收到的数据 :" + outputData);
// 按照编码规则进行编码
ByteBuffer outputBuffer = encode("echo : " +outputData);
while (outputBuffer.hasRemaining()) {
socketChannel.write(outputBuffer);
}
// 更新buffer的position
ByteBuffer temp = encode(outputData);
buffer.position(temp.limit());
// 删除buffer已经处理过的数据
buffer.compact();
if (outputData.equals("end\r\n")) {
key.channel();
socketChannel.close();
System.out.println("连接关闭");
}
}
private ByteBuffer encode(String data) {
return charset.encode( data);
}
private String decode(ByteBuffer buffer) {
CharBuffer charBuffer = charset.decode(buffer);
return charBuffer.toString();
}
// 如果没有读完就还会继续触发此事件
private void receive(SelectionKey key) throws IOException {
System.out.println("readable");
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
// 因为在非阻塞中不一定一次就讲数据全部读取进来
ByteBuffer readBuffer = ByteBuffer.allocate(64);
socketChannel.read(readBuffer);
// limit = position position = 0
readBuffer.flip();
buffer.limit(buffer.capacity());
// 将tempBuffer内容拷贝到buffer
buffer.put(readBuffer);
}
private void handleAccept(SelectionKey key) throws IOException {
System.out.println("acceptable");
ServerSocketChannel sSC = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = sSC.accept();
System.out.println("接收到客户端的连接请求 :" + socketChannel.getLocalAddress());
socketChannel.configureBlocking(false);
// 连接到了下一步可能就要进行数据的读
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 给客户的注册读写监听器,并将Buffer关联给这个Channel,下次用的时候直接attach
socketChannel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE, buffer);
}
}
Client
package NIO.none_blocking;
/*
* @Author Wrial
* @Date Created in 20:52 2020/4/11
* @Description
*/
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;
public class NoneBlockingClient {
public static void main(String[] args) throws IOException {
final NoneBlockingClient client = new NoneBlockingClient(8090);
Thread thread = new Thread(() -> {
try {
client.transMsgFromConsoleToSendBuffer();
} catch (IOException e) {
e.printStackTrace();
}
});
thread.start();
client.talk();
}
private SocketChannel socketChannel = null;
private int port;
private Charset charset;
private Selector selector;
private static ByteBuffer sendBuffer = ByteBuffer.allocate(1024);
private static ByteBuffer receiveBuffer = ByteBuffer.allocate(1024);
public NoneBlockingClient(int port) throws IOException {
this.port = port;
charset = Charset.forName("UTF-8");
socketChannel = SocketChannel.open();
// 连接
socketChannel.socket().connect(new InetSocketAddress("localhost", port));
// 非阻塞
socketChannel.configureBlocking(false);
// 在选择器进行事件的注册
System.out.println("client connect success ");
selector = Selector.open();
}
private void talk() {
try {
socketChannel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
while (selector.select() > 0) {
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> it = selectionKeys.iterator();
while (it.hasNext()) {
SelectionKey selectionKey = it.next();
it.remove();
try {
handle(selectionKey);
} catch (IOException e) {
e.printStackTrace();
try {
if (selectionKey!=null){
selectionKey.cancel();
selectionKey.channel().close();
}
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 用完一定要关闭channel
try {
socketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void handle(SelectionKey selectionKey) throws IOException {
if (selectionKey.isReadable()) {
receive(selectionKey);
}
if (selectionKey.isWritable()) {
send(selectionKey);
}
}
private void send(SelectionKey selectionKey) throws IOException {
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
synchronized (sendBuffer){
sendBuffer.flip();
socketChannel.write(sendBuffer);
sendBuffer.compact();
}
}
private void receive(SelectionKey selectionKey) throws IOException {
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
socketChannel.read(receiveBuffer);
receiveBuffer.flip();
String receiveData = decode(receiveBuffer);
if (receiveData.indexOf("\n")==-1) return;
String outputData = receiveData.substring(0, receiveData.indexOf("\n")+1);
System.out.println("receive data : " + outputData);
if (outputData.equals("echo : " + "end\r\n")){
selectionKey.channel();
socketChannel.close();
System.out.println("关闭和服务器的连接");
selector.close();
System.exit(0);
}
ByteBuffer temp = encode(outputData);
receiveBuffer.position(temp.limit());
receiveBuffer.compact();
}
private ByteBuffer encode(String outputData) {
return charset.encode(outputData);
}
private String decode(ByteBuffer receiveBuffer) {
return charset.decode(receiveBuffer).toString();
}
private void transMsgFromConsoleToSendBuffer() throws IOException {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
String msg = null;
while ((msg = bufferedReader.readLine()) != null) {
synchronized (sendBuffer) {
sendBuffer.put(charset.encode(msg + "\r\n"));
}
if (msg.equals("end")) {
System.out.println("退出连接");
break;
}
}
}
}
主从混合式服务端
主从混合式的服务端就类似于Netty的主从Reactor模式,一个负责进行请求的监听和分配,另外一个负责请求的处理。这里主要是通过wakeup方法来进行的,设计的很巧妙,通过锁的获取和释放来进行请求的处理,也是很巧妙(service的同步代码块只有线程启动时或者是有accept()时才能通过)。
Server
package NIO.mixture;
/*
* @Author Wrial
* @Date Created in 11:09 2020/4/11
* @Description 使用nio包下类创建主从Server,主只接收请求,从进行处理
*/
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;
public class MixtureServer {
public static void main(String[] args) throws IOException {
MixtureServer server = new MixtureServer(8090);
new Thread(()->{
server.accept();
}).start();
server.service();
}
private ServerSocketChannel serverSocketChannel = null;
private int port;
private Selector selector;
private Charset charset;
// 主采用阻塞模式 主要是用来接收请求
public MixtureServer(int port) throws IOException {
this.port = port;
charset = Charset.forName("UTF-8");
serverSocketChannel = ServerSocketChannel.open();
selector = Selector.open();
serverSocketChannel.socket().bind(new InetSocketAddress(8090));
serverSocketChannel.configureBlocking(true);
serverSocketChannel.socket().setReuseAddress(true);
// 在选择器进行事件的注册
System.out.println("server init ");
}
private Object lock = new Object();
// 接收请求并分配非worker
public void accept() {
while (true) {
try {
SocketChannel socketChannel = serverSocketChannel.accept();
System.out.println("接收到了客户端的连接 :" + socketChannel.socket().getInetAddress().getHostAddress());
socketChannel.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocate(1024);
synchronized (lock) {
// 用于对其他线程的select()方法的阻塞解除,只能生效一次
selector.wakeup();
socketChannel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE, buffer);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public void service() throws IOException {
while (true) {
// 这个lock是极为重要的,需要先能获取到这个锁才行,否则就处于阻塞状态
synchronized (lock) {
}
int n = selector.select();
if (n == 0) continue;
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> it = selectionKeys.iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
it.remove();
// 对各种状态进行处理
try {
handle(key);
} catch (IOException e) {
e.printStackTrace();
// 只有在出现异常的时候进行关闭
try {
if (key != null) {
key.cancel();
key.channel().close();
}
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
}
}
private void handle(SelectionKey key) throws IOException {
if (key.isAcceptable()) {
handleAccept(key);
}
if (key.isReadable()) {
receive(key);
}
if (key.isWritable()) {
send(key);
}
}
private void send(SelectionKey key) throws IOException {
// System.out.println("writable send");
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
// limit = position position = 0 因为读已经到了limit了,需要进行覆盖写
buffer.flip();
String data = decode(buffer);
// 未到一行 就是无效数据
if (data.indexOf("\r\n") == -1) {
return;
}
// 截取一行
String outputData = data.substring(0, data.indexOf("\n") + 1);
System.out.println("收到的数据 :" + outputData);
// 按照编码规则进行编码
ByteBuffer outputBuffer = encode("echo : " + outputData);
while (outputBuffer.hasRemaining()) {
socketChannel.write(outputBuffer);
}
// 更新buffer的position
ByteBuffer temp = encode(outputData);
buffer.position(temp.limit());
// 删除buffer已经处理过的数据
buffer.compact();
if (outputData.equals("end\r\n")) {
key.channel();
socketChannel.close();
System.out.println("连接关闭");
}
}
private ByteBuffer encode(String data) {
return charset.encode(data);
}
private String decode(ByteBuffer buffer) {
CharBuffer charBuffer = charset.decode(buffer);
return charBuffer.toString();
}
// 如果没有读完就还会继续触发此事件
private void receive(SelectionKey key) throws IOException {
System.out.println("readable");
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
// 因为在非阻塞中不一定一次就讲数据全部读取进来
ByteBuffer readBuffer = ByteBuffer.allocate(64);
socketChannel.read(readBuffer);
// limit = position position = 0
readBuffer.flip();
buffer.limit(buffer.capacity());
// 将tempBuffer内容拷贝到buffer
buffer.put(readBuffer);
}
private void handleAccept(SelectionKey key) throws IOException {
System.out.println("acceptable");
ServerSocketChannel sSC = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = sSC.accept();
System.out.println("接收到客户端的连接请求 :" + socketChannel.getLocalAddress());
socketChannel.configureBlocking(false);
// 连接到了下一步可能就要进行数据的读
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 给客户的注册读写监听器,并将Buffer关联给这个Channel,下次用的时候直接attach
socketChannel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE, buffer);
}
}
结果演示
可以看出NIO和BIO是不冲突的,是可以共存的。进一步说明的ServerSocketchannel就是ServerSocket的替代,SocketChannel就是Socket的替代,本质还是相同的!
NIO编程却是是比BIO麻烦很多,一个小点没有写正确就会出错,因为不仅需要去调整buffer指针,还要去对不同请求不同处理,因此能熟练掌握NIO后,学Netty就不再那么难懂了!
参考书籍:Java网络编程精解 不错的一本书