关于一个netty的问题!(语言-java)

一个netty的问题,求解决
代码是github上面的,发现https的post请求接口会一直pending
源码地址:https://github.com/puhaiyang/easyHttpProxy
原文地址:https://blog.csdn.net/puhaiyang/article/details/102649498?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522167871597916800182760177%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=167871597916800182760177&biz_id

**https处理片段**
package com.github.puhiayang.handler.proxy;

import com.github.puhiayang.bean.ClientRequest;
import com.github.puhiayang.handler.response.HttpProxyResponseHandler;
import com.github.puhiayang.utils.HttpsSupport;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.*;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;

import io.netty.util.Attribute;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static com.github.puhiayang.bean.Constans.CLIENTREQUEST_ATTRIBUTE_KEY;

/**
 * 对https请求进行代理
 * created on 2019/10/25 18:00
 *
 * @author puhaiyang
 */
public class HttpsProxyHandler extends ChannelInboundHandlerAdapter implements IProxyHandler {
    private Logger logger = LoggerFactory.getLogger(HttpsProxyHandler.class);
    private ChannelFuture httpsRequestCf;

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        logger.debug("进入https处理器------");
        Attribute clientRequestAttribute = ctx.channel().attr(CLIENTREQUEST_ATTRIBUTE_KEY);
        ClientRequest clientRequest = clientRequestAttribute.get();
        if (msg instanceof HttpRequest) {
            HttpRequest req = (HttpRequest)msg;
            logger.debug("请求信息---" + req + "\n端口" + clientRequest.getPort());
            sendToServer(clientRequest, ctx, msg);
        } else if (msg instanceof HttpContent) {
            logger.debug("HttpContent不作处理!");
            //content不做处理
//            ReferenceCountUtil.release(msg);
        } else {
            ByteBuf byteBuf = (ByteBuf) msg;
            // ssl握手
            if (byteBuf.getByte(0) == 22) {
                logger.debug("进入SSL握手--------");
                sendToClient(clientRequest, ctx, msg);
            }
        }
    }

    @Override
    public void sendToServer(ClientRequest clientRequest, ChannelHandlerContext ctx, Object msg) {
        logger.debug("进入发送https请求到服务端-------------");
        Channel clientChannel = ctx.channel();
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(new NioEventLoopGroup(1))
                // 注册线程池
                .channel(NioSocketChannel.class)
                // 使用NioSocketChannel来作为连接用的channel类
                .handler(new ChannelInitializer() {

                    @Override
                    protected void initChannel(Channel ch) throws Exception {
                        //添加一个ssl处理器进行处理
                        ch.pipeline().addLast(
                                HttpsSupport.getInstance().getClientSslCtx().newHandler(ch.alloc(),
                                        clientRequest.getHost(), clientRequest.getPort()));
                        ch.pipeline().addLast("httpCodec", new HttpClientCodec());
                        //添加响应处理器
                        ch.pipeline().addLast("proxyClientHandle", new HttpProxyResponseHandler(clientChannel));
                    }
                });
        httpsRequestCf = bootstrap.connect(clientRequest.getHost(), clientRequest.getPort());
        //建立连接
        httpsRequestCf.addListener((ChannelFutureListener) future -> {
            if (future.isSuccess()) {
                future.channel().writeAndFlush(msg);
                logger.debug("https建立连接成功------");
            } else {
                logger.error("[HttpsProxyHandler][sendToServer]连接远程server失败");
            }
        });
    }

    @Override
    public void sendToClient(ClientRequest clientRequest, ChannelHandlerContext ctx, Object msg) {
        try {
            logger.debug("进入与客户端进行https握手方法------");
//            SslProvider provider =
//                    SslProvider.JDK;

//            SelfSignedCertificate ssc = new SelfSignedCertificate();
//            SslContextBuilder sslContextBuilder = SslContextBuilder.forClient().
//                    sslProvider(SslProvider.OPENSSL).clientAuth(ClientAuth.REQUIRE);
//            SslContext sslCtx = sslContextBuilder.build();
            SslContext sslCtx = SslContextBuilder
                    .forServer(HttpsSupport.getInstance().getServerPriKey(), HttpsSupport.getInstance().getCert(clientRequest.getHost())).build();
            //接收客户端请求,将客户端的请求内容解码
            ctx.pipeline().addFirst("httpRequestDecoder", new HttpRequestDecoder());
            //发送响应给客户端,并将发送内容编码
            ctx.pipeline().addFirst("httpResponseEncoder", new HttpResponseEncoder());
            //http聚合
            ctx.pipeline().addLast("httpAggregator", new HttpObjectAggregator(65536));
            //ssl处理
            ctx.pipeline().addFirst("sslHandle", sslCtx.newHandler(ctx.alloc()));
            // 重新过一遍pipeline,拿到解密后的的http报文
            ctx.pipeline().fireChannelRead(msg);
            Attribute clientRequestAttribute = ctx.channel().attr(CLIENTREQUEST_ATTRIBUTE_KEY);
            clientRequest.setHttps(true);
            clientRequestAttribute.set(clientRequest);
        } catch (Exception e) {
            logger.error("握手报错--- err:{}", e.getMessage());
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
        httpsRequestCf.channel().close();
        httpsRequestCf.channel().eventLoop().parent().shutdownGracefully();
    }

}

证书处理片段


package com.github.puhiayang.utils;

import com.github.puhiayang.EasyHttpProxyServer;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import org.apache.commons.lang3.StringUtils;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.math.BigInteger;
import java.security.*;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.EncodedKeySpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

/**
 * https支持工具类
 *
 * @author puhaiyang
 * created on 2019/10/25 22:27
 */
public class HttpsSupport {
    /**
     * 证书
     */
    private SslContext clientSslCtx;
    /**
     * 证书使用者
     */
    private String issuer;
    /**
     * 证书开始时间
     */
    private Date caNotBefore;
    /**
     * 证书结束时间
     */
    private Date caNotAfter;
    /**
     * ca私钥
     */
    private PrivateKey caPriKey;
    /**
     * 服务端私钥
     */
    private PrivateKey serverPriKey;
    /**
     * 服务端公钥
     */
    private PublicKey serverPubKey;

    /**
     * 证书cahce
     */
    private Map certCache = new HashMap<>();
    /**
     *
     */
    private KeyFactory keyFactory = null;

    private static Logger logger = LoggerFactory.getLogger(HttpsSupport.class);

    private HttpsSupport() {
        initHttpsConfig();
    }

    private static HttpsSupport httpsSupport;

    public static HttpsSupport getInstance() {
        logger.debug("进入证书方法getInstance第1个");
        if (httpsSupport == null) {
            httpsSupport = new HttpsSupport();
        }
        return httpsSupport;
    }

    private void initHttpsConfig() {
        logger.debug("进入证书方法initHttpsConfig第2个");
        try {
            keyFactory = KeyFactory.getInstance("RSA");
            //信任客户端的所有证书,不进行校验
            setClientSslCtx(SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE).build());
            //加载证书
            ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
            //从项目目录加入ca根证书
            X509Certificate caCert = loadCert(classLoader.getResourceAsStream("ca.crt"));
            //从项目目录加入ca私钥
            PrivateKey caPriKey = loadPriKey(classLoader.getResourceAsStream("ca_private.der"));
            setCaPriKey(caPriKey);
            //从证书中获取使用者信息
            setIssuer(getSubjectByCert(caCert));
            //设置ca证书有效期
            setCaNotBefore(caCert.getNotBefore());
            setCaNotAfter(caCert.getNotAfter());
            //生产一对随机公私钥用于网站SSL证书动态创建
            KeyPair keyPair = genKeyPair();
            //server端私钥
            setServerPriKey(keyPair.getPrivate());
            //server端公钥
            setServerPubKey(keyPair.getPublic());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 生成RSA公私密钥对,长度为2048
     */
    private KeyPair genKeyPair() throws Exception {
        logger.debug("进入证书方法genKeyPair第3个");
        Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
        KeyPairGenerator caKeyPairGen = KeyPairGenerator.getInstance("RSA", "BC");
        caKeyPairGen.initialize(2048, new SecureRandom());
        return caKeyPairGen.genKeyPair();
    }

    /**
     * 获取证书中的subject信息
     */
    private String getSubjectByCert(X509Certificate certificate) {
        logger.debug("进入证书方法getSubjectByCert第4个");
        //读出来顺序是反的需要反转下
        List tempList = Arrays.asList(certificate.getIssuerDN().toString().split(", "));
        return IntStream.rangeClosed(0, tempList.size() - 1)
                .mapToObj(i -> tempList.get(tempList.size() - i - 1)).collect(Collectors.joining(", "));
    }

    /**
     * 加载ca的私钥
     *
     * @param inputStream ca私钥文件流
     */
    private PrivateKey loadPriKey(InputStream inputStream) throws Exception {
        logger.debug("进入证书方法loadPriKey第5个");
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        byte[] bts = new byte[1024];
        int len;
        while ((len = inputStream.read(bts)) != -1) {
            outputStream.write(bts, 0, len);
        }
        inputStream.close();
        outputStream.close();
        return loadPriKey(outputStream.toByteArray());
    }

    /**
     * 从文件加载RSA私钥 openssl pkcs8 -topk8 -nocrypt -inform PEM -outform DER -in ca.key -out
     * ca_private.der
     */
    private PrivateKey loadPriKey(byte[] bts)
            throws Exception {
        logger.debug("进入证书方法loadPriKey第6个");
        EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(bts);
        return keyFactory.generatePrivate(privateKeySpec);
    }

    /**
     * 加载ca根证书
     *
     * @param inputStream 证书文件流
     */
    private X509Certificate loadCert(InputStream inputStream) throws Exception {
        logger.debug("进入证书方法loadCert第7个");
        CertificateFactory cf = CertificateFactory.getInstance("X.509");
        return (X509Certificate) cf.generateCertificate(inputStream);
    }

    public SslContext getClientSslCtx() {
        return clientSslCtx;
    }

    public void setClientSslCtx(SslContext clientSslCtx) {
        this.clientSslCtx = clientSslCtx;
    }

    public String getIssuer() {
        return issuer;
    }

    public void setIssuer(String issuer) {
        this.issuer = issuer;
    }

    public Date getCaNotBefore() {
        return caNotBefore;
    }

    public void setCaNotBefore(Date caNotBefore) {
        this.caNotBefore = caNotBefore;
    }

    public Date getCaNotAfter() {
        return caNotAfter;
    }

    public void setCaNotAfter(Date caNotAfter) {
        this.caNotAfter = caNotAfter;
    }

    public PrivateKey getCaPriKey() {
        return caPriKey;
    }

    public void setCaPriKey(PrivateKey caPriKey) {
        this.caPriKey = caPriKey;
    }

    public PrivateKey getServerPriKey() {
        return serverPriKey;
    }

    public void setServerPriKey(PrivateKey serverPriKey) {
        this.serverPriKey = serverPriKey;
    }

    public PublicKey getServerPubKey() {
        return serverPubKey;
    }

    public void setServerPubKey(PublicKey serverPubKey) {
        this.serverPubKey = serverPubKey;
    }


    /**
     * 获取证书
     *
     * @param host host
     * @return host对应的证书
     */
    public X509Certificate getCert(String host) throws Exception {
        logger.debug("进入证书方法getCert第8个");
        if (StringUtils.isBlank(host)) {
            return null;
        }
        X509Certificate cacheCert = certCache.get(host);
        if (cacheCert != null) {
            //将缓存的证书返回
            return cacheCert;
        }
        //生成新的证书,并将它放到缓存中去
        host = host.trim().toLowerCase();
        String hostLowerCase = host.trim().toLowerCase();
        X509Certificate cert = genCert(getIssuer(), getCaPriKey(), getCaNotBefore(), getCaNotAfter(), getServerPubKey(), hostLowerCase);
        //添加到缓存
        certCache.put(host, cert);
        return certCache.get(host);
    }

    /**
     * 动态生成服务器证书,并进行CA签授
     *
     * @param issuer 颁发机构
     */
    /**
     * @param issuer        颁发机构
     * @param caPriKey      ca私钥
     * @param certStartTime 证书开始时间
     * @param certEndTime   证书结束时间
     * @param serverPubKey  server证书的公钥
     * @param hosts         host,支持同时生成多个host
     * @return 证书
     * @throws Exception Exception
     */
    public static X509Certificate genCert(String issuer, PrivateKey caPriKey, Date certStartTime,
                                          Date certEndTime, PublicKey serverPubKey,
                                          String... hosts) throws Exception {
        logger.debug("进入证书方法genCert第9个");
        //根据CA证书subject来动态生成目标服务器证书的issuer和subject
        String subject = "C=CN, ST=SC, L=CD, O=hai, OU=study, CN=" + hosts[0];
        JcaX509v3CertificateBuilder jv3Builder = new JcaX509v3CertificateBuilder(new X500Name(issuer),
                //序列号,需要唯一;ElementaryOS上证书不安全问题(serialNumber为1时证书会提示不安全),避免serialNumber冲突,采用时间戳+4位随机数生成
                BigInteger.valueOf(System.currentTimeMillis() + (long) (Math.random() * 10000) + 1000),
                certStartTime,
                certEndTime,
                new X500Name(subject),
                serverPubKey);
        //SAN扩展证书支持的域名,否则浏览器提示证书不安全
        GeneralName[] generalNames = new GeneralName[hosts.length];
        for (int i = 0; i < hosts.length; i++) {
            generalNames[i] = new GeneralName(GeneralName.dNSName, hosts[i]);
        }
        GeneralNames subjectAltName = new GeneralNames(generalNames);
        //添加多域名支持
        jv3Builder.addExtension(Extension.subjectAlternativeName, false, subjectAltName);
        //SHA256 用SHA1浏览器可能会提示证书不安全
        ContentSigner signer = new JcaContentSignerBuilder("SHA256WithRSAEncryption").build(caPriKey);
        return new JcaX509CertificateConverter().getCertificate(jv3Builder.build(signer));
    }
}

以下答案由GPT-3.5大模型与博主波罗歌共同编写:
根据提供的代码描述,可以猜测出 HttpsProxyHandler 的工作原理如下:

  • 在收到 HTTP/HTTPS 请求时,判断该请求是否为 HTTPS 请求,若是便通过添加 SSL 处理器定向到服务端,建立与服务端的 SSL 握手。
  • 在 SSL 握手完成后,将客户端请求进行解码并将相应的响应内容发送给客户端,对 HTTP 请求进行反向代理。

对于进入 pending 状态的 HTTPS POST 请求,很有可能是服务端在处理 POST 请求时发生了一些错误而造成的,我们可以关注以下几点:

  • 检查服务端处理 POST 请求的代码(尤其是前端接口
    如果我的回答解决了您的问题,请采纳!

参考GPT和自己的思路:这个问题可能是由于没有正确释放HttpContent导致的。在channelRead方法中,当msg是HttpContent时,没有进行正确的释放操作,而是注释掉了。这会导致ByteBuf中的字节数不断增加,直到占满内存,从而导致请求一直挂起。

因此,您可以将HttpContent的释放操作取消注释,让其正确地释放。具体操作是取消下面代码中的注释:

if (msg instanceof HttpRequest) {
    HttpRequest req = (HttpRequest)msg;
    logger.debug("请求信息---" + req + "\n端口" + clientRequest.getPort());
    sendToServer(clientRequest, ctx, msg);
} else if (msg instanceof HttpContent) {
    logger.debug("HttpContent不作处理!");
    ReferenceCountUtil.release(msg); // 添加这一行进行正确的释放
} else {
    ByteBuf byteBuf = (ByteBuf) msg;
    // ssl握手
    if (byteBuf.getByte(0) == 22) {
        logger.debug("进入SSL握手--------");
        sendToClient(clientRequest, ctx, msg);
    }
}

另外,为了保证资源的正确释放,您还需要确保在HttpProxyResponseHandler类中释放资源。您可以在channelInactive方法中进行资源的释放,例如:

@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
    if (clientChannel != null && clientChannel.isActive()) {
        logger.debug("关闭客户端通道");
        clientChannel.close();
    }
    logger.debug("关闭服务端通道");
    ctx.channel().close();

    // 释放聚合缓冲区
    if (this.aggregator != null) {
        this.aggregator.release();
    }
}


除此之外,您还需要确保所有的ByteBuf对象都被正确释放。在HttpProxyResponseHandler类中,您可以使用ReferenceCountUtil.release()方法对接收到的ByteBuf进行释放,例如:

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    if (msg instanceof HttpResponse) {
        HttpResponse response = (HttpResponse) msg;
        // ...
    } else if (msg instanceof HttpContent) {
        HttpContent content = (HttpContent) msg;
        ByteBuf buf = content.content();
        // 处理响应内容
        // ...
        // 释放 ByteBuf 对象
        ReferenceCountUtil.release(buf);
        // 判断是否是最后一个 HttpContent,如果是,则发送响应
        if (content instanceof LastHttpContent) {
            // ...
        }
    }
}


希望能帮到您!

关于netty ,发现https的post请求接口会一直pending的问题:
你可以使用postman等测试下接口的请求是否正常;
其次检查你的post参数或者header没有设置好,比如在iPhone手机上,需要将bool值true或false转换为字符串‘true’或‘false’才行。
再次,post请求被拦截了也会出现这个情况,在你这个例子中不是用java实现http/https抓包拦截了吗。关闭拦截,应该就可以了

在Netty中,https的post请求接口一直pending可能是以下原因引起的:

1.异步请求没有正确响应:由于Netty是基于NIO的异步非阻塞模型实现的,所以请确保异步请求正确响应。我们可以使用Netty提供的ChannelFuture和Promise来判断是否异步请求成功或失败。


```java
ChannelFuture channelFuture = channel.writeAndFlush(requestMsg);
channelFuture.addListener(future -> {
    if (future.isSuccess()) {
        // 处理异步成功逻辑
    } else {
        // 处理异步失败逻辑
    }
});

或者:

Promise<FullHttpResponse> promise = new DefaultPromise<>(channel.eventLoop());
channel.pipeline().addLast(new HttpClientHandler(promise));

channel.writeAndFlush(request);
 
promise.addListener(future -> {
    // 处理异步完成的逻辑
});


2.证书验证失败:如果你的服务器是一个HTTPS服务器,那么请确保你的证书是合法的,否则HTTPS请求将会一直pending。对于这种情况,你可以使用如下代码来设置SSL参数:

```java
SSLContext sslCtx = null;
try {
    sslCtx = SSLContext.getInstance("SSL");
    TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
    KeyStore ks = KeyStore.getInstance("JKS");
    ks.load(new FileInputStream("mytruststore.jks"), "changeit".toCharArray());
    tmf.init(ks);
    sslCtx.init(null, tmf.getTrustManagers(), null);
} catch (Exception e) {
    e.printStackTrace();
}


其中,mytruststore.jks是你的证书库。

3.服务端响应超时:如果服务端响应超时,那么请求将会一直pending,为了解决这个问题,你需要在客户端配置所需的超时时间,如下所示:

Bootstrap bootstrap = new Bootstrap();
...
bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000);


其中,CONNECT_TIMEOUT_MILLIS 表示连接超时时间(5秒)。

以上3种情况都需要进行排查,有助于解决https的post请求接口pending的问题。

进一步定位到,接收不到post的body参数

对于 Netty 中 HTTPS POST 请求接口一直 pending 的问题,可能有以下几个原因:

证书问题:如果您的 SSL 证书不是有效的自签名证书或 CA 颁发的证书,那么浏览器可能会拒绝连接。请检查您的 SSL 证书是否正确安装和配置。

客户端问题:请检查您的客户端代码是否正确设置了 SSL 参数,包括证书、密码等。

服务端问题:请检查您的服务器是否正确配置了 SSL 参数,包括证书、密码等。

防火墙问题:请检查您的防火墙是否允许 SSL 请求通过。如果您的请求在防火墙上被阻止,那么它可能会一直处于 pending 状态。

针对以上原因,您可以按照以下步骤进行排查和解决:

检查 SSL 证书是否正确安装和配置。

检查客户端代码是否正确设置了 SSL 参数。

检查服务器是否正确配置了 SSL 参数。

检查防火墙设置,确保 SSL 请求能够通过。

如果以上步骤均未解决问题,您可以尝试使用 Wireshark 等工具进行抓包分析,以确定问题所在。