使用netty开发简易http代理服务器

前言

netty作为一款java高性能网络框架,具备支持OIO(阻塞式传输)/NIO(非阻塞式传输)等能力,同时屏蔽了网络底层现, 可以使开发人员专注于应用逻辑开发.为学习netty框架,我决定从头开始写一款http代理服务器,既可以使自己学习netty框架,又可以充分了解http代理的工作方式.之前在项目中遇到了代理设备的bug,使项目进度受到了很大的影响, 在解决问题的过程中使自己对代理的工作方式产生了一定的兴趣.

支持的功能

  1. http代理
  2. https代理
  3. 代理安全认证
  4. 代理黑白名单
  5. channel存活检测
  6. 日志打印

代码

  1. 引入netty依赖
	<dependency>
	   <groupId>io.netty</groupId>
	   <artifactId>netty-all</artifactId>
	   <version>4.1.45.Final</version>
	</dependency>
  1. 启动类
    使用ServerBootstrap类启动,EventLoopGroup使用两个,一个用来处理新客户端的连接,一个用来处理已有连接.
    childHandler使用了netty自带的HttpServerCodec/HttpObjectAggregator类,用来对http请求进行编码,最后依次添加自定义的ProxyBlackWhiteIPListHandler/ ProxyAuthorizationHandler/ HttpServerHandler类.
    将这些handler添加到ChannelPipeline后,一个http请求将依次经过HttpServerCodec/ HttpObjectAggregator(编码聚合) → ProxyBlackWhiteIPListHandler(黑白名单) → ProxyAuthorizationHandler(用户认证) → HttpServerHandler(业务处理),并完成相应的业务功能.
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import lombok.extern.slf4j.Slf4j;

/**
 * @description:代理服务器启动类,依靠此类启动代理服务器程序
 * @projectName:proxy-wg
 * @see:com.wg.proxy
 * @author:wanggang
 * @createTime:2020/1/25 16:29
 * @version:1.0
 */
@Slf4j
public class HttpServer {
    private final int port;

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

    public static void main(String[] args) throws InterruptedException {
        //获取启动port前实例化ProxyConfig,加载配置文件
        int port = new ProxyConfig().PORT;
        if (args.length == 1) {
            try {
                port = Integer.valueOf(args[0]);
            } catch (NumberFormatException e) {
                log.error("输入的自定义启动端口:\"{}\"不合法", args[0]);
                return;
            }
        }
        log.info("在<<< {} >>>端口启动了代理服务器", port);
        new HttpServer(port).bind();
    }

    public void bind() throws InterruptedException {
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workGroup = new NioEventLoopGroup();
        ServerBootstrap b = new ServerBootstrap();
        try {
            b.group(bossGroup, workGroup)
                    .channel(NioServerSocketChannel.class)
                    .localAddress(this.port)
                    //存放待建立连接的队列,满了之后客户端无法在与代理建立连接
                    .option(ChannelOption.SO_BACKLOG, 1024)
                    .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
                    .childOption(ChannelOption.SO_KEEPALIVE, false)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            //首先进行http编码
                            pipeline.addLast("httpCodec", new HttpServerCodec());
                            //最大接收的http请求为100MB
                            pipeline.addLast("aggregator", new HttpObjectAggregator(100 * 1024 * 1024));
                            //是否开启代理黑白名单
                            if (ProxyConfig.USE_BLCAK_WHITE_IP) {
                                pipeline.addLast("proxyBalckWhiteIPList", new ProxyBlackWhiteIPListHandler());
                            }
                            //是否使用认证
                            if (ProxyConfig.USE_PROXY_AUTHORIZATION) {
                                pipeline.addLast("proxyAuthorization", new ProxyAuthorizationHandler());
                            }
                            //进行自定义handler处理
                            pipeline.addLast("httpServer", new HttpServerHandler());
                        }
                    });
            ChannelFuture cf = b.bind().sync();
            cf.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            bossGroup.shutdownGracefully().sync();
            workGroup.shutdownGracefully().sync();
        }
    }
}

  1. 黑白名单管理handler
    http请求解码完成后会首先到达此handler,此handler主要完成以下功能:
    * 生成唯一请求流水号,如果不在黑名单中则放入http请求头中,向下传递,用于唯一确认一笔请求
    * 检查请求ip是否在黑名单中,如果在则发送拒绝response
    * 检查此ip是否已进行过认证,如果已进行过认证则将ProxyAuthorizationHandler移除,避免重复认证
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.util.ReferenceCountUtil;
import lombok.extern.slf4j.Slf4j;
import redis.clients.jedis.JedisCluster;

/**
 * @description:
 * @projectName:proxy-wg
 * @see:com.wg.proxy
 * @author:wanggang
 * @createTime:2020/2/7 20:22
 * @version:1.0
 */
@Slf4j
public class ProxyBlackWhiteIPListHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
    /*唯一请求业务号*/
    private String requestId;
    /*redis客户端*/
    private JedisCluster cluster = RedisUtil.getRedisPool();
    /*client IP*/
    private String remoteIP = null;
    /*存储到redis中的client IP key值*/
    private String redisRemoteIPKey = null;

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest msg) throws Exception {
        requestId = HttpUtils.getUUID();
        remoteIP = HttpUtils.getRemoteIP(ctx);
        //如果remoteIP在黑名单中,则不代理
        if (checkBlackProxyIP(remoteIP)) {
            sendRejectResponse(ctx, msg);
            return;
        }
        redisRemoteIPKey = "ip:" + remoteIP;
        //检查ip在规定时间内是否已经认证过,不在重复认证
        if (checkWhiteProxyIP(ctx)) {
            log.info("{}-此IP<{}>已进行过验证,不再校验", requestId, remoteIP);
            //如果开启了代理认证
            if (ProxyConfig.USE_PROXY_AUTHORIZATION) {
                ctx.pipeline().remove(ProxyAuthorizationHandler.class);
            }
        }
        ReferenceCountUtil.retain(msg);
        msg.headers().set("requestId", requestId);
        ctx.fireChannelRead(msg);
    }

    /**
     * 检查远程ip是否在redis中,如果在redis中返回true,否则返回false
     *
     * @param ctx
     * @return
     */
    private boolean checkWhiteProxyIP(ChannelHandlerContext ctx) {
        String redisRemoteIP = cluster.get(redisRemoteIPKey);
        return redisRemoteIP != null;
    }

    /**
     * 检查client ip是否在白名单中,如果在返回true,否则返回false
     *
     * @param remoteIP
     * @return
     */
    private boolean checkBlackProxyIP(String remoteIP) {
        return ProxyConfig.BLACK_PROXY_IP.contains(remoteIP);
    }

    /**
     * 黑名单ip发送拒绝响应
     *
     * @param ctx
     * @param msg
     */
    private void sendRejectResponse(ChannelHandlerContext ctx, FullHttpRequest msg) {
        log.info("{}-<{}>发送的代理请求<{}>IP在黑名单中,发送拒绝响应", requestId, ctx.channel().remoteAddress().toString(), msg.uri());
        ctx.writeAndFlush(HttpUtils.getFailResponse()).addListener(ChannelFutureListener.CLOSE);
    }
}
  1. 用户验证handler
    此handler可支持BASIC和DIGEST两种认证方式,其实这两种认证方式都算不上安全,但浏览器都支持这两种认证方式,用于学习的话足够了,其主要原理是使用407口令验证,DIGEST相较于BASIC而言增加了对请求信息的验证,更加安全一些.
import io.netty.channel.*;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.util.ReferenceCountUtil;
import lombok.extern.slf4j.Slf4j;
import redis.clients.jedis.JedisCluster;
import sun.misc.BASE64Decoder;

import java.io.IOException;
import java.util.Arrays;
import java.util.Map;

/**
 * @description:当client与proxy进行首次连接时,如携带Proxy-Authorization头,则进行校验,如未携带,则返回407,校验通过后将本handler从channelpipeline中移除
 * @projectName:proxy-wg
 * @see:com.wg.proxy
 * @author:wanggang
 * @createTime:2020/2/3 21:07
 * @version:1.0
 */
@Slf4j
@ChannelHandler.Sharable
public class ProxyAuthorizationHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
    /*BASIC认证解码用*/
    private BASE64Decoder decoder = new BASE64Decoder();
    /*唯一请求业务号*/
    private String requestId;
    /*redis客户端*/
    private JedisCluster cluster = RedisUtil.getRedisPool();
    /*client IP*/
    private String remoteIP = null;
    /*存储到redis中的client IP key值*/
    private String redisRemoteIPKey = null;

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest msg) throws Exception {
        requestId = msg.headers().get("requestId");
        remoteIP = HttpUtils.getRemoteIP(ctx);
        redisRemoteIPKey = "ip:" + remoteIP;
        //根据配置项,决定采用哪种认证,目前支持BASIC和DIGEST两种
        if (ProxyConfig.PROXY_AUTHORIZATION_TYPE.equals("BASIC")) {
            checkBasicAuth(ctx, msg);
        } else if (ProxyConfig.PROXY_AUTHORIZATION_TYPE.equals("DIGEST")) {
            checkDigest(ctx, msg);
        } else {
            ctx.pipeline().remove(ProxyAuthorizationHandler.class);
            ReferenceCountUtil.retain(msg);
            ctx.fireChannelRead(msg);
        }
    }


    /**
     * 使用BASIC校验方式验证账号和密码
     *
     * @param ctx
     * @param msg
     * @throws IOException
     */
    private void checkBasicAuth(ChannelHandlerContext ctx, FullHttpRequest msg) throws IOException {
        //获取request中Proxy-Authorization头
        String proxyAuthorization = msg.headers().get("Proxy-Authorization");
        if (proxyAuthorization == null || "".equals(proxyAuthorization)) {
            sendCheckBasicAuthFailResponse(ctx, msg);
            return;
        }
        //格式为BASIC [base64后的auth:pwd]
        String[] basicAuth = proxyAuthorization.split(" ");
        String auth = basicAuth[1];
        if (auth == null || "".equals(auth)) {
            sendCheckBasicAuthFailResponse(ctx, msg);
            return;
        }
        //解码后进行校验,如不通过则返回校验失败,407code
        byte[] decodeBuffer = decoder.decodeBuffer(auth);
        String authPwd = new String(decodeBuffer, "UTF-8");
        log.info("{}-proxy对<{}>发送的代理请求<{}>进行校验,AuthorizationBasic为<{}>", requestId, ctx.channel().remoteAddress().toString(), msg.uri(), authPwd);
        if ((ProxyConfig.AUTH + ":" + ProxyConfig.PWD).equals(authPwd)) {
            log.info("{}-用户名或密码校验通过", requestId);
            //将IP存入redis中,3600秒内不在校验
            cluster.set(redisRemoteIPKey, remoteIP);
            cluster.expire(redisRemoteIPKey, 3600);
            //将requestId写入请求中,传递给HTTPServerHandler
            msg.headers().set("requestId", requestId);
            ctx.pipeline().remove(ProxyAuthorizationHandler.class);
            ReferenceCountUtil.retain(msg);
            ctx.fireChannelRead(msg);
        } else {
            log.info("{}-用户名或密码错误,校验失败", requestId);
            ctx.writeAndFlush(HttpUtils.getProxyAuthorizationBasic()).addListener(ChannelFutureListener.CLOSE);
        }
    }

    /**
     * 使用DIGEST方式验证账号和密码
     * 1.检验请求头中是否携带proxyAuthorization,未携带发送407响应
     * 2.已携带对proxyAuthorization进行格式化分割到map中
     * 3.检测发送的nonce是否过期
     * 4.校验response项计算是否正确
     * 5.计算正确将本handler移除,错误返回407响应继续要求验证
     *
     * @param ctx
     * @param msg
     */
    private void checkDigest(ChannelHandlerContext ctx, FullHttpRequest msg) {
        String proxyAuthorization = msg.headers().get("Proxy-Authorization");
        String key = "nonce:" + requestId;
        cluster.set(key, requestId);
        cluster.expire(key, 1800);
        if (proxyAuthorization == null || "".equals(proxyAuthorization)) {
            log.info("{}-<{}>发送的代理请求<{}>Proxy-Authorization为空,校验失败", requestId, ctx.channel().remoteAddress().toString(), msg.uri());
            sendCheckDigestAuthFailResponse(ctx, msg);
            return;
        }
        //格式为Proxy-Authorization:
        //   Proxy-Authorization: Digest username=wg, realm=proxy-wg, nonce="N6yEOiDGTvOx9hwloHW7AQ==", uri="www.baidu.com:443",
        //   cnonce=5c732d2f0f435ae18030c3bdb90f6a16, nc=00000001, response=6ab3961d12135bd470884a467a0c65a2, qop=auth
        log.info("{}-获取到的proxyAuthorization为:{}", requestId, proxyAuthorization);
        //为便于分割,将"和空格全部替换为空,按照,分割
        proxyAuthorization = proxyAuthorization.replaceAll("\"", "");
        proxyAuthorization = proxyAuthorization.replaceAll(" ", "");
        String[] digestAuth = proxyAuthorization.split(",");
        String method = msg.method().name();
        //按照配置项中的账号和密码以及上送的信息计算response
        Map<String, String> splitDigestAuth = HttpUtils.getSplitDigestAuth(digestAuth, method);
        String proxyNonce = cluster.get("nonce:" + splitDigestAuth.get("nonce"));
        if (proxyNonce == null) {
            log.info("{}-nonce过期,验证失败", requestId);
            ctx.writeAndFlush(HttpUtils.getProxyAuthorizationDigest(requestId)).addListener(ChannelFutureListener.CLOSE);
            return;
        }
        String proxyDigestResponse = HttpUtils.http_da_calc_HA1(splitDigestAuth.get("username"), splitDigestAuth.get("realm")
                , splitDigestAuth.get("password"), splitDigestAuth.get("nonce"), splitDigestAuth.get("nc")
                , splitDigestAuth.get("cnonce"), splitDigestAuth.get("qop"), splitDigestAuth.get("method")
                , splitDigestAuth.get("uri"), splitDigestAuth.get("algorithm"));
        log.info("{}-计算出的response为:<{}>", requestId, proxyDigestResponse);
        String clientDigestResponse = null;
        //获取上送的response
        clientDigestResponse = splitDigestAuth.get("response");
        //解码后进行校验,如不通过则返回校验失败,407code
        log.info("{}-proxy对<{}>发送的代理请求<{}>进行校验,AuthorizationDigest为<{}>", requestId, ctx.channel().remoteAddress().toString(), msg.uri(), Arrays.asList(digestAuth).toString());
        if ((proxyDigestResponse).equals(clientDigestResponse)) {
            log.info("{}-用户名或密码校验通过", requestId);
            //将IP存入redis中,3600秒内不在校验
            cluster.set(redisRemoteIPKey, remoteIP);
            cluster.expire(redisRemoteIPKey, 3600);
            //将requestId写入请求中,传递给HTTPServerHandler
            msg.headers().set("requestId", requestId);
            ctx.pipeline().remove(ProxyAuthorizationHandler.class);
            ReferenceCountUtil.retain(msg);
            ctx.fireChannelRead(msg);
        } else {
            log.info("{}-用户名或密码错误,校验失败", requestId);
            cluster.del(key);
            ctx.writeAndFlush(HttpUtils.getProxyAuthorizationDigest(requestId)).addListener(ChannelFutureListener.CLOSE);
        }
    }

    /**
     * BASIC校验失败时写回校验失败response
     *
     * @param ctx
     * @param msg
     */
    private void sendCheckBasicAuthFailResponse(ChannelHandlerContext ctx, FullHttpRequest msg) {
        log.info("{}-<{}>发送的代理请求<{}>Proxy-Authorization为空,校验失败", requestId, ctx.channel().remoteAddress().toString(), msg.uri());
        ctx.writeAndFlush(HttpUtils.getProxyAuthorizationBasic());
    }

    /**
     * DIGEST校验失败时写回校验失败response
     *
     * @param ctx
     * @param msg
     */
    private void sendCheckDigestAuthFailResponse(ChannelHandlerContext ctx, FullHttpRequest msg) {
        ctx.writeAndFlush(ctx.writeAndFlush(HttpUtils.getProxyAuthorizationDigest(requestId)));
    }
}
  1. 业务处理handler
    用于接收代理请求,根据是http还是https代理请求做出不同的响应:
    * HTTPS:将HTTP请求发送到remote,建立一条连接,并实时将remote返回的数据写回client channel中(透传)
    * HTTP:直接连接到远程服务器,将client连接到proxy的channel放入ConnectToRemoteHandler中,将remote返回的数据实时写入client channel中
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.*;
import io.netty.handler.timeout.IdleStateHandler;
import io.netty.util.ReferenceCountUtil;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;

/**
 * @description:处理接收到的HTTP请求,并发起对远程服务器的连接
 * @projectName:proxy-wg
 * @see:com.wg.proxy
 * @author:wanggang
 * @createTime:2020/1/25 16:29
 * @version:1.0
 */
@Slf4j
@ChannelHandler.Sharable
public class HttpServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
    private String host;
    private int port;
    /*client请求流水号*/
    private String requestId;
    /*proxy与remote服务器建立连接的bootstrap*/
    private Bootstrap proxyBootstrap = new Bootstrap();
    /*proxy与remote服务器的channel*/
    private ChannelFuture cf;
    /*Host头是否没有port,如果没有则根据是http还是https请求使用默认端口*/
    private boolean isNoPort = false;
    /*read0读取到的msg,用于debug内存泄漏*/
    private Object msg;

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.flush();
        if (requestId == null || requestId.equals("")) {
            requestId = HttpUtils.getUUID();
        }
        log.debug("{}-complete msg.count = {}", requestId, ReferenceCountUtil.refCnt(msg));
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        log.error("{}-HttpServerHandler exception:{}", requestId, cause.getMessage());
        if (log.isDebugEnabled()) {
            cause.printStackTrace();
        }
        log.info("{}-HttpServerHandler 关闭client/remote channel", requestId);
        ctx.close();
        if (cf != null) {
            cf.channel().close();
        }
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest msg) throws Exception {
        this.msg = msg;
        this.requestId = msg.headers().get("requestId");
        if (requestId == null || requestId.equals("")) {
            requestId = HttpUtils.getUUID();
        }
        log.debug("{}-读取msg", requestId);
        //获取远程服务器host和port
        log.info("{}-proxy接收到的<{}>代理请求为:{}-{}", requestId, ctx.channel().remoteAddress().toString(), msg.method(), msg.uri());
        //收到 / url会引起本地死循环,不处理这种请求
        String uriHost = msg.headers().get("Host");
        this.host = uriHost.split(":")[0];
        this.port = 80;
        if (uriHost.split(":").length > 1) {
            this.port = Integer.valueOf(uriHost.split(":")[1]);
        } else {
            this.isNoPort = true;
        }
        //不处理本地ip发来的请求(防止恶意请求引发死循环)
        if (("localhost".equals(host) || "127.0.0.1".equals(host)) && port == 8080) {
            ctx.writeAndFlush(HttpUtils.getFailResponse());
            log.error("{}-uri为:{}", requestId, msg.uri());
            return;
        }
        HttpMethod method = msg.method();
        //判断是http还是https请求
        if ("CONNECT".equals(method.name())) {
            if (this.isNoPort) {
                this.port = 443;
            }
            //https请求
            this.connectToRemote(ctx, msg);
            cf.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture future) throws Exception {
                    //利用之前添加的httpCodec将okresponse写回到client,然后将其移除,保留channel进行tcp级别的透传
                    ctx.writeAndFlush(HttpUtils.getOKResponse());
                    if (ctx.pipeline().get("httpCodec") != null) {
                        ctx.pipeline().remove("httpCodec");
                    }
                }
            });
        } else {
            if (this.isNoPort) {
                this.port = 80;
            }
            ReferenceCountUtil.retain(msg);
            //http请求,直接发起到远程服务器的请求
            this.connectToRemote(ctx, msg);
            cf.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture future) throws Exception {
                    //将httpCodec移除,添加一个与remote server的httpEncoder,将request发送到remote,然后将其移除,接收到返回数据后直接写回到client channel
                    if (ctx.pipeline().get("httpCodec") != null) {
                        ctx.pipeline().remove("httpCodec");
                    }
                    future.channel().pipeline().addLast("httpEncoder", new HttpRequestEncoder());
                    log.info("{}-将client发送的代理请求转向remote服务器", requestId);
                    future.channel().writeAndFlush(msg);
                    //将请求发送到远程服务器后不再需要
                    future.channel().pipeline().remove("httpEncoder");
                }
            });
        }
        log.debug("{}-read0 msg.count = {}", requestId, ReferenceCountUtil.refCnt(this.msg));
    }

    /**
     * 连接到远程服务器,并将ConnectToRemoteHandler添加到Pipeline中
     * 1HTTPS:将HTTP请求发送到remote,建立一条连接,并实时将remote返回的数据写回client channel中
     * 2.HTTP:直接连接到远程服务器,将client连接到proxy的channel放入ConnectToRemoteHandler中,将remote返回的数据实时写入client channel中
     * * 2.1分为两次handler,第一次建立连接的handler用来转发请求,连接建立之后的其他请求使用childHandler
     * * 2.2.HTTPS:将HTTP请求发送到remote,建立一条连接,并实时将remote返回的数据写回client channel中
     *
     * @param ctx client与proxy之间channel的ChannelHandlerContext变量
     * @param msg client向proxy发送的完整httprequest
     */
    private ChannelFuture connectToRemote(ChannelHandlerContext ctx, FullHttpRequest msg) throws InterruptedException {
        try {
            cf = proxyBootstrap.group(ctx.channel().eventLoop())
                    .channel(NioSocketChannel.class)
                    .remoteAddress(host, port)
                    .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000)
                    //处理第一次建立连接时的请求
                    .handler(new ConnectToRemoteHandler(ctx.channel(), requestId))
                    .connect()
                    //这个listener会比上面的listener先执行
                    .addListener(new ChannelFutureListener() {
                        @Override
                        public void operationComplete(ChannelFuture future) throws Exception {
                            if (future.isSuccess()) {
                                //打开与proxy的一条tcp连接,如是HTTPS请求则返回200响应,在channel中透传tcp数据
                                log.info("{}-连接到远程服务器{}:{}成功", requestId, host, port);
                                //建立连接后将handler移除,便于接收后续请求
                                if (ctx.pipeline().get("aggregator") != null) {
                                    ctx.pipeline().remove("aggregator");
                                }
                                if (ctx.pipeline().get("httpServer") != null) {
                                    ctx.pipeline().remove("httpServer");
                                }
                                //如无数据传输每隔30s发起一次IdleStateEvent,与remote进行存活检测,发送心跳失败后关闭channel
                                ctx.pipeline().addLast(new IdleStateHandler(0, 0, 60, TimeUnit.SECONDS));
                                //处理连接建立后的后续请求
                                ctx.pipeline().addLast("remoteHandler", new ConnectToRemoteHandler(future.channel(), requestId));
                            } else {
                                log.warn("{}-连接到远程服务器{}:{}失败", requestId, host, port);
                                ctx.writeAndFlush(HttpUtils.getFailResponse())
                                        .addListener(ChannelFutureListener.CLOSE);
                            }
                        }
                    });
        } catch (Exception e) {
            log.error("{}-与远程服务器建立连接转发数据的过程中出错", requestId);
            e.printStackTrace();
            cf.channel().closeFuture().sync();
        } finally {
        }
        return cf;
    }
}
  1. 连接远程服务器handler
    用于连接到客户端需要请求的远程服务器,传递数据,并分别与客户端和远程服务器进行定时存活检测,如果连接已关闭则将此channel关闭,避免资源占用.
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.util.CharsetUtil;
import lombok.extern.slf4j.Slf4j;

/**
 * @description:
 * @projectName:proxy-wg
 * @see:com.wg.proxy
 * @author:wanggang
 * @createTime:2020/1/25 21:36
 * @version:1.0
 */
@Slf4j
@ChannelHandler.Sharable
public class ConnectToRemoteHandler extends ChannelInboundHandlerAdapter {

    private Channel channel;
    private String requestId;
    private final ByteBuf HEARTBEAT_SEQUENCE = Unpooled.unreleasableBuffer(Unpooled.copiedBuffer("HEARTBEAT", CharsetUtil.UTF_8));

    public ConnectToRemoteHandler(Channel channel, String requestId) {
        this.channel = channel;
        this.requestId = requestId;
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        channel.flush();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        log.error("{}-connectToremoteHandler exception-{}", requestId, cause.getMessage());
        if (log.isDebugEnabled()) {
            cause.printStackTrace();
        }
        log.info("{}-connectToremoteHandler 关闭ctx", requestId);
        ctx.close();
        channel.close();
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        channel.write(msg);
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent) {
            log.info("{}-与remote服务器进行心跳检测", requestId);
            ChannelFuture remoteCf = ctx.writeAndFlush(HEARTBEAT_SEQUENCE.duplicate())
                    .addListener(new ChannelFutureListener() {
                        @Override
                        public void operationComplete(ChannelFuture future) throws Exception {
                            if (future.isSuccess()) {
                                log.info("{}-发送心跳成功", requestId);
                            } else {
                                log.info("{}-发送心跳失败,关闭channel", requestId);
                                future.channel().close();
                                channel.close();
                            }
                        }
                    });
            log.info("{}-与client进行心跳检测", requestId);
            channel.writeAndFlush(HEARTBEAT_SEQUENCE.duplicate())
                    .addListener(new ChannelFutureListener() {
                        @Override
                        public void operationComplete(ChannelFuture future) throws Exception {
                            if (future.isSuccess()) {
                                log.info("{}-发送心跳成功", requestId);
                            } else {
                                log.info("{}-发送心跳失败,关闭channel", requestId);
                                future.channel().close();
                                remoteCf.channel().close();
                            }
                        }
                    });
        } else {
            super.userEventTriggered(ctx, evt);
        }
    }
}

总结

  1. netty框架大量运用了直接内存用来提高效率,编写代码时一定要注意及时释放资源,避免内存泄漏问题.
  2. 编写类似于此种需要服务端再向远程发起请求的服务时,注意eventLoop的复用,可以节省大量的资源.
    本人也是netty使用上的小白,可能有很多理解不到位或错误的地方,完成这个之后将再使用netty完成一个dns服务,如有任何问题欢迎交流.
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值