Java网关服务-AIO(二)

概述

AIO的特点就是用户程序注册一个事件后就可以做其他事情,当事件被内核执行并得到结果后,我们的CompletionHandler会在I/O回调线程中被自动调用,有点类似观察者模式;因此我们的服务端会有很多个CompletionHandler

Handlers

接口定义

为了区别一个Handler是从接收缓冲区读入数据到ByteBuffer,还是将ByteBuffer中的内容写入发送缓冲区,我们定义了两个接口

InboundHandler

/**
* server端处理请求的handler
*/
public interface InboundHandler { /**
* 从通道接收缓冲区读取数据
* @param socketChannel client的channel
* @param in 分配的byteBuffer缓冲
* @return
*/
Object read(AsynchronousSocketChannel socketChannel, ByteBuffer in);
}

OutboundHandler

/**
* 对外输出的handler
*/
public interface OutboundHandler { /**
* 向通道写输出的数据
*
* @param socketChannel client对应的channel
* @param out 向通道输出的数据
* @return
*/
Object write(AsynchronousSocketChannel socketChannel, ByteBuffer out); }

ChannelAcceptedHandler

/**
* accept完成时的handler
* deocder: 4 + body
* 前4个字节为一个int,值为body.length,后面紧跟body
* BEFORE DECODE (16 bytes) AFTER DECODE (12 bytes)
* +--------+----------------+ +----------------+
* | Length | Actual Content |----->| Actual Content |
* | 0x000C | "HELLO, WORLD" | | "HELLO, WORLD" |
* +--------+----------------+ +----------------+
* <p/>
* 无论成功还是失败,都要继续使server accept请求
* 不要再AcceptHandler中同步的读取数据
*/
public class ChannelAcceptedHandler implements CompletionHandler<AsynchronousSocketChannel, AsynchronousServerSocketChannel>, InboundHandler { private final static Logger LOGGER = LoggerFactory.getLogger(ChannelAcceptedHandler.class); /**
* 仅接受localhost请求
*/
private boolean onlyAcceptLocalhost; public ChannelAcceptedHandler(boolean onlyAcceptLocalhost) {
this.onlyAcceptLocalhost = onlyAcceptLocalhost;
} public void completed(AsynchronousSocketChannel socketChannel, AsynchronousServerSocketChannel attachment) {
//继续接受其他客户端的连接
attachment.accept(attachment, this); try {
InetSocketAddress inetSocketAddress = (InetSocketAddress) socketChannel.getRemoteAddress();
String host = inetSocketAddress.getAddress().getHostAddress();
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("remote host:{}", host);
} if (onlyAcceptLocalhost && !"127.0.0.1".equals(host)) {
LOGGER.warn("拒绝ip:{}的连接", host);
socketChannel.close();
return;
}
} catch (IOException e) {
e.printStackTrace();
} //读取前4个字节,也就是body的长度
ByteBuffer buffer = ByteBuffer.allocate(4); read(socketChannel, buffer); } @Override
public Object read(AsynchronousSocketChannel socketChannel, ByteBuffer in) {
//开始监听读头部buffer
socketChannel.read(in, in, new ChannelReadHeadHandler(socketChannel));
return null;
} public void failed(Throwable exc, AsynchronousServerSocketChannel attachment) {
attachment.accept(attachment, this);
LOGGER.error("连接错误", exc);
}
}

ChannelAcceptedHandler实现了InboundHandler,表明它是接收请求信息的CompletionHandler

onlyAcceptLocalhost

onlyAcceptLocalhost是用来进行ip过滤的,是否只接受127.0.0.1的连接,如果连接应该被拒绝,则将socketChannel.close()调,不再向下继续处理

继续接受其他客户端的连接

attachment.accept(attachment, this);

我们在AsyncServer执行了serverChannel.accept方法,为什么这里要继续执行呢?

答:执行serverChannel.accept仅注册了一次监听连接的事件,当服务端在accept完一个连接后,如果不继续注册,则服务端就无法继续接受请求了

需要非常注意的是,很多人只知道accept方法需要在每次accept成功后都要接着调用一次,却忽略了在accept失败时也需要继续调用。试想如果一个客户端在connect到server后,立即将连接close掉,而这个时候,server正准备处理这个尝试建立连接的请求,却发现已经不能用了,此时会触发failed方法,而非completed方法。网络情况是及其复杂的,这样的情况不可谓不常见。在实际测试中,我有过这样的测试用例,这也是在被坑后才注意到的。

协议

如果继续网下面讲,就需要聊到协议了。通过查看socketChannel.read方法,你会发现AIO是先声明一个ByteBuffer,让内核往里面读入对应个数的byte,也就是说,需要先确定缓冲区大小,再调用read方法,注册回调事件。

纳尼,为什么不让我有机会先知道一下总的长度呢,例如Http协议有一个请求头,可以通过请求头中的信息,知道整个请求的大小。

这也是AIO看似编码简单,但是却很麻烦的地方。

我们采用了一个极其简单的协议:int(4 byte) + realbody的协议,也就是说先读到4个字节,解析成int类型,就知道后面的realbody有多少个字节,和客户端提前约定好;

    ByteBuffer buffer = ByteBuffer.allocate(4);

    read(socketChannel, buffer);

先分配4个字节到buffer中,再继续注册read事件

	 socketChannel.read(in, in, new ChannelReadHeadHandler(socketChannel));

ChannelReadHeadHandler

ChannelReadHeadHandler将会成功读到4个字节,从而获取body的长度

/**
* 读取请求的内容,业务处理,取前4个字节,获取body的具体长度
*/
public class ChannelReadHeadHandler implements CompletionHandler<Integer, ByteBuffer>, InboundHandler { private final static Logger LOGGER = LoggerFactory.getLogger(ChannelReadHeadHandler.class); private AsynchronousSocketChannel channel; private static int BODY_PAYLOAD = Constants.MB_1; static {
String maxBodySize = System.getProperty("max_body_size");
if (maxBodySize != null && maxBodySize.trim().equals("")) {
try {
BODY_PAYLOAD = Integer.valueOf(maxBodySize);
} catch (Exception e) {
LOGGER.warn("环境变量max_body_size转换int失败:{}", maxBodySize);
}
} if (BODY_PAYLOAD < 1) {
BODY_PAYLOAD = Constants.MB_1;
} LOGGER.info("body-payload:{}", BODY_PAYLOAD);
} public ChannelReadHeadHandler(AsynchronousSocketChannel channel) {
this.channel = channel;
} public void completed(Integer result, ByteBuffer attachment) {
//如果条件成立,说明客户端主动终止了TCP套接字,这时服务端终止就可以了
if (result == -1 || result < 4) {
System.out.println("remote is close");
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
} return;
} attachment.flip();
//获取实际body的长度
Integer bodyLen = attachment.getInt();
LOGGER.info("bodyLen:" + bodyLen);
if (bodyLen > BODY_PAYLOAD) {
LOGGER.warn("请求体过大,直接丢弃,currentBodyLen:{}, maxLen:{}", bodyLen, BODY_PAYLOAD);
// result.read()
releaseConnection(channel);
return;
}
//为body生成一个buffer
ByteBuffer bodyBuffer = ByteBuffer.allocate(bodyLen); read(channel, bodyBuffer);
} @Override
public Object read(AsynchronousSocketChannel socketChannel, ByteBuffer in) {
//开始监听读buffer
socketChannel.read(in, in, new ChannelServerHandler(socketChannel));
return null;
} public void failed(Throwable exc, ByteBuffer attachment) {
releaseConnection(this.channel);
} private void releaseConnection(Channel channel) {
try {
channel.close();
LOGGER.warn("server close client channle");
} catch (IOException e) {
e.printStackTrace();
}
} }

BODY_PAYLOAD

约定一个body的最大长度,我们的服务端不可能接受无止境的body,一定需要设置一个最大长度,超过这个长度的,就直接拒绝,如1MB

长度判断

		//如果条件成立,说明客户端主动终止了TCP套接字,这时服务端终止就可以了
if (result == -1 || result < 4) {
System.out.println("remote is close");
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
} return;
}

result是成功读取到的byte个数

  • 如果为-1,标识没有读到任何东西,这次socket传输已经到达了end of stream,这个我们预期或者约定的有出入,直接关闭连接
  • 如果<4,异常情况,server无法处理一个3个字节的长度,这也和约定不符合,关闭连接,结束

获取body长度

    attachment.flip();
//获取实际body的长度
Integer bodyLen = attachment.getInt();
LOGGER.info("bodyLen:" + bodyLen);
if (bodyLen > BODY_PAYLOAD) {
LOGGER.warn("请求体过大,直接丢弃,currentBodyLen:{}, maxLen:{}", bodyLen, BODY_PAYLOAD);
releaseConnection(channel);
return;
}

ByteBuffer的常规操作,要读时先调用flip()方法,切换为读模式

attachment.getInt()从buffer中读出一个int,此处一共4个字节,恰好可以读到一个int,也就是body的长度,成功获取长度后立即判断长度是否过大

继续读body内容

 socketChannel.read(in, in, new ChannelServerHandler(socketChannel));

既然已经确定了长度,那么我们可以分配指定长度大小的Buffer继续从通道里面取数据了

ChannelServerHandler

下一节将讲解具体处理请求Handler

Java网关服务-AIO(二)的更多相关文章

  1. Java网关服务-AIO(三)

    Java网关服务-AIO(三) 概述 前两节中,我们已经获取了body的总长度,剩下的就是读出body,处理请求 ChannelServerHandler ChannelServerHandler即从 ...

  2. Java网关服务-AIO(一)

    Java网关-AIO(一) aio:声明一个byteBuffer,异步读,读完了之后回调,相比于Future.get(),可以减少阻塞.减少线程等待,充分利用有限的线程 nio:声明一个byteBuf ...

  3. Java微服务(二):负载均衡、序列化、熔断

    本文接着上一篇写的<Java微服务(二):服务消费者与提供者搭建>,上一篇文章主要讲述了消费者与服务者的搭建与简单的实现.其中重点需要注意配置文件中的几个坑. 本章节介绍一些零散的内容:服 ...

  4. Spring Cloud 网关服务 zuul 二

    有一点上篇文章忘了 讲述,nacos的加载优先级别最高.服务启动优先拉去配置信息.所以上一篇服务搭建我没有讲述在nacos 中心创建的配置文件 可以看到服务端口和注册中心都在配置文件中配置化 属性信息 ...

  5. Java微服务(二):服务消费者与提供者搭建

    本文接着上一篇写的<Java微服务(一):dubbo-admin控制台的使用>,上篇文章介绍了docker,zookeeper环境的安装,并参考dubbo官网演示了dubbo-admin控 ...

  6. Spring Cloud gateway 网关服务二 断言、过滤器

    微服务当前这么火爆的程度,如果不能学会一种微服务框架技术.怎么能升职加薪,增加简历的筹码?spring cloud 和 Dubbo 需要单独学习.说没有时间?没有精力?要学俩个框架?而Spring C ...

  7. Spring Cloud 网关服务 zuul 三 动态路由

    zuul动态路由 网关服务是流量的唯一入口.不能随便停服务.所以动态路由就显得尤为必要. 数据库动态路由基于事件刷新机制热修改zuul的路由属性. DiscoveryClientRouteLocato ...

  8. Java进阶(五十二)利用LOG4J生成服务日志

    Java进阶(五十二)利用LOG4J生成服务日志 前言 由于论文写作需求,需要进行流程挖掘.前提是需要有真实的事件日志数据.真实的事件日志数据可以用来发现.监控和提升业务流程. 为了获得真实的事件日志 ...

  9. 服务网关zuul之二:过滤器--请求过滤执行过程(源码分析)

    Zuul的核心是一系列的过滤器,这些过滤器可以完成以下功能: 身份认证与安全:识别每个资源的验证要求,并拒绝那些与要求不符的请求. 审查与监控:在边缘位置追踪有意义的数据和统计结果,从而带来精确的生成 ...

随机推荐

  1. 实验 3:Mininet 实验——测量路径的损耗率

    实验目的 在实验 2 的基础上进一步熟悉 Mininet 自定义拓扑脚本,以及与损耗率相关的设 定:初步了解 Mininet 安装时自带的 POX 控制器脚本编写,测试路径损耗率. 实验任务 h0 向 ...

  2. modelviewset views

    Python 1.4创建user/serializers.py写序列化器 from rest_ framework import serializers from user .models impor ...

  3. 操作系统:x86下内存分页机制 (1)

    前置知识: 分段的概念(当然手写过肯定是坠吼的 为什么要分页 当我们写程序的时候,总是倾向于把一个完整的程序分成最基本的数据段,代码段,栈段.并且普通的分段机制就是在进程所属的LDT中把每一个段给标识 ...

  4. Python编程学习第三课之编程从Hello World开始

    在搞定了前几节课的情况下,大家是否有一种想要跃跃欲试的赶脚,接下来就是我们开始练手的实战时刻. 每个编程人员入门编程的第一课都是向我们马上要进入的编程世界问好,"你好,世界"英文说 ...

  5. 086 01 Android 零基础入门 02 Java面向对象 01 Java面向对象基础 03 面向对象基础总结 01 面向对象基础(类和对象)总结

    086 01 Android 零基础入门 02 Java面向对象 01 Java面向对象基础 03 面向对象基础总结 01 面向对象基础(类和对象)总结 本文知识点:面向对象基础(类和对象)总结 说明 ...

  6. 题解 SP1026 【FAVDICE - Favorite Dice】

    首先,这是一道经典的期望dp题 因为最终状态 $ (所有面都被筛到过) $ 是确定的,所以才用 逆推 ,设状态 $ f[i] $ 表示已经筛到了 $ i $ 个不同的面,有 $ i\over n $ ...

  7. Spring Boot+Spring Security+JWT 实现 RESTful Api 认证(一)

    标题 Spring Boot+Spring Security+JWT 实现 RESTful Api 认证(一) 技术 Spring Boot 2.Spring Security 5.JWT 运行环境 ...

  8. 【Flutter Widgets大全】电子书开源

    [Flutter Widgets大全]是老孟耗费大量精力整理的,总共有330多个组件的详细用法,开源到Github上,希望可以帮助到大家,开源不易,点个赞可不可以. [Flutter Widgets ...

  9. PHP的学习(提前学习了,业余爱好) (一)

    一个函数一个函数地堆 strstr()函数 在本地测试的时候,代码与显示如下 1.代码: <?php echo strstr("I love Shanghai!123",&q ...

  10. C语法-函数不定长参数

    目录 前言 语法 va_list va_start va_arg va_end 前言 基于头文件 stdarg.h 基于 STM32 基于 C 如果读者对指针和堆栈的知识点比较熟悉,本笔记就一眼飘过, ...