netty系列之:在netty中使用proxy protocol

flydean發表於2022-11-23

簡介

我們知道proxy protocol是haproxy提出的一個代理協議,透過這個協議,所有實現這個協議的proxy或者LBS,都可以附帶真實客戶端的IP地址和埠號,這使得proxy protocol在實際應用中非常有用。

這麼優秀的協議,沒有理由netty不支援。本文將會談一下netty中對proxy protoco代理協議的支援。

netty對proxy protocol協議的支援

proxy protocol協議其實很簡單,就是在請求前面帶了proxy header資訊。

在netty中這個header資訊叫做HAProxyMessage:

public final class HAProxyMessage extends AbstractReferenceCounted {

HAProxyMessage是一個ReferenceCounted,這一點和ByteBuf很類似,說明HAProxyMessage保留著和ByteBuf很類似的特性。

根據proxy protocol協議,該協議可以分為兩個版本,分別是v1和v2,其中v1版本是文字協議,而v2版本支援二進位制的格式。

顯然從程式碼編寫和除錯的角度來看v1更加友好,但是從程式的角度來看,v2可能效能更高。

HAProxyMessage中有個專門的HAProxyProtocolVersion類,來表示proxy protocol的版本資訊:

public enum HAProxyProtocolVersion {

    V1(VERSION_ONE_BYTE),

    V2(VERSION_TWO_BYTE);

HAProxyProtocolVersion是一個列舉類,在它裡面定義了和proxy協議相對應的兩個版本號。

在版本號之後是command,在netty中用HAProxyCommand來表示:

public enum HAProxyCommand {

    LOCAL(HAProxyConstants.COMMAND_LOCAL_BYTE),

    PROXY(HAProxyConstants.COMMAND_PROXY_BYTE);

HAProxyCommand也是一個列舉類,裡面定義了兩個command的值,分別是local和proxy。

其中local表示該請求是代理伺服器主動發起的,而不是客戶端發起的,比如監控檢測等請求。

proxy表示該請求是一個代理請求。

接下來是AddressFamily和TransportProtocol,這兩個欄位用同一個byte來表示,所以這兩個類都是HAProxyProxiedProtocol的內部類。

先看下AddressFamily的定義:

    public enum AddressFamily {

        AF_UNSPEC(AF_UNSPEC_BYTE),

        AF_IPv4(AF_IPV4_BYTE),

        AF_IPv6(AF_IPV6_BYTE),

        AF_UNIX(AF_UNIX_BYTE);

AddressFamily中定義了4個address family型別,分別是unspec,ipv4,ipv6和unix。分別對應未知family,ipv4,ipv6和unix domain socket。

再看下TransportProtocol的定義:

    public enum TransportProtocol {

        UNSPEC(TRANSPORT_UNSPEC_BYTE),

        STREAM(TRANSPORT_STREAM_BYTE),

        DGRAM(TRANSPORT_DGRAM_BYTE);

TransportProtocol有3個值,分別是unspec,stream和dgram。分別對應未知協議,http/https協議,udp/tcp協議。

因為AddressFamily和TransportProtocol實際上是同一個byte,所以經過組合之後可以得到下面的幾個列舉值:

    UNKNOWN(TPAF_UNKNOWN_BYTE, AddressFamily.AF_UNSPEC, TransportProtocol.UNSPEC),

    TCP4(TPAF_TCP4_BYTE, AddressFamily.AF_IPv4, TransportProtocol.STREAM),

    TCP6(TPAF_TCP6_BYTE, AddressFamily.AF_IPv6, TransportProtocol.STREAM),

    UDP4(TPAF_UDP4_BYTE, AddressFamily.AF_IPv4, TransportProtocol.DGRAM),

    UDP6(TPAF_UDP6_BYTE, AddressFamily.AF_IPv6, TransportProtocol.DGRAM),

    UNIX_STREAM(TPAF_UNIX_STREAM_BYTE, AddressFamily.AF_UNIX, TransportProtocol.STREAM),

    UNIX_DGRAM(TPAF_UNIX_DGRAM_BYTE, AddressFamily.AF_UNIX, TransportProtocol.DGRAM);

以上的列舉值也是HAProxyProxiedProtocol中定義的值。

接下就是源ip地址,目標地ip地址,源埠和目標埠這幾個值,定義為屬性表示如下:

    private final String sourceAddress;
    private final String destinationAddress;
    private final int sourcePort;
    private final int destinationPort;

最後,proxy protocol中還可以包含額外的欄位tlv,tlv在netty中也是一種byteBuf,使用HAProxyTLV表示:

public class HAProxyTLV extends DefaultByteBufHolder 

因為tlv是key value結構,所以看下HAProxyTLV的建構函式:

    public HAProxyTLV(Type type, ByteBuf content) {
        this(type, Type.byteValueForType(type), content);
    }

HAProxyTLV接受一個type和byteBuf的value。

Type是一個列舉類,在netty中可以支援下面的值:

    public enum Type {
        PP2_TYPE_ALPN,
        PP2_TYPE_AUTHORITY,
        PP2_TYPE_SSL,
        PP2_TYPE_SSL_VERSION,
        PP2_TYPE_SSL_CN,
        PP2_TYPE_NETNS,
        OTHER;

在HAProxyMessage中,tlv是一個list來儲存的:

private final List<HAProxyTLV> tlvs;

到此,所有HAProxyMessage所需要的引數都齊了,我們看下HAProxyMessage的建構函式:

    public HAProxyMessage(
            HAProxyProtocolVersion protocolVersion, HAProxyCommand command, HAProxyProxiedProtocol proxiedProtocol,
            String sourceAddress, String destinationAddress, int sourcePort, int destinationPort,
            List<? extends HAProxyTLV> tlvs)

HAProxyMessage會將所有的引數都儲存到本地的變數中,供後續使用。

因為proxy protocol有兩個版本,v1和v2,所以HAProxyMessage中提供了兩個將header編碼為AProxyMessage物件的方法,分別是:

static HAProxyMessage decodeHeader(ByteBuf header) 

和:

static HAProxyMessage decodeHeader(String header)

有了proxy protocol的java表示之後,我們再來看一下HAProxyMessage的編碼解碼器。

HAProxyMessage的編碼解碼器

netty對HAProxyMessage物件的支援表現在兩個地方,netty提供了兩個類分別對HAProxyMessage進行編碼和解碼,這兩個類是HAProxyMessageEncoder和HAProxyMessageDecoder。

先看一下HAProxyMessageEncoder:

public final class HAProxyMessageEncoder extends MessageToByteEncoder<HAProxyMessage> 

HAProxyMessageEncoder繼承自MessageToByteEncoder,傳入的泛型是HAProxyMessage,表示是將HAProxyMessage編碼成為ByteBuf。

它的encode方法很簡單,根據HAProxyMessage傳入的message版本資訊,分別進行編碼:

    protected void encode(ChannelHandlerContext ctx, HAProxyMessage msg, ByteBuf out) throws Exception {
        switch (msg.protocolVersion()) {
            case V1:
                encodeV1(msg, out);
                break;
            case V2:
                encodeV2(msg, out);
                break;
            default:
                throw new HAProxyProtocolException("Unsupported version: " + msg.protocolVersion());
        }
    }

HAProxyMessageDecoder是跟HAProxyMessageEncoder相反的動作,是將接收到的ByteBuf解析成為HAProxyMessage:

public class HAProxyMessageDecoder extends ByteToMessageDecoder 

因為HAProxyMessage有兩個版本,那麼怎麼判斷接收到的ByeBuf是哪個版本呢?

其實很簡單,因為v1版本和v2版本的開始字元是不一樣的,v1版本的開頭是一個text:"PROXY", v2版本的開頭是一個固定的二進位制串,如下所示:

    static final byte[] BINARY_PREFIX = {
            (byte) 0x0D,
            (byte) 0x0A,
            (byte) 0x0D,
            (byte) 0x0A,
            (byte) 0x00,
            (byte) 0x0D,
            (byte) 0x0A,
            (byte) 0x51,
            (byte) 0x55,
            (byte) 0x49,
            (byte) 0x54,
            (byte) 0x0A
    };

    static final byte[] TEXT_PREFIX = {
            (byte) 'P',
            (byte) 'R',
            (byte) 'O',
            (byte) 'X',
            (byte) 'Y',
    };

看下它的decode方法實現:

    protected final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        if (version == -1) {
            if ((version = findVersion(in)) == -1) {
                return;
            }
        }

        ByteBuf decoded;

        if (version == 1) {
            decoded = decodeLine(ctx, in);
        } else {
            decoded = decodeStruct(ctx, in);
        }

        if (decoded != null) {
            finished = true;
            try {
                if (version == 1) {
                    out.add(HAProxyMessage.decodeHeader(decoded.toString(CharsetUtil.US_ASCII)));
                } else {
                    out.add(HAProxyMessage.decodeHeader(decoded));
                }
            } catch (HAProxyProtocolException e) {
                fail(ctx, null, e);
            }
        }
    }

上面程式碼的邏輯是先從ByteBuf中根據版本號decode出header資訊放到ByteBuf中。

然後再根據版本號的不同,分別呼叫HAProxyMessage的兩個不同版本的decodeHeader方法進行解碼。最終得到HAProxyMessage。

netty中proxy protocol的程式碼示例

有了netty對proxy protocol的支援,那麼在netty中搭建支援proxy protocol的伺服器和客戶端就很容易了。

先看一下如何搭建支援proxy protocol的伺服器:

    private static void startServer(int port) throws InterruptedException {
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
             .channel(NioServerSocketChannel.class)
             .handler(new LoggingHandler(LogLevel.INFO))
             .childHandler(new ServerInitializer());
            b.bind(port).sync().channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

程式碼和常規的netty server一樣,這裡使用了NioEventLoopGroup和NioServerSocketChannel,搭建了一個支援TCP協議的netty伺服器。

ServerInitializer中包含了netty自帶的HAProxy編碼器和自定義的訊息處理器:

class ServerInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    public void initChannel(SocketChannel ch) throws Exception {
        ch.pipeline().addLast(
                new LoggingHandler(LogLevel.DEBUG),
                new HAProxyMessageDecoder(),
                new SimpleChannelInboundHandler() {
                    @Override
                    protected void channelRead0(ChannelHandlerContext ctx, Object msg) {
                        if (msg instanceof HAProxyMessage) {
                            log.info("proxy message is : {}", msg);
                        } else if (msg instanceof ByteBuf) {
                            log.info("bytebuf message is : {}", ByteBufUtil.prettyHexDump((ByteBuf) msg));
                        }
                    }
                });
    }
}

這裡使用netty自帶的HAProxyMessageDecoder,用來將ByteBuf訊息解碼為HAProxyMessage,然後在自定義的SimpleChannelInboundHandler中對HAProxyMessage進行處理。

這裡的伺服器可以處理兩種訊息,一種是HAProxyMessage,一種是原始的ByteBuf。處理的結果就是將訊息列印出來。

然後看下客戶端的定義:

EventLoopGroup group = new NioEventLoopGroup();
            Bootstrap b = new Bootstrap();
            b.group(group)
                    .channel(NioSocketChannel.class)
                    .handler(new ClientHander());
            Channel ch = b.connect(host, port).sync().channel();

客戶端使用的是EventLoopGroup和NioSocketChannel,是基於TCP協議的請求。

這裡新增了自定義的handler:ClientHander,ClientHander繼承自ChannelOutboundHandlerAdapter用來對client發出的訊息進行處理。

這裡看一下它的handlerAdded方法:

    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        ctx.pipeline().addBefore(ctx.name(), null, HAProxyMessageEncoder.INSTANCE);
        super.handlerAdded(ctx);
    }

可以看到handlerAdded方法向channelPipeline中新增了HAProxyMessageEncoder,用於編碼HAProxyMessage。

因為對於一個connection來說,HAProxyMessage只需要用到一次,後續的正常訊息就不需要這個編碼器了,所以我們需要在write方法中監聽HAProxyMessage的狀態,如果寫入成功之後,就從pipeline中移出HAProxyMessageEncoder和ClientHander。

    public void write(final ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
        ChannelFuture future1 = ctx.write(msg, promise);
        if (msg instanceof HAProxyMessage) {
            future1.addListener((ChannelFutureListener) future2 -> {
                if (future2.isSuccess()) {
                    ctx.pipeline().remove(HAProxyMessageEncoder.INSTANCE);
                    ctx.pipeline().remove(ClientHander.this);
                } else {
                    ctx.close();
                }
            });
        }
    }

最後我們構建了一個虛擬的HAProxyMessage,然後透過netty客戶端進行傳送:

HAProxyMessage message = new HAProxyMessage(
                    HAProxyProtocolVersion.V2, HAProxyCommand.PROXY, HAProxyProxiedProtocol.TCP4,
                    "127.0.0.1", "127.0.0.2", 8000, 9000);
            ch.writeAndFlush(message).sync();
            ch.writeAndFlush(Unpooled.copiedBuffer("this is a proxy protocol message!", CharsetUtil.UTF_8)).sync();
            ch.close().sync();

總結

上面的程式碼只是一個簡單的模擬proxy protocol在netty中的使用情況,並不代表上面的程式碼就可以在實際的專案中應用了。如果你想使用的話,可以在下面的程式碼上面繼續豐富和完善。

本文的程式碼,大家可以參考:

learn-netty4

相關文章