本项目作为计网课程的大作业,目的是用Java语言实现frp(Fast Reverse Proxy)即快速反向代理,用于内网穿透服务。
由于是课程大作业,而且我也是第一次了解到内网穿透的原理和实践,故本文档会写的比较啰嗦,记录很多我学习到的东西。 实现的过程中参考了一些github上的源码实现,与deepseek-V3、deepseek-R1、doubao-1.6进行了多轮对话。
frp官方实现:https://github.com/fatedier/frp
本项目使用maven构建。
frp本质是反向代理工具,通过公网服务端-frps和内网客户端frpc配合,将内网服务暴露到公网,核心功能包括:
- TCP/UDP端口映射:将公网端口请求转发到内网服务;
- HTTP/HTTPS代理:基于域名路由到内网不同HTTP服务;
- 控制连接:客户端与服务端建立长连接,传递代理配置和控制指令;
- 数据转发:公网请求通过服务端转发到客户端,再到内网服务。
- 采用经典的C/S架构:进行公网中转的服务端和内网代理的客户端
- 网络框架:使用Netty(高性能异步NIO框架),处理高并发连接和数据转发,比Java原生NIO易用,内置编解码器与连接管理;
- 协议设置:自定义二进制协议(参考frp官方协议),用于控制指令(入注册代理、心跳)和数据转发;
- 配置解析:支持JSON/INI配置文件(Jackson解析JSON,ini4j解析INI);
- 日志:SLF4J+Logback日志;
┌───────────────┐ ┌─────────────────────────────────────────┐ ┌─────────────────────────┐
│ │ │ 服务端 (公网服务器) │ │ 客户端 (内网设备) │
│ │ │ │ │ │
│ 公网用户 │ │ ┌─────────────┐ ┌─────────────┐ │ │ ┌─────────────────┐ │
│ (浏览器/App) │◄────────┼─►│ 远程端口监听 │◄────►│ 远程代理处理器 │ │ │ │ │ │
│ │ │ │ (如: 8081) │ │ (数据转发) │ │ │ │ │ │
└───────────────┘ │ └─────────────┘ └───────┬─────┘ │ │ │ │ │
│ │ │ │ │ │ │
│ ┌─────────────┐ ┌───────▼─────┐ │ │ │ 控制连接管理 │ │
│ │ 控制端口监听 │◄────►│ 控制连接处理器 │◄──┼─────────┼─►│ (ClientControl) │ │
│ │ (7000) │ │ (注册/心跳) │ │ │ │ │ │
│ └─────────────┘ └───────┬─────┘ │ │ │ │ │
│ │ │ │ │ │ │
│ ┌─────────────┐ ┌───────▼─────┐ │ │ └────────┬────────┘ │
│ │ │ │ │ │ │ │ │
│ │ 代理管理器 │◄────►│ 连接状态管理 │ │ │ ┌────────▼────────┐ │
│ │ (ProxyManager) │ (心跳/超时) │ │ │ │ │ │
│ │ │ │ │ │ │ │ 内网代理处理 │ │
│ └─────────────┘ └─────────────┘ │ │ │ (LocalProxy) │ │
│ │ │ │ │ │
└─────────────────────────────────────────┘ │ └────────┬────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ │ │
│ │ 内网服务 │ │
│ │ (如: Nginx) │ │
│ │ │ │
│ └─────────────────┘ │
│ │
└─────────────────────────┘
项目根目录
frp-java/ # 项目根目录
├── pom.xml # 父POM,声明公共依赖(如Netty、Jackson)
├── frp-common/ # 公共模块(客户端/服务端共用代码)
├── frp-server/ # 服务端模块(frps,公网中转节点)
└── frp-client/ # 客户端模块(frpc,内网代理节点)
frp-common/
├── pom.xml # 公共模块依赖(如Jackson、Netty)
└── src/main/java/com/example/frp/common/
├── protocol/ # 协议定义(帧类型、控制指令、实体类)
│ ├── FrameType.java # 帧类型枚举(CONTROL/DATA)
│ ├── FrpFrame.java # 协议帧实体类(封装帧信息)
│ ├── ControlType.java # 控制指令枚举(REGISTER/HEARTBEAT等)
│ ├── RegisterRequest.java # 注册请求实体类(客户端→服务端)
│ └── RegisterResponse.java # 注册响应实体类(服务端→客户端)
│
├── codec/ # 编解码器(Netty编解码逻辑)
│ ├── FrpFrameDecoder.java # 帧解码器(字节流→FrpFrame对象)
│ ├── FrpFrameEncoder.java # 帧编码器(FrpFrame对象→字节流)
│ └── ControlFrameCodec.java # 控制帧JSON编解码器(对象→JSON字节数组)
│
└── util/ # 通用工具类
├── Constants.java # 常量定义(如默认端口、心跳间隔)
└── LogUtils.java # 日志工具类(简化日志调用)
frpc 内网代理节点
frp-client/
├── pom.xml # 客户端依赖(引入frp-common、Netty等)
├── src/main/java/com/example/frp/client/
│ ├── boot/ # 客户端启动入口
│ │ └── FrpClient.java # 客户端启动类(连接服务端、加载代理配置)
│ │
│ ├── config/ # 客户端配置(代理规则、服务端地址等)
│ │ ├── ClientConfig.java # 客户端配置实体类(服务端地址、代理列表)
│ │ ├── ProxyConfig.java # 单个代理配置(localIp、localPort、remotePort等)
│ │ └── ConfigLoader.java # 配置加载器(从文件读取代理规则)
│ │
│ └── handler/ # 网络事件处理器(Netty Handler)
│ ├── ClientControlHandler.java # 控制连接处理器(注册/心跳/断线重连)
│ └── LocalProxyHandler.java # 内网代理处理器(转发请求到内网服务)
│
└── src/main/resources/ # 客户端配置文件
└── frpc.properties # 客户端配置(示例:服务端地址、代理规则列表)
frps 公网中转节点
frp-server/
├── pom.xml # 服务端依赖(引入frp-common、Netty等)
├── src/main/java/com/example/frp/server/
│ ├── boot/ # 服务端启动入口
│ │ └── FrpServer.java # 服务端启动类(初始化Netty、绑定端口)
│ │
│ ├── config/ # 服务端配置(端口、Token等)
│ │ ├── ServerConfig.java # 配置实体类(控制端口、认证Token等)
│ │ └── ConfigLoader.java # 配置加载器(从文件/命令行读取配置)
│ │
│ ├── handler/ # 网络事件处理器(Netty Handler)
│ │ ├── ServerControlHandler.java # 控制连接处理器(注册/心跳/注销)
│ │ └── RemoteProxyHandler.java # 远程代理处理器(公网请求转发)
│ │
│ └── manager/ # 代理管理(维护代理生命周期和映射关系)
│ ├── ProxyManager.java # 代理管理器(创建/销毁代理、端口映射)
│ ├── Proxy.java # 代理实体类(存储代理配置和状态)
│ └── ProxyStatus.java # 代理状态枚举(INIT/ACTIVE/INACTIVE)
│
└── src/main/resources/ # 服务端配置文件
└── frps.properties # 服务端配置(示例:控制端口7000、Token=abc123)
协议的最小单位为帧(FrpFrame),所有数据(包括控制指令和业务数据)均通过帧传输。帧结构基于“长度前缀+类型+内容”,具体格式:
┌──────────────┬──────────┬──────────┬──────────────┬────────────────┬─────────────────┐
│ 长度字段 │ 帧类型 │ 保留字段 │ proxyId长度 │ proxyId内容 │ 有效载荷(payload) │
│ 4字节(int) │ 1字节 │ 1字节 │ 1字节(无符号)│ N字节(UTF-8) │ M字节 │
└──────────────┴──────────┴──────────┴──────────────┴────────────────┴─────────────────┘
字段解释:
- 长度字段:除自身外整个帧的总字节数,即3+M+N;
- 帧类型:区分是控制帧还是数据帧,取值来自FrameType枚举
- 保留字段:预留未来扩展(如新增加密标识、压缩标识等)
- proxyId长度:表明proxyId内容长度,最大值255
- proxyId:代理的唯一标识,由客户端在注册时指定,服务端全局唯一,标识当前帧属于哪个代理规则;
- 有效载荷:帧的实际数据内容,格式与帧类型相关
- 控制帧CONTROL:payLoad为JSON数组,存储控制指令对象,如注册请求RegisterRequest、心跳请求HeartbeatRequest;
- 数据帧DATA:payload为原始字节数组,存储公网用户请求或内网服务响应(如HTTP请求报文、MySQL协议数据)