Netty - 从Nginx 四层(TCP/UDP)流量中获取客户端真实/网络出口IP

news2025/4/1 9:45:55

文章目录

  • 一、背景与原理
    • 1.1 问题场景
      • 网络架构影响分析
        • 1.1 客户端与Nginx之间存在的NAT/VPN
        • 1.2 Nginx与RPC服务之间的NAT
    • 1.2 技术原理
  • 二、环境配置验证
    • 2.1 Nginx配置
    • 2.2 版本要求
  • 三、Netty服务端实现
    • 3.1 Pipeline配置(核心代码)
    • 3.2 协议处理器实现
    • 3.3 业务处理器调用示例
  • 四、关键实现细节
    • 4.1 解码顺序保障
    • 4.2 地址类型处理
    • 4.3 安全性增强
  • 五、异常处理方案
  • 六、 HAProxyMessageDecoder
  • 七、总结

在这里插入图片描述

一、背景与原理

1.1 问题场景

在TCP四层代理场景下,Nginx作为反向代理将客户端请求转发至Netty实现的RPC服务。由于经过代理转发,RPC服务默认只能获取到Nginx服务器的IP地址。需要通过Proxy Protocol协议传递客户端真实IP。


网络架构影响分析

1.1 客户端与Nginx之间存在的NAT/VPN
场景可获取IP类型技术原理
企业级NAT网关仅NAT出口公网IPNAT设备替换源IP地址
家用路由器NAT路由器WAN口IP私有地址转换为公网IP
全局VPN接入VPN服务器出口IP流量封装后隧道传输

结论:Proxy Protocol只能传递Nginx直接看到的IP(即NAT/VPN出口地址)

1.2 Nginx与RPC服务之间的NAT
场景影响程度解决方案
普通NAT转发无影响保持现有Proxy Protocol配置
复杂SDN网络需验证确保TCP连接透传代理协议头

1.2 技术原理

  • Proxy Protocol:由HAProxy提出的传输层协议扩展,在建立TCP连接时发送包含源地址信息的头部
  • Nginx配置proxy_protocol on指令启用协议支持
  • Netty解码:通过HAProxyMessageDecoder解析协议头

二、环境配置验证

2.1 Nginx配置

仅用于演示,仅展示核心配置。

# 定义后端服务器组
upstream rpc_backend {
    server 10.0.0.1:12345;
    server 10.0.0.2:12345;
}

# 配置TCP代理
stream {
    server {
        listen 12345;
        proxy_pass rpc_backend;
        proxy_protocol on;
    }
}

在Nginx的stream模块中,设置一个TCP代理服务器,监听12345端口,将所有到达该端口的连接通过proxy protocol转发到rpc_backend定的后端服务器组。这样后端服务器可以获取到原始客户端的IP地址等信息,前提是后端服务支持proxy protocol。

proxy_protocol on

  • 启用PROXY协议(版本1或2),在转发流量时,将客户端的原始信息(如源IP、端口)附加到数据包头部。
  • 作用:使后端服务器能获取客户端真实IP,而非Nginx代理的IP。
  • 要求:后端服务必须支持并配置解析PROXY协议。

2.2 版本要求

  • Nginx ≥ 1.9.0 我这里是1.27.1
  • Netty ≥ 4.1.x 我这里是 4.1.109

三、Netty服务端实现

3.1 Pipeline配置(核心代码)

public class RpcServerInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel ch) {
        ChannelPipeline pipeline = ch.pipeline();
        
        // 添加PROXY协议解码器
        pipeline.addLast(new HAProxyMessageDecoder());
        
        // 自定义协议处理器
        pipeline.addLast(new ProxyProtocolHandler());
        pipeline.addLast(new RpcMessageDecoder());
        pipeline.addLast(new RpcMessageHandler());
    }
}

3.2 协议处理器实现

提取IP

public class ProxyProtocolHandler extends ChannelInboundHandlerAdapter {
    
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        if (msg instanceof HAProxyMessage) {
            HAProxyMessage proxyMessage = (HAProxyMessage) msg;
            
            // 提取真实客户端地址
            String clientIp = proxyMessage.sourceAddress();
            int clientPort = proxyMessage.sourcePort();
            
            // 存储到Channel属性中
            ctx.channel().attr(CLIENT_IP_ATTRIBUTE).set(clientIp);
            
            // 释放资源并移除当前消息
            ReferenceCountUtil.release(msg);
            return;
        }
        
        // 非PROXY协议消息继续传递
        ctx.fireChannelRead(msg);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        log.error("Proxy protocol parsing failed", cause);
        ctx.close();
    }
}

3.3 业务处理器调用示例

使用IP

public class RpcMessageHandler extends SimpleChannelInboundHandler<RpcRequest> {
    
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, RpcRequest request) {
        String clientIp = ctx.channel().attr(CLIENT_IP_ATTRIBUTE).get();
        
        log.info("Received request from {}: {}", clientIp, request);
        // 业务处理逻辑...
    }
}

四、关键实现细节

4.1 解码顺序保障

  • HAProxyMessageDecoder必须作为第一个入站处理器
  • 需要处理完PROXY协议头后立即移除解码器(自动完成)

我抓了个包,如下

在这里插入图片描述

右键 Proxyv1 追踪流
在这里插入图片描述


4.2 地址类型处理

// 支持IPv4/IPv6地址类型判断
if (proxyMessage.proxyProtocol().addressType() == HAProxyAddressType.IPV4) {
    // IPv4处理逻辑
} else if (proxyMessage.proxyProtocol().addressType() == HAProxyAddressType.IPV6) {
    // IPv6处理逻辑
}

4.3 安全性增强

// 限制允许的代理服务器IP(可选)
List<String> allowedProxies = Arrays.asList("10.0.0.0/8", "192.168.0.0/16");
if (!isAllowedProxy(proxyMessage.destinationAddress())) {
    ctx.close();
    return;
}

五、异常处理方案

异常场景处理方案
无效PROXY头记录日志并关闭连接
协议版本不匹配返回错误响应码
地址格式错误使用默认地址并告警

六、 HAProxyMessageDecoder

/*
 * Copyright 2014 The Netty Project
 *
 * The Netty Project licenses this file to you under the Apache License,
 * version 2.0 (the "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at:
 *
 *   https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */
package io.netty.handler.codec.haproxy;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.codec.ProtocolDetectionResult;
import io.netty.util.CharsetUtil;

import java.util.List;

import static io.netty.handler.codec.haproxy.HAProxyConstants.*;

/**
 * Decodes an HAProxy proxy protocol header
 *
 * @see <a href="https://haproxy.1wt.eu/download/1.5/doc/proxy-protocol.txt">Proxy Protocol Specification</a>
 */
public class HAProxyMessageDecoder extends ByteToMessageDecoder {
    /**
     * Maximum possible length of a v1 proxy protocol header per spec
     */
    private static final int V1_MAX_LENGTH = 108;

    /**
     * Maximum possible length of a v2 proxy protocol header (fixed 16 bytes + max unsigned short)
     */
    private static final int V2_MAX_LENGTH = 16 + 65535;

    /**
     * Minimum possible length of a fully functioning v2 proxy protocol header (fixed 16 bytes + v2 address info space)
     */
    private static final int V2_MIN_LENGTH = 16 + 216;

    /**
     * Maximum possible length for v2 additional TLV data (max unsigned short - max v2 address info space)
     */
    private static final int V2_MAX_TLV = 65535 - 216;

    /**
     * Binary header prefix length
     */
    private static final int BINARY_PREFIX_LENGTH = BINARY_PREFIX.length;

    /**
     * {@link ProtocolDetectionResult} for {@link HAProxyProtocolVersion#V1}.
     */
    private static final ProtocolDetectionResult<HAProxyProtocolVersion> DETECTION_RESULT_V1 =
            ProtocolDetectionResult.detected(HAProxyProtocolVersion.V1);

    /**
     * {@link ProtocolDetectionResult} for {@link HAProxyProtocolVersion#V2}.
     */
    private static final ProtocolDetectionResult<HAProxyProtocolVersion> DETECTION_RESULT_V2 =
            ProtocolDetectionResult.detected(HAProxyProtocolVersion.V2);

    /**
     * Used to extract a header frame out of the {@link ByteBuf} and return it.
     */
    private HeaderExtractor headerExtractor;

    /**
     * {@code true} if we're discarding input because we're already over maxLength
     */
    private boolean discarding;

    /**
     * Number of discarded bytes
     */
    private int discardedBytes;

    /**
     * Whether or not to throw an exception as soon as we exceed maxLength.
     */
    private final boolean failFast;

    /**
     * {@code true} if we're finished decoding the proxy protocol header
     */
    private boolean finished;

    /**
     * Protocol specification version
     */
    private int version = -1;

    /**
     * The latest v2 spec (2014/05/18) allows for additional data to be sent in the proxy protocol header beyond the
     * address information block so now we need a configurable max header size
     */
    private final int v2MaxHeaderSize;

    /**
     * Creates a new decoder with no additional data (TLV) restrictions, and should throw an exception as soon as
     * we exceed maxLength.
     */
    public HAProxyMessageDecoder() {
        this(true);
    }

    /**
     * Creates a new decoder with no additional data (TLV) restrictions, whether or not to throw an exception as soon
     * as we exceed maxLength.
     *
     * @param failFast Whether or not to throw an exception as soon as we exceed maxLength
     */
    public HAProxyMessageDecoder(boolean failFast) {
        v2MaxHeaderSize = V2_MAX_LENGTH;
        this.failFast = failFast;
    }

    /**
     * Creates a new decoder with restricted additional data (TLV) size, and should throw an exception as soon as
     * we exceed maxLength.
     * <p>
     * <b>Note:</b> limiting TLV size only affects processing of v2, binary headers. Also, as allowed by the 1.5 spec
     * TLV data is currently ignored. For maximum performance it would be best to configure your upstream proxy host to
     * <b>NOT</b> send TLV data and instantiate with a max TLV size of {@code 0}.
     * </p>
     *
     * @param maxTlvSize maximum number of bytes allowed for additional data (Type-Length-Value vectors) in a v2 header
     */
    public HAProxyMessageDecoder(int maxTlvSize) {
        this(maxTlvSize, true);
    }

    /**
     * Creates a new decoder with restricted additional data (TLV) size, whether or not to throw an exception as soon
     * as we exceed maxLength.
     *
     * @param maxTlvSize maximum number of bytes allowed for additional data (Type-Length-Value vectors) in a v2 header
     * @param failFast Whether or not to throw an exception as soon as we exceed maxLength
     */
    public HAProxyMessageDecoder(int maxTlvSize, boolean failFast) {
        if (maxTlvSize < 1) {
            v2MaxHeaderSize = V2_MIN_LENGTH;
        } else if (maxTlvSize > V2_MAX_TLV) {
            v2MaxHeaderSize = V2_MAX_LENGTH;
        } else {
            int calcMax = maxTlvSize + V2_MIN_LENGTH;
            if (calcMax > V2_MAX_LENGTH) {  // lgtm[java/constant-comparison]
                v2MaxHeaderSize = V2_MAX_LENGTH;
            } else {
                v2MaxHeaderSize = calcMax;
            }
        }
        this.failFast = failFast;
    }

    /**
     * Returns the proxy protocol specification version in the buffer if the version is found.
     * Returns -1 if no version was found in the buffer.
     */
    private static int findVersion(final ByteBuf buffer) {
        final int n = buffer.readableBytes();
        // per spec, the version number is found in the 13th byte
        if (n < 13) {
            return -1;
        }

        int idx = buffer.readerIndex();
        // 主要修改这里
        if (match(TEXT_PREFIX, buffer, idx)) {
            return 1;
        }
        if (match(BINARY_PREFIX, buffer, idx)) {
            return buffer.getByte(idx + BINARY_PREFIX_LENGTH);
        }
        return -1;
//        return match(BINARY_PREFIX, buffer, idx) ? buffer.getByte(idx + BINARY_PREFIX_LENGTH) : 1;
    }

    /**
     * Returns the index in the buffer of the end of header if found.
     * Returns -1 if no end of header was found in the buffer.
     */
    private static int findEndOfHeader(final ByteBuf buffer) {
        final int n = buffer.readableBytes();

        // per spec, the 15th and 16th bytes contain the address length in bytes
        if (n < 16) {
            return -1;
        }

        int offset = buffer.readerIndex() + 14;

        // the total header length will be a fixed 16 byte sequence + the dynamic address information block
        int totalHeaderBytes = 16 + buffer.getUnsignedShort(offset);

        // ensure we actually have the full header available
        if (n >= totalHeaderBytes) {
            return totalHeaderBytes;
        } else {
            return -1;
        }
    }

    /**
     * Returns the index in the buffer of the end of line found.
     * Returns -1 if no end of line was found in the buffer.
     */
    private static int findEndOfLine(final ByteBuf buffer) {
        final int n = buffer.writerIndex();
        for (int i = buffer.readerIndex(); i < n; i++) {
            final byte b = buffer.getByte(i);
            if (b == '\r' && i < n - 1 && buffer.getByte(i + 1) == '\n') {
                return i;  // \r\n
            }
        }
        return -1;  // Not found.
    }

    @Override
    public boolean isSingleDecode() {
        // ByteToMessageDecoder uses this method to optionally break out of the decoding loop after each unit of work.
        // Since we only ever want to decode a single header we always return true to save a bit of work here.
        return true;
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        super.channelRead(ctx, msg);
        if (finished) {
            ctx.pipeline().remove(this);
        }
    }

    @Override
    protected final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        // determine the specification version
        if (version == -1) {
            if ((version = findVersion(in)) == -1) {
                // 头部
                finished = true;
                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);
            }
        }
    }

    /**
     * Create a frame out of the {@link ByteBuf} and return it.
     *
     * @param ctx     the {@link ChannelHandlerContext} which this {@link HAProxyMessageDecoder} belongs to
     * @param buffer  the {@link ByteBuf} from which to read data
     * @return frame  the {@link ByteBuf} which represent the frame or {@code null} if no frame could
     *                be created
     */
    private ByteBuf decodeStruct(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception {
        if (headerExtractor == null) {
            headerExtractor = new StructHeaderExtractor(v2MaxHeaderSize);
        }
        return headerExtractor.extract(ctx, buffer);
    }

    /**
     * Create a frame out of the {@link ByteBuf} and return it.
     *
     * @param ctx     the {@link ChannelHandlerContext} which this {@link HAProxyMessageDecoder} belongs to
     * @param buffer  the {@link ByteBuf} from which to read data
     * @return frame  the {@link ByteBuf} which represent the frame or {@code null} if no frame could
     *                be created
     */
    private ByteBuf decodeLine(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception {
        if (headerExtractor == null) {
            headerExtractor = new LineHeaderExtractor(V1_MAX_LENGTH);
        }
        return headerExtractor.extract(ctx, buffer);
    }

    private void failOverLimit(final ChannelHandlerContext ctx, int length) {
        failOverLimit(ctx, String.valueOf(length));
    }

    private void failOverLimit(final ChannelHandlerContext ctx, String length) {
        int maxLength = version == 1 ? V1_MAX_LENGTH : v2MaxHeaderSize;
        fail(ctx, "header length (" + length + ") exceeds the allowed maximum (" + maxLength + ')', null);
    }

    private void fail(final ChannelHandlerContext ctx, String errMsg, Exception e) {
        finished = true;
        ctx.close(); // drop connection immediately per spec
        HAProxyProtocolException ppex;
        if (errMsg != null && e != null) {
            ppex = new HAProxyProtocolException(errMsg, e);
        } else if (errMsg != null) {
            ppex = new HAProxyProtocolException(errMsg);
        } else if (e != null) {
            ppex = new HAProxyProtocolException(e);
        } else {
            ppex = new HAProxyProtocolException();
        }
        throw ppex;
    }

    /**
     * Returns the {@link ProtocolDetectionResult} for the given {@link ByteBuf}.
     */
    public static ProtocolDetectionResult<HAProxyProtocolVersion> detectProtocol(ByteBuf buffer) {
        if (buffer.readableBytes() < 12) {
            return ProtocolDetectionResult.needsMoreData();
        }

        int idx = buffer.readerIndex();

        if (match(BINARY_PREFIX, buffer, idx)) {
            return DETECTION_RESULT_V2;
        }
        if (match(TEXT_PREFIX, buffer, idx)) {
            return DETECTION_RESULT_V1;
        }
        return ProtocolDetectionResult.invalid();
    }

    private static boolean match(byte[] prefix, ByteBuf buffer, int idx) {
        for (int i = 0; i < prefix.length; i++) {
            final byte b = buffer.getByte(idx + i);
            if (b != prefix[i]) {
                return false;
            }
        }
        return true;
    }

    /**
     * HeaderExtractor create a header frame out of the {@link ByteBuf}.
     */
    private abstract class HeaderExtractor {
        /** Header max size */
        private final int maxHeaderSize;

        protected HeaderExtractor(int maxHeaderSize) {
            this.maxHeaderSize = maxHeaderSize;
        }

        /**
         * Create a frame out of the {@link ByteBuf} and return it.
         *
         * @param ctx     the {@link ChannelHandlerContext} which this {@link HAProxyMessageDecoder} belongs to
         * @param buffer  the {@link ByteBuf} from which to read data
         * @return frame  the {@link ByteBuf} which represent the frame or {@code null} if no frame could
         *                be created
         * @throws Exception if exceed maxLength
         */
        public ByteBuf extract(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception {
            final int eoh = findEndOfHeader(buffer);
            if (!discarding) {
                if (eoh >= 0) {
                    final int length = eoh - buffer.readerIndex();
                    if (length > maxHeaderSize) {
                        buffer.readerIndex(eoh + delimiterLength(buffer, eoh));
                        failOverLimit(ctx, length);
                        return null;
                    }
                    ByteBuf frame = buffer.readSlice(length);
                    buffer.skipBytes(delimiterLength(buffer, eoh));
                    return frame;
                } else {
                    final int length = buffer.readableBytes();
                    if (length > maxHeaderSize) {
                        discardedBytes = length;
                        buffer.skipBytes(length);
                        discarding = true;
                        if (failFast) {
                            failOverLimit(ctx, "over " + discardedBytes);
                        }
                    }
                    return null;
                }
            } else {
                if (eoh >= 0) {
                    final int length = discardedBytes + eoh - buffer.readerIndex();
                    buffer.readerIndex(eoh + delimiterLength(buffer, eoh));
                    discardedBytes = 0;
                    discarding = false;
                    if (!failFast) {
                        failOverLimit(ctx, "over " + length);
                    }
                } else {
                    discardedBytes += buffer.readableBytes();
                    buffer.skipBytes(buffer.readableBytes());
                }
                return null;
            }
        }

        /**
         * Find the end of the header from the given {@link ByteBuf},the end may be a CRLF, or the length given by the
         * header.
         *
         * @param buffer the buffer to be searched
         * @return {@code -1} if can not find the end, otherwise return the buffer index of end
         */
        protected abstract int findEndOfHeader(ByteBuf buffer);

        /**
         * Get the length of the header delimiter.
         *
         * @param buffer the buffer where delimiter is located
         * @param eoh index of delimiter
         * @return length of the delimiter
         */
        protected abstract int delimiterLength(ByteBuf buffer, int eoh);
    }

    private final class LineHeaderExtractor extends HeaderExtractor {

        LineHeaderExtractor(int maxHeaderSize) {
            super(maxHeaderSize);
        }

        @Override
        protected int findEndOfHeader(ByteBuf buffer) {
            return findEndOfLine(buffer);
        }

        @Override
        protected int delimiterLength(ByteBuf buffer, int eoh) {
            return buffer.getByte(eoh) == '\r' ? 2 : 1;
        }
    }

    private final class StructHeaderExtractor extends HeaderExtractor {

        StructHeaderExtractor(int maxHeaderSize) {
            super(maxHeaderSize);
        }

        @Override
        protected int findEndOfHeader(ByteBuf buffer) {
            return HAProxyMessageDecoder.findEndOfHeader(buffer);
        }

        @Override
        protected int delimiterLength(ByteBuf buffer, int eoh) {
            return 0;
        }
    }
}


七、总结

在存在NAT或VPN的网络架构中,通过Proxy Protocol获取客户端真实IP的能力受限于网络设备的位置。

若NAT/VPN位于客户端与Nginx之间(如企业VPN或家庭路由),Proxy Protocol仅能传递经过NAT转换或VPN隧道出口的IP(如公网IP或VPN分配地址),无法穿透获取终端设备的内网真实IP。若需突破此限制,可采取混合方案:客户端主动上报IP(需改造客户端代码)并配合网络设备改造(如VPN网关记录原始IP、专用隧道协议)。但需注意隐私合规风险,避免采集敏感信息。

对于常规业务场景,Proxy Protocol结合Nginx配置已能满足“获取客户端侧网络出口IP”的需求,但若涉及终端设备溯源,需结合应用层协议与网络基础设施深度协同实现。

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2324201.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Ubuntu下载docker、xshell

配置&#xff1a;VMware虚拟机、Ubuntu24.04.1 首先打开vm启动虚拟机 下载docker Ubuntu启动之后&#xff0c;按CTRLALTT 打开终端 1.更新软件包索引并安装依赖 sudo apt-get updatesudo apt-get install \ca-certificates \curl \gnupg \lsb-release 2.添加docker官方的GP…

迅为iTOP-RK3576人工智能开发板Android 系统接口功能测试

2.1 开机启动 开发板接通电源&#xff0c;并按下电源开关&#xff0c;系统即启动&#xff0c;在启动过程中&#xff0c;系统会显示下图中的开机画面&#xff0c;它们分别是 Android 系统启动时的 Logo 画面&#xff1a; 最后会显示如下解锁画面&#xff1a; 2.2 命令终端 将…

office_word中使用宏以及DeepSeek

前言 Word中可以利用DeepSeek来生成各种宏&#xff0c;从而生成我们需要各种数据和图表&#xff0c;这样可以大大减少我们手工的操作。 1、Office的版本 采用的是微软的office2016&#xff0c;如下图&#xff1a; 2、新建一个Word文档 3、开启开发工具 这样菜单中的“开发工具…

数据结构day04

一 栈 1栈的基本概念 各位同学大家好&#xff0c;从这个小节开始&#xff0c;我们会正式进入第三章的学习&#xff0c;我们会学习栈和队列&#xff0c;那这个小节中我们会先认识栈的基本概念。我们会从栈的定义和栈的基本操作来认识栈这种数据结构&#xff0c;也就是要探讨栈的…

质量工程:数字化转型时代的质量体系重构

前言&#xff1a;质量理念的范式转移阅读原文 如果把软件开发比作建造摩天大楼&#xff1a; 传统测试 竣工后检查裂缝&#xff08;高成本返工&#xff09; 质量工程 从地基开始的全流程监理体系&#xff08;设计图纸→施工工艺→建材选择→竣工验收&#xff09; IEEE研究…

pyQt学习笔记——Qt资源文件(.qrc)的创建与使用

Qt资源文件&#xff08;.qrc&#xff09;的创建与使用 1. 选择打开资源2. 创建新资源3. 添加资源文件夹4. 选择要加载的图片文件5. 编译resource.qrc文件6. 替换PySlide6为PyQt57. 其他说明 1. 选择打开资源 在Qt项目中&#xff0c;可以通过windowIcon点击选择打开资源。 2. 创…

优雅的开始一个Python项目

优雅的开始一个Python项目 这是我在初始化一个Python项目时&#xff0c;一键生成的项目文件。它自动完成了git初始化、环境管理、日志模块这三件事情&#xff0c;并在最后进入了虚拟环境。 uv安装 uv是一个现代的Python包管理和项目管理工具。uv中文文档 安装uv: # unix: …

[学成在线]07-视频转码

视频转码 视频上传成功后需要对视频进行转码处理。 首先我们要分清文件格式和编码格式&#xff1a; 文件格式&#xff1a;是指.mp4、.avi、.rmvb等这些不同扩展名的视频文件的文件格式 &#xff0c;视频文件的内容主要包括视频和音频&#xff0c;其文件格式是按照一定的编码…

qt+opengl 加载三维obj文件

1前面我们已经熟悉了opengl自定义顶点生成一个立方体&#xff0c;并且我们实现了立方体的旋转&#xff0c;光照等功能。下面我们来用opengl来加载一个obj文件。准备我们首先准备一个简单的obj文件&#xff08;head.obj&#xff09;。资源在本页下载 2 在obj文件里面&#xff0c…

【实战ES】实战 Elasticsearch:快速上手与深度实践-2.2.1 Bulk API的正确使用与错误处理

&#x1f449; 点击关注不迷路 &#x1f449; 点击关注不迷路 &#x1f449; 点击关注不迷路 文章大纲 Elasticsearch Bulk API 深度实践&#xff1a;性能调优与容错设计1. Bulk API 核心机制解析1.1 批量写入原理剖析1.1.1 各阶段性能瓶颈 2. 高性能批量写入实践2.1 客户端最佳…

鸿蒙Flutter开发故事:不,你不需要鸿蒙化

在华为牵头下&#xff0c;Flutter 鸿蒙化如火如荼进行&#xff0c;当第一次看到一份上百个插件的Excel 列表时&#xff0c;我也感到震惊&#xff0c;排名前 100 的插件赫然在列&#xff0c;这无疑是一次大规模的军团作战。 然后&#xff0c;参战团队鱼龙混杂&#xff0c;难免有…

中间件框架漏洞攻略

中间件&#xff08;英语&#xff1a;Middleware&#xff09;是提供系统软件和应⽤软件之间连接的软件&#xff0c;以便于软件各部件之间的沟通。 中间件处在操作系统和更⾼⼀级应⽤程序之间。他充当的功能是&#xff1a;将应⽤程序运⾏环境与操作系统隔离&#xff0c;从⽽实…

第21周:RestNet-50算法实践

目录 前言 理论知识 1.CNN算法发展 2.-残差网络的由来 一、导入数据 二、数据处理 四、编译 五、模型评估 六、总结 前言 &#x1f368; 本文为&#x1f517;365天深度学习训练营中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 理论知识 1.CNN算法发展 该图列举出…

构建大语言模型应用:数据准备(第二部分)

本专栏通过检索增强生成&#xff08;RAG&#xff09;应用的视角来学习大语言模型&#xff08;LLM&#xff09;。 本系列文章 简介数据准备&#xff08;本文&#xff09;句子转换器向量数据库搜索与检索大语言模型开源检索增强生成评估大语言模型服务高级检索增强生成 RAG 如上…

AI-Sphere-Butler之Ubuntu服务器如何部署Nginx代理,并将HTTP升级成HTTPS,用于移动设备访问

环境&#xff1a; AI-Sphere-Butler WSL2 Ubuntu22.04 Nginx 问题描述&#xff1a; AI-Sphere-Butler之Ubuntu服务器如何部署Nginx代理&#xff0c;并将HTTP升级成HTTPS&#xff0c;用于移动设备访问 解决方案&#xff1a; 一、生成加密证书 1.配置OpenSSL生成本地不加…

飞致云荣获“Alibaba Cloud Linux最佳AI镜像服务商”称号

2025年3月24日&#xff0c;阿里云云市场联合龙蜥社区发布“2024年度Alibaba Cloud Linux最佳AI镜像服务商”评选结果。 经过主办方的严格考量&#xff0c;飞致云&#xff08;即杭州飞致云信息科技有限公司&#xff09;凭借旗下MaxKB开源知识库问答系统、1Panel开源面板、Halo开…

Django项目之订单管理part6(message组件和组合搜索组件)

一.前言 我们前面讲的差不多了&#xff0c;接着上节课讲&#xff0c;今天要来做一个撤单要求&#xff0c;我们可以用ajax请求&#xff0c;但是我这里介绍最后一个知识点&#xff0c;message组件&#xff0c;但是我会把两种方式都讲出来的&#xff0c;讲完这个就开始讲我们最重…

Taro创建微信小程序项目 第一步搭建项目

1.node: 2.第一步&#xff1a; 安装taro npm install -g tarojs/cli 3.创建文件夹wxxcx, 创建demos的文件夹的项目&#xff08;demos项目名称&#xff09; taro init demos 出现以下信息&#xff1a;可以根据自己的需求选择 出现安装项目依赖失败不要紧 4.进入demos文件夹…

S32K144外设实验(六):FTM输出单路PWM

文章目录 1. 概述1.1 时钟系统1.2 实验目的2. 代码的配置2.1 时钟配置2.2 FTM模块配置2.3 输出引脚配置2.4 API函数调用1. 概述 1.1 时钟系统 FTM的CPU接口时钟为SYS_CLK,在RUN模式下最高80MHz。模块的时钟结构如下图所示。 从上图中可以看出,FTM模块的功能时钟为SYS_CLK,…

地下管线三维建模软件工具MagicPipe3D V3.6.1

经纬管网建模系统MagicPipe3D&#xff0c;基于二维矢量管线管点数据本地离线参数化构建地下管网三维模型&#xff08;包括管道、接头、附属设施等&#xff09;&#xff0c;输出标准3DTiles、Obj模型等格式&#xff0c;支持Cesium、Unreal、Unity、Osg等引擎加载进行三维可视化、…