0%

概述

在对多路IO复用进行实现的时候,ByteBuf这个结构是必不可少的,尽管NIO中也实现了ByteBuf,但使用起来并不是特别方便,而且有可以优化的地方。 因此,Netty在ByteBuf这一块完全是自己的一套方案,不仅使用起来方便,有着丰富的API,而且性能也是非常的出色,本篇将对ByteBuf这一组件进行学习。

ByteBuf结构

1
2
3
4
5
6
/*     +-------------------+------------------+------------------+
* | discardable bytes | readable bytes | writable bytes |
* +-------------------+------------------+------------------+
* | | | |
* 0 <= readerIndex <= writerIndex <= capacity
*/

源码的注释很形象,这里就直接拿来用了,ByteBuf分为下面三个区间:

  1. [0, readerIndex) 这个区间是已经读过的数据,
  2. [readerIndex, writerIndex) 这个区间是可读数据区间
  3. [writeIndex, capacity] 这段区间是可写区间
  4. 还有一个变量maxCapacity, 即如果capacity不够写了,则可以扩容,最大不超过maxCapacity,默认Integer.MAX_VALUE

ByteBuf的高频API,readXxx, writeXxx, markXxx, resetXxx… 限于篇幅这里就不过多的记录用法了…

ByteBuf的分类

bytebuf

上面的继承关系图中间删掉了几层关系,这样看起来比较方便理解(好像也没方便到哪去…名字一个个的都那么长

从三个角度对ByeBuf分类

下面从三个角度来对ByteBuf进行分类:

  1. Pooled/Unpooled(是否池化)
  2. Unsafe(是否使用Unsafe来进行数据读写)
  3. Heap/Direct(是堆中内存还是直接内存)
阅读全文 »

Pipline的创建

经过前面几篇的分析之后,可以知道Pipline的创建是在创建Channel的时候被创建的,在AbstractChannel的构造方法中,如下:

1
2
3
4
5
6
protected AbstractChannel(Channel parent) {
this.parent = parent;
id = newId();
unsafe = newUnsafe();
pipeline = newChannelPipeline();
}

newChannelPipeline方法会返回一个DefaultPipline

1
2
3
protected DefaultChannelPipeline newChannelPipeline() {
return new DefaultChannelPipeline(this);
}

下面来看看DefaultChannelPipline的具体内容:

1
2
3
4
5
6
7
8
9
10
11
12
protected DefaultChannelPipeline(Channel channel) {
this.channel = ObjectUtil.checkNotNull(channel, "channel");
succeededFuture = new SucceededChannelFuture(channel, null);
voidPromise = new VoidChannelPromise(channel, true);

// 在初始的时候创建tail,head这两个节点
tail = new TailContext(this);
head = new HeadContext(this);

head.next = tail;
tail.prev = head;
}

可以看出pipline是一个含有头节点的双向队列(和AQS底层维护的队列类似)

头节点的存在使得添加/删除一个节点都变得方便。

阅读全文 »

概述

Netty中Channel接口的描述:

A nexus to a network socket or a component which is capable of I/O operations such as read, write, connect, and bind.

即是一个对socket连接的抽象,在都满足这一抽象的前提下,Netty对Channel的实现也有很多种类,本篇将主要记录对NioServerSocketChannel和NioSocketChannel这两种Channel实现的学习。

Channel的继承关系

channel

基于上面的这个这个继承关系图,来一层一层分析每一层的抽象主要都是做了什么事情.

1. Channel

提供了接口规范,是对连接的抽象。 Channel接口中还定义了Unsafe接口,简单来说的话,是要使用Unsafe对象来去实现Channel的一些操作(例如read, write等)

2. AbstractChannel

  • 作用:

    A skeletal {@link Channel} implementation.

  • 主要做了什么:

  1. 定义了一系列的变量(如id, unsafe, pipline等等)
  2. 对id, pipline和unsafe对象进行了初始化, 其中unsafe对象的初始化使用了模版模式,让子类去实现
阅读全文 »

什么是JDK空轮询Bug

JDK NIO的空轮询BUG其实是JDK NIO在Linux系统下的epoll空轮询问题。

epoll是Linux下一种高效的IO复用方式,相较于select和poll机制来说。其高效的原因是将基于事件的fd放到内核中来完成,在内核中基于红黑树+链表数据结构来实现,链表存放有事件发生的fd集合,然后在调用epoll_wait时返回给应用程序,由应用程序来处理这些fd事件。

使用IO多路复用,Linux下一般默认就是epoll,Java NIO在Linux下默认也是epoll机制,但是JDK中epoll的实现却是有漏洞的。其中一个就是Epoll的空轮询Bug, 就是即使是关注的select轮询事件返回数量为0,NIO照样不断的从select本应该阻塞的Selector.select()/Selector.select(timeout)中wake up出来,导致CPU飙到100%问题。

官方给的Bug复现方法:

A DESCRIPTION OF THE PROBLEM :
The NIO selector wakes up infinitely in this situation..
0. server waits for connection

  1. client connects and write message
  2. server accepts and register OP_READ
  3. server reads message and remove OP_READ from interest op set
  4. client close the connection
  5. server write message (without any reading.. surely OP_READ is not set)
  6. server’s select wakes up infinitely with return value 0

产生这一Bug的原因:

因为poll和epoll对于突然中断的连接socket会对返回的eventSet事件集合置为EPOLLHUP或者EPOLLERR,eventSet事件集合发生了变化,这就导致Selector会被唤醒,如果仅仅是因为这个原因唤醒且没有感兴趣的时间发生的话,就会变成空轮询。

epoll感兴趣的事件集合

符号 描述
EPOLLIN 表示对应的文件描述符可以读(包括对端SOCKET正常关闭)
EPOLLOUT 表示对应的文件描述符可以写;
EPOLLPRI 表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR 表示对应的文件描述符发生错误;
EPOLLHUP 表示对应的文件描述符被挂断;
EPOLLET 将 EPOLL设为边缘触发(Edge Triggered)模式(默认为水平触发),这是相对水平触发(Level Triggered)来说的。
EPOLLONESHOT 只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socke的话,需要再次把这个socket加入到EPOLL队列里
阅读全文 »

NioEventLoopGroup和NioEventLoop的创建

NioEventLoop的创建是在创建EventLoopGroup中进行的,因此,先来快速过一遍EventLoopGroup的创建.

EventLoopGroup的创建中,主要是做了3件事:

  1. 创建ThreadPerTaskExcutor
  2. 创建EventLoop
  3. 创建Chooser
1
2
3
4
// 对于无参构造器,默认创建 2*cpu 个NioEventLoop
public NioEventLoopGroup() {
this(0);
}

经过多个构造器之后,到达MultithreadEventExecutorGroup, 这是NioEventLoopGroup的基类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public abstract class MultithreadEventExecutorGroup extends AbstractEventExecutorGroup {
...
...
protected MultithreadEventExecutorGroup(int nThreads, Executor executor,
EventExecutorChooserFactory chooserFactory, Object... args) {
if (nThreads <= 0) {
throw new IllegalArgumentException(String.format("nThreads: %d (expected: > 0)", nThreads));
}
// 1. 创建Executor
if (executor == null) {
// 对于execute的每一个task,都会创建一个新的线程去执行它
// 并且创建的线程是使用同一个线程工厂来创建的,有着同一个命名格式:nioEventLoop-x-yy
executor = new ThreadPerTaskExecutor(newDefaultThreadFactory());
}
children = new EventExecutor[nThreads];

// 2. 创建EventLoop数组
for (int i = 0; i < nThreads; i ++) {
boolean success = false;
try {
children[i] = newChild(executor, args);
success = true;
} catch (Exception e) {
// 抛异常
} finally {
// 优雅的关闭
}
}
// 3. 获取chooser
chooser = chooserFactory.newChooser(children);
....
....

}
...
...
}

1. 创建Executor——ThreadPerTaskExecutor

ThreadPerTaskExecutor的作用就和它的名字一样,每提交一个任务都新创建一个线程去做。

ThreadPerTaskExecutor除了实现上面这个功能以外,还使用一个专用的ThreadFactory来去创建线程,这就保证ThreadPerTaskExecutor创建的所有线程都是以“nioEventLoopGroup-x-yy”的格式来命名的。

阅读全文 »

服务端启动样例

为了方便理解,先来看以下服务端的启动流程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class NettyServer{
// ...
public void start() throws Exception {
// 创建NIO的EventLoop实例
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try{
ServerBootstrap b = new ServerBootstrap();
// 配置
b.group(bossGroup,workerGroup)
.channel(NioServerSocketChannel.class)
.localAddress(new InetSocketAddress(port))
.childHandler(new ChannelInitializer<SocketChannel>(){
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(...);
}
});
// 启动
ChannelFuture f = b.bind().sync();
f.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully().sync();
workerGroup.shutdownGracefully().sync();
}
}
}

概述

Netty服务端在启动时,主要有以下几个流程:

  1. 创建NioSocketChannel
  2. 初始化NioSocketChannel
  3. 注册NioSocketChannel到Selector中
  4. 做Bind操作
  5. 添加Read事件
阅读全文 »

一、前言

在基于netty做了几个小项目之后,越发的觉得还是有必要看一下源码的,不然用起来还是有些不踏实,故近期打算每天抽一点时间阅读下其源码并做一下学习笔记。

二、Netty基本组件

Java自带的NIO是对多路复用Reactor模型的实现,但使用起来不是特别方便,因此Netty就在NIO基础上又进行了一定程度的封装与优化,新增了很多NIO不具备的特性,使用起来较为方便,同时性能也很好。 Netty的几个基本组件,有些也是在NIO已有的基础上进行的封装(如Channel,ByteBuf)。 下面是几个是netty的主要组件,这里只是做了一下简述,之后会一个个地剖析其源码。

阅读全文 »

注解启动SpringMVC的原理

  1. Web容器在启动的时候,会扫描每个jar包下的
    META-INF/services/javax.servlet.ServletContainerInitializer(SPI机制), 加载这个文件指定的类SpringServletContainerInitializer

  2. SpringServletContainerInitializer使用HandleTypes注解
    获取了所有实现了WebApplicationInitializer接口的非抽象非接口类,然后实例化他们,并调用该接口的onStartUp(ServletContext ctx)方法.

阅读全文 »

Eureka是做什么的?

Eureka作为SpringCloud中的一个关键组件,其主要负责完成服务治理的功能。 服务治理就是用来实现各个微服务实例的注册和发现。 软件设计中的一个常用解耦手段就是引入一个第三方“角色”来进行解耦,例如spring里通过引入容器来进行IOC对“创建对象”这一动作进行了解耦,通过动态代理对象完成了对“使用方”和“被代理对象”之间进行解耦,引入消息队列对服务消费方和服务提供方进行解耦等等… 在微服务中,通过引入注册中心,就能够满足:消费方只需要关心调用哪一个服务而不用关心这个服务由谁提供,而服务方也只提供服务而不用关心这个服务会被谁调用,其他不需要被关心的这层“依赖关系”就交给注册中心去做.

Eureka的几种“角色”

1. 作为Servver:服务注册中心

提供注册与发现的功能,注册中心的集群由多个Eureka互相注册构成

阅读全文 »

Bean的生命周期(创建一个Bean的过程):

  1. 解析配置文件得到BeanDefinition并注册BeanDefinition。
    根据传入的信息来解析生成BeanDefinition,如果是注解的形式的话,就是解析注解上的信息,如果是配置文件形式的话,ResourceLoader会根据传入的location来去使用不同的Resource接口的实现类来完成对配置文件的加载和解析,这是策略模式的一个应用。 解析之后得到了一个< beanName, beanDefinition >的一个KV对,然后将其注册到底层容器中,其实就是把它们存到一个Map中,底层容器叫DefaultListableBeanFactory.
阅读全文 »