├── .gitignore ├── LICENSE ├── README.md ├── README_zh.md ├── pom.xml └── src └── main ├── java └── org │ └── yeauty │ ├── annotation │ ├── BeforeHandshake.java │ ├── EnableWebSocket.java │ ├── NettyWebSocketSelector.java │ ├── OnBinary.java │ ├── OnClose.java │ ├── OnError.java │ ├── OnEvent.java │ ├── OnMessage.java │ ├── OnOpen.java │ ├── PathVariable.java │ ├── RequestParam.java │ └── ServerEndpoint.java │ ├── autoconfigure │ └── NettyWebSocketAutoConfigure.java │ ├── exception │ └── DeploymentException.java │ ├── pojo │ ├── PojoEndpointServer.java │ ├── PojoMethodMapping.java │ └── Session.java │ ├── standard │ ├── EndpointClassPathScanner.java │ ├── HttpServerHandler.java │ ├── ServerEndpointConfig.java │ ├── ServerEndpointExporter.java │ ├── WebSocketServerHandler.java │ └── WebsocketServer.java │ ├── support │ ├── AntPathMatcherWrapper.java │ ├── ByteMethodArgumentResolver.java │ ├── DefaultPathMatcher.java │ ├── EventMethodArgumentResolver.java │ ├── HttpHeadersMethodArgumentResolver.java │ ├── MethodArgumentResolver.java │ ├── PathVariableMapMethodArgumentResolver.java │ ├── PathVariableMethodArgumentResolver.java │ ├── RequestParamMapMethodArgumentResolver.java │ ├── RequestParamMethodArgumentResolver.java │ ├── SessionMethodArgumentResolver.java │ ├── TextMethodArgumentResolver.java │ ├── ThrowableMethodArgumentResolver.java │ └── WsPathMatcher.java │ └── util │ └── SslUtils.java └── resources └── META-INF └── spring.factories /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | .idea 25 | *.iml 26 | target/ 27 | .DS_Store 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | netty-websocket-spring-boot-starter [![License](http://img.shields.io/:license-apache-brightgreen.svg)](http://www.apache.org/licenses/LICENSE-2.0.html) 2 | =================================== 3 | 4 | [中文文档](https://github.com/YeautyYE/netty-websocket-spring-boot-starter/blob/master/README_zh.md) (Chinese Docs) 5 | 6 | ### About 7 | netty-websocket-spring-boot-starter will help you develop WebSocket server by using Netty in spring-boot,it is easy to develop by using annotation like spring-websocket 8 | 9 | ### Requirement 10 | - jdk version 17 11 | 12 | 13 | ### Quick Start 14 | 15 | - add Dependencies: 16 | 17 | ```xml 18 | 19 | org.yeauty 20 | netty-websocket-spring-boot-starter 21 | 0.13.0 22 | 23 | ``` 24 | 25 | - annotate `@ServerEndpoint` on endpoint class,and annotate `@BeforeHandshake`,`@OnOpen`,`@OnClose`,`@OnError`,`@OnMessage`,`@OnBinary`,`@OnEvent` on the method. e.g. 26 | 27 | ```java 28 | @ServerEndpoint(path = "/ws/{arg}") 29 | public class MyWebSocket { 30 | 31 | @BeforeHandshake 32 | public void handshake(Session session, HttpHeaders headers, @RequestParam String req, @RequestParam MultiValueMap reqMap, @PathVariable String arg, @PathVariable Map pathMap){ 33 | session.setSubprotocols("stomp"); 34 | if (!"ok".equals(req)){ 35 | System.out.println("Authentication failed!"); 36 | session.close(); 37 | } 38 | } 39 | 40 | @OnOpen 41 | public void onOpen(Session session, HttpHeaders headers, @RequestParam String req, @RequestParam MultiValueMap reqMap, @PathVariable String arg, @PathVariable Map pathMap){ 42 | System.out.println("new connection"); 43 | System.out.println(req); 44 | } 45 | 46 | @OnClose 47 | public void onClose(Session session) throws IOException { 48 | System.out.println("one connection closed"); 49 | } 50 | 51 | @OnError 52 | public void onError(Session session, Throwable throwable) { 53 | throwable.printStackTrace(); 54 | } 55 | 56 | @OnMessage 57 | public void onMessage(Session session, String message) { 58 | System.out.println(message); 59 | session.sendText("Hello Netty!"); 60 | } 61 | 62 | @OnBinary 63 | public void onBinary(Session session, byte[] bytes) { 64 | for (byte b : bytes) { 65 | System.out.println(b); 66 | } 67 | session.sendBinary(bytes); 68 | } 69 | 70 | @OnEvent 71 | public void onEvent(Session session, Object evt) { 72 | if (evt instanceof IdleStateEvent) { 73 | IdleStateEvent idleStateEvent = (IdleStateEvent) evt; 74 | switch (idleStateEvent.state()) { 75 | case READER_IDLE: 76 | System.out.println("read idle"); 77 | break; 78 | case WRITER_IDLE: 79 | System.out.println("write idle"); 80 | break; 81 | case ALL_IDLE: 82 | System.out.println("all idle"); 83 | break; 84 | default: 85 | break; 86 | } 87 | } 88 | } 89 | 90 | } 91 | ``` 92 | 93 | - use Websocket client to connect `ws://127.0.0.1:80/ws/xxx` 94 | 95 | 96 | ### Annotation 97 | ###### @ServerEndpoint 98 | > declaring `ServerEndpointExporter` in Spring configuration,it will scan for WebSocket endpoints that be annotated with `ServerEndpoint` . 99 | > beans that be annotated with `ServerEndpoint` will be registered as a WebSocket endpoint. 100 | > all [configurations](#configuration) are inside this annotation ( e.g. `@ServerEndpoint("/ws")` ) 101 | 102 | ###### @BeforeHandshake 103 | > when there is a connection accepted,the method annotated with `@BeforeHandshake` will be called 104 | > classes which be injected to the method are:Session,HttpHeaders... 105 | 106 | ###### @OnOpen 107 | > when there is a WebSocket connection completed,the method annotated with `@OnOpen` will be called 108 | > classes which be injected to the method are:Session,HttpHeaders... 109 | 110 | ###### @OnClose 111 | > when a WebSocket connection closed,the method annotated with `@OnClose` will be called 112 | > classes which be injected to the method are:Session 113 | 114 | ###### @OnError 115 | > when a WebSocket connection throw Throwable, the method annotated with `@OnError` will be called 116 | > classes which be injected to the method are:Session,Throwable 117 | 118 | ###### @OnMessage 119 | > when a WebSocket connection received a message,the method annotated with `@OnMessage` will be called 120 | > classes which be injected to the method are:Session,String 121 | 122 | ###### @OnBinary 123 | > when a WebSocket connection received the binary,the method annotated with `@OnBinary` will be called 124 | > classes which be injected to the method are:Session,byte[] 125 | 126 | ###### @OnEvent 127 | > when a WebSocket connection received the event of Netty,the method annotated with `@OnEvent` will be called 128 | > classes which be injected to the method are:Session,Object 129 | 130 | ### Configuration 131 | > all configurations are configured in `@ServerEndpoint`'s property 132 | 133 | | property | default | description 134 | |---|---|--- 135 | |path|"/"|path of WebSocket can be aliased for `value` 136 | |host|"0.0.0.0"|host of WebSocket.`"0.0.0.0"` means all of local addresses 137 | |port|80|port of WebSocket。if the port equals to 0,it will use a random and available port(to get the port [Multi-Endpoint](#multi-endpoint)) 138 | |bossLoopGroupThreads|0|num of threads in bossEventLoopGroup 139 | |workerLoopGroupThreads|0|num of threads in workerEventLoopGroup 140 | |useCompressionHandler|false|whether add WebSocketServerCompressionHandler to pipeline 141 | |optionConnectTimeoutMillis|30000|the same as `ChannelOption.CONNECT_TIMEOUT_MILLIS` in Netty 142 | |optionSoBacklog|128|the same as `ChannelOption.SO_BACKLOG` in Netty 143 | |childOptionWriteSpinCount|16|the same as `ChannelOption.WRITE_SPIN_COUNT` in Netty 144 | |childOptionWriteBufferHighWaterMark|64*1024|the same as `ChannelOption.WRITE_BUFFER_HIGH_WATER_MARK` in Netty,but use `ChannelOption.WRITE_BUFFER_WATER_MARK` in fact. 145 | |childOptionWriteBufferLowWaterMark|32*1024|the same as `ChannelOption.WRITE_BUFFER_LOW_WATER_MARK` in Netty,but use `ChannelOption.WRITE_BUFFER_WATER_MARK` in fact. 146 | |childOptionSoRcvbuf|-1(mean not set)|the same as `ChannelOption.SO_RCVBUF` in Netty 147 | |childOptionSoSndbuf|-1(mean not set)|the same as `ChannelOption.SO_SNDBUF` in Netty 148 | |childOptionTcpNodelay|true|the same as `ChannelOption.TCP_NODELAY` in Netty 149 | |childOptionSoKeepalive|false|the same as `ChannelOption.SO_KEEPALIVE` in Netty 150 | |childOptionSoLinger|-1|the same as `ChannelOption.SO_LINGER` in Netty 151 | |childOptionAllowHalfClosure|false|the same as `ChannelOption.ALLOW_HALF_CLOSURE` in Netty 152 | |readerIdleTimeSeconds|0|the same as `readerIdleTimeSeconds` in `IdleStateHandler` and add `IdleStateHandler` to `pipeline` when it is not 0 153 | |writerIdleTimeSeconds|0|the same as `writerIdleTimeSeconds` in `IdleStateHandler` and add `IdleStateHandler` to `pipeline` when it is not 0 154 | |allIdleTimeSeconds|0|the same as `allIdleTimeSeconds` in `IdleStateHandler` and add `IdleStateHandler` to `pipeline` when it is not 0 155 | |maxFramePayloadLength|65536|Maximum allowable frame payload length. 156 | |useEventExecutorGroup|true|Whether to use another thread pool to perform time-consuming synchronous business logic 157 | |eventExecutorGroupThreads|16|num of threads in bossEventLoopGroup 158 | |sslKeyPassword|""(mean not set)|the same as `server.ssl.key-password` in spring-boot 159 | |sslKeyStore|""(mean not set)|the same as `server.ssl.key-store` in spring-boot 160 | |sslKeyStorePassword|""(mean not set)|the same as `server.ssl.key-store-password` in spring-boot 161 | |sslKeyStoreType|""(mean not set)|the same as `server.ssl.key-store-type` in spring-boot 162 | |sslTrustStore|""(mean not set)|the same as `server.ssl.trust-store` in spring-boot 163 | |sslTrustStorePassword|""(mean not set)|the same as `server.ssl.trust-store-password` in spring-boot 164 | |sslTrustStoreType|""(mean not set)|the same as `server.ssl.trust-store-type` in spring-boot 165 | |corsOrigins|{}(mean not set)|the same as `@CrossOrigin#origins` in spring-boot 166 | |corsAllowCredentials|""(mean not set)|the same as `@CrossOrigin#allowCredentials` in spring-boot 167 | 168 | ### Configuration by application.properties 169 | > You can get the configurate of `application.properties` by using `${...}` placeholders. for example: 170 | 171 | - first,use `${...}` in `@ServerEndpoint` 172 | ```java 173 | @ServerEndpoint(host = "${ws.host}",port = "${ws.port}") 174 | public class MyWebSocket { 175 | ... 176 | } 177 | ``` 178 | - then configurate in `application.properties` 179 | ``` 180 | ws.host=0.0.0.0 181 | ws.port=80 182 | ``` 183 | 184 | ### Custom Favicon 185 | The way of configure favicon is the same as spring-boot.If `favicon.ico` is presented in the root of the classpath,it will be automatically used as the favicon of the application.the example is following: 186 | ``` 187 | src/ 188 | +- main/ 189 | +- java/ 190 | | + 191 | +- resources/ 192 | +- favicon.ico 193 | ``` 194 | 195 | ### Custom Error Pages 196 | The way of configure favicon is the same as spring-boot.you can add a file to an `/public/error` 197 | folder.The name of the error page should be the exact status code or a series mask.the example is following: 198 | ``` 199 | src/ 200 | +- main/ 201 | +- java/ 202 | | + 203 | +- resources/ 204 | +- public/ 205 | +- error/ 206 | | +- 404.html 207 | | +- 5xx.html 208 | +- 209 | ``` 210 | 211 | ### Multi Endpoint 212 | - base on [Quick-Start](#quick-start),use annotation `@ServerEndpoint` and `@Component` in classes which hope to become a endpoint. 213 | - you can get all socket addresses in `ServerEndpointExporter.getInetSocketAddressSet()`. 214 | - when there are different addresses(different host or different port) in WebSocket,they will use different `ServerBootstrap` instance. 215 | - when the addresses are the same,but path is different,they will use the same `ServerBootstrap` instance. 216 | - when multiple port of endpoint is 0 ,they will use the same random port 217 | - when multiple port of endpoint is the same as the path,host can't be set as "0.0.0.0",because it means it binds all of the addresses 218 | 219 | --- 220 | ### Change Log 221 | 222 | #### 0.8.0 223 | 224 | - Auto-Configuration 225 | 226 | #### 0.9.0 227 | 228 | - Support RESTful by `@PathVariable` 229 | - Get param by`@RequestParam` from query 230 | - Remove `ParameterMap` ,instead of `@RequestParam MultiValueMap` 231 | - Add `@BeforeHandshake` annotation,you can close the connect before handshake 232 | - Set sub-protocol in `@BeforeHandshake` event 233 | - Remove the `@Component` on endpoint class 234 | - Update `Netty` version to `4.1.44.Final` 235 | 236 | #### 0.9.1 237 | 238 | - Bug fixed : it was null when using `@RequestParam MultiValueMap` to get value 239 | - Update `Netty` version to `4.1.45.Final` 240 | 241 | #### 0.9.2 242 | 243 | - There are compatibility version under 0.8.0 that can configure the `ServerEndpointExporter` manully 244 | 245 | #### 0.9.3 246 | 247 | - Bug fixed :when there is no `@BeforeHandshake` , NullPointerException will appear 248 | 249 | #### 0.9.4 250 | 251 | - Bug fixed :when there is no `@BeforeHandshake` , `Session` in `OnOpen` is null 252 | 253 | #### 0.9.5 254 | 255 | - Bug fixed :`Throwable` in `OnError` event is null 256 | 257 | #### 0.10.0 258 | 259 | - Modified the default value of `bossLoopGroupThreads` to 1 260 | - Supports configuring `useEventExecutorGroup` to run synchronous and time-consuming business logic in EventExecutorGroup, so that the I/O thread is not blocked by a time-consuming task 261 | - SSL supported 262 | - CORS supported 263 | - Update `Netty` version to `4.1.49.Final` 264 | 265 | #### 0.11.0 266 | 267 | - When the `ServerEndpoint` class is proxied by CGLIB (as with AOP enhancement), it still works 268 | 269 | #### 0.12.0 270 | 271 | - `@enableWebSocket` adds the `scanBasePackages` attribute 272 | - `@serverEndpoint` no longer depends on `@Component` 273 | - Update `Netty` version to `4.1.67.Final` 274 | 275 | #### 0.13.0 276 | 277 | - Fixed the issue where WebSocket compression was not working. 278 | - Upgraded support for Spring Boot 3. 279 | - Included the client's `Sec-WebSocket-Protocol` in the response header. 280 | - When closing the connection, sends a `bye` command first instead of directly closing. 281 | - Updated `Netty` version to `4.1.118.Final`. -------------------------------------------------------------------------------- /README_zh.md: -------------------------------------------------------------------------------- 1 | netty-websocket-spring-boot-starter [![License](http://img.shields.io/:license-apache-brightgreen.svg)](http://www.apache.org/licenses/LICENSE-2.0.html) 2 | =================================== 3 | 4 | [English Docs](https://github.com/YeautyYE/netty-websocket-spring-boot-starter/blob/master/README.md) 5 | 6 | ### 简介 7 | 本项目帮助你在spring-boot中使用Netty来开发WebSocket服务器,并像spring-websocket的注解开发一样简单 8 | 9 | ### 要求 10 | - jdk版本为17 11 | 12 | 13 | ### 快速开始 14 | 15 | - 添加依赖: 16 | 17 | ```xml 18 | 19 | org.yeauty 20 | netty-websocket-spring-boot-starter 21 | 0.13.0 22 | 23 | ``` 24 | 25 | - 在端点类上加上`@ServerEndpoint`注解,并在相应的方法上加上`@BeforeHandshake`、`@OnOpen`、`@OnClose`、`@OnError`、`@OnMessage`、`@OnBinary`、`@OnEvent`注解,样例如下: 26 | 27 | ```java 28 | @ServerEndpoint(path = "/ws/{arg}") 29 | public class MyWebSocket { 30 | 31 | @BeforeHandshake 32 | public void handshake(Session session, HttpHeaders headers, @RequestParam String req, @RequestParam MultiValueMap reqMap, @PathVariable String arg, @PathVariable Map pathMap){ 33 | session.setSubprotocols("stomp"); 34 | if (!"ok".equals(req)){ 35 | System.out.println("Authentication failed!"); 36 | session.close(); 37 | } 38 | } 39 | 40 | @OnOpen 41 | public void onOpen(Session session, HttpHeaders headers, @RequestParam String req, @RequestParam MultiValueMap reqMap, @PathVariable String arg, @PathVariable Map pathMap){ 42 | System.out.println("new connection"); 43 | System.out.println(req); 44 | } 45 | 46 | @OnClose 47 | public void onClose(Session session) throws IOException { 48 | System.out.println("one connection closed"); 49 | } 50 | 51 | @OnError 52 | public void onError(Session session, Throwable throwable) { 53 | throwable.printStackTrace(); 54 | } 55 | 56 | @OnMessage 57 | public void onMessage(Session session, String message) { 58 | System.out.println(message); 59 | session.sendText("Hello Netty!"); 60 | } 61 | 62 | @OnBinary 63 | public void onBinary(Session session, byte[] bytes) { 64 | for (byte b : bytes) { 65 | System.out.println(b); 66 | } 67 | session.sendBinary(bytes); 68 | } 69 | 70 | @OnEvent 71 | public void onEvent(Session session, Object evt) { 72 | if (evt instanceof IdleStateEvent) { 73 | IdleStateEvent idleStateEvent = (IdleStateEvent) evt; 74 | switch (idleStateEvent.state()) { 75 | case READER_IDLE: 76 | System.out.println("read idle"); 77 | break; 78 | case WRITER_IDLE: 79 | System.out.println("write idle"); 80 | break; 81 | case ALL_IDLE: 82 | System.out.println("all idle"); 83 | break; 84 | default: 85 | break; 86 | } 87 | } 88 | } 89 | 90 | } 91 | ``` 92 | 93 | - 打开WebSocket客户端,连接到`ws://127.0.0.1:80/ws/xxx` 94 | 95 | 96 | ### 注解 97 | ###### @ServerEndpoint 98 | > 当ServerEndpointExporter类通过Spring配置进行声明并被使用,它将会去扫描带有@ServerEndpoint注解的类 99 | > 被注解的类将被注册成为一个WebSocket端点 100 | > 所有的[配置项](#%E9%85%8D%E7%BD%AE)都在这个注解的属性中 ( 如:`@ServerEndpoint("/ws")` ) 101 | 102 | ###### @BeforeHandshake 103 | > 当有新的连接进入时,对该方法进行回调 104 | > 注入参数的类型:Session、HttpHeaders... 105 | 106 | ###### @OnOpen 107 | > 当有新的WebSocket连接完成时,对该方法进行回调 108 | > 注入参数的类型:Session、HttpHeaders... 109 | 110 | ###### @OnClose 111 | > 当有WebSocket连接关闭时,对该方法进行回调 112 | > 注入参数的类型:Session 113 | 114 | ###### @OnError 115 | > 当有WebSocket抛出异常时,对该方法进行回调 116 | > 注入参数的类型:Session、Throwable 117 | 118 | ###### @OnMessage 119 | > 当接收到字符串消息时,对该方法进行回调 120 | > 注入参数的类型:Session、String 121 | 122 | ###### @OnBinary 123 | > 当接收到二进制消息时,对该方法进行回调 124 | > 注入参数的类型:Session、byte[] 125 | 126 | ###### @OnEvent 127 | > 当接收到Netty的事件时,对该方法进行回调 128 | > 注入参数的类型:Session、Object 129 | 130 | ### 配置 131 | > 所有的配置项都在这个注解的属性中 132 | 133 | | 属性 | 默认值 | 说明 134 | |---|---|--- 135 | |path|"/"|WebSocket的path,也可以用`value`来设置 136 | |host|"0.0.0.0"|WebSocket的host,`"0.0.0.0"`即是所有本地地址 137 | |port|80|WebSocket绑定端口号。如果为0,则使用随机端口(端口获取可见 [多端点服务](#%E5%A4%9A%E7%AB%AF%E7%82%B9%E6%9C%8D%E5%8A%A1)) 138 | |bossLoopGroupThreads|0|bossEventLoopGroup的线程数 139 | |workerLoopGroupThreads|0|workerEventLoopGroup的线程数 140 | |useCompressionHandler|false|是否添加WebSocketServerCompressionHandler到pipeline 141 | |optionConnectTimeoutMillis|30000|与Netty的`ChannelOption.CONNECT_TIMEOUT_MILLIS`一致 142 | |optionSoBacklog|128|与Netty的`ChannelOption.SO_BACKLOG`一致 143 | |childOptionWriteSpinCount|16|与Netty的`ChannelOption.WRITE_SPIN_COUNT`一致 144 | |childOptionWriteBufferHighWaterMark|64*1024|与Netty的`ChannelOption.WRITE_BUFFER_HIGH_WATER_MARK`一致,但实际上是使用`ChannelOption.WRITE_BUFFER_WATER_MARK` 145 | |childOptionWriteBufferLowWaterMark|32*1024|与Netty的`ChannelOption.WRITE_BUFFER_LOW_WATER_MARK`一致,但实际上是使用 `ChannelOption.WRITE_BUFFER_WATER_MARK` 146 | |childOptionSoRcvbuf|-1(即未设置)|与Netty的`ChannelOption.SO_RCVBUF`一致 147 | |childOptionSoSndbuf|-1(即未设置)|与Netty的`ChannelOption.SO_SNDBUF`一致 148 | |childOptionTcpNodelay|true|与Netty的`ChannelOption.TCP_NODELAY`一致 149 | |childOptionSoKeepalive|false|与Netty的`ChannelOption.SO_KEEPALIVE`一致 150 | |childOptionSoLinger|-1|与Netty的`ChannelOption.SO_LINGER`一致 151 | |childOptionAllowHalfClosure|false|与Netty的`ChannelOption.ALLOW_HALF_CLOSURE`一致 152 | |readerIdleTimeSeconds|0|与`IdleStateHandler`中的`readerIdleTimeSeconds`一致,并且当它不为0时,将在`pipeline`中添加`IdleStateHandler` 153 | |writerIdleTimeSeconds|0|与`IdleStateHandler`中的`writerIdleTimeSeconds`一致,并且当它不为0时,将在`pipeline`中添加`IdleStateHandler` 154 | |allIdleTimeSeconds|0|与`IdleStateHandler`中的`allIdleTimeSeconds`一致,并且当它不为0时,将在`pipeline`中添加`IdleStateHandler` 155 | |maxFramePayloadLength|65536|最大允许帧载荷长度 156 | |useEventExecutorGroup|true|是否使用另一个线程池来执行耗时的同步业务逻辑 157 | |eventExecutorGroupThreads|16|eventExecutorGroup的线程数 158 | |sslKeyPassword|""(即未设置)|与spring-boot的`server.ssl.key-password`一致 159 | |sslKeyStore|""(即未设置)|与spring-boot的`server.ssl.key-store`一致 160 | |sslKeyStorePassword|""(即未设置)|与spring-boot的`server.ssl.key-store-password`一致 161 | |sslKeyStoreType|""(即未设置)|与spring-boot的`server.ssl.key-store-type`一致 162 | |sslTrustStore|""(即未设置)|与spring-boot的`server.ssl.trust-store`一致 163 | |sslTrustStorePassword|""(即未设置)|与spring-boot的`server.ssl.trust-store-password`一致 164 | |sslTrustStoreType|""(即未设置)|与spring-boot的`server.ssl.trust-store-type`一致 165 | |corsOrigins|{}(即未设置)|与spring-boot的`@CrossOrigin#origins`一致 166 | |corsAllowCredentials|""(即未设置)|与spring-boot的`@CrossOrigin#allowCredentials`一致 167 | 168 | ### 通过application.properties进行配置 169 | > 所有参数皆可使用`${...}`占位符获取`application.properties`中的配置。如下: 170 | 171 | - 首先在`@ServerEndpoint`注解的属性中使用`${...}`占位符 172 | ```java 173 | @ServerEndpoint(host = "${ws.host}",port = "${ws.port}") 174 | public class MyWebSocket { 175 | ... 176 | } 177 | ``` 178 | - 接下来即可在`application.properties`中配置 179 | ``` 180 | ws.host=0.0.0.0 181 | ws.port=80 182 | ``` 183 | 184 | ### 自定义Favicon 185 | 配置favicon的方式与spring-boot中完全一致。只需将`favicon.ico`文件放到classpath的根目录下即可。如下: 186 | ``` 187 | src/ 188 | +- main/ 189 | +- java/ 190 | | + 191 | +- resources/ 192 | +- favicon.ico 193 | ``` 194 | 195 | ### 自定义错误页面 196 | 配置自定义错误页面的方式与spring-boot中完全一致。你可以添加一个 `/public/error` 目录,错误页面将会是该目录下的静态页面,错误页面的文件名必须是准确的错误状态或者是一串掩码,如下: 197 | ``` 198 | src/ 199 | +- main/ 200 | +- java/ 201 | | + 202 | +- resources/ 203 | +- public/ 204 | +- error/ 205 | | +- 404.html 206 | | +- 5xx.html 207 | +- 208 | ``` 209 | 210 | ### 多端点服务 211 | - 在[快速启动](#%E5%BF%AB%E9%80%9F%E5%BC%80%E5%A7%8B)的基础上,在多个需要成为端点的类上使用`@ServerEndpoint`、`@Component`注解即可 212 | - 可通过`ServerEndpointExporter.getInetSocketAddressSet()`获取所有端点的地址 213 | - 当地址不同时(即host不同或port不同),使用不同的`ServerBootstrap`实例 214 | - 当地址相同,路径(path)不同时,使用同一个`ServerBootstrap`实例 215 | - 当多个端点服务的port为0时,将使用同一个随机的端口号 216 | - 当多个端点的port和path相同时,host不能设为`"0.0.0.0"`,因为`"0.0.0.0"`意味着绑定所有的host 217 | 218 | --- 219 | ### 更新日志 220 | 221 | #### 0.8.0 222 | 223 | - 自动装配 224 | 225 | #### 0.9.0 226 | 227 | - 通过`@PathVariable`支持RESTful风格中获取参数 228 | - 通过`@RequestParam`实现请求中query的获取参数 229 | - 移除原来的ParameterMap,用`@RequestParam MultiValueMap`代替 230 | - 新增 `@BeforeHandshake` 注解,可在握手之前对连接进行关闭 231 | - 在`@BeforeHandshake`事件中可设置子协议 232 | - 去掉配置端点类上的 `@Component` 233 | - 更新`Netty`版本到 `4.1.44.Final` 234 | 235 | #### 0.9.1 236 | 237 | - 修复bug:当使用`@RequestParam MultiValueMap`时获取的对象为null 238 | - 更新`Netty`版本到 `4.1.45.Final` 239 | 240 | #### 0.9.2 241 | 242 | - 兼容 0.8.0 以下版本,可以手动装配`ServerEndpointExporter`对象 243 | 244 | #### 0.9.3 245 | 246 | - 修复bug:当没有 `@BeforeHandshake`时会出现空指针异常 247 | 248 | #### 0.9.4 249 | 250 | - 修复bug:当没有 `@BeforeHandshake`时 `OnOpen`中的`Session`为null. 251 | 252 | #### 0.9.5 253 | 254 | - 修复bug:`OnError`事件中的`Throwable`为null. 255 | 256 | #### 0.10.0 257 | 258 | - 修改`bossLoopGroupThreads`默认值为1 259 | - 支持通过配置`useEventExecutorGroup`让同步且耗时的业务逻辑在EventExecutorGroup中执行,防止I/O线程被耗时的任务阻塞 260 | - 支持SSL 261 | - 支持跨域 262 | - 更新`Netty`版本到 `4.1.59.Final` 263 | 264 | #### 0.11.0 265 | 266 | - 当`ServerEndpoint`类被cglib代理时(如aop增强),仍能正常运行 267 | 268 | #### 0.12.0 269 | 270 | - `@EnableWebSocket`增加`scanBasePackages`属性 271 | - `@ServerEndpoint`不再依赖`@Component` 272 | - 更新`Netty`版本到 `4.1.67.Final` 273 | 274 | #### 0.13.0 275 | 276 | - 修复了无法进行WebSocket压缩的问题 277 | - 升级支持spring-boot3 278 | - 响应头带上前端的`Sec-WebSocket-Protocol` 279 | - 关闭连接时,会先发送`bye`命令,而不是直接close 280 | - 更新`Netty`版本到 `4.1.118.Final` 281 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | org.yeauty 8 | netty-websocket-spring-boot-starter 9 | 0.13.0 10 | 11 | netty-websocket-spring-boot-starter 12 | 13 | netty-websocket-spring-boot-starter is a Java WebSocket Framework based on Netty 14 | 15 | http://www.yeauty.org/ 16 | 17 | 18 | 19 | The Apache Software License, Version 2.0 20 | http://www.apache.org/licenses/LICENSE-2.0.txt 21 | 22 | 23 | 24 | 25 | 26 | Yeauty 27 | YeautyYE@gmail.com 28 | 29 | 30 | 31 | 32 | scm:git:git://github.com/YeautyYE/netty-websocket-spring-boot-starter.git 33 | scm:git:ssh://github.com/YeautyYE/netty-websocket-spring-boot-starter.git 34 | 35 | https://github.com/YeautyYE/netty-websocket-spring-boot-starter 36 | 37 | 38 | 39 | 4.1.118.Final 40 | 3.3.5 41 | 42 | 43 | 44 | 45 | org.springframework.boot 46 | spring-boot-autoconfigure 47 | ${spring-boot.version} 48 | true 49 | 50 | 51 | 52 | io.netty 53 | netty-codec-http 54 | ${netty.version} 55 | 56 | 57 | io.netty 58 | netty-handler 59 | ${netty.version} 60 | 61 | 62 | 63 | 64 | 65 | 66 | release 67 | 68 | 69 | 70 | 71 | org.apache.maven.plugins 72 | maven-source-plugin 73 | 3.0.1 74 | 75 | 76 | 77 | jar-no-fork 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | org.apache.maven.plugins 86 | maven-javadoc-plugin 87 | 3.0.1 88 | 89 | 90 | 91 | jar 92 | 93 | 94 | 95 | 96 | -Xdoclint:none 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | org.apache.maven.plugins 106 | maven-gpg-plugin 107 | 1.6 108 | 109 | 110 | verify 111 | 112 | sign 113 | 114 | 115 | 116 | 117 | 118 | 119 | org.sonatype.plugins 120 | nexus-staging-maven-plugin 121 | 1.6.13 122 | true 123 | 124 | ossrh 125 | https://oss.sonatype.org/ 126 | true 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | ossrh 135 | https://oss.sonatype.org/content/repositories/snapshots 136 | 137 | 138 | ossrh 139 | https://oss.sonatype.org/service/local/staging/deploy/maven2 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | org.apache.maven.plugins 149 | maven-compiler-plugin 150 | 3.10.1 151 | 152 | 17 153 | 17 154 | 155 | 156 | 157 | 158 | 159 | -------------------------------------------------------------------------------- /src/main/java/org/yeauty/annotation/BeforeHandshake.java: -------------------------------------------------------------------------------- 1 | package org.yeauty.annotation; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Retention(RetentionPolicy.RUNTIME) 9 | @Target(ElementType.METHOD) 10 | public @interface BeforeHandshake { 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/org/yeauty/annotation/EnableWebSocket.java: -------------------------------------------------------------------------------- 1 | package org.yeauty.annotation; 2 | 3 | import org.springframework.context.annotation.Import; 4 | 5 | import java.lang.annotation.ElementType; 6 | import java.lang.annotation.Retention; 7 | import java.lang.annotation.RetentionPolicy; 8 | import java.lang.annotation.Target; 9 | 10 | @Retention(RetentionPolicy.RUNTIME) 11 | @Target(ElementType.TYPE) 12 | @Import(NettyWebSocketSelector.class) 13 | public @interface EnableWebSocket { 14 | 15 | String[] scanBasePackages() default {}; 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/org/yeauty/annotation/NettyWebSocketSelector.java: -------------------------------------------------------------------------------- 1 | package org.yeauty.annotation; 2 | 3 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.yeauty.standard.ServerEndpointExporter; 7 | 8 | @ConditionalOnMissingBean(ServerEndpointExporter.class) 9 | @Configuration 10 | public class NettyWebSocketSelector { 11 | 12 | @Bean 13 | public ServerEndpointExporter serverEndpointExporter() { 14 | return new ServerEndpointExporter(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/org/yeauty/annotation/OnBinary.java: -------------------------------------------------------------------------------- 1 | package org.yeauty.annotation; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Retention(RetentionPolicy.RUNTIME) 9 | @Target(ElementType.METHOD) 10 | public @interface OnBinary { 11 | } -------------------------------------------------------------------------------- /src/main/java/org/yeauty/annotation/OnClose.java: -------------------------------------------------------------------------------- 1 | package org.yeauty.annotation; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Retention(RetentionPolicy.RUNTIME) 9 | @Target(ElementType.METHOD) 10 | public @interface OnClose { 11 | } -------------------------------------------------------------------------------- /src/main/java/org/yeauty/annotation/OnError.java: -------------------------------------------------------------------------------- 1 | package org.yeauty.annotation; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Retention(RetentionPolicy.RUNTIME) 9 | @Target(ElementType.METHOD) 10 | public @interface OnError { 11 | } -------------------------------------------------------------------------------- /src/main/java/org/yeauty/annotation/OnEvent.java: -------------------------------------------------------------------------------- 1 | package org.yeauty.annotation; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Retention(RetentionPolicy.RUNTIME) 9 | @Target(ElementType.METHOD) 10 | public @interface OnEvent { 11 | } -------------------------------------------------------------------------------- /src/main/java/org/yeauty/annotation/OnMessage.java: -------------------------------------------------------------------------------- 1 | package org.yeauty.annotation; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Retention(RetentionPolicy.RUNTIME) 9 | @Target(ElementType.METHOD) 10 | public @interface OnMessage { 11 | } -------------------------------------------------------------------------------- /src/main/java/org/yeauty/annotation/OnOpen.java: -------------------------------------------------------------------------------- 1 | package org.yeauty.annotation; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Retention(RetentionPolicy.RUNTIME) 9 | @Target(ElementType.METHOD) 10 | public @interface OnOpen { 11 | } -------------------------------------------------------------------------------- /src/main/java/org/yeauty/annotation/PathVariable.java: -------------------------------------------------------------------------------- 1 | package org.yeauty.annotation; 2 | 3 | import org.springframework.core.annotation.AliasFor; 4 | 5 | import java.lang.annotation.ElementType; 6 | import java.lang.annotation.Retention; 7 | import java.lang.annotation.RetentionPolicy; 8 | import java.lang.annotation.Target; 9 | 10 | @Target(ElementType.PARAMETER) 11 | @Retention(RetentionPolicy.RUNTIME) 12 | public @interface PathVariable { 13 | 14 | /** 15 | * Alias for {@link #name}. 16 | */ 17 | @AliasFor("name") 18 | String value() default ""; 19 | 20 | /** 21 | * The name of the path variable to bind to. 22 | */ 23 | @AliasFor("value") 24 | String name() default ""; 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/org/yeauty/annotation/RequestParam.java: -------------------------------------------------------------------------------- 1 | package org.yeauty.annotation; 2 | 3 | import org.springframework.core.annotation.AliasFor; 4 | 5 | import java.lang.annotation.ElementType; 6 | import java.lang.annotation.Retention; 7 | import java.lang.annotation.RetentionPolicy; 8 | import java.lang.annotation.Target; 9 | 10 | @Target(ElementType.PARAMETER) 11 | @Retention(RetentionPolicy.RUNTIME) 12 | public @interface RequestParam { 13 | 14 | /** 15 | * Alias for {@link #name}. 16 | */ 17 | @AliasFor("name") 18 | String value() default ""; 19 | 20 | /** 21 | * The name of the request parameter to bind to. 22 | */ 23 | @AliasFor("value") 24 | String name() default ""; 25 | 26 | /** 27 | * Whether the parameter is required. 28 | *

Defaults to {@code true}, leading to an exception being thrown 29 | * if the parameter is missing in the request. Switch this to 30 | * {@code false} if you prefer a {@code null} value if the parameter is 31 | * not present in the request. 32 | *

Alternatively, provide a {@link #defaultValue}, which implicitly 33 | * sets this flag to {@code false}. 34 | */ 35 | boolean required() default true; 36 | 37 | /** 38 | * The default value to use as a fallback when the request parameter is 39 | * not provided or has an empty value. 40 | *

Supplying a default value implicitly sets {@link #required} to 41 | * {@code false}. 42 | */ 43 | String defaultValue() default "\n\t\t\n\t\t\n\uE000\uE001\uE002\n\t\t\t\t\n"; 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/org/yeauty/annotation/ServerEndpoint.java: -------------------------------------------------------------------------------- 1 | package org.yeauty.annotation; 2 | 3 | import org.springframework.core.annotation.AliasFor; 4 | 5 | import java.lang.annotation.ElementType; 6 | import java.lang.annotation.Retention; 7 | import java.lang.annotation.RetentionPolicy; 8 | import java.lang.annotation.Target; 9 | 10 | /** 11 | * @author Yeauty 12 | */ 13 | @Retention(RetentionPolicy.RUNTIME) 14 | @Target(ElementType.TYPE) 15 | public @interface ServerEndpoint { 16 | 17 | @AliasFor("path") 18 | String value() default "/"; 19 | 20 | @AliasFor("value") 21 | String path() default "/"; 22 | 23 | String host() default "0.0.0.0"; 24 | 25 | String port() default "80"; 26 | 27 | String bossLoopGroupThreads() default "1"; 28 | 29 | String workerLoopGroupThreads() default "0"; 30 | 31 | String useCompressionHandler() default "false"; 32 | 33 | //------------------------- option ------------------------- 34 | 35 | String optionConnectTimeoutMillis() default "30000"; 36 | 37 | String optionSoBacklog() default "128"; 38 | 39 | //------------------------- childOption ------------------------- 40 | 41 | String childOptionWriteSpinCount() default "16"; 42 | 43 | String childOptionWriteBufferHighWaterMark() default "65536"; 44 | 45 | String childOptionWriteBufferLowWaterMark() default "32768"; 46 | 47 | String childOptionSoRcvbuf() default "-1"; 48 | 49 | String childOptionSoSndbuf() default "-1"; 50 | 51 | String childOptionTcpNodelay() default "true"; 52 | 53 | String childOptionSoKeepalive() default "false"; 54 | 55 | String childOptionSoLinger() default "-1"; 56 | 57 | String childOptionAllowHalfClosure() default "false"; 58 | 59 | //------------------------- idleEvent ------------------------- 60 | 61 | String readerIdleTimeSeconds() default "0"; 62 | 63 | String writerIdleTimeSeconds() default "0"; 64 | 65 | String allIdleTimeSeconds() default "0"; 66 | 67 | //------------------------- handshake ------------------------- 68 | 69 | String maxFramePayloadLength() default "65536"; 70 | 71 | //------------------------- eventExecutorGroup ------------------------- 72 | 73 | String useEventExecutorGroup() default "true"; //use EventExecutorGroup(another thread pool) to perform time-consuming synchronous business logic 74 | 75 | String eventExecutorGroupThreads() default "16"; 76 | 77 | //------------------------- ssl (refer to spring Ssl) ------------------------- 78 | 79 | /** 80 | * {@link org.springframework.boot.web.server.Ssl} 81 | */ 82 | 83 | String sslKeyPassword() default ""; 84 | 85 | String sslKeyStore() default ""; //e.g. classpath:server.jks 86 | 87 | String sslKeyStorePassword() default ""; 88 | 89 | String sslKeyStoreType() default ""; //e.g. JKS 90 | 91 | String sslTrustStore() default ""; 92 | 93 | String sslTrustStorePassword() default ""; 94 | 95 | String sslTrustStoreType() default ""; 96 | 97 | //------------------------- cors (refer to spring CrossOrigin) ------------------------- 98 | 99 | /** 100 | * {@link org.springframework.web.bind.annotation.CrossOrigin} 101 | */ 102 | 103 | String[] corsOrigins() default {}; 104 | 105 | String corsAllowCredentials() default ""; 106 | 107 | 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/org/yeauty/autoconfigure/NettyWebSocketAutoConfigure.java: -------------------------------------------------------------------------------- 1 | package org.yeauty.autoconfigure; 2 | 3 | import org.yeauty.annotation.EnableWebSocket; 4 | 5 | @EnableWebSocket 6 | public class NettyWebSocketAutoConfigure { 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/org/yeauty/exception/DeploymentException.java: -------------------------------------------------------------------------------- 1 | package org.yeauty.exception; 2 | 3 | public class DeploymentException extends Exception { 4 | 5 | private static final long serialVersionUID = 1L; 6 | 7 | public DeploymentException(String message) { 8 | super(message); 9 | } 10 | 11 | public DeploymentException(String message, Throwable cause) { 12 | super(message, cause); 13 | } 14 | } -------------------------------------------------------------------------------- /src/main/java/org/yeauty/pojo/PojoEndpointServer.java: -------------------------------------------------------------------------------- 1 | package org.yeauty.pojo; 2 | 3 | import io.netty.channel.Channel; 4 | import io.netty.handler.codec.http.FullHttpRequest; 5 | import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; 6 | import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; 7 | import io.netty.handler.codec.http.websocketx.WebSocketFrame; 8 | import io.netty.util.Attribute; 9 | import io.netty.util.AttributeKey; 10 | import io.netty.util.internal.logging.InternalLogger; 11 | import io.netty.util.internal.logging.InternalLoggerFactory; 12 | import org.springframework.beans.TypeMismatchException; 13 | import org.yeauty.standard.ServerEndpointConfig; 14 | import org.yeauty.support.*; 15 | 16 | import java.lang.reflect.Method; 17 | import java.util.*; 18 | 19 | /** 20 | * @author Yeauty 21 | * @version 1.0 22 | */ 23 | public class PojoEndpointServer { 24 | 25 | private static final AttributeKey POJO_KEY = AttributeKey.valueOf("WEBSOCKET_IMPLEMENT"); 26 | 27 | public static final AttributeKey SESSION_KEY = AttributeKey.valueOf("WEBSOCKET_SESSION"); 28 | 29 | private static final AttributeKey PATH_KEY = AttributeKey.valueOf("WEBSOCKET_PATH"); 30 | 31 | public static final AttributeKey> URI_TEMPLATE = AttributeKey.valueOf("WEBSOCKET_URI_TEMPLATE"); 32 | 33 | public static final AttributeKey>> REQUEST_PARAM = AttributeKey.valueOf("WEBSOCKET_REQUEST_PARAM"); 34 | 35 | private final Map pathMethodMappingMap = new HashMap<>(); 36 | 37 | private final ServerEndpointConfig config; 38 | 39 | private Set pathMatchers = new HashSet<>(); 40 | 41 | private static final InternalLogger logger = InternalLoggerFactory.getInstance(PojoEndpointServer.class); 42 | 43 | public PojoEndpointServer(PojoMethodMapping methodMapping, ServerEndpointConfig config, String path) { 44 | addPathPojoMethodMapping(path, methodMapping); 45 | this.config = config; 46 | } 47 | 48 | public boolean hasBeforeHandshake(Channel channel, String path) { 49 | PojoMethodMapping methodMapping = getPojoMethodMapping(path, channel); 50 | return methodMapping.getBeforeHandshake()!=null; 51 | } 52 | 53 | public void doBeforeHandshake(Channel channel, FullHttpRequest req, String path) { 54 | PojoMethodMapping methodMapping = null; 55 | methodMapping = getPojoMethodMapping(path, channel); 56 | 57 | Object implement = null; 58 | try { 59 | implement = methodMapping.getEndpointInstance(); 60 | } catch (Exception e) { 61 | logger.error(e); 62 | return; 63 | } 64 | channel.attr(POJO_KEY).set(implement); 65 | Session session = new Session(channel); 66 | channel.attr(SESSION_KEY).set(session); 67 | Method beforeHandshake = methodMapping.getBeforeHandshake(); 68 | if (beforeHandshake != null) { 69 | try { 70 | beforeHandshake.invoke(implement, methodMapping.getBeforeHandshakeArgs(channel, req)); 71 | } catch (TypeMismatchException e) { 72 | throw e; 73 | } catch (Throwable t) { 74 | logger.error(t); 75 | } 76 | } 77 | } 78 | 79 | public void doOnOpen(Channel channel, FullHttpRequest req, String path) { 80 | PojoMethodMapping methodMapping = getPojoMethodMapping(path, channel); 81 | 82 | Object implement = channel.attr(POJO_KEY).get(); 83 | if (implement==null){ 84 | try { 85 | implement = methodMapping.getEndpointInstance(); 86 | channel.attr(POJO_KEY).set(implement); 87 | } catch (Exception e) { 88 | logger.error(e); 89 | return; 90 | } 91 | Session session = new Session(channel); 92 | channel.attr(SESSION_KEY).set(session); 93 | } 94 | 95 | Method onOpenMethod = methodMapping.getOnOpen(); 96 | if (onOpenMethod != null) { 97 | try { 98 | onOpenMethod.invoke(implement, methodMapping.getOnOpenArgs(channel, req)); 99 | } catch (TypeMismatchException e) { 100 | throw e; 101 | } catch (Throwable t) { 102 | logger.error(t); 103 | } 104 | } 105 | } 106 | 107 | public void doOnClose(Channel channel) { 108 | Attribute attrPath = channel.attr(PATH_KEY); 109 | PojoMethodMapping methodMapping = null; 110 | if (pathMethodMappingMap.size() == 1) { 111 | methodMapping = pathMethodMappingMap.values().iterator().next(); 112 | } else { 113 | String path = attrPath.get(); 114 | methodMapping = pathMethodMappingMap.get(path); 115 | if (methodMapping == null) { 116 | return; 117 | } 118 | } 119 | if (methodMapping.getOnClose() != null) { 120 | if (!channel.hasAttr(SESSION_KEY)) { 121 | return; 122 | } 123 | Object implement = channel.attr(POJO_KEY).get(); 124 | try { 125 | methodMapping.getOnClose().invoke(implement, 126 | methodMapping.getOnCloseArgs(channel)); 127 | } catch (Throwable t) { 128 | logger.error(t); 129 | } 130 | } 131 | } 132 | 133 | 134 | public void doOnError(Channel channel, Throwable throwable) { 135 | Attribute attrPath = channel.attr(PATH_KEY); 136 | PojoMethodMapping methodMapping = null; 137 | if (pathMethodMappingMap.size() == 1) { 138 | methodMapping = pathMethodMappingMap.values().iterator().next(); 139 | } else { 140 | String path = attrPath.get(); 141 | methodMapping = pathMethodMappingMap.get(path); 142 | } 143 | if (methodMapping.getOnError() != null) { 144 | if (!channel.hasAttr(SESSION_KEY)) { 145 | return; 146 | } 147 | Object implement = channel.attr(POJO_KEY).get(); 148 | try { 149 | Method method = methodMapping.getOnError(); 150 | Object[] args = methodMapping.getOnErrorArgs(channel, throwable); 151 | method.invoke(implement, args); 152 | } catch (Throwable t) { 153 | logger.error(t); 154 | } 155 | } 156 | } 157 | 158 | public void doOnMessage(Channel channel, WebSocketFrame frame) { 159 | Attribute attrPath = channel.attr(PATH_KEY); 160 | PojoMethodMapping methodMapping = null; 161 | if (pathMethodMappingMap.size() == 1) { 162 | methodMapping = pathMethodMappingMap.values().iterator().next(); 163 | } else { 164 | String path = attrPath.get(); 165 | methodMapping = pathMethodMappingMap.get(path); 166 | } 167 | if (methodMapping.getOnMessage() != null) { 168 | TextWebSocketFrame textFrame = (TextWebSocketFrame) frame; 169 | Object implement = channel.attr(POJO_KEY).get(); 170 | try { 171 | methodMapping.getOnMessage().invoke(implement, methodMapping.getOnMessageArgs(channel, textFrame)); 172 | } catch (Throwable t) { 173 | logger.error(t); 174 | } 175 | } 176 | } 177 | 178 | public void doOnBinary(Channel channel, WebSocketFrame frame) { 179 | Attribute attrPath = channel.attr(PATH_KEY); 180 | PojoMethodMapping methodMapping = null; 181 | if (pathMethodMappingMap.size() == 1) { 182 | methodMapping = pathMethodMappingMap.values().iterator().next(); 183 | } else { 184 | String path = attrPath.get(); 185 | methodMapping = pathMethodMappingMap.get(path); 186 | } 187 | if (methodMapping.getOnBinary() != null) { 188 | BinaryWebSocketFrame binaryWebSocketFrame = (BinaryWebSocketFrame) frame; 189 | Object implement = channel.attr(POJO_KEY).get(); 190 | try { 191 | methodMapping.getOnBinary().invoke(implement, methodMapping.getOnBinaryArgs(channel, binaryWebSocketFrame)); 192 | } catch (Throwable t) { 193 | logger.error(t); 194 | } 195 | } 196 | } 197 | 198 | public void doOnEvent(Channel channel, Object evt) { 199 | Attribute attrPath = channel.attr(PATH_KEY); 200 | PojoMethodMapping methodMapping = null; 201 | if (pathMethodMappingMap.size() == 1) { 202 | methodMapping = pathMethodMappingMap.values().iterator().next(); 203 | } else { 204 | String path = attrPath.get(); 205 | methodMapping = pathMethodMappingMap.get(path); 206 | } 207 | if (methodMapping.getOnEvent() != null) { 208 | if (!channel.hasAttr(SESSION_KEY)) { 209 | return; 210 | } 211 | Object implement = channel.attr(POJO_KEY).get(); 212 | try { 213 | methodMapping.getOnEvent().invoke(implement, methodMapping.getOnEventArgs(channel, evt)); 214 | } catch (Throwable t) { 215 | logger.error(t); 216 | } 217 | } 218 | } 219 | 220 | public String getHost() { 221 | return config.getHost(); 222 | } 223 | 224 | public int getPort() { 225 | return config.getPort(); 226 | } 227 | 228 | public Set getPathMatcherSet() { 229 | return pathMatchers; 230 | } 231 | 232 | public void addPathPojoMethodMapping(String path, PojoMethodMapping pojoMethodMapping) { 233 | pathMethodMappingMap.put(path, pojoMethodMapping); 234 | for (MethodArgumentResolver onOpenArgResolver : pojoMethodMapping.getOnOpenArgResolvers()) { 235 | if (onOpenArgResolver instanceof PathVariableMethodArgumentResolver || onOpenArgResolver instanceof PathVariableMapMethodArgumentResolver) { 236 | pathMatchers.add(new AntPathMatcherWrapper(path)); 237 | return; 238 | } 239 | } 240 | pathMatchers.add(new DefaultPathMatcher(path)); 241 | } 242 | 243 | private PojoMethodMapping getPojoMethodMapping(String path, Channel channel) { 244 | PojoMethodMapping methodMapping; 245 | if (pathMethodMappingMap.size() == 1) { 246 | methodMapping = pathMethodMappingMap.values().iterator().next(); 247 | } else { 248 | Attribute attrPath = channel.attr(PATH_KEY); 249 | attrPath.set(path); 250 | methodMapping = pathMethodMappingMap.get(path); 251 | if (methodMapping == null) { 252 | throw new RuntimeException("path " + path + " is not in pathMethodMappingMap "); 253 | } 254 | } 255 | return methodMapping; 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /src/main/java/org/yeauty/pojo/PojoMethodMapping.java: -------------------------------------------------------------------------------- 1 | package org.yeauty.pojo; 2 | 3 | import io.netty.channel.Channel; 4 | import io.netty.handler.codec.http.FullHttpRequest; 5 | import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; 6 | import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; 7 | import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor; 8 | import org.springframework.beans.factory.config.AutowireCapableBeanFactory; 9 | import org.springframework.beans.factory.support.AbstractBeanFactory; 10 | import org.springframework.context.ApplicationContext; 11 | import org.springframework.core.DefaultParameterNameDiscoverer; 12 | import org.springframework.core.MethodParameter; 13 | import org.springframework.core.ParameterNameDiscoverer; 14 | import org.yeauty.annotation.*; 15 | import org.yeauty.exception.DeploymentException; 16 | import org.yeauty.support.*; 17 | 18 | import java.lang.annotation.Annotation; 19 | import java.lang.reflect.InvocationTargetException; 20 | import java.lang.reflect.Method; 21 | import java.lang.reflect.Modifier; 22 | import java.util.ArrayList; 23 | import java.util.Arrays; 24 | import java.util.List; 25 | 26 | public class PojoMethodMapping { 27 | 28 | private static final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); 29 | 30 | private final Method beforeHandshake; 31 | private final Method onOpen; 32 | private final Method onClose; 33 | private final Method onError; 34 | private final Method onMessage; 35 | private final Method onBinary; 36 | private final Method onEvent; 37 | private final MethodParameter[] beforeHandshakeParameters; 38 | private final MethodParameter[] onOpenParameters; 39 | private final MethodParameter[] onCloseParameters; 40 | private final MethodParameter[] onErrorParameters; 41 | private final MethodParameter[] onMessageParameters; 42 | private final MethodParameter[] onBinaryParameters; 43 | private final MethodParameter[] onEventParameters; 44 | private final MethodArgumentResolver[] beforeHandshakeArgResolvers; 45 | private final MethodArgumentResolver[] onOpenArgResolvers; 46 | private final MethodArgumentResolver[] onCloseArgResolvers; 47 | private final MethodArgumentResolver[] onErrorArgResolvers; 48 | private final MethodArgumentResolver[] onMessageArgResolvers; 49 | private final MethodArgumentResolver[] onBinaryArgResolvers; 50 | private final MethodArgumentResolver[] onEventArgResolvers; 51 | private final Class pojoClazz; 52 | private final ApplicationContext applicationContext; 53 | private final AbstractBeanFactory beanFactory; 54 | 55 | public PojoMethodMapping(Class pojoClazz, ApplicationContext context, AbstractBeanFactory beanFactory) throws DeploymentException { 56 | this.applicationContext = context; 57 | this.pojoClazz = pojoClazz; 58 | this.beanFactory = beanFactory; 59 | Method handshake = null; 60 | Method open = null; 61 | Method close = null; 62 | Method error = null; 63 | Method message = null; 64 | Method binary = null; 65 | Method event = null; 66 | Method[] pojoClazzMethods = null; 67 | Class currentClazz = pojoClazz; 68 | while (!currentClazz.equals(Object.class)) { 69 | Method[] currentClazzMethods = currentClazz.getDeclaredMethods(); 70 | if (currentClazz == pojoClazz) { 71 | pojoClazzMethods = currentClazzMethods; 72 | } 73 | for (Method method : currentClazzMethods) { 74 | if (method.getAnnotation(BeforeHandshake.class) != null) { 75 | checkPublic(method); 76 | if (handshake == null) { 77 | handshake = method; 78 | } else { 79 | if (currentClazz == pojoClazz || 80 | !isMethodOverride(handshake, method)) { 81 | // Duplicate annotation 82 | throw new DeploymentException( 83 | "pojoMethodMapping.duplicateAnnotation BeforeHandshake"); 84 | } 85 | } 86 | } else if (method.getAnnotation(OnOpen.class) != null) { 87 | checkPublic(method); 88 | if (open == null) { 89 | open = method; 90 | } else { 91 | if (currentClazz == pojoClazz || 92 | !isMethodOverride(open, method)) { 93 | // Duplicate annotation 94 | throw new DeploymentException( 95 | "pojoMethodMapping.duplicateAnnotation OnOpen"); 96 | } 97 | } 98 | } else if (method.getAnnotation(OnClose.class) != null) { 99 | checkPublic(method); 100 | if (close == null) { 101 | close = method; 102 | } else { 103 | if (currentClazz == pojoClazz || 104 | !isMethodOverride(close, method)) { 105 | // Duplicate annotation 106 | throw new DeploymentException( 107 | "pojoMethodMapping.duplicateAnnotation OnClose"); 108 | } 109 | } 110 | } else if (method.getAnnotation(OnError.class) != null) { 111 | checkPublic(method); 112 | if (error == null) { 113 | error = method; 114 | } else { 115 | if (currentClazz == pojoClazz || 116 | !isMethodOverride(error, method)) { 117 | // Duplicate annotation 118 | throw new DeploymentException( 119 | "pojoMethodMapping.duplicateAnnotation OnError"); 120 | } 121 | } 122 | } else if (method.getAnnotation(OnMessage.class) != null) { 123 | checkPublic(method); 124 | if (message == null) { 125 | message = method; 126 | } else { 127 | if (currentClazz == pojoClazz || 128 | !isMethodOverride(message, method)) { 129 | // Duplicate annotation 130 | throw new DeploymentException( 131 | "pojoMethodMapping.duplicateAnnotation onMessage"); 132 | } 133 | } 134 | } else if (method.getAnnotation(OnBinary.class) != null) { 135 | checkPublic(method); 136 | if (binary == null) { 137 | binary = method; 138 | } else { 139 | if (currentClazz == pojoClazz || 140 | !isMethodOverride(binary, method)) { 141 | // Duplicate annotation 142 | throw new DeploymentException( 143 | "pojoMethodMapping.duplicateAnnotation OnBinary"); 144 | } 145 | } 146 | } else if (method.getAnnotation(OnEvent.class) != null) { 147 | checkPublic(method); 148 | if (event == null) { 149 | event = method; 150 | } else { 151 | if (currentClazz == pojoClazz || 152 | !isMethodOverride(event, method)) { 153 | // Duplicate annotation 154 | throw new DeploymentException( 155 | "pojoMethodMapping.duplicateAnnotation OnEvent"); 156 | } 157 | } 158 | } else { 159 | // Method not annotated 160 | } 161 | } 162 | currentClazz = currentClazz.getSuperclass(); 163 | } 164 | // If the methods are not on pojoClazz and they are overridden 165 | // by a non annotated method in pojoClazz, they should be ignored 166 | if (handshake != null && handshake.getDeclaringClass() != pojoClazz) { 167 | if (isOverridenWithoutAnnotation(pojoClazzMethods, handshake, BeforeHandshake.class)) { 168 | handshake = null; 169 | } 170 | } 171 | if (open != null && open.getDeclaringClass() != pojoClazz) { 172 | if (isOverridenWithoutAnnotation(pojoClazzMethods, open, OnOpen.class)) { 173 | open = null; 174 | } 175 | } 176 | if (close != null && close.getDeclaringClass() != pojoClazz) { 177 | if (isOverridenWithoutAnnotation(pojoClazzMethods, close, OnClose.class)) { 178 | close = null; 179 | } 180 | } 181 | if (error != null && error.getDeclaringClass() != pojoClazz) { 182 | if (isOverridenWithoutAnnotation(pojoClazzMethods, error, OnError.class)) { 183 | error = null; 184 | } 185 | } 186 | if (message != null && message.getDeclaringClass() != pojoClazz) { 187 | if (isOverridenWithoutAnnotation(pojoClazzMethods, message, OnMessage.class)) { 188 | message = null; 189 | } 190 | } 191 | if (binary != null && binary.getDeclaringClass() != pojoClazz) { 192 | if (isOverridenWithoutAnnotation(pojoClazzMethods, binary, OnBinary.class)) { 193 | binary = null; 194 | } 195 | } 196 | if (event != null && event.getDeclaringClass() != pojoClazz) { 197 | if (isOverridenWithoutAnnotation(pojoClazzMethods, event, OnEvent.class)) { 198 | event = null; 199 | } 200 | } 201 | 202 | this.beforeHandshake = handshake; 203 | this.onOpen = open; 204 | this.onClose = close; 205 | this.onError = error; 206 | this.onMessage = message; 207 | this.onBinary = binary; 208 | this.onEvent = event; 209 | beforeHandshakeParameters = getParameters(beforeHandshake); 210 | onOpenParameters = getParameters(onOpen); 211 | onCloseParameters = getParameters(onClose); 212 | onMessageParameters = getParameters(onMessage); 213 | onErrorParameters = getParameters(onError); 214 | onBinaryParameters = getParameters(onBinary); 215 | onEventParameters = getParameters(onEvent); 216 | beforeHandshakeArgResolvers = getResolvers(beforeHandshakeParameters); 217 | onOpenArgResolvers = getResolvers(onOpenParameters); 218 | onCloseArgResolvers = getResolvers(onCloseParameters); 219 | onMessageArgResolvers = getResolvers(onMessageParameters); 220 | onErrorArgResolvers = getResolvers(onErrorParameters); 221 | onBinaryArgResolvers = getResolvers(onBinaryParameters); 222 | onEventArgResolvers = getResolvers(onEventParameters); 223 | } 224 | 225 | private void checkPublic(Method m) throws DeploymentException { 226 | if (!Modifier.isPublic(m.getModifiers())) { 227 | throw new DeploymentException( 228 | "pojoMethodMapping.methodNotPublic " + m.getName()); 229 | } 230 | } 231 | 232 | private boolean isMethodOverride(Method method1, Method method2) { 233 | return (method1.getName().equals(method2.getName()) 234 | && method1.getReturnType().equals(method2.getReturnType()) 235 | && Arrays.equals(method1.getParameterTypes(), method2.getParameterTypes())); 236 | } 237 | 238 | private boolean isOverridenWithoutAnnotation(Method[] methods, Method superclazzMethod, Class annotation) { 239 | for (Method method : methods) { 240 | if (isMethodOverride(method, superclazzMethod) 241 | && (method.getAnnotation(annotation) == null)) { 242 | return true; 243 | } 244 | } 245 | return false; 246 | } 247 | 248 | Object getEndpointInstance() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { 249 | Object implement = pojoClazz.getDeclaredConstructor().newInstance(); 250 | AutowireCapableBeanFactory beanFactory = applicationContext.getAutowireCapableBeanFactory(); 251 | beanFactory.autowireBean(implement); 252 | return implement; 253 | } 254 | 255 | Method getBeforeHandshake() { 256 | return beforeHandshake; 257 | } 258 | 259 | Object[] getBeforeHandshakeArgs(Channel channel, FullHttpRequest req) throws Exception { 260 | return getMethodArgumentValues(channel, req, beforeHandshakeParameters, beforeHandshakeArgResolvers); 261 | } 262 | 263 | Method getOnOpen() { 264 | return onOpen; 265 | } 266 | 267 | Object[] getOnOpenArgs(Channel channel, FullHttpRequest req) throws Exception { 268 | return getMethodArgumentValues(channel, req, onOpenParameters, onOpenArgResolvers); 269 | } 270 | 271 | MethodArgumentResolver[] getOnOpenArgResolvers() { 272 | return onOpenArgResolvers; 273 | } 274 | 275 | Method getOnClose() { 276 | return onClose; 277 | } 278 | 279 | Object[] getOnCloseArgs(Channel channel) throws Exception { 280 | return getMethodArgumentValues(channel, null, onCloseParameters, onCloseArgResolvers); 281 | } 282 | 283 | Method getOnError() { 284 | return onError; 285 | } 286 | 287 | Object[] getOnErrorArgs(Channel channel, Throwable throwable) throws Exception { 288 | return getMethodArgumentValues(channel, throwable, onErrorParameters, onErrorArgResolvers); 289 | } 290 | 291 | Method getOnMessage() { 292 | return onMessage; 293 | } 294 | 295 | Object[] getOnMessageArgs(Channel channel, TextWebSocketFrame textWebSocketFrame) throws Exception { 296 | return getMethodArgumentValues(channel, textWebSocketFrame, onMessageParameters, onMessageArgResolvers); 297 | } 298 | 299 | Method getOnBinary() { 300 | return onBinary; 301 | } 302 | 303 | Object[] getOnBinaryArgs(Channel channel, BinaryWebSocketFrame binaryWebSocketFrame) throws Exception { 304 | return getMethodArgumentValues(channel, binaryWebSocketFrame, onBinaryParameters, onBinaryArgResolvers); 305 | } 306 | 307 | Method getOnEvent() { 308 | return onEvent; 309 | } 310 | 311 | Object[] getOnEventArgs(Channel channel, Object evt) throws Exception { 312 | return getMethodArgumentValues(channel, evt, onEventParameters, onEventArgResolvers); 313 | } 314 | 315 | private Object[] getMethodArgumentValues(Channel channel, Object object, MethodParameter[] parameters, MethodArgumentResolver[] resolvers) throws Exception { 316 | Object[] objects = new Object[parameters.length]; 317 | for (int i = 0; i < parameters.length; i++) { 318 | MethodParameter parameter = parameters[i]; 319 | MethodArgumentResolver resolver = resolvers[i]; 320 | Object arg = resolver.resolveArgument(parameter, channel, object); 321 | objects[i] = arg; 322 | } 323 | return objects; 324 | } 325 | 326 | private MethodArgumentResolver[] getResolvers(MethodParameter[] parameters) throws DeploymentException { 327 | MethodArgumentResolver[] methodArgumentResolvers = new MethodArgumentResolver[parameters.length]; 328 | List resolvers = getDefaultResolvers(); 329 | for (int i = 0; i < parameters.length; i++) { 330 | MethodParameter parameter = parameters[i]; 331 | for (MethodArgumentResolver resolver : resolvers) { 332 | if (resolver.supportsParameter(parameter)) { 333 | methodArgumentResolvers[i] = resolver; 334 | break; 335 | } 336 | } 337 | if (methodArgumentResolvers[i] == null) { 338 | throw new DeploymentException("pojoMethodMapping.paramClassIncorrect parameter name : " + parameter.getParameterName()); 339 | } 340 | } 341 | return methodArgumentResolvers; 342 | } 343 | 344 | private List getDefaultResolvers() { 345 | List resolvers = new ArrayList<>(); 346 | resolvers.add(new SessionMethodArgumentResolver()); 347 | resolvers.add(new HttpHeadersMethodArgumentResolver()); 348 | resolvers.add(new TextMethodArgumentResolver()); 349 | resolvers.add(new ThrowableMethodArgumentResolver()); 350 | resolvers.add(new ByteMethodArgumentResolver()); 351 | resolvers.add(new RequestParamMapMethodArgumentResolver()); 352 | resolvers.add(new RequestParamMethodArgumentResolver(beanFactory)); 353 | resolvers.add(new PathVariableMapMethodArgumentResolver()); 354 | resolvers.add(new PathVariableMethodArgumentResolver(beanFactory)); 355 | resolvers.add(new EventMethodArgumentResolver(beanFactory)); 356 | return resolvers; 357 | } 358 | 359 | private static MethodParameter[] getParameters(Method m) { 360 | if (m == null) { 361 | return new MethodParameter[0]; 362 | } 363 | int count = m.getParameterCount(); 364 | MethodParameter[] result = new MethodParameter[count]; 365 | for (int i = 0; i < count; i++) { 366 | MethodParameter methodParameter = new MethodParameter(m, i); 367 | methodParameter.initParameterNameDiscovery(parameterNameDiscoverer); 368 | result[i] = methodParameter; 369 | } 370 | return result; 371 | } 372 | } -------------------------------------------------------------------------------- /src/main/java/org/yeauty/pojo/Session.java: -------------------------------------------------------------------------------- 1 | package org.yeauty.pojo; 2 | 3 | import io.netty.buffer.ByteBuf; 4 | import io.netty.buffer.ByteBufAllocator; 5 | import io.netty.channel.*; 6 | import io.netty.channel.socket.DatagramChannel; 7 | import io.netty.channel.socket.DatagramPacket; 8 | import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; 9 | import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame; 10 | import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; 11 | import io.netty.handler.codec.http.websocketx.WebSocketCloseStatus; 12 | import io.netty.util.AttributeKey; 13 | 14 | import java.net.InetSocketAddress; 15 | import java.net.SocketAddress; 16 | import java.nio.ByteBuffer; 17 | 18 | /** 19 | * @author Yeauty 20 | * @version 1.0 21 | */ 22 | public class Session { 23 | 24 | private final Channel channel; 25 | 26 | Session(Channel channel) { 27 | this.channel = channel; 28 | } 29 | 30 | /** 31 | * set subprotocols on {@link org.yeauty.annotation.BeforeHandshake} 32 | * @param subprotocols 33 | */ 34 | public void setSubprotocols(String subprotocols) { 35 | setAttribute("subprotocols",subprotocols); 36 | } 37 | 38 | public ChannelFuture sendText(String message) { 39 | return channel.writeAndFlush(new TextWebSocketFrame(message)); 40 | } 41 | 42 | public ChannelFuture sendText(ByteBuf byteBuf) { 43 | return channel.writeAndFlush(new TextWebSocketFrame(byteBuf)); 44 | } 45 | 46 | public ChannelFuture sendText(ByteBuffer byteBuffer) { 47 | ByteBuf buffer = channel.alloc().buffer(byteBuffer.remaining()); 48 | buffer.writeBytes(byteBuffer); 49 | return channel.writeAndFlush(new TextWebSocketFrame(buffer)); 50 | } 51 | 52 | public ChannelFuture sendText(TextWebSocketFrame textWebSocketFrame) { 53 | return channel.writeAndFlush(textWebSocketFrame); 54 | } 55 | 56 | public ChannelFuture sendBinary(byte[] bytes) { 57 | ByteBuf buffer = channel.alloc().buffer(bytes.length); 58 | return channel.writeAndFlush(new BinaryWebSocketFrame(buffer.writeBytes(bytes))); 59 | } 60 | 61 | public ChannelFuture sendBinary(ByteBuf byteBuf) { 62 | return channel.writeAndFlush(new BinaryWebSocketFrame(byteBuf)); 63 | } 64 | 65 | public ChannelFuture sendBinary(ByteBuffer byteBuffer) { 66 | ByteBuf buffer = channel.alloc().buffer(byteBuffer.remaining()); 67 | buffer.writeBytes(byteBuffer); 68 | return channel.writeAndFlush(new BinaryWebSocketFrame(buffer)); 69 | } 70 | 71 | public ChannelFuture sendBinary(BinaryWebSocketFrame binaryWebSocketFrame) { 72 | return channel.writeAndFlush(binaryWebSocketFrame); 73 | } 74 | 75 | public void setAttribute(String name, T value) { 76 | AttributeKey sessionIdKey = AttributeKey.valueOf(name); 77 | channel.attr(sessionIdKey).set(value); 78 | } 79 | 80 | public T getAttribute(String name) { 81 | AttributeKey sessionIdKey = AttributeKey.valueOf(name); 82 | return channel.attr(sessionIdKey).get(); 83 | } 84 | 85 | public Channel channel() { 86 | return channel; 87 | } 88 | 89 | /** 90 | * Returns the globally unique identifier of this {@link Channel}. 91 | */ 92 | public ChannelId id() { 93 | return channel.id(); 94 | } 95 | 96 | /** 97 | * Returns the configuration of this channel. 98 | */ 99 | public ChannelConfig config() { 100 | return channel.config(); 101 | } 102 | 103 | /** 104 | * Returns {@code true} if the {@link Channel} is open and may get active later 105 | */ 106 | public boolean isOpen() { 107 | return channel.isOpen(); 108 | } 109 | 110 | /** 111 | * Returns {@code true} if the {@link Channel} is registered with an {@link EventLoop}. 112 | */ 113 | public boolean isRegistered() { 114 | return channel.isRegistered(); 115 | } 116 | 117 | /** 118 | * Return {@code true} if the {@link Channel} is active and so connected. 119 | */ 120 | public boolean isActive() { 121 | return channel.isActive(); 122 | } 123 | 124 | /** 125 | * Return the {@link ChannelMetadata} of the {@link Channel} which describe the nature of the {@link Channel}. 126 | */ 127 | public ChannelMetadata metadata() { 128 | return channel.metadata(); 129 | } 130 | 131 | /** 132 | * Returns the local address where this channel is bound to. The returned 133 | * {@link SocketAddress} is supposed to be down-cast into more concrete 134 | * type such as {@link InetSocketAddress} to retrieve the detailed 135 | * information. 136 | * 137 | * @return the local address of this channel. 138 | * {@code null} if this channel is not bound. 139 | */ 140 | public SocketAddress localAddress() { 141 | return channel.localAddress(); 142 | } 143 | 144 | /** 145 | * Returns the remote address where this channel is connected to. The 146 | * returned {@link SocketAddress} is supposed to be down-cast into more 147 | * concrete type such as {@link InetSocketAddress} to retrieve the detailed 148 | * information. 149 | * 150 | * @return the remote address of this channel. 151 | * {@code null} if this channel is not connected. 152 | * If this channel is not connected but it can receive messages 153 | * from arbitrary remote addresses (e.g. {@link DatagramChannel}, 154 | * use {@link DatagramPacket#recipient()} to determine 155 | * the origination of the received message as this method will 156 | * return {@code null}. 157 | */ 158 | public SocketAddress remoteAddress() { 159 | return channel.remoteAddress(); 160 | } 161 | 162 | /** 163 | * Returns the {@link ChannelFuture} which will be notified when this 164 | * channel is closed. This method always returns the same future instance. 165 | */ 166 | public ChannelFuture closeFuture() { 167 | return channel.closeFuture(); 168 | } 169 | 170 | /** 171 | * Returns {@code true} if and only if the I/O thread will perform the 172 | * requested write operation immediately. Any write requests made when 173 | * this method returns {@code false} are queued until the I/O thread is 174 | * ready to process the queued write requests. 175 | */ 176 | public boolean isWritable() { 177 | return channel.isWritable(); 178 | } 179 | 180 | /** 181 | * Get how many bytes can be written until {@link #isWritable()} returns {@code false}. 182 | * This quantity will always be non-negative. If {@link #isWritable()} is {@code false} then 0. 183 | */ 184 | public long bytesBeforeUnwritable() { 185 | return channel.bytesBeforeUnwritable(); 186 | } 187 | 188 | /** 189 | * Get how many bytes must be drained from underlying buffers until {@link #isWritable()} returns {@code true}. 190 | * This quantity will always be non-negative. If {@link #isWritable()} is {@code true} then 0. 191 | */ 192 | public long bytesBeforeWritable() { 193 | return channel.bytesBeforeWritable(); 194 | } 195 | 196 | /** 197 | * Returns an internal-use-only object that provides unsafe operations. 198 | */ 199 | public Channel.Unsafe unsafe() { 200 | return channel.unsafe(); 201 | } 202 | 203 | /** 204 | * Return the assigned {@link ChannelPipeline}. 205 | */ 206 | public ChannelPipeline pipeline() { 207 | return channel.pipeline(); 208 | } 209 | 210 | /** 211 | * Return the assigned {@link ByteBufAllocator} which will be used to allocate {@link ByteBuf}s. 212 | */ 213 | public ByteBufAllocator alloc() { 214 | return channel.alloc(); 215 | } 216 | 217 | public Channel read() { 218 | return channel.read(); 219 | } 220 | 221 | public Channel flush() { 222 | return channel.flush(); 223 | } 224 | 225 | public ChannelFuture close() { 226 | return channel.close(); 227 | } 228 | 229 | public ChannelFuture close(ChannelPromise promise) { 230 | channel.writeAndFlush(new CloseWebSocketFrame(WebSocketCloseStatus.NORMAL_CLOSURE)); 231 | return channel.close(promise); 232 | } 233 | 234 | } 235 | -------------------------------------------------------------------------------- /src/main/java/org/yeauty/standard/EndpointClassPathScanner.java: -------------------------------------------------------------------------------- 1 | package org.yeauty.standard; 2 | 3 | import org.springframework.beans.factory.config.BeanDefinitionHolder; 4 | import org.springframework.beans.factory.support.BeanDefinitionRegistry; 5 | import org.springframework.context.annotation.ClassPathBeanDefinitionScanner; 6 | import org.springframework.core.type.filter.AnnotationTypeFilter; 7 | import org.yeauty.annotation.ServerEndpoint; 8 | 9 | import java.util.Set; 10 | 11 | public class EndpointClassPathScanner extends ClassPathBeanDefinitionScanner { 12 | 13 | public EndpointClassPathScanner(BeanDefinitionRegistry registry, boolean useDefaultFilters) { 14 | super(registry, useDefaultFilters); 15 | } 16 | 17 | @Override 18 | protected Set doScan(String... basePackages) { 19 | addIncludeFilter(new AnnotationTypeFilter(ServerEndpoint.class)); 20 | return super.doScan(basePackages); 21 | } 22 | } -------------------------------------------------------------------------------- /src/main/java/org/yeauty/standard/HttpServerHandler.java: -------------------------------------------------------------------------------- 1 | package org.yeauty.standard; 2 | 3 | import io.netty.buffer.ByteBuf; 4 | import io.netty.buffer.ByteBufAllocator; 5 | import io.netty.buffer.Unpooled; 6 | import io.netty.channel.*; 7 | import io.netty.handler.codec.http.*; 8 | import io.netty.handler.codec.http.cors.CorsHandler; 9 | import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame; 10 | import io.netty.handler.codec.http.websocketx.WebSocketFrameAggregator; 11 | import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker; 12 | import io.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory; 13 | import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketServerCompressionHandler; 14 | import io.netty.handler.timeout.IdleStateHandler; 15 | import io.netty.util.AttributeKey; 16 | import io.netty.util.CharsetUtil; 17 | import io.netty.util.concurrent.EventExecutorGroup; 18 | import org.springframework.beans.TypeMismatchException; 19 | import org.springframework.util.StringUtils; 20 | import org.yeauty.pojo.PojoEndpointServer; 21 | import org.yeauty.support.WsPathMatcher; 22 | 23 | import java.io.InputStream; 24 | import java.util.Set; 25 | 26 | import static io.netty.handler.codec.http.HttpHeaderNames.*; 27 | import static io.netty.handler.codec.http.HttpMethod.GET; 28 | import static io.netty.handler.codec.http.HttpResponseStatus.*; 29 | import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; 30 | 31 | class HttpServerHandler extends SimpleChannelInboundHandler { 32 | 33 | private final PojoEndpointServer pojoEndpointServer; 34 | private final ServerEndpointConfig config; 35 | private final EventExecutorGroup eventExecutorGroup; 36 | private final boolean isCors; 37 | 38 | private static ByteBuf faviconByteBuf = null; 39 | private static ByteBuf notFoundByteBuf = null; 40 | private static ByteBuf badRequestByteBuf = null; 41 | private static ByteBuf forbiddenByteBuf = null; 42 | private static ByteBuf internalServerErrorByteBuf = null; 43 | 44 | static { 45 | faviconByteBuf = buildStaticRes("/favicon.ico"); 46 | notFoundByteBuf = buildStaticRes("/public/error/404.html"); 47 | badRequestByteBuf = buildStaticRes("/public/error/400.html"); 48 | forbiddenByteBuf = buildStaticRes("/public/error/403.html"); 49 | internalServerErrorByteBuf = buildStaticRes("/public/error/500.html"); 50 | if (notFoundByteBuf == null) { 51 | notFoundByteBuf = buildStaticRes("/public/error/4xx.html"); 52 | } 53 | if (badRequestByteBuf == null) { 54 | badRequestByteBuf = buildStaticRes("/public/error/4xx.html"); 55 | } 56 | if (forbiddenByteBuf == null) { 57 | forbiddenByteBuf = buildStaticRes("/public/error/4xx.html"); 58 | } 59 | if (internalServerErrorByteBuf == null) { 60 | internalServerErrorByteBuf = buildStaticRes("/public/error/5xx.html"); 61 | } 62 | } 63 | 64 | 65 | private static ByteBuf buildStaticRes(String resPath) { 66 | try { 67 | InputStream inputStream = HttpServerHandler.class.getResourceAsStream(resPath); 68 | if (inputStream != null) { 69 | int available = inputStream.available(); 70 | if (available != 0) { 71 | byte[] bytes = new byte[available]; 72 | inputStream.read(bytes); 73 | return ByteBufAllocator.DEFAULT.buffer(bytes.length).writeBytes(bytes); 74 | } 75 | } 76 | } catch (Exception e) { 77 | } 78 | return null; 79 | } 80 | 81 | public HttpServerHandler(PojoEndpointServer pojoEndpointServer, ServerEndpointConfig config, EventExecutorGroup eventExecutorGroup, boolean isCors) { 82 | this.pojoEndpointServer = pojoEndpointServer; 83 | this.config = config; 84 | this.eventExecutorGroup = eventExecutorGroup; 85 | this.isCors = isCors; 86 | } 87 | 88 | @Override 89 | protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest msg) throws Exception { 90 | try { 91 | handleHttpRequest(ctx, msg); 92 | } catch (TypeMismatchException e) { 93 | FullHttpResponse res = new DefaultFullHttpResponse(HTTP_1_1, BAD_REQUEST); 94 | sendHttpResponse(ctx, msg, res); 95 | e.printStackTrace(); 96 | } catch (Exception e) { 97 | FullHttpResponse res; 98 | if (internalServerErrorByteBuf != null) { 99 | res = new DefaultFullHttpResponse(HTTP_1_1, INTERNAL_SERVER_ERROR, internalServerErrorByteBuf.retainedDuplicate()); 100 | } else { 101 | res = new DefaultFullHttpResponse(HTTP_1_1, INTERNAL_SERVER_ERROR); 102 | } 103 | sendHttpResponse(ctx, msg, res); 104 | e.printStackTrace(); 105 | } 106 | } 107 | 108 | @Override 109 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { 110 | pojoEndpointServer.doOnError(ctx.channel(), cause); 111 | } 112 | 113 | @Override 114 | public void channelInactive(ChannelHandlerContext ctx) throws Exception { 115 | pojoEndpointServer.doOnClose(ctx.channel()); 116 | super.channelInactive(ctx); 117 | } 118 | 119 | private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest req) { 120 | FullHttpResponse res; 121 | // Handle a bad request. 122 | if (!req.decoderResult().isSuccess()) { 123 | if (badRequestByteBuf != null) { 124 | res = new DefaultFullHttpResponse(HTTP_1_1, BAD_REQUEST, badRequestByteBuf.retainedDuplicate()); 125 | } else { 126 | res = new DefaultFullHttpResponse(HTTP_1_1, BAD_REQUEST); 127 | } 128 | sendHttpResponse(ctx, req, res); 129 | return; 130 | } 131 | 132 | // Allow only GET methods. 133 | if (req.method() != GET) { 134 | if (forbiddenByteBuf != null) { 135 | res = new DefaultFullHttpResponse(HTTP_1_1, FORBIDDEN, forbiddenByteBuf.retainedDuplicate()); 136 | } else { 137 | res = new DefaultFullHttpResponse(HTTP_1_1, FORBIDDEN); 138 | } 139 | sendHttpResponse(ctx, req, res); 140 | return; 141 | } 142 | 143 | HttpHeaders headers = req.headers(); 144 | String host = headers.get(HttpHeaderNames.HOST); 145 | if (StringUtils.isEmpty(host)) { 146 | if (forbiddenByteBuf != null) { 147 | res = new DefaultFullHttpResponse(HTTP_1_1, FORBIDDEN, forbiddenByteBuf.retainedDuplicate()); 148 | } else { 149 | res = new DefaultFullHttpResponse(HTTP_1_1, FORBIDDEN); 150 | } 151 | sendHttpResponse(ctx, req, res); 152 | return; 153 | } 154 | 155 | if (!StringUtils.isEmpty(pojoEndpointServer.getHost()) && !pojoEndpointServer.getHost().equals("0.0.0.0") && !pojoEndpointServer.getHost().equals(host.split(":")[0])) { 156 | if (forbiddenByteBuf != null) { 157 | res = new DefaultFullHttpResponse(HTTP_1_1, FORBIDDEN, forbiddenByteBuf.retainedDuplicate()); 158 | } else { 159 | res = new DefaultFullHttpResponse(HTTP_1_1, FORBIDDEN); 160 | } 161 | sendHttpResponse(ctx, req, res); 162 | return; 163 | } 164 | 165 | QueryStringDecoder decoder = new QueryStringDecoder(req.uri()); 166 | String path = decoder.path(); 167 | if ("/favicon.ico".equals(path)) { 168 | if (faviconByteBuf != null) { 169 | res = new DefaultFullHttpResponse(HTTP_1_1, OK, faviconByteBuf.retainedDuplicate()); 170 | } else { 171 | res = new DefaultFullHttpResponse(HTTP_1_1, NOT_FOUND); 172 | } 173 | sendHttpResponse(ctx, req, res); 174 | return; 175 | } 176 | 177 | Channel channel = ctx.channel(); 178 | 179 | //path match 180 | String pattern = null; 181 | Set pathMatcherSet = pojoEndpointServer.getPathMatcherSet(); 182 | for (WsPathMatcher pathMatcher : pathMatcherSet) { 183 | if (pathMatcher.matchAndExtract(decoder, channel)) { 184 | pattern = pathMatcher.getPattern(); 185 | break; 186 | } 187 | } 188 | 189 | if (pattern == null) { 190 | if (notFoundByteBuf != null) { 191 | res = new DefaultFullHttpResponse(HTTP_1_1, NOT_FOUND, notFoundByteBuf.retainedDuplicate()); 192 | } else { 193 | res = new DefaultFullHttpResponse(HTTP_1_1, NOT_FOUND); 194 | } 195 | sendHttpResponse(ctx, req, res); 196 | return; 197 | } 198 | 199 | if (!req.headers().contains(UPGRADE) || !req.headers().contains(SEC_WEBSOCKET_KEY) || !req.headers().contains(SEC_WEBSOCKET_VERSION)) { 200 | if (forbiddenByteBuf != null) { 201 | res = new DefaultFullHttpResponse(HTTP_1_1, FORBIDDEN, forbiddenByteBuf.retainedDuplicate()); 202 | } else { 203 | res = new DefaultFullHttpResponse(HTTP_1_1, FORBIDDEN); 204 | } 205 | sendHttpResponse(ctx, req, res); 206 | return; 207 | } 208 | 209 | String subprotocols = null; 210 | 211 | if (pojoEndpointServer.hasBeforeHandshake(channel, pattern)) { 212 | pojoEndpointServer.doBeforeHandshake(channel, req, pattern); 213 | if (!channel.isActive()) { 214 | return; 215 | } 216 | 217 | AttributeKey subprotocolsAttrKey = AttributeKey.valueOf("subprotocols"); 218 | if (channel.hasAttr(subprotocolsAttrKey)) { 219 | subprotocols = ctx.channel().attr(subprotocolsAttrKey).get(); 220 | } 221 | } 222 | ChannelPipeline pipeline = ctx.pipeline(); 223 | if (config.isUseCompressionHandler()) { 224 | // Add WebSocketServerCompressionHandler, but don't shake hands 225 | pipeline.addLast(new WebSocketServerCompressionHandler()); 226 | // Let the request by WebSocketServerCompressionHandler forwarding to the next handler 227 | ctx.fireChannelRead(req.retain()); 228 | } 229 | // Handshake 230 | WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(getWebSocketLocation(req), subprotocols, true, config.getmaxFramePayloadLength()); 231 | WebSocketServerHandshaker handshaker = wsFactory.newHandshaker(req); 232 | if (handshaker == null) { 233 | WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(channel); 234 | } else { 235 | pipeline.remove(ctx.name()); 236 | if (config.getReaderIdleTimeSeconds() != 0 || config.getWriterIdleTimeSeconds() != 0 || config.getAllIdleTimeSeconds() != 0) { 237 | pipeline.addLast(new IdleStateHandler(config.getReaderIdleTimeSeconds(), config.getWriterIdleTimeSeconds(), config.getAllIdleTimeSeconds())); 238 | } 239 | pipeline.addLast(new WebSocketFrameAggregator(Integer.MAX_VALUE)); 240 | if (config.isUseEventExecutorGroup()) { 241 | pipeline.addLast(eventExecutorGroup, new WebSocketServerHandler(pojoEndpointServer)); 242 | } else { 243 | pipeline.addLast(new WebSocketServerHandler(pojoEndpointServer)); 244 | } 245 | String finalPattern = pattern; 246 | 247 | String header = headers.get(HttpHeaders.Names.SEC_WEBSOCKET_PROTOCOL); 248 | HttpHeaders httpHeaders = null; 249 | if (header!=null) { 250 | httpHeaders = new DefaultHttpHeaders().add(HttpHeaders.Names.SEC_WEBSOCKET_PROTOCOL, header); 251 | } 252 | final ChannelFuture handshakeFuture = handshaker.handshake(ctx.channel(), req,httpHeaders,ctx.channel().newPromise()); 253 | 254 | handshakeFuture.addListener((ChannelFutureListener) future -> { 255 | if (future.isSuccess()) { 256 | if (isCors) { 257 | pipeline.remove(CorsHandler.class); 258 | } 259 | pojoEndpointServer.doOnOpen(channel, req, finalPattern); 260 | } else { 261 | handshaker.close(channel, new CloseWebSocketFrame()); 262 | } 263 | }); 264 | } 265 | 266 | } 267 | 268 | private static void sendHttpResponse( 269 | ChannelHandlerContext ctx, FullHttpRequest req, FullHttpResponse res) { 270 | // Generate an error page if response getStatus code is not OK (200). 271 | int statusCode = res.status().code(); 272 | if (statusCode != OK.code() && res.content().readableBytes() == 0) { 273 | ByteBuf buf = Unpooled.copiedBuffer(res.status().toString(), CharsetUtil.UTF_8); 274 | res.content().writeBytes(buf); 275 | buf.release(); 276 | } 277 | HttpUtil.setContentLength(res, res.content().readableBytes()); 278 | 279 | // Send the response and close the connection if necessary. 280 | ChannelFuture f = ctx.channel().writeAndFlush(res); 281 | if (!HttpUtil.isKeepAlive(req) || statusCode != 200) { 282 | f.addListener(ChannelFutureListener.CLOSE); 283 | } 284 | } 285 | 286 | private static String getWebSocketLocation(FullHttpRequest req) { 287 | String location = req.headers().get(HttpHeaderNames.HOST) + req.uri(); 288 | return "ws://" + location; 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /src/main/java/org/yeauty/standard/ServerEndpointConfig.java: -------------------------------------------------------------------------------- 1 | package org.yeauty.standard; 2 | 3 | import org.springframework.util.StringUtils; 4 | 5 | import java.io.IOException; 6 | import java.net.InetSocketAddress; 7 | import java.net.Socket; 8 | 9 | /** 10 | * @author Yeauty 11 | * @version 1.0 12 | */ 13 | public class ServerEndpointConfig { 14 | 15 | private final String HOST; 16 | private final int PORT; 17 | private final int BOSS_LOOP_GROUP_THREADS; 18 | private final int WORKER_LOOP_GROUP_THREADS; 19 | private final boolean USE_COMPRESSION_HANDLER; 20 | private final int CONNECT_TIMEOUT_MILLIS; 21 | private final int SO_BACKLOG; 22 | private final int WRITE_SPIN_COUNT; 23 | private final int WRITE_BUFFER_HIGH_WATER_MARK; 24 | private final int WRITE_BUFFER_LOW_WATER_MARK; 25 | private final int SO_RCVBUF; 26 | private final int SO_SNDBUF; 27 | private final boolean TCP_NODELAY; 28 | private final boolean SO_KEEPALIVE; 29 | private final int SO_LINGER; 30 | private final boolean ALLOW_HALF_CLOSURE; 31 | private final int READER_IDLE_TIME_SECONDS; 32 | private final int WRITER_IDLE_TIME_SECONDS; 33 | private final int ALL_IDLE_TIME_SECONDS; 34 | private final int MAX_FRAME_PAYLOAD_LENGTH; 35 | private final boolean USE_EVENT_EXECUTOR_GROUP; 36 | private final int EVENT_EXECUTOR_GROUP_THREADS; 37 | 38 | private final String KEY_PASSWORD; 39 | private final String KEY_STORE; 40 | private final String KEY_STORE_PASSWORD; 41 | private final String KEY_STORE_TYPE; 42 | private final String TRUST_STORE; 43 | private final String TRUST_STORE_PASSWORD; 44 | private final String TRUST_STORE_TYPE; 45 | 46 | private final String[] CORS_ORIGINS; 47 | private final Boolean CORS_ALLOW_CREDENTIALS; 48 | 49 | private static Integer randomPort; 50 | 51 | public ServerEndpointConfig(String host, int port, int bossLoopGroupThreads, int workerLoopGroupThreads, boolean useCompressionHandler, int connectTimeoutMillis, int soBacklog, int writeSpinCount, int writeBufferHighWaterMark, int writeBufferLowWaterMark, int soRcvbuf, int soSndbuf, boolean tcpNodelay, boolean soKeepalive, int soLinger, boolean allowHalfClosure, int readerIdleTimeSeconds, int writerIdleTimeSeconds, int allIdleTimeSeconds, int maxFramePayloadLength, boolean useEventExecutorGroup, int eventExecutorGroupThreads, String keyPassword, String keyStore, String keyStorePassword, String keyStoreType, String trustStore, String trustStorePassword, String trustStoreType, String[] corsOrigins, Boolean corsAllowCredentials) { 52 | if (StringUtils.isEmpty(host) || "0.0.0.0".equals(host) || "0.0.0.0/0.0.0.0".equals(host)) { 53 | this.HOST = "0.0.0.0"; 54 | } else { 55 | this.HOST = host; 56 | } 57 | this.PORT = getAvailablePort(port); 58 | this.BOSS_LOOP_GROUP_THREADS = bossLoopGroupThreads; 59 | this.WORKER_LOOP_GROUP_THREADS = workerLoopGroupThreads; 60 | this.USE_COMPRESSION_HANDLER = useCompressionHandler; 61 | this.CONNECT_TIMEOUT_MILLIS = connectTimeoutMillis; 62 | this.SO_BACKLOG = soBacklog; 63 | this.WRITE_SPIN_COUNT = writeSpinCount; 64 | this.WRITE_BUFFER_HIGH_WATER_MARK = writeBufferHighWaterMark; 65 | this.WRITE_BUFFER_LOW_WATER_MARK = writeBufferLowWaterMark; 66 | this.SO_RCVBUF = soRcvbuf; 67 | this.SO_SNDBUF = soSndbuf; 68 | this.TCP_NODELAY = tcpNodelay; 69 | this.SO_KEEPALIVE = soKeepalive; 70 | this.SO_LINGER = soLinger; 71 | this.ALLOW_HALF_CLOSURE = allowHalfClosure; 72 | this.READER_IDLE_TIME_SECONDS = readerIdleTimeSeconds; 73 | this.WRITER_IDLE_TIME_SECONDS = writerIdleTimeSeconds; 74 | this.ALL_IDLE_TIME_SECONDS = allIdleTimeSeconds; 75 | this.MAX_FRAME_PAYLOAD_LENGTH = maxFramePayloadLength; 76 | this.USE_EVENT_EXECUTOR_GROUP = useEventExecutorGroup; 77 | this.EVENT_EXECUTOR_GROUP_THREADS = eventExecutorGroupThreads; 78 | 79 | this.KEY_PASSWORD = keyPassword; 80 | this.KEY_STORE = keyStore; 81 | this.KEY_STORE_PASSWORD = keyStorePassword; 82 | this.KEY_STORE_TYPE = keyStoreType; 83 | this.TRUST_STORE = trustStore; 84 | this.TRUST_STORE_PASSWORD = trustStorePassword; 85 | this.TRUST_STORE_TYPE = trustStoreType; 86 | 87 | this.CORS_ORIGINS = corsOrigins; 88 | this.CORS_ALLOW_CREDENTIALS = corsAllowCredentials; 89 | } 90 | 91 | private int getAvailablePort(int port) { 92 | if (port != 0) { 93 | return port; 94 | } 95 | if (randomPort != null && randomPort != 0) { 96 | return randomPort; 97 | } 98 | InetSocketAddress inetSocketAddress = new InetSocketAddress(0); 99 | Socket socket = new Socket(); 100 | try { 101 | socket.bind(inetSocketAddress); 102 | } catch (IOException e) { 103 | e.printStackTrace(); 104 | } 105 | int localPort = socket.getLocalPort(); 106 | try { 107 | socket.close(); 108 | } catch (IOException e) { 109 | e.printStackTrace(); 110 | } 111 | randomPort = localPort; 112 | return localPort; 113 | } 114 | 115 | public String getHost() { 116 | return HOST; 117 | } 118 | 119 | public int getPort() { 120 | return PORT; 121 | } 122 | 123 | /*public Set getPathSet() { 124 | 125 | return PATH_SET; 126 | }*/ 127 | 128 | public int getBossLoopGroupThreads() { 129 | return BOSS_LOOP_GROUP_THREADS; 130 | } 131 | 132 | public int getWorkerLoopGroupThreads() { 133 | return WORKER_LOOP_GROUP_THREADS; 134 | } 135 | 136 | public boolean isUseCompressionHandler() { 137 | return USE_COMPRESSION_HANDLER; 138 | } 139 | 140 | public int getConnectTimeoutMillis() { 141 | return CONNECT_TIMEOUT_MILLIS; 142 | } 143 | 144 | public int getSoBacklog() { 145 | return SO_BACKLOG; 146 | } 147 | 148 | public int getWriteSpinCount() { 149 | return WRITE_SPIN_COUNT; 150 | } 151 | 152 | public int getWriteBufferHighWaterMark() { 153 | return WRITE_BUFFER_HIGH_WATER_MARK; 154 | } 155 | 156 | public int getWriteBufferLowWaterMark() { 157 | return WRITE_BUFFER_LOW_WATER_MARK; 158 | } 159 | 160 | public int getSoRcvbuf() { 161 | return SO_RCVBUF; 162 | } 163 | 164 | public int getSoSndbuf() { 165 | return SO_SNDBUF; 166 | } 167 | 168 | public boolean isTcpNodelay() { 169 | return TCP_NODELAY; 170 | } 171 | 172 | public boolean isSoKeepalive() { 173 | return SO_KEEPALIVE; 174 | } 175 | 176 | public int getSoLinger() { 177 | return SO_LINGER; 178 | } 179 | 180 | public boolean isAllowHalfClosure() { 181 | return ALLOW_HALF_CLOSURE; 182 | } 183 | 184 | public static Integer getRandomPort() { 185 | return randomPort; 186 | } 187 | 188 | public int getReaderIdleTimeSeconds() { 189 | return READER_IDLE_TIME_SECONDS; 190 | } 191 | 192 | public int getWriterIdleTimeSeconds() { 193 | return WRITER_IDLE_TIME_SECONDS; 194 | } 195 | 196 | public int getAllIdleTimeSeconds() { 197 | return ALL_IDLE_TIME_SECONDS; 198 | } 199 | 200 | public int getmaxFramePayloadLength() { 201 | return MAX_FRAME_PAYLOAD_LENGTH; 202 | } 203 | 204 | public boolean isUseEventExecutorGroup() { 205 | return USE_EVENT_EXECUTOR_GROUP; 206 | } 207 | 208 | public int getEventExecutorGroupThreads() { 209 | return EVENT_EXECUTOR_GROUP_THREADS; 210 | } 211 | 212 | public String getKeyPassword() { 213 | return KEY_PASSWORD; 214 | } 215 | 216 | public String getKeyStore() { 217 | return KEY_STORE; 218 | } 219 | 220 | public String getKeyStorePassword() { 221 | return KEY_STORE_PASSWORD; 222 | } 223 | 224 | public String getKeyStoreType() { 225 | return KEY_STORE_TYPE; 226 | } 227 | 228 | public String getTrustStore() { 229 | return TRUST_STORE; 230 | } 231 | 232 | public String getTrustStorePassword() { 233 | return TRUST_STORE_PASSWORD; 234 | } 235 | 236 | public String getTrustStoreType() { 237 | return TRUST_STORE_TYPE; 238 | } 239 | 240 | public String[] getCorsOrigins() { 241 | return CORS_ORIGINS; 242 | } 243 | 244 | public Boolean getCorsAllowCredentials() { 245 | return CORS_ALLOW_CREDENTIALS; 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /src/main/java/org/yeauty/standard/ServerEndpointExporter.java: -------------------------------------------------------------------------------- 1 | package org.yeauty.standard; 2 | 3 | import org.springframework.beans.TypeConverter; 4 | import org.springframework.beans.TypeMismatchException; 5 | import org.springframework.beans.factory.BeanFactory; 6 | import org.springframework.beans.factory.BeanFactoryAware; 7 | import org.springframework.beans.factory.SmartInitializingSingleton; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.beans.factory.config.BeanExpressionContext; 10 | import org.springframework.beans.factory.config.BeanExpressionResolver; 11 | import org.springframework.beans.factory.support.AbstractBeanFactory; 12 | import org.springframework.beans.factory.support.BeanDefinitionRegistry; 13 | import org.springframework.boot.autoconfigure.SpringBootApplication; 14 | import org.springframework.context.ApplicationContext; 15 | import org.springframework.context.ResourceLoaderAware; 16 | import org.springframework.context.support.ApplicationObjectSupport; 17 | import org.springframework.core.annotation.AnnotatedElementUtils; 18 | import org.springframework.core.annotation.AnnotationUtils; 19 | import org.springframework.core.env.Environment; 20 | import org.springframework.core.io.ResourceLoader; 21 | import org.springframework.util.ClassUtils; 22 | import org.yeauty.annotation.EnableWebSocket; 23 | import org.yeauty.annotation.ServerEndpoint; 24 | import org.yeauty.exception.DeploymentException; 25 | import org.yeauty.pojo.PojoEndpointServer; 26 | import org.yeauty.pojo.PojoMethodMapping; 27 | 28 | import javax.net.ssl.SSLException; 29 | import java.net.InetSocketAddress; 30 | import java.util.*; 31 | 32 | /** 33 | * @author Yeauty 34 | */ 35 | public class ServerEndpointExporter extends ApplicationObjectSupport implements SmartInitializingSingleton, BeanFactoryAware, ResourceLoaderAware { 36 | 37 | @Autowired 38 | Environment environment; 39 | 40 | private AbstractBeanFactory beanFactory; 41 | 42 | private ResourceLoader resourceLoader; 43 | 44 | private final Map addressWebsocketServerMap = new HashMap<>(); 45 | 46 | @Override 47 | public void afterSingletonsInstantiated() { 48 | registerEndpoints(); 49 | } 50 | 51 | @Override 52 | public void setBeanFactory(BeanFactory beanFactory) { 53 | if (!(beanFactory instanceof AbstractBeanFactory)) { 54 | throw new IllegalArgumentException( 55 | "AutowiredAnnotationBeanPostProcessor requires a AbstractBeanFactory: " + beanFactory); 56 | } 57 | this.beanFactory = (AbstractBeanFactory) beanFactory; 58 | } 59 | 60 | protected void registerEndpoints() { 61 | ApplicationContext context = getApplicationContext(); 62 | 63 | scanPackage(context); 64 | 65 | String[] endpointBeanNames = context.getBeanNamesForAnnotation(ServerEndpoint.class); 66 | Set> endpointClasses = new LinkedHashSet<>(); 67 | for (String beanName : endpointBeanNames) { 68 | endpointClasses.add(context.getType(beanName)); 69 | } 70 | 71 | for (Class endpointClass : endpointClasses) { 72 | if (ClassUtils.isCglibProxyClass(endpointClass)) { 73 | registerEndpoint(endpointClass.getSuperclass()); 74 | } else { 75 | registerEndpoint(endpointClass); 76 | } 77 | } 78 | 79 | init(); 80 | } 81 | 82 | private void scanPackage(ApplicationContext context) { 83 | String[] basePackages = null; 84 | 85 | String[] enableWebSocketBeanNames = context.getBeanNamesForAnnotation(EnableWebSocket.class); 86 | if (enableWebSocketBeanNames.length != 0) { 87 | for (String enableWebSocketBeanName : enableWebSocketBeanNames) { 88 | Object enableWebSocketBean = context.getBean(enableWebSocketBeanName); 89 | EnableWebSocket enableWebSocket = AnnotationUtils.findAnnotation(enableWebSocketBean.getClass(), EnableWebSocket.class); 90 | assert enableWebSocket != null; 91 | if (enableWebSocket.scanBasePackages().length != 0) { 92 | basePackages = enableWebSocket.scanBasePackages(); 93 | break; 94 | } 95 | } 96 | } 97 | 98 | // use @SpringBootApplication package 99 | if (basePackages == null) { 100 | String[] springBootApplicationBeanName = context.getBeanNamesForAnnotation(SpringBootApplication.class); 101 | Object springBootApplicationBean = context.getBean(springBootApplicationBeanName[0]); 102 | SpringBootApplication springBootApplication = AnnotationUtils.findAnnotation(springBootApplicationBean.getClass(), SpringBootApplication.class); 103 | assert springBootApplication != null; 104 | if (springBootApplication.scanBasePackages().length != 0) { 105 | basePackages = springBootApplication.scanBasePackages(); 106 | } else { 107 | String packageName = ClassUtils.getPackageName(springBootApplicationBean.getClass().getName()); 108 | basePackages = new String[1]; 109 | basePackages[0] = packageName; 110 | } 111 | } 112 | 113 | EndpointClassPathScanner scanHandle = new EndpointClassPathScanner((BeanDefinitionRegistry) context, false); 114 | if (resourceLoader != null) { 115 | scanHandle.setResourceLoader(resourceLoader); 116 | } 117 | 118 | for (String basePackage : basePackages) { 119 | scanHandle.doScan(basePackage); 120 | } 121 | } 122 | 123 | private void init() { 124 | for (Map.Entry entry : addressWebsocketServerMap.entrySet()) { 125 | WebsocketServer websocketServer = entry.getValue(); 126 | try { 127 | websocketServer.init(); 128 | PojoEndpointServer pojoEndpointServer = websocketServer.getPojoEndpointServer(); 129 | StringJoiner stringJoiner = new StringJoiner(","); 130 | pojoEndpointServer.getPathMatcherSet().forEach(pathMatcher -> stringJoiner.add("'" + pathMatcher.getPattern() + "'")); 131 | logger.info(String.format("\033[34mNetty WebSocket started on port: %s with context path(s): %s .\033[0m", pojoEndpointServer.getPort(), stringJoiner.toString())); 132 | } catch (InterruptedException e) { 133 | logger.error(String.format("websocket [%s] init fail", entry.getKey()), e); 134 | } catch (SSLException e) { 135 | logger.error(String.format("websocket [%s] ssl create fail", entry.getKey()), e); 136 | 137 | } 138 | } 139 | } 140 | 141 | private void registerEndpoint(Class endpointClass) { 142 | ServerEndpoint annotation = AnnotatedElementUtils.findMergedAnnotation(endpointClass, ServerEndpoint.class); 143 | if (annotation == null) { 144 | throw new IllegalStateException("missingAnnotation ServerEndpoint"); 145 | } 146 | ServerEndpointConfig serverEndpointConfig = buildConfig(annotation); 147 | 148 | ApplicationContext context = getApplicationContext(); 149 | PojoMethodMapping pojoMethodMapping = null; 150 | try { 151 | pojoMethodMapping = new PojoMethodMapping(endpointClass, context, beanFactory); 152 | } catch (DeploymentException e) { 153 | throw new IllegalStateException("Failed to register ServerEndpointConfig: " + serverEndpointConfig, e); 154 | } 155 | 156 | InetSocketAddress inetSocketAddress = new InetSocketAddress(serverEndpointConfig.getHost(), serverEndpointConfig.getPort()); 157 | String path = resolveAnnotationValue(annotation.value(), String.class, "path"); 158 | 159 | WebsocketServer websocketServer = addressWebsocketServerMap.get(inetSocketAddress); 160 | if (websocketServer == null) { 161 | PojoEndpointServer pojoEndpointServer = new PojoEndpointServer(pojoMethodMapping, serverEndpointConfig, path); 162 | websocketServer = new WebsocketServer(pojoEndpointServer, serverEndpointConfig); 163 | addressWebsocketServerMap.put(inetSocketAddress, websocketServer); 164 | } else { 165 | websocketServer.getPojoEndpointServer().addPathPojoMethodMapping(path, pojoMethodMapping); 166 | } 167 | } 168 | 169 | private ServerEndpointConfig buildConfig(ServerEndpoint annotation) { 170 | String host = resolveAnnotationValue(annotation.host(), String.class, "host"); 171 | int port = resolveAnnotationValue(annotation.port(), Integer.class, "port"); 172 | String path = resolveAnnotationValue(annotation.value(), String.class, "value"); 173 | int bossLoopGroupThreads = resolveAnnotationValue(annotation.bossLoopGroupThreads(), Integer.class, "bossLoopGroupThreads"); 174 | int workerLoopGroupThreads = resolveAnnotationValue(annotation.workerLoopGroupThreads(), Integer.class, "workerLoopGroupThreads"); 175 | boolean useCompressionHandler = resolveAnnotationValue(annotation.useCompressionHandler(), Boolean.class, "useCompressionHandler"); 176 | 177 | int optionConnectTimeoutMillis = resolveAnnotationValue(annotation.optionConnectTimeoutMillis(), Integer.class, "optionConnectTimeoutMillis"); 178 | int optionSoBacklog = resolveAnnotationValue(annotation.optionSoBacklog(), Integer.class, "optionSoBacklog"); 179 | 180 | int childOptionWriteSpinCount = resolveAnnotationValue(annotation.childOptionWriteSpinCount(), Integer.class, "childOptionWriteSpinCount"); 181 | int childOptionWriteBufferHighWaterMark = resolveAnnotationValue(annotation.childOptionWriteBufferHighWaterMark(), Integer.class, "childOptionWriteBufferHighWaterMark"); 182 | int childOptionWriteBufferLowWaterMark = resolveAnnotationValue(annotation.childOptionWriteBufferLowWaterMark(), Integer.class, "childOptionWriteBufferLowWaterMark"); 183 | int childOptionSoRcvbuf = resolveAnnotationValue(annotation.childOptionSoRcvbuf(), Integer.class, "childOptionSoRcvbuf"); 184 | int childOptionSoSndbuf = resolveAnnotationValue(annotation.childOptionSoSndbuf(), Integer.class, "childOptionSoSndbuf"); 185 | boolean childOptionTcpNodelay = resolveAnnotationValue(annotation.childOptionTcpNodelay(), Boolean.class, "childOptionTcpNodelay"); 186 | boolean childOptionSoKeepalive = resolveAnnotationValue(annotation.childOptionSoKeepalive(), Boolean.class, "childOptionSoKeepalive"); 187 | int childOptionSoLinger = resolveAnnotationValue(annotation.childOptionSoLinger(), Integer.class, "childOptionSoLinger"); 188 | boolean childOptionAllowHalfClosure = resolveAnnotationValue(annotation.childOptionAllowHalfClosure(), Boolean.class, "childOptionAllowHalfClosure"); 189 | 190 | int readerIdleTimeSeconds = resolveAnnotationValue(annotation.readerIdleTimeSeconds(), Integer.class, "readerIdleTimeSeconds"); 191 | int writerIdleTimeSeconds = resolveAnnotationValue(annotation.writerIdleTimeSeconds(), Integer.class, "writerIdleTimeSeconds"); 192 | int allIdleTimeSeconds = resolveAnnotationValue(annotation.allIdleTimeSeconds(), Integer.class, "allIdleTimeSeconds"); 193 | 194 | int maxFramePayloadLength = resolveAnnotationValue(annotation.maxFramePayloadLength(), Integer.class, "maxFramePayloadLength"); 195 | 196 | boolean useEventExecutorGroup = resolveAnnotationValue(annotation.useEventExecutorGroup(), Boolean.class, "useEventExecutorGroup"); 197 | int eventExecutorGroupThreads = resolveAnnotationValue(annotation.eventExecutorGroupThreads(), Integer.class, "eventExecutorGroupThreads"); 198 | 199 | String sslKeyPassword = resolveAnnotationValue(annotation.sslKeyPassword(), String.class, "sslKeyPassword"); 200 | String sslKeyStore = resolveAnnotationValue(annotation.sslKeyStore(), String.class, "sslKeyStore"); 201 | String sslKeyStorePassword = resolveAnnotationValue(annotation.sslKeyStorePassword(), String.class, "sslKeyStorePassword"); 202 | String sslKeyStoreType = resolveAnnotationValue(annotation.sslKeyStoreType(), String.class, "sslKeyStoreType"); 203 | String sslTrustStore = resolveAnnotationValue(annotation.sslTrustStore(), String.class, "sslTrustStore"); 204 | String sslTrustStorePassword = resolveAnnotationValue(annotation.sslTrustStorePassword(), String.class, "sslTrustStorePassword"); 205 | String sslTrustStoreType = resolveAnnotationValue(annotation.sslTrustStoreType(), String.class, "sslTrustStoreType"); 206 | 207 | String[] corsOrigins = annotation.corsOrigins(); 208 | if (corsOrigins.length != 0) { 209 | for (int i = 0; i < corsOrigins.length; i++) { 210 | corsOrigins[i] = resolveAnnotationValue(corsOrigins[i], String.class, "corsOrigins"); 211 | } 212 | } 213 | Boolean corsAllowCredentials = resolveAnnotationValue(annotation.corsAllowCredentials(), Boolean.class, "corsAllowCredentials"); 214 | 215 | ServerEndpointConfig serverEndpointConfig = new ServerEndpointConfig(host, port, bossLoopGroupThreads, workerLoopGroupThreads 216 | , useCompressionHandler, optionConnectTimeoutMillis, optionSoBacklog, childOptionWriteSpinCount, childOptionWriteBufferHighWaterMark 217 | , childOptionWriteBufferLowWaterMark, childOptionSoRcvbuf, childOptionSoSndbuf, childOptionTcpNodelay, childOptionSoKeepalive 218 | , childOptionSoLinger, childOptionAllowHalfClosure, readerIdleTimeSeconds, writerIdleTimeSeconds, allIdleTimeSeconds 219 | , maxFramePayloadLength, useEventExecutorGroup, eventExecutorGroupThreads 220 | , sslKeyPassword, sslKeyStore, sslKeyStorePassword, sslKeyStoreType 221 | , sslTrustStore, sslTrustStorePassword, sslTrustStoreType 222 | , corsOrigins, corsAllowCredentials); 223 | 224 | return serverEndpointConfig; 225 | } 226 | 227 | private T resolveAnnotationValue(Object value, Class requiredType, String paramName) { 228 | if (value == null) { 229 | return null; 230 | } 231 | TypeConverter typeConverter = beanFactory.getTypeConverter(); 232 | 233 | if (value instanceof String) { 234 | String strVal = beanFactory.resolveEmbeddedValue((String) value); 235 | BeanExpressionResolver beanExpressionResolver = beanFactory.getBeanExpressionResolver(); 236 | if (beanExpressionResolver != null) { 237 | value = beanExpressionResolver.evaluate(strVal, new BeanExpressionContext(beanFactory, null)); 238 | } else { 239 | value = strVal; 240 | } 241 | } 242 | try { 243 | return typeConverter.convertIfNecessary(value, requiredType); 244 | } catch (TypeMismatchException e) { 245 | throw new IllegalArgumentException("Failed to convert value of parameter '" + paramName + "' to required type '" + requiredType.getName() + "'"); 246 | } 247 | } 248 | 249 | @Override 250 | public void setResourceLoader(ResourceLoader resourceLoader) { 251 | this.resourceLoader = resourceLoader; 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /src/main/java/org/yeauty/standard/WebSocketServerHandler.java: -------------------------------------------------------------------------------- 1 | package org.yeauty.standard; 2 | 3 | import io.netty.channel.ChannelFutureListener; 4 | import io.netty.channel.ChannelHandlerContext; 5 | import io.netty.channel.SimpleChannelInboundHandler; 6 | import io.netty.handler.codec.http.websocketx.*; 7 | import org.yeauty.pojo.PojoEndpointServer; 8 | 9 | class WebSocketServerHandler extends SimpleChannelInboundHandler { 10 | 11 | private final PojoEndpointServer pojoEndpointServer; 12 | 13 | public WebSocketServerHandler(PojoEndpointServer pojoEndpointServer) { 14 | this.pojoEndpointServer = pojoEndpointServer; 15 | } 16 | 17 | @Override 18 | protected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame msg) throws Exception { 19 | handleWebSocketFrame(ctx, msg); 20 | } 21 | 22 | @Override 23 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { 24 | pojoEndpointServer.doOnError(ctx.channel(), cause); 25 | } 26 | 27 | @Override 28 | public void channelInactive(ChannelHandlerContext ctx) throws Exception { 29 | pojoEndpointServer.doOnClose(ctx.channel()); 30 | } 31 | 32 | @Override 33 | public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { 34 | pojoEndpointServer.doOnEvent(ctx.channel(), evt); 35 | } 36 | 37 | private void handleWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) { 38 | if (frame instanceof TextWebSocketFrame) { 39 | pojoEndpointServer.doOnMessage(ctx.channel(), frame); 40 | return; 41 | } 42 | if (frame instanceof PingWebSocketFrame) { 43 | ctx.writeAndFlush(new PongWebSocketFrame(frame.content().retain())); 44 | return; 45 | } 46 | if (frame instanceof CloseWebSocketFrame) { 47 | ctx.writeAndFlush(frame.retainedDuplicate()).addListener(ChannelFutureListener.CLOSE); 48 | return; 49 | } 50 | if (frame instanceof BinaryWebSocketFrame) { 51 | pojoEndpointServer.doOnBinary(ctx.channel(), frame); 52 | return; 53 | } 54 | if (frame instanceof PongWebSocketFrame) { 55 | return; 56 | } 57 | } 58 | 59 | } -------------------------------------------------------------------------------- /src/main/java/org/yeauty/standard/WebsocketServer.java: -------------------------------------------------------------------------------- 1 | package org.yeauty.standard; 2 | 3 | import io.netty.bootstrap.ServerBootstrap; 4 | import io.netty.channel.*; 5 | import io.netty.channel.nio.NioEventLoopGroup; 6 | import io.netty.channel.socket.nio.NioServerSocketChannel; 7 | import io.netty.channel.socket.nio.NioSocketChannel; 8 | import io.netty.handler.codec.http.HttpObjectAggregator; 9 | import io.netty.handler.codec.http.HttpServerCodec; 10 | import io.netty.handler.codec.http.cors.CorsConfig; 11 | import io.netty.handler.codec.http.cors.CorsConfigBuilder; 12 | import io.netty.handler.codec.http.cors.CorsHandler; 13 | import io.netty.handler.logging.LogLevel; 14 | import io.netty.handler.logging.LoggingHandler; 15 | import io.netty.handler.ssl.SslContext; 16 | import io.netty.util.concurrent.DefaultEventExecutorGroup; 17 | import io.netty.util.concurrent.EventExecutorGroup; 18 | import io.netty.util.internal.logging.InternalLogger; 19 | import io.netty.util.internal.logging.InternalLoggerFactory; 20 | import org.springframework.util.StringUtils; 21 | import org.yeauty.pojo.PojoEndpointServer; 22 | import org.yeauty.util.SslUtils; 23 | 24 | import javax.net.ssl.SSLException; 25 | import java.net.InetAddress; 26 | import java.net.InetSocketAddress; 27 | import java.net.UnknownHostException; 28 | 29 | /** 30 | * @author Yeauty 31 | * @version 1.0 32 | */ 33 | public class WebsocketServer { 34 | 35 | private final PojoEndpointServer pojoEndpointServer; 36 | 37 | private final ServerEndpointConfig config; 38 | 39 | 40 | private static final InternalLogger logger = InternalLoggerFactory.getInstance(WebsocketServer.class); 41 | 42 | public WebsocketServer(PojoEndpointServer webSocketServerHandler, ServerEndpointConfig serverEndpointConfig) { 43 | this.pojoEndpointServer = webSocketServerHandler; 44 | this.config = serverEndpointConfig; 45 | 46 | } 47 | 48 | public void init() throws InterruptedException, SSLException { 49 | EventExecutorGroup eventExecutorGroup = null; 50 | final SslContext sslCtx; 51 | if (!StringUtils.isEmpty(config.getKeyStore())) { 52 | sslCtx = SslUtils.createSslContext(config.getKeyPassword(), config.getKeyStore(), config.getKeyStoreType(), config.getKeyStorePassword(), config.getTrustStore(), config.getTrustStoreType(), config.getTrustStorePassword()); 53 | } else { 54 | sslCtx = null; 55 | } 56 | String[] corsOrigins = config.getCorsOrigins(); 57 | Boolean corsAllowCredentials = config.getCorsAllowCredentials(); 58 | final CorsConfig corsConfig = createCorsConfig(corsOrigins, corsAllowCredentials); 59 | 60 | if (config.isUseEventExecutorGroup()) { 61 | eventExecutorGroup = new DefaultEventExecutorGroup(config.getEventExecutorGroupThreads() == 0 ? 16 : config.getEventExecutorGroupThreads()); 62 | } 63 | EventLoopGroup boss = new NioEventLoopGroup(config.getBossLoopGroupThreads()); 64 | EventLoopGroup worker = new NioEventLoopGroup(config.getWorkerLoopGroupThreads()); 65 | ServerBootstrap bootstrap = new ServerBootstrap(); 66 | EventExecutorGroup finalEventExecutorGroup = eventExecutorGroup; 67 | bootstrap.group(boss, worker) 68 | .channel(NioServerSocketChannel.class) 69 | .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, config.getConnectTimeoutMillis()) 70 | .option(ChannelOption.SO_BACKLOG, config.getSoBacklog()) 71 | .childOption(ChannelOption.WRITE_SPIN_COUNT, config.getWriteSpinCount()) 72 | .childOption(ChannelOption.WRITE_BUFFER_WATER_MARK, new WriteBufferWaterMark(config.getWriteBufferLowWaterMark(), config.getWriteBufferHighWaterMark())) 73 | .childOption(ChannelOption.TCP_NODELAY, config.isTcpNodelay()) 74 | .childOption(ChannelOption.SO_KEEPALIVE, config.isSoKeepalive()) 75 | .childOption(ChannelOption.SO_LINGER, config.getSoLinger()) 76 | .childOption(ChannelOption.ALLOW_HALF_CLOSURE, config.isAllowHalfClosure()) 77 | .handler(new LoggingHandler(LogLevel.DEBUG)) 78 | .childHandler(new ChannelInitializer() { 79 | @Override 80 | protected void initChannel(NioSocketChannel ch) { 81 | ChannelPipeline pipeline = ch.pipeline(); 82 | if (sslCtx != null) { 83 | pipeline.addFirst(sslCtx.newHandler(ch.alloc())); 84 | } 85 | pipeline.addLast(new HttpServerCodec()); 86 | pipeline.addLast(new HttpObjectAggregator(65536)); 87 | if (corsConfig != null) { 88 | pipeline.addLast(new CorsHandler(corsConfig)); 89 | } 90 | pipeline.addLast(new HttpServerHandler(pojoEndpointServer, config, finalEventExecutorGroup, corsConfig != null)); 91 | } 92 | }); 93 | 94 | if (config.getSoRcvbuf() != -1) { 95 | bootstrap.childOption(ChannelOption.SO_RCVBUF, config.getSoRcvbuf()); 96 | } 97 | 98 | if (config.getSoSndbuf() != -1) { 99 | bootstrap.childOption(ChannelOption.SO_SNDBUF, config.getSoSndbuf()); 100 | } 101 | 102 | ChannelFuture channelFuture; 103 | if ("0.0.0.0".equals(config.getHost())) { 104 | channelFuture = bootstrap.bind(config.getPort()); 105 | } else { 106 | try { 107 | channelFuture = bootstrap.bind(new InetSocketAddress(InetAddress.getByName(config.getHost()), config.getPort())); 108 | } catch (UnknownHostException e) { 109 | channelFuture = bootstrap.bind(config.getHost(), config.getPort()); 110 | e.printStackTrace(); 111 | } 112 | } 113 | 114 | channelFuture.addListener(future -> { 115 | if (!future.isSuccess()) { 116 | future.cause().printStackTrace(); 117 | } 118 | }); 119 | 120 | Runtime.getRuntime().addShutdownHook(new Thread(() -> { 121 | boss.shutdownGracefully().syncUninterruptibly(); 122 | worker.shutdownGracefully().syncUninterruptibly(); 123 | })); 124 | } 125 | 126 | private CorsConfig createCorsConfig(String[] corsOrigins, Boolean corsAllowCredentials) { 127 | if (corsOrigins.length == 0) { 128 | return null; 129 | } 130 | CorsConfigBuilder corsConfigBuilder = null; 131 | for (String corsOrigin : corsOrigins) { 132 | if ("*".equals(corsOrigin)) { 133 | corsConfigBuilder = CorsConfigBuilder.forAnyOrigin(); 134 | break; 135 | } 136 | } 137 | if (corsConfigBuilder == null) { 138 | corsConfigBuilder = CorsConfigBuilder.forOrigins(corsOrigins); 139 | } 140 | if (corsAllowCredentials != null && corsAllowCredentials) { 141 | corsConfigBuilder.allowCredentials(); 142 | } 143 | corsConfigBuilder.allowNullOrigin(); 144 | return corsConfigBuilder.build(); 145 | } 146 | 147 | public PojoEndpointServer getPojoEndpointServer() { 148 | return pojoEndpointServer; 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/main/java/org/yeauty/support/AntPathMatcherWrapper.java: -------------------------------------------------------------------------------- 1 | package org.yeauty.support; 2 | 3 | import io.netty.channel.Channel; 4 | import io.netty.handler.codec.http.QueryStringDecoder; 5 | import org.springframework.util.AntPathMatcher; 6 | 7 | import java.util.LinkedHashMap; 8 | import java.util.Map; 9 | 10 | import static org.yeauty.pojo.PojoEndpointServer.URI_TEMPLATE; 11 | 12 | public class AntPathMatcherWrapper extends AntPathMatcher implements WsPathMatcher { 13 | 14 | private String pattern; 15 | 16 | public AntPathMatcherWrapper(String pattern) { 17 | this.pattern = pattern; 18 | } 19 | 20 | @Override 21 | public String getPattern() { 22 | return this.pattern; 23 | } 24 | 25 | @Override 26 | public boolean matchAndExtract(QueryStringDecoder decoder, Channel channel) { 27 | Map variables = new LinkedHashMap<>(); 28 | boolean result = doMatch(pattern, decoder.path(), true, variables); 29 | if (result) { 30 | channel.attr(URI_TEMPLATE).set(variables); 31 | return true; 32 | } 33 | return false; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/org/yeauty/support/ByteMethodArgumentResolver.java: -------------------------------------------------------------------------------- 1 | package org.yeauty.support; 2 | 3 | import io.netty.buffer.ByteBuf; 4 | import io.netty.channel.Channel; 5 | import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; 6 | import org.springframework.core.MethodParameter; 7 | import org.yeauty.annotation.OnBinary; 8 | 9 | public class ByteMethodArgumentResolver implements MethodArgumentResolver { 10 | @Override 11 | public boolean supportsParameter(MethodParameter parameter) { 12 | return parameter.getMethod().isAnnotationPresent(OnBinary.class) && byte[].class.isAssignableFrom(parameter.getParameterType()); 13 | } 14 | 15 | @Override 16 | public Object resolveArgument(MethodParameter parameter, Channel channel, Object object) throws Exception { 17 | BinaryWebSocketFrame binaryWebSocketFrame = (BinaryWebSocketFrame) object; 18 | ByteBuf content = binaryWebSocketFrame.content(); 19 | byte[] bytes = new byte[content.readableBytes()]; 20 | content.readBytes(bytes); 21 | return bytes; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/org/yeauty/support/DefaultPathMatcher.java: -------------------------------------------------------------------------------- 1 | package org.yeauty.support; 2 | 3 | import io.netty.channel.Channel; 4 | import io.netty.handler.codec.http.QueryStringDecoder; 5 | 6 | 7 | public class DefaultPathMatcher implements WsPathMatcher { 8 | 9 | private String pattern; 10 | 11 | public DefaultPathMatcher(String pattern) { 12 | this.pattern = pattern; 13 | } 14 | 15 | @Override 16 | public String getPattern() { 17 | return this.pattern; 18 | } 19 | 20 | @Override 21 | public boolean matchAndExtract(QueryStringDecoder decoder, Channel channel) { 22 | if (!pattern.equals(decoder.path())) { 23 | return false; 24 | } 25 | return true; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/org/yeauty/support/EventMethodArgumentResolver.java: -------------------------------------------------------------------------------- 1 | package org.yeauty.support; 2 | 3 | import io.netty.channel.Channel; 4 | import org.springframework.beans.TypeConverter; 5 | import org.springframework.beans.factory.support.AbstractBeanFactory; 6 | import org.springframework.core.MethodParameter; 7 | import org.yeauty.annotation.OnEvent; 8 | 9 | public class EventMethodArgumentResolver implements MethodArgumentResolver { 10 | 11 | private AbstractBeanFactory beanFactory; 12 | 13 | public EventMethodArgumentResolver(AbstractBeanFactory beanFactory) { 14 | this.beanFactory = beanFactory; 15 | } 16 | 17 | 18 | @Override 19 | public boolean supportsParameter(MethodParameter parameter) { 20 | return parameter.getMethod().isAnnotationPresent(OnEvent.class); 21 | } 22 | 23 | @Override 24 | public Object resolveArgument(MethodParameter parameter, Channel channel, Object object) throws Exception { 25 | if (object==null) { 26 | return null; 27 | } 28 | TypeConverter typeConverter = beanFactory.getTypeConverter(); 29 | return typeConverter.convertIfNecessary(object, parameter.getParameterType()); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/org/yeauty/support/HttpHeadersMethodArgumentResolver.java: -------------------------------------------------------------------------------- 1 | package org.yeauty.support; 2 | 3 | import io.netty.channel.Channel; 4 | import io.netty.handler.codec.http.FullHttpRequest; 5 | import io.netty.handler.codec.http.HttpHeaders; 6 | import org.springframework.core.MethodParameter; 7 | 8 | public class HttpHeadersMethodArgumentResolver implements MethodArgumentResolver { 9 | @Override 10 | public boolean supportsParameter(MethodParameter parameter) { 11 | return HttpHeaders.class.isAssignableFrom(parameter.getParameterType()); 12 | } 13 | 14 | @Override 15 | public Object resolveArgument(MethodParameter parameter, Channel channel, Object object) throws Exception { 16 | return ((FullHttpRequest) object).headers(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/org/yeauty/support/MethodArgumentResolver.java: -------------------------------------------------------------------------------- 1 | package org.yeauty.support; 2 | 3 | import io.netty.channel.Channel; 4 | import org.springframework.core.MethodParameter; 5 | import org.springframework.lang.Nullable; 6 | 7 | 8 | public interface MethodArgumentResolver { 9 | 10 | /** 11 | * Whether the given {@linkplain MethodParameter method parameter} is 12 | * supported by this resolver. 13 | * @param parameter the method parameter to check 14 | * @return {@code true} if this resolver supports the supplied parameter; 15 | * {@code false} otherwise 16 | */ 17 | boolean supportsParameter(MethodParameter parameter); 18 | 19 | 20 | @Nullable 21 | Object resolveArgument(MethodParameter parameter, Channel channel,Object object) throws Exception; 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/org/yeauty/support/PathVariableMapMethodArgumentResolver.java: -------------------------------------------------------------------------------- 1 | package org.yeauty.support; 2 | 3 | import io.netty.channel.Channel; 4 | import org.springframework.core.MethodParameter; 5 | import org.springframework.util.CollectionUtils; 6 | import org.springframework.util.StringUtils; 7 | import org.yeauty.annotation.PathVariable; 8 | 9 | import java.util.Collections; 10 | import java.util.Map; 11 | 12 | import static org.yeauty.pojo.PojoEndpointServer.URI_TEMPLATE; 13 | 14 | public class PathVariableMapMethodArgumentResolver implements MethodArgumentResolver { 15 | @Override 16 | public boolean supportsParameter(MethodParameter parameter) { 17 | PathVariable ann = parameter.getParameterAnnotation(PathVariable.class); 18 | return (ann != null && Map.class.isAssignableFrom(parameter.getParameterType()) && 19 | !StringUtils.hasText(ann.value())); 20 | } 21 | 22 | @Override 23 | public Object resolveArgument(MethodParameter parameter, Channel channel, Object object) throws Exception { 24 | PathVariable ann = parameter.getParameterAnnotation(PathVariable.class); 25 | String name = ann.name(); 26 | if (name.isEmpty()) { 27 | name = parameter.getParameterName(); 28 | if (name == null) { 29 | throw new IllegalArgumentException( 30 | "Name for argument type [" + parameter.getNestedParameterType().getName() + 31 | "] not available, and parameter name information not found in class file either."); 32 | } 33 | } 34 | Map uriTemplateVars = channel.attr(URI_TEMPLATE).get(); 35 | if (!CollectionUtils.isEmpty(uriTemplateVars)) { 36 | return uriTemplateVars; 37 | } else { 38 | return Collections.emptyMap(); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/org/yeauty/support/PathVariableMethodArgumentResolver.java: -------------------------------------------------------------------------------- 1 | package org.yeauty.support; 2 | 3 | import io.netty.channel.Channel; 4 | import org.springframework.beans.TypeConverter; 5 | import org.springframework.beans.factory.support.AbstractBeanFactory; 6 | import org.springframework.core.MethodParameter; 7 | import org.yeauty.annotation.PathVariable; 8 | 9 | import java.util.Map; 10 | 11 | import static org.yeauty.pojo.PojoEndpointServer.URI_TEMPLATE; 12 | 13 | public class PathVariableMethodArgumentResolver implements MethodArgumentResolver { 14 | 15 | private AbstractBeanFactory beanFactory; 16 | 17 | public PathVariableMethodArgumentResolver(AbstractBeanFactory beanFactory) { 18 | this.beanFactory = beanFactory; 19 | } 20 | 21 | @Override 22 | public boolean supportsParameter(MethodParameter parameter) { 23 | return parameter.hasParameterAnnotation(PathVariable.class); 24 | } 25 | 26 | @Override 27 | public Object resolveArgument(MethodParameter parameter, Channel channel, Object object) throws Exception { 28 | PathVariable ann = parameter.getParameterAnnotation(PathVariable.class); 29 | String name = ann.name(); 30 | if (name.isEmpty()) { 31 | name = parameter.getParameterName(); 32 | if (name == null) { 33 | throw new IllegalArgumentException( 34 | "Name for argument type [" + parameter.getNestedParameterType().getName() + 35 | "] not available, and parameter name information not found in class file either."); 36 | } 37 | } 38 | Map uriTemplateVars = channel.attr(URI_TEMPLATE).get(); 39 | Object arg = (uriTemplateVars != null ? uriTemplateVars.get(name) : null); 40 | TypeConverter typeConverter = beanFactory.getTypeConverter(); 41 | return typeConverter.convertIfNecessary(arg, parameter.getParameterType()); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/org/yeauty/support/RequestParamMapMethodArgumentResolver.java: -------------------------------------------------------------------------------- 1 | package org.yeauty.support; 2 | 3 | import io.netty.channel.Channel; 4 | import io.netty.handler.codec.http.FullHttpRequest; 5 | import io.netty.handler.codec.http.QueryStringDecoder; 6 | import org.springframework.beans.TypeConverter; 7 | import org.springframework.beans.factory.support.AbstractBeanFactory; 8 | import org.springframework.core.MethodParameter; 9 | import org.springframework.util.LinkedMultiValueMap; 10 | import org.springframework.util.MultiValueMap; 11 | import org.springframework.util.StringUtils; 12 | import org.yeauty.annotation.RequestParam; 13 | 14 | import java.util.List; 15 | import java.util.Map; 16 | 17 | import static org.yeauty.pojo.PojoEndpointServer.REQUEST_PARAM; 18 | 19 | public class RequestParamMapMethodArgumentResolver implements MethodArgumentResolver { 20 | 21 | @Override 22 | public boolean supportsParameter(MethodParameter parameter) { 23 | RequestParam requestParam = parameter.getParameterAnnotation(RequestParam.class); 24 | return (requestParam != null && Map.class.isAssignableFrom(parameter.getParameterType()) && 25 | !StringUtils.hasText(requestParam.name())); 26 | } 27 | 28 | @Override 29 | public Object resolveArgument(MethodParameter parameter, Channel channel, Object object) throws Exception { 30 | RequestParam ann = parameter.getParameterAnnotation(RequestParam.class); 31 | String name = ann.name(); 32 | if (name.isEmpty()) { 33 | name = parameter.getParameterName(); 34 | if (name == null) { 35 | throw new IllegalArgumentException( 36 | "Name for argument type [" + parameter.getNestedParameterType().getName() + 37 | "] not available, and parameter name information not found in class file either."); 38 | } 39 | } 40 | 41 | if (!channel.hasAttr(REQUEST_PARAM)) { 42 | QueryStringDecoder decoder = new QueryStringDecoder(((FullHttpRequest) object).uri()); 43 | channel.attr(REQUEST_PARAM).set(decoder.parameters()); 44 | } 45 | 46 | Map> requestParams = channel.attr(REQUEST_PARAM).get(); 47 | MultiValueMap multiValueMap = new LinkedMultiValueMap(requestParams); 48 | if (MultiValueMap.class.isAssignableFrom(parameter.getParameterType())) { 49 | return multiValueMap; 50 | } else { 51 | return multiValueMap.toSingleValueMap(); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/org/yeauty/support/RequestParamMethodArgumentResolver.java: -------------------------------------------------------------------------------- 1 | package org.yeauty.support; 2 | 3 | import io.netty.channel.Channel; 4 | import io.netty.handler.codec.http.FullHttpRequest; 5 | import io.netty.handler.codec.http.QueryStringDecoder; 6 | import org.springframework.beans.TypeConverter; 7 | import org.springframework.beans.factory.support.AbstractBeanFactory; 8 | import org.springframework.core.MethodParameter; 9 | import org.yeauty.annotation.RequestParam; 10 | 11 | import java.util.List; 12 | import java.util.Map; 13 | 14 | import static org.yeauty.pojo.PojoEndpointServer.REQUEST_PARAM; 15 | 16 | public class RequestParamMethodArgumentResolver implements MethodArgumentResolver { 17 | 18 | private AbstractBeanFactory beanFactory; 19 | 20 | public RequestParamMethodArgumentResolver(AbstractBeanFactory beanFactory) { 21 | this.beanFactory = beanFactory; 22 | } 23 | 24 | @Override 25 | public boolean supportsParameter(MethodParameter parameter) { 26 | return parameter.hasParameterAnnotation(RequestParam.class); 27 | } 28 | 29 | @Override 30 | public Object resolveArgument(MethodParameter parameter, Channel channel, Object object) throws Exception { 31 | RequestParam ann = parameter.getParameterAnnotation(RequestParam.class); 32 | String name = ann.name(); 33 | if (name.isEmpty()) { 34 | name = parameter.getParameterName(); 35 | if (name == null) { 36 | throw new IllegalArgumentException( 37 | "Name for argument type [" + parameter.getNestedParameterType().getName() + 38 | "] not available, and parameter name information not found in class file either."); 39 | } 40 | } 41 | 42 | if (!channel.hasAttr(REQUEST_PARAM)) { 43 | QueryStringDecoder decoder = new QueryStringDecoder(((FullHttpRequest) object).uri()); 44 | channel.attr(REQUEST_PARAM).set(decoder.parameters()); 45 | } 46 | 47 | Map> requestParams = channel.attr(REQUEST_PARAM).get(); 48 | List arg = (requestParams != null ? requestParams.get(name) : null); 49 | TypeConverter typeConverter = beanFactory.getTypeConverter(); 50 | if (arg == null) { 51 | if ("\n\t\t\n\t\t\n\uE000\uE001\uE002\n\t\t\t\t\n".equals(ann.defaultValue())) { 52 | return null; 53 | }else { 54 | return typeConverter.convertIfNecessary(ann.defaultValue(), parameter.getParameterType()); 55 | } 56 | } 57 | if (List.class.isAssignableFrom(parameter.getParameterType())) { 58 | return typeConverter.convertIfNecessary(arg, parameter.getParameterType()); 59 | } else { 60 | return typeConverter.convertIfNecessary(arg.get(0), parameter.getParameterType()); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/org/yeauty/support/SessionMethodArgumentResolver.java: -------------------------------------------------------------------------------- 1 | package org.yeauty.support; 2 | 3 | import io.netty.channel.Channel; 4 | import org.springframework.core.MethodParameter; 5 | import org.yeauty.pojo.Session; 6 | 7 | import static org.yeauty.pojo.PojoEndpointServer.SESSION_KEY; 8 | 9 | public class SessionMethodArgumentResolver implements MethodArgumentResolver { 10 | @Override 11 | public boolean supportsParameter(MethodParameter parameter) { 12 | return Session.class.isAssignableFrom(parameter.getParameterType()); 13 | } 14 | 15 | @Override 16 | public Object resolveArgument(MethodParameter parameter, Channel channel, Object object) throws Exception { 17 | Session session = channel.attr(SESSION_KEY).get(); 18 | return session; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/org/yeauty/support/TextMethodArgumentResolver.java: -------------------------------------------------------------------------------- 1 | package org.yeauty.support; 2 | 3 | import io.netty.channel.Channel; 4 | import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; 5 | import org.springframework.core.MethodParameter; 6 | import org.yeauty.annotation.OnMessage; 7 | 8 | public class TextMethodArgumentResolver implements MethodArgumentResolver { 9 | @Override 10 | public boolean supportsParameter(MethodParameter parameter) { 11 | return parameter.getMethod().isAnnotationPresent(OnMessage.class) && String.class.isAssignableFrom(parameter.getParameterType()); 12 | } 13 | 14 | @Override 15 | public Object resolveArgument(MethodParameter parameter, Channel channel, Object object) throws Exception { 16 | TextWebSocketFrame textFrame = (TextWebSocketFrame) object; 17 | return textFrame.text(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/org/yeauty/support/ThrowableMethodArgumentResolver.java: -------------------------------------------------------------------------------- 1 | package org.yeauty.support; 2 | 3 | import io.netty.channel.Channel; 4 | import org.springframework.core.MethodParameter; 5 | import org.yeauty.annotation.OnError; 6 | 7 | public class ThrowableMethodArgumentResolver implements MethodArgumentResolver { 8 | @Override 9 | public boolean supportsParameter(MethodParameter parameter) { 10 | return parameter.getMethod().isAnnotationPresent(OnError.class) && Throwable.class.isAssignableFrom(parameter.getParameterType()); 11 | } 12 | 13 | @Override 14 | public Object resolveArgument(MethodParameter parameter, Channel channel, Object object) throws Exception { 15 | if (object instanceof Throwable) { 16 | return object; 17 | } 18 | return null; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/org/yeauty/support/WsPathMatcher.java: -------------------------------------------------------------------------------- 1 | package org.yeauty.support; 2 | 3 | import io.netty.channel.Channel; 4 | import io.netty.handler.codec.http.QueryStringDecoder; 5 | 6 | 7 | public interface WsPathMatcher { 8 | 9 | String getPattern(); 10 | 11 | boolean matchAndExtract(QueryStringDecoder decoder, Channel channel); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/org/yeauty/util/SslUtils.java: -------------------------------------------------------------------------------- 1 | package org.yeauty.util; 2 | 3 | import io.netty.handler.ssl.SslContext; 4 | import io.netty.handler.ssl.SslContextBuilder; 5 | import org.springframework.util.ResourceUtils; 6 | import org.springframework.util.StringUtils; 7 | 8 | import javax.net.ssl.KeyManagerFactory; 9 | import javax.net.ssl.SSLException; 10 | import javax.net.ssl.TrustManagerFactory; 11 | import java.net.URL; 12 | import java.security.KeyStore; 13 | 14 | /** 15 | * refer to {@link org.springframework.boot.web.embedded.netty.SslServerCustomizer} 16 | */ 17 | public final class SslUtils { 18 | 19 | public static SslContext createSslContext(String keyPassword, String keyStoreResource, String keyStoreType, String keyStorePassword, String trustStoreResource, String trustStoreType, String trustStorePassword) throws SSLException { 20 | SslContextBuilder sslBuilder = SslContextBuilder 21 | .forServer(getKeyManagerFactory(keyStoreType, keyStoreResource, keyPassword, keyStorePassword)) 22 | .trustManager(getTrustManagerFactory(trustStoreType, trustStoreResource, trustStorePassword)); 23 | return sslBuilder.build(); 24 | } 25 | 26 | private static KeyManagerFactory getKeyManagerFactory(String type, String resource, String keyPassword, String keyStorePassword) { 27 | try { 28 | KeyStore keyStore = loadKeyStore(type, resource, keyStorePassword); 29 | KeyManagerFactory keyManagerFactory = KeyManagerFactory 30 | .getInstance(KeyManagerFactory.getDefaultAlgorithm()); 31 | char[] keyPasswordBytes = (!StringUtils.isEmpty(keyPassword) 32 | ? keyPassword.toCharArray() : null); 33 | if (keyPasswordBytes == null && !StringUtils.isEmpty(keyStorePassword)) { 34 | keyPasswordBytes = keyStorePassword.toCharArray(); 35 | } 36 | keyManagerFactory.init(keyStore, keyPasswordBytes); 37 | return keyManagerFactory; 38 | } catch (Exception ex) { 39 | throw new IllegalStateException(ex); 40 | } 41 | } 42 | 43 | private static TrustManagerFactory getTrustManagerFactory(String trustStoreType, String trustStoreResource, String trustStorePassword) { 44 | try { 45 | KeyStore store = loadKeyStore(trustStoreType, trustStoreResource, trustStorePassword); 46 | TrustManagerFactory trustManagerFactory = TrustManagerFactory 47 | .getInstance(TrustManagerFactory.getDefaultAlgorithm()); 48 | trustManagerFactory.init(store); 49 | return trustManagerFactory; 50 | } catch (Exception ex) { 51 | throw new IllegalStateException(ex); 52 | } 53 | } 54 | 55 | private static KeyStore loadKeyStore(String type, String resource, String password) 56 | throws Exception { 57 | type = (StringUtils.isEmpty(type) ? "JKS" : type); 58 | if (StringUtils.isEmpty(resource)) { 59 | return null; 60 | } 61 | KeyStore store = KeyStore.getInstance(type); 62 | URL url = ResourceUtils.getURL(resource); 63 | store.load(url.openStream(), StringUtils.isEmpty(password) ? null : password.toCharArray()); 64 | return store; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/spring.factories: -------------------------------------------------------------------------------- 1 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ 2 | org.yeauty.autoconfigure.NettyWebSocketAutoConfigure --------------------------------------------------------------------------------