netty权威指南学习笔记一——NIO入门(3)NIO
经过前面的铺垫,在这一节我们进入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的更多相关文章
- netty权威指南学习笔记二——netty入门应用
经过了前面的NIO基础知识准备,我们已经对NIO有了较大了解,现在就进入netty的实际应用中来看看吧.重点体会整个过程. 按照权威指南写程序的过程中,发现一些问题:当我们在定义handler继承Ch ...
- netty权威指南学习笔记六——编解码技术之MessagePack
编解码技术主要应用在网络传输中,将对象比如BOJO进行编解码以利于网络中进行传输.平常我们也会将编解码说成是序列化/反序列化 定义:当进行远程跨进程服务调用时,需要把被传输的java对象编码为字节数组 ...
- netty权威指南学习笔记一——NIO入门(1)BIO
公司的一些项目采用了netty框架,为了加速适应公司开发,本博主认真学习netty框架,前一段时间主要看了看书,发现编程这东西,不上手还是觉得差点什么,于是为了加深理解,深入学习,本博主还是决定多动手 ...
- netty权威指南学习笔记一——NIO入门(4)AIO
NIO2.0引入了新的异步通道的概念,并提供了异步文件通道和异步套接字通道的实现.异步通道提供以下两种方式获取操作结果. 1.通过java.util.concurrent.Future 类来表示异步操 ...
- netty权威指南学习笔记一——NIO入门(2)伪异步IO
在上一节我们介绍了四种IO相关编程的各个特点,并通过代码进行复习了传统的网络编程代码,伪异步主要是引用了线程池,对BIO中服务端进行了相应的改造优化,线程池的引入,使得我们在应对大量客户端请求的时候不 ...
- netty权威指南学习笔记八——编解码技术之JBoss Marshalling
JBoss Marshalling 是一个java序列化包,对JDK默认的序列化框架进行了优化,但又保持跟java.io.Serializable接口的兼容,同时增加了一些可调参数和附加特性,这些参数 ...
- netty权威指南学习笔记五——分隔符和定长解码器的应用
TCP以流的方式进行数据传输,上层应用协议为了对消息进行区分,通常采用以下4中方式: 消息长度固定,累计读取到长度综合为定长LEN的报文后,就认为读取到了一个完整的消息,将计数器置位,重新开始读取下一 ...
- netty权威指南学习笔记三——TCP粘包/拆包之粘包现象
TCP是个流协议,流没有一定界限.TCP底层不了解业务,他会根据TCP缓冲区的实际情况进行包划分,在业务上,一个业务完整的包,可能会被TCP底层拆分为多个包进行发送,也可能多个小包组合成一个大的数据包 ...
- netty权威指南学习笔记七——编解码技术之GoogleProtobuf
首先我们来看一下protobuf的优点: 谷歌长期使用成熟度高: 跨语言支持多种语言如:C++,java,Python: 编码后消息更小,更利于存储传输: 编解码性能高: 支持不同协议版本的兼容性: ...
随机推荐
- 收藏 40 2 CPD (广告合作方式)
CPD,Cost per day的缩写,意思是按天收费,是一种广告合作方式. 在实际的广告合作中根据行业不同还包括Cost per Download的缩写含义,意思是依据实际下载量收费. “CPD ...
- JAVA--文件内容属性替换
说明:文件中执行内容是变量,随着环境不同会配置不同,在程序启动后,读取配置进行变量替换 1.测试类如下: public class FileUtilsTest { //public static bo ...
- 技术|Android安装包极限优化
版权声明 1.本文版权归原作者所有,转载需注明作者信息及原文出处. 2.本文作者:赵裕(vimerzhao),永久链接:https://github.com/vimerzhao/vimerzhao.g ...
- Spring Boot整合Mybatis(注解方式和XML方式)
其实对我个人而言还是不够熟悉JPA.hibernate,所以觉得这两种框架使用起来好麻烦啊. 一直用的Mybatis作为持久层框架, JPA(Hibernate)主张所有的SQL都用Java代码生成, ...
- 2.Git知识
Git安装 Git在不同的操作系统上安装是不一样的,这里只讲解我们需要的,也就是在Windows下的安装,主要安装msysGit和TortoiseGit 安装msysGit,到https://code ...
- 你需要知道的 JavaScript 类(class)的这些知识
作者: Dmitri Pavlutin译者:前端小智来源:dmitripavlutin 点赞再看,养成习惯 本文 GitHub https://github.com/qq44924588... 上已经 ...
- 回顾PHP:第一章:PHP基础语法(2)
十.PHP常量和变量——用常量限制用户跳过某些文件(重要) 十.1常量在代码中定义.书写方式: define(常量名,常量值) 注:1.常量值只能为标量 2.常量名可以小写,但是通常大写 3.常量名可 ...
- JS数据统计表 highcharts.js的运用
参考地址 http://www.runoob.com/highcharts/highcharts-column-basic.html 1.下载JS文件引入,或者用CDN function getCou ...
- MySQL更新时间
Mysql时间加减函数为date_add().date_sub()定义和用法DATE_ADD() 函数向日期添加指定的时间间隔.DATE_SUB() 函数向日期减少指定的时间间隔.语法DATE_ADD ...
- bzoj 4747: [Usaco2016 Dec]Counting Haybales
23333,在扒了一天题解之后发现我竟然还能秒题,虽然这是个pj的sb题... (排个序,然后upper_bound和lower_bound一用就行了(是不是有O(1)的查询方法啊??貌似要离散啊,一 ...