前言
netty作为一款java高性能网络框架,具备支持OIO(阻塞式传输)/NIO(非阻塞式传输)等能力,同时屏蔽了网络底层现, 可以使开发人员专注于应用逻辑开发.为学习netty框架,我决定从头开始写一款http代理服务器,既可以使自己学习netty框架,又可以充分了解http代理的工作方式.之前在项目中遇到了代理设备的bug,使项目进度受到了很大的影响, 在解决问题的过程中使自己对代理的工作方式产生了一定的兴趣.
支持的功能
- http代理
- https代理
- 代理安全认证
- 代理黑白名单
- channel存活检测
- 日志打印
代码
- 引入netty依赖
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.45.Final</version>
</dependency>
- 启动类
使用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();
}
}
}
- 黑白名单管理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);
}
}
- 用户验证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)));
}
}
- 业务处理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;
}
}
- 连接远程服务器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);
}
}
}
总结
- netty框架大量运用了直接内存用来提高效率,编写代码时一定要注意及时释放资源,避免内存泄漏问题.
- 编写类似于此种需要服务端再向远程发起请求的服务时,注意eventLoop的复用,可以节省大量的资源.
本人也是netty使用上的小白,可能有很多理解不到位或错误的地方,完成这个之后将再使用netty完成一个dns服务,如有任何问题欢迎交流.