Repository: Pluto-Whong/natcross2 Branch: master Commit: 505da5d623c9 Files: 94 Total size: 238.6 KB Directory structure: gitextract_mhw2jg3q/ ├── .gitignore ├── LICENSE ├── README.md ├── VERSION.md ├── pom.xml └── src/ ├── main/ │ ├── java/ │ │ └── person/ │ │ └── pluto/ │ │ └── natcross2/ │ │ ├── ClientApp.java │ │ ├── CommonConstants.java │ │ ├── ServerApp.java │ │ ├── api/ │ │ │ ├── IBelongControl.java │ │ │ ├── IHttpRouting.java │ │ │ ├── passway/ │ │ │ │ ├── SecretPassway.java │ │ │ │ └── SimplePassway.java │ │ │ ├── secret/ │ │ │ │ ├── AESSecret.java │ │ │ │ └── ISecret.java │ │ │ └── socketpart/ │ │ │ ├── AbsSocketPart.java │ │ │ ├── HttpRouteSocketPart.java │ │ │ ├── SecretSocketPart.java │ │ │ └── SimpleSocketPart.java │ │ ├── channel/ │ │ │ ├── Channel.java │ │ │ ├── InteractiveChannel.java │ │ │ ├── JsonChannel.java │ │ │ ├── LengthChannel.java │ │ │ ├── SecretInteractiveChannel.java │ │ │ ├── SocketChannel.java │ │ │ └── StringChannel.java │ │ ├── clientside/ │ │ │ ├── ClientControlThread.java │ │ │ ├── adapter/ │ │ │ │ ├── IClientAdapter.java │ │ │ │ └── InteractiveSimpleClientAdapter.java │ │ │ ├── config/ │ │ │ │ ├── AllSecretInteractiveClientConfig.java │ │ │ │ ├── HttpRouteClientConfig.java │ │ │ │ ├── IClientConfig.java │ │ │ │ ├── InteractiveClientConfig.java │ │ │ │ └── SecretInteractiveClientConfig.java │ │ │ ├── handler/ │ │ │ │ ├── CommonReplyHandler.java │ │ │ │ ├── IClientHandler.java │ │ │ │ ├── ServerHeartHandler.java │ │ │ │ └── ServerWaitClientHandler.java │ │ │ └── heart/ │ │ │ ├── ClientHeartThread.java │ │ │ └── IClientHeartThread.java │ │ ├── common/ │ │ │ ├── CommonFormat.java │ │ │ └── Optional.java │ │ ├── executor/ │ │ │ ├── IExecutor.java │ │ │ ├── NatcrossExecutor.java │ │ │ └── SimpleExecutor.java │ │ ├── model/ │ │ │ ├── HttpRoute.java │ │ │ ├── InteractiveModel.java │ │ │ ├── NatcrossResultModel.java │ │ │ ├── SecretInteractiveModel.java │ │ │ ├── enumeration/ │ │ │ │ ├── InteractiveTypeEnum.java │ │ │ │ └── NatcrossResultEnum.java │ │ │ └── interactive/ │ │ │ ├── ClientConnectModel.java │ │ │ ├── ClientControlModel.java │ │ │ └── ServerWaitModel.java │ │ ├── nio/ │ │ │ ├── INioProcessor.java │ │ │ ├── NioHallows.java │ │ │ └── ProcesserHolder.java │ │ ├── serverside/ │ │ │ ├── client/ │ │ │ │ ├── ClientServiceThread.java │ │ │ │ ├── adapter/ │ │ │ │ │ ├── DefaultReadAheadPassValueAdapter.java │ │ │ │ │ ├── IClientServiceAdapter.java │ │ │ │ │ ├── PassValueNextEnum.java │ │ │ │ │ └── ReadAheadPassValueAdapter.java │ │ │ │ ├── config/ │ │ │ │ │ ├── IClientServiceConfig.java │ │ │ │ │ ├── SecretSimpleClientServiceConfig.java │ │ │ │ │ └── SimpleClientServiceConfig.java │ │ │ │ ├── handler/ │ │ │ │ │ ├── DefaultInteractiveProcessHandler.java │ │ │ │ │ ├── IPassValueHandler.java │ │ │ │ │ └── InteractiveProcessHandler.java │ │ │ │ └── process/ │ │ │ │ ├── ClientConnectProcess.java │ │ │ │ ├── ClientControlProcess.java │ │ │ │ └── IProcess.java │ │ │ └── listen/ │ │ │ ├── IServerListen.java │ │ │ ├── ListenServerControl.java │ │ │ ├── ServerListenThread.java │ │ │ ├── clear/ │ │ │ │ ├── ClearInvalidSocketPartThread.java │ │ │ │ └── IClearInvalidSocketPartThread.java │ │ │ ├── config/ │ │ │ │ ├── AllSecretSimpleListenServerConfig.java │ │ │ │ ├── IListenServerConfig.java │ │ │ │ ├── MultControlListenServerConfig.java │ │ │ │ ├── SecretSimpleListenServerConfig.java │ │ │ │ └── SimpleListenServerConfig.java │ │ │ ├── control/ │ │ │ │ ├── ControlSocket.java │ │ │ │ ├── IControlSocket.java │ │ │ │ └── MultiControlSocket.java │ │ │ ├── recv/ │ │ │ │ ├── ClientHeartHandler.java │ │ │ │ ├── CommonReplyHandler.java │ │ │ │ └── IRecvHandler.java │ │ │ └── serversocket/ │ │ │ └── ICreateServerSocket.java │ │ └── utils/ │ │ ├── AESUtil.java │ │ ├── Assert.java │ │ ├── CountWaitLatch.java │ │ ├── MD5Signature.java │ │ └── Tools.java │ └── resources/ │ └── logback.xml └── test/ └── java/ └── person/ └── pluto/ └── TestMain.java ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ .* !/.gitignore target/ *.iml ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [2020] [plutoppppp] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ # natcross2 内网穿透工具 ********************* **Q群:438793541** **群密码:natcross2** ## natcross是做什么的? - 需要自己提供硬件支持、部署的内网穿透工具 - 提供TCP协议类型的内网穿透服务,包括但不限于http(s)、数据库连接、ssh等协议 - 支持https协议转http协议与应用交互方式 - 支持无加密、控制端口加密交互、数据加密交互方式 - 主要服务场景,需要将内网的应用开放到公网,如微信小程序开发调试、支付回调等 - 支持HTTP根据host进行反向代理;目标依然是内网应用,只是可以根据HTTP协议header中的host字段区分选择目标应用(注意:只是有人提出来了HTTP监听统一端口并用域名访问的问题,并且有做的价值才补充的该功能;没做负载功能,这个是内网穿透,不是nginx,更不建议直接用在生产上,需要负载的可以自己去实现) ## 打包使用 服务端-ServerApp打包: ```shell # 修改ServerApp.java中serviceIp为公网服务器的IP mvn clean compile package -PserverApp ``` 客户端-ClientApp打包: ```shell mvn clean compile package -PclientApp ``` ## 安装 1. 需要一台可在公网访问的机器,并将控制端口(默认 servicePort 10010)、数据端口(如tomcat,开放公网 listenPort 8081端口,外网连接时要对应数据端口8081)开放出来 2. 将ServerApp.jar包放在公网可访问的机器并运行,打包前需要修改serviceIp为公网服务器的IP 3. 将clientApp为入口进行运行,destIp:destPort为要开放的内网应用,如本地的tomcat(127.0.0.1:8080) 4. 用浏览器访问serviceIp:listenPort便可以访问到内网的tomcat ## 参数解释 CommonConstants: |字段|解释| |:-:|:-| |serviceIp|公网服务器的IP| |servicePort|服务端的控制端口,主要用来与客户端进行指令交互| |listenPort|服务端的监听端口,也就是部署完成后,在外网访问 serviceIp:listenPort 的方式对内网应用进行访问| |destIp|要开放到公网的内网应用所在机器的IP| |destPort|要开放到公网的内网应用的端口| |aesKey|交互密钥key,保证数据的秘密性,可以查看 SecretInteractiveModel.java 中的fullMessage和checkAutograph中确认密钥的使用方式。
如果你使用了secretAll方式进行部署,这个key还是数据加密的key,可以在 SecretPassway.java 中确认密钥的使用方式
注意使用长度,windows版本的java只能用最大128长度的密钥| |tokenKey|交互签名key,签名同aesKey| ServerApp: |字段|解释| |:-:|:-| |sslKeyStorePath|ssl证书的路径,默认方法只支持pkcs12的证书格式,使用这个证书可以做到https协议转http协议| |sslKeyStorePassword|证书密码| |createServerSocket|创建socket的方式,主要针对普通socket和sslSocket的方式进行封装,结合ssl证书使用| ClientApp: |字段|解释| |:-:|:-| |#secretHttpRoute:routes|http方式,根据不同host路由选择不同的目标应用| ## 内网穿透思路 因NAT网内CLIENT可以正常连接到SERVER端,并且能够保持一段时间的长连接,则由CLIENT发起连接,建立SOCKET对,在SERVER收到外部请求时,可以通过已经建立好的SOCKET将数据传输给CLIENT,CLIENT使用相同的方式将数据发送给指定的网络程序,网络程序回发数据后则按原路返回给请求方。 ![时序图](./doc/sequence.svg) ## 相关技术 |技术|体现点| |:-:|:-| |Socket|核心技术概念| |NIO|nio.NioHallows,使用Selector作为注册监听器(多路复用),有事件唤起后会创建子线程进行异步处理| |TCP粘包、拆包的解决|channel.LengthChannel,此处用的是一个大端序列的长度加消息内容的方式| |线程管控|clientside.ClientControlThread、serverside.client.ClientServiceThread、serverside.listen.ServerListenThread作为独立管控及子线程异步处理的主要体现,亦可通过executor包下相关类进行追踪| |HTTP路由|api.socketpart.HttpRouteSocketPart#routeHost,一个简单的对host头部字段的处理应用| |消息加密|对AES、MD5联合的使用示例,channel.SecretInteractiveChannel、model.SecretInteractiveModel,对实际消息进行加密,增加辅助字段保证消息的真实性、准确性和完整性| |计数门闩|utils.CountWaitLatch,类CountDownLatch,增加了countUp,不只受初始化的值决定,可以增加、减少,主要用来解决nio.NioHallows唤醒后批量channel注册的问题(等下,怎么感觉可以用读写锁来解决呢?)| |线程池|虽然用了线程池,但默认的是Executors.newCachedThreadPool()来生成的,具体的还是需要根据机器来自定义线程池,主要还是对子线程的管控体现吧| ================================================ FILE: VERSION.md ================================================ # VERSION 2.3 1. 支持多客户端连接同一监听端口,从服务端进行负载分发(场景特殊,主要是面对客户端处在非稳定内网,使用多客户端进行多活保证) # VERSION 2.2 1. socket使用channel方式,交互使用DMA策略 2. 监听端口、控制端口获得新连接后使用子线程方式进行异步操作 3. 优化了http路由算法,提高了路由选择时的速度 4. passWay使用了nio策略,统一交付NioHallows处理 5. ServerListenThread(当前长连接存在问题,故而目前使用bio形式获取)、ClientServiceThread使用了nio策略,统一交付NioHallows处理 6. 增加了CountWaitLatch(对标CountDownLatch,但增加的countUp,不再依赖初始化数量),用于NioHallows的等待注册过程 7. 修改pom.xml,去除无用的包,改为dependencyManagement方式进行版本管理,避免和引入项目发生版本冲突 # VERSION 2.1 支持http路由了,可以像nginx那样监听同一个端口,根据访问域名路由目标应用 注意http标准,需要头部有Host,并且注意换行严格要符合http的标准,且如果是长连接路由目标只会在第一次连接时选择目标 虽然上面有很多要求,但是一般使用标准的浏览器都不会出现问题的 ================================================ FILE: pom.xml ================================================ 4.0.0 person.pluto natcross2 2.3.2 natcross2 natcross 2 1.8 1.8 1.8 UTF-8 UTF-8 3.4.1 org.projectlombok lombok 1.18.26 org.slf4j slf4j-api 1.7.36 ch.qos.logback logback-classic 1.2.13 org.apache.commons commons-lang3 3.18.0 com.alibaba fastjson 2.0.9 org.projectlombok lombok provided true org.slf4j slf4j-api ch.qos.logback logback-classic org.apache.commons commons-lang3 com.alibaba fastjson org.apache.maven.plugins maven-compiler-plugin 3.9.0 1.8 1.8 UTF-8 compile true install false src/main/resources ** serverApp ServerApp org.apache.maven.plugins maven-shade-plugin ${maven.shade.version} package shade person.pluto.natcross2.ServerApp clientApp ClientApp org.apache.maven.plugins maven-shade-plugin ${maven.shade.version} package shade person.pluto.natcross2.ClientApp nexus-aliyun Nexus aliyun http://maven.aliyun.com/nexus/content/groups/public ================================================ FILE: src/main/java/person/pluto/natcross2/ClientApp.java ================================================ package person.pluto.natcross2; import person.pluto.natcross2.CommonConstants.ListenDest; import person.pluto.natcross2.clientside.ClientControlThread; import person.pluto.natcross2.clientside.config.AllSecretInteractiveClientConfig; import person.pluto.natcross2.clientside.config.HttpRouteClientConfig; import person.pluto.natcross2.clientside.config.InteractiveClientConfig; import person.pluto.natcross2.clientside.config.SecretInteractiveClientConfig; import person.pluto.natcross2.model.HttpRoute; /** *

* 客户端,放在内网侧 *

* * @author Pluto * @since 2020-01-09 16:26:44 */ public class ClientApp { public static void main(String[] args) throws Exception { // simple(); secret(); // secretAll(); // secretHttpRoute(); } /** * http路由 *

* 默认使用交互加密、数据不加密的策略 */ public static void secretHttpRoute() throws Exception { HttpRoute[] routes = new HttpRoute[]{ // HttpRoute.of("localhost", "127.0.0.1", 8080), // HttpRoute.of(true, "127.0.0.1", "127.0.0.1", 8080), // }; for (ListenDest model : CommonConstants.listenDestArray) { SecretInteractiveClientConfig baseConfig = new SecretInteractiveClientConfig(); // 设置服务端IP和端口 baseConfig.setClientServiceIp(CommonConstants.serviceIp); baseConfig.setClientServicePort(CommonConstants.servicePort); // 设置对外暴露的端口,该端口的启动在服务端,这里只是表明要跟服务端的那个监听服务对接 baseConfig.setListenServerPort(model.listenPort); // 设置交互密钥和签名key baseConfig.setBaseAesKey(CommonConstants.aesKey); baseConfig.setTokenKey(CommonConstants.tokenKey); HttpRouteClientConfig config = new HttpRouteClientConfig(baseConfig); config.addRoute(routes); new ClientControlThread(config).createControl(); } } /** * 交互、隧道都进行加密 */ public static void secretAll() throws Exception { for (ListenDest model : CommonConstants.listenDestArray) { AllSecretInteractiveClientConfig config = new AllSecretInteractiveClientConfig(); // 设置服务端IP和端口 config.setClientServiceIp(CommonConstants.serviceIp); config.setClientServicePort(CommonConstants.servicePort); // 设置对外暴露的端口,该端口的启动在服务端,这里只是表明要跟服务端的那个监听服务对接 config.setListenServerPort(model.listenPort); // 设置要暴露的目标IP和端口 config.setDestIp(model.destIp); config.setDestPort(model.destPort); // 设置交互密钥和签名key config.setBaseAesKey(CommonConstants.aesKey); config.setTokenKey(CommonConstants.tokenKey); // 设置隧道交互密钥 config.setBasePasswayKey(CommonConstants.aesKey); new ClientControlThread(config).createControl(); } } /** * 交互加密,即交互验证 */ public static void secret() throws Exception { for (ListenDest model : CommonConstants.listenDestArray) { SecretInteractiveClientConfig config = new SecretInteractiveClientConfig(); // 设置服务端IP和端口 config.setClientServiceIp(CommonConstants.serviceIp); config.setClientServicePort(CommonConstants.servicePort); // 设置对外暴露的端口,该端口的启动在服务端,这里只是表明要跟服务端的那个监听服务对接 config.setListenServerPort(model.listenPort); // 设置要暴露的目标IP和端口 config.setDestIp(model.destIp); config.setDestPort(model.destPort); // 设置交互密钥和签名key config.setBaseAesKey(CommonConstants.aesKey); config.setTokenKey(CommonConstants.tokenKey); new ClientControlThread(config).createControl(); } } /** * 无加密、无验证 */ public static void simple() throws Exception { for (ListenDest model : CommonConstants.listenDestArray) { InteractiveClientConfig config = new InteractiveClientConfig(); // 设置服务端IP和端口 config.setClientServiceIp(CommonConstants.serviceIp); config.setClientServicePort(CommonConstants.servicePort); // 设置对外暴露的端口,该端口的启动在服务端,这里只是表明要跟服务端的那个监听服务对接 config.setListenServerPort(model.listenPort); // 设置要暴露的目标IP和端口 config.setDestIp(model.destIp); config.setDestPort(model.destPort); new ClientControlThread(config).createControl(); } } } ================================================ FILE: src/main/java/person/pluto/natcross2/CommonConstants.java ================================================ package person.pluto.natcross2; import lombok.AccessLevel; import lombok.NoArgsConstructor; /** *

* 公共参数 *

* * @author Pluto * @since 2020-04-10 12:29:01 */ @NoArgsConstructor(access = AccessLevel.PRIVATE) public final class CommonConstants { // 服务端地址,支持IP或域名,这个根据服务端放的网络位置进行设置 public static final String serviceIp = "127.0.0.1"; // 客户端服务的端口 public static final int servicePort = 10010; // 映射对 public static ListenDest[] listenDestArray = new ListenDest[]{ // ListenDest.of(8081, "127.0.0.1", 8080), // }; // 交互密钥 AES public static final String aesKey = "8AUWlb+IWD+Fhbs0xnXCCg=="; // 交互签名key public static final String tokenKey = "tokenKey"; /** *

* 监听、映射对 *

*/ static class ListenDest { public static ListenDest of(int listenPort, String destIp, int destPort) { ListenDest model = new ListenDest(); model.listenPort = listenPort; model.destIp = destIp; model.destPort = destPort; return model; } // 服务端监听的端口,外网访问服务端IP:listengPort即可进行穿透 public int listenPort; // 穿透的目标,即要暴露在外网的内网IP public String destIp; // 要暴露的内网端口 public int destPort; } } ================================================ FILE: src/main/java/person/pluto/natcross2/ServerApp.java ================================================ package person.pluto.natcross2; import org.apache.commons.lang3.StringUtils; import person.pluto.natcross2.CommonConstants.ListenDest; import person.pluto.natcross2.serverside.client.ClientServiceThread; import person.pluto.natcross2.serverside.client.config.SecretSimpleClientServiceConfig; import person.pluto.natcross2.serverside.client.config.SimpleClientServiceConfig; import person.pluto.natcross2.serverside.listen.ListenServerControl; import person.pluto.natcross2.serverside.listen.config.AllSecretSimpleListenServerConfig; import person.pluto.natcross2.serverside.listen.config.MultControlListenServerConfig; import person.pluto.natcross2.serverside.listen.config.SecretSimpleListenServerConfig; import person.pluto.natcross2.serverside.listen.config.SimpleListenServerConfig; import person.pluto.natcross2.serverside.listen.serversocket.ICreateServerSocket; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLServerSocketFactory; import java.nio.file.Files; import java.nio.file.Paths; import java.security.KeyStore; /** *

* 服务端,放在外网侧 *

* * @author Pluto * @since 2020-01-09 16:27:03 */ public class ServerApp { // 你的p12格式的证书路径 private static final String sslKeyStorePath = System.getenv("sslKeyStorePath"); // 你的证书密码 private static final String sslKeyStorePassword = System.getenv("sslKeyStorePassword"); public static ICreateServerSocket createServerSocket; public static void main(String[] args) throws Exception { // 如果需要HTTPS协议的支持,则填写sslKeyStorePath、sslKeyStorePassword或在环境变量中定义 if (StringUtils.isNoneBlank(sslKeyStorePath, sslKeyStorePassword)) { createServerSocket = listenPort -> { KeyStore keystore = KeyStore.getInstance("PKCS12"); keystore.load(Files.newInputStream(Paths.get(sslKeyStorePath)), sslKeyStorePassword.toCharArray()); KeyManagerFactory keyFactory = KeyManagerFactory.getInstance("sunx509"); keyFactory.init(keystore, sslKeyStorePassword.toCharArray()); SSLContext ctx = SSLContext.getInstance("TLSv1.2"); ctx.init(keyFactory.getKeyManagers(), null, null); SSLServerSocketFactory serverSocketFactory = ctx.getServerSocketFactory(); return serverSocketFactory.createServerSocket(listenPort); }; } // simple(); secret(); // secretAll(); // multControlSecret(); } /** * 多客户端,控制通道加密 */ public static void multControlSecret() throws Exception { // 设置并启动客户端服务线程 SecretSimpleClientServiceConfig config = new SecretSimpleClientServiceConfig(CommonConstants.servicePort); // 设置交互aes密钥和签名密钥 config.setBaseAesKey(CommonConstants.aesKey); config.setTokenKey(CommonConstants.tokenKey); new ClientServiceThread(config).start(); for (ListenDest model : CommonConstants.listenDestArray) { // 设置并启动一个穿透端口 SecretSimpleListenServerConfig baseListengConfig = new SecretSimpleListenServerConfig(model.listenPort); // 设置交互aes密钥和签名密钥,这里使用和客户端服务相同的密钥,可以根据需要设置不同的 baseListengConfig.setBaseAesKey(CommonConstants.aesKey); baseListengConfig.setTokenKey(CommonConstants.tokenKey); baseListengConfig.setCreateServerSocket(createServerSocket); MultControlListenServerConfig listengConfig = new MultControlListenServerConfig(baseListengConfig); ListenServerControl.createNewListenServer(listengConfig); } } /** * 交互、隧道都进行加密 */ public static void secretAll() throws Exception { // 设置并启动客户端服务线程 SecretSimpleClientServiceConfig config = new SecretSimpleClientServiceConfig(CommonConstants.servicePort); // 设置交互aes密钥和签名密钥 config.setBaseAesKey(CommonConstants.aesKey); config.setTokenKey(CommonConstants.tokenKey); new ClientServiceThread(config).start(); for (ListenDest model : CommonConstants.listenDestArray) { AllSecretSimpleListenServerConfig listengConfig = new AllSecretSimpleListenServerConfig(model.listenPort); // 设置交互aes密钥和签名密钥,这里使用和客户端服务相同的密钥,可以根据需要设置不同的 listengConfig.setBaseAesKey(CommonConstants.aesKey); listengConfig.setTokenKey(CommonConstants.tokenKey); // 设置隧道密钥 listengConfig.setBasePasswayKey(CommonConstants.aesKey); listengConfig.setCreateServerSocket(createServerSocket); ListenServerControl.createNewListenServer(listengConfig); } } /** * 交互加密,即交互验证 */ public static void secret() throws Exception { // 设置并启动客户端服务线程 SecretSimpleClientServiceConfig config = new SecretSimpleClientServiceConfig(CommonConstants.servicePort); // 设置交互aes密钥和签名密钥 config.setBaseAesKey(CommonConstants.aesKey); config.setTokenKey(CommonConstants.tokenKey); new ClientServiceThread(config).start(); for (ListenDest model : CommonConstants.listenDestArray) { // 设置并启动一个穿透端口 SecretSimpleListenServerConfig listengConfig = new SecretSimpleListenServerConfig(model.listenPort); // 设置交互aes密钥和签名密钥,这里使用和客户端服务相同的密钥,可以根据需要设置不同的 listengConfig.setBaseAesKey(CommonConstants.aesKey); listengConfig.setTokenKey(CommonConstants.tokenKey); listengConfig.setCreateServerSocket(createServerSocket); ListenServerControl.createNewListenServer(listengConfig); } } /** * 无加密、无验证 */ public static void simple() throws Exception { // 设置并启动客户端服务线程 SimpleClientServiceConfig config = new SimpleClientServiceConfig(CommonConstants.servicePort); new ClientServiceThread(config).start(); for (ListenDest model : CommonConstants.listenDestArray) { // 设置并启动一个穿透端口 SimpleListenServerConfig listengConfig = new SimpleListenServerConfig(model.listenPort); listengConfig.setCreateServerSocket(createServerSocket); ListenServerControl.createNewListenServer(listengConfig); } } } ================================================ FILE: src/main/java/person/pluto/natcross2/api/IBelongControl.java ================================================ package person.pluto.natcross2.api; /** *

* 通知上次停止的统一类,为适应不同的类型进行不同的函数封装 *

* * @author Pluto * @since 2019-07-12 08:39:25 */ public interface IBelongControl { /** * 无标记通知 */ default void noticeStop() { // do nothing } /** * 有标记通知 * * @param socketPartKey * @return */ default boolean stopSocketPart(String socketPartKey) { return true; } } ================================================ FILE: src/main/java/person/pluto/natcross2/api/IHttpRouting.java ================================================ package person.pluto.natcross2.api; import person.pluto.natcross2.model.HttpRoute; /** *

* http 路由器 *

* * @author Pluto * @since 2021-04-26 08:54:44 */ public interface IHttpRouting { /** * 获取有效路由 * * @param host * @return */ HttpRoute pickEffectiveRoute(String host); } ================================================ FILE: src/main/java/person/pluto/natcross2/api/passway/SecretPassway.java ================================================ package person.pluto.natcross2.api.passway; import lombok.Data; import lombok.extern.slf4j.Slf4j; import person.pluto.natcross2.api.IBelongControl; import person.pluto.natcross2.api.secret.ISecret; import person.pluto.natcross2.channel.LengthChannel; import person.pluto.natcross2.executor.NatcrossExecutor; import person.pluto.natcross2.nio.NioHallows; import person.pluto.natcross2.utils.Tools; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; import java.util.Objects; /** *

* 加密型隧道,一侧加密,一侧原样输入、输出 *

* * @author Pluto * @since 2020-01-08 15:55:29 */ @Data @Slf4j public class SecretPassway implements Runnable { /** * 通道模式 */ public enum Mode { /** * 从无加密接受到加密输出 */ noToSecret, /** * 从加密接受到无加密输出 */ secretToNo } private boolean alive = false; private ISecret secret; private Mode mode; private IBelongControl belongControl; private int streamCacheSize = 8192; private Socket recvSocket; private Socket sendSocket; @Override public void run() { try { if (Mode.noToSecret.equals(this.mode)) { noToSecret(); } else { secretToNo(); } } catch (Exception e) { // } log.debug("one InputToOutputThread closed"); // 传输完成后退出 this.cancel(); } /** * 从加密侧输入,解密后输出到无加密侧 * * @throws Exception */ private void secretToNo() throws Exception { LengthChannel recvChannel = new LengthChannel(this.recvSocket); OutputStream outputStream = this.sendSocket.getOutputStream(); SocketChannel outputChannel = this.sendSocket.getChannel(); ISecret secret = this.secret; byte[] read; byte[] decrypt; while (this.alive) { read = recvChannel.read(); if (read == null) { break; } decrypt = secret.decrypt(read); if (Objects.isNull(outputChannel)) { outputStream.write(decrypt); outputStream.flush(); } else { Tools.channelWrite(outputChannel, ByteBuffer.wrap(decrypt)); } } } /** * 从无加密侧,经过加密后输出到加密侧 * * @throws Exception */ private void noToSecret() throws Exception { InputStream inputStream = this.recvSocket.getInputStream(); LengthChannel sendChannel = new LengthChannel(this.sendSocket); // 字段赋值局部变量,入栈 boolean alive = this.alive; ISecret secret = this.secret; int len; byte[] arrayTemp = new byte[this.streamCacheSize]; byte[] encrypt; while (alive && (len = inputStream.read(arrayTemp)) > 0) { encrypt = secret.encrypt(arrayTemp, 0, len); sendChannel.writeAndFlush(encrypt); } } /** * 判断是否有效 * * @return */ public boolean isValid() { return this.alive; } /** * 退出 * * @author Pluto * @since 2020-01-08 15:57:48 */ public void cancel() { if (!this.alive) { return; } this.alive = false; NioHallows.release(this.recvSocket.getChannel()); try { Socket sendSocket; if ((sendSocket = this.sendSocket) != null) { // TCP 挥手步骤,对方调用 shutdownOutput 后等价完成 socket.close sendSocket.shutdownOutput(); } } catch (IOException e) { // do nothing } IBelongControl belong; if ((belong = this.belongControl) != null) { this.belongControl = null; belong.noticeStop(); } } /** * 启动 */ public void start() { if (this.alive) { return; } this.alive = true; NatcrossExecutor.executePassway(this); } } ================================================ FILE: src/main/java/person/pluto/natcross2/api/passway/SimplePassway.java ================================================ package person.pluto.natcross2.api.passway; import lombok.AccessLevel; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import person.pluto.natcross2.api.IBelongControl; import person.pluto.natcross2.executor.NatcrossExecutor; import person.pluto.natcross2.nio.INioProcessor; import person.pluto.natcross2.nio.NioHallows; import person.pluto.natcross2.utils.Tools; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.SocketChannel; import java.util.Objects; /** *

* 简单的隧道,无任何处理,只从输入侧原样输出到输出侧 *

* * @author Pluto * @since 2020-01-08 15:58:11 */ @Slf4j public class SimplePassway implements Runnable, INioProcessor { private boolean alive = false; /** * 所属对象,完成后通知 */ @Setter private IBelongControl belongControl; /** * 缓存大小 */ @Setter private int streamCacheSize = 8192; @Setter private Socket recvSocket; @Setter private Socket sendSocket; private OutputStream outputStream; private SocketChannel outputChannel; private OutputStream getOutputStream() throws IOException { OutputStream outputStream = this.outputStream; if (Objects.isNull(outputStream)) { outputStream = this.sendSocket.getOutputStream(); this.outputStream = outputStream; } return outputStream; } private SocketChannel getOutputChannel() { SocketChannel outputChannel = this.outputChannel; if (Objects.isNull(outputChannel)) { outputChannel = this.sendSocket.getChannel(); this.outputChannel = outputChannel; } return outputChannel; } /** * 向输出通道输出数据 *

* 这里不只是为了DMA而去用DMA,而是这里有奇葩问题 *

* 如能采用了SocketChannel,而去用outputStream的时候,不管输入输出,都会有奇怪的问题,比如输出会莫名的阻塞住 *

* 整体就是如果能用nio的方法,但是用了bio形式都会各种什么 NullPointException、IllageSateException 等等错误 *

* * @param byteBuffer * @throws IOException */ private void write(ByteBuffer byteBuffer) throws IOException { SocketChannel outputChannel = this.getOutputChannel(); if (Objects.nonNull(outputChannel)) { Tools.channelWrite(outputChannel, byteBuffer); } else { OutputStream outputStream = this.getOutputStream(); outputStream.write(byteBuffer.array(), byteBuffer.position(), byteBuffer.limit()); outputStream.flush(); } } @Override public void run() { try { InputStream inputStream = this.recvSocket.getInputStream(); int len; byte[] arrayTemp = new byte[this.streamCacheSize]; while (this.alive && (len = inputStream.read(arrayTemp)) > 0) { this.write(ByteBuffer.wrap(arrayTemp, 0, len)); } } catch (IOException e) { // do nothing } log.debug("one InputToOutputThread closed"); // 传输完成后退出 this.cancel(); } // ============== nio ================= @Setter(AccessLevel.NONE) @Getter(AccessLevel.NONE) private ByteBuffer byteBuffer; private ByteBuffer obtainByteBuffer() { ByteBuffer byteBuffer = this.byteBuffer; if (Objects.isNull(byteBuffer)) { if (Objects.isNull(this.getOutputChannel())) { byteBuffer = ByteBuffer.allocate(this.streamCacheSize); } else { // 输入输出可以使用channel,此处则使用DirectByteBuffer,这时候才真正体现出了DMA byteBuffer = ByteBuffer.allocateDirect(this.streamCacheSize); } this.byteBuffer = byteBuffer; } return byteBuffer; } @Override public void process(SelectionKey key) { if (this.alive && key.isValid()) { ByteBuffer buffer = this.obtainByteBuffer(); SocketChannel inputChannel = (SocketChannel) key.channel(); try { int len; do { buffer.clear(); len = inputChannel.read(buffer); if (len > 0) { buffer.flip(); if (buffer.hasRemaining()) { this.write(buffer); } } } while (len > 0); // 如果不是负数,则还没有断开连接,返回继续等待 if (len == 0) { return; } } catch (IOException e) { // } } log.debug("one InputToOutputThread closed"); this.cancel(); } /** * 判断是否有效 * * @return */ public boolean isValid() { return this.alive; } /** * 退出 */ public void cancel() { if (!this.alive) { return; } this.alive = false; NioHallows.release(this.recvSocket.getChannel()); try { Socket sendSocket = this.sendSocket; if (Objects.nonNull(sendSocket)) { // TCP 挥手步骤,对方调用 shutdownOutput 后等价完成 socket.close sendSocket.shutdownOutput(); } } catch (IOException e) { // do nothing } IBelongControl belong; if ((belong = this.belongControl) != null) { this.belongControl = null; belong.noticeStop(); } } /** * 启动 */ public void start() { if (this.alive) { return; } this.alive = true; SocketChannel recvChannel = this.recvSocket.getChannel(); if (Objects.isNull(recvChannel)) { NatcrossExecutor.executePassway(this); } else { try { NioHallows.register(recvChannel, SelectionKey.OP_READ, this); } catch (IOException e) { log.error("nio register failed", e); this.cancel(); } } } } ================================================ FILE: src/main/java/person/pluto/natcross2/api/secret/AESSecret.java ================================================ package person.pluto.natcross2.api.secret; import lombok.Data; import person.pluto.natcross2.utils.AESUtil; import java.security.Key; /** *

* AES加密方式 *

* * @author Pluto * @since 2020-01-08 16:01:40 */ @Data public class AESSecret implements ISecret { private Key aesKey; @Override public byte[] encrypt(byte[] content, int offset, int len) throws Exception { return AESUtil.encrypt(this.aesKey, content, offset, len); } @Override public byte[] decrypt(byte[] result) throws Exception { return AESUtil.decrypt(this.aesKey, result); } /** * 设置密钥 * * @param aesKey */ public void setBaseAesKey(String aesKey) { this.aesKey = AESUtil.createKeyByBase64(aesKey); } } ================================================ FILE: src/main/java/person/pluto/natcross2/api/secret/ISecret.java ================================================ package person.pluto.natcross2.api.secret; /** *

* 加密方法 *

* * @author Pluto * @since 2020-01-08 16:01:28 */ public interface ISecret { /** * 加密数据 * * @param content * @param offset * @param len * @return * @throws Exception */ byte[] encrypt(byte[] content, int offset, int len) throws Exception; /** * 解密数据 * * @param result * @return * @throws Exception */ byte[] decrypt(byte[] result) throws Exception; } ================================================ FILE: src/main/java/person/pluto/natcross2/api/socketpart/AbsSocketPart.java ================================================ package person.pluto.natcross2.api.socketpart; import lombok.AccessLevel; import lombok.Data; import lombok.Getter; import lombok.Setter; import person.pluto.natcross2.api.IBelongControl; import java.net.Socket; import java.time.Duration; import java.time.LocalDateTime; /** *

* socketPart抽象类 *

* * @author Pluto * @since 2020-01-08 16:01:56 */ @Data public abstract class AbsSocketPart { @Setter(AccessLevel.PRIVATE) protected volatile boolean isAlive = false; @Setter(AccessLevel.PRIVATE) protected volatile boolean canceled = false; @Setter(AccessLevel.PRIVATE) protected LocalDateTime createTime; /** * 等待连接有效时间,ms */ @Getter @Setter private long invalidMillis = 60000L; @Setter(AccessLevel.NONE) @Getter(AccessLevel.PROTECTED) protected IBelongControl belongThread; protected String socketPartKey; /** * 接受数据的socket;接受与发送的区分主要是主动发送方 */ protected Socket recvSocket; /** * 发送的socket */ protected Socket sendSocket; protected AbsSocketPart(IBelongControl belongThread) { this.belongThread = belongThread; this.createTime = LocalDateTime.now(); } /** * 是否有效 */ public boolean isValid() { if (this.canceled) { return false; } if (this.isAlive) { return true; } long millis = Duration.between(this.createTime, LocalDateTime.now()).toMillis(); return millis < this.invalidMillis; } /** * 退出 */ public abstract void cancel(); /** * 打通隧道 * * @return */ public abstract boolean createPassWay(); } ================================================ FILE: src/main/java/person/pluto/natcross2/api/socketpart/HttpRouteSocketPart.java ================================================ package person.pluto.natcross2.api.socketpart; import lombok.extern.slf4j.Slf4j; import person.pluto.natcross2.api.IBelongControl; import person.pluto.natcross2.api.IHttpRouting; import person.pluto.natcross2.api.passway.SimplePassway; import person.pluto.natcross2.model.HttpRoute; import person.pluto.natcross2.utils.Assert; import person.pluto.natcross2.utils.Tools; import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; import java.io.OutputStream; import java.net.InetSocketAddress; import java.net.Socket; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; /** *

* http路由socket对 *

* * @author Pluto * @since 2020-04-23 16:57:28 */ @Slf4j public class HttpRouteSocketPart extends SimpleSocketPart { private static final Charset httpCharset = StandardCharsets.ISO_8859_1; // 这里的 : 好巧不巧的 0x20 位是1,可以利用一波 private static final byte colonByte = ':'; private static final byte[] hostMatcher = new byte[]{'h', 'o', 's', 't', colonByte}; private static final int colonIndex = hostMatcher.length - 1; private final IHttpRouting httpRouting; public HttpRouteSocketPart(IBelongControl belongThread, IHttpRouting httpRouting) { super(belongThread); this.httpRouting = httpRouting; } /** * 选择路由并连接至目标 * * @throws Exception * @author Pluto * @since 2020-04-24 11:01:24 */ protected void routeHost() throws Exception { String host = null; BufferedInputStream inputStream = new BufferedInputStream(this.sendSocket.getInputStream()); // 缓存数据,不能我们处理了就不给实际应用 ByteArrayOutputStream headerBufferStream = new ByteArrayOutputStream(1024); // 临时输出列,用于读取一整行后进行字符串判断 ByteArrayOutputStream lineBufferStream = new ByteArrayOutputStream(); for (int flag = 0, lineCount = 0, matchFlag = 0; ; lineCount++) { // 依次读取 int read = inputStream.read(); lineBufferStream.write(read); if (read < 0) { break; } // 记录换行状态 if (read == '\r' || read == '\n') { flag++; } else { flag = 0; if ( // 这里matchFlag与lineCount不相等的频次比例较大,先比较 matchFlag == lineCount // 肯定要小于了呀 && matchFlag < hostMatcher.length // 大写转小写,如果是冒号的位置,需要完全相等 && hostMatcher[matchFlag] == (read | 0x20) && (matchFlag != colonIndex || colonByte == read) // ) { matchFlag++; } } // 如果大于等于4则就表示http头结束了 if (flag >= 4) { break; } // 等于2表示一行结束了,需要进行处理 if (flag == 2) { boolean isHostLine = (matchFlag == hostMatcher.length); // for循环特性,设置-1,营造line为0 lineCount = -1; matchFlag = 0; // 省去一次toByteArray拷贝的可能 lineBufferStream.writeTo(headerBufferStream); if (isHostLine) { byte[] byteArray = lineBufferStream.toByteArray(); // 重置行输出流 lineBufferStream.reset(); int left, right; byte rightByte; for (left = right = hostMatcher.length; right < byteArray.length; right++) { if (byteArray[left] == ' ') { // 左边先去掉空白,去除期间right不用判断 left++; } else if ( // (rightByte = byteArray[right]) == colonByte // || rightByte == ' ' || rightByte == '\r' || rightByte == '\n') { // right位置到left位置必有字符,遇到空白或 : 则停下,与left中间的组合为host地址 break; } } // 将缓存中的数据进行字符串化,根据http标准,字符集为 ISO-8859-1 host = new String(byteArray, left, right - left, httpCharset); break; } else { // 重置临时输出流 lineBufferStream.reset(); } } } // 将最后残留的输出 lineBufferStream.writeTo(headerBufferStream); Socket recvSocket = this.recvSocket; HttpRoute willConnect = this.httpRouting.pickEffectiveRoute(host); InetSocketAddress destAddress = new InetSocketAddress(willConnect.getDestIp(), willConnect.getDestPort()); recvSocket.connect(destAddress); OutputStream outputStream = recvSocket.getOutputStream(); headerBufferStream.writeTo(outputStream); // emmm.... 用bufferedStream每次read不用单字节从硬件缓存里读呀,快了些呢,咋地了,不就是再拷贝一次嘛! Tools.streamCopy(inputStream, outputStream); // flush的原因,不排除这里全部读完了,导致缓存中没有数据,那即使创建了passway也不会主动flush而是挂在那里,防止遇到lazy的自动刷新特性 outputStream.flush(); } @Override public boolean createPassWay() { Assert.state(!this.canceled, "不得重启已退出的socketPart"); if (this.isAlive) { return true; } this.isAlive = true; try { this.routeHost(); SimplePassway outToInPassway = this.outToInPassway = new SimplePassway(); outToInPassway.setBelongControl(this); outToInPassway.setSendSocket(this.sendSocket); outToInPassway.setRecvSocket(this.recvSocket); outToInPassway.setStreamCacheSize(this.getStreamCacheSize()); SimplePassway inToOutPassway = this.inToOutPassway = new SimplePassway(); inToOutPassway.setBelongControl(this); inToOutPassway.setSendSocket(this.recvSocket); inToOutPassway.setRecvSocket(this.sendSocket); inToOutPassway.setStreamCacheSize(this.getStreamCacheSize()); outToInPassway.start(); inToOutPassway.start(); } catch (Exception e) { log.error("socketPart [" + this.socketPartKey + "] 隧道建立异常", e); this.stop(); return false; } return true; } } ================================================ FILE: src/main/java/person/pluto/natcross2/api/socketpart/SecretSocketPart.java ================================================ package person.pluto.natcross2.api.socketpart; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import person.pluto.natcross2.api.IBelongControl; import person.pluto.natcross2.api.passway.SecretPassway; import person.pluto.natcross2.api.passway.SecretPassway.Mode; import person.pluto.natcross2.api.secret.ISecret; import person.pluto.natcross2.utils.Assert; import java.io.IOException; import java.net.Socket; import java.util.concurrent.CountDownLatch; /** *

* 加密-无加密socket对 *

* * @author Pluto * @since 2020-01-08 16:05:25 */ @Slf4j public class SecretSocketPart extends AbsSocketPart implements IBelongControl { @Getter @Setter private ISecret secret; private SecretPassway noToSecretPassway; private SecretPassway secretToNoPassway; private final CountDownLatch cancelLatch = new CountDownLatch(2); @Getter @Setter private int streamCacheSize = 8192; public SecretSocketPart(IBelongControl belongThread) { super(belongThread); } @Override public void cancel() { if (this.canceled) { return; } this.canceled = true; this.isAlive = false; log.debug("socketPart {} will cancel", this.socketPartKey); SecretPassway noToSecretPassway; if ((noToSecretPassway = this.noToSecretPassway) != null) { this.noToSecretPassway = null; noToSecretPassway.cancel(); } SecretPassway secretToNoPassway; if ((secretToNoPassway = this.secretToNoPassway) != null) { this.secretToNoPassway = null; secretToNoPassway.cancel(); } Socket recvSocket; if ((recvSocket = this.recvSocket) != null) { this.recvSocket = null; try { recvSocket.close(); } catch (IOException e) { log.debug("socketPart [{}] 监听端口 关闭异常", socketPartKey); } } Socket sendSocket; if ((sendSocket = this.sendSocket) != null) { this.sendSocket = null; try { sendSocket.close(); } catch (IOException e) { log.debug("socketPart [{}] 发送端口 关闭异常", socketPartKey); } } log.debug("socketPart {} is cancelled", this.socketPartKey); } @Override public boolean createPassWay() { Assert.state(!this.canceled, "不得重启已退出的socketPart"); if (this.isAlive) { return true; } this.isAlive = true; try { // 主要面向服务端-客户端过程加密 SecretPassway noToSecretPassway = this.noToSecretPassway = new SecretPassway(); noToSecretPassway.setBelongControl(this); noToSecretPassway.setMode(Mode.noToSecret); noToSecretPassway.setRecvSocket(this.recvSocket); noToSecretPassway.setSendSocket(this.sendSocket); noToSecretPassway.setStreamCacheSize(getStreamCacheSize()); noToSecretPassway.setSecret(this.secret); SecretPassway secretToNoPassway = this.secretToNoPassway = new SecretPassway(); secretToNoPassway.setBelongControl(this); secretToNoPassway.setMode(Mode.secretToNo); secretToNoPassway.setRecvSocket(this.sendSocket); secretToNoPassway.setSendSocket(this.recvSocket); secretToNoPassway.setSecret(this.secret); noToSecretPassway.start(); secretToNoPassway.start(); } catch (Exception e) { log.error("socketPart [" + this.socketPartKey + "] 隧道建立异常", e); this.stop(); return false; } return true; } /** * 停止 */ public void stop() { this.cancel(); IBelongControl belong; if ((belong = this.belongThread) != null) { this.belongThread = null; belong.noticeStop(); } } @Override public void noticeStop() { CountDownLatch cancelLatch = this.cancelLatch; cancelLatch.countDown(); if (cancelLatch.getCount() <= 0) { this.stop(); } } } ================================================ FILE: src/main/java/person/pluto/natcross2/api/socketpart/SimpleSocketPart.java ================================================ package person.pluto.natcross2.api.socketpart; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import person.pluto.natcross2.api.IBelongControl; import person.pluto.natcross2.api.passway.SimplePassway; import person.pluto.natcross2.utils.Assert; import java.io.IOException; import java.net.Socket; import java.util.concurrent.CountDownLatch; /** *

* socket匹配对 *

* * @author Pluto * @since 2019-07-12 08:36:30 */ @Slf4j public class SimpleSocketPart extends AbsSocketPart implements IBelongControl { protected SimplePassway outToInPassway; protected SimplePassway inToOutPassway; private final CountDownLatch cancelLatch = new CountDownLatch(2); @Getter @Setter private int streamCacheSize = 8192; public SimpleSocketPart(IBelongControl belongThread) { super(belongThread); } /** * 停止,并告知上层处理掉 */ public void stop() { this.cancel(); IBelongControl belong; if ((belong = this.belongThread) != null) { this.belongThread = null; belong.stopSocketPart(this.socketPartKey); } } @Override public void cancel() { if (this.canceled) { return; } this.canceled = true; this.isAlive = false; log.debug("socketPart {} will cancel", this.socketPartKey); SimplePassway outToInPassway; if ((outToInPassway = this.outToInPassway) != null) { this.outToInPassway = null; outToInPassway.cancel(); } SimplePassway inToOutPassway; if ((inToOutPassway = this.inToOutPassway) != null) { this.inToOutPassway = null; inToOutPassway.cancel(); } Socket recvSocket; if ((recvSocket = this.recvSocket) != null) { this.recvSocket = null; try { recvSocket.close(); } catch (IOException e) { log.debug("socketPart [{}] 监听端口 关闭异常", socketPartKey); } } Socket sendSocket; if ((sendSocket = this.sendSocket) != null) { this.sendSocket = null; try { sendSocket.close(); } catch (IOException e) { log.debug("socketPart [{}] 发送端口 关闭异常", socketPartKey); } } log.debug("socketPart {} is cancelled", this.socketPartKey); } @Override public boolean createPassWay() { Assert.state(!this.canceled, "不得重启已退出的socketPart"); if (this.isAlive) { return true; } this.isAlive = true; try { SimplePassway outToInPassway = this.outToInPassway = new SimplePassway(); outToInPassway.setBelongControl(this); outToInPassway.setRecvSocket(this.recvSocket); outToInPassway.setSendSocket(this.sendSocket); outToInPassway.setStreamCacheSize(getStreamCacheSize()); SimplePassway inToOutPassway = this.inToOutPassway = new SimplePassway(); inToOutPassway.setBelongControl(this); inToOutPassway.setRecvSocket(this.sendSocket); inToOutPassway.setSendSocket(this.recvSocket); inToOutPassway.setStreamCacheSize(getStreamCacheSize()); outToInPassway.start(); inToOutPassway.start(); } catch (Exception e) { log.error("socketPart [" + this.socketPartKey + "] 隧道建立异常", e); this.stop(); return false; } return true; } @Override public void noticeStop() { CountDownLatch cancellLatch = this.cancelLatch; cancellLatch.countDown(); if (cancellLatch.getCount() <= 0) { this.stop(); } } } ================================================ FILE: src/main/java/person/pluto/natcross2/channel/Channel.java ================================================ package person.pluto.natcross2.channel; import java.io.Closeable; import java.nio.charset.Charset; /** *

* 读写通道 *

* * @param 读取返回的类型 * @param 写入的类型 * @author Pluto * @since 2020-01-03 15:40:28 */ public interface Channel extends Closeable { /** * 简单的读取方式 * * @return * @throws Exception */ R read() throws Exception; /** * 简单的写入 * * @param value * @throws Exception */ void write(W value) throws Exception; /** * 刷新 * * @throws Exception */ void flush() throws Exception; /** * 简单的写入并刷新 * * @param value * @throws Exception */ void writeAndFlush(W value) throws Exception; /** * 设置交互编码 * * @param charset */ default void setCharset(Charset charset) { throw new UnsupportedOperationException("不支持的操作"); } } ================================================ FILE: src/main/java/person/pluto/natcross2/channel/InteractiveChannel.java ================================================ package person.pluto.natcross2.channel; import com.alibaba.fastjson.JSONObject; import person.pluto.natcross2.model.InteractiveModel; import java.io.IOException; import java.net.Socket; import java.nio.charset.Charset; /** *

* InteractiveModel 模式读写 *

* * @author Pluto * @since 2020-01-08 16:11:50 */ public class InteractiveChannel extends SocketChannel { /** * 实际通道 */ private final JsonChannel channel; public InteractiveChannel() { this.channel = new JsonChannel(); } public InteractiveChannel(Socket socket) throws IOException { this.channel = new JsonChannel(socket); } @Override public InteractiveModel read() throws Exception { JSONObject read = this.channel.read(); return read.toJavaObject(InteractiveModel.class); } @Override public void write(InteractiveModel value) throws Exception { this.channel.write(value); } @Override public void flush() throws Exception { this.channel.flush(); } @Override public void writeAndFlush(InteractiveModel value) throws Exception { this.channel.writeAndFlush(value); } /** * 获取charset * * @return */ public Charset getCharset() { return this.channel.getCharset(); } @Override public void setCharset(Charset charset) { this.channel.setCharset(charset); } @Override public Socket getSocket() { return this.channel.getSocket(); } @Override public void setSocket(Socket socket) throws IOException { this.channel.setSocket(socket); } @Override public void closeSocket() throws IOException { this.channel.closeSocket(); } } ================================================ FILE: src/main/java/person/pluto/natcross2/channel/JsonChannel.java ================================================ package person.pluto.natcross2.channel; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONAware; import com.alibaba.fastjson.JSONObject; import java.io.IOException; import java.net.Socket; import java.nio.charset.Charset; /** *

* json方式读写 *

* * @author Pluto * @since 2020-01-08 16:13:02 */ public class JsonChannel extends SocketChannel { /** * 实际通道 */ private final StringChannel channel; public JsonChannel() { this.channel = new StringChannel(); } public JsonChannel(Socket socket) throws IOException { this.channel = new StringChannel(socket); } @Override public JSONObject read() throws Exception { String read = this.channel.read(); return JSON.parseObject(read); } private String valueConvert(Object value) { String string; if (value instanceof JSONAware) { string = ((JSONAware) value).toJSONString(); } else { string = JSON.toJSONString(value); } return string; } @Override public void write(Object value) throws Exception { this.channel.write(this.valueConvert(value)); } @Override public void flush() throws Exception { this.channel.flush(); } @Override public void writeAndFlush(Object value) throws Exception { this.channel.writeAndFlush(this.valueConvert(value)); } /** * 获取charset * * @return */ public Charset getCharset() { return this.channel.getCharset(); } @Override public void setCharset(Charset charset) { this.channel.setCharset(charset); } @Override public Socket getSocket() { return this.channel.getSocket(); } @Override public void setSocket(Socket socket) throws IOException { this.channel.setSocket(socket); } @Override public void closeSocket() throws IOException { this.channel.closeSocket(); } } ================================================ FILE: src/main/java/person/pluto/natcross2/channel/LengthChannel.java ================================================ package person.pluto.natcross2.channel; import person.pluto.natcross2.utils.Tools; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; import java.nio.ByteBuffer; import java.util.Objects; import java.util.concurrent.locks.ReentrantLock; /** *

* 长度限定读写通道 *

* * @author Pluto * @since 2021-04-08 12:42:38 */ public class LengthChannel extends SocketChannel { private Socket socket; private java.nio.channels.SocketChannel socketChannel; private InputStream inputStream; private OutputStream outputStream; private final ReentrantLock readLock = new ReentrantLock(true); private final ReentrantLock writerLock = new ReentrantLock(true); private final byte[] lenBytes = new byte[4]; public LengthChannel() { } public LengthChannel(Socket socket) throws IOException { this.setSocket(socket); } @Override public byte[] read() throws Exception { ReentrantLock readLock = this.readLock; byte[] lenBytes = this.lenBytes; readLock.lock(); try { int offset = 0; InputStream is = getInputSteam(); int len; while (offset < lenBytes.length) { len = is.read(lenBytes, offset, lenBytes.length - offset); if (len < 0) { // 如果-1,提前关闭了,又没有获得足够的数据,那么就抛出异常 throw new IOException("Insufficient byte length[" + lenBytes.length + "] when io closed"); } offset += len; } int length = Tools.bytes2int(lenBytes); offset = 0; byte[] b = new byte[length]; while (offset < length) { len = is.read(b, offset, length - offset); if (len < 0) { // 如果-1,提前关闭了,又没有获得足够的数据,那么就抛出异常 throw new IOException("Insufficient byte length[" + length + "] when io closed"); } offset += len; } return b; } finally { readLock.unlock(); } } @Override public void write(byte[] value) throws Exception { ReentrantLock writerLock = this.writerLock; writerLock.lock(); try { java.nio.channels.SocketChannel socketChannel; if (Objects.nonNull(socketChannel = this.socketChannel)) { Tools.channelWrite(socketChannel, ByteBuffer.wrap(Tools.intToBytes(value.length))); Tools.channelWrite(socketChannel, ByteBuffer.wrap(value)); } else { OutputStream os = getOutputStream(); os.write(Tools.intToBytes(value.length)); os.write(value); } } finally { writerLock.unlock(); } } @Override public void flush() throws Exception { ReentrantLock writerLock = this.writerLock; writerLock.lock(); try { getOutputStream().flush(); } finally { writerLock.unlock(); } } @Override public void writeAndFlush(byte[] value) throws Exception { ReentrantLock writerLock = this.writerLock; writerLock.lock(); try { this.write(value); this.flush(); } finally { writerLock.unlock(); } } @Override public Socket getSocket() { return this.socket; } @Override public void setSocket(Socket socket) throws IOException { if (Objects.nonNull(this.socket)) { throw new UnsupportedOperationException("socket cannot be set repeatedly"); } this.socket = socket; this.socketChannel = socket.getChannel(); this.inputStream = socket.getInputStream(); this.outputStream = socket.getOutputStream(); } @Override public void closeSocket() throws IOException { this.socket.close(); } /** * 惰性获取输入流 * * @return * @throws IOException * @author Pluto * @since 2020-01-08 16:15:32 */ private InputStream getInputSteam() throws IOException { InputStream inputStream; if ((inputStream = this.inputStream) == null) { inputStream = this.inputStream = this.socket.getInputStream(); } return inputStream; } /** * 惰性获取输出流 * * @return * @throws IOException * @author Pluto * @since 2020-01-08 16:15:48 */ private OutputStream getOutputStream() throws IOException { OutputStream outputStream; if ((outputStream = this.outputStream) == null) { outputStream = this.outputStream = this.getSocket().getOutputStream(); } return outputStream; } } ================================================ FILE: src/main/java/person/pluto/natcross2/channel/SecretInteractiveChannel.java ================================================ package person.pluto.natcross2.channel; import com.alibaba.fastjson.JSONObject; import lombok.*; import lombok.EqualsAndHashCode.Exclude; import person.pluto.natcross2.model.InteractiveModel; import person.pluto.natcross2.model.SecretInteractiveModel; import person.pluto.natcross2.utils.AESUtil; import java.io.IOException; import java.net.Socket; import java.nio.charset.Charset; import java.security.Key; /** *

* InteractiveModel 加密型通道,AES加密 *

* * @author Pluto * @since 2020-01-08 16:16:12 */ @Data @EqualsAndHashCode(callSuper = false) public class SecretInteractiveChannel extends SocketChannel { @Exclude @Setter(AccessLevel.NONE) @Getter(AccessLevel.NONE) private JsonChannel channel; /** * 签名混淆key */ private String tokenKey; /** * aes密钥 */ private Key aesKey; /** * 超时时间,毫秒 */ private Long overtimeMills = 5000L; public SecretInteractiveChannel() { this.channel = new JsonChannel(); } public SecretInteractiveChannel(Socket socket) throws IOException { this.channel = new JsonChannel(socket); } @Override public InteractiveModel read() throws Exception { JSONObject read = this.channel.read(); SecretInteractiveModel secretInteractiveModel = read.toJavaObject(SecretInteractiveModel.class); if (Math.abs(System.currentTimeMillis() - secretInteractiveModel.getTimestamp()) > this.overtimeMills) { throw new IllegalStateException("超时"); } boolean checkAutograph = secretInteractiveModel.checkAutograph(this.tokenKey); if (!checkAutograph) { throw new IllegalStateException("签名错误"); } secretInteractiveModel.decryptMsg(this.aesKey); return secretInteractiveModel; } /** * 统一数据转换方法,使 {@link #write(InteractiveModel)} 与 * {@link #writeAndFlush(InteractiveModel)} 转换结果保持一致 * * @param value * @return * @throws Exception */ private Object valueConvert(InteractiveModel value) throws Exception { SecretInteractiveModel secretInteractiveModel = new SecretInteractiveModel(value); secretInteractiveModel.setCharset(this.getCharset().name()); secretInteractiveModel.fullMessage(this.aesKey, this.tokenKey); return secretInteractiveModel; } @Override public void write(InteractiveModel value) throws Exception { this.channel.write(this.valueConvert(value)); } @Override public void flush() throws Exception { this.channel.flush(); } @Override public void writeAndFlush(InteractiveModel value) throws Exception { this.channel.writeAndFlush(this.valueConvert(value)); } /** * 获取charset * * @return */ public Charset getCharset() { return this.channel.getCharset(); } @Override public void setCharset(Charset charset) { this.channel.setCharset(charset); } @Override public Socket getSocket() { return this.channel.getSocket(); } @Override public void setSocket(Socket socket) throws IOException { this.channel.setSocket(socket); } @Override public void closeSocket() throws IOException { this.channel.closeSocket(); } /** * 使用base64格式设置aes密钥 * * @param aesKey */ public void setBaseAesKey(String aesKey) { this.aesKey = AESUtil.createKeyByBase64(aesKey); } } ================================================ FILE: src/main/java/person/pluto/natcross2/channel/SocketChannel.java ================================================ package person.pluto.natcross2.channel; import java.io.IOException; import java.net.Socket; /** *

* socket通道 *

* * @param 读取返回的类型 * @param 写入的类型 * @author Pluto * @since 2020-01-08 16:19:51 */ public abstract class SocketChannel implements Channel { /** * 获取socket * * @return */ public abstract Socket getSocket(); /** * 设置socket * * @param socket * @throws IOException */ public abstract void setSocket(Socket socket) throws IOException; /** * 关闭socket * * @throws IOException */ public abstract void closeSocket() throws IOException; @Override public void close() throws IOException { this.closeSocket(); } } ================================================ FILE: src/main/java/person/pluto/natcross2/channel/StringChannel.java ================================================ package person.pluto.natcross2.channel; import java.io.IOException; import java.net.Socket; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; /** *

* 字符型通道 *

* * @author Pluto * @since 2020-01-08 16:21:31 */ public class StringChannel extends SocketChannel { private final LengthChannel channel; private Charset charset = StandardCharsets.UTF_8; public StringChannel() { this.channel = new LengthChannel(); } public StringChannel(Socket socket) throws IOException { this.channel = new LengthChannel(socket); } @Override public String read() throws Exception { byte[] read = this.channel.read(); return new String(read, this.charset); } /** * 统一数据转换方法 * * @param value * @return */ private byte[] valueConvert(String value) { return value.getBytes(this.charset); } @Override public void write(String value) throws Exception { this.channel.write(this.valueConvert(value)); } @Override public void flush() throws Exception { this.channel.flush(); } @Override public void writeAndFlush(String value) throws Exception { this.channel.writeAndFlush(this.valueConvert(value)); } /** * 获取charset * * @return */ public Charset getCharset() { return this.charset; } @Override public void setCharset(Charset charset) { this.charset = charset; } @Override public Socket getSocket() { return this.channel.getSocket(); } @Override public void setSocket(Socket socket) throws IOException { this.channel.setSocket(socket); } @Override public void closeSocket() throws IOException { this.channel.closeSocket(); } } ================================================ FILE: src/main/java/person/pluto/natcross2/clientside/ClientControlThread.java ================================================ package person.pluto.natcross2.clientside; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import person.pluto.natcross2.api.IBelongControl; import person.pluto.natcross2.api.socketpart.AbsSocketPart; import person.pluto.natcross2.clientside.adapter.IClientAdapter; import person.pluto.natcross2.clientside.config.IClientConfig; import person.pluto.natcross2.clientside.heart.IClientHeartThread; import java.time.LocalDateTime; import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; /** *

* 客户端控制服务 *

* * @author Pluto * @since 2019-07-05 10:53:33 */ @Slf4j public final class ClientControlThread implements Runnable, IBelongControl { private volatile Thread myThread = null; private volatile boolean isAlive = false; @Getter private volatile boolean cancelled = false; private final Map socketPartMap = new ConcurrentHashMap<>(); private final IClientConfig config; private volatile IClientHeartThread clientHeartThread; private volatile IClientAdapter clientAdapter; public ClientControlThread(IClientConfig config) { this.config = config; } /** * 触发控制服务 * * @return * @throws Exception */ public boolean createControl() throws Exception { this.stopClient(); if (this.clientAdapter == null) { this.clientAdapter = this.config.newCreateControlAdapter(this); } boolean flag = this.clientAdapter.createControlChannel(); if (!flag) { return false; } this.start(); return true; } @Override public void run() { while (this.isAlive) { try { // 使用适配器代理执行 this.clientAdapter.waitMessage(); } catch (Exception e) { log.warn("client control [{}] to server is exception,will stopClient", this.config.getListenServerPort()); this.stopClient(); } } } @Override public boolean stopSocketPart(String socketPartKey) { log.debug("stopSocketPart[{}]", socketPartKey); AbsSocketPart socketPart = this.socketPartMap.remove(socketPartKey); if (socketPart == null) { return false; } socketPart.cancel(); return true; } /** * * 启动 */ private void start() { this.isAlive = true; this.cancelled = false; Thread myThread = this.myThread; if (Objects.isNull(myThread) || !myThread.isAlive()) { IClientHeartThread clientHeartThread = this.clientHeartThread; if (Objects.isNull(clientHeartThread) || !clientHeartThread.isAlive()) { clientHeartThread = this.clientHeartThread = this.config.newClientHeartThread(this); if (Objects.nonNull(clientHeartThread)) { clientHeartThread.start(); } } myThread = this.myThread = new Thread(this); myThread.setName("client-" + this.formatInfo()); myThread.start(); } } /** * * 停止客户端监听 */ public void stopClient() { this.isAlive = false; Thread myThread = this.myThread; if (myThread != null) { this.myThread = null; myThread.interrupt(); } IClientAdapter clientAdapter = this.clientAdapter; if (Objects.nonNull(clientAdapter)) { try { clientAdapter.close(); } catch (Exception e) { // do nothing } } } /** * 全部退出 */ public void cancell() { if (this.cancelled) { return; } this.cancelled = true; this.stopClient(); IClientHeartThread clientHeartThread; if ((clientHeartThread = this.clientHeartThread) != null) { this.clientHeartThread = null; try { clientHeartThread.cancel(); } catch (Exception e) { // do no thing } } IClientAdapter clientAdapter; if ((clientAdapter = this.clientAdapter) != null) { this.clientAdapter = null; try { clientAdapter.close(); } catch (Exception e) { // do no thing } } String[] array = this.socketPartMap.keySet().toArray(new String[0]); for (String key : array) { this.stopSocketPart(key); } } /** * 服务端监听的端口 * * @return */ public Integer getListenServerPort() { return this.config.getListenServerPort(); } /** * 重设目标端口 * * @param destIp * @param destPort */ public void setDestIpPort(String destIp, Integer destPort) { this.config.setDestIpPort(destIp, destPort); } /** * 检测是否还活着 * * @return */ public boolean isAlive() { return this.isAlive; } /** * 发送心跳测试 * * @throws Exception */ public void sendHeartTest() throws Exception { // 无需判空,空指针异常也是异常 this.clientAdapter.sendHeartTest(); } /** * 获取服务端上次心跳测试成功时间 * * @return */ public LocalDateTime obtainServerHeartLastRecvTime() { return this.clientAdapter.obtainServerHeartLastRecvTime(); } /** * 设置隧道伙伴 * * @param socketPartKey * @param socketPart */ public void putSocketPart(String socketPartKey, AbsSocketPart socketPart) { this.socketPartMap.put(socketPartKey, socketPart); } /** * 格式化信息 * * @return */ public String formatInfo() { return String.valueOf(this.getListenServerPort()); } } ================================================ FILE: src/main/java/person/pluto/natcross2/clientside/adapter/IClientAdapter.java ================================================ package person.pluto.natcross2.clientside.adapter; import person.pluto.natcross2.channel.SocketChannel; import person.pluto.natcross2.model.interactive.ServerWaitModel; import java.time.LocalDateTime; /** *

* 客户端适配器 *

* * @param 处理的对象 * @param 可写的对象 * @author Pluto * @since 2020-01-08 16:22:43 */ public interface IClientAdapter { /** * 请求建立控制器 * * @return * @throws Exception */ boolean createControlChannel() throws Exception; /** * 请求建立隧道连接 * * @param serverWaitModel * @return */ boolean clientConnect(ServerWaitModel serverWaitModel); /** * 等待消息处理 * * @throws Exception */ void waitMessage() throws Exception; /** * 关闭 * * @throws Exception */ void close() throws Exception; /** * 向控制器发送心跳 * * @throws Exception */ void sendHeartTest() throws Exception; /** * 获取服务端最后心跳测试/回复的时间 * * @return */ LocalDateTime obtainServerHeartLastRecvTime(); /** * 重设服务端最后心跳测试/回复时间 *

* 取 {@link LocalDateTime#now()} 为 设定值 * * @return */ default LocalDateTime resetServerHeartLastRecvTime() { return this.resetServerHeartLastRecvTime(LocalDateTime.now()); } /** * 重设服务端最后心跳测试/回复时间 * * @param time 自有设置 * @return */ LocalDateTime resetServerHeartLastRecvTime(LocalDateTime time); /** * 获取socket读写通道 * * @return */ SocketChannel getSocketChannel(); } ================================================ FILE: src/main/java/person/pluto/natcross2/clientside/adapter/InteractiveSimpleClientAdapter.java ================================================ package person.pluto.natcross2.clientside.adapter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import person.pluto.natcross2.api.socketpart.AbsSocketPart; import person.pluto.natcross2.channel.SocketChannel; import person.pluto.natcross2.clientside.ClientControlThread; import person.pluto.natcross2.clientside.config.IClientConfig; import person.pluto.natcross2.clientside.handler.IClientHandler; import person.pluto.natcross2.executor.NatcrossExecutor; import person.pluto.natcross2.model.InteractiveModel; import person.pluto.natcross2.model.NatcrossResultModel; import person.pluto.natcross2.model.enumeration.InteractiveTypeEnum; import person.pluto.natcross2.model.enumeration.NatcrossResultEnum; import person.pluto.natcross2.model.interactive.ClientControlModel; import person.pluto.natcross2.model.interactive.ServerWaitModel; import java.io.IOException; import java.net.Socket; import java.time.LocalDateTime; import java.util.LinkedList; import java.util.List; import java.util.Objects; /** *

* 基于InteractiveModel的客户端适配器 *

* * @author Pluto * @since 2020-01-08 16:24:07 */ @Slf4j public class InteractiveSimpleClientAdapter implements IClientAdapter { /** * 所属的客户端线程 */ private final ClientControlThread clientControlThread; /** * 客户端设置 */ private final IClientConfig config; /** * 适配器通道 */ private SocketChannel socketChannel; private LocalDateTime serverHeartLastRecvTime = LocalDateTime.now(); /** * 客户端消息接收处理链 */ protected List> messageHandlerList = new LinkedList<>(); public InteractiveSimpleClientAdapter(ClientControlThread clientControlThread, IClientConfig clientConfig) { this.clientControlThread = clientControlThread; this.config = clientConfig; } /** * 创建客户端通道 * * @return */ protected SocketChannel newClientChannel() { return this.config.newClientChannel(); } /** * 向穿透目标socket * * @return * @throws Exception */ protected Socket newDestSocket() throws Exception { return this.config.newDestSocket(); } /** * 向服务端和暴露目标创建socketPart * * @return */ protected AbsSocketPart newSocketPart() { return this.config.newSocketPart(this.clientControlThread); } @Override public boolean createControlChannel() throws Exception { SocketChannel socketChannel = this.newClientChannel(); if (socketChannel == null) { log.error("向服务端[{}:{}]建立控制通道失败", this.config.getClientServiceIp(), this.config.getClientServicePort()); return false; } InteractiveModel interactiveModel = InteractiveModel.of(InteractiveTypeEnum.CLIENT_CONTROL, new ClientControlModel(this.config.getListenServerPort())); socketChannel.writeAndFlush(interactiveModel); InteractiveModel recv = socketChannel.read(); log.info("建立控制端口回复:{}", recv); NatcrossResultModel javaObject = recv.getData().toJavaObject(NatcrossResultModel.class); if (StringUtils.equals(NatcrossResultEnum.SUCCESS.getCode(), javaObject.getRetCod())) { // 使用相同的 this.socketChannel = socketChannel; this.resetServerHeartLastRecvTime(); return true; } return false; } /** * 建立连接 * * @param serverWaitModel */ @Override public boolean clientConnect(ServerWaitModel serverWaitModel) { // 首先向暴露目标建立socket Socket destSocket; try { destSocket = this.newDestSocket(); } catch (Exception e) { log.error("向目标建立连接失败 {}:{}", this.config.getDestIp(), this.config.getDestPort()); return false; } SocketChannel passwayClientChannel = null; try { // 向服务端请求建立隧道 passwayClientChannel = this.newClientChannel(); InteractiveModel model = InteractiveModel.of(InteractiveTypeEnum.CLIENT_CONNECT, new ServerWaitModel(serverWaitModel.getSocketPartKey())); passwayClientChannel.writeAndFlush(model); InteractiveModel recv = passwayClientChannel.read(); log.info("建立隧道回复:{}", recv); NatcrossResultModel javaObject = recv.getData().toJavaObject(NatcrossResultModel.class); if (!StringUtils.equals(NatcrossResultEnum.SUCCESS.getCode(), javaObject.getRetCod())) { throw new RuntimeException("绑定失败"); } } catch (Exception e) { log.error( "打通隧道发生异常 " + this.config.getClientServiceIp() + ":" + this.config.getClientServicePort() + "<->" + this.config.getDestIp() + ":" + this.config.getDestPort(), e); try { destSocket.close(); } catch (IOException e1) { // do nothing } if (passwayClientChannel != null) { try { passwayClientChannel.closeSocket(); } catch (IOException e1) { // do nothing } } return false; } // 将两个socket建立伙伴关系 AbsSocketPart socketPart = this.newSocketPart(); socketPart.setSocketPartKey(serverWaitModel.getSocketPartKey()); socketPart.setSendSocket(passwayClientChannel.getSocket()); socketPart.setRecvSocket(destSocket); // 尝试打通隧道 boolean createPassWay = socketPart.createPassWay(); if (!createPassWay) { socketPart.cancel(); return false; } // 将socket伙伴放入客户端线程进行统一管理 this.clientControlThread.putSocketPart(serverWaitModel.getSocketPartKey(), socketPart); return true; } @Override public void waitMessage() throws Exception { InteractiveModel read = this.socketChannel.read(); // 只要有有效信息推送过来,则重置心跳时间 this.resetServerHeartLastRecvTime(); NatcrossExecutor.executeClientMessageProc(() -> this.procMethod(read)); } /** * 消息处理方法 * * @param recvInteractiveModel */ protected void procMethod(InteractiveModel recvInteractiveModel) { log.info("接收到新的指令: {}", recvInteractiveModel); try { boolean proceedFlag = false; for (IClientHandler handler : this.messageHandlerList) { proceedFlag = handler.proc(recvInteractiveModel, this); if (proceedFlag) { break; } } if (!proceedFlag) { log.warn("无处理方法的信息:[{}]", recvInteractiveModel); InteractiveModel result = InteractiveModel.of(recvInteractiveModel.getInteractiveSeq(), InteractiveTypeEnum.COMMON_REPLY, NatcrossResultEnum.UNKNOWN_INTERACTIVE_TYPE.toResultModel()); this.getSocketChannel().writeAndFlush(result); } } catch (Exception e) { // 只记录,其他的交给心跳之类的去把控 log.error("读取或写入异常", e); } } @Override public SocketChannel getSocketChannel() { return this.socketChannel; } /** * 添加消息处理器 * * @param handler * @return */ public InteractiveSimpleClientAdapter addMessageHandler( IClientHandler handler) { this.messageHandlerList.add(handler); return this; } @Override public void close() throws Exception { SocketChannel socketChannel = this.socketChannel; if (Objects.nonNull(socketChannel)) { this.socketChannel = null; socketChannel.closeSocket(); } } @Override public void sendHeartTest() throws Exception { InteractiveModel interactiveModel = InteractiveModel.of(InteractiveTypeEnum.HEART_TEST, null); this.socketChannel.writeAndFlush(interactiveModel); } @Override public LocalDateTime obtainServerHeartLastRecvTime() { return this.serverHeartLastRecvTime; } @Override public LocalDateTime resetServerHeartLastRecvTime(LocalDateTime time) { LocalDateTime tempTime = this.serverHeartLastRecvTime; this.serverHeartLastRecvTime = time; return tempTime; } } ================================================ FILE: src/main/java/person/pluto/natcross2/clientside/config/AllSecretInteractiveClientConfig.java ================================================ package person.pluto.natcross2.clientside.config; import lombok.Getter; import lombok.Setter; import person.pluto.natcross2.api.secret.AESSecret; import person.pluto.natcross2.api.socketpart.AbsSocketPart; import person.pluto.natcross2.api.socketpart.SecretSocketPart; import person.pluto.natcross2.clientside.ClientControlThread; import person.pluto.natcross2.utils.AESUtil; import java.security.Key; /** *

* 交互及隧道都加密 *

* * @author Pluto * @since 2020-01-08 15:01:44 */ public class AllSecretInteractiveClientConfig extends SecretInteractiveClientConfig { @Setter @Getter private Key passwayKey; @Override public AbsSocketPart newSocketPart(ClientControlThread clientControlThread) { AESSecret secret = new AESSecret(); secret.setAesKey(this.passwayKey); SecretSocketPart secretSocketPart = new SecretSocketPart(clientControlThread); secretSocketPart.setSecret(secret); return secretSocketPart; } /** * base64格式设置密钥 * * @param key */ public void setBasePasswayKey(String key) { this.passwayKey = AESUtil.createKeyByBase64(key); } } ================================================ FILE: src/main/java/person/pluto/natcross2/clientside/config/HttpRouteClientConfig.java ================================================ package person.pluto.natcross2.clientside.config; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import person.pluto.natcross2.api.IHttpRouting; import person.pluto.natcross2.api.socketpart.AbsSocketPart; import person.pluto.natcross2.api.socketpart.HttpRouteSocketPart; import person.pluto.natcross2.channel.SocketChannel; import person.pluto.natcross2.clientside.ClientControlThread; import person.pluto.natcross2.clientside.adapter.IClientAdapter; import person.pluto.natcross2.clientside.adapter.InteractiveSimpleClientAdapter; import person.pluto.natcross2.clientside.handler.CommonReplyHandler; import person.pluto.natcross2.clientside.handler.ServerHeartHandler; import person.pluto.natcross2.clientside.handler.ServerWaitClientHandler; import person.pluto.natcross2.clientside.heart.IClientHeartThread; import person.pluto.natcross2.model.HttpRoute; import person.pluto.natcross2.model.InteractiveModel; import person.pluto.natcross2.utils.Assert; import java.net.Socket; import java.nio.channels.spi.SelectorProvider; import java.nio.charset.Charset; import java.util.*; import java.util.concurrent.locks.StampedLock; /** *

* http路由客户端配置 *

* * @author Pluto * @since 2020-04-24 10:09:46 */ @Slf4j public class HttpRouteClientConfig extends InteractiveClientConfig implements IHttpRouting { private final InteractiveClientConfig baseConfig; @Getter private HttpRoute masterRoute = null; // 不可对routeMap进行修改,只得以重新赋值方式重设 private Map routeMap = Collections.emptyMap(); private final StampedLock routeLock = new StampedLock(); public HttpRouteClientConfig() { this.baseConfig = new InteractiveClientConfig(); } public HttpRouteClientConfig(InteractiveClientConfig baseConfig) { this.baseConfig = baseConfig; } /** * 预设置 * * @param masterRoute * @param routeMap */ public void presetRoute(HttpRoute masterRoute, LinkedHashMap routeMap) { Objects.requireNonNull(masterRoute, "主路由不得为空"); Objects.requireNonNull(routeMap, "路由表不得为null"); LinkedHashMap routeMapTemp = new LinkedHashMap<>(routeMap); StampedLock routeLock = this.routeLock; long stamp = routeLock.writeLock(); try { this.masterRoute = masterRoute; this.routeMap = routeMapTemp; } finally { routeLock.unlockWrite(stamp); } } /** * 获取路由表 * * @return {@link Collections#unmodifiableMap(Map)} 不得对对象进行修改 */ public Map getRouteMap() { return Collections.unmodifiableMap(this.routeMap); } /** * 增加路由 * * @param httpRoutes */ public void addRoute(HttpRoute... httpRoutes) { if (httpRoutes == null || httpRoutes.length < 1) { return; } StampedLock routeLock = this.routeLock; long stamp = routeLock.writeLock(); try { if (Objects.isNull(this.masterRoute)) { this.masterRoute = httpRoutes[0]; } LinkedHashMap routeMap = new LinkedHashMap<>(this.routeMap); for (HttpRoute model : httpRoutes) { routeMap.put(model.getHost(), model); if (model.isMaster()) { this.masterRoute = model; } } this.routeMap = routeMap; } finally { routeLock.unlockWrite(stamp); } } /** * 清理路由 * * @param hosts */ public void clearRoute(String... hosts) { if (Objects.isNull(hosts) || hosts.length < 1) { return; } StampedLock routeLock = this.routeLock; long stamp = routeLock.writeLock(); try { LinkedHashMap routeMap = new LinkedHashMap<>(this.routeMap); HttpRoute masterRoute = this.masterRoute; String masterRouteHost = masterRoute.getHost(); for (String host : hosts) { routeMap.remove(host); if (StringUtils.equals(masterRouteHost, host)) { masterRoute = null; // 减少string比较复杂度 masterRouteHost = null; } } if (Objects.isNull(masterRoute)) { Iterator iterator = routeMap.values().iterator(); if (iterator.hasNext()) { // 先将第一个设置为主路由,再遍历所有,如果是主标志则设置为主,以最后的主为准 masterRoute = iterator.next(); while (iterator.hasNext()) { HttpRoute model = iterator.next(); if (model.isMaster()) { // 因使用的LinkedHashMap,就是让其符合初期定义,将后续加入有isMaster标志的路由设置为masterRoute masterRoute = model; } } } else { log.warn("{}:{} 路由是空的,若需要重新设置,请使用preset进行设置", this.getClientServiceIp(), this.getClientServicePort()); } } this.masterRoute = masterRoute; this.routeMap = routeMap; } finally { routeLock.unlockWrite(stamp); } } @Override public void setDestIpPort(String destIp, Integer destPort) { // do nothing } @Override public IClientHeartThread newClientHeartThread(ClientControlThread clientControlThread) { return this.baseConfig.newClientHeartThread(clientControlThread); } @Override public IClientAdapter newCreateControlAdapter( ClientControlThread clientControlThread) { InteractiveSimpleClientAdapter simpleClientAdapter = new InteractiveSimpleClientAdapter(clientControlThread, this); simpleClientAdapter.addMessageHandler(CommonReplyHandler.INSTANCE); simpleClientAdapter.addMessageHandler(ServerHeartHandler.INSTANCE); simpleClientAdapter.addMessageHandler(ServerWaitClientHandler.INSTANCE); return simpleClientAdapter; } @Override public SocketChannel newClientChannel() { return this.baseConfig.newClientChannel(); } @Override public AbsSocketPart newSocketPart(ClientControlThread clientControlThread) { HttpRouteSocketPart httpRouteSocketPart = new HttpRouteSocketPart(clientControlThread, this); httpRouteSocketPart.setStreamCacheSize(this.getStreamCacheSize()); return httpRouteSocketPart; } @Override public HttpRoute pickEffectiveRoute(String host) { StampedLock routeLock = this.routeLock; long stamp = routeLock.tryOptimisticRead(); Map routeMap = this.routeMap; HttpRoute masterRoute = this.masterRoute; if (!routeLock.validate(stamp)) { stamp = routeLock.readLock(); try { routeMap = this.routeMap; masterRoute = this.masterRoute; } finally { routeLock.unlockRead(stamp); } } HttpRoute httpRoute = routeMap.get(host); if (Objects.isNull(httpRoute)) { httpRoute = masterRoute; } Assert.state(Objects.nonNull(httpRoute), "未能获取有效的路由"); return httpRoute; } @Override public Socket newDestSocket() throws Exception { java.nio.channels.SocketChannel openSocketChannel = SelectorProvider.provider().openSocketChannel(); return openSocketChannel.socket(); // return new Socket(); } @Override public String getClientServiceIp() { return this.baseConfig.getClientServiceIp(); } @Override public void setClientServiceIp(String clientServiceIp) { this.baseConfig.setClientServiceIp(clientServiceIp); } @Override public Integer getClientServicePort() { return this.baseConfig.getClientServicePort(); } @Override public void setClientServicePort(Integer clientServicePort) { this.baseConfig.setClientServicePort(clientServicePort); } @Override public Integer getListenServerPort() { return this.baseConfig.getListenServerPort(); } @Override public void setListenServerPort(Integer listenServerPort) { this.baseConfig.setListenServerPort(listenServerPort); } @Override public String getDestIp() { return null; } @Override public void setDestIp(String destIp) { // do nothing } @Override public Integer getDestPort() { return null; } @Override public void setDestPort(Integer destPort) { // do nothing } @Override public Charset getCharset() { return this.baseConfig.getCharset(); } @Override public void setCharset(Charset charset) { this.baseConfig.setCharset(charset); } @Override public int getStreamCacheSize() { return this.baseConfig.getStreamCacheSize(); } @Override public void setStreamCacheSize(int streamCacheSize) { this.baseConfig.setStreamCacheSize(streamCacheSize); } } ================================================ FILE: src/main/java/person/pluto/natcross2/clientside/config/IClientConfig.java ================================================ package person.pluto.natcross2.clientside.config; import person.pluto.natcross2.api.socketpart.AbsSocketPart; import person.pluto.natcross2.channel.SocketChannel; import person.pluto.natcross2.clientside.ClientControlThread; import person.pluto.natcross2.clientside.adapter.IClientAdapter; import person.pluto.natcross2.clientside.heart.IClientHeartThread; import java.net.InetSocketAddress; import java.net.Socket; import java.nio.channels.spi.SelectorProvider; /** *

* 客户端配置接口 *

* * @param 通道读取的类型 * @param 通道写入的类型 * @author Pluto * @since 2020-01-08 16:30:04 */ public interface IClientConfig { /** * 获取服务端IP * * @return */ String getClientServiceIp(); /** * 获取服务端端口 * * @return */ Integer getClientServicePort(); /** * 对应的监听端口 * * @return */ Integer getListenServerPort(); /** * 目标IP * * @return */ String getDestIp(); /** * 目标端口 * * @return */ Integer getDestPort(); /** * 设置目标IP * * @param destIp */ void setDestIpPort(String destIp, Integer destPort); /** * 新建心跳测试线程 * * @param clientControlThread * @return */ IClientHeartThread newClientHeartThread(ClientControlThread clientControlThread); /** * 新建适配器 * * @param clientControlThread * @return */ IClientAdapter newCreateControlAdapter(ClientControlThread clientControlThread); /** * 新建与服务端的交互线程 * * @return */ SocketChannel newClientChannel(); /** * 创建新的socketPart * * @return */ AbsSocketPart newSocketPart(ClientControlThread clientControlThread); /** * 创建目标端口 * * @return * @throws Exception */ default Socket newDestSocket() throws Exception { java.nio.channels.SocketChannel openSocketChannel = SelectorProvider.provider().openSocketChannel(); openSocketChannel.connect(new InetSocketAddress(this.getDestIp(), this.getDestPort())); return openSocketChannel.socket(); // return new Socket(this.getDestIp(), this.getDestPort()); } } ================================================ FILE: src/main/java/person/pluto/natcross2/clientside/config/InteractiveClientConfig.java ================================================ package person.pluto.natcross2.clientside.config; import lombok.Data; import lombok.extern.slf4j.Slf4j; import person.pluto.natcross2.api.socketpart.AbsSocketPart; import person.pluto.natcross2.api.socketpart.SimpleSocketPart; import person.pluto.natcross2.channel.InteractiveChannel; import person.pluto.natcross2.channel.SocketChannel; import person.pluto.natcross2.clientside.ClientControlThread; import person.pluto.natcross2.clientside.adapter.IClientAdapter; import person.pluto.natcross2.clientside.adapter.InteractiveSimpleClientAdapter; import person.pluto.natcross2.clientside.handler.CommonReplyHandler; import person.pluto.natcross2.clientside.handler.ServerHeartHandler; import person.pluto.natcross2.clientside.handler.ServerWaitClientHandler; import person.pluto.natcross2.clientside.heart.ClientHeartThread; import person.pluto.natcross2.clientside.heart.IClientHeartThread; import person.pluto.natcross2.model.InteractiveModel; import java.io.IOException; import java.net.InetSocketAddress; import java.net.Socket; import java.nio.channels.spi.SelectorProvider; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; /** *

* 简单的以InteractiveModel为交互模型的配置 *

* * @author Pluto * @since 2020-01-08 16:30:53 */ @Slf4j @Data public class InteractiveClientConfig implements IClientConfig { private String clientServiceIp; private Integer clientServicePort; private Integer listenServerPort; private String destIp; private Integer destPort; private Charset charset = StandardCharsets.UTF_8; private int streamCacheSize = 8196; /** * 心跳检测间隔(s) */ private long heartIntervalSeconds = 10L; /** * 尝试重连次数,若超过则中断链接 */ private int tryRecipientCount = 10; @Override public void setDestIpPort(String destIp, Integer destPort) { this.destIp = destIp; this.destPort = destPort; } @Override public IClientHeartThread newClientHeartThread(ClientControlThread clientControlThread) { ClientHeartThread clientHeartThread = new ClientHeartThread(clientControlThread); clientHeartThread.setHeartIntervalSeconds(this.heartIntervalSeconds); clientHeartThread.setTryRecipientCount(this.tryRecipientCount); return clientHeartThread; } @Override public IClientAdapter newCreateControlAdapter( ClientControlThread clientControlThread) { InteractiveSimpleClientAdapter simpleClientAdapter = new InteractiveSimpleClientAdapter(clientControlThread, this); simpleClientAdapter.addMessageHandler(CommonReplyHandler.INSTANCE); simpleClientAdapter.addMessageHandler(ServerHeartHandler.INSTANCE); simpleClientAdapter.addMessageHandler(ServerWaitClientHandler.INSTANCE); return simpleClientAdapter; } @Override public SocketChannel newClientChannel() { InteractiveChannel interactiveChannel = new InteractiveChannel(); try { java.nio.channels.SocketChannel openSocketChannel = SelectorProvider.provider().openSocketChannel(); openSocketChannel.connect(new InetSocketAddress(this.getClientServiceIp(), this.getClientServicePort())); Socket socket = openSocketChannel.socket(); // Socket socket = new Socket(this.getClientServiceIp(), this.getClientServicePort()); interactiveChannel.setSocket(socket); } catch (IOException e) { log.debug("connect client service exception", e); throw new RuntimeException(e); } interactiveChannel.setCharset(this.charset); return interactiveChannel; } @Override public AbsSocketPart newSocketPart(ClientControlThread clientControlThread) { SimpleSocketPart simpleSocketPart = new SimpleSocketPart(clientControlThread); simpleSocketPart.setStreamCacheSize(this.getStreamCacheSize()); return simpleSocketPart; } } ================================================ FILE: src/main/java/person/pluto/natcross2/clientside/config/SecretInteractiveClientConfig.java ================================================ package person.pluto.natcross2.clientside.config; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.extern.slf4j.Slf4j; import person.pluto.natcross2.channel.SecretInteractiveChannel; import person.pluto.natcross2.channel.SocketChannel; import person.pluto.natcross2.model.InteractiveModel; import person.pluto.natcross2.utils.AESUtil; import java.io.IOException; import java.net.Socket; import java.security.Key; /** *

* 交互加密的配置方案(AES加密) *

* * @author Pluto * @since 2020-01-08 16:32:18 */ @Slf4j @Data @EqualsAndHashCode(callSuper = false) public class SecretInteractiveClientConfig extends InteractiveClientConfig { private String tokenKey; private Key aesKey; @Override public SocketChannel newClientChannel() { SecretInteractiveChannel channel = new SecretInteractiveChannel(); channel.setCharset(this.getCharset()); channel.setTokenKey(this.tokenKey); channel.setAesKey(this.aesKey); try { Socket socket = new Socket(this.getClientServiceIp(), this.getClientServicePort()); channel.setSocket(socket); } catch (IOException e) { log.debug("connect client service exception", e); throw new RuntimeException(e); } return channel; } /** * 设置交互密钥 * * @param aesKey */ public void setBaseAesKey(String aesKey) { this.aesKey = AESUtil.createKeyByBase64(aesKey); } } ================================================ FILE: src/main/java/person/pluto/natcross2/clientside/handler/CommonReplyHandler.java ================================================ package person.pluto.natcross2.clientside.handler; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import person.pluto.natcross2.clientside.adapter.IClientAdapter; import person.pluto.natcross2.model.InteractiveModel; import person.pluto.natcross2.model.enumeration.InteractiveTypeEnum; import java.util.Objects; /** *

* 统一回复 处理器 *

* * @author Pluto * @since 2020-04-15 13:02:09 */ @Slf4j public class CommonReplyHandler implements IClientHandler { public static final CommonReplyHandler INSTANCE = new CommonReplyHandler(); @Getter @Setter private IClientHandler handler; @Override public boolean proc(InteractiveModel model, IClientAdapter clientAdapter) throws Exception { InteractiveTypeEnum interactiveTypeEnum = InteractiveTypeEnum.getEnumByName(model.getInteractiveType()); if (!InteractiveTypeEnum.COMMON_REPLY.equals(interactiveTypeEnum)) { return false; } IClientHandler handler; if (Objects.isNull(handler = this.handler)) { log.info("common reply: {}", model); return true; } return handler.proc(model, clientAdapter); } } ================================================ FILE: src/main/java/person/pluto/natcross2/clientside/handler/IClientHandler.java ================================================ package person.pluto.natcross2.clientside.handler; import person.pluto.natcross2.clientside.adapter.IClientAdapter; /** *

* 接收处理器 *

* * @author Pluto * @since 2020-04-15 11:13:20 */ public interface IClientHandler { /** * 执行方法 * * @param model * @param clientAdapter * @return * @throws Exception */ boolean proc(R model, IClientAdapter clientAdapter) throws Exception; } ================================================ FILE: src/main/java/person/pluto/natcross2/clientside/handler/ServerHeartHandler.java ================================================ package person.pluto.natcross2.clientside.handler; import lombok.extern.slf4j.Slf4j; import person.pluto.natcross2.clientside.adapter.IClientAdapter; import person.pluto.natcross2.model.InteractiveModel; import person.pluto.natcross2.model.enumeration.InteractiveTypeEnum; import person.pluto.natcross2.model.enumeration.NatcrossResultEnum; /** *

* 心跳检测 *

* * @author Pluto * @since 2020-04-15 13:02:09 */ @Slf4j public class ServerHeartHandler implements IClientHandler { public static final ServerHeartHandler INSTANCE = new ServerHeartHandler(); @Override public boolean proc(InteractiveModel model, IClientAdapter clientAdapter) throws Exception { InteractiveTypeEnum interactiveTypeEnum = InteractiveTypeEnum.getEnumByName(model.getInteractiveType()); if (InteractiveTypeEnum.HEART_TEST.equals(interactiveTypeEnum)) { InteractiveModel sendModel = InteractiveModel.of(model.getInteractiveSeq(), InteractiveTypeEnum.HEART_TEST_REPLY, NatcrossResultEnum.SUCCESS.toResultModel()); clientAdapter.getSocketChannel().writeAndFlush(sendModel); return true; } else if (InteractiveTypeEnum.HEART_TEST_REPLY.equals(interactiveTypeEnum)) { log.debug("HEART_TEST_REPLY ignore"); return true; } return false; } } ================================================ FILE: src/main/java/person/pluto/natcross2/clientside/handler/ServerWaitClientHandler.java ================================================ package person.pluto.natcross2.clientside.handler; import person.pluto.natcross2.clientside.adapter.IClientAdapter; import person.pluto.natcross2.model.InteractiveModel; import person.pluto.natcross2.model.enumeration.InteractiveTypeEnum; import person.pluto.natcross2.model.interactive.ServerWaitModel; /** *

* 心跳检测 *

* * @author Pluto * @since 2020-04-15 13:02:09 */ public class ServerWaitClientHandler implements IClientHandler { public static final ServerWaitClientHandler INSTANCE = new ServerWaitClientHandler(); @Override public boolean proc(InteractiveModel model, IClientAdapter clientAdapter) throws Exception { InteractiveTypeEnum interactiveTypeEnum = InteractiveTypeEnum.getEnumByName(model.getInteractiveType()); if (!InteractiveTypeEnum.SERVER_WAIT_CLIENT.equals(interactiveTypeEnum)) { return false; } ServerWaitModel serverWaitModel = model.getData().toJavaObject(ServerWaitModel.class); clientAdapter.clientConnect(serverWaitModel); return true; } } ================================================ FILE: src/main/java/person/pluto/natcross2/clientside/heart/ClientHeartThread.java ================================================ package person.pluto.natcross2.clientside.heart; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import person.pluto.natcross2.clientside.ClientControlThread; import person.pluto.natcross2.executor.NatcrossExecutor; import java.time.Duration; import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; import java.util.Objects; import java.util.concurrent.ScheduledFuture; /** *

* 心跳检测线程 *

* * @author Pluto * @since 2021-04-25 17:53:44 */ @Slf4j public class ClientHeartThread implements IClientHeartThread, Runnable { private final ClientControlThread clientControlThread; private volatile boolean isAlive = false; @Setter @Getter private long heartIntervalSeconds = 10L; @Setter @Getter private int tryRecipientCount = 10; @Setter @Getter private long serverHeartMaxMissDurationSeconds = 25L; private volatile ScheduledFuture scheduledFuture; private int failCount = 0; public ClientHeartThread(ClientControlThread clientControlThread) { this.clientControlThread = clientControlThread; } /** * 检查服务端是否心跳超时 * * @param clientControlThread * @param timeoutSeconds * @return */ private boolean lastServerHeartEffective(ClientControlThread clientControlThread, Long timeoutSeconds) { return Duration.between(clientControlThread.obtainServerHeartLastRecvTime(), LocalDateTime.now()) .get(ChronoUnit.SECONDS) < timeoutSeconds; } @Override public void run() { ClientControlThread clientControlThread = this.clientControlThread; if (clientControlThread.isCancelled() || !this.isAlive()) { this.cancel(); } log.debug("send client heart data to {}", clientControlThread.getListenServerPort()); try { // 如果服务端心跳超时则直接判定为失败,否则进行心跳检查 if (this.lastServerHeartEffective(clientControlThread, this.serverHeartMaxMissDurationSeconds)) { clientControlThread.sendHeartTest(); this.failCount = 0; return; } } catch (Exception e) { log.warn("{} 心跳异常", clientControlThread.getListenServerPort()); clientControlThread.stopClient(); } if (!this.isAlive) { return; } this.failCount++; boolean createControl = false, logFlag = true; try { createControl = clientControlThread.createControl(); } catch (Exception reClientException) { log.warn("重新建立连接" + clientControlThread.getListenServerPort() + "失败第 " + this.failCount + " 次", reClientException); logFlag = false; } if (createControl) { log.info("重新建立连接 {} 成功,在第 {} 次", clientControlThread.getListenServerPort(), this.failCount); this.failCount = 0; return; } if (logFlag) { log.warn("重新建立连接" + clientControlThread.getListenServerPort() + "失败第 " + this.failCount + " 次"); } if (this.failCount >= this.tryRecipientCount) { log.error("尝试重新连接 {} 超过最大次数,关闭客户端", clientControlThread.getListenServerPort()); clientControlThread.cancell(); this.cancel(); } } @Override public synchronized void start() { this.isAlive = true; ScheduledFuture scheduledFuture = this.scheduledFuture; if (Objects.isNull(scheduledFuture) || scheduledFuture.isCancelled()) { this.failCount = 0; this.scheduledFuture = NatcrossExecutor.scheduledClientHeart(this, this.heartIntervalSeconds); } } @Override public void cancel() { if (!this.isAlive) { return; } this.isAlive = false; ScheduledFuture scheduledFuture = this.scheduledFuture; if (Objects.nonNull(scheduledFuture) && !scheduledFuture.isCancelled()) { this.scheduledFuture = null; scheduledFuture.cancel(false); } } @Override public boolean isAlive() { return this.isAlive; } } ================================================ FILE: src/main/java/person/pluto/natcross2/clientside/heart/IClientHeartThread.java ================================================ package person.pluto.natcross2.clientside.heart; /** *

* 心跳测试线程 *

* * @author Pluto * @since 2020-01-08 16:33:03 */ public interface IClientHeartThread { /** * 是否还活着 * * @return */ boolean isAlive(); /** * 退出 */ void cancel(); /** * 开始 */ void start(); } ================================================ FILE: src/main/java/person/pluto/natcross2/common/CommonFormat.java ================================================ package person.pluto.natcross2.common; import lombok.AccessLevel; import lombok.NoArgsConstructor; import org.apache.commons.lang3.RandomStringUtils; import java.text.DecimalFormat; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; /** *

* 公用的格式化类 *

* * @author Pluto * @since 2019-07-05 13:35:04 */ @NoArgsConstructor(access = AccessLevel.PRIVATE) public final class CommonFormat { /** * 获取socket匹配对key * * @param listenPort * @return */ public static String generateSocketPartKey(Integer listenPort) { DecimalFormat fiveLenFormat = new DecimalFormat("00000"); String dateTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS")); String randomNum = RandomStringUtils.randomNumeric(4); return String.join("-", "SK", fiveLenFormat.format(listenPort), dateTime, randomNum); } /** * 根据socketPartKey获取端口号 * * @param socketPartKey * @return */ public static Integer getSocketPortByPartKey(String socketPartKey) { String[] split = socketPartKey.split("-"); return Integer.valueOf(split[1]); } /** * 获取交互流水号 * * @return */ public static String generateInteractiveSeq() { String dateTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS")); String randomNum = RandomStringUtils.randomNumeric(4); return String.join("-", "IS", dateTime, randomNum); } } ================================================ FILE: src/main/java/person/pluto/natcross2/common/Optional.java ================================================ package person.pluto.natcross2.common; /** *

* 操作对象,主要是让值能够通过引用进行传递 *

* * @param 存放的类型 * @author Pluto * @since 2020-01-08 16:35:46 */ public class Optional { public static Optional of(T value) { return new Optional<>(value); } public Optional(T value) { this.value = value; } private T value; public T getValue() { return value; } public void setValue(T value) { this.value = value; } } ================================================ FILE: src/main/java/person/pluto/natcross2/executor/IExecutor.java ================================================ package person.pluto.natcross2.executor; import java.util.concurrent.ScheduledFuture; /** *

* 执行器实现 *

* * @author Pluto * @since 2021-04-08 14:38:23 */ public interface IExecutor { /** * 关闭 * * @throws Exception */ void shutdown(); /** * 默认执行方法 * * @param runnable */ void execute(Runnable runnable); /** * 服务监听线程任务执行器 *

* For {@link person.pluto.natcross2.serverside.listen.ServerListenThread} * * @param runnable */ default void executeServerListenAccept(Runnable runnable) { execute(runnable); } /** * 客户端监听线程任务执行器 *

* For {@link person.pluto.natcross2.serverside.client.ClientServiceThread} * * @param runnable */ default void executeClientServiceAccept(Runnable runnable) { execute(runnable); } /** * 客户端消息处理任务执行器 *

* For * {@link person.pluto.natcross2.clientside.adapter.InteractiveSimpleClientAdapter#waitMessage()} * * @param runnable */ default void executeClientMessageProc(Runnable runnable) { execute(runnable); } /** * 隧道线程执行器 *

* For {@link person.pluto.natcross2.api.passway} * * @param runnable */ default void executePassway(Runnable runnable) { execute(runnable); } /** * nio事件任务执行器 *

* For {@link person.pluto.natcross2.nio.NioHallows#run()} * * @param runnable */ default void executeNioAction(Runnable runnable) { execute(runnable); } /** * 心跳检测定时循环任务执行 * * @param runnable * @param delaySeconds */ ScheduledFuture scheduledClientHeart(Runnable runnable, long delaySeconds); /** * 服务监听清理无效socket对 * * @param runnable * @param delaySeconds * @return */ ScheduledFuture scheduledClearInvalidSocketPart(Runnable runnable, long delaySeconds); } ================================================ FILE: src/main/java/person/pluto/natcross2/executor/NatcrossExecutor.java ================================================ package person.pluto.natcross2.executor; import lombok.AccessLevel; import lombok.NoArgsConstructor; import java.util.Objects; import java.util.concurrent.ScheduledFuture; /** *

* 线程执行器 *

* 主要是为了统一位置,方便管理 *

* * @author Pluto * @since 2021-04-08 14:37:52 */ @NoArgsConstructor(access = AccessLevel.PRIVATE) public final class NatcrossExecutor { private static volatile IExecutor INSTANCE = new SimpleExecutor(); public static void shutdown() { INSTANCE.shutdown(); } /** * 重设执行器 *

* 会将旧的执行器进行执行 {@link IExecutor#shutdown()} 方法,建议重设执行器的操作在初始化程序时 * * @param executor */ public static void resetExecutor(IExecutor executor) { IExecutor oldExecutor = INSTANCE; if (Objects.nonNull(oldExecutor)) { try { oldExecutor.shutdown(); } catch (Exception e) { // } } INSTANCE = executor; } /** * 服务监听线程任务执行器 *

* For {@link person.pluto.natcross2.serverside.listen.ServerListenThread} * * @param runnable */ public static void executeServerListenAccept(Runnable runnable) { INSTANCE.executeServerListenAccept(runnable); } /** * 客户端监听线程任务执行器 *

* For {@link person.pluto.natcross2.serverside.client.ClientServiceThread} * * @param runnable */ public static void executeClientServiceAccept(Runnable runnable) { INSTANCE.executeClientServiceAccept(runnable); } /** * 客户端消息处理任务执行器 *

* For * {@link person.pluto.natcross2.clientside.adapter.InteractiveSimpleClientAdapter#waitMessage()} * * @param runnable */ public static void executeClientMessageProc(Runnable runnable) { INSTANCE.executeClientMessageProc(runnable); } /** * 隧道线程执行器 *

* For {@link person.pluto.natcross2.api.passway} * * @param runnable */ public static void executePassway(Runnable runnable) { INSTANCE.executePassway(runnable); } /** * nio事件任务执行器 *

* For {@link person.pluto.natcross2.nio.NioHallows#run()} * * @param runnable */ public static void executeNioAction(Runnable runnable) { INSTANCE.executeNioAction(runnable); } /** * 心跳检测定时循环任务执行 * * @param runnable * @param delaySeconds */ public static ScheduledFuture scheduledClientHeart(Runnable runnable, long delaySeconds) { return INSTANCE.scheduledClientHeart(runnable, delaySeconds); } /** * 服务监听清理无效socket对 * * @param runnable * @param delaySeconds * @return */ public static ScheduledFuture scheduledClearInvalidSocketPart(Runnable runnable, long delaySeconds) { return INSTANCE.scheduledClearInvalidSocketPart(runnable, delaySeconds); } } ================================================ FILE: src/main/java/person/pluto/natcross2/executor/SimpleExecutor.java ================================================ package person.pluto.natcross2.executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; /** *

* 线程执行器 *

* * @author Pluto * @since 2021-04-09 12:46:36 */ public class SimpleExecutor implements IExecutor { private final ExecutorService executor = Executors.newCachedThreadPool(); private final ScheduledExecutorService scheduledExecutor = Executors.newSingleThreadScheduledExecutor(); @Override public void shutdown() { this.executor.shutdownNow(); this.scheduledExecutor.shutdownNow(); } @Override public void execute(Runnable runnable) { this.executor.execute(runnable); } @Override public ScheduledFuture scheduledClientHeart(Runnable runnable, long delaySeconds) { return this.scheduledExecutor.scheduleWithFixedDelay(runnable, delaySeconds, delaySeconds, TimeUnit.SECONDS); } @Override public ScheduledFuture scheduledClearInvalidSocketPart(Runnable runnable, long delaySeconds) { return this.scheduledExecutor.scheduleWithFixedDelay(runnable, delaySeconds, delaySeconds, TimeUnit.SECONDS); } } ================================================ FILE: src/main/java/person/pluto/natcross2/model/HttpRoute.java ================================================ package person.pluto.natcross2.model; import lombok.AccessLevel; import lombok.Getter; import lombok.Setter; /** *

* http路由表 *

* * @author Pluto * @since 2020-04-24 09:31:51 */ @Getter @Setter(AccessLevel.PRIVATE) public class HttpRoute { public static HttpRoute of(String host, String destIp, Integer destPort) { return HttpRoute.of(false, host, destIp, destPort); } public static HttpRoute of(boolean master, String host, String destIp, Integer destPort) { HttpRoute model = new HttpRoute(); model.setMaster(master); model.setHost(host); model.setDestIp(destIp); model.setDestPort(destPort); return model; } // 主路由,如果是多个则会去队列最后设置的那个 private boolean master; // 请求时的域名host private String host; // 目标IP private String destIp; // 目标端口 private Integer destPort; } ================================================ FILE: src/main/java/person/pluto/natcross2/model/InteractiveModel.java ================================================ package person.pluto.natcross2.model; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONAware; import com.alibaba.fastjson.JSONObject; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import lombok.experimental.Accessors; import person.pluto.natcross2.common.CommonFormat; import person.pluto.natcross2.model.enumeration.InteractiveTypeEnum; /** *

* 交互基础类型 *

* * @author Pluto * @since 2019-07-18 18:16:34 */ @Data @Accessors(chain = true) @NoArgsConstructor @AllArgsConstructor public class InteractiveModel implements JSONAware { public static InteractiveModel of(InteractiveTypeEnum interactiveTypeEnum, String key, String value) { JSONObject jsonObject = new JSONObject(); jsonObject.put(key, value); return new InteractiveModel(CommonFormat.generateInteractiveSeq(), interactiveTypeEnum.name(), jsonObject); } public static InteractiveModel of(String interactiveSeq, InteractiveTypeEnum interactiveTypeEnum, Object data) { return new InteractiveModel(interactiveSeq, interactiveTypeEnum.name(), data == null ? null : JSON.parseObject(JSON.toJSONString(data))); } public static InteractiveModel of(InteractiveTypeEnum interactiveTypeEnum, Object data) { return new InteractiveModel(CommonFormat.generateInteractiveSeq(), interactiveTypeEnum.name(), data == null ? null : JSON.parseObject(JSON.toJSONString(data))); } public static InteractiveModel of(String interactiveType, Object data) { return new InteractiveModel(CommonFormat.generateInteractiveSeq(), interactiveType, JSON.parseObject(JSON.toJSONString(data))); } public InteractiveModel(InteractiveModel model) { this.fullValue(model); } public void fullValue(InteractiveModel model) { this.setInteractiveSeq(model.getInteractiveSeq()); this.setInteractiveType(model.getInteractiveType()); this.setData(model.getData()); } /** * 交互序列,用于异步通信 */ private String interactiveSeq; /** * 交互类型 */ private String interactiveType; /** * 交互实体内容 */ private JSONObject data; @Override public String toJSONString() { JSONObject jsonObject = new JSONObject(); jsonObject.put("interactiveSeq", this.interactiveSeq); jsonObject.put("interactiveType", this.interactiveType); jsonObject.put("data", this.data); return jsonObject.toJSONString(); } } ================================================ FILE: src/main/java/person/pluto/natcross2/model/NatcrossResultModel.java ================================================ package person.pluto.natcross2.model; import com.alibaba.fastjson.JSONAware; import com.alibaba.fastjson.JSONObject; import lombok.Data; import lombok.extern.slf4j.Slf4j; import person.pluto.natcross2.model.enumeration.NatcrossResultEnum; import java.lang.reflect.Field; /** *

* 常规类型的前后端返回model *

* * @author Pluto * @since 2019-03-28 10:45:41 */ @Data @Slf4j public class NatcrossResultModel implements JSONAware { public static NatcrossResultModel of(String retCode, String retMsg, Object object) { return new NatcrossResultModel(retCode, retMsg, object); } public static NatcrossResultModel of(NatcrossResultEnum resultEnum, Object data) { return new NatcrossResultModel(resultEnum.getCode(), resultEnum.getName(), data); } public static NatcrossResultModel of(NatcrossResultEnum resultEnum) { return new NatcrossResultModel(resultEnum.getCode(), resultEnum.getName(), null); } public static NatcrossResultModel ofFail(Object data) { return new NatcrossResultModel(NatcrossResultEnum.FAIL.getCode(), NatcrossResultEnum.FAIL.getName(), data); } public static NatcrossResultModel ofFail() { return new NatcrossResultModel(NatcrossResultEnum.FAIL.getCode(), NatcrossResultEnum.FAIL.getName(), null); } public static NatcrossResultModel ofSuccess(Object data) { return new NatcrossResultModel(NatcrossResultEnum.SUCCESS.getCode(), NatcrossResultEnum.SUCCESS.getName(), data); } public static NatcrossResultModel ofSuccess() { return new NatcrossResultModel(NatcrossResultEnum.SUCCESS.getCode(), NatcrossResultEnum.SUCCESS.getName(), null); } private String retCod; private String retMsg; private Object data; public NatcrossResultModel(String retCod, String retMsg, Object data) { this.retCod = retCod; this.retMsg = retMsg; this.data = data; } /** * 反射方式修改值 * * @param fieldStr * @param object * @return * @author Pluto * @since 2019-05-10 14:04:48 */ public NatcrossResultModel set(String fieldStr, Object object) { Field field; try { field = this.getClass().getDeclaredField(fieldStr); } catch (NoSuchFieldException | SecurityException e) { log.warn("ResultModel get field failed!", e); return this; } field.setAccessible(true); try { field.set(this, object); } catch (IllegalArgumentException | IllegalAccessException e) { log.warn("ResultModel set field failed!", e); return this; } return this; } @Override public String toString() { return this.toJSONString(); } @Override public String toJSONString() { JSONObject jsonObject = new JSONObject(); jsonObject.put("retCod", this.retCod); jsonObject.put("retMsg", this.retMsg); jsonObject.put("data", this.data); return jsonObject.toJSONString(); } } ================================================ FILE: src/main/java/person/pluto/natcross2/model/SecretInteractiveModel.java ================================================ package person.pluto.natcross2.model; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import lombok.ToString; import org.apache.commons.lang3.StringUtils; import person.pluto.natcross2.utils.AESUtil; import person.pluto.natcross2.utils.MD5Signature; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.security.Key; /** *

* 基于InteractiveModel模型的加密交互模型 *

* * @author Pluto * @since 2020-01-08 16:38:52 */ @Data @NoArgsConstructor @ToString(callSuper = true) @EqualsAndHashCode(callSuper = true) public class SecretInteractiveModel extends InteractiveModel { public SecretInteractiveModel(InteractiveModel model) { super(model); } /** * 时间戳 */ private Long timestamp; /** * 签名 */ private String autograph; /** * InteractiveModel模型jsonString加密值 */ private String encrypt; /** * 字符编码 */ private String charset = StandardCharsets.UTF_8.name(); /** * 加密消息 * * @param key * @throws Exception * @author Pluto * @since 2020-01-08 16:39:46 */ public void encryptMsg(Key key) throws Exception { this.encrypt = AESUtil.encryptBase64(key, super.toJSONString().getBytes(this.charset)); } /** * 解密消息 * * @param key * @throws Exception * @author Pluto * @since 2020-01-08 16:39:53 */ public void decryptMsg(Key key) throws Exception { byte[] decryptBase64 = AESUtil.decryptBase64(key, this.encrypt); String interactiveJsonString = new String(decryptBase64, this.charset); InteractiveModel model = JSON.parseObject(interactiveJsonString, InteractiveModel.class); super.fullValue(model); } /** * 签名模型 * * @param tokenKey * @author Pluto * @since 2020-01-08 16:39:59 */ public void autographMsg(String tokenKey) { this.autograph = MD5Signature.getSignature(Charset.forName(this.charset), tokenKey, this.timestamp.toString(), this.encrypt, this.charset); } /** * 检查签名 * * @param tokenKey * @return * @author Pluto * @since 2020-01-08 16:40:09 */ public boolean checkAutograph(String tokenKey) { String signature = MD5Signature.getSignature(Charset.forName(this.charset), tokenKey, this.timestamp.toString(), this.encrypt, this.charset); return StringUtils.equals(this.autograph, signature); } /** * 填充消息 * * @param key * @param tokenKey * @throws Exception * @author Pluto * @since 2020-01-08 16:40:17 */ public void fullMessage(Key key, String tokenKey) throws Exception { this.timestamp = System.currentTimeMillis(); this.encryptMsg(key); this.autographMsg(tokenKey); } @Override public String toJSONString() { JSONObject jsonObject = new JSONObject(); jsonObject.put("charset", this.charset); jsonObject.put("timestamp", this.timestamp); jsonObject.put("encrypt", this.encrypt); jsonObject.put("autograph", this.autograph); return jsonObject.toJSONString(); } } ================================================ FILE: src/main/java/person/pluto/natcross2/model/enumeration/InteractiveTypeEnum.java ================================================ package person.pluto.natcross2.model.enumeration; import lombok.Getter; import org.apache.commons.lang3.StringUtils; /** *

* 交互类型enum *

* * @author Pluto * @since 2019-07-17 09:50:33 */ @Getter public enum InteractiveTypeEnum { // UNKNOWN("未知"), // COMMON_REPLY("通用回复标签"), // HEART_TEST("发送心跳"), // HEART_TEST_REPLY("心跳测试回复"), // SERVER_WAIT_CLIENT("需求客户端建立连接"), // CLIENT_CONNECT("客户端建立通道连接"), // CLIENT_CONTROL("客户端控制端口建立连接"), // ; private final String describe; InteractiveTypeEnum(String describe) { this.describe = describe; } public static InteractiveTypeEnum getEnumByName(String name) { if (StringUtils.isBlank(name)) { return null; } for (InteractiveTypeEnum e : InteractiveTypeEnum.values()) { if (e.name().equals(name)) { return e; } } return null; } } ================================================ FILE: src/main/java/person/pluto/natcross2/model/enumeration/NatcrossResultEnum.java ================================================ package person.pluto.natcross2.model.enumeration; import org.apache.commons.lang3.StringUtils; import person.pluto.natcross2.model.NatcrossResultModel; /** *

* 客户端服务端返回码 *

* * @author Pluto * @since 2019-03-28 10:59:53 */ public enum NatcrossResultEnum { // 成功 SUCCESS("1000", "成功"), // UNKNOWN_INTERACTIVE_TYPE("3001", "未知的通信类型"), // NO_HAS_SERVER_LISTEN("3002", "不存在请求的监听接口"), // 未知错误 FAIL("9999", "未知错误"); private final String code; private final String name; NatcrossResultEnum(String code, String name) { this.code = code; this.name = name; } public static NatcrossResultEnum getEnumByCode(String code) { if (StringUtils.isBlank(code)) { return null; } for (NatcrossResultEnum e : NatcrossResultEnum.values()) { if (e.code.equals(code)) { return e; } } return null; } public NatcrossResultModel toResultModel() { return NatcrossResultModel.of(this); } public String getCode() { return code; } public String getName() { return name; } } ================================================ FILE: src/main/java/person/pluto/natcross2/model/interactive/ClientConnectModel.java ================================================ package person.pluto.natcross2.model.interactive; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; /** *

* 请求建立隧道模型 *

* * @author Pluto * @since 2020-01-08 16:36:58 */ @Data @NoArgsConstructor @AllArgsConstructor public class ClientConnectModel { private String socketPartKey; } ================================================ FILE: src/main/java/person/pluto/natcross2/model/interactive/ClientControlModel.java ================================================ package person.pluto.natcross2.model.interactive; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; /** *

* 请求建立控制器模型 *

* * @author Pluto * @since 2020-01-08 16:37:12 */ @Data @NoArgsConstructor @AllArgsConstructor public class ClientControlModel { private Integer listenPort; } ================================================ FILE: src/main/java/person/pluto/natcross2/model/interactive/ServerWaitModel.java ================================================ package person.pluto.natcross2.model.interactive; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; /** *

* 服务端等待建立隧道模型 *

* * @author Pluto * @since 2020-01-08 16:37:26 */ @Data @NoArgsConstructor @AllArgsConstructor public class ServerWaitModel { private String socketPartKey; } ================================================ FILE: src/main/java/person/pluto/natcross2/nio/INioProcessor.java ================================================ package person.pluto.natcross2.nio; import java.nio.channels.SelectionKey; /** *

* nio 执行器 *

* * @author Pluto * @since 2021-04-12 17:51:37 */ @FunctionalInterface public interface INioProcessor { /** * 需要执行的方法 * * @param key */ void process(SelectionKey key); } ================================================ FILE: src/main/java/person/pluto/natcross2/nio/NioHallows.java ================================================ package person.pluto.natcross2.nio; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import person.pluto.natcross2.executor.NatcrossExecutor; import person.pluto.natcross2.utils.Assert; import person.pluto.natcross2.utils.CountWaitLatch; import java.io.IOException; import java.nio.channels.SelectableChannel; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.util.Iterator; import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; /** *

* nio 容器 *

* * @author Pluto * @since 2021-04-13 09:25:51 */ @Slf4j @NoArgsConstructor(access = AccessLevel.PRIVATE) public final class NioHallows implements Runnable { public static final NioHallows INSTANCE = new NioHallows(); /** * 注册监听动作 *

* 要注意这里只拿最后的一次注册为准,即 {@code channel} 只能与一个 {@code proccesser} 动作对应 * * @param channel * @param ops 依据以下值进行或运算进行最后结果设定,并且 {@code channel} 要支持相应的动作 *

* - {@link SelectionKey#OP_ACCEPT} *

* - {@link SelectionKey#OP_CONNECT} *

* - {@link SelectionKey#OP_READ} *

* - {@link SelectionKey#OP_WRITE} * @param processor 要执行的动作 * @throws IOException * @author Pluto * @since 2021-04-26 15:55:38 */ public static void register(SelectableChannel channel, int ops, INioProcessor processor) throws IOException { INSTANCE.register0(channel, ops, processor); } /** * 根据 {@link SelectionKey} 恢复监听事件的注册 * * @param key 原始的key * @param ops 要与通过 {@link #register(SelectableChannel, int, INioProcessor)} * 注册的事件统一 * @throws IOException * @author Pluto * @since 2021-05-07 13:21:49 */ public static boolean reRegisterByKey(SelectionKey key, int ops) { return INSTANCE.reRegisterByKey0(key, ops); } /** * 释放注册 * * @param channel * @author Pluto * @since 2021-04-26 16:03:51 */ public static void release(SelectableChannel channel) { INSTANCE.release0(channel); } private volatile Thread myThread = null; private volatile boolean alive = false; private volatile boolean canceled = false; private volatile Selector selector; private final Object selectorLock = new Object(); private final CountWaitLatch countWaitLatch = new CountWaitLatch(); private final Map channelProcesserMap = new ConcurrentHashMap<>(); @Setter @Getter private long selectTimeout = 10L; @Setter @Getter private long wakeupSleepNanos = 1000000L; /** * 获取 {@link #selector} *

* 若 {@link #selector} 未有值,则会进行初始化:打开selector,并执行 {@link #start()} * * @return * @throws IOException * @author Pluto * @since 2021-04-26 16:04:30 */ public Selector getSelector() throws IOException { // 判空、返回逻辑,按第一次取值进行,缺点是不能判断是否已经关闭,但与 this.cancel() // 方法中的执行顺序来看,会先被设置为null,再去close,所以可以大概率认为若不为null即为没有关闭 Selector selector = this.selector; if (Objects.isNull(selector)) { synchronized (this.selectorLock) { // 二次校验 // 若是主动退出,则不在创建,避免退出时有新任务而被重启,若要重新启用,则需要主动调用 start() 方法来启动 if (Objects.isNull(this.selector) && !this.canceled) { this.selector = Selector.open(); this.start(); } } selector = this.selector; if (Objects.isNull(selector)) { throw new IOException("NioHallows's selector is closed"); } } return selector; } /** * 获取唤醒后的 {@link #selector} *

* 注意,若 {@link #run()} 快于你的任务,还是会被再次阻塞,只是执行了一次 {@link Selector#wakeup()} * * @return * @throws IOException * @author Pluto * @since 2021-04-26 16:07:00 */ public Selector getWakeupSelector() throws IOException { return this.getSelector().wakeup(); } /** * 注册监听动作 *

* 要注意这里只拿最后的一次注册为准,即 {@code channel} 只能与一个 {@code proccesser} 动作对应 * * @param channel * @param ops 依据以下值进行或运算进行最后结果设定,并且 {@code channel} 要支持相应的动作 *

* - {@link SelectionKey#OP_ACCEPT} *

* - {@link SelectionKey#OP_CONNECT} *

* - {@link SelectionKey#OP_READ} *

* - {@link SelectionKey#OP_WRITE} * @param proccesser 要执行的动作 * @throws IOException * @author Pluto * @since 2021-04-26 15:55:38 */ public void register0(SelectableChannel channel, int ops, INioProcessor proccesser) throws IOException { Objects.requireNonNull(channel, "channel non null"); try { this.channelProcesserMap.put(channel, ProcesserHolder.of(channel, ops, proccesser)); channel.configureBlocking(false); this.countWaitLatch.countUp(); // 这里有个坑点,如果在select中,这里会被阻塞 channel.register(this.getWakeupSelector(), ops); } catch (Throwable e) { this.channelProcesserMap.remove(channel); throw e; } finally { this.countWaitLatch.countDown(); } } /** * 根据 {@link SelectionKey} 恢复监听事件的注册 * * @param key 原始的key * @param ops 要与通过 {@link #register0(SelectableChannel, int, INioProcessor)} * 注册的事件统一 * @throws IOException * @author Pluto * @since 2021-05-07 13:21:49 */ public boolean reRegisterByKey0(SelectionKey key, int ops) { Objects.requireNonNull(key, "key non null"); Assert.state(key.selector() == this.selector, "this SelectionKey is not belong NioHallows's selector"); if (!key.isValid()) { return false; } // 通过事件和源码分析,恢复注册是通过updateKeys.addLast进行,虽然没有被阻塞,但是需要进行一次唤醒才可以成功恢复事件监听 // 因无法获知是否成功注入selector,所以必须要进行一次唤醒操作,并且没有阻塞的问题,所以这里不通过countWaitLatch进行同步 key.interestOps(ops); try { this.getWakeupSelector(); } catch (IOException e) { // 出错了交给其他的流程逻辑,这里只进行一次唤醒 } return true; } /** * 释放注册 * * @param channel * @author Pluto * @since 2021-04-26 16:03:51 */ public void release0(SelectableChannel channel) { if (Objects.isNull(channel)) { return; } this.channelProcesserMap.remove(channel); SelectionKey key = channel.keyFor(this.selector); if (Objects.nonNull(key)) { key.cancel(); } } @Override public void run() { CountWaitLatch countWaitLatch = this.countWaitLatch; while (this.alive) { // 给注册事务一个时间,如果等待时间太长(可能需要注入的太多),就跳出再去获取新事件,防止饿死 try { countWaitLatch.await(this.getWakeupSleepNanos(), TimeUnit.NANOSECONDS); } catch (InterruptedException e) { log.warn("selector wait register timeout"); } Selector selector; try { selector = getSelector(); // 采用有期限的监听,以免线程太快,没有来的及注册,就永远阻塞在那里了 int select = selector.select(this.getSelectTimeout()); if (select <= 0) { continue; } } catch (IOException e) { log.error("NioHallows run exception", e); continue; } Iterator iterator = selector.selectedKeys().iterator(); while (iterator.hasNext()) { SelectionKey key = iterator.next(); iterator.remove(); try { key.interestOps(0); } catch (Exception e) { // do nothing } ProcesserHolder processerHolder = this.channelProcesserMap.get(key.channel()); if (Objects.isNull(processerHolder)) { key.cancel(); continue; } NatcrossExecutor.executeNioAction(() -> processerHolder.process(key)); } } } /** * 启动nio事件监听 * * @author Pluto * @since 2021-04-26 16:33:07 */ public synchronized void start() { this.canceled = false; this.alive = true; Thread myThread = this.myThread; if (myThread == null || !myThread.isAlive()) { myThread = this.myThread = new Thread(this); myThread.setName("nio-hallows"); myThread.start(); log.info("NioHallows is started!"); } } /** * 退出nio事件监听 * * @author Pluto * @since 2021-04-26 16:33:33 */ public void cancel() { // 假设A线程执行到了 this.selector = Selector.open() 但是调用 this.cancel() // 方法的B线程抢占cpu成功,并一直到执行完成,此时A线程抢占CPU继续执行,又会进行重启,与关停项目时的关停期望不同。 // // 此处锁定 this.selectorLock 后再去设置 this.canceled,形成与 this.getSelector() // 的线程同步,同时避免了被动调用 this.start() 时与 this.cancel() 的同步问题,最终可关闭。 // 虽与主动调用 this.start() 有不同步的风险,但 this.start() 、 this.cancel() // 主动调用的场景有极大对立性,所以不进行过多的关照。 // // 注意:若 this.cancel() 添加了synchronized,存在死锁的可能!!! synchronized (this.selectorLock) { this.canceled = true; } log.info("NioHallows cancel"); this.alive = false; Selector selector; if ((selector = this.selector) != null) { this.selector = null; try { selector.close(); } catch (IOException e) { // do nothing } } Thread myThread; if ((myThread = this.myThread) != null) { this.myThread = null; myThread.interrupt(); } } } ================================================ FILE: src/main/java/person/pluto/natcross2/nio/ProcesserHolder.java ================================================ package person.pluto.natcross2.nio; import lombok.AllArgsConstructor; import lombok.Data; import java.nio.channels.SelectableChannel; import java.nio.channels.SelectionKey; /** *

* 执行器暂存 *

* * @author Pluto * @since 2021-04-13 09:53:54 */ @Data @AllArgsConstructor(staticName = "of") public class ProcesserHolder { private SelectableChannel channel; private int interestOps; private INioProcessor processor; /** * 执行事件的任务 * * @param key * @author Pluto * @since 2021-04-26 16:35:36 */ public void process(SelectionKey key) { this.processor.process(key); if (!NioHallows.reRegisterByKey(key, this.interestOps)) { NioHallows.release(this.channel); } } } ================================================ FILE: src/main/java/person/pluto/natcross2/serverside/client/ClientServiceThread.java ================================================ package person.pluto.natcross2.serverside.client; import lombok.extern.slf4j.Slf4j; import person.pluto.natcross2.executor.NatcrossExecutor; import person.pluto.natcross2.nio.INioProcessor; import person.pluto.natcross2.nio.NioHallows; import person.pluto.natcross2.serverside.client.config.IClientServiceConfig; import person.pluto.natcross2.utils.Assert; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; import java.nio.channels.SelectionKey; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.Objects; /** *

* 客户端服务进程 *

* * @author Pluto * @since 2019-07-05 10:53:33 */ @Slf4j public final class ClientServiceThread implements Runnable, INioProcessor { private volatile Thread myThread = null; private volatile boolean isAlive = false; private volatile boolean canceled = false; private final ServerSocket listenServerSocket; private final IClientServiceConfig config; public ClientServiceThread(IClientServiceConfig config) throws Exception { this.config = config; // 启动时配置,若启动失败则执行cancell并再次抛出异常让上级处理 this.listenServerSocket = config.createServerSocket(); log.info("client service [{}] is created!", this.config.getListenPort()); } @Override public void run() { while (this.isAlive) { try { Socket listenSocket = this.listenServerSocket.accept(); this.procMethod(listenSocket); } catch (Exception e) { log.warn("客户端服务进程 轮询等待出现异常", e); this.cancel(); } } } @Override public void process(SelectionKey key) { if (!key.isValid()) { this.cancel(); } try { ServerSocketChannel channel = (ServerSocketChannel) key.channel(); SocketChannel accept = channel.accept(); for (; Objects.nonNull(accept); accept = channel.accept()) { this.procMethod(accept.socket()); } } catch (IOException e) { log.warn("客户端服务进程 轮询等待出现异常", e); this.cancel(); } } /** * 处理客户端发来的消息 * * @param listenSocket * @author Pluto * @since 2020-01-03 14:46:36 */ public void procMethod(Socket listenSocket) { NatcrossExecutor.executeClientServiceAccept(() -> { try { this.config.getClientServiceAdapter().procMethod(listenSocket); } catch (Exception e) { log.error("处理socket异常", e); try { listenSocket.close(); } catch (IOException sce) { log.warn("处理新socket时异常,并在关闭socket时异常", e); } } }); } /** * 启动 * * @author Pluto * @since 2020-01-03 14:05:59 */ public synchronized void start() { Assert.state(!this.canceled, "已退出,不得重新启动"); log.info("client service [{}] starting ...", this.config.getListenPort()); this.isAlive = true; ServerSocketChannel channel = this.listenServerSocket.getChannel(); if (Objects.nonNull(channel)) { try { NioHallows.register(channel, SelectionKey.OP_ACCEPT, this); } catch (IOException e) { log.error("register clientService channel[{}] faild!", config.getListenPort()); this.cancel(); throw new RuntimeException("nio注册时异常", e); } } else { if (this.myThread == null || !this.myThread.isAlive()) { this.myThread = new Thread(this); this.myThread.setName("client-server-" + this.formatInfo()); this.myThread.start(); } } log.info("client service [{}] start success", this.config.getListenPort()); } /** * 退出 * * @author Pluto * @since 2019-07-18 18:32:03 */ public synchronized void cancel() { if (this.canceled) { return; } this.canceled = true; log.info("client service [{}] will cancell", this.config.getListenPort()); this.isAlive = false; ServerSocket listenServerSocket; if ((listenServerSocket = this.listenServerSocket) != null) { NioHallows.release(listenServerSocket.getChannel()); try { listenServerSocket.close(); } catch (IOException e) { log.warn("监听端口关闭异常", e); } } Thread myThread; if ((myThread = this.myThread) != null) { myThread.interrupt(); } log.info("client service [{}] cancell success", this.config.getListenPort()); } /** * * 是否激活状态 * * @return * @author Pluto * @since 2020-01-07 09:43:28 */ public boolean isAlive() { return this.isAlive; } /** * 是否已退出 * * @return * @author Pluto * @since 2021-04-13 13:39:11 */ public boolean isCanceled() { return this.canceled; } /** * 获取监听端口 * * @return * @author Pluto * @since 2019-07-18 18:32:40 */ public Integer getListenPort() { return this.config.getListenPort(); } /** * 格式化为短小的可识别信息 * * @return * @author Pluto * @since 2020-04-15 14:17:41 */ public String formatInfo() { return String.valueOf(this.getListenPort()); } } ================================================ FILE: src/main/java/person/pluto/natcross2/serverside/client/adapter/DefaultReadAheadPassValueAdapter.java ================================================ package person.pluto.natcross2.serverside.client.adapter; import person.pluto.natcross2.model.InteractiveModel; import person.pluto.natcross2.serverside.client.config.IClientServiceConfig; import person.pluto.natcross2.serverside.client.handler.DefaultInteractiveProcessHandler; /** *

* 默认的预读后处理适配器 *

* * @author Pluto * @since 2021-04-26 17:06:25 */ public class DefaultReadAheadPassValueAdapter extends ReadAheadPassValueAdapter { public DefaultReadAheadPassValueAdapter(IClientServiceConfig config) { super(config); this.addLast(DefaultInteractiveProcessHandler.INSTANCE); } } ================================================ FILE: src/main/java/person/pluto/natcross2/serverside/client/adapter/IClientServiceAdapter.java ================================================ package person.pluto.natcross2.serverside.client.adapter; import java.net.Socket; /** *

* 客户端服务适配器 *

* * @author Pluto * @since 2020-01-08 16:40:35 */ public interface IClientServiceAdapter { /** * 处理方法 * * @param listenSocket * @throws Exception */ void procMethod(Socket listenSocket) throws Exception; } ================================================ FILE: src/main/java/person/pluto/natcross2/serverside/client/adapter/PassValueNextEnum.java ================================================ package person.pluto.natcross2.serverside.client.adapter; import lombok.Getter; /** *

* 传值适配器的handler回复信息 *

* * @author Pluto * @since 2020-01-08 16:40:54 */ @Getter public enum PassValueNextEnum { // 停止并关闭 STOP_CLOSE(false, true), // 停止但不关闭 STOP_KEEP(false, false), // 继续执行,默认关闭 NEXT(true, true), // 继续执行,但不要关闭 NEXT_KEEP(true, false), // ; private final boolean nextFlag; private final boolean closeFlag; PassValueNextEnum(boolean nextFlag, boolean closeFlag) { this.nextFlag = nextFlag; this.closeFlag = closeFlag; } } ================================================ FILE: src/main/java/person/pluto/natcross2/serverside/client/adapter/ReadAheadPassValueAdapter.java ================================================ package person.pluto.natcross2.serverside.client.adapter; import lombok.extern.slf4j.Slf4j; import person.pluto.natcross2.channel.SocketChannel; import person.pluto.natcross2.common.Optional; import person.pluto.natcross2.serverside.client.config.IClientServiceConfig; import person.pluto.natcross2.serverside.client.handler.IPassValueHandler; import java.io.IOException; import java.net.Socket; import java.util.LinkedList; import java.util.List; /** *

* 预读后处理适配器 *

* * @param * @param * @author Pluto * @since 2020-01-06 08:56:22 */ @Slf4j public class ReadAheadPassValueAdapter implements IClientServiceAdapter { /** * 处理器队列 */ private List> handlerList = new LinkedList<>(); /** * 客户端服务配置 */ private final IClientServiceConfig config; public ReadAheadPassValueAdapter(IClientServiceConfig config) { this.config = config; } @Override public void procMethod(Socket listenSocket) throws Exception { // 建立交互通道 SocketChannel socketChannel; try { socketChannel = this.config.newSocketChannel(listenSocket); } catch (Exception e) { log.error("创建socket通道异常", e); throw e; } Optional optional; try { R read = socketChannel.read(); optional = Optional.of(read); } catch (Exception e) { log.error("读取数据异常", e); throw e; } boolean closeFlag = true; for (IPassValueHandler handler : handlerList) { // 按照队列进行执行 PassValueNextEnum proc = handler.proc(socketChannel, optional); // 只要有一个需要不关闭通道则就不得关闭 if (!proc.isCloseFlag()) { closeFlag = false; } // 如果无需继续向下则退出 if (!proc.isNextFlag()) { break; } } if (closeFlag) { try { socketChannel.closeSocket(); } catch (IOException e) { log.error("关闭socket异常", e); } } } /** * 添加处理器到最后一个 * * @param handler * @return * @author Pluto * @since 2020-01-06 09:06:55 */ public ReadAheadPassValueAdapter addLast(IPassValueHandler handler) { this.handlerList.add(handler); return this; } /** * 设置handlerList * * @param handlerList * @return * @author Pluto * @since 2020-01-06 09:07:53 */ public ReadAheadPassValueAdapter setHandlerList(List> handlerList) { this.handlerList = handlerList; return this; } /** * 获取处理器,可以自行更改 * * @return * @author Pluto * @since 2020-01-06 09:06:28 */ public List> getHandlerList() { return this.handlerList; } } ================================================ FILE: src/main/java/person/pluto/natcross2/serverside/client/config/IClientServiceConfig.java ================================================ package person.pluto.natcross2.serverside.client.config; import person.pluto.natcross2.channel.SocketChannel; import person.pluto.natcross2.serverside.client.adapter.IClientServiceAdapter; import java.net.ServerSocket; import java.net.Socket; import java.nio.charset.Charset; /** *

* 客户端服务配置 *

* * @param 交互通道读取的类 * @param 交互通道可写的类 * @author Pluto * @since 2020-01-08 16:43:17 */ public interface IClientServiceConfig { /** * 监听端口 * * @return */ Integer getListenPort(); /** * 创建监听端口 * * @return * @throws Exception */ ServerSocket createServerSocket() throws Exception; /** * 客户端适配器 * * @return */ IClientServiceAdapter getClientServiceAdapter(); /** * 交互通道 * * @param listenSocket * @return * @throws Exception */ SocketChannel newSocketChannel(Socket listenSocket) throws Exception; /** * 字符集 * * @return */ Charset getCharset(); } ================================================ FILE: src/main/java/person/pluto/natcross2/serverside/client/config/SecretSimpleClientServiceConfig.java ================================================ package person.pluto.natcross2.serverside.client.config; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import person.pluto.natcross2.channel.SecretInteractiveChannel; import person.pluto.natcross2.channel.SocketChannel; import person.pluto.natcross2.model.InteractiveModel; import person.pluto.natcross2.utils.AESUtil; import java.net.Socket; import java.security.Key; /** *

* 隧道过程加密的配置类 *

* * @author Pluto * @since 2020-01-08 16:44:42 */ @Data @NoArgsConstructor @EqualsAndHashCode(callSuper = false) public class SecretSimpleClientServiceConfig extends SimpleClientServiceConfig { /** * 签名混淆key */ private String tokenKey; /** * 隧道过程加密key AES */ private Key aesKey; public SecretSimpleClientServiceConfig(Integer listenPort) { super(listenPort); } @Override public SocketChannel newSocketChannel(Socket listenSocket) throws Exception { SecretInteractiveChannel channel = new SecretInteractiveChannel(); channel.setCharset(this.getCharset()); channel.setTokenKey(this.tokenKey); channel.setAesKey(this.aesKey); channel.setSocket(listenSocket); return channel; } /** * BASE64格式设置隧道加密密钥 * * @param aesKey */ public void setBaseAesKey(String aesKey) { this.aesKey = AESUtil.createKeyByBase64(aesKey); } } ================================================ FILE: src/main/java/person/pluto/natcross2/serverside/client/config/SimpleClientServiceConfig.java ================================================ package person.pluto.natcross2.serverside.client.config; import lombok.NoArgsConstructor; import person.pluto.natcross2.channel.InteractiveChannel; import person.pluto.natcross2.channel.SocketChannel; import person.pluto.natcross2.model.InteractiveModel; import person.pluto.natcross2.serverside.client.adapter.DefaultReadAheadPassValueAdapter; import person.pluto.natcross2.serverside.client.adapter.IClientServiceAdapter; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; import java.nio.channels.ServerSocketChannel; import java.nio.channels.spi.SelectorProvider; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; /** *

* 简单交互的客户端服务配置 *

* * @author Pluto * @since 2020-01-08 16:45:47 */ @NoArgsConstructor public class SimpleClientServiceConfig implements IClientServiceConfig { private Integer listenPort; private IClientServiceAdapter clientServiceAdapter = new DefaultReadAheadPassValueAdapter(this); private Charset charset = StandardCharsets.UTF_8; public SimpleClientServiceConfig(Integer listenPort) { this.listenPort = listenPort; } public void setListenPort(int listenPort) { this.listenPort = listenPort; } @Override public Integer getListenPort() { return this.listenPort; } @Override public ServerSocket createServerSocket() throws Exception { ServerSocketChannel openServerSocketChannel = SelectorProvider.provider().openServerSocketChannel(); openServerSocketChannel.bind(new InetSocketAddress(this.getListenPort())); return openServerSocketChannel.socket(); // return new ServerSocket(this.getListenPort()); } /** * 设置适配器 * * @param clientServiceAdapter */ public void setClientServiceAdapter(IClientServiceAdapter clientServiceAdapter) { this.clientServiceAdapter = clientServiceAdapter; } @Override public IClientServiceAdapter getClientServiceAdapter() { return this.clientServiceAdapter; } @Override public SocketChannel newSocketChannel(Socket listenSocket) throws Exception { InteractiveChannel channel = new InteractiveChannel(); channel.setSocket(listenSocket); channel.setCharset(this.charset); return channel; } @Override public Charset getCharset() { return this.charset; } /** * 设置字符编码 * * @param charset */ public void setCharset(Charset charset) { this.charset = charset; } } ================================================ FILE: src/main/java/person/pluto/natcross2/serverside/client/handler/DefaultInteractiveProcessHandler.java ================================================ package person.pluto.natcross2.serverside.client.handler; import person.pluto.natcross2.serverside.client.process.ClientConnectProcess; import person.pluto.natcross2.serverside.client.process.ClientControlProcess; /** *

* 默认的接收处理handler *

* * @author Pluto * @since 2021-04-26 17:22:31 */ public class DefaultInteractiveProcessHandler extends InteractiveProcessHandler { public static final DefaultInteractiveProcessHandler INSTANCE = new DefaultInteractiveProcessHandler(); public DefaultInteractiveProcessHandler() { this.addLast(ClientControlProcess.INSTANCE); this.addLast(ClientConnectProcess.INSTANCE); } } ================================================ FILE: src/main/java/person/pluto/natcross2/serverside/client/handler/IPassValueHandler.java ================================================ package person.pluto.natcross2.serverside.client.handler; import person.pluto.natcross2.channel.SocketChannel; import person.pluto.natcross2.common.Optional; import person.pluto.natcross2.serverside.client.adapter.PassValueNextEnum; /** *

* 传值方式客户端是配置的处理接口 *

* * @param * @param * @author Pluto * @since 2020-01-08 16:47:40 */ public interface IPassValueHandler { /** * 处理方法 * * @param socketChannel 交互通道 * @param optional 可以重设值 * @return */ PassValueNextEnum proc(SocketChannel socketChannel, Optional optional); } ================================================ FILE: src/main/java/person/pluto/natcross2/serverside/client/handler/InteractiveProcessHandler.java ================================================ package person.pluto.natcross2.serverside.client.handler; import lombok.extern.slf4j.Slf4j; import person.pluto.natcross2.channel.SocketChannel; import person.pluto.natcross2.common.Optional; import person.pluto.natcross2.model.InteractiveModel; import person.pluto.natcross2.model.enumeration.InteractiveTypeEnum; import person.pluto.natcross2.model.enumeration.NatcrossResultEnum; import person.pluto.natcross2.serverside.client.adapter.PassValueNextEnum; import person.pluto.natcross2.serverside.client.process.IProcess; import java.util.LinkedList; import java.util.List; /** *

* 常规接收处理handler *

* * @author Pluto * @since 2020-01-06 10:20:11 */ @Slf4j public class InteractiveProcessHandler implements IPassValueHandler { /** * 处理链表 */ private List processList = new LinkedList<>(); @Override public PassValueNextEnum proc(SocketChannel socketChannel, Optional optional) { InteractiveModel value = optional.getValue(); log.info("接收到新消息:[ {} ]", value); for (IProcess process : this.processList) { boolean wouldProc = process.wouldProc(value); if (wouldProc) { boolean processMethod; try { processMethod = process.processMethod(socketChannel, value); } catch (Exception e) { log.error("处理任务时发生异常", e); return PassValueNextEnum.STOP_CLOSE; } if (processMethod) { return PassValueNextEnum.STOP_KEEP; } else { return PassValueNextEnum.STOP_CLOSE; } } } try { socketChannel.writeAndFlush(InteractiveModel.of(value.getInteractiveSeq(), InteractiveTypeEnum.COMMON_REPLY, NatcrossResultEnum.UNKNOWN_INTERACTIVE_TYPE.toResultModel())); } catch (Exception e) { log.error("发送消息时异常", e); } return PassValueNextEnum.STOP_CLOSE; } /** * 将处理方法加入后面 * * @param process * @return */ public InteractiveProcessHandler addLast(IProcess process) { this.processList.add(process); return this; } /** * 获取处理链表,可以代理维护 * * @return */ public List getProcessList() { return this.processList; } /** * 设置处理链表 * * @param processList * @return */ public InteractiveProcessHandler setProcessList(List processList) { this.processList = processList; return this; } } ================================================ FILE: src/main/java/person/pluto/natcross2/serverside/client/process/ClientConnectProcess.java ================================================ package person.pluto.natcross2.serverside.client.process; import person.pluto.natcross2.channel.SocketChannel; import person.pluto.natcross2.common.CommonFormat; import person.pluto.natcross2.model.InteractiveModel; import person.pluto.natcross2.model.NatcrossResultModel; import person.pluto.natcross2.model.enumeration.InteractiveTypeEnum; import person.pluto.natcross2.model.enumeration.NatcrossResultEnum; import person.pluto.natcross2.model.interactive.ClientConnectModel; import person.pluto.natcross2.serverside.listen.ListenServerControl; import person.pluto.natcross2.serverside.listen.ServerListenThread; /** *

* 请求建立隧道处理器 *

* * @author Pluto * @since 2020-01-08 16:48:25 */ public class ClientConnectProcess implements IProcess { public static final ClientConnectProcess INSTANCE = new ClientConnectProcess(); @Override public boolean wouldProc(InteractiveModel recvInteractiveModel) { InteractiveTypeEnum interactiveTypeEnum = InteractiveTypeEnum.getEnumByName( recvInteractiveModel.getInteractiveType()); return InteractiveTypeEnum.CLIENT_CONNECT.equals(interactiveTypeEnum); } @Override public boolean processMethod(SocketChannel socketChannel, InteractiveModel recvInteractiveModel) throws Exception { ClientConnectModel clientConnectModel = recvInteractiveModel.getData().toJavaObject(ClientConnectModel.class); Integer listenPort = CommonFormat.getSocketPortByPartKey(clientConnectModel.getSocketPartKey()); ServerListenThread serverListenThread = ListenServerControl.get(listenPort); if (serverListenThread == null) { socketChannel.writeAndFlush( InteractiveModel.of(recvInteractiveModel.getInteractiveSeq(), InteractiveTypeEnum.COMMON_REPLY, NatcrossResultEnum.NO_HAS_SERVER_LISTEN.toResultModel())); return false; } // 回复设置成功,如果doSetPartClient没有找到对应的搭档,则直接按关闭处理 socketChannel.writeAndFlush( InteractiveModel.of(recvInteractiveModel.getInteractiveSeq(), InteractiveTypeEnum.COMMON_REPLY, NatcrossResultModel.ofSuccess())); // 若设置成功,则上层无需关闭 // 若设置失败,则由上层关闭 return serverListenThread.doSetPartClient(clientConnectModel.getSocketPartKey(), socketChannel.getSocket()); } } ================================================ FILE: src/main/java/person/pluto/natcross2/serverside/client/process/ClientControlProcess.java ================================================ package person.pluto.natcross2.serverside.client.process; import person.pluto.natcross2.channel.SocketChannel; import person.pluto.natcross2.model.InteractiveModel; import person.pluto.natcross2.model.NatcrossResultModel; import person.pluto.natcross2.model.enumeration.InteractiveTypeEnum; import person.pluto.natcross2.model.enumeration.NatcrossResultEnum; import person.pluto.natcross2.model.interactive.ClientControlModel; import person.pluto.natcross2.serverside.listen.ListenServerControl; import person.pluto.natcross2.serverside.listen.ServerListenThread; /** *

* 请求建立控制器处理方法 *

* * @author Pluto * @since 2020-01-08 16:48:43 */ public class ClientControlProcess implements IProcess { public static final ClientControlProcess INSTANCE = new ClientControlProcess(); @Override public boolean wouldProc(InteractiveModel recvInteractiveModel) { InteractiveTypeEnum interactiveTypeEnum = InteractiveTypeEnum.getEnumByName( recvInteractiveModel.getInteractiveType()); return InteractiveTypeEnum.CLIENT_CONTROL.equals(interactiveTypeEnum); } @Override public boolean processMethod(SocketChannel socketChannel, InteractiveModel recvInteractiveModel) throws Exception { ClientControlModel clientControlModel = recvInteractiveModel.getData().toJavaObject(ClientControlModel.class); ServerListenThread serverListenThread = ListenServerControl.get(clientControlModel.getListenPort()); if (serverListenThread == null) { socketChannel.writeAndFlush( InteractiveModel.of(recvInteractiveModel.getInteractiveSeq(), InteractiveTypeEnum.COMMON_REPLY, NatcrossResultEnum.NO_HAS_SERVER_LISTEN.toResultModel())); return false; } socketChannel.writeAndFlush( InteractiveModel.of(recvInteractiveModel.getInteractiveSeq(), InteractiveTypeEnum.COMMON_REPLY, NatcrossResultModel.ofSuccess())); serverListenThread.setControlSocket(socketChannel.getSocket()); return true; } } ================================================ FILE: src/main/java/person/pluto/natcross2/serverside/client/process/IProcess.java ================================================ package person.pluto.natcross2.serverside.client.process; import person.pluto.natcross2.channel.SocketChannel; import person.pluto.natcross2.model.InteractiveModel; /** *

* 处理方法接口 *

* * @author Pluto * @since 2020-01-08 16:49:06 */ public interface IProcess { /** * 判断是否是由这个处理 * * @param recvInteractiveModel * @return */ boolean wouldProc(InteractiveModel recvInteractiveModel); /** * 处理方法,需要回复信息的,自己使用socketChannel回复 * * @param socketChannel * @param recvInteractiveModel * @return 是否保持socket开启状态 */ boolean processMethod(SocketChannel socketChannel, InteractiveModel recvInteractiveModel) throws Exception; } ================================================ FILE: src/main/java/person/pluto/natcross2/serverside/listen/IServerListen.java ================================================ package person.pluto.natcross2.serverside.listen; import person.pluto.natcross2.serverside.listen.control.IControlSocket; /** *

* 端口监听服务接口 *

* * @author Pluto * @since 2021-07-01 13:46:20 */ public interface IServerListen { /** * 格式化信息 * * @return */ String formatInfo(); /** * 控制端口通知关闭 * * @param controlSocket */ void controlCloseNotice(IControlSocket controlSocket); } ================================================ FILE: src/main/java/person/pluto/natcross2/serverside/listen/ListenServerControl.java ================================================ package person.pluto.natcross2.serverside.listen; import lombok.AccessLevel; import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; import person.pluto.natcross2.serverside.listen.config.IListenServerConfig; import java.util.LinkedList; import java.util.List; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; /** *

* 转发监听服务控制类 *

* * @author Pluto * @since 2019-07-05 11:25:44 */ @Slf4j @NoArgsConstructor(access = AccessLevel.PRIVATE) public final class ListenServerControl { private static final ConcurrentHashMap serverListenMap = new ConcurrentHashMap<>(); /** * 加入新的监听服务进程 * * @param serverListen * @return */ public static boolean add(ServerListenThread serverListen) { if (serverListen == null) { return false; } Integer listenPort = serverListen.getListenPort(); ServerListenThread serverListenThread = serverListenMap.get(listenPort); if (serverListenThread != null) { // 必须要先remove掉才能add,讲道理如果原先的存在应该直接报错才对,也就是参数为null,所以这里不自动remove return false; } serverListenMap.put(listenPort, serverListen); return true; } /** * 去除指定端口的监听服务端口 * * @param listenPort * @return */ public static boolean remove(Integer listenPort) { ServerListenThread serverListenThread = serverListenMap.remove(listenPort); if (Objects.nonNull(serverListenThread)) { serverListenThread.cancel(); } return true; } /** * 根据端口获取监听服务端口 * * @param listenPort * @return */ public static ServerListenThread get(Integer listenPort) { return serverListenMap.get(listenPort); } /** * 获取全部监听服务 * * @return */ public static List getAll() { List list = new LinkedList<>(); serverListenMap.forEach((key, value) -> list.add(value)); return list; } /** * 关闭所有监听服务 */ public static void closeAll() { Integer[] array = serverListenMap.keySet().toArray(new Integer[0]); for (Integer key : array) { ListenServerControl.remove(key); } } /** * 创建新的监听服务 * * @param config * @return * @author Pluto * @since 2019-07-19 13:59:24 */ public static ServerListenThread createNewListenServer(IListenServerConfig config) { ServerListenThread serverListenThread; try { serverListenThread = new ServerListenThread(config); } catch (Exception e) { log.warn("create listen server [" + config.getListenPort() + "] failed", e); return null; } // 若没有报错则说明没有监听该端口的线程,即不可正常使用原有端口,所以先进行强行remove,再进行add ListenServerControl.remove(config.getListenPort()); ListenServerControl.add(serverListenThread); return serverListenThread; } } ================================================ FILE: src/main/java/person/pluto/natcross2/serverside/listen/ServerListenThread.java ================================================ package person.pluto.natcross2.serverside.listen; import lombok.extern.slf4j.Slf4j; import person.pluto.natcross2.api.IBelongControl; import person.pluto.natcross2.api.socketpart.AbsSocketPart; import person.pluto.natcross2.common.CommonFormat; import person.pluto.natcross2.executor.NatcrossExecutor; import person.pluto.natcross2.nio.INioProcessor; import person.pluto.natcross2.nio.NioHallows; import person.pluto.natcross2.serverside.listen.clear.IClearInvalidSocketPartThread; import person.pluto.natcross2.serverside.listen.config.IListenServerConfig; import person.pluto.natcross2.serverside.listen.control.IControlSocket; import person.pluto.natcross2.utils.Assert; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; import java.nio.channels.SelectionKey; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.*; import java.util.concurrent.ConcurrentHashMap; /** *

* 监听转发服务进程 *

* * @author Pluto * @since 2019-07-05 10:53:33 */ @Slf4j public final class ServerListenThread implements Runnable, INioProcessor, IBelongControl, IServerListen { private volatile Thread myThread = null; private volatile boolean isAlive = false; private volatile boolean canceled = false; private final IListenServerConfig config; private final ServerSocket listenServerSocket; private final ConcurrentHashMap socketPartMap = new ConcurrentHashMap<>(); private volatile IControlSocket controlSocket; private volatile IClearInvalidSocketPartThread clearInvalidSocketPartThread; public ServerListenThread(IListenServerConfig config) throws Exception { this.config = config; // 此处就开始占用端口,防止重复占用端口,和启动时已被占用 this.listenServerSocket = this.config.createServerSocket(); log.info("server listen port[{}] is created!", this.getListenPort()); } @Override public void run() { while (this.isAlive) { try { Socket listenSocket = this.listenServerSocket.accept(); this.procMethod(listenSocket); } catch (Exception e) { log.warn("监听服务[" + this.getListenPort() + "]服务异常", e); this.cancel(); } } } @Override public void process(SelectionKey key) { if (!key.isValid()) { this.cancel(); } try { ServerSocketChannel channel = (ServerSocketChannel) key.channel(); SocketChannel accept = channel.accept(); for (; Objects.nonNull(accept); accept = channel.accept()) { this.procMethod(accept.socket()); } } catch (IOException e) { log.warn("监听服务[" + this.getListenPort() + "]服务异常", e); this.cancel(); } } /** * 任务执行方法 * * @param listenSocket */ private void procMethod(Socket listenSocket) { NatcrossExecutor.executeServerListenAccept(() -> { // 如果没有控制接收socket,则取消接入,不主动关闭所有接口,防止controlSocket临时掉线,讲道理没有controlSocket也不会启动 if (Objects.isNull(this.controlSocket)) { try { listenSocket.close(); } catch (IOException e) { // do nothing } return; } String socketPartKey = CommonFormat.generateSocketPartKey(this.getListenPort()); AbsSocketPart socketPart = this.config.newSocketPart(this); socketPart.setSocketPartKey(socketPartKey); socketPart.setRecvSocket(listenSocket); this.socketPartMap.put(socketPartKey, socketPart); // 发送指令失败,同controlSocket为空,不使用异步执行,毕竟接口发送只能顺序,异步的方式也会被锁,等同同步 if (!this.sendClientWait(socketPartKey)) { this.socketPartMap.remove(socketPartKey); socketPart.cancel(); } }); } /** * * 告知客户端,有新连接 * * @param socketPartKey * @author Pluto * @since 2019-07-11 15:45:14 */ private boolean sendClientWait(String socketPartKey) { log.info("告知新连接 sendClientWait[{}]", socketPartKey); boolean sendClientWait; IControlSocket controlSocket = this.controlSocket; try { sendClientWait = controlSocket.sendClientWait(socketPartKey); } catch (Throwable e) { log.error("告知新连接 sendClientWait[" + socketPartKey + "] 发生未知异常", e); sendClientWait = false; } if (!sendClientWait) { log.warn("告知新连接 sendClientWait[" + socketPartKey + "] 失败"); if (!controlSocket.isValid()) { // 保证control为置空状态 this.stopListen(); } return false; } return true; } /** * * 启动 * * @throws Exception * @author Pluto * @since 2020-01-07 09:36:30 */ private void start() { Assert.state(!this.canceled, "已退出,不得重新启动"); if (this.isAlive) { return; } this.isAlive = true; log.info("server listen port[{}] starting ...", this.getListenPort()); if (this.clearInvalidSocketPartThread == null) { this.clearInvalidSocketPartThread = this.config.newClearInvalidSocketPartThread(this); if (this.clearInvalidSocketPartThread != null) { this.clearInvalidSocketPartThread.start(); } } ServerSocketChannel channel = this.listenServerSocket.getChannel(); if (Objects.nonNull(channel)) { try { NioHallows.register(channel, SelectionKey.OP_ACCEPT, this); } catch (IOException e) { log.error("register serverListen channel[{}] failed!", config.getListenPort()); this.cancel(); throw new RuntimeException("nio注册时异常", e); } } else { Thread myThread = this.myThread; if (myThread == null || !myThread.isAlive()) { myThread = this.myThread = new Thread(this); myThread.setName("server-listen-" + this.formatInfo()); myThread.start(); } } log.info("server listen port[{}] start success!", this.getListenPort()); } /** * * 关停监听服务,不注销已经建立的,并置空controlSocket * * @author Pluto * @since 2019-07-18 18:43:43 */ private synchronized void stopListen() { log.info("stopListen[{}]", this.getListenPort()); this.isAlive = false; NioHallows.release(this.listenServerSocket.getChannel()); Thread myThread; if ((myThread = this.myThread) != null) { this.myThread = null; myThread.interrupt(); } IControlSocket controlSocket = this.controlSocket; if (Objects.nonNull(controlSocket)) { this.controlSocket = null; try { controlSocket.close(); } catch (Exception e) { log.debug("监听服务控制端口关闭异常", e); } } } /** * * 退出 * * @author Pluto * @since 2020-01-07 09:37:00 */ public synchronized void cancel() { if (this.canceled) { return; } this.canceled = true; log.info("serverListen cancelling[{}]", this.getListenPort()); ListenServerControl.remove(this.getListenPort()); this.stopListen(); try { this.listenServerSocket.close(); } catch (Exception e) { // do nothing } IClearInvalidSocketPartThread clearInvalidSocketPartThread; if ((clearInvalidSocketPartThread = this.clearInvalidSocketPartThread) != null) { this.clearInvalidSocketPartThread = null; try { clearInvalidSocketPartThread.cancel(); } catch (Exception e) { // do nothing } } String[] socketPartKeyArray = this.socketPartMap.keySet().toArray(new String[0]); for (String key : socketPartKeyArray) { this.stopSocketPart(key); } log.debug("serverListen cancel[{}] is success", this.getListenPort()); } /** * * 停止指定的端口 * * @param socketPartKey * @return * @author Pluto * @since 2019-07-11 16:33:10 */ @Override public boolean stopSocketPart(String socketPartKey) { log.debug("停止接口 stopSocketPart[{}]", socketPartKey); AbsSocketPart socketPart = this.socketPartMap.remove(socketPartKey); if (socketPart == null) { return false; } socketPart.cancel(); return true; } /** * * 清理无效socketPart * * @author Pluto * @since 2019-08-21 12:50:57 */ public void clearInvalidSocketPart() { log.debug("clearInvalidSocketPart[{}]", this.getListenPort()); ConcurrentHashMap socketPartMap = this.socketPartMap; Set keySet = socketPartMap.keySet(); // 被去除的时候 set会变化而导致空值问题 String[] array = keySet.toArray(new String[0]); for (String key : array) { AbsSocketPart socketPart = socketPartMap.get(key); if (socketPart != null && !socketPart.isValid()) { this.stopSocketPart(key); } } } /** * * 将接受到的连接进行设置组合 * * @param socketPartKey * @param sendSocket * @return */ public boolean doSetPartClient(String socketPartKey, Socket sendSocket) { log.debug("接入接口 doSetPartClient[{}]", socketPartKey); AbsSocketPart socketPart = this.socketPartMap.get(socketPartKey); if (socketPart == null) { return false; } socketPart.setSendSocket(sendSocket); boolean createPassWay = socketPart.createPassWay(); if (!createPassWay) { socketPart.cancel(); this.stopSocketPart(socketPartKey); return false; } return true; } /** * * 设置控制端口 * * @param socket */ public synchronized void setControlSocket(Socket socket) { log.info("setControlSocket[{}]", this.getListenPort()); IControlSocket controlSocketNew = this.config.newControlSocket(socket, null); IControlSocket controlSocket; if ((controlSocket = this.controlSocket) != null) { this.controlSocket = null; try { controlSocket.replaceClose(); } catch (Exception e) { log.debug("监听服务控制端口关闭异常", e); } } controlSocketNew.setServerListen(this); controlSocketNew.startRecv(); this.controlSocket = controlSocketNew; this.start(); } @Override public synchronized void controlCloseNotice(IControlSocket controlSocket) { if (Objects.equals(controlSocket, this.controlSocket)) { this.stopListen(); } } /** * * 获取监听端口 * * @return * @author Pluto * @since 2019-07-18 18:45:57 */ public Integer getListenPort() { return this.config.getListenPort(); } /** * * 获取已建立的连接 * * @return * @author Pluto * @since 2019-07-19 16:32:09 */ public List getSocketPartList() { return new LinkedList<>(this.socketPartMap.keySet()); } /** * * 获取socket对 * * @return * @author Pluto * @since 2020-01-07 09:43:08 */ public Map getSocketPartMap() { return this.socketPartMap; } /** * * 是否激活状态 * * @return * @author Pluto * @since 2020-01-07 09:43:28 */ public boolean isAlive() { return this.isAlive; } /** * 是否已退出 * * @return * @author Pluto * @since 2021-04-13 13:39:11 */ public boolean isCanceled() { return this.canceled; } /** * 获取配置 * * @return * @author Pluto * @since 2020-01-08 16:55:41 */ public IListenServerConfig getConfig() { return this.config; } public String formatInfo() { return String.valueOf(this.getListenPort()); } } ================================================ FILE: src/main/java/person/pluto/natcross2/serverside/listen/clear/ClearInvalidSocketPartThread.java ================================================ package person.pluto.natcross2.serverside.listen.clear; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import person.pluto.natcross2.executor.NatcrossExecutor; import person.pluto.natcross2.serverside.listen.ServerListenThread; import java.util.Objects; import java.util.concurrent.ScheduledFuture; /** *

* 清理无效端口 *

* * @author Pluto * @since 2019-08-21 12:59:03 */ @Slf4j public class ClearInvalidSocketPartThread implements IClearInvalidSocketPartThread { private final ServerListenThread serverListenThread; private ScheduledFuture scheduledFuture; /** * 清理间隔 */ @Setter @Getter private long clearIntervalSeconds = 10L; public ClearInvalidSocketPartThread(ServerListenThread serverListenThread) { this.serverListenThread = serverListenThread; } @Override public void run() { this.serverListenThread.clearInvalidSocketPart(); } @Override public synchronized void start() { ScheduledFuture scheduledFuture = this.scheduledFuture; if (Objects.isNull(scheduledFuture) || scheduledFuture.isCancelled()) { this.scheduledFuture = NatcrossExecutor.scheduledClearInvalidSocketPart(this, this.clearIntervalSeconds); } log.info("ClearInvalidSocketPartThread for [{}] started !", this.serverListenThread.getListenPort()); } @Override public void cancel() { ScheduledFuture scheduledFuture = this.scheduledFuture; if (Objects.nonNull(scheduledFuture) && !scheduledFuture.isCancelled()) { this.scheduledFuture = null; scheduledFuture.cancel(false); } log.info("ClearInvalidSocketPartThread for [{}] cancell !", this.serverListenThread.getListenPort()); } } ================================================ FILE: src/main/java/person/pluto/natcross2/serverside/listen/clear/IClearInvalidSocketPartThread.java ================================================ package person.pluto.natcross2.serverside.listen.clear; /** *

* 清理无效端口 线程 *

* * @author Pluto * @since 2020-01-08 16:50:09 */ public interface IClearInvalidSocketPartThread extends Runnable { /** * 启动 */ void start(); /** * 退出 */ void cancel(); } ================================================ FILE: src/main/java/person/pluto/natcross2/serverside/listen/config/AllSecretSimpleListenServerConfig.java ================================================ package person.pluto.natcross2.serverside.listen.config; import lombok.Getter; import lombok.Setter; import person.pluto.natcross2.api.secret.AESSecret; import person.pluto.natcross2.api.socketpart.AbsSocketPart; import person.pluto.natcross2.api.socketpart.SecretSocketPart; import person.pluto.natcross2.serverside.listen.ServerListenThread; import person.pluto.natcross2.utils.AESUtil; import java.security.Key; /** *

* 交互及隧道都加密 *

* * @author Pluto * @since 2020-01-08 15:05:55 */ public class AllSecretSimpleListenServerConfig extends SecretSimpleListenServerConfig { @Setter @Getter private Key passwayKey; public AllSecretSimpleListenServerConfig(Integer listenPort) { super(listenPort); } @Override public AbsSocketPart newSocketPart(ServerListenThread serverListenThread) { AESSecret secret = new AESSecret(); secret.setAesKey(this.passwayKey); SecretSocketPart secretSocketPart = new SecretSocketPart(serverListenThread); secretSocketPart.setSecret(secret); return secretSocketPart; } /** * BASE64格式设置隧道加密密钥 * * @param key */ public void setBasePasswayKey(String key) { this.passwayKey = AESUtil.createKeyByBase64(key); } } ================================================ FILE: src/main/java/person/pluto/natcross2/serverside/listen/config/IListenServerConfig.java ================================================ package person.pluto.natcross2.serverside.listen.config; import com.alibaba.fastjson.JSONObject; import person.pluto.natcross2.api.socketpart.AbsSocketPart; import person.pluto.natcross2.serverside.listen.ServerListenThread; import person.pluto.natcross2.serverside.listen.clear.IClearInvalidSocketPartThread; import person.pluto.natcross2.serverside.listen.control.IControlSocket; import java.net.ServerSocket; import java.net.Socket; import java.nio.charset.Charset; /** *

* 穿透监听服务配置 *

* * @author Pluto * @since 2020-01-08 16:51:04 */ public interface IListenServerConfig { /** * 获取监听的端口 * * @return */ Integer getListenPort(); /** * 新建无效端口处理线程 * * @param serverListenThread * @return */ IClearInvalidSocketPartThread newClearInvalidSocketPartThread(ServerListenThread serverListenThread); /** * 创建隧道伙伴 * * @param serverListenThread * @return */ AbsSocketPart newSocketPart(ServerListenThread serverListenThread); /** * 获取字符集 * * @return */ Charset getCharset(); /** * 新建控制器 * * @param socket * @param config * @return */ IControlSocket newControlSocket(Socket socket, JSONObject config); /** * 创建监听端口 * * @return * @throws Exception */ ServerSocket createServerSocket() throws Exception; } ================================================ FILE: src/main/java/person/pluto/natcross2/serverside/listen/config/MultControlListenServerConfig.java ================================================ package person.pluto.natcross2.serverside.listen.config; import com.alibaba.fastjson.JSONObject; import person.pluto.natcross2.api.socketpart.AbsSocketPart; import person.pluto.natcross2.serverside.listen.ServerListenThread; import person.pluto.natcross2.serverside.listen.clear.IClearInvalidSocketPartThread; import person.pluto.natcross2.serverside.listen.control.IControlSocket; import person.pluto.natcross2.serverside.listen.control.MultiControlSocket; import java.net.ServerSocket; import java.net.Socket; import java.nio.charset.Charset; /** *

* 多客户端;监听服务配置 *

* * @author Pluto * @since 2020-01-08 16:53:17 */ public class MultControlListenServerConfig implements IListenServerConfig { protected final IListenServerConfig baseConfig; protected final MultiControlSocket multControlSocket = new MultiControlSocket(); public MultControlListenServerConfig(IListenServerConfig baseConfig) { this.baseConfig = baseConfig; } @Override public ServerSocket createServerSocket() throws Exception { return this.baseConfig.createServerSocket(); } @Override public IControlSocket newControlSocket(Socket socket, JSONObject config) { IControlSocket controlSocket = this.baseConfig.newControlSocket(socket, config); multControlSocket.addControlSocket(controlSocket); return multControlSocket; } @Override public IClearInvalidSocketPartThread newClearInvalidSocketPartThread(ServerListenThread serverListenThread) { return this.baseConfig.newClearInvalidSocketPartThread(serverListenThread); } @Override public AbsSocketPart newSocketPart(ServerListenThread serverListenThread) { return this.baseConfig.newSocketPart(serverListenThread); } @Override public Integer getListenPort() { return this.baseConfig.getListenPort(); } @Override public Charset getCharset() { return this.baseConfig.getCharset(); } } ================================================ FILE: src/main/java/person/pluto/natcross2/serverside/listen/config/SecretSimpleListenServerConfig.java ================================================ package person.pluto.natcross2.serverside.listen.config; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; import person.pluto.natcross2.channel.SecretInteractiveChannel; import person.pluto.natcross2.channel.SocketChannel; import person.pluto.natcross2.model.InteractiveModel; import person.pluto.natcross2.utils.AESUtil; import java.io.IOException; import java.net.Socket; import java.security.Key; /** *

* 交互加密配置 *

* * @author Pluto * @since 2020-01-08 16:52:51 */ @Slf4j @Data @NoArgsConstructor @EqualsAndHashCode(callSuper = false) public class SecretSimpleListenServerConfig extends SimpleListenServerConfig { private String tokenKey; private Key aesKey; public SecretSimpleListenServerConfig(Integer listenPort) { super(listenPort); } @Override protected SocketChannel newControlSocketChannel( Socket socket) { SecretInteractiveChannel channel = new SecretInteractiveChannel(); channel.setCharset(this.getCharset()); channel.setTokenKey(this.tokenKey); channel.setAesKey(this.aesKey); try { channel.setSocket(socket); } catch (IOException e) { log.error("newControlSocketChannel exception", e); return null; } return channel; } /** * BASE64格式设置交互加密密钥 * * @param aesKey */ public void setBaseAesKey(String aesKey) { this.aesKey = AESUtil.createKeyByBase64(aesKey); } } ================================================ FILE: src/main/java/person/pluto/natcross2/serverside/listen/config/SimpleListenServerConfig.java ================================================ package person.pluto.natcross2.serverside.listen.config; import com.alibaba.fastjson.JSONObject; import lombok.Data; import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; import person.pluto.natcross2.api.socketpart.AbsSocketPart; import person.pluto.natcross2.api.socketpart.SimpleSocketPart; import person.pluto.natcross2.channel.InteractiveChannel; import person.pluto.natcross2.channel.SocketChannel; import person.pluto.natcross2.model.InteractiveModel; import person.pluto.natcross2.serverside.listen.ServerListenThread; import person.pluto.natcross2.serverside.listen.clear.ClearInvalidSocketPartThread; import person.pluto.natcross2.serverside.listen.clear.IClearInvalidSocketPartThread; import person.pluto.natcross2.serverside.listen.control.ControlSocket; import person.pluto.natcross2.serverside.listen.control.IControlSocket; import person.pluto.natcross2.serverside.listen.recv.ClientHeartHandler; import person.pluto.natcross2.serverside.listen.recv.CommonReplyHandler; import person.pluto.natcross2.serverside.listen.serversocket.ICreateServerSocket; import java.io.IOException; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; import java.nio.channels.ServerSocketChannel; import java.nio.channels.spi.SelectorProvider; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; /** *

* 简单的交互、隧道;监听服务配置 *

* * @author Pluto * @since 2020-01-08 16:53:17 */ @Slf4j @Data @NoArgsConstructor public class SimpleListenServerConfig implements IListenServerConfig { private Integer listenPort; private Long invaildMillis = 60000L; private Long clearInterval = 10L; private Charset charset = StandardCharsets.UTF_8; private ICreateServerSocket createServerSocket; private int streamCacheSize = 8196; public SimpleListenServerConfig(Integer listenPort) { this.listenPort = listenPort; } @Override public ServerSocket createServerSocket() throws Exception { if (this.createServerSocket == null) { ServerSocketChannel openServerSocketChannel = SelectorProvider.provider().openServerSocketChannel(); openServerSocketChannel.bind(new InetSocketAddress(this.getListenPort())); return openServerSocketChannel.socket(); // return new ServerSocket(this.getListenPort()); } else { return this.createServerSocket.createServerSocket(this.getListenPort()); } } /** * 创建controlSocket使用channel * * @param socket * @return * @author Pluto * @since 2020-04-15 13:19:49 */ protected SocketChannel newControlSocketChannel( Socket socket) { InteractiveChannel interactiveChannel; try { interactiveChannel = new InteractiveChannel(socket); } catch (IOException e) { log.error("newControlSocketChannel exception", e); return null; } return interactiveChannel; } @Override public IControlSocket newControlSocket(Socket socket, JSONObject config) { SocketChannel controlSocketChannel = this .newControlSocketChannel(socket); ControlSocket controlSocket = new ControlSocket(controlSocketChannel); controlSocket.addRecvHandler(CommonReplyHandler.INSTANCE); controlSocket.addRecvHandler(ClientHeartHandler.INSTANCE); return controlSocket; } @Override public IClearInvalidSocketPartThread newClearInvalidSocketPartThread(ServerListenThread serverListenThread) { ClearInvalidSocketPartThread clearInvalidSocketPartThread = new ClearInvalidSocketPartThread( serverListenThread); clearInvalidSocketPartThread.setClearIntervalSeconds(this.getClearInterval()); return clearInvalidSocketPartThread; } @Override public AbsSocketPart newSocketPart(ServerListenThread serverListenThread) { SimpleSocketPart socketPart = new SimpleSocketPart(serverListenThread); socketPart.setInvalidMillis(this.getInvaildMillis()); socketPart.setStreamCacheSize(this.getStreamCacheSize()); return socketPart; } } ================================================ FILE: src/main/java/person/pluto/natcross2/serverside/listen/control/ControlSocket.java ================================================ package person.pluto.natcross2.serverside.listen.control; import lombok.extern.slf4j.Slf4j; import person.pluto.natcross2.channel.SocketChannel; import person.pluto.natcross2.model.InteractiveModel; import person.pluto.natcross2.model.enumeration.InteractiveTypeEnum; import person.pluto.natcross2.model.enumeration.NatcrossResultEnum; import person.pluto.natcross2.model.interactive.ServerWaitModel; import person.pluto.natcross2.serverside.listen.IServerListen; import person.pluto.natcross2.serverside.listen.recv.IRecvHandler; import java.io.IOException; import java.net.Socket; import java.util.LinkedList; import java.util.List; import java.util.Objects; /** *

* 控制socket实例 *

* * @author Pluto * @since 2019-07-17 11:03:56 */ @Slf4j public class ControlSocket implements IControlSocket, Runnable { private volatile Thread myThread = null; private volatile boolean started = false; private volatile boolean cancelled = false; protected final SocketChannel socketChannel; protected List> recvHandlerList = new LinkedList<>(); protected IServerListen serverListen; public ControlSocket(SocketChannel socketChannel) { this.socketChannel = socketChannel; } @Override public boolean isValid() { SocketChannel socketChannel = this.socketChannel; Socket socket = (socketChannel == null) ? null : socketChannel.getSocket(); boolean closeFlag = socket == null || !socket.isConnected() || socket.isClosed() || socket.isInputShutdown() || socket.isOutputShutdown(); if (closeFlag) { return false; } try { // 心跳测试 InteractiveModel interactiveModel = InteractiveModel.of(InteractiveTypeEnum.HEART_TEST, null); socketChannel.writeAndFlush(interactiveModel); } catch (Exception e) { return false; } return true; } @Override public void close() { if (this.cancelled) { return; } this.cancelled = true; Thread myThread; if ((myThread = this.myThread) != null) { this.myThread = null; myThread.interrupt(); } if (this.socketChannel != null) { try { this.socketChannel.close(); } catch (IOException e) { // do nothing } } IServerListen serverListenThread = this.serverListen; if (Objects.nonNull(serverListenThread)) { this.serverListen = null; serverListenThread.controlCloseNotice(this); } } @Override public boolean sendClientWait(String socketPartKey) { InteractiveModel model = InteractiveModel.of(InteractiveTypeEnum.SERVER_WAIT_CLIENT, new ServerWaitModel(socketPartKey)); try { this.socketChannel.writeAndFlush(model); } catch (Throwable e) { return false; } return true; } @Override public void startRecv() { if (this.started) { return; } this.started = true; Thread myThread = this.myThread; if (myThread == null || !myThread.isAlive()) { myThread = this.myThread = new Thread(this); myThread.setName("control-recv-" + this.formatServerListenInfo()); myThread.start(); } } @Override public void run() { SocketChannel socketChannel = this.socketChannel; while (this.started && !this.cancelled) { try { InteractiveModel interactiveModel = socketChannel.read(); log.info("监听线程 [{}] 接收到控制端口发来的消息:[ {} ]", this.formatServerListenInfo(), interactiveModel); boolean proc = false; for (IRecvHandler handler : this.recvHandlerList) { proc = handler.proc(interactiveModel, this.socketChannel); if (proc) { break; } } if (!proc) { log.warn("无处理方法的信息:[{}]", interactiveModel); InteractiveModel result = InteractiveModel.of(interactiveModel.getInteractiveSeq(), InteractiveTypeEnum.COMMON_REPLY, NatcrossResultEnum.UNKNOWN_INTERACTIVE_TYPE.toResultModel()); socketChannel.writeAndFlush(result); } } catch (Exception e) { log.error("读取或写入异常", e); if (e instanceof IOException || !this.isValid()) { this.close(); } } } } private String formatServerListenInfo() { if (Objects.isNull(this.serverListen)) { return null; } return this.serverListen.formatInfo(); } @Override public void setServerListen(IServerListen serverListen) { this.serverListen = serverListen; } /** * 添加处理器 * * @param handler * @return * @author Pluto * @since 2020-04-15 13:13:24 */ public ControlSocket addRecvHandler(IRecvHandler handler) { this.recvHandlerList.add(handler); return this; } } ================================================ FILE: src/main/java/person/pluto/natcross2/serverside/listen/control/IControlSocket.java ================================================ package person.pluto.natcross2.serverside.listen.control; import person.pluto.natcross2.serverside.listen.IServerListen; /** *

* 控制端口接口 *

* * @author Pluto * @since 2020-01-07 09:52:51 */ public interface IControlSocket { /** * 是否有效 * * @return */ boolean isValid(); /** * 发送隧道等待状态 * * @param socketPartKey * @return */ boolean sendClientWait(String socketPartKey); /** * 关闭 */ void close(); /** * 替换关闭 *

* 适配多客户端模式,若是替换关闭则可能不进行关闭
* 若是传统1对1模式,则等价调用 {@link #close()} * * @since 2.3 */ default void replaceClose() { this.close(); } /** * 开启接收线程 *

* 实现的类需要自己进行幂等性处理 */ void startRecv(); /** * 设置控制的监听线程 * * @param serverListen */ void setServerListen(IServerListen serverListen); } ================================================ FILE: src/main/java/person/pluto/natcross2/serverside/listen/control/MultiControlSocket.java ================================================ package person.pluto.natcross2.serverside.listen.control; import person.pluto.natcross2.serverside.listen.IServerListen; import java.util.Iterator; import java.util.LinkedList; import java.util.Objects; /** *

* 复合 控制socket实例 *

* * @author Pluto * @since 2021-06-18 12:27:47 */ public class MultiControlSocket implements IControlSocket { protected final LinkedList controlSockets = new LinkedList<>(); protected IServerListen serverListen; /** * 增加控制socket * * @param controlSocket * @return */ public synchronized boolean addControlSocket(IControlSocket controlSocket) { return this.controlSockets.add(controlSocket); } @Override public synchronized boolean isValid() { Iterator iterator = controlSockets.iterator(); while (iterator.hasNext()) { IControlSocket controlSocket = iterator.next(); if (controlSocket.isValid()) { return true; } iterator.remove(); } return false; } protected synchronized IControlSocket pollControlSocket(IControlSocket lastControlSocket) { LinkedList controlSockets = this.controlSockets; if (Objects.nonNull(lastControlSocket) && !lastControlSocket.isValid()) { lastControlSocket.close(); controlSockets.remove(lastControlSocket); } IControlSocket controlSocket; if (controlSockets.size() > 1) { controlSocket = controlSockets.poll(); controlSockets.add(controlSocket); } else { controlSocket = controlSockets.peekFirst(); } return controlSocket; } @Override public boolean sendClientWait(String socketPartKey) { IControlSocket controlSocket = null; for (; ; ) { controlSocket = this.pollControlSocket(controlSocket); if (Objects.isNull(controlSocket)) { return false; } boolean sendClientWait = controlSocket.sendClientWait(socketPartKey); if (sendClientWait) { return true; } } } @Override public synchronized void close() { LinkedList controlSockets = this.controlSockets; Iterator iterator = controlSockets.iterator(); while (iterator.hasNext()) { IControlSocket controlSocket = iterator.next(); try { controlSocket.close(); } catch (Throwable e) { // do nothing } iterator.remove(); } } @Override public synchronized void replaceClose() { // do nothing } @Override public synchronized void startRecv() { LinkedList controlSockets = this.controlSockets; for (IControlSocket controlSocket : controlSockets) { try { controlSocket.startRecv(); } catch (Throwable e) { // do nothing } } } private synchronized boolean removeControlSocket(IControlSocket controlSocket) { return controlSockets.remove(controlSocket); } @Override public synchronized void setServerListen(final IServerListen serverListen) { this.serverListen = serverListen; IServerListen serverListenTemp = new IServerListen() { @Override public String formatInfo() { return serverListen.formatInfo(); } @Override public void controlCloseNotice(IControlSocket controlSocket) { MultiControlSocket.this.removeControlSocket(controlSocket); } }; LinkedList controlSockets = this.controlSockets; for (IControlSocket controlSocket : controlSockets) { try { controlSocket.setServerListen(serverListenTemp); } catch (Throwable e) { // do nothing } } } } ================================================ FILE: src/main/java/person/pluto/natcross2/serverside/listen/recv/ClientHeartHandler.java ================================================ package person.pluto.natcross2.serverside.listen.recv; import person.pluto.natcross2.channel.SocketChannel; import person.pluto.natcross2.model.InteractiveModel; import person.pluto.natcross2.model.enumeration.InteractiveTypeEnum; import person.pluto.natcross2.model.enumeration.NatcrossResultEnum; /** *

* 心跳检测 *

* * @author Pluto * @since 2020-04-15 13:02:09 */ public class ClientHeartHandler implements IRecvHandler { public static final ClientHeartHandler INSTANCE = new ClientHeartHandler(); @Override public boolean proc(InteractiveModel model, SocketChannel channel) throws Exception { InteractiveTypeEnum interactiveTypeEnum = InteractiveTypeEnum.getEnumByName(model.getInteractiveType()); if (!InteractiveTypeEnum.HEART_TEST.equals(interactiveTypeEnum)) { return false; } InteractiveModel sendModel = InteractiveModel.of(model.getInteractiveSeq(), InteractiveTypeEnum.HEART_TEST_REPLY, NatcrossResultEnum.SUCCESS.toResultModel()); channel.writeAndFlush(sendModel); return true; } } ================================================ FILE: src/main/java/person/pluto/natcross2/serverside/listen/recv/CommonReplyHandler.java ================================================ package person.pluto.natcross2.serverside.listen.recv; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import person.pluto.natcross2.channel.SocketChannel; import person.pluto.natcross2.model.InteractiveModel; import person.pluto.natcross2.model.enumeration.InteractiveTypeEnum; import java.util.Objects; /** *

* 统一回复 处理器 *

* * @author Pluto * @since 2020-04-15 13:02:09 */ @Slf4j public class CommonReplyHandler implements IRecvHandler { public static final CommonReplyHandler INSTANCE = new CommonReplyHandler(); @Getter @Setter private IRecvHandler handler; @Override public boolean proc(InteractiveModel model, SocketChannel channel) throws Exception { InteractiveTypeEnum interactiveTypeEnum = InteractiveTypeEnum.getEnumByName(model.getInteractiveType()); if (!InteractiveTypeEnum.COMMON_REPLY.equals(interactiveTypeEnum)) { return false; } IRecvHandler handler; if (Objects.isNull(handler = this.handler)) { log.info("common reply: {}", model); return true; } return handler.proc(model, channel); } } ================================================ FILE: src/main/java/person/pluto/natcross2/serverside/listen/recv/IRecvHandler.java ================================================ package person.pluto.natcross2.serverside.listen.recv; import person.pluto.natcross2.channel.SocketChannel; /** *

* 接收处理器 *

* * @author Pluto * @since 2020-04-15 11:13:20 */ public interface IRecvHandler { /** * 处理方法 * * @param model * @param channel * @return * @throws Exception */ boolean proc(R model, SocketChannel channel) throws Exception; } ================================================ FILE: src/main/java/person/pluto/natcross2/serverside/listen/serversocket/ICreateServerSocket.java ================================================ package person.pluto.natcross2.serverside.listen.serversocket; import java.net.ServerSocket; /** *

* 创建服务端口接口 *

* * @author Pluto * @since 2020-01-09 13:28:12 */ public interface ICreateServerSocket { /** * 创建监听服务 * * @param listenPort * @return */ ServerSocket createServerSocket(int listenPort) throws Exception; } ================================================ FILE: src/main/java/person/pluto/natcross2/utils/AESUtil.java ================================================ package person.pluto.natcross2.utils; import lombok.AccessLevel; import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; import javax.crypto.Cipher; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import java.security.Key; import java.security.NoSuchAlgorithmException; import java.util.Base64; /** *

* AES加解密工具 *

* * @author Pluto * @since 2020-01-08 16:56:05 */ @Slf4j @NoArgsConstructor(access = AccessLevel.PRIVATE) public final class AESUtil { /** * 根据base64获取aes密钥 * * @param baseKey * @return */ public static Key createKeyByBase64(String baseKey) { byte[] keyBytes = Base64.getDecoder().decode(baseKey); return new SecretKeySpec(keyBytes, "AES"); } /** * 生成随机密钥 * * @param length * @return */ public static Key createKey(int length) { try { // 生成key KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); // 生成一个length位的随机源,根据传入的字节数组 keyGenerator.init(length); // 产生原始对称密钥 SecretKey secretKey = keyGenerator.generateKey(); // 获得原始对称密钥的字节数组 byte[] keyBytes = secretKey.getEncoded(); // key转换,根据字节数组生成AES密钥 return new SecretKeySpec(keyBytes, "AES"); } catch (NoSuchAlgorithmException e) { log.error("create aes key exception", e); return null; } } /** * 加密并转换为base64,默认加密工作方式 ECB/PKCS5Padding * * @param key * @param content * @return * @throws Exception */ public static String encryptBase64(Key key, byte[] content) throws Exception { byte[] encrypt = encrypt(key, content); return Base64.getEncoder().encodeToString(encrypt); } /** * 加密,默认加密工作方式 ECB/PKCS5Padding * * @param key * @param content * @return * @throws Exception */ public static byte[] encrypt(Key key, byte[] content) throws Exception { return encrypt(key, content, 0, content.length); } /** * 截断,默认加密 * * @param key * @param content * @param inputOffset * @param inputLen * @return * @throws Exception */ public static byte[] encrypt(Key key, byte[] content, int inputOffset, int inputLen) throws Exception { return encrypt(key, content, "ECB/PKCS5Padding", inputOffset, inputLen); } /** * 无截断加密 * * @param key * @param content * @param encryptMethod * @return * @throws Exception */ public static byte[] encrypt(Key key, byte[] content, String encryptMethod) throws Exception { return encrypt(key, content, encryptMethod, 0, content.length); } /** * 对数据进行加密 * * @param key * @param content * @param encryptMethod 加解密工作方式,如 AES/ECB/PKCS5Padding * 只写ECB/PKCS5Padding,前面的AES/已被写死 * @param inputOffset * @param inputLen * @return * @throws Exception */ public static byte[] encrypt(Key key, byte[] content, String encryptMethod, int inputOffset, int inputLen) throws Exception { Cipher cipher = Cipher.getInstance("AES/" + encryptMethod); cipher.init(Cipher.ENCRYPT_MODE, key); return cipher.doFinal(content, inputOffset, inputLen); } /** * 解密BASE64,默认解密工作方式 ECB/PKCS5Padding * * @param key * @param result * @return * @throws Exception */ public static byte[] decryptBase64(Key key, String result) throws Exception { byte[] decode = Base64.getDecoder().decode(result); return decrypt(key, decode); } /** * 解密,默认解密工作方式 ECB/PKCS5Padding * * @param key * @param result * @return * @throws Exception */ public static byte[] decrypt(Key key, byte[] result) throws Exception { return decrypt(key, result, "ECB/PKCS5Padding"); } /** * 对数据进行解密 * * @param key * @param result * @param decryptMethod 加解密工作方式,如 AES/ECB/PKCS5Padding * 只写ECB/PKCS5Padding,前面的AES已被写死 * @return * @throws Exception */ public static byte[] decrypt(Key key, byte[] result, String decryptMethod) throws Exception { Cipher cipher = Cipher.getInstance("AES/" + decryptMethod); cipher.init(Cipher.DECRYPT_MODE, key); return cipher.doFinal(result); } } ================================================ FILE: src/main/java/person/pluto/natcross2/utils/Assert.java ================================================ package person.pluto.natcross2.utils; /** *

* 断言 *

* * @author Pluto * @since 2021-04-13 13:46:51 */ public final class Assert { private Assert() { } /** * 状态判断 * * @param expression * @param message */ public static void state(boolean expression, String message) { if (!expression) { throw new IllegalStateException(message); } } /** * 是否为true * * @param expression * @param message */ public static void isTrue(boolean expression, String message) { if (!expression) { throw new IllegalArgumentException(message); } } /** * 是否为空 * * @param object * @param message */ public static void isNull(Object object, String message) { if (object != null) { throw new IllegalArgumentException(message); } } /** * 是否非空 * * @param object * @param message */ public static void notNull(Object object, String message) { if (object == null) { throw new IllegalArgumentException(message); } } } ================================================ FILE: src/main/java/person/pluto/natcross2/utils/CountWaitLatch.java ================================================ package person.pluto.natcross2.utils; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.AbstractQueuedSynchronizer; /** *

* 可增 计数 门闩 *

* * @author Pluto * @since 2021-04-14 14:19:35 */ public class CountWaitLatch { /** * Synchronization control For CountWaitLatch. Uses AQS state to represent * count. */ private static final class Sync extends AbstractQueuedSynchronizer { private static final long serialVersionUID = 4982264981922014374L; Sync(int count) { setState(count); } int getCount() { return getState(); } @Override protected int tryAcquireShared(int acquires) { if (acquires == 0) { return (getState() == 0) ? acquires : -1; } for (; ; ) { int c = getState(); int nextc = c + acquires; if (compareAndSetState(c, nextc)) { return acquires; } } } @Override protected boolean tryReleaseShared(int releases) { // Decrement count; signal when transition to zero for (; ; ) { int c = getState(); if (c == 0) { return false; } int nextc = c - releases; if (compareAndSetState(c, nextc)) { return nextc == 0; } } } } private final Sync sync; /** * count == 0 */ public CountWaitLatch() { this(0); } /** * @param count >=0 */ public CountWaitLatch(int count) { if (count < 0) { throw new IllegalArgumentException("count < 0"); } this.sync = new Sync(count); } /** * 等待释放 * * @throws InterruptedException */ public void await() throws InterruptedException { sync.acquireSharedInterruptibly(0); } /** * 等待释放 * * @param timeout * @param unit * @return * @throws InterruptedException */ public boolean await(long timeout, TimeUnit unit) throws InterruptedException { return sync.tryAcquireSharedNanos(0, unit.toNanos(timeout)); } /** * count++ */ public void countUp() { sync.acquireShared(1); } /** * --count >= 0 */ public void countDown() { sync.releaseShared(1); } /** * count >= 0 */ public long getCount() { return sync.getCount(); } @Override public String toString() { return super.toString() + "[Count = " + sync.getCount() + "]"; } } ================================================ FILE: src/main/java/person/pluto/natcross2/utils/MD5Signature.java ================================================ package person.pluto.natcross2.utils; import lombok.AccessLevel; import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.Random; /** *

* MD5散列签名 *

* * @author Pluto * @since 2020-01-08 10:13:50 */ @Slf4j @NoArgsConstructor(access = AccessLevel.PRIVATE) public final class MD5Signature { private static final Charset CHARSET = StandardCharsets.UTF_8; private static final String RANDOMBASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; private static final Random RANDOM = new Random(); private static final String HEXBASE = "0123456789abcdef"; /** * 转换为16进制字符 * * @param bytes * @return * @author Pluto * @since 2019-12-05 12:34:48 */ public static String toHexString(byte[] bytes) { StringBuilder stringBuilder = new StringBuilder(bytes.length << 1); for (byte tmp : bytes) { stringBuilder.append(HEXBASE.charAt(tmp >> 4 & 0xf)); stringBuilder.append(HEXBASE.charAt(tmp & 0xf)); } return stringBuilder.toString(); } /** * 获取随机数 * * @param count * @return * @author Pluto * @since 2019-12-05 11:20:35 */ public static String getRandomStr(int count) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < count; ++i) { sb.append(RANDOMBASE.charAt(RANDOM.nextInt(RANDOMBASE.length()))); } return sb.toString(); } /** * integer 转换为 byte[] * * @param source * @return * @author Pluto * @since 2019-12-05 11:20:53 */ public static byte[] intToBytes(int source) { return new byte[]{(byte) ((source >> 24) & 0xFF), (byte) ((source >> 16) & 0xFF), (byte) ((source >> 8) & 0xFF), (byte) (source & 0xFF)}; } /** * byte[] 转 integer * * @param byteArr * @return * @author Pluto * @since 2019-12-05 11:21:07 */ public static int bytes2int(byte[] byteArr) { int count = 0; for (int i = 0; i < 4; ++i) { count <<= 8; count |= byteArr[i] & 255; } return count; } public static String getSignature(String... params) { return getSignature(CHARSET, params); } /** * 对参数进行MD5散列 * * @param params * @return * @throws NoSuchAlgorithmException * @author Pluto * @since 2019-12-05 12:35:52 */ public static String getSignature(Charset charset, String... params) { Arrays.sort(params); StringBuilder stringBuffer = new StringBuilder(); for (int i = 0; i < 4; ++i) { stringBuffer.append(params[i]); } MessageDigest md; try { md = MessageDigest.getInstance("MD5"); } catch (NoSuchAlgorithmException e) { log.error("MessageDigest.getInstance exception", e); return null; } byte[] digest = md.digest(stringBuffer.toString().getBytes(charset)); return toHexString(digest); } } ================================================ FILE: src/main/java/person/pluto/natcross2/utils/Tools.java ================================================ package person.pluto.natcross2.utils; import lombok.AccessLevel; import lombok.NoArgsConstructor; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.channels.WritableByteChannel; /** *

* 无归类的工具集 *

* * @author Pluto * @since 2020-01-08 16:56:35 */ @NoArgsConstructor(access = AccessLevel.PRIVATE) public final class Tools { /** * integer 转换为 byte[] * * @param source * @return */ public static byte[] intToBytes(int source) { return new byte[]{(byte) ((source >> 24) & 0xFF), (byte) ((source >> 16) & 0xFF), (byte) ((source >> 8) & 0xFF), (byte) (source & 0xFF)}; } /** * byte[] 转 integer * * @param byteArr * @return */ public static int bytes2int(byte[] byteArr) { int count = 0; for (int i = 0; i < 4; ++i) { count <<= 8; count |= byteArr[i] & 255; } return count; } /** * 拷贝数据流 * * @param inputStream * @param outputStream * @return * @throws IOException */ public static int streamCopy(InputStream inputStream, OutputStream outputStream) throws IOException { byte[] temp = new byte[8192]; int sum = 0; int len; while ((len = inputStream.read(temp)) > 0) { sum += len; outputStream.write(temp, 0, len); if (inputStream.available() <= 0) { break; } } return sum; } public static int channelWrite(WritableByteChannel channel, ByteBuffer buffer) throws IOException { int sum = 0; while (buffer.hasRemaining()) { sum += channel.write(buffer); } return sum; } } ================================================ FILE: src/main/resources/logback.xml ================================================ natcross2 ${consoleLevel} %d{HH:mm:ss.SSS} [%-10thread] [%-45logger{36}] [%-5level] [%-4line] - %msg%n ================================================ FILE: src/test/java/person/pluto/TestMain.java ================================================ package person.pluto; import java.security.Key; import java.util.Base64; import person.pluto.natcross2.utils.AESUtil; public class TestMain { public static void main(String[] args) throws Exception { Key createKey = AESUtil.createKey(128); System.out.println(Base64.getEncoder().encodeToString(createKey.getEncoded())); } }