共计 7128 个字符,预计需要花费 18 分钟才能阅读完成。
Netty-TCP代理转发程序
本文将使用Netty实现一个TCP代理服务器,该服务器可以将我们的TCP连接转发到我们指定的目标程序例如:MySQL、Redis等。
设计图
Handler
首先确定我们网络程序的ChannelHandler。在整个过程中,我们其实可以发现无论是哪个Channel,我们的处理都是向Channel中写数据。例如:用户和代理服务器建立连接,用户发送数据,代理服务器接收到数据后,与代理目标建立一个Channel向里面写入数据;代理目标返回数据时,会触发Channel的读事件,此时就需要向用户与代理服务器之间的Channel写数据。所以我们的Handler就是向一个Channel中写数据,但是我们不能使用下面的方式向Channel中写数据:
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ctx.channel().writeAndFlush(msg);
}
这种方式是向当前建立的Channel回写数据。例如:MySQL返回数据时,会触发代理服务器与MySQL之间建立的Channel的读事件,如果使用上面的代码,其效果就是:代理服务器接收到来自MySQL的数据后,将数据发写回给MySQL,而不是用户。
所以,我们在创建ChannelHandler时,需要给它指定一个Channel,让它往我们指定的Channel中写数据。代码如下:
package me.pgthinker.tcp_proxy;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;
/**
* @Project: me.pgthinker.tcp_proxy
* @Author: pgthinker
* @GitHub: https://github.com/ningning0111
* @Date: 2024/9/2 09:36
* @Description: 建立连接时处理 这里的连接可以是用户连接到服务端,也可以是服务端连接到目标程序(MySQL、Redis等)
*/
public class OnConnectHandler extends ChannelInboundHandlerAdapter {
private Channel targetChannel;
public OnConnectHandler(Channel targetChannel) {
this.targetChannel = targetChannel;
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 连接时 接收到的数据 写回去即可
ByteBuf byteBuf = (ByteBuf) msg;
System.out.println("接收到数据:" + byteBuf.toString(CharsetUtil.UTF_8));
// 缓存区 复位
// 复位是为了保证当前通道处理完byteBuf后,数据能够继续保留供下一个Handler处理
// 这里的byteBuf可能来自目标程序到代理服务器这条路径,后续我们还需要将这个byteBuf从代理服务器流入到我们用户这 因此需要保留
byteBuf.retain();
targetChannel.writeAndFlush(byteBuf);
}
}
ProxyServer
现在我们需要实现代理服务器的逻辑。代理服务器需要实现这样一个效果:当用户与我们的代理服务器建立连接后,我们的代理服务器要与代理目标建立起TCP连接。这种建立起连接后执行xxx业务,可以在initChannel
回调函数中实现,当出发initChannel回调时,我们要创建一个Bootstrap与代理目标建立连接,同时要将用户与代理服务器的Channel和代理服务器与代理目标的Channel记录下来。如何获取这两个Channel呢?
对于用户与代理服务器的Channel,我们在initChannel回调执行时,会由回调函数传入:
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// 开启一个Channel 与目标程序建立连接
Channel targetChannel = createTargetChannel(ch);
ch.pipeline().addLast(new OnConnectHandler(targetChannel));
}
传进来的这个ch其实就是用户与代理服务器的Channel。拿到这个后,我们还需要创建一个代理服务器与代理目标的Channel,创建方式自然就是使用Bootstrap建立连接拿到channel。需要注意的是,拿到的这个Channel需要实现这样一个功能:触发读事件时,需要往用户与代理服务器之间的Channel写数据。因为触发读事件,是因为代理目标往我们代理服务器这边写数据了。所以我们就需要添加一个ChannelHandler,这个ChannelHandler的处理对象是ch(用户与代理服务器之间建立起的Channel)。
private Channel createTargetChannel(Channel userChannel) throws InterruptedException {
OnConnectHandler onConnectHandler = new OnConnectHandler(userChannel);
if(bootstrap == null){
synchronized (Bootstrap.class) {
if(bootstrap == null){
bootstrap = new Bootstrap()
.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class);
}
}
}
ChannelFuture future = bootstrap.handler(onConnectHandler).connect(host, port).sync();
return future.channel();
}
同时我们还需要将代理服务器与代理目标建立起的Channel返回。此时返回的这个Channel就是代理服务器与代理目标建立起来的Channel。现在我们已经完成了代理服务器与代理目标的建立和处理,接下来就要继续完成用户与代理服务器之间的处理,处理逻辑其实就是:将用户的数据写入到代理服务器与代理目标的Channel中,也就是刚刚建立连接后返回的Channel。因此,我们需要创建一个新的ChannelHandler,targetChannel就是返回的这个Channel。
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// 开启一个Channel 与目标程序建立连接
Channel targetChannel = createTargetChannel(ch);
ch.pipeline().addLast(new OnConnectHandler(targetChannel));
}
完整的ChannelInitializer
如下:
package me.pgthinker.tcp_proxy;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
/**
* @Project: me.pgthinker.tcp_proxy
* @Author: pgthinker
* @GitHub: https://github.com/ningning0111
* @Date: 2024/9/2 09:47
* @Description: 用户与代理服务器建立连接后触发
*/
public class OnConnectInitializer extends ChannelInitializer<SocketChannel> {
private String host;
private int port;
private Bootstrap bootstrap;
public OnConnectInitializer(String host, int port) {
this.host = host;
this.port = port;
}
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// 开启一个Channel 与目标程序建立连接
Channel targetChannel = createTargetChannel(ch);
ch.pipeline().addLast(new OnConnectHandler(targetChannel));
}
private Channel createTargetChannel(Channel userChannel) throws InterruptedException {
OnConnectHandler onConnectHandler = new OnConnectHandler(userChannel);
if(bootstrap == null){
synchronized (Bootstrap.class) {
if(bootstrap == null){
bootstrap = new Bootstrap()
.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class);
}
}
}
ChannelFuture future = bootstrap.handler(onConnectHandler).connect(host, port).sync();
return future.channel();
}
}
上面的代码中,代理服务器与代理目标建立连接时使用的bootstrap采用的是双检锁校验单例。serverBootstrap代码如下:
package me.pgthinker.tcp_proxy;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LoggingHandler;
/**
* @Project: me.pgthinker.tcp_proxy
* @Author: pgthinker
* @GitHub: https://github.com/ningning0111
* @Date: 2024/9/2 09:35
* @Description:
*/
public class ProxyServer {
private final static int serverPort = 8434;
private final static String targetHost = "localhost";
private final static int targetPort = 3306;
public static void main(String[] args) throws InterruptedException {
NioEventLoopGroup group = new NioEventLoopGroup();
OnConnectInitializer onConnectInitializer = new OnConnectInitializer(targetHost, targetPort);
ChannelFuture future = new ServerBootstrap()
.group(group)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler())
.childHandler(onConnectInitializer)
.option(ChannelOption.SO_BACKLOG, 1024)
.option(ChannelOption.SO_RCVBUF, 16 * 1024)
.bind(serverPort)
.sync();
future.channel().closeFuture().sync();
group.shutdownGracefully();
}
}
测试
MySQL测试
我们的代理服务器端口是8434,代理目标是:localhost:3306,也就是MySQL。当我们以连接MySQL的方式去连接8434端口而不是3306端口时:
连接成功,然后执行一些SQL语句:
正常出结果。为什么控制台是乱码呢?我估计是和MySQL的连接协议有关,连接后传输的数据获取可能都是以字节形式,或许采取了一些加密方式。
Redis测试
将代理目标端口改成6379,也就是Redis的端口,然后尝试连接:
使用Redis后,控制台明显能看到一些能看懂的信息,比方说jedis,Redis官方提高的Java客户端,还有一些Redis的配置信息:
甚至Redis最经典的Ping-Pong测试:
执行一下基本命令:
程序都是正常的。
程序扩展说明
本篇文章实现的代理程序是一个经典的反向代理程序。说到反向代理,自然就要引入正向代理并将它们放在一起比较讨论。
正向代理
客户端为了从目标服务器获取内容,会向一个代理服务器发送请求,让这个代理服务器去代替我们获取数据并将获取到的数据返回给我们客户端。
讲到这,了解一些科学上网的朋友肯定忍不住道:这不就是翻墙吗?没错,正向代理技术一般就是用来突破访问限制的。正常来说,我们在中国大陆是访问不了google.com的,但是我们可以找一个国外的服务器代替我们访问,这个服务器就是代理服务器,代理服务器负责将我们的请求转发给目标服务器(google.com),目标服务器进行处理并把响应结果返回给代理服务器时,代理服务器再将响应包转发给我们。对目标服务器来讲,它并不知道具体是谁访问了我,只知道请求来自我们的代理服务器。
正向代理代理的是客户端,让目标服务器不知道具体访问它的客户端是谁。
反向代理
代理服务器接收各种网络请求,然后将请求转发给内部网络的服务器,并将从内部网络服务器处理后得到的结果返回给发起该请求的客户端。
例如:我们有时需要隐藏安装了MySQL、Redis的服务器,对外只想暴露一个服务器,此时这个服务器就负责将来自MySQL的连接转发到内部的、具体的MySQL服务器,来自Redis的连接,就转发到内部Redis的服务器。客户端只知晓是向一台代理服务器发起MySQL连接或Redis连接,并不知道MySQL或Redis所在的具体服务器。对客户端来讲,并不知道我的连接请求最终发到了哪台服务器上,只知道发送到了代理服务器上。
反向代理代理的是服务端,让客户端不知道它请求的数据最终发送到了哪里,从而隐藏了内部服务器。
用途
正向代理
-
突破访问限制:通过代理服务器,可以突破自身IP访问限制,访问国外网站,教育网等。
-
提高访问速度:通常代理服务器都设置一个较大的硬盘缓冲区,会将部分请求的响应保存到缓冲区中,当其他用户再访问相同的信息时, 则直接由缓冲区中取出信息,传给用户,以提高访问速度。
-
隐藏客户端真实IP:上网者也可以通过这种方法隐藏自己的IP,免受攻击。
反向代理
-
隐藏服务器真实IP:使用反向代理,可以对客户端隐藏服务器的IP地址。
-
负载均衡:反向代理服务器可以做负载均衡,根据所有真实服务器的负载情况,将客户端请求分发到不同的真实服务器上。
-
提高访问速度:反向代理服务器可以对于静态内容及短时间内有大量访问请求的动态内容提供缓存服务,提高访问速度。
-
提供安全保障:反向代理服务器可以作为应用层防火墙,为网站提供对基于Web的攻击行为(例如DoS/DDoS)的防护,更容易排查恶意软件等。还可以为后端服务器统一提供加密和SSL加速(如SSL终端代理),提供HTTP访问认证等。