了解cmake

什么是cmake

cmake是一个跨平台的构建工具,主要是用来解决不同平台上Makefile不同的问题,通过编写CMakeLists.txt文件,然后通过cmake命令自动生成对应平台的Makefile。

编写CMakeLists

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
// 指定运行此配置文件所需的 CMake 的最低版本
cmake_minimum_required(VERSION 3.4.1)

// 该指令的主要作用就是将指定的源文件生成链接文件,然后添加到工程中去。
// native-lib表示生成的链接文件的名字。
// SHARED表示生成的文件类型:STATIC类型在链接其它目标的时候使用。SHARED库会被动态链接,在运行时会被加载。MODULE库是一种不会被链接到其它目标中的插件,但是可能会在运行时使用dlopen-系列的函数。
// native-lib.cpp 表示源文件,可以有多个。
add_library(native-lib SHARED native-lib.cpp)

// 查找一个库,log库已经包含在NDK中。
// log-lib用来存储查找结果
// log 表示查找库的名字
find_library(log-lib log)

// 为了确保CMake可以在编译时定位您的标头文件,使用该命令来指定包含标头文件的路径
include_directories(${openCVPath}/native/jni/include)

// set用来设置一些变量
// CMAKE_CURRENT_SOURCE_DIR 表示当前CMakeLists所在的目录
set(openCVPath ${CMAKE_SOURCE_DIR}/../../../../opencv-sdk )

// 使用static来添加依赖库,IMPORTED表示您只希望将库导入到项目中,
// 然后需要通过set_target_properties来指定库的路径
add_library(lib_opencv STATIC IMPORTED)

// lib_opencv就是上面导入的库的变量
// PROPERTIES 表示设置属性
// IMPORTED_LOCATION 设置导入库的位置
set_target_properties(lib_opencv PROPERTIES IMPORTED_LOCATION
${openCVPath}/native/libs/${ANDROID_ABI}/libopencv_java4.so)

// 将预构建库关联到您自己的原生库
// native-lib库就是原生库,将上面查找的系统库和添加的依赖库都链接到目标库上。
target_link_libraries(native-lib ${log-lib} lib_opencv)

运行cmake

运行cmake只需要执行cmake命令,后面跟上CMakeLists所在的目录位置,运行后在当前目录中生成Makefile。

1
cmake .

然后再在当前目录执行make命令,就可以生成对应的项目编译文件。

Java是解释执行吗?

Java是解释执行吗?

并不完全是,我们编写的Java代码,首先通过javac编译成字节码,然后在运行时被JVM内嵌的解释器将字节码逐行解释为机器码。但是每次都逐行解释效率非常低,所以JVM提供了JIT编译器,用于将热点代码直接编译成机器码并缓存,后续直接调用而不需要再次编译或解释。那么对于部分热点代码就是编译执行了,剩下的非热点代码依旧还是解释执行。

动态编译–JIT编译器

原理

JIT工作原理

对于Java代码,刚开始都是被编译器编译成字节码文件,然后字节码文件会被交给JVM解释执行,而JIT编译技术会将运行频率很高的字节码直接编译成机器指令以提高性能。

server模式和client模式

JIT编译器在运行时有两种模式,server模式相对于client模式来说,启动时速度较慢,但是一旦运行起来,性能会有很大提升。原因是client模式使用的是C1轻量级编译器,而server模式启动的是C2重量级编译器,C2比C1编译的更加彻底,所以启动较慢,但是启动后性能更好。

FastThreadLocal快在哪

ThreadLocal原理

ThreadLocal本质上就是在Thread对象中维护了一个Map对象,Map的key为ThreadLocal自身,value为要存的值。将数据保存在Thread中而不是保存在ThreadLocal中,这一设计使得当线程对象销毁时,在该线程的所有变量都会随之销毁。以ThreadLocal对象为key,使得一个Thread对象可以同时被多个ThreadLocal对象使用,每个ThreadLocal对应一个值,保存在Thread对象的Map中。

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
// 该Map在Thread对象中,由ThreadLocal对象维护
ThreadLocal.ThreadLocalMap threadLocals = null;

public void set(T value) {
Thread t = Thread.currentThread();
// 获取该线程的Map
ThreadLocalMap map = getMap(t);
// 将当前ThreadLocal和值存入Map
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}

FastThreadLocal优化点

由于ThreadLocal使用的是hash map数据结构,其查找需要通过hash值计算索引,再通过索引查找值,其速度比数组结构要慢。所以Netty提供了一种数组结构的ThreadLocal,也就是FastThreadLocal。其实现原理是在Thread对象中维护了一个InternalThreadLocalMap对象,该InternalThreadLocalMap对象内部维护了一个数组,数组中保存要存的值,不保存对应的FastThreadLocal对象,而是每个FastThreadLocal自己维护该值在数组中对应的索引。一个FastThreadLocal所保存值对应的索引在其创建的时候已经确定,其所保存的所有Thread都应用该索引位置。

1
2
3
4
5
6
// 创建FastThreadLocal,确定其所对应的数组索引。
// 该值是静态原子类型的,所有ThreadLocal对象共享,所以不会出现索引冲突值被覆盖等问题。
static final AtomicInteger nextIndex = new AtomicInteger();
public FastThreadLocal() {
index = InternalThreadLocalMap.nextVariableIndex();
}

Netty编解码器

ByteToMessageCodec

字节转换成消息对象的编解码器。实现了ChannelInboundHandlerAdapter和ChannelOutboundHandler,说明无论是对外还是对内都会经过该Handler。ByteToMessageCodec内部维护encoder和decoder两个对象,负责编码和解码。对于编解码器只需要处理read和write两种请求,当发生read时会将请求传递给decoder对象去解码,当发生write时会将请求传递给encoder对象编码。

1
2
3
4
5
6
7
8
9
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
decoder.channelRead(ctx, msg);
}

@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
encoder.write(ctx, msg, promise);
}

再来看看encoder和decoder这两个对象,encoder是一个MessageToByteEncoder消息对象转换成字节的编码器,decoder是一个ByteToMessageDecoder字节转换成消息对象的解码器。对外的请求会经过Encoder编码器编码成字节数据,而读取远端的数据则会经过Decoder解码成消息对象再传递给上层。所以上层在设置读取消息的Handler时必须设置在编解码器之后。

1
2
3
public abstract class ByteToMessageDecoder extends ChannelInboundHandlerAdapter {}

public abstract class MessageToByteEncoder<I> extends ChannelOutboundHandlerAdapter {}

Pipeline之Handler链

ChannelPipeline采用责任链模式设计。在责任链模式中,由每个对象对其下个对象的引用而连接起来的一条链,请求在这条链上传递,知道链上某个对象处理该请求并中止传递。发送请求者并不知道链上的哪个对象处理该请求,从而使得系统可以在不影响发送端的情况下动态的重新分配责任。
Netty-pipeline责任链

DefaultChannelPipeline

Handler链的默认实现类,维护了链中的头结点和尾结点,DefaultChannelPipeline对象在构建时,就会创建头结点和尾结点对象。之后添加的Handler都会添加到两者中间,addLast会添加到尾结点的前面,addFirst会添加到头结点的后面。在添加时可以给Handler指定名称,如果名称有重复会抛出异常。
Handler分为两种,一种是实现ChannelInboundHandler接口的,一种是实现ChannelOutboundHandler接口的。ChannelInboundHandler接口主要处理从外到内的请求,将远端操作通知本地(比如注册,激活,读等),从Head结点开始传递,直到tail结点。而ChannelOutboundHandler接口主要处理从内到外的操作,将本地操作通知远端(比如连接,绑定,写)等,从tail结点开始传递,到Head结点后转由unsafe类进行真正处理。

1
2
tail = new TailContext(this); // tail实现了ChannelInboundHandler接口
head = new HeadContext(this); // head实现了ChannelOutboundHandler和ChannelInboundHandler接口

上文讲到的ServerBootstrapAcceptor类,就是一个ChannelInboundHandlerAdapter类型的Handler。所以当连接完成对上层发出通知时,通知从Head结点开始传递,到达ServerBootstrapAcceptor中处理并中止传递。

AbstractChannelHandlerContext

AbstractChannelHandlerContext是对ChannelHandler的一个包装类,Pipeline中真正的对象实际上是AbstractChannelHandlerContext。在AbstractChannelHandlerContext中维护了next、prev属性,所以这是一个双向链表结构。

1
2
3
volatile AbstractChannelHandlerContext next; // 下个结点
volatile AbstractChannelHandlerContext prev; // 上个结点
private final ChannelHandler handler; // 真正Handler对象

接下来讲到的编解码器也是根据Pipeline来实现的。

ByteBuf

ByteBuf

为了简化ByteBuffer的操作,Netty自定义了ByteBuf,相对于ByteBuffer的读写索引共用,Netty将其分开,定义了读索引和写索引,避免了每次读写都需要flip方法来转换。并且定义了扩容方法,解决了ByteBuffer容量无法修改的问题。

基本属性

  • readerIndex : 读索引
  • writerIndex : 写索引
  • markReaderIndex : 标记读索引位置
  • markWriterIndex : 标记写索引位置
  • readableBytes : 可读字节大小(writerIndex - readerIndex)
  • writableBytes : 可写字节大小(capacity - writerIndex)
  • resetReaderIndex : 还原到标记读索引位置
  • resetWriterIndex : 还原到标记写索引位置

池化:将用过的对象保存,重复使用。

堆外内存:使用JVM堆栈外的内存,该内存JVM无法干预,JVM的GC只能回收ByteBuf对象本身,而不能回收其指向的堆外内存,所以需要手动释放。

Netty的ByteBuf实现分为池化和非池化,其中又分为堆内存和堆外内存实现。由于堆外内存需要手动释放,所以需要自己维护引用数来标记是否释放。

Netty线程模型

服务端线程模型

Netty的服务端线程模型类似与Reactor多线程模型。采用的是Acceptor线程和IO线程分离的模式。将连接请求处理和读写处理分开,避免了因读写阻塞而导致无法处理客户端连接请求,导致客户端连接超时。
Netty服务端线程模型

  • 有一个专门的线程池(Acceptor)用来处理客户端的连接请求。
  • 有一个专门的线程池(IO)用来处理读写操作。
  • 一个线程处理N条连接,一个连接对应一个线程。

从Acceptor线程池中随机选择一个线程处理客户端连接请求,连接建立后将创建的SocketChannel注册到IO线程池中的某个线程上,由该线程负责SocketChannel的读写和编解码工作。

示例代码

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
// Acceptor线程池
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
// IO线程池
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1000)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
if (sslCtx != null) {
p.addLast(sslCtx.newHandler(ch.alloc()));
}
//p.addLast(new LoggingHandler(LogLevel.INFO));
p.addLast(new EchoServerHandler());
}
});

// Start the server.
ChannelFuture f = b.bind(PORT).sync();

// Wait until the server socket is closed.
f.channel().closeFuture().sync();
} finally {
// Shut down all event loops to terminate all threads.
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}

Netty服务端Bind-Accept流程

NIO深入

Buffer

NIO中的Buffer用于和NIOChannel进行交互,数据从Channel中读到Buffer中,数据从Buffer中写入到Channel中。Buffer本质是是一块可读写的内存区域,这块内存区域被包装成Buffer对象,并提供了一系列方法访问该内存。Buffer不是线程安全的,多线程时需要同步。

基础属性

  • 容量capacity : 缓冲区能够容纳的数据的最大数量,在创建的时候设定,不可再更改。
  • 上界limit : 缓冲区现存数据的计数,用来读写转换记录写的位置。
  • 位置position : 下一个要被读写的元素的索引。每次读写都会更新位置。
  • 标记mark : 备忘位置,用于重置位置。
    0 <= mark <= position <= limit <= capacity

重要方法

  • allocate : 创建一个Buffer对象,参数为capacity。
  • wrap :使用包装方式创建Buffer对象。
  • duplicate : 创建一个与原始Buffer相似的Buffer,两者共享数据,但是每个Buffer都有各自的position和limit。
  • slice : 创建一个切片Buffer,共享数据。
  • flip :转换读写状态,每次转换都将position的值赋给limit,将position设为0。从写转读,是从0开始读到limit(也就是已写数据)。从读转写,会将limit的值赋给position,也就是抵消了flip的操作,还原到上上次,从上次写的位置开始写。
  • remaining :limit-position,未读数据大小。
  • clear : 清空Buffer,position设为0,limit设为capacity的值。数据并未清除。可以重新开始写数据。
  • compact : 将未读的数据拷贝到Buffer头部,将position设为未读数据后面,这样未读数据会保留,重写数据从其后面开始写。
  • mark : 标记。
  • reset : 恢复到mark的position。
  • equals : 两个Buffer相等的条件,1.类型相同,2.Buffer中剩余的数据个数相同,3.Buffer中剩余的数据相同。

实现类型,Buffer的实现类型分为两种。

  • direct类型:采用直接内存实现,直接内存不受JVM控制,是由系统直接分配的,JVM无法GC回收,必须手动回收该部分内存。
  • heap类型:采用JVM堆栈实现。

大端、小端
每个基本数据类型都是以连续字节序列的形式存储在内存中。如果字节的最高字节在左边,位于低位地址中,则就是大端字节顺序。如果字节的最低字节在左边,位于低位地址中,则就是小端字节顺序。

TCP讲解

TCP是一个面向连接的、可靠的、流协议。在OSI模型中属于传输层协议。

序列号

序列号是按顺序给发送数据的每个字节都标上的编号。接收端查询接收数据TCP首部的序列号和数据的长度,计算出自己下一步应该接收的序列号,将其作为确认应答返回给发送端。

确认应答

当发送端的数据到达接收端主机时,接收端主机会返回一个已经收到消息的通知,这个消息叫做确认应答(ACK)。如果在一定时间内没有收到ACK,发送端认为可能丢包,则进行重发。接收端主机如果收到相同数据会放弃接收该数据,不会造成重复接收相同数据的情况。

超时重发

TCP每次发包都会计算往返时间及其偏差,并将这个时间作为重发超时的时间,如果超过该时间仍然没有收到接收端的确认应答,则认为发送超时,准备重发。如果重发一定次数仍然失败,则判断网络异常,强制关闭连接。
因为有重发机制,所以数据并不是一发送就从缓冲区中删除,而是等到接收端的确认应答返回,确认该数据已经被接收,才会从缓冲区删除。

连接管理

三次握手

TCP在数据通信之前,通过TCP首部发送一个SYN包作为建立连接的请求并等待确认应答,如果接收端发来确认应答,则认为可以进行数据通信,并向接收端返回确认应答。因此连接的建立需要来回发送三个包才能完成。

  1. 客户端发送SYN(向服务端请求建立连接)
  2. 服务端发送SYN/ACK(通知客户端服务端已经准备好了)
  3. 客户端回应ACK(通知服务端客户端已经准备好了)

三次握手后,双方都知道对方已经准备好了,连接建立。
TCP三次握手

四次挥手

在通信结束后也会进行断开连接的处理,发送FIN包给接收端。由于TCP是全双工模式,可以单端发送数据,所以需要4次挥手。
2、3步不能合并,是因为此时服务端可能还有数据没有发送,不能立刻就关闭,所以先发送ACK回应,等数据处理完再发送FIN请求客户端关闭。

  1. 客户端发送FIN/ACK(向服务端请求断开连接,标明客户端没有数据需要发送,可以关闭,但是还可以接收服务端数据)
  2. 服务端回应ACK(通知客户端可以关闭连接)
  3. 服务端发送FIN/ACK(向客户端请求断开连接,标明服务端没有数据需要发送,可以关闭)
  4. 客户端回应ACK(通知服务端可以关闭连接,服务端关闭连接,客户端等待一段时间后也关闭连接)

TCP四次挥手

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×