惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - 三生石上(FineUI控件)
Blog — PlanetScale
Blog — PlanetScale
B
Blog
GbyAI
GbyAI
爱范儿
爱范儿
月光博客
月光博客
N
Netflix TechBlog - Medium
T
Tailwind CSS Blog
G
Google Developers Blog
大猫的无限游戏
大猫的无限游戏
Vercel News
Vercel News
H
Hackread – Cybersecurity News, Data Breaches, AI and More
WordPress大学
WordPress大学
The GitHub Blog
The GitHub Blog
Recent Announcements
Recent Announcements
腾讯CDC
MyScale Blog
MyScale Blog
V
Visual Studio Blog
The Cloudflare Blog
Microsoft Security Blog
Microsoft Security Blog
A
About on SuperTechFans
Google DeepMind News
Google DeepMind News
Last Week in AI
Last Week in AI
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻

Java

开源:用刚发布的 Spring AI 2.0.1,撸了一个全功能 Agent - V2EX 纯开源 Nexus 替代方案 kkRepo 1.0.0 正式发布,新增 R/CRAN、Go Hosted 和多套 UI 主题 - V2EX 不想写代码,但想要集成一个登录页面? Sa-Token-Quick-Login 帮你实现! - V2EX 善于学习的 Java 后端朋友们,想请教下你是如何榨干自己的工作项目经验,把它沉淀下来慢慢积攒成自己的能力的? - V2EX Java Crac 几乎正式可用 - V2EX Sa-Token v1.46.0 发布 🚀,来看看有没有令你心动的功能! - V2EX 可以下线 Nexus 了:纯开源制品仓库 kkRepo v0.9.0 发布,新增 Alpine、Hugging Face Models 与全局搜索 - V2EX Java 转 native 究竟行不行?无需精通 graalvm native-image 配置就能打包 native exe 方法分享。 - V2EX fastjson 居然停止维护了 - V2EX happens-before 详解 华为的毕昇 JDK 8 AppCDS 特性稳定吗? 我把 Java 开发的 kkRepo 支持 AOT 编译, 1s 极速启动,空闲内存小于 200MB fastjson1.x 最新版本疑似又出 RCE 漏洞了 讲讲我怎么获得 kkRepo 的首个企业用户,成功替换掉 Nexus,每年节省 $1,620 PRO 订阅费用 Sonatype 正引入 Maven Central 仓库发布使用情况可见性和针对高流量发布活动的限制 让 Java 再次伟大,没有人比我更懂得如何打包!(分享一个四两拨千斤的多 jar 打包 exe 方式) 使用 kkRepo 搭建 Maven 私服 Nexus 的平替 neuxs-plus 开源涉嫌商标侵权,被迫改名 kkRepo 我在 IDEA tab 里养了一只猫 Java 确实是内存高效的 用 AI 把 visualvm 的部分功能界面换了个马甲 javaer 现在出去面试 问哪些内容 sa-token + spring cloud gateway 过滤器顺序问题 关于在 wsl 中使用 idea 进行 Java 开发的问题 求助, Fortify 漏扫问题解决 微信 ClawBot 和企微 AIBot 的 Java SDK 老业务信创重构选型大家有啥建议 ai 编程的情况在你们使用什么 IDE 下面这段 Java 多态的代码,如何才能改为 Go、 Python 、JS、C++版本? JDK 26 发布,非 lts 版本
用 netty 编写代理服务器,切换出口 IP,不能及时生效
montaro2017 · 2025-10-30 · via Java

公司有一台服务器,有很多个公网 IP ,就想着能不能利用起来。

然后现在有一个任务是用浏览器打开指定网址,返回网页源代码,我就打算把这个服务器做成代理服务器。

本来是计划每个 IP 做一个代理服务器,代理服务器根据入口 IP 用对应的 IP 连接目标服务器。

开启每个浏览器的时候设置代理地址,比如 1.2.3.4:8639, 1.2.3.5:8639 这样 。

然后发现多开浏览器非常吃性能,要充分利用所有的公网 IP 得开几十个浏览器,这时候已经卡到动不了了,肯定不行。

所以我就想开 4 个浏览器,每个浏览器设置一个代理,然后通过接口去切换代理后端的出口。

代理服务器是用 netty 写的,逻辑改成了绑定不同端口,然后通过接口指定端口号和出口 IP ,来切换不同端口对应代理的出口 IP 。

其实就是存了一个 Map<Integer, String>,调接口修改这个 map ,netty 代理服务器连接目标服务器的时候使用出口地址去连接。

现在问题在于,调用接口切换出口 IP 后,日志显示已经使用新的出口 IP 了,但是访问查询 IP 的网站,还是使用之前的 IP ,好像要等一段时间才生效,这是什么问题,求各位大佬指教

@Log4j2
public class ProxyFrontendHandler extends SimpleChannelInboundHandler<FullHttpRequest> {

    private final AddressFunction addressFunction;

    public ProxyFrontendHandler(AddressFunction addressFunction) {
        this.addressFunction = addressFunction;
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest req) {
        if (HttpMethod.CONNECT.equals(req.method())) {
            handleConnectRequest(ctx, req);
            return;
        }
        handleHttpRequest(ctx, req);
    }

    private void handleConnectRequest(ChannelHandlerContext ctx, FullHttpRequest req) {
        List<String> split = StrUtil.split(req.uri(), ":");
        String host = CollUtil.getFirst(split);
        int port = Convert.toInt(CollUtil.get(split, 1), 443);

        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(ctx.channel().eventLoop())
                .channel(NioSocketChannel.class)
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) {
                        ch.pipeline().addLast(new RelayHandler(ctx.channel()));
                    }
                });

        ChannelFuture connectFuture;
        InetSocketAddress remoteAddress = new InetSocketAddress(host, port);
        InetSocketAddress sourceAddress = addressFunction.apply(ctx);
        if (sourceAddress != null) {
            log.info("Using outbound: {} | host: {}", sourceAddress, remoteAddress.getHostString());
            connectFuture = bootstrap.connect(remoteAddress, sourceAddress);
        } else {
            connectFuture = bootstrap.connect(remoteAddress);
        }
        connectFuture.addListener((ChannelFutureListener) future -> {
            if (future.isSuccess()) {
                Channel outboundChannel = future.channel();
                DefaultFullHttpResponse response = new DefaultFullHttpResponse(
                        HttpVersion.HTTP_1_1,
                        HttpResponseStatus.OK
                );
                response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
                ctx.writeAndFlush(response).addListener((ChannelFutureListener) f -> {
                    try {
                        ctx.pipeline().remove(HttpServerCodec.class);
                        ctx.pipeline().remove(HttpObjectAggregator.class);
                        ctx.pipeline().addLast(new RelayHandler(outboundChannel));
                    } catch (Exception ignored) {
                    }
                });
            } else {
                sendErrorResponse(ctx, "无法连接到目标服务器");
                closeOnFlush(ctx.channel());
            }
        });
    }

    private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest req) {
        String host = req.headers().get(HttpHeaderNames.HOST);
        if (host == null) {
            sendErrorResponse(ctx, "缺少 Host 头");
            closeOnFlush(ctx.channel());
            return;
        }
        String[] hostParts = host.split(":");
        String targetHost = hostParts[0];
        int targetPort = hostParts.length > 1 ? Integer.parseInt(hostParts[1]) : 80;
        // 修改请求 URI 为绝对路径
        req.setUri(req.uri().replace("http://" + host, ""));
        req.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);

        // 复制请求以避免在异步操作期间被释放
        FullHttpRequest copiedReq = req.copy();
        // 创建到目标服务器的连接
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(ctx.channel().eventLoop())
                .channel(NioSocketChannel.class)
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) {
                        ch.pipeline().addLast(new HttpClientCodec());
                        ch.pipeline().addLast(new HttpObjectAggregator(1024 * 1024)); // 增加到 1MB
                        ch.pipeline().addLast(new RelayHandler(ctx.channel()));
                    }
                });
        ChannelFuture connectFuture;
        InetSocketAddress remoteAddress = new InetSocketAddress(targetHost, targetPort);
        InetSocketAddress sourceAddress = addressFunction.apply(ctx);
        if (sourceAddress != null) {
            log.info("Using outbound: {} | host: {}", sourceAddress, remoteAddress.getHostString());
            connectFuture = bootstrap.connect(remoteAddress, sourceAddress);
        } else {
            connectFuture = bootstrap.connect(remoteAddress);
        }
        connectFuture.addListener((ChannelFutureListener) future -> {
            if (future.isSuccess()) {
                future.channel().writeAndFlush(copiedReq);
            } else {
                closeOnFlush(ctx.channel());
            }
            if (copiedReq.refCnt() != 0) {
                copiedReq.release();
            }
        });
    }

    private void sendErrorResponse(ChannelHandlerContext ctx, String message) {
        FullHttpResponse response = new DefaultFullHttpResponse(
                HttpVersion.HTTP_1_1,
                HttpResponseStatus.BAD_GATEWAY,
                Unpooled.wrappedBuffer(message.getBytes())
        );
        response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8");
        response.headers().set(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
        ctx.writeAndFlush(response);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        if (cause instanceof SocketException) {
            closeOnFlush(ctx.channel());
            return;
        }
        log.error(cause.getMessage());
        closeOnFlush(ctx.channel());
    }

    private void closeOnFlush(Channel ch) {
        if (ch.isActive()) {
            ch.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
        }
    }
}
@Log4j2
public class RelayHandler extends ChannelInboundHandlerAdapter {

    private final Channel relayChannel;

    public RelayHandler(Channel relayChannel) {
        this.relayChannel = relayChannel;
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        ctx.read();
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        if (relayChannel.isActive()) {
            relayChannel.writeAndFlush(msg).addListener((ChannelFutureListener) future -> {
                if (future.isSuccess()) {
                    ctx.read(); // 继续读取数据
                } else {
                    future.channel().close();
                }
            });
        } else {
            closeOnFlush(ctx.channel());
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        log.error(cause);
        closeOnFlush(ctx.channel());
    }

    private void closeOnFlush(Channel ch) {
        if (ch.isActive()) {
            ch.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
        }
    }
}