概述
场景描述:
netty tcp 服务端与客户端进行交互,之前采用服务器直连的方式,即:
客户端 -----------> 服务端(tcp)
此时服务端可以通过:
ctx.channel().remoteAddress() 获取客户端IP进行相关的业务使用。
但是后续随着客户端增加,服务端就上来了,此时使用nginx代理的方式增加netty服务端数量一次来处理更多的连接数。
客户端 -----------> nginx --------------->服务端(tcp,多台部署)
这样就导致了nginx代理后 ctx.channel().remoteAddress()获取的是nginx代理服务器的地址,无法获取客户端真实IP。
解决办法(个人的一种方式,且自认为不是最棒的,供大家围观):
1、nginx
先说nginx,nginx代理tcp需要安装stream模块,此时涉及到一个代理协议proxy protocol的使用。
推荐博文: https://www.jianshu.com/p/cc8d592582c9 大家也可以找资料更深入的理解。
简而言之就是:nginx代理tcp(四层tcp代理)时增加一个头信息,其中包含了客户端IP的信息,后端则想办法从此头报文中获取IP。
代理协议proxy protocol的使用场景(都是为了获取客户端信息):
4层tcp代理 转 7层http代理
4层tcp代理 转 4层tcp代理 (多层tcp代理)
这样的场景下需要使用代理协议进行客户端信息的传递。
我现在要解决的就是简单的一层nginx(tcp代理)的场景。
配置示例:
stream {
log_format proxy '$remote_addr [$time_local] '
'$protocol $status $bytes_sent $bytes_received '
'$session_time "$upstream_addr" '
'"$upstream_bytes_sent" "$upstream_bytes_received" "$upstream_connect_time"';
access_log logs/tcp.log proxy;
upstream netty-tcp{
server 0.0.0.0:8899 weight=5;
}
server {
listen 18899 ssl;
ssl_certificate /root/nginx/cert/3565893_xxx.pem;
ssl_certificate_key /root/nginx/cert/3565893_xxx.key;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers HIGH:!aNULL:!MD5;
proxy_pass netty-tcp;
proxy_protocol on; #仅此一句重点
}
}
重点是开启代理:
proxy_protocol on;
更多配置请自行查看 http://www.weixueyuan.net/a/751.html
2、netty服务端
netty服务端就是要拿到这段代理头报文进行IP获取。
备注:此段报文只会在首次建立连接时发送一次,建立连接后不会发送,断开连接再次连接时仍会发送。
通过博文:https://www.jianshu.com/p/cc8d592582c9 我们大概得知这段报文的格式:
PROXY TCP4 101.106.236.66 192.168.0.150 12646 5683rn
思路:以tcp为例,您的编码、解码方式,客户端、服务端肯定是保持一致的,而我要做的是自定义一个解码器(仅服务端),然后对这段PROXY报文进行处理,对于没有包含这段报文的tcp流不做处理放行即可。
代码如下:
/**
* @Author
* @Date
* @Description nginx代理netty tcp服务端负载均衡,nginx stream要打开 proxy_protocol on; 配置
*/
public class DecodeProxy extends ByteToMessageDecoder {
private Logger logger = LoggerFactory.getLogger(DecodeProxy.class);
/**
* 保存客户端IP
*/
public static AttributeKey<String> key = AttributeKey.valueOf("IP");
/**
* decode() 会根据接收的数据,被调用多次,直到确定没有新的元素添加到list,
* 或者是 ByteBuf 没有更多的可读字节为止。
* 如果 list 不为空,就会将 list 的内容传递给下一个 handler
* @param ctx 上下文对象
* @param byteBuf 入站后的 ByteBuf
* @param out 将解码后的数据传递给下一个 handler
* @throws Exception
*/
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf byteBuf, List<Object> out) throws Exception {
/*消息打印--------------------------*/
byte[] bytes = printSz(byteBuf);
String message = new String(bytes, Charset.forName("UTF-8"));
//logger.info("从客户端收到的字符串:" + message);
/*消息打印--------------------------*/
if(bytes.length > 0){
//判断是否有代理
if(message.indexOf("PROXY") != -1){
//PROXY TCP4 101.106.236.66 192.168.0.150 12646 5683rn
logger.info("PROXY MSG: " + message.substring(0,message.length()-2));
if(message.indexOf("n") != -1){
String[] str = message.split("n")[0].split(" ");
logger.info("Real Client IP: " + str[2]);
Attribute<String> channelAttr = ctx.channel().attr(key);
//基于channel的属性
if(null == channelAttr.get()){
channelAttr.set(str[2]);
}
}
//清空数据,重要不能省略
byteBuf.clear();
}
if(byteBuf.readableBytes() > 0){
//logger.info("out add!!!");
out.add(byteBuf.readBytes(byteBuf.readableBytes()));
}
}
}
/**
* 打印byte数组
* @param newBuf
*/
public byte[] printSz(ByteBuf newBuf){
ByteBuf copy = newBuf.copy();
byte[] bytes = new byte[copy.readableBytes()];
copy.readBytes(bytes);
//logger.info("字节数组打印:" + Arrays.toString(bytes));
return bytes;
}
}
以我的编码解码为例:
客户端:
pipeline.addLast("frameDecoder",new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 2, 0, 2));
pipeline.addLast("frameEncoder", new LengthFieldPrepender(2, false));
服务端:
pipeline.addLast("decoder",new DecodeProxy());//增加这个自定义的解码器
pipeline.addLast("frameDecoder",new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 2, 0, 2));
pipeline.addLast("frameEncoder", new LengthFieldPrepender(2, false));
说明:这个自定义的解码器是nginx代理后,nginx与服务端建立连接时使用代理协议发送的一段包含客户端信息的报文,即上面的:PROXY TCP4 101.106.236.66 192.168.0.150 12646 5683rn,而自定义解码器就是为了解析到这个报文进行IP的获取,其他的报文放行不做任何处理。
知识点:AttributeKey 推荐博客:https://blog.csdn.net/zcf9916/article/details/84914630
简单说就是:类似于tomcat服务器的session一样,保存数据绑定自己的上下文,多客户端连接数据隔离。(我理解不深)
这样就能通过:
Attribute<String> channelAttr = ctx.channel().attr(DecodeProxy.key);
//基于channel的属性
if(null != channelAttr.get()){
logger.info("IP地址--------------- :" + channelAttr.get());
clientIp = channelAttr.get();
}
获取到客户端IP地址了。
知识点总结:
1、nginx 代理协议proxy protocol
2、netty AttributeKey对象的使用
个人简单总结,描述有不清楚或者瑕疵的地方多多包涵,netty真是还有好多要学习的啊!
写在最后,感谢两个大神的博客:
https://www.jianshu.com/p/cc8d592582c9
https://blog.csdn.net/zcf9916/article/details/84914630
最后
以上就是矮小毛巾为你收集整理的nginx代理 netty tcp服务端获取客户端真实IP的全部内容,希望文章能够帮你解决nginx代理 netty tcp服务端获取客户端真实IP所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复