经过前面的铺垫,在这一节我们进入NIO编程,NIO弥补了原来同步阻塞IO的不足,他提供了高速的、面向块的I/O,NIO中加入的Buffer缓冲区,体现了与原I/O的一个重要区别。在面向流的I/O中,可以将数据直接写入或者将数据直接读到Stream对象中。下面看一些概念

  Buffer缓冲区

  而在NIO中,所有数据都是用缓冲区处理的,在读取数据时,直接读到缓冲区,在写数据时,也是写到缓冲区,任何时候访问NIO中的数据,都是通过缓冲区进行操作。最常用的是个ByteBuffer缓冲区,他提供了一组功能用于操作字节数组。

  Channel通道

  Channel 是一个通道,网络数据通过它进行读写,通道和流的区别在于通道是双向的,流是单向的,一个流必须是读或者写,而通道则可以读写同时进行。Channel可以分为用于网路读写的SelectableChannel和用于文件操作的FileChannel。

  Selector多路复用器

  首先强调一点,多路复用器对于NIO编程非常重要,非常重要,多路复用器提供选择已经就绪的任务的能力。简单的讲,Selector会不断的轮询注册在其上的Channel,如果某个Channel上面发生读或者写事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续I/O操作。一个多路复用器可以轮询多个Channel,意味着只需要一个线程负责selector轮询,就可以接入成千上万个客户端。

现在改造上一节的代码使其成为NIO server端

 package com.example.biodemo;

 import java.io.*;
import java.net.ServerSocket;
import java.net.Socket; public class TimeServer {
public static void main(String[] args) throws IOException {
int port = 8090;
if (args != null && args.length > 0) {
try {
port = Integer.valueOf(args[0]);
} catch (NumberFormatException e) {
port = 8090;
}
}
// 创建多路复用线程类并初始化多路复用器,绑定端口等以及轮询注册功能
MultiplexerTimeServer timeServer = new MultiplexerTimeServer(port);
// 启动多路复用类线程负责轮询多路复用器Selector,IO数据处理等操作
new Thread(timeServer, "NIO-MultiplexerTimeSever-001").start(); // ===================以下内容注释掉================================= /* ServerSocket server = null;
try {
server = new ServerSocket(port);
System.out.println("the timeServer is start in port :" + port);
Socket socket = null;
// 引入线程池start
TimeServerHandlerExecutePool singleExecutor = new TimeServerHandlerExecutePool(50,10000);
while (true) {
socket = server.accept();
// 替换BIO中new Thread(new TimeServerHandler(socket)).start();为下一行代码
singleExecutor.execute(new TimeServerHandler(socket));
// 引入线程池end }
} finally {
if (server != null) {
System.out.println("the time server close");
server.close();
server = null;
}
}*/ }
}

多路复用类代码,其注册轮询功能及请求消息处理返回在这里进行

 package com.example.biodemo;

 import java.io.IOException;
import java.net.InetSocketAddress;
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.util.Date;
import java.util.Iterator;
import java.util.Set; public class MultiplexerTimeServer implements Runnable {
// 定义多路复用器
private Selector selector;
// 定义ServerSocketChannel
private ServerSocketChannel servChannel;
// 定义停止标识
private boolean stop; // 构造函数初始化多路复用器、并绑定监听端口
public MultiplexerTimeServer(int port) {
try {
// 打开一个多路复用器
selector = Selector.open();
// 打开ServerSocketChannel通道
servChannel = ServerSocketChannel.open();
// 绑定监听端口号,并设置backlog为1024
servChannel.socket().bind(new InetSocketAddress(port), 1024);
// 设置severSocketChannel为异步非阻塞模式
servChannel.configureBlocking(false);
// 将ServerSocketChannel 注册到Reactor 线程的多路复用器Selector上,监听ACCEPT事件,并返回一个SelectionKey类型的值
SelectionKey selectionKey = servChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("The time server is start in port:" + port);
} catch (IOException ioe) {
ioe.printStackTrace();
// 资源初始化失败则退出
System.exit(1);
}
} public void stop() {
this.stop = true;
} @Override
public void run() {
// 在线程中遍历轮询多路复用器selector
while (!stop) {
try {
/*selector.select();选择一些I/O操作已经准备好的channel。每个channel对应着一个key。这个方法是一个阻塞的选择操作。
当至少有一个通道被选择时才返回。当这个方法被执行时,当前线程是允许被中断的。*/
// 该方法是阻塞的,选择一组键,其相应的通道已为 I/O 操作准备就绪。最多等1s,如果还没有就绪的就返回0
/*如果 timeout为正,则select(long timeout)在等待有通道被选择时至多会阻塞timeout毫秒
如果timeout为零,则永远阻塞直到有至少一个通道准备就绪。
timeout不能为负数*/
selector.select(1000);
// 当有处于就绪状态的channel时,返回该channel的selectionKey集合,此通道是已准备就绪的键集,已选择键集(I/O操作已就绪返回key)始终是键集的一个子集。
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> it = selectionKeys.iterator();
// Selector如果发现channel有OP_ACCEPT或READ事件发生,下列遍历就会进行。
SelectionKey key = null;
while (it.hasNext()) {
// 来一个事件 第一次触发一个accepter线程,SocketReadHandler
key = it.next();
// 从iterator中移除该元素
it.remove();
try {
// 去对该已经准备就绪的I/O操作进行处理
dispatch(key);
} catch (Exception e) {
// 删除处理完不为空的键,关闭相关通道
if (key != null) {
key.cancel();
if (key.channel() != null) {
key.channel().close();
}
}
}
}
} catch (IOException ioe1) {
ioe1.printStackTrace();
}
}
// 最后关闭多路复用器
if (selector != null) {
try {
selector.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
//该方法用于处理轮询到的I/O已经就绪的SelectionKey键
private void dispatch(SelectionKey key) throws IOException {
if (key.isValid()) {
// 处理请求接入的信息
if (key.isAcceptable()) {
// 接受新连接,通过SelectionKey获取其通道
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
// 接收客户端连接请求并创建SocketChannel实例
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
// 添加新连接到多路复用器上
sc.register(selector, SelectionKey.OP_READ);
}
if (key.isReadable()) {
// 本方法体读取客户端发来的请求数据
SocketChannel sc = (SocketChannel) key.channel();
// 开辟一个缓冲区,这里开辟了1M
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
// 读取请求码流,返回值>0,读到字节数,返回值=0,没有读到字节数,返回值<0,说明链路已经关闭,
// 需要关闭SocketChannel
int readBytes = sc.read(readBuffer);
if (readBytes > 0) {
// 读到字节数后的解码操作,对 readBuffer 进行 flip 操作, 作用是将缓冲区当前的 limit 设置为 position
// position 设置为 0,用于后续对缓冲区的读取操作。
readBuffer.flip();
// 根据缓冲区的可读的字节个数创建字节数组
byte[] bytes = new byte[readBuffer.remaining()];
// 调用 ByteBuffer的 get 操作将缓冲区可读的字节数复制到新创建的字节数组中
readBuffer.get(bytes);
// 调用字符串中的构造函数创建请求消息体并打印。
String body = new String(bytes, "utf-8");
System.out.println("The time server receive order :" + body);
String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ? new Date(System.currentTimeMillis()).toString() : "BAD ORDER";
// 将应答消息异步发送给客户端
doWrite(sc, currentTime);
} else if (readBytes < 0) {
// 对端链路关闭
key.cancel();
sc.close();
} else {
;//读到0字节,忽略。
}
}
}
} private void doWrite(SocketChannel channel, String response) throws IOException {
if (response != null && response.trim().length() > 0) {
// 将字符创编码为字节数组
byte[] bytes = response.getBytes();
// 根据字节数组大小创建缓冲区
ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
// 复制字节数组内容进入缓冲区
writeBuffer.put(bytes);
// 进行flip操作,作用同上面
writeBuffer.flip();
// 将缓冲区中的字节数组发送出去
channel.write(writeBuffer);
}
}
}

下面是客户端的详细代码及注释,需要注意的是在客户端代码判断是否连接成功时需要进行两步判断(需要判断连接状态和连接结果是否成功),第二步判断成功需要注册状态,否则客户端发送不了消息。

 package com.example.biodemo;

 import java.io.*;
import java.net.Socket; public class TimeClient {
public static void main(String[] args) {
int port = 8090;
if (args != null && args.length > 0) {
try {
port = Integer.valueOf(args[0]);
} catch (NumberFormatException ne) {
port = 8090;
}
}
new Thread(new TimeClientHandles("127.0.0.1",port),"TimeClient-001").start();
/* 代码改造注释掉以下代码
Socket socket = null;
BufferedReader in = null;
PrintWriter out = null;
try {
socket = new Socket("127.0.0.1", port);
System.out.println(socket.getInputStream());
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = new PrintWriter(socket.getOutputStream(), true);
out.println("QUERY TIME ORDER");
System.out.println("send order 2 server succeed.");
String resp = in.readLine();
System.out.println("now is :" + resp);
} catch (IOException e1) { } finally {
if (out != null) {
out.close();
out = null;
} if (in != null) {
try {
in.close();
} catch (IOException e2) {
e2.printStackTrace();
}
in = null;
if (socket != null) {
try {
socket.close();
} catch (IOException e3) {
e3.printStackTrace();
} }
socket = null;
}
}*/
}
}

客户端HandleInput代码

 package com.example.biodemo;

 import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set; //该类用来处理异步连接和读写操作
public class TimeClientHandles implements Runnable {
private String host;
private int port;
private Selector selector;
private SocketChannel socketChannel;
private volatile boolean stop; // 构造函数初始化并连接服务器
public TimeClientHandles(String host, int port) {
this.host = host == null ? "127.0.0.1" : host;
this.port = port;
try {
selector = Selector.open();
socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
} @Override
public void run() {
// 发送连接请求
try {
doConnection();
}catch (Exception e){
e.printStackTrace();
System.exit(1);
}
// 在循环体内轮询多路复用器Selector,当有就绪的Channel时,执行handleInput(key);
while (!stop) {
try {
selector.select(1000);
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> it = keys.iterator();
SelectionKey key = null;
while (it.hasNext()) {
key = it.next();
it.remove();
try {
handleInput(key);
} catch (Exception e) {
if (key != null) {
key.cancel();
if (key.channel() != null) {
key.channel().close();
}
}
} }
} catch (IOException e) {
e.printStackTrace();
} }
} private void handleInput(SelectionKey key) throws IOException {
if (key.isValid()) {
// 判断是否连接成功(需要判断连接状态和连接结果是否成功)
SocketChannel sc = (SocketChannel) key.channel();
// 连接状态判断,是连接状态返回true,判断的是服务端是否已经返回ACK应答消息
if (key.isConnectable()) {
// 是连接状态则需要对连接结果进行判断,如果true,说明客户端连接成功,如果flase则抛出IO异常,连接失败
if(sc.finishConnect()){
sc.register(selector, SelectionKey.OP_READ);
doWrite(sc);
}else {
// 连接失败则进程退出
System.exit(1);
}
}
if (key.isReadable()) {
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int readBytes = sc.read(readBuffer);
if (readBytes > 0) {
readBuffer.flip();
byte[] bytes = new byte[readBuffer.remaining()];
readBuffer.get(bytes);
String body = new String(bytes, "utf-8");
System.out.println("Now is :" + body);
this.stop = true;
} else if (readBytes < 0) {
key.cancel();
sc.close();
} else {
;//没有读到字节什么都不做
}
}
}
} private void doConnection() {
try {
// 如果直接连接成功,则注册到多路复用器上,并注册SelectionKey.OP_READ,发送请求消息,读应答
if (socketChannel.connect(new InetSocketAddress(host, port))) {
socketChannel.register(selector, SelectionKey.OP_READ);
doWrite(socketChannel);
} else {
// 连接不成功说明服务端没有返回TCP握手应答消息,但是不代表连接失败,注册socketChannel到多路复用器上,
// 并注册SelectionKey.OP_CONNECT,当服务器返回TCP syn-ack 消息后,Selector 就能轮询到这个SocketChannel
// 处于连接就绪状态
socketChannel.register(selector, SelectionKey.OP_CONNECT);
}
} catch (IOException e) {
e.printStackTrace();
}
} private void doWrite(SocketChannel socketChannel) throws IOException {
byte[] req = "QUERY TIME ORDER".getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(req.length);
writeBuffer.put(req);
writeBuffer.flip();
socketChannel.write(writeBuffer);
if (!writeBuffer.hasRemaining()) {
System.out.println("send order 2 server succeed.");
} }
}

  通过上面代码的学习,我们对NIO的思路有一定的了解,尽管现在还不熟悉,但是认真看过,就会发现其套路脉络还是很清楚的。

netty权威指南学习笔记一——NIO入门(3)NIO的更多相关文章

  1. netty权威指南学习笔记二——netty入门应用

    经过了前面的NIO基础知识准备,我们已经对NIO有了较大了解,现在就进入netty的实际应用中来看看吧.重点体会整个过程. 按照权威指南写程序的过程中,发现一些问题:当我们在定义handler继承Ch ...

  2. netty权威指南学习笔记六——编解码技术之MessagePack

    编解码技术主要应用在网络传输中,将对象比如BOJO进行编解码以利于网络中进行传输.平常我们也会将编解码说成是序列化/反序列化 定义:当进行远程跨进程服务调用时,需要把被传输的java对象编码为字节数组 ...

  3. netty权威指南学习笔记一——NIO入门(1)BIO

    公司的一些项目采用了netty框架,为了加速适应公司开发,本博主认真学习netty框架,前一段时间主要看了看书,发现编程这东西,不上手还是觉得差点什么,于是为了加深理解,深入学习,本博主还是决定多动手 ...

  4. netty权威指南学习笔记一——NIO入门(4)AIO

    NIO2.0引入了新的异步通道的概念,并提供了异步文件通道和异步套接字通道的实现.异步通道提供以下两种方式获取操作结果. 1.通过java.util.concurrent.Future 类来表示异步操 ...

  5. netty权威指南学习笔记一——NIO入门(2)伪异步IO

    在上一节我们介绍了四种IO相关编程的各个特点,并通过代码进行复习了传统的网络编程代码,伪异步主要是引用了线程池,对BIO中服务端进行了相应的改造优化,线程池的引入,使得我们在应对大量客户端请求的时候不 ...

  6. netty权威指南学习笔记八——编解码技术之JBoss Marshalling

    JBoss Marshalling 是一个java序列化包,对JDK默认的序列化框架进行了优化,但又保持跟java.io.Serializable接口的兼容,同时增加了一些可调参数和附加特性,这些参数 ...

  7. netty权威指南学习笔记五——分隔符和定长解码器的应用

    TCP以流的方式进行数据传输,上层应用协议为了对消息进行区分,通常采用以下4中方式: 消息长度固定,累计读取到长度综合为定长LEN的报文后,就认为读取到了一个完整的消息,将计数器置位,重新开始读取下一 ...

  8. netty权威指南学习笔记三——TCP粘包/拆包之粘包现象

    TCP是个流协议,流没有一定界限.TCP底层不了解业务,他会根据TCP缓冲区的实际情况进行包划分,在业务上,一个业务完整的包,可能会被TCP底层拆分为多个包进行发送,也可能多个小包组合成一个大的数据包 ...

  9. netty权威指南学习笔记七——编解码技术之GoogleProtobuf

    首先我们来看一下protobuf的优点: 谷歌长期使用成熟度高: 跨语言支持多种语言如:C++,java,Python: 编码后消息更小,更利于存储传输: 编解码性能高: 支持不同协议版本的兼容性: ...

随机推荐

  1. 「JOI2019 Final」解题报告

    传送门 「JOI2019 Final」勇者比太郎 看懂题就很简单了,后缀和随便维护一下就好了,别用树状数组强加一个\(\log\)就行. 「JOI2019 Final」画展 显然可以先把所有的画框按大 ...

  2. crontab Yii commands 使用方法

    基本知识介绍 #crontab -u <-l, -r, -e> -u指定一个用户-l列出某个用户的任务计划-r删除某个用户的任务-e编辑某个用户的任务 cron文件语法与写法 Minute ...

  3. 如何使用ffmpeg进行音视频裁剪命令和音视频合成命令

    音视频剪裁命令 ffmpeg -i input.mp4 -ss 00:00:00 -t 10 out.ts -i : 指定视频 -ss : 开始时间 -t : 指定裁剪的秒数 音视频合并的命令 ffm ...

  4. 吴裕雄 Bootstrap 前端框架开发——Bootstrap 表格:精简表格

    <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title> ...

  5. 越南FCK批量拿站

    关键词:inurl:detail_product.asp?lang= /FCKeditor/_samples/asp/sample01.asp/FCKeditor/_samples/asp/sampl ...

  6. PAT乙级完结有感

    去年10月开始刷的题,拖拖拉拉的终于借这个假期刷完了,总的来说还是有点小激动的,毕竟,第一次刷完一个体系,在这之前,我在杭电.南阳.洛谷.leetcode.以及我们自己学校的OJ上刷过,但都没有完完整 ...

  7. .net高手:forms验证中中<forms loginUrl="" defaultUrl="">defaulturl和loginurl的区别

    .net高手:forms验证中中<forms  loginUrl="" defaultUrl="">defaulturl和loginurl的区别 d ...

  8. C# 篇基础知识3——面向对象编程

    面向过程的结构化编程,例如1972年美国贝尔研究所推出的C语言,这类编程方式重点放在在定函数上,将较大任务分解成若干小任务,每个小任务由函数实现,分而治之的思想,然而随着软件规模的不断扩张,软件的复杂 ...

  9. LINQ -- 匿名类型

    匿名类型注意事项: 匿名类型只能和局部变量配合使用,不能用于成员. 由于匿名类型没有名字,我们必须使用var关键字作为变量类型. 不能设置匿名类型对象的属性.编译器为匿名类型穿件的属性是只读的. 除了 ...

  10. 第2节 网站点击流项目(下):6、访客visit分析

    0: jdbc:hive2://node03:10000> select * from ods_click_stream_visit limit 2;+--------------------- ...