commit 260381856c28aa9c80462ba062d0cfab04d01230 Author: yanzuoguang Date: Sun Aug 27 10:27:58 2023 +0800 Initial commit diff --git a/learn-netty4/.gitignore b/learn-netty4/.gitignore new file mode 100644 index 0000000..1f168b4 --- /dev/null +++ b/learn-netty4/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/** +!**/src/test/** + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ + +### VS Code ### +.vscode/ + +.DS_Store diff --git a/learn-netty4/LICENSE.txt b/learn-netty4/LICENSE.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/learn-netty4/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/learn-netty4/NOTICE.txt b/learn-netty4/NOTICE.txt new file mode 100644 index 0000000..e1307b2 --- /dev/null +++ b/learn-netty4/NOTICE.txt @@ -0,0 +1,34 @@ + + Learn Netty4 Project + ================= + +Please visit the flydean web site for more information: + + * http://www.flydean.com/ + +Copyright 2022 learn-netty4 Project + +The learn-netty4 Project licenses this file to you under the Apache License, +version 2.0 (the "License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at: + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +License for the specific language governing permissions and limitations +under the License. + +Also, please refer to each LICENSE..txt file, which is located in +the 'license' directory of the distribution file, for the license terms of the +components that this product depends on. + +------------------------------------------------------------------------------- +This project contains a modified portion of 'Netty Project',an asynchronous +event-driven network application framework,which can be obtained at: + + * LICENSE: + * license/LICENSE.netty.txt (Apache License 2.0) + * HOMEPAGE: + * https://github.com/netty/netty diff --git a/learn-netty4/README.md b/learn-netty4/README.md new file mode 100644 index 0000000..eede121 --- /dev/null +++ b/learn-netty4/README.md @@ -0,0 +1,76 @@ +# learn-netty4 +跟我一起学netty4.X + +## LICENSE + +Copyright 2022 learn-netty4 Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +------------------------------------------------------------------------------- +This project contains a modified portion of 'Netty Project',an asynchronous +event-driven network application framework,which can be obtained at: + +* LICENSE: + * license/LICENSE.netty.txt (Apache License 2.0) +* HOMEPAGE: + * https://github.com/netty/netty + +## 本项目的内容 + +毕生的收藏都在这了,看得上的小伙伴点个star吧 ![Github stars](https://img.shields.io/github/stars/ddean2009/learn-netty4.svg) + +系列文章请参考:[netty系列教程](http://www.flydean.com/category/%e5%93%8d%e5%ba%94%e5%bc%8f%e7%b3%bb%e7%bb%9f/netty/) + + +文章列表如下: + +* [01 Netty Startup](http://www.flydean.com/01-netty-startup) +* [02 Netty Bytebuf](http://www.flydean.com/02-netty-bytebuf) +* [03 Netty Architecture](http://www.flydean.com/03-netty-architecture) +* [04 Netty Channel](http://www.flydean.com/04-netty-Channel) +* [05 Netty Channel Event](http://www.flydean.com/05-netty-ChannelEvent) +* [06 Netty Cheerup China](http://www.flydean.com/06-netty-cheerup-china) +* [07 Netty Stream Based Transport](http://www.flydean.com/07-netty-stream-based-transport) +* [08 Netty Pojo Buf](http://www.flydean.com/08-netty-pojo-buf) +* [09 Netty Reconnect](http://www.flydean.com/09-netty-reconnect) +* [10 Netty Chat](http://www.flydean.com/10-netty-chat) +* [11 Netty Udp](http://www.flydean.com/11-netty-udp) +* [12 Netty Securechat](http://www.flydean.com/12-netty-securechat) +* [13 Netty Customprotocol](http://www.flydean.com/13-netty-customprotocol) +* [14 Netty Cust Codec](http://www.flydean.com/14-netty-cust-codec) +* [15 Netty Buildin Frame Detection](http://www.flydean.com/15-netty-buildin-frame-detection) +* [16 Netty Buildin Codec Common](http://www.flydean.com/16-netty-buildin-codec-common) +* [17 Netty Protobuf](http://www.flydean.com/17-netty-protobuf) +* [18 Netty Http Request](http://www.flydean.com/18-netty-http-request) +* [19 Netty Http Client Request](http://www.flydean.com/19-netty-http-client-request) +* [20 Netty Fileserver](http://www.flydean.com/20-netty-fileserver) +* [21 Netty Http Fileupload](http://www.flydean.com/21-netty-http-fileupload) +* [22 Netty Cors](http://www.flydean.com/22-netty-cors) +* [23 Netty Websocket Server](http://www.flydean.com/23-netty-websocket-server) +* [24 Netty Websocket Server 2](http://www.flydean.com/24-netty-websocket-server2) +* [25 Netty Websocket Client](http://www.flydean.com/25-netty-websocket-client) +* [26 Netty Secure Http 2](http://www.flydean.com/26-netty-secure-http2) +* [27 Netty Http 2](http://www.flydean.com/27-netty-http2) +* [28 Netty Wrap Http 2](http://www.flydean.com/28-netty-wrap-http2) +* [29 Netty Flowcontrol](http://www.flydean.com/29-netty-flowcontrol) +* [30 Netty Http 2 Client](http://www.flydean.com/30-netty-http2client) +* [31 Netty Framecodec Http 2](http://www.flydean.com/31-netty-framecodec-http2) +* [32 Netty Http 2 Client Framecodec](http://www.flydean.com/32-netty-http2client-framecodec) +* [33 Netty Multiplex Http 2 Server](http://www.flydean.com/33-netty-multiplex-http2server) +* [34 Netty Multiple Server](http://www.flydean.com/34-netty-multiple-server) +* [35 Netty Simple Proxy](http://www.flydean.com/35-netty-simple-proxy) +* [36 Netty Socks Support](http://www.flydean.com/36-netty-socks-support) +* [37 Netty Cust Socks Server](http://www.flydean.com/37-netty-cust-socks-server) +* [38 Netty Cust Port Unification](http://www.flydean.com/38-netty-cust-port-unification) + diff --git a/learn-netty4/file.txt b/learn-netty4/file.txt new file mode 100644 index 0000000..2e20b82 --- /dev/null +++ b/learn-netty4/file.txt @@ -0,0 +1,1440 @@ +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com +www.flydean.com diff --git a/learn-netty4/lib/bin/lib/x86_64-MacOSX-gpp/jni/libbarchart-udt-core-2.3.0-SNAPSHOT.jnilib b/learn-netty4/lib/bin/lib/x86_64-MacOSX-gpp/jni/libbarchart-udt-core-2.3.0-SNAPSHOT.jnilib new file mode 100644 index 0000000..932f10b Binary files /dev/null and b/learn-netty4/lib/bin/lib/x86_64-MacOSX-gpp/jni/libbarchart-udt-core-2.3.0-SNAPSHOT.jnilib differ diff --git a/learn-netty4/license/LICENSE.netty.txt b/learn-netty4/license/LICENSE.netty.txt new file mode 100644 index 0000000..39a46d0 --- /dev/null +++ b/learn-netty4/license/LICENSE.netty.txt @@ -0,0 +1,20 @@ + The Netty Project + ================= + +Please visit the Netty web site for more information: + + * https://netty.io/ + +Copyright 2014 The Netty Project + +The Netty Project licenses this file to you under the Apache License, +version 2.0 (the "License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at: + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +License for the specific language governing permissions and limitations +under the License. \ No newline at end of file diff --git a/learn-netty4/pom.xml b/learn-netty4/pom.xml new file mode 100644 index 0000000..75c1132 --- /dev/null +++ b/learn-netty4/pom.xml @@ -0,0 +1,131 @@ + + + 4.0.0 + + org.example + learn-netty + 1.0-SNAPSHOT + + + 8 + 8 + 1.2.11 + + + + + io.netty + netty-all + 4.1.77.Final + + + org.projectlombok + lombok + 1.18.24 + + + ch.qos.logback + logback-access + ${logback.version} + + + ch.qos.logback + logback-classic + ${logback.version} + + + ch.qos.logback + logback-core + ${logback.version} + + + + + com.google.protobuf + protobuf-java + 3.20.1 + + + com.google.protobuf + protobuf-java-util + 3.20.1 + + + + + org.jboss.marshalling + jboss-marshalling + 2.0.12.Final + + + org.jboss.marshalling + jboss-marshalling-serial + 2.0.12.Final + test + + + org.jboss.marshalling + jboss-marshalling-river + 2.0.12.Final + + + + org.conscrypt + conscrypt-openjdk-uber + 2.5.2 + + + + + io.netty + netty-tcnative + 2.0.52.Final + + + io.netty + netty-tcnative-boringssl-static + 2.0.52.Final + + + + com.barchart.udt + barchart-udt-core + 2.3.0 + + + + com.barchart.udt + barchart-udt-bundle + 2.3.0 + + + + net.openhft + affinity + 3.20.0 + + + + + + + + + + + + org.bouncycastle + bcpkix-jdk15on + 1.70 + + + org.bouncycastle + bcprov-jdk15on + 1.70 + + + + + \ No newline at end of file diff --git a/learn-netty4/src/main/java/com/flydean01/first/FirstClient.java b/learn-netty4/src/main/java/com/flydean01/first/FirstClient.java new file mode 100644 index 0000000..bf2c370 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean01/first/FirstClient.java @@ -0,0 +1,59 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean01.first; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; + +/** + * 第一个netty的客户端 + */ +public final class FirstClient { + + static final String HOST = System.getProperty("host", "127.0.0.1"); + static final int PORT = Integer.parseInt(System.getProperty("port", "8000")); + + public static void main(String[] args) throws Exception { + + EventLoopGroup group = new NioEventLoopGroup(); + try { + Bootstrap b = new Bootstrap(); + b.group(group) + .channel(NioSocketChannel.class) + .handler(new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel ch) throws Exception { + ChannelPipeline p = ch.pipeline(); + p.addLast(new FirstClientHandler()); + } + }); + + // 连接服务器 + ChannelFuture f = b.connect(HOST, PORT).sync(); + + // 等待关闭 + f.channel().closeFuture().sync(); + } finally { + group.shutdownGracefully(); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean01/first/FirstClientHandler.java b/learn-netty4/src/main/java/com/flydean01/first/FirstClientHandler.java new file mode 100644 index 0000000..899cd65 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean01/first/FirstClientHandler.java @@ -0,0 +1,52 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean01.first; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.*; +import lombok.extern.slf4j.Slf4j; + +import java.nio.charset.StandardCharsets; + +/** + * Handles a client-side channel. + */ +@Slf4j +public class FirstClientHandler extends ChannelInboundHandlerAdapter { + + private ByteBuf content; + private ChannelHandlerContext ctx; + + @Override + public void channelActive(ChannelHandlerContext ctx) { + this.ctx = ctx; + content = ctx.alloc().directBuffer(256).writeBytes("Hello flydean.com".getBytes(StandardCharsets.UTF_8)); + // 发送消息 + sayHello(); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + // 异常处理 + log.error("出现异常",cause); + ctx.close(); + } + + private void sayHello() { + // 向服务器输出消息 + ctx.writeAndFlush(content.retain()); + } +} diff --git a/learn-netty4/src/main/java/com/flydean01/first/FirstServer.java b/learn-netty4/src/main/java/com/flydean01/first/FirstServer.java new file mode 100644 index 0000000..9bb9177 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean01/first/FirstServer.java @@ -0,0 +1,70 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean01.first; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import lombok.AllArgsConstructor; + +/** + * @author wayne + * @version FirstServer, 2021/8/1 + */ +@AllArgsConstructor +public class FirstServer { + + private final int port; + + public void start() throws InterruptedException { + //建立两个EventloopGroup用来处理连接和消息 + EventLoopGroup bossGroup = new NioEventLoopGroup(); + EventLoopGroup workerGroup = new NioEventLoopGroup(); + try { + ServerBootstrap b = new ServerBootstrap(); + b.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .childHandler(new ChannelInitializer() { + @Override + public void initChannel(SocketChannel ch) throws Exception { + ch.pipeline().addLast(new FirstServerHandler()); + } + }) + .option(ChannelOption.SO_BACKLOG, 128) + .childOption(ChannelOption.SO_KEEPALIVE, true); + + // 绑定端口并开始接收连接 + ChannelFuture f = b.bind(port).sync(); + // 等待server socket关闭 + f.channel().closeFuture().sync(); + } finally { + //关闭group + workerGroup.shutdownGracefully(); + bossGroup.shutdownGracefully(); + } + } + + public static void main(String[] args) throws InterruptedException { + int port=8000; + new FirstServer(port).start(); + } + +} diff --git a/learn-netty4/src/main/java/com/flydean01/first/FirstServerHandler.java b/learn-netty4/src/main/java/com/flydean01/first/FirstServerHandler.java new file mode 100644 index 0000000..44a80f3 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean01/first/FirstServerHandler.java @@ -0,0 +1,47 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean01.first; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.util.ReferenceCountUtil; +import lombok.extern.slf4j.Slf4j; + +/** + * @author wayne + * @version FirstServer, 2021/8/1 + */ +@Slf4j +public class FirstServerHandler extends ChannelInboundHandlerAdapter { + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + // 对消息进行处理 + ByteBuf in = (ByteBuf) msg; + try { + log.info("收到消息:{}",in.toString(io.netty.util.CharsetUtil.US_ASCII)); + }finally { + ReferenceCountUtil.release(msg); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + // 异常处理 + log.error("出现异常",cause); + ctx.close(); + } +} diff --git a/learn-netty4/src/main/java/com/flydean02/bytebuf/ByteBufUsage.java b/learn-netty4/src/main/java/com/flydean02/bytebuf/ByteBufUsage.java new file mode 100644 index 0000000..5be9c8d --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean02/bytebuf/ByteBufUsage.java @@ -0,0 +1,56 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean02.bytebuf; + +import io.netty.buffer.ByteBuf; + +import java.util.Random; + +import static io.netty.buffer.Unpooled.*; + +/** + * @author wayne + * @version ByteBufUsage, 2021/8/2 + */ +public class ByteBufUsage { + + public static void main(String[] args) { + //创建ByteBuf + ByteBuf heapBuffer = buffer(128); + ByteBuf directBuffer = directBuffer(256); + ByteBuf wrappedBuffer = wrappedBuffer(new byte[128], new byte[256]); + + //随机访问 + ByteBuf buffer = heapBuffer; + for (int i = 0; i < buffer.capacity(); i ++) { + byte b = buffer.getByte(i); + System.out.println((char) b); + } + + //遍历readable bytes + while (directBuffer.isReadable()) { + System.out.println(directBuffer.readByte()); + } + + //写入writable bytes + while (wrappedBuffer.maxWritableBytes() >= 4) { + wrappedBuffer.writeInt(new Random().nextInt()); + } + + + + } +} diff --git a/learn-netty4/src/main/java/com/flydean03/handler/MyHandler.java b/learn-netty4/src/main/java/com/flydean03/handler/MyHandler.java new file mode 100644 index 0000000..e1eb7e8 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean03/handler/MyHandler.java @@ -0,0 +1,47 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean03.handler; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.util.ReferenceCountUtil; +import lombok.extern.slf4j.Slf4j; + +/** + * Handles a server-side channel. + */ +@Slf4j +public class MyHandler extends SimpleChannelInboundHandler { + + @Override + public void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception { + // 对消息进行处理 + ByteBuf in = (ByteBuf) msg; + try { + log.info("收到消息:{}",in.toString(io.netty.util.CharsetUtil.US_ASCII)); + }finally { + ReferenceCountUtil.release(msg); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + //异常处理 + cause.printStackTrace(); + ctx.close(); + } +} diff --git a/learn-netty4/src/main/java/com/flydean06/cheerup/CheerUpServer.java b/learn-netty4/src/main/java/com/flydean06/cheerup/CheerUpServer.java new file mode 100644 index 0000000..099e135 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean06/cheerup/CheerUpServer.java @@ -0,0 +1,72 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean06.cheerup; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.*; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; + +/** + * 收到Client的消息之后会输出"加油" + */ +public final class CheerUpServer { + + static final int PORT = Integer.parseInt(System.getProperty("port", "8007")); + static final int SIZE = Integer.parseInt(System.getProperty("size", "20")); + + public static void main(String[] args) throws Exception { + + // Server配置 + //boss loop + EventLoopGroup bossGroup = new NioEventLoopGroup(1); + //worker loop + EventLoopGroup workerGroup = new NioEventLoopGroup(); + final CheerUpServerHandler serverHandler = new CheerUpServerHandler(); + try { + ServerBootstrap b = new ServerBootstrap(); + b.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + // tcp/ip协议listen函数中的backlog参数,等待连接池的大小 + .option(ChannelOption.SO_BACKLOG, 100) + //日志处理器 + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new ChannelInitializer() { + @Override + //初始化channel,添加handler + public void initChannel(SocketChannel ch) throws Exception { + ChannelPipeline p = ch.pipeline(); + //日志处理器 + p.addLast(new LoggingHandler(LogLevel.INFO)); + p.addLast(serverHandler); + } + }); + + // 启动服务器 + ChannelFuture f = b.bind(PORT).sync(); + + // 等待channel关闭 + f.channel().closeFuture().sync(); + } finally { + // 关闭所有的event loop + bossGroup.shutdownGracefully(); + workerGroup.shutdownGracefully(); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean06/cheerup/CheerUpServerHandler.java b/learn-netty4/src/main/java/com/flydean06/cheerup/CheerUpServerHandler.java new file mode 100644 index 0000000..d346486 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean06/cheerup/CheerUpServerHandler.java @@ -0,0 +1,65 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean06.cheerup; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandler.Sharable; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import lombok.extern.slf4j.Slf4j; + +import java.nio.charset.StandardCharsets; + +/** + * 加油服务器的处理器 + */ +@Slf4j +@Sharable +public class CheerUpServerHandler extends ChannelInboundHandlerAdapter { + + private ByteBuf message; + + public CheerUpServerHandler(){ + message = Unpooled.buffer(CheerUpServer.SIZE); + message.writeBytes("加油!".getBytes(StandardCharsets.UTF_8)); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + log.info("收到消息:{}",msg); + log.info("服务器端收到消息:{}",((ByteBuf)msg).toString(StandardCharsets.UTF_8)); + log.info("可读字节:{},readerIndex:{}",message.readableBytes(),message.readerIndex()); + log.info("可写字节:{},writerIndex:{}",message.writableBytes(),message.writerIndex()); + +// message = Unpooled.buffer(CheerUpServer.SIZE); +// message.writeBytes("加油!".getBytes(StandardCharsets.UTF_8)); + message.retain(); + ctx.writeAndFlush(message); + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + ctx.flush(); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + // 异常处理 + log.error("出现异常",cause); + ctx.close(); + } +} diff --git a/learn-netty4/src/main/java/com/flydean06/cheerup/ChinaClient.java b/learn-netty4/src/main/java/com/flydean06/cheerup/ChinaClient.java new file mode 100644 index 0000000..2c31b4e --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean06/cheerup/ChinaClient.java @@ -0,0 +1,64 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean06.cheerup; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.*; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; + +/** + * 客户端 + */ +public final class ChinaClient { + + static final String HOST = System.getProperty("host", "127.0.0.1"); + static final int PORT = Integer.parseInt(System.getProperty("port", "8007")); + static final int SIZE = Integer.parseInt(System.getProperty("size", "20")); + + public static void main(String[] args) throws Exception { + + // 客户端的eventLoop + EventLoopGroup group = new NioEventLoopGroup(); + try { + Bootstrap b = new Bootstrap(); + b.group(group) + .channel(NioSocketChannel.class) + .option(ChannelOption.TCP_NODELAY, true) + .handler(new ChannelInitializer() { + @Override + public void initChannel(SocketChannel ch) throws Exception { + ChannelPipeline p = ch.pipeline(); + //添加日志处理器 + p.addLast(new LoggingHandler(LogLevel.INFO)); + p.addLast(new ChinaClientHandler()); + } + }); + + // 启动客户端 + ChannelFuture f = b.connect(HOST, PORT).sync(); + + // 等待channel关闭 + f.channel().closeFuture().sync(); + } finally { + // 关闭所有的event loop + group.shutdownGracefully(); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean06/cheerup/ChinaClientHandler.java b/learn-netty4/src/main/java/com/flydean06/cheerup/ChinaClientHandler.java new file mode 100644 index 0000000..91bf711 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean06/cheerup/ChinaClientHandler.java @@ -0,0 +1,78 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean06.cheerup; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import lombok.extern.slf4j.Slf4j; + +import java.nio.charset.StandardCharsets; + +/** + * 加油服务的客户端, + */ +@Slf4j +public class ChinaClientHandler extends ChannelInboundHandlerAdapter { + + private ByteBuf message; + + /** + * 客户端处理器 + */ + public ChinaClientHandler() { + message = Unpooled.buffer(ChinaClient.SIZE); + message.writeBytes("中国".getBytes(StandardCharsets.UTF_8)); + } + + @Override + public void channelActive(ChannelHandlerContext ctx) { + log.info("可读字节:{},readerIndex:{}",message.readableBytes(),message.readerIndex()); + log.info("可写字节:{},writerIndex:{}",message.writableBytes(),message.writerIndex()); + log.info("capacity:{},refCnt{}",message.capacity(),message.refCnt()); + message.retain(); + ctx.writeAndFlush(message); +// ctx.writeAndFlush("中国"); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + log.info("客户端收到消息:{}",((ByteBuf)msg).toString(StandardCharsets.UTF_8)); + log.info("可读字节:{},readerIndex:{}",message.readableBytes(),message.readerIndex()); + log.info("可写字节:{},writerIndex:{}",message.writableBytes(),message.writerIndex()); + log.info("capacity:{},refCnt{}",message.capacity(),message.refCnt()); + + log.info("可读字节:{},readerIndex:{}",message.readableBytes(),message.readerIndex()); + log.info("可写字节:{},writerIndex:{}",message.writableBytes(),message.writerIndex()); +// message = Unpooled.buffer(ChinaClient.SIZE); +// message.writeBytes("中国".getBytes(StandardCharsets.UTF_8)); + message.retain(); + ctx.writeAndFlush(message); + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + ctx.flush(); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + // 异常处理 + log.error("出现异常",cause); + ctx.close(); + } +} diff --git a/learn-netty4/src/main/java/com/flydean08/pojo/PojoClient.java b/learn-netty4/src/main/java/com/flydean08/pojo/PojoClient.java new file mode 100644 index 0000000..32d4db4 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean08/pojo/PojoClient.java @@ -0,0 +1,63 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean08.pojo; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.serialization.ClassResolvers; +import io.netty.handler.codec.serialization.ObjectDecoder; +import io.netty.handler.codec.serialization.ObjectEncoder; + +/** + * Pojo对象的client + */ +public final class PojoClient { + + static final String HOST = System.getProperty("host", "127.0.0.1"); + static final int PORT = Integer.parseInt(System.getProperty("port", "8007")); + static final int SIZE = Integer.parseInt(System.getProperty("size", "256")); + + public static void main(String[] args) throws Exception { + + EventLoopGroup group = new NioEventLoopGroup(); + try { + Bootstrap b = new Bootstrap(); + b.group(group) + .channel(NioSocketChannel.class) + .handler(new ChannelInitializer() { + @Override + public void initChannel(SocketChannel ch) throws Exception { + ChannelPipeline p = ch.pipeline(); + p.addLast( + // 添加encoder和decoder + new ObjectEncoder(), + new ObjectDecoder(ClassResolvers.cacheDisabled(null)), + new PojoClientHandler()); + } + }); + + // 建立连接 + b.connect(HOST, PORT).sync().channel().closeFuture().sync(); + } finally { + group.shutdownGracefully(); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean08/pojo/PojoClientHandler.java b/learn-netty4/src/main/java/com/flydean08/pojo/PojoClientHandler.java new file mode 100644 index 0000000..aeacbf9 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean08/pojo/PojoClientHandler.java @@ -0,0 +1,72 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean08.pojo; + +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import lombok.extern.slf4j.Slf4j; + +import java.util.ArrayList; +import java.util.List; + +import static io.netty.channel.ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE; + +/** + * client端处理器, + */ +@Slf4j +public class PojoClientHandler extends ChannelInboundHandlerAdapter { + + private final List firstMessage; + + /** + * 初始化handler + */ + public PojoClientHandler() { + firstMessage = new ArrayList<>(PojoClient.SIZE); + for (int i = 0; i < PojoClient.SIZE; i ++) { + firstMessage.add(i); + } + } + + @Override + public void channelActive(ChannelHandlerContext ctx) { + // 在channel active的时候发送消息 + ChannelFuture future = ctx.writeAndFlush("中国"); + // 将ChannelFuture中的Throwable转发到ChannelPipeline中。 + future.addListener(FIRE_EXCEPTION_ON_FAILURE); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + // 将消息写回channel + log.info("客户端收到对象:{}",msg); + ctx.write(msg); + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + ctx.flush(); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + // 异常处理 + log.error("出现异常",cause); + ctx.close(); + } +} diff --git a/learn-netty4/src/main/java/com/flydean08/pojo/PojoServer.java b/learn-netty4/src/main/java/com/flydean08/pojo/PojoServer.java new file mode 100644 index 0000000..3278fff --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean08/pojo/PojoServer.java @@ -0,0 +1,67 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean08.pojo; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.codec.serialization.ClassResolvers; +import io.netty.handler.codec.serialization.ObjectDecoder; +import io.netty.handler.codec.serialization.ObjectEncoder; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; + +/** + * 可以发送PoJo的Sever + */ +public final class PojoServer { + + static final int PORT = Integer.parseInt(System.getProperty("port", "8007")); + + public static void main(String[] args) throws Exception { + + //定义bossGroup和workerGroup + EventLoopGroup bossGroup = new NioEventLoopGroup(1); + EventLoopGroup workerGroup = new NioEventLoopGroup(); + try { + ServerBootstrap b = new ServerBootstrap(); + b.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new ChannelInitializer() { + @Override + public void initChannel(SocketChannel ch) throws Exception { + ChannelPipeline p = ch.pipeline(); + p.addLast( + // 添加encoder和decoder + new ObjectEncoder(), + new ObjectDecoder(ClassResolvers.cacheDisabled(null)), + new PojoServerHandler()); + } + }); + + // 绑定端口,并准备接受连接 + b.bind(PORT).sync().channel().closeFuture().sync(); + } finally { + bossGroup.shutdownGracefully(); + workerGroup.shutdownGracefully(); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean08/pojo/PojoServerHandler.java b/learn-netty4/src/main/java/com/flydean08/pojo/PojoServerHandler.java new file mode 100644 index 0000000..e638374 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean08/pojo/PojoServerHandler.java @@ -0,0 +1,45 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean08.pojo; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import lombok.extern.slf4j.Slf4j; + +/** + * 处理客户端发来的消息 + */ +@Slf4j +public class PojoServerHandler extends ChannelInboundHandlerAdapter { + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + log.info("server收到对象:{}",msg); + ctx.write("加油!"); + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + ctx.flush(); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + // 异常处理 + log.error("出现异常",cause); + ctx.close(); + } +} diff --git a/learn-netty4/src/main/java/com/flydean09/reconnect/ReconnectClient.java b/learn-netty4/src/main/java/com/flydean09/reconnect/ReconnectClient.java new file mode 100644 index 0000000..2c0c98f --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean09/reconnect/ReconnectClient.java @@ -0,0 +1,64 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean09.reconnect; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.timeout.IdleStateHandler; + + +/** + * netty客户端 + */ +public final class ReconnectClient { + + static final String HOST = System.getProperty("host", "127.0.0.1"); + static final int PORT = Integer.parseInt(System.getProperty("port", "8080")); + // 在reconnect之前 Sleep 5 秒钟 + static final int RECONNECT_DELAY = Integer.parseInt(System.getProperty("reconnectDelay", "5")); + //如果在10秒中之内没有任何相应则重连 + private static final int READ_TIMEOUT = Integer.parseInt(System.getProperty("readTimeout", "10")); + + private static final ReconnectClientHandler handler = new ReconnectClientHandler(); + private static final Bootstrap bs = new Bootstrap(); + + public static void main(String[] args) { + EventLoopGroup group = new NioEventLoopGroup(); + bs.group(group) + .channel(NioSocketChannel.class) + .remoteAddress(HOST, PORT) + .handler(new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel ch) throws Exception { + ch.pipeline().addLast(new IdleStateHandler(READ_TIMEOUT, 0, 0), handler); + } + }); + bs.connect(); + } + + static void connect() { + bs.connect().addListener(future -> { + if (future.cause() != null) { + handler.startTime = -1; + handler.println("建立连接失败: " + future.cause()); + } + }); + } +} diff --git a/learn-netty4/src/main/java/com/flydean09/reconnect/ReconnectClientHandler.java b/learn-netty4/src/main/java/com/flydean09/reconnect/ReconnectClientHandler.java new file mode 100644 index 0000000..a74773a --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean09/reconnect/ReconnectClientHandler.java @@ -0,0 +1,91 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean09.reconnect; + +import io.netty.channel.ChannelHandler.Sharable; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.timeout.IdleState; +import io.netty.handler.timeout.IdleStateEvent; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.TimeUnit; + +/** + * 重连handler + */ +@Sharable +@Slf4j +public class ReconnectClientHandler extends SimpleChannelInboundHandler { + + long startTime = -1; + + @Override + public void channelActive(ChannelHandlerContext ctx) { + if (startTime < 0) { + startTime = System.currentTimeMillis(); + } + println("连接到: " + ctx.channel().remoteAddress()); + } + + @Override + public void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception { + // 读取消息 + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + if (!(evt instanceof IdleStateEvent)) { + return; + } + IdleStateEvent e = (IdleStateEvent) evt; + if (e.state() == IdleState.READER_IDLE) { + // 在Idle状态 + println("Idle状态,关闭连接"); + ctx.close(); + } + } + + @Override + public void channelInactive(final ChannelHandlerContext ctx) { + println("连接断开:" + ctx.channel().remoteAddress()); + } + + @Override + public void channelUnregistered(final ChannelHandlerContext ctx) throws Exception { + println("sleep:" + ReconnectClient.RECONNECT_DELAY + 's'); + + ctx.channel().eventLoop().schedule(() -> { + println("重连接: " + ReconnectClient.HOST + ':' + ReconnectClient.PORT); + ReconnectClient.connect(); + }, ReconnectClient.RECONNECT_DELAY, TimeUnit.SECONDS); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + // 异常处理 + log.error("出现异常",cause); + ctx.close(); + } + + void println(String msg) { + if (startTime < 0) { + log.error("服务下线:{}",msg); + } else { + log.error("服务运行时间:{},{}",(System.currentTimeMillis() - startTime) / 1000, msg); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean09/reconnect/ReconnectServer.java b/learn-netty4/src/main/java/com/flydean09/reconnect/ReconnectServer.java new file mode 100644 index 0000000..e1b6434 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean09/reconnect/ReconnectServer.java @@ -0,0 +1,64 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean09.reconnect; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; + +/** + * netty 服务器 + */ +public final class ReconnectServer { + private static final int PORT = Integer.parseInt(System.getProperty("port", "8080")); + private static final ReconnectServerHandler handler = new ReconnectServerHandler(); + + private ReconnectServer() { + } + + public static void main(String[] args) throws Exception { + + EventLoopGroup bossGroup = new NioEventLoopGroup(1); + EventLoopGroup workerGroup = new NioEventLoopGroup(); + try { + ServerBootstrap b = new ServerBootstrap(); + b.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new ChannelInitializer() { + @Override + public void initChannel(SocketChannel ch) { + ch.pipeline().addLast(handler); + } + }); + + // 绑定端口并启动 + ChannelFuture f = b.bind(PORT).sync(); + + // 等待所有的socket都关闭 + f.channel().closeFuture().sync(); + } finally { + workerGroup.shutdownGracefully(); + bossGroup.shutdownGracefully(); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean09/reconnect/ReconnectServerHandler.java b/learn-netty4/src/main/java/com/flydean09/reconnect/ReconnectServerHandler.java new file mode 100644 index 0000000..f29f10e --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean09/reconnect/ReconnectServerHandler.java @@ -0,0 +1,37 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean09.reconnect; + +import io.netty.channel.ChannelHandler.Sharable; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import lombok.extern.slf4j.Slf4j; + +@Sharable +@Slf4j +public class ReconnectServerHandler extends SimpleChannelInboundHandler { + @Override + public void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception { + // 读取消息 + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + // 异常处理 + log.error("出现异常",cause); + ctx.close(); + } +} diff --git a/learn-netty4/src/main/java/com/flydean10/chat/ChatClient.java b/learn-netty4/src/main/java/com/flydean10/chat/ChatClient.java new file mode 100644 index 0000000..f7d8973 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean10/chat/ChatClient.java @@ -0,0 +1,76 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean10.chat; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioSocketChannel; +import lombok.extern.slf4j.Slf4j; + +import java.io.BufferedReader; +import java.io.InputStreamReader; + +/** + * 聊天室客户端 + */ +@Slf4j +public final class ChatClient { + + static final String HOST = System.getProperty("host", "127.0.0.1"); + static final int PORT = Integer.parseInt(System.getProperty("port", "8000")); + + public static void main(String[] args) throws Exception { + + EventLoopGroup group = new NioEventLoopGroup(); + try { + Bootstrap b = new Bootstrap(); + b.group(group) + .channel(NioSocketChannel.class) + .handler(new ChatClientInitializer()); + + // 建立连接 + Channel ch = b.connect(HOST, PORT).sync().channel(); + log.info("client channel: {}", ch); + + // 从命令行输入 + ChannelFuture lastWriteFuture = null; + BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); + for (;;) { + String line = in.readLine(); + if (line == null) { + break; + } + // 将从命令行输入的一行字符写到channel中 + lastWriteFuture = ch.writeAndFlush(line + "\r\n"); + // 如果输入'再见',则等待server端关闭channel + if ("再见".equalsIgnoreCase(line)) { + ch.closeFuture().sync(); + break; + } + } + + // 等待所有的消息都写入channel中 + if (lastWriteFuture != null) { + lastWriteFuture.sync(); + } + } finally { + group.shutdownGracefully(); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean10/chat/ChatClientHandler.java b/learn-netty4/src/main/java/com/flydean10/chat/ChatClientHandler.java new file mode 100644 index 0000000..57b0172 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean10/chat/ChatClientHandler.java @@ -0,0 +1,42 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean10.chat; + +import io.netty.channel.ChannelHandler.Sharable; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import lombok.extern.slf4j.Slf4j; + +/** + * 客户端处理器 + */ +@Sharable +@Slf4j +public class ChatClientHandler extends SimpleChannelInboundHandler { + + @Override + protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception { + log.info("client accepted channel: {}", ctx.channel()); + log.info("接收到消息:{}",msg); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + // 异常处理 + log.error("出现异常",cause); + ctx.close(); + } +} diff --git a/learn-netty4/src/main/java/com/flydean10/chat/ChatClientInitializer.java b/learn-netty4/src/main/java/com/flydean10/chat/ChatClientInitializer.java new file mode 100644 index 0000000..2e7fde8 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean10/chat/ChatClientInitializer.java @@ -0,0 +1,48 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean10.chat; + +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.DelimiterBasedFrameDecoder; +import io.netty.handler.codec.Delimiters; +import io.netty.handler.codec.string.StringDecoder; +import io.netty.handler.codec.string.StringEncoder; + +/** + * 客户端ChannelPipeline的配置 + */ +public class ChatClientInitializer extends ChannelInitializer { + + private static final StringDecoder DECODER = new StringDecoder(); + private static final StringEncoder ENCODER = new StringEncoder(); + + private static final ChatClientHandler CLIENT_HANDLER = new ChatClientHandler(); + + @Override + public void initChannel(SocketChannel ch) { + ChannelPipeline pipeline = ch.pipeline(); + + // 添加行分割器 + pipeline.addLast(new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter())); + pipeline.addLast(DECODER); + pipeline.addLast(ENCODER); + + // 添加客户端处理器 + pipeline.addLast(CLIENT_HANDLER); + } +} diff --git a/learn-netty4/src/main/java/com/flydean10/chat/ChatServer.java b/learn-netty4/src/main/java/com/flydean10/chat/ChatServer.java new file mode 100644 index 0000000..bd22fd3 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean10/chat/ChatServer.java @@ -0,0 +1,54 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean10.chat; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import lombok.extern.slf4j.Slf4j; + +/** + * chat 服务器 + */ +@Slf4j +public final class ChatServer { + + static final int PORT = Integer.parseInt(System.getProperty("port", "8000")); + + public static void main(String[] args) throws Exception { + + EventLoopGroup bossGroup = new NioEventLoopGroup(1); + EventLoopGroup workerGroup = new NioEventLoopGroup(); + try { + ServerBootstrap b = new ServerBootstrap(); + b.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new ChatServerInitializer()); + + Channel channel = b.bind(PORT).sync().channel(); + log.info("server channel:{}", channel); + channel.closeFuture().sync(); + } finally { + bossGroup.shutdownGracefully(); + workerGroup.shutdownGracefully(); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean10/chat/ChatServerHandler.java b/learn-netty4/src/main/java/com/flydean10/chat/ChatServerHandler.java new file mode 100644 index 0000000..40937ca --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean10/chat/ChatServerHandler.java @@ -0,0 +1,75 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean10.chat; + +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandler.Sharable; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import lombok.extern.slf4j.Slf4j; + +/** + * server端的处理器 + */ +@Sharable +@Slf4j +public class ChatServerHandler extends SimpleChannelInboundHandler { + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + log.info("accepted channel: {}", ctx.channel()); + log.info("accepted channel parent: {}", ctx.channel().parent()); + // channel活跃 + ctx.write("Channel Active状态!\r\n"); + ctx.flush(); + } + + @Override + public void channelRead0(ChannelHandlerContext ctx, String request) throws Exception { + // 如果读取到"再见"就关闭channel + String response; + // 判断是否关闭 + boolean close = false; + if (request.isEmpty()) { + response = "你说啥?\r\n"; + } else if ("再见".equalsIgnoreCase(request)) { + response = "再见,我的朋友!\r\n"; + close = true; + } else { + response = "你是不是说: '" + request + "'?\r\n"; + } + + // 写入消息 + ChannelFuture future = ctx.write(response); + // 添加CLOSE listener,用来关闭channel + if (close) { + future.addListener(ChannelFutureListener.CLOSE); + } + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + ctx.flush(); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + // 异常处理 + log.error("出现异常",cause); + ctx.close(); + } +} diff --git a/learn-netty4/src/main/java/com/flydean10/chat/ChatServerInitializer.java b/learn-netty4/src/main/java/com/flydean10/chat/ChatServerInitializer.java new file mode 100644 index 0000000..dc33b5d --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean10/chat/ChatServerInitializer.java @@ -0,0 +1,47 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean10.chat; + +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.DelimiterBasedFrameDecoder; +import io.netty.handler.codec.Delimiters; +import io.netty.handler.codec.string.StringDecoder; +import io.netty.handler.codec.string.StringEncoder; + +/** + * 初始化一个ChannelPipeline + */ +public class ChatServerInitializer extends ChannelInitializer { + + private static final StringDecoder DECODER = new StringDecoder(); + private static final StringEncoder ENCODER = new StringEncoder(); + + private static final ChatServerHandler SERVER_HANDLER = new ChatServerHandler(); + + @Override + public void initChannel(SocketChannel ch) throws Exception { + ChannelPipeline pipeline = ch.pipeline(); + // 添加行分割器 + pipeline.addLast(new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter())); + // 添加String Decoder和String Encoder,用来进行字符串的转换 + pipeline.addLast(DECODER); + pipeline.addLast(ENCODER); + // 最后添加真正的处理器 + pipeline.addLast(SERVER_HANDLER); + } +} diff --git a/learn-netty4/src/main/java/com/flydean11/udp/UDPClient.java b/learn-netty4/src/main/java/com/flydean11/udp/UDPClient.java new file mode 100644 index 0000000..d94a260 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean11/udp/UDPClient.java @@ -0,0 +1,63 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean11.udp; + +import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.DatagramPacket; +import io.netty.channel.socket.nio.NioDatagramChannel; +import io.netty.util.CharsetUtil; +import io.netty.util.internal.SocketUtils; +import lombok.extern.slf4j.Slf4j; + +/** + * UDP client + */ +@Slf4j +public final class UDPClient { + + static final int PORT = Integer.parseInt(System.getProperty("port", "8000")); + + public static void main(String[] args) throws Exception { + + EventLoopGroup group = new NioEventLoopGroup(); + try { + Bootstrap b = new Bootstrap(); + b.group(group) + .channel(NioDatagramChannel.class) + .option(ChannelOption.SO_BROADCAST, true) + .handler(new UDPClientHandler()); + + Channel ch = b.bind(0).sync().channel(); + + // 将消息广播给UDP服务器 + ch.writeAndFlush(new DatagramPacket( + Unpooled.copiedBuffer("开始广播", CharsetUtil.UTF_8), + SocketUtils.socketAddress("255.255.255.255", PORT))).sync(); + + // 等待channel关闭,如果Channel没在5秒钟之内关闭,则打印异常 + if (!ch.closeFuture().await(5000)) { + log.info("channel没在5秒内关闭!"); + } + } finally { + group.shutdownGracefully(); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean11/udp/UDPClientHandler.java b/learn-netty4/src/main/java/com/flydean11/udp/UDPClientHandler.java new file mode 100644 index 0000000..4688cba --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean11/udp/UDPClientHandler.java @@ -0,0 +1,41 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean11.udp; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.channel.socket.DatagramPacket; +import io.netty.util.CharsetUtil; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class UDPClientHandler extends SimpleChannelInboundHandler { + + @Override + public void channelRead0(ChannelHandlerContext ctx, DatagramPacket msg) throws Exception { + String response = msg.content().toString(CharsetUtil.UTF_8); + if (response.startsWith("广播: ")) { + log.info("收到广播: {}", response.substring(4)); + ctx.close(); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + cause.printStackTrace(); + ctx.close(); + } +} diff --git a/learn-netty4/src/main/java/com/flydean11/udp/UDPServer.java b/learn-netty4/src/main/java/com/flydean11/udp/UDPServer.java new file mode 100644 index 0000000..0470913 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean11/udp/UDPServer.java @@ -0,0 +1,45 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean11.udp; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioDatagramChannel; + +/** + * UDP 服务器 + */ +public final class UDPServer { + + private static final int PORT = Integer.parseInt(System.getProperty("port", "8000")); + + public static void main(String[] args) throws Exception { + EventLoopGroup group = new NioEventLoopGroup(); + try { + Bootstrap b = new Bootstrap(); + b.group(group) + .channel(NioDatagramChannel.class) + .option(ChannelOption.SO_BROADCAST, true) + .handler(new UDPServerHandler()); + + b.bind(PORT).sync().channel().closeFuture().await(); + } finally { + group.shutdownGracefully(); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean11/udp/UDPServerHandler.java b/learn-netty4/src/main/java/com/flydean11/udp/UDPServerHandler.java new file mode 100644 index 0000000..31123e5 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean11/udp/UDPServerHandler.java @@ -0,0 +1,67 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean11.udp; + +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.channel.socket.DatagramPacket; +import io.netty.util.CharsetUtil; +import lombok.extern.slf4j.Slf4j; + +import java.util.Random; + +@Slf4j +public class UDPServerHandler extends SimpleChannelInboundHandler { + + private static final Random random = new Random(); + + // 广播消息 + private static final String[] quotes = { + "鹅鹅鹅", + "曲项向天歌", + "白毛浮绿水", + "红掌拨清波", + }; + + private static String nextQuote() { + int quoteId; + synchronized (random) { + quoteId = random.nextInt(quotes.length); + } + return quotes[quoteId]; + } + + @Override + public void channelRead0(ChannelHandlerContext ctx, DatagramPacket packet) throws Exception { + log.info("服务器收到消息:{}",packet); + if ("开始广播".equals(packet.content().toString(CharsetUtil.UTF_8))) { + ctx.write(new DatagramPacket( + Unpooled.copiedBuffer("广播: " + nextQuote(), CharsetUtil.UTF_8), packet.sender())); + } + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + ctx.flush(); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + // 异常处理 + log.error("出现异常",cause); + } +} diff --git a/learn-netty4/src/main/java/com/flydean12/securechat/SecureChatClient.java b/learn-netty4/src/main/java/com/flydean12/securechat/SecureChatClient.java new file mode 100644 index 0000000..1623c33 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean12/securechat/SecureChatClient.java @@ -0,0 +1,79 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean12.securechat; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; + +import java.io.BufferedReader; +import java.io.InputStreamReader; + +/** + * SSL 聊天室客户端 + */ +public final class SecureChatClient { + + static final String HOST = System.getProperty("host", "127.0.0.1"); + static final int PORT = Integer.parseInt(System.getProperty("port", "8000")); + + public static void main(String[] args) throws Exception { + // 配置 SSL. + final SslContext sslCtx = SslContextBuilder.forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE).build(); + + EventLoopGroup group = new NioEventLoopGroup(); + try { + Bootstrap b = new Bootstrap(); + b.group(group) + .channel(NioSocketChannel.class) + .handler(new SecureChatClientInitializer(sslCtx)); + + // 建立连接 + Channel ch = b.connect(HOST, PORT).sync().channel(); + + // 从命令行输入 + ChannelFuture lastWriteFuture = null; + BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); + for (;;) { + String line = in.readLine(); + if (line == null) { + break; + } + // 将从命令行输入的一行字符写到channel中 + lastWriteFuture = ch.writeAndFlush(line + "\r\n"); + // 如果输入'再见',则等待server端关闭channel + if ("再见".equalsIgnoreCase(line)) { + ch.closeFuture().sync(); + break; + } + } + + // 等待所有的消息都写入channel中 + if (lastWriteFuture != null) { + lastWriteFuture.sync(); + } + } finally { + group.shutdownGracefully(); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean12/securechat/SecureChatClient2.java b/learn-netty4/src/main/java/com/flydean12/securechat/SecureChatClient2.java new file mode 100644 index 0000000..badc6c7 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean12/securechat/SecureChatClient2.java @@ -0,0 +1,79 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean12.securechat; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; + +import java.io.BufferedReader; +import java.io.InputStreamReader; + +/** + * SSL 聊天室客户端 + */ +public final class SecureChatClient2 { + + static final String HOST = System.getProperty("host", "127.0.0.1"); + static final int PORT = Integer.parseInt(System.getProperty("port", "8000")); + + public static void main(String[] args) throws Exception { + // 配置 SSL. + final SslContext sslCtx = SslContextBuilder.forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE).build(); + + EventLoopGroup group = new NioEventLoopGroup(); + try { + Bootstrap b = new Bootstrap(); + b.group(group) + .channel(NioSocketChannel.class) + .handler(new SecureChatClientInitializer(sslCtx)); + + // 建立连接 + Channel ch = b.connect(HOST, PORT).sync().channel(); + + // 从命令行输入 + ChannelFuture lastWriteFuture = null; + BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); + for (;;) { + String line = in.readLine(); + if (line == null) { + break; + } + // 将从命令行输入的一行字符写到channel中 + lastWriteFuture = ch.writeAndFlush(line + "\r\n"); + // 如果输入'再见',则等待server端关闭channel + if ("再见".equalsIgnoreCase(line)) { + ch.closeFuture().sync(); + break; + } + } + + // 等待所有的消息都写入channel中 + if (lastWriteFuture != null) { + lastWriteFuture.sync(); + } + } finally { + group.shutdownGracefully(); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean12/securechat/SecureChatClientHandler.java b/learn-netty4/src/main/java/com/flydean12/securechat/SecureChatClientHandler.java new file mode 100644 index 0000000..c74b606 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean12/securechat/SecureChatClientHandler.java @@ -0,0 +1,41 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean12.securechat; + +import io.netty.channel.ChannelHandler.Sharable; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import lombok.extern.slf4j.Slf4j; + +/** + * 客户端处理器 + */ +@Sharable +@Slf4j +public class SecureChatClientHandler extends SimpleChannelInboundHandler { + + @Override + protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception { + log.info("接收到消息:{}",msg); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + // 异常处理 + log.error("出现异常",cause); + ctx.close(); + } +} diff --git a/learn-netty4/src/main/java/com/flydean12/securechat/SecureChatClientInitializer.java b/learn-netty4/src/main/java/com/flydean12/securechat/SecureChatClientInitializer.java new file mode 100644 index 0000000..a5c2b29 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean12/securechat/SecureChatClientInitializer.java @@ -0,0 +1,58 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean12.securechat; + +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.DelimiterBasedFrameDecoder; +import io.netty.handler.codec.Delimiters; +import io.netty.handler.codec.string.StringDecoder; +import io.netty.handler.codec.string.StringEncoder; +import io.netty.handler.ssl.SslContext; + +/** + * 客户端ChannelPipeline的配置 + */ +public class SecureChatClientInitializer extends ChannelInitializer { + + private static final StringDecoder DECODER = new StringDecoder(); + private static final StringEncoder ENCODER = new StringEncoder(); + + private static final SecureChatClientHandler CLIENT_HANDLER = new SecureChatClientHandler(); + + private final SslContext sslCtx; + + public SecureChatClientInitializer(SslContext sslCtx){ + this.sslCtx = sslCtx; + } + + @Override + public void initChannel(SocketChannel ch) { + ChannelPipeline pipeline = ch.pipeline(); + + // 添加SSL处理机制 + pipeline.addLast(sslCtx.newHandler(ch.alloc(), SecureChatClient.HOST, SecureChatClient.PORT)); + + // 添加行分割器 + pipeline.addLast(new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter())); + pipeline.addLast(DECODER); + pipeline.addLast(ENCODER); + + // 添加客户端处理器 + pipeline.addLast(CLIENT_HANDLER); + } +} diff --git a/learn-netty4/src/main/java/com/flydean12/securechat/SecureChatServer.java b/learn-netty4/src/main/java/com/flydean12/securechat/SecureChatServer.java new file mode 100644 index 0000000..725f83d --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean12/securechat/SecureChatServer.java @@ -0,0 +1,55 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean12.securechat; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.util.SelfSignedCertificate; + +/** + * telnet 服务器 + */ +public final class SecureChatServer { + + static final int PORT = Integer.parseInt(System.getProperty("port", "8000")); + + public static void main(String[] args) throws Exception { + SelfSignedCertificate ssc = new SelfSignedCertificate(); + SslContext sslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) + .build(); + + EventLoopGroup bossGroup = new NioEventLoopGroup(1); + EventLoopGroup workerGroup = new NioEventLoopGroup(); + try { + ServerBootstrap b = new ServerBootstrap(); + b.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new SecureChatServerInitializer(sslCtx)); + + b.bind(PORT).sync().channel().closeFuture().sync(); + } finally { + bossGroup.shutdownGracefully(); + workerGroup.shutdownGracefully(); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean12/securechat/SecureChatServerHandler.java b/learn-netty4/src/main/java/com/flydean12/securechat/SecureChatServerHandler.java new file mode 100644 index 0000000..1fa35e4 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean12/securechat/SecureChatServerHandler.java @@ -0,0 +1,100 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean12.securechat; + +import io.netty.channel.*; +import io.netty.channel.ChannelHandler.Sharable; +import io.netty.channel.group.ChannelGroup; +import io.netty.channel.group.DefaultChannelGroup; +import io.netty.handler.ssl.SslHandler; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.GenericFutureListener; +import io.netty.util.concurrent.GlobalEventExecutor; +import lombok.extern.slf4j.Slf4j; + +import java.net.InetAddress; + +/** + * server端的处理器 + */ +@Sharable +@Slf4j +public class SecureChatServerHandler extends SimpleChannelInboundHandler { + //一个全局共享的channel group + static final ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE); + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + // channel活跃 + ctx.write("Channel Active状态!\r\n"); + + ctx.pipeline().get(SslHandler.class).handshakeFuture().addListener( + (GenericFutureListener>) future -> { + ctx.writeAndFlush( + "欢迎你: " + InetAddress.getLocalHost().getHostName() + " !\n"); + ctx.writeAndFlush( + "从现在起,你的会话将使用: " + + ctx.pipeline().get(SslHandler.class).engine().getSession().getCipherSuite() + + " 进行加密保护.\n"); + // 将新创建的channel添加到全局的channel group中,用于后续的消息广播 + channels.add(ctx.channel()); + }); + } + + @Override + public void channelRead0(ChannelHandlerContext ctx, String message) throws Exception { + // 将一个客户端的消息广播到所有的channel中 + for (Channel c: channels) { + if (c != ctx.channel()) { + c.writeAndFlush( ctx.channel().remoteAddress() + "说: " + message + '\n'); + } else { + c.writeAndFlush("你是不是说: " + message + '\n'); + } + } + + // 如果读取到"再见"就关闭channel + String response; + // 判断是否关闭 + boolean close = false; + if (message.isEmpty()) { + response = "你说啥?\r\n"; + } else if ("再见".equalsIgnoreCase(message)) { + response = "再见,我的朋友!\r\n"; + close = true; + } else { + response = "你是不是说: '" + message + "'?\r\n"; + } + + // 写入消息 + ChannelFuture future = ctx.write(response); + // 添加CLOSE listener,用来关闭channel + if (close) { + future.addListener(ChannelFutureListener.CLOSE); + } + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + ctx.flush(); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + // 异常处理 + log.error("出现异常",cause); + ctx.close(); + } +} diff --git a/learn-netty4/src/main/java/com/flydean12/securechat/SecureChatServerInitializer.java b/learn-netty4/src/main/java/com/flydean12/securechat/SecureChatServerInitializer.java new file mode 100644 index 0000000..fa1de57 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean12/securechat/SecureChatServerInitializer.java @@ -0,0 +1,57 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean12.securechat; + +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.DelimiterBasedFrameDecoder; +import io.netty.handler.codec.Delimiters; +import io.netty.handler.codec.string.StringDecoder; +import io.netty.handler.codec.string.StringEncoder; +import io.netty.handler.ssl.SslContext; + +/** + * 初始化一个ChannelPipeline + */ +public class SecureChatServerInitializer extends ChannelInitializer { + + private static final StringDecoder DECODER = new StringDecoder(); + private static final StringEncoder ENCODER = new StringEncoder(); + + private static final SecureChatServerHandler SERVER_HANDLER = new SecureChatServerHandler(); + + private final SslContext sslCtx; + + public SecureChatServerInitializer(SslContext sslCtx){ + this.sslCtx = sslCtx; + } + + @Override + public void initChannel(SocketChannel ch) throws Exception { + ChannelPipeline pipeline = ch.pipeline(); + + //添加SSL处理器 + pipeline.addLast(sslCtx.newHandler(ch.alloc())); + // 添加行分割器 + pipeline.addLast(new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter())); + // 添加String Decoder和String Encoder,用来进行字符串的转换 + pipeline.addLast(DECODER); + pipeline.addLast(ENCODER); + // 最后添加真正的处理器 + pipeline.addLast(SERVER_HANDLER); + } +} diff --git a/learn-netty4/src/main/java/com/flydean13/customprotocol/CustomProtocolClient.java b/learn-netty4/src/main/java/com/flydean13/customprotocol/CustomProtocolClient.java new file mode 100644 index 0000000..734b53a --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean13/customprotocol/CustomProtocolClient.java @@ -0,0 +1,56 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean13.customprotocol; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioSocketChannel; +import lombok.extern.slf4j.Slf4j; + +/** + * 自定义协议客户端 + */ +@Slf4j +public final class CustomProtocolClient { + + static final String HOST = System.getProperty("host", "127.0.0.1"); + static final int PORT = Integer.parseInt(System.getProperty("port", "8000")); + static final int COUNT = Integer.parseInt(System.getProperty("count", "100")); + + public static void main(String[] args) throws Exception { + EventLoopGroup group = new NioEventLoopGroup(); + try { + Bootstrap b = new Bootstrap(); + b.group(group) + .channel(NioSocketChannel.class) + .handler(new CustomProtocolClientInitializer()); + + // 建立连接 + ChannelFuture f = b.connect(HOST, PORT).sync(); + + // 获取自定义handler + CustomProtocolClientHandler handler = + (CustomProtocolClientHandler) f.channel().pipeline().last(); + + // 打印结果 + log.info("2的{}次方是:{}",COUNT, handler.getResult()); + } finally { + group.shutdownGracefully(); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean13/customprotocol/CustomProtocolClientHandler.java b/learn-netty4/src/main/java/com/flydean13/customprotocol/CustomProtocolClientHandler.java new file mode 100644 index 0000000..8785a42 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean13/customprotocol/CustomProtocolClientHandler.java @@ -0,0 +1,103 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean13.customprotocol; + +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import lombok.extern.slf4j.Slf4j; + +import java.math.BigInteger; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +/** + * 客户端的handler + */ +@Slf4j +public class CustomProtocolClientHandler extends SimpleChannelInboundHandler { + + private ChannelHandlerContext ctx; + private int receivedMessages; + private int next = 1; + final BlockingQueue answer = new LinkedBlockingQueue<>(); + + public BigInteger getResult() { + boolean interrupted = false; + try { + for (;;) { + try { + return answer.take(); + } catch (InterruptedException ignore) { + interrupted = true; + } + } + } finally { + if (interrupted) { + Thread.currentThread().interrupt(); + } + } + } + + @Override + public void channelActive(ChannelHandlerContext ctx) { + this.ctx = ctx; + sendNumbers(); + } + + @Override + public void channelRead0(ChannelHandlerContext ctx, final BigInteger msg) { + receivedMessages ++; + if (receivedMessages == CustomProtocolClient.COUNT) { + // 计算完毕,将结果放入answer中 + ctx.channel().close().addListener(future -> { + boolean offered = answer.offer(msg); + assert offered; + }); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + // 异常处理 + log.error("出现异常",cause); + ctx.close(); + } + + private void sendNumbers() { + // 最大计算2的1000次方 + ChannelFuture future = null; + for (int i = 0; i < 1000 && next <= CustomProtocolClient.COUNT; i++) { + future = ctx.write(2); + next++; + } + if (next <= CustomProtocolClient.COUNT) { + assert future != null; + future.addListener(numberSender); + } + ctx.flush(); + } + + private final ChannelFutureListener numberSender = future -> { + if (future.isSuccess()) { + sendNumbers(); + } else { + future.cause().printStackTrace(); + future.channel().close(); + } + }; +} diff --git a/learn-netty4/src/main/java/com/flydean13/customprotocol/CustomProtocolClientInitializer.java b/learn-netty4/src/main/java/com/flydean13/customprotocol/CustomProtocolClientInitializer.java new file mode 100644 index 0000000..f3f562e --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean13/customprotocol/CustomProtocolClientInitializer.java @@ -0,0 +1,44 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean13.customprotocol; + +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.compression.ZlibCodecFactory; +import io.netty.handler.codec.compression.ZlibWrapper; + +/** + * 客户端pipeline初始化器 + */ +public class CustomProtocolClientInitializer extends ChannelInitializer { + + @Override + public void initChannel(SocketChannel ch) { + ChannelPipeline pipeline = ch.pipeline(); + + // stream压缩 + pipeline.addLast(ZlibCodecFactory.newZlibEncoder(ZlibWrapper.GZIP)); + pipeline.addLast(ZlibCodecFactory.newZlibDecoder(ZlibWrapper.GZIP)); + + // 添加Number编码器 + pipeline.addLast(new NumberDecoder()); + pipeline.addLast(new NumberEncoder()); + + // 最后添加业务逻辑 + pipeline.addLast(new CustomProtocolClientHandler()); + } +} diff --git a/learn-netty4/src/main/java/com/flydean13/customprotocol/CustomProtocolServer.java b/learn-netty4/src/main/java/com/flydean13/customprotocol/CustomProtocolServer.java new file mode 100644 index 0000000..c3bf2f3 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean13/customprotocol/CustomProtocolServer.java @@ -0,0 +1,48 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean13.customprotocol; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; + +/** + * 自定义协议服务器 + */ +public final class CustomProtocolServer { + + static final int PORT = Integer.parseInt(System.getProperty("port", "8000")); + + public static void main(String[] args) throws Exception { + EventLoopGroup bossGroup = new NioEventLoopGroup(1); + EventLoopGroup workerGroup = new NioEventLoopGroup(); + try { + ServerBootstrap b = new ServerBootstrap(); + b.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new CustomProtocolServerInitializer()); + + b.bind(PORT).sync().channel().closeFuture().sync(); + } finally { + bossGroup.shutdownGracefully(); + workerGroup.shutdownGracefully(); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean13/customprotocol/CustomProtocolServerHandler.java b/learn-netty4/src/main/java/com/flydean13/customprotocol/CustomProtocolServerHandler.java new file mode 100644 index 0000000..44ca3f0 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean13/customprotocol/CustomProtocolServerHandler.java @@ -0,0 +1,52 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean13.customprotocol; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import lombok.extern.slf4j.Slf4j; + +import java.math.BigInteger; + +/** + * 自定义协议处理器 + */ +@Slf4j +public class CustomProtocolServerHandler extends SimpleChannelInboundHandler { + + private int count = 0; + private BigInteger result=BigInteger.valueOf(1); + + @Override + public void channelRead0(ChannelHandlerContext ctx, BigInteger msg) throws Exception { + // 将接收到的msg乘以2,然后返回给客户端 + count++; + result = result.multiply(msg); + ctx.writeAndFlush(result); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + log.info("2的{}次方是{}",count,result); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + // 异常处理 + log.error("出现异常",cause); + ctx.close(); + } +} diff --git a/learn-netty4/src/main/java/com/flydean13/customprotocol/CustomProtocolServerInitializer.java b/learn-netty4/src/main/java/com/flydean13/customprotocol/CustomProtocolServerInitializer.java new file mode 100644 index 0000000..decb4a0 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean13/customprotocol/CustomProtocolServerInitializer.java @@ -0,0 +1,44 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean13.customprotocol; + +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.compression.ZlibCodecFactory; +import io.netty.handler.codec.compression.ZlibWrapper; + +/** + * 服务器端pipeline初始化 + */ +public class CustomProtocolServerInitializer extends ChannelInitializer { + + @Override + public void initChannel(SocketChannel ch) { + ChannelPipeline pipeline = ch.pipeline(); + + // 对流进行压缩 + pipeline.addLast(ZlibCodecFactory.newZlibEncoder(ZlibWrapper.GZIP)); + pipeline.addLast(ZlibCodecFactory.newZlibDecoder(ZlibWrapper.GZIP)); + + // 添加number编码解码器 + pipeline.addLast(new NumberDecoder()); + pipeline.addLast(new NumberEncoder()); + + // 添加业务处理逻辑 + pipeline.addLast(new CustomProtocolServerHandler()); + } +} diff --git a/learn-netty4/src/main/java/com/flydean13/customprotocol/NumberDecoder.java b/learn-netty4/src/main/java/com/flydean13/customprotocol/NumberDecoder.java new file mode 100644 index 0000000..eafe831 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean13/customprotocol/NumberDecoder.java @@ -0,0 +1,55 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean13.customprotocol; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToMessageDecoder; +import io.netty.handler.codec.CorruptedFrameException; + +import java.math.BigInteger; +import java.util.List; + +/** + * 将编码过后的byte[] 转换成BigInteger对象 + */ +public class NumberDecoder extends ByteToMessageDecoder { + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) { + // 保证魔法词和数组长度有效 + if (in.readableBytes() < 5) { + return; + } + in.markReaderIndex(); + // 检查魔法词 + int magicNumber = in.readUnsignedByte(); + if (magicNumber != 'N') { + in.resetReaderIndex(); + throw new CorruptedFrameException("无效的魔法词: " + magicNumber); + } + // 读取所有的数据 + int dataLength = in.readInt(); + if (in.readableBytes() < dataLength) { + in.resetReaderIndex(); + return; + } + // 将剩下的数据转换成为BigInteger + byte[] decoded = new byte[dataLength]; + in.readBytes(decoded); + out.add(new BigInteger(decoded)); + } +} diff --git a/learn-netty4/src/main/java/com/flydean13/customprotocol/NumberEncoder.java b/learn-netty4/src/main/java/com/flydean13/customprotocol/NumberEncoder.java new file mode 100644 index 0000000..beca58e --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean13/customprotocol/NumberEncoder.java @@ -0,0 +1,50 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean13.customprotocol; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToByteEncoder; + +import java.math.BigInteger; + +/** + * 将Number编码成byte[]格式,第一个byte表示魔法词"N", + * 接下来的4个byte代表数组的长度 + * 最后的才是真正的数字 + */ +public class NumberEncoder extends MessageToByteEncoder { + + @Override + protected void encode(ChannelHandlerContext ctx, Number msg, ByteBuf out) { + // 将number编码成为ByteBuf + BigInteger v; + if (msg instanceof BigInteger) { + v = (BigInteger) msg; + } else { + v = new BigInteger(String.valueOf(msg)); + } + + // 将BigInteger转换成为byte[]数组 + byte[] data = v.toByteArray(); + int dataLength = data.length; + + // 将Number进行编码 + out.writeByte((byte) 'N'); // 魔法词 + out.writeInt(dataLength); // 数组长度 + out.writeBytes(data); // 最终的数据 + } +} diff --git a/learn-netty4/src/main/java/com/flydean14/custcodec/IntegerEncoder.java b/learn-netty4/src/main/java/com/flydean14/custcodec/IntegerEncoder.java new file mode 100644 index 0000000..62ed919 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean14/custcodec/IntegerEncoder.java @@ -0,0 +1,32 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean14.custcodec; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToByteEncoder; + +/** + * @author wayne + * @version IntegerEncoder, 2021/8/13 + */ +public class IntegerEncoder extends MessageToByteEncoder { + @Override + public void encode(ChannelHandlerContext ctx, Integer msg, ByteBuf out) + throws Exception { + out.writeInt(msg); + } +} \ No newline at end of file diff --git a/learn-netty4/src/main/java/com/flydean14/custcodec/IntegerHeaderFrameDecoder.java b/learn-netty4/src/main/java/com/flydean14/custcodec/IntegerHeaderFrameDecoder.java new file mode 100644 index 0000000..e4962ea --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean14/custcodec/IntegerHeaderFrameDecoder.java @@ -0,0 +1,48 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean14.custcodec; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToMessageDecoder; + +import java.util.List; + +/** + * @author wayne + * @version IntegerHeaderFrameDecoder, 2021/8/13 + */ +public class IntegerHeaderFrameDecoder extends ByteToMessageDecoder { + + @Override + protected void decode(ChannelHandlerContext ctx, + ByteBuf buf, List out) throws Exception { + + if (buf.readableBytes() < 4) { + return; + } + + buf.markReaderIndex(); + int length = buf.readInt(); + + if (buf.readableBytes() < length) { + buf.resetReaderIndex(); + return; + } + + out.add(buf.readBytes(length)); + } +} diff --git a/learn-netty4/src/main/java/com/flydean14/custcodec/IntegerHeaderFrameDecoderReplay.java b/learn-netty4/src/main/java/com/flydean14/custcodec/IntegerHeaderFrameDecoderReplay.java new file mode 100644 index 0000000..43459b3 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean14/custcodec/IntegerHeaderFrameDecoderReplay.java @@ -0,0 +1,35 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean14.custcodec; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ReplayingDecoder; + +import java.util.List; + +/** + * @author wayne + * @version IntegerHeaderFrameDecoderReplay, 2021/8/13 + */ +public class IntegerHeaderFrameDecoderReplay extends ReplayingDecoder { + + protected void decode(ChannelHandlerContext ctx, + ByteBuf buf, List out) throws Exception { + + out.add(buf.readBytes(buf.readInt())); + } +} diff --git a/learn-netty4/src/main/java/com/flydean14/custcodec/IntegerHeaderFrameDecoderSteps.java b/learn-netty4/src/main/java/com/flydean14/custcodec/IntegerHeaderFrameDecoderSteps.java new file mode 100644 index 0000000..f5ce63e --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean14/custcodec/IntegerHeaderFrameDecoderSteps.java @@ -0,0 +1,53 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean14.custcodec; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ReplayingDecoder; + +import java.util.List; + +/** + * @author wayne + * @version IntegerHeaderFrameDecoderSteps, 2021/8/13 + */ +public class IntegerHeaderFrameDecoderSteps extends ReplayingDecoder { + + private int length; + + public IntegerHeaderFrameDecoderSteps() { + // Set the initial state. + super(MyDecoderState.READ_LENGTH); + } + + @Override + protected void decode(ChannelHandlerContext ctx, + ByteBuf buf, List out) throws Exception { + switch (state()) { + case READ_LENGTH: + length = buf.readInt(); + checkpoint(MyDecoderState.READ_CONTENT); + case READ_CONTENT: + ByteBuf frame = buf.readBytes(length); + checkpoint(MyDecoderState.READ_LENGTH); + out.add(frame); + break; + default: + throw new Error("Shouldn't reach here."); + } + } +} \ No newline at end of file diff --git a/learn-netty4/src/main/java/com/flydean14/custcodec/IntegerToStringEncoder.java b/learn-netty4/src/main/java/com/flydean14/custcodec/IntegerToStringEncoder.java new file mode 100644 index 0000000..a3195d6 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean14/custcodec/IntegerToStringEncoder.java @@ -0,0 +1,35 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean14.custcodec; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToMessageEncoder; + +import java.util.List; + +/** + * @author wayne + * @version IntegerToStringEncoder, 2021/8/13 + */ +public class IntegerToStringEncoder extends + MessageToMessageEncoder { + + @Override + public void encode(ChannelHandlerContext ctx, Integer message, List out) + throws Exception { + out.add(message.toString()); + } +} diff --git a/learn-netty4/src/main/java/com/flydean14/custcodec/MyDecoderState.java b/learn-netty4/src/main/java/com/flydean14/custcodec/MyDecoderState.java new file mode 100644 index 0000000..0fc5f4a --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean14/custcodec/MyDecoderState.java @@ -0,0 +1,25 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean14.custcodec; + +/** + * @author wayne + * @version MyDecoderState, 2021/8/13 + */ +public enum MyDecoderState { + READ_LENGTH, + READ_CONTENT; +} diff --git a/learn-netty4/src/main/java/com/flydean14/custcodec/SquareDecoder.java b/learn-netty4/src/main/java/com/flydean14/custcodec/SquareDecoder.java new file mode 100644 index 0000000..98ba0da --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean14/custcodec/SquareDecoder.java @@ -0,0 +1,34 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean14.custcodec; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToMessageDecoder; + +import java.util.List; + +/** + * @author wayne + * @version SquareDecoder, 2021/8/13 + */ +public class SquareDecoder extends ByteToMessageDecoder { + @Override + public void decode(ChannelHandlerContext ctx, ByteBuf in, List out) + throws Exception { + out.add(in.readBytes(in.readableBytes())); + } +} diff --git a/learn-netty4/src/main/java/com/flydean14/custcodec/StringToIntegerDecoder.java b/learn-netty4/src/main/java/com/flydean14/custcodec/StringToIntegerDecoder.java new file mode 100644 index 0000000..360d849 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean14/custcodec/StringToIntegerDecoder.java @@ -0,0 +1,35 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean14.custcodec; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToMessageDecoder; + +import java.util.List; + +/** + * @author wayne + * @version StringToIntegerDecoder, 2021/8/13 + */ +public class StringToIntegerDecoder extends + MessageToMessageDecoder { + + @Override + public void decode(ChannelHandlerContext ctx, String message, + List out) throws Exception { + out.add(message.length()); + } +} \ No newline at end of file diff --git a/learn-netty4/src/main/java/com/flydean17/marshalling/MarshallingReader.java b/learn-netty4/src/main/java/com/flydean17/marshalling/MarshallingReader.java new file mode 100644 index 0000000..2400106 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean17/marshalling/MarshallingReader.java @@ -0,0 +1,50 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean17.marshalling; + +import lombok.extern.slf4j.Slf4j; +import org.jboss.marshalling.MarshallerFactory; +import org.jboss.marshalling.Marshalling; +import org.jboss.marshalling.MarshallingConfiguration; +import org.jboss.marshalling.Unmarshaller; + +import java.io.FileInputStream; +import java.io.IOException; + +@Slf4j +public class MarshallingReader { + + public void marshallingRead(String fileName) throws IOException, ClassNotFoundException { + // 使用river协议创建MarshallerFactory + MarshallerFactory marshallerFactory = Marshalling.getProvidedMarshallerFactory("river"); + // 创建配置文件 + MarshallingConfiguration configuration = new MarshallingConfiguration(); + // 使用版本号4 + configuration.setVersion(4); + final Unmarshaller unmarshaller = marshallerFactory.createUnmarshaller(configuration); + try(FileInputStream is = new FileInputStream(fileName)){ + unmarshaller.start(Marshalling.createByteInput(is)); + Student student=(Student)unmarshaller.readObject(); + log.info("student:{}",student); + unmarshaller.finish(); + } + } + + public static void main(String[] args) throws IOException, ClassNotFoundException { + MarshallingReader reader= new MarshallingReader(); + reader.marshallingRead("/tmp/marshall.txt"); + } +} diff --git a/learn-netty4/src/main/java/com/flydean17/marshalling/MarshallingWriter.java b/learn-netty4/src/main/java/com/flydean17/marshalling/MarshallingWriter.java new file mode 100644 index 0000000..c718949 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean17/marshalling/MarshallingWriter.java @@ -0,0 +1,53 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean17.marshalling; + +import org.jboss.marshalling.*; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Collections; + +public class MarshallingWriter { + + public void marshallingWrite(String fileName, Object obj) throws IOException { + // 使用river作为marshalling的方式 + MarshallerFactory marshallerFactory = Marshalling.getProvidedMarshallerFactory("river"); + // 创建marshalling的配置 + MarshallingConfiguration configuration = new MarshallingConfiguration(); + // 使用版本号4 + configuration.setVersion(4); +// configuration.setClassCount(10); +// configuration.setBufferSize(8096); +// configuration.setInstanceCount(100); +// configuration.setExceptionListener(new MarshallingException()); +// configuration.setClassResolver(new SimpleClassResolver(getClass().getClassLoader())); +// configuration.setObjectPreResolver(new ChainingObjectResolver(Collections.singletonList(new HibernateDetachResolver()))); + final Marshaller marshaller = marshallerFactory.createMarshaller(configuration); + try(FileOutputStream os = new FileOutputStream(fileName)){ + marshaller.start(Marshalling.createByteOutput(os)); + marshaller.writeObject(obj); + marshaller.finish(); + } + } + + public static void main(String[] args) throws IOException { + MarshallingWriter writer = new MarshallingWriter(); + Student student= new Student("jack", 18, "first grade"); + writer.marshallingWrite("/tmp/marshall.txt",student); + } + +} diff --git a/learn-netty4/src/main/java/com/flydean17/marshalling/Student.java b/learn-netty4/src/main/java/com/flydean17/marshalling/Student.java new file mode 100644 index 0000000..1484dd2 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean17/marshalling/Student.java @@ -0,0 +1,31 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean17.marshalling; + +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.io.Serializable; + +@Data +@AllArgsConstructor +public class Student implements Serializable { + + private String name; + private int age; + private String className; + +} diff --git a/learn-netty4/src/main/java/com/flydean17/protobuf/Student.java b/learn-netty4/src/main/java/com/flydean17/protobuf/Student.java new file mode 100644 index 0000000..7cf0407 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean17/protobuf/Student.java @@ -0,0 +1,675 @@ +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: student.proto + +package com.flydean17.protobuf; + +/** + * Protobuf type {@code com.flydean17.protobuf.Student} + */ +public final class Student extends + com.google.protobuf.GeneratedMessageV3 implements + // @@protoc_insertion_point(message_implements:com.flydean17.protobuf.Student) + StudentOrBuilder { +private static final long serialVersionUID = 0L; + // Use Student.newBuilder() to construct. + private Student(com.google.protobuf.GeneratedMessageV3.Builder builder) { + super(builder); + } + private Student() { + name_ = ""; + } + + @java.lang.Override + @SuppressWarnings({"unused"}) + protected java.lang.Object newInstance( + UnusedPrivateParameter unused) { + return new Student(); + } + + @java.lang.Override + public final com.google.protobuf.UnknownFieldSet + getUnknownFields() { + return this.unknownFields; + } + private Student( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + this(); + if (extensionRegistry == null) { + throw new java.lang.NullPointerException(); + } + int mutable_bitField0_ = 0; + com.google.protobuf.UnknownFieldSet.Builder unknownFields = + com.google.protobuf.UnknownFieldSet.newBuilder(); + try { + boolean done = false; + while (!done) { + int tag = input.readTag(); + switch (tag) { + case 0: + done = true; + break; + case 8: { + bitField0_ |= 0x00000001; + age_ = input.readInt32(); + break; + } + case 18: { + java.lang.String s = input.readStringRequireUtf8(); + bitField0_ |= 0x00000002; + name_ = s; + break; + } + default: { + if (!parseUnknownField( + input, unknownFields, extensionRegistry, tag)) { + done = true; + } + break; + } + } + } + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.setUnfinishedMessage(this); + } catch (java.io.IOException e) { + throw new com.google.protobuf.InvalidProtocolBufferException( + e).setUnfinishedMessage(this); + } finally { + this.unknownFields = unknownFields.build(); + makeExtensionsImmutable(); + } + } + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return com.flydean17.protobuf.StudentOuterClass.internal_static_com_flydean17_protobuf_Student_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable + internalGetFieldAccessorTable() { + return com.flydean17.protobuf.StudentOuterClass.internal_static_com_flydean17_protobuf_Student_fieldAccessorTable + .ensureFieldAccessorsInitialized( + com.flydean17.protobuf.Student.class, com.flydean17.protobuf.Student.Builder.class); + } + + private int bitField0_; + public static final int AGE_FIELD_NUMBER = 1; + private int age_; + /** + * int32 age = 1; + * @return Whether the age field is set. + */ + @java.lang.Override + public boolean hasAge() { + return ((bitField0_ & 0x00000001) != 0); + } + /** + * int32 age = 1; + * @return The age. + */ + @java.lang.Override + public int getAge() { + return age_; + } + + public static final int NAME_FIELD_NUMBER = 2; + private volatile java.lang.Object name_; + /** + * string name = 2; + * @return Whether the name field is set. + */ + @java.lang.Override + public boolean hasName() { + return ((bitField0_ & 0x00000002) != 0); + } + /** + * string name = 2; + * @return The name. + */ + @java.lang.Override + public java.lang.String getName() { + java.lang.Object ref = name_; + if (ref instanceof java.lang.String) { + return (java.lang.String) ref; + } else { + com.google.protobuf.ByteString bs = + (com.google.protobuf.ByteString) ref; + java.lang.String s = bs.toStringUtf8(); + name_ = s; + return s; + } + } + /** + * string name = 2; + * @return The bytes for name. + */ + @java.lang.Override + public com.google.protobuf.ByteString + getNameBytes() { + java.lang.Object ref = name_; + if (ref instanceof java.lang.String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (java.lang.String) ref); + name_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + + private byte memoizedIsInitialized = -1; + @java.lang.Override + public final boolean isInitialized() { + byte isInitialized = memoizedIsInitialized; + if (isInitialized == 1) return true; + if (isInitialized == 0) return false; + + memoizedIsInitialized = 1; + return true; + } + + @java.lang.Override + public void writeTo(com.google.protobuf.CodedOutputStream output) + throws java.io.IOException { + if (((bitField0_ & 0x00000001) != 0)) { + output.writeInt32(1, age_); + } + if (((bitField0_ & 0x00000002) != 0)) { + com.google.protobuf.GeneratedMessageV3.writeString(output, 2, name_); + } + unknownFields.writeTo(output); + } + + @java.lang.Override + public int getSerializedSize() { + int size = memoizedSize; + if (size != -1) return size; + + size = 0; + if (((bitField0_ & 0x00000001) != 0)) { + size += com.google.protobuf.CodedOutputStream + .computeInt32Size(1, age_); + } + if (((bitField0_ & 0x00000002) != 0)) { + size += com.google.protobuf.GeneratedMessageV3.computeStringSize(2, name_); + } + size += unknownFields.getSerializedSize(); + memoizedSize = size; + return size; + } + + @java.lang.Override + public boolean equals(final java.lang.Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof com.flydean17.protobuf.Student)) { + return super.equals(obj); + } + com.flydean17.protobuf.Student other = (com.flydean17.protobuf.Student) obj; + + if (hasAge() != other.hasAge()) return false; + if (hasAge()) { + if (getAge() + != other.getAge()) return false; + } + if (hasName() != other.hasName()) return false; + if (hasName()) { + if (!getName() + .equals(other.getName())) return false; + } + if (!unknownFields.equals(other.unknownFields)) return false; + return true; + } + + @java.lang.Override + public int hashCode() { + if (memoizedHashCode != 0) { + return memoizedHashCode; + } + int hash = 41; + hash = (19 * hash) + getDescriptor().hashCode(); + if (hasAge()) { + hash = (37 * hash) + AGE_FIELD_NUMBER; + hash = (53 * hash) + getAge(); + } + if (hasName()) { + hash = (37 * hash) + NAME_FIELD_NUMBER; + hash = (53 * hash) + getName().hashCode(); + } + hash = (29 * hash) + unknownFields.hashCode(); + memoizedHashCode = hash; + return hash; + } + + public static com.flydean17.protobuf.Student parseFrom( + java.nio.ByteBuffer data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static com.flydean17.protobuf.Student parseFrom( + java.nio.ByteBuffer data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static com.flydean17.protobuf.Student parseFrom( + com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static com.flydean17.protobuf.Student parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static com.flydean17.protobuf.Student parseFrom(byte[] data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static com.flydean17.protobuf.Student parseFrom( + byte[] data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static com.flydean17.protobuf.Student parseFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3 + .parseWithIOException(PARSER, input); + } + public static com.flydean17.protobuf.Student parseFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3 + .parseWithIOException(PARSER, input, extensionRegistry); + } + public static com.flydean17.protobuf.Student parseDelimitedFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3 + .parseDelimitedWithIOException(PARSER, input); + } + public static com.flydean17.protobuf.Student parseDelimitedFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3 + .parseDelimitedWithIOException(PARSER, input, extensionRegistry); + } + public static com.flydean17.protobuf.Student parseFrom( + com.google.protobuf.CodedInputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3 + .parseWithIOException(PARSER, input); + } + public static com.flydean17.protobuf.Student parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3 + .parseWithIOException(PARSER, input, extensionRegistry); + } + + @java.lang.Override + public Builder newBuilderForType() { return newBuilder(); } + public static Builder newBuilder() { + return DEFAULT_INSTANCE.toBuilder(); + } + public static Builder newBuilder(com.flydean17.protobuf.Student prototype) { + return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); + } + @java.lang.Override + public Builder toBuilder() { + return this == DEFAULT_INSTANCE + ? new Builder() : new Builder().mergeFrom(this); + } + + @java.lang.Override + protected Builder newBuilderForType( + com.google.protobuf.GeneratedMessageV3.BuilderParent parent) { + Builder builder = new Builder(parent); + return builder; + } + /** + * Protobuf type {@code com.flydean17.protobuf.Student} + */ + public static final class Builder extends + com.google.protobuf.GeneratedMessageV3.Builder implements + // @@protoc_insertion_point(builder_implements:com.flydean17.protobuf.Student) + com.flydean17.protobuf.StudentOrBuilder { + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return com.flydean17.protobuf.StudentOuterClass.internal_static_com_flydean17_protobuf_Student_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable + internalGetFieldAccessorTable() { + return com.flydean17.protobuf.StudentOuterClass.internal_static_com_flydean17_protobuf_Student_fieldAccessorTable + .ensureFieldAccessorsInitialized( + com.flydean17.protobuf.Student.class, com.flydean17.protobuf.Student.Builder.class); + } + + // Construct using com.flydean17.protobuf.Student.newBuilder() + private Builder() { + maybeForceBuilderInitialization(); + } + + private Builder( + com.google.protobuf.GeneratedMessageV3.BuilderParent parent) { + super(parent); + maybeForceBuilderInitialization(); + } + private void maybeForceBuilderInitialization() { + if (com.google.protobuf.GeneratedMessageV3 + .alwaysUseFieldBuilders) { + } + } + @java.lang.Override + public Builder clear() { + super.clear(); + age_ = 0; + bitField0_ = (bitField0_ & ~0x00000001); + name_ = ""; + bitField0_ = (bitField0_ & ~0x00000002); + return this; + } + + @java.lang.Override + public com.google.protobuf.Descriptors.Descriptor + getDescriptorForType() { + return com.flydean17.protobuf.StudentOuterClass.internal_static_com_flydean17_protobuf_Student_descriptor; + } + + @java.lang.Override + public com.flydean17.protobuf.Student getDefaultInstanceForType() { + return com.flydean17.protobuf.Student.getDefaultInstance(); + } + + @java.lang.Override + public com.flydean17.protobuf.Student build() { + com.flydean17.protobuf.Student result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException(result); + } + return result; + } + + @java.lang.Override + public com.flydean17.protobuf.Student buildPartial() { + com.flydean17.protobuf.Student result = new com.flydean17.protobuf.Student(this); + int from_bitField0_ = bitField0_; + int to_bitField0_ = 0; + if (((from_bitField0_ & 0x00000001) != 0)) { + result.age_ = age_; + to_bitField0_ |= 0x00000001; + } + if (((from_bitField0_ & 0x00000002) != 0)) { + to_bitField0_ |= 0x00000002; + } + result.name_ = name_; + result.bitField0_ = to_bitField0_; + onBuilt(); + return result; + } + + @java.lang.Override + public Builder clone() { + return super.clone(); + } + @java.lang.Override + public Builder setField( + com.google.protobuf.Descriptors.FieldDescriptor field, + java.lang.Object value) { + return super.setField(field, value); + } + @java.lang.Override + public Builder clearField( + com.google.protobuf.Descriptors.FieldDescriptor field) { + return super.clearField(field); + } + @java.lang.Override + public Builder clearOneof( + com.google.protobuf.Descriptors.OneofDescriptor oneof) { + return super.clearOneof(oneof); + } + @java.lang.Override + public Builder setRepeatedField( + com.google.protobuf.Descriptors.FieldDescriptor field, + int index, java.lang.Object value) { + return super.setRepeatedField(field, index, value); + } + @java.lang.Override + public Builder addRepeatedField( + com.google.protobuf.Descriptors.FieldDescriptor field, + java.lang.Object value) { + return super.addRepeatedField(field, value); + } + @java.lang.Override + public Builder mergeFrom(com.google.protobuf.Message other) { + if (other instanceof com.flydean17.protobuf.Student) { + return mergeFrom((com.flydean17.protobuf.Student)other); + } else { + super.mergeFrom(other); + return this; + } + } + + public Builder mergeFrom(com.flydean17.protobuf.Student other) { + if (other == com.flydean17.protobuf.Student.getDefaultInstance()) return this; + if (other.hasAge()) { + setAge(other.getAge()); + } + if (other.hasName()) { + bitField0_ |= 0x00000002; + name_ = other.name_; + onChanged(); + } + this.mergeUnknownFields(other.unknownFields); + onChanged(); + return this; + } + + @java.lang.Override + public final boolean isInitialized() { + return true; + } + + @java.lang.Override + public Builder mergeFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + com.flydean17.protobuf.Student parsedMessage = null; + try { + parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry); + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + parsedMessage = (com.flydean17.protobuf.Student) e.getUnfinishedMessage(); + throw e.unwrapIOException(); + } finally { + if (parsedMessage != null) { + mergeFrom(parsedMessage); + } + } + return this; + } + private int bitField0_; + + private int age_ ; + /** + * int32 age = 1; + * @return Whether the age field is set. + */ + @java.lang.Override + public boolean hasAge() { + return ((bitField0_ & 0x00000001) != 0); + } + /** + * int32 age = 1; + * @return The age. + */ + @java.lang.Override + public int getAge() { + return age_; + } + /** + * int32 age = 1; + * @param value The age to set. + * @return This builder for chaining. + */ + public Builder setAge(int value) { + bitField0_ |= 0x00000001; + age_ = value; + onChanged(); + return this; + } + /** + * int32 age = 1; + * @return This builder for chaining. + */ + public Builder clearAge() { + bitField0_ = (bitField0_ & ~0x00000001); + age_ = 0; + onChanged(); + return this; + } + + private java.lang.Object name_ = ""; + /** + * string name = 2; + * @return Whether the name field is set. + */ + public boolean hasName() { + return ((bitField0_ & 0x00000002) != 0); + } + /** + * string name = 2; + * @return The name. + */ + public java.lang.String getName() { + java.lang.Object ref = name_; + if (!(ref instanceof java.lang.String)) { + com.google.protobuf.ByteString bs = + (com.google.protobuf.ByteString) ref; + java.lang.String s = bs.toStringUtf8(); + name_ = s; + return s; + } else { + return (java.lang.String) ref; + } + } + /** + * string name = 2; + * @return The bytes for name. + */ + public com.google.protobuf.ByteString + getNameBytes() { + java.lang.Object ref = name_; + if (ref instanceof String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (java.lang.String) ref); + name_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + /** + * string name = 2; + * @param value The name to set. + * @return This builder for chaining. + */ + public Builder setName( + java.lang.String value) { + if (value == null) { + throw new NullPointerException(); + } + bitField0_ |= 0x00000002; + name_ = value; + onChanged(); + return this; + } + /** + * string name = 2; + * @return This builder for chaining. + */ + public Builder clearName() { + bitField0_ = (bitField0_ & ~0x00000002); + name_ = getDefaultInstance().getName(); + onChanged(); + return this; + } + /** + * string name = 2; + * @param value The bytes for name to set. + * @return This builder for chaining. + */ + public Builder setNameBytes( + com.google.protobuf.ByteString value) { + if (value == null) { + throw new NullPointerException(); + } + checkByteStringIsUtf8(value); + bitField0_ |= 0x00000002; + name_ = value; + onChanged(); + return this; + } + @java.lang.Override + public final Builder setUnknownFields( + final com.google.protobuf.UnknownFieldSet unknownFields) { + return super.setUnknownFields(unknownFields); + } + + @java.lang.Override + public final Builder mergeUnknownFields( + final com.google.protobuf.UnknownFieldSet unknownFields) { + return super.mergeUnknownFields(unknownFields); + } + + + // @@protoc_insertion_point(builder_scope:com.flydean17.protobuf.Student) + } + + // @@protoc_insertion_point(class_scope:com.flydean17.protobuf.Student) + private static final com.flydean17.protobuf.Student DEFAULT_INSTANCE; + static { + DEFAULT_INSTANCE = new com.flydean17.protobuf.Student(); + } + + public static com.flydean17.protobuf.Student getDefaultInstance() { + return DEFAULT_INSTANCE; + } + + private static final com.google.protobuf.Parser + PARSER = new com.google.protobuf.AbstractParser() { + @java.lang.Override + public Student parsePartialFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return new Student(input, extensionRegistry); + } + }; + + public static com.google.protobuf.Parser parser() { + return PARSER; + } + + @java.lang.Override + public com.google.protobuf.Parser getParserForType() { + return PARSER; + } + + @java.lang.Override + public com.flydean17.protobuf.Student getDefaultInstanceForType() { + return DEFAULT_INSTANCE; + } + +} + diff --git a/learn-netty4/src/main/java/com/flydean17/protobuf/StudentClient.java b/learn-netty4/src/main/java/com/flydean17/protobuf/StudentClient.java new file mode 100644 index 0000000..e9c02e6 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean17/protobuf/StudentClient.java @@ -0,0 +1,52 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean17.protobuf; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.Channel; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioSocketChannel; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +/** + * 发送一个Student的对象到server端 + */ +@Slf4j +public final class StudentClient { + + static final String HOST = System.getProperty("host", "127.0.0.1"); + static final int PORT = Integer.parseInt(System.getProperty("port", "8000")); + + public static void main(String[] args) throws Exception { + + EventLoopGroup group = new NioEventLoopGroup(); + try { + Bootstrap b = new Bootstrap(); + b.group(group) + .channel(NioSocketChannel.class) + .handler(new StudentClientInitializer()); + // 建立连接 + Channel ch = b.connect(HOST, PORT).sync().channel(); + // 等待关闭 + ch.closeFuture().sync(); + } finally { + group.shutdownGracefully(); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean17/protobuf/StudentClientHandler.java b/learn-netty4/src/main/java/com/flydean17/protobuf/StudentClientHandler.java new file mode 100644 index 0000000..e07532c --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean17/protobuf/StudentClientHandler.java @@ -0,0 +1,46 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean17.protobuf; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class StudentClientHandler extends SimpleChannelInboundHandler { + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + // channel活跃 + //构建一个Student,并将其写入到channel中 + Student student= Student.newBuilder().setAge(22).setName("flydean").build(); + log.info("client发送消息{}",student); + ctx.write(student); + ctx.flush(); + } + + @Override + public void channelRead0(ChannelHandlerContext ctx, Student student) throws Exception { + log.info("client收到消息{}",student); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + // 异常处理 + log.error("出现异常",cause); + ctx.close(); + } +} diff --git a/learn-netty4/src/main/java/com/flydean17/protobuf/StudentClientInitializer.java b/learn-netty4/src/main/java/com/flydean17/protobuf/StudentClientInitializer.java new file mode 100644 index 0000000..bd52ada --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean17/protobuf/StudentClientInitializer.java @@ -0,0 +1,40 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean17.protobuf; + +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.protobuf.ProtobufDecoder; +import io.netty.handler.codec.protobuf.ProtobufEncoder; +import io.netty.handler.codec.protobuf.ProtobufVarint32FrameDecoder; +import io.netty.handler.codec.protobuf.ProtobufVarint32LengthFieldPrepender; + +public class StudentClientInitializer extends ChannelInitializer { + + @Override + public void initChannel(SocketChannel ch) { + ChannelPipeline p = ch.pipeline(); + + p.addLast(new ProtobufVarint32FrameDecoder()); + p.addLast(new ProtobufDecoder(Student.getDefaultInstance())); + + p.addLast(new ProtobufVarint32LengthFieldPrepender()); + p.addLast(new ProtobufEncoder()); + + p.addLast(new StudentClientHandler()); + } +} diff --git a/learn-netty4/src/main/java/com/flydean17/protobuf/StudentOrBuilder.java b/learn-netty4/src/main/java/com/flydean17/protobuf/StudentOrBuilder.java new file mode 100644 index 0000000..4a15acb --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean17/protobuf/StudentOrBuilder.java @@ -0,0 +1,49 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean17.protobuf; + +public interface StudentOrBuilder extends + // @@protoc_insertion_point(interface_extends:com.flydean17.protobuf.Student) + com.google.protobuf.MessageOrBuilder { + + /** + * int32 age = 1; + * @return Whether the age field is set. + */ + boolean hasAge(); + /** + * int32 age = 1; + * @return The age. + */ + int getAge(); + + /** + * string name = 2; + * @return Whether the name field is set. + */ + boolean hasName(); + /** + * string name = 2; + * @return The name. + */ + java.lang.String getName(); + /** + * string name = 2; + * @return The bytes for name. + */ + com.google.protobuf.ByteString + getNameBytes(); +} diff --git a/learn-netty4/src/main/java/com/flydean17/protobuf/StudentOuterClass.java b/learn-netty4/src/main/java/com/flydean17/protobuf/StudentOuterClass.java new file mode 100644 index 0000000..77e5155 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean17/protobuf/StudentOuterClass.java @@ -0,0 +1,61 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean17.protobuf; + +public final class StudentOuterClass { + private StudentOuterClass() {} + public static void registerAllExtensions( + com.google.protobuf.ExtensionRegistryLite registry) { + } + + public static void registerAllExtensions( + com.google.protobuf.ExtensionRegistry registry) { + registerAllExtensions( + (com.google.protobuf.ExtensionRegistryLite) registry); + } + static final com.google.protobuf.Descriptors.Descriptor + internal_static_com_flydean17_protobuf_Student_descriptor; + static final + com.google.protobuf.GeneratedMessageV3.FieldAccessorTable + internal_static_com_flydean17_protobuf_Student_fieldAccessorTable; + + public static com.google.protobuf.Descriptors.FileDescriptor + getDescriptor() { + return descriptor; + } + private static com.google.protobuf.Descriptors.FileDescriptor + descriptor; + static { + java.lang.String[] descriptorData = { + "\n\rstudent.proto\022\026com.flydean17.protobuf\"" + + "?\n\007Student\022\020\n\003age\030\001 \001(\005H\000\210\001\001\022\021\n\004name\030\002 \001" + + "(\tH\001\210\001\001B\006\n\004_ageB\007\n\005_nameB\032\n\026com.flydean1" + + "7.protobufP\001b\006proto3" + }; + descriptor = com.google.protobuf.Descriptors.FileDescriptor + .internalBuildGeneratedFileFrom(descriptorData, + new com.google.protobuf.Descriptors.FileDescriptor[] { + }); + internal_static_com_flydean17_protobuf_Student_descriptor = + getDescriptor().getMessageTypes().get(0); + internal_static_com_flydean17_protobuf_Student_fieldAccessorTable = new + com.google.protobuf.GeneratedMessageV3.FieldAccessorTable( + internal_static_com_flydean17_protobuf_Student_descriptor, + new java.lang.String[] { "Age", "Name", "Age", "Name", }); + } + + // @@protoc_insertion_point(outer_class_scope) +} diff --git a/learn-netty4/src/main/java/com/flydean17/protobuf/StudentServer.java b/learn-netty4/src/main/java/com/flydean17/protobuf/StudentServer.java new file mode 100644 index 0000000..d0ec793 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean17/protobuf/StudentServer.java @@ -0,0 +1,48 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean17.protobuf; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; + +/** + * student server用来接收student消息 + */ +public final class StudentServer { + + static final int PORT = Integer.parseInt(System.getProperty("port", "8000")); + + public static void main(String[] args) throws Exception { + EventLoopGroup bossGroup = new NioEventLoopGroup(1); + EventLoopGroup workerGroup = new NioEventLoopGroup(); + try { + ServerBootstrap b = new ServerBootstrap(); + b.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new StudentServerInitializer()); + + b.bind(PORT).sync().channel().closeFuture().sync(); + } finally { + bossGroup.shutdownGracefully(); + workerGroup.shutdownGracefully(); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean17/protobuf/StudentServerHandler.java b/learn-netty4/src/main/java/com/flydean17/protobuf/StudentServerHandler.java new file mode 100644 index 0000000..5313704 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean17/protobuf/StudentServerHandler.java @@ -0,0 +1,45 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean17.protobuf; + +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class StudentServerHandler extends SimpleChannelInboundHandler { + + @Override + public void channelRead0(ChannelHandlerContext ctx, Student student) throws Exception { + log.info("server收到消息{}",student); + // 写入消息 + ChannelFuture future = ctx.write(student); + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + ctx.flush(); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + // 异常处理 + log.error("出现异常",cause); + ctx.close(); + } + +} diff --git a/learn-netty4/src/main/java/com/flydean17/protobuf/StudentServerInitializer.java b/learn-netty4/src/main/java/com/flydean17/protobuf/StudentServerInitializer.java new file mode 100644 index 0000000..607ebae --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean17/protobuf/StudentServerInitializer.java @@ -0,0 +1,40 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean17.protobuf; + +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.protobuf.ProtobufDecoder; +import io.netty.handler.codec.protobuf.ProtobufEncoder; +import io.netty.handler.codec.protobuf.ProtobufVarint32FrameDecoder; +import io.netty.handler.codec.protobuf.ProtobufVarint32LengthFieldPrepender; + +public class StudentServerInitializer extends ChannelInitializer { + + @Override + public void initChannel(SocketChannel ch) throws Exception { + ChannelPipeline p = ch.pipeline(); + + p.addLast(new ProtobufVarint32FrameDecoder()); + p.addLast(new ProtobufDecoder(Student.getDefaultInstance())); + + p.addLast(new ProtobufVarint32LengthFieldPrepender()); + p.addLast(new ProtobufEncoder()); + + p.addLast(new StudentServerHandler()); + } +} diff --git a/learn-netty4/src/main/java/com/flydean17/protobuf/student.proto b/learn-netty4/src/main/java/com/flydean17/protobuf/student.proto new file mode 100644 index 0000000..4b04735 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean17/protobuf/student.proto @@ -0,0 +1,12 @@ +syntax = "proto3"; + +package com.flydean17.protobuf; + +option java_multiple_files = true; +option java_package = "com.flydean17.protobuf"; +//option java_outer_classname = "StudentWrapper"; + +message Student { + optional int32 age = 1; + optional string name =2; +} \ No newline at end of file diff --git a/learn-netty4/src/main/java/com/flydean18/httprequest/HttpRequestServer.java b/learn-netty4/src/main/java/com/flydean18/httprequest/HttpRequestServer.java new file mode 100644 index 0000000..8efd64c --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean18/httprequest/HttpRequestServer.java @@ -0,0 +1,56 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean18.httprequest; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import lombok.extern.slf4j.Slf4j; + +/** + * 一个简单的http server,用来从web端接收http请求 + */ +@Slf4j +public final class HttpRequestServer { + + static final int PORT = Integer.parseInt(System.getProperty("port", "8000")); + + public static void main(String[] args) throws Exception { + // server配置 + EventLoopGroup bossGroup = new NioEventLoopGroup(1); + EventLoopGroup workerGroup = new NioEventLoopGroup(); + try { + ServerBootstrap b = new ServerBootstrap(); + b.option(ChannelOption.SO_BACKLOG, 1024); + b.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new HttpRequestServerInitializer()); + + Channel ch = b.bind(PORT).sync().channel(); + log.info("请打开你的浏览器,访问 http://127.0.0.1:8000/"); + ch.closeFuture().sync(); + } finally { + bossGroup.shutdownGracefully(); + workerGroup.shutdownGracefully(); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean18/httprequest/HttpRequestServerHandler.java b/learn-netty4/src/main/java/com/flydean18/httprequest/HttpRequestServerHandler.java new file mode 100644 index 0000000..d0118ad --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean18/httprequest/HttpRequestServerHandler.java @@ -0,0 +1,77 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean18.httprequest; + +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http.*; +import lombok.extern.slf4j.Slf4j; + +import java.nio.charset.StandardCharsets; + +import static io.netty.handler.codec.http.HttpHeaderNames.*; +import static io.netty.handler.codec.http.HttpHeaderValues.KEEP_ALIVE; +import static io.netty.handler.codec.http.HttpHeaderValues.*; +import static io.netty.handler.codec.http.HttpResponseStatus.OK; + +@Slf4j +public class HttpRequestServerHandler extends SimpleChannelInboundHandler { + private static final byte[] CONTENT = "欢迎来到www.flydean.com!".getBytes(StandardCharsets.UTF_8); + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + ctx.flush(); + } + + @Override + public void channelRead0(ChannelHandlerContext ctx, HttpObject msg) { + if (msg instanceof HttpRequest) { + HttpRequest req = (HttpRequest) msg; + + boolean keepAlive = HttpUtil.isKeepAlive(req); + FullHttpResponse response = new DefaultFullHttpResponse(req.protocolVersion(), OK, + Unpooled.wrappedBuffer(CONTENT)); + response.headers() +// .set(CONTENT_TYPE, TEXT_PLAIN) + .set(CONTENT_TYPE, "text/plain;charset=utf-8") + .setInt(CONTENT_LENGTH, response.content().readableBytes()); + + if (keepAlive) { + if (!req.protocolVersion().isKeepAliveDefault()) { + //设置header connection=keep-alive + response.headers().set(CONNECTION, KEEP_ALIVE); + } + } else { + // 如果keepAlive是false,则设置header connection=close + response.headers().set(CONNECTION, CLOSE); + } + ChannelFuture f = ctx.write(response); + if (!keepAlive) { + f.addListener(ChannelFutureListener.CLOSE); + } + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + // 异常处理 + log.error("出现异常",cause); + ctx.close(); + } +} diff --git a/learn-netty4/src/main/java/com/flydean18/httprequest/HttpRequestServerInitializer.java b/learn-netty4/src/main/java/com/flydean18/httprequest/HttpRequestServerInitializer.java new file mode 100644 index 0000000..a691340 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean18/httprequest/HttpRequestServerInitializer.java @@ -0,0 +1,33 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean18.httprequest; + +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.codec.http.HttpServerExpectContinueHandler; + +public class HttpRequestServerInitializer extends ChannelInitializer { + + @Override + public void initChannel(SocketChannel ch) { + ChannelPipeline p = ch.pipeline(); + p.addLast(new HttpServerCodec()); + p.addLast(new HttpServerExpectContinueHandler()); + p.addLast(new HttpRequestServerHandler()); + } +} diff --git a/learn-netty4/src/main/java/com/flydean19/httpclientrequest/ClientRequestClient.java b/learn-netty4/src/main/java/com/flydean19/httpclientrequest/ClientRequestClient.java new file mode 100644 index 0000000..3f62f4b --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean19/httpclientrequest/ClientRequestClient.java @@ -0,0 +1,75 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean19.httpclientrequest; + +import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.http.*; +import io.netty.handler.codec.http.cookie.ClientCookieEncoder; +import io.netty.handler.codec.http.cookie.DefaultCookie; + +import java.net.URI; + +/** + * 一个自定义的HTTP Client + */ +public final class ClientRequestClient { + + static final String URL = System.getProperty("url", "http://127.0.0.1:8000/"); + + public static void main(String[] args) throws Exception { + URI uri = new URI(URL); + String host = uri.getHost() == null? "127.0.0.1" : uri.getHost(); + int port = uri.getPort(); + + EventLoopGroup group = new NioEventLoopGroup(); + try { + Bootstrap b = new Bootstrap(); + b.group(group) + .channel(NioSocketChannel.class) + .handler(new ClientRequestClientInitializer()); + + // 建立连接 + Channel ch = b.connect(host, port).sync().channel(); + + // HTTP请求 + HttpRequest request = new DefaultFullHttpRequest( + HttpVersion.HTTP_1_1, HttpMethod.GET, uri.getRawPath(), Unpooled.EMPTY_BUFFER); + request.headers().set(HttpHeaderNames.HOST, host); + request.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE); + request.headers().set(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.GZIP); + + // 设置cookie + request.headers().set( + HttpHeaderNames.COOKIE, + ClientCookieEncoder.STRICT.encode( + new DefaultCookie("name", "flydean"), + new DefaultCookie("site", "www.flydean.com"))); + + // 发送HTTP请求 + ch.writeAndFlush(request); + // 关闭连接 + ch.closeFuture().sync(); + } finally { + // 关闭服务器 + group.shutdownGracefully(); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean19/httpclientrequest/ClientRequestClientHandler.java b/learn-netty4/src/main/java/com/flydean19/httpclientrequest/ClientRequestClientHandler.java new file mode 100644 index 0000000..40e3191 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean19/httpclientrequest/ClientRequestClientHandler.java @@ -0,0 +1,67 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean19.httpclientrequest; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http.*; +import io.netty.util.CharsetUtil; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class ClientRequestClientHandler extends SimpleChannelInboundHandler { + + @Override + public void channelRead0(ChannelHandlerContext ctx, HttpObject msg) { + if (msg instanceof HttpResponse) { + HttpResponse response = (HttpResponse) msg; + + log.info("STATUS: {}" , response.status()); + log.info("VERSION: {}" , response.protocolVersion()); + + if (!response.headers().isEmpty()) { + for (CharSequence name: response.headers().names()) { + for (CharSequence value: response.headers().getAll(name)) { + log.info("HEADER: {}={}" ,name , value); + } + } + } + + if (HttpUtil.isTransferEncodingChunked(response)) { + log.info("CHUNKED CONTENT {"); + } else { + log.info("CONTENT {"); + } + } + if (msg instanceof HttpContent) { + HttpContent content = (HttpContent) msg; + + log.info(content.content().toString(CharsetUtil.UTF_8)); + + if (content instanceof LastHttpContent) { + log.info("} END OF CONTENT"); + ctx.close(); + } + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + // 异常处理 + log.error("出现异常",cause); + ctx.close(); + } +} diff --git a/learn-netty4/src/main/java/com/flydean19/httpclientrequest/ClientRequestClientInitializer.java b/learn-netty4/src/main/java/com/flydean19/httpclientrequest/ClientRequestClientInitializer.java new file mode 100644 index 0000000..e866dfe --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean19/httpclientrequest/ClientRequestClientInitializer.java @@ -0,0 +1,35 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean19.httpclientrequest; + +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.http.HttpClientCodec; +import io.netty.handler.codec.http.HttpContentDecompressor; + +public class ClientRequestClientInitializer extends ChannelInitializer { + + @Override + public void initChannel(SocketChannel ch) { + ChannelPipeline p = ch.pipeline(); + + p.addLast(new HttpClientCodec()); + p.addLast(new HttpContentDecompressor()); + //p.addLast(new HttpObjectAggregator(1048576)); + p.addLast(new ClientRequestClientHandler()); + } +} diff --git a/learn-netty4/src/main/java/com/flydean19/httpclientrequest/HttpRequestServer.java b/learn-netty4/src/main/java/com/flydean19/httpclientrequest/HttpRequestServer.java new file mode 100644 index 0000000..ec2c9f8 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean19/httpclientrequest/HttpRequestServer.java @@ -0,0 +1,56 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean19.httpclientrequest; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import lombok.extern.slf4j.Slf4j; + +/** + * HTTP 服务器 + */ +@Slf4j +public final class HttpRequestServer { + + static final int PORT = Integer.parseInt(System.getProperty("port", "8000")); + + public static void main(String[] args) throws Exception { + // server配置 + EventLoopGroup bossGroup = new NioEventLoopGroup(1); + EventLoopGroup workerGroup = new NioEventLoopGroup(); + try { + ServerBootstrap b = new ServerBootstrap(); + b.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new HttpRequestServerInitializer()); + + Channel ch = b.bind(PORT).sync().channel(); + + log.info("请打开你的浏览器,访问 http://127.0.0.1:8000/"); + + ch.closeFuture().sync(); + } finally { + bossGroup.shutdownGracefully(); + workerGroup.shutdownGracefully(); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean19/httpclientrequest/HttpRequestServerHandler.java b/learn-netty4/src/main/java/com/flydean19/httpclientrequest/HttpRequestServerHandler.java new file mode 100644 index 0000000..c739d85 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean19/httpclientrequest/HttpRequestServerHandler.java @@ -0,0 +1,198 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean19.httpclientrequest; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.DecoderResult; +import io.netty.handler.codec.http.*; +import io.netty.handler.codec.http.cookie.Cookie; +import io.netty.handler.codec.http.cookie.ServerCookieDecoder; +import io.netty.handler.codec.http.cookie.ServerCookieEncoder; +import io.netty.util.CharsetUtil; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import static io.netty.handler.codec.http.HttpResponseStatus.*; +import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; + +@Slf4j +public class HttpRequestServerHandler extends SimpleChannelInboundHandler { + + private HttpRequest request; + //存储请求内容 + private final StringBuilder buf = new StringBuilder(); + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + ctx.flush(); + } + + @Override + protected void channelRead0(ChannelHandlerContext ctx, Object msg) { + if (msg instanceof HttpRequest) { + HttpRequest request = this.request = (HttpRequest) msg; + + if (HttpUtil.is100ContinueExpected(request)) { + send100Continue(ctx); + } + + buf.setLength(0); + buf.append("欢迎来到www.flydean.com\r\n"); + buf.append("===================================\r\n"); + + buf.append("VERSION: ").append(request.protocolVersion()).append("\r\n"); + buf.append("HOSTNAME: ").append(request.headers().get(HttpHeaderNames.HOST, "unknown")).append("\r\n"); + buf.append("REQUEST_URI: ").append(request.uri()).append("\r\n\r\n"); + + HttpHeaders headers = request.headers(); + if (!headers.isEmpty()) { + for (Entry h: headers) { + CharSequence key = h.getKey(); + CharSequence value = h.getValue(); + buf.append("HEADER: ").append(key).append(" = ").append(value).append("\r\n"); + } + buf.append("\r\n"); + } + + QueryStringDecoder queryStringDecoder = new QueryStringDecoder(request.uri()); + Map> params = queryStringDecoder.parameters(); + if (!params.isEmpty()) { + for (Entry> p: params.entrySet()) { + String key = p.getKey(); + List vals = p.getValue(); + for (String val : vals) { + buf.append("PARAM: ").append(key).append(" = ").append(val).append("\r\n"); + } + } + buf.append("\r\n"); + } + + appendDecoderResult(buf, request); + } + + if (msg instanceof HttpContent) { + HttpContent httpContent = (HttpContent) msg; + + ByteBuf content = httpContent.content(); + if (content.isReadable()) { + buf.append("CONTENT: "); + buf.append(content.toString(CharsetUtil.UTF_8)); + buf.append("\r\n"); + appendDecoderResult(buf, request); + } + + //如果消息是HttpContent的最后一部分,则发送响应 + if (msg instanceof LastHttpContent) { + log.info("LastHttpContent:{}",msg); + buf.append("END OF CONTENT\r\n"); + + LastHttpContent trailer = (LastHttpContent) msg; + if (!trailer.trailingHeaders().isEmpty()) { + buf.append("\r\n"); + for (CharSequence name: trailer.trailingHeaders().names()) { + for (CharSequence value: trailer.trailingHeaders().getAll(name)) { + buf.append("TRAILING HEADER: "); + buf.append(name).append(" = ").append(value).append("\r\n"); + } + } + buf.append("\r\n"); + } + + if (!writeResponse(trailer, ctx)) { + // 如果不是长连接,那么在返回所有的响应之后关闭连接 + ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE); + } + } + } + } + + private static void appendDecoderResult(StringBuilder buf, HttpObject o) { + DecoderResult result = o.decoderResult(); + if (result.isSuccess()) { + return; + } + + buf.append(".. WITH DECODER FAILURE: "); + buf.append(result.cause()); + buf.append("\r\n"); + } + + /** + * 将响应写回channel + * @param currentObj + * @param ctx + * @return + */ + private boolean writeResponse(HttpObject currentObj, ChannelHandlerContext ctx) { + //是否长连接 + boolean keepAlive = HttpUtil.isKeepAlive(request); + // 构建响应对象 + FullHttpResponse response = new DefaultFullHttpResponse( + HTTP_1_1, currentObj.decoderResult().isSuccess()? OK : BAD_REQUEST, + Unpooled.copiedBuffer(buf.toString(), CharsetUtil.UTF_8)); + + response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8"); + + if (keepAlive) { + // 添加 'Content-Length' header + response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes()); + // 添加 keep alive header + response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); + } + // 对cookie进行decode和encode + String cookieString = request.headers().get(HttpHeaderNames.COOKIE); + log.info("cookieString:{}",cookieString); + if (cookieString != null) { + Set cookies = ServerCookieDecoder.STRICT.decode(cookieString); + if (!cookies.isEmpty()) { + // 重置cookie + for (Cookie cookie: cookies) { + response.headers().add(HttpHeaderNames.SET_COOKIE, ServerCookieEncoder.STRICT.encode(cookie)); + } + } + } else { + //如果本身没有cookie,则添加cookie + log.info("没有cookie,设置cookie"); + response.headers().add(HttpHeaderNames.SET_COOKIE, ServerCookieEncoder.STRICT.encode("name", "flydean")); + response.headers().add(HttpHeaderNames.SET_COOKIE, ServerCookieEncoder.STRICT.encode("site", "www.flydean.com")); + } + + // 将请求写回 + ctx.write(response); + + return keepAlive; + } + + private static void send100Continue(ChannelHandlerContext ctx) { + FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, CONTINUE, Unpooled.EMPTY_BUFFER); + ctx.write(response); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + // 异常处理 + log.error("出现异常",cause); + ctx.close(); + } +} diff --git a/learn-netty4/src/main/java/com/flydean19/httpclientrequest/HttpRequestServerInitializer.java b/learn-netty4/src/main/java/com/flydean19/httpclientrequest/HttpRequestServerInitializer.java new file mode 100644 index 0000000..fe72c28 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean19/httpclientrequest/HttpRequestServerInitializer.java @@ -0,0 +1,37 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean19.httpclientrequest; + +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.http.HttpContentCompressor; +import io.netty.handler.codec.http.HttpRequestDecoder; +import io.netty.handler.codec.http.HttpResponseEncoder; + +public class HttpRequestServerInitializer extends ChannelInitializer { + + @Override + public void initChannel(SocketChannel ch) { + ChannelPipeline p = ch.pipeline(); + + p.addLast(new HttpRequestDecoder()); + //p.addLast(new HttpObjectAggregator(1048576)); + p.addLast(new HttpResponseEncoder()); +// p.addLast(new HttpContentCompressor()); + p.addLast(new HttpRequestServerHandler()); + } +} diff --git a/learn-netty4/src/main/java/com/flydean20/httpfile/HttpFileServer.java b/learn-netty4/src/main/java/com/flydean20/httpfile/HttpFileServer.java new file mode 100644 index 0000000..5aa0fc9 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean20/httpfile/HttpFileServer.java @@ -0,0 +1,53 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean20.httpfile; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public final class HttpFileServer { + + static final int PORT = Integer.parseInt(System.getProperty("port", "8000")); + + public static void main(String[] args) throws Exception { + + EventLoopGroup bossGroup = new NioEventLoopGroup(1); + EventLoopGroup workerGroup = new NioEventLoopGroup(); + try { + ServerBootstrap b = new ServerBootstrap(); + b.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new HttpFileServerInitializer()); + + Channel ch = b.bind(PORT).sync().channel(); + + log.info("请打开你的浏览器,访问 http://127.0.0.1:8000/"); + + ch.closeFuture().sync(); + } finally { + bossGroup.shutdownGracefully(); + workerGroup.shutdownGracefully(); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean20/httpfile/HttpFileServerHandler.java b/learn-netty4/src/main/java/com/flydean20/httpfile/HttpFileServerHandler.java new file mode 100644 index 0000000..1fec428 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean20/httpfile/HttpFileServerHandler.java @@ -0,0 +1,347 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean20.httpfile; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.*; +import io.netty.handler.codec.http.*; +import io.netty.handler.stream.ChunkedFile; +import io.netty.util.CharsetUtil; +import io.netty.util.internal.SystemPropertyUtil; +import lombok.extern.slf4j.Slf4j; + +import javax.activation.MimetypesFileTypeMap; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.RandomAccessFile; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.regex.Pattern; + +import static io.netty.handler.codec.http.HttpMethod.GET; +import static io.netty.handler.codec.http.HttpResponseStatus.*; +import static io.netty.handler.codec.http.HttpVersion.HTTP_1_0; +import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; + +@Slf4j +public class HttpFileServerHandler extends SimpleChannelInboundHandler { + + public static final String HTTP_DATE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss zzz"; + public static final String HTTP_DATE_GMT_TIMEZONE = "GMT+8:00"; + public static final int HTTP_CACHE_SECONDS = 60; + + private FullHttpRequest request; + + @Override + public void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception { + this.request = request; + if (!request.decoderResult().isSuccess()) { + sendError(ctx, BAD_REQUEST); + return; + } + + if (!GET.equals(request.method())) { + sendError(ctx, METHOD_NOT_ALLOWED); + return; + } + + final boolean keepAlive = HttpUtil.isKeepAlive(request); + final String uri = request.uri(); + final String path = sanitizeUri(uri); + if (path == null) { + sendError(ctx, FORBIDDEN); + return; + } + + File file = new File(path); + if (file.isHidden() || !file.exists()) { + sendError(ctx, NOT_FOUND); + return; + } + + if (file.isDirectory()) { + if (uri.endsWith("/")) { + sendListing(ctx, file, uri); + } else { + sendRedirect(ctx, uri + '/'); + } + return; + } + + if (!file.isFile()) { + sendError(ctx, FORBIDDEN); + return; + } + + // 文件缓存判断 + String ifModifiedSince = request.headers().get(HttpHeaderNames.IF_MODIFIED_SINCE); + if (ifModifiedSince != null && !ifModifiedSince.isEmpty()) { + SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US); + Date ifModifiedSinceDate = dateFormatter.parse(ifModifiedSince); + + // 比较文件修改的时间 + long ifModifiedSinceDateSeconds = ifModifiedSinceDate.getTime() / 1000; + long fileLastModifiedSeconds = file.lastModified() / 1000; + if (ifModifiedSinceDateSeconds == fileLastModifiedSeconds) { + sendNotModified(ctx); + return; + } + } + + RandomAccessFile raf; + try { + raf = new RandomAccessFile(file, "r"); + } catch (FileNotFoundException ignore) { + sendError(ctx, NOT_FOUND); + return; + } + long fileLength = raf.length(); + + HttpResponse response = new DefaultHttpResponse(HTTP_1_1, OK); + HttpUtil.setContentLength(response, fileLength); + setContentTypeHeader(response, file); + setDateAndCacheHeaders(response, file); + + if (!keepAlive) { + response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE); + } else if (request.protocolVersion().equals(HTTP_1_0)) { + response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); + } + + // 头部部分 + ctx.write(response); + + // 内容部分 + ChannelFuture sendFileFuture; + ChannelFuture lastContentFuture; +// sendFileFuture = +// ctx.write(new DefaultFileRegion(raf.getChannel(), 0, fileLength), ctx.newProgressivePromise()); +// // 结束部分 +// lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT); + + sendFileFuture = + ctx.writeAndFlush(new HttpChunkedInput(new ChunkedFile(raf, 0, fileLength, 8192)), + ctx.newProgressivePromise()); +// HttpChunkedInput 会自动填写 LastHttpContent部分 + lastContentFuture = sendFileFuture; + + sendFileFuture.addListener(new ChannelProgressiveFutureListener() { + @Override + public void operationProgressed(ChannelProgressiveFuture future, long progress, long total) { + if (total < 0) { + log.info(future.channel() + " 传输进度: " + progress); + } else { + log.info(future.channel() + " 传输进度: " + progress + " / " + total); + } + } + + @Override + public void operationComplete(ChannelProgressiveFuture future) { + log.info(future.channel() + " 传输完毕."); + } + }); + + // 是否关闭连接 + if (!keepAlive) { + // 当最后一部分也写出之后,再关闭 + lastContentFuture.addListener(ChannelFutureListener.CLOSE); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + // 异常处理 + log.error("出现异常",cause); + if (ctx.channel().isActive()) { + sendError(ctx, INTERNAL_SERVER_ERROR); + } + } + + private static final Pattern INSECURE_URI = Pattern.compile(".*[<>&\"].*"); + + //对URL进行处理 + private static String sanitizeUri(String uri) { + // Decode the path. + try { + uri = URLDecoder.decode(uri, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new Error(e); + } + + if (uri.isEmpty() || uri.charAt(0) != '/') { + return null; + } + + // 替换文件路径为服务器中的路径 + uri = uri.replace('/', File.separatorChar); + + // 保证文件路径访问的安全性 + if (uri.contains(File.separator + '.') || + uri.contains('.' + File.separator) || + uri.charAt(0) == '.' || uri.charAt(uri.length() - 1) == '.' || + INSECURE_URI.matcher(uri).matches()) { + return null; + } + + String filePath=SystemPropertyUtil.get("user.dir") + File.separator + uri; + // 转换成绝对路径 + return filePath; + } + + //允许被列出的文件 + private static final Pattern ALLOWED_FILE_NAME = Pattern.compile("[^-\\._]?[^<>&\\\"]*"); + + + private void sendListing(ChannelHandlerContext ctx, File dir, String dirPath) { + StringBuilder buf = new StringBuilder() + .append("\r\n") + .append("") + .append(dirPath) + .append("中的文件列表") + .append("\r\n") + + .append("

") + .append(dirPath) + .append("中的文件列表") + .append("

\r\n") + + .append("
    ") + .append("
  • ..
  • \r\n"); + + File[] files = dir.listFiles(); + if (files != null) { + for (File f: files) { + if (f.isHidden() || !f.canRead()) { + continue; + } + + String name = f.getName(); + if (!ALLOWED_FILE_NAME.matcher(name).matches()) { + continue; + } + + buf.append("
  • ") + .append(name) + .append("
  • \r\n"); + } + } + + buf.append("
\r\n"); + + ByteBuf buffer = ctx.alloc().buffer(buf.length()); + buffer.writeCharSequence(buf.toString(), CharsetUtil.UTF_8); + + FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK, buffer); + response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8"); + + sendAndCleanupConnection(ctx, response); + } + + /** + * 重定向 + */ + private void sendRedirect(ChannelHandlerContext ctx, String newUri) { + FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, FOUND, Unpooled.EMPTY_BUFFER); + response.headers().set(HttpHeaderNames.LOCATION, newUri); + + sendAndCleanupConnection(ctx, response); + } + + /** + * 异常 + */ + private void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) { + FullHttpResponse response = new DefaultFullHttpResponse( + HTTP_1_1, status, Unpooled.copiedBuffer("异常: " + status + "\r\n", CharsetUtil.UTF_8)); + response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8"); + + sendAndCleanupConnection(ctx, response); + } + + /** + * 如果文件没有修改,则发送 "304 Not Modified" + */ + private void sendNotModified(ChannelHandlerContext ctx) { + FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, NOT_MODIFIED, Unpooled.EMPTY_BUFFER); + setDateHeader(response); + + sendAndCleanupConnection(ctx, response); + } + + /** + * 判断请求是否是 Keep-Alive,根据请求的Keep-Alive判断是否关闭连接 + */ + private void sendAndCleanupConnection(ChannelHandlerContext ctx, FullHttpResponse response) { + final boolean keepAlive = HttpUtil.isKeepAlive(request); + HttpUtil.setContentLength(response, response.content().readableBytes()); + if (!keepAlive) { + response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE); + } else if (request.protocolVersion().equals(HTTP_1_0)) { + response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); + } + + ChannelFuture flushPromise = ctx.writeAndFlush(response); + + if (!keepAlive) { + flushPromise.addListener(ChannelFutureListener.CLOSE); + } + } + + /** + * 设置时间头 + */ + private static void setDateHeader(FullHttpResponse response) { + SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US); + dateFormatter.setTimeZone(TimeZone.getTimeZone(HTTP_DATE_GMT_TIMEZONE)); + + Calendar time = new GregorianCalendar(); + response.headers().set(HttpHeaderNames.DATE, dateFormatter.format(time.getTime())); + } + + /** + * 设置时间头和缓存头 + */ + private static void setDateAndCacheHeaders(HttpResponse response, File fileToCache) { + SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US); + dateFormatter.setTimeZone(TimeZone.getTimeZone(HTTP_DATE_GMT_TIMEZONE)); + + // 日期 header + Calendar time = new GregorianCalendar(); + log.info(dateFormatter.format(time.getTime())); + + response.headers().set(HttpHeaderNames.DATE, dateFormatter.format(time.getTime())); + + // 缓存 headers + time.add(Calendar.SECOND, HTTP_CACHE_SECONDS); + response.headers().set(HttpHeaderNames.EXPIRES, dateFormatter.format(time.getTime())); + response.headers().set(HttpHeaderNames.CACHE_CONTROL, "private, max-age=" + HTTP_CACHE_SECONDS); + response.headers().set( + HttpHeaderNames.LAST_MODIFIED, dateFormatter.format(new Date(fileToCache.lastModified()))); + } + + /** + * 根据文件的名字设置合适的content-type + */ + private static void setContentTypeHeader(HttpResponse response, File file) { + MimetypesFileTypeMap mimeTypesMap = new MimetypesFileTypeMap(); + response.headers().set(HttpHeaderNames.CONTENT_TYPE, mimeTypesMap.getContentType(file.getPath())); + } +} diff --git a/learn-netty4/src/main/java/com/flydean20/httpfile/HttpFileServerInitializer.java b/learn-netty4/src/main/java/com/flydean20/httpfile/HttpFileServerInitializer.java new file mode 100644 index 0000000..f035961 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean20/httpfile/HttpFileServerInitializer.java @@ -0,0 +1,35 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean20.httpfile; + +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.stream.ChunkedWriteHandler; + +public class HttpFileServerInitializer extends ChannelInitializer { + + @Override + public void initChannel(SocketChannel ch) { + ChannelPipeline pipeline = ch.pipeline(); + pipeline.addLast(new HttpServerCodec()); + pipeline.addLast(new HttpObjectAggregator(65536)); + pipeline.addLast(new ChunkedWriteHandler()); + pipeline.addLast(new HttpFileServerHandler()); + } +} diff --git a/learn-netty4/src/main/java/com/flydean21/httpupload/HttpUploadClient.java b/learn-netty4/src/main/java/com/flydean21/httpupload/HttpUploadClient.java new file mode 100644 index 0000000..e5762e3 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean21/httpupload/HttpUploadClient.java @@ -0,0 +1,227 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean21.httpupload; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.http.*; +import io.netty.handler.codec.http.cookie.ClientCookieEncoder; +import io.netty.handler.codec.http.cookie.DefaultCookie; +import io.netty.handler.codec.http.multipart.*; +import io.netty.util.internal.SocketUtils; +import lombok.extern.slf4j.Slf4j; + +import java.io.File; +import java.io.FileNotFoundException; +import java.net.URI; +import java.util.List; +import java.util.Map.Entry; + +/** + * 文件上传客户端 + */ +@Slf4j +public final class HttpUploadClient { + + static final String BASE_URL = System.getProperty("baseUrl", "http://127.0.0.1:8000/"); + static final String FILE = System.getProperty("file", "file.txt"); + + public static void main(String[] args) throws Exception { + String postSimple = BASE_URL + "post"; + String postFile= BASE_URL + "postmultipart"; + String get= BASE_URL + "get"; + + URI uriSimple = new URI(postSimple); + String host = uriSimple.getHost(); + int port = uriSimple.getPort(); + + URI uriFile = new URI(postFile); + File file = new File(FILE); + log.info(file.getCanonicalPath()); + if (!file.canRead()) { + throw new FileNotFoundException(FILE); + } + + EventLoopGroup group = new NioEventLoopGroup(); + + // 创建HttpDataFactory 默认是放在内存空间,当超出MINSIZE则会存放在Disk中 + HttpDataFactory factory = new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE); + + DiskFileUpload.deleteOnExitTemporaryFile = true; // 在退出的时候删除文件 + DiskFileUpload.baseDirectory = null; + DiskAttribute.deleteOnExitTemporaryFile = true; // 在退出的时候删除文件 + DiskAttribute.baseDirectory = null; + + try { + Bootstrap b = new Bootstrap(); + b.group(group).channel(NioSocketChannel.class) + .handler(new HttpUploadClientInitializer()); + + // Simple Get form + List> headers = formget(b, host, port, get, uriSimple); + if (headers == null) { + factory.cleanAllHttpData(); + return; + } + + // Simple Post form + List bodylist = formpost(b, host, port, uriSimple, file, factory, headers); + if (bodylist == null) { + factory.cleanAllHttpData(); + return; + } + + // Multipart Post form + formpostmultipart(b, host, port, uriFile, factory, headers, bodylist); + } finally { + group.shutdownGracefully(); + // 清除所有的temp文件 + factory.cleanAllHttpData(); + } + } + + /** + * 一个标准的HTTP get请求 + **/ + private static List> formget( + Bootstrap bootstrap, String host, int port, String get, URI uriSimple) throws Exception { + Channel channel = bootstrap.connect(host, port).sync().channel(); + // HTTP请求 + QueryStringEncoder encoder = new QueryStringEncoder(get); + // 添加请求参数 + encoder.addParam("method", "GET"); + encoder.addParam("name", "flydean"); + encoder.addParam("site", "www.flydean.com"); + + URI uriGet = new URI(encoder.toString()); + HttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, uriGet.toASCIIString()); + HttpHeaders headers = request.headers(); + headers.set(HttpHeaderNames.HOST, host); + headers.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE); + headers.set(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.GZIP + "," + HttpHeaderValues.DEFLATE); + headers.set(HttpHeaderNames.ACCEPT_LANGUAGE, "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2"); + headers.set(HttpHeaderNames.REFERER, uriSimple.toString()); + headers.set(HttpHeaderNames.USER_AGENT, "Netty Simple Http Client side"); + headers.set(HttpHeaderNames.ACCEPT, "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"); + + headers.set( + HttpHeaderNames.COOKIE, ClientCookieEncoder.STRICT.encode( + new DefaultCookie("name", "flydean"), + new DefaultCookie("site", "www.flydean.com")) + ); + + channel.writeAndFlush(request); + + channel.closeFuture().sync(); + // 返回header + return headers.entries(); + } + + /** + * 标准POST请求 + */ + private static List formpost( + Bootstrap bootstrap, + String host, int port, URI uriSimple, File file, HttpDataFactory factory, + List> headers) throws Exception { + + ChannelFuture future = bootstrap.connect(SocketUtils.socketAddress(host, port)); + Channel channel = future.sync().channel(); + + // 构建HTTP request + HttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, uriSimple.toASCIIString()); + + // Use the PostBody encoder + HttpPostRequestEncoder bodyRequestEncoder = + new HttpPostRequestEncoder(factory, request, false); // false => not multipart + + // 添加headers + for (Entry entry : headers) { + request.headers().set(entry.getKey(), entry.getValue()); + } + + // 添加form属性 + bodyRequestEncoder.addBodyAttribute("method", "POST"); + bodyRequestEncoder.addBodyAttribute("name", "flydean"); + bodyRequestEncoder.addBodyAttribute("site", "www.flydean.com"); + bodyRequestEncoder.addBodyFileUpload("myfile", file, "application/x-zip-compressed", false); + + // finalize request, 判断是否需要chunk + request = bodyRequestEncoder.finalizeRequest(); + + // 创建bodylist + List bodylist = bodyRequestEncoder.getBodyListAttributes(); + + // 发送请求 + channel.write(request); + + // 判断bodyRequestEncoder是否是Chunked,发送请求内容 + if (bodyRequestEncoder.isChunked()) { + channel.write(bodyRequestEncoder); + } + channel.flush(); + + //清除请求 + // bodyRequestEncoder.cleanFiles(); + + // 等待channel关闭 + channel.closeFuture().sync(); + return bodylist; + } + + /** + * Multipart form + */ + private static void formpostmultipart( + Bootstrap bootstrap, String host, int port, URI uriFile, HttpDataFactory factory, + Iterable> headers, List bodylist) throws Exception { + ChannelFuture future = bootstrap.connect(SocketUtils.socketAddress(host, port)); + Channel channel = future.sync().channel(); + + // 创建HTTP request + HttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, uriFile.toASCIIString()); + + // Use the PostBody encoder + HttpPostRequestEncoder bodyRequestEncoder = + new HttpPostRequestEncoder(factory, request, true); // true => multipart + + // 添加headers + for (Entry entry : headers) { + request.headers().set(entry.getKey(), entry.getValue()); + } + // 添加body http data + bodyRequestEncoder.setBodyHttpDatas(bodylist); + // finalize request,判断是否需要chunk + request = bodyRequestEncoder.finalizeRequest(); + // 发送请求头 + channel.write(request); + // 判断bodyRequestEncoder是否是Chunked,发送请求内容 + if (bodyRequestEncoder.isChunked()) { + channel.write(bodyRequestEncoder); + } + channel.flush(); + // 清除文件 + bodyRequestEncoder.cleanFiles(); + channel.closeFuture().sync(); + } + + + +} diff --git a/learn-netty4/src/main/java/com/flydean21/httpupload/HttpUploadClientHandler.java b/learn-netty4/src/main/java/com/flydean21/httpupload/HttpUploadClientHandler.java new file mode 100644 index 0000000..078df89 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean21/httpupload/HttpUploadClientHandler.java @@ -0,0 +1,78 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean21.httpupload; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http.*; +import io.netty.util.CharsetUtil; +import lombok.extern.slf4j.Slf4j; + +/** + * 上传文件处理器 + */ +@Slf4j +public class HttpUploadClientHandler extends SimpleChannelInboundHandler { + + private boolean readingChunks; + + @Override + public void channelRead0(ChannelHandlerContext ctx, HttpObject msg) { + if (msg instanceof HttpResponse) { + HttpResponse response = (HttpResponse) msg; + + log.info("STATUS: " + response.status()); + log.info("VERSION: " + response.protocolVersion()); + + if (!response.headers().isEmpty()) { + for (CharSequence name : response.headers().names()) { + for (CharSequence value : response.headers().getAll(name)) { + log.info("HEADER: " + name + " = " + value); + } + } + } + + if (response.status().code() == 200 && HttpUtil.isTransferEncodingChunked(response)) { + readingChunks = true; + log.info("CHUNKED CONTENT {"); + } else { + log.info("CONTENT {"); + } + } + if (msg instanceof HttpContent) { + HttpContent chunk = (HttpContent) msg; + + if (chunk instanceof LastHttpContent) { + log.info(chunk.content().toString(CharsetUtil.UTF_8)); + if (readingChunks) { + log.info("} END OF CHUNKED CONTENT"); + } else { + log.info("} END OF CONTENT"); + } + readingChunks = false; + } else { + log.info(chunk.content().toString(CharsetUtil.UTF_8)); + } + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + // 异常处理 + log.error("出现异常",cause); + ctx.channel().close(); + } +} diff --git a/learn-netty4/src/main/java/com/flydean21/httpupload/HttpUploadClientInitializer.java b/learn-netty4/src/main/java/com/flydean21/httpupload/HttpUploadClientInitializer.java new file mode 100644 index 0000000..d76cf5d --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean21/httpupload/HttpUploadClientInitializer.java @@ -0,0 +1,36 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean21.httpupload; + +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.http.HttpClientCodec; +import io.netty.handler.codec.http.HttpContentDecompressor; +import io.netty.handler.stream.ChunkedWriteHandler; + +public class HttpUploadClientInitializer extends ChannelInitializer { + + @Override + public void initChannel(SocketChannel ch) { + ChannelPipeline pipeline = ch.pipeline(); + + pipeline.addLast("codec", new HttpClientCodec()); + pipeline.addLast("inflater", new HttpContentDecompressor()); + pipeline.addLast("chunkedWriter", new ChunkedWriteHandler()); + pipeline.addLast("handler", new HttpUploadClientHandler()); + } +} diff --git a/learn-netty4/src/main/java/com/flydean21/httpupload/HttpUploadServer.java b/learn-netty4/src/main/java/com/flydean21/httpupload/HttpUploadServer.java new file mode 100644 index 0000000..b6cc220 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean21/httpupload/HttpUploadServer.java @@ -0,0 +1,56 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean21.httpupload; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import lombok.extern.slf4j.Slf4j; + +/** + * HTTP文件上传服务器 + */ +@Slf4j +public final class HttpUploadServer { + + static final int PORT = Integer.parseInt(System.getProperty("port", "8000")); + + public static void main(String[] args) throws Exception { + + EventLoopGroup bossGroup = new NioEventLoopGroup(1); + EventLoopGroup workerGroup = new NioEventLoopGroup(); + try { + ServerBootstrap b = new ServerBootstrap(); + b.group(bossGroup, workerGroup); + b.channel(NioServerSocketChannel.class); + b.handler(new LoggingHandler(LogLevel.INFO)); + b.childHandler(new HttpUploadServerInitializer()); + + Channel ch = b.bind(PORT).sync().channel(); + + log.info("请打开你的浏览器,访问 http://127.0.0.1:8000/"); + + ch.closeFuture().sync(); + } finally { + bossGroup.shutdownGracefully(); + workerGroup.shutdownGracefully(); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean21/httpupload/HttpUploadServerHandler.java b/learn-netty4/src/main/java/com/flydean21/httpupload/HttpUploadServerHandler.java new file mode 100644 index 0000000..cd340ce --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean21/httpupload/HttpUploadServerHandler.java @@ -0,0 +1,303 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean21.httpupload; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.*; +import io.netty.handler.codec.http.*; +import io.netty.handler.codec.http.cookie.Cookie; +import io.netty.handler.codec.http.cookie.ServerCookieDecoder; +import io.netty.handler.codec.http.cookie.ServerCookieEncoder; +import io.netty.handler.codec.http.multipart.*; +import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.EndOfDataDecoderException; +import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.ErrorDataDecoderException; +import io.netty.handler.codec.http.multipart.InterfaceHttpData.HttpDataType; +import io.netty.util.CharsetUtil; +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import static io.netty.buffer.Unpooled.copiedBuffer; + +@Slf4j +public class HttpUploadServerHandler extends SimpleChannelInboundHandler { + + private HttpRequest request; + + private HttpData partialContent; + + private final StringBuilder responseContent = new StringBuilder(); + + private static final HttpDataFactory factory = + new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE); + + private HttpPostRequestDecoder decoder; + + static { + DiskFileUpload.deleteOnExitTemporaryFile = true; // 退出的时候删除临时文件 + DiskFileUpload.baseDirectory = null; + DiskAttribute.deleteOnExitTemporaryFile = true; // 退出的时候删除临时文件 + DiskAttribute.baseDirectory = null; + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) { + if (decoder != null) { + decoder.cleanFiles(); + } + } + + @Override + public void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception { + //处理HttpRequest + if (msg instanceof HttpRequest) { + HttpRequest request = this.request = (HttpRequest) msg; + + responseContent.setLength(0); + responseContent.append("请求响应开始\r\n"); + responseContent.append("===================================\r\n"); + responseContent.append("VERSION: ").append(request.protocolVersion().text()).append("\r\n"); + responseContent.append("REQUEST_URI: ").append(request.uri()).append("\r\n"); + + // 添加header值 + for (Entry entry : request.headers()) { + responseContent.append("HEADER: ").append(entry.getKey()).append('=').append(entry.getValue()).append("\r\n"); + } + + // 设置cookie值 + Set cookies; + String value = request.headers().get(HttpHeaderNames.COOKIE); + if (value == null) { + cookies = Collections.emptySet(); + } else { + cookies = ServerCookieDecoder.STRICT.decode(value); + } + for (Cookie cookie : cookies) { + responseContent.append("COOKIE: ").append(cookie).append("\r\n"); + } + + //解析URL中的参数 + QueryStringDecoder decoderQuery = new QueryStringDecoder(request.uri()); + Map> uriAttributes = decoderQuery.parameters(); + for (Entry> attr: uriAttributes.entrySet()) { + for (String attrVal: attr.getValue()) { + responseContent.append("URI: ").append(attr.getKey()).append('=').append(attrVal).append("\r\n"); + } + } + + //GET请求 + if (HttpMethod.GET.equals(request.method())) { + responseContent.append("\r\n\r\nEND OF GET CONTENT\r\n"); + return; + } + try { + //POST请求 + decoder = new HttpPostRequestDecoder(factory, request); + } catch (ErrorDataDecoderException e1) { + // 异常处理 + log.error("出现异常",e1); + responseContent.append(e1.getMessage()); + writeResponse(ctx.channel(), true); + return; + } + + boolean readingChunks = HttpUtil.isTransferEncodingChunked(request); + responseContent.append("Is Chunked: ").append(readingChunks).append("\r\n"); + responseContent.append("IsMultipart: ").append(decoder.isMultipart()).append("\r\n"); + if (readingChunks) { + responseContent.append("Chunks: "); + } + } + + //处理HttpContent + // 如果POST的decoder存在 + if (decoder != null) { + if (msg instanceof HttpContent) { + HttpContent chunk = (HttpContent) msg; + try { + decoder.offer(chunk); + } catch (ErrorDataDecoderException e1) { + // 异常处理 + log.error("出现异常",e1); + responseContent.append(e1.getMessage()); + writeResponse(ctx.channel(), true); + return; + } + //读取httpData + log.info("readHttpDataChunkByChunk"); + readHttpDataChunkByChunk(); + // 读最后一部分 + if (chunk instanceof LastHttpContent) { + writeResponse(ctx.channel()); + reset(); + } + } + } else { + writeResponse(ctx.channel()); + } + } + + private void reset() { + request = null; + //destory decoder + decoder.destroy(); + decoder = null; + } + + /** + * 读取HttpData + */ + private void readHttpDataChunkByChunk() { + try { + while (decoder.hasNext()) { + log.info("decoder has next"); + InterfaceHttpData data = decoder.next(); + if (data != null) { + // 检测当前的 HttpData 如果是一个 FileUpload 并且和上一个 partialContent一致,表示是最后一个HttpData + if (partialContent == data) { + log.info(" 100% (FinalSize: " + partialContent.length() + ")"); + partialContent = null; + } + // new value + writeHttpData(data); + } + } + //如果decoder并没有结束 + // 检测当前的 partial 数据 + InterfaceHttpData data = decoder.currentPartialHttpData(); + if (data != null) { + StringBuilder builder = new StringBuilder(); + if (partialContent == null) { + partialContent = (HttpData) data; + if (partialContent instanceof FileUpload) { + builder.append("Start FileUpload: ") + .append(((FileUpload) partialContent).getFilename()).append(" "); + } else { + builder.append("Start Attribute: ") + .append(partialContent.getName()).append(" "); + } + builder.append("(DefinedSize: ").append(partialContent.definedLength()).append(")"); + } + if (partialContent.definedLength() > 0) { + builder.append(" ").append(partialContent.length() * 100 / partialContent.definedLength()) + .append("% "); + log.info(builder.toString()); + } else { + builder.append(" ").append(partialContent.length()).append(" "); + log.info(builder.toString()); + } + } + } catch (EndOfDataDecoderException e1) { + // end + responseContent.append("END OF CONTENT CHUNK BY CHUNK\r\n\r\n"); + } + } + + private void writeHttpData(InterfaceHttpData data) { + if (data.getHttpDataType() == HttpDataType.Attribute) { + Attribute attribute = (Attribute) data; + String value; + try { + value = attribute.getValue(); + } catch (IOException e1) { + // 异常处理 + log.error("出现异常",e1); + responseContent.append("BODY Attribute: ").append(attribute.getHttpDataType().name()).append(": ").append(attribute.getName()).append(" 读取数据出错: ").append(e1.getMessage()).append("\r\n"); + return; + } + if (value.length() > 100) { + responseContent.append("BODY Attribute: ").append(attribute.getHttpDataType().name()).append(": ").append(attribute.getName()).append("数据太长了\r\n"); + } else { + responseContent.append("BODY Attribute: ").append(attribute.getHttpDataType().name()).append(": ").append(attribute).append("\r\n"); + } + } else { + responseContent.append("BODY FileUpload: ").append(data.getHttpDataType().name()).append(": ").append(data).append("\r\n"); + if (data.getHttpDataType() == HttpDataType.FileUpload) { + FileUpload fileUpload = (FileUpload) data; + if (fileUpload.isCompleted()) { + if (fileUpload.length() < 10000) { + responseContent.append("文件内容如下:\r\n"); + try { + responseContent.append(fileUpload.getString(fileUpload.getCharset())); + } catch (IOException e1) { + // 异常处理 + log.error("出现异常",e1); + } + responseContent.append("\r\n"); + } else { + responseContent.append("文件太长了:").append(fileUpload.length()).append("\r\n"); + } + } else { + responseContent.append("文件接收有误!\r\n"); + } + } + } + } + + private void writeResponse(Channel channel) { + writeResponse(channel, false); + } + + private void writeResponse(Channel channel, boolean forceClose) { + ByteBuf buf = copiedBuffer(responseContent.toString(), CharsetUtil.UTF_8); + responseContent.setLength(0); + + boolean keepAlive = HttpUtil.isKeepAlive(request) && !forceClose; + + FullHttpResponse response = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, HttpResponseStatus.OK, buf); + response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8"); + response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, buf.readableBytes()); + + if (!keepAlive) { + response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE); + } else if (request.protocolVersion().equals(HttpVersion.HTTP_1_0)) { + response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); + } + + Set cookies; + String value = request.headers().get(HttpHeaderNames.COOKIE); + if (value == null) { + cookies = Collections.emptySet(); + } else { + cookies = ServerCookieDecoder.STRICT.decode(value); + } + if (!cookies.isEmpty()) { + //设置cookie + for (Cookie cookie : cookies) { + response.headers().add(HttpHeaderNames.SET_COOKIE, ServerCookieEncoder.STRICT.encode(cookie)); + } + } + // 写入response + ChannelFuture future = channel.writeAndFlush(response); + // 关闭连接 + if (!keepAlive) { + future.addListener(ChannelFutureListener.CLOSE); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + // 异常处理 + log.error("出现异常",cause); + ctx.channel().close(); + } +} diff --git a/learn-netty4/src/main/java/com/flydean21/httpupload/HttpUploadServerInitializer.java b/learn-netty4/src/main/java/com/flydean21/httpupload/HttpUploadServerInitializer.java new file mode 100644 index 0000000..a002a01 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean21/httpupload/HttpUploadServerInitializer.java @@ -0,0 +1,36 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean21.httpupload; + +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.http.HttpContentCompressor; +import io.netty.handler.codec.http.HttpRequestDecoder; +import io.netty.handler.codec.http.HttpResponseEncoder; + +public class HttpUploadServerInitializer extends ChannelInitializer { + + @Override + public void initChannel(SocketChannel ch) { + ChannelPipeline pipeline = ch.pipeline(); + + pipeline.addLast(new HttpRequestDecoder()); + pipeline.addLast(new HttpResponseEncoder()); + pipeline.addLast(new HttpContentCompressor()); + pipeline.addLast(new HttpUploadServerHandler()); + } +} diff --git a/learn-netty4/src/main/java/com/flydean22/cors/CustResponseHandler.java b/learn-netty4/src/main/java/com/flydean22/cors/CustResponseHandler.java new file mode 100644 index 0000000..3580bd0 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean22/cors/CustResponseHandler.java @@ -0,0 +1,38 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean22.cors; + +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpVersion; + +/** + * 自定义响应handler + */ +public class CustResponseHandler extends SimpleChannelInboundHandler { + @Override + public void channelRead0(ChannelHandlerContext ctx, Object msg) { + final FullHttpResponse response = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.EMPTY_BUFFER); + response.headers().set("site", "www.flydean.com"); + ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); + } +} diff --git a/learn-netty4/src/main/java/com/flydean22/cors/HttpCorsServer.java b/learn-netty4/src/main/java/com/flydean22/cors/HttpCorsServer.java new file mode 100644 index 0000000..5466725 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean22/cors/HttpCorsServer.java @@ -0,0 +1,46 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean22.cors; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; + +public final class HttpCorsServer { + + static final int PORT = Integer.parseInt(System.getProperty("port","8000")); + + public static void main(String[] args) throws Exception { + + EventLoopGroup bossGroup = new NioEventLoopGroup(1); + EventLoopGroup workerGroup = new NioEventLoopGroup(); + try { + ServerBootstrap b = new ServerBootstrap(); + b.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new HttpCorsServerInitializer()); + + b.bind(PORT).sync().channel().closeFuture().sync(); + } finally { + bossGroup.shutdownGracefully(); + workerGroup.shutdownGracefully(); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean22/cors/HttpCorsServerInitializer.java b/learn-netty4/src/main/java/com/flydean22/cors/HttpCorsServerInitializer.java new file mode 100644 index 0000000..10bc88e --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean22/cors/HttpCorsServerInitializer.java @@ -0,0 +1,46 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean22.cors; + +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpRequestDecoder; +import io.netty.handler.codec.http.HttpResponseEncoder; +import io.netty.handler.codec.http.cors.CorsConfig; +import io.netty.handler.codec.http.cors.CorsConfigBuilder; +import io.netty.handler.codec.http.cors.CorsHandler; +import io.netty.handler.stream.ChunkedWriteHandler; + +public class HttpCorsServerInitializer extends ChannelInitializer { + + @Override + public void initChannel(SocketChannel ch) { + + ChannelPipeline pipeline = ch.pipeline(); + pipeline.addLast(new HttpResponseEncoder()); + pipeline.addLast(new HttpRequestDecoder()); + pipeline.addLast(new HttpObjectAggregator(65536)); + pipeline.addLast(new ChunkedWriteHandler()); + + CorsConfig corsConfig = CorsConfigBuilder.forAnyOrigin().allowNullOrigin().allowCredentials().build(); + pipeline.addLast(new CorsHandler(corsConfig)); + + pipeline.addLast(new CustResponseHandler()); + } + +} diff --git a/learn-netty4/src/main/java/com/flydean23/socketserver/TestSocketHttpPage.java b/learn-netty4/src/main/java/com/flydean23/socketserver/TestSocketHttpPage.java new file mode 100644 index 0000000..007255c --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean23/socketserver/TestSocketHttpPage.java @@ -0,0 +1,49 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean23.socketserver; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.util.CharsetUtil; +import lombok.extern.slf4j.Slf4j; + +import java.io.*; +import java.net.URL; + +/** + * 读取html页面的数据,展示给用户 + */ +@Slf4j +public final class TestSocketHttpPage { + + public static ByteBuf getContent() throws IOException { + URL url= TestSocketHttpPage.class.getClassLoader().getResource("socket.html"); + log.info("url: {}",url); + String filePath = url.getFile(); + File file = new File(filePath); + log.info(file.getCanonicalPath()); + FileReader fileReader = new FileReader(file); + BufferedReader reader = new BufferedReader(fileReader); + StringBuilder builder= new StringBuilder(); + reader.lines().forEach(builder::append); + reader.close(); + fileReader.close(); + return Unpooled.copiedBuffer(builder.toString(), CharsetUtil.UTF_8); + } + + private TestSocketHttpPage() { + } +} diff --git a/learn-netty4/src/main/java/com/flydean23/socketserver/TestSocketServer.java b/learn-netty4/src/main/java/com/flydean23/socketserver/TestSocketServer.java new file mode 100644 index 0000000..49fcd66 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean23/socketserver/TestSocketServer.java @@ -0,0 +1,51 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean23.socketserver; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import lombok.extern.slf4j.Slf4j; + + +@Slf4j +public final class TestSocketServer { + + static final int PORT = Integer.parseInt(System.getProperty("port", "8000")); + + public static void main(String[] args) throws Exception { + + EventLoopGroup bossGroup = new NioEventLoopGroup(1); + EventLoopGroup workerGroup = new NioEventLoopGroup(); + try { + ServerBootstrap b = new ServerBootstrap(); + b.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .childHandler(new TestSocketServerInitializer()); + + Channel ch = b.bind(PORT).sync().channel(); + + log.info("使用你的浏览器访问:" + "http://127.0.0.1:" + PORT + '/'); + + ch.closeFuture().sync(); + } finally { + bossGroup.shutdownGracefully(); + workerGroup.shutdownGracefully(); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean23/socketserver/TestSocketServerHandler.java b/learn-netty4/src/main/java/com/flydean23/socketserver/TestSocketServerHandler.java new file mode 100644 index 0000000..47d4ce4 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean23/socketserver/TestSocketServerHandler.java @@ -0,0 +1,145 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean23.socketserver; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http.*; +import io.netty.handler.codec.http.websocketx.*; +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; + +import static io.netty.handler.codec.http.HttpMethod.GET; +import static io.netty.handler.codec.http.HttpResponseStatus.*; + +/** + * 同时处理消息和 + */ +@Slf4j +public class TestSocketServerHandler extends SimpleChannelInboundHandler { + + private static final String WEBSOCKET_PATH = "/websocket"; + + private WebSocketServerHandshaker handshaker; + + @Override + public void channelRead0(ChannelHandlerContext ctx, Object msg) throws IOException { + //根据消息类型,处理两种不同的消息 + if (msg instanceof FullHttpRequest) { + handleHttpRequest(ctx, (FullHttpRequest) msg); + } else if (msg instanceof WebSocketFrame) { + handleWebSocketFrame(ctx, (WebSocketFrame) msg); + } + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + ctx.flush(); + } + + private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest req) throws IOException { + // 处理异常 + if (!req.decoderResult().isSuccess()) { + sendHttpResponse(ctx, req, new DefaultFullHttpResponse(req.protocolVersion(), BAD_REQUEST, + ctx.alloc().buffer(0))); + return; + } + + // 只允许get请求 + if (!GET.equals(req.method())) { + sendHttpResponse(ctx, req, new DefaultFullHttpResponse(req.protocolVersion(), FORBIDDEN, + ctx.alloc().buffer(0))); + return; + } + + // 发送测试页面 + if ("/".equals(req.uri())) { + ByteBuf content = TestSocketHttpPage.getContent(); + FullHttpResponse res = new DefaultFullHttpResponse(req.protocolVersion(), OK, content); + + res.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8"); + HttpUtil.setContentLength(res, content.readableBytes()); + + sendHttpResponse(ctx, req, res); + return; + } + + // websocket握手 + WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory( + getWebSocketLocation(req), null, true, 5 * 1024 * 1024); + handshaker = wsFactory.newHandshaker(req); + if (handshaker == null) { + WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel()); + } else { + handshaker.handshake(ctx.channel(), req); + } + } + + private void handleWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) { + + // 处理各种websocket的frame信息 + if (frame instanceof CloseWebSocketFrame) { + handshaker.close(ctx, (CloseWebSocketFrame) frame.retain()); + return; + } + if (frame instanceof PingWebSocketFrame) { + ctx.write(new PongWebSocketFrame(frame.content().retain())); + return; + } + if (frame instanceof TextWebSocketFrame) { + // 直接返回 + ctx.write(frame.retain()); + return; + } + if (frame instanceof BinaryWebSocketFrame) { + // 直接返回 + ctx.write(frame.retain()); + } + } + + private static void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest req, FullHttpResponse res) { + // 生成错误页面 + HttpResponseStatus responseStatus = res.status(); + if (responseStatus.code() != 200) { + ByteBufUtil.writeUtf8(res.content(), responseStatus.toString()); + HttpUtil.setContentLength(res, res.content().readableBytes()); + } + // 发送response + boolean keepAlive = HttpUtil.isKeepAlive(req) && responseStatus.code() == 200; + HttpUtil.setKeepAlive(res, keepAlive); + ChannelFuture future = ctx.write(res); + if (!keepAlive) { + future.addListener(ChannelFutureListener.CLOSE); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + // 异常处理 + log.error("出现异常",cause); + ctx.close(); + } + + private static String getWebSocketLocation(FullHttpRequest req) { + String location = req.headers().get(HttpHeaderNames.HOST) + WEBSOCKET_PATH; + return "ws://" + location; + } +} diff --git a/learn-netty4/src/main/java/com/flydean23/socketserver/TestSocketServerInitializer.java b/learn-netty4/src/main/java/com/flydean23/socketserver/TestSocketServerInitializer.java new file mode 100644 index 0000000..fd1bc51 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean23/socketserver/TestSocketServerInitializer.java @@ -0,0 +1,36 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean23.socketserver; + +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpServerCodec; + +/** + */ +public class TestSocketServerInitializer extends ChannelInitializer { + + @Override + public void initChannel(SocketChannel ch) throws Exception { + ChannelPipeline pipeline = ch.pipeline(); + + pipeline.addLast(new HttpServerCodec()); + pipeline.addLast(new HttpObjectAggregator(65536)); + pipeline.addLast(new TestSocketServerHandler()); + } +} diff --git a/learn-netty4/src/main/java/com/flydean24/socketserver2/Server2FrameHandler.java b/learn-netty4/src/main/java/com/flydean24/socketserver2/Server2FrameHandler.java new file mode 100644 index 0000000..bd574c1 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean24/socketserver2/Server2FrameHandler.java @@ -0,0 +1,42 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean24.socketserver2; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; +import io.netty.handler.codec.http.websocketx.WebSocketFrame; + +import java.util.Locale; + +/** + * 专门处理WebSocketFrame + */ +public class Server2FrameHandler extends SimpleChannelInboundHandler { + + @Override + protected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame frame) throws Exception { + + if (frame instanceof TextWebSocketFrame) { + // 将接收到的消息转换成为大写 + String request = ((TextWebSocketFrame) frame).text(); + ctx.channel().writeAndFlush(new TextWebSocketFrame(request.toUpperCase(Locale.CHINA))); + } else { + String message = "不支持的Frame类型: " + frame.getClass().getName(); + throw new UnsupportedOperationException(message); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean24/socketserver2/Server2HttpHandler.java b/learn-netty4/src/main/java/com/flydean24/socketserver2/Server2HttpHandler.java new file mode 100644 index 0000000..2688038 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean24/socketserver2/Server2HttpHandler.java @@ -0,0 +1,88 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean24.socketserver2; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.channel.*; +import io.netty.handler.codec.http.*; +import lombok.extern.slf4j.Slf4j; + +import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE; +import static io.netty.handler.codec.http.HttpMethod.GET; +import static io.netty.handler.codec.http.HttpResponseStatus.*; + +/** + * 专门处理HTTP请求 + */ +@Slf4j +public class Server2HttpHandler extends SimpleChannelInboundHandler { + + @Override + protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest req) throws Exception { + // 处理异常请求 + if (!req.decoderResult().isSuccess()) { + sendHttpResponse(ctx, req, new DefaultFullHttpResponse(req.protocolVersion(), BAD_REQUEST, + ctx.alloc().buffer(0))); + return; + } + + // 只运行get方法 + if (!GET.equals(req.method())) { + sendHttpResponse(ctx, req, new DefaultFullHttpResponse(req.protocolVersion(), FORBIDDEN, + ctx.alloc().buffer(0))); + return; + } + + // index页面 + if ("/".equals(req.uri()) || "/index.html".equals(req.uri())) { + ByteBuf content = Server2HttpPage.getContent(); + FullHttpResponse res = new DefaultFullHttpResponse(req.protocolVersion(), OK, content); + + res.headers().set(CONTENT_TYPE, "text/html; charset=UTF-8"); + HttpUtil.setContentLength(res, content.readableBytes()); + + sendHttpResponse(ctx, req, res); + } else { + sendHttpResponse(ctx, req, new DefaultFullHttpResponse(req.protocolVersion(), NOT_FOUND, + ctx.alloc().buffer(0))); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + // 异常处理 + log.error("出现异常",cause); + ctx.close(); + } + + private static void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest req, FullHttpResponse res) { + // 生成error页面 + HttpResponseStatus responseStatus = res.status(); + if (responseStatus.code() != 200) { + ByteBufUtil.writeUtf8(res.content(), responseStatus.toString()); + HttpUtil.setContentLength(res, res.content().readableBytes()); + } + // 发送响应并关闭连接 + boolean keepAlive = HttpUtil.isKeepAlive(req) && responseStatus.code() == 200; + HttpUtil.setKeepAlive(res, keepAlive); + ChannelFuture future = ctx.writeAndFlush(res); + if (!keepAlive) { + future.addListener(ChannelFutureListener.CLOSE); + } + } + +} diff --git a/learn-netty4/src/main/java/com/flydean24/socketserver2/Server2HttpPage.java b/learn-netty4/src/main/java/com/flydean24/socketserver2/Server2HttpPage.java new file mode 100644 index 0000000..6c849a1 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean24/socketserver2/Server2HttpPage.java @@ -0,0 +1,49 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean24.socketserver2; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.util.CharsetUtil; +import lombok.extern.slf4j.Slf4j; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.net.URL; + +@Slf4j +public final class Server2HttpPage { + + public static ByteBuf getContent() throws IOException { + URL url= Server2HttpPage.class.getClassLoader().getResource("socket2.html"); + log.info("url: {}",url); + String filePath = url.getFile(); + File file = new File(filePath); + log.info(file.getCanonicalPath()); + FileReader fileReader = new FileReader(file); + BufferedReader reader = new BufferedReader(fileReader); + StringBuilder builder= new StringBuilder(); + reader.lines().forEach(builder::append); + reader.close(); + fileReader.close(); + return Unpooled.copiedBuffer(builder.toString(), CharsetUtil.UTF_8); + } + + private Server2HttpPage() { + } +} diff --git a/learn-netty4/src/main/java/com/flydean24/socketserver2/Server2Initializer.java b/learn-netty4/src/main/java/com/flydean24/socketserver2/Server2Initializer.java new file mode 100644 index 0000000..3ee7112 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean24/socketserver2/Server2Initializer.java @@ -0,0 +1,43 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean24.socketserver2; + +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler; +import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketServerCompressionHandler; + +/** + */ +public class Server2Initializer extends ChannelInitializer { + + private static final String WEBSOCKET_PATH = "/websocket"; + + @Override + public void initChannel(SocketChannel ch) throws Exception { + ChannelPipeline pipeline = ch.pipeline(); + + pipeline.addLast(new HttpServerCodec()); + pipeline.addLast(new HttpObjectAggregator(65536)); + pipeline.addLast(new WebSocketServerCompressionHandler()); + pipeline.addLast(new WebSocketServerProtocolHandler(WEBSOCKET_PATH, null, true)); + pipeline.addLast(new Server2HttpHandler()); + pipeline.addLast(new Server2FrameHandler()); + } +} diff --git a/learn-netty4/src/main/java/com/flydean24/socketserver2/Server2Server.java b/learn-netty4/src/main/java/com/flydean24/socketserver2/Server2Server.java new file mode 100644 index 0000000..194f9ac --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean24/socketserver2/Server2Server.java @@ -0,0 +1,53 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean24.socketserver2; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public final class Server2Server { + + static final int PORT = Integer.parseInt(System.getProperty("port", "8000")); + + public static void main(String[] args) throws Exception { + + EventLoopGroup bossGroup = new NioEventLoopGroup(1); + EventLoopGroup workerGroup = new NioEventLoopGroup(); + try { + ServerBootstrap b = new ServerBootstrap(); + b.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new Server2Initializer()); + + Channel ch = b.bind(PORT).sync().channel(); + + log.info("使用你的浏览器访问:" + "http://127.0.0.1:" + PORT + '/'); + + ch.closeFuture().sync(); + } finally { + bossGroup.shutdownGracefully(); + workerGroup.shutdownGracefully(); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean25/socketclient/TestSocketClient.java b/learn-netty4/src/main/java/com/flydean25/socketclient/TestSocketClient.java new file mode 100644 index 0000000..f830f24 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean25/socketclient/TestSocketClient.java @@ -0,0 +1,91 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean25.socketclient; + +import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.http.DefaultHttpHeaders; +import io.netty.handler.codec.http.HttpClientCodec; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.websocketx.*; +import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketClientCompressionHandler; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.URI; + +public final class TestSocketClient { + + static final String URL = System.getProperty("url", "ws://127.0.0.1:8000/websocket"); + + public static void main(String[] args) throws Exception { + URI uri = new URI(URL); + final int port = uri.getPort(); + + EventLoopGroup group = new NioEventLoopGroup(); + try { + TestSocketClientHandler handler = + new TestSocketClientHandler( + WebSocketClientHandshakerFactory.newHandshaker( + uri, WebSocketVersion.V13, null, true, new DefaultHttpHeaders())); + + Bootstrap b = new Bootstrap(); + b.group(group) + .channel(NioSocketChannel.class) + .handler(new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel ch) { + ChannelPipeline p = ch.pipeline(); + p.addLast( + new HttpClientCodec(), + new HttpObjectAggregator(8192), + WebSocketClientCompressionHandler.INSTANCE, + handler); + } + }); + + Channel ch = b.connect(uri.getHost(), port).sync().channel(); + handler.handshakeFuture().sync(); + + BufferedReader console = new BufferedReader(new InputStreamReader(System.in)); + while (true) { + String msg = console.readLine(); + if (msg == null) { + break; + } else if ("再见".equalsIgnoreCase(msg)) { + ch.writeAndFlush(new CloseWebSocketFrame()); + ch.closeFuture().sync(); + break; + } else if ("ping".equalsIgnoreCase(msg)) { + WebSocketFrame frame = new PingWebSocketFrame(Unpooled.wrappedBuffer(new byte[] { 8, 1, 8, 1 })); + ch.writeAndFlush(frame); + } else { + WebSocketFrame frame = new TextWebSocketFrame(msg); + ch.writeAndFlush(frame); + } + } + } finally { + group.shutdownGracefully(); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean25/socketclient/TestSocketClientHandler.java b/learn-netty4/src/main/java/com/flydean25/socketclient/TestSocketClientHandler.java new file mode 100644 index 0000000..3b47940 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean25/socketclient/TestSocketClientHandler.java @@ -0,0 +1,96 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean25.socketclient; + +import io.netty.channel.*; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.websocketx.*; +import io.netty.util.CharsetUtil; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class TestSocketClientHandler extends SimpleChannelInboundHandler { + + private final WebSocketClientHandshaker handshaker; + private ChannelPromise handshakeFuture; + + public TestSocketClientHandler(WebSocketClientHandshaker handshaker) { + this.handshaker = handshaker; + } + + public ChannelFuture handshakeFuture() { + return handshakeFuture; + } + + @Override + public void handlerAdded(ChannelHandlerContext ctx) { + handshakeFuture = ctx.newPromise(); + } + + @Override + public void channelActive(ChannelHandlerContext ctx) { + handshaker.handshake(ctx.channel()); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) { + log.info("channelInactive!"); + } + + @Override + public void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception { + Channel ch = ctx.channel(); + if (!handshaker.isHandshakeComplete()) { + try { + handshaker.finishHandshake(ch, (FullHttpResponse) msg); + log.info("websocket Handshake 完成!"); + handshakeFuture.setSuccess(); + } catch (WebSocketHandshakeException e) { + log.info("websocket连接失败!"); + handshakeFuture.setFailure(e); + } + return; + } + + if (msg instanceof FullHttpResponse) { + FullHttpResponse response = (FullHttpResponse) msg; + throw new IllegalStateException( + "Unexpected FullHttpResponse (getStatus=" + response.status() + + ", content=" + response.content().toString(CharsetUtil.UTF_8) + ')'); + } + + WebSocketFrame frame = (WebSocketFrame) msg; + if (frame instanceof TextWebSocketFrame) { + TextWebSocketFrame textFrame = (TextWebSocketFrame) frame; + log.info("接收到TXT消息: " + textFrame.text()); + } else if (frame instanceof PongWebSocketFrame) { + log.info("接收到pong消息"); + } else if (frame instanceof CloseWebSocketFrame) { + log.info("接收到closing消息"); + ch.close(); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + // 异常处理 + log.error("出现异常",cause); + if (!handshakeFuture.isDone()) { + handshakeFuture.setFailure(cause); + } + ctx.close(); + } +} diff --git a/learn-netty4/src/main/java/com/flydean26/http2server/CustHttp1Handler.java b/learn-netty4/src/main/java/com/flydean26/http2server/CustHttp1Handler.java new file mode 100644 index 0000000..fd97642 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean26/http2server/CustHttp1Handler.java @@ -0,0 +1,88 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean26.http2server; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpUtil; +import lombok.extern.slf4j.Slf4j; + +import static io.netty.handler.codec.http.HttpHeaderNames.*; +import static io.netty.handler.codec.http.HttpHeaderValues.CLOSE; +import static io.netty.handler.codec.http.HttpHeaderValues.KEEP_ALIVE; +import static io.netty.handler.codec.http.HttpResponseStatus.CONTINUE; +import static io.netty.handler.codec.http.HttpResponseStatus.OK; +import static io.netty.handler.codec.http.HttpVersion.HTTP_1_0; +import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; +import static io.netty.util.internal.ObjectUtil.checkNotNull; + +/** + * 处理HTTP1协议 + */ +@Slf4j +public class CustHttp1Handler extends SimpleChannelInboundHandler { + private final String establishApproach; + + public CustHttp1Handler(String establishApproach) { + this.establishApproach = establishApproach; + } + + @Override + public void channelRead0(ChannelHandlerContext ctx, FullHttpRequest req) throws Exception { + if (HttpUtil.is100ContinueExpected(req)) { + ctx.write(new DefaultFullHttpResponse(HTTP_1_1, CONTINUE, Unpooled.EMPTY_BUFFER)); + } + boolean keepAlive = HttpUtil.isKeepAlive(req); + + ByteBuf content = ctx.alloc().buffer(); + content.writeBytes(CustHttp2Handler.RESPONSE_BYTES.duplicate()); + ByteBufUtil.writeUtf8(content, " - 使用 " + req.protocolVersion() + " (" + establishApproach + ")"); + + FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK, content); + response.headers().set(CONTENT_TYPE, "text/plain; charset=UTF-8"); + response.headers().setInt(CONTENT_LENGTH, response.content().readableBytes()); + + if (keepAlive) { + if (req.protocolVersion().equals(HTTP_1_0)) { + response.headers().set(CONNECTION, KEEP_ALIVE); + } + ctx.write(response); + } else { + // 关闭连接 + response.headers().set(CONNECTION, CLOSE); + ctx.write(response).addListener(ChannelFutureListener.CLOSE); + } + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { + ctx.flush(); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + // 异常处理 + log.error("出现异常",cause); + ctx.close(); + } +} diff --git a/learn-netty4/src/main/java/com/flydean26/http2server/CustHttp2Handler.java b/learn-netty4/src/main/java/com/flydean26/http2server/CustHttp2Handler.java new file mode 100644 index 0000000..c79c7ff --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean26/http2server/CustHttp2Handler.java @@ -0,0 +1,155 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean26.http2server; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.*; +import io.netty.handler.codec.http2.*; +import io.netty.util.CharsetUtil; +import lombok.extern.slf4j.Slf4j; + +import static io.netty.buffer.Unpooled.copiedBuffer; +import static io.netty.buffer.Unpooled.unreleasableBuffer; +import static io.netty.handler.codec.http.HttpResponseStatus.OK; + +/** + * Http2 处理器 + */ +@Slf4j +public final class CustHttp2Handler extends Http2ConnectionHandler implements Http2FrameListener { + + static final ByteBuf RESPONSE_BYTES = unreleasableBuffer(copiedBuffer("我在使用HTTP2", CharsetUtil.UTF_8)); + + CustHttp2Handler(Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder, + Http2Settings initialSettings) { + super(decoder, encoder, initialSettings); + } + + private static Http2Headers upgradeToHttp2Headers(FullHttpRequest request) { + CharSequence host = request.headers().get(HttpHeaderNames.HOST); + Http2Headers http2Headers = new DefaultHttp2Headers() + .method(HttpMethod.GET.asciiName()) + .path(request.uri()) + .scheme(HttpScheme.HTTP.name()); + if (host != null) { + http2Headers.authority(host); + } + return http2Headers; + } + + /** + * 处理HTTP upgrade事件 + */ + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt instanceof HttpServerUpgradeHandler.UpgradeEvent) { + HttpServerUpgradeHandler.UpgradeEvent upgradeEvent = + (HttpServerUpgradeHandler.UpgradeEvent) evt; + onHeadersRead(ctx, 1, upgradeToHttp2Headers(upgradeEvent.upgradeRequest()), 0 , true); + } + super.userEventTriggered(ctx, evt); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + super.exceptionCaught(ctx, cause); + // 异常处理 + log.error("出现异常",cause); + ctx.close(); + } + + /** + * 发送响应数据到客户端 + */ + private void sendResponse(ChannelHandlerContext ctx, int streamId, ByteBuf payload) { + Http2Headers headers = new DefaultHttp2Headers().status(OK.codeAsText()); + //支持中文 + headers.set(HttpHeaderNames.CONTENT_TYPE,"text/plain;charset=utf-8"); + encoder().writeHeaders(ctx, streamId, headers, 0, false, ctx.newPromise()); + encoder().writeData(ctx, streamId, payload, 0, true, ctx.newPromise()); + } + + @Override + public int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding, boolean endOfStream) { + int processed = data.readableBytes() + padding; + if (endOfStream) { + sendResponse(ctx, streamId, data.retain()); + } + return processed; + } + + @Override + public void onHeadersRead(ChannelHandlerContext ctx, int streamId, + Http2Headers headers, int padding, boolean endOfStream) { + if (endOfStream) { + ByteBuf content = ctx.alloc().buffer(); + content.writeBytes(RESPONSE_BYTES.duplicate()); + ByteBufUtil.writeUtf8(content, " - 使用 HTTP/2"); + sendResponse(ctx, streamId, content); + } + } + + @Override + public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int streamDependency, + short weight, boolean exclusive, int padding, boolean endOfStream) { + onHeadersRead(ctx, streamId, headers, padding, endOfStream); + } + + @Override + public void onPriorityRead(ChannelHandlerContext ctx, int streamId, int streamDependency, + short weight, boolean exclusive) { + } + + @Override + public void onRstStreamRead(ChannelHandlerContext ctx, int streamId, long errorCode) { + } + + @Override + public void onSettingsAckRead(ChannelHandlerContext ctx) { + } + + @Override + public void onSettingsRead(ChannelHandlerContext ctx, Http2Settings settings) { + } + + @Override + public void onPingRead(ChannelHandlerContext ctx, long data) { + } + + @Override + public void onPingAckRead(ChannelHandlerContext ctx, long data) { + } + + @Override + public void onPushPromiseRead(ChannelHandlerContext ctx, int streamId, int promisedStreamId, + Http2Headers headers, int padding) { + } + + @Override + public void onGoAwayRead(ChannelHandlerContext ctx, int lastStreamId, long errorCode, ByteBuf debugData) { + } + + @Override + public void onWindowUpdateRead(ChannelHandlerContext ctx, int streamId, int windowSizeIncrement) { + } + + @Override + public void onUnknownFrame(ChannelHandlerContext ctx, byte frameType, int streamId, + Http2Flags flags, ByteBuf payload) { + } +} diff --git a/learn-netty4/src/main/java/com/flydean26/http2server/CustHttp2HandlerBuilder.java b/learn-netty4/src/main/java/com/flydean26/http2server/CustHttp2HandlerBuilder.java new file mode 100644 index 0000000..0dee793 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean26/http2server/CustHttp2HandlerBuilder.java @@ -0,0 +1,44 @@ + +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean26.http2server; + +import io.netty.handler.codec.http2.*; + +import static io.netty.handler.logging.LogLevel.INFO; + +public final class CustHttp2HandlerBuilder + extends AbstractHttp2ConnectionHandlerBuilder { + + private static final Http2FrameLogger logger = new Http2FrameLogger(INFO, CustHttp2Handler.class); + + public CustHttp2HandlerBuilder() { + frameLogger(logger); + } + + @Override + public CustHttp2Handler build() { + return super.build(); + } + + @Override + protected CustHttp2Handler build(Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder, + Http2Settings initialSettings) { + CustHttp2Handler handler = new CustHttp2Handler(decoder, encoder, initialSettings); + frameListener(handler); + return handler; + } +} diff --git a/learn-netty4/src/main/java/com/flydean26/http2server/CustHttp2Server.java b/learn-netty4/src/main/java/com/flydean26/http2server/CustHttp2Server.java new file mode 100644 index 0000000..e2f12d9 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean26/http2server/CustHttp2Server.java @@ -0,0 +1,87 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean26.http2server; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.codec.http2.Http2SecurityUtil; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import io.netty.handler.ssl.*; +import io.netty.handler.ssl.ApplicationProtocolConfig.Protocol; +import io.netty.handler.ssl.ApplicationProtocolConfig.SelectedListenerFailureBehavior; +import io.netty.handler.ssl.ApplicationProtocolConfig.SelectorFailureBehavior; +import io.netty.handler.ssl.util.SelfSignedCertificate; +import lombok.extern.slf4j.Slf4j; + +/** + * HTTP2服务器 + */ +@Slf4j +public final class CustHttp2Server { + + static final boolean SSL = true; + + static final int PORT = Integer.parseInt(System.getProperty("port", SSL? "8443" : "8000")); + + public static void main(String[] args) throws Exception { + // 配置SSL + final SslContext sslCtx; + if (SSL) { + SslProvider provider = SslProvider.isAlpnSupported(SslProvider.OPENSSL) ? SslProvider.OPENSSL : SslProvider.JDK; + log.info("provider:{}",provider); + SelfSignedCertificate ssc = new SelfSignedCertificate(); + sslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) + .sslProvider(provider) + //支持的cipher + .ciphers(Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE) + .applicationProtocolConfig(new ApplicationProtocolConfig( + Protocol.ALPN, + // 目前 OpenSsl 和 JDK providers只支持NO_ADVERTISE + SelectorFailureBehavior.NO_ADVERTISE, + // 目前 OpenSsl 和 JDK providers只支持ACCEPT + SelectedListenerFailureBehavior.ACCEPT, + ApplicationProtocolNames.HTTP_2, + ApplicationProtocolNames.HTTP_1_1)) + .build(); + } else { + sslCtx = null; + } + // 配置服务器 + EventLoopGroup group = new NioEventLoopGroup(); + try { + ServerBootstrap b = new ServerBootstrap(); + b.option(ChannelOption.SO_BACKLOG, 1024); + b.group(group) + .channel(NioServerSocketChannel.class) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new CustHttp2ServerInitializer(sslCtx)); + + Channel ch = b.bind(PORT).sync().channel(); + + log.info("使用支持HTTP/2的浏览器访问 " + + (SSL? "https" : "http") + "://127.0.0.1:" + PORT + '/'); + + ch.closeFuture().sync(); + } finally { + group.shutdownGracefully(); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean26/http2server/CustHttp2ServerInitializer.java b/learn-netty4/src/main/java/com/flydean26/http2server/CustHttp2ServerInitializer.java new file mode 100644 index 0000000..1a645cb --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean26/http2server/CustHttp2ServerInitializer.java @@ -0,0 +1,114 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean26.http2server; + +import io.netty.channel.*; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.http.HttpMessage; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.codec.http.HttpServerUpgradeHandler; +import io.netty.handler.codec.http.HttpServerUpgradeHandler.UpgradeCodecFactory; +import io.netty.handler.codec.http2.CleartextHttp2ServerUpgradeHandler; +import io.netty.handler.codec.http2.Http2CodecUtil; +import io.netty.handler.codec.http2.Http2ServerUpgradeCodec; +import io.netty.handler.ssl.SslContext; +import io.netty.util.AsciiString; +import io.netty.util.ReferenceCountUtil; +import lombok.extern.slf4j.Slf4j; + +import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; + +/** + * 初始化HTTP2 pipline + */ +@Slf4j +public class CustHttp2ServerInitializer extends ChannelInitializer { + + private static final UpgradeCodecFactory upgradeCodecFactory = protocol -> { + if (AsciiString.contentEquals(Http2CodecUtil.HTTP_UPGRADE_PROTOCOL_NAME, protocol)) { + return new Http2ServerUpgradeCodec(new CustHttp2HandlerBuilder().build()); + } else { + return null; + } + }; + + private final SslContext sslCtx; + private final int maxHttpContentLength; + + public CustHttp2ServerInitializer(SslContext sslCtx) { + this(sslCtx, 16 * 1024); + } + + public CustHttp2ServerInitializer(SslContext sslCtx, int maxHttpContentLength) { + this.sslCtx = sslCtx; + this.maxHttpContentLength = checkPositiveOrZero(maxHttpContentLength, "maxHttpContentLength"); + } + + @Override + public void initChannel(SocketChannel ch) { + if (sslCtx != null) { + configureSsl(ch); + } else { + configureClearText(ch); + } + } + + /** + * 为SSL配置自定义handler + */ + private void configureSsl(SocketChannel ch) { + ch.pipeline().addLast(sslCtx.newHandler(ch.alloc()), new CustProtocolNegotiationHandler()); + } + + /** + * 处理 HTTP1 升级到HTTP2 + */ + private void configureClearText(SocketChannel ch) { + final ChannelPipeline p = ch.pipeline(); + final HttpServerCodec sourceCodec = new HttpServerCodec(); + final HttpServerUpgradeHandler upgradeHandler = new HttpServerUpgradeHandler(sourceCodec, upgradeCodecFactory); + final CleartextHttp2ServerUpgradeHandler cleartextHttp2ServerUpgradeHandler = + new CleartextHttp2ServerUpgradeHandler(sourceCodec, upgradeHandler, + new CustHttp2HandlerBuilder().build()); + + p.addLast(cleartextHttp2ServerUpgradeHandler); + p.addLast(new SimpleChannelInboundHandler() { + @Override + protected void channelRead0(ChannelHandlerContext ctx, HttpMessage msg) throws Exception { + // 如果是HttpMessage,则说明是直接使用HTTP1,没有升级 + log.info("直接使用: " + msg.protocolVersion() + " (没有尝试升级)"); + ChannelPipeline pipeline = ctx.pipeline(); + pipeline.addAfter(ctx.name(), null, new CustHttp1Handler("直接使用,无升级")); + pipeline.replace(this, null, new HttpObjectAggregator(maxHttpContentLength)); + ctx.fireChannelRead(ReferenceCountUtil.retain(msg)); + } + }); + + p.addLast(new UserEventLogger()); + } + + /** + * 记录用户触发的事件 + */ + private static class UserEventLogger extends ChannelInboundHandlerAdapter { + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + log.info("用户触发事件: " + evt); + ctx.fireUserEventTriggered(evt); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean26/http2server/CustProtocolNegotiationHandler.java b/learn-netty4/src/main/java/com/flydean26/http2server/CustProtocolNegotiationHandler.java new file mode 100644 index 0000000..fec99b9 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean26/http2server/CustProtocolNegotiationHandler.java @@ -0,0 +1,51 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean26.http2server; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.ssl.ApplicationProtocolNames; +import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler; + +/** + * 自定义的ProtocolNegotiationHandler,根据传入的protocol不同,使用不同的HTTP handler + */ +public class CustProtocolNegotiationHandler extends ApplicationProtocolNegotiationHandler { + + private static final int MAX_CONTENT_LENGTH = 1024 * 100; + + protected CustProtocolNegotiationHandler() { + super(ApplicationProtocolNames.HTTP_1_1); + } + + @Override + protected void configurePipeline(ChannelHandlerContext ctx, String protocol) { + if (ApplicationProtocolNames.HTTP_2.equals(protocol)) { + ctx.pipeline().addLast(new CustHttp2HandlerBuilder().build()); + return; + } + + if (ApplicationProtocolNames.HTTP_1_1.equals(protocol)) { + ctx.pipeline().addLast(new HttpServerCodec(), + new HttpObjectAggregator(MAX_CONTENT_LENGTH), + new CustHttp1Handler("使用ALPN协议")); + return; + } + + throw new IllegalStateException("未知协议: " + protocol); + } +} diff --git a/learn-netty4/src/main/java/com/flydean30/http2client/CustHttp2Client.java b/learn-netty4/src/main/java/com/flydean30/http2client/CustHttp2Client.java new file mode 100644 index 0000000..f29c475 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean30/http2client/CustHttp2Client.java @@ -0,0 +1,132 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean30.http2client; + +import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.http.*; +import io.netty.handler.codec.http2.Http2SecurityUtil; +import io.netty.handler.codec.http2.HttpConversionUtil; +import io.netty.handler.ssl.*; +import io.netty.handler.ssl.ApplicationProtocolConfig.Protocol; +import io.netty.handler.ssl.ApplicationProtocolConfig.SelectedListenerFailureBehavior; +import io.netty.handler.ssl.ApplicationProtocolConfig.SelectorFailureBehavior; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import io.netty.util.AsciiString; +import io.netty.util.CharsetUtil; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.TimeUnit; + +import static io.netty.buffer.Unpooled.wrappedBuffer; +import static io.netty.handler.codec.http.HttpMethod.GET; +import static io.netty.handler.codec.http.HttpMethod.POST; +import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; + +/** + * 一个使用HTTP1-style InboundHttp2ToHttpAdapter方式发送HTTP2 frames的客户端 + */ +@Slf4j +public final class CustHttp2Client { + + static final boolean SSL = true; + static final String HOST = System.getProperty("host", "127.0.0.1"); + static final int PORT = Integer.parseInt(System.getProperty("port", SSL? "8443" : "8000")); + static final String GETURL = "/"; + static final String POSTURL = "/post"; + static final String POSTDATA = "我是POST数据"; + + public static void main(String[] args) throws Exception { + // SSL配置 + final SslContext sslCtx; + if (SSL) { + SslProvider provider = + SslProvider.isAlpnSupported(SslProvider.OPENSSL)? SslProvider.OPENSSL : SslProvider.JDK; + sslCtx = SslContextBuilder.forClient() + .sslProvider(provider) + .ciphers(Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE) + // 因为我们的证书是自生成的,所以需要信任放行 + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .applicationProtocolConfig(new ApplicationProtocolConfig( + Protocol.ALPN, + SelectorFailureBehavior.NO_ADVERTISE, + SelectedListenerFailureBehavior.ACCEPT, + ApplicationProtocolNames.HTTP_2, + ApplicationProtocolNames.HTTP_1_1)) + .build(); + } else { + sslCtx = null; + } + + EventLoopGroup workerGroup = new NioEventLoopGroup(); + CustHttp2ClientInitializer initializer = new CustHttp2ClientInitializer(sslCtx, Integer.MAX_VALUE); + + try { + // 配置client + Bootstrap b = new Bootstrap(); + b.group(workerGroup); + b.channel(NioSocketChannel.class); + b.option(ChannelOption.SO_KEEPALIVE, true); + b.remoteAddress(HOST, PORT); + b.handler(initializer); + + Channel channel = b.connect().syncUninterruptibly().channel(); + log.info("连接到 [" + HOST + ':' + PORT + ']'); + + //等待 HTTP/2 升级 + CustHttp2SettingsHandler http2SettingsHandler = initializer.settingsHandler(); + http2SettingsHandler.awaitSettings(5, TimeUnit.SECONDS); + + CustHttpResponseHandler responseHandler = initializer.responseHandler(); + int streamId = 3; + HttpScheme scheme = SSL ? HttpScheme.HTTPS : HttpScheme.HTTP; + AsciiString hostName = new AsciiString(HOST + ':' + PORT); + log.info("发送请求..."); + if (GETURL != null) { + // 创建一个get请求 + FullHttpRequest request = new DefaultFullHttpRequest(HTTP_1_1, GET, GETURL, Unpooled.EMPTY_BUFFER); + request.headers().add(HttpHeaderNames.HOST, hostName); + request.headers().add(HttpConversionUtil.ExtensionHeaderNames.SCHEME.text(), scheme.name()); + request.headers().add(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.GZIP); + request.headers().add(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.DEFLATE); + responseHandler.put(streamId, channel.write(request), channel.newPromise()); + streamId += 2; + } + if (POSTURL != null) { + // 创建一个post请求 + FullHttpRequest request = new DefaultFullHttpRequest(HTTP_1_1, POST, POSTURL, + wrappedBuffer(POSTDATA.getBytes(CharsetUtil.UTF_8))); + request.headers().add(HttpHeaderNames.HOST, hostName); + request.headers().add(HttpConversionUtil.ExtensionHeaderNames.SCHEME.text(), scheme.name()); + request.headers().add(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.GZIP); + request.headers().add(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.DEFLATE); + responseHandler.put(streamId, channel.write(request), channel.newPromise()); + } + channel.flush(); + responseHandler.awaitResponses(5, TimeUnit.SECONDS); + log.info("HTTP/2 请求完毕"); + + channel.close().syncUninterruptibly(); + } finally { + workerGroup.shutdownGracefully(); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean30/http2client/CustHttp2ClientInitializer.java b/learn-netty4/src/main/java/com/flydean30/http2client/CustHttp2ClientInitializer.java new file mode 100644 index 0000000..9372f55 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean30/http2client/CustHttp2ClientInitializer.java @@ -0,0 +1,127 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean30.http2client; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.http.*; +import io.netty.handler.codec.http2.*; +import io.netty.handler.ssl.ApplicationProtocolNames; +import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler; +import io.netty.handler.ssl.SslContext; +import lombok.extern.slf4j.Slf4j; + +import static io.netty.handler.logging.LogLevel.INFO; + +/** + * 初始化client,让其支持http2 + */ +@Slf4j +public class CustHttp2ClientInitializer extends ChannelInitializer { + private static final Http2FrameLogger logger = new Http2FrameLogger(INFO, CustHttp2ClientInitializer.class); + + private final SslContext sslCtx; + private final int maxContentLength; + private HttpToHttp2ConnectionHandler connectionHandler; + private CustHttpResponseHandler responseHandler; + private CustHttp2SettingsHandler settingsHandler; + + public CustHttp2ClientInitializer(SslContext sslCtx, int maxContentLength) { + this.sslCtx = sslCtx; + this.maxContentLength = maxContentLength; + } + + @Override + public void initChannel(SocketChannel ch) throws Exception { + Http2Connection connection = new DefaultHttp2Connection(false); + connectionHandler = new HttpToHttp2ConnectionHandlerBuilder() + .frameListener(new DelegatingDecompressorFrameListener( + connection, + new InboundHttp2ToHttpAdapterBuilder(connection) + .maxContentLength(maxContentLength) + .propagateSettings(true) + .build())) + .frameLogger(logger) + .connection(connection) + .build(); + responseHandler = new CustHttpResponseHandler(); + settingsHandler = new CustHttp2SettingsHandler(ch.newPromise()); + if (sslCtx != null) { + configureSsl(ch); + } else { + configureClearText(ch); + } + } + + public CustHttpResponseHandler responseHandler() { + return responseHandler; + } + + public CustHttp2SettingsHandler settingsHandler() { + return settingsHandler; + } + + /** + * 配置TLS的ProtocolNegotiationHandler + */ + private void configureSsl(SocketChannel ch) { + ChannelPipeline pipeline = ch.pipeline(); + // 指定TLS支持的host和port + pipeline.addLast(sslCtx.newHandler(ch.alloc(), CustHttp2Client.HOST, CustHttp2Client.PORT)); + // 握手中的ProtocolNegotiation + pipeline.addLast(new ApplicationProtocolNegotiationHandler("") { + @Override + protected void configurePipeline(ChannelHandlerContext ctx, String protocol) { + if (ApplicationProtocolNames.HTTP_2.equals(protocol)) { + ChannelPipeline p = ctx.pipeline(); + p.addLast(connectionHandler); + p.addLast(settingsHandler, responseHandler); + return; + } + ctx.close(); + throw new IllegalStateException("未知协议: " + protocol); + } + }); + } + + /** + * h2c 从 HTTP 升级到 HTTP/2. + */ + private void configureClearText(SocketChannel ch) { + HttpClientCodec sourceCodec = new HttpClientCodec(); + Http2ClientUpgradeCodec upgradeCodec = new Http2ClientUpgradeCodec(connectionHandler); + HttpClientUpgradeHandler upgradeHandler = new HttpClientUpgradeHandler(sourceCodec, upgradeCodec, 65536); + + ch.pipeline().addLast(sourceCodec, + upgradeHandler, + new CustUpgradeRequestHandler(this), + new UserEventLogger()); + } + + /** + * 记录用户触发的事件 + */ + private static class UserEventLogger extends ChannelInboundHandlerAdapter { + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + log.info("用户触发事件: " + evt); + ctx.fireUserEventTriggered(evt); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean30/http2client/CustHttp2SettingsHandler.java b/learn-netty4/src/main/java/com/flydean30/http2client/CustHttp2SettingsHandler.java new file mode 100644 index 0000000..3e4e2d1 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean30/http2client/CustHttp2SettingsHandler.java @@ -0,0 +1,56 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean30.http2client; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http2.Http2Settings; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.TimeUnit; + +/** + * 处理 Http2Setting + */ +@Slf4j +public class CustHttp2SettingsHandler extends SimpleChannelInboundHandler { + private final ChannelPromise promise; + + public CustHttp2SettingsHandler(ChannelPromise promise) { + this.promise = promise; + } + + /** + * 等待设置完毕 + */ + public void awaitSettings(long timeout, TimeUnit unit){ + if (!promise.awaitUninterruptibly(timeout, unit)) { + throw new IllegalStateException("设置时间超时"); + } + if (!promise.isSuccess()) { + throw new RuntimeException(promise.cause()); + } + } + + @Override + protected void channelRead0(ChannelHandlerContext ctx, Http2Settings msg) throws Exception { + log.info("接收到Http2Settings消息:{}",msg); + promise.setSuccess(); + //处理完毕,删除handler + ctx.pipeline().remove(this); + } +} diff --git a/learn-netty4/src/main/java/com/flydean30/http2client/CustHttpResponseHandler.java b/learn-netty4/src/main/java/com/flydean30/http2client/CustHttpResponseHandler.java new file mode 100644 index 0000000..14fd052 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean30/http2client/CustHttpResponseHandler.java @@ -0,0 +1,101 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean30.http2client; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http2.HttpConversionUtil; +import io.netty.util.CharsetUtil; +import io.netty.util.internal.PlatformDependent; +import lombok.extern.slf4j.Slf4j; + +import java.util.AbstractMap.SimpleEntry; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.TimeUnit; + +/** + * 处理 FullHttpResponse + */ +@Slf4j +public class CustHttpResponseHandler extends SimpleChannelInboundHandler { + + private final Map> streamidPromiseMap; + + public CustHttpResponseHandler() { + streamidPromiseMap = PlatformDependent.newConcurrentHashMap(); + } + + public void put(int streamId, ChannelFuture writeFuture, ChannelPromise promise) { + streamidPromiseMap.put(streamId, new SimpleEntry<>(writeFuture, promise)); + } + + /** + * 等待响应 + */ + public void awaitResponses(long timeout, TimeUnit unit) { + Iterator>> itr = streamidPromiseMap.entrySet().iterator(); + while (itr.hasNext()) { + Entry> entry = itr.next(); + ChannelFuture writeFuture = entry.getValue().getKey(); + if (!writeFuture.awaitUninterruptibly(timeout, unit)) { + throw new IllegalStateException("写入时间超时 stream id " + entry.getKey()); + } + if (!writeFuture.isSuccess()) { + throw new RuntimeException(writeFuture.cause()); + } + ChannelPromise promise = entry.getValue().getValue(); + if (!promise.awaitUninterruptibly(timeout, unit)) { + throw new IllegalStateException("等待响应超时 stream id " + entry.getKey()); + } + if (!promise.isSuccess()) { + throw new RuntimeException(promise.cause()); + } + log.info("接收到stream id={} 的数据",entry.getKey()); + itr.remove(); + } + } + + @Override + protected void channelRead0(ChannelHandlerContext ctx, FullHttpResponse msg) throws Exception { + Integer streamId = msg.headers().getInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text()); + if (streamId == null) { + log.info("接收到未知消息: {}" , msg); + return; + } + + Entry entry = streamidPromiseMap.get(streamId); + if (entry == null) { + log.info("接收到未知 stream id={} 的数据",streamId); + } else { + // 处理消息 + ByteBuf content = msg.content(); + if (content.isReadable()) { + int contentLength = content.readableBytes(); + byte[] arr = new byte[contentLength]; + content.readBytes(arr); + log.info("接收到数据:{}",new String(arr, 0, contentLength, CharsetUtil.UTF_8)); + } + + entry.getValue().setSuccess(); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean30/http2client/CustUpgradeRequestHandler.java b/learn-netty4/src/main/java/com/flydean30/http2client/CustUpgradeRequestHandler.java new file mode 100644 index 0000000..a8d2b5a --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean30/http2client/CustUpgradeRequestHandler.java @@ -0,0 +1,58 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean30.http2client; + +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.codec.http.DefaultFullHttpRequest; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpVersion; + +import java.net.InetSocketAddress; + +/** + * upgrade处理器 + */ +final class CustUpgradeRequestHandler extends ChannelInboundHandlerAdapter { + + private final CustHttp2ClientInitializer custHttp2ClientInitializer; + + public CustUpgradeRequestHandler(CustHttp2ClientInitializer custHttp2ClientInitializer) { + this.custHttp2ClientInitializer = custHttp2ClientInitializer; + } + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + DefaultFullHttpRequest upgradeRequest = + new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/", Unpooled.EMPTY_BUFFER); + + // 设置upgradeRequest的host地址 + InetSocketAddress remote = (InetSocketAddress) ctx.channel().remoteAddress(); + String hostString = remote.getHostString(); + if (hostString == null) { + hostString = remote.getAddress().getHostAddress(); + } + upgradeRequest.headers().set(HttpHeaderNames.HOST, hostString + ':' + remote.getPort()); + + ctx.writeAndFlush(upgradeRequest); + ctx.fireChannelActive(); + // 升级完毕,从pipeline中删除 + ctx.pipeline().remove(this); + ctx.pipeline().addLast(custHttp2ClientInitializer.settingsHandler(), custHttp2ClientInitializer.responseHandler()); + } +} diff --git a/learn-netty4/src/main/java/com/flydean31/http2framecodecserver/CustHttp1Handler.java b/learn-netty4/src/main/java/com/flydean31/http2framecodecserver/CustHttp1Handler.java new file mode 100644 index 0000000..58f0a2e --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean31/http2framecodecserver/CustHttp1Handler.java @@ -0,0 +1,93 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean31.http2framecodecserver; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpUtil; +import io.netty.util.CharsetUtil; +import lombok.extern.slf4j.Slf4j; + +import static io.netty.buffer.Unpooled.copiedBuffer; +import static io.netty.buffer.Unpooled.unreleasableBuffer; +import static io.netty.handler.codec.http.HttpHeaderNames.*; +import static io.netty.handler.codec.http.HttpHeaderValues.CLOSE; +import static io.netty.handler.codec.http.HttpHeaderValues.KEEP_ALIVE; +import static io.netty.handler.codec.http.HttpResponseStatus.CONTINUE; +import static io.netty.handler.codec.http.HttpResponseStatus.OK; +import static io.netty.handler.codec.http.HttpVersion.HTTP_1_0; +import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; + +/** + * 处理HTTP1协议 + */ +@Slf4j +public class CustHttp1Handler extends SimpleChannelInboundHandler { + + static final ByteBuf RESPONSE_BYTES = unreleasableBuffer(copiedBuffer("我在使用HTTP2", CharsetUtil.UTF_8)); + + private final String establishApproach; + + public CustHttp1Handler(String establishApproach) { + this.establishApproach = establishApproach; + } + + @Override + public void channelRead0(ChannelHandlerContext ctx, FullHttpRequest req) throws Exception { + if (HttpUtil.is100ContinueExpected(req)) { + ctx.write(new DefaultFullHttpResponse(HTTP_1_1, CONTINUE, Unpooled.EMPTY_BUFFER)); + } + boolean keepAlive = HttpUtil.isKeepAlive(req); + + ByteBuf content = ctx.alloc().buffer(); + content.writeBytes(RESPONSE_BYTES.duplicate()); + ByteBufUtil.writeAscii(content, " - 使用 " + req.protocolVersion() + " (" + establishApproach + ")"); + + FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK, content); + response.headers().set(CONTENT_TYPE, "text/plain; charset=UTF-8"); + response.headers().setInt(CONTENT_LENGTH, response.content().readableBytes()); + + if (keepAlive) { + if (req.protocolVersion().equals(HTTP_1_0)) { + response.headers().set(CONNECTION, KEEP_ALIVE); + } + ctx.write(response); + } else { + // 关闭连接 + response.headers().set(CONNECTION, CLOSE); + ctx.write(response).addListener(ChannelFutureListener.CLOSE); + } + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { + ctx.flush(); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + // 异常处理 + log.error("出现异常",cause); + ctx.close(); + } +} diff --git a/learn-netty4/src/main/java/com/flydean31/http2framecodecserver/CustHttp2Handler.java b/learn-netty4/src/main/java/com/flydean31/http2framecodecserver/CustHttp2Handler.java new file mode 100644 index 0000000..bb98167 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean31/http2framecodecserver/CustHttp2Handler.java @@ -0,0 +1,101 @@ + +package com.flydean31.http2framecodecserver; +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.channel.ChannelHandler.Sharable; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http2.*; +import io.netty.util.CharsetUtil; +import lombok.extern.slf4j.Slf4j; + +import static io.netty.buffer.Unpooled.copiedBuffer; +import static io.netty.buffer.Unpooled.unreleasableBuffer; +import static io.netty.handler.codec.http.HttpResponseStatus.OK; + +/** + * 和frame codec配合使用的Http2 处理器 + */ +@Slf4j +@Sharable +public class CustHttp2Handler extends Http2ChannelDuplexHandler { + + static final ByteBuf RESPONSE_BYTES = unreleasableBuffer(copiedBuffer("我在使用HTTP2", CharsetUtil.UTF_8)); + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + super.exceptionCaught(ctx, cause); + // 异常处理 + log.error("出现异常",cause); + ctx.close(); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (msg instanceof Http2HeadersFrame) { + onHeadersRead(ctx, (Http2HeadersFrame) msg); + } else if (msg instanceof Http2DataFrame) { + onDataRead(ctx, (Http2DataFrame) msg); + } else { + super.channelRead(ctx, msg); + } + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { + ctx.flush(); + } + + /** + * 处理data frame消息 + */ + private static void onDataRead(ChannelHandlerContext ctx, Http2DataFrame data){ + Http2FrameStream stream = data.stream(); + if (data.isEndStream()) { + sendResponse(ctx, stream, data.content()); + } else { + // 不是end stream不发送,但是需要释放引用 + data.release(); + } + // 处理完data,需要更新window frame,增加处理过的Data大小 + ctx.write(new DefaultHttp2WindowUpdateFrame(data.initialFlowControlledBytes()).stream(stream)); + } + + /** + * 处理header frame消息 + */ + private static void onHeadersRead(ChannelHandlerContext ctx, Http2HeadersFrame headers) { + if (headers.isEndStream()) { + ByteBuf content = ctx.alloc().buffer(); + content.writeBytes(RESPONSE_BYTES.duplicate()); + ByteBufUtil.writeUtf8(content, " - 使用 HTTP/2"); + sendResponse(ctx, headers.stream(), content); + } + } + + /** + * 发送响应到客户端 + */ + private static void sendResponse(ChannelHandlerContext ctx, Http2FrameStream stream, ByteBuf payload) { + Http2Headers headers = new DefaultHttp2Headers().status(OK.codeAsText()); + //支持中文 + headers.set(HttpHeaderNames.CONTENT_TYPE,"text/plain;charset=utf-8"); + ctx.write(new DefaultHttp2HeadersFrame(headers).stream(stream)); + ctx.write(new DefaultHttp2DataFrame(payload, true).stream(stream)); + } +} diff --git a/learn-netty4/src/main/java/com/flydean31/http2framecodecserver/CustHttp2Server.java b/learn-netty4/src/main/java/com/flydean31/http2framecodecserver/CustHttp2Server.java new file mode 100644 index 0000000..2b0dd0d --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean31/http2framecodecserver/CustHttp2Server.java @@ -0,0 +1,87 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean31.http2framecodecserver; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.codec.http2.Http2SecurityUtil; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import io.netty.handler.ssl.*; +import io.netty.handler.ssl.ApplicationProtocolConfig.Protocol; +import io.netty.handler.ssl.ApplicationProtocolConfig.SelectedListenerFailureBehavior; +import io.netty.handler.ssl.ApplicationProtocolConfig.SelectorFailureBehavior; +import io.netty.handler.ssl.util.SelfSignedCertificate; +import lombok.extern.slf4j.Slf4j; + +/** + * HTTP2服务器 + */ +@Slf4j +public final class CustHttp2Server { + + static final boolean SSL = true; + + static final int PORT = Integer.parseInt(System.getProperty("port", SSL? "8443" : "8000")); + + public static void main(String[] args) throws Exception { + // 配置SSL + final SslContext sslCtx; + if (SSL) { + SslProvider provider = SslProvider.isAlpnSupported(SslProvider.OPENSSL) ? SslProvider.OPENSSL : SslProvider.JDK; + log.info("provider:{}",provider); + SelfSignedCertificate ssc = new SelfSignedCertificate(); + sslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) + .sslProvider(provider) + //支持的cipher + .ciphers(Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE) + .applicationProtocolConfig(new ApplicationProtocolConfig( + Protocol.ALPN, + // 目前 OpenSsl 和 JDK providers只支持NO_ADVERTISE + SelectorFailureBehavior.NO_ADVERTISE, + // 目前 OpenSsl 和 JDK providers只支持ACCEPT + SelectedListenerFailureBehavior.ACCEPT, + ApplicationProtocolNames.HTTP_2, + ApplicationProtocolNames.HTTP_1_1)) + .build(); + } else { + sslCtx = null; + } + // 配置服务器 + EventLoopGroup group = new NioEventLoopGroup(); + try { + ServerBootstrap b = new ServerBootstrap(); + b.option(ChannelOption.SO_BACKLOG, 1024); + b.group(group) + .channel(NioServerSocketChannel.class) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new CustHttp2ServerInitializer(sslCtx)); + + Channel ch = b.bind(PORT).sync().channel(); + + log.info("使用支持HTTP/2的浏览器访问 " + + (SSL? "https" : "http") + "://127.0.0.1:" + PORT + '/'); + + ch.closeFuture().sync(); + } finally { + group.shutdownGracefully(); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean31/http2framecodecserver/CustHttp2ServerInitializer.java b/learn-netty4/src/main/java/com/flydean31/http2framecodecserver/CustHttp2ServerInitializer.java new file mode 100644 index 0000000..deb95f3 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean31/http2framecodecserver/CustHttp2ServerInitializer.java @@ -0,0 +1,112 @@ + +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean31.http2framecodecserver; + +import io.netty.channel.*; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.http.HttpMessage; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.codec.http.HttpServerUpgradeHandler; +import io.netty.handler.codec.http.HttpServerUpgradeHandler.UpgradeCodecFactory; +import io.netty.handler.codec.http2.Http2CodecUtil; +import io.netty.handler.codec.http2.Http2FrameCodecBuilder; +import io.netty.handler.codec.http2.Http2ServerUpgradeCodec; +import io.netty.handler.ssl.SslContext; +import io.netty.util.AsciiString; +import io.netty.util.ReferenceCountUtil; + +import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; + +/** + * Sets up the Netty pipeline for the example server. Depending on the endpoint config, sets up the + * pipeline for NPN or cleartext HTTP upgrade to HTTP/2. + */ +public class CustHttp2ServerInitializer extends ChannelInitializer { + + private static final UpgradeCodecFactory upgradeCodecFactory = protocol -> { + if (AsciiString.contentEquals(Http2CodecUtil.HTTP_UPGRADE_PROTOCOL_NAME, protocol)) { + return new Http2ServerUpgradeCodec( + Http2FrameCodecBuilder.forServer().build(), new CustHttp2Handler()); + } else { + return null; + } + }; + + private final SslContext sslCtx; + private final int maxHttpContentLength; + + public CustHttp2ServerInitializer(SslContext sslCtx) { + this(sslCtx, 16 * 1024); + } + + public CustHttp2ServerInitializer(SslContext sslCtx, int maxHttpContentLength) { + this.sslCtx = sslCtx; + this.maxHttpContentLength = checkPositiveOrZero(maxHttpContentLength, "maxHttpContentLength"); + } + + @Override + public void initChannel(SocketChannel ch) { + if (sslCtx != null) { + configureSsl(ch); + } else { + configureClearText(ch); + } + } + + /** + * Configure the pipeline for TLS NPN negotiation to HTTP/2. + */ + private void configureSsl(SocketChannel ch) { + ch.pipeline().addLast(sslCtx.newHandler(ch.alloc()), new CustProtocolNegotiationHandler()); + } + + /** + * Configure the pipeline for a cleartext upgrade from HTTP to HTTP/2.0 + */ + private void configureClearText(SocketChannel ch) { + final ChannelPipeline p = ch.pipeline(); + final HttpServerCodec sourceCodec = new HttpServerCodec(); + + p.addLast(sourceCodec); + p.addLast(new HttpServerUpgradeHandler(sourceCodec, upgradeCodecFactory)); + p.addLast(new SimpleChannelInboundHandler() { + @Override + protected void channelRead0(ChannelHandlerContext ctx, HttpMessage msg) throws Exception { + // If this handler is hit then no upgrade has been attempted and the client is just talking HTTP. + System.err.println("Directly talking: " + msg.protocolVersion() + " (no upgrade was attempted)"); + ChannelPipeline pipeline = ctx.pipeline(); + pipeline.addAfter(ctx.name(), null, new CustHttp1Handler("Direct. No Upgrade Attempted.")); + pipeline.replace(this, null, new HttpObjectAggregator(maxHttpContentLength)); + ctx.fireChannelRead(ReferenceCountUtil.retain(msg)); + } + }); + + p.addLast(new UserEventLogger()); + } + + /** + * Class that logs any User Events triggered on this channel. + */ + private static class UserEventLogger extends ChannelInboundHandlerAdapter { + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + System.out.println("User Event Triggered: " + evt); + ctx.fireUserEventTriggered(evt); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean31/http2framecodecserver/CustProtocolNegotiationHandler.java b/learn-netty4/src/main/java/com/flydean31/http2framecodecserver/CustProtocolNegotiationHandler.java new file mode 100644 index 0000000..eb465ea --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean31/http2framecodecserver/CustProtocolNegotiationHandler.java @@ -0,0 +1,52 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean31.http2framecodecserver; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.codec.http2.Http2FrameCodecBuilder; +import io.netty.handler.ssl.ApplicationProtocolNames; +import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler; + +/** + * 自定义的ProtocolNegotiationHandler,根据传入的protocol不同,使用不同的HTTP handler + */ +public class CustProtocolNegotiationHandler extends ApplicationProtocolNegotiationHandler { + + private static final int MAX_CONTENT_LENGTH = 1024 * 100; + + protected CustProtocolNegotiationHandler() { + super(ApplicationProtocolNames.HTTP_1_1); + } + + @Override + protected void configurePipeline(ChannelHandlerContext ctx, String protocol) { + if (ApplicationProtocolNames.HTTP_2.equals(protocol)) { + ctx.pipeline().addLast(Http2FrameCodecBuilder.forServer().build(), new CustHttp2Handler()); + return; + } + + if (ApplicationProtocolNames.HTTP_1_1.equals(protocol)) { + ctx.pipeline().addLast(new HttpServerCodec(), + new HttpObjectAggregator(MAX_CONTENT_LENGTH), + new CustHttp1Handler("使用ALPN协议")); + return; + } + + throw new IllegalStateException("未知协议: " + protocol); + } +} diff --git a/learn-netty4/src/main/java/com/flydean32/http2framecodecclient/Http2ClientFrameInitializer.java b/learn-netty4/src/main/java/com/flydean32/http2framecodecclient/Http2ClientFrameInitializer.java new file mode 100644 index 0000000..eab41de --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean32/http2framecodecclient/Http2ClientFrameInitializer.java @@ -0,0 +1,60 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean32.http2framecodecclient; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http2.Http2FrameCodec; +import io.netty.handler.codec.http2.Http2FrameCodecBuilder; +import io.netty.handler.codec.http2.Http2MultiplexHandler; +import io.netty.handler.codec.http2.Http2Settings; +import io.netty.handler.ssl.SslContext; +import lombok.extern.slf4j.Slf4j; + +/** + * 通过添加Http2FrameCodec和Http2MultiplexHandler,让客户端支持HTTP/2 frames. + */ +@Slf4j +public final class Http2ClientFrameInitializer extends ChannelInitializer { + + private final SslContext sslCtx; + + public Http2ClientFrameInitializer(SslContext sslCtx) { + this.sslCtx = sslCtx; + } + + @Override + protected void initChannel(Channel ch) throws Exception { + if (sslCtx != null) { + ch.pipeline().addFirst(sslCtx.newHandler(ch.alloc())); + } + + Http2FrameCodec http2FrameCodec = Http2FrameCodecBuilder.forClient() + .initialSettings(Http2Settings.defaultSettings()) + .build(); + ch.pipeline().addLast(http2FrameCodec); + ch.pipeline().addLast(new Http2MultiplexHandler(new SimpleChannelInboundHandler() { + @Override + protected void channelRead0(ChannelHandlerContext ctx, Object msg) { + // 处理inbound streams + log.info("Http2MultiplexHandler接收到消息: {}",msg); + } + })); + } + +} diff --git a/learn-netty4/src/main/java/com/flydean32/http2framecodecclient/Http2ClientStreamFrameHandler.java b/learn-netty4/src/main/java/com/flydean32/http2framecodecclient/Http2ClientStreamFrameHandler.java new file mode 100644 index 0000000..916fc5f --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean32/http2framecodecclient/Http2ClientStreamFrameHandler.java @@ -0,0 +1,65 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean32.http2framecodecclient; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http2.Http2DataFrame; +import io.netty.handler.codec.http2.Http2HeadersFrame; +import io.netty.handler.codec.http2.Http2StreamFrame; +import io.netty.util.CharsetUtil; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * 处理 Http2StreamFrame 响应. + */ +@Slf4j +public final class Http2ClientStreamFrameHandler extends SimpleChannelInboundHandler { + + private final CountDownLatch latch = new CountDownLatch(1); + + @Override + protected void channelRead0(ChannelHandlerContext ctx, Http2StreamFrame msg) throws Exception { + log.info("接收 Http2StreamFrame: {}",msg); + + // 判断stream是否endStream + if (msg instanceof Http2DataFrame && ((Http2DataFrame) msg).isEndStream()) { + log.info("Http2DataFrame接收到endStream flag"); + log.info("msg:{}",((Http2DataFrame) msg).content().getCharSequence(0,33, CharsetUtil.UTF_8)); + latch.countDown(); + } else if (msg instanceof Http2HeadersFrame && ((Http2HeadersFrame) msg).isEndStream()) { + log.info("Http2HeadersFrame接收到endStream flag"); + latch.countDown(); + } + } + + /** + * 等待latch countDown或者超时5秒钟 + * @return true 表示成功接收到了一个endStream + */ + public boolean responseSuccessfullyCompleted() { + try { + return latch.await(5, TimeUnit.SECONDS); + } catch (InterruptedException ie) { + log.error("Latch exception: {}" + ie.getMessage()); + return false; + } + } + +} diff --git a/learn-netty4/src/main/java/com/flydean32/http2framecodecclient/Http2FrameClient.java b/learn-netty4/src/main/java/com/flydean32/http2framecodecclient/Http2FrameClient.java new file mode 100644 index 0000000..503a96d --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean32/http2framecodecclient/Http2FrameClient.java @@ -0,0 +1,113 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean32.http2framecodecclient; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.http2.*; +import io.netty.handler.ssl.*; +import io.netty.handler.ssl.ApplicationProtocolConfig.Protocol; +import io.netty.handler.ssl.ApplicationProtocolConfig.SelectedListenerFailureBehavior; +import io.netty.handler.ssl.ApplicationProtocolConfig.SelectorFailureBehavior; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import lombok.extern.slf4j.Slf4j; + +/** + * 使用Http2FrameCodec发送HTTP2 frame的http2 client + */ +@Slf4j +public final class Http2FrameClient { + + static final boolean SSL = true; + static final String HOST = System.getProperty("host", "127.0.0.1"); + static final int PORT = Integer.parseInt(System.getProperty("port", SSL? "8443" : "8000")); + static final String PATH = System.getProperty("path", "/"); + + private Http2FrameClient() { + } + + public static void main(String[] args) throws Exception { + final EventLoopGroup clientWorkerGroup = new NioEventLoopGroup(); + + // SSL配置 + final SslContext sslCtx; + if (SSL) { + SslProvider provider = + SslProvider.isAlpnSupported(SslProvider.OPENSSL)? SslProvider.OPENSSL : SslProvider.JDK; + sslCtx = SslContextBuilder.forClient() + .sslProvider(provider) + .ciphers(Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE) + // 因为我们的证书是自生成的,所以需要信任放行 + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .applicationProtocolConfig(new ApplicationProtocolConfig( + Protocol.ALPN, + SelectorFailureBehavior.NO_ADVERTISE, + SelectedListenerFailureBehavior.ACCEPT, + ApplicationProtocolNames.HTTP_2, + ApplicationProtocolNames.HTTP_1_1)) + .build(); + } else { + sslCtx = null; + } + + try { + final Bootstrap b = new Bootstrap(); + b.group(clientWorkerGroup); + b.channel(NioSocketChannel.class); + b.option(ChannelOption.SO_KEEPALIVE, true); + b.remoteAddress(HOST, PORT); + b.handler(new Http2ClientFrameInitializer(sslCtx)); + + // 启动客户端 + final Channel channel = b.connect().syncUninterruptibly().channel(); + log.info("连接到 [" + HOST + ':' + PORT + ']'); + + //从当前channel中,新建一个streamChannel,发送请求消息 + final Http2ClientStreamFrameHandler streamFrameResponseHandler = + new Http2ClientStreamFrameHandler(); + + final Http2StreamChannelBootstrap streamChannelBootstrap = new Http2StreamChannelBootstrap(channel); + final Http2StreamChannel streamChannel = streamChannelBootstrap.open().syncUninterruptibly().getNow(); + streamChannel.pipeline().addLast(streamFrameResponseHandler); + + // 发送HTTP2 get请求 + final DefaultHttp2Headers headers = new DefaultHttp2Headers(); + headers.method("GET"); + headers.path(PATH); + headers.scheme(SSL? "https" : "http"); + Http2HeadersFrame headersFrame = new DefaultHttp2HeadersFrame(headers, true); + streamChannel.writeAndFlush(headersFrame); + log.info("发送 HTTP/2 GET 请求 " + PATH); + + // 等待响应结束或者超时 + if (!streamFrameResponseHandler.responseSuccessfullyCompleted()) { + log.info("在5s之内没有收到系统响应."); + } + + log.info("HTTP2请求结束,关闭连接."); + + // 关闭连接 + channel.close().syncUninterruptibly(); + } finally { + clientWorkerGroup.shutdownGracefully(); + } + } + +} diff --git a/learn-netty4/src/main/java/com/flydean33/http2multiplexserver/CustMultiplexHttp2Handler.java b/learn-netty4/src/main/java/com/flydean33/http2multiplexserver/CustMultiplexHttp2Handler.java new file mode 100644 index 0000000..2cc876b --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean33/http2multiplexserver/CustMultiplexHttp2Handler.java @@ -0,0 +1,100 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package com.flydean33.http2multiplexserver; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandler.Sharable; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http2.*; +import io.netty.util.CharsetUtil; +import lombok.extern.slf4j.Slf4j; + +import static io.netty.buffer.Unpooled.copiedBuffer; +import static io.netty.buffer.Unpooled.unreleasableBuffer; +import static io.netty.handler.codec.http.HttpResponseStatus.OK; + +/** + * 多路复用的http2 handler + */ +@Sharable +@Slf4j +public class CustMultiplexHttp2Handler extends ChannelDuplexHandler { + + static final ByteBuf RESPONSE_BYTES = unreleasableBuffer(copiedBuffer("我在使用http2呀", CharsetUtil.UTF_8)); + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + super.exceptionCaught(ctx, cause); + // 异常处理 + log.error("出现异常",cause); + ctx.close(); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (msg instanceof Http2HeadersFrame) { + onHeadersRead(ctx, (Http2HeadersFrame) msg); + } else if (msg instanceof Http2DataFrame) { + onDataRead(ctx, (Http2DataFrame) msg); + } else { + super.channelRead(ctx, msg); + } + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { + ctx.flush(); + } + + /** + * 处理Http2DataFrame数据 + */ + private static void onDataRead(ChannelHandlerContext ctx, Http2DataFrame data) { + if (data.isEndStream()) { + sendResponse(ctx, data.content()); + } else { + // 释放data + data.release(); + } + } + + /** + * 处理Http2HeadersFrame消息 + */ + private static void onHeadersRead(ChannelHandlerContext ctx, Http2HeadersFrame headers){ + if (headers.isEndStream()) { + ByteBuf content = ctx.alloc().buffer(); + content.writeBytes(RESPONSE_BYTES.duplicate()); + ByteBufUtil.writeUtf8(content, " - 使用 HTTP/2"); + sendResponse(ctx, content); + } + } + + /** + * 发送响应 + */ + private static void sendResponse(ChannelHandlerContext ctx, ByteBuf payload) { + Http2Headers headers = new DefaultHttp2Headers().status(OK.codeAsText()); + //支持中文 + headers.set(HttpHeaderNames.CONTENT_TYPE,"text/plain;charset=utf-8"); + ctx.write(new DefaultHttp2HeadersFrame(headers)); + ctx.write(new DefaultHttp2DataFrame(payload, true)); + } +} diff --git a/learn-netty4/src/main/java/com/flydean33/http2multiplexserver/MultiplexHttp2Server.java b/learn-netty4/src/main/java/com/flydean33/http2multiplexserver/MultiplexHttp2Server.java new file mode 100644 index 0000000..e5cfb59 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean33/http2multiplexserver/MultiplexHttp2Server.java @@ -0,0 +1,88 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package com.flydean33.http2multiplexserver; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.codec.http2.Http2SecurityUtil; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import io.netty.handler.ssl.*; +import io.netty.handler.ssl.ApplicationProtocolConfig.Protocol; +import io.netty.handler.ssl.ApplicationProtocolConfig.SelectedListenerFailureBehavior; +import io.netty.handler.ssl.ApplicationProtocolConfig.SelectorFailureBehavior; +import io.netty.handler.ssl.util.SelfSignedCertificate; +import lombok.extern.slf4j.Slf4j; + +/** + * 支持多路复用的http2 server + */ +@Slf4j +public final class MultiplexHttp2Server { + + static final boolean SSL = true; + + static final int PORT = Integer.parseInt(System.getProperty("port", SSL? "8443" : "8000")); + + public static void main(String[] args) throws Exception { + // 配置SSL + final SslContext sslCtx; + if (SSL) { + SslProvider provider = SslProvider.isAlpnSupported(SslProvider.OPENSSL) ? SslProvider.OPENSSL : SslProvider.JDK; + log.info("provider:{}",provider); + SelfSignedCertificate ssc = new SelfSignedCertificate(); + sslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) + .sslProvider(provider) + //支持的cipher + .ciphers(Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE) + .applicationProtocolConfig(new ApplicationProtocolConfig( + Protocol.ALPN, + // 目前 OpenSsl 和 JDK providers只支持NO_ADVERTISE + SelectorFailureBehavior.NO_ADVERTISE, + // 目前 OpenSsl 和 JDK providers只支持ACCEPT + SelectedListenerFailureBehavior.ACCEPT, + ApplicationProtocolNames.HTTP_2, + ApplicationProtocolNames.HTTP_1_1)) + .build(); + } else { + sslCtx = null; + } + // 配置server + EventLoopGroup group = new NioEventLoopGroup(); + try { + ServerBootstrap b = new ServerBootstrap(); + b.option(ChannelOption.SO_BACKLOG, 1024); + b.group(group) + .channel(NioServerSocketChannel.class) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new MultiplexHttp2ServerInitializer(sslCtx)); + + Channel ch = b.bind(PORT).sync().channel(); + + log.info("使用支持HTTP/2的浏览器访问 " + + (SSL? "https" : "http") + "://127.0.0.1:" + PORT + '/'); + + ch.closeFuture().sync(); + } finally { + group.shutdownGracefully(); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean33/http2multiplexserver/MultiplexHttp2ServerInitializer.java b/learn-netty4/src/main/java/com/flydean33/http2multiplexserver/MultiplexHttp2ServerInitializer.java new file mode 100644 index 0000000..0861edf --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean33/http2multiplexserver/MultiplexHttp2ServerInitializer.java @@ -0,0 +1,114 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package com.flydean33.http2multiplexserver; + +import com.flydean26.http2server.CustHttp1Handler; +import io.netty.channel.*; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.http.HttpMessage; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.codec.http.HttpServerUpgradeHandler; +import io.netty.handler.codec.http.HttpServerUpgradeHandler.UpgradeCodecFactory; +import io.netty.handler.codec.http2.Http2CodecUtil; +import io.netty.handler.codec.http2.Http2FrameCodecBuilder; +import io.netty.handler.codec.http2.Http2MultiplexHandler; +import io.netty.handler.codec.http2.Http2ServerUpgradeCodec; +import io.netty.handler.ssl.SslContext; +import io.netty.util.AsciiString; +import io.netty.util.ReferenceCountUtil; +import lombok.extern.slf4j.Slf4j; + +import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; + +/** + * 配置使用多路复用http2 + */ +@Slf4j +public class MultiplexHttp2ServerInitializer extends ChannelInitializer { + + private static final UpgradeCodecFactory upgradeCodecFactory = protocol -> { + if (AsciiString.contentEquals(Http2CodecUtil.HTTP_UPGRADE_PROTOCOL_NAME, protocol)) { + return new Http2ServerUpgradeCodec( + Http2FrameCodecBuilder.forServer().build(), + new Http2MultiplexHandler(new CustMultiplexHttp2Handler())); + } else { + return null; + } + }; + + private final SslContext sslCtx; + private final int maxHttpContentLength; + + public MultiplexHttp2ServerInitializer(SslContext sslCtx) { + this(sslCtx, 16 * 1024); + } + + public MultiplexHttp2ServerInitializer(SslContext sslCtx, int maxHttpContentLength) { + this.sslCtx = sslCtx; + this.maxHttpContentLength = checkPositiveOrZero(maxHttpContentLength, "maxHttpContentLength"); + } + + @Override + public void initChannel(SocketChannel ch) { + if (sslCtx != null) { + configureSsl(ch); + } else { + configureClearText(ch); + } + } + + /** + * 配置TLS的http2处理器 + */ + private void configureSsl(SocketChannel ch) { + ch.pipeline().addLast(sslCtx.newHandler(ch.alloc()), new MultiplexProtocolNegotiationHandler()); + } + + /** + * 配置 cleartext 从HTTP1.1 升级到 HTTP/2.0 + */ + private void configureClearText(SocketChannel ch) { + final ChannelPipeline p = ch.pipeline(); + final HttpServerCodec sourceCodec = new HttpServerCodec(); + p.addLast(sourceCodec); + p.addLast(new HttpServerUpgradeHandler(sourceCodec, upgradeCodecFactory)); + p.addLast(new SimpleChannelInboundHandler() { + @Override + protected void channelRead0(ChannelHandlerContext ctx, HttpMessage msg) { + // 如果是HttpMessage,则说明是直接使用HTTP1,没有升级 + log.info("直接使用: {}, 无升级版本" + msg.protocolVersion()); + ChannelPipeline pipeline = ctx.pipeline(); + pipeline.addAfter(ctx.name(), null, new CustHttp1Handler("直接使用,无升级")); + pipeline.replace(this, null, new HttpObjectAggregator(maxHttpContentLength)); + ctx.fireChannelRead(ReferenceCountUtil.retain(msg)); + } + }); + p.addLast(new UserEventLogger()); + } + + /** + * 记录用户触发的事件 + */ + private static class UserEventLogger extends ChannelInboundHandlerAdapter { + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + log.info("用户触发事件: " + evt); + ctx.fireUserEventTriggered(evt); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean33/http2multiplexserver/MultiplexProtocolNegotiationHandler.java b/learn-netty4/src/main/java/com/flydean33/http2multiplexserver/MultiplexProtocolNegotiationHandler.java new file mode 100644 index 0000000..84ef4d7 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean33/http2multiplexserver/MultiplexProtocolNegotiationHandler.java @@ -0,0 +1,56 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean33.http2multiplexserver; + +import com.flydean26.http2server.CustHttp1Handler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.codec.http2.Http2FrameCodecBuilder; +import io.netty.handler.codec.http2.Http2MultiplexHandler; +import io.netty.handler.ssl.ApplicationProtocolNames; +import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler; + +/** + * ProtocolNegotiationHandler + */ +public class MultiplexProtocolNegotiationHandler extends ApplicationProtocolNegotiationHandler { + + private static final int MAX_CONTENT_LENGTH = 1024 * 100; + + protected MultiplexProtocolNegotiationHandler() { + super(ApplicationProtocolNames.HTTP_1_1); + } + + @Override + protected void configurePipeline(ChannelHandlerContext ctx, String protocol) { + if (ApplicationProtocolNames.HTTP_2.equals(protocol)) { + //添加多路复用支持 + ctx.pipeline().addLast(Http2FrameCodecBuilder.forServer().build()); + ctx.pipeline().addLast(new Http2MultiplexHandler(new CustMultiplexHttp2Handler())); + return; + } + + if (ApplicationProtocolNames.HTTP_1_1.equals(protocol)) { + ctx.pipeline().addLast(new HttpServerCodec(), + new HttpObjectAggregator(MAX_CONTENT_LENGTH), + new CustHttp1Handler("ALPN Negotiation")); + return; + } + + throw new IllegalStateException("未知协议: " + protocol); + } +} diff --git a/learn-netty4/src/main/java/com/flydean34/http2images/CustProtocolNegotiationHandler.java b/learn-netty4/src/main/java/com/flydean34/http2images/CustProtocolNegotiationHandler.java new file mode 100644 index 0000000..2f902f2 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean34/http2images/CustProtocolNegotiationHandler.java @@ -0,0 +1,73 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package com.flydean34.http2images; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.codec.http2.DefaultHttp2Connection; +import io.netty.handler.codec.http2.HttpToHttp2ConnectionHandlerBuilder; +import io.netty.handler.codec.http2.InboundHttp2ToHttpAdapter; +import io.netty.handler.codec.http2.InboundHttp2ToHttpAdapterBuilder; +import io.netty.handler.ssl.ApplicationProtocolNames; +import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler; + +/** + * protocol协商处理器 + */ +public class CustProtocolNegotiationHandler extends ApplicationProtocolNegotiationHandler { + + private static final int MAX_CONTENT_LENGTH = 1024 * 100; + + protected CustProtocolNegotiationHandler() { + super(ApplicationProtocolNames.HTTP_1_1); + } + + @Override + protected void configurePipeline(ChannelHandlerContext ctx, String protocol) { + if (ApplicationProtocolNames.HTTP_2.equals(protocol)) { + configureHttp2(ctx); + return; + } + + if (ApplicationProtocolNames.HTTP_1_1.equals(protocol)) { + configureHttp1(ctx); + return; + } + + throw new IllegalStateException("未知协议: " + protocol); + } + + private static void configureHttp2(ChannelHandlerContext ctx) { + DefaultHttp2Connection connection = new DefaultHttp2Connection(true); + InboundHttp2ToHttpAdapter listener = new InboundHttp2ToHttpAdapterBuilder(connection) + .propagateSettings(true).validateHttpHeaders(false) + .maxContentLength(MAX_CONTENT_LENGTH).build(); + + ctx.pipeline().addLast(new HttpToHttp2ConnectionHandlerBuilder() + .frameListener(listener) + .connection(connection).build()); + + ctx.pipeline().addLast(new Http2RequestHandler()); + } + + private static void configureHttp1(ChannelHandlerContext ctx) { + ctx.pipeline().addLast(new HttpServerCodec(), + new HttpObjectAggregator(MAX_CONTENT_LENGTH), + new Http1RequestHandler()); + } +} diff --git a/learn-netty4/src/main/java/com/flydean34/http2images/Http1RequestHandler.java b/learn-netty4/src/main/java/com/flydean34/http2images/Http1RequestHandler.java new file mode 100644 index 0000000..fcd81ea --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean34/http2images/Http1RequestHandler.java @@ -0,0 +1,63 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package com.flydean34.http2images; + +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpUtil; + +import static io.netty.handler.codec.http.HttpHeaderNames.CONNECTION; +import static io.netty.handler.codec.http.HttpHeaderValues.CLOSE; +import static io.netty.handler.codec.http.HttpHeaderValues.KEEP_ALIVE; +import static io.netty.handler.codec.http.HttpResponseStatus.CONTINUE; +import static io.netty.handler.codec.http.HttpUtil.isKeepAlive; +import static io.netty.handler.codec.http.HttpVersion.HTTP_1_0; +import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; + +/** + * Http1.1处理器 + */ +public final class Http1RequestHandler extends Http2RequestHandler { + + @Override + protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception { + if (HttpUtil.is100ContinueExpected(request)) { + ctx.write(new DefaultFullHttpResponse(HTTP_1_1, CONTINUE, Unpooled.EMPTY_BUFFER)); + } + super.channelRead0(ctx, request); + } + + @Override + protected void sendResponse(final ChannelHandlerContext ctx, String streamId, + final FullHttpResponse response, final FullHttpRequest request) { + HttpUtil.setContentLength(response, response.content().readableBytes()); + if (isKeepAlive(request)) { + if (request.protocolVersion().equals(HTTP_1_0)) { + response.headers().set(CONNECTION, KEEP_ALIVE); + } + ctx.writeAndFlush(response); + } else { + // 关闭连接 + response.headers().set(CONNECTION, CLOSE); + ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean34/http2images/Http2RequestHandler.java b/learn-netty4/src/main/java/com/flydean34/http2images/Http2RequestHandler.java new file mode 100644 index 0000000..912dad6 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean34/http2images/Http2RequestHandler.java @@ -0,0 +1,108 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package com.flydean34.http2images; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.QueryStringDecoder; +import io.netty.handler.codec.http2.HttpConversionUtil; +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; +import java.util.List; + +import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE; +import static io.netty.handler.codec.http.HttpResponseStatus.OK; +import static io.netty.handler.codec.http.HttpUtil.setContentLength; +import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; +import static io.netty.util.internal.ObjectUtil.checkNotNull; +import static java.lang.Integer.parseInt; + +/** + * 支持http2的处理器,因为添加了InboundHttp2ToHttpAdapter对http2的消息格式进行了转换,所以这里处理的是FullHttpRequest + */ +@Slf4j +public class Http2RequestHandler extends SimpleChannelInboundHandler { + + private static final String IMAGE_ID = "id"; + + @Override + protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception { + QueryStringDecoder queryString = new QueryStringDecoder(request.uri()); + String streamId = getStreamId(request); + String id = getValue(queryString, IMAGE_ID); + if (id == null) { + handlePage(ctx, streamId, request); + } else { + handleImage(id, ctx, streamId, request); + } + } + + private void handleImage(String id, ChannelHandlerContext ctx, String streamId, + FullHttpRequest request) { + ByteBuf image = ImagePage.getImage(parseInt(id)); + FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK, image); + response.headers().set(CONTENT_TYPE, "image/jpeg"); + sendResponse(ctx, streamId, response, request); + } + + private void handlePage(ChannelHandlerContext ctx, String streamId, FullHttpRequest request) throws IOException { + ByteBuf content =ImagePage.getContent(); + FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK, content); + response.headers().set(CONTENT_TYPE, "text/html; charset=UTF-8"); + sendResponse(ctx, streamId, response, request); + } + + protected void sendResponse(final ChannelHandlerContext ctx, String streamId, + final FullHttpResponse response, final FullHttpRequest request) { + setContentLength(response, response.content().readableBytes()); + setStreamId(response, streamId); + ctx.writeAndFlush(response); + } + + private static String getStreamId(FullHttpRequest request) { + return request.headers().get(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text()); + } + + private static void setStreamId(FullHttpResponse response, String streamId) { + response.headers().set(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), streamId); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + // 异常处理 + log.error("出现异常",cause); + ctx.close(); + } + + + /** + * 从query参数中获取值 + */ + public static String getValue(QueryStringDecoder query, String key) { + checkNotNull(query, "查询参数不能为空!"); + List values = query.parameters().get(key); + if (values == null || values.isEmpty()) { + return null; + } + return values.get(0); + } +} diff --git a/learn-netty4/src/main/java/com/flydean34/http2images/ImageHttp1Server.java b/learn-netty4/src/main/java/com/flydean34/http2images/ImageHttp1Server.java new file mode 100644 index 0000000..fa1b4bf --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean34/http2images/ImageHttp1Server.java @@ -0,0 +1,60 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package com.flydean34.http2images; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.*; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpRequestDecoder; +import io.netty.handler.codec.http.HttpResponseEncoder; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; + +/** + * 支持http1 的image服务器 + */ +public final class ImageHttp1Server { + + public static final int PORT = 8000; + private static final int MAX_CONTENT_LENGTH = 1024 * 100; + + private final EventLoopGroup group; + + public ImageHttp1Server(EventLoopGroup eventLoopGroup) { + group = eventLoopGroup; + } + + public ChannelFuture start() throws Exception { + ServerBootstrap b = new ServerBootstrap(); + b.option(ChannelOption.SO_BACKLOG, 1024); + b.group(group).channel(NioServerSocketChannel.class).handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel ch){ + ch.pipeline().addLast(new HttpRequestDecoder(), + new HttpResponseEncoder(), + new HttpObjectAggregator(MAX_CONTENT_LENGTH), + new Http1RequestHandler()); + } + }); + + Channel ch = b.bind(PORT).sync().channel(); + return ch.closeFuture(); + } +} diff --git a/learn-netty4/src/main/java/com/flydean34/http2images/ImageHttp2Server.java b/learn-netty4/src/main/java/com/flydean34/http2images/ImageHttp2Server.java new file mode 100644 index 0000000..5f59542 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean34/http2images/ImageHttp2Server.java @@ -0,0 +1,73 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package com.flydean34.http2images; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.*; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.ssl.*; +import io.netty.handler.ssl.ApplicationProtocolConfig.Protocol; +import io.netty.handler.ssl.ApplicationProtocolConfig.SelectedListenerFailureBehavior; +import io.netty.handler.ssl.ApplicationProtocolConfig.SelectorFailureBehavior; +import io.netty.handler.ssl.util.SelfSignedCertificate; + +import static io.netty.handler.codec.http2.Http2SecurityUtil.CIPHERS; + +/** + * 支持http2的server + */ +public class ImageHttp2Server { + + public static final int PORT = 8443; + + private final EventLoopGroup group; + + public ImageHttp2Server(EventLoopGroup eventLoopGroup) { + group = eventLoopGroup; + } + + public void start() throws Exception { + final SslContext sslCtx; + SelfSignedCertificate ssc = new SelfSignedCertificate(); + ApplicationProtocolConfig apn = new ApplicationProtocolConfig( + Protocol.ALPN, + // 目前 OpenSsl 和 JDK providers只支持NO_ADVERTISE + SelectorFailureBehavior.NO_ADVERTISE, + // 目前 OpenSsl 和 JDK providers只支持ACCEPT + SelectedListenerFailureBehavior.ACCEPT, + ApplicationProtocolNames.HTTP_2, + ApplicationProtocolNames.HTTP_1_1); + + sslCtx= SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey(), null) + .ciphers(CIPHERS, SupportedCipherSuiteFilter.INSTANCE) + .applicationProtocolConfig(apn).build(); + + ServerBootstrap b = new ServerBootstrap(); + b.option(ChannelOption.SO_BACKLOG, 1024); + b.group(group).channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel ch) { + ch.pipeline().addLast(sslCtx.newHandler(ch.alloc()), new CustProtocolNegotiationHandler()); + } + }); + + Channel ch = b.bind(PORT).sync().channel(); + ch.closeFuture(); + } + +} diff --git a/learn-netty4/src/main/java/com/flydean34/http2images/ImagePage.java b/learn-netty4/src/main/java/com/flydean34/http2images/ImagePage.java new file mode 100644 index 0000000..b711dc5 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean34/http2images/ImagePage.java @@ -0,0 +1,97 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean34.http2images; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.util.CharsetUtil; +import lombok.extern.slf4j.Slf4j; + +import java.io.*; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +import static io.netty.buffer.Unpooled.unreleasableBuffer; + +/** + * @author wayne + * @version ImagePage, 2021/9/28 + */ +@Slf4j +public class ImagePage { + + private static final int BLOCK_SIZE = 1024; + + private static final Map imageMap = new HashMap<>(200); + + public static String getName(int id) { + return "img" + id + ".jpg"; + } + + public static ByteBuf getImage(int id) { + if(imageMap.size()==0){ + init(); + } + return imageMap.get(getName(id)); + } + + private static void init() { + for (int id = 0; id < 6; id++) { + try { + String name = getName(id); + log.info("image:{}",name); + ByteBuf fileBytes = unreleasableBuffer(toByteBuf(ImagePage.class + .getResourceAsStream(name)).asReadOnly()); + imageMap.put(name, fileBytes); + } catch (IOException e) { + log.error(e.getMessage(),e); + } + } + } + + + /** + * 将 InputStream 转换成为ByteBuf + * + */ + public static ByteBuf toByteBuf(InputStream input) throws IOException { + ByteBuf buf = Unpooled.buffer(); + int n = 0; + do { + n = buf.writeBytes(input, BLOCK_SIZE); + } while (n > 0); + return buf; + } + + + public static ByteBuf getContent() throws IOException { + URL url= ImagePage.class.getResource("image.html"); + log.info("url: {}",url); + String filePath = url.getFile(); + File file = new File(filePath); + log.info(file.getCanonicalPath()); + FileReader fileReader = new FileReader(file); + BufferedReader reader = new BufferedReader(fileReader); + StringBuilder builder= new StringBuilder(); + reader.lines().forEach(builder::append); + reader.close(); + fileReader.close(); + return Unpooled.copiedBuffer(builder.toString(), CharsetUtil.UTF_8); + } + + +} diff --git a/learn-netty4/src/main/java/com/flydean34/http2images/ImageServer.java b/learn-netty4/src/main/java/com/flydean34/http2images/ImageServer.java new file mode 100644 index 0000000..3f15fab --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean34/http2images/ImageServer.java @@ -0,0 +1,40 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean34.http2images; + +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import lombok.extern.slf4j.Slf4j; + +/** + * 一个同时支持HTTP和HTTP2的image服务器 + */ +@Slf4j +public final class ImageServer { + + public static void main(String[] args) throws Exception { + EventLoopGroup group = new NioEventLoopGroup(); + ImageHttp2Server http2 = new ImageHttp2Server(group); + ImageHttp1Server http = new ImageHttp1Server(group); + try { + http2.start(); + log.info("使用你的浏览器访问: " + "http://127.0.0.1:" + ImageHttp1Server.PORT); + http.start().sync(); + } finally { + group.shutdownGracefully(); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean35/proxy/SimpleDumpProxy.java b/learn-netty4/src/main/java/com/flydean35/proxy/SimpleDumpProxy.java new file mode 100644 index 0000000..1fb48cd --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean35/proxy/SimpleDumpProxy.java @@ -0,0 +1,56 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean35.proxy; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import lombok.extern.slf4j.Slf4j; + +/** + * 代理服务器 + */ +@Slf4j +public final class SimpleDumpProxy { + + static final int LOCAL_PORT = 8000; + static final String REMOTE_HOST = "www.163.com"; + static final int REMOTE_PORT = 80; + + public static void main(String[] args) throws Exception { + log.info("Proxying *:" + LOCAL_PORT + " to " + REMOTE_HOST + ':' + REMOTE_PORT + " ..."); + + // 配置 eventloop + EventLoopGroup bossGroup = new NioEventLoopGroup(1); + EventLoopGroup workerGroup = new NioEventLoopGroup(); + try { + ServerBootstrap b = new ServerBootstrap(); + b.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new SimpleDumpProxyInitializer(REMOTE_HOST, REMOTE_PORT)) + .childOption(ChannelOption.AUTO_READ, false) + .bind(LOCAL_PORT).sync().channel().closeFuture().sync(); + } finally { + bossGroup.shutdownGracefully(); + workerGroup.shutdownGracefully(); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean35/proxy/SimpleDumpProxyInboundHandler.java b/learn-netty4/src/main/java/com/flydean35/proxy/SimpleDumpProxyInboundHandler.java new file mode 100644 index 0000000..15b501a --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean35/proxy/SimpleDumpProxyInboundHandler.java @@ -0,0 +1,95 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean35.proxy; + +import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.Unpooled; +import io.netty.channel.*; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class SimpleDumpProxyInboundHandler extends ChannelInboundHandlerAdapter { + + private final String remoteHost; + private final int remotePort; + + // outboundChannel和inboundChannel使用同一个eventloop + private Channel outboundChannel; + + public SimpleDumpProxyInboundHandler(String remoteHost, int remotePort) { + this.remoteHost = remoteHost; + this.remotePort = remotePort; + } + + @Override + public void channelActive(ChannelHandlerContext ctx) { + final Channel inboundChannel = ctx.channel(); + + // 开启outbound连接 + Bootstrap b = new Bootstrap(); + b.group(inboundChannel.eventLoop()) + .channel(ctx.channel().getClass()) + .handler(new SimpleDumpProxyOutboundHandler(inboundChannel)) + .option(ChannelOption.AUTO_READ, false); + ChannelFuture f = b.connect(remoteHost, remotePort); + outboundChannel = f.channel(); + f.addListener(future -> { + if (future.isSuccess()) { + // 连接建立完毕,读取inbound数据 + inboundChannel.read(); + } else { + // 关闭inbound channel + inboundChannel.close(); + } + }); + } + + @Override + public void channelRead(final ChannelHandlerContext ctx, Object msg) { + // 将inboundChannel中的消息读取,并写入到outboundChannel + if (outboundChannel.isActive()) { + outboundChannel.writeAndFlush(msg).addListener((ChannelFutureListener) future -> { + if (future.isSuccess()) { + // flush成功,读取下一个消息 + ctx.channel().read(); + } else { + future.channel().close(); + } + }); + } + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) { + if (outboundChannel != null) { + closeOnFlush(outboundChannel); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + // 异常处理 + log.error("出现异常",cause); + closeOnFlush(ctx.channel()); + } + + + static void closeOnFlush(Channel ch) { + if (ch.isActive()) { + ch.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean35/proxy/SimpleDumpProxyInitializer.java b/learn-netty4/src/main/java/com/flydean35/proxy/SimpleDumpProxyInitializer.java new file mode 100644 index 0000000..ba392a4 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean35/proxy/SimpleDumpProxyInitializer.java @@ -0,0 +1,39 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean35.proxy; + +import io.netty.channel.ChannelInitializer; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; + +public class SimpleDumpProxyInitializer extends ChannelInitializer { + + private final String remoteHost; + private final int remotePort; + + public SimpleDumpProxyInitializer(String remoteHost, int remotePort) { + this.remoteHost = remoteHost; + this.remotePort = remotePort; + } + + @Override + public void initChannel(SocketChannel ch) { + ch.pipeline().addLast( + new LoggingHandler(LogLevel.INFO), + new SimpleDumpProxyInboundHandler(remoteHost, remotePort)); + } +} diff --git a/learn-netty4/src/main/java/com/flydean35/proxy/SimpleDumpProxyOutboundHandler.java b/learn-netty4/src/main/java/com/flydean35/proxy/SimpleDumpProxyOutboundHandler.java new file mode 100644 index 0000000..ac50cce --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean35/proxy/SimpleDumpProxyOutboundHandler.java @@ -0,0 +1,58 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean35.proxy; + +import io.netty.channel.*; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class SimpleDumpProxyOutboundHandler extends ChannelInboundHandlerAdapter { + + private final Channel inboundChannel; + + public SimpleDumpProxyOutboundHandler(Channel inboundChannel) { + this.inboundChannel = inboundChannel; + } + + @Override + public void channelActive(ChannelHandlerContext ctx) { + ctx.read(); + } + + @Override + public void channelRead(final ChannelHandlerContext ctx, Object msg) { + // 将outboundChannel中的消息读取,并写入到inboundChannel中 + inboundChannel.writeAndFlush(msg).addListener((ChannelFutureListener) future -> { + if (future.isSuccess()) { + ctx.channel().read(); + } else { + future.channel().close(); + } + }); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) { + SimpleDumpProxyInboundHandler.closeOnFlush(inboundChannel); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + // 异常处理 + log.error("出现异常",cause); + SimpleDumpProxyInboundHandler.closeOnFlush(ctx.channel()); + } +} diff --git a/learn-netty4/src/main/java/com/flydean36/socksproxy/ClientPromiseHandler.java b/learn-netty4/src/main/java/com/flydean36/socksproxy/ClientPromiseHandler.java new file mode 100644 index 0000000..83f83da --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean36/socksproxy/ClientPromiseHandler.java @@ -0,0 +1,41 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean36.socksproxy; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.util.concurrent.Promise; + +public final class ClientPromiseHandler extends ChannelInboundHandlerAdapter { + + private final Promise promise; + + public ClientPromiseHandler(Promise promise) { + this.promise = promise; + } + + @Override + public void channelActive(ChannelHandlerContext ctx) { + ctx.pipeline().remove(this); + promise.setSuccess(ctx.channel()); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable throwable) { + promise.setFailure(throwable); + } +} diff --git a/learn-netty4/src/main/java/com/flydean36/socksproxy/RelayHandler.java b/learn-netty4/src/main/java/com/flydean36/socksproxy/RelayHandler.java new file mode 100644 index 0000000..e1376f8 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean36/socksproxy/RelayHandler.java @@ -0,0 +1,61 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean36.socksproxy; + +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.util.ReferenceCountUtil; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public final class RelayHandler extends ChannelInboundHandlerAdapter { + + private final Channel relayChannel; + + public RelayHandler(Channel relayChannel) { + this.relayChannel = relayChannel; + } + + @Override + public void channelActive(ChannelHandlerContext ctx) { + ctx.writeAndFlush(Unpooled.EMPTY_BUFFER); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + if (relayChannel.isActive()) { + relayChannel.writeAndFlush(msg); + } else { + ReferenceCountUtil.release(msg); + } + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) { + if (relayChannel.isActive()) { + relayChannel.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + log.error(cause.getMessage(),cause); + ctx.close(); + } +} diff --git a/learn-netty4/src/main/java/com/flydean36/socksproxy/SocksServer.java b/learn-netty4/src/main/java/com/flydean36/socksproxy/SocksServer.java new file mode 100644 index 0000000..9fac2fc --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean36/socksproxy/SocksServer.java @@ -0,0 +1,44 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean36.socksproxy; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; + +public final class SocksServer { + + static final int PORT = 1080; + + public static void main(String[] args) throws Exception { + EventLoopGroup bossGroup = new NioEventLoopGroup(1); + EventLoopGroup workerGroup = new NioEventLoopGroup(); + try { + ServerBootstrap b = new ServerBootstrap(); + b.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new SocksServerInitializer()); + b.bind(PORT).sync().channel().closeFuture().sync(); + } finally { + bossGroup.shutdownGracefully(); + workerGroup.shutdownGracefully(); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean36/socksproxy/SocksServerConnectHandler.java b/learn-netty4/src/main/java/com/flydean36/socksproxy/SocksServerConnectHandler.java new file mode 100644 index 0000000..e1ddbf0 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean36/socksproxy/SocksServerConnectHandler.java @@ -0,0 +1,140 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean36.socksproxy; + +import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.Unpooled; +import io.netty.channel.*; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.socksx.SocksMessage; +import io.netty.handler.codec.socksx.v4.DefaultSocks4CommandResponse; +import io.netty.handler.codec.socksx.v4.Socks4CommandRequest; +import io.netty.handler.codec.socksx.v4.Socks4CommandStatus; +import io.netty.handler.codec.socksx.v5.DefaultSocks5CommandResponse; +import io.netty.handler.codec.socksx.v5.Socks5CommandRequest; +import io.netty.handler.codec.socksx.v5.Socks5CommandStatus; +import io.netty.util.concurrent.FutureListener; +import io.netty.util.concurrent.Promise; + +public final class SocksServerConnectHandler extends SimpleChannelInboundHandler { + + private final Bootstrap b = new Bootstrap(); + + @Override + public void channelRead0(final ChannelHandlerContext ctx, final SocksMessage message) throws Exception { + if (message instanceof Socks4CommandRequest) { + final Socks4CommandRequest request = (Socks4CommandRequest) message; + Promise promise = ctx.executor().newPromise(); + promise.addListener( + (FutureListener) future -> { + final Channel outboundChannel = future.getNow(); + if (future.isSuccess()) { + ChannelFuture responseFuture = ctx.channel().writeAndFlush( + new DefaultSocks4CommandResponse(Socks4CommandStatus.SUCCESS)); + //成功建立连接,删除SocksServerConnectHandler,添加RelayHandler + responseFuture.addListener(channelFuture -> { + ctx.pipeline().remove(SocksServerConnectHandler.this); + outboundChannel.pipeline().addLast(new RelayHandler(ctx.channel())); + ctx.pipeline().addLast(new RelayHandler(outboundChannel)); + }); + } else { + ctx.channel().writeAndFlush( + new DefaultSocks4CommandResponse(Socks4CommandStatus.REJECTED_OR_FAILED)); + closeOnFlush(ctx.channel()); + } + }); + + Channel inboundChannel = ctx.channel(); + b.group(inboundChannel.eventLoop()) + .channel(NioSocketChannel.class) + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000) + .option(ChannelOption.SO_KEEPALIVE, true) + .handler(new ClientPromiseHandler(promise)); + + b.connect(request.dstAddr(), request.dstPort()).addListener(future -> { + if (future.isSuccess()) { + // 成功建立连接 + } else { + // 关闭连接 + ctx.channel().writeAndFlush( + new DefaultSocks4CommandResponse(Socks4CommandStatus.REJECTED_OR_FAILED) + ); + closeOnFlush(ctx.channel()); + } + }); + } else if (message instanceof Socks5CommandRequest) { + final Socks5CommandRequest request = (Socks5CommandRequest) message; + Promise promise = ctx.executor().newPromise(); + promise.addListener( + (FutureListener) future -> { + final Channel outboundChannel = future.getNow(); + if (future.isSuccess()) { + ChannelFuture responseFuture = + ctx.channel().writeAndFlush(new DefaultSocks5CommandResponse( + Socks5CommandStatus.SUCCESS, + request.dstAddrType(), + request.dstAddr(), + request.dstPort())); + + //成功建立连接,删除SocksServerConnectHandler,添加RelayHandler + responseFuture.addListener(channelFuture -> { + ctx.pipeline().remove(SocksServerConnectHandler.this); + outboundChannel.pipeline().addLast(new RelayHandler(ctx.channel())); + ctx.pipeline().addLast(new RelayHandler(outboundChannel)); + }); + } else { + ctx.channel().writeAndFlush(new DefaultSocks5CommandResponse( + Socks5CommandStatus.FAILURE, request.dstAddrType())); + closeOnFlush(ctx.channel()); + } + }); + + final Channel inboundChannel = ctx.channel(); + b.group(inboundChannel.eventLoop()) + .channel(NioSocketChannel.class) + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000) + .option(ChannelOption.SO_KEEPALIVE, true) + .handler(new ClientPromiseHandler(promise)); + + b.connect(request.dstAddr(), request.dstPort()).addListener( future -> { + if (future.isSuccess()) { + // 成功建立连接 + } else { + // 关闭连接 + ctx.channel().writeAndFlush( + new DefaultSocks5CommandResponse(Socks5CommandStatus.FAILURE, request.dstAddrType())); + closeOnFlush(ctx.channel()); + } + }); + } else { + ctx.close(); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + closeOnFlush(ctx.channel()); + } + + /** + * 关闭channel + */ + public void closeOnFlush(Channel ch) { + if (ch.isActive()) { + ch.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean36/socksproxy/SocksServerHandler.java b/learn-netty4/src/main/java/com/flydean36/socksproxy/SocksServerHandler.java new file mode 100644 index 0000000..c2c65ff --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean36/socksproxy/SocksServerHandler.java @@ -0,0 +1,86 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean36.socksproxy; + +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.socksx.SocksMessage; +import io.netty.handler.codec.socksx.v4.Socks4CommandRequest; +import io.netty.handler.codec.socksx.v4.Socks4CommandType; +import io.netty.handler.codec.socksx.v5.*; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public final class SocksServerHandler extends SimpleChannelInboundHandler { + + public static final SocksServerHandler INSTANCE = new SocksServerHandler(); + + private SocksServerHandler() { } + + @Override + public void channelRead0(ChannelHandlerContext ctx, SocksMessage socksRequest) throws Exception { + switch (socksRequest.version()) { + case SOCKS4a: + Socks4CommandRequest socksV4CmdRequest = (Socks4CommandRequest) socksRequest; + if (socksV4CmdRequest.type() == Socks4CommandType.CONNECT) { + ctx.pipeline().addLast(new SocksServerConnectHandler()); + ctx.pipeline().remove(this); + ctx.fireChannelRead(socksRequest); + } else { + ctx.close(); + } + break; + case SOCKS5: + if (socksRequest instanceof Socks5InitialRequest) { + ctx.pipeline().addFirst(new Socks5CommandRequestDecoder()); + ctx.write(new DefaultSocks5InitialResponse(Socks5AuthMethod.NO_AUTH)); + } else if (socksRequest instanceof Socks5PasswordAuthRequest) { + ctx.pipeline().addFirst(new Socks5CommandRequestDecoder()); + ctx.write(new DefaultSocks5PasswordAuthResponse(Socks5PasswordAuthStatus.SUCCESS)); + } else if (socksRequest instanceof Socks5CommandRequest) { + Socks5CommandRequest socks5CmdRequest = (Socks5CommandRequest) socksRequest; + if (socks5CmdRequest.type() == Socks5CommandType.CONNECT) { + ctx.pipeline().addLast(new SocksServerConnectHandler()); + ctx.pipeline().remove(this); + ctx.fireChannelRead(socksRequest); + } else { + ctx.close(); + } + } else { + ctx.close(); + } + break; + case UNKNOWN: + ctx.close(); + break; + } + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + ctx.flush(); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable throwable) { + log.error(throwable.getMessage(),throwable); + if (ctx.channel().isActive()) { + ctx.channel().writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean36/socksproxy/SocksServerInitializer.java b/learn-netty4/src/main/java/com/flydean36/socksproxy/SocksServerInitializer.java new file mode 100644 index 0000000..131f4b2 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean36/socksproxy/SocksServerInitializer.java @@ -0,0 +1,32 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean36.socksproxy; + +import io.netty.channel.ChannelInitializer; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.socksx.SocksPortUnificationServerHandler; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; + +public final class SocksServerInitializer extends ChannelInitializer { + @Override + public void initChannel(SocketChannel ch) throws Exception { + ch.pipeline().addLast( + new LoggingHandler(LogLevel.DEBUG), + new SocksPortUnificationServerHandler(), + SocksServerHandler.INSTANCE); + } +} diff --git a/learn-netty4/src/main/java/com/flydean37/custportunification/CustPortUnificationServer.java b/learn-netty4/src/main/java/com/flydean37/custportunification/CustPortUnificationServer.java new file mode 100644 index 0000000..8e8afe6 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean37/custportunification/CustPortUnificationServer.java @@ -0,0 +1,57 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean37.custportunification; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; + +/** + * 可以同时支持不同协议的服务器,这里同时支持SSL和gzip + */ +public final class CustPortUnificationServer { + + static final int PORT = Integer.parseInt(System.getProperty("port", "8000")); + + public static void main(String[] args) throws Exception { + + EventLoopGroup bossGroup = new NioEventLoopGroup(1); + EventLoopGroup workerGroup = new NioEventLoopGroup(); + try { + ServerBootstrap b = new ServerBootstrap(); + b.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new ChannelInitializer() { + @Override + public void initChannel(SocketChannel ch) { + ch.pipeline().addLast(new CustPortUnificationServerHandler()); + } + }); + + // 启动连接 + b.bind(PORT).sync().channel().closeFuture().sync(); + } finally { + bossGroup.shutdownGracefully(); + workerGroup.shutdownGracefully(); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean37/custportunification/CustPortUnificationServerHandler.java b/learn-netty4/src/main/java/com/flydean37/custportunification/CustPortUnificationServerHandler.java new file mode 100644 index 0000000..80bf504 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean37/custportunification/CustPortUnificationServerHandler.java @@ -0,0 +1,88 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean37.custportunification; + +import com.flydean19.httpclientrequest.HttpRequestServerHandler; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPipeline; +import io.netty.handler.codec.ByteToMessageDecoder; +import io.netty.handler.codec.compression.ZlibCodecFactory; +import io.netty.handler.codec.compression.ZlibWrapper; +import io.netty.handler.codec.http.HttpContentCompressor; +import io.netty.handler.codec.http.HttpRequestDecoder; +import io.netty.handler.codec.http.HttpResponseEncoder; + +import java.util.List; + +/** + * 同时支持gzip和http的handler + */ +public class CustPortUnificationServerHandler extends ByteToMessageDecoder { + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { + final int readerIndex = in.readerIndex(); + if (in.writerIndex() == readerIndex) { + return; + } + final int magic1 = in.getUnsignedByte(readerIndex); + final int magic2 = in.getUnsignedByte(in.readerIndex() + 1); + if (isGzip(magic1,magic2)) { + enableGzip(ctx); + } else if (isHttp(magic1,magic2)) { + switchToHttp(ctx); + } else { + // 未知协议,关闭连接 + in.clear(); + ctx.close(); + } + } + + private boolean isGzip(int magic1, int magic2) { + return magic1 == 31 && magic2 == 139; + } + + private static boolean isHttp(int magic1, int magic2) { + return + magic1 == 'G' && magic2 == 'E' || // GET + magic1 == 'P' && magic2 == 'O' || // POST + magic1 == 'P' && magic2 == 'U' || // PUT + magic1 == 'H' && magic2 == 'E' || // HEAD + magic1 == 'O' && magic2 == 'P' || // OPTIONS + magic1 == 'P' && magic2 == 'A' || // PATCH + magic1 == 'D' && magic2 == 'E' || // DELETE + magic1 == 'T' && magic2 == 'R' || // TRACE + magic1 == 'C' && magic2 == 'O'; // CONNECT + } + + private void enableGzip(ChannelHandlerContext ctx) { + ChannelPipeline p = ctx.pipeline(); + p.addLast("gzipEncoder", ZlibCodecFactory.newZlibEncoder(ZlibWrapper.GZIP)); + p.addLast("gzipDecoder", ZlibCodecFactory.newZlibDecoder(ZlibWrapper.GZIP)); + p.remove(this); + } + + private void switchToHttp(ChannelHandlerContext ctx) { + ChannelPipeline p = ctx.pipeline(); + p.addLast("decoder", new HttpRequestDecoder()); + p.addLast("encoder", new HttpResponseEncoder()); + p.addLast("compressor", new HttpContentCompressor()); + p.addLast("handler", new HttpRequestServerHandler()); + p.remove(this); + } + +} diff --git a/learn-netty4/src/main/java/com/flydean40/udtByte/UDTByteEchoClient.java b/learn-netty4/src/main/java/com/flydean40/udtByte/UDTByteEchoClient.java new file mode 100644 index 0000000..47a64af --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean40/udtByte/UDTByteEchoClient.java @@ -0,0 +1,65 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean40.udtByte; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.udt.UdtChannel; +import io.netty.channel.udt.nio.NioUdtProvider; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import io.netty.util.concurrent.DefaultThreadFactory; + +import java.util.concurrent.ThreadFactory; + +/** + * UDT 客户端 + */ +public final class UDTByteEchoClient { + + static final String HOST = "127.0.0.1"; + static final int PORT = 8000; + static final int SIZE = 256; + + public static void main(String[] args) throws Exception { + // 客户端连接 + final ThreadFactory connectFactory = new DefaultThreadFactory("connect"); + final NioEventLoopGroup connectGroup = new NioEventLoopGroup(1, + connectFactory, NioUdtProvider.BYTE_PROVIDER); + try { + final Bootstrap boot = new Bootstrap(); + boot.group(connectGroup) + .channelFactory(NioUdtProvider.BYTE_CONNECTOR) + .handler(new ChannelInitializer() { + @Override + public void initChannel(final UdtChannel ch) { + ch.pipeline().addLast( + new LoggingHandler(LogLevel.INFO), + new UDTByteEchoClientHandler()); + } + }); + // 启动客户端 + final ChannelFuture f = boot.connect(HOST, PORT).sync(); + // 等待连接关闭 + f.channel().closeFuture().sync(); + } finally { + // 关闭event loop + connectGroup.shutdownGracefully(); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean40/udtByte/UDTByteEchoClientHandler.java b/learn-netty4/src/main/java/com/flydean40/udtByte/UDTByteEchoClientHandler.java new file mode 100644 index 0000000..15ed56d --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean40/udtByte/UDTByteEchoClientHandler.java @@ -0,0 +1,63 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean40.udtByte; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.channel.udt.nio.NioUdtProvider; +import lombok.extern.slf4j.Slf4j; + +import java.nio.charset.StandardCharsets; + +/** + * 客户端处理器 + */ +@Slf4j +public class UDTByteEchoClientHandler extends SimpleChannelInboundHandler { + + private final ByteBuf message; + + public UDTByteEchoClientHandler() { + super(false); + message = Unpooled.buffer(UDTByteEchoClient.SIZE); + message.writeBytes("www.flydean.com".getBytes(StandardCharsets.UTF_8)); + } + + @Override + public void channelActive(final ChannelHandlerContext ctx) { + log.info("channel active {}" , NioUdtProvider.socketUDT(ctx.channel()).toStringOptions()); + ctx.writeAndFlush(message); + } + + @Override + public void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) { + ctx.write(msg); + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + ctx.flush(); + } + + @Override + public void exceptionCaught(final ChannelHandlerContext ctx, final Throwable cause) { + log.error(cause.getMessage(),cause); + ctx.close(); + } + +} diff --git a/learn-netty4/src/main/java/com/flydean40/udtByte/UDTByteEchoServer.java b/learn-netty4/src/main/java/com/flydean40/udtByte/UDTByteEchoServer.java new file mode 100644 index 0000000..64299f7 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean40/udtByte/UDTByteEchoServer.java @@ -0,0 +1,69 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean40.udtByte; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.udt.UdtChannel; +import io.netty.channel.udt.nio.NioUdtProvider; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import io.netty.util.concurrent.DefaultThreadFactory; + +import java.util.concurrent.ThreadFactory; + +/** + * 一个支持UDT协议的服务器,简单的返回客户端发送的数据 + */ +public final class UDTByteEchoServer { + + static final int PORT = 8000; + + public static void main(String[] args) throws Exception { + final ThreadFactory acceptFactory = new DefaultThreadFactory("accept"); + final ThreadFactory connectFactory = new DefaultThreadFactory("connect"); + final NioEventLoopGroup acceptGroup = new NioEventLoopGroup(1, acceptFactory, NioUdtProvider.BYTE_PROVIDER); + final NioEventLoopGroup connectGroup = new NioEventLoopGroup(1, connectFactory, NioUdtProvider.BYTE_PROVIDER); + + // Configure the server. + try { + final ServerBootstrap boot = new ServerBootstrap(); + boot.group(acceptGroup, connectGroup) + .channelFactory(NioUdtProvider.BYTE_ACCEPTOR) + .option(ChannelOption.SO_BACKLOG, 10) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new ChannelInitializer() { + @Override + public void initChannel(final UdtChannel ch) { + ch.pipeline().addLast( + new LoggingHandler(LogLevel.INFO), + new UDTByteEchoServerHandler()); + } + }); + // 开启服务 + final ChannelFuture future = boot.bind(PORT).sync(); + // 等待socket关闭 + future.channel().closeFuture().sync(); + } finally { + // 关闭event loop + acceptGroup.shutdownGracefully(); + connectGroup.shutdownGracefully(); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean40/udtByte/UDTByteEchoServerHandler.java b/learn-netty4/src/main/java/com/flydean40/udtByte/UDTByteEchoServerHandler.java new file mode 100644 index 0000000..ce0e3ca --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean40/udtByte/UDTByteEchoServerHandler.java @@ -0,0 +1,51 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean40.udtByte; + +import io.netty.channel.ChannelHandler.Sharable; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.udt.nio.NioUdtProvider; +import lombok.extern.slf4j.Slf4j; + +/** + * 服务端的处理器 + */ +@Sharable +@Slf4j +public class UDTByteEchoServerHandler extends ChannelInboundHandlerAdapter { + + @Override + public void channelActive(final ChannelHandlerContext ctx) { + log.info("UDT handler active " + NioUdtProvider.socketUDT(ctx.channel()).toStringOptions()); + } + + @Override + public void channelRead(final ChannelHandlerContext ctx, Object msg) { + ctx.write(msg); + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + ctx.flush(); + } + + @Override + public void exceptionCaught(final ChannelHandlerContext ctx, final Throwable cause) { + log.error(cause.getMessage(),cause); + ctx.close(); + } +} diff --git a/learn-netty4/src/main/java/com/flydean41/udtMessage/UDTMsgEchoClient.java b/learn-netty4/src/main/java/com/flydean41/udtMessage/UDTMsgEchoClient.java new file mode 100644 index 0000000..844dde9 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean41/udtMessage/UDTMsgEchoClient.java @@ -0,0 +1,69 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean41.udtMessage; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.udt.UdtChannel; +import io.netty.channel.udt.nio.NioUdtProvider; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import io.netty.util.concurrent.DefaultThreadFactory; + +import java.util.concurrent.ThreadFactory; +import java.util.logging.Logger; + +/** + * UDT Message客户端 + */ +public final class UDTMsgEchoClient { + + private static final Logger log = Logger.getLogger(UDTMsgEchoClient.class.getName()); + + static final String HOST = "127.0.0.1"; + static final int PORT = 8000; + static final int SIZE = 256; + + public static void main(String[] args) throws Exception { + + // Configure the client. + final ThreadFactory connectFactory = new DefaultThreadFactory("connect"); + final NioEventLoopGroup connectGroup = new NioEventLoopGroup(1, + connectFactory, NioUdtProvider.MESSAGE_PROVIDER); + try { + final Bootstrap boot = new Bootstrap(); + boot.group(connectGroup) + .channelFactory(NioUdtProvider.MESSAGE_CONNECTOR) + .handler(new ChannelInitializer() { + @Override + public void initChannel(final UdtChannel ch) + throws Exception { + ch.pipeline().addLast( + new LoggingHandler(LogLevel.INFO), + new UDTMsgEchoClientHandler()); + } + }); + // 启动客户端 + final ChannelFuture f = boot.connect(HOST, PORT).sync(); + // 等待关闭 + f.channel().closeFuture().sync(); + } finally { + connectGroup.shutdownGracefully(); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean41/udtMessage/UDTMsgEchoClientHandler.java b/learn-netty4/src/main/java/com/flydean41/udtMessage/UDTMsgEchoClientHandler.java new file mode 100644 index 0000000..ddcfee5 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean41/udtMessage/UDTMsgEchoClientHandler.java @@ -0,0 +1,64 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean41.udtMessage; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.channel.udt.UdtMessage; +import io.netty.channel.udt.nio.NioUdtProvider; +import lombok.extern.slf4j.Slf4j; + +import java.nio.charset.StandardCharsets; + +/** + * UDT message客户端处理器 + */ +@Slf4j +public class UDTMsgEchoClientHandler extends SimpleChannelInboundHandler { + + private final UdtMessage message; + + public UDTMsgEchoClientHandler() { + super(false); + final ByteBuf byteBuf = Unpooled.buffer(UDTMsgEchoClient.SIZE); + byteBuf.writeBytes("www.flydean.com".getBytes(StandardCharsets.UTF_8)); + message = new UdtMessage(byteBuf); + } + + @Override + public void channelActive(final ChannelHandlerContext ctx) { + log.info("channel active {}", NioUdtProvider.socketUDT(ctx.channel()).toStringOptions()); + ctx.writeAndFlush(message); + } + + @Override + public void channelRead0(ChannelHandlerContext ctx, UdtMessage msg) { + ctx.write(msg); + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + ctx.flush(); + } + + @Override + public void exceptionCaught(final ChannelHandlerContext ctx, final Throwable cause) { + log.error(cause.getMessage(),cause); + ctx.close(); + } +} diff --git a/learn-netty4/src/main/java/com/flydean41/udtMessage/UDTMsgEchoServer.java b/learn-netty4/src/main/java/com/flydean41/udtMessage/UDTMsgEchoServer.java new file mode 100644 index 0000000..6e98061 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean41/udtMessage/UDTMsgEchoServer.java @@ -0,0 +1,69 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean41.udtMessage; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.udt.UdtChannel; +import io.netty.channel.udt.nio.NioUdtProvider; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import io.netty.util.concurrent.DefaultThreadFactory; + +import java.util.concurrent.ThreadFactory; + +/** + * UDT Message 服务器端 + */ +public final class UDTMsgEchoServer { + + static final int PORT =8000; + + public static void main(String[] args) throws Exception { + final ThreadFactory acceptFactory = new DefaultThreadFactory("accept"); + final ThreadFactory connectFactory = new DefaultThreadFactory("connect"); + final NioEventLoopGroup acceptGroup = + new NioEventLoopGroup(1, acceptFactory, NioUdtProvider.MESSAGE_PROVIDER); + final NioEventLoopGroup connectGroup = + new NioEventLoopGroup(1, connectFactory, NioUdtProvider.MESSAGE_PROVIDER); + + try { + final ServerBootstrap boot = new ServerBootstrap(); + boot.group(acceptGroup, connectGroup) + .channelFactory(NioUdtProvider.MESSAGE_ACCEPTOR) + .option(ChannelOption.SO_BACKLOG, 10) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new ChannelInitializer() { + @Override + public void initChannel(final UdtChannel ch) + throws Exception { + ch.pipeline().addLast( + new LoggingHandler(LogLevel.INFO), + new UDTMsgEchoServerHandler()); + } + }); + // 启动服务器 + final ChannelFuture future = boot.bind(PORT).sync(); + future.channel().closeFuture().sync(); + } finally { + acceptGroup.shutdownGracefully(); + connectGroup.shutdownGracefully(); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean41/udtMessage/UDTMsgEchoServerHandler.java b/learn-netty4/src/main/java/com/flydean41/udtMessage/UDTMsgEchoServerHandler.java new file mode 100644 index 0000000..8a816b4 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean41/udtMessage/UDTMsgEchoServerHandler.java @@ -0,0 +1,51 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean41.udtMessage; + +import io.netty.channel.ChannelHandler.Sharable; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.udt.nio.NioUdtProvider; +import lombok.extern.slf4j.Slf4j; + +/** + * UDT message 服务器端处理器 + */ +@Sharable +@Slf4j +public class UDTMsgEchoServerHandler extends ChannelInboundHandlerAdapter { + + @Override + public void channelActive(final ChannelHandlerContext ctx) { + log.info("ECHO active {}" , NioUdtProvider.socketUDT(ctx.channel()).toStringOptions()); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + ctx.write(msg); + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + ctx.flush(); + } + + @Override + public void exceptionCaught(final ChannelHandlerContext ctx, final Throwable cause) { + log.error(cause.getMessage(),cause); + ctx.close(); + } +} diff --git a/learn-netty4/src/main/java/com/flydean42/udtRendezvous/UDTByteHandler.java b/learn-netty4/src/main/java/com/flydean42/udtRendezvous/UDTByteHandler.java new file mode 100644 index 0000000..560d091 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean42/udtRendezvous/UDTByteHandler.java @@ -0,0 +1,59 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean42.udtRendezvous; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.channel.udt.nio.NioUdtProvider; +import lombok.extern.slf4j.Slf4j; + +import java.nio.charset.StandardCharsets; + +@Slf4j +public class UDTByteHandler extends SimpleChannelInboundHandler { + + private final ByteBuf message; + + public UDTByteHandler(final int messageSize) { + super(false); + message = Unpooled.buffer(messageSize); + message.writeBytes("www.flydean.com".getBytes(StandardCharsets.UTF_8)); + } + + @Override + public void channelActive(ChannelHandlerContext ctx) { + log.info("ECHO active {}" ,NioUdtProvider.socketUDT(ctx.channel()).toStringOptions()); + ctx.writeAndFlush(message); + } + + @Override + public void channelRead0(ChannelHandlerContext ctx, ByteBuf buf) { + ctx.write(buf); + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + ctx.flush(); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + log.error(cause.getMessage(),cause); + ctx.close(); + } +} diff --git a/learn-netty4/src/main/java/com/flydean42/udtRendezvous/UDTByteRendezvousBase.java b/learn-netty4/src/main/java/com/flydean42/udtRendezvous/UDTByteRendezvousBase.java new file mode 100644 index 0000000..7d4e891 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean42/udtRendezvous/UDTByteRendezvousBase.java @@ -0,0 +1,65 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean42.udtRendezvous; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.udt.UdtChannel; +import io.netty.channel.udt.nio.NioUdtProvider; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import io.netty.util.concurrent.DefaultThreadFactory; + +import java.net.SocketAddress; +import java.util.concurrent.ThreadFactory; + +public class UDTByteRendezvousBase { + + protected final int messageSize; + protected final SocketAddress myAddress; + protected final SocketAddress peerAddress; + + public UDTByteRendezvousBase(int messageSize, SocketAddress myAddress, SocketAddress peerAddress) { + this.messageSize = messageSize; + this.myAddress = myAddress; + this.peerAddress = peerAddress; + } + + public void run() throws Exception { + final ThreadFactory connectFactory = new DefaultThreadFactory("rendezvous"); + final NioEventLoopGroup connectGroup = new NioEventLoopGroup(1, + connectFactory, NioUdtProvider.BYTE_PROVIDER); + try { + final Bootstrap bootstrap = new Bootstrap(); + bootstrap.group(connectGroup) + .channelFactory(NioUdtProvider.BYTE_RENDEZVOUS) + .handler(new ChannelInitializer() { + @Override + protected void initChannel(UdtChannel ch) throws Exception { + ch.pipeline().addLast( + new LoggingHandler(LogLevel.INFO), + new UDTByteHandler(messageSize)); + } + }); + final ChannelFuture future = bootstrap.connect(peerAddress, myAddress).sync(); + future.channel().closeFuture().sync(); + } finally { + connectGroup.shutdownGracefully(); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean42/udtRendezvous/UDTByteRendezvousOne.java b/learn-netty4/src/main/java/com/flydean42/udtRendezvous/UDTByteRendezvousOne.java new file mode 100644 index 0000000..b1c0ea5 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean42/udtRendezvous/UDTByteRendezvousOne.java @@ -0,0 +1,35 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean42.udtRendezvous; + +import io.netty.util.internal.SocketUtils; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; + +public class UDTByteRendezvousOne extends UDTByteRendezvousBase { + + public UDTByteRendezvousOne(int messageSize, SocketAddress myAddress, SocketAddress peerAddress) { + super(messageSize, myAddress, peerAddress); + } + + public static void main(String[] args) throws Exception { + final int messageSize = 64 * 1024; + final InetSocketAddress myAddress = SocketUtils.socketAddress("127.0.0.1", 8000); + final InetSocketAddress peerAddress = SocketUtils.socketAddress("127.0.0.1", 8001); + new UDTByteRendezvousOne(messageSize, myAddress, peerAddress).run(); + } +} diff --git a/learn-netty4/src/main/java/com/flydean42/udtRendezvous/UDTByteRendezvousTwo.java b/learn-netty4/src/main/java/com/flydean42/udtRendezvous/UDTByteRendezvousTwo.java new file mode 100644 index 0000000..565e7e9 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean42/udtRendezvous/UDTByteRendezvousTwo.java @@ -0,0 +1,36 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean42.udtRendezvous; + +import io.netty.util.internal.SocketUtils; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; + + +public class UDTByteRendezvousTwo extends UDTByteRendezvousBase { + + public UDTByteRendezvousTwo(int messageSize, SocketAddress myAddress, SocketAddress peerAddress) { + super(messageSize, myAddress, peerAddress); + } + + public static void main(String[] args) throws Exception { + final int messageSize = 64 * 1024; + final InetSocketAddress myAddress = SocketUtils.socketAddress("127.0.0.1", 8001); + final InetSocketAddress peerAddress = SocketUtils.socketAddress("127.0.0.1", 8000); + new UDTByteRendezvousTwo(messageSize, myAddress, peerAddress).run(); + } +} diff --git a/learn-netty4/src/main/java/com/flydean42/udtRendezvous/UDTMsgHandler.java b/learn-netty4/src/main/java/com/flydean42/udtRendezvous/UDTMsgHandler.java new file mode 100644 index 0000000..237a4d3 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean42/udtRendezvous/UDTMsgHandler.java @@ -0,0 +1,61 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean42.udtRendezvous; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.channel.udt.UdtMessage; +import io.netty.channel.udt.nio.NioUdtProvider; +import lombok.extern.slf4j.Slf4j; + +import java.nio.charset.StandardCharsets; + +@Slf4j +public class UDTMsgHandler extends SimpleChannelInboundHandler { + + private final UdtMessage message; + + public UDTMsgHandler(final int messageSize) { + super(false); + final ByteBuf byteBuf = Unpooled.buffer(messageSize); + byteBuf.writeBytes("www.flydean.com".getBytes(StandardCharsets.UTF_8)); + message = new UdtMessage(byteBuf); + } + + @Override + public void channelActive(final ChannelHandlerContext ctx) { + log.info("ECHO active {}", NioUdtProvider.socketUDT(ctx.channel()).toStringOptions()); + ctx.writeAndFlush(message); + } + + @Override + public void channelRead0(ChannelHandlerContext ctx, UdtMessage message) { + ctx.write(message); + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + ctx.flush(); + } + + @Override + public void exceptionCaught(final ChannelHandlerContext ctx, final Throwable cause) { + log.error(cause.getMessage(),cause); + ctx.close(); + } +} diff --git a/learn-netty4/src/main/java/com/flydean42/udtRendezvous/UDTMsgRendezvousBase.java b/learn-netty4/src/main/java/com/flydean42/udtRendezvous/UDTMsgRendezvousBase.java new file mode 100644 index 0000000..07b7bc6 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean42/udtRendezvous/UDTMsgRendezvousBase.java @@ -0,0 +1,68 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean42.udtRendezvous; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.udt.UdtChannel; +import io.netty.channel.udt.nio.NioUdtProvider; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import io.netty.util.concurrent.DefaultThreadFactory; + +import java.net.InetSocketAddress; +import java.util.concurrent.ThreadFactory; + +public class UDTMsgRendezvousBase { + + protected final int messageSize; + protected final InetSocketAddress self; + protected final InetSocketAddress peer; + + protected UDTMsgRendezvousBase(final InetSocketAddress self, final InetSocketAddress peer, final int messageSize) { + this.messageSize = messageSize; + this.self = self; + this.peer = peer; + } + + public void run() throws Exception { + + final ThreadFactory connectFactory = new DefaultThreadFactory("rendezvous"); + final NioEventLoopGroup connectGroup = new NioEventLoopGroup(1, + connectFactory, NioUdtProvider.MESSAGE_PROVIDER); + try { + final Bootstrap boot = new Bootstrap(); + boot.group(connectGroup) + .channelFactory(NioUdtProvider.MESSAGE_RENDEZVOUS) + .handler(new ChannelInitializer() { + @Override + public void initChannel(final UdtChannel ch) + throws Exception { + ch.pipeline().addLast( + new LoggingHandler(LogLevel.INFO), + new UDTMsgHandler(messageSize)); + } + }); + // 启动节点 + final ChannelFuture f = boot.connect(peer, self).sync(); + f.channel().closeFuture().sync(); + } finally { + connectGroup.shutdownGracefully(); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean42/udtRendezvous/UDTMsgRendezvousOne.java b/learn-netty4/src/main/java/com/flydean42/udtRendezvous/UDTMsgRendezvousOne.java new file mode 100644 index 0000000..15b4076 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean42/udtRendezvous/UDTMsgRendezvousOne.java @@ -0,0 +1,35 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean42.udtRendezvous; + +import io.netty.util.internal.SocketUtils; + +import java.net.InetSocketAddress; + + +public class UDTMsgRendezvousOne extends UDTMsgRendezvousBase { + + public UDTMsgRendezvousOne(final InetSocketAddress self, final InetSocketAddress peer, final int messageSize) { + super(self, peer, messageSize); + } + + public static void main(final String[] args) throws Exception { + final int messageSize = 64 * 1024; + final InetSocketAddress self = SocketUtils.socketAddress("127.0.0.1", 8000); + final InetSocketAddress peer = SocketUtils.socketAddress("127.0.0.1", 8001); + new UDTMsgRendezvousOne(self, peer, messageSize).run(); + } +} diff --git a/learn-netty4/src/main/java/com/flydean42/udtRendezvous/UDTMsgRendezvousTwo.java b/learn-netty4/src/main/java/com/flydean42/udtRendezvous/UDTMsgRendezvousTwo.java new file mode 100644 index 0000000..1fd0d8a --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean42/udtRendezvous/UDTMsgRendezvousTwo.java @@ -0,0 +1,34 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean42.udtRendezvous; + +import io.netty.util.internal.SocketUtils; + +import java.net.InetSocketAddress; + +public class UDTMsgRendezvousTwo extends UDTMsgRendezvousBase { + + public UDTMsgRendezvousTwo(final InetSocketAddress self, final InetSocketAddress peer, final int messageSize) { + super(self, peer, messageSize); + } + + public static void main(final String[] args) throws Exception { + final int messageSize = 64 * 1024; + final InetSocketAddress self = SocketUtils.socketAddress("127.0.0.1", 8001); + final InetSocketAddress peer = SocketUtils.socketAddress("127.0.0.1", 8000); + new UDTMsgRendezvousTwo(self, peer, messageSize).run(); + } +} diff --git a/learn-netty4/src/main/java/com/flydean43/byteBufRef/ByteBufRef.java b/learn-netty4/src/main/java/com/flydean43/byteBufRef/ByteBufRef.java new file mode 100644 index 0000000..b47aff1 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean43/byteBufRef/ByteBufRef.java @@ -0,0 +1,56 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean43.byteBufRef; + +import io.netty.buffer.ByteBuf; +import io.netty.util.IllegalReferenceCountException; +import lombok.extern.slf4j.Slf4j; + +import static io.netty.buffer.Unpooled.directBuffer; + +@Slf4j +public class ByteBufRef { + + public static void main(String[] args) { + + ByteBuf buf = directBuffer(); + assert buf.refCnt() == 1; + + boolean destroyed = buf.release(); + assert destroyed; + assert buf.refCnt() == 0; + + try { + buf.writeByte(10); + } catch (IllegalReferenceCountException e) { + log.error(e.getMessage(),e); + } + + +// buf.retain(); +// assert buf.refCnt() == 1; + + buf = directBuffer(); + buf.retain(); + assert buf.refCnt() == 2; + + buf = directBuffer(); + ByteBuf derived = buf.duplicate(); + assert buf.refCnt() == 1; + assert derived.refCnt() == 1; + + } +} diff --git a/learn-netty4/src/main/java/com/flydean44/tfo/TFOClient.java b/learn-netty4/src/main/java/com/flydean44/tfo/TFOClient.java new file mode 100644 index 0000000..2029bc3 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean44/tfo/TFOClient.java @@ -0,0 +1,69 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean44.tfo; + +import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.ByteBuf; +import io.netty.channel.*; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.util.internal.SocketUtils; + +import java.net.SocketAddress; +import java.nio.charset.StandardCharsets; + +import static io.netty.buffer.Unpooled.directBuffer; + +/** + * TFO netty的客户端 + */ +public final class TFOClient { + + static final String HOST = System.getProperty("host", "127.0.0.1"); + static final int PORT = Integer.parseInt(System.getProperty("port", "8000")); + + public static void main(String[] args) throws Exception { + + EventLoopGroup group = new NioEventLoopGroup(); + try { + Bootstrap b = new Bootstrap(); + b.group(group) + .channel(NioSocketChannel.class) + .option(ChannelOption.TCP_FASTOPEN_CONNECT, true) + .handler(new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel ch) throws Exception { + ChannelPipeline p = ch.pipeline(); + p.addLast(new TFOClientHandler()); + } + }); + + Channel channel = b.register().sync().channel(); + ByteBuf fastOpenData = directBuffer(); + fastOpenData.writeBytes("TFO message".getBytes(StandardCharsets.UTF_8)); + channel.write(fastOpenData); + // 连接服务器 + SocketAddress serverAddress = SocketUtils.socketAddress("127.0.0.1", 8000); + ChannelFuture f = channel.connect(serverAddress).sync(); + + // 等待关闭 + f.channel().closeFuture().sync(); + } finally { + group.shutdownGracefully(); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean44/tfo/TFOClientHandler.java b/learn-netty4/src/main/java/com/flydean44/tfo/TFOClientHandler.java new file mode 100644 index 0000000..5952e11 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean44/tfo/TFOClientHandler.java @@ -0,0 +1,53 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean44.tfo; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import lombok.extern.slf4j.Slf4j; + +import java.nio.charset.StandardCharsets; + +/** + * TFO client 处理器 + */ +@Slf4j +public class TFOClientHandler extends ChannelInboundHandlerAdapter { + + private ByteBuf content; + private ChannelHandlerContext ctx; + + @Override + public void channelActive(ChannelHandlerContext ctx) { + this.ctx = ctx; + content = ctx.alloc().directBuffer(256).writeBytes("Hello flydean.com".getBytes(StandardCharsets.UTF_8)); + // 发送消息 + sayHello(); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + // 异常处理 + log.error("出现异常",cause); + ctx.close(); + } + + private void sayHello() { + // 向服务器输出消息 + ctx.writeAndFlush(content.retain()); + } +} diff --git a/learn-netty4/src/main/java/com/flydean44/tfo/TFOServer.java b/learn-netty4/src/main/java/com/flydean44/tfo/TFOServer.java new file mode 100644 index 0000000..29fffab --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean44/tfo/TFOServer.java @@ -0,0 +1,71 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean44.tfo; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import lombok.AllArgsConstructor; + +/** + * @author wayne + * @version TFOServer, 2021/8/1 + */ +@AllArgsConstructor +public class TFOServer { + + private final int port; + + public void start() throws InterruptedException { + //建立两个EventloopGroup用来处理连接和消息 + EventLoopGroup bossGroup = new NioEventLoopGroup(); + EventLoopGroup workerGroup = new NioEventLoopGroup(); + try { + ServerBootstrap b = new ServerBootstrap(); + b.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .childHandler(new ChannelInitializer() { + @Override + public void initChannel(SocketChannel ch) throws Exception { + ch.pipeline().addLast(new TFOServerHandler()); + } + }) + .option(ChannelOption.SO_BACKLOG, 128) + .option(ChannelOption.TCP_FASTOPEN, 50) + .childOption(ChannelOption.SO_KEEPALIVE, true); + + // 绑定端口并开始接收连接 + ChannelFuture f = b.bind(port).sync(); + // 等待server socket关闭 + f.channel().closeFuture().sync(); + } finally { + //关闭group + workerGroup.shutdownGracefully(); + bossGroup.shutdownGracefully(); + } + } + + public static void main(String[] args) throws InterruptedException { + int port=8000; + new TFOServer(port).start(); + } + +} diff --git a/learn-netty4/src/main/java/com/flydean44/tfo/TFOServerHandler.java b/learn-netty4/src/main/java/com/flydean44/tfo/TFOServerHandler.java new file mode 100644 index 0000000..73cca65 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean44/tfo/TFOServerHandler.java @@ -0,0 +1,53 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean44.tfo; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.util.ReferenceCountUtil; +import lombok.extern.slf4j.Slf4j; + +/** + * @author wayne + * @version TFOServer, 2021/8/1 + */ +@Slf4j +public class TFOServerHandler extends ChannelInboundHandlerAdapter { + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + // 对消息进行处理 + ByteBuf in = (ByteBuf) msg; + try { + log.info("收到消息:{}",in.toString(io.netty.util.CharsetUtil.US_ASCII)); + }finally { + ReferenceCountUtil.release(msg); + } + +// try { +// // 消息处理 +// } finally { +// ReferenceCountUtil.release(msg); +// } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + // 异常处理 + log.error("出现异常",cause); + ctx.close(); + } +} diff --git a/learn-netty4/src/main/java/com/flydean49/extendEnum/Programmer.java b/learn-netty4/src/main/java/com/flydean49/extendEnum/Programmer.java new file mode 100644 index 0000000..8f29291 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean49/extendEnum/Programmer.java @@ -0,0 +1,24 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean49.extendEnum; + +public class Programmer { + + Programmer(int type, String desc){ + + } + +} diff --git a/learn-netty4/src/main/java/com/flydean49/extendEnum/StatusEnum.java b/learn-netty4/src/main/java/com/flydean49/extendEnum/StatusEnum.java new file mode 100644 index 0000000..936c88b --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean49/extendEnum/StatusEnum.java @@ -0,0 +1,41 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean49.extendEnum; + +import lombok.Data; + +public enum StatusEnum { + START(1,"start"), + INPROCESS(2,"inprocess"), + END(3,"end"); + + private int code; + private String desc; + + StatusEnum(int code, String desc){ + this.code=code; + this.desc=desc; + } + + public static void main(String[] args) { + StatusEnum start = START; + System.out.println(start.name()); + System.out.println(start.ordinal()); + System.out.println(start.code); + System.out.println(start.desc); + } + +} diff --git a/learn-netty4/src/main/java/com/flydean49/extendEnum/SumEnum.java b/learn-netty4/src/main/java/com/flydean49/extendEnum/SumEnum.java new file mode 100644 index 0000000..cb7c85f --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean49/extendEnum/SumEnum.java @@ -0,0 +1,4 @@ +package com.flydean49.extendEnum; + +//public enum SumEnum extends StatusEnum{ +//} diff --git a/learn-netty4/src/main/java/com/flydean51/affinity/AffinityServer.java b/learn-netty4/src/main/java/com/flydean51/affinity/AffinityServer.java new file mode 100644 index 0000000..4a3186a --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean51/affinity/AffinityServer.java @@ -0,0 +1,81 @@ +package com.flydean51.affinity; +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import lombok.AllArgsConstructor; +import net.openhft.affinity.AffinityStrategies; +import net.openhft.affinity.AffinityThreadFactory; + +import java.util.concurrent.ThreadFactory; + +/** + * @author wayne + * @version AffinityServer, 2021/8/1 + */ +@AllArgsConstructor +public class AffinityServer { + + final int acceptorThreads = 2; + final int workerThreads = 5; + + private final int port; + + public void start() throws InterruptedException { + + //建立两个EventloopGroup用来处理连接和消息 + EventLoopGroup acceptorGroup = new NioEventLoopGroup(acceptorThreads); + //创建AffinityThreadFactory + ThreadFactory threadFactory = new AffinityThreadFactory("affinityWorker", AffinityStrategies.DIFFERENT_CORE,AffinityStrategies.DIFFERENT_SOCKET,AffinityStrategies.ANY); + //将AffinityThreadFactory加入workerGroup + EventLoopGroup workerGroup = new NioEventLoopGroup(workerThreads,threadFactory); + try { + ServerBootstrap b = new ServerBootstrap(); + b.group(acceptorGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .childHandler(new ChannelInitializer() { + @Override + public void initChannel(SocketChannel ch) throws Exception { + ch.pipeline().addLast(new AffinityServerHandler()); + } + }) + .option(ChannelOption.SO_BACKLOG, 128) + .childOption(ChannelOption.SO_KEEPALIVE, true); + + // 绑定端口并开始接收连接 + ChannelFuture f = b.bind(port).sync(); + + // 等待server socket关闭 + f.channel().closeFuture().sync(); + } finally { + //关闭group + workerGroup.shutdownGracefully(); + acceptorGroup.shutdownGracefully(); + } + } + + public static void main(String[] args) throws InterruptedException { + int port=8000; + new AffinityServer(port).start(); + } + +} diff --git a/learn-netty4/src/main/java/com/flydean51/affinity/AffinityServerHandler.java b/learn-netty4/src/main/java/com/flydean51/affinity/AffinityServerHandler.java new file mode 100644 index 0000000..f4696c9 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean51/affinity/AffinityServerHandler.java @@ -0,0 +1,46 @@ +package com.flydean51.affinity; +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.util.ReferenceCountUtil; +import lombok.extern.slf4j.Slf4j; + +/** + * @author wayne + * @version AffinityServerHandler, 2021/8/1 + */ +@Slf4j +public class AffinityServerHandler extends ChannelInboundHandlerAdapter { + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + // 对消息进行处理 + ByteBuf in = (ByteBuf) msg; + try { + log.info("收到消息:{}",in.toString(io.netty.util.CharsetUtil.US_ASCII)); + }finally { + ReferenceCountUtil.release(msg); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + // 异常处理 + log.error("出现异常",cause); + ctx.close(); + } +} diff --git a/learn-netty4/src/main/java/com/flydean52/nativetransport/epoll/NativeChatClient.java b/learn-netty4/src/main/java/com/flydean52/nativetransport/epoll/NativeChatClient.java new file mode 100644 index 0000000..daa6747 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean52/nativetransport/epoll/NativeChatClient.java @@ -0,0 +1,77 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean52.nativetransport.epoll; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.epoll.EpollEventLoopGroup; +import io.netty.channel.epoll.EpollSocketChannel; +import io.netty.channel.kqueue.KQueueEventLoopGroup; +import io.netty.channel.kqueue.KQueueSocketChannel; +import lombok.extern.slf4j.Slf4j; + +import java.io.BufferedReader; +import java.io.InputStreamReader; + +/** + * 聊天室客户端 + */ +@Slf4j +public final class NativeChatClient { + + static final String HOST = System.getProperty("host", "127.0.0.1"); + static final int PORT = Integer.parseInt(System.getProperty("port", "8000")); + + public static void main(String[] args) throws Exception { + + EventLoopGroup group = new EpollEventLoopGroup(); + try { + Bootstrap b = new Bootstrap(); + b.group(group) + .channel(EpollSocketChannel.class) + .handler(new NativeChatClientInitializer()); + + // 建立连接 + Channel ch = b.connect(HOST, PORT).sync().channel(); + log.info("client channel: {}", ch); + + // 从命令行输入 + ChannelFuture lastWriteFuture = null; + BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); + for (;;) { + String line = in.readLine(); + if (line == null) { + break; + } + // 将从命令行输入的一行字符写到channel中 + lastWriteFuture = ch.writeAndFlush(line + "\r\n"); + // 如果输入'再见',则等待server端关闭channel + if ("再见".equalsIgnoreCase(line)) { + ch.closeFuture().sync(); + break; + } + } + // 等待所有的消息都写入channel中 + if (lastWriteFuture != null) { + lastWriteFuture.sync(); + } + } finally { + group.shutdownGracefully(); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean52/nativetransport/epoll/NativeChatClientHandler.java b/learn-netty4/src/main/java/com/flydean52/nativetransport/epoll/NativeChatClientHandler.java new file mode 100644 index 0000000..78d37ca --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean52/nativetransport/epoll/NativeChatClientHandler.java @@ -0,0 +1,42 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean52.nativetransport.epoll; + +import io.netty.channel.ChannelHandler.Sharable; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import lombok.extern.slf4j.Slf4j; + +/** + * 客户端处理器 + */ +@Sharable +@Slf4j +public class NativeChatClientHandler extends SimpleChannelInboundHandler { + + @Override + protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception { + log.info("client accepted channel: {}", ctx.channel()); + log.info("接收到消息:{}",msg); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + // 异常处理 + log.error("出现异常",cause); + ctx.close(); + } +} diff --git a/learn-netty4/src/main/java/com/flydean52/nativetransport/epoll/NativeChatClientInitializer.java b/learn-netty4/src/main/java/com/flydean52/nativetransport/epoll/NativeChatClientInitializer.java new file mode 100644 index 0000000..a64ec36 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean52/nativetransport/epoll/NativeChatClientInitializer.java @@ -0,0 +1,48 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean52.nativetransport.epoll; + +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.DelimiterBasedFrameDecoder; +import io.netty.handler.codec.Delimiters; +import io.netty.handler.codec.string.StringDecoder; +import io.netty.handler.codec.string.StringEncoder; + +/** + * 客户端ChannelPipeline的配置 + */ +public class NativeChatClientInitializer extends ChannelInitializer { + + private static final StringDecoder DECODER = new StringDecoder(); + private static final StringEncoder ENCODER = new StringEncoder(); + + private static final NativeChatClientHandler CLIENT_HANDLER = new NativeChatClientHandler(); + + @Override + public void initChannel(SocketChannel ch) { + ChannelPipeline pipeline = ch.pipeline(); + + // 添加行分割器 + pipeline.addLast(new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter())); + pipeline.addLast(DECODER); + pipeline.addLast(ENCODER); + + // 添加客户端处理器 + pipeline.addLast(CLIENT_HANDLER); + } +} diff --git a/learn-netty4/src/main/java/com/flydean52/nativetransport/epoll/NativeChatServer.java b/learn-netty4/src/main/java/com/flydean52/nativetransport/epoll/NativeChatServer.java new file mode 100644 index 0000000..17ba5a6 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean52/nativetransport/epoll/NativeChatServer.java @@ -0,0 +1,56 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean52.nativetransport.epoll; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.epoll.EpollEventLoopGroup; +import io.netty.channel.epoll.EpollServerSocketChannel; +import io.netty.channel.kqueue.KQueueEventLoopGroup; +import io.netty.channel.kqueue.KQueueServerSocketChannel; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import lombok.extern.slf4j.Slf4j; + +/** + * native chat server 服务器 + */ +@Slf4j +public final class NativeChatServer { + + static final int PORT = Integer.parseInt(System.getProperty("port", "8000")); + + public static void main(String[] args) throws Exception { + + EventLoopGroup bossGroup = new EpollEventLoopGroup(1); + EventLoopGroup workerGroup = new EpollEventLoopGroup(); + try { + ServerBootstrap b = new ServerBootstrap(); + b.group(bossGroup, workerGroup) + .channel(EpollServerSocketChannel.class) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new NativeChatServerInitializer()); + + Channel channel = b.bind(PORT).sync().channel(); + log.info("server channel:{}", channel); + channel.closeFuture().sync(); + } finally { + bossGroup.shutdownGracefully(); + workerGroup.shutdownGracefully(); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean52/nativetransport/epoll/NativeChatServerHandler.java b/learn-netty4/src/main/java/com/flydean52/nativetransport/epoll/NativeChatServerHandler.java new file mode 100644 index 0000000..d3e543c --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean52/nativetransport/epoll/NativeChatServerHandler.java @@ -0,0 +1,75 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean52.nativetransport.epoll; + +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandler.Sharable; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import lombok.extern.slf4j.Slf4j; + +/** + * native server端的处理器 + */ +@Sharable +@Slf4j +public class NativeChatServerHandler extends SimpleChannelInboundHandler { + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + log.info("accepted channel: {}", ctx.channel()); + log.info("accepted channel parent: {}", ctx.channel().parent()); + // channel活跃 + ctx.write("Channel Active状态!\r\n"); + ctx.flush(); + } + + @Override + public void channelRead0(ChannelHandlerContext ctx, String request) throws Exception { + // 如果读取到"再见"就关闭channel + String response; + // 判断是否关闭 + boolean close = false; + if (request.isEmpty()) { + response = "你说啥?\r\n"; + } else if ("再见".equalsIgnoreCase(request)) { + response = "再见,我的朋友!\r\n"; + close = true; + } else { + response = "你是不是说: '" + request + "'?\r\n"; + } + + // 写入消息 + ChannelFuture future = ctx.write(response); + // 添加CLOSE listener,用来关闭channel + if (close) { + future.addListener(ChannelFutureListener.CLOSE); + } + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + ctx.flush(); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + // 异常处理 + log.error("出现异常",cause); + ctx.close(); + } +} diff --git a/learn-netty4/src/main/java/com/flydean52/nativetransport/epoll/NativeChatServerInitializer.java b/learn-netty4/src/main/java/com/flydean52/nativetransport/epoll/NativeChatServerInitializer.java new file mode 100644 index 0000000..6b14b8e --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean52/nativetransport/epoll/NativeChatServerInitializer.java @@ -0,0 +1,47 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean52.nativetransport.epoll; + +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.DelimiterBasedFrameDecoder; +import io.netty.handler.codec.Delimiters; +import io.netty.handler.codec.string.StringDecoder; +import io.netty.handler.codec.string.StringEncoder; + +/** + * 初始化一个ChannelPipeline + */ +public class NativeChatServerInitializer extends ChannelInitializer { + + private static final StringDecoder DECODER = new StringDecoder(); + private static final StringEncoder ENCODER = new StringEncoder(); + + private static final NativeChatServerHandler SERVER_HANDLER = new NativeChatServerHandler(); + + @Override + public void initChannel(SocketChannel ch) throws Exception { + ChannelPipeline pipeline = ch.pipeline(); + // 添加行分割器 + pipeline.addLast(new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter())); + // 添加String Decoder和String Encoder,用来进行字符串的转换 + pipeline.addLast(DECODER); + pipeline.addLast(ENCODER); + // 最后添加真正的处理器 + pipeline.addLast(SERVER_HANDLER); + } +} diff --git a/learn-netty4/src/main/java/com/flydean52/nativetransport/kqueue/NativeChatClient.java b/learn-netty4/src/main/java/com/flydean52/nativetransport/kqueue/NativeChatClient.java new file mode 100644 index 0000000..cccd61f --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean52/nativetransport/kqueue/NativeChatClient.java @@ -0,0 +1,75 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean52.nativetransport.kqueue; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.kqueue.KQueueEventLoopGroup; +import io.netty.channel.kqueue.KQueueSocketChannel; +import lombok.extern.slf4j.Slf4j; + +import java.io.BufferedReader; +import java.io.InputStreamReader; + +/** + * 聊天室客户端 + */ +@Slf4j +public final class NativeChatClient { + + static final String HOST = System.getProperty("host", "127.0.0.1"); + static final int PORT = Integer.parseInt(System.getProperty("port", "8000")); + + public static void main(String[] args) throws Exception { + + EventLoopGroup group = new KQueueEventLoopGroup(); + try { + Bootstrap b = new Bootstrap(); + b.group(group) + .channel(KQueueSocketChannel.class) + .handler(new NativeChatClientInitializer()); + + // 建立连接 + Channel ch = b.connect(HOST, PORT).sync().channel(); + log.info("client channel: {}", ch); + + // 从命令行输入 + ChannelFuture lastWriteFuture = null; + BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); + for (;;) { + String line = in.readLine(); + if (line == null) { + break; + } + // 将从命令行输入的一行字符写到channel中 + lastWriteFuture = ch.writeAndFlush(line + "\r\n"); + // 如果输入'再见',则等待server端关闭channel + if ("再见".equalsIgnoreCase(line)) { + ch.closeFuture().sync(); + break; + } + } + // 等待所有的消息都写入channel中 + if (lastWriteFuture != null) { + lastWriteFuture.sync(); + } + } finally { + group.shutdownGracefully(); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean52/nativetransport/kqueue/NativeChatClientHandler.java b/learn-netty4/src/main/java/com/flydean52/nativetransport/kqueue/NativeChatClientHandler.java new file mode 100644 index 0000000..127cdf4 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean52/nativetransport/kqueue/NativeChatClientHandler.java @@ -0,0 +1,42 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean52.nativetransport.kqueue; + +import io.netty.channel.ChannelHandler.Sharable; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import lombok.extern.slf4j.Slf4j; + +/** + * 客户端处理器 + */ +@Sharable +@Slf4j +public class NativeChatClientHandler extends SimpleChannelInboundHandler { + + @Override + protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception { + log.info("client accepted channel: {}", ctx.channel()); + log.info("接收到消息:{}",msg); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + // 异常处理 + log.error("出现异常",cause); + ctx.close(); + } +} diff --git a/learn-netty4/src/main/java/com/flydean52/nativetransport/kqueue/NativeChatClientInitializer.java b/learn-netty4/src/main/java/com/flydean52/nativetransport/kqueue/NativeChatClientInitializer.java new file mode 100644 index 0000000..85501aa --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean52/nativetransport/kqueue/NativeChatClientInitializer.java @@ -0,0 +1,48 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean52.nativetransport.kqueue; + +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.DelimiterBasedFrameDecoder; +import io.netty.handler.codec.Delimiters; +import io.netty.handler.codec.string.StringDecoder; +import io.netty.handler.codec.string.StringEncoder; + +/** + * 客户端ChannelPipeline的配置 + */ +public class NativeChatClientInitializer extends ChannelInitializer { + + private static final StringDecoder DECODER = new StringDecoder(); + private static final StringEncoder ENCODER = new StringEncoder(); + + private static final NativeChatClientHandler CLIENT_HANDLER = new NativeChatClientHandler(); + + @Override + public void initChannel(SocketChannel ch) { + ChannelPipeline pipeline = ch.pipeline(); + + // 添加行分割器 + pipeline.addLast(new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter())); + pipeline.addLast(DECODER); + pipeline.addLast(ENCODER); + + // 添加客户端处理器 + pipeline.addLast(CLIENT_HANDLER); + } +} diff --git a/learn-netty4/src/main/java/com/flydean52/nativetransport/kqueue/NativeChatServer.java b/learn-netty4/src/main/java/com/flydean52/nativetransport/kqueue/NativeChatServer.java new file mode 100644 index 0000000..6d31377 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean52/nativetransport/kqueue/NativeChatServer.java @@ -0,0 +1,55 @@ + +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean52.nativetransport.kqueue; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.kqueue.KQueueEventLoopGroup; +import io.netty.channel.kqueue.KQueueServerSocketChannel; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import lombok.extern.slf4j.Slf4j; + +/** + * native chat server 服务器 + */ +@Slf4j +public final class NativeChatServer { + + static final int PORT = Integer.parseInt(System.getProperty("port", "8000")); + + public static void main(String[] args) throws Exception { + + EventLoopGroup bossGroup = new KQueueEventLoopGroup(1); + EventLoopGroup workerGroup = new KQueueEventLoopGroup(); + try { + ServerBootstrap b = new ServerBootstrap(); + b.group(bossGroup, workerGroup) + .channel(KQueueServerSocketChannel.class) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new NativeChatServerInitializer()); + + Channel channel = b.bind(PORT).sync().channel(); + log.info("server channel:{}", channel); + channel.closeFuture().sync(); + } finally { + bossGroup.shutdownGracefully(); + workerGroup.shutdownGracefully(); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean52/nativetransport/kqueue/NativeChatServerHandler.java b/learn-netty4/src/main/java/com/flydean52/nativetransport/kqueue/NativeChatServerHandler.java new file mode 100644 index 0000000..f0562dd --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean52/nativetransport/kqueue/NativeChatServerHandler.java @@ -0,0 +1,75 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean52.nativetransport.kqueue; + +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandler.Sharable; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import lombok.extern.slf4j.Slf4j; + +/** + * native server端的处理器 + */ +@Sharable +@Slf4j +public class NativeChatServerHandler extends SimpleChannelInboundHandler { + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + log.info("accepted channel: {}", ctx.channel()); + log.info("accepted channel parent: {}", ctx.channel().parent()); + // channel活跃 + ctx.write("Channel Active状态!\r\n"); + ctx.flush(); + } + + @Override + public void channelRead0(ChannelHandlerContext ctx, String request) throws Exception { + // 如果读取到"再见"就关闭channel + String response; + // 判断是否关闭 + boolean close = false; + if (request.isEmpty()) { + response = "你说啥?\r\n"; + } else if ("再见".equalsIgnoreCase(request)) { + response = "再见,我的朋友!\r\n"; + close = true; + } else { + response = "你是不是说: '" + request + "'?\r\n"; + } + + // 写入消息 + ChannelFuture future = ctx.write(response); + // 添加CLOSE listener,用来关闭channel + if (close) { + future.addListener(ChannelFutureListener.CLOSE); + } + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + ctx.flush(); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + // 异常处理 + log.error("出现异常",cause); + ctx.close(); + } +} diff --git a/learn-netty4/src/main/java/com/flydean52/nativetransport/kqueue/NativeChatServerInitializer.java b/learn-netty4/src/main/java/com/flydean52/nativetransport/kqueue/NativeChatServerInitializer.java new file mode 100644 index 0000000..a4320dd --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean52/nativetransport/kqueue/NativeChatServerInitializer.java @@ -0,0 +1,47 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean52.nativetransport.kqueue; + +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.DelimiterBasedFrameDecoder; +import io.netty.handler.codec.Delimiters; +import io.netty.handler.codec.string.StringDecoder; +import io.netty.handler.codec.string.StringEncoder; + +/** + * 初始化一个ChannelPipeline + */ +public class NativeChatServerInitializer extends ChannelInitializer { + + private static final StringDecoder DECODER = new StringDecoder(); + private static final StringEncoder ENCODER = new StringEncoder(); + + private static final NativeChatServerHandler SERVER_HANDLER = new NativeChatServerHandler(); + + @Override + public void initChannel(SocketChannel ch) throws Exception { + ChannelPipeline pipeline = ch.pipeline(); + // 添加行分割器 + pipeline.addLast(new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter())); + // 添加String Decoder和String Encoder,用来进行字符串的转换 + pipeline.addLast(DECODER); + pipeline.addLast(ENCODER); + // 最后添加真正的处理器 + pipeline.addLast(SERVER_HANDLER); + } +} diff --git a/learn-netty4/src/main/java/com/flydean54/dnstcp/Do53TcpChannelInboundHandler.java b/learn-netty4/src/main/java/com/flydean54/dnstcp/Do53TcpChannelInboundHandler.java new file mode 100644 index 0000000..cddbbdb --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean54/dnstcp/Do53TcpChannelInboundHandler.java @@ -0,0 +1,52 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean54.dnstcp; + +import io.netty.buffer.ByteBufUtil; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.dns.*; +import io.netty.util.NetUtil; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +class Do53TcpChannelInboundHandler extends SimpleChannelInboundHandler { + @Override + protected void channelRead0(ChannelHandlerContext ctx, DefaultDnsResponse msg) { + try { + readMsg(msg); + } finally { + ctx.close(); + } + } + + private static void readMsg(DefaultDnsResponse msg) { + if (msg.count(DnsSection.QUESTION) > 0) { + DnsQuestion question = msg.recordAt(DnsSection.QUESTION, 0); + log.info("question is :{}",question); + } + int i = 0, count = msg.count(DnsSection.ANSWER); + while (i < count) { + DnsRecord record = msg.recordAt(DnsSection.ANSWER, i); + //A记录用来指定主机名或者域名对应的IP地址 + if (record.type() == DnsRecordType.A) { + DnsRawRecord raw = (DnsRawRecord) record; + log.info("ip address is: {}",NetUtil.bytesToIpAddress(ByteBufUtil.getBytes(raw.content()))); + } + i++; + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean54/dnstcp/Do53TcpChannelInitializer.java b/learn-netty4/src/main/java/com/flydean54/dnstcp/Do53TcpChannelInitializer.java new file mode 100644 index 0000000..48676f7 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean54/dnstcp/Do53TcpChannelInitializer.java @@ -0,0 +1,32 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean54.dnstcp; + +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.dns.TcpDnsQueryEncoder; +import io.netty.handler.codec.dns.TcpDnsResponseDecoder; + +public class Do53TcpChannelInitializer extends ChannelInitializer { + @Override + protected void initChannel(SocketChannel ch) { + ChannelPipeline p = ch.pipeline(); + p.addLast(new TcpDnsQueryEncoder()) + .addLast(new TcpDnsResponseDecoder()) + .addLast(new Do53TcpChannelInboundHandler()); + } +} diff --git a/learn-netty4/src/main/java/com/flydean54/dnstcp/Do53TcpClient.java b/learn-netty4/src/main/java/com/flydean54/dnstcp/Do53TcpClient.java new file mode 100644 index 0000000..90ce3c3 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean54/dnstcp/Do53TcpClient.java @@ -0,0 +1,63 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean54.dnstcp; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.Channel; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.dns.*; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.TimeUnit; + +@Slf4j +public final class Do53TcpClient { + + public void startDnsClient(String dnsServer,int dnsPort, String queryDomain) throws InterruptedException { + EventLoopGroup group = new NioEventLoopGroup(); + try { + Bootstrap b = new Bootstrap(); + b.group(group) + .channel(NioSocketChannel.class) + .handler(new Do53TcpChannelInitializer()); + + final Channel ch = b.connect(dnsServer, dnsPort).sync().channel(); + + int randomID = (int) (System.currentTimeMillis() / 1000); + DnsQuery query = new DefaultDnsQuery(randomID, DnsOpCode.QUERY) + .setRecord(DnsSection.QUESTION, new DefaultDnsQuestion(queryDomain, DnsRecordType.A)); + ch.writeAndFlush(query).sync(); + boolean result = ch.closeFuture().await(10, TimeUnit.SECONDS); + if (!result) { + log.error("DNS查询失败"); + ch.close().sync(); + } + } finally { + group.shutdownGracefully(); + } + } + + public static void main(String[] args) throws Exception { + Do53TcpClient client = new Do53TcpClient(); + final String dnsServer = "223.5.5.5"; + final int dnsPort = 53; + final String queryDomain ="www.flydean.com"; + client.startDnsClient(dnsServer,dnsPort,queryDomain); + } + +} diff --git a/learn-netty4/src/main/java/com/flydean55/dnsudp/Do53UdpChannelInboundHandler.java b/learn-netty4/src/main/java/com/flydean55/dnsudp/Do53UdpChannelInboundHandler.java new file mode 100644 index 0000000..eb7fd82 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean55/dnsudp/Do53UdpChannelInboundHandler.java @@ -0,0 +1,52 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean55.dnsudp; + +import io.netty.buffer.ByteBufUtil; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.dns.*; +import io.netty.util.NetUtil; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +class Do53UdpChannelInboundHandler extends SimpleChannelInboundHandler { + @Override + protected void channelRead0(ChannelHandlerContext ctx, DatagramDnsResponse msg) { + try { + readMsg(msg); + } finally { + ctx.close(); + } + } + + private static void readMsg(DatagramDnsResponse msg) { + if (msg.count(DnsSection.QUESTION) > 0) { + DnsQuestion question = msg.recordAt(DnsSection.QUESTION, 0); + log.info("question is :{}", question); + } + int i = 0, count = msg.count(DnsSection.ANSWER); + while (i < count) { + DnsRecord record = msg.recordAt(DnsSection.ANSWER, i); + if (record.type() == DnsRecordType.A) { + //A记录用来指定主机名或者域名对应的IP地址 + DnsRawRecord raw = (DnsRawRecord) record; + log.info("ip address is: {}",NetUtil.bytesToIpAddress(ByteBufUtil.getBytes(raw.content()))); + } + i++; + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean55/dnsudp/Do53UdpChannelInitializer.java b/learn-netty4/src/main/java/com/flydean55/dnsudp/Do53UdpChannelInitializer.java new file mode 100644 index 0000000..8cd4bf4 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean55/dnsudp/Do53UdpChannelInitializer.java @@ -0,0 +1,34 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean55.dnsudp; + +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.socket.DatagramChannel; +import io.netty.handler.codec.dns.DatagramDnsQueryEncoder; +import io.netty.handler.codec.dns.DatagramDnsResponseDecoder; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +class Do53UdpChannelInitializer extends ChannelInitializer { + @Override + protected void initChannel(DatagramChannel ch) throws Exception { + ChannelPipeline p = ch.pipeline(); + p.addLast(new DatagramDnsQueryEncoder()) + .addLast(new DatagramDnsResponseDecoder()) + .addLast(new Do53UdpChannelInboundHandler()); + } +} diff --git a/learn-netty4/src/main/java/com/flydean55/dnsudp/Do53UdpClient.java b/learn-netty4/src/main/java/com/flydean55/dnsudp/Do53UdpClient.java new file mode 100644 index 0000000..a5867fa --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean55/dnsudp/Do53UdpClient.java @@ -0,0 +1,67 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean55.dnsudp; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.Channel; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioDatagramChannel; +import io.netty.handler.codec.dns.*; +import lombok.extern.slf4j.Slf4j; + +import java.net.InetSocketAddress; +import java.util.concurrent.TimeUnit; + +@Slf4j +public final class Do53UdpClient { + + public void startDnsClient(String dnsServer, int dnsPort, String queryDomain) throws InterruptedException { + InetSocketAddress addr = new InetSocketAddress(dnsServer, dnsPort); + EventLoopGroup group = new NioEventLoopGroup(); + try { + Bootstrap b = new Bootstrap(); + b.group(group) + .channel(NioDatagramChannel.class) + .handler(new Do53UdpChannelInitializer()); + + final Channel ch = b.bind(0).sync().channel(); + + int randomID = (int) (System.currentTimeMillis() / 1000); + DnsQuery query = new DatagramDnsQuery(null, addr, randomID).setRecord( + DnsSection.QUESTION, + new DefaultDnsQuestion(queryDomain, DnsRecordType.A)); + ch.writeAndFlush(query).sync(); + boolean result = ch.closeFuture().await(10, TimeUnit.SECONDS); + if (!result) { + log.error("DNS查询失败"); + ch.close().sync(); + } + } finally { + group.shutdownGracefully(); + } + } + + public static void main(String[] args) throws Exception { + + Do53UdpClient client = new Do53UdpClient(); + final String dnsServer = "223.5.5.5"; + final int dnsPort = 53; + final String queryDomain = "www.flydean.com"; + client.startDnsClient(dnsServer, dnsPort, queryDomain); + } + +} diff --git a/learn-netty4/src/main/java/com/flydean56/dnsdot/DoTClient.java b/learn-netty4/src/main/java/com/flydean56/dnsdot/DoTClient.java new file mode 100644 index 0000000..8a3f561 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean56/dnsdot/DoTClient.java @@ -0,0 +1,76 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean56.dnsdot; + +import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.ByteBufUtil; +import io.netty.channel.*; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.dns.*; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.SslProvider; +import io.netty.util.NetUtil; +import lombok.extern.slf4j.Slf4j; + +import javax.net.ssl.SSLException; +import java.util.concurrent.TimeUnit; + +@Slf4j +public final class DoTClient { + + public static void main(String[] args) throws Exception { + DoTClient client = new DoTClient(); + final String dnsServer = "223.5.5.5"; + final int dnsPort = 853; + final String queryDomain = "www.flydean.com"; + client.startClient(dnsServer,dnsPort,queryDomain); + } + + private void startClient(String dnsServer, int dnsPort, String queryDomain) throws SSLException, InterruptedException { + + EventLoopGroup group = new NioEventLoopGroup(); + + try { + SslProvider provider = + SslProvider.isAlpnSupported(SslProvider.OPENSSL)? SslProvider.OPENSSL : SslProvider.JDK; + final SslContext sslContext = SslContextBuilder.forClient() + .sslProvider(provider) + .protocols("TLSv1.3", "TLSv1.2") + .build(); + + Bootstrap b = new Bootstrap(); + b.group(group) + .channel(NioSocketChannel.class) + .handler(new DotChannelInitializer(sslContext, dnsServer, dnsPort)); + final Channel ch = b.connect(dnsServer, dnsPort).sync().channel(); + + int randomID = (int) (System.currentTimeMillis() / 1000); + DnsQuery query = new DefaultDnsQuery(randomID, DnsOpCode.QUERY) + .setRecord(DnsSection.QUESTION, new DefaultDnsQuestion(queryDomain, DnsRecordType.A)); + ch.writeAndFlush(query).sync(); + boolean result = ch.closeFuture().await(10, TimeUnit.SECONDS); + if (!result) { + log.error("DNS查询失败"); + ch.close().sync(); + } + } finally { + group.shutdownGracefully(); + } + } + +} diff --git a/learn-netty4/src/main/java/com/flydean56/dnsdot/DotChannelInboundHandler.java b/learn-netty4/src/main/java/com/flydean56/dnsdot/DotChannelInboundHandler.java new file mode 100644 index 0000000..8fb3c35 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean56/dnsdot/DotChannelInboundHandler.java @@ -0,0 +1,52 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean56.dnsdot; + +import io.netty.buffer.ByteBufUtil; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.dns.*; +import io.netty.util.NetUtil; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +class DotChannelInboundHandler extends SimpleChannelInboundHandler { + @Override + protected void channelRead0(ChannelHandlerContext ctx, DefaultDnsResponse msg) { + try { + readMsg(msg); + } finally { + ctx.close(); + } + } + + private static void readMsg(DefaultDnsResponse msg) { + if (msg.count(DnsSection.QUESTION) > 0) { + DnsQuestion question = msg.recordAt(DnsSection.QUESTION, 0); + log.info("question is :{}", question); + } + int i = 0, count = msg.count(DnsSection.ANSWER); + while (i < count) { + DnsRecord record = msg.recordAt(DnsSection.ANSWER, i); + if (record.type() == DnsRecordType.A) { + //A记录用来指定主机名或者域名对应的IP地址 + DnsRawRecord raw = (DnsRawRecord) record; + log.info("ip address is: {}",NetUtil.bytesToIpAddress(ByteBufUtil.getBytes(raw.content()))); + } + i++; + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean56/dnsdot/DotChannelInitializer.java b/learn-netty4/src/main/java/com/flydean56/dnsdot/DotChannelInitializer.java new file mode 100644 index 0000000..c728938 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean56/dnsdot/DotChannelInitializer.java @@ -0,0 +1,45 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean56.dnsdot; + +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.dns.TcpDnsQueryEncoder; +import io.netty.handler.codec.dns.TcpDnsResponseDecoder; +import io.netty.handler.ssl.SslContext; + +class DotChannelInitializer extends ChannelInitializer { + private final SslContext sslContext; + private final String dnsServer; + private final int dnsPort; + + public DotChannelInitializer(SslContext sslContext, String dnsServer, int dnsPort) { + this.sslContext = sslContext; + this.dnsServer = dnsServer; + this.dnsPort = dnsPort; + } + + @Override + protected void initChannel(SocketChannel ch) { + ChannelPipeline p = ch.pipeline(); + p.addLast(sslContext.newHandler(ch.alloc(), dnsServer, dnsPort)) + .addLast(new TcpDnsQueryEncoder()) + .addLast(new TcpDnsResponseDecoder()) + .addLast(new DotChannelInboundHandler()); + } + +} diff --git a/learn-netty4/src/main/java/com/flydean57/dnsserver/Do53ServerChannelInitializer.java b/learn-netty4/src/main/java/com/flydean57/dnsserver/Do53ServerChannelInitializer.java new file mode 100644 index 0000000..6325ea6 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean57/dnsserver/Do53ServerChannelInitializer.java @@ -0,0 +1,30 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean57.dnsserver; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelInitializer; +import io.netty.handler.codec.dns.*; + +class Do53ServerChannelInitializer extends ChannelInitializer { + @Override + protected void initChannel(Channel ch) throws Exception { + ch.pipeline().addLast( + new TcpDnsQueryDecoder(), + new TcpDnsResponseEncoder(), + new Do53ServerInboundHandler()); + } +} diff --git a/learn-netty4/src/main/java/com/flydean57/dnsserver/Do53ServerInboundHandler.java b/learn-netty4/src/main/java/com/flydean57/dnsserver/Do53ServerInboundHandler.java new file mode 100644 index 0000000..731150e --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean57/dnsserver/Do53ServerInboundHandler.java @@ -0,0 +1,51 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean57.dnsserver; + +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.dns.*; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +class Do53ServerInboundHandler extends SimpleChannelInboundHandler { + + private static final byte[] QUERY_RESULT = new byte[]{46, 53, 107, 110}; + + @Override + protected void channelRead0(ChannelHandlerContext ctx, + DnsQuery msg) throws Exception { + DnsQuestion question = msg.recordAt(DnsSection.QUESTION); + log.info("Query is: {}", question); + ctx.writeAndFlush(newResponse(msg, question, 1000, QUERY_RESULT)); + } + + private DefaultDnsResponse newResponse(DnsQuery query, + DnsQuestion question, + long ttl, byte[]... addresses) { + DefaultDnsResponse response = new DefaultDnsResponse(query.id()); + response.addRecord(DnsSection.QUESTION, question); + + for (byte[] address : addresses) { + DefaultDnsRawRecord queryAnswer = new DefaultDnsRawRecord( + question.name(), + DnsRecordType.A, ttl, Unpooled.wrappedBuffer(address)); + response.addRecord(DnsSection.ANSWER, queryAnswer); + } + return response; + } +} diff --git a/learn-netty4/src/main/java/com/flydean57/dnsserver/Do53TcpClient.java b/learn-netty4/src/main/java/com/flydean57/dnsserver/Do53TcpClient.java new file mode 100644 index 0000000..5331ea7 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean57/dnsserver/Do53TcpClient.java @@ -0,0 +1,64 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean57.dnsserver; + +import com.flydean54.dnstcp.Do53TcpChannelInitializer; +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.Channel; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.dns.*; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.TimeUnit; + +@Slf4j +public final class Do53TcpClient { + + public void startDnsClient(String dnsServer,int dnsPort, String queryDomain) throws InterruptedException { + EventLoopGroup group = new NioEventLoopGroup(); + try { + Bootstrap b = new Bootstrap(); + b.group(group) + .channel(NioSocketChannel.class) + .handler(new Do53TcpChannelInitializer()); + + final Channel ch = b.connect(dnsServer, dnsPort).sync().channel(); + + int randomID = (int) (System.currentTimeMillis() / 1000); + DnsQuery query = new DefaultDnsQuery(randomID, DnsOpCode.QUERY) + .setRecord(DnsSection.QUESTION, new DefaultDnsQuestion(queryDomain, DnsRecordType.A)); + ch.writeAndFlush(query).sync(); + boolean result = ch.closeFuture().await(10, TimeUnit.SECONDS); + if (!result) { + log.error("DNS查询失败"); + ch.close().sync(); + } + } finally { + group.shutdownGracefully(); + } + } + + public static void main(String[] args) throws Exception { + Do53TcpClient client = new Do53TcpClient(); + final String dnsServer = "127.0.0.1"; + final int dnsPort = 53; + final String queryDomain ="www.flydean.com"; + client.startDnsClient(dnsServer,dnsPort,queryDomain); + } + +} diff --git a/learn-netty4/src/main/java/com/flydean57/dnsserver/Do53TcpServer.java b/learn-netty4/src/main/java/com/flydean57/dnsserver/Do53TcpServer.java new file mode 100644 index 0000000..f57329c --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean57/dnsserver/Do53TcpServer.java @@ -0,0 +1,44 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean57.dnsserver; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.*; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; + +public final class Do53TcpServer { + + public static void main(String[] args) throws Exception { + int dnsServerPort = 53; + startServer(dnsServerPort); + } + + private static void startServer(int dnsServerPort) throws InterruptedException { + EventLoopGroup bossGroup = new NioEventLoopGroup(1); + EventLoopGroup workerGroup = new NioEventLoopGroup(); + ServerBootstrap bootstrap = new ServerBootstrap().group(bossGroup, + workerGroup) + .channel(NioServerSocketChannel.class) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new Do53ServerChannelInitializer()); + final Channel channel = bootstrap.bind(dnsServerPort).channel(); + channel.closeFuture().sync(); + } + +} diff --git a/learn-netty4/src/main/java/com/flydean58/proxyprotocol/ClientHander.java b/learn-netty4/src/main/java/com/flydean58/proxyprotocol/ClientHander.java new file mode 100644 index 0000000..e4c96c7 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean58/proxyprotocol/ClientHander.java @@ -0,0 +1,44 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean58.proxyprotocol; + +import io.netty.channel.*; +import io.netty.handler.codec.haproxy.HAProxyMessage; +import io.netty.handler.codec.haproxy.HAProxyMessageEncoder; + +public class ClientHander extends ChannelOutboundHandlerAdapter { + + @Override + public void handlerAdded(ChannelHandlerContext ctx) throws Exception { + ctx.pipeline().addBefore(ctx.name(), null, HAProxyMessageEncoder.INSTANCE); + super.handlerAdded(ctx); + } + + @Override + public void write(final ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + ChannelFuture future1 = ctx.write(msg, promise); + if (msg instanceof HAProxyMessage) { + future1.addListener((ChannelFutureListener) future2 -> { + if (future2.isSuccess()) { + ctx.pipeline().remove(HAProxyMessageEncoder.INSTANCE); + ctx.pipeline().remove(ClientHander.this); + } else { + ctx.close(); + } + }); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean58/proxyprotocol/ProxyProtocolClient.java b/learn-netty4/src/main/java/com/flydean58/proxyprotocol/ProxyProtocolClient.java new file mode 100644 index 0000000..ac9cd9c --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean58/proxyprotocol/ProxyProtocolClient.java @@ -0,0 +1,62 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package com.flydean58.proxyprotocol; + +import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.haproxy.HAProxyCommand; +import io.netty.handler.codec.haproxy.HAProxyMessage; +import io.netty.handler.codec.haproxy.HAProxyProtocolVersion; +import io.netty.handler.codec.haproxy.HAProxyProxiedProtocol; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import io.netty.util.CharsetUtil; + +public final class ProxyProtocolClient { + + public static void main(String[] args) throws Exception { + String host = "127.0.0.1"; + int port = 8080; + startClient(host, port); + } + + private static void startClient(String host, int port) throws InterruptedException { + EventLoopGroup group = new NioEventLoopGroup(); + try { + Bootstrap b = new Bootstrap(); + b.group(group) + .channel(NioSocketChannel.class) + .handler(new LoggingHandler(LogLevel.INFO)) + .handler(new ClientHander()); + + Channel ch = b.connect(host, port).sync().channel(); + + HAProxyMessage message = new HAProxyMessage( + HAProxyProtocolVersion.V2, HAProxyCommand.PROXY, HAProxyProxiedProtocol.TCP4, + "127.0.0.1", "127.0.0.2", 8000, 9000); + ch.writeAndFlush(message).sync(); + ch.writeAndFlush(Unpooled.copiedBuffer("this is a proxy protocol message!", CharsetUtil.UTF_8)).sync(); + ch.close().sync(); + } finally { + group.shutdownGracefully(); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean58/proxyprotocol/ProxyProtocolServer.java b/learn-netty4/src/main/java/com/flydean58/proxyprotocol/ProxyProtocolServer.java new file mode 100644 index 0000000..e53763e --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean58/proxyprotocol/ProxyProtocolServer.java @@ -0,0 +1,49 @@ + +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean58.proxyprotocol; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; + +public final class ProxyProtocolServer { + + public static void main(String[] args) throws Exception { + int port= 8080; + startServer(port); + } + + private static void startServer(int port) throws InterruptedException { + EventLoopGroup bossGroup = new NioEventLoopGroup(1); + EventLoopGroup workerGroup = new NioEventLoopGroup(); + try { + ServerBootstrap b = new ServerBootstrap(); + b.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new ServerInitializer()); + b.bind(port).sync().channel().closeFuture().sync(); + } finally { + bossGroup.shutdownGracefully(); + workerGroup.shutdownGracefully(); + } + } + +} diff --git a/learn-netty4/src/main/java/com/flydean58/proxyprotocol/ServerInitializer.java b/learn-netty4/src/main/java/com/flydean58/proxyprotocol/ServerInitializer.java new file mode 100644 index 0000000..143924a --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean58/proxyprotocol/ServerInitializer.java @@ -0,0 +1,48 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean58.proxyprotocol; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.haproxy.HAProxyMessage; +import io.netty.handler.codec.haproxy.HAProxyMessageDecoder; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +class ServerInitializer extends ChannelInitializer { + @Override + public void initChannel(SocketChannel ch) throws Exception { + ch.pipeline().addLast( + new LoggingHandler(LogLevel.DEBUG), + new HAProxyMessageDecoder(), + new SimpleChannelInboundHandler() { + @Override + protected void channelRead0(ChannelHandlerContext ctx, Object msg) { + if (msg instanceof HAProxyMessage) { + log.info("proxy message is : {}", msg); + } else if (msg instanceof ByteBuf) { + log.info("bytebuf message is : {}", ByteBufUtil.prettyHexDump((ByteBuf) msg)); + } + } + }); + } +} diff --git a/learn-netty4/src/main/java/com/flydean59/sctp/SctpChatClient.java b/learn-netty4/src/main/java/com/flydean59/sctp/SctpChatClient.java new file mode 100644 index 0000000..363621e --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean59/sctp/SctpChatClient.java @@ -0,0 +1,60 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean59.sctp; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.sctp.SctpChannel; +import io.netty.channel.sctp.SctpChannelOption; +import io.netty.channel.sctp.nio.NioSctpChannel; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; + + +public final class SctpChatClient { + + public static void main(String[] args) throws Exception { + String host = "127.0.0.1"; + int port = 8000; + startClient(host, port); + } + + private static void startClient(String host, int port) throws InterruptedException { + EventLoopGroup group = new NioEventLoopGroup(); + try { + Bootstrap b = new Bootstrap(); + b.group(group) + .channel(NioSctpChannel.class) + .option(SctpChannelOption.SCTP_NODELAY, true) + .handler(new ChannelInitializer() { + @Override + public void initChannel(SctpChannel ch) { + ch.pipeline().addLast( + new LoggingHandler(LogLevel.INFO), + new SctpChatClientHandler()); + } + }); + // 启动client + ChannelFuture f = b.connect(host, port).sync(); + f.channel().closeFuture().sync(); + } finally { + group.shutdownGracefully(); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean59/sctp/SctpChatClientHandler.java b/learn-netty4/src/main/java/com/flydean59/sctp/SctpChatClientHandler.java new file mode 100644 index 0000000..5721627 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean59/sctp/SctpChatClientHandler.java @@ -0,0 +1,58 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean59.sctp; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.sctp.SctpMessage; +import lombok.extern.slf4j.Slf4j; + + +@Slf4j +public class SctpChatClientHandler extends ChannelInboundHandlerAdapter { + + private final ByteBuf firstMessage; + + public SctpChatClientHandler() { + firstMessage = Unpooled.buffer(256); + for (int i = 0; i < firstMessage.capacity(); i++) { + firstMessage.writeByte((byte) i); + } + } + + @Override + public void channelActive(ChannelHandlerContext ctx) { + ctx.writeAndFlush(new SctpMessage(0, 0, firstMessage)); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + ctx.write(msg); + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + ctx.flush(); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + log.error("SctpChatClientHandler exception:{}", cause.getMessage()); + ctx.close(); + } +} diff --git a/learn-netty4/src/main/java/com/flydean59/sctp/SctpChatServer.java b/learn-netty4/src/main/java/com/flydean59/sctp/SctpChatServer.java new file mode 100644 index 0000000..823af46 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean59/sctp/SctpChatServer.java @@ -0,0 +1,62 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean59.sctp; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.sctp.SctpChannel; +import io.netty.channel.sctp.nio.NioSctpServerChannel; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; + +public final class SctpChatServer { + + public static void main(String[] args) throws Exception { + int serverPort = 8000; + startServer(serverPort); + } + + private static void startServer(int serverPort) throws InterruptedException { + EventLoopGroup bossGroup = new NioEventLoopGroup(1); + EventLoopGroup workerGroup = new NioEventLoopGroup(); + final SctpChatServerHandler serverHandler = new SctpChatServerHandler(); + try { + ServerBootstrap b = new ServerBootstrap(); + b.group(bossGroup, workerGroup) + .channel(NioSctpServerChannel.class) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new ChannelInitializer() { + @Override + public void initChannel(SctpChannel ch) { + ch.pipeline().addLast( + new LoggingHandler(LogLevel.INFO), + serverHandler); + } + }); + + // 启动服务器 + ChannelFuture f = b.bind(serverPort).sync(); + // 等待server socket关闭 + f.channel().closeFuture().sync(); + } finally { + bossGroup.shutdownGracefully(); + workerGroup.shutdownGracefully(); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean59/sctp/SctpChatServerHandler.java b/learn-netty4/src/main/java/com/flydean59/sctp/SctpChatServerHandler.java new file mode 100644 index 0000000..1272a2e --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean59/sctp/SctpChatServerHandler.java @@ -0,0 +1,42 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean59.sctp; + +import io.netty.channel.ChannelHandler.Sharable; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import lombok.extern.slf4j.Slf4j; + +@Sharable +@Slf4j +public class SctpChatServerHandler extends ChannelInboundHandlerAdapter { + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + ctx.write(msg); + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + ctx.flush(); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + log.error("SctpChatServerHandler exception:{}",cause.getMessage()); + ctx.close(); + } +} diff --git a/learn-netty4/src/main/java/com/flydean59/sctp/SctpMultiHomingChatClient.java b/learn-netty4/src/main/java/com/flydean59/sctp/SctpMultiHomingChatClient.java new file mode 100644 index 0000000..066f3c6 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean59/sctp/SctpMultiHomingChatClient.java @@ -0,0 +1,83 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean59.sctp; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.sctp.SctpChannel; +import io.netty.channel.sctp.SctpChannelOption; +import io.netty.channel.sctp.nio.NioSctpChannel; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import io.netty.util.internal.SocketUtils; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; + +public final class SctpMultiHomingChatClient { + + public static void main(String[] args) throws Exception { + String clientPrimaryAddress = "127.0.0.1"; + String clientSecondAddress ="127.0.0.2"; + int clientPort = 8001; + String serverAddress= "127.0.0.1"; + int serverPort= 8000; + startClient(clientPrimaryAddress,clientSecondAddress,serverAddress,clientPort,serverPort); + } + + private static void startClient(String clientPrimaryAddress, + String clientSecondAddress, + String serverAddress, + int clientPort, + int serverPort) throws UnknownHostException, InterruptedException { + EventLoopGroup group = new NioEventLoopGroup(); + try { + Bootstrap b = new Bootstrap(); + b.group(group) + .channel(NioSctpChannel.class) + .option(SctpChannelOption.SCTP_NODELAY, true) + .handler(new ChannelInitializer() { + @Override + public void initChannel(SctpChannel ch) throws Exception { + ch.pipeline().addLast( + new LoggingHandler(LogLevel.INFO), + new SctpChatClientHandler()); + } + }); + + InetSocketAddress localAddress = SocketUtils.socketAddress(clientPrimaryAddress, clientPort); + // 绑定第一个address + ChannelFuture bindFuture = b.bind(localAddress).sync(); + + SctpChannel channel = (SctpChannel) bindFuture.channel(); + InetAddress localSecondaryAddress = SocketUtils.addressByName(clientSecondAddress); + // 绑定第二个address + channel.bindAddress(localSecondaryAddress).sync(); + + //连接到服务器 + InetSocketAddress remoteAddress = SocketUtils.socketAddress(serverAddress, serverPort); + ChannelFuture connectFuture = channel.connect(remoteAddress).sync(); + + connectFuture.channel().closeFuture().sync(); + } finally { + group.shutdownGracefully(); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean59/sctp/SctpMultiHomingChatServer.java b/learn-netty4/src/main/java/com/flydean59/sctp/SctpMultiHomingChatServer.java new file mode 100644 index 0000000..c2a68e5 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean59/sctp/SctpMultiHomingChatServer.java @@ -0,0 +1,78 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean59.sctp; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.sctp.SctpChannel; +import io.netty.channel.sctp.SctpServerChannel; +import io.netty.channel.sctp.nio.NioSctpServerChannel; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import io.netty.util.internal.SocketUtils; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; + + +public final class SctpMultiHomingChatServer { + + public static void main(String[] args) throws Exception { + String primaryAddress = "127.0.0.1"; + String secondAddress = "127.0.0.2"; + int port=8000; + startServer(primaryAddress, secondAddress, port); + } + + private static void startServer(String primaryAddress, String secondAddress, int port) throws UnknownHostException, InterruptedException { + EventLoopGroup bossGroup = new NioEventLoopGroup(1); + EventLoopGroup workerGroup = new NioEventLoopGroup(); + try { + ServerBootstrap b = new ServerBootstrap(); + b.group(bossGroup, workerGroup) + .channel(NioSctpServerChannel.class) + .option(ChannelOption.SO_BACKLOG, 100) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new ChannelInitializer() { + @Override + public void initChannel(SctpChannel ch){ + ch.pipeline().addLast( + new LoggingHandler(LogLevel.INFO), + new SctpChatServerHandler()); + } + }); + + InetSocketAddress localAddress = SocketUtils.socketAddress(primaryAddress, port); + InetAddress localSecondaryAddress = SocketUtils.addressByName(secondAddress); + + // 绑定第一个地址 + ChannelFuture bindFuture = b.bind(localAddress).sync(); + SctpServerChannel channel = (SctpServerChannel) bindFuture.channel(); + + //绑定第二个地址 + ChannelFuture connectFuture = channel.bindAddress(localSecondaryAddress).sync(); + connectFuture.channel().closeFuture().sync(); + } finally { + bossGroup.shutdownGracefully(); + workerGroup.shutdownGracefully(); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean61/memcached/MemcachedClient.java b/learn-netty4/src/main/java/com/flydean61/memcached/MemcachedClient.java new file mode 100644 index 0000000..e0d84f6 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean61/memcached/MemcachedClient.java @@ -0,0 +1,72 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean61.memcached; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.*; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioSocketChannel; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; + +public final class MemcachedClient { + + public static void main(String[] args) throws Exception { + String host= "127.0.0.1"; + int port = 11211; + startClient(host, port); + } + + private static void startClient(String host, int port) throws InterruptedException, IOException { + EventLoopGroup group = new NioEventLoopGroup(); + try { + Bootstrap b = new Bootstrap(); + b.group(group) + .channel(NioSocketChannel.class) + .handler(new MemcachedInitializer()); + //启动客户端 + Channel ch = b.connect(host, port).sync().channel(); + // 从命令行读取命令 + System.out.println("输入命令 (退出请输入quit)"); + System.out.println("get "); + System.out.println("set "); + ChannelFuture lastWriteFuture = null; + BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); + while (true) { + String line = in.readLine(); + if (line == null) { + break; + } + if ("quit".equalsIgnoreCase(line)) { + ch.close().sync(); + break; + } + //发送接受到的命令到服务器端 + lastWriteFuture = ch.writeAndFlush(line); + } + + // 等等所有消息都同步 + if (lastWriteFuture != null) { + lastWriteFuture.sync(); + } + } finally { + group.shutdownGracefully(); + } + } + +} diff --git a/learn-netty4/src/main/java/com/flydean61/memcached/MemcachedClientHandler.java b/learn-netty4/src/main/java/com/flydean61/memcached/MemcachedClientHandler.java new file mode 100644 index 0000000..1fd4a9e --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean61/memcached/MemcachedClientHandler.java @@ -0,0 +1,75 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean61.memcached; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.memcache.binary.*; +import io.netty.util.CharsetUtil; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class MemcachedClientHandler extends ChannelDuplexHandler { + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) { + String command = (String) msg; + if (command.startsWith("get ")) { + String keyString = command.substring("get ".length()); + ByteBuf key = Unpooled.wrappedBuffer(keyString.getBytes(CharsetUtil.UTF_8)); + + BinaryMemcacheRequest req = new DefaultBinaryMemcacheRequest(key); + req.setOpcode(BinaryMemcacheOpcodes.GET); + + ctx.write(req, promise); + } else if (command.startsWith("set ")) { + String[] parts = command.split(" ", 3); + if (parts.length < 3) { + throw new IllegalArgumentException("命令格式异常: " + command); + } + String keyString = parts[1]; + String value = parts[2]; + + ByteBuf key = Unpooled.wrappedBuffer(keyString.getBytes(CharsetUtil.UTF_8)); + ByteBuf content = Unpooled.wrappedBuffer(value.getBytes(CharsetUtil.UTF_8)); + ByteBuf extras = ctx.alloc().buffer(8); + extras.writeZero(8); + + BinaryMemcacheRequest req = new DefaultFullBinaryMemcacheRequest(key, extras, content); + req.setOpcode(BinaryMemcacheOpcodes.SET); + + ctx.write(req, promise); + } else { + throw new IllegalStateException("未知消息: " + msg); + } + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + FullBinaryMemcacheResponse res = (FullBinaryMemcacheResponse) msg; + log.info("channelRead: {}",res.content().toString(CharsetUtil.UTF_8)); + res.release(); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + log.error("caught exception: {}", cause.getMessage()); + ctx.close(); + } +} diff --git a/learn-netty4/src/main/java/com/flydean61/memcached/MemcachedInitializer.java b/learn-netty4/src/main/java/com/flydean61/memcached/MemcachedInitializer.java new file mode 100644 index 0000000..3b45c48 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean61/memcached/MemcachedInitializer.java @@ -0,0 +1,32 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean61.memcached; + +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.memcache.binary.BinaryMemcacheClientCodec; +import io.netty.handler.codec.memcache.binary.BinaryMemcacheObjectAggregator; + +class MemcachedInitializer extends ChannelInitializer { + @Override + protected void initChannel(SocketChannel ch) { + ChannelPipeline p = ch.pipeline(); + p.addLast(new BinaryMemcacheClientCodec()); + p.addLast(new BinaryMemcacheObjectAggregator(Integer.MAX_VALUE)); + p.addLast(new MemcachedClientHandler()); + } +} diff --git a/learn-netty4/src/main/java/com/flydean62/redis/RedisChannelInitializer.java b/learn-netty4/src/main/java/com/flydean62/redis/RedisChannelInitializer.java new file mode 100644 index 0000000..5c22676 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean62/redis/RedisChannelInitializer.java @@ -0,0 +1,36 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean62.redis; + +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.redis.RedisArrayAggregator; +import io.netty.handler.codec.redis.RedisBulkStringAggregator; +import io.netty.handler.codec.redis.RedisDecoder; +import io.netty.handler.codec.redis.RedisEncoder; + +class RedisChannelInitializer extends ChannelInitializer { + @Override + protected void initChannel(SocketChannel ch) { + ChannelPipeline p = ch.pipeline(); + p.addLast(new RedisDecoder()); + p.addLast(new RedisBulkStringAggregator()); + p.addLast(new RedisArrayAggregator()); + p.addLast(new RedisEncoder()); + p.addLast(new RedisClientHandler()); + } +} diff --git a/learn-netty4/src/main/java/com/flydean62/redis/RedisClient.java b/learn-netty4/src/main/java/com/flydean62/redis/RedisClient.java new file mode 100644 index 0000000..033eb21 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean62/redis/RedisClient.java @@ -0,0 +1,76 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package com.flydean62.redis; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.*; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioSocketChannel; +import lombok.extern.slf4j.Slf4j; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; + +@Slf4j +public class RedisClient { + + public static void main(String[] args) throws Exception { + String host = "127.0.0.1"; + int port = 6379; + startClient(host, port); + } + + private static void startClient(String host, int port) throws InterruptedException, IOException { + EventLoopGroup group = new NioEventLoopGroup(); + try { + Bootstrap b = new Bootstrap(); + b.group(group) + .channel(NioSocketChannel.class) + .handler(new RedisChannelInitializer()); + // 启动客户端 + Channel ch = b.connect(host, port).sync().channel(); + // 从stdin中读取命令 + System.out.println("输入redis命令(输入quit退出)"); + ChannelFuture lastWriteFuture = null; + BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); + while (true) { + final String input = in.readLine(); + final String line = input != null ? input.trim() : null; + if (line == null || "quit".equalsIgnoreCase(line)) { + ch.close().sync(); + break; + } else if (line.isEmpty()) { + continue; + } + // 将命令发送到服务器端 + lastWriteFuture = ch.writeAndFlush(line); + lastWriteFuture.addListener(future -> { + if (!future.isSuccess()) { + log.error("服务器端写入错误"); + } + }); + } + if (lastWriteFuture != null) { + lastWriteFuture.sync(); + } + } finally { + group.shutdownGracefully(); + } + } + +} diff --git a/learn-netty4/src/main/java/com/flydean62/redis/RedisClientHandler.java b/learn-netty4/src/main/java/com/flydean62/redis/RedisClientHandler.java new file mode 100644 index 0000000..fe181f6 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean62/redis/RedisClientHandler.java @@ -0,0 +1,58 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean62.redis; + +import io.netty.buffer.ByteBufUtil; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.redis.ArrayRedisMessage; +import io.netty.handler.codec.redis.FullBulkStringRedisMessage; +import io.netty.handler.codec.redis.RedisMessage; +import io.netty.util.ReferenceCountUtil; +import lombok.extern.slf4j.Slf4j; + +import java.util.ArrayList; +import java.util.List; + +@Slf4j +public class RedisClientHandler extends ChannelDuplexHandler { + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) { + String[] commands = ((String) msg).split("\\s+"); + List children = new ArrayList<>(commands.length); + for (String cmdString : commands) { + children.add(new FullBulkStringRedisMessage(ByteBufUtil.writeUtf8(ctx.alloc(), cmdString))); + } + RedisMessage request = new ArrayRedisMessage(children); + ctx.write(request, promise); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + RedisMessage redisMessage = (RedisMessage) msg; + log.info("msg is :{}", redisMessage); + ReferenceCountUtil.release(redisMessage); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + log.error("出现异常"); + ctx.close(); + } + +} diff --git a/learn-netty4/src/main/java/com/flydean64/mqttbroker/MqttCustBroker.java b/learn-netty4/src/main/java/com/flydean64/mqttbroker/MqttCustBroker.java new file mode 100644 index 0000000..fc69f0a --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean64/mqttbroker/MqttCustBroker.java @@ -0,0 +1,52 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean64.mqttbroker; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public final class MqttCustBroker { + + public static void main(String[] args) throws Exception { + startBroker(); + } + + private static void startBroker() throws InterruptedException { + EventLoopGroup bossGroup = new NioEventLoopGroup(1); + EventLoopGroup workerGroup = new NioEventLoopGroup(); + + try { + ServerBootstrap b = new ServerBootstrap(); + b.group(bossGroup, workerGroup); + b.option(ChannelOption.SO_BACKLOG, 1024); + b.channel(NioServerSocketChannel.class); + b.childHandler(new MqttCustBrokerInitializer()); + + ChannelFuture f = b.bind(1883).sync(); + f.channel().closeFuture().sync(); + } finally { + workerGroup.shutdownGracefully(); + bossGroup.shutdownGracefully(); + } + } + +} diff --git a/learn-netty4/src/main/java/com/flydean64/mqttbroker/MqttCustBrokerHandler.java b/learn-netty4/src/main/java/com/flydean64/mqttbroker/MqttCustBrokerHandler.java new file mode 100644 index 0000000..b4dba90 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean64/mqttbroker/MqttCustBrokerHandler.java @@ -0,0 +1,72 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean64.mqttbroker; + +import io.netty.channel.ChannelHandler.Sharable; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.codec.mqtt.*; +import io.netty.handler.timeout.IdleState; +import io.netty.handler.timeout.IdleStateEvent; +import io.netty.util.ReferenceCountUtil; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Sharable +public final class MqttCustBrokerHandler extends ChannelInboundHandlerAdapter { + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + MqttMessage mqttMessage = (MqttMessage) msg; + log.info("channel read:{}", mqttMessage); + + switch (mqttMessage.fixedHeader().messageType()) { + case CONNECT: + MqttFixedHeader connectFixedHeader = + new MqttFixedHeader(MqttMessageType.CONNACK, false, MqttQoS.AT_MOST_ONCE, false, 0); + MqttConnAckVariableHeader connectAckVariableHeader = + new MqttConnAckVariableHeader(MqttConnectReturnCode.CONNECTION_ACCEPTED, false); + MqttConnAckMessage connAck = new MqttConnAckMessage(connectFixedHeader, connectAckVariableHeader); + ctx.writeAndFlush(connAck); + break; + case PINGREQ: + MqttMessage pingResp = MqttMessage.PINGRESP; + ctx.writeAndFlush(pingResp); + break; + case DISCONNECT: + ctx.close(); + break; + default: + log.info("未知消息类型:{}", msg); + ReferenceCountUtil.release(msg); + ctx.close(); + } + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + log.info("idle state"); + if (evt instanceof IdleStateEvent && IdleState.READER_IDLE == ((IdleStateEvent) evt).state()) { + ctx.close(); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + log.error("消息异常", cause); + ctx.close(); + } +} diff --git a/learn-netty4/src/main/java/com/flydean64/mqttbroker/MqttCustBrokerInitializer.java b/learn-netty4/src/main/java/com/flydean64/mqttbroker/MqttCustBrokerInitializer.java new file mode 100644 index 0000000..e3a8d68 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean64/mqttbroker/MqttCustBrokerInitializer.java @@ -0,0 +1,34 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package com.flydean64.mqttbroker; + +import io.netty.channel.ChannelInitializer; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.mqtt.MqttDecoder; +import io.netty.handler.codec.mqtt.MqttEncoder; +import io.netty.handler.timeout.IdleStateHandler; + +import java.util.concurrent.TimeUnit; + +class MqttCustBrokerInitializer extends ChannelInitializer { + protected void initChannel(SocketChannel ch) throws Exception { + ch.pipeline().addLast(MqttEncoder.INSTANCE); + ch.pipeline().addLast(new MqttDecoder()); + ch.pipeline().addLast(new IdleStateHandler(60, 0, 0, TimeUnit.SECONDS)); + ch.pipeline().addLast(new MqttCustBrokerHandler()); + } +} diff --git a/learn-netty4/src/main/java/com/flydean65/mqttclient/MqttClient.java b/learn-netty4/src/main/java/com/flydean65/mqttclient/MqttClient.java new file mode 100644 index 0000000..974909e --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean65/mqttclient/MqttClient.java @@ -0,0 +1,50 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean65.mqttclient; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioSocketChannel; + +public final class MqttClient { + + public static void main(String[] args) throws Exception { + String brokerHost = "127.0.0.1"; + int hostPort = 1883; + String clientId = "clientId"; + String userName = "jack"; + String password = "ma"; + startClient(brokerHost, hostPort, clientId, userName, password); + } + + private static void startClient(String brokerHost, int hostPort, String clientId, String userName, String password) throws InterruptedException { + EventLoopGroup workerGroup = new NioEventLoopGroup(); + try { + Bootstrap b = new Bootstrap(); + b.group(workerGroup); + b.channel(NioSocketChannel.class); + b.handler(new MqttClientChannelInitializer(clientId, userName, password)); + + ChannelFuture f = b.connect(brokerHost, hostPort).sync(); + f.channel().closeFuture().sync(); + } finally { + workerGroup.shutdownGracefully(); + } + } + +} diff --git a/learn-netty4/src/main/java/com/flydean65/mqttclient/MqttClientChannelInitializer.java b/learn-netty4/src/main/java/com/flydean65/mqttclient/MqttClientChannelInitializer.java new file mode 100644 index 0000000..c50b6ed --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean65/mqttclient/MqttClientChannelInitializer.java @@ -0,0 +1,39 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean65.mqttclient; + +import io.netty.channel.ChannelInitializer; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.mqtt.MqttDecoder; +import io.netty.handler.codec.mqtt.MqttEncoder; +import io.netty.handler.timeout.IdleStateHandler; +import lombok.AllArgsConstructor; + +import java.util.concurrent.TimeUnit; + +@AllArgsConstructor +class MqttClientChannelInitializer extends ChannelInitializer { + private final String clientId; + private final String userName; + private final String password; + + protected void initChannel(SocketChannel ch) { + ch.pipeline().addLast( MqttEncoder.INSTANCE); + ch.pipeline().addLast(new MqttDecoder()); + ch.pipeline().addLast(new IdleStateHandler(0, 30, 0, TimeUnit.SECONDS)); + ch.pipeline().addLast(new MqttClientHandler(clientId, userName, password)); + } +} diff --git a/learn-netty4/src/main/java/com/flydean65/mqttclient/MqttClientHandler.java b/learn-netty4/src/main/java/com/flydean65/mqttclient/MqttClientHandler.java new file mode 100644 index 0000000..6471a4e --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean65/mqttclient/MqttClientHandler.java @@ -0,0 +1,81 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean65.mqttclient; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.codec.mqtt.*; +import io.netty.handler.timeout.IdleStateEvent; +import io.netty.util.ReferenceCountUtil; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class MqttClientHandler extends ChannelInboundHandlerAdapter { + + private final String clientId; + private final String userName; + private final byte[] password; + + public MqttClientHandler(String clientId, String userName, String password) { + this.clientId = clientId; + this.userName = userName; + this.password = password.getBytes(); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + log.info("channel read:{}", msg); + ReferenceCountUtil.release(msg); + } + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + + MqttFixedHeader connectFixedHeader = + new MqttFixedHeader(MqttMessageType.CONNECT, false, MqttQoS.AT_MOST_ONCE, false, 0); + MqttConnectVariableHeader connectVariableHeader = + new MqttConnectVariableHeader(MqttVersion.MQTT_3_1_1.protocolName(), MqttVersion.MQTT_3_1_1.protocolLevel(), true, true, false, + 0, false, false, 20, MqttProperties.NO_PROPERTIES); + MqttConnectPayload connectPayload = new MqttConnectPayload(clientId, + MqttProperties.NO_PROPERTIES, + null, + null, + userName, + password); + MqttConnectMessage connectMessage = + new MqttConnectMessage(connectFixedHeader, connectVariableHeader, connectPayload); + log.info("发送connect消息"); + ctx.writeAndFlush(connectMessage); + } + + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt instanceof IdleStateEvent) { + MqttMessage pingReqMessage = MqttMessage.PINGREQ; + log.info("发送pingReq消息"); + ctx.writeAndFlush(pingReqMessage); + } else { + super.userEventTriggered(ctx, evt); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + log.error("出现异常",cause); + ctx.close(); + } +} diff --git a/learn-netty4/src/main/java/com/flydean67/ocspclient/CustOcspClientHandler.java b/learn-netty4/src/main/java/com/flydean67/ocspclient/CustOcspClientHandler.java new file mode 100644 index 0000000..4856441 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean67/ocspclient/CustOcspClientHandler.java @@ -0,0 +1,70 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean67.ocspclient; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.ssl.ReferenceCountedOpenSslEngine; +import io.netty.handler.ssl.ocsp.OcspClientHandler; +import lombok.extern.slf4j.Slf4j; +import org.bouncycastle.asn1.ocsp.OCSPResponseStatus; +import org.bouncycastle.cert.ocsp.BasicOCSPResp; +import org.bouncycastle.cert.ocsp.CertificateStatus; +import org.bouncycastle.cert.ocsp.OCSPResp; +import org.bouncycastle.cert.ocsp.SingleResp; + +import javax.net.ssl.SSLSession; +import javax.security.cert.X509Certificate; +import java.math.BigInteger; + +@Slf4j +public class CustOcspClientHandler extends OcspClientHandler { + + CustOcspClientHandler(ReferenceCountedOpenSslEngine engine) { + super(engine); + } + + @Override + protected boolean verify(ChannelHandlerContext ctx, ReferenceCountedOpenSslEngine engine) throws Exception { + byte[] staple = engine.getOcspResponse(); + if (staple == null) { + throw new IllegalStateException("Server didn't provide an OCSP staple!"); + } + + OCSPResp response = new OCSPResp(staple); + if (response.getStatus() != OCSPResponseStatus.SUCCESSFUL) { + return false; + } + + SSLSession session = engine.getSession(); + X509Certificate[] chain = session.getPeerCertificateChain(); + BigInteger certSerial = chain[0].getSerialNumber(); + + BasicOCSPResp basicResponse = (BasicOCSPResp) response.getResponseObject(); + SingleResp first = basicResponse.getResponses()[0]; + + CertificateStatus status = first.getCertStatus(); + BigInteger ocspSerial = first.getCertID().getSerialNumber(); + String message = "OCSP status of " + ctx.channel().remoteAddress() + + "\n Status: " + (status == CertificateStatus.GOOD ? "Good" : status) + + "\n This Update: " + first.getThisUpdate() + + "\n Next Update: " + first.getNextUpdate() + + "\n Cert Serial: " + certSerial + + "\n OCSP Serial: " + ocspSerial; + log.info("verify message:{}",message); + + return status == CertificateStatus.GOOD && certSerial.equals(ocspSerial); + } +} diff --git a/learn-netty4/src/main/java/com/flydean67/ocspclient/OcspClient.java b/learn-netty4/src/main/java/com/flydean67/ocspclient/OcspClient.java new file mode 100644 index 0000000..cae8a7b --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean67/ocspclient/OcspClient.java @@ -0,0 +1,75 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean67.ocspclient; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.ssl.OpenSsl; +import io.netty.handler.ssl.ReferenceCountedOpenSslContext; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.SslProvider; +import lombok.extern.slf4j.Slf4j; + +import javax.net.ssl.SSLException; + +/** + * 一个HTTPS客户端,向支持OCSP stapling的服务器请求消息,并校验OCSP响应的正确性 + */ +@Slf4j +public class OcspClient { + + public static void main(String[] args) throws Exception { + String host = "www.microsoft.com"; + startClient(host); + } + + private static void startClient(String host) throws SSLException, InterruptedException { + if (!OpenSsl.isAvailable()) { + log.error("客户端必须支持SSL"); + } + if (!OpenSsl.isOcspSupported()) { + log.error("客户端必须支持ocsp"); + } + + ReferenceCountedOpenSslContext context + = (ReferenceCountedOpenSslContext) SslContextBuilder.forClient() + .sslProvider(SslProvider.OPENSSL) + .enableOcsp(true) + .build(); + + try { + EventLoopGroup group = new NioEventLoopGroup(); + try { + Bootstrap b = new Bootstrap() + .channel(NioSocketChannel.class) + .group(group) + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5 * 1000) + .handler(new OcspClientInitializer(context, host)); + + ChannelFuture f = b.connect(host, 443).sync(); + f.channel().closeFuture().sync(); + } finally { + group.shutdownGracefully(); + } + } finally { + context.release(); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean67/ocspclient/OcspClientHandlerAdapter.java b/learn-netty4/src/main/java/com/flydean67/ocspclient/OcspClientHandlerAdapter.java new file mode 100644 index 0000000..2450c97 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean67/ocspclient/OcspClientHandlerAdapter.java @@ -0,0 +1,66 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean67.ocspclient; + +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.codec.http.*; +import io.netty.util.ReferenceCountUtil; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +class OcspClientHandlerAdapter extends ChannelInboundHandlerAdapter { + + private final String host; + + OcspClientHandlerAdapter(String host) { + this.host = host; + } + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + FullHttpRequest request = new DefaultFullHttpRequest( + HttpVersion.HTTP_1_1, HttpMethod.GET, "/", Unpooled.EMPTY_BUFFER); + request.headers().set(HttpHeaderNames.HOST, host); + request.headers().set(HttpHeaderNames.USER_AGENT, "netty-ocspclient-example/1.0"); + + ctx.writeAndFlush(request).addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE); + + ctx.fireChannelActive(); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) { + ctx.fireChannelInactive(); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (msg instanceof FullHttpResponse) { + log.info("channelRead:{}", msg); + ReferenceCountUtil.release(msg); + return; + } + ctx.fireChannelRead(msg); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + ctx.fireExceptionCaught(cause); + } +} diff --git a/learn-netty4/src/main/java/com/flydean67/ocspclient/OcspClientInitializer.java b/learn-netty4/src/main/java/com/flydean67/ocspclient/OcspClientInitializer.java new file mode 100644 index 0000000..3477b82 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean67/ocspclient/OcspClientInitializer.java @@ -0,0 +1,59 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean67.ocspclient; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.handler.codec.http.HttpClientCodec; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.ssl.ReferenceCountedOpenSslContext; +import io.netty.handler.ssl.ReferenceCountedOpenSslEngine; +import io.netty.handler.ssl.SslHandler; +import lombok.AllArgsConstructor; + +@AllArgsConstructor +public class OcspClientInitializer extends ChannelInitializer { + + private ReferenceCountedOpenSslContext context; + private String host; + + @Override + protected void initChannel(Channel ch) throws Exception { + SslHandler sslHandler = context.newHandler(ch.alloc()); + ReferenceCountedOpenSslEngine engine + = (ReferenceCountedOpenSslEngine) sslHandler.engine(); + + ChannelPipeline pipeline = ch.pipeline(); + pipeline.addLast(sslHandler); + pipeline.addLast(new CustOcspClientHandler(engine)); + + pipeline.addLast(new HttpClientCodec()); + pipeline.addLast(new HttpObjectAggregator(1024 * 1024)); + pipeline.addLast(new OcspClientHandlerAdapter(host)); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) { + ctx.fireChannelInactive(); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + ctx.fireExceptionCaught(cause); + } +} diff --git a/learn-netty4/src/main/java/com/flydean68/ocspstapling/OcspRequestBuilder.java b/learn-netty4/src/main/java/com/flydean68/ocspstapling/OcspRequestBuilder.java new file mode 100644 index 0000000..18b0916 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean68/ocspstapling/OcspRequestBuilder.java @@ -0,0 +1,89 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean68.ocspstapling; + +import org.bouncycastle.asn1.DEROctetString; +import org.bouncycastle.asn1.ocsp.OCSPObjectIdentifiers; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.Extensions; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.ocsp.CertificateID; +import org.bouncycastle.cert.ocsp.OCSPException; +import org.bouncycastle.cert.ocsp.OCSPReq; +import org.bouncycastle.cert.ocsp.OCSPReqBuilder; +import org.bouncycastle.operator.DigestCalculator; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.SecureRandom; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; + +/** + * 使用BC的OCSPReqBuilder来构建Request + */ + +public class OcspRequestBuilder { + + private static final SecureRandom GENERATOR = new SecureRandom(); + + private final SecureRandom generator = GENERATOR; + + private final DigestCalculator calculator = new SHA1DigestCalculator(); + + private X509Certificate certificate; + + private X509Certificate issuer; + + + public OcspRequestBuilder certificate(X509Certificate certificate) { + this.certificate = certificate; + return this; + } + + public OcspRequestBuilder issuer(X509Certificate issuer) { + this.issuer = issuer; + return this; + } + + public OCSPReq build() throws OCSPException, IOException, CertificateEncodingException { + SecureRandom generator = checkNotNull(this.generator, "generator"); + DigestCalculator calculator = checkNotNull(this.calculator, "calculator"); + X509Certificate certificate = checkNotNull(this.certificate, "certificate"); + X509Certificate issuer = checkNotNull(this.issuer, "issuer"); + + BigInteger serial = certificate.getSerialNumber(); + + CertificateID certId = new CertificateID(calculator, + new X509CertificateHolder(issuer.getEncoded()), serial); + + OCSPReqBuilder builder = new OCSPReqBuilder(); + builder.addRequest(certId); + + byte[] nonce = new byte[8]; + generator.nextBytes(nonce); + + Extension[] extensions = new Extension[]{ + new Extension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce, false, + new DEROctetString(nonce))}; + + builder.setRequestExtensions(new Extensions(extensions)); + + return builder.build(); + } +} diff --git a/learn-netty4/src/main/java/com/flydean68/ocspstapling/OcspStaplingClient.java b/learn-netty4/src/main/java/com/flydean68/ocspstapling/OcspStaplingClient.java new file mode 100644 index 0000000..982b0a0 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean68/ocspstapling/OcspStaplingClient.java @@ -0,0 +1,121 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean68.ocspstapling; + +import io.netty.util.CharsetUtil; +import org.bouncycastle.asn1.ocsp.OCSPResponseStatus; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.ocsp.*; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.openssl.PEMParser; + +import java.io.*; +import java.math.BigInteger; +import java.net.URI; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * 需要证书和private key + */ +@SuppressWarnings("unused") +public class OcspStaplingClient { + public static void main(String[] args) throws Exception { + + // 加载CA证书链,证书链中只有两个证书,第二个是根证书,根证书中包含OCSP的信息 + X509Certificate[] keyCertChain = parseCertificates(OcspStaplingClient.class, "netty_io_chain.pem"); + + X509Certificate certificate = keyCertChain[0]; + X509Certificate issuer = keyCertChain[keyCertChain.length - 1]; + + //从根证书中取OCSP信息 + URI uri = OcspUtils.ocspUri(certificate); + System.out.println("OCSP Responder URI: " + uri); + + if (uri == null) { + throw new IllegalStateException("The CA/certificate doesn't have an OCSP responder"); + } + + // 创建OCSPReq + OCSPReq request = new OcspRequestBuilder() + .certificate(certificate) + .issuer(issuer) + .build(); + + // 向OCSP responder请求数据 + OCSPResp response = OcspUtils.request(uri, request, 5L, TimeUnit.SECONDS); + if (response.getStatus() != OCSPResponseStatus.SUCCESSFUL) { + throw new IllegalStateException("response-status=" + response.getStatus()); + } + + BasicOCSPResp basicResponse = (BasicOCSPResp) response.getResponseObject(); + SingleResp first = basicResponse.getResponses()[0]; + + CertificateStatus status = first.getCertStatus(); + System.out.println("Status: " + (status == CertificateStatus.GOOD ? "Good" : status)); + System.out.println("This Update: " + first.getThisUpdate()); + System.out.println("Next Update: " + first.getNextUpdate()); + + if (status != null) { + throw new IllegalStateException("certificate-status=" + status); + } + + BigInteger certSerial = certificate.getSerialNumber(); + BigInteger ocspSerial = first.getCertID().getSerialNumber(); + if (!certSerial.equals(ocspSerial)) { + throw new IllegalStateException("Bad Serials=" + certSerial + " vs. " + ocspSerial); + } + } + + private static X509Certificate[] parseCertificates(Class clazz, String name) throws Exception { + InputStream in = clazz.getResourceAsStream(name); + if (in == null) { + throw new FileNotFoundException("clazz=" + clazz + ", name=" + name); + } + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(in, CharsetUtil.US_ASCII))) { + return parseCertificates(reader); + } finally { + in.close(); + } + } + + private static X509Certificate[] parseCertificates(Reader reader) throws Exception { + + JcaX509CertificateConverter converter = new JcaX509CertificateConverter() + .setProvider(new BouncyCastleProvider()); + + List dst = new ArrayList<>(); + + try (PEMParser parser = new PEMParser(reader)) { + X509CertificateHolder holder; + + while ((holder = (X509CertificateHolder) parser.readObject()) != null) { + X509Certificate certificate = converter.getCertificate(holder); + if (certificate == null) { + continue; + } + + dst.add(certificate); + } + } + + return dst.toArray(new X509Certificate[0]); + } +} diff --git a/learn-netty4/src/main/java/com/flydean68/ocspstapling/OcspUtils.java b/learn-netty4/src/main/java/com/flydean68/ocspstapling/OcspUtils.java new file mode 100644 index 0000000..cfa05fb --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean68/ocspstapling/OcspUtils.java @@ -0,0 +1,156 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean68.ocspstapling; + +import io.netty.util.CharsetUtil; +import org.bouncycastle.asn1.*; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils; +import org.bouncycastle.cert.ocsp.OCSPReq; +import org.bouncycastle.cert.ocsp.OCSPResp; + +import javax.net.ssl.HttpsURLConnection; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URL; +import java.security.cert.X509Certificate; +import java.util.concurrent.TimeUnit; + +public final class OcspUtils { + + private static final ASN1ObjectIdentifier OCSP_RESPONDER_OID + = new ASN1ObjectIdentifier("1.3.6.1.5.5.7.48.1").intern(); + + private static final String OCSP_REQUEST_TYPE = "application/ocspclient-request"; + + private static final String OCSP_RESPONSE_TYPE = "application/ocspclient-response"; + + private OcspUtils() { + } + + /** + * Returns the OCSP responder {@link URI} or {@code null} if it doesn't have one. + */ + public static URI ocspUri(X509Certificate certificate) throws IOException { + byte[] value = certificate.getExtensionValue(Extension.authorityInfoAccess.getId()); + if (value == null) { + return null; + } + + ASN1Primitive authorityInfoAccess = JcaX509ExtensionUtils.parseExtensionValue(value); + if (!(authorityInfoAccess instanceof DLSequence)) { + return null; + } + + DLSequence aiaSequence = (DLSequence) authorityInfoAccess; + DERTaggedObject taggedObject = findObject(aiaSequence, OCSP_RESPONDER_OID, DERTaggedObject.class); + if (taggedObject == null) { + return null; + } + + if (taggedObject.getTagNo() != BERTags.OBJECT_IDENTIFIER) { + return null; + } + + byte[] encoded = taggedObject.getEncoded(); + int length = (int) encoded[1] & 0xFF; + String uri = new String(encoded, 2, length, CharsetUtil.UTF_8); + return URI.create(uri); + } + + private static T findObject(DLSequence sequence, ASN1ObjectIdentifier oid, Class type) { + for (ASN1Encodable element : sequence) { + if (!(element instanceof DLSequence)) { + continue; + } + DLSequence subSequence = (DLSequence) element; + if (subSequence.size() != 2) { + continue; + } + ASN1Encodable key = subSequence.getObjectAt(0); + ASN1Encodable value = subSequence.getObjectAt(1); + + if (key.equals(oid) && type.isInstance(value)) { + return type.cast(value); + } + } + + return null; + } + + public static OCSPResp request(URI uri, OCSPReq request, long timeout, TimeUnit unit) throws IOException { + byte[] encoded = request.getEncoded(); + + URL url = uri.toURL(); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + try { + connection.setConnectTimeout((int) unit.toMillis(timeout)); + connection.setReadTimeout((int) unit.toMillis(timeout)); + connection.setDoOutput(true); + connection.setDoInput(true); + connection.setRequestMethod("POST"); + connection.setRequestProperty("host", uri.getHost()); + connection.setRequestProperty("content-type", OCSP_REQUEST_TYPE); + connection.setRequestProperty("accept", OCSP_RESPONSE_TYPE); + connection.setRequestProperty("content-length", String.valueOf(encoded.length)); + + try (OutputStream out = connection.getOutputStream()) { + out.write(encoded); + out.flush(); + + try (InputStream in = connection.getInputStream()) { + int code = connection.getResponseCode(); + if (code != HttpsURLConnection.HTTP_OK) { + throw new IOException("Unexpected status-code=" + code); + } + + String contentType = connection.getContentType(); + if (!contentType.equalsIgnoreCase(OCSP_RESPONSE_TYPE)) { + throw new IOException("Unexpected content-type=" + contentType); + } + + int contentLength = connection.getContentLength(); + if (contentLength == -1) { + contentLength = Integer.MAX_VALUE; + } + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + byte[] buffer = new byte[8192]; + int length; + + while ((length = in.read(buffer)) != -1) { + baos.write(buffer, 0, length); + + if (baos.size() >= contentLength) { + break; + } + } + } finally { + baos.close(); + } + return new OCSPResp(baos.toByteArray()); + } + } + } finally { + connection.disconnect(); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean68/ocspstapling/SHA1DigestCalculator.java b/learn-netty4/src/main/java/com/flydean68/ocspstapling/SHA1DigestCalculator.java new file mode 100644 index 0000000..345a9f1 --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean68/ocspstapling/SHA1DigestCalculator.java @@ -0,0 +1,53 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean68.ocspstapling; + +import org.bouncycastle.asn1.oiw.OIWObjectIdentifiers; +import org.bouncycastle.asn1.x509.AlgorithmIdentifier; +import org.bouncycastle.crypto.digests.SHA1Digest; +import org.bouncycastle.operator.DigestCalculator; + +import java.io.ByteArrayOutputStream; +import java.io.OutputStream; + +/** + * 从BcX509ExtensionUtils中拷贝的SHA1DigestCalculator + */ +public class SHA1DigestCalculator implements DigestCalculator { + private ByteArrayOutputStream bOut; + + public SHA1DigestCalculator() { + this.bOut = new ByteArrayOutputStream(); + } + + public AlgorithmIdentifier getAlgorithmIdentifier() { + return new AlgorithmIdentifier(OIWObjectIdentifiers.idSHA1); + } + + public OutputStream getOutputStream() { + return this.bOut; + } + + public byte[] getDigest() { + byte[] var1 = this.bOut.toByteArray(); + this.bOut.reset(); + SHA1Digest var2 = new SHA1Digest(); + var2.update(var1, 0, var1.length); + byte[] var3 = new byte[var2.getDigestSize()]; + var2.doFinal(var3, 0); + return var3; + } +} diff --git a/learn-netty4/src/main/java/com/flydean69/stomp/StompClient.java b/learn-netty4/src/main/java/com/flydean69/stomp/StompClient.java new file mode 100644 index 0000000..4d2ce0b --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean69/stomp/StompClient.java @@ -0,0 +1,61 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean69.stomp; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.stomp.StompSubframeAggregator; +import io.netty.handler.codec.stomp.StompSubframeDecoder; +import io.netty.handler.codec.stomp.StompSubframeEncoder; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; + +public final class StompClient { + + public static void main(String[] args) throws Exception { + String host="127.0.0.1"; + int port = 61613; + String topic = "stomp.topic.example"; + startClient(host, port, topic); + } + + private static void startClient(String host, int port, String topic) throws InterruptedException { + EventLoopGroup group = new NioEventLoopGroup(); + try { + Bootstrap b = new Bootstrap(); + b.group(group).channel(NioSocketChannel.class); + b.handler(new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel ch) { + ChannelPipeline pipeline = ch.pipeline(); + pipeline.addLast("decoder", new StompSubframeDecoder()); + pipeline.addLast("encoder", new StompSubframeEncoder()); + pipeline.addLast("aggregator", new StompSubframeAggregator(100000)); + pipeline.addLast("handler", new StompClientHandler(topic,host)); + } + }); + + b.connect(host, port).sync().channel().closeFuture().sync(); + } finally { + group.shutdownGracefully(); + } + } +} diff --git a/learn-netty4/src/main/java/com/flydean69/stomp/StompClientHandler.java b/learn-netty4/src/main/java/com/flydean69/stomp/StompClientHandler.java new file mode 100644 index 0000000..31062ac --- /dev/null +++ b/learn-netty4/src/main/java/com/flydean69/stomp/StompClientHandler.java @@ -0,0 +1,87 @@ +/* + * Copyright 2022 learn-netty4 Project + * + * The learn-netty4 Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.flydean69.stomp; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.stomp.DefaultStompFrame; +import io.netty.handler.codec.stomp.StompCommand; +import io.netty.handler.codec.stomp.StompFrame; +import io.netty.handler.codec.stomp.StompHeaders; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class StompClientHandler extends SimpleChannelInboundHandler { + + private final String topic; + private final String host; + + public StompClientHandler(String topic,String host){ + this.topic=topic; + this.host=host; + } + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + StompFrame connFrame = new DefaultStompFrame(StompCommand.CONNECT); + connFrame.headers().set(StompHeaders.ACCEPT_VERSION, "1.2"); + connFrame.headers().set(StompHeaders.HOST, host); + ctx.writeAndFlush(connFrame); + } + + @Override + protected void channelRead0(ChannelHandlerContext ctx, StompFrame frame) throws Exception { + log.info("read frame:{}",frame); + switch (frame.command()) { + case CONNECTED: + StompFrame subscribeFrame = new DefaultStompFrame(StompCommand.SUBSCRIBE); + subscribeFrame.headers().set(StompHeaders.DESTINATION, topic); + subscribeFrame.headers().set(StompHeaders.RECEIPT, "10000"); + subscribeFrame.headers().set(StompHeaders.ID, "1"); + log.info("subscribeFrame:{}",subscribeFrame); + ctx.writeAndFlush(subscribeFrame); + break; + case RECEIPT: + String receiptHeader = frame.headers().getAsString(StompHeaders.RECEIPT_ID); + if (receiptHeader.equals("10000")) { + StompFrame msgFrame = new DefaultStompFrame(StompCommand.SEND); + msgFrame.headers().set(StompHeaders.DESTINATION, topic); + msgFrame.content().writeBytes("hello world".getBytes()); + log.info("msgFrame:{}",msgFrame); + ctx.writeAndFlush(msgFrame); + } else if (receiptHeader.equals("10001")) { + log.info("disconnected"); + ctx.close(); + } else { + throw new IllegalStateException("received: " + frame); + } + break; + case MESSAGE: + StompFrame disconnFrame = new DefaultStompFrame(StompCommand.DISCONNECT); + disconnFrame.headers().set(StompHeaders.RECEIPT, "10001"); + log.info("disconnFrame:{}",disconnFrame); + ctx.writeAndFlush(disconnFrame); + break; + default: + break; + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + ctx.close(); + } +} diff --git a/learn-netty4/src/main/resources/com/flydean34/http2images/image.html b/learn-netty4/src/main/resources/com/flydean34/http2images/image.html new file mode 100644 index 0000000..724d429 --- /dev/null +++ b/learn-netty4/src/main/resources/com/flydean34/http2images/image.html @@ -0,0 +1,13 @@ + + +Netty平铺图片测试 + + + +这里我们展示2*3的图片


+

+
+
+
+ + \ No newline at end of file diff --git a/learn-netty4/src/main/resources/com/flydean34/http2images/img0.jpg b/learn-netty4/src/main/resources/com/flydean34/http2images/img0.jpg new file mode 100644 index 0000000..1d5f324 Binary files /dev/null and b/learn-netty4/src/main/resources/com/flydean34/http2images/img0.jpg differ diff --git a/learn-netty4/src/main/resources/com/flydean34/http2images/img1.jpg b/learn-netty4/src/main/resources/com/flydean34/http2images/img1.jpg new file mode 100644 index 0000000..82aa89d Binary files /dev/null and b/learn-netty4/src/main/resources/com/flydean34/http2images/img1.jpg differ diff --git a/learn-netty4/src/main/resources/com/flydean34/http2images/img2.jpg b/learn-netty4/src/main/resources/com/flydean34/http2images/img2.jpg new file mode 100644 index 0000000..efca154 Binary files /dev/null and b/learn-netty4/src/main/resources/com/flydean34/http2images/img2.jpg differ diff --git a/learn-netty4/src/main/resources/com/flydean34/http2images/img3.jpg b/learn-netty4/src/main/resources/com/flydean34/http2images/img3.jpg new file mode 100644 index 0000000..cb181cb Binary files /dev/null and b/learn-netty4/src/main/resources/com/flydean34/http2images/img3.jpg differ diff --git a/learn-netty4/src/main/resources/com/flydean34/http2images/img4.jpg b/learn-netty4/src/main/resources/com/flydean34/http2images/img4.jpg new file mode 100644 index 0000000..4958a5e Binary files /dev/null and b/learn-netty4/src/main/resources/com/flydean34/http2images/img4.jpg differ diff --git a/learn-netty4/src/main/resources/com/flydean34/http2images/img5.jpg b/learn-netty4/src/main/resources/com/flydean34/http2images/img5.jpg new file mode 100644 index 0000000..3bb26c8 Binary files /dev/null and b/learn-netty4/src/main/resources/com/flydean34/http2images/img5.jpg differ diff --git a/learn-netty4/src/main/resources/logback.xml b/learn-netty4/src/main/resources/logback.xml new file mode 100644 index 0000000..7692e8f --- /dev/null +++ b/learn-netty4/src/main/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + diff --git a/learn-netty4/src/main/resources/socket.html b/learn-netty4/src/main/resources/socket.html new file mode 100644 index 0000000..6f55b61 --- /dev/null +++ b/learn-netty4/src/main/resources/socket.html @@ -0,0 +1,139 @@ +Web Socket测试页面 + +

Web Socket测试页面

+ +
+
+ 消息大小:
+ 消息数目:
+ 消息类型:文本二进制
+ 模式:
+ 一次发送一条消息
+ 将所有消息合并一起发送
+ 验证返回的响应
+ +

Output

+ +
+ +
+ + + diff --git a/learn-netty4/src/main/resources/socket2.html b/learn-netty4/src/main/resources/socket2.html new file mode 100644 index 0000000..b0b899d --- /dev/null +++ b/learn-netty4/src/main/resources/socket2.html @@ -0,0 +1,42 @@ +分离websocket处理器 + + +
+ +

输出:

+ +
+ + diff --git a/learn-netty文章/1.netty系列之netty初探.md b/learn-netty文章/1.netty系列之netty初探.md new file mode 100644 index 0000000..1aa131c --- /dev/null +++ b/learn-netty文章/1.netty系列之netty初探.md @@ -0,0 +1,208 @@ +# netty系列之:netty初探 + +# 简介 + +我们常用浏览器来访问web页面得到相关的信息,通常来说使用的都是HTTP或者HTTPS协议,这些协议的本质上都是IO,客户端的请求就是In,服务器的返回就是Out。但是在目前的协议框架中,并不能完全满足我们所有的需求。比如使用HTTP下载大文件,可能需要长连接等待等。 +我们也知道IO方式有多种多样的,包括同步IO,异步IO,阻塞IO和非阻塞IO等。不同的IO方式其性能也是不同的,而netty就是一个基于异步事件驱动的NIO框架。 + +本系列文章将会探讨netty的详细使用,通过原理+例子的具体结合,让大家了解和认识netty的魅力。 + +# netty介绍 + +netty是一个优秀的NIO框架,大家对IO的第一映像应该是比较复杂,尤其是跟各种HTTP、TCP、UDP协议打交道,使用起来非常复杂。但是netty提供了对这些协议的友好封装,通过netty可以快速而且简洁的进行IO编程。netty易于开发、性能优秀同时兼具稳定性和灵活性。如果你希望开发高性能的服务,那么使用netty总是没错的。 + +netty的最新版本是4.1.66.Final,事实上这个版本是官方推荐的最稳定的版本,netty还有5.x的版本,但是官方并不推荐。 + +如果要在项目中使用,则可以引入下面的代码: + +```xml + + io.netty + netty-all + 4.1.66.Final + +``` + +下面我们将会从一个最简单的例子,体验netty的魅力。 + +# netty的第一个服务器 + +什么叫做服务器?能够对外提供服务的程序就可以被称为是服务器。建立服务器是所有对外服务的第一步,怎么使用netty建立一个服务器呢?服务器主要负责处理各种服务端的请求,netty提供了一个ChannelInboundHandlerAdapter的类来处理这类请求,我们只需要继承这个类即可。 + +在NIO中每个channel都是客户端和服务器端沟通的通道。ChannelInboundHandlerAdapter定义了在这个channel上可能出现一些事件和情况,如下图所示: + +![img](8ed5c09fd5d4443487994e57ad5a8bb8.png) + +如上图所示,channel上可以出现很多事件,比如建立连接,关闭连接,读取数据,读取完成,注册,取消注册等。这些方法都是可以被重写的,我们只需要新建一个类,继承ChannelInboundHandlerAdapter即可。 + +这里我们新建一个FirstServerHandler类,并重写channelRead和exceptionCaught两个方法,第一个方法是从channel中读取消息,第二个方法是对异常进行处理。 + +```java +public class FirstServerHandler extends ChannelInboundHandlerAdapter { + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + // 对消息进行处理 + ByteBuf in = (ByteBuf) msg; + try { + log.info("收到消息:{}",in.toString(io.netty.util.CharsetUtil.US_ASCII)); + }finally { + ReferenceCountUtil.release(msg); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + // 异常处理 + log.error("出现异常",cause); + ctx.close(); + } +} +``` + +上面例子中,我们收到消息后调用release()方法将其释放,并不进行实际的处理。调用release方法是在消息使用完成之后常用的做法。上面代码将msg进行了ByteBuf的强制转换,如果并不想进行转换的话,可以直接这样使用: + +```java +try { + // 消息处理 +} finally { + ReferenceCountUtil.release(msg); +} +``` + +在异常处理方法中,我们打印出异常信息,并关闭异常的上下文。 + +有了Handler,我们需要新建一个Server类用来使用Handler创建channel和接收消息。接下来我们看一下netty的消息处理流程。 + +在netty中,对IO进行处理是使用多线程的event loop来实现的。netty中的EventLoopGroup就是这些event loop的抽象类。 + +我们来观察一下EventLoopGroup的类结构。 + +![img](fe7d545f9a3647fc8383e57c8e3391d8.png) + +可以看出EventLoopGroup继承自EventExecutorGroup,而EventExecutorGroup继承自JDK自带的ScheduledExecutorService。 + +所以EventLoopGroup本质是是一个线程池服务,之所以叫做Group,是因为它里面包含了很多个EventLoop,可以通过调用next方法对EventLoop进行遍历。 + +EventLoop是用来处理注册到该EventLoop的channel中的IO信息,一个EventLoop就是一个Executor,通过不断的提交任务进行执行。当然,一个EventLoop可以注册多个channel,不过一般情况下并不这样处理。 + +EventLoopGroup将多个EventLoop组成了一个Group,通过其中的next方法,可以对Group中的EventLoop进行遍历。另外EventLoopGroup提供了一些register方法,将Channel注册到当前的EventLoop中。 + +从上图可以看到,register的返回结果是一个ChannelFuture,Future大家都很清楚,可以用来获得异步任务的执行结果,同样的ChannelFuture也是一个异步的结果承载器,可以通过调用sync方法来阻塞Future直到获得执行结果。 + +可以看到,register方法还可以传入一个ChannelPromise对象,ChannelPromise它同时是ChannelFuture和Promise的子类,Promise又是Future的子类,它是一个特殊的可以控制Future状态的Future。 + +EventLoopGroup有很多子类的实现,这里我们使用NioEventLoopGroup,Nio使用Selector对channel进行选择。还有一个特性是NioEventLoopGroup可以添加子EventLoopGroup。 + +对于NIO服务器程序来说,我们需要两个Group,一个group叫做bossGroup,主要用来监控连接,一个group叫做worker group,用来处理被boss accept的连接,这些连接需要被注册到worker group中才能进行处理。 + +将这两个group传给ServerBootstrap,就可以从ServerBootstrap启动服务了,相应的代码如下: + +```java +//建立两个EventloopGroup用来处理连接和消息 +EventLoopGroup bossGroup = new NioEventLoopGroup(); +EventLoopGroup workerGroup = new NioEventLoopGroup(); +ServerBootstrap b = new ServerBootstrap(); +b.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .childHandler(new ChannelInitializer() { + @Override + public void initChannel(SocketChannel ch) throws Exception { + ch.pipeline().addLast(new FirstServerHandler()); + } + }) + .option(ChannelOption.SO_BACKLOG, 128) + .childOption(ChannelOption.SO_KEEPALIVE, true); + +// 绑定端口并开始接收连接 +ChannelFuture f = b.bind(port).sync(); +``` + +我们最开始创建的FirstServerHandler最作为childHandler的处理器在初始化Channel的时候就被添加进去了。 + +这样,当有新建立的channel时,FirstServerHandler就会被用来处理该channel的数据。 + +上例中,我们还指定了一些ChannelOption,用于对channel的一些属性进行设定。 + +最后,我们绑定了对应的端口,并启动服务器。 + +# netty的第一个客户端 + +上面我们已经写好了服务器,并将其启动,现在还需要一个客户端和其进行交互。 + +如果不想写代码的话,可以直接telnet localhost 8000和server端进行交互即可,但是这里我们希望使用netty的API来构建一个client和Server进行交互。 + +构建netty客户端的流程和构建netty server端的流程基本一致。首先也需要创建一个Handler用来处理具体的消息,同样,这里我们也继承ChannelInboundHandlerAdapter。 + +上一节讲到了ChannelInboundHandlerAdapter里面有很多方法,可以根据自己业务的需要进行重写,这里我们希望当Channel active的时候向server发送一个消息。那么就需要重写channelActive方法,同时也希望对异常进行一些处理,所以还需要重写exceptionCaught方法。如果你想在channel读取消息的时候进行处理,那么可以重写channelRead方法。 + +创建的FirstClientHandler代码如下: + +```java +@Slf4j +public class FirstClientHandler extends ChannelInboundHandlerAdapter { + + private ByteBuf content; + private ChannelHandlerContext ctx; + + @Override + public void channelActive(ChannelHandlerContext ctx) { + this.ctx = ctx; + content = ctx.alloc().directBuffer(256).writeBytes("Hello flydean.com".getBytes(StandardCharsets.UTF_8)); + // 发送消息 + sayHello(); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + // 异常处理 + log.error("出现异常",cause); + ctx.close(); + } + + private void sayHello() { + // 向服务器输出消息 + ctx.writeAndFlush(content.retain()); + } +} +``` + +上面的代码中,我们首先从ChannelHandlerContext申请了一个ByteBuff,然后调用它的writeBytes方法,写入要传输的数据。最后调用ctx的writeAndFlush方法,向服务器输出消息。 + +接下来就是启动客户端服务了,在服务端我们建了两个NioEventLoopGroup,是兼顾了channel的选择和channel中消息的读取两部分。对于客户端来说,并不存在这个问题,这里只需要一个NioEventLoopGroup即可。 + +服务器端使用ServerBootstrap来启动服务,客户端使用的是Bootstrap,其启动的业务逻辑基本和服务器启动一致: + +```java +EventLoopGroup group = new NioEventLoopGroup(); +Bootstrap b = new Bootstrap(); +b.group(group) + .channel(NioSocketChannel.class) + .handler(new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel ch) throws Exception { + ChannelPipeline p = ch.pipeline(); + p.addLast(new FirstClientHandler()); + } + }); + +// 连接服务器 +ChannelFuture f = b.connect(HOST, PORT).sync(); +``` + +# 运行服务器和客户端 + +有了上述的准备工作,我们就可以运行了。首先运行服务器,再运行客户端。 + +如果没有问题的话,应该会输出下面的内容: + +``` +[nioEventLoopGroup-3-1] INFO com.flydean01.FirstServerHandler - 收到消息:Hello flydean.com +``` + +# 总结 + +一个完整的服务器,客户端的例子就完成了。我们总结一下netty的工作流程,对于服务器端,首先建立handler用于对消息的实际处理,然后使用ServerBootstrap对EventLoop进行分组,并绑定端口启动。对于客户端来说,同样需要建立handler对消息进行处理,然后调用Bootstrap对EventLoop进行分组,并绑定端口启动。 + +有了上面的讨论就可以开发属于自己的NIO服务了。是不是很简单? 后续文章将会对netty的架构和背后的原理进行深入讨论,敬请期待。 + +本文的例子可以参考:[learn-netty4](https://github.com/ddean2009/learn-netty4) \ No newline at end of file diff --git a/learn-netty文章/10.netty系列之文本聊天室.md b/learn-netty文章/10.netty系列之文本聊天室.md new file mode 100644 index 0000000..a1131e8 --- /dev/null +++ b/learn-netty文章/10.netty系列之文本聊天室.md @@ -0,0 +1,153 @@ +# netty系列之:文本聊天室 + +# 简介 + +经过之前的系列文章,我们已经知道了netty的运行原理,还介绍了基本的netty服务搭建流程和消息处理器的写法。今天本文会给大家介绍一个更加复杂的例子,文本聊天室。 + +# 聊天室的工作流程 + +今天要介绍的是文本聊天室,对于文本聊天室来说,首先需要建立一个服务器,用于处理各个客户端的连接,对于客户端来说,需要建立和服务器的连接,然后向服务器输入聊天信息。服务器收到聊天信息之后,会对消息进行响应,并将消息返回至客户端,这样一个聊天室的流程就完成了。 + +# 文本处理器 + +之前的文章中,我们有提到过,netty的传输只支持ByteBuf类型,对于聊天室直接输入的字符串是不支持的,需要对字符串进行encode和decode转换。 + +之前我们介绍的encode和decode的类叫做ObjectDecoder和ObjectEncoder。今天我们再介绍两个专门处理字符串的StringDecoder和StringEncoder。 + +StringEncoder要比ObjectEncoder简单很多,因为对于对象来说,我们还需要在Byte数组的头部设置Byte数组的大小,从而保证对象所有数据读取正确。对于String来说,就比较简单了,只需要保证一次读入的数据都是字符串即可。 + +StringEncoder继承自MessageToMessageEncoder,其核心的encode代码如下: + +```java +protected void encode(ChannelHandlerContext ctx, CharSequence msg, List out) throws Exception { + if (msg.length() == 0) { + return; + } + + out.add(ByteBufUtil.encodeString(ctx.alloc(), CharBuffer.wrap(msg), charset)); +} +``` + +从上面的代码可以看出,核心实际上是调用了ByteBufUtil.encodeString方法,将String转换成了ByteBuf。 + +对于字符串编码来说,还需要界定一个编码的范围,比如我们需要知道需要一次编码多少字符串,一般来说我们通过回车符来界定一次字符串输入的结束。 + +netty也提供了这样的非常便利的类叫做DelimiterBasedFrameDecoder,通过传入不同的Delimiter,我们可以将输入拆分成不同的Frame,从而对一行字符串进行处理。 + +```java +new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter())) +``` + +我再看一下StringDecoder的核心代码,StringDecoder继承自MessageToMessageDecoder: + +```java +protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List out) throws Exception { + out.add(msg.toString(charset)); +} +``` + +通过调用ByteBuf的toString方法,将BuyteBuf转换成为字符串,并且输出到channel中。 + +# 初始化ChannelHandler + +在initChannel的时候,我们需要向ChannelPipeline中添加有效的Handler。对于本例来说,需要添加StringDecoder、StringEncoder、DelimiterBasedFrameDecoder和真正处理消息的自定义handler。 + +我们将初始化Pipeline的操作都放在一个新的ChatServerInitializer类中,这个类继承自ChannelInitializer,其核心的initChannel方法如下: + +```java +public void initChannel(SocketChannel ch) throws Exception { + ChannelPipeline pipeline = ch.pipeline(); + // 添加行分割器 + pipeline.addLast(new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter())); + // 添加String Decoder和String Encoder,用来进行字符串的转换 + pipeline.addLast(DECODER); + pipeline.addLast(ENCODER); + // 最后添加真正的处理器 + pipeline.addLast(SERVER_HANDLER); +} +``` + +ChatServerInitializer在Bootstrap中的childHandler中进行添加: + +``` +childHandler(new ChatServerInitializer()) +``` + +# 真正的消息处理逻辑 + +有了上面的逻辑之后,我们最后只需要专注于真正的消息处理逻辑即可。 + +这里我们的逻辑是当客户端输入“再见”的时候,就关闭channel,否则就将消息回写给客户端。 + +其核心逻辑如下: + +```java +public void channelRead0(ChannelHandlerContext ctx, String request) throws Exception { + // 如果读取到"再见"就关闭channel + String response; + // 判断是否关闭 + boolean close = false; + if (request.isEmpty()) { + response = "你说啥?\r\n"; + } else if ("再见".equalsIgnoreCase(request)) { + response = "再见,我的朋友!\r\n"; + close = true; + } else { + response = "你是不是说: '" + request + "'?\r\n"; + } + + // 写入消息 + ChannelFuture future = ctx.write(response); + // 添加CLOSE listener,用来关闭channel + if (close) { + future.addListener(ChannelFutureListener.CLOSE); + } +} +``` + +通过判断客户端的出入,来设置是否关闭按钮,这里的关闭channel是通过向ChannelFuture中添加ChannelFutureListener.CLOSE来实现的。 + +ChannelFutureListener.CLOSE是一个ChannelFutureListener,它会在channel执行完毕之后关闭channel,事实上这是一个非常优雅的关闭方式。 + +```java +ChannelFutureListener CLOSE = new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) { + future.channel().close(); + } +}; +``` + +对于客户端来说,其核心就是从命令行读取输入,这里使用InputStreamReader接收命令行输入,并使用BufferedReader对其缓存。 + +然后将命令行输入通过调用 ch.writeAndFlush写入到channel中,最后监听命令行输入,如果监听到“再见“,则等待server端关闭channel,其核心代码如下。 + +```java +// 从命令行输入 +ChannelFuture lastWriteFuture = null; +BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); +for (;;) { + String line = in.readLine(); + if (line == null) { + break; + } + // 将从命令行输入的一行字符写到channel中 + lastWriteFuture = ch.writeAndFlush(line + "\r\n"); + // 如果输入'再见',则等待server端关闭channel + if ("再见".equalsIgnoreCase(line)) { + ch.closeFuture().sync(); + break; + } +} + +// 等待所有的消息都写入channel中 +if (lastWriteFuture != null) { + lastWriteFuture.sync(); +} +``` + +# 总结 + +经过上面的介绍,一个简单的聊天室就建成了。后续我们会继续探索更加复杂的应用,希望大家能够喜欢。 + +本文的例子可以参考:[learn-netty4](https://github.com/ddean2009/learn-netty4) \ No newline at end of file diff --git a/learn-netty文章/11.netty系列之使用UDP协议.md b/learn-netty文章/11.netty系列之使用UDP协议.md new file mode 100644 index 0000000..499b15c --- /dev/null +++ b/learn-netty文章/11.netty系列之使用UDP协议.md @@ -0,0 +1,119 @@ +# netty系列之:使用UDP协议 + +# 简介 + +在之前的系列文章中,我们到了使用netty做聊天服务器,聊天服务器使用的SocketChannel,也就是说底层的协议使用的是Scoket。今天我们将会给大家介绍如何在netty中使用UDP协议。 + +# UDP协议 + +UDP( User Datagram Protocol ),也叫用户数据报协议。 + +UDP 的主要功能和亮点并不在于它引入了什么特性,而在于它忽略的那些特性:不保证消息交付,不保证交付顺序,不跟踪连接状态,不需要拥塞控制。 + +我们来看一下UDP的数据包: + +![img](20200617170251803.png) + +UDP是一种无连接的协议,发送者只管发送数据包即可,并不负责处理和保证数据是否成功发送,数据是否被处理完成等。它的唯一作用就是发送。 + +在JDK中表示UDP的有一个专门的类叫做:java.net.DatagramPacket,在NIO中还有一个java.nio.channels.DatagramChannel,专门负责处理UDP的channel。 + +这里我们要将的是netty,netty中对于UDP协议也有上面的两个类,名字虽然是一样的,但是对应的包不同。他们分别是: + +io.netty.channel.socket.DatagramPacket 和 io.netty.channel.socket.DatagramChannel,当然netty中的这两个类是对JDK自带类的增强。 + +先看一下netty中DatagramPacket的定义: + +```java +public class DatagramPacket + extends DefaultAddressedEnvelope implements ByteBufHolder +``` + +DatagramPacket类实现了ByteBufHolder接口,表示它里面存放的是ByteBuf。然后他又继承自DefaultAddressedEnvelope,这个类是对地址的封装,其中ByteBuf表示传递消息的类型,InetSocketAddress表示目标的地址,它是一个IP地址+端口号的封装。 + +从上面的UDP协议我们知道,UDP只需要知道目标地址和对应的消息即可,所以DatagramPacket中包含的数据已经够用了。 + +DatagramChannel是用来传递DatagramPacket的,因为DatagramChannel是一个接口,所以一般使用NioDatagramChannel作为真正使用的类。 + +# String和ByteBuf的转换 + +之前我们讲到过,netty中的channel只接受ByteBuf数据类型,如果直接写入String会报错,之前的系列文章中,我们讲过两种处理方法,第一种是使用ObjectEncoder和ObjectDecoder在写入ByteBuf之前,对对象进行序列化,这一种不仅适合String,也适合Object对象。 + +第二种是使用StringEncoder和StringDecoder专门处理String的encode和decode,这种处理只能处理String的转换,对Object无效。 + +如果你不想使用这些encoder和decoder还可以直接使用ByteBuf和String进行转换。 + +比如要将String写入ByteBuf可以调用Unpooled.copiedBuffer的命令如下: + +```java +Unpooled.copiedBuffer("开始广播", CharsetUtil.UTF_8) +``` + +将ByteBuf转换成为String则可以调用: + +```java +byteBuf.toString(CharsetUtil.UTF_8) +``` + +# 构建DatagramPacket + +DatagramPacket总共可以接受三个参数,分别是要发送的数据data,要接收数据包的地址和要发送数据包的地址。 + +这里我们并不关心发送数据包的地址,那么只需要两个参数即可,对于客户端来说,我们发送一个”开始广播“的消息给服务器端,告诉服务器端可以向客户发送回复消息了,如下所示: + +```java +new DatagramPacket( + Unpooled.copiedBuffer("开始广播", CharsetUtil.UTF_8), + SocketUtils.socketAddress("255.255.255.255", PORT)) +``` + +上我们使用SocketUtils.socketAddress创建了一个特殊的地址,255.255.255.255是一个特殊的广播地址,意味着所有的主机,因为我们的客户端并不知道服务器的地址,所以使用255.255.255.255来广播。 + +构建好的DatagramPacket,里面有一个sender()方法,可以用来获取客户端的地址,所以在服务器端可以这样构建要发送的packge: + +```java +new DatagramPacket( + Unpooled.copiedBuffer("广播: " + nextQuote(), CharsetUtil.UTF_8), packet.sender()) +``` + +# 启动客户端和服务器 + +UDP的客户端和服务器启动和socket稍微有所不同,如果是socket,那么使用的channel是NioSocketChannel,如果是UDP,则使用的是NioDatagramChannel。如下是服务器端启动的代码: + +```java +try { + EventLoopGroup group = new NioEventLoopGroup(); + Bootstrap b = new Bootstrap(); + b.group(group) + .channel(NioDatagramChannel.class) + .option(ChannelOption.SO_BROADCAST, true) + .handler(new UDPServerHandler()); + + b.bind(PORT).sync().channel().closeFuture().await(); +} finally { + group.shutdownGracefully(); +} +``` + +> 注意,这里我们需要设置ChannelOption.SO_BROADCAST为true,因为UDP是以广播的形式发送消息的。 + +客户端的实现和socket稍微有所不同,下面是客户端的启动实现: + +```java +EventLoopGroup group = new NioEventLoopGroup(); +Bootstrap b = new Bootstrap(); +b.group(group) + .channel(NioDatagramChannel.class) + .option(ChannelOption.SO_BROADCAST, true) + .handler(new UDPClientHandler()); + +Channel ch = b.bind(0).sync().channel(); +``` + +对于UDP来说,并不存在地址绑定一说,所以上Bootstrap调用bind(0)。 + +# 总结 + +本文讲解了netty中UDP协议的实现,UDP相较于Socket连接而言更加简单。 + +本文的例子可以参考:[learn-netty4](https://github.com/ddean2009/learn-netty4) \ No newline at end of file diff --git a/learn-netty文章/119283e9b8d04854940abc0fc159c604.png b/learn-netty文章/119283e9b8d04854940abc0fc159c604.png new file mode 100644 index 0000000..dd8fdb2 Binary files /dev/null and b/learn-netty文章/119283e9b8d04854940abc0fc159c604.png differ diff --git a/learn-netty文章/12.netty系列之对聊天进行加密.md b/learn-netty文章/12.netty系列之对聊天进行加密.md new file mode 100644 index 0000000..c8c6c48 --- /dev/null +++ b/learn-netty文章/12.netty系列之对聊天进行加密.md @@ -0,0 +1,140 @@ +# netty系列之:对聊天进行加密 + +# 简介 + +在之前的文章中,我们讲到了怎么使用netty建立聊天室,但是这样的简单的聊天室太容易被窃听了,如果想要在里面说点悄悄话是很不安全的,怎么办呢?学过密码学的朋友可能就想到了一个解决办法,聊天的时候对消息加密,处理的时候再对消息解密即可。 + +当然在netty中上述的工作都不需要我们手动来实现,netty已经提供了支持SSL的channel供我们选择,一起来看看吧。 + +# PKI标准 + +在讲netty的具体支持之前,我们需要先了解一下公钥和私钥的加密标准体系PKI。PKI的全称是Public Key Infrastructure,也就是公钥体系。用于规范公钥私募进行加密解密的规则,从而便于不同系统的对接。 + +事实上PKI标准已经有两代协议了。 + +第一代的PKI标准主要是由美国RSA公司的公钥加密标准PKCS,国际电信联盟的ITU-T X.509,IETF的X.509,WAP和WPKI等标准组成。但是因为第一代PKI标准是基于抽象语法符号ASN.1进行编码的,实现起来比较复杂和困难,所以产生了第二代PKI标准。 + +第二代PKI标准是由微软、VeriSign和webMethods三家公司在2001年发布的基于XML的密钥管理规范也叫做XKMS。 + +事实上现在CA中心使用的最普遍的规范还是X.509系列和PKCS系列。 + +X.509系列主要由X.209、X.500和X.509组成,其中X.509是由国际电信联盟(ITU-T)制定的数字证书标准。在X.500基础上进行了功能增强, +X.509是在1988年发布的。X.509证书由用户公共密钥和用户标识符组成。此外还包括版本号、证书序列号、CA标识符、签名算法标识、签发者名称、证书有效期等信息。 + +而PKCS是美国RSA公司的公钥加密标准,包括了证书申请、证书更新、证书作废表发布、扩展证书内容以及数字签名、数字信封的格式等方面的一系列相关协议。它定义了一系列从PKCS#1到PKCS#15的标准。 + +其中最常用的是PKCS#7、PKCS#12和PKCS#10。PKCS#7 是消息请求语法,常用于数字签名与加密,PKCS#12是个人消息交换与打包语法主要用来生成公钥和私钥。PKCS#10是证书请求语法。 + +# 各类证书的后缀和转换 + +操作过证书的朋友可能会对证书的后缀眼花缭乱,一般来说会有DER、CRT、CER、PEM这几种证书的后缀。 + +DER表示证书的内容是用二进制进行编码的。 + +PEM文件是一个文本文件,其内容是以“ – BEGIN -” 开头的,Base64编码的字符。 + +CRT和CER基本上是等价的,他们都是证书的扩展,也是文本文件,不同的是CRT通常用在liunx和unix系统中,而CER通常用在windows系统中。并且在windows系统中,CER文件会被MS cryptoAPI命令识别,可以直接显示导入和/或查看证书内容的对话框。 + +KEY文件,主要用来保存PKCS#8标准的公钥和私钥。 + +下面的命令可以用来查看文本证书内容: + +```java +openssl x509 -in cert.pem -text -noout +openssl x509 -in cert.cer -text -noout +openssl x509 -in cert.crt -text -noout +``` + +下面的命令可以用来查看二进制证书内容: + +```java +openssl x509 -in cert.der -inform der -text -noout +``` + +下面是常见的PEM和DER相互转换: + +```java +PEM到DER + +openssl x509 -in cert.crt -outform der-out cert.der + +DER到PEM + +openssl x509 -in cert.crt -inform der -outform pem -out cert.pem +``` + +# netty中启动SSL server + +事实上这个标题是不对的,netty中启动的server还是原来那个server,只是对发送的消息进行了加密解密处理。也就是说添加了一个专门进行SSL操作的Handler。 + +netty中代表ssl处理器的类叫做SslHandler,它是SslContext工程类的一个内部类,所以我们只需要创建好SslContext即可通过调用newHandler方法来返回SslHandler。 + +让服务器端支持SSL的代码: + +```java +ChannelPipeline p = channel.pipeline(); +SslContext sslCtx = SslContextBuilder.forServer(...).build(); +p.addLast("ssl", sslCtx.newHandler(channel.alloc())); +``` + +让客户端支持SSL的代码: + +```java +ChannelPipeline p = channel.pipeline(); +SslContext sslCtx = SslContextBuilder.forClient().build(); +p.addLast("ssl", sslCtx.newHandler(channel.alloc(), host, port)); +``` + +netty中SSL的实现有两种方式,默认情况下使用的是OpenSSL,如果OpenSSL不可以,那么将会使用JDK的实现。 + +要创建SslContext,可以调用SslContextBuilder.forServer或者SslContextBuilder.forClient方法。 + +这里以server为例,看下创建流程。SslContextBuilder有多种forServer的方法,这里取最简单的一个进行分析: + +```java +public static SslContextBuilder forServer(File keyCertChainFile, File keyFile) { + return new SslContextBuilder(true).keyManager(keyCertChainFile, keyFile); +} +``` + +该方法接收两个参数,keyCertChainFile是一个PEM格式的X.509证书文件,keyFile是一个PKCS#8的私钥文件。 + +熟悉OpenSSL的童鞋应该知道使用openssl命令可以生成私钥文件和对应的自签名证书文件。 + +具体openssl的操作可以查看我的其他文章,这里就不详细讲解了。 + +除了手动创建证书文件和私钥文件之外,如果是在开发环境中,大家可能希望有一个非常简单的方法来创建证书和私钥文件,netty为大家提供了SelfSignedCertificate类。 + +看这个类的名字就是知道它是一个自签名的证书类,并且会自动将证书文件和私钥文件生成在系统的temp文件夹中,所以这个类在生产环境中是不推荐使用的。默认情况下该类会使用OpenJDK’s X.509来生成证书的私钥,如果不可以,则使用 Bouncy Castle作为替代。 + +# netty中启动SSL client + +同样的在client中支持SSL也需要创建一个handler。客户端的SslContext创建代码如下: + +```java +// 配置 SSL. +final SslContext sslCtx = SslContextBuilder.forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE).build(); +``` + +上面的代码我们使用了一个InsecureTrustManagerFactory.INSTANCE作为trustManager。什么是trustManager呢? + +当客户端和服务器端进行SSL连接的时候,客户端需要验证服务器端发过来证书的正确性,通常情况下,这个验证是到CA服务器中进行验证的,不过这样需要一个真实的CA证书环境,所以在测试中,我们使用InsecureTrustManagerFactory,这个类会默认接受所有的证书,忽略所有的证书异常。 + +当然,CA服务器也不是必须的,客户端校验的目的是查看证书中的公钥和发送方的公钥是不是一致的,那么对于不能联网的环境,或者自签名的环境中,我们只需要在客户端校验证书中的指纹是否一致即可。 + +netty中提供了一个FingerprintTrustManagerFactory类,可以对证书中的指纹进行校验。 + +该类中有个fingerprints数组,用来存储安全的授权过的指纹信息。通过对比传入的证书和指纹,如果一致则校验通过。 + +使用openssl从证书中提取指纹的步骤如下: + +```java +openssl x509 -fingerprint -sha256 -in my_certificate.crt +``` + +# 总结 + +通过设置client和server端的SSL handler,就可以实现客户端和服务器端的加密消息传输。 + +本文的例子可以参考:[learn-netty4](https://github.com/ddean2009/learn-netty4) \ No newline at end of file diff --git a/learn-netty文章/13.netty系列之自定义编码解码器.md b/learn-netty文章/13.netty系列之自定义编码解码器.md new file mode 100644 index 0000000..db359e6 --- /dev/null +++ b/learn-netty文章/13.netty系列之自定义编码解码器.md @@ -0,0 +1,152 @@ +# netty系列之:自定义编码解码器 + +# 简介 + +在之前的netty系列文章中,我们讲到了如何将对象或者String转换成为ByteBuf,通过使用netty自带的encoder和decoder可以实现非常方便的对象和ByteBuf之间的转换,然后就可以向channel中随意写入对象和字符串了。 + +使用netty自带的编码器当然很好,但是如果你有些特殊的需求,比如希望在编码的过程中对数据进行变换,或者对对象的字段进行选择,那么可能就需要自定义编码解码器了。 + +# 自定义编码器 + +自定义编码器需要继承MessageToByteEncoder *类,并实现encode方法,在该方法中写入具体的编码逻辑。* + +本例我们希望计算2的N次方,据说将一张纸折叠100次可以达到地球到月亮的高度,这么大的数据普通的number肯定是装不下的,我们将会使用BigInteger来对这个巨大的数字进行保存。 + +那么对于被编码器来说,则需要将这个BigInteger转换成为byte数组。同时在byte数组读取的过程中,我们需要界定到底哪些byte数据是属于同一个BigInteger的,这就需要对写入的数据格式做一个约定。 + +这里我们使用三部分的数据结构来表示一个BigInteger。第一部分是一个magic word也就是魔法词,这里我们使用魔法词“N”,当读取到这个魔法词就表示接下来的数字是BigInteger。第二部分是表示bigInteger数字的byte数组的长度,获取到这个长度值,就可以读取到所有的byte数组值,最后将其转换成为BigInteger。 + +因为BigInteger是Number的子类,为了更加泛化编码器,我们使用Number作为MessageToByteEncoder的泛型,核心编码代码如下: + +```java +protected void encode(ChannelHandlerContext ctx, Number msg, ByteBuf out) { + // 将number编码成为ByteBuf + BigInteger v; + if (msg instanceof BigInteger) { + v = (BigInteger) msg; + } else { + v = new BigInteger(String.valueOf(msg)); + } + + // 将BigInteger转换成为byte[]数组 + byte[] data = v.toByteArray(); + int dataLength = data.length; + + // 将Number进行编码 + out.writeByte((byte) 'N'); // 魔法词 + out.writeInt(dataLength); // 数组长度 + out.writeBytes(data); // 最终的数据 +} +``` + +# 自定义解码器 + +有了编码之后的byte数组,就可以在解码器中对其解码了。 + +上一节介绍了,编码过后的数据格式是魔法词N+数组长度+真正的数据。 + +其中魔法词长度是一个字节,数组长度是四个字节,前面部分总共是5个字节。所以在解码的时候,首先判断ByteBuf中可读字节的长度是否小于5,如果小于5说明数据是无效的,可以直接return。 + +如果可读字节的长度大于5,则表示数据是有效的,可以进行数据的解码了。 + +解码过程中需要注意的是,并不是所有的数据都是我们所希望的格式,如果在读取的过程中读到了我们不认识的格式,那么说明这个数据并不是我们想要的,则可以交由其他的handler进行处理。 + +但是对于ByteBuf来说,一旦调用read方法,就会导致reader index移动位置,所以在真正的读取数据之前需要调用ByteBuf的markReaderIndex方法,对readerIndex进行记录。然后分别读取魔法词、数组长度和剩余的数据,最后将数据转换成为BigInteger,如下所示: + +```java +protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) { + // 保证魔法词和数组长度有效 + if (in.readableBytes() < 5) { + return; + } + in.markReaderIndex(); + // 检查魔法词 + int magicNumber = in.readUnsignedByte(); + if (magicNumber != 'N') { + in.resetReaderIndex(); + throw new CorruptedFrameException("无效的魔法词: " + magicNumber); + } + // 读取所有的数据 + int dataLength = in.readInt(); + if (in.readableBytes() < dataLength) { + in.resetReaderIndex(); + return; + } + // 将剩下的数据转换成为BigInteger + byte[] decoded = new byte[dataLength]; + in.readBytes(decoded); + out.add(new BigInteger(decoded)); +} +``` + +# 添加编码解码器到pipeline + +有了两个编码解码器,还需要将其添加到pipeline中进行调用。 + +在实现ChannelInitializer中的initChannel中,可以对ChannelPipeline进行初始化,本例中的初始化代码如下: + +```java +// 对流进行压缩 +pipeline.addLast(ZlibCodecFactory.newZlibEncoder(ZlibWrapper.GZIP)); +pipeline.addLast(ZlibCodecFactory.newZlibDecoder(ZlibWrapper.GZIP)); + +// 添加number编码解码器 +pipeline.addLast(new NumberDecoder()); +pipeline.addLast(new NumberEncoder()); + +// 添加业务处理逻辑 +pipeline.addLast(new CustomProtocolServerHandler()); +``` + +其中最后一行是真正的业务处理逻辑,NumberDecoder和NumberEncoder是编码和解码器。这里我们还使用了一个ZlibEncoder用于对流数据进行压缩,这里使用的压缩方式是GZIP。 + +压缩的好处就是可以减少数据传输的数量,提升传输效率。其本质也是一个编码解码器。 + +# 计算2的N次方 + +计算2的N次方的逻辑是这样的,首先客户端发送2给服务器端,服务器端接收到该消息和结果1相乘,并将结果写回给客户端,客户端收到消息之后再发送2给服务器端,服务器端将上次的计算结果乘以2,再发送给客户端,以此类推直到执行N次。 + +首先看下客户端的发送逻辑: + +```java +// 最大计算2的1000次方 +ChannelFuture future = null; +for (int i = 0; i < 1000 && next <= CustomProtocolClient.COUNT; i++) { + future = ctx.write(2); + next++; +} +``` + +当next小于等于要计算的COUNT时,就将2写入到channel中。 + +对于服务器来说,在channelRead0方法中,读取消息,并将其和结果相乘,再把结果写回给客户端。 + +```java +public void channelRead0(ChannelHandlerContext ctx, BigInteger msg) throws Exception { + // 将接收到的msg乘以2,然后返回给客户端 + count++; + result = result.multiply(msg); + ctx.writeAndFlush(result); +} +``` + +客户端统计读取到的消息个数,如果消息个数=COUNT,说明计算完毕,就可以将结果保存起来供后续使用,其核心代码如下: + +```java +public void channelRead0(ChannelHandlerContext ctx, final BigInteger msg) { + receivedMessages ++; + if (receivedMessages == CustomProtocolClient.COUNT) { + // 计算完毕,将结果放入answer中 + ctx.channel().close().addListener(future -> { + boolean offered = answer.offer(msg); + assert offered; + }); + } +} +``` + +# 总结 + +本文实现了一个Number的编码解码器,事实上你可以自定义实现任何对象的编码解码器。 + +本文的例子可以参考:[learn-netty4](https://github.com/ddean2009/learn-netty4) \ No newline at end of file diff --git a/learn-netty文章/14.netty系列之自定义编码和解码器要注意的问题.md b/learn-netty文章/14.netty系列之自定义编码和解码器要注意的问题.md new file mode 100644 index 0000000..fdb600d --- /dev/null +++ b/learn-netty文章/14.netty系列之自定义编码和解码器要注意的问题.md @@ -0,0 +1,170 @@ +# netty系列之:自定义编码和解码器要注意的问题 + +# 简介 + +在之前的系列文章中,我们提到了netty中的channel只接受ByteBuf类型的对象,如果不是ByteBuf对象的话,需要用编码和解码器对其进行转换,今天来聊一下netty自定义的编码和解码器实现中需要注意的问题。 + +# 自定义编码器和解码器的实现 + +在介绍netty自带的编码器和解码器之前,告诉大家怎么实现自定义的编码器和解码器。 + +netty中所有的编码器和解码器都是从ChannelInboundHandlerAdapter和ChannelOutboundHandlerAdapter衍生而来的。 + +对于ChannelOutboundHandlerAdapter来说,最重要的两个类是MessageToByteEncoder 和 MessageToMessageEncoder 。 + +MessageToByteEncoder是将消息编码成为ByteBuf,这个类也是我们自定义编码最常用的类,直接继承这个类并实现encode方法即可。注意到这个类有一个泛型,这个泛型指定的就是消息的对象类型。 + +例如我们想将Integer转换成为ByteBuf,可以这样写: + +```java +public class IntegerEncoder extends MessageToByteEncoder { + @Override + public void encode(ChannelHandlerContext ctx, Integer msg, ByteBuf out) + throws Exception { + out.writeInt(msg); + } +} +``` + +MessageToMessageEncoder是在消息和消息之间进行转换,因为消息并不能直接写入到channel中,所以需要和MessageToByteEncoder配合使用。 + +下面是一个Integer到String的例子: + +```java +public class IntegerToStringEncoder extends + MessageToMessageEncoder { + + @Override + public void encode(ChannelHandlerContext ctx, Integer message, List out) + throws Exception { + out.add(message.toString()); + } +} +``` + +对于ChannelInboundHandlerAdapter来说,最重要的两个类是ByteToMessageDecoder和MessageToMessageDecoder *。* + +ByteToMessageDecoder是将ByteBuf转换成对应的消息类型,我们需要继承这个类,并实现decode方法,下面是一个从ByteBuf中读取所有可读的字节,并将结果放到一个新的ByteBuf中, + +```java +public class SquareDecoder extends ByteToMessageDecoder { + @Override + public void decode(ChannelHandlerContext ctx, ByteBuf in, List out) + throws Exception { + out.add(in.readBytes(in.readableBytes())); + } +} +``` + +MessageToMessageDecoder是消息和消息之间的转换,同样的只需要实现decode方法即可,如下从String转换到Integer: + +```java +public class StringToIntegerDecoder extends + MessageToMessageDecoder { + + @Override + public void decode(ChannelHandlerContext ctx, String message, + List out) throws Exception { + out.add(message.length()); + } +} +``` + +# ReplayingDecoder + +上面的代码看起来很简单,但是在实现的过程中还有一些问题要注意。 + +对于Decoder来说,我们从ByteBuf中读取数据,然后进行转换。但是在读取的过程中,并不知道ByteBuf中数据的变动情况,有可能在读取的过程中ByteBuf还没有准备好,那么就需要在读取的时候对ByteBuf中可读字节的大小进行判断。 + +比如我们需要解析一个数据结构,这个数据结构的前4个字节是一个int,表示后面byte数组的长度,我们需要先判断ByteBuf中是否有4个字节,然后读取这4个字节作为Byte数组的长度,然后再读取这个长度的Byte数组,最终得到要读取的结果,如果其中的某一步出现问题,或者说可读的字节长度不够,那么就需要直接返回,等待下一次的读取。如下所示: + +```java +public class IntegerHeaderFrameDecoder extends ByteToMessageDecoder { + + @Override + protected void decode(ChannelHandlerContext ctx, + ByteBuf buf, List out) throws Exception { + + if (buf.readableBytes() < 4) { + return; + } + + buf.markReaderIndex(); + int length = buf.readInt(); + + if (buf.readableBytes() < length) { + buf.resetReaderIndex(); + return; + } + + out.add(buf.readBytes(length)); + } +} +``` + +这种判断是比较复杂同时也是可能出错的,为了解决这个问题,netty提供了 ReplayingDecoder用来简化上面的操作,在ReplayingDecoder中,假设所有的ByteBuf已经处于准备好的状态,直接从中间读取即可。 + +上面的例子用ReplayingDecoder重写如下: + +```java +public class IntegerHeaderFrameDecoder + extends ReplayingDecoder { + + protected void decode(ChannelHandlerContext ctx, + ByteBuf buf, List out) throws Exception { + + out.add(buf.readBytes(buf.readInt())); + } +} +``` + +它的实现原理是去尝试读取对应的字节信息,如果没有读到,则抛出异常,ReplayingDecoder接收到异常之后,会重新调用decode方法。 + +虽然ReplayingDecoder使用起来非常简单,但是它有两个问题。 + +第一个问题是性能问题,因为会去重复调用decode方法,如果ByteBuf本身并没有变化,就会导致重复decode同一个ByteBuf,照成性能的浪费。解决这个问题就是在在decode的过程中分阶段进行,比如上面的例子中,我们需要先读取Byte数组的长度,然后再读取真正的byte数组。所以在读完byte数组长度之和,可以调用checkpoint()方法做一个保存点,下次再执行decode方法的时候就可以跳过这个保存点,继续后续的执行过程,如下所示: + +```java +public enum MyDecoderState { + READ_LENGTH, + READ_CONTENT; +} + +public class IntegerHeaderFrameDecoder + extends ReplayingDecoder { + + private int length; + + public IntegerHeaderFrameDecoder() { + // Set the initial state. + super(MyDecoderState.READ_LENGTH); + } + + @Override + protected void decode(ChannelHandlerContext ctx, + ByteBuf buf, List out) throws Exception { + switch (state()) { + case READ_LENGTH: + length = buf.readInt(); + checkpoint(MyDecoderState.READ_CONTENT); + case READ_CONTENT: + ByteBuf frame = buf.readBytes(length); + checkpoint(MyDecoderState.READ_LENGTH); + out.add(frame); + break; + default: + throw new Error("Shouldn't reach here."); + } + } +} +``` + +第二个问题是同一个实例的decode方法可能会被调用多次,如果我们在ReplayingDecoder中有私有变量的话,则需要考虑对这个私有变量的清洗工作,避免多次调用造成的数据污染。 + +# 总结 + +通过继承上面的几个类,我们就可以自己实现编码和解码的逻辑了。但是好像还有点问题,自定义编码和解码器是不是太复杂了?还需要判断要读取的byte数组的大小。有没有更加简单的方法呢? + +有的,敬请期待netty系列的下一篇文章:netty自带的编码器和解码器. + +本文的例子可以参考:[learn-netty4](https://github.com/ddean2009/learn-netty4) \ No newline at end of file diff --git a/learn-netty文章/15.netty系列之内置的Frame detection.md b/learn-netty文章/15.netty系列之内置的Frame detection.md new file mode 100644 index 0000000..cb2b754 --- /dev/null +++ b/learn-netty文章/15.netty系列之内置的Frame detection.md @@ -0,0 +1,150 @@ +# netty系列之:内置的Frame detection + +# 简介 + +上篇文章我们讲到了netty中怎么自定义编码和解码器,但是自定义实现起来还是挺复杂的,一般没有特殊必要的情况下,大家都希望越简单越好,其难点就是找到ByteBuf中的分割点,将ByteBuf分割成为一个个的可以处理的单元。今天本文讲讲netty中自带的分割处理机制。 + +# Frame detection + +在上一章,我们提到了需要有一种手段来区分ByteBuf中不同的数据,也就是说找到ByteBuf中不同数据的分割点。如果首先将ByteBuf分割成一个个的独立的ByteBuf,再对独立的ByteBuf进行处理就会简单很多。 + +netty中提供了4个分割点的编码器,我们可以称之为Frame detection,他们分别是DelimiterBasedFrameDecoder, FixedLengthFrameDecoder, LengthFieldBasedFrameDecoder, 和 LineBasedFrameDecoder。 + +这几个类都是ByteToMessageDecoder的子类,接下来我们一一进行介绍。 + +## DelimiterBasedFrameDecoder + +首先是DelimiterBasedFrameDecoder,看名字就知道这个是根据delimiter对bytebuf进行分割的解码器。什么是delimiter呢? + +netty中有一个Delimiters类,专门定义分割的字符,主要有两个delimiter,分别是nulDelimiter和lineDelimiter: + +```java +public static ByteBuf[] nulDelimiter() { + return new ByteBuf[] { + Unpooled.wrappedBuffer(new byte[] { 0 }) }; +} + +public static ByteBuf[] lineDelimiter() { + return new ByteBuf[] { + Unpooled.wrappedBuffer(new byte[] { '\r', '\n' }), + Unpooled.wrappedBuffer(new byte[] { '\n' }), + }; +} +``` + +nullDelimiter用来处理0x00,主要用来处理Flash XML socket或者其他的类似的协议。 + +lineDelimiter用来处理回车和换行符,主要用来文本文件的处理中。 + +对于DelimiterBasedFrameDecoder来说,如果有多个delimiter的话,会选择将ByteBuf分割最短的那个,举个例子,如果我们使用DelimiterBasedFrameDecoder(Delimiters.lineDelimiter()) ,因为lineDelimiter中实际上有两个分割方式,回车+换行或者换行,如果遇到下面的情况: + +```java + +--------------+ + | ABC\nDEF\r\n | + +--------------+ +``` + +DelimiterBasedFrameDecoder会选择最短的分割结果,也就说将上面的内容分割成为: + +```java + +-----+-----+ + | ABC | DEF | + +-----+-----+ +``` + +而不是 + +```java + +----------+ + | ABC\nDEF | + +----------+ +``` + +## FixedLengthFrameDecoder + +这个类会将ByteBuf分成固定的长度,比如收到了下面的4块byte信息: + +```java + +---+----+------+----+ + | A | BC | DEFG | HI | + +---+----+------+----+ +``` + +如果使用一个FixedLengthFrameDecoder(3) ,则会将上面的ByteBuf分成下面的几个部分: + +```java + +-----+-----+-----+ + | ABC | DEF | GHI | + +-----+-----+-----+ +``` + +## LengthFieldBasedFrameDecoder + +这个类就更加灵活一点,可以根据数据中的length字段取出后续的byte数组。LengthFieldBasedFrameDecoder非常灵活,它有4个属性来控制他们分别是lengthFieldOffset、lengthFieldLength、lengthAdjustment和initialBytesToStrip。 + +lengthFieldOffset是长度字段的起始位置,lengthFieldLength是长度字段本身的长度,lengthAdjustment是对目标数据长度进行调整,initialBytesToStrip是解密过程中需要删除的byte数目。理解不了?没关系,我们来举几个例子。 + +首先看一个最简单的: + +```java + lengthFieldOffset = 0 + lengthFieldLength = 2 + lengthAdjustment = 0 + initialBytesToStrip = 0 + + BEFORE DECODE (14 bytes) AFTER DECODE (14 bytes) + +--------+----------------+ +--------+----------------+ + | Length | Actual Content |----->| Length | Actual Content | + | 0x000C | "HELLO, WORLD" | | 0x000C | "HELLO, WORLD" | + +--------+----------------+ +--------+----------------+ +``` + +上面的设置表示,length是从第0位开始的,长度是2个字节。其中Ox00C=12, 这也是“HELLO, WORLD” 的长度。 + +如果不想要Length字段,可以通过设置initialBytesToStrip把length删除: + +```java + lengthFieldOffset = 0 + lengthFieldLength = 2 + lengthAdjustment = 0 + initialBytesToStrip = 2 (= length 字段的长度) + + BEFORE DECODE (14 bytes) AFTER DECODE (12 bytes) + +--------+----------------+ +----------------+ + | Length | Actual Content |----->| Actual Content | + | 0x000C | "HELLO, WORLD" | | "HELLO, WORLD" | + +--------+----------------+ +----------------+ +``` + +lengthAdjustment是对Length字段的值进行调整,因为在有些情况下Length字段可能包含了整条数据的长度,也就是Length+内容,所以需要在解析的时候进行调整,比如下面的例子,真实长度其实是0x0C,但是传入的却是0x0E,所以需要减去Length字段的长度2,也就是将lengthAdjustment设置为-2。 + +```java + lengthFieldOffset = 0 + lengthFieldLength = 2 + lengthAdjustment = -2 (= Length字段的长度) + initialBytesToStrip = 0 + + BEFORE DECODE (14 bytes) AFTER DECODE (14 bytes) + +--------+----------------+ +--------+----------------+ + | Length | Actual Content |----->| Length | Actual Content | + | 0x000E | "HELLO, WORLD" | | 0x000E | "HELLO, WORLD" | + +--------+----------------+ +--------+----------------+ +``` + +## LineBasedFrameDecoder + +LineBasedFrameDecoder专门处理文本文件中的一行结束。也就是 “\n” 和 “\r\n”,他和DelimiterBasedFrameDecoder很类似,但是DelimiterBasedFrameDecoder更加通用。 + +# 总结 + +有了上面4个Frame detection装置之后,就可以在pipline中首先添加这些Frame detection,然后再添加自定义的handler,这样在自定义的handler中就不用考虑读取ByteBuf的长度问题了。 + +比如在StringDecoder中,如果已经使用了 LineBasedFrameDecoder , 那么在decode方法中可以假设传入的ByteBuf就是一行字符串,那么可以直接这样使用: + +```java +protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List out) throws Exception { + out.add(msg.toString(charset)); +} +``` + +是不是很简单? \ No newline at end of file diff --git a/learn-netty文章/16.netty系列之最基本的内置编码解码器.md b/learn-netty文章/16.netty系列之最基本的内置编码解码器.md new file mode 100644 index 0000000..e39bc35 --- /dev/null +++ b/learn-netty文章/16.netty系列之最基本的内置编码解码器.md @@ -0,0 +1,253 @@ +# netty系列之:最基本的内置编码解码器 + +# 简介 + +netty之所以强大,是因为它内置了很多非常有用的编码解码器,通过使用这些编码解码器可以很方便的搭建出非常强大的应用程序,今天给大家讲讲netty中最基本的内置编码解码器。 + +# netty中的内置编码器 + +在对netty的包进行引入的时候,我们可以看到netty有很多以netty-codec开头的artifactId,统计一下,有这么多个: + +``` +netty-codec +netty-codec-http +netty-codec-http2 +netty-codec-memcache +netty-codec-redis +netty-codec-socks +netty-codec-stomp +netty-codec-mqtt +netty-codec-haproxy +netty-codec-dns +``` + +总共10个codec包,其中netty-codec是最基础的一个,其他的9个是对不同的协议包进行的扩展和适配,可以看到netty支持常用的和流行的协议格式,非常的强大。因为codec的内容非常多,要讲解他们也不是很容易,本文将会以netty-codec做一个例子,讲解其中最基本的也是最通用的编码解码器。 + +# 使用codec要注意的问题 + +虽然netty提供了很方便的codec编码解码器,但是正如我们在前一篇文章中提到的,有些codec是需要和Frame detection一起配合使用的,先使用Frame detection将ByteBuf拆分成一个个代表真实数据的ByteBuf,再交由netty内置的codec或者自定义的codec进行处理,这样才能起到应有的效果。 + +# netty内置的基本codec + +netty中基本的codec有base64、bytes、compression、json、marshalling、protobuf、serialization、string和xml这几种。 + +下面将会一一进行讲解。 + +## base64 + +这个codec是负责ByteBuf和base64过后的ByteBuf之间的转换。虽然都是从ByteBuf到ByteBuf,但是其中的内容发生了变化。 + +有两个关键的类,分别是Base64Encoder和Base64Decoder。因为Base64Decoder是一个MessageToMessageDecoder,所以需要使用一个DelimiterBasedFrameDecoder提前进行处理,常用的例子如下: + +```java +ChannelPipeline pipeline = ...; + +// Decoders +pipeline.addLast("frameDecoder", new DelimiterBasedFrameDecoder(80, Delimiters.nulDelimiter())); +pipeline.addLast("base64Decoder", new Base64Decoder()); + +// Encoder +pipeline.addLast("base64Encoder", new Base64Encoder()); +``` + +## bytes + +bytes是将bytes数组和ByteBuf之间进行转换,同样的在decode之前,也需要使用FrameDecoder,通常的使用方式如下: + +```java +ChannelPipeline pipeline = ...; + +// Decoders +pipeline.addLast("frameDecoder", + new LengthFieldBasedFrameDecoder(1048576, 0, 4, 0, 4)); +pipeline.addLast("bytesDecoder", + new ByteArrayDecoder()); + +// Encoder +pipeline.addLast("frameEncoder", new LengthFieldPrepender(4)); +pipeline.addLast("bytesEncoder", new ByteArrayEncoder()); +``` + +## compression + +compression这个包的内容就比较丰富了,主要是对数据的压缩和解压缩服务。其支持的算法如下: + +```java +brotli +Bzip2 +FastLZ +JdkZlib +Lz4 +Lzf +Snappy +Zlib +Zstandard +``` + +compression对于大数据量的传输特别有帮助,通过压缩可以节省传输的数据量,从而提高传输速度。 + +但是压缩是使用特定的算法来计算的,所以它是一个高CPU的操作,我们在使用的时候需要兼顾网络速度和CPU性能,并从中得到平衡。 + +## json + +json这个包里面只有一个JsonObjectDecoder类,主要负责将Byte流的JSON对象或者数组转换成JSON对象和数组。 + +JsonObjectDecoder直接就是一个ByteToMessageDecoder的子类,所以它不需要FrameDecoder,它是根据括号的匹配来判断Byte数组的起始位置,从而区分哪些Byte数据是属于同一个Json对象或者数组。 + +我们如果希望使用JSON来传输数据的话,这个类就非常有用了。 + +## marshalling + +Marshalling的全称叫做JBoss Marshalling,它是JBoss出品的一个对象序列化的方式,但是JBoss Marshalling的最新API还是在2011-04-27,已经有10年没更新了,是不是已经被废弃了? + +所以这里我们不详细介绍这个序列化的内容,感兴趣的小伙伴可以自行探索。 + +## protobuf + +protobuf大家应该都很熟悉了,它是google出品的一种信息交换格式,可以将其看做是一种序列化的方式。它是语言中立、平台中立、可扩展的结构化数据序列化机制,和XML类似,但是比XML更小、更快、更简单。 + +netty对protobuf的支持在于可以将protobuf中的message和MessageLite对象跟ByteBuf进行转换。 + +protobuf的两个编码器也是message到message直接的转换,所以也需要使用frame detection。当然你也可以使用其他的frame detection比如LengthFieldPrepender和LengthFieldBasedFrameDecoder如下所示: + +```java +ChannelPipeline pipeline = ...; + +// Decoders +pipeline.addLast("frameDecoder", + new LengthFieldBasedFrameDecoder(1048576, 0, 4, 0, 4)); +pipeline.addLast("protobufDecoder", + new ProtobufDecoder(MyMessage.getDefaultInstance())); + +// Encoder +pipeline.addLast("frameEncoder", new LengthFieldPrepender(4)); +pipeline.addLast("protobufEncoder", new ProtobufEncoder()); +``` + +其中LengthFieldPrepender会自动给字段前面加上一个长度字段: + +``` +之前: + +----------------+ + | "HELLO, WORLD" | + +----------------+ + +之后: + +--------+----------------+ + + 0x000C | "HELLO, WORLD" | + +--------+----------------+ +``` + +当然netty为protobuf准备了两个专门的frame detection,他们是ProtobufVarint32FrameDecoder和ProtobufVarint32LengthFieldPrepender。在讲解这两个类之前,我们需要了解一下protobuf中的Base 128 Varints。 + +什么叫Varints呢?就是序列化整数的时候,占用的空间大小是不一样的,小的整数占用的空间小,大的整数占用的空间大,这样不用固定一个具体的长度,可以减少数据的长度,但是会带来解析的复杂度。 + +那么怎么知道这个数据到底需要几个byte呢?在protobuf中,每个byte的最高位是一个判断位,如果这个位被置位1,则表示后面一个byte和该byte是一起的,表示同一个数,如果这个位被置位0,则表示后面一个byte和该byte没有关系,数据到这个byte就结束了。 + +举个例子,一个byte是8位,如果表示的是整数1,那么可以用下面的byte来表示: + +``` +0000 0001 +``` + +如果一个byte装不下的整数,那么就需要使用多个byte来进行连接操作,比如下面的数据表示的是300: + +``` +1010 1100 0000 0010 +``` + +为什么是300呢?首先看第一个byte,它的首位是1,表示后面还有一个byte。再看第二个byte,它的首位是0,表示到此就结束了。我们把判断位去掉,变成下面的数字: + +``` +010 1100 000 0010 +``` + +这时候还不能计算数据的值,因为在protobuf中,byte的位数是反过来的,所以我们需要把上面的两个byte交换一下位置: + +``` +000 0010 010 1100 +``` + +也就是: + +``` +10 010 1100 +``` + +=256 + 32 + 8 + 4 = 300 + +在protobuf中一般使用Varint作为字段的长度位,所以netty提供了ProtobufVarint32LengthFieldPrepender和ProtobufVarint32FrameDecoder对ByteBuf进行转换。 + +比如为ByteBuf添加varint的length: + +```java + BEFORE ENCODE (300 bytes) AFTER ENCODE (302 bytes) + +---------------+ +--------+---------------+ + | Protobuf Data |-------------->| Length | Protobuf Data | + | (300 bytes) | | 0xAC02 | (300 bytes) | + +---------------+ +--------+---------------+ +``` + +解码的时候删除varint的length字段: + +```java + BEFORE DECODE (302 bytes) AFTER DECODE (300 bytes) + +--------+---------------+ +---------------+ + | Length | Protobuf Data |----->| Protobuf Data | + | 0xAC02 | (300 bytes) | | (300 bytes) | + +--------+---------------+ +---------------+ +``` + +## serialization + +序列化就是把对象转换成二进制数据,事实上所有的codec都可以成为序列化。他们提供了对象和byte之间的转换方法。 + +netty也提供了两个对象的转换方法:ObjectDecoder和ObjectEncoder。 + +要注意的是,这两个对象和JDK自带的ObjectInputStream和ObjectOutputStream,是不兼容的,如果要兼容,可以使用CompactObjectInputStream、CompactObjectOutputStream和CompatibleObjectEncoder。 + +## string + +String是我们最常使用到的对象,netty为string提供了StringDecoder和StringEncoder。 + +同样的,在使用这两个类之前,需要将消息进行转换,通常使用的是 LineBasedFrameDecoder按行进行转换: + +```java +ChannelPipeline pipeline = ...; + +// Decoders +pipeline.addLast("frameDecoder", new LineBasedFrameDecoder(80)); +pipeline.addLast("stringDecoder", new StringDecoder(CharsetUtil.UTF_8)); + +// Encoder +pipeline.addLast("stringEncoder", new StringEncoder(CharsetUtil.UTF_8)); +``` + +## xml + +xml也是一个非常常用的格式,但是它的体积会比较大,现在应该用的比较少了。netty提供了一个XmlFrameDecoder来进行解析。 + +因为xml有自己的开始和结束符,所以不需要再做frame detection,直接转换即可,如: + +```java + +-----+-----+-----------+ + | | + +-----+-----+-----------+ +转换成: + +-----------------+ + | | + +-----------------+ + +-----+-----+-----------+-----+----------------------------------+ + | | content | + +-----+-----+-----------+-----+----------------------------------+ + 转换成: + +-----------------+-------------------------------------+ + | | content | + +-----------------+-------------------------------------+ +``` + +都是可以的。 + +# 总结 + +netty提供了很多优秀的codec来适配各种应用协议,大家可以多用用,找找不同协议的不同之处。 \ No newline at end of file diff --git a/learn-netty文章/17.netty系列之在netty中使用protobuf协议.md b/learn-netty文章/17.netty系列之在netty中使用protobuf协议.md new file mode 100644 index 0000000..ed88636 --- /dev/null +++ b/learn-netty文章/17.netty系列之在netty中使用protobuf协议.md @@ -0,0 +1,184 @@ +# netty系列之:在netty中使用protobuf协议 + +# 简介 + +netty中有很多适配不同协议的编码工具,对于流行的google出品的protobuf也不例外。netty为其提供了ProtobufDecoder和ProtobufEncoder两个工具还有对应的frame detection,接下来我们会通过一个例子来详细讲解如何在netty中使用protobuf。 + +# 定义protobuf + +我们举个最简单的例子,首先定义一个需要在网络中进行传输的message,这里我们定义一个student对象,他有一个age和一个name属性,如下所示: + +```java +syntax = "proto3"; + +package com.flydean17.protobuf; + +option java_multiple_files = true; +option java_package = "com.flydean17.protobuf"; +option java_outer_classname = "StudentWrapper"; + +message Student { + optional int32 age = 1; + optional string name =2; +} +``` + +使用下面的命令,对其进行编译: + +```java + protoc --experimental_allow_proto3_optional -I=. --java_out=. student.proto +``` + +可以看到生成了3个文件,分别是Student,StudentOrBuilder和StudentWrapper。其中Student和StudentOrBuilder是我们真正需要用到的对象。 + +# 定义handler + +在handler中,我们主要进行对消息进行处理,这里我们在clientHandler中进行消息的构建和发送,StudentClientHandler继承SimpleChannelInboundHandler并重新channelActive方法, 在该方法中我们使用protobuf的语法,构建一个新的Student实例,并给他设置好age和name两个属性。 + +然后使用ctx.write和ctx.flush方法将其发送到server端: + +```java +public void channelActive(ChannelHandlerContext ctx) throws Exception { + // channel活跃 + //构建一个Student,并将其写入到channel中 + Student student= Student.newBuilder().setAge(22).setName("flydean").build(); + log.info("client发送消息{}",student); + ctx.write(student); + ctx.flush(); +} +``` + +StudentServerHandler也是继承SimpleChannelInboundHandler,并重写channelRead0方法,当server端读取到student消息的时候,日志输出,并将其回写到channel中,供clientHandler读取: + +```java +public void channelRead0(ChannelHandlerContext ctx, Student student) throws Exception { + log.info("server收到消息{}",student); + // 写入消息 + ChannelFuture future = ctx.write(student); +} +``` + +当client读取到消息之后,直接日志输出,不再做进一步处理,到此,一轮client和server端的交互就完成了: + +```java +public void channelRead0(ChannelHandlerContext ctx, Student student) throws Exception { + log.info("client收到消息{}",student); +} +``` + +# 设置ChannelPipeline + +在上一节,不管是在StudentClientHandler还是在StudentServerHandler中,我们都假设channel中传递的对象就是Student,而不是原始的ByteBuf。这是怎么做到的呢? + +这里我们需要使用到netty提供的frame detection,netty为protobuf协议专门提供了ProtobufDecoder和ProtobufEncoder,用于对protobuf对象进行编码和解码。 + +但是这两个编码和解码器分别是MessageToMessageEncoder和MessageToMessageDecoder,他们是消息到消息的编码和解码器,所以还需要和frame detection配合使用。 + +netty同样提供了和protobuf配合使用的frame detector,他们是ProtobufVarint32FrameDecoder和ProtobufVarint32LengthFieldPrepender。 + +Varint32指的是protobuf的编码格式,第一个字节使用的是可变的varint。 + +有了frame detector和编码解码器,我们只需要将其顺序加入ChannelPipeline即可。 + +在客户端,StudentClientInitializer继承自ChannelInitializer,我们需要重写其initChannel方法: + +```java +public void initChannel(SocketChannel ch) { + ChannelPipeline p = ch.pipeline(); + + p.addLast(new ProtobufVarint32FrameDecoder()); + p.addLast(new ProtobufDecoder(Student.getDefaultInstance())); + + p.addLast(new ProtobufVarint32LengthFieldPrepender()); + p.addLast(new ProtobufEncoder()); + + p.addLast(new StudentClientHandler()); +} +``` + +在服务器端,同样StudentServerInitializer也继承自ChannelInitializer,也需要重写其initChannel方法: + +```java +public void initChannel(SocketChannel ch) throws Exception { + ChannelPipeline p = ch.pipeline(); + + p.addLast(new ProtobufVarint32FrameDecoder()); + p.addLast(new ProtobufDecoder(Student.getDefaultInstance())); + + p.addLast(new ProtobufVarint32LengthFieldPrepender()); + p.addLast(new ProtobufEncoder()); + + p.addLast(new StudentServerHandler()); +} +``` + +这样ChannelPipeline也设置完成了。 + +# 构建client和server端并运行 + +最后好做的就是构建client和server端并运行,这和普通的netty客户端和服务器端并没有什么区别: + +构建StudentClient: + +```java +public static void main(String[] args) throws Exception { + + EventLoopGroup group = new NioEventLoopGroup(); + try { + Bootstrap b = new Bootstrap(); + b.group(group) + .channel(NioSocketChannel.class) + .handler(new StudentClientInitializer()); + // 建立连接 + Channel ch = b.connect(HOST, PORT).sync().channel(); + // 等待关闭 + ch.closeFuture().sync(); + } finally { + group.shutdownGracefully(); + } +} +``` + +构建StudentServer: + +```java +public static void main(String[] args) throws Exception { + EventLoopGroup bossGroup = new NioEventLoopGroup(1); + EventLoopGroup workerGroup = new NioEventLoopGroup(); + try { + ServerBootstrap b = new ServerBootstrap(); + b.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new StudentServerInitializer()); + + b.bind(PORT).sync().channel().closeFuture().sync(); + } finally { + bossGroup.shutdownGracefully(); + workerGroup.shutdownGracefully(); + } +} +``` + +运行可得: + +```java +server端: +[nioEventLoopGroup-3-1] INFO c.f.protobuf.StudentServerHandler - server收到消息age: 22 +name: "flydean" + +[nioEventLoopGroup-2-1] INFO c.f.protobuf.StudentClientHandler - client发送消息age: 22 +name: "flydean" + +client端: +[nioEventLoopGroup-2-1] INFO c.f.protobuf.StudentClientHandler - client收到消息age: 22 +name: "flydean" +``` + +可见Student消息已经发送和接收成功了。 + +# 总结 + +netty提供了很多和协议适配的工具类,这样我们就可以专注于业务逻辑,不需要考虑具体的编码转换的问题,非常好用。 + +本文的例子可以参考:[learn-netty4](https://github.com/ddean2009/learn-netty4) \ No newline at end of file diff --git a/learn-netty文章/1793009756544508a2582d689d1d065e.png b/learn-netty文章/1793009756544508a2582d689d1d065e.png new file mode 100644 index 0000000..e78c72d Binary files /dev/null and b/learn-netty文章/1793009756544508a2582d689d1d065e.png differ diff --git a/learn-netty文章/18.netty系列之轻轻松松搭个支持中文的服务器.md b/learn-netty文章/18.netty系列之轻轻松松搭个支持中文的服务器.md new file mode 100644 index 0000000..43ae45e --- /dev/null +++ b/learn-netty文章/18.netty系列之轻轻松松搭个支持中文的服务器.md @@ -0,0 +1,150 @@ +# netty系列之:轻轻松松搭个支持中文的服务器 + +# 简介 + +之前讲了那么多关于netty的文章,都是讲netty的底层原理和实现,各位小伙伴一定都在想了,看了这么多篇文章,netty到底能干啥呢?今天让我们来使用netty简简单单搭一个支持中文的服务器,展示一下netty的威力。 + +# netty的HTTP支持 + +今天我们搭的服务器是支持HTTP1.1的服务器。在netty中搭建服务器就像是拼房子,找到合适的工具就可以事半功倍。那么要搭建HTTP的房子,netty提供了什么样的工具呢? + +在讲解netty对HTTP的支持之前,我们先看一下HTTP的版本发展情况。 + +HTTP的全称是Hypertext Transfer Protocol,是在1989年World Wide Web发展起来之后出现的标准协议,用来在WWW上传输数据。HTTP/1.1是1997年在原始的HTTP协议基础上进行的补充和优化。 + +到了2015年,为了适应快速发送的web应用和现代浏览器的需求,发展出了新的HTTP/2协议,主要在手机浏览器、延时处理、图像处理和视频处理方面进行了优化。 + +基本上所有的现代浏览器都支持HTTP/2协议了,但是还有很多应用程序使用的是老的HTTP/1.1协议。netty为HTTP2和HTTP1提供了不同的支持包,对于HTTP1的支持包叫做netty-codec-http,对HTTP2支持的包叫做netty-codec-http2。 + +本文会讲解netty对HTTP1的支持,将会在后续的文章中继续HTTP2的介绍。 + +netty-codec-http提供了对HTTP的非常有用的一些封装。 + +首先是代表HTTP中传输对象的类HttpObject,这个类代表着传输中的所有对象。继承这个类的对象有两个非常重要的对象,分别是HttpMessage和HttpContent。 + +HttpMessage可能跟我想象的不太一样,它实际上只包含了两部分内容,分别是HttpVersion和HttpHeaders,但是并不包含任何内容。 + +```java +public interface HttpMessage extends HttpObject { + + HttpVersion protocolVersion(); + + HttpMessage setProtocolVersion(HttpVersion version); + + HttpHeaders headers(); +} +``` + +这里HttpVersion只支持HTTP/1.0和HTTP/1.1协议。而HttpHeaders就是对HTTP请求中头对象的封装。 + +HttpMessage的子类是HttpRequest和HttpResponse,所以这两个类本身是不带请求内容的。 + +而具体请求的内容是在HttpContent中,HttpContent继承自ByteBufHolder,表示它中间可以带有ByteBuf的内容信息。 + +而HttpContent真正的实现类就是DefaultFullHttpRequest和DefaultFullHttpResponse,这两个内包含了HTTP头和HTTP请求响应内容信息。 + +那么问题来了,为什么要把HTTP头和HTTP内容分开呢? + +这就涉及到HTTP1.1中消息传输中的压缩机制了。为了提升传输的效率,一般来说在传输的的过程中都会对象消息进行压缩,但是对于HTTP1.1来说,头部的内容是没办法压缩的,只能压缩content部分,所以需要区别对待。 + +# netty中使用HTTP的原理 + +我们知道netty底层是客户端和服务器端构建通道,通过通道来传输ByteBuf消息。那么netty是怎么支持HTTP请求呢? + +当客户端向服务器端发送HTTP请求之后,服务器端需要把接收到的数据使用解码器解码成为可以被应用程序使用的各种HttpObject对象,从而能够在应用程序中对其解析。 + +netty提供了HttpResponseEncoder和HttpRequestDecoder类,来对HTTP的消息进行编码和解码。 + +如果不想分别使用两个类来进行编码和解码,netty还提供了HttpServerCodec类来进行编码和解码工作。 + +这个类包含了HttpRequestDecoder和HttpResponseEncoder两部分的工作,可以同时用来进行编码和解码。 + +## 100 (Continue) Status + +在HTTP中有一个独特的功能叫做,100 (Continue) Status,就是说client在不确定server端是否会接收请求的时候,可以先发送一个请求头,并在这个头上加一个”100-continue”字段,但是暂时还不发送请求body。直到接收到服务器端的响应之后再发送请求body。 + +为了处理这种请求,netty提供了一个HttpServerExpectContinueHandler对象,用来处理100 Status的情况。 + +当然,如果你的客户端没有这种请求,那么可以直接使用HttpObjectAggregator来将HttpMessage和HttpContent和合并成为FullHttpRequest或者FullHttpResponse。 + +# 为netty搭建HTTP服务器 + +有了上面的工作,我们就可以使用netty搭建http服务器了。最关键的一点就是在HttpRequestServerInitializer添加对应的codec和自定义handler。 + +```java +public void initChannel(SocketChannel ch) { + ChannelPipeline p = ch.pipeline(); + p.addLast(new HttpServerCodec()); + p.addLast(new HttpServerExpectContinueHandler()); + p.addLast(new HttpRequestServerHandler()); +} +``` + +在自定义的handler中,我们需要实现一个功能,就是当收到客户端的请求时候,需要返回给客户端一段欢迎语。 + +首先将获得的HttpObject转换成为HttpRequest对象,然后根据请求对象构建一个DefaultFullHttpResponse对象,然后设置该response对象的头,最后将该对象写到channel中。 + +对应的关键代码如下: + +```java +private static final byte[] CONTENT = "欢迎来到www.flydean.com!".getBytes(StandardCharsets.UTF_8); + +public void channelRead0(ChannelHandlerContext ctx, HttpObject msg) { + if (msg instanceof HttpRequest) { + HttpRequest req = (HttpRequest) msg; + + boolean keepAlive = HttpUtil.isKeepAlive(req); + FullHttpResponse response = new DefaultFullHttpResponse(req.protocolVersion(), OK, + Unpooled.wrappedBuffer(CONTENT)); + response.headers() + //.set(CONTENT_TYPE, TEXT_PLAIN) + .set(CONTENT_TYPE, "text/plain;charset=utf-8") + .setInt(CONTENT_LENGTH, response.content().readableBytes()); + + if (keepAlive) { + if (!req.protocolVersion().isKeepAliveDefault()) { + //设置header connection=keep-alive + response.headers().set(CONNECTION, KEEP_ALIVE); + } + } else { + // 如果keepAlive是false,则设置header connection=close + response.headers().set(CONNECTION, CLOSE); + } + ChannelFuture f = ctx.write(response); + if (!keepAlive) { + f.addListener(ChannelFutureListener.CLOSE); + } + } +} +``` + +上面的关键代码中CONTENT包含了中文字符串,我们使用getBytes将其转换成了UTF-8编码的byte数组。那么如果要想客户端能够正确识别UTF-8编码,需要在response的header中设置内容类型文件为:”text/plain;charset=utf-8″。 + +最后,使用下面的代码启动server: + +```java +// server配置 +EventLoopGroup bossGroup = new NioEventLoopGroup(1); +EventLoopGroup workerGroup = new NioEventLoopGroup(); +try { + ServerBootstrap b = new ServerBootstrap(); + b.option(ChannelOption.SO_BACKLOG, 1024); + b.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new HttpRequestServerInitializer()); + + Channel ch = b.bind(PORT).sync().channel(); + log.info("请打开你的浏览器,访问 http://127.0.0.1:8000/"); + ch.closeFuture().sync(); +} finally { + bossGroup.shutdownGracefully(); + workerGroup.shutdownGracefully(); +} +``` + +# 总结 + +现在,使用你的浏览器访问你搭建的服务器地址,你就可以得到”欢迎来到www.flydean.com!”。 到此一个简单的netty服务器就完成了。 + +本文的例子可以参考:[learn-netty4](https://github.com/ddean2009/learn-netty4) \ No newline at end of file diff --git a/learn-netty文章/19.netty系列之自建客户端和HTTP服务器交互.md b/learn-netty文章/19.netty系列之自建客户端和HTTP服务器交互.md new file mode 100644 index 0000000..0fd196f --- /dev/null +++ b/learn-netty文章/19.netty系列之自建客户端和HTTP服务器交互.md @@ -0,0 +1,179 @@ +# netty系列之:自建客户端和HTTP服务器交互 + +# 简介 + +上一篇文章,我们搭建了一个支持中文的HTTP服务器,并且能够从浏览器访问,并获取到相应的结果。虽然浏览器在日常的应用中很普遍,但是有时候我们也有可能从自建的客户端来调用HTTP服务器的服务。 + +今天给大家介绍如何自建一个HTTP客户端来和HTTP服务器进行交互。 + +# 使用客户端构建请求 + +在上一篇文章中,我们使用浏览器来访问服务器,并得到到了响应的结果,那么如何在客户端构建请求呢? + +netty中的HTTP请求可以分成两个部分,分别是HttpRequest和HttpContent。其中HttpRequest只包含了请求的版本号和消息头的信息,HttpContent才包含了真正的请求内容信息。 + +但是如果要构建一个请求的话,需要同时包含HttpRequest和HttpContent的信息。netty提供了一个请求类叫做DefaultFullHttpRequest,这个类同时包含了两部分的信息,可以直接使用。 + +使用DefaultFullHttpRequest的构造函数,我们就可以构造出一个HttpRequest请求如下: + +```java +HttpRequest request = new DefaultFullHttpRequest( + HttpVersion.HTTP_1_1, HttpMethod.GET, uri.getRawPath(), Unpooled.EMPTY_BUFFER); +``` + +上面的代码中,我们使用的协议是HTTP1.1,方法是GET,请求的content是一个空的buffer。 + +构建好基本的request信息之后,我们可能还需要在header中添加一下额外的信息,比如connection,accept-encoding还有cookie的信息。 + +比如设置下面的信息: + +```java +request.headers().set(HttpHeaderNames.HOST, host); +request.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE); +request.headers().set(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.GZIP); +``` + +还有设置cookie: + +```java +request.headers().set( + HttpHeaderNames.COOKIE, + ClientCookieEncoder.STRICT.encode( + new DefaultCookie("name", "flydean"), + new DefaultCookie("site", "www.flydean.com"))); +``` + +设置cookie我们使用的是ClientCookieEncoder.encode方法,ClientCookieEncoder有两种encoder模式,一种是STRICT,一种是LAX。 + +在STRICT模式下,会对cookie的name和value进行校验和排序。 + +和encoder对应的就是ClientCookieDecoder,用于对cookie进行解析。 + +设置好我们所有的request之后就可以写入到channel中了。 + +# accept-encoding + +在客户端写入请求的时候,我们在请求头上添加了accept-encoding,并将其值设置为GZIP,表示客户端接收的编码方式是GZIP。 + +如果服务器端发送了GZIP的编码内容之后,客户端怎么进行解析呢?我们需要对GZIP的编码格式进行解码。 + +netty提供了HttpContentDecompressor类,可以对gzip或者deflate格式的编码进行解码。在解码之后,会同时修改响应头中的“Content-Encoding”和“Content-Length”。 + +我们只需要将其添加到pipline中即可。 + +和它对应的类是HttpContentCompressor,用于对HttpMessage和HttpContent进行gzip或者deflate编码。 + +所以说HttpContentDecompressor应该被添加到client的pipline中,而HttpContentCompressor应该被添加到server端的pipline中。 + +# server解析HTTP请求 + +server需要一个handler来解析客户端请求过来的消息。对于服务器来说,解析客户端的请求应该注意哪些问题呢? + +首先要注意的是客户端100 Continue请求的问题。 + +在HTTP中有一个独特的功能叫做,100 (Continue) Status,就是说client在不确定server端是否会接收请求的时候,可以先发送一个请求头,并在这个头上加一个”100-continue”字段,但是暂时还不发送请求body。直到接收到服务器端的响应之后再发送请求body。 + +如果服务器收到100Continue请求的话,直接返回确认即可: + +```java +if (HttpUtil.is100ContinueExpected(request)) { + send100Continue(ctx); +} + +private static void send100Continue(ChannelHandlerContext ctx) { + FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, CONTINUE, Unpooled.EMPTY_BUFFER); + ctx.write(response); +} +``` + +如果不是100请求的话,server端就可以准备要返回的内容了: + +这里用一个StringBuilder来存储要返回的内容: + +```java +StringBuilder buf = new StringBuilder(); +``` + +为什么要用StringBuf呢?是因为有可能server端一次并不能完全接受客户端的请求,所以将所有的要返回的内容都放到buffer中,等全部接受之后再一起返回。 + +我们可以向server端添加欢迎信息,可以可以添加从客户端获取的各种信息: + +```java +buf.setLength(0); +buf.append("欢迎来到www.flydean.com\r\n"); +buf.append("===================================\r\n"); + +buf.append("VERSION: ").append(request.protocolVersion()).append("\r\n"); +buf.append("HOSTNAME: ").append(request.headers().get(HttpHeaderNames.HOST, "unknown")).append("\r\n"); +buf.append("REQUEST_URI: ").append(request.uri()).append("\r\n\r\n"); +``` + +还可以向buffer中添加请求头信息: + +```java +HttpHeaders headers = request.headers(); +if (!headers.isEmpty()) { + for (Entry h: headers) { + CharSequence key = h.getKey(); + CharSequence value = h.getValue(); + buf.append("HEADER: ").append(key).append(" = ").append(value).append("\r\n"); + } + buf.append("\r\n"); +} +``` + +可以向buffer中添加请求参数信息: + +```java +QueryStringDecoder queryStringDecoder = new QueryStringDecoder(request.uri()); +Map> params = queryStringDecoder.parameters(); +if (!params.isEmpty()) { + for (Entry> p: params.entrySet()) { + String key = p.getKey(); + List vals = p.getValue(); + for (String val : vals) { + buf.append("PARAM: ").append(key).append(" = ").append(val).append("\r\n"); + } + } + buf.append("\r\n"); +} +``` + +要注意的是当读取到HttpContent的时候的处理方式。如果读取的消息是HttpContent,那么将content的内容添加到buffer中: + +```java +if (msg instanceof HttpContent) { + HttpContent httpContent = (HttpContent) msg; + + ByteBuf content = httpContent.content(); + if (content.isReadable()) { + buf.append("CONTENT: "); + buf.append(content.toString(CharsetUtil.UTF_8)); + buf.append("\r\n"); + appendDecoderResult(buf, request); + } +``` + +那么怎么判断一个请求是否结束了呢?netty提供了一个类叫做LastHttpContent,这个类表示的是消息的最后一部分,当收到这一部分消息之后,我们就可以判断一个HTTP请求已经完成了,可以正式的返回消息了: + +```java +if (msg instanceof LastHttpContent) { + log.info("LastHttpContent:{}",msg); + buf.append("END OF CONTENT\r\n"); +``` + +要写回channel,同样需要构建一个DefaultFullHttpResponse,这里使用buffer来进行构建: + +```java +FullHttpResponse response = new DefaultFullHttpResponse( + HTTP_1_1, currentObj.decoderResult().isSuccess()? OK : BAD_REQUEST, + Unpooled.copiedBuffer(buf.toString(), CharsetUtil.UTF_8)); +``` + +然后添加一些必须的header信息就可以调用ctx.write进行回写了。 + +# 总结 + +本文介绍了如何在client构建HTTP请求,并详细讲解了HTTP server对HTTP请求的解析流程。 + +本文的例子可以参考:[learn-netty4](https://github.com/ddean2009/learn-netty4) \ No newline at end of file diff --git a/learn-netty文章/1d9c19d567084c199dfade76c8a0d52a.png b/learn-netty文章/1d9c19d567084c199dfade76c8a0d52a.png new file mode 100644 index 0000000..04ad590 Binary files /dev/null and b/learn-netty文章/1d9c19d567084c199dfade76c8a0d52a.png differ diff --git a/learn-netty文章/2.netty系列之netty中的ByteBuf详解.md b/learn-netty文章/2.netty系列之netty中的ByteBuf详解.md new file mode 100644 index 0000000..f49a6ee --- /dev/null +++ b/learn-netty文章/2.netty系列之netty中的ByteBuf详解.md @@ -0,0 +1,192 @@ +# netty系列之:netty中的ByteBuf详解 + +# 简介 + +netty中用于进行信息承载和交流的类叫做ByteBuf,从名字可以看出这是Byte的缓存区,那么ByteBuf都有哪些特性呢?一起来看看。 + +# ByteBuf详解 + +netty提供了一个io.netty.buffer的包,该包里面定义了各种类型的ByteBuf和其衍生的类型。 + +netty Buffer的基础是ByteBuf类,这是一个抽象类,其他的Buffer类基本上都是由该类衍生而得的,这个类也定义了netty整体Buffer的基调。 + +先来看下ByteBuf的定义: + +``` +public abstract class ByteBuf implements ReferenceCounted, Comparable { +``` + +ByteBuf实现了两个接口,分别是ReferenceCounted和Comparable。Comparable是JDK自带的接口,表示该类之间是可以进行比较的。而ReferenceCounted表示的是对象的引用统计。当一个ReferenceCounted被实例化之后,其引用count=1,每次调用retain() 方法,就会增加count,调用release() 方法又会减少count。当count减为0之后,对象将会被释放,如果试图访问被释放过后的对象,则会报访问异常。 + +如果一个对象实现了ReferenceCounted,并且这个对象里面包含的其他对象也实现了ReferenceCounted,那么当容器对象的count=0的时候,其内部的其他对象也会被调用release()方法进行释放。 + +综上,ByteBuf是一个可以比较的,可以计算引用次数的对象。他提供了序列或者随机的byte访问机制。 + +> 注意的是,虽然JDK中有自带的ByteBuffer类,但是netty中的 ByteBuf 算是对Byte Buffer的重新实现。他们没有关联关系。 + +## 创建一个Buff + +ByteBuf是一个抽象类,并不能直接用来实例化,虽然可以使用ByteBuf的子类进行实例化操作,但是netty并不推荐。netty推荐使用io.netty.buffer.Unpooled来进行Buff的创建工作。Unpooled是一个工具类,可以为ByteBuf分配空间、拷贝或者封装操作。 + +下面是创建几个不同ByteBuf的例子: + +```java + import static io.netty.buffer.Unpooled.*; + + ByteBuf heapBuffer = buffer(128); + ByteBuf directBuffer = directBuffer(256); + ByteBuf wrappedBuffer = wrappedBuffer(new byte[128], new byte[256]); + ByteBuf copiedBuffer = copiedBuffer(ByteBuffer.allocate(128)); +``` + +上面我们看到了4种不同的buff构建方式,普通的buff、directBuffer、wrappedBuffer和copiedBuffer。 + +普通的buff是固定大小的堆buff,而directBuffer是固定大小的direct buff。direct buff使用的是堆外内存,省去了数据到内核的拷贝,因此效率比普通的buff要高。 + +wrappedBuffer是对现有的byte arrays或者byte buffers的封装,可以看做是一个视图,当底层的数据发生变化的时候,Wrapped buffer中的数据也会发生变化。 + +Copied buffer是对现有的byte arrays、byte buffers 或者 string的深拷贝,所以它和wrappedBuffer是不同的,Copied buffer和原数据之间并不共享数据。 + +## 随机访问Buff + +熟悉集合的朋友应该都知道,要想随机访问某个集合,一定是通过index来访问的,ByteBuf也一样,可以通过capacity或得其容量,然后通过getByte方法随机访问其中的byte,如下所示: + +```java +//随机访问 +ByteBuf buffer = heapBuffer; +for (int i = 0; i < buffer.capacity(); i ++) { + byte b = buffer.getByte(i); + System.out.println((char) b); +} +``` + +## 序列读写 + +读写要比访问复杂一点,ByteBuf 提供了两个index用来定位读和写的位置,分别是readerIndex 和 writerIndex ,两个index分别控制读和写的位置。 + +下图显示的一个buffer被分成了三部分,分别是可废弃的bytes、可读的bytes和可写的bytes。 + +``` + +-------------------+------------------+------------------+ + | discardable bytes | readable bytes | writable bytes | + | | (CONTENT) | | + +-------------------+------------------+------------------+ + | | | | + 0 <= readerIndex <= writerIndex <= capacity +``` + +上图还表明了readerIndex、writerIndex和capacity的大小关系。 + +其中readable bytes是真正的内容,可以通过调用read* 或者skip* 的方法来进行访问或者跳过,调用这些方法的时候,readerIndex会同步增加,如果超出了readable bytes的范围,则会抛出IndexOutOfBoundsException。默认情况下readerIndex=0。 + +下面是一个遍历readable bytes的例子: + +```java +//遍历readable bytes +while (directBuffer.isReadable()) { + System.out.println(directBuffer.readByte()); +} +``` + +首先通过判断是否是readable来决定是否调用readByte方法。 + +Writable bytes是一个未确定的区域,等待被填充。可以通过调用write*方法对其操作,同时writerIndex 会同步更新,同样的,如果空间不够的话,也会抛出IndexOutOfBoundsException。默认情况下 新分配的writerIndex =0 ,而wrapped 或者copied buffer的writerIndex=buf的capacity。 + +下面是一个使用writable Byte的例子: + +```java +//写入writable bytes +while (wrappedBuffer.maxWritableBytes() >= 4) { + wrappedBuffer.writeInt(new Random().nextInt()); +} +``` + +Discardable bytes是已经被读取过的bytes,初始情况下它的值=0,每当readerIndex右移的时候,Discardable bytes的空间就会增加。如果想要完全删除或重置Discardable bytes,则可以调用discardReadBytes()方法,该方法会将Discardable bytes空间删除,将多余的空间放到writable bytes中,如下所示: + +``` +调用 discardReadBytes() 之前: + + +-------------------+------------------+------------------+ + | discardable bytes | readable bytes | writable bytes | + +-------------------+------------------+------------------+ + | | | | + 0 <= readerIndex <= writerIndex <= capacity + + +调用 discardReadBytes()之后: + + +------------------+--------------------------------------+ + | readable bytes | writable bytes (got more space) | + +------------------+--------------------------------------+ + | | | +``` + +readerIndex (0) <= writerIndex (decreased) <= capacity + +> 注意,虽然writable bytes变多了,但是其内容是不可控的,并不能保证里面的内容是空的或者不变。 + +调用clear()方法会将readerIndex 和 writerIndex 清零,注意clear方法只会设置readerIndex 和 writerIndex 的值,并不会清空content,看下面的示意图: + +``` +调用 clear()之前: + + +-------------------+------------------+------------------+ + | discardable bytes | readable bytes | writable bytes | + +-------------------+------------------+------------------+ + | | | | + 0 <= readerIndex <= writerIndex <= capacity + + +调用 clear()之后: + + +---------------------------------------------------------+ + | writable bytes (got more space) | + +---------------------------------------------------------+ + | | + 0 = readerIndex = writerIndex <= capacity +``` + +## 搜索 + +ByteBuf提供了单个byte的搜索功能,如 indexOf(int, int, byte) 和 bytesBefore(int, int, byte)两个方法。 + +如果是要对ByteBuf遍历进行搜索处理的话,可以使用 forEachByte(int, int, ByteProcessor),这个方法接收一个ByteProcessor用于进行复杂的处理。 + +## 其他衍生buffer方法 + +ByteBuf还提供了很多方法用来创建衍生的buffer,如下所示: + +```java +duplicate() +slice() +slice(int, int) +readSlice(int) +retainedDuplicate() +retainedSlice() +retainedSlice(int, int) +readRetainedSlice(int) +``` + +要注意的是,这些buf是建立在现有buf基础上的衍生品,他们的底层内容是一样的,只有readerIndex, writerIndex 和做标记的index不一样。所以他们和原buf是有共享数据的。如果你希望的是新建一个全新的buffer,那么可以使用copy()方法或者前面提到的Unpooled.copiedBuffer。 + +在前面小节中,我们讲到ByteBuf是一个ReferenceCounted,这个特征在衍生buf中就用到了。我们知道调用retain() 方法的时候,引用count会增加,但是对于 duplicate(), slice(), slice(int, int) 和 readSlice(int) 这些方法来说,虽然他们也是引用,但是没有调用retain()方法,这样原始数据会在任意一个Buf调用release()方法之后被回收。 + +如果不想有上面的副作用,那么可以将方法替换成retainedDuplicate(), retainedSlice(), retainedSlice(int, int) 和 readRetainedSlice(int) ,这些方法会调用retain()方法以增加一个引用。 + +## 和现有JDK类型的转换 + +之前提到了ByteBuf 是对ByteBuffer的重写,他们是不同的实现。虽然这两个不同,但是不妨碍将ByteBuf转换ByteBuffer。 + +当然,最简单的转换是把ByteBuf转换成byte数组byte[]。要想转换成byte数组,可以先调用hasArray() 进行判断,然后再调用array()方法进行转换。 + +同样的ByteBuf还可以转换成为ByteBuffer ,可以先调用 nioBufferCount()判断能够转换成为 ByteBuffers的个数,再调用nioBuffer() 进行转换。 + +返回的ByteBuffer是对现有buf的共享或者复制,对返回之后buffer的position和limit修改不会影响到原buf。 + +最后,使用toString(Charset) 方法可以将ByteBuf转换成为String。 + +# 总结 + +ByteBuf是netty的底层基础,是传输数据的承载对象,深入理解ByteBuf就可以搞懂netty的设计思想,非常不错。 + +本文的例子可以参考:[learn-netty4](https://github.com/ddean2009/learn-netty4) \ No newline at end of file diff --git a/learn-netty文章/20.netty系列之搭建自己的下载文件服务器.md b/learn-netty文章/20.netty系列之搭建自己的下载文件服务器.md new file mode 100644 index 0000000..c14f94a --- /dev/null +++ b/learn-netty文章/20.netty系列之搭建自己的下载文件服务器.md @@ -0,0 +1,206 @@ +# netty系列之:搭建自己的下载文件服务器 + +# 简介 + +上一篇文章我们学习了如何在netty中搭建一个HTTP服务器,讨论了如何对客户端发送的请求进行处理和响应,今天我们来讨论一下在netty中搭建文件服务器进行文件传输中应该注意的问题。 + +# 文件的content-type + +客户端向服务器端请求一个文件,服务器端在返回的HTTP头中会包含一个content-type的内容,这个content-type表示的是返回的文件类型。这个类型应该怎么确认呢? + +一般来说,文件类型是根据文件的的扩展名来确认的,根据 RFC 4288的规范,所有的网络媒体类型都必须注册。apache也提供了一个文件MIME type和扩展名的映射关系表。 + +因为文件类型比较多,我们看几个比较常用到的类型如下: + +| MIME type | 扩展名 | +| :----------------------------------------------------------: | :---------------------------: | +| image/jpeg | jpg | +| image/jpeg | jpeg | +| image/png | png | +| text/plain | txt text conf def list log in | +| image/webp | webp | +| application/vnd.ms-excel | xls | +| application/vnd.openxmlformats-officedocument.spreadsheetml.sheet | xlsx | +| application/msword | doc | +| application/vnd.openxmlformats-officedocument.wordprocessingml.document | docx | +| application/vnd.openxmlformats-officedocument.presentationml.presentation | pptx | +| application/vnd.ms-powerpoint | ppt | +| application/pdf | pdf | + +JDK提供了一个MimetypesFileTypeMap的类,这个类提供了一个getContentType方法,可以根据请求的文件path信息,来推断其MIME type类型: + +```java +private static void setContentTypeHeader(HttpResponse response, File file) { + MimetypesFileTypeMap mimeTypesMap = new MimetypesFileTypeMap(); + response.headers().set(HttpHeaderNames.CONTENT_TYPE, mimeTypesMap.getContentType(file.getPath())); +} +``` + +# 客户端缓存文件 + +对于HTTP的文件请求来说,为了保证请求的速度,会使用客户端缓存的机制。比如客户端向服务器端请求一个文件A.txt。服务器在接收到该请求之后会将A.txt文件发送给客户端。 + +其请求流程如下: + +```java + 步骤1:客户端请求服务器端的文件 + =================== + GET /file1.txt HTTP/1.1 + 步骤2:服务器端返回文件,并且附带额外的文件时间信息: + =================== + HTTP/1.1 200 OK + Date: Mon, 23 Aug 2021 17:52:30 GMT+08:00 + Last-Modified: Tue, 10 Aug 2021 18:05:35 GMT+08:00 + Expires: Mon, 23 Aug 2021 17:53:30 GMT+08:00 + Cache-Control: private, max-age=60 +``` + +一般来说如果客户端是现代浏览器的话,就会把A.txt缓存起来。在下次调用的时候只需要在head中添加If-Modified-Since,询问服务器该文件是否被修改了即可,如果文件没有被修改,则服务器会返回一个304 Not Modified,客户端得到该状态之后就会使用本地的缓存文件。 + +```java + 步骤3:客户端再次请求该文件 + =================== + GET /file1.txt HTTP/1.1 + If-Modified-Since: Mon, 23 Aug 2021 17:55:30 GMT+08:00 + + 步骤4:服务器端响应该请求 + =================== + HTTP/1.1 304 Not Modified + Date: Mon, 23 Aug 2021 17:55:32 GMT+08:00 +``` + +在服务器的代码层面,我们首先需要返回一个响应中通常需要的日期字段,如Date、Last-Modified、Expires、Cache-Control等: + +```java +SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US); +dateFormatter.setTimeZone(TimeZone.getTimeZone(HTTP_DATE_GMT_TIMEZONE)); + +// 日期 header +Calendar time = new GregorianCalendar(); +log.info(dateFormatter.format(time.getTime())); + +response.headers().set(HttpHeaderNames.DATE, dateFormatter.format(time.getTime())); + +// 缓存 headers +time.add(Calendar.SECOND, HTTP_CACHE_SECONDS); +response.headers().set(HttpHeaderNames.EXPIRES, dateFormatter.format(time.getTime())); +response.headers().set(HttpHeaderNames.CACHE_CONTROL, "private, max-age=" + HTTP_CACHE_SECONDS); +response.headers().set( + HttpHeaderNames.LAST_MODIFIED, dateFormatter.format(new Date(fileToCache.lastModified()))); +``` + +然后在收到客户端的二次请求之后,需要比较文件的最后修改时间和If-Modified-Since中自带的时间,如果没有发送变化,则发送304状态: + +```java +FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, NOT_MODIFIED, Unpooled.EMPTY_BUFFER); +setDateHeader(response); +``` + +# 其他HTTP中常用的处理 + +我们讨论了文件类型和缓存,对于一个通用的HTTP服务器来说,还需要考虑很多其他常用的处理,比如异常、重定向和Keep-Alive设置。 + +对于异常,我们需要根据异常的代码来构造一个DefaultFullHttpResponse,并且设置相应的CONTENT_TYPE头即可,如下所示: + +```java +FullHttpResponse response = new DefaultFullHttpResponse( + HTTP_1_1, status, Unpooled.copiedBuffer("异常: " + status + "\r\n", CharsetUtil.UTF_8)); +response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8"); +``` + +重定向同样需要构建一个DefaultFullHttpResponse,其状态是302 Found,并且在响应头中设置location为要跳转的URL地址即可: + +```java +FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, FOUND, Unpooled.EMPTY_BUFFER); +response.headers().set(HttpHeaderNames.LOCATION, newUri); +``` + +Keep-Alive是HTTP中为了避免每次请求都建立连接而做的一个优化方式。在HTTP/1.0中默认是的keep-alive是false,在HTTP/1.1中默认的keep-alive是true。如果在header中手动设置了connection:false,则server端请求返回也需要同样设置connection:false。 + +另外,因为HTTP/1.1中默认的keep-alive是true,如果通过HttpUtil.isKeepAlive判断通过之后,还需要判断是否是HTTP/1.0,并显示设置keep-alive为true。 + +```java +final boolean keepAlive = HttpUtil.isKeepAlive(request); +HttpUtil.setContentLength(response, response.content().readableBytes()); +if (!keepAlive) { + response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE); +} else if (request.protocolVersion().equals(HTTP_1_0)) { + response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); +} +``` + +# 文件内容展示处理 + +文件内容展示处理是http服务器的核心,也是比较难以理解的地方。 + +首先要设置的是ContentLength,也就是响应的文件长度,这个可以使用file的length方法来获取: + +```java +RandomAccessFile raf; +raf = new RandomAccessFile(file, "r"); +long fileLength = raf.length(); +HttpUtil.setContentLength(response, fileLength); +``` + +然后我们需要根据文件的扩展名设置对应的CONTENT_TYPE,这个在第一小节已经介绍过了。 + +然后再设置date和缓存属性。这样我们就得到了一个只包含响应头的DefaultHttpResponse,我们先把这个只包含响应头的respose写到ctx中。 + +写完HTTP头,接下来就是写HTTP的Content了。 + +对于HTTP传递的文件来说,有两种处理方式,第一种方式情况下如果知道整个响应的content大小,则可以在后台直接进行整个文件的拷贝传输。如果服务器本身支持零拷贝的话,则可以使用DefaultFileRegion的transferTo方法将File或者Channel的文件进行转移。 + +```java +sendFileFuture = ctx.write(new DefaultFileRegion(raf.getChannel(), 0, fileLength), ctx.newProgressivePromise()); +// 结束部分 +lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT); +``` + +如果并不知道整个响应的context大小,则可以将大文件拆分成为一个个的chunk,并且在响应的头中设置transfer-coding为chunked,netty提供了HttpChunkedInput和ChunkedFile,用来将大文件拆分成为一个个的Chunk进行传输。 + +```java +sendFileFuture = ctx.writeAndFlush(new HttpChunkedInput(new ChunkedFile(raf, 0, fileLength, 8192)), + ctx.newProgressivePromise()); +``` + +如果向channel中写入ChunkedFile,则需要添加相应的ChunkedWriteHandler对chunked文件进行处理。 + +```java +pipeline.addLast(new ChunkedWriteHandler()); +``` + +注意,如果是完整文件传输,则需要手动添加last content部分: + +```java +lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT); +``` + +如果是ChunkedFile,last content部分已经包含在了chunkedFile中,不需要再手动添加了。 + +# 文件传输进度 + +ChannelFuture可以添加对应的listner,用来监控文件传输的进度,netty提供了一个ChannelProgressiveFutureListener,用于监控文件的进程,可以重写operationProgressed和operationComplete方法对进度监控进行定制: + +```java +sendFileFuture.addListener(new ChannelProgressiveFutureListener() { + @Override + public void operationProgressed(ChannelProgressiveFuture future, long progress, long total) { + if (total < 0) { + log.info(future.channel() + " 传输进度: " + progress); + } else { + log.info(future.channel() + " 传输进度: " + progress + " / " + total); + } + } + + @Override + public void operationComplete(ChannelProgressiveFuture future) { + log.info(future.channel() + " 传输完毕."); + } +}); +``` + +# 总结 + +我们考虑了一个HTTP文件服务器最基本的一些考虑因素,现在可以使用这个文件服务器来提供服务啦! + +本文的例子可以参考:[learn-netty4](https://github.com/ddean2009/learn-netty4) \ No newline at end of file diff --git a/learn-netty文章/20200617170251803.png b/learn-netty文章/20200617170251803.png new file mode 100644 index 0000000..eebc075 Binary files /dev/null and b/learn-netty文章/20200617170251803.png differ diff --git a/learn-netty文章/21.netty系列之搭建HTTP上传文件服务器.md b/learn-netty文章/21.netty系列之搭建HTTP上传文件服务器.md new file mode 100644 index 0000000..3a7f624 --- /dev/null +++ b/learn-netty文章/21.netty系列之搭建HTTP上传文件服务器.md @@ -0,0 +1,216 @@ +# netty系列之:搭建HTTP上传文件服务器 + +# 简介 + +上一篇的文章中,我们讲到了如何从HTTP服务器中下载文件,和搭建下载文件服务器应该注意的问题,使用的GET方法。本文将会讨论一下常用的向服务器提交数据的POST方法和如何向服务器上传文件。 + +# GET方法上传数据 + +按照HTTP的规范,PUT一般是向服务器上传数据,虽然不提倡,但是也可以使用GET向服务器端上传数据。 + +先看下GET客户端的构建中需要注意的问题。 + +GET请求实际上就是一个URI,URI后面带有请求的参数,netty提供了一个QueryStringEncoder专门用来构建参数内容: + +```java +// HTTP请求 +QueryStringEncoder encoder = new QueryStringEncoder(get); +// 添加请求参数 +encoder.addParam("method", "GET"); +encoder.addParam("name", "flydean"); +encoder.addParam("site", "www.flydean.com"); +URI uriGet = new URI(encoder.toString()); +``` + +有了请求URI,就可以创建HttpRequest了,当然这个HttpRequest中还需要有对应的HTTP head数据: + +```java +HttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, uriGet.toASCIIString()); +HttpHeaders headers = request.headers(); +headers.set(HttpHeaderNames.HOST, host); +headers.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE); +headers.set(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.GZIP + "," + HttpHeaderValues.DEFLATE); +headers.set(HttpHeaderNames.ACCEPT_LANGUAGE, "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2"); +headers.set(HttpHeaderNames.REFERER, uriSimple.toString()); +headers.set(HttpHeaderNames.USER_AGENT, "Netty Simple Http Client side"); +headers.set(HttpHeaderNames.ACCEPT, "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"); + +headers.set( + HttpHeaderNames.COOKIE, ClientCookieEncoder.STRICT.encode( + new DefaultCookie("name", "flydean"), + new DefaultCookie("site", "www.flydean.com")) +); +``` + +我们知道HttpRequest中只有两部分数据,分别是HttpVersion和HttpHeaders。HttpVersion就是HTTP协议的版本号,HttpHeaders就是设置的header内容。 + +对于GET请求来说,因为所有的内容都包含在URI中,所以不需要额外的HTTPContent,直接发送HttpRequest到服务器就可以了。 + +```java +channel.writeAndFlush(request); +``` + +然后看下服务器端接收GET请求之后怎么进行处理。 + +服务器端收到HttpObject对象的msg之后,需要将其转换成HttpRequest对象,就可以通过protocolVersion(),uri()和headers()拿到相应的信息。 + +对于URI中的参数,netty提供了QueryStringDecoder类可以方便的对URI中参数进行解析: + +```java +//解析URL中的参数 +QueryStringDecoder decoderQuery = new QueryStringDecoder(request.uri()); +Map> uriAttributes = decoderQuery.parameters(); +for (Entry> attr: uriAttributes.entrySet()) { + for (String attrVal: attr.getValue()) { + responseContent.append("URI: ").append(attr.getKey()).append('=').append(attrVal).append("\r\n"); + } +} +``` + +# POST方法上传数据 + +对于POST请求,它比GET请求多了一个HTTPContent,也就是说除了基本的HttpRequest数据之外,还需要一个PostBody。 + +如果只是一个普通的POST,也就是POST内容都是key=value的形式,则比较简单,如果POST中包含有文件,那么会比较复杂,需要用到ENCTYPE=”multipart/form-data”。 + +netty提供了一个HttpPostRequestEncoder类,用于快速对request body进行编码,先看下HttpPostRequestEncoder类的完整构造函数: + +```java +public HttpPostRequestEncoder( + HttpDataFactory factory, HttpRequest request, boolean multipart, Charset charset, + EncoderMode encoderMode) +``` + +其中request就是要编码的HttpRequest,multipart表示是否是”multipart/form-data”的格式,charset编码方式,默认情况下是CharsetUtil.UTF_8。encoderMode是编码的模式,目前有三种编码模式,分别是RFC1738,RFC3986和HTML5。 + +默认情况下的编码模式是RFC1738,这也是大多数form提交数据的编码方式。但是它并不适用于OAUTH,如果要使用OAUTH的话,则可以使用RFC3986。HTML5禁用了multipart/form-data的混合模式。 + +最后,我们讲讲HttpDataFactory。factory主要用来创建InterfaceHttpData。它有一个minSize参数,如果创建的HttpData大小大于minSize则会存放在磁盘中,否则直接在内存中创建。 + +InterfaceHttpData有三种HttpData的类型,分别是Attribute, FileUpload和InternalAttribute。 + +Attribute就是POST请求中传入的属性值。FileUpload就是POST请求中传入的文件,还有InternalAttribute是在encoder内部使用的,这里不过多讨论。 + +因此,根据传入的minSize参数大小,Attribute和FileUpload可以被分成下面几种: + +MemoryAttribute, DiskAttribute or MixedAttribute +MemoryFileUpload, DiskFileUpload or MixedFileUpload + +在这一节我们先看一下在POST请求中并不上传文件的处理方式,首先创建HTTP request和PostBody encoder: + +```java +// 构建HTTP request +HttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, uriSimple.toASCIIString()); + +HttpPostRequestEncoder bodyRequestEncoder = + new HttpPostRequestEncoder(factory, request, false); +``` + +向request中添加headers: + +```java +// 添加headers +for (Entry entry : headers) { + request.headers().set(entry.getKey(), entry.getValue()); +} +``` + +然后再向bodyRequestEncoder中添加form属性: + +```java +// 添加form属性 +bodyRequestEncoder.addBodyAttribute("method", "POST"); +bodyRequestEncoder.addBodyAttribute("name", "flydean"); +bodyRequestEncoder.addBodyAttribute("site", "www.flydean.com"); +bodyRequestEncoder.addBodyFileUpload("myfile", file, "application/x-zip-compressed", false); +``` + +注意,上面我们向bodyRequestEncoder中添加了method,name和site这几个属性。然后添加了一个FileUpload。但是因为我们的编码方式并不是”multipart/form-data”,所以这里传递的只是文件名,并不是整个文件。 + +最后,我们要调用bodyRequestEncoder的finalizeRequest方法,返回最终要发送的request。在finalizeRequest的过程中,还会根据传输数据的大小来设置transfer-encoding是否为chunked。 + +如果传输的内容比较大,则需要分段进行传输,这时候需要设置transfer-encoding = chunked,否则不进行设置。 + +最后发送请求: + +```java +// 发送请求 +channel.write(request); +``` + +在server端,我们同样需要构造一个HttpDataFactory,然后使用这个factory来构造一个HttpPostRequestDecoder,来对encoder出来的数据进行decode: + +```java +HttpDataFactory factory = + new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE); +//POST请求 +decoder = new HttpPostRequestDecoder(factory, request); +``` + +因为server端收到的消息根据发送消息的长度可以能是HttpContent,也可能是LastHttpContent。如果是HttpContent,我们将解析的结果放到一个StringBuilder中缓存起来,等接收到LastHttpContent再一起发送出去即可。 + +在收到HttpContent之后,我们调用decoder.offer方法,对HttpContent进行解码: + +```java +decoder.offer(chunk); +``` + +在decoder内部有两个存储HttpData数据的容器,分别是: + +```java +List bodyListHttpData; +// 和 +Map> bodyMapHttpData; +``` + +decoder.offer就是对chunk进行解析,然后将解析过后的数据填充到bodyListHttpData和bodyMapHttpData中。 + +解析过后,就可以对解析过后的数据进行读取了。 + +可以通过decoder的hasNext和next方法对bodyListHttpData进行遍历,从而获取到对应的InterfaceHttpData。 + +通过data.getHttpDataType()可以拿到InterfaceHttpData的数据类型,上面也讲过了有Attribute和FileUpload两种类型。 + +# POST方法上传文件 + +如果要POST文件,客户端在创建HttpPostRequestEncoder的时候传入multipart=true即可: + +```java +HttpPostRequestEncoder bodyRequestEncoder = + new HttpPostRequestEncoder(factory, request, true); +``` + +然后分别调用setBodyHttpDatas和finalizeRequest方法,生成HttpRequest就可以向channel写入了: + +```java +// 添加body http data +bodyRequestEncoder.setBodyHttpDatas(bodylist); +// finalize request,判断是否需要chunk +request = bodyRequestEncoder.finalizeRequest(); +// 发送请求头 +channel.write(request); +``` + +要注意,如果是transfer-encoding = chunked,那么这个HttpRequest只是请求头的信息,我们还需要手动将HttpContent写入到channel中: + +```java +// 判断bodyRequestEncoder是否是Chunked,发送请求内容 +if (bodyRequestEncoder.isChunked()) { + channel.write(bodyRequestEncoder); +} +``` + +在server端,通过判断InterfaceHttpData的getHttpDataType,如果是FileUpload类型,则说明拿到了上传的文件,则可以通过下面的方法来读取到文件的内容: + +```java +FileUpload fileUpload = (FileUpload) data; +responseContent.append(fileUpload.getString(fileUpload.getCharset())); +``` + +这样我们就可以在服务器端拿到客户端传过来的文件了。 + +# 总结 + +HTTP的文件上传需要考虑的问题比较多,大家有不明白的可以参考我的例子。或者留言给我一起讨论。 + +本文的例子可以参考:[learn-netty4](https://github.com/ddean2009/learn-netty4) \ No newline at end of file diff --git a/learn-netty文章/22.netty系列之在netty中处理CORS.md b/learn-netty文章/22.netty系列之在netty中处理CORS.md new file mode 100644 index 0000000..cff188c --- /dev/null +++ b/learn-netty文章/22.netty系列之在netty中处理CORS.md @@ -0,0 +1,144 @@ +# netty系列之:在netty中处理CORS + +# 简介 + +CORS的全称是跨域资源共享,他是一个基于HTTP-header检测的机制,通过对HTTP-header进行控制,可以实现对跨域资源的权限管理功能。在之前的CORS详解文章中,我们已经对CORS有了基本的解释。 + +本文将会从netty的实现角度,讲解如何在netty中实现CORS。 + +# 服务端的CORS配置 + +熟悉CORS的朋友应该知道,CORS所有的操作都是在HTTP协议之上通过控制HTTP头来实现的。所以说如果要在服务器端实现CORS的支持,事实上也是对HTTP协议的头进行各种设置完成的。 + +为了方便大家的使用,netty提供了一个CorsConfig类,来统一CORS的头设置。 + +先看下CorsConfig类中定义的属性: + +```java +private final Set origins; +private final boolean anyOrigin; +private final boolean enabled; +private final Set exposeHeaders; +private final boolean allowCredentials; +private final long maxAge; +private final Set allowedRequestMethods; +private final Set allowedRequestHeaders; +private final boolean allowNullOrigin; +private final Map> preflightHeaders; +private final boolean shortCircuit; +``` + +这些属性和CORS的HTTP头设置是一一对应的。比如说origins表示的是允许的源,anyOrigin表示允许所有的源。 + +是和下面的设置对应的: + +```java +Origin: +``` + +exposeHeaders是和Access-Control-Expose-Headers一一对应的,表示服务器端允许客户端获取CORS资源的同时能够访问到的header信息。其格式如下: + +```java +Access-Control-Expose-Headers: [, ]* +``` + +allowCredentials表示是否开启CORS的权限认证。表示服务器端是否接受客户端带有credentials字段的请求。如果用在preflight请求中,则表示后续的真实请求是否支持credentials,其格式如下: + +```java +Access-Control-Allow-Credentials: true +``` + +allowedRequestMethods表示访问资源允许的方法,主要用在preflight request中。其格式如下: + +```java +Access-Control-Allow-Methods: [, ]* +``` + +allowedRequestHeaders用在preflight request中,表示真正能够被用来做请求的header字段,其格式如下: + +```java +Access-Control-Allow-Headers: [, ]* +``` + +当客户端发送OPTIONS方法给服务器的时候,为了安全起见,因为服务器并不一定能够接受这些OPTIONS的方法,所以客户端需要首先发送一个 +preflighted requests,等待服务器响应,等服务器确认之后,再发送真实的请求。我们举一个例子。preflightHeaders表示的就是服务器允许额preflight的请求头。 + +shortCircuit表示请求是否是一个有效的CORS请求,如果请求被拒绝之后,就会返回一个true。 + +# CorsConfigBuilder + +CorsConfig使用来表示Cors的配置类,那么怎么去构造这个配置类呢?我们看下CorsConfig的构造函数: + +```java +CorsConfig(final CorsConfigBuilder builder) { + origins = new LinkedHashSet(builder.origins); + anyOrigin = builder.anyOrigin; + enabled = builder.enabled; + exposeHeaders = builder.exposeHeaders; + allowCredentials = builder.allowCredentials; + maxAge = builder.maxAge; + allowedRequestMethods = builder.requestMethods; + allowedRequestHeaders = builder.requestHeaders; + allowNullOrigin = builder.allowNullOrigin; + preflightHeaders = builder.preflightHeaders; + shortCircuit = builder.shortCircuit; +} +``` + +可以看到CorsConfig是通过CorsConfigBuilder来构造的。通过设置CorsConfigBuilder中的各种属性即可。CorsConfigBuilder中提供了多种设置属性的方法。 + +![img](5c1ced36f1d84bdda67329044f47c767.png) + +可以使用这样的方法来构造CorsConfig如下: + +```java +CorsConfig corsConfig = CorsConfigBuilder.forAnyOrigin().allowNullOrigin().allowCredentials().build(); +``` + +# CorsHandler + +有了corsConfig,我们还需要将这个config配置在netty的handler中,netty提供了一个CorsHandler类来专门处理corsConfig,这个类就叫CorsHandler。 + +首先看下CorsHandler的构造函数: + +```java +public CorsHandler(final CorsConfig config) { + this(Collections.singletonList(checkNotNull(config, "config")), config.isShortCircuit()); +} + +public CorsHandler(final List configList, boolean isShortCircuit) { + checkNonEmpty(configList, "configList"); + this.configList = configList; + this.isShortCircuit = isShortCircuit; +} +``` + +CorsHandler有两个构造函数,一个是传入CorsConfig,一个是传入一个CorsConfig的列表。 + +CorsHandler的主要工作原理就是在channelRead的时候,对responseHeader进行处理,设置CORS头。 + +# netty对cors的支持 + +上面我们已经讲过了netty中cors的核心类和方法,最后一步就是把cors的支持类加入到netty的pipeline中,其核心代码如下: + +```java +public void initChannel(SocketChannel ch) { + + ChannelPipeline pipeline = ch.pipeline(); + pipeline.addLast(new HttpResponseEncoder()); + pipeline.addLast(new HttpRequestDecoder()); + pipeline.addLast(new HttpObjectAggregator(65536)); + pipeline.addLast(new ChunkedWriteHandler()); + + CorsConfig corsConfig = CorsConfigBuilder.forAnyOrigin().allowNullOrigin().allowCredentials().build(); + pipeline.addLast(new CorsHandler(corsConfig)); + + pipeline.addLast(new CustResponseHandler()); +} +``` + +# 总结 + +cors比较简单,netty也为其提供了住够的方法支持。大家可以直接使用。 + +本文的例子可以参考:[learn-netty4](https://github.com/ddean2009/learn-netty4) \ No newline at end of file diff --git a/learn-netty文章/23.netty系列之使用netty搭建websocket服务器.md b/learn-netty文章/23.netty系列之使用netty搭建websocket服务器.md new file mode 100644 index 0000000..1315390 --- /dev/null +++ b/learn-netty文章/23.netty系列之使用netty搭建websocket服务器.md @@ -0,0 +1,239 @@ +# netty系列之:使用netty搭建websocket服务器 + +# 简介 + +websocket是一个优秀的协议,它是建立在TCP基础之上的,兼容HTTP的网络协议。通过Websocket我们可以实现客户端和服务器端的即时通讯,免除了客户端多次轮循带来的性能损耗。 + +既然websocket这么优秀,那么怎么在netty中使用websocket呢? + +# netty中的websocket + +虽然websocket是一个单独的和HTTP协议完全不同的协议,但是在netty中还是将其放到了http包中。我们回想一下netty中对于各种协议的支持。如果要支持这种协议,肯定需要一个decoder和encoder编码和解码器用于对协议进行编解码。将传输的数据从ByteBuf转换到协议类型,或者将协议类型转换成为ByteBuf。 + +这是netty的工作核心原理,也是后续自定义netty扩展的基础。 + +那么对于websocket来说,是怎么样的呢? + +## websocket的版本 + +WebSocket作为一种协议,自然不是凭空而来的,通过不断的发展才到了今天的WebSocket协议。具体的webSocket的发展史我们就不去深究了。我们先看下netty提供的各种WebSocket的版本。 + +在WebSocketVersion类中,我们可以看到: + +```java +UNKNOWN(AsciiString.cached(StringUtil.EMPTY_STRING)), +V00(AsciiString.cached("0")), +V07(AsciiString.cached("7")), +V08(AsciiString.cached("8")), +V13(AsciiString.cached("13")); +``` + +WebSocketVersion是一个枚举类型,它里面定义了websocket的4个版本,除了UNKNOWN之外,我们可以看到websocket的版本有0,7,8,13这几个。 + +## FrameDecoder和FrameEncoder + +我们知道websocket的消息是通过frame来传递的,因为不同websocket的版本影响到的是frame的格式的不同。所以我们需要不同的FrameDecoder和FrameEncoder来在WebSocketFrame和ByteBuf之间进行转换。 + +既然websocket有四个版本,那么相对应的就有4个版本的decoder和encoder: + +```java +WebSocket00FrameDecoder +WebSocket00FrameEncoder +WebSocket07FrameDecoder +WebSocket07FrameEncoder +WebSocket08FrameDecoder +WebSocket08FrameEncoder +WebSocket13FrameDecoder +WebSocket13FrameEncoder +``` + +至于每个版本之间的frame有什么区别,我们这里就不细讲了,感兴趣的朋友可以关注我的后续文章。 + +熟悉netty的朋友应该都知道,不管是encoder还是decoder都是作用在channel中对消息进行转换的。那么在netty中对websocket的支持是怎么样的呢? + +## WebSocketServerHandshaker + +netty提供了一个WebSocketServerHandshaker类来统一使用encoder和decoder的使用。netty提供一个工厂类WebSocketServerHandshakerFactory根据客户端请求header的websocket版本不同,来返回不同的WebSocketServerHandshaker。 + +```java +public WebSocketServerHandshaker newHandshaker(HttpRequest req) { + + CharSequence version = req.headers().get(HttpHeaderNames.SEC_WEBSOCKET_VERSION); + if (version != null) { + if (version.equals(WebSocketVersion.V13.toHttpHeaderValue())) { + // Version 13 of the wire protocol - RFC 6455 (version 17 of the draft hybi specification). + return new WebSocketServerHandshaker13( + webSocketURL, subprotocols, decoderConfig); + } else if (version.equals(WebSocketVersion.V08.toHttpHeaderValue())) { + // Version 8 of the wire protocol - version 10 of the draft hybi specification. + return new WebSocketServerHandshaker08( + webSocketURL, subprotocols, decoderConfig); + } else if (version.equals(WebSocketVersion.V07.toHttpHeaderValue())) { + // Version 8 of the wire protocol - version 07 of the draft hybi specification. + return new WebSocketServerHandshaker07( + webSocketURL, subprotocols, decoderConfig); + } else { + return null; + } + } else { + // Assume version 00 where version header was not specified + return new WebSocketServerHandshaker00(webSocketURL, subprotocols, decoderConfig); + } +} +``` + +同样的, 我们可以看到,netty为websocket也定义了4种不同的WebSocketServerHandshaker。 + +WebSocketServerHandshaker中定义了handleshake方法,通过传入channel,并向其添加encoder和decoder + +```java +public final ChannelFuture handshake(Channel channel, FullHttpRequest req, + HttpHeaders responseHeaders, final ChannelPromise promise) + +p.addBefore(ctx.name(), "wsencoder", newWebSocketEncoder()); +p.addBefore(ctx.name(), "wsdecoder", newWebsocketDecoder()); +``` + +而添加的这两个newWebSocketEncoder和newWebsocketDecoder就是各个WebSocketServerHandshaker的具体实现中定义的。 + +## WebSocketFrame + +所有的ecode和decode都是在WebSocketFrame和ByteBuf中进行转换。WebSocketFrame继承自DefaultByteBufHolder,表示它是一个ByteBuf的容器。除了保存有ByteBuf之外,它还有两个额外的属性,分别是finalFragment和rsv。 + +finalFragment表示该frame是不是最后一个Frame。对于大数据量的消息来说,会将消息拆分成为不同的frame,这个属性特别有用。 + +我们再看一下websocket协议消息的格式: + +``` + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-------+-+-------------+-------------------------------+ + |F|R|R|R| opcode|M| Payload len | Extended payload length | + |I|S|S|S| (4) |A| (7) | (16/64) | + |N|V|V|V| |S| | (if payload len==126/127) | + | |1|2|3| |K| | | + +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + + | Extended payload length continued, if payload len == 127 | + + - - - - - - - - - - - - - - - +-------------------------------+ + | |Masking-key, if MASK set to 1 | + +-------------------------------+-------------------------------+ + | Masking-key (continued) | Payload Data | + +-------------------------------- - - - - - - - - - - - - - - - + + : Payload Data continued ... : + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + | Payload Data continued ... | + +---------------------------------------------------------------+ +``` + +rsv代表的是消息中的扩展字段,也就是RSV1,RSV2和RSV3。 + +除此之外就是ByteBuf的一些基本操作了。 + +WebSocketFrame是一个抽象类,它的具体实现类有下面几种: + +``` +BinaryWebSocketFrame +CloseWebSocketFrame +ContinuationWebSocketFrame +PingWebSocketFrame +PongWebSocketFrame +TextWebSocketFrame +``` + +BinaryWebSocketFrame和TextWebSocketFrame很好理解,他们代表消息传输的两种方式。 + +CloseWebSocketFrame是代表关闭连接的frame。ContinuationWebSocketFrame表示消息中多于一个frame的表示。 + +而PingWebSocketFrame和PongWebSocketFrame是两个特殊的frame,他们主要用来做服务器和客户端的探测。 + +这些frame都是跟Websocket的消息类型一一对应的,理解了websocket的消息类型,对应理解这些frame类还是很有帮助的。 + +# netty中使用websocket + +讲了这么多websocket的原理和实现类,接下来就是实战了。 + +在这个例子中,我们使用netty创建一个websocket server,然后使用浏览器客户端来对server进行访问。 + +创建websocket server和普通netty服务器的过程没有什么两样。只是在ChannelPipeline中,需要加入自定义的WebSocketServerHandler: + +```java +pipeline.addLast(new WebSocketServerHandler()); +``` + +这个WebSocketServerHandler需要做什么事情呢? + +它需要同时处理普通的HTTP请求和webSocket请求。 + +这两种请求可以通过接收到的msg类型的不同来进行判断: + +```java +public void channelRead0(ChannelHandlerContext ctx, Object msg) throws IOException { + //根据消息类型,处理两种不同的消息 + if (msg instanceof FullHttpRequest) { + handleHttpRequest(ctx, (FullHttpRequest) msg); + } else if (msg instanceof WebSocketFrame) { + handleWebSocketFrame(ctx, (WebSocketFrame) msg); + } +} +``` + +在客户端进行websocket连接之前,需要借用当前的channel通道,开启handleshake: + +```java +// websocket握手 +WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory( + getWebSocketLocation(req), null, true, 5 * 1024 * 1024); +handshaker = wsFactory.newHandshaker(req); +if (handshaker == null) { + WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel()); +} else { + handshaker.handshake(ctx.channel(), req); +} +``` + +我们得到handshaker之后,就可以对后续的WebSocketFrame进行处理: + +```java +private void handleWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) { + + // 处理各种websocket的frame信息 + if (frame instanceof CloseWebSocketFrame) { + handshaker.close(ctx, (CloseWebSocketFrame) frame.retain()); + return; + } + if (frame instanceof PingWebSocketFrame) { + ctx.write(new PongWebSocketFrame(frame.content().retain())); + return; + } + if (frame instanceof TextWebSocketFrame) { + // 直接返回 + ctx.write(frame.retain()); + return; + } + if (frame instanceof BinaryWebSocketFrame) { + // 直接返回 + ctx.write(frame.retain()); + } +} +``` + +这里我们只是机械的返回消息,大家可以根据自己业务逻辑的不同,对消息进行解析。 + +有了服务器端,客户端该怎么连接呢?很简单首选构造WebSocket对象,然后处理各种回调即可: + +```java +socket = new WebSocket("ws://127.0.0.1:8000/websocket"); +socket.onmessage = function (event) { + +} +socket.onopen = function(event) { +}; +socket.onclose = function(event) { +}; +``` + +# 总结 + +以上就是使用netty搭建websocket服务器的完整流程,本文中的服务器可以同时处理普通HTTP请求和webSocket请求,但是稍显复杂,有没有更加简单的方式呢?敬请期待。 + +本文的例子可以参考:[learn-netty4](https://github.com/ddean2009/learn-netty4) \ No newline at end of file diff --git a/learn-netty文章/24.netty系列之分离websocket处理器.md b/learn-netty文章/24.netty系列之分离websocket处理器.md new file mode 100644 index 0000000..50f6b3b --- /dev/null +++ b/learn-netty文章/24.netty系列之分离websocket处理器.md @@ -0,0 +1,127 @@ +# netty系列之:分离websocket处理器 + +# 简介 + +在上一篇文章中,我们使用了netty构建了可以处理websocket协议的服务器,在这个服务器中,我们构建了特制的handler用来处理HTTP或者websocket请求。 + +在一个handler中处理两种不同的请求,对于某些有代码洁癖的人可能忍受不了。那么,有没有可能将普通的HTTP请求和websocket请求使用不同的handler来进行处理呢?答案是肯定的。 + +# netty的消息处理 + +我们知道netty中所有的消息处理都是通过handler来实现的,为了方便起见,netty提供了一个简单的消息处理类SimpleChannelInboundHandler,大家通过继承它来重写channelRead0方法即可: + +```java +protected abstract void channelRead0(ChannelHandlerContext ctx, I msg) throws Exception; +``` + +我们再看一下SimpleChannelInboundHandler的定义: + +```java +public abstract class SimpleChannelInboundHandler extends ChannelInboundHandlerAdapter +``` + +可以看到SimpleChannelInboundHandler本身是带有泛型I的,而这个I就是我们要探讨的方向。 + +如果我们要使用这个handler来处理所有的消息,那么可以将I取值为Object。 + +如果我们只需要处理String消息,那么可以这样: + +```java +public class StringHandler extends + SimpleChannelInboundHandler { + + @Override + protected void channelRead0(ChannelHandlerContext ctx, String message) + throws Exception { + System.out.println(message); + } +} +``` + +同样的,如果要同时处理HTTP和WebSocket消息,只需要将I设置为不同的类型即可。 + +对于WebSocketFrame,我们有: + +```java +public class Server2FrameHandler extends SimpleChannelInboundHandler +``` + +对于FullHttpRequest,我们有: + +```java +public class Server2HttpHandler extends SimpleChannelInboundHandler +``` + +## 处理WebSocketFrame + +对于WebSocketFrame消息,从上一节我们知道它有6种类型,分别是: + +``` +BinaryWebSocketFrame +CloseWebSocketFrame +ContinuationWebSocketFrame +PingWebSocketFrame +PongWebSocketFrame +TextWebSocketFrame +``` + +其中真正包含内容的是TextWebSocketFrame和BinaryWebSocketFrame,这里我们对TextWebSocketFrame进行专门处理: + +```java +protected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame frame) throws Exception { + + if (frame instanceof TextWebSocketFrame) { + // 将接收到的消息转换成为大写 + String request = ((TextWebSocketFrame) frame).text(); + ctx.channel().writeAndFlush(new TextWebSocketFrame(request.toUpperCase(Locale.CHINA))); + } else { + String message = "不支持的Frame类型: " + frame.getClass().getName(); + throw new UnsupportedOperationException(message); + } +} +``` + +## 处理HTTP + +对于HTTP请求中的FullHttpRequest,我们就安装正常的HTTP服务请求的处理流程来就行。 + +这里不做过多阐述。 + +## 编码和解码器 + +等等,我们是不是忘记了什么东西?对,那就是编码和解码器。 + +在上一节中,我们使用的是WebSocketServerHandshaker来对websocket消息进行编码和解码。不过其实是放在我们自定义的hadler代码里面的,使用起来略显不优雅。 + +没关系,netty为我们提供了一个WebSocketServerProtocolHandler类,专门负责websocket的编码和解码问题。 + +除了处理正常的websocket握手之外,WebSocketServerProtocolHandler类还为我们处理了Close, Ping, Pong这几种通用的消息类型。而我们只需要专注于真正的业务逻辑消息即可,十分的方便。 + +对于剩下的Text或者Binary frame数据,会被交由pipline中的下一个handler进行处理。 + +其中Handshake有两个状态,分别是: + +HANDSHAKE_COMPLETE 和 HANDSHAKE_TIMEOUT。 + +而HandshakeComplete又包含了requestUri,requestHeaders和selectedSubprotocol这三个方面的信息。 + +最后,将WebSocketServerProtocolHandler加入到pipeline中,最终得到: + +```java +public void initChannel(SocketChannel ch) throws Exception { + ChannelPipeline pipeline = ch.pipeline(); + + pipeline.addLast(new HttpServerCodec()); + pipeline.addLast(new HttpObjectAggregator(65536)); + pipeline.addLast(new WebSocketServerCompressionHandler()); + pipeline.addLast(new WebSocketServerProtocolHandler(WEBSOCKET_PATH, null, true)); + pipeline.addLast(new Server2HttpHandler()); + pipeline.addLast(new Server2FrameHandler()); +} +``` + +# 总结 + +一个分离了HTTP请求和webSocket请求的服务器就完成了。简单直观才是一个程序员追求的世界! + +本文的例子可以参考:[learn-netty4](https://github.com/ddean2009/learn-netty4) \ No newline at end of file diff --git a/learn-netty文章/25.netty系列之使用netty搭建websocket客户端.md b/learn-netty文章/25.netty系列之使用netty搭建websocket客户端.md new file mode 100644 index 0000000..a2e5686 --- /dev/null +++ b/learn-netty文章/25.netty系列之使用netty搭建websocket客户端.md @@ -0,0 +1,160 @@ +# netty系列之:使用netty搭建websocket客户端 + +# 简介 + +在网速快速提升的时代,浏览器已经成为我们访问各种服务的入口,很难想象如果离开了浏览器,我们的网络世界应该如何运作。现在恨不得把操作系统都搬上浏览器。但是并不是所有的应用都需要浏览器来执行,比如服务器和服务器之间的通信,就需要使用到自建客户端来和服务器进行交互。 + +本文将会介绍使用netty客户端连接websocket的原理和具体实现。 + +# 浏览器客户端 + +在介绍netty客户端之前,我们先看一个简单的浏览器客户端连接websocket的例子: + +```java +// 创建连接 +const socket = new WebSocket('ws://localhost:8000'); + +// 开启连接 +socket.addEventListener('open', function (event) { + socket.send('没错,开启了!'); +}); + +// 监听消息 +socket.addEventListener('message', function (event) { + console.log('监听到服务器的消息 ', event.data); +}); +``` + +这里使用了浏览器最通用的语言javascript,并使用了浏览器提供的websocket API进行操作,非常的简单。 + +那么用netty客户端实现websocket的连接是否和javascript使用一样呢?我们一起来探索。 + +# netty对websocket客户端的支持 + +先看看netty对websocket的支持类都有哪些,接着我们看下怎么具体去使用这些工具类。 + +## WebSocketClientHandshaker + +和websocket server一样,client中最核心的类也是handshaker,这里叫做WebSocketClientHandshaker。这个类有什么作用呢?一起来看看。 + +这个类主要实现的就是client和server端之间的握手。 + +我们看一下它的最长参数的构造类: + +```java +protected WebSocketClientHandshaker(URI uri, WebSocketVersion version, String subprotocol, + HttpHeaders customHeaders, int maxFramePayloadLength, + long forceCloseTimeoutMillis, boolean absoluteUpgradeUrl) +``` + +参数中有websocket连接的URI,像是:”ws://flydean.com/mypath”。 + +有请求子协议的类型subprotocol,有自定义的HTTP headers:customHeaders,有最大的frame payload的长度:maxFramePayloadLength,有强制timeout关闭的时间,有使用HTTP协议进行升级的URI地址。 + +怎么创建handshaker呢?同样的,netty提供了一个WebSocketClientHandshakerFactory方法。 + +WebSocketClientHandshakerFactory提供了一个newHandshaker方法,可以方便的创建各种不同版本的handshaker: + +```java +if (version == V13) { + return new WebSocketClientHandshaker13( + webSocketURL, V13, subprotocol, allowExtensions, customHeaders, + maxFramePayloadLength, performMasking, allowMaskMismatch, forceCloseTimeoutMillis); +} +if (version == V08) { + return new WebSocketClientHandshaker08( + webSocketURL, V08, subprotocol, allowExtensions, customHeaders, + maxFramePayloadLength, performMasking, allowMaskMismatch, forceCloseTimeoutMillis); +} +if (version == V07) { + return new WebSocketClientHandshaker07( + webSocketURL, V07, subprotocol, allowExtensions, customHeaders, + maxFramePayloadLength, performMasking, allowMaskMismatch, forceCloseTimeoutMillis); +} +if (version == V00) { + return new WebSocketClientHandshaker00( + webSocketURL, V00, subprotocol, customHeaders, maxFramePayloadLength, forceCloseTimeoutMillis); +} +``` + +可以看到,根据传入协议版本的不同,可以分为WebSocketClientHandshaker13、WebSocketClientHandshaker08、WebSocketClientHandshaker07、WebSocketClientHandshaker00这几种。 + +## WebSocketClientCompressionHandler + +通常来说,对于webSocket协议,为了提升传输的性能和速度,降低网络带宽占用量,在使用过程中通常会带上额外的压缩扩展。为了处理这样的压缩扩展,netty同时提供了服务器端和客户端的支持。 + +对于服务器端来说对应的handler叫做WebSocketServerCompressionHandler,对于客户端来说对应的handler叫做WebSocketClientCompressionHandler。 + +通过将这两个handler加入对应pipline中,可以实现对websocket中压缩协议扩展的支持。 + +对于协议的扩展有两个级别分别是permessage-deflate和perframe-deflate,分别对应PerMessageDeflateClientExtensionHandshaker和DeflateFrameClientExtensionHandshaker。 + +至于具体怎么压缩的,这里就不详细进行讲解了, 感兴趣的小伙伴可以自行了解。 + +# netty客户端的处理流程 + +前面讲解了netty对websocket客户端的支持之后,本节将会讲解netty到底是如何使用这些工具进行消息处理的。 + +首先是按照正常的逻辑创建客户端的Bootstrap,并添加handler。这里的handler就是专门为websocket定制的client端handler。 + +除了上面提到的WebSocketClientCompressionHandler,就是自定义的handler了。 + +在自定义handler中,我们需要处理两件事情,一件事情就是在channel ready的时候创建handshaker。另外一件事情就是具体websocket消息的处理了。 + +## 创建handshaker + +首先使用WebSocketClientHandshakerFactory创建handler: + +```java +TestSocketClientHandler handler = new TestSocketClientHandler( + WebSocketClientHandshakerFactory.newHandshaker(uri, WebSocketVersion.V13, null, true, new DefaultHttpHeaders())); +``` + +然后在channel active的时候使用handshaker进行握手连接: + +```java +public void channelActive(ChannelHandlerContext ctx) { + handshaker.handshake(ctx.channel()); +} +``` + +然后在进行消息接收处理的时候还需要判断handshaker的状态是否完成,如果未完成则调用handshaker.finishHandshake方法进行手动完成: + +```java +if (!handshaker.isHandshakeComplete()) { + try { + handshaker.finishHandshake(ch, (FullHttpResponse) msg); + log.info("websocket Handshake 完成!"); + handshakeFuture.setSuccess(); + } catch (WebSocketHandshakeException e) { + log.info("websocket连接失败!"); + handshakeFuture.setFailure(e); + } + return; +} +``` + +当handshake完成之后,就可以进行正常的websocket消息读写操作了。 + +## websocket消息的处理 + +websocket的消息处理比较简单,将接收到的消息转换成为WebSocketFrame进行处理即可。 + +```java +WebSocketFrame frame = (WebSocketFrame) msg; +if (frame instanceof TextWebSocketFrame) { + TextWebSocketFrame textFrame = (TextWebSocketFrame) frame; + log.info("接收到TXT消息: " + textFrame.text()); +} else if (frame instanceof PongWebSocketFrame) { + log.info("接收到pong消息"); +} else if (frame instanceof CloseWebSocketFrame) { + log.info("接收到closing消息"); + ch.close(); +} +``` + +# 总结 + +本文讲解了netty提供的websocket客户端的支持和具体的对接流程,大家可以再次基础上进行扩展,以实现自己的业务逻辑。 + +本文的例子可以参考:[learn-netty4](https://github.com/ddean2009/learn-netty4) \ No newline at end of file diff --git a/learn-netty文章/26.netty系列之让TLS支持http2.md b/learn-netty文章/26.netty系列之让TLS支持http2.md new file mode 100644 index 0000000..890141c --- /dev/null +++ b/learn-netty文章/26.netty系列之让TLS支持http2.md @@ -0,0 +1,211 @@ +# netty系列之:让TLS支持http2 + +# 简介 + +我们知道虽然HTTP2协议并不强制使用HTTPS,但是对大多数浏览器来说,如果要使用HTTP2的话,则必须使用HTTPS,所以我们需要了解如何在netty的TLS中支持http2。 + +# TLS的扩展协议NPN和ALPN + +HTTP2协议是从spdy协议发展而来的,无论是spdy还是http2都为了能在HTTPS的环境下工作,发展出来了TLS协议的扩展。 + +他们分别叫做NPN(Next Protocol Negotiation) 和 ALPN (Application Layer Protocol Negotiation) 。 + +他们规定了在TLS协议握手之后,客户端和服务器端进行应用数据通信的协议。其中ALPN可以在客户端首次和服务器端进行握手的时候,就列出客户端支持的应用层数据协议,服务器端直接选择即可,因此可以比NPN少一个交互流程,更加优秀。 + +那么spdy和http2分别支持的协议都有哪些呢? + +netty提供了一个ApplicationProtocolNames类,在其中定义了各自对应的协议,其中ALPN对应了http2和http1.1,而sydy对应了spdy/1,spdy/2,spdy/3: + +```java +/** + * HTTP version 2 + */ +public static final String HTTP_2 = "h2"; + +/** + * {@code "http/1.1"}: HTTP version 1.1 + */ +public static final String HTTP_1_1 = "http/1.1"; + +/** + * {@code "spdy/3.1"}: SPDY version 3.1 + */ +public static final String SPDY_3_1 = "spdy/3.1"; + +/** + * {@code "spdy/3"}: SPDY version 3 + */ +public static final String SPDY_3 = "spdy/3"; + +/** + * {@code "spdy/2"}: SPDY version 2 + */ +public static final String SPDY_2 = "spdy/2"; + +/** + * {@code "spdy/1"}: SPDY version 1 + */ +public static final String SPDY_1 = "spdy/1"; +``` + +# SslProvider + +目前来说,netty中有两种SSL的实现方式,一种是JDK,一种是OPENSSL,不同的实现方式对TLS协议扩展的支持也不一样。它提供了一个isAlpnSupported方法,根据传入provider的不同来判断,是否支持ALPN。 + +```java +public static boolean isAlpnSupported(final SslProvider provider) { + switch (provider) { + case JDK: + return JdkAlpnApplicationProtocolNegotiator.isAlpnSupported(); + case OPENSSL: + case OPENSSL_REFCNT: + return OpenSsl.isAlpnSupported(); + default: + throw new Error("Unknown SslProvider: " + provider); + } +} +``` + +如果你使用的是JDK8,那么运行之后,可能会得到下面的错误提示: + +``` +ALPN is only supported in Java9 or if you use conscrypt as your provider or have the jetty alpn stuff on the class path. +``` + +也就是说如果是用JDK作为默认的SSL provider的话,它是不支持ALPN的。必须升级到java9. + +根据提示如果添加conscrypt到classpath中: + +```xml + + org.conscrypt + conscrypt-openjdk-uber + 2.5.2 + +``` + +运行之后会得到下面的错误: + +``` +Unable to wrap SSLEngine of type 'sun.security.ssl.SSLEngineImpl' +``` + +怎么办呢?答案就是使用Open SSL,还需要添加: + +```xml + + io.netty + netty-tcnative-boringssl-static + 2.0.40.Final + +``` + +经过测试,完美执行。 + +# ApplicationProtocolConfig + +ApplicationProtocolConfig是netty提供了传递给SSLEngine的协议配置类,它主要有四个属性: + +```java +private final List supportedProtocols; +private final Protocol protocol; +private final SelectorFailureBehavior selectorBehavior; +private final SelectedListenerFailureBehavior selectedBehavior; +``` + +supportedProtocols是支持的数据传输协议,像上面的HTTP2,HTTP1.1或者spdy/1,spdy/2,spdy/3等。 + +protocol是TLS的扩展协议,像ALPN或者NPN等。 + +selectorBehavior是在选择协议的时候的表现方式,有3种方式: + +FATAL_ALERT: 如果选择应用程序协议的节点没有找到匹配项,那么握手将会失败。 +NO_ADVERTISE: 如果选择应用程序协议的节点没有找到匹配项,它将通过在握手中假装不支持 TLS 扩展。 +CHOOSE_MY_LAST_PROTOCOL: 如果选择应用程序协议的节点没有找到匹配项,将会使用上一次建议使用的协议。 + +selectedBehavior是通知被选择的协议之后的表现方式,也有3种方式: + +ACCEPT: 如果节点不支持对方节点选择的应用程序协议,则该节点默认不支持该TLS扩展,然后继续握手。 +FATAL_ALERT: 如果节点不支持对方节点选择的应用程序协议,则握手失败。 +CHOOSE_MY_LAST_PROTOCOL: 如果节点不支持对方节点选择的应用程序协议,将会使用上一次建议使用的协议。 + +# 构建SslContext + +有了provider,ApplicationProtocolConfig 之后,就可以构建SslContext了。首先创建SSL provider: + +```java +SslProvider provider = SslProvider.isAlpnSupported(SslProvider.OPENSSL) ? SslProvider.OPENSSL : SslProvider.JDK; +``` + +默认情况下使用JDK作为ssl provider,如果你使用的是OpenSSL的话,就使用OpenSSL。 + +我们使用SslContextBuilder.forServer来创建SslContext,这个方法需要传入certificate和privateKey,为了简单起见,我们使用自签名的SelfSignedCertificate: + +```java +SelfSignedCertificate ssc = new SelfSignedCertificate(); +sslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()).build(); +``` + +还可以为其设置sslProvider,ciphers和applicationProtocolConfig等信息: + +```java +sslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) + .sslProvider(provider) + //支持的cipher + .ciphers(Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE) + .applicationProtocolConfig(new ApplicationProtocolConfig( + Protocol.ALPN, + // 目前 OpenSsl 和 JDK providers只支持NO_ADVERTISE + SelectorFailureBehavior.NO_ADVERTISE, + // 目前 OpenSsl 和 JDK providers只支持ACCEPT + SelectedListenerFailureBehavior.ACCEPT, + ApplicationProtocolNames.HTTP_2, + ApplicationProtocolNames.HTTP_1_1)) + .build(); +``` + +# ProtocolNegotiationHandler + +最后,我们需要根据协商使用的不同协议,进行不同的处理。netty提供了一个ApplicationProtocolNegotiationHandler,自定义的话,只需要继承该类即可,比如,我们根据protocol的名称不同,来分别处理HTTP1和HTTP2请求: + +```java +public class MyNegotiationHandler extends ApplicationProtocolNegotiationHandler { + public MyNegotiationHandler() { + super(ApplicationProtocolNames.HTTP_1_1); + } + + protected void configurePipeline(ChannelHandlerContext ctx, String protocol) { + if (ApplicationProtocolNames.HTTP_2.equals(protocol) { + configureHttp2(ctx); + } else if (ApplicationProtocolNames.HTTP_1_1.equals(protocol)) { + configureHttp1(ctx); + } else { + throw new IllegalStateException("unknown protocol: " + protocol); + } + } +} +``` + +然后将其加入到ChannelPipeline中即可: + +```java +public class MyInitializer extends ChannelInitializer { + private final SslContext sslCtx; + + public MyInitializer(SslContext sslCtx) { + this.sslCtx = sslCtx; + } + + protected void initChannel(Channel ch) { + ChannelPipeline p = ch.pipeline(); + p.addLast(sslCtx.newHandler(...)); // Adds SslHandler + p.addLast(new MyNegotiationHandler()); + } +} +``` + +# 总结 + +以上就是在netty中配置TLS支持HTTP2的完整流程了。 + +本文的例子可以参考:[learn-netty4](https://github.com/ddean2009/learn-netty4) \ No newline at end of file diff --git a/learn-netty文章/27.netty系列之使用netty实现支持http2的服务器.md b/learn-netty文章/27.netty系列之使用netty实现支持http2的服务器.md new file mode 100644 index 0000000..c253d65 --- /dev/null +++ b/learn-netty文章/27.netty系列之使用netty实现支持http2的服务器.md @@ -0,0 +1,160 @@ +# netty系列之:使用netty实现支持http2的服务器 + +# 简介 + +上一篇文章中,我们提到了如何在netty中配置TLS,让他支持HTTP2。事实上TLS并不是https的一个必须要求,它只是建议的标准。那么除了TLS之外,还需要如何设置才能让netty支持http2呢?一起来看看吧。 + +# 基本流程 + +netty支持http2有两种情况,第一种情况是使用tls,在这种情况下需要添加一个ProtocolNegotiationHandler来对握手之后的协议进行协商,在协商之后,需要决定到底使用哪一种协议。 + +上一篇文章,我们已经介绍TLS支持http2的细节了,这里不再赘述,感兴趣的朋友可以查看我之前的文章。 + +如果不使用tls,那么有两种情况,一种是直接使用http1.1了,我们需要为http1.1添加一个ChannelInboundHandler即可。 + +另一种情况就是使用clear text从HTTP1.1升级到HTTP2。 + +HTTP/2 ClearText也叫做h2c,我们看一个简单的升级请求,首先是客户端请求: + +```http +GET /index HTTP/1.1 +Host: server.flydean.com +Connection: Upgrade, HTTP2-Settings +Upgrade: h2c +HTTP2-Settings: (SETTINGS payload) +``` + +然后是服务器端的响应,如果服务器端不支持升级,则返回: + +```http +HTTP/1.1 200 OK +Content-length: 100 +Content-type: text/html + +(... HTTP/1.1 response ...) +``` + +如果服务器支持升级,则返回: + +```http +HTTP/1.1 101 Switching Protocols +Connection: Upgrade +Upgrade: h2c + +(... HTTP/2 response ...) +``` + +# CleartextHttp2ServerUpgradeHandler + +有了上面的基本流程,我们只需要在netty中提供对应的handler类就可以解决netty对http2的支持了。 + +不过上面的升级流程看起来比较复杂,所以netty为我们提供了一个封装好的类:CleartextHttp2ServerUpgradeHandler来实现h2c的功能。 + +这个类需要传入3个参数,分别是HttpServerCodec、HttpServerUpgradeHandler和ChannelHandler。 + +HttpServerCodec就是处理http server的编码类,一般我们使用HttpServerCodec。 + +HttpServerUpgradeHandler是从http1.1升级到http2的处理类。 + +netty也提供了一个现成的类:HttpServerUpgradeHandler,来处理升级的编码。 + +HttpServerUpgradeHandler需要两个参数,一个是sourceCodec,也就是http原始的编码类HttpServerCodec,一个是用来返回UpgradeCodec的工厂类,返回netty自带的Http2ServerUpgradeCodec。 + +```java +public HttpServerUpgradeHandler(SourceCodec sourceCodec, UpgradeCodecFactory upgradeCodecFactory) { + this(sourceCodec, upgradeCodecFactory, 0); +} +``` + +ChannelHandler是真正处理HTTP2的handler,我们可以根据需要对这个handler进行自定义。 + +有了UpgradeHandler,将其加入ChannelPipeline即可。 + +# Http2ConnectionHandler + +不管是HttpServerUpgradeHandler,还是CleartextHttp2ServerUpgradeHandler,都需要传入一个真正能够处理http2的handler。这个handler就是Http2ConnectionHandler。 + +Http2ConnectionHandler是一个实现类,它已经实现了处理各种inbound frame events的事件,然后将这些事件委托给 Http2FrameListener。 + +所以Http2ConnectionHandler需要跟Http2FrameListener配合使用。 + +这里要详细讲解一下Http2FrameListener,它主要处理HTTP2 frame的各种事件。 + +先来看下http2FrameListener中提供的event trigger方法: + +![img](27a9e2a6aea945e6a6cca440a8f09eff.png) + +从上图可以看到,主要是各种frame的事件触发方法,其中http2中有这样几种frame: + +- DATA frame +- HEADERS frame +- PRIORITY frame +- RST_STREAM frame +- SETTINGS acknowledgment frame +- SETTINGS frame +- PING frame +- PING acknowledgment +- PUSH_PROMISE frame +- GO_AWAY frame +- WINDOW_UPDATE frame +- Unknown Frame + +这几种frame基本上列举了http2 frame中所有的类型。 + +我们要做的就是自定义一个handler类,继承Http2ConnectionHandler,然后实现Http2FrameListener接口即可。 + +```java +public final class CustHttp2Handler extends Http2ConnectionHandler implements Http2FrameListener +``` + +在使用clear text从HTTP1.1升级到HTTP2的过程中,我们需要处理两个事情,第一个事情就是处理http1.1使用http头升级到http2,可以重写继承自Http2ConnectionHandler的userEventTriggered方法,通过判断event的类型是否是UpgradeEvent,来触发对应的Http2FrameListener接口中的方法,比如这里的onHeadersRead: + +```java +/** + * 处理HTTP upgrade事件 + */ +@Override +public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt instanceof HttpServerUpgradeHandler.UpgradeEvent) { + HttpServerUpgradeHandler.UpgradeEvent upgradeEvent = + (HttpServerUpgradeHandler.UpgradeEvent) evt; + onHeadersRead(ctx, 1, upgradeToHttp2Headers(upgradeEvent.upgradeRequest()), 0 , true); + } + super.userEventTriggered(ctx, evt); +} +``` + +upgradeToHttp2Headers方法将传入的FullHttpRequest,转换成为Http2Headers: + +```java +private static Http2Headers upgradeToHttp2Headers(FullHttpRequest request) { + CharSequence host = request.headers().get(HttpHeaderNames.HOST); + Http2Headers http2Headers = new DefaultHttp2Headers() + .method(HttpMethod.GET.asciiName()) + .path(request.uri()) + .scheme(HttpScheme.HTTP.name()); + if (host != null) { + http2Headers.authority(host); + } + return http2Headers; +} +``` + +还有一个要实现的方法,就是sendResponse方法,将数据写回给客户端,回写需要包含headers和data两部分,如下所示: + +```java +/** + * 发送响应数据到客户端 + */ +private void sendResponse(ChannelHandlerContext ctx, int streamId, ByteBuf payload) { + Http2Headers headers = new DefaultHttp2Headers().status(OK.codeAsText()); + encoder().writeHeaders(ctx, streamId, headers, 0, false, ctx.newPromise()); + encoder().writeData(ctx, streamId, payload, 0, true, ctx.newPromise()); +} +``` + +# 总结 + +到此,一个处理clear text从HTTP1.1升级到HTTP2的handler就做好了。加上之前讲解的TLS扩展协议的支持,就构成了一个完整的支持http2的netty服务器。 + +本文的例子可以参考:[learn-netty4](https://github.com/ddean2009/learn-netty4) \ No newline at end of file diff --git a/learn-netty文章/27a9e2a6aea945e6a6cca440a8f09eff.png b/learn-netty文章/27a9e2a6aea945e6a6cca440a8f09eff.png new file mode 100644 index 0000000..79776a5 Binary files /dev/null and b/learn-netty文章/27a9e2a6aea945e6a6cca440a8f09eff.png differ diff --git a/learn-netty文章/28.netty系列之netty对http2消息的封装.md b/learn-netty文章/28.netty系列之netty对http2消息的封装.md new file mode 100644 index 0000000..5a5fe8d --- /dev/null +++ b/learn-netty文章/28.netty系列之netty对http2消息的封装.md @@ -0,0 +1,126 @@ +# netty系列之:netty对http2消息的封装 + +# 简介 + +无论是什么协议,如果要真正被使用的话,需要将该协议转换成为对应的语言才好真正的进行应用,本文将从http2消息的结构出发,探讨一下netty对http2消息的封装,带大家领略一下真正的框架应该做到什么程度。 + +# http2消息的结构 + +http2和http1.1不同的是它使用了新的二进制分帧,通过客户端和服务器端建立数据流steam来进行客户端和服务器端之间消息的交互。其中数据流是一个双向字节流,用来发送一条或者多条消息。 + +消息是客户端和服务端发送的一个逻辑上完整的数据。根据数据大小的不同,可以将消息划分为不同的帧Frame。也就是说message是由不同的frame组成的。 + +frame就是http2中进行通信的最小单位,根据上一节的介绍,我们知道frame有这样几种: + +- DATA frame +- HEADERS frame +- PRIORITY frame +- RST_STREAM frame +- SETTINGS acknowledgment frame +- SETTINGS frame +- PING frame +- PING acknowledgment +- PUSH_PROMISE frame +- GO_AWAY frame +- WINDOW_UPDATE frame +- Unknown Frame + +我们看一下http2中stream和frame的一个大体的结构: + +![img](1793009756544508a2582d689d1d065e.png) + +在http2中,一个TCP连接,可以承载多个数据流stream,多个stream中的不同frame可以交错发送。 + +每个frame通过stream id来标记其所属的stream。 + +有了上面的http2的基本概念,我们接下来就看下netty对http2的封装了。 + +# netty对http2的封装 + +## Http2Stream + +作为一个TCP连接下面的最大的单位stream,netty中提供了接口Http2Stream。注意,Http2Stream是一个接口,它有两个实现类,分别是DefaultStream和ConnectionStream。 + +Http2Stream中有两个非常重要的属性,分别是id和state。 + +id前面已经介绍了,是stream的唯一标记。这里要注意由客户端建立的 Stream ID 必须是奇数,而由服务端建立的 Stream ID 必须是偶数。另外Stream ID 为 0 的流有特殊的作用,它是CONNECTION_STREAM_ID,1 表示HTTP_UPGRADE_STREAM_ID。 + +state表示stream的状态,具体而言,stream有下面几个状态: + +```java +IDLE(false, false), +RESERVED_LOCAL(false, false), +RESERVED_REMOTE(false, false), +OPEN(true, true), +HALF_CLOSED_LOCAL(false, true), +HALF_CLOSED_REMOTE(true, false), +CLOSED(false, false); +``` + +为什么状态需要区分local和remote呢?这是因为stream连接的两端,所以有两端的状态。 + +和stream状态相对应的就是http2的生命周期了。netty提供了Http2LifecycleManager来表示对http2生命周期的管理: + +```java +void closeStreamLocal(Http2Stream stream, ChannelFuture future); +void closeStreamRemote(Http2Stream stream, ChannelFuture future); +void closeStream(Http2Stream stream, ChannelFuture future); +ChannelFuture resetStream(ChannelHandlerContext ctx, int streamId, long errorCode, + ChannelPromise promise); +ChannelFuture goAway(ChannelHandlerContext ctx, int lastStreamId, long errorCode, + ByteBuf debugData, ChannelPromise promise); +void onError(ChannelHandlerContext ctx, boolean outbound, Throwable cause); +``` + +分别是关闭stream,重置stream,拒绝新建stream:goAway,和处理出错状态这几种。 + +## Http2Frame + +stream之后,就是真实承载http2消息的Http2Frame了。在netty中,Http2Frame是一个接口,它有很多具体的实现。 + +Http2Frame的直接子类包括HTTP2GoAwayFrame、HTTPPingFrame、Http2SettingsFrame和HTTP2SettingsAckFrame。 + +其中goAway表示不接受新的stream,ping用来进行心跳检测。SETTINGS用来修改连接或者 Stream 流的配置。 + +netty中专门有一个Http2Settings类和其对应。 + +在这个类中定义了一些特别的setting名字: + +| SETTINGS 名字 | 含义 | +| :-----------------------------: | :------------------------------------: | +| SETTINGS_HEADER_TABLE_SIZE | 对端索引表的最大尺寸 | +| SETTINGS_ENABLE_PUSH | 是否启用服务器推送功能 | +| SETTINGS_MAX_CONCURRENT_STREAMS | 接收端允许的最大并发 Stream 数量 | +| SETTINGS_INITIAL_WINDOW_SIZE | 发送端的窗口大小,用于 Stream 级别流控 | +| SETTINGS_MAX_FRAME_SIZE | 设置帧的最大大小 | +| SETTINGS_MAX_HEADER_LIST_SIZE | 对端头部索引表的最大尺寸 | + +除了上面讲的4个frame之外,其他的frame实现都继承自Http2StreamFrame,具体而言有PriorityFrame,ResetFrame,HeadersFrame,DataFrame,WindowUpdateFrame,PushPromiseFrame和UnknownFrame。 + +各个frame分别代表了不同的功能。这里最重要的就是Http2HeadersFrame和Http2DataFrame。 + +Http2HeadersFrame主要是客户端发送给服务器端的http2请求。 + +具体而言除了标准的http1.1的header之外,http2还支持下面的header: + +```java +METHOD(":method", true), + +SCHEME(":scheme", true), + +AUTHORITY(":authority", true), + +PATH(":path", true), + +STATUS(":status", false), + +PROTOCOL(":protocol", true); +``` + +对于Http2DataFrame来说,他本身是一个ByteBufHolder,用来传递具体的数据信息。data frame的Payload直接存储在ByteBuf中。 + +# 总结 + +以上就是netty对http2消息的封装了。 + +本文的例子可以参考:[learn-netty4](https://github.com/ddean2009/learn-netty4) \ No newline at end of file diff --git a/learn-netty文章/29.netty系列之netty实现http2中的流控制.md b/learn-netty文章/29.netty系列之netty实现http2中的流控制.md new file mode 100644 index 0000000..764ffdb --- /dev/null +++ b/learn-netty文章/29.netty系列之netty实现http2中的流控制.md @@ -0,0 +1,72 @@ +# netty系列之:netty实现http2中的流控制 + +# 简介 + +HTTP2相对于http1.1来说一个重要的提升就是流控制flowcontrol。为什么会有流控制呢?这是因为不管是哪种协议,客户端和服务器端在接收数据的时候都有一个缓冲区来临时存储暂时处理不了的数据,但是缓冲区的大小是有限制的,所以有可能会出现缓冲区溢出的情况,比如客户端向服务器端上传一个大的图片,就有可能导致服务器端的缓冲区溢出,从而导致一些额外的数据包丢失。 + +为了避免缓冲区溢出,各个HTTP协议都提供了一定的解决办法。 + +在HTTP1.1中,流量的控制依赖的是底层TCP协议,在客户端和服务器端建立连接的时候,会使用系统默认的设置来建立缓冲区。在数据进行通信的时候,会告诉对方它的接收窗口的大小,这个接收窗口就是缓冲区中剩余的可用空间。如果接收窗口大小为零,则说明接收方缓冲区已满,则发送方将不再发送数据,直到客户端清除其内部缓冲区,然后请求恢复数据传输。 + +HTTP2通过客户端和服务器端的应用中进行缓冲区大小消息的传输,通过在应用层层面控制数据流,所以各个应用端可以自行控制流量的大小,从而实现更高的连接效率。 + +本文将会介绍netty对http2流控制的支持。 + +# http2中的流控制 + +在简介中我们也提到了,传统的HTTP1.1使用的是系统底层的流量控制机制,具体来说就是TCP的流控制。但是TCP的流控制在HTTP2中就不够用了。因为HTTP2使用的是多路复用的机制,一个TCP连接可以有多个http2连接。所以对http2来说TCP本身的流控制机制太粗糙了,不够精细。 + +所以在HTTP2中,实现了更加精细的流控制机制,它允许客户端和服务器实现其自己的数据流和连接级流控制。 + +具体的流程是这样的,当客户端和服务器端建立连接之后,会发送Http2SettingsFrame,这个settings frame中包含了SETTINGS_INITIAL_WINDOW_SIZE,这个是发送端的窗口大小,用于 Stream 级别流控。流控制窗口的默认值设为65,535字节,但是接收方可以对其进行修改,最大值为2^31-1 字节。 + +建立好初始windows size之后,对于接收方来说,每次发送方发送data frame就会减少window的的大小,而接收方每次发送WINDOW_UPDATE frame时候就会增加window的大小,从达到动态控制的目的。 + +# netty对http2流控制的封装 + +## Http2FlowController + +从上面的介绍我们知道,http2对流控制是通过两个方面来实施的,第一个方面就是初始化的Http2SettingsFrame,通过设置SETTINGS_INITIAL_WINDOW_SIZE来控制初始window的大小。第二个方面就是在后续的WINDOW_UPDATE frame中对window的大小进行动态增减。 + +对于netty来说,这一切都是封装在Http2FlowController类中的。Http2FlowController是一个抽象类,它有两个实现,分别是Http2LocalFlowController和Http2RemoteFlowController。他们分别表示对inbound flow of DATA 和 outbound flow of DATA的处理。 + +Http2FlowController中主要有5个方法,分别是: + +- set channelHandlerContext:绑定flowcontrol到ChannelHandlerContext上。 +- set initialWindowSize:初始化window size,等同于设置SETTINGS_INITIAL_WINDOW_SIZE。 +- get initialWindowSize: 返回初始化window size。 +- windowSize: 获取当前的windowSize。 +- incrementWindowSize: 增加flow control window的大小。 + +接下来我们看下他的两个实现类,有什么不一样的地方。 + +## Http2LocalFlowController + +LocalFlowController用来对远程节点发过来的DATA frames做flow control。它有5个主要的方法。 + +- set frameWriter: 用来设置发送WINDOW_UPDATE frames的frame writer。 +- receiveFlowControlledFrame: 接收inbound DATA frame,并且对其进行flow control。 +- consumeBytes: 表示应用已经消费了一定数目的bytes,可以接受更多从远程节点发过来的数据。flow control可以发送 WINDOW_UPDATE frame来重置window大小。 +- unconsumedBytes: 接收到,但是未消费的bytes。 +- initialWindowSize: 给定stream的初始window大小。 + +## Http2RemoteFlowController + +remoteFlowController用来处理发送给远程节点的outbound DATA frames。它提供了8个方法: + +- get channelHandlerContext: 获取当前flow control的context. +- addFlowControlled: 将flow control payload添加到发送到远程节点的queue中。 +- hasFlowControlled: 判断当前stream是否有 FlowControlled frames在queue中。 +- writePendingBytes: 将流量控制器中的所有待处理数据写入流量控制限制。 +- listener: 给 flow-controller添加listener。 +- isWritable: 确定流是否有剩余字节可用于流控制窗口。 +- channelWritabilityChanged: context的writable状态是否变化。 +- updateDependencyTree: 更新stream之间的依赖关系,因为stream是可以有父子结构的。 + +# 流控制的使用 + +flowControl相关的类主要被用在Http2Connection,Http2ConnectionDecoder,Http2ConnectionEncoder中,在建立http2连接的时候起到相应的作用。 + +# 总结 + +flowControl是http2中的一个比较底层的概念,大家在深入了解netty的http2实现中应该会遇到。 \ No newline at end of file diff --git a/learn-netty文章/3.netty系列之netty架构概述.md b/learn-netty文章/3.netty系列之netty架构概述.md new file mode 100644 index 0000000..a43a3e4 --- /dev/null +++ b/learn-netty文章/3.netty系列之netty架构概述.md @@ -0,0 +1,116 @@ +# netty系列之:netty架构概述 + +# 简介 + +Netty为什么这么优秀,它在JDK本身的NIO基础上又做了什么改进呢?它的架构和工作流程如何呢?请走进今天的netty系列文章之:netty架构概述。 + +# netty架构图 + +netty的主要作用就是提供一个简单的NIO框架可以和上层的各种协议相结合,最终实现高性能的服务器。下面是netty官网提供的架构图: + +![img](dda8e9896b454c6f8c42300c1170a0c3.png) + +从上图可以看到netty的核心主要分成三部分,分别是可扩展的event model、统一的API、和强大的Byte Buffer。这三个特性是netty的制胜法宝。 + +下面会从这几个方面对netty的特性进行详细说明,务必让读者了解到netty的优秀之处。 + +# 丰富的Buffer数据机构 + +首先从最底层的Buffer数据结构开始,netty提供了一个io.netty.buffer的包,该包里面定义了各种类型的ByteBuf和其衍生的类型。 + +netty Buffer的基础是ByteBuf类,这是一个抽象类,其他的Buffer类基本上都是由该类衍生而得的,这个类也定义了netty整体Buffer的基调。 + +netty重写ByteBuf其目的是让最底层的ByteBuf比JDK自带的更快更适合扩展。具体而言,netty的ByteBuf要比JDK中的ByteBuffer要快,同时,扩展也更加容易,大家可以根据需要Buf进行自定义。另外netty有一些内置的复合缓冲区类型,所以可以实现透明的零拷贝。对于动态缓冲区类型来说,可以和StringBuffer一样按需扩展,非常好用。 + +## 零拷贝 + +什么是零拷贝呢?零拷贝的意思是在需要拷贝的时候不做拷贝。我们知道数据在使用底层协议进行传输的过程中是会被封装成为一个个的包进行传输。当传输的数据过大,一个包放不下的时候,还需要对数据进行拆分,目的方在接收到数据之后,需要对收到的数据进行组装,一般情况下这个组装的操作是对数据的拷贝,将拆分过后的对象拷贝到一个长的数据空间中。 + +比如下面的例子所示,将底层的TCP包组合称为顶层的HTTP包,但是并没有进行拷贝: + +![img](5cefd4a7dcf64051af18800c42605675.png) + +具体怎么拷贝呢?在上一篇文章中,我们知道netty提供了一个工具类方法Unpooled,这个工具类中有很多wrapped开头的方法,我们举几个例子: + +```java +public static ByteBuf wrappedBuffer(byte[]... arrays) { + return wrappedBuffer(arrays.length, arrays); +} + +public static ByteBuf wrappedBuffer(ByteBuf... buffers) { + return wrappedBuffer(buffers.length, buffers); +} + +public static ByteBuf wrappedBuffer(ByteBuffer... buffers) { + return wrappedBuffer(buffers.length, buffers); +} +``` + +上面三个方法分别是封装byte数组、封装ByteBuf和封装ByteBuffer,这些方法都是零拷贝。大家可以在实际的项目中根据实际情况,自行选用。 + +# 统一的API + +一般来说,在传统的JDK的IO API中,根据传输类型或者协议的不同,使用的API也是不同的。我们需要对不同的传输方式开发不同的应用程序,不能做到统一。这样的结果就是无法平滑的迁移,并且在程序扩展的时候需要进行额外的处理。 + +> 什么是传输方式呢?这里是指以什么样的方式来实现IO,比如传统的阻塞型IO,我们可以称之为OIO,java的new IO可以称之为NIO,异步IO可以称之为AIO等等。 + +并且JDK中的传统IO和NIO是割裂的,如果在最开始你使用的是传统IO,那么当你的客户数目增长到一定的程度准备切换到NIO的时候,就会发现切换起来异常复杂,因为他们是割裂的。 + +为了解决这个问题,netty提供了一个统一的类Channel来提供统一的API。 + +先看下Channel中定义的方法: + +![img](82c99c6dc89c4dd19675b6b2079539f2.png) + +从上图我们可以看到使用Channel可以判断channel当前的状态,可以对其进行参数配置,可以对其进行I/O操作,还有和channel相关的ChannelPipeline用来处理channel关联的IO请求和事件。 + +使用Channel就可以对NIO的TCP/IP,OIO的TCP/IP,OIO的UDP/IP和本地传输都能提供很好的支持。 + +传输方式的切换,只需要进行很小的成本替换。 + +当然,如果你对现有的实现都不满意的话,还可以对核心API进行自定义扩展。 + +# 事件驱动 + +netty是一个事件驱动的框架,事件驱动框架的基础就是事件模型,Netty专门为IO定义了一个非常有效的事件模型。可以在不破坏现有代码的情况下实现自己的事件类型。netty中自定义的事件类型通过严格的类型层次结构跟其他事件类型区分开来,所以可扩展性很强。 + +netty中的事件驱动是由ChannelEvent、ChannelHandler和ChannelPipeline共同作用的结果。其中ChannelEvent表示发生的事件,ChannelHandler定义了如何对事件进行处理,而ChannelPipeline类似一个拦截器,可以让用户自行对定义好的ChannelHandler进行控制,从而达到控制事件处理的结果。 + +我们看一个简单的自定义Handler: + +```java +public class MyHandler extends SimpleChannelInboundHandler { + + @Override + public void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception { + // 对消息进行处理 + ByteBuf in = (ByteBuf) msg; + try { + log.info("收到消息:{}",in.toString(io.netty.util.CharsetUtil.US_ASCII)); + }finally { + ReferenceCountUtil.release(msg); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + //异常处理 + cause.printStackTrace(); + ctx.close(); + } +} +``` + +上面的例子中,我们定义了如何对接收到的消息和异常进行处理。在后续的文章中我们会详细对ChannelEvent、ChannelHandler和ChannelPipeline之间的交互使用进行介绍。 + +# 其他优秀的特性 + +除了上面提到的三大核心特性之外,netty还有其他几个优点,方便程序员的开发工作。 + +比如对SSL / TLS的支持,对HTTP协议的实现,对WebSockets 实现和Google Protocol Buffers的实现等等,表明netty在各个方面多个场景都有很强的应用能力。 + +# 总结 + +netty是由三个核心组件构成:缓冲区、通道和事件模型,通过理解这三个核心组件是如何相互工作的,那么再去理解建立在netty之上的高级功能就不难了。 + +本文的例子可以参考:[learn-netty4](https://github.com/ddean2009/learn-netty4) \ No newline at end of file diff --git a/learn-netty文章/30.netty系列之搭建客户端使用http1.1的方式连接http2服务器.md b/learn-netty文章/30.netty系列之搭建客户端使用http1.1的方式连接http2服务器.md new file mode 100644 index 0000000..822afb4 --- /dev/null +++ b/learn-netty文章/30.netty系列之搭建客户端使用http1.1的方式连接http2服务器.md @@ -0,0 +1,156 @@ +# netty系列之:搭建客户端使用http1.1的方式连接http2服务器 + +# 简介 + +对于http2协议来说,它的底层跟http1.1是完全不同的,但是为了兼容http1.1协议,http2提供了一个从http1.1升级到http2的方式,这个方式叫做cleartext upgrade,也可以简称为h2c。 + +在netty中,http2的数据对应的是各种http2Frame对象,而http1的数据对应的是HttpRequest和HttpHeaders。一般来说要想从客户端发送http2消息给支持http2的服务器,那么需要发送这些http2Frame的对象,那么可不可以像http1.1这样发送HttpRequest对象呢? + +今天的文章将会给大家揭秘。 + +# 使用http1.1的方式处理http2 + +netty当然考虑到了客户的这种需求,所以提供了两个对应的类,分别是:InboundHttp2ToHttpAdapter和HttpToHttp2ConnectionHandler。 + +他们是一对方法,其中InboundHttp2ToHttpAdapter将接收到的HTTP/2 frames 转换成为HTTP/1.x objects,而HttpToHttp2ConnectionHandler则是相反的将HTTP/1.x objects转换成为HTTP/2 frames。 这样我们在程序中只需要处理http1的对象即可。 + +他们的底层实际上调用了HttpConversionUtil类中的转换方法,将HTTP2对象和HTTP1对象进行转换。 + +# 处理TLS连接 + +和服务器一样,客户端的连接也需要区分是TLS还是clear text,TLS简单点,只需要处理HTTP2数据即可,clear text复杂点,需要考虑http升级的情况。 + +先看下TLS的连接处理。 + +首先是创建SslContext,客户端的创建和服务器端的创建没什么两样,这里要注意的是SslContextBuilder调用的是forClient()方法: + +```java +SslProvider provider = + SslProvider.isAlpnSupported(SslProvider.OPENSSL)? SslProvider.OPENSSL : SslProvider.JDK; +sslCtx = SslContextBuilder.forClient() + .sslProvider(provider) + .ciphers(Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE) + // 因为我们的证书是自生成的,所以需要信任放行 + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .applicationProtocolConfig(new ApplicationProtocolConfig( + Protocol.ALPN, + SelectorFailureBehavior.NO_ADVERTISE, + SelectedListenerFailureBehavior.ACCEPT, + ApplicationProtocolNames.HTTP_2, + ApplicationProtocolNames.HTTP_1_1)) + .build(); +``` + +然后将sslCtx的newHandler方法传入到pipeline中: + +```java +pipeline.addLast(sslCtx.newHandler(ch.alloc(), CustHttp2Client.HOST, CustHttp2Client.PORT)); +``` + +最后加入ApplicationProtocolNegotiationHandler,用于TLS扩展协议的协商: + +```java +pipeline.addLast(new ApplicationProtocolNegotiationHandler("") { + @Override + protected void configurePipeline(ChannelHandlerContext ctx, String protocol) { + if (ApplicationProtocolNames.HTTP_2.equals(protocol)) { + ChannelPipeline p = ctx.pipeline(); + p.addLast(connectionHandler); + p.addLast(settingsHandler, responseHandler); + return; + } + ctx.close(); + throw new IllegalStateException("未知协议: " + protocol); + } +}); +``` + +如果是HTTP2协议,则需要向pipline中加入三个handler,分别是connectionHandler,settingsHandler和responseHandler。 + +connectionHandler用于处理客户端和服务器端的连接,这里使用HttpToHttp2ConnectionHandlerBuilder来构建一个上一节提到的HttpToHttp2ConnectionHandler,用来将http1.1对象转换成为http2对象。 + +```java +Http2Connection connection = new DefaultHttp2Connection(false); +connectionHandler = new HttpToHttp2ConnectionHandlerBuilder() + .frameListener(new DelegatingDecompressorFrameListener( + connection, + new InboundHttp2ToHttpAdapterBuilder(connection) + .maxContentLength(maxContentLength) + .propagateSettings(true) + .build())) + .frameLogger(logger) + .connection(connection) + .build(); +``` + +但是连接其实是双向的,HttpToHttp2ConnectionHandler是将http1.1转换成为http2,它实际上是一个outbound处理器,我们还需要一个inbound处理器,用来将接收到的http2对象转换成为http1.1对象,这里通过添加framelistener来实现。 + +frameListener传入一个DelegatingDecompressorFrameListener,其内部又传入了前一节介绍的InboundHttp2ToHttpAdapterBuilder用来对http2对象进行转换。 + +settingsHandler用来处理Http2Settings inbound消息,responseHandler用来处理FullHttpResponse inbound消息。 + +这两个是自定义的handler类。 + +# 处理h2c消息 + +从上面的代码可以看出,我们在TLS的ProtocolNegotiation中只处理了HTTP2协议,如果是HTTP1协议,直接会报错。如果是HTTP1协议,则可以通过clear text upgrade来实现,也就是h2c协议。 + +我们看下h2c需要添加的handler: + +```java +private void configureClearText(SocketChannel ch) { + HttpClientCodec sourceCodec = new HttpClientCodec(); + Http2ClientUpgradeCodec upgradeCodec = new Http2ClientUpgradeCodec(connectionHandler); + HttpClientUpgradeHandler upgradeHandler = new HttpClientUpgradeHandler(sourceCodec, upgradeCodec, 65536); + + ch.pipeline().addLast(sourceCodec, + upgradeHandler, + new CustUpgradeRequestHandler(this), + new UserEventLogger()); +} +``` + +首先添加的是HttpClientCodec作为source编码handler,然后添加HttpClientUpgradeHandler作为upgrade handler。最后添加自定义的CustUpgradeRequestHandler和事件记录器UserEventLogger。 + +自定义的CustUpgradeRequestHandler负责在channelActive的时候,创建upgradeRequest并发送到channel中。 + +因为upgradeCodec中已经包含了处理http2连接的connectionHandler,所以还需要手动添加settingsHandler和responseHandler。 + +```java +ctx.pipeline().addLast(custHttp2ClientInitializer.settingsHandler(), custHttp2ClientInitializer.responseHandler()); +``` + +# 发送消息 + +handler配置好了之后,我们就可以直接以http1的方式来发送http2消息了。 + +首先发送一个get请求: + +```java +// 创建一个get请求 +FullHttpRequest request = new DefaultFullHttpRequest(HTTP_1_1, GET, GETURL, Unpooled.EMPTY_BUFFER); +request.headers().add(HttpHeaderNames.HOST, hostName); +request.headers().add(HttpConversionUtil.ExtensionHeaderNames.SCHEME.text(), scheme.name()); +request.headers().add(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.GZIP); +request.headers().add(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.DEFLATE); +responseHandler.put(streamId, channel.write(request), channel.newPromise()); +``` + +然后是一个post请求: + +```java +// 创建一个post请求 +FullHttpRequest request = new DefaultFullHttpRequest(HTTP_1_1, POST, POSTURL, + wrappedBuffer(POSTDATA.getBytes(CharsetUtil.UTF_8))); +request.headers().add(HttpHeaderNames.HOST, hostName); +request.headers().add(HttpConversionUtil.ExtensionHeaderNames.SCHEME.text(), scheme.name()); +request.headers().add(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.GZIP); +request.headers().add(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.DEFLATE); +responseHandler.put(streamId, channel.write(request), channel.newPromise()); +``` + +和普通的http1请求没太大区别。 + +# 总结 + +通过使用InboundHttp2ToHttpAdapter和HttpToHttp2ConnectionHandler可以方便的使用http1的方法来发送http2的消息,非常方便。 \ No newline at end of file diff --git a/learn-netty文章/31.netty系列之在http2中使用framecodec.md b/learn-netty文章/31.netty系列之在http2中使用framecodec.md new file mode 100644 index 0000000..8d73c1e --- /dev/null +++ b/learn-netty文章/31.netty系列之在http2中使用framecodec.md @@ -0,0 +1,116 @@ +# netty系列之:在http2中使用framecodec + +# 简介 + +netty为我们提供了很多http2的封装,让我们可以轻松的搭建出一个支持http2的服务器。其中唯一需要我们自定义的就是http2 handler。 + +在之前的文章中,我们介绍了自定义http2handler继承自Http2ConnectionHandler并且实现Http2FrameListener。这种实现方式是netty目前比较推荐的实现方式,今天给大家介绍的一种实现方式是netty中准备替换继承Http2ConnectionHandler的实现方式,但是这种实现方式并不成熟,还在不断的完善中。 + +今天给大家介绍一下这种实现方式。 + +# Http2FrameCodec + +这种实现方式的核心类是Http2FrameCodec。事实上Http2FrameCodec也是继承自Http2ConnectionHandler。 + +它的主要作用是将HTTP/2中的frames和Http2Frame对象进行映射。Http2Frame是netty中对应所有http2 frame的封装,这样就可以在后续的handler中专注于处理Http2Frame对象即可,从而摆脱了http2协议的各种细节,可以减少使用者的工作量。 + +对于每个进入的HTTP/2 frame,Http2FrameCodec都会创建一个Http2Frame对象,并且将其传递给channelRead方法,用于对该对象进行处理。 + +通过调用write方法,可以对outbound的 Http2Frame 转换成为http2 frame的格式。 + +## Http2Frame、Http2FrameStream和Http2StreamFrame + +netty中有三个非常类似的类,他们是Http2Frame、Http2FrameStream和Http2StreamFrame。 + +我们知道netty中一个tcp连接可以建立多个stream,Http2FrameStream就是和stream对应的类,这个类中包含了stream的id和stream当前的状态。 + +一个stream中又包含了多个消息,每个消息都是由多个frame组成的,所以Http2Frame是和这些frame对应的netty类。 + +Http2StreamFrame本身也是一个frame,事实上它继承自Http2Frame。 + +为什么会有这个类呢?因为对应frame本身来说,一般情况下它是和一个特定的stream相关联的,Http2StreamFrame表示这种关联关系,可以通过它的set stream方法来指定其关联的stream。如果想要该frame应用到整个连接而不是特定的某个stream,如果是关联到整个连接,那么stream()方法的返回就是null。 + +## Http2FrameCodec的构造 + +虽然Http2FrameCodec有构造函数,但是netty推荐使用Http2FrameCodecBuilder来构造它: + +```java +Http2FrameCodecBuilder.forServer().build(); +``` + +可以看到Http2FrameCodecBuilder有一个forServer还有一个forClient方法。他们一个是使用在服务器端,一个是使用在客户端。 + +主要是通过里面的server属性来进行区分。 + +## Stream的生命周期 + +frame codec将会向有效的stream发送和写入frames。之前讲过了 Http2StreamFrame 是和一个Http2FrameStream对象相关联的。 + +对于一个有效的stream来说,如果任意一方发送一个RST_STREAM frame,那么该stream就会被关闭。 + +或者发送方或者接收方任意一方发送的frame中带有END_STREAM标记,该stream也会被关闭。 + +## 流控制 + +Http2FrameCodec提供了对流的自动控制,但是我们仍然需要做一些操作,来对window进行更新。 + +具体而言,当我们在接收到Http2DataFrame消息的时候,对消息进行处理之后,需要增大window的大小,表示该data已经被处理了,可以有更多的空间去容纳新的数据。 + +也就是说需要向ctx中写入一个Http2WindowUpdateFrame,在这个Http2WindowUpdateFrame中需要传入处理的data的大小和对应stream的id,下面是一个处理data frame的例子: + +```java +/** + * 处理data frame消息 + */ +private static void onDataRead(ChannelHandlerContext ctx, Http2DataFrame data){ + Http2FrameStream stream = data.stream(); + if (data.isEndStream()) { + sendResponse(ctx, stream, data.content()); + } else { + // 不是end stream不发送,但是需要释放引用 + data.release(); + } + // 处理完data,需要更新window frame,增加处理过的Data大小 + ctx.write(new DefaultHttp2WindowUpdateFrame(data.initialFlowControlledBytes()).stream(stream)); +} +``` + +上的例子中,我们向DefaultHttp2WindowUpdateFrame传入了对应的stream id,如果stream id为0,则表示处理的是整个connection,而不是单独的某个stream。 + +除了window update frame之外,对于某个特定stream的初始window还可以发送一个 Http2SettingsFrame,通过设置Http2Settings.initialWindowSize() 达到初始化的目的。 + +## 接收消息 + +对于每个HTTP/2 stream来说,其中包含的frame可以分为 Http2HeadersFrame和Http2DataFrame,而Http2HeadersFrame必定是第一个接收到的frame,并且这个headerFrame还关联了对应的stream对象:Http2FrameStream。 + +所以我们在处理的时候可以针对这两种不同的frame分别进行处理: + +```java +public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (msg instanceof Http2HeadersFrame) { + onHeadersRead(ctx, (Http2HeadersFrame) msg); + } else if (msg instanceof Http2DataFrame) { + onDataRead(ctx, (Http2DataFrame) msg); + } else { + super.channelRead(ctx, msg); + } +} +``` + +# 自定义handler + +如果使用Http2FrameCodec,我们只需要在pipline中添加Http2FrameCodec之后,再添加自定义的handler即可: + +```java +ctx.pipeline().addLast(Http2FrameCodecBuilder.forServer().build(), new CustHttp2Handler()); +``` + +因为Http2FrameCodec已经对http2中的frame进行了转换,所以我们在CustHttp2Handler中只需要处理自定义逻辑即可。 + +netty推荐自定义的handler继承自Http2ChannelDuplexHandler,因为它比普通的ChannelDuplexHandler多了一个创建newStream()和遍历所有有效stream的 forEachActiveStream(Http2FrameStreamVisitor)方法。 + +# 总结 + +本文讲解了Http2FrameCodec的原理,和与其搭配的handler实现中要注意的事项。 + +本文的例子可以参考:[learn-netty4](https://github.com/ddean2009/learn-netty4) \ No newline at end of file diff --git a/learn-netty文章/32.netty系列之手持framecodec神器,创建多路复用http2客户端.md b/learn-netty文章/32.netty系列之手持framecodec神器,创建多路复用http2客户端.md new file mode 100644 index 0000000..d89e50a --- /dev/null +++ b/learn-netty文章/32.netty系列之手持framecodec神器,创建多路复用http2客户端.md @@ -0,0 +1,141 @@ +# netty系列之:手持framecodec神器,创建多路复用http2客户端 + +# 简介 + +在之前的文章中,我们实现了支持http2的netty服务器,并且使用支持http2的浏览器成功的进行访问。虽然浏览器非常通用,但是有时候我们也需要使用特定的netty客户端去和服务器进行通信。 + +今天我们来探讨一下netty客户端对http2的支持。 + +# 配置SslContext + +虽然http2并不强制要求支持TLS,但是现代浏览器都是需要在TLS的环境中开启http2,所以对于客户端来说,同样需要配置好支持http2的SslContext。客户端和服务器端配置SslContext的内容没有太大的区别,唯一的区别就是需要调用SslContextBuilder.forClient()而不是forServer()方法来获取SslContextBuilder,创建SslContext的代码如下: + +```java +SslProvider provider = + SslProvider.isAlpnSupported(SslProvider.OPENSSL)? SslProvider.OPENSSL : SslProvider.JDK; +sslCtx = SslContextBuilder.forClient() + .sslProvider(provider) + .ciphers(Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE) + // 因为我们的证书是自生成的,所以需要信任放行 + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .applicationProtocolConfig(new ApplicationProtocolConfig( + Protocol.ALPN, + SelectorFailureBehavior.NO_ADVERTISE, + SelectedListenerFailureBehavior.ACCEPT, + ApplicationProtocolNames.HTTP_2, + ApplicationProtocolNames.HTTP_1_1)) + .build(); +``` + +如果使用SSL,那么ssl handler必须是pipline中的第一个handler,所以将SslContext加入到pipline中的代码如下: + +```java +ch.pipeline().addFirst(sslCtx.newHandler(ch.alloc())); +``` + +# 客户端的handler + +## 使用Http2FrameCodec + +netty的channel默认只能接收ByteBuf消息,对于http2来说,底层传输的是一个个的frame,直接操作底层的frame对于普通程序员来说并不是特别友好,所以netty提供了一个Http2FrameCodec来对底层的http2 frame进行封装成Http2Frame对象,方便程序的处理。 + +在服务器端我们使用Http2FrameCodecBuilder.forServer()来创建Http2FrameCodec,在客户端我们使用Http2FrameCodecBuilder.forClient()来创建Http2FrameCodec: + +```java +Http2FrameCodec http2FrameCodec = Http2FrameCodecBuilder.forClient() + .initialSettings(Http2Settings.defaultSettings()) + .build(); +``` + +然后将其加入到pipline中即可使用: + +```java +ch.pipeline().addLast(http2FrameCodec); +``` + +## Http2MultiplexHandler和Http2MultiplexCodec + +我们知道对于http2来说一个TCP连接中可以创建多个stream,每个stream又是由多个frame来组成的。考虑到多路复用的情况,netty可以为每一个stream创建一个单独的channel,对于新创建的每个channel来说,都可以使用netty的ChannelInboundHandler来对channel的消息进行处理,从而提升netty处理http2的效率。 + +而这个对stream创建新channel的支持,在netty中有两个专门的类,他们是Http2MultiplexHandler和Http2MultiplexCodec。 + +他们的功能是一样的,Http2MultiplexHandler继承自Http2ChannelDuplexHandler,它必须和 Http2FrameCodec一起使用。而Http2MultiplexCodec本身就是继承自Http2FrameCodec,已经结合了Http2FrameCodec的功能。 + +```java +public final class Http2MultiplexHandler extends Http2ChannelDuplexHandler + + @Deprecated + public class Http2MultiplexCodec extends Http2FrameCodec +``` + +但是通过检查源代码,我们发现Http2MultiplexCodec是不推荐使用的API,所以这里我们主要介绍Http2MultiplexHandler。 + +对于Http2MultiplexHandler来说,每次新创建一个stream,都会创建一个新的对应的channel,应用程序使用这个新创建的channel来发送和接收Http2StreamFrame。 + +新创建的子channel会被注册到netty的EventLoop中,所以对于一个有效的子channel来说,并不是立刻就会被匹配到HTTP/2 stream上去,而是当第一个Http2HeadersFrame成功被发送或者接收之后,才会触发Event事件,进而进行绑定操作。 + +因为是子channel,所以对于connection level的事件,比如Http2SettingsFrame 和 Http2GoAwayFrame会首先被父channel进行处理,然后再广播到子channel中进行处理。 + +同时,虽然Http2GoAwayFrame 和 Http2ResetFrame表示远程节点已经不再接收新的frame了,但是因为channel本身还可能有queue的消息,所以需要等待Channel.read()为空之后,才会进行关闭操作。 + +另外对于子channel来说,因为不能知道connection-level流控制window,所以如果有溢出的消息会被缓存在父channel的buff中。 + +有了Http2MultiplexHandler,将其加入client的pipline就可以让客户端支持多路的channel了: + +```java +ch.pipeline().addLast(new Http2MultiplexHandler(new SimpleChannelInboundHandler() { + @Override + protected void channelRead0(ChannelHandlerContext ctx, Object msg) { + // 处理inbound streams + log.info("Http2MultiplexHandler接收到消息: {}",msg); + } +})) +``` + +## 使用子channel发送消息 + +从上面的介绍我们知道,一旦使用了Http2MultiplexHandler,那么具体的消息处理就是在子channel中了。那么怎么才能从父channel中获取子channel,然后使用子channel来发送信息呢? + +netty提供Http2StreamChannelBootstrap类,它提供了open方法,来创建子channel: + +```java +final Http2StreamChannel streamChannel; +try { + if (ctx.handler() instanceof Http2MultiplexCodec) { + streamChannel = ((Http2MultiplexCodec) ctx.handler()).newOutboundStream(); + } else { + streamChannel = ((Http2MultiplexHandler) ctx.handler()).newOutboundStream(); + } +``` + +我们要做的就是调用这个方法,来创建子channel: + +```java +final Http2StreamChannel streamChannel = streamChannelBootstrap.open().syncUninterruptibly().getNow(); +``` + +然后将自定义的,专门处理Http2StreamFrame的Http2ClientStreamFrameHandler,添加到子channel的pipline中即可: + +```java +final Http2ClientStreamFrameHandler streamFrameResponseHandler = + new Http2ClientStreamFrameHandler(); +streamChannel.pipeline().addLast(streamFrameResponseHandler); +``` + +准备完毕,构建http2消息,使用streamChannel进行发送: + +```java +// 发送HTTP2 get请求 +final DefaultHttp2Headers headers = new DefaultHttp2Headers(); +headers.method("GET"); +headers.path(PATH); +headers.scheme(SSL? "https" : "http"); +Http2HeadersFrame headersFrame = new DefaultHttp2HeadersFrame(headers, true); +streamChannel.writeAndFlush(headersFrame); +``` + +# 总结 + +以上就是使用netty的framecode构建http2的客户端和服务器端进行通信的基本操作了。 + +本文的例子可以参考:[learn-netty4](https://github.com/ddean2009/learn-netty4) \ No newline at end of file diff --git a/learn-netty文章/33.netty系列之性能为王!创建多路复用http2服务器.md b/learn-netty文章/33.netty系列之性能为王!创建多路复用http2服务器.md new file mode 100644 index 0000000..75e2b8b --- /dev/null +++ b/learn-netty文章/33.netty系列之性能为王!创建多路复用http2服务器.md @@ -0,0 +1,103 @@ +# netty系列之:性能为王!创建多路复用http2服务器 + +# 简介 + +在之前的文章中,我们提到了在netty的客户端通过使用Http2FrameCodec和Http2MultiplexHandler可以支持多路复用,也就是说在一个连接的channel基础上创建多个子channel,通过子channel来处理不同的stream,从而达到多路复用的目的。 + +既然客户端可以做到多路复用,同样的服务器端也可以,今天给大家介绍一下如何在netty的服务器端打造一个支持http2协议的多路复用服务器。 + +# 多路复用的基础 + +netty中对于http2多路复用的基础类是Http2FrameCodec、Http2MultiplexHandler和Http2MultiplexCodec。 + +Http2FrameCodec是将底层的HTTP/2 frames消息映射成为netty中的Http2Frame对象。 + +有了Http2Frame对象就可以通过Http2MultiplexHandler对新创建的stream开启不同的channel。 + +Http2MultiplexCodec是Http2FrameCodec和Http2MultiplexHandler的结合体,但是已经不再被推荐使用了。 + +因为Http2FrameCodec继承自Http2ConnectionHandler,而Http2MultiplexHandler继承自Http2ChannelDuplexHandler,所以这两个类可以同时在客户端和服务器端使用。 + +客户端使用Http2FrameCodecBuilder.forClient().build()来获得Http2FrameCodec,而服务器端通过Http2FrameCodecBuilder.forServer().build()来获得Http2FrameCodec。 + +# 多路复用在server端的使用 + +## 配置TLS处理器 + +对于服务器端,同样需要处理TLS和普通clear text两种情况。对于TLS来说,我们需要自建ProtocolNegotiationHandler继承自ApplicationProtocolNegotiationHandler,然后实现configurePipeline方法,在其中分别处理http2和http1.1的连接: + +```java +protected void configurePipeline(ChannelHandlerContext ctx, String protocol) { + if (ApplicationProtocolNames.HTTP_2.equals(protocol)) { + //添加多路复用支持 + ctx.pipeline().addLast(Http2FrameCodecBuilder.forServer().build()); + ctx.pipeline().addLast(new Http2MultiplexHandler(new CustMultiplexHttp2Handler())); + return; + } + + if (ApplicationProtocolNames.HTTP_1_1.equals(protocol)) { + ctx.pipeline().addLast(new HttpServerCodec(), + new HttpObjectAggregator(MAX_CONTENT_LENGTH), + new CustHttp1Handler("ALPN Negotiation")); + return; + } + + throw new IllegalStateException("未知协议: " + protocol); +} +``` + +首先添加Http2FrameCodec,然后添加Http2MultiplexHandler。因为Http2MultiplexHandler已经封装了多路复用的细节,所以自定义的handler只需要实现正常的消息处理逻辑即可。 + +因为Http2FrameCodec已经对消息进行了转换成为HTTP2Frame对象,所以只需要处理具体的Frame对象: + +```java +public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (msg instanceof Http2HeadersFrame) { + onHeadersRead(ctx, (Http2HeadersFrame) msg); + } else if (msg instanceof Http2DataFrame) { + onDataRead(ctx, (Http2DataFrame) msg); + } else { + super.channelRead(ctx, msg); + } +} +``` + +## 配置clear text upgrade + +对于h2c的升级来说,需要向pipline中传入sourceCodec和upgradeHandler两个处理器。 + +sourceCodec可以直接使用HttpServerCodec。 + +upgradeHandler可以使用HttpServerUpgradeHandler。 + +HttpServerUpgradeHandler的构造函数需要传入一个sourceCodec和一个upgradeCodecFactory。 + +sourceCodec我们已经有了,再构造一个upgradeCodecFactory即可: + +```java +private static final UpgradeCodecFactory upgradeCodecFactory = protocol -> { + if (AsciiString.contentEquals(Http2CodecUtil.HTTP_UPGRADE_PROTOCOL_NAME, protocol)) { + return new Http2ServerUpgradeCodec( + Http2FrameCodecBuilder.forServer().build(), + new Http2MultiplexHandler(new CustMultiplexHttp2Handler())); + } else { + return null; + } +}; +``` + +从代码中可以看出,upgradeCodecFactory内部又调用了Http2FrameCodec和Http2MultiplexHandler。这和使用TLS的处理器是一致的。 + +```java +final ChannelPipeline p = ch.pipeline(); +final HttpServerCodec sourceCodec = new HttpServerCodec(); +p.addLast(sourceCodec); +p.addLast(new HttpServerUpgradeHandler(sourceCodec, upgradeCodecFactory)); +``` + +# 总结 + +通过上述方式,就可以创建出支持多路复用的http2 netty服务器了。 + +本文的例子可以参考:[learn-netty4](https://github.com/ddean2009/learn-netty4) + diff --git a/learn-netty文章/34.netty系列之一个价值上亿的网站速度优化方案.md b/learn-netty文章/34.netty系列之一个价值上亿的网站速度优化方案.md new file mode 100644 index 0000000..17032a6 --- /dev/null +++ b/learn-netty文章/34.netty系列之一个价值上亿的网站速度优化方案.md @@ -0,0 +1,152 @@ +# netty系列之:一个价值上亿的网站速度优化方案 + +# 简介 + +其实软件界最赚钱的不是写代码的,写代码的只能叫马龙,高级点的叫做程序员,都是苦力活。那么有没有高大上的职业呢?这个必须有,他们的名字就叫做咨询师。 + +咨询师就是去帮企业做方案、做架构、做优化的,有时候一个简单的代码改动、一个架构的调整都可以让软件或者流程更加高效的运行,从而为企业节省上亿的开支。 + +今天除了要给大家介绍一下如何在netty中同时支持http和https协议之外,还给大家介绍一个价值上亿的网站数据优化方案,有了这个方案,年薪百万不是梦! + +# 本文的目标 + +本文将会给大家介绍一下如何在一个netty服务中同时支持http和http2两种协议,在这两个服务器中,提供了对多图片的访问支持,我们介绍如何从服务器端返回多个图片。最后介绍一个价值上亿的速度优化方案,肯定大家会受益匪浅。 + +# 支持多个图片服务 + +对于服务器端来说,是通过ServerBootstrap来启动服务的,ServerBootstrap有一个group方法用来指定acceptor的group和client的group。 + +```java +public ServerBootstrap group(EventLoopGroup group); +public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup); +``` + +当然你可以指定两个不同的group,也可以指定同一个group,它提供了两个group方法,效果上没太大的区别。 + +这里我们现在主服务器中创建一个EventLoopGroup,然后将其传入到ImageHttp1Server和ImageHttp2Server中。然后分别在两个server中调用group方法,然后配置handler即可。 + +先看一下ImageHttp1Server的构造: + +```java +ServerBootstrap b = new ServerBootstrap(); +b.option(ChannelOption.SO_BACKLOG, 1024); +b.group(group).channel(NioServerSocketChannel.class).handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel ch){ + ch.pipeline().addLast(new HttpRequestDecoder(), + new HttpResponseEncoder(), + new HttpObjectAggregator(MAX_CONTENT_LENGTH), + new Http1RequestHandler()); + } + }); +``` + +我们传入了netty自带的HttpRequestDecoder、HttpResponseEncoder和HttpObjectAggregator。还有一个自定义的Http1RequestHandler。 + +再看一下ImageHttp2Server的构造: + +```java +ServerBootstrap b = new ServerBootstrap(); +b.option(ChannelOption.SO_BACKLOG, 1024); +b.group(group).channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel ch) { + ch.pipeline().addLast(sslCtx.newHandler(ch.alloc()), new CustProtocolNegotiationHandler()); + } +}); +``` + +为了简单起见,我们默认如果从http来访问的话,就使用http1服务,如果是从https来访问的话,就使用http2服务。 + +所以在http2服务中,我们只需要自定义ProtocolNegotiationHandler即可,而不用处理clear text升级的请求。 + +# http2处理器 + +在TLS环境中,我们通过自定义CustProtocolNegotiationHandler,继承自ApplicationProtocolNegotiationHandler来进行客户端和服务器端协议的交互。 + +对于http2协议来说,使用了netty自带的InboundHttp2ToHttpAdapterBuilder和HttpToHttp2ConnectionHandlerBuilder将http2 frame转换成为http1的FullHttpRequest对象。这样我们直接处理http1格式的消息即可。 + +转换过程如下: + +```java +DefaultHttp2Connection connection = new DefaultHttp2Connection(true); +InboundHttp2ToHttpAdapter listener = new InboundHttp2ToHttpAdapterBuilder(connection) + .propagateSettings(true).validateHttpHeaders(false) + .maxContentLength(MAX_CONTENT_LENGTH).build(); + +ctx.pipeline().addLast(new HttpToHttp2ConnectionHandlerBuilder() + .frameListener(listener) + .connection(connection).build()); + +ctx.pipeline().addLast(new Http2RequestHandler()); +``` + +转换转换的http2 handler和普通的http1的handler唯一不同的是需要额外设置一个streamId属性到请求头和响应头中。 + +并且不需要处理http1特有的100-continue和KeepAlive。其他的和http1 handler没什么两样。 + +# 处理页面和图像 + +因为我们使用转换器将http2的frame转换成了http1的普通对象,所以对请求相应的页面和图像来说,跟http1的处理没什么太大区别。 + +对于页面来说,我们需要获取要返回的html,然后设置CONTENT_TYPE为”text/html; charset=UTF-8″,返回即可: + +```java +private void handlePage(ChannelHandlerContext ctx, String streamId, FullHttpRequest request) throws IOException { + ByteBuf content =ImagePage.getContent(); + FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK, content); + response.headers().set(CONTENT_TYPE, "text/html; charset=UTF-8"); + sendResponse(ctx, streamId, response, request); +} +``` + +对于图像来说,我们获取到要返回的图像,将其转换成为ByteBuf,然后设置CONTENT_TYPE为”image/jpeg”,返回即可: + +```java +private void handleImage(String id, ChannelHandlerContext ctx, String streamId, + FullHttpRequest request) { + ByteBuf image = ImagePage.getImage(parseInt(id)); + FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK, image); + response.headers().set(CONTENT_TYPE, "image/jpeg"); + sendResponse(ctx, streamId, response, request); +} +``` + +这样,我们就能够在netty服务器端同时处理页面请求和图片请求了。 + +# 价值上亿的速度优化方案 + +终于要到本文中最精彩的部分了,价值上亿的速度优化方案是什么呢? + +在讲这个方案之前,先给大家讲一个抗洪抢险的故事。有两个县都住在一条大河的旁边。这条大河很不安稳,经常会发洪灾,但是两个县的县长做法很不同。 + +A县的县长认真负责,派人定期巡逻检查所属的河段,筑堤、种树、巡视,一刻都不放松,在他的任期平平安安,没有发生任何洪水溃堤的情况。 + +B县的县长从来不巡检,一道河水泛滥的时候,B县长就组织人抗洪抢险,然后媒体全都报道的是B县长抗洪的丰功伟绩,最后B县长由于政绩突出,升任市长。 + +好了,故事讲完了,接下来是我们的优化。不管是用户请求页面还是图片,最终都需要调用ctx.writeAndFlush(response)方法进行响应回写。 + +如果将其放入一个定时任务中,来定时执行,如下所示: + +```java +ctx.executor().schedule(new Runnable() { + @Override + public void run() { + ctx.writeAndFlush(response); + } +}, latency, TimeUnit.MILLISECONDS); +``` + +那么服务器在经过latency指定的毫秒之后,才会发送对应的响应。比如这里我们设置latency的值为5秒。 + +当然5秒是不能够让人满意的,于是领导或者客户找到你,说让你给优化一下。你说这个性能问题是很难的,涉及到了麦克斯韦方程组和热力学第三定律,需要一个月时间。领导说好,撸起袖子加油干,下个月给你工资涨50%。 + +一个月后,你把latency改成2.5秒,性能提升了100%,这个优化值不值几个亿? + +# 总结 + +当然,上一节给大家开个玩笑,不过在netty响应中使用定时任务的技巧,大家也应该牢牢掌握,原因你懂的! + +本文的例子可以参考:[learn-netty4](https://github.com/ddean2009/learn-netty4) + diff --git a/learn-netty文章/35.netty系列之小白福利!手把手教你做一个简单的代理服务器.md b/learn-netty文章/35.netty系列之小白福利!手把手教你做一个简单的代理服务器.md new file mode 100644 index 0000000..55563ae --- /dev/null +++ b/learn-netty文章/35.netty系列之小白福利!手把手教你做一个简单的代理服务器.md @@ -0,0 +1,121 @@ +# netty系列之:小白福利!手把手教你做一个简单的代理服务器 + +# 简介 + +爱因斯坦说过:所有的伟大,都产生于简单的细节中。netty为我们提供了如此强大的eventloop、channel通过对这些简单东西的有效利用,可以得到非常强大的应用程序,比如今天要讲的代理。 + +# 代理和反向代理 + +相信只要是程序员应该都听过nginx服务器了,这个超级优秀nginx一个很重要的功能就是做反向代理。那么有小伙伴要问了,有反向代理肯定就有正向代理,那么他们两个有什么区别呢? + +先讲一下正向代理,举个例子,最近流量明星备受打击,虽然被打压,但是明星就是明星,一般人是见不到的,如果有人需要跟明星对话的话,需要首先经过明星的经纪人,有经纪人将话转达给明星。这个经纪人就是正向代理。我们通过正向代理来访问要访问的对象。 + +那么什么是反向代理呢?比如现在出现了很多人工智能,假如我们跟智能机器人A对话,然后A把我们之间的对话转给了后面的藏着的人,这个人用他的智慧,回答了我们的对话,交由智能机器人A输出,最终实现了人工智能。这个过程就叫做反向代理。 + +# netty实现代理的原理 + +那么在netty中怎么实现这个代理服务器呢? + +首选我们首先代理服务器是一个服务器,所以我们需要在netty中使用ServerBootstrap创建一个服务器: + +```java +EventLoopGroup bossGroup = new NioEventLoopGroup(1); +EventLoopGroup workerGroup = new NioEventLoopGroup(); +ServerBootstrap b = new ServerBootstrap(); +b.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new SimpleDumpProxyInitializer(REMOTE_HOST, REMOTE_PORT)) + .childOption(ChannelOption.AUTO_READ, false) + .bind(LOCAL_PORT).sync().channel().closeFuture().sync(); +``` + +在这个local服务器中,我们传入ProxyInitializer。在这个handler初始化器中,我们传入自定义的handler: + +```java +public void initChannel(SocketChannel ch) { + ch.pipeline().addLast( + new LoggingHandler(LogLevel.INFO), + new SimpleDumpProxyInboundHandler(remoteHost, remotePort)); +} +``` + +在自定义的handler中,我们使用Bootstrap创建一个client,用来连接远程要代理的服务器,我们将这个client端的创建放在channelActive方法中: + +```java +// 开启outbound连接 +Bootstrap b = new Bootstrap(); +b.group(inboundChannel.eventLoop()) + .channel(ctx.channel().getClass()) + .handler(new SimpleDumpProxyOutboundHandler(inboundChannel)) + .option(ChannelOption.AUTO_READ, false); +ChannelFuture f = b.connect(remoteHost, remotePort); +``` + +然后在client建立好连接之后,就可以从inboundChannel中读取数据了: + +```java +outboundChannel = f.channel(); +f.addListener(future -> { + if (future.isSuccess()) { + // 连接建立完毕,读取inbound数据 + inboundChannel.read(); + } else { + // 关闭inbound channel + inboundChannel.close(); + } +}); +``` + +因为是代理服务,所以需要将inboundChannel读取的数据,转发给outboundChannel,所以在channelRead中我们需要这样写: + +```java +public void channelRead(final ChannelHandlerContext ctx, Object msg) { + // 将inboundChannel中的消息读取,并写入到outboundChannel + if (outboundChannel.isActive()) { + outboundChannel.writeAndFlush(msg).addListener((ChannelFutureListener) future -> { + if (future.isSuccess()) { + // flush成功,读取下一个消息 + ctx.channel().read(); + } else { + future.channel().close(); + } + }); + } +} +``` + +当outboundChannel写成功之后,再继续inboundChannel的读取工作。 + +同样对于client的outboundChannel来说,也有一个handler,在这个handler中,我们需要将outboundChannel读取到的数据反写会inboundChannel中: + +``` java +public void channelRead(final ChannelHandlerContext ctx, Object msg) { + // 将outboundChannel中的消息读取,并写入到inboundChannel中 + inboundChannel.writeAndFlush(msg).addListener((ChannelFutureListener) future -> { + if (future.isSuccess()) { + ctx.channel().read(); + } else { + future.channel().close(); + } + }); +} +``` + +当inboundChannel写成功之后,再继续outboundChannel的读取工作。 + +如此一个简单的代理服务器就完成了。 + +# 实战 + +如果我们将本地的8000端口,代理到www.163.com的80端口,会发生什么情况呢?运行我们的程序,访问http://localhost:8000, 我们会看到下面的页面: + +![img](f025f1e1e9d94e8db5b0e0979e136116.png) + +为什么没有如我们想象的那样展示正常的页面呢?那是因为我们代理过去之后的域名是localhost,而不是正常的www.163.com, 所以服务器端不认识我们的请求,从而报错。 + +# 总结 + +本文的代理服务器之间简单的转发请求,并不能够处理上述的场景,那么该怎么解决上面的问题呢? 敬请期待我的后续文章! + +本文的例子可以参考:[learn-netty4](https://github.com/ddean2009/learn-netty4) \ No newline at end of file diff --git a/learn-netty文章/36.netty系列之从零到壹,搭建一个SOCKS代理服务器.md b/learn-netty文章/36.netty系列之从零到壹,搭建一个SOCKS代理服务器.md new file mode 100644 index 0000000..7b5d908 --- /dev/null +++ b/learn-netty文章/36.netty系列之从零到壹,搭建一个SOCKS代理服务器.md @@ -0,0 +1,207 @@ +# netty系列之:从零到壹,搭建一个SOCKS代理服务器 + +# 简介 + +上一篇文章,我们讲到了netty对SOCKS消息提供了SocksMessage对象的封装,并且区分SOCKS4和SOCKS5,同时提供了连接和响应的各种状态。 + +有了SOCKS消息的封装之后,我们还需要做些什么工作才能搭建一个SOCKS服务器呢? + +# 使用SSH搭建SOCKS服务器 + +其实最简单的办法就是使用SSH工具来建立SOCKS代理服务器。 + +先看下SSH建立SOCKS服务的命令: + +```shell +ssh -f -C -N -D bindaddress:port name@server +``` + +-f 表示SSH作为守护进程进入后台执行。 + +-N 表示不执行远程命令,只用于端口转发。 + +-D 表示是端口上的动态转发。这个命令支持SOCKS4和SOCKS5。 + +-C 表示发送前压缩数据。 + +bindaddress 本地服务器的绑定地址。 + +port 表示本地服务器的指定侦听端口。 + +name 表示ssh服务器登录名。 + +server表示ssh服务器地址。 + +上面命令的意思是,在本机建立端口绑定,然后将其转发到远程的代理服务器上。 + +比如我们可以在本机开一个2000的端口,将其转发到远程168.121.100.23这台机子上: + +```shell +ssh -f -N -D 0.0.0.0:2000 root@168.121.100.23 +``` + +有了代理服务器之后,就可以使用了,首先介绍一个怎么在curl命令中使用SOCKS代理。 + +我们想通过代理服务器,访问www.flydean.com,该怎么做呢? + +```shell +curl -x socks5h://localhost:2000 -v -k -X GET http://www.flydean.com:80 +``` + +要想检测SOCKS的连接,还可以使用netcat命令如下: + +```shell +ncat –proxy 127.0.0.1:2000 –proxy-type socks5 www.flydean.com 80 -nv +``` + +# 使用netty搭建SOCKS服务器 + +使用netty搭建SOCKS服务器的关键是使用netty服务器做中继,它需要建立两个连接,一个是客户端到代理服务器的连接,一个是代理服务器到目标地址的连接。接下来,我们一步一步探讨如何在netty中构建SOCKS服务器。 + +搭建服务器的基本步骤和普通的服务器基本一致,要注意的就是对消息的编码、解码和在消息读取处理过程中的转发。 + +## encoder和decoder + +对于一种协议来说,最终要的就是对应的encoder和decoder,用于协议对象和ByteBuf之间进行转换。 + +netty提供的SOCKS转换器叫做SocksPortUnificationServerHandler。先看下它的定义: + +```java +public class SocksPortUnificationServerHandler extends ByteToMessageDecoder +``` + +它继承自ByteToMessageDecoder表示是ByteBuf和Socks对象之间的转换。 + +所以我们在ChannelInitializer中只需要加上SocksPortUnificationServerHandler和自定义的处Socks消息的handler即可: + +```java +public void initChannel(SocketChannel ch) throws Exception { + ch.pipeline().addLast( + new LoggingHandler(LogLevel.DEBUG), + new SocksPortUnificationServerHandler(), + SocksServerHandler.INSTANCE); +} +``` + +等等,不对呀!有细心的小伙伴可能发现了,SocksPortUnificationServerHandler只是一个decoder,我们还缺少一个encoder,用来将Socks对象转换成本ByteBuf,这个encoder在哪里呢? + +别急,我们再回到SocksPortUnificationServerHandler中,在它的decode方法中,有这样一段代码: + +```java +case SOCKS4a: + logKnownVersion(ctx, version); + p.addAfter(ctx.name(), null, Socks4ServerEncoder.INSTANCE); + p.addAfter(ctx.name(), null, new Socks4ServerDecoder()); + break; +case SOCKS5: + logKnownVersion(ctx, version); + p.addAfter(ctx.name(), null, socks5encoder); + p.addAfter(ctx.name(), null, new Socks5InitialRequestDecoder()); + break; +``` + +原来是在decode方法里面,根据Socks的版本不同,给ctx添加了对应的encoder和decoder,非常的巧妙。 + +对应的encoder分别是Socks4ServerEncoder和Socks5ServerEncoder。 + +# 建立连接 + +对于Socks4来说,只有一个建立连接的请求类型,在netty中用Socks4CommandRequest来表示。 + +所以我们只需要在channelRead0中判断请求的版本即可: + +```java +case SOCKS4a: + Socks4CommandRequest socksV4CmdRequest = (Socks4CommandRequest) socksRequest; + if (socksV4CmdRequest.type() == Socks4CommandType.CONNECT) { + ctx.pipeline().addLast(new SocksServerConnectHandler()); + ctx.pipeline().remove(this); + ctx.fireChannelRead(socksRequest); + } else { + ctx.close(); + } +``` + +这里我们添加了一个自定义的SocksServerConnectHandler,用来处理Socks连接,这个自定义handler会在后面进行详细讲解,这里大家知道它使用来建立连接即可。 + +对于Socks5来说,就比较复杂点,包含了初始化请求、认证请求和建立连接三个部分,所以需要分别处理: + +```java +case SOCKS5: + if (socksRequest instanceof Socks5InitialRequest) { + ctx.pipeline().addFirst(new Socks5CommandRequestDecoder()); + ctx.write(new DefaultSocks5InitialResponse(Socks5AuthMethod.NO_AUTH)); + } else if (socksRequest instanceof Socks5PasswordAuthRequest) { + ctx.pipeline().addFirst(new Socks5CommandRequestDecoder()); + ctx.write(new DefaultSocks5PasswordAuthResponse(Socks5PasswordAuthStatus.SUCCESS)); + } else if (socksRequest instanceof Socks5CommandRequest) { + Socks5CommandRequest socks5CmdRequest = (Socks5CommandRequest) socksRequest; + if (socks5CmdRequest.type() == Socks5CommandType.CONNECT) { + ctx.pipeline().addLast(new SocksServerConnectHandler()); + ctx.pipeline().remove(this); + ctx.fireChannelRead(socksRequest); + } else { + ctx.close(); + } +``` + +注意,这里我们的认证请求只支持用户名密码认证。 + +# ConnectHandler + +既然是作为一个代理服务器,就需要建立两个连接,一个是客户端到代理服务器的连接,一个是代理服务器到目标服务器的连接。 + +对于netty来说,这两个连接可以用两个Bootstrap来建立。 + +其中客户端到代理服务器端的连接我们在启动netty服务器的时候已经建立了,所以需要在ConnectHandler中,建立一个新的代理服务器到目标服务器的连接: + +```java +private final Bootstrap b = new Bootstrap(); + +Channel inboundChannel = ctx.channel(); +b.group(inboundChannel.eventLoop()) + .channel(NioSocketChannel.class) + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000) + .option(ChannelOption.SO_KEEPALIVE, true) + .handler(new ClientPromiseHandler(promise)); + +b.connect(request.dstAddr(), request.dstPort()).addListener(future -> { + if (future.isSuccess()) { + // 成功建立连接 + } else { + // 关闭连接 + ctx.channel().writeAndFlush( + new DefaultSocks4CommandResponse(Socks4CommandStatus.REJECTED_OR_FAILED) + ); + closeOnFlush(ctx.channel()); + } +}); +``` + +新的Bootstrap需要从接收到的Socks消息中取出目标服务器的地址和端口,然后建立连接。 + +然后判断新建立连接的状态,如果成功则添加一个转发器将outboundChannel的消息转发到inboundChannel中,同时将inboundChannel的消息转发到outboundChannel中,从而达到服务器代理的目的。 + +```java +final Channel outboundChannel = future.getNow(); +if (future.isSuccess()) { + ChannelFuture responseFuture = ctx.channel().writeAndFlush( + new DefaultSocks4CommandResponse(Socks4CommandStatus.SUCCESS)); + //成功建立连接,删除SocksServerConnectHandler,添加RelayHandler + responseFuture.addListener(channelFuture -> { + ctx.pipeline().remove(SocksServerConnectHandler.this); + outboundChannel.pipeline().addLast(new RelayHandler(ctx.channel())); + ctx.pipeline().addLast(new RelayHandler(outboundChannel)); + }); +} else { + ctx.channel().writeAndFlush( + new DefaultSocks4CommandResponse(Socks4CommandStatus.REJECTED_OR_FAILED)); + closeOnFlush(ctx.channel()); +} +``` + +# 总结 + +说白了,代理服务器就是建立两个连接,将其中一个连接的消息转发给另外一个连接。这种操作在netty中是非常简便的。 + +本文的例子可以参考:[learn-netty4](https://github.com/ddean2009/learn-netty4) \ No newline at end of file diff --git a/learn-netty文章/37.netty系列之netty对SOCKS协议的支持.md b/learn-netty文章/37.netty系列之netty对SOCKS协议的支持.md new file mode 100644 index 0000000..5e36faa --- /dev/null +++ b/learn-netty文章/37.netty系列之netty对SOCKS协议的支持.md @@ -0,0 +1,162 @@ +# netty系列之:netty对SOCKS协议的支持 + +# 简介 + +SOCKS是一个优秀的网络协议,主要被用来做代理,它的两个主要版本是SOCKS4和SOCKS5,其中SOCKS5提供了对认证的支持。通常来说我们使用SSH工具可以构建简单的SOCKS协议通道,那么对于netty来说,是怎么提供对SOCKS的支持呢?一起来看看吧。 + +# SocksMessage + +首先是代表SOCKS消息对象的SocksMessage。SocksMessage是一个接口,它里面只有一个返回SocksVersion的version方法。 + +SocksVersion表示的是Socks的版本号。在netty中,支持三个版本,分别是: + +```java +SOCKS4a((byte) 0x04), + +SOCKS5((byte) 0x05), + +UNKNOWN((byte) 0xff); +``` + +其对应的数值是SOCKS协议中的VER字段,我们以SOCKS4协议为例,再复习一下SOCKS的协议结构: + +| 含义 | VER | CMD | DSTPORT | DSTIP | ID | +| :------: | :--: | :--: | :-----: | :---: | :--: | +| 字节个数 | 1 | 1 | 2 | 4 | 可变 | + +既然netty中SOCKS有两个版本,相对于的SocksMessage接口就有两个实现,分别是Socks4Message和Socks5Message。 + +## Socks4Message + +Socks4Messag继承自SocksMessage,表示的是SOCKS4的消息。 + +事实上,Socks4Messag是一个tag interface,它里面什么内容都没有。 + +```java +public interface Socks4Message extends SocksMessage { + // Tag interface +} +``` + +对于SOCKS4来说,有两种数据请求类型,分别是CONNECT和BIND,这两种请求类型被定义在Socks4CommandType中: + +```java +public static final Socks4CommandType CONNECT = new Socks4CommandType(0x01, "CONNECT"); +public static final Socks4CommandType BIND = new Socks4CommandType(0x02, "BIND"); +``` + +有请求就有响应,对应的有两个类,分别是Socks4CommandRequest和Socks4CommandResponse。 + +对于Request来说,我们需要请求类型,USERID,DSTIP和DSTPORT这几个数据: + +```java +Socks4CommandType type(); + +String userId(); + +String dstAddr(); + +int dstPort(); +``` + +对于响应来说,有四个不同的状态,分别是SUCCESS、REJECTED_OR_FAILED、IDENTD_UNREACHABLE、IDENTD_AUTH_FAILURE。 + +```java +public static final Socks4CommandStatus SUCCESS = new Socks4CommandStatus(0x5a, "SUCCESS"); +public static final Socks4CommandStatus REJECTED_OR_FAILED = new Socks4CommandStatus(0x5b, "REJECTED_OR_FAILED"); +public static final Socks4CommandStatus IDENTD_UNREACHABLE = new Socks4CommandStatus(0x5c, "IDENTD_UNREACHABLE"); +public static final Socks4CommandStatus IDENTD_AUTH_FAILURE = new Socks4CommandStatus(0x5d, "IDENTD_AUTH_FAILURE"); +``` + +除了Socks4CommandStatus之外,响应请求还有DSTIP和DSTPORT两个属性。 + +```java +Socks4CommandStatus status(); + +String dstAddr(); + +int dstPort(); +``` + +## Socks5Message + +同样的,对于SOCKS5来说,也有一个对应的接口Socks5Message,这个接口也是一个Tag interface,它里面什么都没有: + +```java +public interface Socks5Message extends SocksMessage { + // Tag interface +} +``` + +对于SOCKS5来说,它的请求要比SOKCS4要复杂,首先的请求是一个初始化请求Socks5InitialRequest,该请求包含了可以接受的认证列表。 + +这个列表用Socks5AuthMethod来表示,它包含4个方法: + +```java +public static final Socks5AuthMethod NO_AUTH = new Socks5AuthMethod(0x00, "NO_AUTH"); +public static final Socks5AuthMethod GSSAPI = new Socks5AuthMethod(0x01, "GSSAPI"); +public static final Socks5AuthMethod PASSWORD = new Socks5AuthMethod(0x02, "PASSWORD"); +public static final Socks5AuthMethod UNACCEPTED = new Socks5AuthMethod(0xff, "UNACCEPTED"); +``` + +对于Socks5InitialRequest来说,它包含了一个authMethods的列表: + +```java +public interface Socks5InitialRequest extends Socks5Message { + List authMethods(); +} +``` + +对于InitialRequest来说,对应的也有Socks5InitialResponse,它包含了服务端选择的Socks5AuthMethod,所以对Socks5InitialResponse来说,它里面只包含了一个Socks5AuthMethod: + +```java +public interface Socks5InitialResponse extends Socks5Message { + + Socks5AuthMethod authMethod(); +} +``` + +客户端和服务器端协商好选择的认证协议之后,接下来就是认证的过程,如果使用的是用户名密码的模式,则对应的是Socks5PasswordAuthRequest: + +```java +public interface Socks5PasswordAuthRequest extends Socks5Message { + + String username(); + + String password(); +} +``` + +password认证的结果只有两种结果,分别是SUCCESS和FAILURE: + +```java +public static final Socks5PasswordAuthStatus SUCCESS = new Socks5PasswordAuthStatus(0x00, "SUCCESS"); +public static final Socks5PasswordAuthStatus FAILURE = new Socks5PasswordAuthStatus(0xFF, "FAILURE"); +``` + +对于Socks5PasswordAuthResponse来说,它包含了一个认证的status:Socks5PasswordAuthStatus。 + +认证完毕之后,接下来就可以发送CommandRequest了。对应的Socks5CommandRequest包含下面几个属性: + +```java +Socks5CommandType type(); + +Socks5AddressType dstAddrType(); + +String dstAddr(); + +int dstPort(); +``` + +对应的Socks5CommandResponse包含下面的属性: + +```java +Socks5CommandStatus status(); +Socks5AddressType bndAddrType(); +String bndAddr(); +int bndPort(); +``` + +# 总结 + +以上就是netty对SOCKS4和SOCKS5协议的消息封装。基本上netty中的对象是和SOCKS协议一致的。 \ No newline at end of file diff --git a/learn-netty文章/38.netty系列之一口多用,使用同一端口运行不同协议.md b/learn-netty文章/38.netty系列之一口多用,使用同一端口运行不同协议.md new file mode 100644 index 0000000..dabd3fe --- /dev/null +++ b/learn-netty文章/38.netty系列之一口多用,使用同一端口运行不同协议.md @@ -0,0 +1,133 @@ +# netty系列之:一口多用,使用同一端口运行不同协议 + +# 简介 + +在之前的文章中,我们介绍了在同一个netty程序中支持多个不同的服务,它的逻辑很简单,就是在一个主程序中启动多个子程序,每个子程序通过一个BootStrap来绑定不同的端口,从而达到访问不同端口就访问了不同服务的目的。 + +但是多个端口虽然区分度够高,但是使用起来还是有诸多不便,那么有没有可能只用一个端口来统一不同的协议服务呢? + +今天给大家介绍一下在netty中使用同一端口运行不同协议的方法,这种方法叫做port unification。 + +# SocksPortUnificationServerHandler + +在讲解自定义port unification之前,我们来看下netty自带的port unification,比如SocksPortUnificationServerHandler。 + +我们知道SOCKS的主要协议有3中,分别是SOCKS4、SOCKS4a和SOCKS5,他们属于同一种协议的不同版本,所以肯定不能使用不同的端口,需要在同一个端口中进行版本的判断。 + +具体而言,SocksPortUnificationServerHandler继承自ByteToMessageDecoder,表示是将ByteBuf转换成为对应的Socks对象。 + +那他是怎么区分不同版本的呢? + +在decode方法中,传入了要解码的ByteBuf in,首先获得它的readerIndex: + +```java +int readerIndex = in.readerIndex(); +``` + +我们知道SOCKS协议的第一个字节表示的是版本,所以从in ByteBuf中读取第一个字节作为版本号: + +```java +byte versionVal = in.getByte(readerIndex); +``` + +有了版本号就可以通过不同的版本号进行处理,具体而言,对于SOCKS4a,需要添加Socks4ServerEncoder和Socks4ServerDecoder: + +```java +case SOCKS4a: + logKnownVersion(ctx, version); + p.addAfter(ctx.name(), null, Socks4ServerEncoder.INSTANCE); + p.addAfter(ctx.name(), null, new Socks4ServerDecoder()); + break; +``` + +对于SOCKS5来说,需要添加Socks5ServerEncoder和Socks5InitialRequestDecoder两个编码和解码器: + +```java +case SOCKS5: + logKnownVersion(ctx, version); + p.addAfter(ctx.name(), null, socks5encoder); + p.addAfter(ctx.name(), null, new Socks5InitialRequestDecoder()); + break; +``` + +这样,一个port unification就完成了,其思路就是通过传入的同一个端口的ByteBuf的首字节,来判断对应的SOCKS的版本号,从而针对不同的SOCKS版本进行处理。 + +# 自定义PortUnificationServerHandler + +在本例中,我们将会创建一个自定义的Port Unification,用来同时接收HTTP请求和gzip请求。 + +在这之前,我们先看一下两个协议的magic word,也就是说我们拿到一个ByteBuf,怎么能够知道这个是一个HTTP协议,还是传输的一个gzip文件呢? + +先看下HTTP协议,这里我们默认是HTTP1.1,对于HTTP1.1的请求协议,下面是一个例子: + +```http +GET / HTTP/1.1 +Host: www.flydean.com +``` + +HTTP请求的第一个单词就是HTTP请求的方法名,具体而言有八种方法,分别是: + +OPTIONS +返回服务器针对特定资源所支持的HTTP请求方法。也可以利用向Web服务器发送’*’的请求来测试服务器的功能性。 +HEAD +向服务器索要与GET请求相一致的响应,只不过响应体将不会被返回。这一方法可以在不必传输整个响应内容的情况下,就可以获取包含在响应消息头中的元信息。 +GET +向特定的资源发出请求。注意:GET方法不应当被用于产生“副作用”的操作中,例如在Web Application中。其中一个原因是GET可能会被网络蜘蛛等随意访问。 +POST +向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST请求可能会导致新的资源的建立和/或已有资源的修改。 +PUT +向指定资源位置上传其最新内容。 +DELETE +请求服务器删除Request-URI所标识的资源。 +TRACE +回显服务器收到的请求,主要用于测试或诊断。 +CONNECT +HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器。 + +那么需要几个字节来区分这八个方法呢?可以看到一个字节是不够的,因为我们有POST和PUT,他们的第一个字节都是P。所以应该使用2个字节来作为magic word。 + +对于gzip协议来说,它也有特殊的格式,其中gzip的前10个字节是header,其中第一个字节是0x1f,第二个字节是0x8b。 + +这样我们用两个字节也能区分gzip协议。 + +这样,我们的handler逻辑就出来了。首先从byteBuf中取出前两个字节,然后对其进行判断,区分出是HTTP请求还是gzip请求: + +```java +private boolean isGzip(int magic1, int magic2) { + return magic1 == 31 && magic2 == 139; +} + +private static boolean isHttp(int magic1, int magic2) { + return + magic1 == 'G' && magic2 == 'E' || // GET + magic1 == 'P' && magic2 == 'O' || // POST + magic1 == 'P' && magic2 == 'U' || // PUT + magic1 == 'H' && magic2 == 'E' || // HEAD + magic1 == 'O' && magic2 == 'P' || // OPTIONS + magic1 == 'P' && magic2 == 'A' || // PATCH + magic1 == 'D' && magic2 == 'E' || // DELETE + magic1 == 'T' && magic2 == 'R' || // TRACE + magic1 == 'C' && magic2 == 'O'; // CONNECT +} +``` + +对应的,我们还需要对其添加相应的编码和解码器,对于gzip来说,netty提供了ZlibCodecFactory: + +```java +p.addLast("gzipEncoder", ZlibCodecFactory.newZlibEncoder(ZlibWrapper.GZIP)); +p.addLast("gzipDecoder", ZlibCodecFactory.newZlibDecoder(ZlibWrapper.GZIP)); +``` + +对于HTTP来说,netty也提供了HttpRequestDecoder和HttpResponseEncoder还有HttpContentCompressor来对HTTP消息进行编码解码和压缩。 + +```java +p.addLast("decoder", new HttpRequestDecoder()); +p.addLast("encoder", new HttpResponseEncoder()); +p.addLast("compressor", new HttpContentCompressor()); +``` + +# 总结 + +添加了编码和解码器之后,如果你想自定义一些操作,只需要再添加自定义的对应的消息handler即可,非常的方便。 + +本文的例子可以参考:[learn-netty4](https://github.com/ddean2009/learn-netty4) \ No newline at end of file diff --git a/learn-netty文章/39.netty系列之好马配好鞍,为channel选择配套的selector.md b/learn-netty文章/39.netty系列之好马配好鞍,为channel选择配套的selector.md new file mode 100644 index 0000000..59a738d --- /dev/null +++ b/learn-netty文章/39.netty系列之好马配好鞍,为channel选择配套的selector.md @@ -0,0 +1,172 @@ +# netty系列之:好马配好鞍,为channel选择配套的selector + +# 简介 + +我们知道netty的基础是channel和在channel之上的selector,当然作为一个nio框架,channel和selector不仅仅是netty的基础,也是所有nio实现的基础。 + +同样的,我们知道netty很多种不同的协议,这些协议都是在channel上进行通讯的,那么对于不同的协议来说,使用的channel和selector会有所不同吗? + +带着这个疑问,我们一起来深入探究一下吧。 + +# netty服务的基本构建方式 + +netty可以分为客户端和服务器端,实际上客户端和服务器端的构造方式差别不大,这里为了简单起见,以netty中服务器端的构建为例子进行研究。 + +回顾一下我们最开始搭建的netty服务器,其对应的代码如下: + +```java +//建立两个EventloopGroup用来处理连接和消息 +EventLoopGroup bossGroup = new NioEventLoopGroup(); +EventLoopGroup workerGroup = new NioEventLoopGroup(); + +ServerBootstrap b = new ServerBootstrap(); +b.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .childHandler(new ChannelInitializer() { + @Override + public void initChannel(SocketChannel ch) throws Exception { + ch.pipeline().addLast(new FirstServerHandler()); + } + }) + .option(ChannelOption.SO_BACKLOG, 128) + .childOption(ChannelOption.SO_KEEPALIVE, true); + +// 绑定端口并开始接收连接 +ChannelFuture f = b.bind(port).sync(); +``` + +我们要注意的是两个地方,一个是ServerBootstrap的group方法,一个是它的channel方法。 + +## EventLoopGroup + +group有两种实现方式,可以带一个参数,也可以带两个参数。参数都是EventLoopGroup,EventLoopGroup主要用来注册channel, 供后续的Selector进行选择。 + +如果使用一个参数的形式,则一个EventLoopGroup同时处理acceptor和client的事件,如果使用两个参数,则会将两者分开。 + +当然,这都不是今天要讲的重点,今天要讲的是EventLoopGroup的构建在不同的协议中有什么不同。 + +EventLoopGroup本身是一个接口,他有很多种实现,但是本质上还是两种EventLoop:SingleThreadEventLoop和MultithreadEventLoopGroup. + +也就是用单线程进行EventLoop处理和多线程进行EventLoop处理。 + +比如上面我们常用的NioEventLoopGroup,就是一个单线程的EventLoop。 + +NioEventLoopGroup通常我们使用的是无参的构造函数,实际上NioEventLoopGroup可以传入ThreadFactory,thread的个数,SelectorProvider和SelectStrategyFactory. + +netty只提供了一个SelectStrategyFactory的实现:DefaultSelectStrategyFactory。 + +而对应SelectorProvider来说,默认的实现是SelectorProvider.provider(), 我们看下这个方法的具体实现: + +```java +public static SelectorProvider provider() { + synchronized (lock) { + if (provider != null) + return provider; + return AccessController.doPrivileged( + new PrivilegedAction() { + public SelectorProvider run() { + if (loadProviderFromProperty()) + return provider; + if (loadProviderAsService()) + return provider; + provider = sun.nio.ch.DefaultSelectorProvider.create(); + return provider; + } + }); + } +} +``` + +可以看到默认情况下,SelectorProvider有三种创建方式。 + +第一种就是从系统属性中查找:java.nio.channels.spi.SelectorProvider: + +```java +String cn = System.getProperty("java.nio.channels.spi.SelectorProvider"); +Class c = Class.forName(cn, true,ClassLoader.getSystemClassLoader()); +provider = (SelectorProvider)c.newInstance(); +``` + +如果有定义,则创建一个实例返回。 + +如果没有的话,则会从”META-INF/services/”中加载service Loader : + +```java +private static boolean loadProviderAsService() { + + ServiceLoader sl = + ServiceLoader.load(SelectorProvider.class, + ClassLoader.getSystemClassLoader()); + Iterator i = sl.iterator(); +``` + +如果servie也没有找到的话,则会使用最后默认的sun.nio.ch.DefaultSelectorProvider. + +## channel + +默认情况下,我们使用的是NioServerSocketChannel。他实际是从上面提到的默认的SelectorProvider来创建的。 + +```java +private static final SelectorProvider DEFAULT_SELECTOR_PROVIDER = SelectorProvider.provider(); +return DEFAULT_SELECTOR_PROVIDER.openServerSocketChannel(); +``` + +所以使用的channel需要跟selector相匹配。 + +我们可以直接使用channel,也可以使用ChannelFactory,通过这些Factory来生成channel。 + +如果要使用ChannelFactory,则可以调用ServerBootstrap的channelFactory方法。 + +# 多种构建方式 + +上面提到了最基本的netty server构建方式。对应的是socket协议。 + +如果是要进行UDP连接,对应的channel应该换成NioDatagramChannel,如下: + +```java +EventLoopGroup group = new NioEventLoopGroup(); +Bootstrap b = new Bootstrap(); +b.group(group) + .channel(NioDatagramChannel.class) + .option(ChannelOption.SO_BROADCAST, true) + .handler(new UDPServerHandler()); + +b.bind(PORT).sync().channel().closeFuture().await(); +``` + +EventLoopGroup可以保持不变。 + +因为netty底层是基于Socket进行通讯的,socket底层又是基于TCP或者UDP协议,所以在netty中实现的http或者http2或者SOCKS协议都是在socket连接基础上进行的。 + +所以对http或者http2来说,channel还是NioServerSocketChannel。 + +可以看到只有UDP协议有所不同。同样的基于UDP协议之上的UDT协议也是不同的,其使用如下: + +```java +final NioEventLoopGroup acceptGroup = new NioEventLoopGroup(1, acceptFactory, NioUdtProvider.BYTE_PROVIDER); +final NioEventLoopGroup connectGroup = new NioEventLoopGroup(1, connectFactory, NioUdtProvider.BYTE_PROVIDER); + +final ServerBootstrap boot = new ServerBootstrap(); +boot.group(acceptGroup, connectGroup) + .channelFactory(NioUdtProvider.BYTE_ACCEPTOR) + .option(ChannelOption.SO_BACKLOG, 10) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new ChannelInitializer() { + @Override + public void initChannel(final UdtChannel ch) { + ch.pipeline().addLast( + new LoggingHandler(LogLevel.INFO), + new UDTEchoServerHandler()); + } + }); +``` + +UDT使用的是NioUdtProvider中提供的BYTE_PROVIDER和BYTE_ACCEPTOR分别作为selector和channelFactory。 + +# 其他的channel + +除了NioSocketChannel之外,还有EpollChannel、KQueueChannel、SctpChannel,这些channel都是针对不同协议来使用的。我们会在后续的文章中详细进行介绍。 + +# 总结 + +channel和selector是netty的基础,在这基础之上,netty可以扩展适配所有基于tcp和udp的协议,可以说非常的强大。 \ No newline at end of file diff --git a/learn-netty文章/4.netty系列之netty中的Channel详解.md b/learn-netty文章/4.netty系列之netty中的Channel详解.md new file mode 100644 index 0000000..d42a257 --- /dev/null +++ b/learn-netty文章/4.netty系列之netty中的Channel详解.md @@ -0,0 +1,133 @@ +# netty系列之:netty中的Channel详解 + +# 简介 + +Channel是连接ByteBuf和Event的桥梁,netty中的Channel提供了统一的API,通过这种统一的API,netty可以轻松的对接多种传输类型,如OIO,NIO等。今天本文将会介绍Channel的使用和Channel相关的一些概念。 + +# Channel详解 + +Channel是什么? Channel是一个连接网络输入和IO处理的桥梁。你可以通过Channel来判断当前的状态,是open还是connected,还可以判断当前Channel支持的IO操作,还可以使用ChannelPipeline对Channel中的消息进行处理。 + +先看下Channel的定义: + +```java +public interface Channel extends AttributeMap, ChannelOutboundInvoker, Comparable { +``` + +可以看到Channel是一个接口,它继承了AttributeMap, ChannelOutboundInvoker, Comparable三个类。Comparable表示这个类可以用来做比较。AttributeMap用来存储Channel的各种属性。ChannelOutboundInvoker主要负责Channel和外部 SocketAddress 进行连接和对写。 + +再看下channel中定义的方法: + +![img](c9b27ad3e0644fcc956f5d2cf904f223.png) + +可以看出channel中定义的方法是多种多样的,这些方法都有些什么特点呢?接下来一一为您讲解。 + +## 异步IO和ChannelFuture + +netty中所有的IO都是异步IO,也就是说所有的IO都是立即返回的,返回的时候,IO可能还没有结束,所以需要返回一个ChannelFuture,当IO有结果之后,会去通知ChannelFuture,这样就可以取出结果了。 + +ChannelFuture是java.util.concurrent.Future的子类,它除了可以拿到线程的执行结果之外,还对其进行了扩展,加入了当前任务状态判断、等待任务执行和添加listener的功能。 + +其他的功能都很好理解,它的突破在于可以对ChannelFuture添加listener,我们列出一个添加listener的方法: + +```java +Future addListeners(GenericFutureListener>... listeners); +``` + +添加的Listener会在future执行结束之后,被通知。不需要自己再去调用get等待future结束。这里实际上就是异步IO概念的实现,不需要主动去调用,当你完成之后来通知我就行。非常的美好! + +ChannelFuture 有两个状态:uncompleted或者completed,分别代表任务的执行状态。 + +当一个IO刚开始的时候,返回一个ChannelFuture对象,这个对象的初始状态是uncompleted。注意,这个状态下的IO是还未开始工作的状态。当IO完成之后,不管是succeeded, failed 或者 cancelled状态,ChannelFuture的状态都会转换成为completed。 + +下图展示的是ChannelFuture状态和IO状态的对应图: + +![img](6d6cc43e5bf94c078d03f0011b897076.png) + +如果要监控IO的状态,可以使用上面我们提到的 addListener 方法,为ChannelFuture添加一个ChannelFutureListener。 + +如果要等待IO执行完毕,还有一个await()方法,但是这个方法会去等待IO执行完毕,是一个同步的方法,所以并不推荐。 + +相比而言,addListener(GenericFutureListener)是一个非阻塞的异步方法,将会把一个ChannelFutureListener添加到ChannelFuture中,当IO结束之后会自动通知ChannelFutureListener,非常好用。 + +对于处理IO操作的ChannelHandler来说,为了避免IO的阻塞,一定不要在ChannelHandler的IO方法中调用await(),这样有可能会导致ChannelHandler因为IO阻塞导致性能下降。 + +下面举两个例子,一个是错误的操作,一个是正确的操作: + +```java +// 错误操作 +@Override +public void channelRead(ChannelHandlerContext ctx, Object msg) { + ChannelFuture future = ctx.channel().close(); + future.awaitUninterruptibly(); + // 调用其他逻辑 +} + +// 正确操作 +@Override +public void channelRead(ChannelHandlerContext ctx, Object msg) { + ChannelFuture future = ctx.channel().close(); + future.addListener(new ChannelFutureListener() { + public void operationComplete(ChannelFuture future) { + // 调用其他逻辑 + } + }); +} +``` + +大家可以对比下上面两种写法的区别。 + +另外要注意的是ChannelFuture中的这些await方法比如:await(long), await(long, TimeUnit), awaitUninterruptibly(long), 或者 awaitUninterruptibly(long, TimeUnit)可以带一个过期时间,大家要注意的是这个过期时间是等待IO执行的时间,并不是IO的timeout时间,也就是说当await超时之后,IO还有可能没有执行完成,这就导致了下面的代码有可能报错: + +```java + Bootstrap b = ...; + ChannelFuture f = b.connect(...); + f.awaitUninterruptibly(10, TimeUnit.SECONDS); + if (f.isCancelled()) { + // 用户取消了Channel + } else if (!f.isSuccess()) { + // 这里可能会报异常,因为底层的IO可能还没有执行完成 + f.cause().printStackTrace(); + } else { + // 成功建立连接 + } +``` + +上面的代码可以改成下面的例子: + +```java + Bootstrap b = ...; + // 配置连接timeout的时间 + b.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000); + ChannelFuture f = b.connect(...); + f.awaitUninterruptibly(); + + // 等待直到底层IO执行完毕 + assert f.isDone(); + + if (f.isCancelled()) { + // 用户手动取消Channel + } else if (!f.isSuccess()) { + f.cause().printStackTrace(); + } else { + // 成功建立连接 + } +``` + +## Channel的层级结构 + +netty中的Channel是有层级结构的,通过parent属性可获取这种层级结构。parent获取的对象和Channel的创建方式有关。比如如果是一个被ServerSocketChannel accepted的SocketChannel,那么它的parent就是ServerSocketChannel。 + +## 释放资源 + +和所有的IO一样,Channel在用完之后也需要被释放,需要调用close()或者close(ChannelPromise) 方法。 + +## 事件处理 + +channel负责建立连接,建立好的连接就可以用来处理事件ChannelEvent了,实际上ChannelEvent是由定义的一个个Channelhandler来处理的。而ChannelPipeline就是连接channel和channelhandler的桥梁。 + +我们将会下下一章详细讲解ChannelEvent、Channelhandler和ChannelPipeline的关联关系,敬请期待。 + +# 总结 + +Channel在netty中是做为一个关键的通道而存在的,后面的Event和Handler是以channel为基础运行的,所以说Channel就是netty的基础,好了,今天的介绍到这里就结束了,敬请期待后续的文章。 \ No newline at end of file diff --git a/learn-netty文章/40.netty系列之请netty再爱UDT一次.md b/learn-netty文章/40.netty系列之请netty再爱UDT一次.md new file mode 100644 index 0000000..aea1bcf --- /dev/null +++ b/learn-netty文章/40.netty系列之请netty再爱UDT一次.md @@ -0,0 +1,221 @@ +# netty系列之:请netty再爱UDT一次 + +# 简介 + +UDT是一个非常优秀的协议,可以提供在UDP协议基础上进行高速数据传输。但是可惜的是在netty 4.1.7中,UDT传输协议已经被标记为Deprecated了! + +意味着在后面的netty版本中,你可能再也看不到UDT协议了. + +优秀的协议怎么能够被埋没,让我们揭开UDT的面纱,展示其优秀的特性,让netty再爱UDT一次吧。 + +# netty对UDT的支持 + +netty对UDT的支持体现在有一个专门的UDT包来处理UDT相关事情:package io.netty.channel.udt。 + +这个包里面主要定义了UDT的各种channel、channel配置、UDT消息和提供ChannelFactory和SelectorProvider的工具类NioUdtProvider. + +## 搭建一个支持UDT的netty服务 + +按照netty的标准流程,现在是需要创建一个netty服务的时候了。 + +netty创建server服务无非就是创建EventLoop、创建ServerBootstrap、绑定EventLoop、指定channel类型就完了,非常的简单。 + +唯一不同的就是具体的childHandler,可能根据具体协议的不同使用不同的处理方式。 + +当然,如果不是NioSocketChannel,那么对应的ChannelFactory和SelectorProvider也会有所变化。 + +没关系,我们先看下如何创建支持UDT的netty服务: + +```java +final ThreadFactory acceptFactory = new DefaultThreadFactory("accept"); +final ThreadFactory connectFactory = new DefaultThreadFactory("connect"); +final NioEventLoopGroup acceptGroup = new NioEventLoopGroup(1, acceptFactory, NioUdtProvider.BYTE_PROVIDER); +final NioEventLoopGroup connectGroup = new NioEventLoopGroup(1, connectFactory, NioUdtProvider.BYTE_PROVIDER); + +final ServerBootstrap boot = new ServerBootstrap(); +boot.group(acceptGroup, connectGroup) + .channelFactory(NioUdtProvider.BYTE_ACCEPTOR) + .option(ChannelOption.SO_BACKLOG, 10) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new ChannelInitializer() { + @Override + public void initChannel(final UdtChannel ch) { + ch.pipeline().addLast( + new LoggingHandler(LogLevel.INFO), + new UDTEchoServerHandler()); + } + }); +// 开启服务 +final ChannelFuture future = boot.bind(PORT).sync(); +``` + +可以看到,UDT和普通netty socket服务不同的地方在于它的selector和channelFactory都是由NioUdtProvider来提供了。 + +NioUdtProvider是netty核心包中的内容,他提供了对UDT的有用封装,我们不需要要懂太多UDT内部的实现,就可以使用UDT协议,是不是很美妙。 + +## 异常来袭 + +如果有小伙伴兴冲冲的拿上面这段代码去尝试运行,那么很可惜你会得到异常,异常大概类似下面的情况: + +``` +包com.barchart.udt找不到! +``` + +什么?直接使用netty包中的类居然会报错?是可忍孰不可忍! + +我们来仔细分析一下,这里只有一个新的类就是NioUdtProvider,打开NioUdtProvider的源码,在import一栏,我们赫然发现居然引用了不属于netty的包,就是这些包报错了: + +```java +import com.barchart.udt.SocketUDT; +import com.barchart.udt.TypeUDT; +import com.barchart.udt.nio.ChannelUDT; +import com.barchart.udt.nio.KindUDT; +import com.barchart.udt.nio.RendezvousChannelUDT; +import com.barchart.udt.nio.SelectorProviderUDT; +``` + +虽然很诡异,但是我们要想程序跑起来还是需要找出这些依赖包,经过本人的跋山涉水、翻山越岭终于功夫不负苦心人,下面的依赖包找到了: + +```xml + + com.barchart.udt + barchart-udt-core + 2.3.0 + + + + com.barchart.udt + barchart-udt-bundle + 2.3.0 + +``` + +netty核心包居然要依赖与第三方库,这可能就是netty准备删除对UDT支持的原因吧。 + +## TypeUDT和KindUDT + +如果你去查看barchart中类的具体信息,就会发现这个包的作者有个癖好,喜欢把类后面带上一个UDT。 + +当你看到满屏的类都是以UDT结尾的时候,没错,它就是netty UDT依赖的包barchart本包了。 + +大牛们开发的包我们不能说他不好,只能说看起来有点累…. + +barchart包中有两个比较核心的用来区分UDT type和kind的两个类,分别叫做TypeUDT和KindUDT. + +Type和kind翻译成中文好像没太大区别。但是两者在UDT中还是有很大不同的。 + +TypeUDT表示的是UDT socket的模式。它有两个值,分别是stream和datagram: + +```java +STREAM(1), +DATAGRAM(2), +``` + +表示数据传输是以字节流的形式还是以数据报文消息的格式来进行传输。 + +KindUDT则用来区分是服务器端还是客户端,它有三种模式: + +``` +ACCEPTOR, +CONNECTOR, +RENDEZVOUS +``` + +server模式对应的是ACCEPTOR,用来监听和接收连接.它的代表是ServerSocketChannelUDT,通过调用accept()方法返回一个CONNECTOR. + +CONNECTOR模式可以同时在客户端和服务器端使用,它的代表类是SocketChannelUDT. + +如果是在server端,则是通过调用server端的accept方法生成的。如果是在客户端,则表示的是客户端和服务器端之间的连接。 + +还有一种模式是RENDEZVOUS模式。这种模式表示的是连接的每一侧都有对称对等的channel。也就是一个双向的模式,它的代表类是RendezvousChannelUDT。 + +## 构建ChannelFactory + +上面提到的两种Type和三种Kind都是用来定义channel的,所以如果将其混合,会生成六种不同的channelFactory,分别是: + +```java +public static final ChannelFactory BYTE_ACCEPTOR = new NioUdtProvider( + TypeUDT.STREAM, KindUDT.ACCEPTOR); + +public static final ChannelFactory BYTE_CONNECTOR = new NioUdtProvider( + TypeUDT.STREAM, KindUDT.CONNECTOR); + +public static final ChannelFactory BYTE_RENDEZVOUS = new NioUdtProvider( + TypeUDT.STREAM, KindUDT.RENDEZVOUS); + +public static final ChannelFactory MESSAGE_ACCEPTOR = new NioUdtProvider( + TypeUDT.DATAGRAM, KindUDT.ACCEPTOR); + +public static final ChannelFactory MESSAGE_CONNECTOR = new NioUdtProvider( + TypeUDT.DATAGRAM, KindUDT.CONNECTOR); + +public static final ChannelFactory MESSAGE_RENDEZVOUS = new NioUdtProvider( + TypeUDT.DATAGRAM, KindUDT.RENDEZVOUS); +``` + +这些channelFactory通过调用newChannel()方法来生成新的channel。 + +但是归根节点,这些channel最后是调用SelectorProviderUDT的from方法来生成channel的。 + +## SelectorProviderUDT + +SelectorProviderUDT根据TypeUDT的不同有两种,分别是: + +```java +public static final SelectorProviderUDT DATAGRAM = new SelectorProviderUDT(TypeUDT.DATAGRAM); + +public static final SelectorProviderUDT STREAM = new SelectorProviderUDT(TypeUDT.STREAM); +``` + +可以通过调用他的三个方法来生成对应的channel: + +```java +public RendezvousChannelUDT openRendezvousChannel() throws IOException { + final SocketUDT socketUDT = new SocketUDT(type); + return new RendezvousChannelUDT(this, socketUDT); +} + +public ServerSocketChannelUDT openServerSocketChannel() throws IOException { + final SocketUDT serverSocketUDT = new SocketUDT(type); + return new ServerSocketChannelUDT(this, serverSocketUDT); +} + +public SocketChannelUDT openSocketChannel() throws IOException { + final SocketUDT socketUDT = new SocketUDT(type); + return new SocketChannelUDT(this, socketUDT); +} +``` + +# 使用UDT + +搭建好了netty服务器,剩下就是编写Handler对数据进行处理了。 + +这里我们简单的将客户端写入的数据再回写。客户端先创建一个message: + +```java +message = Unpooled.buffer(UDTEchoClient.SIZE); +message.writeBytes("www.flydean.com".getBytes(StandardCharsets.UTF_8)); +``` + +再写入到server端: + +```java +public void channelActive(final ChannelHandlerContext ctx) { + log.info("channel active " + NioUdtProvider.socketUDT(ctx.channel()).toStringOptions()); + ctx.writeAndFlush(message); +} +``` + +服务器端通过channelRead方法来接收: + +```java +public void channelRead(final ChannelHandlerContext ctx, Object msg) { + ctx.write(msg); +} +``` + +# 总结 + +以上就是netty中使用UDT的原理和一个简单的例子。 + +本文的例子可以参考:[learn-netty4](https://github.com/ddean2009/learn-netty4) \ No newline at end of file diff --git a/learn-netty文章/41.netty系列之选byte还是选message这是一个问题.md b/learn-netty文章/41.netty系列之选byte还是选message这是一个问题.md new file mode 100644 index 0000000..61605e2 --- /dev/null +++ b/learn-netty文章/41.netty系列之选byte还是选message这是一个问题.md @@ -0,0 +1,142 @@ +# netty系列之:选byte还是选message?这是一个问题 + +# 简介 + +UDT给了你两种选择,byte stream或者message,到底选哪一种呢?经验告诉我们,只有小学生才做选择题,而我们应该全都要! + +# 类型的定义 + +UDT的两种类型是怎么定义的呢? + +翻看com.barchart.udt包,可以发现这两种类型定义在TypeUDT枚举类中。 + +```java +STREAM(1), +DATAGRAM(2), +``` + +一个叫做STREAM,它的code是1。一个叫做DATAGRAM,他的code是2. + +根据两个不同的类型我们可以创建不同的selectorProvider和channelFactory。而这两个正是构建netty服务所需要的。 + +在NioUdtProvider这个工具类中,netty为我们提供了TypeUDT和KindUDT的六种组合ChannelFactory,他们分别是: + +用于Stream的:BYTE_ACCEPTOR,BYTE_CONNECTOR,BYTE_RENDEZVOUS。 + +和用于Message的:MESSAGE_ACCEPTOR,MESSAGE_CONNECTOR和MESSAGE_RENDEZVOUS。 + +同样的,还有两个对应的SelectorProvider,分别是: + +```java +BYTE_PROVIDER 和 MESSAGE_PROVIDER. +``` + +# 搭建UDT stream服务器 + +如果要搭建UDT stream服务器,首先需要使用NioUdtProvider.BYTE_PROVIDER来创建NioEventLoopGroup: + +```java +final NioEventLoopGroup acceptGroup = new NioEventLoopGroup(1, acceptFactory, NioUdtProvider.BYTE_PROVIDER); +final NioEventLoopGroup connectGroup = new NioEventLoopGroup(1, connectFactory, NioUdtProvider.BYTE_PROVIDER); +``` + +这里,我们创建两个eventLoop,分别是acceptLoop和connectLoop。 + +接下来就是在ServerBootstrap中绑定上面的两个group,并且指定channelFactory。这里我们需要NioUdtProvider.BYTE_ACCEPTOR: + +```java +final ServerBootstrap boot = new ServerBootstrap(); +boot.group(acceptGroup, connectGroup) + .channelFactory(NioUdtProvider.BYTE_ACCEPTOR) + .option(ChannelOption.SO_BACKLOG, 10) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new ChannelInitializer() { + @Override + public void initChannel(final UdtChannel ch) { + ch.pipeline().addLast( + new LoggingHandler(LogLevel.INFO), + new UDTByteEchoServerHandler()); + } + }); +``` + +就这么简单。 + +# 搭建UDT message服务器 + +搭建UDT message服务器的步骤和stream很类似,不同的是需要使用NioUdtProvider.MESSAGE_PROVIDER作为selectorProvider: + +```java +final NioEventLoopGroup acceptGroup = + new NioEventLoopGroup(1, acceptFactory, NioUdtProvider.MESSAGE_PROVIDER); +final NioEventLoopGroup connectGroup = + new NioEventLoopGroup(1, connectFactory, NioUdtProvider.MESSAGE_PROVIDER); +``` + +然后在绑定ServerBootstrap的时候使用NioUdtProvider.MESSAGE_ACCEPTOR作为channelFactory: + +```java +final ServerBootstrap boot = new ServerBootstrap(); +boot.group(acceptGroup, connectGroup) + .channelFactory(NioUdtProvider.MESSAGE_ACCEPTOR) + .option(ChannelOption.SO_BACKLOG, 10) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new ChannelInitializer() { + @Override + public void initChannel(final UdtChannel ch) + throws Exception { + ch.pipeline().addLast( + new LoggingHandler(LogLevel.INFO), + new UDTMsgEchoServerHandler()); + } + }); +``` + +同样很简单。 + +# Stream和Message的handler + +不同的UDT类型,需要使用不同的handler。 + +对于Stream来说,它的底层是byte,所以我们的消息处理也是以byte的形式进行的,我们以下面的方式来构建message: + +```java +private final ByteBuf message; +message = Unpooled.buffer(UDTByteEchoClient.SIZE); +message.writeBytes("www.flydean.com".getBytes(StandardCharsets.UTF_8)); +``` + +然后使用ctx.writeAndFlush(message)将其写入到channel中。 + +对于message来说,它实际上格式对ByteBuf的封装。netty中有个对应的类叫做UdtMessage: + +```java +public final class UdtMessage extends DefaultByteBufHolder +``` + +UdtMessage是一个ByteBufHolder,所以它实际上是一个ByteBuf的封装。 + +我们需要将ByteBuf封装成UdtMessage: + +```java +private final UdtMessage message; +final ByteBuf byteBuf = Unpooled.buffer(UDTMsgEchoClient.SIZE); +byteBuf.writeBytes("www.flydean.com".getBytes(StandardCharsets.UTF_8)); +message = new UdtMessage(byteBuf); +``` + +然后将这个UdtMessage发送到channel中: + +```java +ctx.writeAndFlush(message); +``` + +这样你就学会了在UDT协议中使用stream和message两种数据类型了。 + +# 总结 + +大家可能觉得不同的数据类型原来实现起来这么简单。这全都要归功于netty优秀的封装和设计。 + +感谢netty! + +本文的例子可以参考:[learn-netty4](https://github.com/ddean2009/learn-netty4) \ No newline at end of file diff --git a/learn-netty文章/412a02375baf4cea9cbaec5330442747.png b/learn-netty文章/412a02375baf4cea9cbaec5330442747.png new file mode 100644 index 0000000..15945cf Binary files /dev/null and b/learn-netty文章/412a02375baf4cea9cbaec5330442747.png differ diff --git a/learn-netty文章/42.netty系列之真正的平等–UDT中的Rendezvous.md b/learn-netty文章/42.netty系列之真正的平等–UDT中的Rendezvous.md new file mode 100644 index 0000000..33485f8 --- /dev/null +++ b/learn-netty文章/42.netty系列之真正的平等–UDT中的Rendezvous.md @@ -0,0 +1,115 @@ +# netty系列之:真正的平等–UDT中的Rendezvous + +# 简介 + +在我们之前提到的所有netty知识中,netty好像都被分为客户端和服务器端两部分。服务器端监听连接,并对连接中的消息进行处理。而客户端则向服务器端建立请求连接,从而可以发送消息。 + +但是这一切都要在UDT协议中被终结,因为UDT提供了Rendezvous,一种平等的连接类型,节点之间是对等关系。 + +从来都没有救世主,也没有神仙和皇帝,只有同为节点的好兄弟。 + +# 建立支持Rendezvous的服务器 + +因为是对等的关系,所以这里不需要使用到ServerBootstrap,使用普通的Bootstrap就够了。 + +group还是要的,这里使用NioEventLoopGroup,NioEventLoopGroup需要提供了SelectorProvider。UDT提供了两种provider,分别是NioUdtProvider.BYTE_PROVIDER 和 NioUdtProvider.MESSAGE_PROVIDER,分别表示stream和message两种格式: + +```java +final NioEventLoopGroup connectGroup = new NioEventLoopGroup( + 1,connectFactory, NioUdtProvider.BYTE_PROVIDER); + +final NioEventLoopGroup connectGroup = new NioEventLoopGroup( + 1, connectFactory, NioUdtProvider.MESSAGE_PROVIDER); +``` + +接下来就是创建Bootstrap,并绑定group和设置channelFactory. + +当然,这里的channelFactory也有两种,分别是NiNioUdtProvider.BYTE_RENDEZVOUS和NioUdtProvider.BYTE_RENDEZVOUS。 + +那么可以有下面两种创建的方法,第一种是byte stream的: + +```java +final Bootstrap bootstrap = new Bootstrap(); +bootstrap.group(connectGroup) + .channelFactory(NioUdtProvider.BYTE_RENDEZVOUS) + .handler(new ChannelInitializer() { + @Override + protected void initChannel(UdtChannel ch) throws Exception { + ch.pipeline().addLast( + new LoggingHandler(LogLevel.INFO), + new UDTByteHandler(messageSize)); +``` + +第二种是message的: + +```java +final Bootstrap boot = new Bootstrap(); +boot.group(connectGroup) + .channelFactory(NioUdtProvider.MESSAGE_RENDEZVOUS) + .handler(new ChannelInitializer() { + @Override + public void initChannel(final UdtChannel ch) + throws Exception { + ch.pipeline().addLast( + new LoggingHandler(LogLevel.INFO), + new UDTMsgHandler(messageSize)); + } + }); +``` + +至此,两个支持不同UDT类型的Rendezvous服务器就建立起来了。 + +接下来就是对消息的处理了。 + +# 处理不同的消息 + +有了支持byte和message两种格式的服务器,接下来就是如何处理对应的消息了。 + +对于byte格式的UDT,channel中传输的消息就是ByteBuf,我们只需要构建ByteBuf的消息,然后在channel中传输即可: + +```java +private final ByteBuf message; +message = Unpooled.buffer(messageSize); +message.writeBytes("www.flydean.com".getBytes(StandardCharsets.UTF_8)); +ctx.writeAndFlush(message); +``` + +对应message格式的UDT,netty提供了一个专门的类UdtMessage对其进行封装,UdtMessage继承值DefaultByteBufHolder,他就是对ByteBuf的封装。 + +我们可以这样创建一个UdtMessage并发送它: + +```java +private final UdtMessage message; +final ByteBuf byteBuf = Unpooled.buffer(messageSize); +byteBuf.writeBytes("www.flydean.com".getBytes(StandardCharsets.UTF_8)); +message = new UdtMessage(byteBuf); + +ctx.writeAndFlush(message); +``` + +# 节点之间的交互 + +上面我们分别建立了两个节点,这两个节点是对等关系,那么怎么将这两个节点联系起来呢? + +我们调用Bootstrap的connect方法如下: + +```java +final ChannelFuture f = boot.connect(peer, self).sync(); +f.channel().closeFuture().sync(); +``` + +这里的connect传入两个SocketAddress参数,第一个参数是remoteAddress,第二个参数表示的是localAddress. + +当然,connect还有一种常用的用法就是连接到远程的服务器: + +```java +public ChannelFuture connect(String inetHost, int inetPort) +``` + +这也是我们最常见的那种用法。 + +# 总结 + +以上就是UDT中的Rendezvous的使用。 + +本文的例子可以参考:[learn-netty4](https://github.com/ddean2009/learn-netty4) \ No newline at end of file diff --git a/learn-netty文章/43.netty系列之JVM中的Reference count原来netty中也有.md b/learn-netty文章/43.netty系列之JVM中的Reference count原来netty中也有.md new file mode 100644 index 0000000..9fc1740 --- /dev/null +++ b/learn-netty文章/43.netty系列之JVM中的Reference count原来netty中也有.md @@ -0,0 +1,217 @@ +# netty系列之:JVM中的Reference count原来netty中也有 + +# 简介 + +为什么世界上有这么多JAVA的程序员呢?其中一个很重要的原因就是JAVA相对于C++而言,不需要考虑对象的释放,一切都是由垃圾回收器来完成的。在崇尚简单的现代编程世界中,会C++的高手越来越少,会JAVA的程序员越来越多。 + +JVM的垃圾回收器中一个很重要的概念就是Reference count,也就是对象的引用计数,用来控制对象是否还被引用,是否可以被垃圾回收。 + +netty也是运行在JVM中的,所以JVM中的对象引用计数也适用于netty中的对象。这里我们说的对象引用指的是netty中特定的某些对象,通过对象的引用计数来判断这些对象是否还被使用,如果不再被使用的话就可以把它们(或它们的共享资源)返回到对象池(或对象分配器)。 + +这就叫做netty的对象引用计数技术,其中一个最关键的对象就是ByteBuf。 + +# ByteBuf和ReferenceCounted + +netty中的对象引用计数是从4.X版本开始的,ByteBuf是其中最终要的一个应用,它利用引用计数来提高分配和释放性能. + +先来看一下ByteBuf的定义: + +```java +public abstract class ByteBuf implements ReferenceCounted, Comparable +``` + +可以看到ByteBuf是一个抽象类,它实现了ReferenceCounted的接口。 + +ReferenceCounted就是netty中对象引用的基础,它定义了下面几个非常重要的方法,如下所示: + +```java +int refCnt(); + +ReferenceCounted retain(); + +ReferenceCounted retain(int increment); + +boolean release(); + +boolean release(int decrement); +``` + +其中refCnt返回的是当前引用个数,retain用来增加引用,而release用来释放引用。 + +# ByteBuf的基本使用 + +刚分配情况下ByteBuf的引用个数是1: + +```java +ByteBuf buf = ctx.alloc().directBuffer(); +assert buf.refCnt() == 1; +``` + +当调用他的release方法之后,refCnt就变成了0: + +```java +boolean destroyed = buf.release(); +assert destroyed; +assert buf.refCnt() == 0; +``` + +当调用它的retain方法,refCnt就会加一: + +```java +ByteBuf buf = ctx.alloc().directBuffer(); +assert buf.refCnt() == 1; +buf.retain(); +assert buf.refCnt() == 2; +``` + +> 要注意的是,如果ByteBuf的refCnt已经是0了,就表示这个ByteBuf准备被回收了,如果再调用其retain方法,则会抛出IllegalReferenceCountException:refCnt: 0, increment: 1 + +所以我们必须在ByteBuf还未被回收之前调用retain方法。 + +既然refCnt=0的情况下,不能调用retain()方法,那么其他的方法能够调用吗? + +我们来尝试调用一下writeByte方法: + +```java +try { + buf.writeByte(10); +} catch (IllegalReferenceCountException e) { + log.error(e.getMessage(),e); +} +``` + +可以看到,如果refCnt=0的时候,调用它的writeByte方法会抛出IllegalReferenceCountException异常。 + +这样看来,只要refCnt=0,说明这个对象已经被回收了,不能够再使用了。 + +# ByteBuf的回收 + +既然ByteBuf中保存的有refCnt,那么谁来负责ByteBuf的回收呢? + +netty的原则是谁消费ByteBuf,谁就负责ByteBuf的回收工作。 + +在实际的工作中,ByteBuf会在channel中进行传输,根据谁消费谁负责销毁的原则,接收ByteBuf的一方,如果消费了ByteBuf,则需要将其回收。 + +> 这里的回收指的是调用ByteBuf的release()方法。 + +# ByteBuf的衍生方法 + +ByteBuf可以从一个parent buff中衍生出很多子buff。这些子buff并没有自己的reference count,它们的引用计数是和parent buff共享的,这些提供衍生buff的方法有:ByteBuf.duplicate(), ByteBuf.slice() 和 ByteBuf.order(ByteOrder)。 + +```java +buf = directBuffer(); +ByteBuf derived = buf.duplicate(); +assert buf.refCnt() == 1; +assert derived.refCnt() == 1; +``` + +因为衍生的byteBuf和parent buff共享引用计数,所以如果要将衍生的byteBuf传给其他的流程进行处理的话,需要调用retain()方法: + +```java +ByteBuf parent = ctx.alloc().directBuffer(512); +parent.writeBytes(...); + +try { + while (parent.isReadable(16)) { + ByteBuf derived = parent.readSlice(16); + derived.retain(); + process(derived); + } +} finally { + parent.release(); +} +// ... + +public void process(ByteBuf buf) { + // ... + buf.release(); +} +``` + +# ChannelHandler中的引用计数 + +netty根据是读消息还是写消息,可以分为InboundChannelHandler和OutboundChannelHandler,分别用来读消息和写消息。 + +根据谁消费,谁释放的原则,对Inbound消息来说,读取完毕之后,需要调用ByteBuf的release方法: + +```java +public void channelRead(ChannelHandlerContext ctx, Object msg) { + ByteBuf buf = (ByteBuf) msg; + try { + // ... + } finally { + buf.release(); + } +} +``` + +但是如果你只是将byteBuf重发到channel中供其他的步骤进行处理,则不需要release: + +```java +public void channelRead(ChannelHandlerContext ctx, Object msg) { + ByteBuf buf = (ByteBuf) msg; + // ... + ctx.fireChannelRead(buf); +} +``` + +同样的在Outbound中,如果只是简单的重发,则不需要release: + +```java +public void write(ChannelHandlerContext ctx, Object message, ChannelPromise promise) { + System.err.println("Writing: " + message); + ctx.write(message, promise); +} +``` + +如果是处理了消息,则需要release: + +```java +public void write(ChannelHandlerContext ctx, Object message, ChannelPromise promise) { + if (message instanceof HttpContent) { + // Transform HttpContent to ByteBuf. + HttpContent content = (HttpContent) message; + try { + ByteBuf transformed = ctx.alloc().buffer(); + // .... + ctx.write(transformed, promise); + } finally { + content.release(); + } + } else { + // Pass non-HttpContent through. + ctx.write(message, promise); + } +} +``` + +# 内存泄露 + +因为reference count是netty自身来进行维护的,需要在程序中手动进行release,这样会带来一个问题就是内存泄露。因为所有的reference都是由程序自己来控制的,而不是由JVM来控制,所以可能因为程序员个人的原因导致某些对象reference count无法清零。 + +为了解决这个问题,默认情况下,netty会选择1%的buffer allocations样本来检测他们是否存在内存泄露的情况. + +如果发生泄露,则会得到下面的日志: + +``` +LEAK: ByteBuf.release() was not called before it's garbage-collected. Enable advanced leak reporting to find out where the leak occurred. To enable advanced leak reporting, specify the JVM option '-Dio.netty.leakDetectionLevel=advanced' or call ResourceLeakDetector.setLevel() +``` + +上面提到了一个检测内存泄露的level,netty提供了4种level,分别是: + +- DISABLED—禁用泄露检测 +- SIMPLE –默认的检测方式,占用1% 的buff。 +- ADVANCED – 也是1%的buff进行检测,不过这个选项会展示更多的泄露信息。 +- PARANOID – 检测所有的buff。 + +具体的检测选项如下: + +```java +java -Dio.netty.leakDetection.level=advanced ... +``` + +# 总结 + +掌握了netty中的引用计数,就掌握了netty的财富密码! + +本文的例子可以参考:[learn-netty4](https://github.com/ddean2009/learn-netty4) \ No newline at end of file diff --git a/learn-netty文章/44.netty系列之让TCP连接快一点,再快一点.md b/learn-netty文章/44.netty系列之让TCP连接快一点,再快一点.md new file mode 100644 index 0000000..3afa7e3 --- /dev/null +++ b/learn-netty文章/44.netty系列之让TCP连接快一点,再快一点.md @@ -0,0 +1,130 @@ +# netty系列之:让TCP连接快一点,再快一点 + +# 简介 + +经典的TCP三次握手大家应该很熟悉了,三次握手按道理说应该是最优的方案了,当然这是对于通用的情况来说的。那么在某些特殊的情况下是不是可以提升TCP建立连接的速度呢? + +答案是肯定的,这就是今天我们要讲的TCP fast open和netty。 + +# TCP fast open + +什么是TCP fast open呢? + +TCP fast open也可以简写为TFO,它是TCP协议的一种扩展。为什么是fast open呢?这是因为TFO可以在初始化建立连接的时候就带上部分数据,这样在TCP连接建立之后,可以减少和服务器交互的次数,从而在特定的情况下减少响应的时间。 + +既然TFO这么好,为什么我们很少见到使用TFO协议的呢? + +这是因为TFO是有缺陷的,因为TFO会在sync包中带上一些数据信息,那么当sync包重发的时候,就会造成接收方接受到重复的数据。 + +所以,如果是用TFO,那么接收方则需要具有能够处理重复数据的能力。 + +在程序界,防止数据重复提交有一个好听的名字叫做幂等性,只有具有幂等性的服务器才能够使用TFO。 + +# 开启TFO + +既然TFO这么优秀,怎么才能开启TFO呢? + +TFO的开启首先需要操作系统的支持,如果你是mac系统,恭喜你,mac默认情况下已经支持TFO了,你不需要进行任何操作。 + +如果你是Linux系统,那么需要查看/proc/sys/net/ipv4/tcp_fastopen这个文件。 + +tcp_fastopen可以有四种值,如下所示: + +0 — 表示TFO未开启 +1 — 表示TFO开启了,但是只对客户端有效 +2 — 表示TFO开启了,但是只对服务器端有效 +3 — 表示TFO开启了,同时对客户端和服务器端有效 + +通过上面的设置,我们就在操作系统层开启了TFO的支持。 + +接下来,我们看一下如何在netty中使用TFO。 + +# netty对TFO的支持 + +首先我们看下如何在netty的服务器端开启TFO支持。 + +在这之前,我们先回顾一下如何建议一个普通的netty服务器: + +```java +EventLoopGroup bossGroup = new NioEventLoopGroup(); +EventLoopGroup workerGroup = new NioEventLoopGroup(); + +ServerBootstrap b = new ServerBootstrap(); +b.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .childHandler(new ChannelInitializer() { + @Override + public void initChannel(SocketChannel ch) throws Exception { + ch.pipeline().addLast(new TFOServerHandler()); + } + }) + .option(ChannelOption.SO_BACKLOG, 128) + .childOption(ChannelOption.SO_KEEPALIVE, true); + +// 绑定端口并开始接收连接 +ChannelFuture f = b.bind(port).sync(); +``` + +上面的代码中,我们看到ServerBootstrap可以设置option参数,ChannelOption中包含了所有可以设置的channel的参数,对应的TFO的参数是ChannelOption.TCP_FASTOPEN, 所以我们只需要添加到ServerBootstrap中即可: + +```java +sb.option(ChannelOption.TCP_FASTOPEN, 50) +``` + +ChannelOption.TCP_FASTOPEN的值表示的是socket连接中可以处于等待状态的fast-open请求的个数。 + +对于客户端来说,同样需要进行一些改动,先来看看传统的client端是怎么工作的: + +```java +EventLoopGroup group = new NioEventLoopGroup(); + +Bootstrap b = new Bootstrap(); +b.group(group) + .channel(NioSocketChannel.class) + .handler(new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel ch) throws Exception { + ChannelPipeline p = ch.pipeline(); + p.addLast(new TFOClientHandler()); + } + }); + +// 连接服务器 +ChannelFuture f = b.connect(HOST, PORT).sync(); +``` + +client要支持TFO,需要添加这样的操作: + +```java +b.option(ChannelOption.TCP_FASTOPEN_CONNECT, true) +``` + +还记得TFO是做什么的吗?TFO就是在sync包中发送了一些数据。所以我们需要在client端对发送的数据进行处理,也就是说在client和server端建立连接之前就需要向channel中发送消息。 + +要获得非建立连接的channel,则可以调用Bootstrap的register方法来获取channel: + +```java +Channel channel = b.register().sync().channel(); +``` + +然后向该channel中写入byteBuf: + +```java +ByteBuf fastOpenData = directBuffer(); +fastOpenData.writeBytes("TFO message".getBytes(StandardCharsets.UTF_8)); +channel.write(fastOpenData); +``` + +最后再和服务器建立连接: + +```java +// 连接服务器 +SocketAddress serverAddress = SocketUtils.socketAddress("127.0.0.1", 8000); +ChannelFuture f = channel.connect(serverAddress).sync(); +``` + +# 总结 + +这样一个一个支持TFO的客户端和服务器就完成了。尽情使用吧。 + +本文的例子可以参考:[learn-netty4](https://github.com/ddean2009/learn-netty4) \ No newline at end of file diff --git a/learn-netty文章/45.netty系列之不用怀疑,netty中的ByteBuf就是比JAVA中的好用.md b/learn-netty文章/45.netty系列之不用怀疑,netty中的ByteBuf就是比JAVA中的好用.md new file mode 100644 index 0000000..15e1c60 --- /dev/null +++ b/learn-netty文章/45.netty系列之不用怀疑,netty中的ByteBuf就是比JAVA中的好用.md @@ -0,0 +1,122 @@ +# netty系列之:不用怀疑,netty中的ByteBuf就是比JAVA中的好用 + +# 简介 + +netty作为一个优秀的的NIO框架,被广泛应用于各种服务器和框架中。同样是NIO,netty所依赖的JDK在1.4版本中早就提供nio的包,既然JDK已经有了nio的包,为什么netty还要再写一个呢? + +不是因为JDK不优秀,而是因为netty的要求有点高。 + +# ByteBuf和ByteBuffer的可扩展性 + +在讲解netty中的ByteBuf如何优秀之前,我们先来看一下netty中的ByteBuf和jdk中的ByteBuffer有什么关系。 + +事实上,没啥关系,只是名字长的有点像而已。 + +jdk中的ByteBuffer,全称是java.nio.ByteBuffer,属于JAVA nio包中的一个基础类。它的定义如下: + +```java +public abstract class ByteBuffer + extends Buffer + implements Comparable +``` + +而netty中的ByteBuf,全称是io.netty.buffer,属于netty nio包中的一个基础类。它的定义如下: + +```java +public abstract class ByteBuf + implements ReferenceCounted, Comparable +``` + +两者的定义都很类似,两者都是抽象类,都需要具体的类来实现他们。 + +但是,当你尝试去创建一个类来继承JDK的ByteBuffer,则会发现继承不了,为什么命名一个abstract的类会继承不了呢? + +仔细研究会发现,在ByteBuffer中,定义了下面两个没有显示标记其作用域访问的方法: + +```java +abstract byte _get(int i); // package-private +abstract void _put(int i, byte b); // package-private +``` + +根据JDK的定义,没有显示标记作用域的方法,默认其访问访问是package,当这两个方法又都是abstract的,所以只有同一个package的类才能继承JDK的ByteBuffer。 + +当然,JDK本身有5个ByteBuffer的实现,他们分别是DirectByteBuffer,DirectByteBufferR,HeapByteBuffer,HeapByteBufferR和MappedByteBuffer。 + +但是JDK限制了用户自定义类对ByteBuffer的扩展。虽然这样可以保证ByteBuffer类在使用上的安全性,但是同时也现在了用户需求的多样性。 + +既然JDK的ByteBuffer不能扩展,那么很自然的netty中的ByteBuf跟它就没有任何关系了。 + +netty中的ByteBuff是参考了JDK的ByteBuffer,并且做了很多有意义的提升,让ByteBuff更加好用。 + +和JDK的ByteBuffer相比,netty中的ByteBuf并没有扩展的限制,你可以自由的对其进行扩展和修改。 + +# 不同的使用方法 + +JDK中的ByteBuffer和netty中的ByteBuff都提供了对各种类型数据的读写功能。 + +但是相对于netty中的ByteBuff, JDK中的ByteBuffer使用其来比较复杂,因为它定义了4个值来描述ByteBuffer中的数据和使用情况,这四个值分别是:mark,position,limit和capacity。 + +- capacity是它包含的元素数。 capacity永远不会为负且永远不会改变。 +- limit是不应读取或写入的第一个元素的索引。 limit永远不会为负,也永远不会大于其容量。 +- position是要读取或写入的下一个元素的索引。 position永远不会为负,也永远不会大于其限制。 +- mark是调用 reset 方法时其位置将重置到的索引。 mark并不一定有值,但当它有值的时候,它永远不会是负的,也永远不会大于position。 + +上面4个值的关系是: + +```java +0 <= mark <= position <= limit <= capacity +``` + +然后JDK还提供了3个处理上面4个标记的方法: + +- clear : 将 limit设置为capacity,并将position设置为0,表示可以写入。 +- flip : 将 limit设置为当前位置,并将position设置为0.表示可以读取。 +- rewind : limit不变,将position设置为0,表示重新读取。 + +是不是头很大? + +太多的变量,太多的方法,虽然现在你可能记得,但是过一段时间就会忘记到底该怎么正确使用JDK的ByteBuffer了。 + +和JDK不同的是,netty中的ByteBuff,只有两个index,分别是readerIndex 和 writerIndex 。 + +除了index之外,ByteBuff还提供了更加丰富的读写API,方便我们使用。 + +# 性能上的不同 + +对于JDK的java.nio.ByteBuffer来说,当我们为其分配空间的时候,buffer中会被使用0来填充。虽然这些0可能会马上被真正有意义的值来进行替换。但是不可否认,填充的过程消耗了CPU和内存。 + +另外JDK的java.nio.ByteBuffer是依赖于垃圾回收器来进行回收的,但是我们之前讲过了,ByteBuffer有两种内型,一种是HeapBuffer,这种类型是由JVM进行管理的,用垃圾回收器来进行回收是没有问题的。 + +但是问题在于还有一类ByteBuffer是DirectByteBuffer,这种Buffer是直接分配在外部内存上的,并不是由JVM来进行管理.通常来说DirectBuffer可能会存在较长的时间,如果短时间分配大量的短生命周期的DirectBuffer,会导致这些Buffer来不及回收,从而导致OutOfMemoryError. + +另外使用API来回收DirectBuffer的速度也不是那么快。 + +相对而言,netty中的ByteBuf使用的是自己管理的引用计数。当ByteBuf的引用计数归零的时候,底层的内存空间就会被释放,或者返回到内存池中。 + +我们看一下netty中direct ByteBuff的使用: + +```java +ByteBufAllocator alloc = PooledByteBufAllocator.DEFAULT; +ByteBuf buf = alloc.directBuffer(1024); +// ... +buf.release(); // 回收directBuffer +``` + +当然,netty这种自己管理引用计数也有一些缺点,可能会在pooled buffer被垃圾回收之后,pool中的buffer才返回,从而导致内存泄露。 + +还好,netty提供了4种检测引用计数内存泄露的方法,分别是: + +- DISABLED—禁用泄露检测 +- SIMPLE –默认的检测方式,占用1% 的buff。 +- ADVANCED – 也是1%的buff进行检测,不过这个选项会展示更多的泄露信息。 +- PARANOID – 检测所有的buff。 + +具体的检测选项如下: + +```java +java -Dio.netty.leakDetection.level=advanced ... +``` + +# 总结 + +以上就是netty中优秀的ByteBuff和JDK中的对比。还不赶紧用起来。 \ No newline at end of file diff --git a/learn-netty文章/46.netty系列之channel和channelGroup.md b/learn-netty文章/46.netty系列之channel和channelGroup.md new file mode 100644 index 0000000..d782700 --- /dev/null +++ b/learn-netty文章/46.netty系列之channel和channelGroup.md @@ -0,0 +1,266 @@ +# netty系列之:channel和channelGroup + +# 简介 + +channel是netty中数据传输和数据处理的渠道,也是netty程序中不可或缺的一环。在netty中channel是一个接口,针对不同的数据类型或者协议channel会有具体的不同实现。 + +虽然channel很重要,但是在代码中确实很神秘,基本上我们很少能够看到直接使用channel的情况,那么事实真的如此吗?和channel相关的ChannelGroup又有什么作用呢?一起来看看吧。 + +# 神龙见首不见尾的channel + +其实netty的代码是有固定的模板的,首先根据是server端还是client端,然后创建对应的Bootstrap和ServerBootstrap。然后给这个Bootstrap配置对应的group方法。然后为Bootstrap配置channel和handler,最后启动Bootstrap即可。 + +这样一个标准的netty程序就完成了。你需要做的就是为其挑选合适的group、channel和handler。 + +我们先看一个最简单的NioServerSocketChannel的情况: + +```java +EventLoopGroup bossGroup = new NioEventLoopGroup(1); +EventLoopGroup workerGroup = new NioEventLoopGroup(); +try { + ServerBootstrap b = new ServerBootstrap(); + b.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new ChatServerInitializer()); + + b.bind(PORT).sync().channel().closeFuture().sync(); +} finally { + bossGroup.shutdownGracefully(); + workerGroup.shutdownGracefully(); +} +``` + +这里,我们将NioServerSocketChannel设置为ServerBootstrap的channel。 + +这样就完了吗?channel到底是在哪里用到的呢? + +别急,我们仔细看一下try block中的最后一句: + +```java +b.bind(PORT).sync().channel().closeFuture().sync(); +``` + +b.bind(PORT).sync()实际上返回了一个ChannelFuture对象,通过调用它的channel方法,就返回了和它关联的Channel对象。 + +然后我们调用了channel.closeFuture()方法。closeFuture方法会返回一个ChannelFuture对象,这个对象将会在channel关闭的时候收到通知。 + +而sync方法会实现同步阻塞,一直等到channel关闭为止,从而进行后续的eventGroup的shutdown操作。 + +在ServerBootstrap中构建模板中,channel其实有两个作用,第一个作用是指定ServerBootstrap的channel,第二个作用就是通过channel获取到channel关闭的事件,最终关闭整个netty程序。 + +虽然我们基本上看不到channel的直接方法调用,但是channel毋庸置疑,它就是netty的灵魂。 + +接下来我们再看一下具体消息处理的handler的基本操作: + +```java +public void channelActive(ChannelHandlerContext ctx) throws Exception { + // channel活跃 + ctx.write("Channel Active状态!\r\n"); + ctx.flush(); +} +``` + +通常如果需要在handler中向channel写入数据,我们调用的是ChannelHandlerContext的write方法。这个方法和channel有什么关系呢? + +首先write方法是ChannelOutboundInvoker接口中的方法,而ChannelHandlerContext和Channel都继承了ChannelOutboundInvoker接口,也就是说,ChannelHandlerContext和Channel都有write方法: + +```java +ChannelFuture write(Object msg); +``` + +因为这里我们使用的是NioServerSocketChannel,所以我们来具体看一下NioServerSocketChannel中write的实现。 + +经过检查代码我们会发现NioServerSocketChannel继承自AbstractNioMessageChannel,AbstractNioMessageChannel继承自AbstractNioChannel,AbstractNioChannel继承自AbstractChannel,而这个write方法就是AbstractChannel中实现的: + +```java +public ChannelFuture write(Object msg) { + return pipeline.write(msg); +} +``` + +Channel的write方法,实际上调用了pipeline的write方法。下面是pipeLine中的write方法: + +```java +public final ChannelFuture write(Object msg) { + return tail.write(msg); +} +``` + +这里的tail是一个AbstractChannelHandlerContext对象。 + +这样我们就得出了这样的结论:channel中的write方法最终实际上调用的是ChannelHandlerContext中的write方法。 + +所以上面的: + +```java +ctx.write("Channel Active状态!\r\n"); +``` + +实际上可以直接从channel中调用: + +```java +Channel ch = b.bind(0).sync().channel(); + +// 将消息写入channel中 +ch.writeAndFlush("Channel Active状态!\r\n").sync(); +``` + +# channel和channelGroup + +channel是netty的灵魂,对于Bootstrap来说,要获取到对应的channel,可以通过调用: + +```java +b.bind(PORT).sync().channel() +``` + +来得到,从上面代码中我们也可以看到一个Bootstrap只会对应一个channel。 + +> channel中有一个parent()方法,用来返回它的父channel,所以channel是有层级结构的, + +我们再来看一下channelGroup的定义: + +```java +public interface ChannelGroup extends Set, Comparable +``` + +可以看到ChannelGroup实际上是Channel的集合。ChannelGroup用来将类似的Channel构建成集合,从而可以对多个channel进行统一的管理。 + +可以能有小伙伴要问了,一个Bootstrap不是只对应一个channel吗?那么哪里来的channel的集合? + +事实上,在一些复杂的程序中,我们可能启动多个Bootstrap来处理不同的业务,所以相应的就会有多个channel。 + +如果创建的channel过多,并且这些channel又是很同质化的时候,就有需求对这些channel进行统一管理。这时候就需要用到channelGroup了。 + +## channelGroup的基本使用 + +先看下channelGroup的基本使用,首先是创建一个channelGroup: + +```java +ChannelGroup recipients = + new DefaultChannelGroup(GlobalEventExecutor.INSTANCE); +``` + +有了channelGroup之后,可以调用add方法,向其中添加不同的channel: + +```java +recipients.add(channelA); +recipients.add(channelB); +``` + +并且还可以统一向这些channel中发送消息: + +```java +recipients.write(Unpooled.copiedBuffer( + "这是从channelGroup中发出的统一消息.", + CharsetUtil.UTF_8)); +``` + +基本上channelGroup提供了write,flush,flushAndWrite,writeAndFlush,disconnect,close,newCloseFuture等功能,用于对集合中的channel进行统一管理。 + +如果你有多个channel,那么可以考虑使用channelGroup。 + +另外channelGroup还有一些特性,我们来详细了解一下。 + +## 将关闭的channel自动移出 + +ChannelGroup是一个channel的集合,当然我们只希望保存open状态的channel,如果是close状态的channel,还要手动从ChannelGroup中移出的话实在是太麻烦了。 + +所以在ChannelGroup中,如果一个channel被关闭了,那么它会自动从ChannelGroup中移出,这个功能是怎么实现的呢? + +先来看下channelGroup的add方法: + +```java +public boolean add(Channel channel) { + ConcurrentMap map = + channel instanceof ServerChannel? serverChannels : nonServerChannels; + + boolean added = map.putIfAbsent(channel.id(), channel) == null; + if (added) { + channel.closeFuture().addListener(remover); + } + + if (stayClosed && closed) { + channel.close(); + } + + return added; +} +``` + +可以看到,在add方法中,为channel区分了是server channel还是非server channel。然后根据channel id将其存入ConcurrentMap中。 + +如果添加成功,则给channel添加了一个closeFuture的回调。当channel被关闭的时候,会去调用这个remover方法: + +```java +private final ChannelFutureListener remover = new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) throws Exception { + remove(future.channel()); + } +}; +``` + +remover方法会将channel从serverChannels或者nonServerChannels中移出。从而保证ChannelGroup中只保存open状态的channel。 + +## 同时关闭serverChannel和acceptedChannel + +虽然 ServerBootstrap的bind方法只会返回一个channel,但是对于server来说,可以有多个worker EventLoopGroup,所以当客户端和服务器端建立连接之后建立的accepted Channel是server channel的子channel。 + +也就是说一个服务器端有一个server channel和多个accepted channel。 + +那么如果我们想要同时关闭这些channel的话, 就可以使用ChannelGroup的close方法。 + +因为如果Server channel和非Server channel在同一个ChannelGroup的话,所有的IO命令都会先发给server channel,然后才会发给非server channel。 + +所以我们可以将Server channel和非Server channel统统加入同一个ChannelGroup中,在程序的最后,统一调用ChannelGroup的close方法,从而达到该目的: + +```java +ChannelGroup allChannels = + new DefaultChannelGroup(GlobalEventExecutor.INSTANCE); + +public static void main(String[] args) throws Exception { + ServerBootstrap b = new ServerBootstrap(..); + ... + b.childHandler(new MyHandler()); + + // 启动服务器 + b.getPipeline().addLast("handler", new MyHandler()); + Channel serverChannel = b.bind(..).sync(); + allChannels.add(serverChannel); + + ... 等待shutdown指令 ... + + // 关闭serverChannel 和所有的 accepted connections. + allChannels.close().awaitUninterruptibly(); +} + +public class MyHandler extends ChannelInboundHandlerAdapter { + @Override + public void channelActive(ChannelHandlerContext ctx) { + // 将accepted channel添加到allChannels中 + allChannels.add(ctx.channel()); + super.channelActive(ctx); + } +} +``` + +## ChannelGroupFuture + +另外,和channel一样,channelGroup的操作都是异步的,它会返回一个ChannelGroupFuture对象。 + +我们看下ChannelGroupFuture的定义: + +```java +public interface ChannelGroupFuture extends Future, Iterable +``` + +可以看到ChannelGroupFuture是一个Future,同时它也是一个ChannelFuture的遍历器,可以遍历ChannelGroup中所有channel返回的ChannelFuture。 + +同时ChannelGroupFuture提供了isSuccess,isPartialSuccess,isPartialFailure等方法判断命令是否部分成功。 + +ChannelGroupFuture还提供了addListener方法用来监听具体的事件。 + +# 总结 + +channel是netty的核心,当我们有多个channel不便进行管理的时候,就可以使用channelGroup进行统一管理。 \ No newline at end of file diff --git a/learn-netty文章/47.netty系列之可以自动通知执行结果的Future,有见过吗?.md b/learn-netty文章/47.netty系列之可以自动通知执行结果的Future,有见过吗?.md new file mode 100644 index 0000000..94fb8eb --- /dev/null +++ b/learn-netty文章/47.netty系列之可以自动通知执行结果的Future,有见过吗?.md @@ -0,0 +1,207 @@ +# netty系列之:可以自动通知执行结果的Future,有见过吗? + + + +# 简介 + +在我的心中,JDK有两个经典版本,第一个就是现在大部分公司都在使用的JDK8,这个版本引入了Stream、lambda表达式和泛型,让JAVA程序的编写变得更加流畅,减少了大量的冗余代码。 + +另外一个版本要早点,还是JAVA 1.X的时代,我们称之为JDK1.5,这个版本引入了java.util.concurrent并发包,从此在JAVA中可以愉快的使用异步编程。 + +虽然先JDK已经发展到了17版本,但是并发这一块的变动并不是很大。受限于JDK要保持稳定的需求,所以concurrent并发包提供的功能并不能完全满足某些业务场景。所以依赖于JDK的包自行研发了属于自己的并发包。 + +当然,netty也不例外,一起来看看netty并发包都有那些优势吧。 + +# JDK异步缘起 + +怎么在java中创建一个异步任务,或者开启一个异步的线程,每个人可能都有属于自己的回答。 + +大家第一时间可能想到的是创建一个实现Runnable接口的类,然后将其封装到Thread中运行,如下所示: + +```java +new Thread(new(RunnableTask())).start() +``` + +每次都需要new一个Thread是JDK大神们不可接受的,于是他们产生了一个将thread调用进行封装的想法,而这个封装类就叫做Executor. + +Executor是一个interface,首先看一下这个interface的定义: + +```java +public interface Executor { + + void execute(Runnable command); +} +``` + +接口很简单,就是定义了一个execute方法来执行传入的Runnable命令。 + +于是我们可以这样来异步开启任务: + +```java +Executor executor = anExecutor; +executor.execute(new RunnableTask1()); +executor.execute(new RunnableTask2()); +``` + +看到这里,聪明的小伙伴可能就要问了,好像不对呀,Executor自定义了execute接口,好像跟异步和多线程并没有太大的关系呀? + +别急,因为Executor是一个接口,所以我们可以有很多实现。比如下面的直接执行Runnable,让Runnable在当前线程中执行: + +```java +class DirectExecutor implements Executor { + public void execute(Runnable r) { + r.run(); + } +} +``` + +又比如下面的在一个新的线程中执行Runnable: + +```java +class ThreadPerTaskExecutor implements Executor { + public void execute(Runnable r) { + new Thread(r).start(); + } +} +``` + +又比如下面的将多个任务存放在一个Queue中,执行完一个任务再执行下一个任务的序列执行: + +```java +class SerialExecutor implements Executor { + final Queue tasks = new ArrayDeque(); + final Executor executor; + Runnable active; + + SerialExecutor(Executor executor) { + this.executor = executor; + } + + public synchronized void execute(final Runnable r) { + tasks.offer(new Runnable() { + public void run() { + try { + r.run(); + } finally { + scheduleNext(); + } + } + }); + if (active == null) { + scheduleNext(); + } + } + + protected synchronized void scheduleNext() { + if ((active = tasks.poll()) != null) { + executor.execute(active); + } + } +} +``` + +这些Executor都非常完美。但是他们都只能提交任务,提交任务之后就什么都不知道了。这对于好奇的宝宝们是不可忍受的,因为我们需要知道执行的结果,或者对执行任务进行管控。 + +于是就有了ExecutorService。ExecutorService也是一个接口,不过他提供了shutdown方法来停止接受新的任务,和isShutdown来判断关闭的状态。 + +除此之外,它还提供了单独调用任务的submit方法和批量调用任务的invokeAll和invokeAny方法。 + +既然有了execute方法,submit虽然和execute方法基本上执行了相同的操作,但是在方法参数和返回值上有稍许区别。 + +首先是返回值,submit返回的是Future,Future表示异步计算的结果。 它提供了检查计算是否完成、等待其完成以及检索计算结果的方法。 Future提供了get方法,用来获取计算结果。但是如果调用get方法的同时,计算结果并没有准备好,则会发生阻塞。 + +其次是submit的参数,一般来说只有Callable才会有返回值,所以我们常用的调用方式是这样的: + +```java + Future submit(Callable task); +``` + +如果我们传入Runnable,那么虽然也返回一个Future,但是返回的值是null: + +```java +Future submit(Runnable task); +``` + +如果我又想传入Runnable,又想Future有返回值怎么办呢? + +古人告诉我们,鱼和熊掌不可兼得!但是现在是2021年了,有些事情是可以发生改变了: + +```java + Future submit(Runnable task, T result); +``` + +上面我们可以传入一个result,当Future中的任务执行完毕之后直接将result返回。 + +既然ExecutorService这么强大,如何创建ExecutorService呢? + +最简单的办法就是用new去创建对应的实例。但是这样不够优雅,于是JDK提供了一个Executors工具类,他提供了多种创建不同ExecutorService的静态方法,非常好用。 + +# netty中的Executor + +为了兼容JDK的并发框架,虽然netty中也有Executor,但是netty中的Executor都是从JDK的并发包中衍生出来的。 + +具体而言,netty中的Executor叫做EventExecutor,他继承自EventExecutorGroup: + +```java +public interface EventExecutor extends EventExecutorGroup +``` + +而EventExecutorGroup又继承自JDK的ScheduledExecutorService: + +```java +public interface EventExecutorGroup extends ScheduledExecutorService, Iterable +``` + +为什么叫做Group呢?这个Group的意思是它里面包含了一个EventExecutor的集合。这些结合中的EventExecutor通过Iterable的next方法来进行遍历的。 + +这也就是为什么EventExecutorGroup同时继承了Iterable类。 + +然后netty中的其他具体Executor的实现再在EventExecutor的基础之上进行扩展。从而得到了netty自己的EventExecutor实现。 + +# Future的困境和netty的实现 + +那么JDK中的Future会有什么问题呢?前面我们也提到了JDK中的Future虽然保存了计算结果,但是我们要获取的时候还是需要通过调用get方法来获取。 + +但是如果当前计算结果还没出来的话,get方法会造成当前线程的阻塞。 + +别怕,这个问题在netty中被解决了。 + +先看下netty中Future的定义: + +```java +public interface Future extends java.util.concurrent.Future +``` + +可以看到netty中的Future是继承自JDK的Future。同时添加了addListener和removeListener,以及sync和await方法。 + +先讲一下sync和await方法,两者都是等待Future执行结束。不同之处在于,如果在执行过程中,如果future失败了,则会抛出异常。而await方法不会。 + +那么如果不想同步调用Future的get方法来获得计算结果。则可以给Future添加listener。 + +这样当Future执行结束之后,会自动通知listener中的方法,从而实现异步通知的效果,其使用代码如下: + +```java +EventExecutorGroup group = new DefaultEventExecutorGroup(4); // 4 threads +Future f = group.submit(new Runnable() { ... }); +f.addListener(new FutureListener { + public void operationComplete(Future f) { + .. + } +}); +``` + +还有一个问题,每次我们提交任务的时候,都需要创建一个EventExecutorGroup,有没有不需要创建就可以提交任务的方法呢? + +有的! + +netty为那些没有时间创建新的EventExecutorGroup的同志们,特意创建一个全局的GlobalEventExecutor,这是可以直接使用的: + +```java +GlobalEventExecutor.INSTANCE.execute(new Runnable() { ... }); +``` + +GlobalEventExecutor是一个单线程的任务执行器,每隔一秒钟回去检测有没有新的任务,有的话就提交到executor执行。 + +# 总结 + +netty为JDK的并发包提供了非常有用的扩展。大家可以直接使用。 \ No newline at end of file diff --git a/learn-netty文章/48.netty系列之Bootstrap,ServerBootstrap和netty中的实现.md b/learn-netty文章/48.netty系列之Bootstrap,ServerBootstrap和netty中的实现.md new file mode 100644 index 0000000..5015864 --- /dev/null +++ b/learn-netty文章/48.netty系列之Bootstrap,ServerBootstrap和netty中的实现.md @@ -0,0 +1,351 @@ +# netty系列之:Bootstrap,ServerBootstrap和netty中的实现 + +# 简介 + +虽然netty很强大,但是使用netty来构建程序却是很简单,只需要掌握特定的netty套路就可以写出强大的netty程序。每个netty程序都需要一个Bootstrap,什么是Bootstrap呢?Bootstrap翻译成中文来说就是鞋拔子,在计算机世界中,Bootstrap指的是引导程序,通过Bootstrap可以轻松构建和启动程序。 + +在netty中有两种Bootstrap:客户端的Bootstrap和服务器端的ServerBootstrap。两者有什么不同呢?netty中这两种Bootstrap到底是怎么工作的呢? 一起来看看吧。 + +# Bootstrap和ServerBootstrap的联系 + +首先看一下Bootstrap和ServerBootstrap这两个类的继承关系,如下图所示: + +![img](4eda7812e7eb4825aa471bbc679e7cad.png) + +可以看到Bootstrap和ServerBootstrap都是继承自AbstractBootstrap,而AbstractBootstrap则是实现了Cloneable接口。 + +## AbstractBootstrap + +有细心的同学可能会问了,上面图中还有一个Channel,channel跟AbstractBootstrap有什么关系呢? + +我们来看下AbstractBootstrap的定义: + +```java +public abstract class AbstractBootstrap, C extends Channel> implements Cloneable +``` + +AbstractBootstrap接受两个泛型参数,一个是B继承自AbstractBootstrap,一个是C继承自Channel。 + +我们先来观察一下一个简单的Bootstrap启动需要哪些元素: + +```java +EventLoopGroup bossGroup = new NioEventLoopGroup(); +EventLoopGroup workerGroup = new NioEventLoopGroup(); + +ServerBootstrap b = new ServerBootstrap(); +b.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .childHandler(new ChannelInitializer() { + @Override + public void initChannel(SocketChannel ch) throws Exception { + ch.pipeline().addLast(new FirstServerHandler()); + } + }) + .option(ChannelOption.SO_BACKLOG, 128) + .childOption(ChannelOption.SO_KEEPALIVE, true); + +// 绑定端口并开始接收连接 +ChannelFuture f = b.bind(port).sync(); +// 等待server socket关闭 +f.channel().closeFuture().sync(); +``` + +上面的代码是一个最基本也是最标准的netty服务器端的启动代码。可以看到和Bootstrap相关的元素有这样几个: + +1. EventLoopGroup,主要用来进行channel的注册和遍历。 +2. channel或者ChannelFactory,用来指定Bootstrap中使用的channel的类型。 +3. ChannelHandler,用来指定具体channel中消息的处理逻辑。 +4. ChannelOptions,表示使用的channel对应的属性信息。 +5. SocketAddress,bootstrap启动是绑定的ip和端口信息。 + +目前看来和Bootstrap相关的就是这5个值,而AbstractBootstrap的构造函数中也就定义了这些属性的赋值: + +```java +AbstractBootstrap(AbstractBootstrap bootstrap) { + group = bootstrap.group; + channelFactory = bootstrap.channelFactory; + handler = bootstrap.handler; + localAddress = bootstrap.localAddress; + synchronized (bootstrap.options) { + options.putAll(bootstrap.options); + } + attrs.putAll(bootstrap.attrs); +} +``` + +示例代码中的group,channel,option等方法实际上都是向这些属性中赋值,并没有做太多的业务操作。 + +> 注意,AbstractBootstrap中只存在一个group属性,所以两个group属性是在ServerBootstrap中添加的扩展属性。 + +在Bootstrap中,channel其实是有两种赋值方法,一种是直接传入channel,另外一种方法是传入ChannelFactory。两者的本质都是一样的,我们看下channel是怎么转换成为ChannelFactory的: + +```java +public B channel(Class channelClass) { + return channelFactory(new ReflectiveChannelFactory( + ObjectUtil.checkNotNull(channelClass, "channelClass") + )); +} +``` + +channelClass被封装在一个ReflectiveChannelFactory中,最终还是设置的channelFactory属性。 + +AbstractBootstrap中真正启动服务的方法就是bind,bind方法传入的是一个SocketAddress,返回的是ChannelFuture,很明显,bind方法中会创建一个channel。我们来看一下bind方法的具体实现: + +```java +private ChannelFuture doBind(final SocketAddress localAddress) { + final ChannelFuture regFuture = initAndRegister(); + final Channel channel = regFuture.channel(); + if (regFuture.cause() != null) { + return regFuture; + } + + if (regFuture.isDone()) { + // At this point we know that the registration was complete and successful. + ChannelPromise promise = channel.newPromise(); + doBind0(regFuture, channel, localAddress, promise); + return promise; + } else { + // Registration future is almost always fulfilled already, but just in case it's not. + final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel); + regFuture.addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) throws Exception { + Throwable cause = future.cause(); + if (cause != null) { + // Registration on the EventLoop failed so fail the ChannelPromise directly to not cause an + // IllegalStateException once we try to access the EventLoop of the Channel. + promise.setFailure(cause); + } else { + // Registration was successful, so set the correct executor to use. + // See https://github.com/netty/netty/issues/2586 + promise.registered(); + + doBind0(regFuture, channel, localAddress, promise); + } + } + }); + return promise; + } +} +``` + +在doBind方法中,首先调用initAndRegister方法去初始化和注册一个channel。 + +channel是通过channelFactory的newChannel方法来创建的: + +```java +channel = channelFactory.newChannel(); +``` + +接着调用初始化channel的init方法。这个init方法在AbstractBootstrap中并没有实现,需要在具体的实现类中实现。 + +有了channel之后,通过调用EventLoopGroup的register方法将channel注册到 EventLoop中,并将注册生成的ChannelFuture返回。 + +然后通过判断返回的regFuture的状态,来判断channel是否注册成功,如果注册成功,最后调用doBind0方法,完成最后的绑定工作: + +```java +private static void doBind0( + final ChannelFuture regFuture, final Channel channel, + final SocketAddress localAddress, final ChannelPromise promise) { + + // This method is invoked before channelRegistered() is triggered. Give user handlers a chance to set up + // the pipeline in its channelRegistered() implementation. + channel.eventLoop().execute(new Runnable() { + @Override + public void run() { + if (regFuture.isSuccess()) { + channel.bind(localAddress, promise).addListener(ChannelFutureListener.CLOSE_ON_FAILURE); + } else { + promise.setFailure(regFuture.cause()); + } + } + }); +} +``` + +因为eventLoop本身是一个Executor,所以可以执行一个具体的命令的,在它的execute方法中,传入了一个新的Runnable对象,在其中的run方法中执行了channel.bind方法,将channel跟SocketAddress进行绑定。 + +到此,Bootstrap的bind方法执行完毕。 + +我们再来回顾一下bind方法的基本流程: + +1. 通过ChannelFactory创建一个channel。 +2. 将channel注册到Bootstrap中的EventLoopGroup中。 +3. 如果channel注册成功,则调用EventLoopGroup的execute方法,将channel和SocketAddress进行绑定。 + +是不是很清晰? + +讲完AbstractBootstrap,接下来,我们再继续探讨一下Bootstrap和ServerBootstrap。 + +## Bootstrap和ServerBootstrap + +首先来看下Bootstrap,Bootstrap主要使用在客户端使用,或者UDP协议中。 + +先来看下Bootstrap的定义: + +```java +public class Bootstrap extends AbstractBootstrap +``` + +Bootstrap和AbstractBootstrap相比,主要多了一个属性和一个方法。 + +多的一个属性是resolver: + +```java +private static final AddressResolverGroup DEFAULT_RESOLVER = DefaultAddressResolverGroup.INSTANCE; + +private volatile AddressResolverGroup resolver = + (AddressResolverGroup) DEFAULT_RESOLVER; +``` + +AddressResolverGroup里面有一个IdentityHashMap,它的key是EventExecutor,value是AddressResolver: + +```java +private final Map> resolvers = + new IdentityHashMap>(); +``` + +实际上AddressResolverGroup维护了一个EventExecutor和AddressResolver的映射关系。 + +AddressResolver主要用来解析远程的SocketAddress的地址。因为远程的SocketAddress可能并不是一个IP地址,所以需要使用AddressResolver解析一下。 + +这里的EventExecutor实际上就是channel注册的EventLoop。 + +另外Bootstrap作为一个客户端的应用,它需要连接到服务器端,所以Bootstrap类中多了一个connect到远程SocketAddress的方法: + +```java +public ChannelFuture connect(SocketAddress remoteAddress) { + ObjectUtil.checkNotNull(remoteAddress, "remoteAddress"); + validate(); + return doResolveAndConnect(remoteAddress, config.localAddress()); +} +``` + +connect方法和bind方法的逻辑类似,只是多了一个resolver的resolve过程。 + +解析完毕之后,会调用doConnect方法,进行真正的连接: + +```java +private static void doConnect( + final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise connectPromise) { + + // This method is invoked before channelRegistered() is triggered. Give user handlers a chance to set up + // the pipeline in its channelRegistered() implementation. + final Channel channel = connectPromise.channel(); + channel.eventLoop().execute(new Runnable() { + @Override + public void run() { + if (localAddress == null) { + channel.connect(remoteAddress, connectPromise); + } else { + channel.connect(remoteAddress, localAddress, connectPromise); + } + connectPromise.addListener(ChannelFutureListener.CLOSE_ON_FAILURE); + } + }); +} +``` + +可以看到doConnect方法和doBind方法很类似,都是通过当前channel注册的eventLoop来执行channel的connect或许bind方法。 + +再看一下ServerBootstrap的定义: + +```java +public class ServerBootstrap extends AbstractBootstrap +``` + +因为是ServerBootstrap用在服务器端,所以不选Bootstrap那样去解析SocketAddress,所以没有resolver属性。 + +但是对应服务器端来说,可以使用parent EventLoopGroup来接受连接,然后使用child EventLoopGroup来执行具体的命令。所以在ServerBootstrap中多了一个childGroup和对应的childHandler: + +```java +private volatile EventLoopGroup childGroup; +private volatile ChannelHandler childHandler; +``` + +因为ServerBootstrap有两个group,所以ServerBootstrap包含一个含有两个EventLoopGroup的group方法: + +```java +public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) +``` + +还记得bind方法需要实现的init方法吗? 我们看下ServerBootstrap中init的具体逻辑: + +```java +void init(Channel channel) { + setChannelOptions(channel, newOptionsArray(), logger); + setAttributes(channel, newAttributesArray()); + + ChannelPipeline p = channel.pipeline(); + + final EventLoopGroup currentChildGroup = childGroup; + final ChannelHandler currentChildHandler = childHandler; + final Entry, Object>[] currentChildOptions = newOptionsArray(childOptions); + final Entry, Object>[] currentChildAttrs = newAttributesArray(childAttrs); + + p.addLast(new ChannelInitializer() { + @Override + public void initChannel(final Channel ch) { + final ChannelPipeline pipeline = ch.pipeline(); + ChannelHandler handler = config.handler(); + if (handler != null) { + pipeline.addLast(handler); + } + + ch.eventLoop().execute(new Runnable() { + @Override + public void run() { + pipeline.addLast(new ServerBootstrapAcceptor( + ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs)); + } + }); + } + }); +} +``` + +首先是设置channel的一些属性,然后通过channel.pipeline方法获得channel对应的pipeline,然后向pipeline中添加channelHandler。 + +这些都是常规操作,我们要注意的是最后通过channel注册到的eventLoop,将ServerBootstrapAcceptor加入到了pipeline中。 + +很明显ServerBootstrapAcceptor本身应该是一个ChannelHandler,它的主要作用就是用来接受连接: + +```java +private static class ServerBootstrapAcceptor extends ChannelInboundHandlerAdapter +``` + +我们来看一下它的channelRead方法: + +```java +public void channelRead(ChannelHandlerContext ctx, Object msg) { + final Channel child = (Channel) msg; + + child.pipeline().addLast(childHandler); + + setChannelOptions(child, childOptions, logger); + setAttributes(child, childAttrs); + + try { + childGroup.register(child).addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) throws Exception { + if (!future.isSuccess()) { + forceClose(child, future.cause()); + } + } + }); + } catch (Throwable t) { + forceClose(child, t); + } +} +``` + +因为server端接受的是客户端channel的connect操作,所以对应的channelRead中的对象实际上是一个channel。这里把这个接受到的channel称作child。通过给这个child channel添加childHandler,childOptions和childAttrs,一个能够处理child channel请求的逻辑就形成了。 + +最后将child channel注册到childGroup中,至此整个ServerBootstrapAcceptor接受channel的任务就完成了。 + +这里最妙的部分就是将客户端的channel通过server端的channel传到server端,然后在server端为child channel配备handler进行具体的业务处理,非常巧妙。 + +# 总结 + +通过具体分析AbstractBootstrap,Bootstrap和ServerBootstrap的结构和实现逻辑,相信大家对netty服务的启动流程有了大概的认识,后面我们会详细讲解netty中的channel和非常重要的eventLoop。 \ No newline at end of file diff --git a/learn-netty文章/49.netty系列之channel,ServerChannel和netty中的实现.md b/learn-netty文章/49.netty系列之channel,ServerChannel和netty中的实现.md new file mode 100644 index 0000000..8ab006c --- /dev/null +++ b/learn-netty文章/49.netty系列之channel,ServerChannel和netty中的实现.md @@ -0,0 +1,426 @@ +# netty系列之:channel,ServerChannel和netty中的实现 + +# 简介 + +我们知道channel是netty中用于沟通ByteBuf和Event的桥梁,在netty服务的创建过程中,不管是客户端的Bootstrap还是服务器端的ServerBootstrap,都需要调用channel方法来指定对应的channel类型。 + +那么netty中channel到底有哪些类型呢?他们具体是如何工作的呢?一起来看看。 + +# channel和ServerChannel + +Channel在netty中是一个interface,在Channel中定义了很多非常有用的方法。通常来说如果是客户端的话,对应的channel就是普通的channel。如果是服务器端的话,对应的channel就应该是ServerChannel。 + +那么客户端channel和服务器端channel有什么区别呢?我们先来看下ServerChannel的定义: + +```java +public interface ServerChannel extends Channel { + // This is a tag interface. +} +``` + +可以看到ServerChannel继承自Channel,表示服务端的Channel也是Channel的一种。 + +但是很奇怪的是,你可以看到ServerChannel中并没有新增任何新的方法。也就是说ServerChannel和Channel在定义上本质是一样的。你可以把ServerChannel看做是一个tag interface而已。 + +那么channel和ServerChannel有什么联系呢? + +我们知道在Channel中定义了一个parent方法: + +```java +Channel parent(); +``` + +这个parent方法返回的是该channel的父channel。我们以最简单的LocalChannel和LocalServerChannel为例,来查看一下他们的父子关系到底是怎么创建的。 + +首先parent的值是通过LocalChannel和LocalServerChannel的公共父类AbstractChannel来实现的: + +```java +protected AbstractChannel(Channel parent) { + this.parent = parent; + id = newId(); + unsafe = newUnsafe(); + pipeline = newChannelPipeline(); +} +``` + +对于LocalChannel来说,可以通过它的构造函数来设置parent channel: + +```java +protected LocalChannel(LocalServerChannel parent, LocalChannel peer) { + super(parent); + config().setAllocator(new PreferHeapByteBufAllocator(config.getAllocator())); + this.peer = peer; + localAddress = parent.localAddress(); + remoteAddress = peer.localAddress(); +} +``` + +我们知道当client端想要连接到server端的时候,需要调用client channel的connect方法,对于LocalChannel来说,它的connect方法实际上调用的是pipeline的connect方法: + +```java +public ChannelFuture connect(SocketAddress remoteAddress) { + return pipeline.connect(remoteAddress); +} +``` + +最终会调用LocalChannel中的LocalUnsafe.connect方法。 + +而在LocalUnsafe.connect方法中又会调用serverChannel.serve方法。 + +serverChannel的newLocalChannel方法会创建新的LocalChannel并返回: + +```java +protected LocalChannel newLocalChannel(LocalChannel peer) { + return new LocalChannel(this, peer); +} +``` + +这里使用newLocalChannel方法创建的LocalChannel就是serverChannel的子channel。 + +最后返回的LocalChannel会作为client端LocalChannel的peer channel而存在。 + +# netty中channel的实现 + +在netty中channel和Serverchannel有很多个实现类,用来完成不同的业务功能。 + +为了循序渐进一步步了解netty中channel的秘密,这里我们先来探讨一下netty中channel的基本实现LocalChannel和LocalServerChannel的工作原理。 + +下图是LocalChannel和LocalServerChannel的主要继承和依赖关系: + +![img](1d9c19d567084c199dfade76c8a0d52a.png) + +从图中可以看到,LocalChannel继承自AbstractChannel而LocalServerChannel则继承自AbstractServerChannel。 + +因为ServerChannel继承自Channel,所以很自然的AbstractServerChannel又继承自AbstractChannel。 + +接下来,我们通过对比分析AbstractChannel和AbstractServerChannel,LocalChannel和LocalServerChannel来一探netty中channel实现的底层原理。 + +## AbstractChannel和AbstractServerChannel + +AbstractChannel是对Channel的最基本的实现。先来看下AbstractChannel中都有那些功能。 + +首先AbstractChannel中定义了Channel接口中要返回的一些和channel相关的基本属性,包括父channel,channel id,pipline,localAddress,remoteAddress,eventLoop等,如下所示: + +```java +private final Channel parent; +private final ChannelId id; +private final DefaultChannelPipeline pipeline; +private volatile SocketAddress localAddress; +private volatile SocketAddress remoteAddress; +private volatile EventLoop eventLoop; + +private final Unsafe unsafe; +``` + +要注意的是AbstractChannel中还有一个非常中要的Unsafe属性。 + +Unsafe本身就是Channel接口中定义的一个内部接口,它的作用就是为各个不同类型的transport提供特定的实现。 + +从名字可以看出Unsafe是一个不安全的实现,它只是在netty的源代码中使用,它是不能出现在用户代码中的。或者你可以将Unsafe看做是底层的实现,而包裹他的AbstractChannel或者其他的Channel是对底层实现的封装,对于普通用户来说,他们只需要使用Channel就可以了,并不需要深入到更底层的内容。 + +另外,对于Unsafe来说,除了下面几个方法之外,剩余的方法必须从 I/O thread中调用: + +```java +localAddress() +remoteAddress() +closeForcibly() +register(EventLoop, ChannelPromise) +deregister(ChannelPromise) +voidPromise() +``` + +和一些基本的状态相关的数据: + +```java +private volatile boolean registered; +private boolean closeInitiated; +``` + +除了基本的属性设置和读取之外,我们channel中最终要的方法主要有下面几个: + +- 用于建立服务器端服务的bind方法: + +```java +public ChannelFuture bind(SocketAddress localAddress) { + return pipeline.bind(localAddress); +} +``` + +- 用于客户端建立和服务器端连接的connect方法: + +```java +public ChannelFuture connect(SocketAddress remoteAddress) { + return pipeline.connect(remoteAddress); +} +``` + +- 断开连接的disconnect方法: + +```java +public ChannelFuture disconnect() { + return pipeline.disconnect(); +} +``` + +- 关闭channel的close方法: + +```java +public ChannelFuture close() { + return pipeline.close(); +} +``` + +- 取消注册的deregister方法: + +```java +public ChannelFuture deregister() { + return pipeline.deregister(); +} +``` + +- 刷新数据的flush方法: + +```java +public Channel flush() { + pipeline.flush(); + return this; +} +``` + +- 读取数据的read方法: + +```java +public Channel read() { + pipeline.read(); + return this; +} +``` + +- 写入数据的方法: + +```java +public ChannelFuture write(Object msg) { + return pipeline.write(msg); +} +``` + +可以看到这些channel中的读写和绑定工作都是由和channel相关的pipeline来执行的。 + +其实也很好理解,channel只是一个通道,和数据相关的操作,还是需要在管道中执行。 + +我们以bind方法为例子,看一下AbstractChannel中的pipline是怎么实现的。 + +在AbstractChannel中,默认的pipeline是DefaultChannelPipeline,它的bind方法如下: + +```java +public void bind( + ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise) { + unsafe.bind(localAddress, promise); +} +``` + +这里的unsafe实际上就是AbstractChannel中的unsafe,unsafe中的bind方法最终会调用AbstractChannel中的dobind方法: + +```java +protected abstract void doBind(SocketAddress localAddress) throws Exception; +``` + +所以归根到底,如果是基于AbstractChannel的各种实现,那么只需要实现它的这些do*方法即可。 + +好了,AbstractChannel的介绍完毕了。 我们再来看一下AbstractServerChannel。AbstractServerChannel继承自AbstractChannel并且实现了ServerChannel接口。 + +```java +public abstract class AbstractServerChannel extends AbstractChannel implements ServerChannel +``` + +我们知道ServerChannel和Channel实际上是相同的,所以AbstractServerChannel只是在AbstractChannel的实现上进行了一些调整。 + +在AbstractServerChannel中,我们一起来观察一下AbstractServerChannel和AbstractChannel到底有什么不同。 + +首先是AbstractServerChannel的构造函数: + +```java +protected AbstractServerChannel() { + super(null); +} +``` + +构造函数中,super的parent channel是null,表示ServerChannel本身并不存在父channel,这是ServerChannel和client channel +的第一个不同之处。因为server channel可以通过worker event loop来接受client channel,所以server channel是client channel的父channel。 + +另外,我们还观察几个方法的实现: + +```java +public SocketAddress remoteAddress() { + return null; +} +``` + +对于ServerChannel来说不需要主动连接到远程的Server,所以并没有remoteAddress。 + +另外,因为断开连接是由client端主动调用的,所以server channel的doDisconnect会抛出不支持该操作的异常: + +```java +protected void doDisconnect() throws Exception { + throw new UnsupportedOperationException(); +} +``` + +同时ServerChannel只是用来负责accept和client channel建立关联关系,所以server channel本身并不支持向channel内进行的write操作,所以这个doWrite方法也是不支持的: + +```java +protected void doWrite(ChannelOutboundBuffer in) throws Exception { + throw new UnsupportedOperationException(); +} +``` + +最后ServerChannel只支持bind操作,所以DefaultServerUnsafe中的connect方法也会抛出UnsupportedOperationException. + +## LocalChannel和LocalServerChannel + +LocalChannel和LocalServerChannel是AbstractChannel和AbstractServerChannel的最基本的实现。从名字就可以看出来,这两个Channel是本地channel,我们来看一下这两个Channel的具体实现。 + +首先我们来看一下LocalChannel,LocalChannel有几点对AbstractChannel的扩展。 + +第一个扩展点是LocalChannel中添加了channel的几个状态: + +```java +private enum State { OPEN, BOUND, CONNECTED, CLOSED } +``` + +通过不同的状态,可以对channel进行更加细粒度的控制。 + +另外LocalChannel中添加了一个非常重要的属性: + +```java +private volatile LocalChannel peer; +``` + +因为LocalChannel表示的是客户端channel,所以这个peer表示的是client channel对等的server channel。接下来我们看一下具体的实现。 + +首先是LocalChannel的构造函数: + +```java +protected LocalChannel(LocalServerChannel parent, LocalChannel peer) { + super(parent); + config().setAllocator(new PreferHeapByteBufAllocator(config.getAllocator())); + this.peer = peer; + localAddress = parent.localAddress(); + remoteAddress = peer.localAddress(); +} +``` + +LocalChannel可以接受一个LocalServerChannel作为它的parent,还有一个LocalChannel作为它的对等channel。 + +那么这个peer是怎么创建的呢? + +我们来看一下LocalUnsafe中connect的逻辑。 + +```java +if (state != State.BOUND) { + // Not bound yet and no localAddress specified - get one. + if (localAddress == null) { + localAddress = new LocalAddress(LocalChannel.this); + } +} + +if (localAddress != null) { + try { + doBind(localAddress); + } catch (Throwable t) { + safeSetFailure(promise, t); + close(voidPromise()); + return; + } +} +``` + +首先判断当前channel的状态,如果是非绑定状态,那么需要进行绑定操作。首先根据传入的LocalChannel创建对应的LocalAddress。 + +这个LocalAddress只是LocalChannel的一种表现形式,并没有什么特别的功能。 + +我们来看一下这个doBind方法: + +```java +protected void doBind(SocketAddress localAddress) throws Exception { + this.localAddress = + LocalChannelRegistry.register(this, this.localAddress, + localAddress); + state = State.BOUND; +} +``` + +LocalChannelRegistry中维护了一个static的map,这个map中存放的就是注册过的Channel. + +这里注册是为了在后面方便的拿到对应的channel。 + +注册好localChannel之后,接下来就是根据注册好的remoteAddress来获取对应的LocalServerChannel,最后调用LocalServerChannel的serve方法创建一个新的peer channel: + +```java +Channel boundChannel = LocalChannelRegistry.get(remoteAddress); +if (!(boundChannel instanceof LocalServerChannel)) { + Exception cause = new ConnectException("connection refused: " + remoteAddress); + safeSetFailure(promise, cause); + close(voidPromise()); + return; +} + +LocalServerChannel serverChannel = (LocalServerChannel) boundChannel; +peer = serverChannel.serve(LocalChannel.this); +``` + +serve方法首先会创建一个新的LocalChannel: + +```java +protected LocalChannel newLocalChannel(LocalChannel peer) { + return new LocalChannel(this, peer); +} +``` + +如果我们把之前的Localchannel称为channelA,这里创建的新的LocalChannel称为channelB。那么最后的结果就是channelA的peer是channelB,而channelB的parent是LocalServerChannel,channelB的peer是channelA。 + +这样就构成了一个对等channel之间的关系。 + +接下来我们看下localChannel的read和write到底是怎么工作的。 + +首先看一下LocalChannel的doWrite方法: + +```java +Object msg = in.current(); +// ... +peer.inboundBuffer.add(ReferenceCountUtil.retain(msg)); +in.remove(); +// ... +finishPeerRead(peer); +``` + +首先从ChannelOutboundBuffer拿到要写入的msg,将其加入peer的inboundBuffer中,最后调用finishPeerRead方法。 + +从方法名字可以看出finishPeerRead就是调用peer的read方法。 + +事实上该方法会调用peer的readInbound方法,从刚刚写入的inboundBuffer中读取消息: + +```java +private void readInbound() { + RecvByteBufAllocator.Handle handle = unsafe().recvBufAllocHandle(); + handle.reset(config()); + ChannelPipeline pipeline = pipeline(); + do { + Object received = inboundBuffer.poll(); + if (received == null) { + break; + } + pipeline.fireChannelRead(received); + } while (handle.continueReading()); + + pipeline.fireChannelReadComplete(); +} +``` + +所以,对于localChannel来说,它的写实际上写入到peer的inboundBuffer中。然后再调用peer的读方法,从inboundBuffer中读取数据。 + +相较于localChannel来说,localServerChannel多了一个serve方法,用来创建peer channel,并调用readInbound开始从inboundBuffer中读取数据。 + +# 总结 + +本章详细介绍了channel和serverChannel的区别,和他们的最简单的本地实现。希望大家对channel和serverChannel的工作原理有了最基本的了解。 \ No newline at end of file diff --git a/learn-netty文章/4eda7812e7eb4825aa471bbc679e7cad.png b/learn-netty文章/4eda7812e7eb4825aa471bbc679e7cad.png new file mode 100644 index 0000000..af2d26d Binary files /dev/null and b/learn-netty文章/4eda7812e7eb4825aa471bbc679e7cad.png differ diff --git a/learn-netty文章/5.netty系列之Event、Handler和Pipeline.md b/learn-netty文章/5.netty系列之Event、Handler和Pipeline.md new file mode 100644 index 0000000..3081aa6 --- /dev/null +++ b/learn-netty文章/5.netty系列之Event、Handler和Pipeline.md @@ -0,0 +1,260 @@ +# netty系列之:Event、Handler和Pipeline + +# 简介 + +上一节我们讲解了netty中的Channel,知道了channel是事件处理器和外部联通的桥梁。今天本文将会详细讲解netty的剩下几个非常总要的部分Event、Handler和PipeLine。 + +# ChannelPipeline + +pipeLine是连接Channel和handler的桥梁,它实际上是一个filter的实现,用于控制其中handler的处理方式。 + +当一个channel被创建的时候,和它对应的ChannelPipeline也会被创建。 + +先看下ChannelPipeline的定义: + +```java +public interface ChannelPipeline + extends ChannelInboundInvoker, ChannelOutboundInvoker, Iterable +``` + +首先ChannelPipeline继承自Iterable,表示它是可遍历的,而遍历的结果就是其中一个个的Handler。 + +作为一个合格的Iterable,ChannelPipeline提供了一系列的add和remote方法,通过这些方法就可以向ChannelPipeline中添加或者移除Handler。因为ChannelPipeline是一个filter,而过滤器是需要指定对应的filter的顺序的,所以ChannelPipeline中有addFirst和addLast这种添加不同顺序的方法。 + +然后可以看到ChannelPipeline继承了ChannelInboundInvoker和ChannelOutboundInvoker两个接口。 + +先看一张channelPipeline的工作流程图: + +![img](c1978325b8844650b66baa7f37036fe1.png) + +可以看出ChannelPipeline主要有两种操作,一种是读入Inbound,一种是写出OutBound。 + +对于Socket.read()这样的读入操作,调用的实际上就是ChannelInboundInvoker中的方法。对于外部的IO写入的请求,调用的就是ChannelOutboundInvoker中的方法。 + +注意,Inbound和outbound的处理顺序是相反的,比如下面的例子: + +```java +ChannelPipeline p = ...; +p.addLast("1", new InboundHandlerA()); +p.addLast("2", new InboundHandlerB()); +p.addLast("3", new OutboundHandlerA()); +p.addLast("4", new OutboundHandlerB()); +p.addLast("5", new InboundOutboundHandlerX()); +``` + +上面的代码中我们向ChannelPipeline添加了5个handler,其中2个InboundHandler,2个OutboundHandler和一个同时处理In和Out的Handler。 + +那么当channel遇到inbound event的时候,就会按照1,2,3,4,5的顺序进行处理,但是只有InboundHandler才能处理Inbound事件,所以,真正执行的顺序是1,2,5。 + +同样的当channel遇到outbound event的时候,会按照5,4,3,2,1的顺序进行执行,但是只有outboundHandler才能处理Outbound事件,所以真正执行的顺序是5,4,3. + +简单的说,ChannelPipeline指定了Handler的执行顺序。 + +# ChannelHandler + +netty是一个事件驱动的框架,所有的event都是由Handler来进行处理的。ChannelHandler可以处理IO、拦截IO或者将event传递给ChannelPipeline中的下一个Handler进行处理。 + +ChannelHandler的结构很简单,只有三个方法,分别是: + +```java +void handlerAdded(ChannelHandlerContext ctx) throws Exception; +void handlerRemoved(ChannelHandlerContext ctx) throws Exception; +void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception; +``` + +根据inbound和outbound事件的不同,ChannelHandler可以分为两类,分别是ChannelInboundHandler 和ChannelOutboundHandler. + +因为这两个都是interface,实现起来比较麻烦,所以netty为大家提供了三个默认的实现:ChannelInboundHandlerAdapter,ChannelOutboundHandlerAdapter和ChannelDuplexHandler。前面两个很好理解,分别是inbound和outbound,最后一个可以同时处理inbound和outbound。 + +ChannelHandler是由ChannelHandlerContext提供的,并且和ChannelPipeline的交互也是通过ChannelHandlerContext来进行的。 + +# ChannelHandlerContext + +ChannelHandlerContext可以让ChannelHandler和ChannelPipeline或者其他的Handler进行交互。它就是一个上下文环境,使得Handler和Channel可以相互作用。 + +如可以在ChannelHandlerContext中,调用channel()获得绑定的channel。可以通过调用handler()获得绑定的Handler。通过调用fire*方法来触发Channel的事件。 + +看下ChannelHandlerContext的定义: + +```java +public interface ChannelHandlerContext extends AttributeMap, ChannelInboundInvoker, ChannelOutboundInvoker +``` + +可以看到他是一个AttributeMap用来存储属性,还是一个ChannelInboundInvoker和ChannelOutboundInvoker用来触发和传播相应的事件。 + +对于Inbound来说传播事件的方法有: + +```java +ChannelHandlerContext.fireChannelRegistered() +ChannelHandlerContext.fireChannelActive() +ChannelHandlerContext.fireChannelRead(Object) +ChannelHandlerContext.fireChannelReadComplete() +ChannelHandlerContext.fireExceptionCaught(Throwable) +ChannelHandlerContext.fireUserEventTriggered(Object) +ChannelHandlerContext.fireChannelWritabilityChanged() +ChannelHandlerContext.fireChannelInactive() +ChannelHandlerContext.fireChannelUnregistered() +``` + +对于Outbound来说传播事件的方法有: + +```java +ChannelHandlerContext.bind(SocketAddress, ChannelPromise) +ChannelHandlerContext.connect(SocketAddress, SocketAddress, ChannelPromise) +ChannelHandlerContext.write(Object, ChannelPromise) +ChannelHandlerContext.flush() +ChannelHandlerContext.read() +ChannelHandlerContext.disconnect(ChannelPromise) +ChannelHandlerContext.close(ChannelPromise) +ChannelHandlerContext.deregister(ChannelPromise) +``` + +这些方法,在一个Handler中调用,然后将事件传递给下一个Handler,如下所示: + +```java +public class MyInboundHandler extends ChannelInboundHandlerAdapter { + @Override + public void channelActive(ChannelHandlerContext ctx) { + System.out.println("Connected!"); + ctx.fireChannelActive(); + } +} + +public class MyOutboundHandler extends ChannelOutboundHandlerAdapter { + @Override + public void close(ChannelHandlerContext ctx, ChannelPromise promise) { + System.out.println("Closing .."); + ctx.close(promise); + } +} +``` + +# ChannelHandler中的状态变量 + +ChannelHandler是一个Handler类,一般情况下,这个类的实例是可以被多个channel共同使用的,前提是这个ChannelHandler没有共享的状态变量。 + +但有时候,我们必须要在ChannelHandler中保持一个状态,那么就涉及到ChannelHandler中的状态变量的问题,看下面的一个例子: + +```java +public interface Message { + // your methods here +} + +public class DataServerHandler extends SimpleChannelInboundHandler { + + private boolean loggedIn; + + @Override + public void channelRead0(ChannelHandlerContext ctx, Message message) { + if (message instanceof LoginMessage) { + authenticate((LoginMessage) message); + loggedIn = true; + } else (message instanceof GetDataMessage) { + if (loggedIn) { + ctx.writeAndFlush(fetchSecret((GetDataMessage) message)); + } else { + fail(); + } + } + } + ... +} +``` + +这个例子中,我们需要在收到LoginMessage之后,对消息进行认证,并保存认证状态,因为业务逻辑是这样的,所以必须要有一个状态变量。 + +那么这样带有状态变量的Handler就只能绑定一个channel,如果绑定多个channel就有可能出现状态不一致的问题。一个channel绑定一个Handler实例,很简单,只需要在initChannel方法中使用new关键字新建一个对象即可。 + +```java + public class DataServerInitializer extends ChannelInitializer { + @Override + public void initChannel(Channel channel) { + channel.pipeline().addLast("handler", new DataServerHandler()); + } + } +``` + +那么除了新建handler实例之外,还有没有其他的办法呢?当然是有的,那就是 ChannelHandlerContext 中的AttributeKey属性。还是上面的例子,我们看一下使用AttributeKey应该怎么实现: + +```java + public interface Message { + // your methods here + } + + @Sharable + public class DataServerHandler extends SimpleChannelInboundHandler { + private final AttributeKey auth = + AttributeKey.valueOf("auth"); + + @Override + public void channelRead(ChannelHandlerContext ctx, Message message) { + Attribute attr = ctx.attr(auth); + if (message instanceof LoginMessage) { + authenticate((LoginMessage) o); + attr.set(true); + } else (message instanceof GetDataMessage) { + if (Boolean.TRUE.equals(attr.get())) { + ctx.writeAndFlush(fetchSecret((GetDataMessage) o)); + } else { + fail(); + } + } + } + ... + } +``` + +上例中,首先定义了一个AttributeKey,然后使用ChannelHandlerContext的attr方法将Attribute设置到ChannelHandlerContext中,这样该Attribute绑定到这个ChannelHandlerContext中了。后续即使使用同一个Handler在不同的Channel中该属性也是不同的。 + +下面是使用共享Handler的例子: + +```java + public class DataServerInitializer extends ChannelInitializer { + + private static final DataServerHandler SHARED = new DataServerHandler(); + + @Override + public void initChannel(Channel channel) { + channel.pipeline().addLast("handler", SHARED); + } + } +``` + +注意,在定义DataServerHandler的时候,我们加上了@Sharable注解,如果一个ChannelHandler使用了@Sharable注解,那就意味着你可以只创建一次这个Handler,但是可以将其绑定到一个或者多个ChannelPipeline中。 + +> 注意,@Sharable注解是为java文档准备的,并不会影响到实际的代码执行效果。 + +# 异步Handler + +之前介绍了,可以通过调用pipeline.addLast方法将handler加入到pipeline中,因为pipeline是一个filter的结构,所以加入的handler是顺序进行处理的。 + +但是,我希望某些handler是在新的线程中执行该怎么办?如果我们希望这些新的线程中执行的Handler是无序的又该怎么办? + +比如我们现在有3个handler分别是MyHandler1,MyHandler2和MyHandler3。 + +顺序执行的写法是这样的: + +```java +ChannelPipeline pipeline = ch.pipeline(); + +pipeline.addLast("MyHandler1", new MyHandler1()); +pipeline.addLast("MyHandler2", new MyHandler2()); +pipeline.addLast("MyHandler3", new MyHandler3()); +``` + +如果要让MyHandler3在新的线程中执行,则可以加入group选项,从而让handler在新的group中运行: + +```java +static final EventExecutorGroup group = new DefaultEventExecutorGroup(16); +ChannelPipeline pipeline = ch.pipeline(); + +pipeline.addLast("MyHandler1", new MyHandler1()); +pipeline.addLast("MyHandler2", new MyHandler2()); +pipeline.addLast(group,"MyHandler3", new MyHandler3()); +``` + +但是上例中DefaultEventExecutorGroup加入的Handler也是会顺序执行的,如果确实不想顺序执行,那么可以尝试考虑使用UnorderedThreadPoolEventExecutor 。 + +# 总结 + +本文讲解了Event、Handler和PipeLine,并举例说明他们之间的关系和相互作用。后续会从netty的具体实践出发,进一步加深对netty的理解和应用,希望大家能够喜欢。 \ No newline at end of file diff --git a/learn-netty文章/50.netty系列之channelPipeline详解.md b/learn-netty文章/50.netty系列之channelPipeline详解.md new file mode 100644 index 0000000..261ca80 --- /dev/null +++ b/learn-netty文章/50.netty系列之channelPipeline详解.md @@ -0,0 +1,240 @@ +# netty系列之:channelPipeline详解 + +# 简介 + +我们在介绍channel的时候提到过,几乎channel中所有的实现都是通过channelPipeline进行的,作为一个pipline,它到底是如何工作的呢? + +一起来看看吧。 + +# ChannelPipeline + +ChannelPipeline是一个interface,它继承了三个接口,分别是ChannelInboundInvoker,ChannelOutboundInvoker和Iterable: + +```java +public interface ChannelPipeline + extends ChannelInboundInvoker, ChannelOutboundInvoker, Iterable> +``` + +继承自ChannelInboundInvoker,表示ChannelPipeline可以触发channel inboud的一些事件,比如: + +```java +ChannelInboundInvoker fireChannelRegistered(); +ChannelInboundInvoker fireChannelUnregistered(); +ChannelInboundInvoker fireChannelActive(); +ChannelInboundInvoker fireChannelInactive(); +ChannelInboundInvoker fireExceptionCaught(Throwable cause); +ChannelInboundInvoker fireUserEventTriggered(Object event); +ChannelInboundInvoker fireChannelRead(Object msg); +ChannelInboundInvoker fireChannelReadComplete(); +ChannelInboundInvoker fireChannelWritabilityChanged(); +``` + +继承自ChannelOutboundInvoker,表示ChannelPipeline可以进行一些channel的主动操作,如:bind,connect,disconnect,close,deregister,read,write,flush等操作。 + +继承自Iterable,表示ChannelPipeline是可遍历的,为什么ChannelPipeline是可遍历的呢? + +因为ChannelPipeline中可以添加一个或者多个ChannelHandler,ChannelPipeline可以看做是一个ChannelHandler的集合。 + +比如ChannelPipeline提供了一系列的添加ChannelHandler的方法: + +```java +ChannelPipeline addFirst(String name, ChannelHandler handler); +ChannelPipeline addFirst(EventExecutorGroup group, String name, ChannelHandler handler); +ChannelPipeline addFirst(EventExecutorGroup group, ChannelHandler... handlers); +ChannelPipeline addFirst(ChannelHandler... handlers); + +ChannelPipeline addLast(String name, ChannelHandler handler); +ChannelPipeline addLast(EventExecutorGroup group, String name, ChannelHandler handler); +ChannelPipeline addLast(ChannelHandler... handlers); +ChannelPipeline addLast(EventExecutorGroup group, ChannelHandler... handlers); + +ChannelPipeline addBefore(String baseName, String name, ChannelHandler handler); +ChannelPipeline addBefore(EventExecutorGroup group, String baseName, String name, ChannelHandler handler); +ChannelPipeline addAfter(String baseName, String name, ChannelHandler handler); +ChannelPipeline addAfter(EventExecutorGroup group, String baseName, String name, ChannelHandler handler); +``` + +可以从前面添加,也可以从后面添加,或者从特定的位置添加handler。 + +另外还可以从pipeline中删除特定的channelHandler,或者移出和替换特定位置的handler: + +```java +ChannelPipeline remove(ChannelHandler handler); +ChannelHandler remove(String name); +ChannelHandler removeFirst(); +ChannelHandler removeLast(); +ChannelPipeline replace(ChannelHandler oldHandler, String newName, ChannelHandler newHandler); +ChannelHandler replace(String oldName, String newName, ChannelHandler newHandler); +``` + +当然,更少不了对应的查询操作: + +```java +ChannelHandler first(); +ChannelHandler last(); +ChannelHandler get(String name); +List names(); +``` + +还可以根据传入的ChannelHandler获得handler对应的ChannelHandlerContext。 + +```java +ChannelHandlerContext context(ChannelHandler handler); +``` + +ChannelPipeline中还有一些触发channel相关的事件,如: + +```java +ChannelPipeline fireChannelRegistered(); +ChannelPipeline fireChannelUnregistered(); +ChannelPipeline fireChannelActive(); +ChannelPipeline fireChannelInactive(); +ChannelPipeline fireExceptionCaught(Throwable cause); +ChannelPipeline fireUserEventTriggered(Object event); +ChannelPipeline fireChannelRead(Object msg); +ChannelPipeline fireChannelReadComplete(); +ChannelPipeline fireChannelWritabilityChanged(); +``` + +## 事件传递 + +那么有些朋友可能会问了,既然ChannelPipeline中包含了很多个handler,那么handler中的事件是怎么传递的呢? + +其实这些事件是通过调用ChannelHandlerContext中的相应方法来触发的。 + +对于Inbound事件来说,可以调用下面的方法,进行事件的传递: + +```java +ChannelHandlerContext.fireChannelRegistered() +ChannelHandlerContext.fireChannelActive() +ChannelHandlerContext.fireChannelRead(Object) +ChannelHandlerContext.fireChannelReadComplete() +ChannelHandlerContext.fireExceptionCaught(Throwable) +ChannelHandlerContext.fireUserEventTriggered(Object) +ChannelHandlerContext.fireChannelWritabilityChanged() +ChannelHandlerContext.fireChannelInactive() +ChannelHandlerContext.fireChannelUnregistered() +``` + +对于Outbound事件来说,可以调用下面的方法,进行事件的传递: + +```java +ChannelHandlerContext.bind(SocketAddress, ChannelPromise) +ChannelHandlerContext.connect(SocketAddress, SocketAddress, ChannelPromise) +ChannelHandlerContext.write(Object, ChannelPromise) +ChannelHandlerContext.flush() +ChannelHandlerContext.read() +ChannelHandlerContext.disconnect(ChannelPromise) +ChannelHandlerContext.close(ChannelPromise) +ChannelHandlerContext.deregister(ChannelPromise) +``` + +具体而言,就是在handler中调用ChannelHandlerContext中对应的方法: + +```java +public class MyInboundHandler extends ChannelInboundHandlerAdapter { + @Override + public void channelActive(ChannelHandlerContext ctx) { + System.out.println("Connected!"); + ctx.fireChannelActive(); + } +} + +public class MyOutboundHandler extends ChannelOutboundHandlerAdapter { + @Override + public void close(ChannelHandlerContext ctx, ChannelPromise promise) { + System.out.println("Closing .."); + ctx.close(promise); + } +} +``` + +# DefaultChannelPipeline + +ChannelPipeline有一个官方的实现叫做DefaultChannelPipeline,因为对于pipeline来说,主要的功能就是进行handler的管理和事件传递,相对于而言功能比较简单,但是他也有一些特别的实现地方,比如它有两个AbstractChannelHandlerContext类型的head和tail。 + +我们知道ChannelPipeline实际上是很多handler的集合,那么这些集合是怎么进行存储的呢?这种存储的数据结构就是AbstractChannelHandlerContext。每个AbstractChannelHandlerContext中都有一个next节点和一个prev节点,用来组成一个双向链表。 + +同样的在DefaultChannelPipeline中使用head和tail来将封装好的handler存储起来。 + +注意,这里的head和tail虽然都是AbstractChannelHandlerContext,但是两者有稍许不同。先看下head和tail的定义: + +```java +protected DefaultChannelPipeline(Channel channel) { + this.channel = ObjectUtil.checkNotNull(channel, "channel"); + succeededFuture = new SucceededChannelFuture(channel, null); + voidPromise = new VoidChannelPromise(channel, true); + + tail = new TailContext(this); + head = new HeadContext(this); + + head.next = tail; + tail.prev = head; +} +``` + +在DefaultChannelPipeline的构造函数中,对tail和head进行初始化,其中tail是TailContext,而head是HeadContext。 + +其中TailContext实现了ChannelInboundHandler接口: + +```java +final class TailContext extends AbstractChannelHandlerContext implements ChannelInboundHandler +``` + +而HeadContext实现了ChannelOutboundHandler和ChannelInboundHandler接口: + +```java +final class HeadContext extends AbstractChannelHandlerContext + implements ChannelOutboundHandler, ChannelInboundHandler +``` + +下面我们以addFirst方法为例,来看一下handler是怎么被加入pipline的: + +```java +public final ChannelPipeline addFirst(EventExecutorGroup group, String name, ChannelHandler handler) { + final AbstractChannelHandlerContext newCtx; + synchronized (this) { + checkMultiplicity(handler); + name = filterName(name, handler); + + newCtx = newContext(group, name, handler); + + addFirst0(newCtx); + + // If the registered is false it means that the channel was not registered on an eventLoop yet. + // In this case we add the context to the pipeline and add a task that will call + // ChannelHandler.handlerAdded(...) once the channel is registered. + if (!registered) { + newCtx.setAddPending(); + callHandlerCallbackLater(newCtx, true); + return this; + } + + EventExecutor executor = newCtx.executor(); + if (!executor.inEventLoop()) { + callHandlerAddedInEventLoop(newCtx, executor); + return this; + } + } + callHandlerAdded0(newCtx); + return this; +} +``` + +它的工作逻辑是首先根据传入的handler构建一个新的context,然后调用addFirst0方法,将context加入AbstractChannelHandlerContext组成的双向链表中: + +```java +private void addFirst0(AbstractChannelHandlerContext newCtx) { + AbstractChannelHandlerContext nextCtx = head.next; + newCtx.prev = head; + newCtx.next = nextCtx; + head.next = newCtx; + nextCtx.prev = newCtx; +} +``` + +然后调用callHandlerAdded0方法来触发context的handlerAdded方法。 + +# 总结 + +channelPipeline负责管理channel的各种handler,在DefaultChannelPipeline中使用了AbstractChannelHandlerContext的head和tail来对多个handler进行存储,同时借用这个链式结构对handler进行各种管理,非常方便。 \ No newline at end of file diff --git a/learn-netty文章/51.netty系列之channelHandlerContext详解.md b/learn-netty文章/51.netty系列之channelHandlerContext详解.md new file mode 100644 index 0000000..569d4bf --- /dev/null +++ b/learn-netty文章/51.netty系列之channelHandlerContext详解.md @@ -0,0 +1,236 @@ +# netty系列之:channelHandlerContext详解 + +# 简介 + +我们知道ChannelHandler有两个非常重要的子接口,分别是ChannelOutboundHandler和ChannelInboundHandler,基本上这两个handler接口定义了所有channel inbound和outbound的处理逻辑。 + +不管是ChannelHandler还是ChannelOutboundHandler和ChannelInboundHandler,几乎他们中所有的方法都带有一个ChannelHandlerContext参数,那么这个ChannelHandlerContext到底是做什么用的呢?它和handler、channel有什么关系呢? + +# ChannelHandlerContext和它的应用 + +熟悉netty的朋友应该都接触过ChannelHandlerContext,如果没有的话,这里有一个简单的handler的例子: + +```java +public class ChatServerHandler extends SimpleChannelInboundHandler { + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + log.info("accepted channel: {}", ctx.channel()); + log.info("accepted channel parent: {}", ctx.channel().parent()); + // channel活跃 + ctx.write("Channel Active状态!\r\n"); + ctx.flush(); + } +} +``` + +这里的handler继承了SimpleChannelInboundHandler,只需要实现对应的方法即可。这里实现的是channelActive方法,在channelActive方法中,传入了一个ChannelHandlerContext参数,我们可以通过使用ChannelHandlerContext来调用它的一些方法。 + +先来看一下ChannelHandlerContext的定义: + +```java +public interface ChannelHandlerContext extends AttributeMap, ChannelInboundInvoker, ChannelOutboundInvoker { +``` + +首先ChannelHandlerContext是一个AttributeMap,可以用来存储多个数据。 + +然后ChannelHandlerContext继承了ChannelInboundInvoker和ChannelOutboundInvoker,可以触发inbound和outbound的一些方法。 + +除了继承来的一些方法之外,ChannelHandlerContext还可以作为channel,handler和pipline的沟通桥梁,因为可以从ChannelHandlerContext中获取到对应的channel,handler和pipline: + +```java +Channel channel(); +ChannelHandler handler(); +ChannelPipeline pipeline(); +``` + +还要注意的是ChannelHandlerContext还返回一个EventExecutor,用来执行特定的任务: + +```java +EventExecutor executor(); +``` + +接下来,我们具体看一下ChannelHandlerContext的实现。 + +# AbstractChannelHandlerContext + +AbstractChannelHandlerContext是ChannelHandlerContext的一个非常重要的实现,虽然AbstractChannelHandlerContext是一个抽象类,但是它基本上实现了ChannelHandlerContext的所有功能。 + +首先看一下AbstractChannelHandlerContext的定义: + +```java +abstract class AbstractChannelHandlerContext implements ChannelHandlerContext, ResourceLeakHint +``` + +AbstractChannelHandlerContext是ChannelHandlerContext的一个具体实现。 + +通常来说一个handler对应一个ChannelHandlerContext,但是在一个程序中可能会有多于一个handler,那么如何在一个handler中获取其他的handler呢? + +在AbstractChannelHandlerContext中有两个同样是AbstractChannelHandlerContext类型的next和prev,从而使得多个AbstractChannelHandlerContext可以构建一个双向链表。从而可以在一个ChannelHandlerContext中,获取其他的ChannelHandlerContext,从而获得handler处理链。 + +```java +volatile AbstractChannelHandlerContext next; +volatile AbstractChannelHandlerContext prev; +``` + +AbstractChannelHandlerContext中的pipeline和executor都是通过构造函数传入的: + +```java +AbstractChannelHandlerContext(DefaultChannelPipeline pipeline, EventExecutor executor, + String name, Class handlerClass) { + this.name = ObjectUtil.checkNotNull(name, "name"); + this.pipeline = pipeline; + this.executor = executor; + this.executionMask = mask(handlerClass); + // Its ordered if its driven by the EventLoop or the given Executor is an instanceof OrderedEventExecutor. + ordered = executor == null || executor instanceof OrderedEventExecutor; +} +``` + +可能有朋友会有疑问了,ChannelHandlerContext中的channel和handler是如何得到的呢? + +对于channel来说,是通过pipeline来获取的: + +```java +public Channel channel() { + return pipeline.channel(); +} +``` + +对于handler来说,在AbstractChannelHandlerContext中并没有对其进行实现,需要在继承AbstractChannelHandlerContext的类中进行实现。 + +对于EventExecutor来说,可以通过构造函数向AbstractChannelHandlerContext传入一个新的EventExecutor,如果没有传入或者传入为空的话,则会使用channel中自带的EventLoop: + +```java +public EventExecutor executor() { + if (executor == null) { + return channel().eventLoop(); + } else { + return executor; + } +} +``` + +因为EventLoop继承自OrderedEventExecutor,所以它也是一个EventExecutor。 + +EventExecutor主要用来异步提交任务来执行,事实上ChannelHandlerContext中几乎所有来自于ChannelInboundInvoker和ChannelOutboundInvoker的方法都是通过EventExecutor来执行的。 + +对于ChannelInboundInvoker来说,我们以方法fireChannelRegistered为例: + +```java +public ChannelHandlerContext fireChannelRegistered() { + invokeChannelRegistered(findContextInbound(MASK_CHANNEL_REGISTERED)); + return this; +} + +static void invokeChannelRegistered(final AbstractChannelHandlerContext next) { + EventExecutor executor = next.executor(); + if (executor.inEventLoop()) { + next.invokeChannelRegistered(); + } else { + executor.execute(new Runnable() { + @Override + public void run() { + next.invokeChannelRegistered(); + } + }); + } +} +``` + +fireChannelRegistered调用了invokeChannelRegistered方法,invokeChannelRegistered则调用EventExecutor的execute方法,将真实的调用逻辑封装在一个runnable类中执行。 + +注意,在调用executor.execute方法之前有一个executor是否在eventLoop中的判断。如果executor已经在eventLoop中了,那么直接执行任务即可,不需要启用新的线程。 + +对于ChannelOutboundInvoker来说,我们以bind方法为例,看一下EventExecutor是怎么使用的: + +```java +public ChannelFuture bind(final SocketAddress localAddress, final ChannelPromise promise) { + ObjectUtil.checkNotNull(localAddress, "localAddress"); + if (isNotValidPromise(promise, false)) { + // cancelled + return promise; + } + + final AbstractChannelHandlerContext next = findContextOutbound(MASK_BIND); + EventExecutor executor = next.executor(); + if (executor.inEventLoop()) { + next.invokeBind(localAddress, promise); + } else { + safeExecute(executor, new Runnable() { + @Override + public void run() { + next.invokeBind(localAddress, promise); + } + }, promise, null, false); + } + return promise; +} +``` + +可以看到执行的逻辑和invokeChannelRegistered方法很类似,也是先判断executor在不在eventLoop中,如果在的话直接执行,如果不在则放在executor中执行。 + +上面的两个例子中都调用了next的相应方法,分别是next.invokeChannelRegistered和next.invokeBind。 + +我们知道ChannelHandlerContext只是一个封装,它本身并没有太多的业务逻辑,所以next调用的相应方法,实际上是Context中封装的ChannelInboundHandler和ChannelOutboundHandler中的业务逻辑,如下所示: + +```java +private void invokeUserEventTriggered(Object event) { + if (invokeHandler()) { + try { + ((ChannelInboundHandler) handler()).userEventTriggered(this, event); + } catch (Throwable t) { + invokeExceptionCaught(t); + } + } else { + fireUserEventTriggered(event); + } +} +private void invokeBind(SocketAddress localAddress, ChannelPromise promise) { + if (invokeHandler()) { + try { + ((ChannelOutboundHandler) handler()).bind(this, localAddress, promise); + } catch (Throwable t) { + notifyOutboundHandlerException(t, promise); + } + } else { + bind(localAddress, promise); + } +} +``` + +所以,从AbstractChannelHandlerContext可以得知,ChannelHandlerContext接口中定义的方法都是调用的handler中具体的实现,Context只是对handler的封装。 + +# DefaultChannelHandlerContext + +DefaultChannelHandlerContext是AbstractChannelHandlerContext的一个具体实现。 + +我们在讲解AbstractChannelHandlerContext的时候提到过,AbstractChannelHandlerContext中并没有定义具体的handler的实现,而这个实现是在DefaultChannelHandlerContext中进行的。 + +DefaultChannelHandlerContext很简单,我们看一下它的具体实现: + +```java +final class DefaultChannelHandlerContext extends AbstractChannelHandlerContext { + + private final ChannelHandler handler; + + DefaultChannelHandlerContext( + DefaultChannelPipeline pipeline, EventExecutor executor, String name, ChannelHandler handler) { + super(pipeline, executor, name, handler.getClass()); + this.handler = handler; + } + + @Override + public ChannelHandler handler() { + return handler; + } +} +``` + +DefaultChannelHandlerContext中额外提供了一个ChannelHandler属性,用来存储传入的ChannelHandler。 + +到此DefaultChannelHandlerContext可以传入ChannelHandlerContext中一切必须的handler,channel,pipeline和EventExecutor。 + +# 总结 + +本节我们介绍了ChannelHandlerContext和它的几个基本实现,了解到了ChannelHandlerContext是对handler,channel和pipline的封装,ChannelHandlerContext中的业务逻辑,实际上是调用的是底层的handler的对应方法。这也是我们在自定义handler中需要实现的方法。 \ No newline at end of file diff --git a/learn-netty文章/52.netty系列之EventExecutor,EventExecutorGroup和netty中的实现.md b/learn-netty文章/52.netty系列之EventExecutor,EventExecutorGroup和netty中的实现.md new file mode 100644 index 0000000..e194ece --- /dev/null +++ b/learn-netty文章/52.netty系列之EventExecutor,EventExecutorGroup和netty中的实现.md @@ -0,0 +1,336 @@ +# netty系列之:EventExecutor,EventExecutorGroup和netty中的实现 + +# 简介 + +netty作为一个异步NIO框架,多线程肯定是它的基础,但是对于netty的实际使用者来说,一般是不需要接触到多线程的,我们只需要按照netty框架规定的流程走下去,自定义handler来处理对应的消息即可。 + +那么有朋友会问了,作为一个NIO框架,netty的多线程到底体现在什么地方呢?它的底层原理是什么呢? + +今天带大家来看看netty中的任务执行器EventExecutor和EventExecutorGroup。 + +# EventExecutorGroup + +因为EventExecutor继承自EventExecutorGroup,这里我们先来详细讲解一下EventExecutorGroup。 + +先看下EventExecutorGroup的定义: + +``` +public interface EventExecutorGroup extends ScheduledExecutorService, Iterable +``` + +EventExecutorGroup继承自JDK的ScheduledExecutorService,即可以执行定时任务,也可以像普通的任务执行器一样提交任务去执行。 + +同时EventExecutorGroup还继承了Iterable接口,表示EventExecutorGroup是可遍历的,它的遍历对象是EventExecutor。 + +EventExecutorGroup有两个和Iterable相关的方法,分别是next和iterator: + +``` + EventExecutor next(); + + @Override + Iterator iterator(); +``` + +在EventExecutorGroup中调用next方法会返回一个EventExecutor对象,那么EventExecutorGroup和EventExecutor是什么关系呢? + +我们再来看一下EventExecutor的定义: + +``` +public interface EventExecutor extends EventExecutorGroup +``` + +可以看到EventExecutor实际上是EventExecutorGroup的子类。但是在父类EventExecutorGroup中居然有对子类EventExecutor的引用。 + +> 这种在父类的Group中引用返回子类的设计模式在netty中非常常见,大家可以自行体会一下这样的设计到底是好还是坏。 + +EventExecutorGroup作为一个EventExecutor的Group对象,它是用来管理group中的EventExecutor的。所以在EventExecutorGroup中设计了一些对EventExecutor的统一管理接口。 + +比如`boolean isShuttingDown()`方法用来判断这个group中的所有EventExecutor全都在被shutdown或者已经被shutdown。 + +另外EventExecutorGroupt提供了shutdown group中所有EventExector的方法:`Future shutdownGracefully()`和 terminate方法:`Future terminationFuture()`。 + +这两个方法都返回了Future,所以我们可以认为这两个方法是异步方法。 + +EventExecutorGroup中其他的方法都是一些对JDK中ScheduledExecutorService方法的重写,比如submit,schedule,scheduleAtFixedRate,scheduleWithFixedDelay等。 + +# EventExecutor + +接下来我们再研究一下EventExecutor,在上一节中,我们简单的提到了EventExecutor继承自EventExecutorGroup,和EventExecutorGroup相比,EventExecutor有哪些新的方法呢? + +我们知道EventExecutorGroup继承了Iterable,并且定义了一个next方法用来返回Group中的一个EventExecutor对象。 + +因为Group中有很多个EventExecutor,至于具体返回哪一个EventExecutor,还是由具体的实现类来实现的。 + +在EventExecutor中,它重写了这个方法: + +``` +@Override +EventExecutor next(); +``` + +这里的next方法,返回的是EventExecutor本身。 + +另外,因为EventExecutor是由EventExecutorGroup来管理的,所以EventExecutor中还存在一个parent方法,用来返回管理EventExecutor的EventExecutorGroup: + +``` +EventExecutorGroup parent(); +``` + +EventExecutor中新加了两个inEventLoop方法,用来判断给定的线程是否在event loop中执行。 + +``` + boolean inEventLoop(); + + boolean inEventLoop(Thread thread); +``` + +EventExecutor还提供两个方法可以返回Promise和ProgressivePromise. + +``` + Promise newPromise(); + ProgressivePromise newProgressivePromise(); +``` + +熟悉ECMAScript的朋友可能知道,Promise是ES6引入的一个新的语法功能,用来解决回调地狱的问题。这里的netty引入的Promise继承自Future,并且添加了两个success和failure的状态。 + +ProgressivePromise更进一步,在Promise基础上,提供了一个progress来表示进度。 + +除此之外,EventExecutor还提供了对Succeeded的结果和Failed异常封装成为Future的方法。 + +``` + Future newSucceededFuture(V result); + + Future newFailedFuture(Throwable cause); +``` + +# EventExecutorGroup在netty中的基本实现 + +EventExecutorGroup和EventExecutor在netty中有很多非常重要的实现,其中最常见的就是EventLoop和EventLoopGroup,鉴于EventLoop和EventLoopGroup的重要性,我们会在后面的章节中重点讲解他们。这里先来看下netty中的其他实现。 + +netty中EventExecutorGroup的默认实现叫做DefaultEventExecutorGroup,它的继承关系如下所示: + +![img](ae242899a8234b668045716daa2eec1a.png) + +可以看到DefaultEventExecutorGroup继承自MultithreadEventExecutorGroup,而MultithreadEventExecutorGroup又继承自AbstractEventExecutorGroup。 + +先看下AbstractEventExecutorGroup的逻辑。AbstractEventExecutorGroup基本上是对EventExecutorGroup中接口的一些实现。 + +我们知道EventExecutorGroup中定义了一个next()方法,可以返回Group中的一个EventExecutor。 + +在AbstractEventExecutorGroup中,几乎所有EventExecutorGroup中的方法实现,都是调用next()方法来完成的,以submit方法为例: + +``` +public Future submit(Runnable task) { + return next().submit(task); + } +``` + +可以看到submit方法首先调用next获取到的EventExecutor,然后再调用EventExecutor中的submit方法。 + +AbstractEventExecutorGroup中的其他方法都是这样的实现。但是AbstractEventExecutorGroup中并没有实现next()方法,具体如何从Group中获取到EventExecutor,还需要看底层的具体实现。 + +MultithreadEventExecutorGroup继承自AbstractEventExecutorGroup,提供了多线程任务的支持。 + +MultithreadEventExecutorGroup有两类构造函数,在构造函数中可以指定多线程的个数,还有任务执行器Executor,如果没有提供Executor的话,可以提供一个ThreadFactory,MultithreadEventExecutorGroup会调用`new ThreadPerTaskExecutor(threadFactory)`来为每一个线程构造一个Executor: + +``` + protected MultithreadEventExecutorGroup(int nThreads, ThreadFactory threadFactory, Object... args) { + this(nThreads, threadFactory == null ? null : new ThreadPerTaskExecutor(threadFactory), args); + } + + protected MultithreadEventExecutorGroup(int nThreads, Executor executor, Object... args) { + this(nThreads, executor, DefaultEventExecutorChooserFactory.INSTANCE, args); + } +``` + +MultithreadEventExecutorGroup对多线程的支持是怎么实现的呢? + +首先MultithreadEventExecutorGroup提供了两个children,分别是children和readonlyChildren: + +``` + private final EventExecutor[] children; + private final Set readonlyChildren; +``` + +children和MultithreadEventExecutorGroup中的线程个数是一一对应的,有多少个线程,children就有多大。 + +``` +children = new EventExecutor[nThreads]; +``` + +然后通过调用newChild方法,将传入的executor构造成为EventExecutor返回: + +``` +children[i] = newChild(executor, args); +``` + +看一下newChild方法的定义: + +``` + protected abstract EventExecutor newChild(Executor executor, Object... args) throws Exception; +``` + +这个方法在MultithreadEventExecutorGroup中并没有实现,需要在更具体的类中实现它。 + +readonlyChildren是child的只读版本,用来在遍历方法中返回: + +``` +readonlyChildren = Collections.unmodifiableSet(childrenSet); + +public Iterator iterator() { + return readonlyChildren.iterator(); + } +``` + +我们现在有了Group中的所有EventExecutor,那么在MultithreadEventExecutorGroup中,next方法是怎么选择具体返回哪一个EventExecutor呢? + +先来看一下next方法的定义: + +``` +private final EventExecutorChooserFactory.EventExecutorChooser chooser; + +chooser = chooserFactory.newChooser(children); + +public EventExecutor next() { + return chooser.next(); + } +``` + +next方法调用的是chooser的next方法,看一下chooser的next方法具体实现: + +``` +public EventExecutor next() { + return executors[(int) Math.abs(idx.getAndIncrement() % executors.length)]; + } +``` + +可以看到,其实是一个很简单的根据index来获取对象的操作。 + +最后看一下DefaultEventExecutorGroup中对newChild方法的实现: + +``` + protected EventExecutor newChild(Executor executor, Object... args) throws Exception { + return new DefaultEventExecutor(this, executor, (Integer) args[0], (RejectedExecutionHandler) args[1]); + } +``` + +newChild返回的EventExecutor使用的是DefaultEventExecutor。这个类是EventExecutor在netty中的默认实现,我们在下一小结中详细进行讲解。 + +# EventExecutor在netty中的基本实现 + +EventExecutor在netty中的默认实现是DefaultEventExecutor,先看下它的继承结构: + +![img](67a3434b3dc14e1d98baf8cb5dbf07cf.png) + +DefaultEventExecutor继承自SingleThreadEventExecutor,而SingleThreadEventExecutor又继承自AbstractScheduledEventExecutor,AbstractScheduledEventExecutor继承自AbstractEventExecutor。 + +先来看一下AbstractEventExecutor的定义: + +``` +public abstract class AbstractEventExecutor extends AbstractExecutorService implements EventExecutor +``` + +AbstractEventExecutor继承了AbstractExecutorService,并且实现了EventExecutor接口。 + +AbstractExecutorService是JDK中的类,它提供了 ExecutorService 的一些实现,比如submit, invokeAny and invokeAll等方法。 + +AbstractEventExecutor作为ExecutorGroup的一员,它提供了一个EventExecutorGroup类型的parent属性: + +``` +private final EventExecutorGroup parent; + +public EventExecutorGroup parent() { + return parent; + } +``` + +对于next方法来说,AbstractEventExecutor返回的是它本身: + +``` +public EventExecutor next() { + return this; + } +``` + +AbstractScheduledEventExecutor继承自AbstractEventExecutor,它内部使用了一个PriorityQueue来存储包含定时任务的ScheduledFutureTask,从而实现定时任务的功能: + +``` +PriorityQueue> scheduledTaskQueue; +``` + +接下来是SingleThreadEventExecutor,从名字可以看出,SingleThreadEventExecutor使用的是单线程来执行提交的tasks,SingleThreadEventExecutor提供了一个默认的pending执行task的任务大小:DEFAULT_MAX_PENDING_EXECUTOR_TASKS,还定义了任务执行的几种状态: + +``` + private static final int ST_NOT_STARTED = 1; + private static final int ST_STARTED = 2; + private static final int ST_SHUTTING_DOWN = 3; + private static final int ST_SHUTDOWN = 4; + private static final int ST_TERMINATED = 5; +``` + +之前提到了EventExecutor中有一个特有的inEventLoop方法,判断给定的thread是否在eventLoop中,在SingleThreadEventExecutor中,我们看一下具体的实现: + +``` + public boolean inEventLoop(Thread thread) { + return thread == this.thread; + } +``` + +具体而言就是判断给定的线程和SingleThreadEventExecutor中定义的thread属性是不是同一个thread,SingleThreadEventExecutor中的thread是这样定义的: + +``` + +``` + +这个thread是在doStartThread方法中进行初始化的: + +``` +executor.execute(new Runnable() { + @Override + public void run() { + thread = Thread.currentThread(); +``` + +所以这个thread是任务执行的线程,也就是executor中执行任务用到的线程。 + +再看一下非常关键的execute方法: + +``` +private void execute(Runnable task, boolean immediate) { + boolean inEventLoop = inEventLoop(); + addTask(task); + if (!inEventLoop) { + startThread(); +``` + +这个方法首先将task添加到任务队列中,然后调用startThread开启线程来执行任务。 + +最后来看一下DefaultEventExecutor,这个netty中的默认实现: + +``` +public final class DefaultEventExecutor extends SingleThreadEventExecutor +``` + +DefaultEventExecutor继承自SingleThreadEventExecutor,这个类中,它定义了run方法如何实现: + +``` + protected void run() { + for (;;) { + Runnable task = takeTask(); + if (task != null) { + task.run(); + updateLastExecutionTime(); + } + + if (confirmShutdown()) { + break; + } + } + } +``` + +在SingleThreadEventExecutor中,我们会把任务加入到task queue中,在run方法中,会从task queue中取出对应的task,然后调用task的run方法执行。 + +# 总结 + +DefaultEventExecutorGroup继承了MultithreadEventExecutorGroup,MultithreadEventExecutorGroup中实际调用的是SingleThreadEventExecutor来执行具体的任务。 \ No newline at end of file diff --git a/learn-netty文章/53.netty系列之EventLoop,EventLoopGroup和netty的默认实现.md b/learn-netty文章/53.netty系列之EventLoop,EventLoopGroup和netty的默认实现.md new file mode 100644 index 0000000..d72386e --- /dev/null +++ b/learn-netty文章/53.netty系列之EventLoop,EventLoopGroup和netty的默认实现.md @@ -0,0 +1,166 @@ +# netty系列之:EventLoop,EventLoopGroup和netty的默认实现 + +文章目录 + + + +[简介](http://www.flydean.com/05-1-netty-eventloop-eventloopgroup/#简介)[EventLoopGroup和EventLoop](http://www.flydean.com/05-1-netty-eventloop-eventloopgroup/#EventLoopGroup和EventLoop)[EventLoopGroup在netty中的默认实现](http://www.flydean.com/05-1-netty-eventloop-eventloopgroup/#EventLoopGroup在netty中的默认实现)[EventLoop在netty中的默认实现](http://www.flydean.com/05-1-netty-eventloop-eventloopgroup/#EventLoop在netty中的默认实现)[总结](http://www.flydean.com/05-1-netty-eventloop-eventloopgroup/#总结) + +# 简介 + +在netty中不管是服务器端的ServerBootstrap还是客户端的Bootstrap,在创建的时候都需要在group方法中传入一个EventLoopGroup参数,用来处理所有的ServerChannel和Channel中所有的IO操作和event。 + +可能有的小伙伴还稍微看了一下netty的源码,可能会发现还有一个和EventLoopGroup非常类似的类叫做EventLoop。那么EventLoopGroup和EventLoop有什么关系呢?他们的底层和channel的交互关系是怎么样的呢?一起来看看吧。 + +# EventLoopGroup和EventLoop + +EventLoopGroup继承自EventExecutorGroup: + +``` +public interface EventLoopGroup extends EventExecutorGroup +``` + +在前面的文章中我们讲过,EventExecutorGroup中有一个next方法可以返回对应的EventExecutor,这个方法在EventLoopGroup中进行了重写: + +``` + EventLoop next(); +``` + +next方法返回的不再是一个EventExecutor,而是一个EventLoop。 + +事实上,EventLoop和EventLoopGroup的关系与EventExecutor和EventExecutorGroup的关系有些类似,EventLoop也是继承自EventLoopGroup,EventLoopGroup是EventLoop的集合。 + +``` +public interface EventLoop extends OrderedEventExecutor, EventLoopGroup +``` + +在EventLoopGroup中,除了重写的next方法之外,还添加了channel的注册方法register,用于将channel和注册到EventLoop中,从而实现channel和EventLoop的绑定。 + +``` +ChannelFuture register(Channel channel); +``` + +在EventLoop中,自多添加了一个parent方法,用来表示EventLoop和EventLoopGroup的关联关系: + +``` +EventLoopGroup parent(); +``` + +# EventLoopGroup在netty中的默认实现 + +EventLoopGroup在netty中的默认实现叫做DefaultEventLoopGroup,先来看一下它的继承关系: + +![img](119283e9b8d04854940abc0fc159c604.png) + +如果看了之前我讲解的关于EventExecutorGroup的朋友可以看出来,DefaultEventLoopGroup和DefaultEventExecutorGroup的继承关系是很类似的,DefaultEventLoopGroup继承自MultithreadEventLoopGroup,而MultithreadEventLoopGroup又继承自MultithreadEventExecutorGroup并且实现了EventLoopGroup接口: + +``` +public abstract class MultithreadEventLoopGroup extends MultithreadEventExecutorGroup implements EventLoopGroup +``` + +MultithreadEventLoopGroup是用多线程来处理Event Loop。 + +在MultithreadEventLoopGroup中定义了一个DEFAULT_EVENT_LOOP_THREADS来存储默认的处理Event Loop线程的个数: + +``` +DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt( + "io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2)); +``` + +对于EventLoopGroup中新加的几个register方法,MultithreadEventLoopGroup都是调用对应的next方法来实现的: + +``` +public ChannelFuture register(Channel channel) { + return next().register(channel); + } +``` + +这里的next()方法的实现实际上调用的是父类的next方法,也就是MultithreadEventExecutorGroup中的next方法,来选择Group管理的一个EventLoop: + +``` +public EventLoop next() { + return (EventLoop) super.next(); + } +``` + +对于DefaultEventLoopGroup来说,它继承自MultithreadEventLoopGroup,实现了一个newChild方法,用来将传入的executor封装成为EventLoop: + +``` + protected EventLoop newChild(Executor executor, Object... args) throws Exception { + return new DefaultEventLoop(this, executor); + } +``` + +# EventLoop在netty中的默认实现 + +EventLoop在netty中的默认实现叫做DefaultEventLoop,先来看下它的继承关系: + +![img](9c642b58f6c248f9bddfbb71799549f9.png) + +DefaultEventLoop继承自SingleThreadEventLoop,而SingleThreadEventLoop又继承自SingleThreadEventExecutor并且实现了EventLoop接口。 + +先来看下SingleThreadEventLoop的实现: + +``` +public abstract class SingleThreadEventLoop extends SingleThreadEventExecutor implements EventLoop +``` + +SingleThreadEventLoop使用单一线程来执行提交的任务。它和SingleThreadEventExecutor相比有什么变化呢? + +首先 提供了一个tailTasks用来存储pending的tasks: + +``` +private final Queue tailTasks; +``` + +这个tailTasks会被用在任务个数的判断和操作上: + +``` + final boolean removeAfterEventLoopIterationTask(Runnable task) { + return tailTasks.remove(ObjectUtil.checkNotNull(task, "task")); + } + + protected boolean hasTasks() { + return super.hasTasks() || !tailTasks.isEmpty(); + } + + public int pendingTasks() { + return super.pendingTasks() + tailTasks.size(); + } +``` + +SingleThreadEventLoop中对register方法的实现最终调用的是注册的channel中unsafe的register方法: + +``` +channel.unsafe().register(this, promise); +``` + +再来看一下DefaultEventLoop,DefaultEventLoop继承自SingleThreadEventLoop: + +``` +public class DefaultEventLoop extends SingleThreadEventLoop +``` + +除了构造函数之外,DefaultEventLoop实现了一个run方法,用来具体任务的执行逻辑: + +``` + protected void run() { + for (;;) { + Runnable task = takeTask(); + if (task != null) { + task.run(); + updateLastExecutionTime(); + } + + if (confirmShutdown()) { + break; + } + } + } +``` + +如果对比可以发现,DefaultEventLoop和DefaultEventExecutor中run方法的实现是一样的。 + +# 总结 + +本文介绍了netty中EventLoop和EventLoopGroup的默认实现:DefaultEventLoop和DefaultEventLoopGroup,但是不知道小伙伴们有没有发现,即使在最简单的netty应用中也很少看到这两个默认的EventLoop。最常用的反而是NioEventLoopGroup和NioEventLoop,这是因为DefaultEventLoop和DefaultEventLoopGroup只是使用了多线程技术,一个线程代表一个EventLoop,在EventLoop过多的情况下可能会造成线程和性能的浪费,所以在NioEventLoopGroup和NioEventLoop使用了NIO技术,通过使用channel、selector等NIO技术提升了EventLoop的效率。关于NioEventLoopGroup和NioEventLoop的详细介绍,我们会在后一章中详细讲解,敬请期待。 \ No newline at end of file diff --git a/learn-netty文章/54.netty系列之NIO和netty详解.md b/learn-netty文章/54.netty系列之NIO和netty详解.md new file mode 100644 index 0000000..4981263 --- /dev/null +++ b/learn-netty文章/54.netty系列之NIO和netty详解.md @@ -0,0 +1,491 @@ +# netty系列之:NIO和netty详解 + +文章目录 + + + +[简介](http://www.flydean.com/05-2-netty-nioeventloop/#简介)[NIO常用用法](http://www.flydean.com/05-2-netty-nioeventloop/#NIO常用用法)[NIO和EventLoopGroup](http://www.flydean.com/05-2-netty-nioeventloop/#NIO和EventLoopGroup)[NioEventLoopGroup](http://www.flydean.com/05-2-netty-nioeventloop/#NioEventLoopGroup)[NioEventLoop](http://www.flydean.com/05-2-netty-nioeventloop/#NioEventLoop)[总结](http://www.flydean.com/05-2-netty-nioeventloop/#总结) + +# 简介 + +netty为什么快呢?这是因为netty底层使用了JAVA的NIO技术,并在其基础上进行了性能的优化,虽然netty不是单纯的JAVA nio,但是netty的底层还是基于的是nio技术。 + +nio是JDK1.4中引入的,用于区别于传统的IO,所以nio也可以称之为new io。 + +nio的三大核心是Selector,channel和Buffer,本文我们将会深入探究NIO和netty之间的关系。 + +# NIO常用用法 + +在讲解netty中的NIO实现之前,我们先来回顾一下JDK中NIO的selector,channel是怎么工作的。对于NIO来说selector主要用来接受客户端的连接,所以一般用在server端。我们以一个NIO的服务器端和客户端聊天室为例来讲解NIO在JDK中是怎么使用的。 + +因为是一个简单的聊天室,我们选择Socket协议为基础的ServerSocketChannel,首先就是open这个Server channel: + +``` +ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); +serverSocketChannel.bind(new InetSocketAddress("localhost", 9527)); +serverSocketChannel.configureBlocking(false); +``` + +然后向server channel中注册selector: + +``` +Selector selector = Selector.open(); +serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); +``` + +虽然是NIO,但是对于Selector来说,它的select方法是阻塞方法,只有找到匹配的channel之后才会返回,为了多次进行select操作,我们需要在一个while循环里面进行selector的select操作: + +``` +while (true) { + selector.select(); + Set selectedKeys = selector.selectedKeys(); + Iterator iter = selectedKeys.iterator(); + while (iter.hasNext()) { + SelectionKey selectionKey = iter.next(); + if (selectionKey.isAcceptable()) { + register(selector, serverSocketChannel); + } + if (selectionKey.isReadable()) { + serverResponse(byteBuffer, selectionKey); + } + iter.remove(); + } + Thread.sleep(1000); + } +``` + +selector中会有一些SelectionKey,SelectionKey中有一些表示操作状态的OP Status,根据这个OP Status的不同,selectionKey可以有四种状态,分别是isReadable,isWritable,isConnectable和isAcceptable。 + +当SelectionKey处于isAcceptable状态的时候,表示ServerSocketChannel可以接受连接了,我们需要调用register方法将serverSocketChannel accept生成的socketChannel注册到selector中,以监听它的OP READ状态,后续可以从中读取数据: + +``` + private static void register(Selector selector, ServerSocketChannel serverSocketChannel) + throws IOException { + SocketChannel socketChannel = serverSocketChannel.accept(); + socketChannel.configureBlocking(false); + socketChannel.register(selector, SelectionKey.OP_READ); + } +``` + +当selectionKey处于isReadable状态的时候,表示可以从socketChannel中读取数据然后进行处理: + +``` + private static void serverResponse(ByteBuffer byteBuffer, SelectionKey selectionKey) + throws IOException { + SocketChannel socketChannel = (SocketChannel) selectionKey.channel(); + socketChannel.read(byteBuffer); + byteBuffer.flip(); + byte[] bytes= new byte[byteBuffer.limit()]; + byteBuffer.get(bytes); + log.info(new String(bytes).trim()); + if(new String(bytes).trim().equals(BYE_BYE)){ + log.info("说再见不如不见!"); + socketChannel.write(ByteBuffer.wrap("再见".getBytes())); + socketChannel.close(); + }else { + socketChannel.write(ByteBuffer.wrap("你是个好人".getBytes())); + } + byteBuffer.clear(); + } +``` + +上面的serverResponse方法中,从selectionKey中拿到对应的SocketChannel,然后调用SocketChannel的read方法,将channel中的数据读取到byteBuffer中,要想回复消息到channel中,还是使用同一个socketChannel,然后调用write方法回写消息给client端,到这里一个简单的回写客户端消息的server端就完成了。 + +接下来就是对应的NIO客户端,在NIO客户端需要使用SocketChannel,首先建立和服务器的连接: + +``` +socketChannel = SocketChannel.open(new InetSocketAddress("localhost", 9527)); +``` + +然后就可以使用这个channel来发送和接受消息了: + +``` + public String sendMessage(String msg) throws IOException { + byteBuffer = ByteBuffer.wrap(msg.getBytes()); + String response = null; + socketChannel.write(byteBuffer); + byteBuffer.clear(); + socketChannel.read(byteBuffer); + byteBuffer.flip(); + byte[] bytes= new byte[byteBuffer.limit()]; + byteBuffer.get(bytes); + response =new String(bytes).trim(); + byteBuffer.clear(); + return response; + } +``` + +向channel中写入消息可以使用write方法,从channel中读取消息可以使用read方法。 + +这样一个NIO的客户端就完成了。 + +虽然以上是NIO的server和client的基本使用,但是基本上涵盖了NIO的所有要点。接下来我们来详细了解一下netty中NIO到底是怎么使用的。 + +# NIO和EventLoopGroup + +以netty的ServerBootstrap为例,启动的时候需要指定它的group,先来看一下ServerBootstrap的group方法: + +``` +public ServerBootstrap group(EventLoopGroup group) { + return group(group, group); + } + +public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) { + ... +} +``` + +ServerBootstrap可以接受一个EventLoopGroup或者两个EventLoopGroup,EventLoopGroup被用来处理所有的event和IO,对于ServerBootstrap来说,可以有两个EventLoopGroup,对于Bootstrap来说只有一个EventLoopGroup。两个EventLoopGroup表示acceptor group和worker group。 + +EventLoopGroup只是一个接口,我们常用的一个实现就是NioEventLoopGroup,如下所示是一个常用的netty服务器端代码: + +``` + EventLoopGroup bossGroup = new NioEventLoopGroup(); + EventLoopGroup workerGroup = new NioEventLoopGroup(); + try { + ServerBootstrap b = new ServerBootstrap(); + b.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .childHandler(new ChannelInitializer() { + @Override + public void initChannel(SocketChannel ch) throws Exception { + ch.pipeline().addLast(new FirstServerHandler()); + } + }) + .option(ChannelOption.SO_BACKLOG, 128) + .childOption(ChannelOption.SO_KEEPALIVE, true); + + // 绑定端口并开始接收连接 + ChannelFuture f = b.bind(port).sync(); + // 等待server socket关闭 + f.channel().closeFuture().sync(); +``` + +这里和NIO相关的有两个类,分别是NioEventLoopGroup和NioServerSocketChannel,事实上在他们的底层还有两个类似的类分别叫做NioEventLoop和NioSocketChannel,接下来我们分别讲解一些他们的底层实现和逻辑关系。 + +# NioEventLoopGroup + +NioEventLoopGroup和DefaultEventLoopGroup一样都是继承自MultithreadEventLoopGroup: + +``` +public class NioEventLoopGroup extends MultithreadEventLoopGroup +``` + +他们的不同之处在于newChild方法的不同,newChild用来构建Group中的实际对象,NioEventLoopGroup来说,newChild返回的是一个NioEventLoop对象,先来看下NioEventLoopGroup的newChild方法: + +``` + protected EventLoop newChild(Executor executor, Object... args) throws Exception { + SelectorProvider selectorProvider = (SelectorProvider) args[0]; + SelectStrategyFactory selectStrategyFactory = (SelectStrategyFactory) args[1]; + RejectedExecutionHandler rejectedExecutionHandler = (RejectedExecutionHandler) args[2]; + EventLoopTaskQueueFactory taskQueueFactory = null; + EventLoopTaskQueueFactory tailTaskQueueFactory = null; + + int argsLength = args.length; + if (argsLength > 3) { + taskQueueFactory = (EventLoopTaskQueueFactory) args[3]; + } + if (argsLength > 4) { + tailTaskQueueFactory = (EventLoopTaskQueueFactory) args[4]; + } + return new NioEventLoop(this, executor, selectorProvider, + selectStrategyFactory.newSelectStrategy(), + rejectedExecutionHandler, taskQueueFactory, tailTaskQueueFactory); + } +``` + +这个newChild方法除了固定的executor参数之外,还可以根据NioEventLoopGroup的构造函数传入的参数来实现更多的功能。 + +这里参数中传入了SelectorProvider、SelectStrategyFactory、RejectedExecutionHandler、taskQueueFactory和tailTaskQueueFactory这几个参数,其中后面的两个EventLoopTaskQueueFactory并不是必须的。 + +最后所有的参数都会传递给NioEventLoop的构造函数用来构造出一个新的NioEventLoop。 + +在详细讲解NioEventLoop之前,我们来研读一下传入的这几个参数类型的实际作用。 + +## SelectorProvider + +SelectorProvider是JDK中的类,它提供了一个静态的provider()方法可以从Property或者ServiceLoader中加载对应的SelectorProvider类并实例化。 + +另外还提供了openDatagramChannel、openPipe、openSelector、openServerSocketChannel和openSocketChannel等实用的NIO操作方法。 + +## SelectStrategyFactory + +SelectStrategyFactory是一个接口,里面只定义了一个方法,用来返回SelectStrategy: + +``` +public interface SelectStrategyFactory { + + SelectStrategy newSelectStrategy(); +} +``` + +什么是SelectStrategy呢? + +先看下SelectStrategy中定义了哪些Strategy: + +``` + int SELECT = -1; + + int CONTINUE = -2; + + int BUSY_WAIT = -3; +``` + +SelectStrategy中定义了3个strategy,分别是SELECT、CONTINUE和BUSY_WAIT。 + +我们知道一般情况下,在NIO中select操作本身是一个阻塞操作,也就是block操作,这个操作对应的strategy是SELECT,也就是select block状态。 + +如果我们想跳过这个block,重新进入下一个event loop,那么对应的strategy就是CONTINUE。 + +BUSY_WAIT是一个特殊的strategy,是指IO 循环轮询新事件而不阻塞,这个strategy只有在epoll模式下才支持,NIO和Kqueue模式并不支持这个strategy。 + +## RejectedExecutionHandler + +RejectedExecutionHandler是netty自己的类,和 java.util.concurrent.RejectedExecutionHandler类似,但是是特别针对SingleThreadEventExecutor来说的。这个接口定义了一个rejected方法,用来表示因为SingleThreadEventExecutor容量限制导致的任务添加失败而被拒绝的情况: + +``` +void rejected(Runnable task, SingleThreadEventExecutor executor); +``` + +## EventLoopTaskQueueFactory + +EventLoopTaskQueueFactory是一个接口,用来创建存储提交给EventLoop的taskQueue: + +``` +Queue newTaskQueue(int maxCapacity); +``` + +这个Queue必须是线程安全的,并且继承自java.util.concurrent.BlockingQueue. + +讲解完这几个参数,接下来我们就可以详细查看NioEventLoop的具体NIO实现了。 + +# NioEventLoop + +首先NioEventLoop和DefaultEventLoop一样,都是继承自SingleThreadEventLoop: + +``` +public final class NioEventLoop extends SingleThreadEventLoop +``` + +表示的是使用单一线程来执行任务的EventLoop。 + +首先作为一个NIO的实现,必须要有selector,在NioEventLoop中定义了两个selector,分别是selector和unwrappedSelector: + +``` + private Selector selector; + private Selector unwrappedSelector; +``` + +在NioEventLoop的构造函数中,他们是这样定义的: + +``` + final SelectorTuple selectorTuple = openSelector(); + this.selector = selectorTuple.selector; + this.unwrappedSelector = selectorTuple.unwrappedSelector; +``` + +首先调用openSelector方法,然后通过返回的SelectorTuple来获取对应的selector和unwrappedSelector。 + +这两个selector有什么区别呢? + +在openSelector方法中,首先通过调用provider的openSelector方法返回一个Selector,这个Selector就是unwrappedSelector: + +``` +final Selector unwrappedSelector; +unwrappedSelector = provider.openSelector(); +``` + +然后检查DISABLE_KEY_SET_OPTIMIZATION是否设置,如果没有设置那么unwrappedSelector和selector实际上是同一个Selector: + +DISABLE_KEY_SET_OPTIMIZATION表示的是是否对select key set进行优化: + +``` +if (DISABLE_KEY_SET_OPTIMIZATION) { + return new SelectorTuple(unwrappedSelector); + } + + SelectorTuple(Selector unwrappedSelector) { + this.unwrappedSelector = unwrappedSelector; + this.selector = unwrappedSelector; + } +``` + +如果DISABLE_KEY_SET_OPTIMIZATION被设置为false,那么意味着我们需要对select key set进行优化,具体是怎么进行优化的呢? + +先来看下最后的返回: + +``` +return new SelectorTuple(unwrappedSelector, + new SelectedSelectionKeySetSelector(unwrappedSelector, selectedKeySet)); +``` + +最后返回的SelectorTuple第二个参数就是selector,这里的selector是一个SelectedSelectionKeySetSelector对象。 + +SelectedSelectionKeySetSelector继承自selector,构造函数传入的第一个参数是一个delegate,所有的Selector中定义的方法都是通过调用 +delegate来实现的,不同的是对于select方法来说,会首先调用selectedKeySet的reset方法,下面是以isOpen和select方法为例观察一下代码的实现: + +``` + public boolean isOpen() { + return delegate.isOpen(); + } + + public int select(long timeout) throws IOException { + selectionKeys.reset(); + return delegate.select(timeout); + } +``` + +selectedKeySet是一个SelectedSelectionKeySet对象,是一个set集合,用来存储SelectionKey,在openSelector()方法中,使用new来实例化这个对象: + +``` +final SelectedSelectionKeySet selectedKeySet = new SelectedSelectionKeySet(); +``` + +netty实际是想用这个SelectedSelectionKeySet类来管理Selector中的selectedKeys,所以接下来netty用了一个高技巧性的对象替换操作。 + +首先判断系统中有没有sun.nio.ch.SelectorImpl的实现: + +``` + Object maybeSelectorImplClass = AccessController.doPrivileged(new PrivilegedAction() { + @Override + public Object run() { + try { + return Class.forName( + "sun.nio.ch.SelectorImpl", + false, + PlatformDependent.getSystemClassLoader()); + } catch (Throwable cause) { + return cause; + } + } + }); +``` + +SelectorImpl中有两个Set字段: + +``` + private Set publicKeys; + private Set publicSelectedKeys; +``` + +这两个字段就是我们需要替换的对象。如果有SelectorImpl的话,首先使用Unsafe类,调用PlatformDependent中的objectFieldOffset方法拿到这两个字段相对于对象示例的偏移量,然后调用putObject将这两个字段替换成为前面初始化的selectedKeySet对象: + +``` +Field selectedKeysField = selectorImplClass.getDeclaredField("selectedKeys"); +Field publicSelectedKeysField = selectorImplClass.getDeclaredField("publicSelectedKeys"); + +if (PlatformDependent.javaVersion() >= 9 && PlatformDependent.hasUnsafe()) { + // Let us try to use sun.misc.Unsafe to replace the SelectionKeySet. + // This allows us to also do this in Java9+ without any extra flags. + long selectedKeysFieldOffset = PlatformDependent.objectFieldOffset(selectedKeysField); + long publicSelectedKeysFieldOffset = + PlatformDependent.objectFieldOffset(publicSelectedKeysField); + + if (selectedKeysFieldOffset != -1 && publicSelectedKeysFieldOffset != -1) { + PlatformDependent.putObject( + unwrappedSelector, selectedKeysFieldOffset, selectedKeySet); + PlatformDependent.putObject( + unwrappedSelector, publicSelectedKeysFieldOffset, selectedKeySet); + return null; + } +``` + +如果系统设置不支持Unsafe,那么就用反射再做一次: + +``` + Throwable cause = ReflectionUtil.trySetAccessible(selectedKeysField, true); + if (cause != null) { + return cause; + } + cause = ReflectionUtil.trySetAccessible(publicSelectedKeysField, true); + if (cause != null) { + return cause; + } + selectedKeysField.set(unwrappedSelector, selectedKeySet); + publicSelectedKeysField.set(unwrappedSelector, selectedKeySet); +``` + +在NioEventLoop中我们需要关注的一个非常重要的重写方法就是run方法,在run方法中实现了如何执行task的逻辑。 + +还记得前面我们提到的selectStrategy吗?run方法通过调用selectStrategy.calculateStrategy返回了select的strategy,然后通过判断 +strategy的值来进行对应的处理。 + +如果strategy是CONTINUE,这跳过这次循环,进入到下一个loop中。 + +BUSY_WAIT在NIO中是不支持的,如果是SELECT状态,那么会在curDeadlineNanos之后再次进行select操作: + +``` +strategy = selectStrategy.calculateStrategy(selectNowSupplier, hasTasks()); + switch (strategy) { + case SelectStrategy.CONTINUE: + continue; + case SelectStrategy.BUSY_WAIT: + // fall-through to SELECT since the busy-wait is not supported with NIO + case SelectStrategy.SELECT: + long curDeadlineNanos = nextScheduledTaskDeadlineNanos(); + if (curDeadlineNanos == -1L) { + curDeadlineNanos = NONE; // nothing on the calendar + } + nextWakeupNanos.set(curDeadlineNanos); + try { + if (!hasTasks()) { + strategy = select(curDeadlineNanos); + } + } finally { + // This update is just to help block unnecessary selector wakeups + // so use of lazySet is ok (no race condition) + nextWakeupNanos.lazySet(AWAKE); + } + // fall through + default: +``` + +如果strategy > 0,表示有拿到了SelectedKeys,那么需要调用processSelectedKeys方法对SelectedKeys进行处理: + +``` + private void processSelectedKeys() { + if (selectedKeys != null) { + processSelectedKeysOptimized(); + } else { + processSelectedKeysPlain(selector.selectedKeys()); + } + } +``` + +上面提到了NioEventLoop中有两个selector,还有一个selectedKeys属性,这个selectedKeys存储的就是Optimized SelectedKeys,如果这个值不为空,就调用processSelectedKeysOptimized方法,否则就调用processSelectedKeysPlain方法。 + +processSelectedKeysOptimized和processSelectedKeysPlain这两个方法差别不大,只是传入的要处理的selectedKeys不同。 + +处理的逻辑是首先拿到selectedKeys的key,然后调用它的attachment方法拿到attach的对象: + +``` +final SelectionKey k = selectedKeys.keys[i]; + selectedKeys.keys[i] = null; + + final Object a = k.attachment(); + + if (a instanceof AbstractNioChannel) { + processSelectedKey(k, (AbstractNioChannel) a); + } else { + NioTask task = (NioTask) a; + processSelectedKey(k, task); + } +``` + +如果channel还没有建立连接,那么这个对象可能是一个NioTask,用来处理channelReady和channelUnregistered的事件。 + +如果channel已经建立好连接了,那么这个对象可能是一个AbstractNioChannel。 + +针对两种不同的对象,会去分别调用不同的processSelectedKey方法。 + +对第一种情况,会调用task的channelReady方法: + +``` +task.channelReady(k.channel(), k); +``` + +对第二种情况,会根据SelectionKey的readyOps()的各种状态调用ch.unsafe()中的各种方法,去进行read或者close等操作。 + +# 总结 + +NioEventLoop虽然也是一个SingleThreadEventLoop,但是通过使用NIO技术,可以更好的利用现有资源实现更好的效率,这也就是为什么我们在项目中使用NioEventLoopGroup而不是DefaultEventLoopGroup的原因。 \ No newline at end of file diff --git a/learn-netty文章/55.netty系列之netty中各不同种类的channel详解.md b/learn-netty文章/55.netty系列之netty中各不同种类的channel详解.md new file mode 100644 index 0000000..8a24206 --- /dev/null +++ b/learn-netty文章/55.netty系列之netty中各不同种类的channel详解.md @@ -0,0 +1,217 @@ +# netty系列之:netty中各不同种类的channel详解 + +文章目录 + + + +[简介](http://www.flydean.com/04-5-netty-channel-types/#简介)[ServerChannel和它的类型](http://www.flydean.com/04-5-netty-channel-types/#ServerChannel和它的类型)[Channel和它的类型](http://www.flydean.com/04-5-netty-channel-types/#Channel和它的类型)[总结](http://www.flydean.com/04-5-netty-channel-types/#总结) + +# 简介 + +channel是连接客户端和服务器端的桥梁,在netty中我们最常用的就是NIO,一般和NioEventLoopGroup配套使用的就是NioServerSocketChannel和NioSocketChannel,如果是UDP协议,那么配套使用的就是NioDatagramChannel,如果是别的协议还有其他不同的Channel类型。 + +这些不同channel类型有什么区别呢?一个直观的感觉就是不同的channel和channel连接使用的协议有关系,不同的channel可能适配了不同的连接协议。 + +事实到底是不是如此呢?在netty的内部实现中到底有多少种channel呢?今天一起来探讨一下。 + +# ServerChannel和它的类型 + +虽然ServerChannel继承自Channel,但是ServerChannel本身并没有添加任何新的方法: + +``` +public interface ServerChannel extends Channel { + +} +``` + +所以对ServerChannel和Channel来说都可以看做是Channel,他们只是语义上有区别。 + +但是因为ServerChannel继承自Channel,所以相对的ServerChannel的分类和实现要比Channel要少。所以我们先以ServerChannel为例进行讲解。 + +ServerChannel的实现也有很多,我们以Abstract*开头的实现为例,下面是他们的继承关系: + +![img](9613bc4cb9314dc2903a938f80340b47.png) + +从上图我们可以看出,ServerChannel有六个抽象类实现,分别是AbstractEpollServerChannel,AbstractKQueueServerChannel,AbstractServerChannel,ServerSocketChannel,SctpServerChannel和ServerDomainSocketChannel。 + +其中前面三个抽象类同时继承自AbstractChannel。 + +## Epoll和Kqueue + +Epoll和Kqueue是两个独特的依赖于特定平台的NIO协议,其中epoll只在linux平台才支持,而kQueue则在FreeBSD、NetBSD、OpenBSD、macOS 等操作系统支持。 + +我们来看下AbstractEpollServerChannel的构造函数: + +``` + protected AbstractEpollServerChannel(int fd) { + this(new LinuxSocket(fd), false); + } + + AbstractEpollServerChannel(LinuxSocket fd) { + this(fd, isSoErrorZero(fd)); + } + + AbstractEpollServerChannel(LinuxSocket fd, boolean active) { + super(null, fd, active); + } +``` + +所有的构造函数都需要一个LinuxSocket的参数,LinuxSocket是一个socket用来提供对于linux native方法的访问支持。 + +同样的,我们再看一下AbstractKQueueServerChannel的构造函数: + +``` + AbstractKQueueServerChannel(BsdSocket fd) { + this(fd, isSoErrorZero(fd)); + } + + AbstractKQueueServerChannel(BsdSocket fd, boolean active) { + super(null, fd, active); + } +``` + +AbstractKQueueServerChannel的构造函数需要传入一个BsdSocket参数,BsdSocket是一个类用来提供对BSD系统的本地方法的访问。 + +## AbstractServerChannel + +AbstractServerChannel我们在之前的channel一章中已经讲过了,它的唯一实现就是LocalServerChannel,用于本地的transport。 + +## ServerSocketChannel + +ServerSocketChannel是一个以Socket连接为基础的ServerChannel,既然是Socket连接,那么ServerSocketChannel中提供了一个InetSocketAddress类型的localAddress和一个remoteAddress, 另外还有一个ServerSocketChannelConfig属性,用来存储ServerSocketChannel相关的配置信息: + +``` +public interface ServerSocketChannel extends ServerChannel { + @Override + ServerSocketChannelConfig config(); + @Override + InetSocketAddress localAddress(); + @Override + InetSocketAddress remoteAddress(); +} +``` + +## ServerDomainSocketChannel + +ServerDomainSocketChannel是使用DomainSocket来进行通讯的ServerChannel。什么是DomainSocket呢? + +DomainSocket的全称是unix domain socket,它又可以叫做IPC socket,也就是inter-process communication socket,是在unix平台上的同一服务器上的进程通信方式。 + +我们知道,协议是比较复杂的,对于传统的socket通讯来说,需要定制特定的协议,然后进行封包和解包等操作,但是使用DomainSocket,可以直接将进程的数据直接拷贝,从而节约了时间,并提高了程序的效率。 + +DomainSocket的地址是一个文件的路径,实际上是下面的一个结构体: + +``` +struct sockaddr_un { + sa_family_t sun_family; /* AF_UNIX ,2字节*/ + char sun_path[UNIX_PATH_MAX]; /* 路径名 */ +}; +``` + +在ServerDomainSocketChannel中的remoteAddress和localAddress的类型都是DomainSocketAddress,DomainSocketAddress有一个socketPath属性,用来存储DomainSocket文件的路径。 + +## SctpServerChannel + +最后一个要讲解的ServerChannel是SctpServerChannel,Sctp的全称是Stream Control Transmission Protocol,他是一种类似于TCP/IP的协议。和SocketServerChannel一样,SctpServerChannel中也有一个config叫做SctpServerChannelConfig,还提供了多个bindAddress方法用来绑定InetAddress. + +有关Sctp协议的具体内容,本章不深入讨论,感兴趣的朋友可以关注后续的章节。 + +# Channel和它的类型 + +Channel作为ServerChannel的父类,又有哪些实现呢? + +先来看下常用channel的实现类: + +![img](69d7d0d53df847e2b5eb7791da956ef1.png) + +看起来channel的实现类非常多,基本上都是按照channel中使用传输协议的类型来的。 + +我们具体来看一下相应的实现类。 + +## UnixChannel + +UnixChannel表示的unix平台上的操作,它有一个fd方法,返回一个FileDescriptor: + +``` +FileDescriptor fd(); +``` + +这也是unix和windows平台的区别之一,unix平台所有的一切都可以用文件来表示。 + +## SctpChannel + +在上面我讲SctpServerChannel的时候我们提过了,Sctp是一个类似于tcp/ip的协议,SctpChannel中定义了协议中需要使用到的localAddress和remoteAddress: + +``` +InetSocketAddress localAddress(); + +InetSocketAddress remoteAddress(); +``` + +同时还定义了一些绑定方法: + +``` + ChannelFuture bindAddress(InetAddress var1); + + ChannelFuture bindAddress(InetAddress var1, ChannelPromise var2); + + ChannelFuture unbindAddress(InetAddress var1); + + ChannelFuture unbindAddress(InetAddress var1, ChannelPromise var2); +``` + +## DatagramChannel + +DatagramChannel用来处理UDP协议的连接,因为UDP有广播的功能,所以DatagramChannel中提供了joinGroup的方法,来join一个multicast group: + +``` +ChannelFuture joinGroup(InetAddress multicastAddress); +``` + +当然,可以join就可以leave,还有一些leaveGroup的方法: + +``` +ChannelFuture leaveGroup(InetAddress multicastAddress); +``` + +还可以block某些地址在给定的networkInterface上的广播: + +``` +ChannelFuture block( + InetAddress multicastAddress, NetworkInterface networkInterface, + InetAddress sourceToBlock); +``` + +这些方法都和UDP的特性是息息相关的。 + +## DomainDatagramChannel + +DomainDatagramChannel和之前提到的ServerDomainSocketChannel一样,都是使用的IPC内部进程通讯技术,直接进行进程的拷贝,免去了协议解析等步骤,提升了处理速度。 + +## DuplexChannel + +DuplexChannel从名字看就是一个双向的channel,duplex Channel有一个特点,就是channel的两边可以独立的关闭,所以有下面的方法: + +``` +boolean isInputShutdown(); + +ChannelFuture shutdownInput(); + +boolean isOutputShutdown(); + +ChannelFuture shutdownOutput(); +``` + +DuplexChannel的是实现有很多种,比如常见的NIOSocketChannel,KQueueSocketChannel,EpollSocketChannel等。 + +## AbstractChannel + +另外一个channel的非常重要的子类就是AbstractChannel,AbstractChannel有三个非常重要的实现,分别是AbstractNioChannel,AbstractKQueueChannel和AbstractEpollChannel。 + +这三个类使用的都是NIO技术,不同的是第一个使用的是select,后面两个使用的是平台独有的KQueue和Epoll技术。 + +其中NIO又可以分为NioByteChannel和NioMessageChannel,KQueue和Epoll又可以分为StreamChannel和DatagramChannel。 + +# 总结 + +以上就是channel在netty中的基本实现和分类。后面我们会详解讲解具体的channel到底是如何实现的。 \ No newline at end of file diff --git a/learn-netty文章/56.java高级用法之无所不能的java,本地方法调用实况.md b/learn-netty文章/56.java高级用法之无所不能的java,本地方法调用实况.md new file mode 100644 index 0000000..b05e35b --- /dev/null +++ b/learn-netty文章/56.java高级用法之无所不能的java,本地方法调用实况.md @@ -0,0 +1,193 @@ +# java高级用法之:无所不能的java,本地方法调用实况 + +文章目录 + + + +[简介](http://www.flydean.com/01-jni-overview/#简介)[JDK的本地方法](http://www.flydean.com/01-jni-overview/#JDK的本地方法)[自定义native方法](http://www.flydean.com/01-jni-overview/#自定义native方法)[总结](http://www.flydean.com/01-jni-overview/#总结) + +# 简介 + +相信每个程序员都有一个成为C++大师的梦想,毕竟C++程序员处于程序员鄙视链的顶端,他可以俯视任何其他语言的程序员。 + +但事实情况是,无数的程序员从小白到放弃,鉴于C++的难度,最后都投入了java的怀抱。JAVA以他宽广的胸怀接纳了一众无法登顶C++的程序员。 + +开个玩笑,C和C++的优势在于和系统底层的交互和其运行的速度和效率,JAVA的优势在与广泛的应用框架,可以快速搭建所需的应用程序。两者各有所长。 + +框架的好处就是降低了程序开发的难度,让应用程序可以快速批量复制。 + +大家知道,JVM底层是使用C和C++来编写的,而JAVA字节码适合JVM进行交互的,所以直观上看来,JAVA是可以和底层的C++代码进行交互的。那么如何交互呢?会不会很复杂? + +今天本文带大家一一揭晓。 + +# JDK的本地方法 + +所谓本地方法就是调用操作系统或者其他底层库的方法。这些方法属于系统的外部接口,用于程序和操作系统之间进行交互。大家想一下,JDK中有哪些本地的方法呢? + +第一个想到的应该就是文件操作,因为文件操作肯定需要依赖与系统底层提供的IO接口。我们先具体来看一下File的delete方法的实现: + +``` + public boolean delete() { + @SuppressWarnings("removal") + SecurityManager security = System.getSecurityManager(); + if (security != null) { + security.checkDelete(path); + } + if (isInvalid()) { + return false; + } + return fs.delete(this); + } +``` + +File的delete方法首先调用SecurityManager来进行权限判断,看是否可以删除。如果可以删除则继续调用FileSystem的delete方法。 + +我们继续查看FileSystem的delete方法: + +``` +public abstract boolean delete(File f); +``` + +可以看到FileSystem中的delete方法是一个抽象方法,需要具体的实现。 + +而这个实现是和平台有关系的,如果你是linux或者mac系统,那么它的实现类是UnixFileSystem,它的delete方法如下: + +``` + public boolean delete(File f) { + if (useCanonCaches) { + cache.clear(); + } + if (useCanonPrefixCache) { + javaHomePrefixCache.clear(); + } + return delete0(f); + } + private native boolean delete0(File f); +``` + +可以看到,delete方法最终会调用delete0方法,而这个方法是一个native方法,表示该方法需要调用系统本地的方法。 + +JDK提供了一个JAVA调用本地系统方法的实现,叫做JNI,全称是Java Native Interface,它是从JAVA1.1中引入的一项技术。它允许Java代码和其他语言写的代码进行交互。 + +为了验证JNI的可行性,我们接下来自己实现一个native的方法,并在java中调用,看看是否能够成功。 + +# 自定义native方法 + +在JAVA中定义native方法很简单,我们只需要在方法描述前面加上native关键字即可,这个方法并不需要任何实现。举个具体的例子如下: + +``` +public class JNIUsage { + + public native void printMsg(); + + public static void main(String[] args) { + //加载C文件 + System.loadLibrary("JNIUsage"); + JNIUsage jniUsage = new JNIUsage(); + jniUsage.printMsg(); + } +} +``` + +上面的例子中,我们定义了一个native的printMsg,然后在main中首先加载包含该实现的Library文件,之后就可以像正常的JAVA方法一样进行调用。 + +那么这么实现这个native方法呢? + +不管熟悉还是不熟悉C++的朋友应该都听过头文件的概念,一般来说我们在头文件中定义好要实现的方法,然后在具体的内容文件中对头文件中定义的方法进行实现。 + +所以头文件中需要包含这个printMsg的方法,生成头文件可以使用javah命令。 + +首先进入到JNIUsage源文件的根目录,运行下面的命令: + +``` +javah -classpath . -jni com.flydean.JNIUsage +``` + +该命令会在项目源代码的根目录中生成一个com_flydean_JNIUsage.h文件。打开看看,具体的内容如下: + +``` +/* DO NOT EDIT THIS FILE - it is machine generated */ +#include +/* Header for class com_flydean_JNIUsage */ + +#ifndef _Included_com_flydean_JNIUsage +#define _Included_com_flydean_JNIUsage +#ifdef __cplusplus +extern "C" { +#endif +/* + * Class: com_flydean_JNIUsage + * Method: printMsg + * Signature: ()V + */ +JNIEXPORT void JNICALL Java_com_flydean_JNIUsage_printMsg + (JNIEnv *, jobject); + +#ifdef __cplusplus +} +#endif +#endif +``` + +简单点讲,该head文件中定义了一个需要实现的Java_com_flydean_JNIUsage_printMsg方法。 + +接下来,我们需要对这个头文件进行实现。 + +这里我们使用JetBrain公司的Clion开发工具,首先创建一个c++的项目: + +![img](b38d484f06a9484992e301f64472a095.png) + +注意,这个项目的type需要是shared类型。 + +然后将com_flydean_JNIUsage.h文件拷贝到项目的根目录下。 + +这时候是编译不了的,你会发现很多依赖包的错误,我们还需要将JDK home目录中include目录下的jni.h文件,和jni_md.h文件(如果是windows平台该文件在win32目录下,如果是mac平台,该文件在darwin目录下),拷贝到项目的根目录下。 + +这样编译的错误就不见了。 + +最后我们修改默认的library.cpp文件,引入com_flydean_JNIUsage.h并实现其中的方法如下所示: + +``` +#include "com_flydean_JNIUsage.h" + +#include + +JNIEXPORT void JNICALL Java_com_flydean_JNIUsage_printMsg + (JNIEnv *, jobject){ + printf("this is www.flydean.com!"); +} +``` + +目前为止,项目的代码结构应该如下图所示: + +![img](ee0512f8cab94d47ae8a13fd6a062e92.png) + +接着build–>Build ‘JNIUsage’, 生成libJNIUsage.dylib文件: + +``` +====================[ Build | JNIUsage | Debug ]================================ +/Applications/CLion.app/Contents/bin/cmake/mac/bin/cmake --build /Users/flydean/data/git/cplus/JNIUsage/cmake-build-debug --target JNIUsage +[2/2] Linking CXX shared library libJNIUsage.dylib + +Build finished +``` + +有了libJNIUsage.dylib,我们还需要将其加入JAVA项目中的path中: + +![img](f5b938e8a753482d86fc792e870b8fe7.png) + +选择java-jni的module,在依赖中选择JARs or Directories, 选择刚刚的libJNIUsage.dylib 目录。 + +保存之后,就可以运行JAVA代码了,结果如下: + +``` +/Library/Java/JavaVirtualMachines/jdk-17.0.1.jdk/Contents/Home/bin/java -Djava.library.path=/Users/flydean/data/git/cplus/JNIUsage/cmake-build-debug -Dfile.encoding=UTF-8 -classpath /Users/flydean/data/git/learn-java-base-9-to-20/java-jni/target/classes: com.flydean.JNIUsage + +this is www.flydean.com! +``` + +或者你可以在命令行中将libJNIUsage.dylib加入到java运行的classpath中即可。 + +# 总结 + +以上就是一个简单的使用JAVA调用native方法的例子。大家可以看到,步骤还是挺复杂的,那么有没有其他更加简单的方法,让JAVA来调用native方法呢?有的,这就是JNA,我们会在后续的文章中深入进行介绍。 \ No newline at end of file diff --git a/learn-netty文章/57.netty系列之java中的base64编码器.md b/learn-netty文章/57.netty系列之java中的base64编码器.md new file mode 100644 index 0000000..b8e00ce --- /dev/null +++ b/learn-netty文章/57.netty系列之java中的base64编码器.md @@ -0,0 +1,155 @@ +# netty系列之:java中的base64编码器 + +文章目录 + + + +[简介](http://www.flydean.com/14-1-1-java-base64/#简介)[JAVA对base64的支持](http://www.flydean.com/14-1-1-java-base64/#JAVA对base64的支持)[JDK中Base64的分类和实现](http://www.flydean.com/14-1-1-java-base64/#JDK中Base64的分类和实现)[Base64的高级用法](http://www.flydean.com/14-1-1-java-base64/#Base64的高级用法)[总结](http://www.flydean.com/14-1-1-java-base64/#总结) + +# 简介 + +什么是Base64编码呢?在回答这个问题之前,我们需要了解一下计算机中文件的分类,对于计算机来说文件可以分为两类,一类是文本文件,一类是二进制文件。 + +对于二进制文件来说,其内容是用二进制来表示的,对于人类是不可立马理解的。如果你尝试用文本编辑器打开二进制文件,可能会看到乱码。这是因为二进制文件的编码方式和文本文件的编码方式是不一样的,所以当文本编辑器尝试将二进制文件翻译成为文本内容的时候,就会出现乱码。 + +对于文本文件来说,也有很多种编码方式,比如最早的ASCII编码和目前常用的UTF-8和UTF-16等编码方式。即使是文本文件,如果你使用不同的编码方式打开,也可能会看到乱码。 + +所以不管是文本文件还是二进制文件也好,都需要进行编码格式的统一。也就是说写入的编码是什么样子的,那么数据读取的编码也应该和其匹配。 + +Base64编码实际上就是将二进制数据编码成为可视化ASCII字符的一种编码方式。 + +为什么会有这样的要求呢? + +我们知道计算机世界的发展不是一蹴而就的,它是一个慢慢成长的过程,对于字符编码来说,最早只支持ASCII编码,后面才扩展到Unicode等。所以对于很多应用来说除了ASCII编码之外的其他编码格式是不支持的,那么如何在这些系统中展示非ASCII code呢? + +解决的方式就是进行编码映射,将非ASCII的字符映射成为ASCII的字符。而base64就是这样的一种编码方式。 + +常见的使用Base64的地方就是在web网页中,有时候我们需要在网页中展示图片,那么可以将图片进行base64编码,然后填充到html中。 + +还有一种应用就是将文件进行base64编码,然后作为邮件的附件进行发送。 + +# JAVA对base64的支持 + +既然base64编码这么好用,接下来我们来看一下JAVA中的base64实现。 + +java中有一个对应的base64实现,叫做java.util.Base64。这个类是Base64的工具类,是JDK在1.8版本引入的。 + +Base64中提供了三个getEncoder和getDecoder方法,通过获取对应的Encoder和Decoder,然后就可以调用Encoder的encode和decode方法对数据进行编码和解码,非常的方便。 + +我们先来看一下Base64的基本使用例子: + +``` + // 使用encoder进行编码 + String encodedString = Base64.getEncoder().encodeToString("what is your name baby?".getBytes("utf-8")); + System.out.println("Base64编码过后的字符串 :" + encodedString); + + // 使用encoder进行解码 + byte[] decodedBytes = Base64.getDecoder().decode(encodedString); + + System.out.println("解码过后的字符串: " + new String(decodedBytes, "utf-8")); +``` + +作为一个工具类,JDK中提供的Base64工具类还是很好用的。 + +这里就不详细讲解它的使用,本篇文章主要分析JDK中Base64是怎么实现的。 + +# JDK中Base64的分类和实现 + +JDK中Base64类有提供了三个encoder方法,分别是getEncoder,getUrlEncoder和getMimeEncoder: + +``` + public static Encoder getEncoder() { + return Encoder.RFC4648; + } + + public static Encoder getUrlEncoder() { + return Encoder.RFC4648_URLSAFE; + } + + public static Encoder getMimeEncoder() { + return Encoder.RFC2045; + } +``` + +同样的,它也提供了三个对应的decoder,分别是getDecoder,getUrlDecoder,getMimeDecoder: + +``` + public static Decoder getDecoder() { + return Decoder.RFC4648; + } + + public static Decoder getUrlDecoder() { + return Decoder.RFC4648_URLSAFE; + } + + public static Decoder getMimeDecoder() { + return Decoder.RFC2045; + } +``` + +从代码中可以看出,这三种编码分别对应的是RFC4648,RFC4648_URLSAFE和RFC2045。 + +这三种都属于base64编码的变体,我们看下他们有什么区别: + +| 编码名称 | 编码字符 | 编码字符 | 编码字符 | +| :---------------------------------------------------: | :------: | :------: | :----------: | +| | 第62位 | 第63位 | 补全符 | +| RFC 2045: Base64 transfer encoding for MIME | `+` | `/` | `=`mandatory | +| RFC 4648: base64 (standard) | `+` | `/` | `=`optional | +| RFC 4648: base64url (URL- and filename-safe standard) | `-` | `_` | `=`optional | + +可以看到base64和Base64url的区别是第62位和第63位的编码字符不一样,而base64 for MIME跟base64的区别是补全符是否是强制的。 + +另外,对于Basic和base64url来说,不会添加line separator字符,而base64 for MIME在一行超出76字符之后,会添加’\r’ 和 ‘\n’作为line separator。 + +最后,如果在解码的过程中,发现有不存于Base64映射表中的字符的处理方式也不一样,base64和Base64url会直接拒绝,而base64 for MIME则会忽略。 + +base64和Base64url的区别可以通过下面两个方法来看出: + +``` + private static final char[] toBase64 = { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', + 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', + 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/' + }; + private static final char[] toBase64URL = { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', + 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', + 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_' + }; +``` + +而对MIME来说,定义了一个一行的最大字符个数,和换行符: + +``` + private static final int MIMELINEMAX = 76; + private static final byte[] CRLF = new byte[] {'\r', '\n'}; +``` + +# Base64的高级用法 + +一般情况下我们用Base64进行编码的对象长度是固定的,我们只需要将输入对象转换成为byte数组即可调用encode或者decode的方法。 + +但是在某些情况下我们需要对流数据进行转换,这时候就可以用到Base64中提供的两个对Stream进行wrap的方法: + +``` + public OutputStream wrap(OutputStream os) { + Objects.requireNonNull(os); + return new EncOutputStream(os, isURL ? toBase64URL : toBase64, + newline, linemax, doPadding); + } + public InputStream wrap(InputStream is) { + Objects.requireNonNull(is); + return new DecInputStream(is, isURL ? fromBase64URL : fromBase64, isMIME); + } +``` + +这两个方法分别对应于encoder和decoder。 + +# 总结 + +以上就是JDK中对Base64的实现和使用,虽然base64的变种有很多种,但是JDK中的Base64只实现了其中用处最为广泛的3种。大家在使用的时候一定要区分具体是那种Base64的实现方式,以免出现问题。 \ No newline at end of file diff --git a/learn-netty文章/58.java高级用法之在JNA中将本地方法映射到JAVA代码中.md b/learn-netty文章/58.java高级用法之在JNA中将本地方法映射到JAVA代码中.md new file mode 100644 index 0000000..8c0fb4a --- /dev/null +++ b/learn-netty文章/58.java高级用法之在JNA中将本地方法映射到JAVA代码中.md @@ -0,0 +1,187 @@ +# java高级用法之:在JNA中将本地方法映射到JAVA代码中 + +文章目录 + + + +[简介](http://www.flydean.com/03-jna-library-mapping/#简介)[Library Mapping](http://www.flydean.com/03-jna-library-mapping/#Library_Mapping)[Function Mapping](http://www.flydean.com/03-jna-library-mapping/#Function_Mapping)[Invocation Mapping](http://www.flydean.com/03-jna-library-mapping/#Invocation_Mapping)[防止VM崩溃](http://www.flydean.com/03-jna-library-mapping/#防止VM崩溃)[性能考虑](http://www.flydean.com/03-jna-library-mapping/#性能考虑)[总结](http://www.flydean.com/03-jna-library-mapping/#总结) + +# 简介 + +不管是JNI还是JNA,最终调用的都是native的方法,但是对于JAVA程序来说,一定需要一个调用native方法的入口,也就是说我们需要在JAVA方法中定义需要调用的native方法。 + +对于JNI来说,我们可以使用native关键字来定义本地方法。那么在JNA中有那些在JAVA代码中定义本地方法的方式呢? + +# Library Mapping + +要想调用本地的native方法,首选需要做的事情就是加载native的lib文件。我们把这个过程叫做Library Mapping,也就是说把native的library 映射到java代码中。 + +JNA中有两种Library 映射的方法,分别是interface和direct mapping。 + +先看下interface mapping,假如我们要加载 C library, 如果使用interface mapping的方式,我们需要创建一个interface继承Library: + +``` +public interface CLibrary extends Library { + CLibrary INSTANCE = (CLibrary)Native.load("c", CLibrary.class); +} +``` + +上面代码中Library是一个interface,所有的interface mapping都需要继承这个Library。 + +然后在interface内部,通过使用Native.load方法来加载要使用的c library。 + +上面的代码中,load方法传入两个参数,第一个参数是library的name,第二个参数是interfaceClass. + +下面的表格展示了Library Name和传入的name之间的映射关系: + +| OS | Library Name | String | +| :----------------: | :------------------------------------------------: | :----: | +| Windows | user32.dll | user32 | +| Linux | libX11.so | X11 | +| Mac OS X | libm.dylib | m | +| Mac OS X Framework | /System/Library/Frameworks/Carbon.framework/Carbon | Carbon | +| Any Platform | current process | null | + +事实上,load还可以接受一个options的Map参数。默认情况下JAVA interface中要调用的方法名称就是native library中定义的方法名称,但是有些情况下我们可能需要在JAVA代码中使用不同的名字,在这种情况下,可以传入第三个参数map,map的key可以是 OPTION_FUNCTION_MAPPER,而它的value则是一个 FunctionMapper ,用来将JAVA中的方法名称映射到native library中。 + +传入的每一个native library都可以用一个NativeLibrary的实例来表示。这个NativeLibrary的实例也可以通过调用NativeLibrary.getInstance(String)来获得。 + +另外一种加载native libary的方式就是direct mapping,direct mapping使用的是在static block中调用Native.register方式来加载本地库,如下所示: + +``` +public class CLibrary { + static { + Native.register("c"); + } +} +``` + +# Function Mapping + +当我们加载完native library之后,接下来就是定义需要调用的函数了。实际上就是做一个从JAVA代码到native lib中函数的一个映射,我们将其称为Function Mapping。 + +和Library Mapping一样,Function Mapping也有两种方式。分别是interface mapping和direct mapping。 + +在interface mapping中,我们只需要按照native library中的方法名称定义一个一样的方法即可,这个方法不用实现,也不需要像JNI一样使用native来修饰,如下所示: + +``` +public interface CLibrary extends Library { + int atol(String s); +} +``` + +> 注意,上面我们提到了JAVA中的方法名称不一定必须和native library中的方法名称一致,你可以通过给Native.load方法传入一个FunctionMapper来实现。 + +或者,你可以使用direct mapping的方式,通过给方法添加一个native修饰符: + +``` +public class HelloWorld { + + public static native double cos(double x); + public static native double sin(double x); + + static { + Native.register(Platform.C_LIBRARY_NAME); + } + + public static void main(String[] args) { + System.out.println("cos(0)=" + cos(0)); + System.out.println("sin(0)=" + sin(0)); + } +} +``` + +对于direct mapping来说,JAVA方法可以映射到native library中的任何static或者对象方法。 + +虽然direct mapping和我们常用的java JNI有些类似,但是direct mapping存在着一些限制。 + +大部分情况下,direct mapping和interface mapping具有相同的映射类型,但是不支持Pointer/Structure/String/WString/NativeMapped数组作为函数参数值。 + +在使用TypeMapper或者NativeMapped的情况下,direct mapping不支持 NIO Buffers 或者基本类型的数组作为返回值。 + +如果要使用基础类型的包装类,则必须使用自定义的TypeMapper. + +对象JAVA中的方法映射来说,该映射最终会创建一个Function对象。 + +# Invocation Mapping + +讲完library mapping和function mapping之后,我们接下来讲解一下Invocation Mapping。 + +Invocation Mapping代表的是Library中的OPTION_INVOCATION_MAPPER,它对应的值是一个InvocationMapper。 + +之前我们提到了FunctionMapper,可以实现JAVA中定义的方法名和native lib中的方法名不同,但是不能修改方法调用的状态或者过程。 + +而InvocationMapper则更进一步, 允许您任意重新配置函数调用,包括更改方法名称以及重新排序、添加或删除参数。 + +下面举个例子: + +``` + new InvocationMapper() { + public InvocationHandler getInvocationHandler(NativeLibrary lib, Method m) { + if (m.getName().equals("stat")) { + final Function f = lib.getFunction("_xstat"); + return new InvocationHandler() { + public Object invoke(Object proxy, Method method, Object[] args) { + Object[] newArgs = new Object[args.length+1]; + System.arraycopy(args, 0, newArgs, 1, args.length); + newArgs[0] = Integer.valueOf(3); // _xstat version + return f.invoke(newArgs); + } + }; + } + return null; + } + } +``` + +看上面的调用例子,感觉有点像是反射调用,我们在InvocationMapper中实现了getInvocationHandler方法,根据给定的JAVA代码中的method去查找具体的native lib,然后获取到lib中的function,最后调用function的invoke方法实现方法的最终调用。 + +在这个过程中,我们可以修改方传入的参数,或者做任何我们想做的事情。 + +还有一种情况是c语言中的内联函数或者预处理宏,如下所示: + +``` +// Original C code (macro and inline variations) + #define allocblock(x) malloc(x * 1024) + static inline void* allocblock(size_t x) { return malloc(x * 1024); } +``` + +上面的代码中定义了一个allocblock(x)宏,它实际上等于malloc(x * 1024),这种情况就可以使用InvocationMapper,将allocblock使用具体的malloc来替换: + +``` + // Invocation mapping + new InvocationMapper() { + public InvocationHandler getInvocationHandler(NativeLibrary lib, Method m) { + if (m.getName().equals("allocblock")) { + final Function f = lib.getFunction("malloc"); + return new InvocationHandler() { + public Object invoke(Object proxy, Method method, Object[] args) { + args[0] = ((Integer)args[0]).intValue() * 1024; + return f.invoke(newArgs); + } + }; + } + return null; + } + } +``` + +# 防止VM崩溃 + +JAVA方法和native方法映射肯定会出现一些问题,如果映射方法不对或者参数不匹配的话,很有可能出现memory access errors,并且可能会导致VM崩溃。 + +通过调用Native.setProtected(true),可以将VM崩溃转换成为对应的JAVA异常,当然,并不是所有的平台都支持protection,如果平台不支持protection,那么Native.isProtected()会返回false。 + +> 如果要使用protection,还要同时使用 jsig library,以防止信号和JVM的信号冲突。libjsig.so一般存放在JRE的lib目录下,/java.home/lib/{os.arch}/libjsig.so, 可以通过将环境变量设置为LD_PRELOAD (或者LD_PRELOAD_64)来使用。 + +# 性能考虑 + +上面我们提到了JNA的两种mapping方式,分别是interface mapping和direct mapping。相较而言,direct mapping的效率更高,因为direct mapping调用native方法更加高效。 + +但是上面我们也提到了direct mapping在使用上有一些限制,所以我们在使用的时候需要进行权衡。 + +另外,我们需要避免使用基础类型的封装类,因为对于native方法来说,只有基础类型的匹配,如果要使用封装类,则必须使用Type mapping,从而造成性能损失。 + +# 总结 + +JNA是调用native方法的利器,如果数量掌握的话,肯定是如虎添翼 \ No newline at end of file diff --git a/learn-netty文章/59.java高级用法之在JNA中使用类型映射.md b/learn-netty文章/59.java高级用法之在JNA中使用类型映射.md new file mode 100644 index 0000000..b4c6b09 --- /dev/null +++ b/learn-netty文章/59.java高级用法之在JNA中使用类型映射.md @@ -0,0 +1,184 @@ +# java高级用法之:在JNA中使用类型映射 + +文章目录 + + + +[简介](http://www.flydean.com/04-jna-type-mapping/#简介)[类型映射的本质](http://www.flydean.com/04-jna-type-mapping/#类型映射的本质)[TypeMapper](http://www.flydean.com/04-jna-type-mapping/#TypeMapper)[NativeMapped](http://www.flydean.com/04-jna-type-mapping/#NativeMapped)[总结](http://www.flydean.com/04-jna-type-mapping/#总结) + +# 简介 + +JNA中有很多种映射,library的映射,函数的映射还有函数参数和返回值的映射,libary和函数的映射比较简单,我们在之前的文章中已经讲解过了,对于类型映射来说,因为JAVA中的类型种类比较多,所以这里我们将JNA的类型映射提取出来单独讲解。 + +# 类型映射的本质 + +我们之前提到在JNA中有两种方法来映射JAVA中的方法和native libary中的方法,一种方法叫做interface mapping,一种方式叫做direct mapping。 + +但是我们有没有考虑过这两种映射的本质是什么呢? + +比如native有一个方法,我们是如何将JAVA代码中的方法参数传递给native方法,并且将native方法的返回值转换成JAVA中函数的返回类型呢? + +答案就是序列化。 + +因为本质上一切的交互都是二进制的交互。JAVA类型和native类型进行转换,最简单的情况就是JAVA类型和native类型底层的数据长度保持一致,这样在进行数据转换的时候就会更加简单。 + +我们看下JAVA类型和native类型的映射和长度关系: + +| C Type | Native类型的含义 | Java Type | +| :-------------------------: | :--------------: | :---------------: | +| char | 8-bit整型 | byte | +| wchar_t | 和平台相关 | char | +| short | 16-bit整型 | short | +| int | 32-bit整型 | int | +| int | boolean flag | boolean | +| enum | 枚举类型 | int (usually) | +| long long, __int64 | 64-bit整型 | long | +| float | 32-bit浮点数 | float | +| double | 64-bit浮点数 | double | +| pointer (e.g. void*) | 平台相关 | Buffer Pointer | +| pointer (e.g. void*), array | 平台相关 | [] (原始类型数组) | + +上面的JAVA类型都是JDK自带的类型(Pointer除外)。 + +除了JAVA自带的类型映射,JNA内部也定义了一些数据类型,可以跟native的类型进行映射: + +| C Type | Native类型的含义 | Java Type | +| :------------: | :--------------------------------------: | :----------: | +| long | 和平台相关(32- or 64-bit integer) | NativeLong | +| const char* | 字符串 (native encoding or jna.encoding) | String | +| const wchar_t* | 字符串 (unicode) | WString | +| char** | 字符串数组 | String[] | +| wchar_t** | 字符串数组(unicode) | WString[] | +| void** | pointers数组 | Pointer[] | +| struct* struct | 结构体指针和结构体 | Structure | +| union | 结构体 | Union | +| struct[] | 结构体数组 | Structure[] | +| void (*FP)() | 函数指针 (Java or native) | Callback | +| pointer ( *) | 指针 | PointerType | +| other | 整数类型 | IntegerType | +| other | 自定义映射类型 | NativeMapped | + +# TypeMapper + +除了定义好的映射关系之外,大家也可以使用TypeMapper来对参数类型进行自定义转换,先来看下TypeMapper的定义: + +``` +public interface TypeMapper { + + FromNativeConverter getFromNativeConverter(Class javaType); + + ToNativeConverter getToNativeConverter(Class javaType); +} +``` + +TypeMapper是一个interface,它定义了两个converter方法,分别是getFromNativeConverter和getToNativeConverter。 + +如果要使用TypeMapper则需要实现它而这两个方法即可。我们看一下官方的W32APITypeMapper是怎么实现的: + +``` + TypeConverter stringConverter = new TypeConverter() { + @Override + public Object toNative(Object value, ToNativeContext context) { + if (value == null) + return null; + if (value instanceof String[]) { + return new StringArray((String[])value, true); + } + return new WString(value.toString()); + } + @Override + public Object fromNative(Object value, FromNativeContext context) { + if (value == null) + return null; + return value.toString(); + } + @Override + public Class nativeType() { + return WString.class; + } + }; + addTypeConverter(String.class, stringConverter); + addToNativeConverter(String[].class, stringConverter); +``` + +首先定义一个TypeConverter,在TypeConverter中实现了toNative,fromNative和nativeType三个方法。在这个例子中,native type是WString,而JAVA type是String。而这个TypeConverter就是最终要使用的FromNativeConverter和ToNativeConverter。 + +有了typeMapper,应该怎么使用呢?最简单的方法就是将其添加到Native.load的第三个参数中,如下所示: + +``` + TestLibrary lib = Native.load("testlib", TestLibrary.class, Collections.singletonMap(Library.OPTION_TYPE_MAPPER, mapper)); +``` + +# NativeMapped + +TypeMapper需要在调用Native.load方法的时候传入,从而提供JAVA类型和native类型的转换关系。TypeMapper可以看做是类型转换关系的外部维护者。 + +可能很多朋友已经想到了,既然能在JAVA类型外部维护转换关系,那么可不可以在JAVA类型本身对这个转换关系进行维护呢?答案是肯定的,我们只需要在要实现转换类型关系的JAVA类型实现NativeMapped接口即可。 + +先来看下NativeMapped接口的定义: + +``` +public interface NativeMapped { + + Object fromNative(Object nativeValue, FromNativeContext context); + + Object toNative(); + + Class nativeType(); +} +``` + +可以看到NativeMapped中定义要实现的方法基本上和FromNativeConverter、ToNativeConverter中定义的方法一致。 + +下面举一个具体的例子来说明一下NativeMapped到底应该怎么使用。首先我们定义一个enum类实现NativeMapped接口: + +``` + public enum TestEnum implements NativeMapped { + VALUE1, VALUE2; + + @Override + public Object fromNative(Object nativeValue, FromNativeContext context) { + return values()[(Integer) nativeValue]; + } + + @Override + public Object toNative() { + return ordinal(); + } + + @Override + public Class nativeType() { + return Integer.class; + } + } +``` + +这个类实现了从Integer到TestEnum枚举的转换。 + +要想使用该TestEnum类的话,需要定义一个interface: + +``` + public static interface EnumerationTestLibrary extends Library { + TestEnum returnInt32Argument(TestEnum arg); + } +``` + +具体调用逻辑如下: + +``` +EnumerationTestLibrary lib = Native.load("testlib", EnumerationTestLibrary.class); +assertEquals("Enumeration improperly converted", TestEnum.VALUE1, lib.returnInt32Argument(TestEnum.VALUE1)); +assertEquals("Enumeration improperly converted", TestEnum.VALUE2, lib.returnInt32Argument(TestEnum.VALUE2)); +``` + +可以看到,因为NativeMapped中已经包含了类型转换的信息,所以不需要再指定TypeMapper了。 + +> 注意,这里用到了testlib,这个testlib是从JNA的native模块中编译出来的,如果你是MAC环境的话可以拷贝JNA代码,运行ant native即可得到,编译完成之后,将这个libtestlib.dylib拷贝到你项目中的resources目录下面darwin-aarch64或者darwin-x86即可。 + +有不会的同学,可以联系我。 + +# 总结 + +本文讲解了JNA中的类型映射规则和自定义类型映射的方法。 + +本文的代码:https://github.com/ddean2009/learn-java-base-9-to-20.git \ No newline at end of file diff --git a/learn-netty文章/5c1ced36f1d84bdda67329044f47c767.png b/learn-netty文章/5c1ced36f1d84bdda67329044f47c767.png new file mode 100644 index 0000000..982505f Binary files /dev/null and b/learn-netty文章/5c1ced36f1d84bdda67329044f47c767.png differ diff --git a/learn-netty文章/5cefd4a7dcf64051af18800c42605675.png b/learn-netty文章/5cefd4a7dcf64051af18800c42605675.png new file mode 100644 index 0000000..5f49a11 Binary files /dev/null and b/learn-netty文章/5cefd4a7dcf64051af18800c42605675.png differ diff --git a/learn-netty文章/6.netty系列之中国加油.md b/learn-netty文章/6.netty系列之中国加油.md new file mode 100644 index 0000000..6728237 --- /dev/null +++ b/learn-netty文章/6.netty系列之中国加油.md @@ -0,0 +1,202 @@ +# netty系列之:中国加油 + +# 简介 + +之前的系列文章中我们学到了netty的基本结构和工作原理,各位小伙伴一定按捺不住心中的喜悦,想要开始手写代码来体验这神奇的netty框架了,刚好最近东京奥运会,我们写一个netty的客户端和服务器为中国加油可好? + +# 场景规划 + +那么我们今天要搭建什么样的系统呢? + +首先要搭建一个server服务器,用来处理所有的netty客户的连接,并对客户端发送到服务器的消息进行处理。 + +还要搭建一个客户端,这个客户端负责和server服务器建立连接,并发送消息给server服务器。在今天的例子中,客户端在建立连接过后,会首先发送一个“中国”消息给服务器,然后服务器收到消息之后再返回一个”加油!“ 消息给客户端,然后客户端收到消息之后再发送一个“中国”消息给服务器…. 以此往后,循环反复直到奥运结束! + +我们知道客户端和服务器端进行消息处理都是通过handler来进行的,在handler里面,我们可以重写channelRead方法,这样在读取channel中的消息之后,就可以对消息进行处理了,然后将客户端和服务器端的handler配置在Bootstrap中启动就可以了,是不是很简单?一起来做一下吧。 + +# 启动Server + +假设server端的handler叫做CheerUpServerHandler,我们使用ServerBootstrap构建两个EventLoopGroup来启动server,有看过本系列最前面文章的小伙伴可能知道,对于server端需要启动两个EventLoopGroup,一个bossGroup,一个workerGroup,这两个group是父子关系,bossGroup负责处理连接的相关问题,而workerGroup负责处理channel中的具体消息。 + +启动服务的代码千篇一律,如下所示: + +```java +// Server配置 +//boss loop +EventLoopGroup bossGroup = new NioEventLoopGroup(1); +//worker loop +EventLoopGroup workerGroup = new NioEventLoopGroup(); +final CheerUpServerHandler serverHandler = new CheerUpServerHandler(); +try { + ServerBootstrap b = new ServerBootstrap(); + b.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + // tcp/ip协议listen函数中的backlog参数,等待连接池的大小 + .option(ChannelOption.SO_BACKLOG, 100) + //日志处理器 + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new ChannelInitializer() { + @Override + //初始化channel,添加handler + public void initChannel(SocketChannel ch) throws Exception { + ChannelPipeline p = ch.pipeline(); + //日志处理器 + p.addLast(new LoggingHandler(LogLevel.INFO)); + p.addLast(serverHandler); + } + }); + + // 启动服务器 + ChannelFuture f = b.bind(PORT).sync(); + + // 等待channel关闭 + f.channel().closeFuture().sync(); +``` + +不同的服务,启动服务器的代码基本都是一样的,这里我们需要注意这几点。 + +在ServerBootstrap中,我们加入了一个选项:ChannelOption.SO_BACKLOG,ChannelOption.SO_BACKLOG对应的是tcp/ip协议listen(int socketfd,int backlog)函数中的backlog参数,用来初始化服务端可连接队列,backlog参数指定了这个队列的大小。因为对于一个连接来说,处理客户端连接请求是顺序处理的,所以同一时间只能处理一个客户端连接,多个客户端来的时候,服务端将不能处理的客户端连接请求放在队列中等待处理, + +另外我们还添加了两个LoggingHandler,一个是给handler添加的,一个是给childHandler添加的。LoggingHandler主要监控channel中的各种事件,然后输出对应的消息,非常好用。 + +比如在服务器启动的时候会输出下面的日志: + +```java + [nioEventLoopGroup-2-1] INFO i.n.handler.logging.LoggingHandler - [id: 0xd9b41ea4] REGISTERED + [nioEventLoopGroup-2-1] INFO i.n.handler.logging.LoggingHandler - [id: 0xd9b41ea4] BIND: 0.0.0.0/0.0.0.0:8007 + [nioEventLoopGroup-2-1] INFO i.n.handler.logging.LoggingHandler - [id: 0xd9b41ea4, L:/0:0:0:0:0:0:0:0:8007] ACTIVE +``` + +这个日志是第一个LoggingHandler输出的,分别代表了服务器端的REGISTERED、BIND和ACTIVE事件。从输出我们可以看到,服务器本身绑定的是0.0.0.0:8007。 + +在客户端启动和服务器端建立连接的时候会输出下面的日志: + +```java +[nioEventLoopGroup-2-1] INFO i.n.handler.logging.LoggingHandler - [id: 0x37a4ba9f, L:/0:0:0:0:0:0:0:0:8007] READ: [id: 0x6dcbae9c, L:/127.0.0.1:8007 - R:/127.0.0.1:54566] +[nioEventLoopGroup-2-1] INFO i.n.handler.logging.LoggingHandler - [id: 0x37a4ba9f, L:/0:0:0:0:0:0:0:0:8007] READ COMPLETE +``` + +上面日志表示READ和READ COMPLETE两个事件,其中 L:/127.0.0.1:8007 – R:/127.0.0.1:54566 代表本地服务器的8007端口连接了客户端的54566端口。 + +对于第二个LoggingHandler来说,会输出一些具体的消息处理相关的消息。比如REGISTERED、ACTIVE、READ、WRITE、FLUSH、READ COMPLETE等事件,这里面就不一一列举了。 + +# 启动客户端 + +同样的,假设客户端的handler名称叫做ChinaClientHandler,那么可以类似启动server一样启动客户端,如下: + +```java +// 客户端的eventLoop +EventLoopGroup group = new NioEventLoopGroup(); +try { + Bootstrap b = new Bootstrap(); + b.group(group) + .channel(NioSocketChannel.class) + .option(ChannelOption.TCP_NODELAY, true) + .handler(new ChannelInitializer() { + @Override + public void initChannel(SocketChannel ch) throws Exception { + ChannelPipeline p = ch.pipeline(); + //添加日志处理器 + p.addLast(new LoggingHandler(LogLevel.INFO)); + p.addLast(new ChinaClientHandler()); + } + }); + // 启动客户端 + ChannelFuture f = b.connect(HOST, PORT).sync(); +``` + +客户端启动使用的是Bootstrap,我们同样为他配置了一个LoggingHandler,并添加了自定义的ChinaClientHandler。 + +# 消息处理 + +我们知道有两种handler,一种是inboundHandler,一种是outboundHandler,这里我们是要监控从socket读取数据的事件,所以这里客户端和服务器端的handler都继承自ChannelInboundHandlerAdapter即可。 + +消息处理的流程是客户端和服务器建立连接之后,会首先发送一个”中国“的消息给服务器。 + +客户端和服务器建立连接之后,会触发channelActive事件,所以在客户端的handler中就可以发送消息了: + +```java +public void channelActive(ChannelHandlerContext ctx) { + ctx.writeAndFlush("中国"); +} +``` + +服务器端在从channel中读取消息的时候会触发channelRead事件,所以服务器端的handler可以重写channelRead方法: + +```java +public void channelRead(ChannelHandlerContext ctx, Object msg) { + log.info("收到消息:{}",msg); + ctx.writeAndFlush("加油!"); +} +``` + +然后客户端从channel中读取到”加油!”之后,再将”中国“写到channel中,所以客户端也需要重写方法channelRead: + +```java +public void channelRead(ChannelHandlerContext ctx, Object msg) { + ctx.writeAndFlush("中国"); +} +``` + +这样是不是就可以循环往复的进行下去了呢? + +# 消息处理中的陷阱 + +事实上,当你执行上面代码你会发现,客户端确实将”中国“ 消息写入了channel,但是服务器端的channelRead并没有被触发。为什么呢? + +研究发下,如果写入的对象是一个String,程序内部会有这样的错误,但是这个错误是隐藏的,你并不会在运行的程序输出中看到,所以对新手小伙伴还是很不友好的。这个错误就是: + +``` +DefaultChannelPromise@57f5c075(failure: java.lang.UnsupportedOperationException: unsupported message type: String (expected: ByteBuf, FileRegion)) +``` + +从错误的信息可以看出,目前支持的消息类型有两种,分别是ByteBuf和FileRegion。 + +好了,我们将上面的消息类型改成ByteBuf试一试: + +```java +message = Unpooled.buffer(ChinaClient.SIZE); +message.writeBytes("中国".getBytes(StandardCharsets.UTF_8)); + +public void channelActive(ChannelHandlerContext ctx) { + log.info("可读字节:{},index:{}",message.readableBytes(),message.readerIndex()); + log.info("可写字节:{},index:{}",message.writableBytes(),message.writerIndex()); + ctx.writeAndFlush(message); +} +``` + +上面我们定义了一个ByteBuf的全局message对象,并将其发送给server,然后在server端读取到消息之后,再发送一个ByteBuf的全局message对象给client,如此循环往复。 + +但是当你运行上面的程序之后会发现,服务器端确实收到了”中国“,客户端也确实收到了”加油!“,但是客户端后续发送的”中国“消息服务器端却收不到了,怎么回事呢? + +我们知道ByteBuf有readableBytes、readerIndex、writableBytes、writerIndex、capacity和refCnt等属性,我们将这些属性在message发送前和发送之后进行对比: + +在消息发送之前: + +``` +可读字节:6,readerIndex:0 +可写字节:14,writerIndex:6 +capacity:20,refCnt:1 +``` + +在消息发送之后: + +``` +可读字节:6,readerIndex:0 +可写字节:-6,writerIndex:6 +capacity:0,refCnt:0 +``` + +于是问题找到了,由于ByteBuf在处理过一次之后,refCnt变成了0,所以无法继续再次重复写入,怎么解决呢? + +简单的办法就是每次发送的时候再重新new一个ByteBuf,这样就没有问题了。 + +但是每次都新建一个对象好像有点浪费空间,怎么办呢?既然refCnt变成了0,那么我们调用ByteBuf中的retain()方法增加refCnt不就行了? + +答案就是这样,但是要注意,需要在发送之前调用retain()方法,如果是在消息被处理过后调用retain()会报异常。 + +# 总结 + +好了,运行上面的程序就可以一直给中国加油了,YYDS! + +本文的例子可以参考:[learn-netty4](https://github.com/ddean2009/learn-netty4) \ No newline at end of file diff --git a/learn-netty文章/60.java高级用法之JNA类型映射应该注意的问题.md b/learn-netty文章/60.java高级用法之JNA类型映射应该注意的问题.md new file mode 100644 index 0000000..98b54a5 --- /dev/null +++ b/learn-netty文章/60.java高级用法之JNA类型映射应该注意的问题.md @@ -0,0 +1,246 @@ +# java高级用法之:JNA类型映射应该注意的问题 + +文章目录 + + + +[简介](http://www.flydean.com/05-jna-type-mapping-details-md/#简介)[String](http://www.flydean.com/05-jna-type-mapping-details-md/#String)[Buffers,Memory,数组和Pointer](http://www.flydean.com/05-jna-type-mapping-details-md/#Buffers,Memory数组和Pointer)[可变参数](http://www.flydean.com/05-jna-type-mapping-details-md/#可变参数)[总结](http://www.flydean.com/05-jna-type-mapping-details-md/#总结) + +# 简介 + +JNA提供JAVA类型和native类型的映射关系,但是这一种映射关系只是一个大概的映射,我们在实际的应用中还有很多需要注意的事项,本文将会为大家详细讲解在使用类型映射中可能会出现的问题。一起来看看吧。 + +# String + +首先是String的映射,JAVA中的String实际上对应的是两种native类型:const char* 和 const wchar_t*。默认情况下String会被转换成为char* 。 + +> char是ANSI类型的数据类型,而wchar_t是Unicode字符的数据类型,也叫做宽字符。 + +如果JAVA的unicode characters要转换成为char数组,那么需要进行一些编码操作,如果设置了jna.encoding,那么就会使用设置好的编码方式来进行编码。默认情况下编码方式是 “UTF8”. + +如果是WString,那么Unicode values可以直接拷贝到WString中,而不需要进行任何编码。 + +先看一个简单的例子: + +``` + char* returnStringArgument(char *arg) { + return arg; +} + +wchar_t* returnWStringArgument(wchar_t *arg) { + return arg; +} +``` + +上面的native代码可以映射为: + +``` +String returnStringArgument(String s); +WString returnWStringArgument(WString s); +``` + +再来看一个不同的例子,假如native方法的定义是这样的: + +``` +int getString(char* buffer, int bufsize); + +int getUnicodeString(wchar_t* buffer, int bufsize); +``` + +我们定义了两个方法,方法的参数分别是char* 和wchar_t*。 + +接下来看一下怎么在JAVA中定义方法的映射: + +``` +// Mapping A: +int getString(byte[] buf, int bufsize); +// Mapping B: +int getUnicodeString(char[] buf, int bufsize); +``` + +下面是具体的使用: + +``` +byte[] buf = new byte[256]; +int len = getString(buf, buf.length); +String normalCString = Native.toString(buf); +String embeddedNULs = new String(buf, 0, len); +``` + +可能有同学会问了,既然JAVA中的String可以转换成为char*,为什么这里需要使用byte数组呢? + +这是因为getString方法需要对传入的char数组中的内容进行修改,但是因为String是不可变的,所以这里是不能直接使用String的,我们需要使用byte数组。 + +接着我们使用Native.toString(byte[]) 将byte数组转换成为JAVA字符串。 + +再看一个返回值的情况: + +``` +// Example A: Returns a C string directly +const char* getString(); +// Example B: Returns a wide character C string directly +const wchar_t* getString(); +``` + +一般情况下,如果是native方法直接返回string,我们可以使用String进行映射: + +``` +// Mapping A +String getString(); +// Mapping B +WString getString(); +``` + +如果native code为String分配了内存空间,那么我们最好使用JNA中的Pointer作为返回值,这样我们可以在未来某些时候,释放所占用的空间,如下所示: + +``` +Pointer getString(); +``` + +# Buffers,Memory,数组和Pointer + +什么时候需要用到Buffers和Memory呢? + +一般情况下如果是基础数据的数组作为参数传到函数中的话,可以在JAVA中直接使用基础类的数组来替代。但是如果native方法在方法返回之后,还需要访问数组的话(保存了指向数组的指针),这种情况下使用基础类的数组就不太合适了,这种情况下,我们需要用到ByteBuffers或者Memory。 + +我们知道JAVA中的数组是带有长度的,但是对于native方法来说,返回的数组实际上是一个指向数组的指针,我们并不能知道返回数组的长度,所以如果native方法返回的是数组指针的话,JAVA代码中用数组来进行映射就是不合适的。这种情况下,需要用到Pointer. + +Pointer表示的是一个指针,先看一下Pointer的例子,首先是native代码: + +``` +void* returnPointerArgument(void *arg) { + return arg; +} + +void* returnPointerArrayElement(void* args[], int which) { + return args[which]; +} +``` + +接下来是JAVA的映射: + +``` +Pointer returnPointerArgument(Pointer p); + +Pointer returnPointerArrayElement(Pointer[] args, int which); +``` + +除了基本的Pointer之外,你还可以自定义带类型的Pointer,也就是PointerType. 只需要继承PointerType即可,如下所示: + +``` +public static class TestPointerType extends PointerType { + public TestPointerType() { } + public TestPointerType(Pointer p) { super(p); } + } +TestPointerType returnPointerArrayElement(TestPointerType[] args, int which); +``` + +再看一下字符串数组: + +``` +char* returnStringArrayElement(char* args[], int which) { + return args[which]; +} + +wchar_t* returnWideStringArrayElement(wchar_t* args[], int which) { + return args[which]; +} +``` + +对应的JAVA映射如下: + +``` +String returnStringArrayElement(String[] args, int which); + +WString returnWideStringArrayElement(WString[] args, int which); +``` + +对应Buffer来说,JAVA NIO中提供了很多类型的buffer,比如ByteBuffer,ShortBuffer,IntBuffer,LongBuffer,FloatBuffer和DoubleBuffer等。这里以ByteBuffer为例,来看一下具体的使用. + +首先看下native代码: + +``` +int32_t fillInt8Buffer(int8_t *buf, int len, char value) { + int i; + + for (i=0;i < len;i++) { + buf[i] = value; + } + return len; +} +``` + +这里将buff进行填充,很明显后续还需要使用到这个buffer,所以这里使用数组是不合适的,我们可以选择使用ByteBuffer: + +``` +int fillInt8Buffer(ByteBuffer buf, int len, byte value); +``` + +然后看下具体怎么使用: + +``` +TestLibrary lib = Native.load("testlib", TestLibrary.class); + + ByteBuffer buf = ByteBuffer.allocate(1024).order(ByteOrder.nativeOrder()); + final byte MAGIC = (byte)0xED; + lib.fillInt8Buffer(buf, 1024, MAGIC); + for (int i=0;i < buf.capacity();i++) { + assertEquals("Bad value at index " + i, MAGIC, buf.get(i)); + } +``` + +# 可变参数 + +对于native和JAVA本身来说,都是支持可变参数的,我们举个例子,在native方法中: + +``` +int32_t addVarArgs(const char *fmt, ...) { + va_list ap; + int32_t sum = 0; + va_start(ap, fmt); + + while (*fmt) { + switch (*fmt++) { + case 'd': + sum += va_arg(ap, int32_t); + break; + case 'l': + sum += (int) va_arg(ap, int64_t); + break; + case 's': // short (promoted to 'int' when passed through '...') + case 'c': // byte/char (promoted to 'int' when passed through '...') + sum += (int) va_arg(ap, int); + break; + case 'f': // float (promoted to ‘double’ when passed through ‘...’) + case 'g': // double + sum += (int) va_arg(ap, double); + break; + default: + break; + } + } + va_end(ap); + return sum; +} +``` + +对应的JAVA方法映射如下: + +``` +public int addVarArgs(String fmt, Number... args); +``` + +相应的调用代码如下: + +``` +int arg1 = 1; +int arg2 = 2; +assertEquals("32-bit integer varargs not added correctly", arg1 + arg2, + lib.addVarArgs("dd", arg1, arg2)); +``` + +# 总结 + +本文介绍了在使用JNA方法映射中应该注意的一些细节和具体的使用问题。 + +本文的代码:https://github.com/ddean2009/learn-java-base-9-to-20.git \ No newline at end of file diff --git a/learn-netty文章/61.netty系列之netty中的核心MessageToMessage编码器.md b/learn-netty文章/61.netty系列之netty中的核心MessageToMessage编码器.md new file mode 100644 index 0000000..bd9ca2b --- /dev/null +++ b/learn-netty文章/61.netty系列之netty中的核心MessageToMessage编码器.md @@ -0,0 +1,275 @@ +# netty系列之:netty中的核心MessageToMessage编码器 + +文章目录 + + + +[简介](http://www.flydean.com/14-0-1-netty-codec-msg-to-msg/#简介)[框架简介](http://www.flydean.com/14-0-1-netty-codec-msg-to-msg/#框架简介)[MessageToMessageEncoder](http://www.flydean.com/14-0-1-netty-codec-msg-to-msg/#MessageToMessageEncoder)[MessageToMessageDecoder](http://www.flydean.com/14-0-1-netty-codec-msg-to-msg/#MessageToMessageDecoder)[MessageToMessageCodec](http://www.flydean.com/14-0-1-netty-codec-msg-to-msg/#MessageToMessageCodec)[总结](http://www.flydean.com/14-0-1-netty-codec-msg-to-msg/#总结) + +# 简介 + +在netty中我们需要传递各种类型的消息,这些message可以是字符串,可以是数组,也可以是自定义的对象。不同的对象之间可能需要互相转换,这样就需要一个可以自由进行转换的转换器,为了统一编码规则和方便用户的扩展,netty提供了一套消息之间进行转换的框架。本文将会讲解这个框架的具体实现。 + +# 框架简介 + +netty为消息和消息之间的转换提供了三个类,这三个类都是抽象类,分别是MessageToMessageDecoder,MessageToMessageEncoder和MessageToMessageCodec。 + +先来看下他们的定义: + +``` +public abstract class MessageToMessageEncoder extends ChannelOutboundHandlerAdapter +public abstract class MessageToMessageDecoder extends ChannelInboundHandlerAdapter +public abstract class MessageToMessageCodec extends ChannelDuplexHandler +``` + +MessageToMessageEncoder继承自ChannelOutboundHandlerAdapter,负责向channel中写消息。 + +MessageToMessageDecoder继承自ChannelInboundHandlerAdapter,负责从channel中读取消息。 + +MessageToMessageCodec继承自ChannelDuplexHandler,它是一个双向的handler,可以从channel中读取消息,也可以向channel中写入消息。 + +有了这三个抽象类,我们再看下这三个类的具体实现。 + +# MessageToMessageEncoder + +先看一下消息的编码器MessageToMessageEncoder,编码器中最重要的方法就是write,看下write的实现: + +``` + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + CodecOutputList out = null; + try { + if (acceptOutboundMessage(msg)) { + out = CodecOutputList.newInstance(); + @SuppressWarnings("unchecked") + I cast = (I) msg; + try { + encode(ctx, cast, out); + } finally { + ReferenceCountUtil.release(cast); + } + + if (out.isEmpty()) { + throw new EncoderException( + StringUtil.simpleClassName(this) + " must produce at least one message."); + } + } else { + ctx.write(msg, promise); + } + } catch (EncoderException e) { + throw e; + } catch (Throwable t) { + throw new EncoderException(t); + } finally { + if (out != null) { + try { + final int sizeMinusOne = out.size() - 1; + if (sizeMinusOne == 0) { + ctx.write(out.getUnsafe(0), promise); + } else if (sizeMinusOne > 0) { + if (promise == ctx.voidPromise()) { + writeVoidPromise(ctx, out); + } else { + writePromiseCombiner(ctx, out, promise); + } + } + } finally { + out.recycle(); + } + } + } + } +``` + +write方法接受一个需要转换的原始对象msg,和一个表示channel读写进度的ChannelPromise。 + +首先会对msg进行一个类型判断,这个判断方法是在acceptOutboundMessage中实现的。 + +``` + public boolean acceptOutboundMessage(Object msg) throws Exception { + return matcher.match(msg); + } +``` + +这里的matcher是一个TypeParameterMatcher对象,它是一个在MessageToMessageEncoder构造函数中初始化的属性: + +``` + protected MessageToMessageEncoder() { + matcher = TypeParameterMatcher.find(this, MessageToMessageEncoder.class, "I"); + } +``` + +这里的I就是要匹配的msg类型。 + +如果不匹配,则继续调用`ctx.write(msg, promise);`将消息不做任何转换的写入到channel中,供下一个handler调用。 + +如果匹配成功,则会调用核心的encode方法:`encode(ctx, cast, out);` + +注意,encode方法在MessageToMessageEncoder中是一个抽象方法,需要用户在继承类中自行扩展。 + +encode方法实际上是将msg对象转换成为要转换的对象,然后添加到out中。这个out是一个list对象,具体而言是一个CodecOutputList对象,作为一个list,out是一个可以存储多个对象的列表。 + +那么out是什么时候写入到channel中去的呢? + +别急,在write方法中最后有一个finally代码块,在这个代码块中,会将out写入到channel里面。 + +因为out是一个List,可能会出现out中的对象部分写成功的情况,所以这里需要特别处理。 + +首先判断out中是否只有一个对象,如果是一个对象,那么直接写到channel中即可。如果out中多于一个对象,那么又分成两种情况,第一种情况是传入的promise是一个voidPromise,那么调用writeVoidPromise方法。 + +什么是voidPromise呢? + +我们知道Promise有多种状态,可以通过promise的状态变化了解到数据写入的情况。对于voidPromise来说,它只关心一种失败的状态,其他的状态都不关心。 + +如果用户关心promise的其他状态,则会调用writePromiseCombiner方法,将多个对象的状态合并为一个promise返回。 + +事实上,在writeVoidPromise和writePromiseCombiner中,out中的对象都是一个一个的取出来,写入到channel中的,所以才会生成多个promise和需要将promise进行合并的情况: + +``` + private static void writeVoidPromise(ChannelHandlerContext ctx, CodecOutputList out) { + final ChannelPromise voidPromise = ctx.voidPromise(); + for (int i = 0; i < out.size(); i++) { + ctx.write(out.getUnsafe(i), voidPromise); + } + } + + private static void writePromiseCombiner(ChannelHandlerContext ctx, CodecOutputList out, ChannelPromise promise) { + final PromiseCombiner combiner = new PromiseCombiner(ctx.executor()); + for (int i = 0; i < out.size(); i++) { + combiner.add(ctx.write(out.getUnsafe(i))); + } + combiner.finish(promise); + } +``` + +# MessageToMessageDecoder + +和encoder对应的就是decoder了,MessageToMessageDecoder的逻辑和MessageToMessageEncoder差不多。 + +首先也是需要判断读取的消息类型,这里也定义了一个TypeParameterMatcher对象,用来检测传入的消息类型: + +``` + protected MessageToMessageDecoder() { + matcher = TypeParameterMatcher.find(this, MessageToMessageDecoder.class, "I"); + } +``` + +decoder中重要的方法是channelRead方法,我们看下它的实现: + +``` + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + CodecOutputList out = CodecOutputList.newInstance(); + try { + if (acceptInboundMessage(msg)) { + @SuppressWarnings("unchecked") + I cast = (I) msg; + try { + decode(ctx, cast, out); + } finally { + ReferenceCountUtil.release(cast); + } + } else { + out.add(msg); + } + } catch (DecoderException e) { + throw e; + } catch (Exception e) { + throw new DecoderException(e); + } finally { + try { + int size = out.size(); + for (int i = 0; i < size; i++) { + ctx.fireChannelRead(out.getUnsafe(i)); + } + } finally { + out.recycle(); + } + } + } +``` + +首先检测msg的类型,只有接受的类型才进行decode处理,否则将msg加入到CodecOutputList中。 + +最后在finally代码块中将out中的对象一个个取出来,调用ctx.fireChannelRead进行读取。 + +消息转换的关键方法是decode,这个方法也是一个抽象方法,需要在继承类中实现具体的功能。 + +# MessageToMessageCodec + +前面讲解了一个编码器和一个解码器,他们都是单向的。最后要讲解的codec叫做MessageToMessageCodec,这个codec是一个双向的,即可以接收消息,也可以发送消息。 + +先看下它的定义: + +``` +public abstract class MessageToMessageCodec extends ChannelDuplexHandler +``` + +MessageToMessageCodec继承自ChannelDuplexHandler,接收两个泛型参数分别是INBOUND_IN和OUTBOUND_IN。 + +它定义了两个TypeParameterMatcher,分别用来过滤inboundMsg和outboundMsg: + +``` + protected MessageToMessageCodec() { + inboundMsgMatcher = TypeParameterMatcher.find(this, MessageToMessageCodec.class, "INBOUND_IN"); + outboundMsgMatcher = TypeParameterMatcher.find(this, MessageToMessageCodec.class, "OUTBOUND_IN"); + } +``` + +分别实现了channelRead和write方法,用来读写消息: + +``` + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + decoder.channelRead(ctx, msg); + } + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + encoder.write(ctx, msg, promise); + } +``` + +这里的decoder和encoder实际上就是前面我们讲到的MessageToMessageDecoder和MessageToMessageEncoder: + +``` + private final MessageToMessageEncoder encoder = new MessageToMessageEncoder() { + + @Override + public boolean acceptOutboundMessage(Object msg) throws Exception { + return MessageToMessageCodec.this.acceptOutboundMessage(msg); + } + + @Override + @SuppressWarnings("unchecked") + protected void encode(ChannelHandlerContext ctx, Object msg, List out) throws Exception { + MessageToMessageCodec.this.encode(ctx, (OUTBOUND_IN) msg, out); + } + }; + + private final MessageToMessageDecoder decoder = new MessageToMessageDecoder() { + + @Override + public boolean acceptInboundMessage(Object msg) throws Exception { + return MessageToMessageCodec.this.acceptInboundMessage(msg); + } + + @Override + @SuppressWarnings("unchecked") + protected void decode(ChannelHandlerContext ctx, Object msg, List out) throws Exception { + MessageToMessageCodec.this.decode(ctx, (INBOUND_IN) msg, out); + } + }; +``` + +可以看到MessageToMessageCodec实际上就是对MessageToMessageDecoder和MessageToMessageEncoder的封装,如果需要对MessageToMessageCodec进行扩展的话,需要实现下面两个方法: + +``` + protected abstract void encode(ChannelHandlerContext ctx, OUTBOUND_IN msg, List out) + throws Exception; + + protected abstract void decode(ChannelHandlerContext ctx, INBOUND_IN msg, List out) + throws Exception; +``` + +# 总结 + +netty中提供的MessageToMessage的编码框架是后面对编码解码器进行扩展的基础。只有深入了解其中的原理,我们对于新的编码解码器运用起来才能得心应手。 \ No newline at end of file diff --git a/learn-netty文章/62.netty系列之netty中的核心MessageToByte编码器.md b/learn-netty文章/62.netty系列之netty中的核心MessageToByte编码器.md new file mode 100644 index 0000000..529cb64 --- /dev/null +++ b/learn-netty文章/62.netty系列之netty中的核心MessageToByte编码器.md @@ -0,0 +1,189 @@ +# netty系列之:netty中的核心MessageToByte编码器 + +文章目录 + + + +[简介](http://www.flydean.com/14-0-2-netty-codec-msg-to-bytebuf/#简介)[MessageToByte框架简介](http://www.flydean.com/14-0-2-netty-codec-msg-to-bytebuf/#MessageToByte框架简介)[MessageToByteEncoder](http://www.flydean.com/14-0-2-netty-codec-msg-to-bytebuf/#MessageToByteEncoder)[ByteToMessageDecoder](http://www.flydean.com/14-0-2-netty-codec-msg-to-bytebuf/#ByteToMessageDecoder)[ByteToMessageCodec](http://www.flydean.com/14-0-2-netty-codec-msg-to-bytebuf/#ByteToMessageCodec)[总结](http://www.flydean.com/14-0-2-netty-codec-msg-to-bytebuf/#总结) + +# 简介 + +之前的文章中,我们讲解了netty中从一个message转换成为另外一个message的框架叫做MessageToMessage编码器。但是message to message只考虑了channel中消息在处理过程中的转换,但是我们知道channel中最终传输的数据一定是ByteBuf,所以我们还需要一个message和ByteBuf相互转换的框架,这个框架就叫做MessageToByte。 + +注意,这里的byte指的是ByteBuf而不是byte这个字节类型。 + +# MessageToByte框架简介 + +为了方便扩展和用户的自定义,netty封装了一套MessageToByte框架,这个框架中有三个核心的类,分别是MessageToByteEncoder,ByteToMessageDecoder和ByteToMessageCodec。 + +我们分别看一下这三个核心类的定义: + +``` +public abstract class MessageToByteEncoder extends ChannelOutboundHandlerAdapter +public abstract class ByteToMessageDecoder extends ChannelInboundHandlerAdapter +public abstract class ByteToMessageCodec extends ChannelDuplexHandler +``` + +这三个类分别继承自ChannelOutboundHandlerAdapter,ChannelInboundHandlerAdapter和ChannelDuplexHandler,分别表示的是向channel中写消息,从channel中读消息和一个向channel中读写消息的双向操作。 + +这三个类都是抽象类,接下来我们会详细分析这三个类的具体实现逻辑。 + +# MessageToByteEncoder + +先来看encoder,如果你对比MessageToByteEncoder和MessageToMessageEncoder的源码实现,可以发现他们有诸多相似之处。 + +首先在MessageToByteEncoder中定义了一个用作消息类型匹配的TypeParameterMatcher。 + +这个matcher用来匹配收到的消息类型,如果类型匹配则进行消息的转换操作,否则直接将消息写入channel中。 + +和MessageToMessageEncoder不同的是,MessageToByteEncoder多了一个preferDirect字段,这个字段表示消息转换成为ByteBuf的时候是使用diret Buf还是heap Buf。 + +这个字段的使用情况如下: + +``` + protected ByteBuf allocateBuffer(ChannelHandlerContext ctx, @SuppressWarnings("unused") I msg, + boolean preferDirect) throws Exception { + if (preferDirect) { + return ctx.alloc().ioBuffer(); + } else { + return ctx.alloc().heapBuffer(); + } + } +``` + +最后来看一下它的核心方法write: + +``` + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + ByteBuf buf = null; + try { + if (acceptOutboundMessage(msg)) { + @SuppressWarnings("unchecked") + I cast = (I) msg; + buf = allocateBuffer(ctx, cast, preferDirect); + try { + encode(ctx, cast, buf); + } finally { + ReferenceCountUtil.release(cast); + } + + if (buf.isReadable()) { + ctx.write(buf, promise); + } else { + buf.release(); + ctx.write(Unpooled.EMPTY_BUFFER, promise); + } + buf = null; + } else { + ctx.write(msg, promise); + } + } catch (EncoderException e) { + throw e; + } catch (Throwable e) { + throw new EncoderException(e); + } finally { + if (buf != null) { + buf.release(); + } + } + } +``` + +上面我们已经提到了,write方法首先通过matcher来判断是否是要接受的消息类型,如果是的话就调用encode方法,将消息对象转换成为ByteBuf,如果不是,则直接将消息写入channel中。 + +和MessageToMessageEncoder不同的是,encode方法需要传入一个ByteBuf对象,而不是CodecOutputList。 + +MessageToByteEncoder有一个需要实现的抽象方法encode如下, + +``` + protected abstract void encode(ChannelHandlerContext ctx, I msg, ByteBuf out) throws Exception; +``` + +# ByteToMessageDecoder + +ByteToMessageDecoder用来将channel中的ByteBuf消息转换成为特定的消息类型,其中Decoder中最重要的方法就是好channelRead方法,如下所示: + +``` + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (msg instanceof ByteBuf) { + CodecOutputList out = CodecOutputList.newInstance(); + try { + first = cumulation == null; + cumulation = cumulator.cumulate(ctx.alloc(), + first ? Unpooled.EMPTY_BUFFER : cumulation, (ByteBuf) msg); + callDecode(ctx, cumulation, out); + } catch (DecoderException e) { + throw e; + } catch (Exception e) { + throw new DecoderException(e); + } finally { + try { + if (cumulation != null && !cumulation.isReadable()) { + numReads = 0; + cumulation.release(); + cumulation = null; + } else if (++numReads >= discardAfterReads) { + numReads = 0; + discardSomeReadBytes(); + } + + int size = out.size(); + firedChannelRead |= out.insertSinceRecycled(); + fireChannelRead(ctx, out, size); + } finally { + out.recycle(); + } + } + } else { + ctx.fireChannelRead(msg); + } + } +``` + +channelRead接收要进行消息读取的Object对象,因为这里只接受ByteBuf消息,所以在方法内部调用了`msg instanceof ByteBuf`来判断消息的类型,如果不是ByteBuf类型的消息则不进行消息的转换。 + +输出的对象是CodecOutputList,在将ByteBuf转换成为CodecOutputList之后,调用fireChannelRead方法将out对象传递下去。 + +这里的关键就是如何将接收到的ByteBuf转换成为CodecOutputList。 + +转换的方法叫做callDecode,它接收一个叫做cumulation的参数,在上面的方法中,我们还看到一个和cumulation非常类似的名称叫做cumulator。那么他们两个有什么区别呢? + +在ByteToMessageDecoder中cumulation是一个ByteBuf对象,而Cumulator是一个接口,这个接口定义了一个cumulate方法: + +``` + public interface Cumulator { + ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in); + } +``` + +Cumulator用来将传入的ByteBuf合并成为一个新的ByteBuf。 + +ByteToMessageDecoder中定义了两种Cumulator,分别是MERGE_CUMULATOR和COMPOSITE_CUMULATOR。 + +MERGE_CUMULATOR是将传入的ByteBuf通过memory copy的方式拷贝到目标ByteBuf cumulation中。 + +而COMPOSITE_CUMULATOR则是将ByteBuf添加到一个 CompositeByteBuf 的结构中,并不做memory copy,因为目标的结构比较复杂,所以速度会比直接进行memory copy要慢。 + +用户要扩展的方法就是decode方法,用来将一个ByteBuf转换成为其他对象: + +``` + protected abstract void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception; +``` + +# ByteToMessageCodec + +最后要介绍的类是ByteToMessageCodec,ByteToMessageCodec表示的是message和ByteBuf之间的互相转换,它里面的encoder和decoder分别就是上面讲到的MessageToByteEncoder和ByteToMessageDecoder。 + +用户可以继承ByteToMessageCodec来同时实现encode和decode的功能,所以需要实现encode和decode这两个方法: + +``` + protected abstract void encode(ChannelHandlerContext ctx, I msg, ByteBuf out) throws Exception; + + protected abstract void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception; +``` + +ByteToMessageCodec的本质就是封装了MessageToByteEncoder和ByteToMessageDecoder,然后实现了编码和解码的功能。 + +# 总结 + +如果想实现ByteBuf和用户自定义消息的直接转换,那么选择netty提供的上面三个编码器是一个很好的选择。 \ No newline at end of file diff --git a/learn-netty文章/63.netty系列之netty中的核心编码器base64.md b/learn-netty文章/63.netty系列之netty中的核心编码器base64.md new file mode 100644 index 0000000..3a688e0 --- /dev/null +++ b/learn-netty文章/63.netty系列之netty中的核心编码器base64.md @@ -0,0 +1,195 @@ +# netty系列之:netty中的核心编码器base64 + +文章目录 + + + +[简介](http://www.flydean.com/14-1-netty-codec-base64/#简介)[netty codec的实现逻辑](http://www.flydean.com/14-1-netty-codec-base64/#netty_codec的实现逻辑)[netty中Base64的实现](http://www.flydean.com/14-1-netty-codec-base64/#netty中Base64的实现)[netty中的base64编码和解码器](http://www.flydean.com/14-1-netty-codec-base64/#netty中的base64编码和解码器)[总结](http://www.flydean.com/14-1-netty-codec-base64/#总结) + +# 简介 + +我们知道数据在netty中传输是以ByteBuf的形式进行的,可以说ByteBuf是netty的数据传输基础。但是对于现代的应用程序来说,通常我们需要用到其他的数据结构或者类型。 + +为了方便我们在程序中的编写,一种方式就是在将数据传入到netty中的时候由程序员自身将数据格式进行转换,然后再调用netty的系统方法。另外一种方式就是定义一些codec,由netty的内在编码机制将程序中用到的数据格式和ByteBuf进行自动转换。 + +很明显,使用codec的方式更加简捷,也更加符合程序的开发规则。 + +为了方便程序的开发,netty本身在内部定义了一些核心的codec插件,我们在需要的时候直接选用即可。 + +本文将会讲解netty内部实现codec的方式和一个最核心的编码器base64。 + +# netty codec的实现逻辑 + +所有的netty codec的目的就是在数据传输过程中进行数据类型的转换,换句话说就是对数据进行处理。我们知道netty中有两个对数据进行handler的类,分别是ChannelInboundHandlerAdapter和ChannelOutboundHandlerAdapter,他们分别对应channel中的inbound消息和outbound消息进行处理。 + +所以很自然的,我们的codec逻辑只需要在这两个地方添加即可。 + +netty为我们提供了两个HandlerAdapter类的继承类,分别是MessageToMessageDecoder和MessageToMessageEncoder: + +``` +public abstract class MessageToMessageEncoder extends ChannelOutboundHandlerAdapter + +public abstract class MessageToMessageDecoder extends ChannelInboundHandlerAdapter +``` + +从名字就可以看出来这两个类分别使用来编码和解码用的,所以我们的codec只需要分别实现这两个类即可。 + +以下是一个StringToIntegerDecoder和IntegerToStringEncoder的例子: + +``` + public class StringToIntegerDecoder extends + MessageToMessageDecoder { + + @Override + public void decode(ChannelHandlerContext ctx, String message, + List out) throws Exception { + out.add(message.length()); + } + } + public class IntegerToStringEncoder extends + MessageToMessageEncoder { + + @Override + public void encode(ChannelHandlerContext ctx, Integer message, List out) + throws Exception { + out.add(message.toString()); + } + } +``` + +最简单的实现就是分别重构这两个类的decode和encode方法。 + +# netty中Base64的实现 + +我们知道JDK中已经有了Base64实现的工具类叫做java.util.Base64。但是在netty中又使用了一个新的实现类同样叫做Base64,它的全称是io.netty.handler.codec.base64.Base64。 + +这个Base64类中用到了一个Base64Dialect类,也就是netty中Base64支持的Base64编码方式。Base64Dialect中提供了下面的几种类型: + +``` +STANDARD +URL_SAFE +ORDERED +``` + +其中STANDARD对应的是RFC3548也是JDK中的标准Base64,URL_SAFE对应的是RFC3548中的base64url版本,对应的JDK中的getUrlEncode。 + +最后一个是ORDERED,代表的是RFC1940,这个编码实现在JDK中是没有的。 + +为什么JDK中已经有了Base64的工具类,netty中还需要自己创建一个新的类呢? + +我们可以考虑一下在netty中Base64用到的场景,通常来说我们是在handler中添加自定义编码,而这些handler主要是针对于数据流进行处理。 + +JDK中自带的Base64实现在定长的数据上使用还是没问题的,但是如果运用于数据流的处理话,效率就会比较低。所以Netty才需要为base64在流数据的情况下重新实现一个Base64类。 + +netty中的实现方式使用的是Robert Harder’s Public Domain Base64 Encoder/Decoder。这里就不多讲这个算法的实现逻辑了。感兴趣的朋友可以自行探索。 + +Base64提供了将ByteBuf进行base64编码和解码的方法,我们选择参数最长的方法来观察,如下所示: + +``` + public static ByteBuf encode( + ByteBuf src, int off, int len, boolean breakLines, Base64Dialect dialect, ByteBufAllocator allocator) + + public static ByteBuf decode( + ByteBuf src, int off, int len, Base64Dialect dialect, ByteBufAllocator allocator) +``` + +对于encode方法来说,需要下面几个参数: + +1. ByteBuf类型的src,这是我们需要进行编码的源。 +2. int类型的off和len,表示的是ByteBuf中要编码数据的位置。 +3. boolean类型的breakLines,表示是否添加换行符。 +4. Base64Dialect类型的dialect,表示选择的base64编码类型。 +5. ByteBufAllocator的allocator,表示返回的ByteBuf的生成方式。 + +同样的Decode方法,需要下面的几个参数: + +1. ByteBuf类型的src,这是我们需要进行解码的源。 +2. int类型的off和len,表示的是ByteBuf中要解码数据的位置。 +3. Base64Dialect类型的dialect,表示选择的base64编码类型。 +4. ByteBufAllocator的allocator,表示返回的ByteBuf的生成方式。 + +# netty中的base64编码和解码器 + +刚刚我们介绍了netty中提供的新的Base64工具类,这个工具类提供了将ByteBuf中数据进行编码和解码的方法。接下来我们看一下netty是如何使用这个工具类实现netty中的base64编码和解码器。 + +netty中提供了对Base64的编码和解码器,分别是Base64Encoder和Base64Decoder, 先来看下Base64编码解码器的基本使用: + +``` + ChannelPipeline pipeline = ...; + + // Decoders + pipeline.addLast("frameDecoder", new DelimiterBasedFrameDecoder(80, Delimiters.nulDelimiter())); + pipeline.addLast("base64Decoder", new Base64Decoder()); + + // Encoder + pipeline.addLast("base64Encoder", new Base64Encoder()); +``` + +用起来很简单,只需要把Base64Decoder和Base64Encoder添加到pipeline中即可。 + +有时候Base64Decoder需要和DelimiterBasedFrameDecoder一起使用,尤其是在TCP/IP协议中,因为我们需要根据特定的Delimiters来判断ByteBuf应该被分割为几个frames。这样才能保证数据的有效性。 + +## Base64Encoder + +首先来看base64的编码器,Base64Encoder的实现比较简单,首先来看下Base64Encoder的定义: + +``` +public class Base64Encoder extends MessageToMessageEncoder +``` + +Base64Encoder继承自MessageToMessageEncoder,它传入的泛型ByteBuf,表示是将ByteBuf编码为ByteBuf,虽然外部的ByteBuf类型没有变化,但是ByteBuf中的数据已经被编码成为Base64了。 + +接下来是Base64Encoder的构造函数: + +``` + public Base64Encoder(boolean breakLines, Base64Dialect dialect) { + this.dialect = ObjectUtil.checkNotNull(dialect, "dialect"); + this.breakLines = breakLines; + } +``` + +Base64Encoder可以接受两个参数,分别是是否有换行符的breakLines和base64编码方式的Base64Dialect。 + +它的encode方法也很简单: + +``` + protected void encode(ChannelHandlerContext ctx, ByteBuf msg, List out) throws Exception { + out.add(Base64.encode(msg, msg.readerIndex(), msg.readableBytes(), breakLines, dialect)); + } +``` + +直接使用的是我们上面讲到的Base64工具类的encode方法,然后把返回值添加到out对象中。 + +## Base64Decoder + +Base64Decoder用来将ByteBuf中的base64编码的内容解码成为原始内容,先来看下Base64Decoder的定义: + +``` +public class Base64Decoder extends MessageToMessageDecoder +``` + +Base64Decoder继承了MessageToMessageDecoder,传入的泛型是ByteBuf。 + +先看下Base64Decoder的构造函数: + +``` +public Base64Decoder(Base64Dialect dialect) { + this.dialect = ObjectUtil.checkNotNull(dialect, "dialect"); + } +``` + +Base64Decoder的构造函数很简单,和Base64Encoder相比它只需要一个参数就是Base64Dialect类型的dialect,表示的是选择的base64解码的方式。 + +接下来就是它的解码方法: + +``` + protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List out) throws Exception { + out.add(Base64.decode(msg, msg.readerIndex(), msg.readableBytes(), dialect)); + } +``` + +解码方法也是调用Base64工具类的decode方法,然后将其添加到返回的out list中去。 + +# 总结 + +本章介绍了netty中的核心编码器Base64,它负责将ByteBuf中的消息编码为base64格式,同时提供了对应的解码器,大家可以在需要的时候进行使用。 \ No newline at end of file diff --git a/learn-netty文章/64.netty系列之netty中的核心编码器bytes数组.md b/learn-netty文章/64.netty系列之netty中的核心编码器bytes数组.md new file mode 100644 index 0000000..d6fcb98 --- /dev/null +++ b/learn-netty文章/64.netty系列之netty中的核心编码器bytes数组.md @@ -0,0 +1,206 @@ +# netty系列之:netty中的核心编码器bytes数组 + +文章目录 + + + +[简介](http://www.flydean.com/14-2-netty-codec-bytes/#简介)[byte是什么](http://www.flydean.com/14-2-netty-codec-bytes/#byte是什么)[netty中的byte数组的工具类](http://www.flydean.com/14-2-netty-codec-bytes/#netty中的byte数组的工具类)[netty中byte的编码器](http://www.flydean.com/14-2-netty-codec-bytes/#netty中byte的编码器)[总结](http://www.flydean.com/14-2-netty-codec-bytes/#总结) + +# 简介 + +我们知道netty中数据传输的核心是ByteBuf,ByteBuf提供了多种数据读写的方法,包括基本类型和byte数组的读写方法。如果要在netty中传输这些数据,那么需要构建ByteBuf,然后调用ByteBuf中对应的方法写入对应的数据,接着套用netty中标准的模板即可使用。 + +对于byte数组来说,如果每次都将其封装进ByteBuf中,再进行传输显得有些麻烦。于是netty提供了一个基于bytes的核心编码解码器。 + +# byte是什么 + +那么byte是什么呢? byte表示的是一个字节,也就是8bits。用二进制表示就是-128-127的范围。byte是JAVA中的基础类型。 + +同时它还有一个wrap类型叫做Byte。 + +先看下Byte的定义: + +``` +public final class Byte extends Number implements Comparable +``` + +Byte中定义了byte的取值访问: + +``` + public static final byte MIN_VALUE = -128; + + public static final byte MAX_VALUE = 127; +``` + +并且还提供了一些基本的工具方法。 + +因为byte表示的是一个8bits的二进制,如果不算位运算的话,byte基本上是JAVA中最小的数据存储单位了。所以JAVA中所有的对象都可以转换成为byte。 + +基础类型的转换这里就不多讲了。这里主要看一下字符串String和对象Object和byte数组之间的转换。 + +先来看下字符串String和byte数组之间的转换,也就是String和二进制之间的转换。 + +基本的转换思路就是将String中的字符进行编码,然后将编码过后的字符进行存储即可。 + +String类本身提供了一个getBytes方法,可以接受编码类型,以UTF-8来说,我们来看下转换方法的调用: + +``` + public static byte[] stringToBytes(String str) throws UnsupportedEncodingException { + return str.getBytes("utf-8"); + } + + public static String bytesToString(byte[] bs) throws UnsupportedEncodingException { + return new String(bs, "utf-8"); + } +``` + +直接调用String中的方法即可。 + +如果是Object对象的话,因为Object本身并没有提供转换的方法,所以我们需要借助于ByteArrayOutputStream的toByteArray方法和ByteArrayInputStream的readObject方法来实现byte数组和Object之间的转换,如下所示: + +``` + //对象转数组 + public byte[] toByteArray (Object obj) throws IOException { + try(ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(bos)) { + oos.writeObject(obj); + oos.flush(); + return bos.toByteArray(); + } + } + + //数组转对象 + public Object toObject (byte[] bytes) throws IOException, ClassNotFoundException { + try ( + ByteArrayInputStream bis = new ByteArrayInputStream (bytes); + ObjectInputStream ois = new ObjectInputStream (bis)) { + return ois.readObject(); + } + } +``` + +# netty中的byte数组的工具类 + +netty中的核心是ByteBuf,ByteBuf提供了大部分基础数据类型的read和write方法。当然如果要读取对象,那么还是需要将对象转换成为byte然后再写入或者从ByteBuf中读出。 + +当然,netty中不需要这么复杂,netty提供了一个Unpooled的工具类用来方便的将byte数组和ByteBuf进行转换。 + +先看下Unpooled方法提供的ByteBuff构建方法: + +``` + ByteBuf heapBuffer = buffer(128); + ByteBuf directBuffer = directBuffer(256); + ByteBuf wrappedBuffer = wrappedBuffer(new byte[128], new byte[256]); + ByteBuf copiedBuffer = copiedBuffer(ByteBuffer.allocate(128)); +``` + +这是Unpooled提供的几种ByteBuf的构建方式,其中heapBuffer表示的是在用户空间构建的buff,directBuffer表示的是直接在系统空间构建的buff。wrappedBuffer是对现有的byte数组和ByteBuf之上构建的视图,而copiedBuffer是对byte数组,byteBuf和字符串的拷贝。 + +这里我们需要用到wrappedBuffer方法,将byte数组封装到ByteBuf中: + +``` + public static ByteBuf wrappedBuffer(byte[] array) { + if (array.length == 0) { + return EMPTY_BUFFER; + } + return new UnpooledHeapByteBuf(ALLOC, array, array.length); + } +``` + +wrappedBuffer返回了一个UnpooledHeapByteBuf对象,这个对象本身就是一个ByteBuf。这里将byte数组作为构造函数传入UnpooledHeapByteBuf中。 + +这里的array是UnpooledHeapByteBuf中的私有变量: + +``` +byte[] array; +``` + +除了构造函数,UnpooledHeapByteBuf还提供了一个setArray的方法用来设置byte数组: + +``` + private void setArray(byte[] initialArray) { + array = initialArray; + tmpNioBuf = null; + } +``` + +下面是如何从array中构建ByteBuf: + +``` + public ByteBuf setBytes(int index, ByteBuffer src) { + ensureAccessible(); + src.get(array, index, src.remaining()); + return this; + } +``` + +从ByteBuf中读取byte数组,可以调用ByteBufUtil的getBytes方法: + +``` + public static byte[] getBytes(ByteBuf buf) { + return getBytes(buf, buf.readerIndex(), buf.readableBytes()); + } +``` + +# netty中byte的编码器 + +万事俱备只欠东风,有了上面netty提供的工具类,我们就可以使用这些工具类构建基于byte的编码器了。 + +netty中基于byte的编码解码器分别叫做ByteArrayEncoder和ByteArrayDecoder。 + +先来看下这两个类是如何使用的,这里以一个典型的TCP/IP应用为例: + +``` + ChannelPipeline pipeline = ...; + + // Decoders + pipeline.addLast("frameDecoder", + new LengthFieldBasedFrameDecoder(1048576, 0, 4, 0, 4)); + pipeline.addLast("bytesDecoder", + new ByteArrayDecoder()); + + // Encoder + pipeline.addLast("frameEncoder", new LengthFieldPrepender(4)); + pipeline.addLast("bytesEncoder", new ByteArrayEncoder()); +``` + +这里的LengthFieldBasedFrameDecoder和LengthFieldPrepender是以消息长度为分割标准的frame分割器。这里我们主要关注ChannelPipeline中添加的ByteArrayDecoder和ByteArrayEncoder。 + +添加了byte的编码和解码器之后,就可以直接在handler中直接使用byte数组,如下所示: + +``` + void channelRead(ChannelHandlerContext ctx, byte[] bytes) { + ... + } +``` + +先来看下ByteArrayEncoder,这是一个编码器,它的实现很简单: + +``` +public class ByteArrayEncoder extends MessageToMessageEncoder { + @Override + protected void encode(ChannelHandlerContext ctx, byte[] msg, List out) throws Exception { + out.add(Unpooled.wrappedBuffer(msg)); + } +} +``` + +具体就是使用Unpooled.wrappedBuffer方法byte数组封装成为ByteBuf,然后将其添加到out list中。 + +同样的,我们观察一下ByteArrayDecoder,这是一个解码器,实现也比较简单: + +``` +public class ByteArrayDecoder extends MessageToMessageDecoder { + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List out) throws Exception { + // copy the ByteBuf content to a byte array + out.add(ByteBufUtil.getBytes(msg)); + } +} +``` + +具体的实现就是调用ByteBufUtil.getBytes方法,将ByteBuf转换成为byte数组,然后添加到list对象中。 + +# 总结 + +如果要在netty中传输二进制数据,netty提供的byte编码和解码器已经封装了繁琐的细节,大家可以放心使用。 \ No newline at end of file diff --git a/learn-netty文章/65.netty系列之netty中的核心解码器json.md b/learn-netty文章/65.netty系列之netty中的核心解码器json.md new file mode 100644 index 0000000..4082d8a --- /dev/null +++ b/learn-netty文章/65.netty系列之netty中的核心解码器json.md @@ -0,0 +1,188 @@ +# netty系列之:netty中的核心解码器json + +文章目录 + + + +[简介](http://www.flydean.com/14-3-netty-codec-json/#简介)[java中对json的支持](http://www.flydean.com/14-3-netty-codec-json/#java中对json的支持)[netty对json的解码](http://www.flydean.com/14-3-netty-codec-json/#netty对json的解码)[总结](http://www.flydean.com/14-3-netty-codec-json/#总结) + +# 简介 + +程序和程序之间的数据传输方式有很多,可以通过二进制协议来传输,比较流行的像是thrift协议或者google的protobuf。这些二进制协议可以实现数据的有效传输,并且通过二进制的形式可以节省数据的体积,在某些速度和效率优先的情况下是非常有效的。并且如果不同的编程语言之间的相互调用,也可以通过这种二进制的协议来实现。 + +虽然二进制更加快速和有效,但是对于程序员来说不是很友好,因为一个人很难直接读取二进制文件,虽然也存在一些一些文本的数据传输方式,比如XML,但是XML的繁琐的标签导致了XML在使用中有诸多的不便。于是一种通用的文本文件传输格式json诞生了。 + +能读到这篇文章的朋友肯定对json不陌生了,当然还有一些更加简洁的文件格式,比如YAML,感兴趣的朋友可以更深入的了解一下。 + +这里我们想要讲的是netty对json的解码。 + +# java中对json的支持 + +在java中我们json的使用通常是将一个对象转换成为json进行数据传输,或者将接收到json进行解析,将其转换成为对象。 + +可惜的是在JDK中并没有提供给一个好用的JSON工具,所以我们一般需要借助第三方的JSON包来实现Object和JSON之间的转换工作。 + +通常使用的有google的GSON,阿里的FastJSON和jackson等。 + +这里我们使用google的GSON来进行介绍。 + +这里我们主要讲解的是java中对象和json的互相转换,所以GSON中其他更加强大的功能这里就不介绍了。 + +首先我们创建一个JAVA对象,我们定义一个Student类,如下所示: + +``` + static class Student { + String name; + String phone; + Integer age; + + public Student(String name, String phone, Integer age) { + this.name = name; + this.phone = phone; + this.age = age; + } + } +``` + +这个类中,我们为Student定义了几个不同的属性和一个构造函数。接下来我们看下如何使用GSON来对这个对象进行JSON的转换: + +``` + Student obj = new Student("tina","188888888",18); + Gson gson = new Gson(); + String json = gson.toJson(obj); + System.out.println(json); + Student obj2 = gson.fromJson(json, Student.class); + System.out.println(obj2); +``` + +GSON使用起来非常简单,我们构建好Gson对象之后,直接调用它的toJson方法即可将对象转换成为json字符串。 + +然后调用json的fromJson方法就可以将json字符串转换成为对象。 + +上面的代码输出如下: + +``` +{"name":"tina","phone":"188888888","age":18} +com.flydean.JsonTest$Student@4534b60d +``` + +# netty对json的解码 + +netty为json提供了一个解码器叫做JsonObjectDecoder,先来看下JsonObjectDecoder的定义: + +``` +public class JsonObjectDecoder extends ByteToMessageDecoder +``` + +和前面讲解的base64,byte数组不同的是,JsonObjectDecoder继承的是ByteToMessageDecoder而不是MessageToMessageDecoder。 + +这说明JsonObjectDecoder是直接从ByteBuf转换成为Json Object对象。 + +我们知道JDK中并没有JSON这个对象,所有的对象都是从第三方包中引入的,netty并没有引入新的对象,所以netty中从Json中解析出来的对象还是一个ByteBuf对象,在这个ByteBuf中包含了一个Json对象。 + +JsonObjectDecoder的解析逻辑是怎么样的呢? + +首先来看下JsonObjectDecoder中定义的4个state: + +``` + private static final int ST_CORRUPTED = -1; + private static final int ST_INIT = 0; + private static final int ST_DECODING_NORMAL = 1; + private static final int ST_DECODING_ARRAY_STREAM = 2; +``` + +ST_INIT表示的是decode的初始状态,ST_CORRUPTED表示的是decode中出现的异常状态。 + +ST_DECODING_NORMAL代表的是一个普通的json,如下所示: + +``` +{ + "source": "web", + "type": "product_info", + "time": 1641967014440, + "data": { + "id": 30000084318055, + "staging": false + }, + "dataId": "123456" +} +``` + +ST_DECODING_ARRAY_STREAM代表的是一个数组,对于数组来说,数组也是一个对象,所以数组也可以用json表示,下面就是一个常见的json数组: + +``` +[ "Google", "Runoob", "Taobao" ] +``` + +JsonObjectDecoder的解码逻辑比较简单,它主要是读取ByteBuf中的数据,通过判断读取的数据和json中特有的大括号,中括号,逗号等分隔符来分割和解析json对象。 + +要注意的是,JsonObjectDecoder要解码的ByteBuf中的消息应该是UTF-8编码格式的,为什么需要UTF-8格式呢? + +这是因为json中那些特有的分隔符,即使在UTF-8中也是用一个byte来存储的,这样我们在读取数据的过程中,可以通过读取的byte值和json的分隔符进行比较,从而来确定json中不同对象的界限。 + +如果换成其他的编码方式,json中的分隔符可能会用多个byte来表示,这样对我们的解析就提高了难度,因为我们需要知道什么时候是分隔符的开始,什么时候是分隔符的结束。 + +它的核心解码逻辑如下,首先从ByteBuf中读取一个byte: + +``` +byte c = in.getByte(idx); +``` + +然后通过调用`decodeByte(c, in, idx);`来判断当前的位置是开括号,还是闭括号,是在一个对象的字符串中,还是一个新的对象字符串。 + +首先需要对当前的state做一个判断,state判断调用的是initDecoding方法: + +``` + private void initDecoding(byte openingBrace) { + openBraces = 1; + if (openingBrace == '[' && streamArrayElements) { + state = ST_DECODING_ARRAY_STREAM; + } else { + state = ST_DECODING_NORMAL; + } + } +``` + +接着就是对当前的state和自定义的4个状态进行比较,如果是普通的json对象,并且对象已经是闭括号状态,说明该对象已经读取完成,可以将其进行转换并输出了: + +``` + if (state == ST_DECODING_NORMAL) { + decodeByte(c, in, idx); + if (openBraces == 0) { + ByteBuf json = extractObject(ctx, in, in.readerIndex(), idx + 1 - in.readerIndex()); + if (json != null) { + out.add(json); + } + ... +``` + +如果state表示目前是一个数组对象,数组对象中可能包含多个对象,这些对象是通过逗号来区分的。逗号之间还可能会有空格,所以需要对这些数据进行特殊判断和处理,如下所示: + +``` +else if (state == ST_DECODING_ARRAY_STREAM) { + decodeByte(c, in, idx); + + if (!insideString && (openBraces == 1 && c == ',' || openBraces == 0 && c == ']')) { + for (int i = in.readerIndex(); Character.isWhitespace(in.getByte(i)); i++) { + in.skipBytes(1); + } + int idxNoSpaces = idx - 1; + while (idxNoSpaces >= in.readerIndex() && Character.isWhitespace(in.getByte(idxNoSpaces))) { + idxNoSpaces--; + } + ByteBuf json = extractObject(ctx, in, in.readerIndex(), idxNoSpaces + 1 - in.readerIndex()); + if (json != null) { + out.add(json); + } + .... +``` + +最后将解析出来的json对象放入byteBuf的out list中,整个解析到此结束。 + +# 总结 + +以上就是netty中json核心解码器JsonObjectDecoder的使用,它的本质是通过判断json对象中的分割符来分割多个json字符串,然后将分割后的json字符串存入ByteBuf中输出。 + +看到这里,大家可能会疑惑了,decoder不是和encoder一起出现的吗?为什么netty中只有JsonObjectDecoder,而没有JsonObjectEncoder呢? + +事实上,这里的Json对象就是一个包含Json字符的字符串,这个字符串被写入到ByteBuf中,所以这里并不需要特殊的encoder。 \ No newline at end of file diff --git a/learn-netty文章/66.netty系列之netty中的自动解码器ReplayingDecoder.md b/learn-netty文章/66.netty系列之netty中的自动解码器ReplayingDecoder.md new file mode 100644 index 0000000..00e29dc --- /dev/null +++ b/learn-netty文章/66.netty系列之netty中的自动解码器ReplayingDecoder.md @@ -0,0 +1,219 @@ +# netty系列之:netty中的自动解码器ReplayingDecoder + +文章目录 + + + +[简介](http://www.flydean.com/14-4-netty-replayingdecoder/#简介)[ByteToMessageDecoder可能遇到的问题](http://www.flydean.com/14-4-netty-replayingdecoder/#ByteToMessageDecoder可能遇到的问题)[ReplayingDecoder的实现原理](http://www.flydean.com/14-4-netty-replayingdecoder/#ReplayingDecoder的实现原理)[总结](http://www.flydean.com/14-4-netty-replayingdecoder/#总结) + +# 简介 + +netty提供了一个从ByteBuf到用户自定义的message的解码器叫做ByteToMessageDecoder,要使用这个decoder,我们需要继承这个decoder,并实现decode方法,从而在这个方法中实现ByteBuf中的内容到用户自定义message对象的转换。 + +那么在使用ByteToMessageDecoder的过程中会遇到什么问题呢?为什么又会有一个ReplayingDecoder呢?带着这个问题我们一起来看看吧。 + +# ByteToMessageDecoder可能遇到的问题 + +要想实现自己的解码器将ByteBuf转换成为自己的消息对象,可以继承ByteToMessageDecoder,然后实现其中的decode方法即可,先来看下decode方法的定义: + +``` + protected void decode(ChannelHandlerContext ctx, + ByteBuf buf, List out) throws Exception +``` + +输入的参数中buf是要解码的ByteBuf,out是解码过后的对象列表,我们需要把ByteBuf中的数据转换成为我们自己的对象加入out的list中。 + +那么这里可能会遇到一个问题,因为我们在调用decode方法的时候buf中的数据可能还没有准备好,比如我们需要一个Integer,但是buf中的数据不够一个整数,那么就需要一些buf中数据逻辑的判断,我们以一个带有消息长度的Buf对象来描述一下这个过程。 + +所谓带有消息长度的Buf对象,就是说Buf消息中的前4位,构成了一个整数,这个整数表示的是buf中后续消息的长度。 + +所以我们读取消息进行转换的流程是,先读取前面4个字节,得到消息的长度,然后再读取该长度的字节,这就是我们真正要获取的消息内容。 + +来看一下如果是继承自ByteToMessageDecoder应该怎么实现这个逻辑呢? + +``` + public class IntegerHeaderFrameDecoder extends ByteToMessageDecoder { + + @Override + protected void decode(ChannelHandlerContext ctx, + ByteBuf buf, List out) throws Exception { + + if (buf.readableBytes() < 4) { + return; + } + + buf.markReaderIndex(); + int length = buf.readInt(); + + if (buf.readableBytes() < length) { + buf.resetReaderIndex(); + return; + } + + out.add(buf.readBytes(length)); + } + } +``` + +在decode中,我们首先需要判断buf中可读的字节有没有4个,没有的话直接返回。如果有,则先读取这4个字节的长度,然后再判断buf中的可读字节是否小于应该读取的长度,如果小于,则说明数据还没有准备好,需要调用resetReaderIndex进行重置。 + +最后,如果所有的条件都满足,才真正进行读取工作。 + +有没有一个办法可以不提前进行判断,可以直接按照自己想要的内容来读取buf的方式呢?答案就是ReplayingDecoder。 + +我们先来看一下上面的例子用ReplayingDecoder重写是什么情况: + +``` + public class IntegerHeaderFrameDecoder + extends ReplayingDecoder { + + protected void decode(ChannelHandlerContext ctx, + ByteBuf buf, List out) throws Exception { + + out.add(buf.readBytes(buf.readInt())); + } + } +``` + +使用ReplayingDecoder,我们可以忽略buf是否已经接收到了足够的可读数据,直接读取即可。 + +相比之下ReplayingDecoder非常的简单。接下来,我们来探究一下ReplayingDecoder的实现原理。 + +# ReplayingDecoder的实现原理 + +ReplayingDecoder实际上是ByteToMessageDecoder的一个子类,它的定义如下: + +``` +public abstract class ReplayingDecoder extends ByteToMessageDecoder +``` + +在ByteToMessageDecoder中,最重要的方法是channelRead,在这个方法中实际调用了`callDecode(ctx, cumulation, out);`来实现cumulation到out的解码过程。 + +ReplayingDecoder的秘密就在于对这个方法的重写,我们来看下这个方法的具体实现: + +``` + protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List out) { + replayable.setCumulation(in); + try { + while (in.isReadable()) { + int oldReaderIndex = checkpoint = in.readerIndex(); + int outSize = out.size(); + if (outSize > 0) { + fireChannelRead(ctx, out, outSize); + out.clear(); + if (ctx.isRemoved()) { + break; + } + outSize = 0; + } + S oldState = state; + int oldInputLength = in.readableBytes(); + try { + decodeRemovalReentryProtection(ctx, replayable, out); + if (ctx.isRemoved()) { + break; + } + if (outSize == out.size()) { + if (oldInputLength == in.readableBytes() && oldState == state) { + throw new DecoderException( + StringUtil.simpleClassName(getClass()) + ".decode() must consume the inbound " + + "data or change its state if it did not decode anything."); + } else { + continue; + } + } + } catch (Signal replay) { + replay.expect(REPLAY); + if (ctx.isRemoved()) { + break; + } + + // Return to the checkpoint (or oldPosition) and retry. + int checkpoint = this.checkpoint; + if (checkpoint >= 0) { + in.readerIndex(checkpoint); + } else { + } + break; + } + if (oldReaderIndex == in.readerIndex() && oldState == state) { + throw new DecoderException( + StringUtil.simpleClassName(getClass()) + ".decode() method must consume the inbound data " + + "or change its state if it decoded something."); + } + if (isSingleDecode()) { + break; + } + } + } catch (DecoderException e) { + throw e; + } catch (Exception cause) { + throw new DecoderException(cause); + } + } +``` + +这里的实现和ByteToMessageDecoder不同的是ReplayingDecoder中定义了一个checkpoint,这个checkpint是在尝试进行数据解码之初设置的: + +``` +int oldReaderIndex = checkpoint = in.readerIndex(); +``` + +如果是在解码的过程中出现了异常,则使用checkpoint重置index: + +``` + int checkpoint = this.checkpoint; + if (checkpoint >= 0) { + in.readerIndex(checkpoint); + } else { + } +``` + +这里捕获的异常是Signal,Signal是什么呢? + +Signal是一个Error对象: + +``` +public final class Signal extends Error implements Constant +``` + +这个异常是从replayable中抛出来的。 + +replayable是一个特有的ByteBuf对象,叫做ReplayingDecoderByteBuf: + +``` +final class ReplayingDecoderByteBuf extends ByteBuf +``` + +在ReplayingDecoderByteBuf中定义了Signal属性: + +``` + private static final Signal REPLAY = ReplayingDecoder.REPLAY; +``` + +这个Signal异常是从ReplayingDecoderByteBuf中的get方法中抛出的,这里以getInt为例,看一下异常是如何抛出的: + +``` + public int getInt(int index) { + checkIndex(index, 4); + return buffer.getInt(index); + } +``` + +getInt方法首先会去调用checkIndex方法进行buff中的长度检测,如果小于要读取的长度,则会抛出异常REPLAY: + +``` + private void checkIndex(int index, int length) { + if (index + length > buffer.writerIndex()) { + throw REPLAY; + } + } +``` + +这就是Signal异常的由来。 + +# 总结 + +以上就是对ReplayingDecoder的介绍,虽然ReplayingDecoder好用,但是从它的实现可以看出,ReplayingDecoder是通过抛出异常来不断的重试,所以在某些特殊的情况下会造成性能的下降。 + +也就是说在减少我们代码量的同时,降低了程序的执行效率。看来要想马儿跑又想马儿不吃草,这样的好事是不可能的了。 \ No newline at end of file diff --git a/learn-netty文章/67.netty系列之netty中的frame解码器.md b/learn-netty文章/67.netty系列之netty中的frame解码器.md new file mode 100644 index 0000000..0aad5dd --- /dev/null +++ b/learn-netty文章/67.netty系列之netty中的frame解码器.md @@ -0,0 +1,168 @@ +# netty系列之:netty中的frame解码器 + +文章目录 + + + +[简介](http://www.flydean.com/14-5-netty-frame-decoder/#简介)[LineBasedFrameDecoder](http://www.flydean.com/14-5-netty-frame-decoder/#LineBasedFrameDecoder)[DelimiterBasedFrameDecoder](http://www.flydean.com/14-5-netty-frame-decoder/#DelimiterBasedFrameDecoder)[FixedLengthFrameDecoder](http://www.flydean.com/14-5-netty-frame-decoder/#FixedLengthFrameDecoder)[LengthFieldBasedFrameDecoder](http://www.flydean.com/14-5-netty-frame-decoder/#LengthFieldBasedFrameDecoder)[总结](http://www.flydean.com/14-5-netty-frame-decoder/#总结) + +# 简介 + +netty中的数据是通过ByteBuf来进行传输的,一个ByteBuf中可能包含多个有意义的数据,这些数据可以被称作frame,也就是说一个ByteBuf中可以包含多个Frame。 + +对于消息的接收方来说,接收到了ByteBuf,还需要从ByteBuf中解析出有用而数据,那就需要将ByteBuf中的frame进行拆分和解析。 + +一般来说不同的frame之间会有有些特定的分隔符,我们可以通过这些分隔符来区分frame,从而实现对数据的解析。 + +netty为我们提供了一些合适的frame解码器,通过使用这些frame解码器可以有效的简化我们的工作。下图是netty中常见的几个frame解码器: + +![img](6f394018c43a40a6a53a5260fc577575.png) + +接下来我们来详细介绍一下上面几个frame解码器的使用。 + +# LineBasedFrameDecoder + +LineBasedFrameDecoder从名字上看就是按行来进行frame的区分。根据操作系统的不同,换行可以有两种换行符,分别是 “\n” 和 “\r\n” 。 + +LineBasedFrameDecoder的基本原理就是从ByteBuf中读取对应的字符来和”\n” 跟 “\r\n”,可以了可以准确的进行字符的比较,这些frameDecoder对字符的编码也会有一定的要求,一般来说是需要UTF-8编码。因为在这样的编码中,”\n”和”\r”是以一个byte出现的,并且不会用在其他的组合编码中,所以用”\n”和”\r”来进行判断是非常安全的。 + +LineBasedFrameDecoder中有几个比较重要的属性,一个是maxLength的属性,用来检测接收到的消息长度,如果超出了长度限制,则会抛出TooLongFrameException异常。 + +还有一个stripDelimiter属性,用来判断是否需要将delimiter过滤掉。 + +还有一个是failFast,如果该值为true,那么不管frame是否读取完成,只要frame的长度超出了maxFrameLength,就会抛出TooLongFrameException。如果该值为false,那么TooLongFrameException会在整个frame完全读取之后再抛出。 + +LineBasedFrameDecoder的核心逻辑是先找到行的分隔符的位置,然后根据这个位置读取到对应的frame信息,这里来看一下找到行分隔符的findEndOfLine方法: + +``` + private int findEndOfLine(final ByteBuf buffer) { + int totalLength = buffer.readableBytes(); + int i = buffer.forEachByte(buffer.readerIndex() + offset, totalLength - offset, ByteProcessor.FIND_LF); + if (i >= 0) { + offset = 0; + if (i > 0 && buffer.getByte(i - 1) == '\r') { + i--; + } + } else { + offset = totalLength; + } + return i; + } +``` + +这里使用了一个ByteBuf的forEachByte对ByteBuf进行遍历。我们要找的字符是:ByteProcessor.FIND_LF。 + +最后LineBasedFrameDecoder解码之后的对象还是一个ByteBuf。 + +# DelimiterBasedFrameDecoder + +上面讲的LineBasedFrameDecoder只对行分隔符有效,如果我们的frame是以其他的分隔符来分割的话LineBasedFrameDecoder就用不了了,所以netty提供了一个更加通用的DelimiterBasedFrameDecoder,这个frameDecoder可以自定义delimiter: + +``` +public class DelimiterBasedFrameDecoder extends ByteToMessageDecoder { + + public DelimiterBasedFrameDecoder(int maxFrameLength, ByteBuf delimiter) { + this(maxFrameLength, true, delimiter); + } +``` + +传入的delimiter是一个ByteBuf,所以delimiter可能不止一个字符。 + +为了解决这个问题在DelimiterBasedFrameDecoder中定义了一个ByteBuf的数组: + +``` + private final ByteBuf[] delimiters; + + delimiters= delimiter.readableBytes(); +``` + +这个delimiters是通过调用delimiter的readableBytes得到的。 + +DelimiterBasedFrameDecoder的逻辑和LineBasedFrameDecoder差不多,都是通过对比bufer中的字符来对bufer中的数据进行截取,但是DelimiterBasedFrameDecoder可以接受多个delimiters,所以它的用处会根据广泛。 + +# FixedLengthFrameDecoder + +除了进行ByteBuf中字符比较来进行frame拆分之外,还有一些其他常见的frame拆分的方法,比如根据特定的长度来区分,netty提供了一种这样的decoder叫做FixedLengthFrameDecoder。 + +``` +public class FixedLengthFrameDecoder extends ByteToMessageDecoder +``` + +FixedLengthFrameDecoder也是继承自ByteToMessageDecoder,它的定义很简单,可以传入一个frame的长度: + +``` + public FixedLengthFrameDecoder(int frameLength) { + checkPositive(frameLength, "frameLength"); + this.frameLength = frameLength; + } +``` + +然后调用ByteBuf的readRetainedSlice方法来读取固定长度的数据: + +``` +in.readRetainedSlice(frameLength) +``` + +最后将读取到的数据返回。 + +# LengthFieldBasedFrameDecoder + +还有一些frame中包含了特定的长度字段,这个长度字段表示ByteBuf中有多少可读的数据,这样的frame叫做LengthFieldBasedFrame。 + +netty中也提供了一个对应的处理decoder: + +``` +public class LengthFieldBasedFrameDecoder extends ByteToMessageDecoder +``` + +读取的逻辑很简单,首先读取长度,然后再根据长度再读取数据。为了实现这个逻辑,LengthFieldBasedFrameDecoder提供了4个字段,分别是 lengthFieldOffset,lengthFieldLength,lengthAdjustment和initialBytesToStrip。 + +lengthFieldOffset指定了长度字段的开始位置,lengthFieldLength定义的是长度字段的长度,lengthAdjustment是对lengthFieldLength进行调整,initialBytesToStrip表示是否需要去掉长度字段。 + +听起来好像不太好理解,我们举几个例子,首先是最简单的: + +``` + BEFORE DECODE (14 bytes) AFTER DECODE (14 bytes) + +--------+----------------+ +--------+----------------+ + | Length | Actual Content |----->| Length | Actual Content | + | 0x000C | "HELLO, WORLD" | | 0x000C | "HELLO, WORLD" | + +--------+----------------+ +--------+----------------+ +``` + +要编码的消息有个长度字段,长度字段后面就是真实的数据,0x000C是一个十六进制,表示的数据是12,也就是”HELLO, WORLD” 中字符串的长度。 + +这里4个属性的值是: + +``` + lengthFieldOffset = 0 + lengthFieldLength = 2 + lengthAdjustment = 0 + initialBytesToStrip = 0 +``` + +表示的是长度字段从0开始,并且长度字段占有两个字节,长度不需要调整,也不需要对字段进行调整。 + +再来看一个比较复杂的例子,在这个例子中4个属性值如下: + +``` + lengthFieldOffset = 1 + lengthFieldLength = 2 + lengthAdjustment = 1 + initialBytesToStrip = 3 +``` + +对应的编码数据如下所示: + +``` +BEFORE DECODE (16 bytes) AFTER DECODE (13 bytes) + +------+--------+------+----------------+ +------+----------------+ + | HDR1 | Length | HDR2 | Actual Content |----->| HDR2 | Actual Content | + | 0xCA | 0x000C | 0xFE | "HELLO, WORLD" | | 0xFE | "HELLO, WORLD" | + +------+--------+------+----------------+ +------+----------------+ +``` + +上面的例子中长度字段是从第1个字节开始的(第0个字节是HDR1),长度字段占有2个字节,长度再调整一个字节,最终数据的开始位置就是1+2+1=4,然后再截取前3个字节的数据,得到了最后的结果。 + +# 总结 + +netty提供的这几个基于字符集的frame decoder基本上能够满足我们日常的工作需求了。当然,如果你传输的是一些更加复杂的对象,那么可以考虑自定义编码和解码器。自定义的逻辑步骤和上面我们讲解的保持一致就行了。 \ No newline at end of file diff --git a/learn-netty文章/67a3434b3dc14e1d98baf8cb5dbf07cf.png b/learn-netty文章/67a3434b3dc14e1d98baf8cb5dbf07cf.png new file mode 100644 index 0000000..4eea508 Binary files /dev/null and b/learn-netty文章/67a3434b3dc14e1d98baf8cb5dbf07cf.png differ diff --git a/learn-netty文章/68.java高级用法之JNA中的Memory和Pointer.md b/learn-netty文章/68.java高级用法之JNA中的Memory和Pointer.md new file mode 100644 index 0000000..7c98a39 --- /dev/null +++ b/learn-netty文章/68.java高级用法之JNA中的Memory和Pointer.md @@ -0,0 +1,220 @@ +# java高级用法之:JNA中的Memory和Pointer + +文章目录 + + + +[简介](http://www.flydean.com/06-jna-memory/#简介)[Pointer](http://www.flydean.com/06-jna-memory/#Pointer)[Memory](http://www.flydean.com/06-jna-memory/#Memory)[总结](http://www.flydean.com/06-jna-memory/#总结) + +# 简介 + +我们知道在native的代码中有很多指针,这些指针在JNA中被映射成为Pointer。除了Pointer之外,JNA还提供了更加强大的Memory类,本文将会一起探讨JNA中的Pointer和Memory的使用。 + +# Pointer + +Pointer是JNA中引入的类,用来表示native方法中的指针。大家回想一下native方法中的指针到底是什么呢? + +native方法中的指针实际上就是一个地址,这个地址就是真正对象的内存地址。所以在Pointer中定义了一个peer属性,用来存储真正对象的内存地址: + +``` +protected long peer; +``` + +实时上,Pointer的构造函数就需要传入这个peer参数: + +``` +public Pointer(long peer) { + this.peer = peer; + } +``` + +接下来我们看一下如何从Pointer中取出一个真正的对象,这里以byte数组为例: + +``` + public void read(long offset, byte[] buf, int index, int length) { + Native.read(this, this.peer, offset, buf, index, length); + } +``` + +实际上这个方法调用了Native.read方法,我们继续看一下这个read方法: + +``` +static native void read(Pointer pointer, long baseaddr, long offset, byte[] buf, int index, int length); +``` + +可以看到它是一个真正的native方法,用来读取一个指针对象。 + +除了Byte数组之外,Pointer还提供了很多其他类型的读取方法。 + +又读取就有写入,我们再看下Pointer是怎么写入数据的: + +``` + public void write(long offset, byte[] buf, int index, int length) { + Native.write(this, this.peer, offset, buf, index, length); + } +``` + +同样的,还是调用 Native.write方法来写入数据。 + +这里Native.write方法也是一个native方法: + +``` +static native void write(Pointer pointer, long baseaddr, long offset, byte[] buf, int index, int length); +``` + +Pointer还提供了很多其他类型数据的写入方法。 + +当然还有更加直接的get*方法: + +``` +public byte getByte(long offset) { + return Native.getByte(this, this.peer, offset); + } +``` + +## 特殊的Pointer:Opaque + +在Pointer中,还有两个createConstant方法,用来创建不可读也不可写的Pointer: + +``` + public static final Pointer createConstant(long peer) { + return new Opaque(peer); + } + + public static final Pointer createConstant(int peer) { + return new Opaque((long)peer & 0xFFFFFFFF); + } +``` + +实际上返回的而是Opaque类,这个类继承自Pointer,但是它里面的所有read或者write方法,都会抛出UnsupportedOperationException: + +``` + private static class Opaque extends Pointer { + private Opaque(long peer) { super(peer); } + @Override + public Pointer share(long offset, long size) { + throw new UnsupportedOperationException(MSG); + } +``` + +# Memory + +Pointer是基本的指针映射,如果对于通过使用native的malloc方法分配的内存空间而言,除了Pointer指针的开始位置之外,我们还需要知道分配的空间大小。所以一个简单的Pointer是不够用了。 + +这种情况下,我们就需要使用Memory。 + +Memory是一种特殊的Pointer, 它保存了分配出来的空间大小。我们来看一下Memory的定义和它里面包含的属性: + +``` +public class Memory extends Pointer { +... + private static ReferenceQueue QUEUE = new ReferenceQueue(); + private static LinkedReference HEAD; // the head of the doubly linked list used for instance tracking + private static final WeakMemoryHolder buffers = new WeakMemoryHolder(); + private final LinkedReference reference; // used to track the instance + protected long size; // Size of the malloc'ed space +... +} +``` + +Memory里面定义了5个数据,我们接下来一一进行介绍。 + +首先是最为重要的size,size表示的是Memory中内存空间的大小,我们来看下Memory的构造函数: + +``` + public Memory(long size) { + this.size = size; + if (size <= 0) { + throw new IllegalArgumentException("Allocation size must be greater than zero"); + } + peer = malloc(size); + if (peer == 0) + throw new OutOfMemoryError("Cannot allocate " + size + " bytes"); + + reference = LinkedReference.track(this); + } +``` + +可以看到Memory类型的数据需要传入一个size参数,表示Memory占用的空间大小。当然,这个size必须要大于0. + +然后调用native方法的malloc方法来分配一个内存空间,返回的peer保存的是内存空间的开始地址。如果peer0,表示分配失败。 + +如果分配成功,则将当前Memory保存到LinkedReference中,用来跟踪当前的位置。 + +我们可以看到Memory中有两个LinkedReference,一个是HEAD,一个是reference。 + +LinkedReference本身是一个WeakReference,weekReference引用的对象只要垃圾回收执行,就会被回收,而不管是否内存不足。 + +``` +private static class LinkedReference extends WeakReference +``` + +我们看一下LinkedReference的构造函数: + +``` +private LinkedReference(Memory referent) { + super(referent, QUEUE); + } +``` + +这个QUEUE是ReferenceQueue,表示的是GC待回收的对象列表。 + +我们看到Memory的构造函数除了设置size之外,还调用了: + +``` +reference = LinkedReference.track(this); +``` + +仔细看LinkedReference.track方法: + +``` + static LinkedReference track(Memory instance) { + // use a different lock here to allow the finialzier to unlink elements too + synchronized (QUEUE) { + LinkedReference stale; + + // handle stale references here to avoid GC overheating when memory is limited + while ((stale = (LinkedReference) QUEUE.poll()) != null) { + stale.unlink(); + } + } + + // keep object allocation outside the syncronized block + LinkedReference entry = new LinkedReference(instance); + + synchronized (LinkedReference.class) { + if (HEAD != null) { + entry.next = HEAD; + HEAD = HEAD.prev = entry; + } else { + HEAD = entry; + } + } + + return entry; + } +``` + +这个方法的意思是首先从QUEUE中拿出那些准备被垃圾回收的Memory对象,然后将其从LinkedReference中unlink。 最后将新创建的对象加入到LinkedReference中。 + +因为Memory中的QUEUE和HEAD都是类变量,所以这个LinkedReference保存的是JVM中所有的Memory对象。 + +最后Memory中也提供了对应的read和write方法,但是Memory中的方法和Pointer不同,Memory中的方法多了一个boundsCheck,如下所示: + +``` + public void read(long bOff, byte[] buf, int index, int length) { + boundsCheck(bOff, length * 1L); + super.read(bOff, buf, index, length); + } + + public void write(long bOff, byte[] buf, int index, int length) { + boundsCheck(bOff, length * 1L); + super.write(bOff, buf, index, length); + } +``` + +为什么会有boundsCheck呢?这是因为Memory和Pointer不同,Memory中有一个size的属性,用来存储分配的内存大小。使用boundsCheck就是来判断访问的地址是否出界,用来保证程序的安全。 + +# 总结 + +Pointer和Memory算是JNA中的高级功能,大家如果想要和native的alloc方法进行映射的话,就要考虑使用了。 \ No newline at end of file diff --git a/learn-netty文章/69.java高级用法之JNA中的Function.md b/learn-netty文章/69.java高级用法之JNA中的Function.md new file mode 100644 index 0000000..ae4a3b5 --- /dev/null +++ b/learn-netty文章/69.java高级用法之JNA中的Function.md @@ -0,0 +1,115 @@ +# java高级用法之:JNA中的Function + +文章目录 + + + +[简介](http://www.flydean.com/07-jna-function/#简介)[function的定义](http://www.flydean.com/07-jna-function/#function的定义)[Function的实际应用](http://www.flydean.com/07-jna-function/#Function的实际应用)[总结](http://www.flydean.com/07-jna-function/#总结) + +# 简介 + +在JNA中,为了和native的function进行映射,我们可以有两种mapping方式,第一种是interface mapping,第二种是direct mapping。虽然两种方式不同,但是在具体的方法映射中,我们都需要在JAVA中定义一个和native方法进行映射的方法。 + +而这个JAVA中的映射在JNA中就是一个function。通过或者function对象,我们可以实现一些非常强大的功能,一起看看吧。 + +# function的定义 + +先来看下JNA中Function的定义: + +``` +public class Function extends Pointer +``` + +可以看到Function实际上是一个Pointer,指向的是native function的指针。 + +那么怎么得到一个Function的实例呢? + +我们知道JNA的流程是先进行Library的映射,然后再对Library中的Function进行映射。所以很自然的我们应该可以从Library中得到Function。 + +我们看一下根据Library name得到function实例的方法定义: + +``` +public static Function getFunction(String libraryName, String functionName, int callFlags, String encoding) { + return NativeLibrary.getInstance(libraryName).getFunction(functionName, callFlags, encoding); + } +``` + +这个方法可以接受4个参数,前面两个参数大家应该很熟悉了,第三个参数是callFlags,表示的是函数调用的flags,Function定义了三个callFlags: + +``` + public static final int C_CONVENTION = 0; + + public static final int ALT_CONVENTION = 0x3F; + + public static final int THROW_LAST_ERROR = 0x40; +``` + +其中C_CONVENTION表示的是C语言类型的方法调用。 + +ALT_CONVENTION表示的其他的调用方式。 + +THROW_LAST_ERROR表示如果native函数的返回值是非零值的时候,将会抛出一个LastErrorException。 + +最后一个参数是encoding,表示的是字符串的编码方式,实际上指的是 Java unicode和native (const char*) strings 的转换方式。 + +除了根据Library name获取Function之外,JNA还提供了根据Pointer来获取Function的方法。 + +``` + public static Function getFunction(Pointer p, int callFlags, String encoding) { + return new Function(p, callFlags, encoding); + } +``` + +这里的Pointer指的是一个执行native方法的指针,因为Function本身就是继承自Pointer。所以跟Pointer来创建Function的本质就是在Pointer的基础上添加了一些Function特有的属性。 + +有了Function的定义,更为重要的是如何通过Function来调用对应的方法。跟反射很类似,Function中也有一个invoke方法,通过调用invoke,我们就可以执行对应的Function的功能。 + +Function中的invoke方法有两种,一种是通用的返回对象Object,一种是带有返回值的invoke方法,比如invokeString,invokePointer,invokeInt等。 + +# Function的实际应用 + +Function的实际使用和JAVA中的反射有点类似,其工作流程是首先获得要加载的NativeLibrary,然后从该NativeLibrary中找到要调用的Function,最后invoke该Function的某些方法。 + +C语言中的printf应该是大家最熟悉的native方法了。我们看一下如何使用Function来调用这个方法吧: + +``` + NativeLibrary lib = NativeLibrary.getInstance(Platform.C_LIBRARY_NAME); + Function f = lib.getFunction("printf"); + try { + f.invoke(getClass(), new Object[] { getName() }); + fail("Invalid return types should throw an exception"); + } catch(IllegalArgumentException e) { + // expected + } +``` + +可以看到调用的流程非常简洁。如果是用interface Mapping或者direct Mapping的形式,我们还需要自定义一个interface或者class,并且在其中定义一个相应的java方法映射。但是如果使用Function的话,这些都不需要了。我们直接可以从NativeLibrary中拿到对应的函数,并最终调用其中的方法。 + +C语言中的printf的原型如下: + +``` +# include +int printf(const char *format, ...); +``` + +printf带有返回值的,如果要输出这个返回值,则可以调用Function中的invokeInt命令。我们再来看一个有返回值的调用例子: + +``` +NativeLibrary lib = NativeLibrary.getInstance(Platform.C_LIBRARY_NAME); + Function f = lib.getFunction("printf"); + Object[] args = new Object[Function.MAX_NARGS+1]; + // Make sure we don't break 'printf' + args[0] = getName(); + try { + f.invokeInt(args); + fail("Arguments should be limited to " + Function.MAX_NARGS); + } catch(UnsupportedOperationException e) { + // expected + } +``` + +# 总结 + +使用Function可以减少手写Mapping的工作,在某些情况下是非常好用的,但是Function的invoke支持TypeMapper,并不支持FunctionMapper,所以在使用中还是有一些限制。 + +大家可以在使用过程中酌情考虑。 \ No newline at end of file diff --git a/learn-netty文章/69d7d0d53df847e2b5eb7791da956ef1.png b/learn-netty文章/69d7d0d53df847e2b5eb7791da956ef1.png new file mode 100644 index 0000000..6ff3513 Binary files /dev/null and b/learn-netty文章/69d7d0d53df847e2b5eb7791da956ef1.png differ diff --git a/learn-netty文章/6d6cc43e5bf94c078d03f0011b897076.png b/learn-netty文章/6d6cc43e5bf94c078d03f0011b897076.png new file mode 100644 index 0000000..ba12029 Binary files /dev/null and b/learn-netty文章/6d6cc43e5bf94c078d03f0011b897076.png differ diff --git a/learn-netty文章/6f394018c43a40a6a53a5260fc577575.png b/learn-netty文章/6f394018c43a40a6a53a5260fc577575.png new file mode 100644 index 0000000..cac0beb Binary files /dev/null and b/learn-netty文章/6f394018c43a40a6a53a5260fc577575.png differ diff --git a/learn-netty文章/7.netty系列之基于流的数据传输.md b/learn-netty文章/7.netty系列之基于流的数据传输.md new file mode 100644 index 0000000..12152d4 --- /dev/null +++ b/learn-netty文章/7.netty系列之基于流的数据传输.md @@ -0,0 +1,176 @@ +# netty系列之:基于流的数据传输 + +# 简介 + +我们知道由两种数据的传输方式,分别是字符流和字节流,字符流的意思是传输的对象就是字符串,格式已经被设置好了,发送方和接收方按照特定的格式去读取就行了,而字节流是指将数据作为最原始的二进制字节来进行传输。 + +今天给大家介绍一下在netty中的基于流的数据传输。 + +# package和byte + +熟悉TCP/IP协议的同学应该知道,在TCP/IP中,因为底层协议有支持的数据包的最大值,所以对于大数据传输来说,需要对数据进行拆分和封包处理,并将这些拆分组装过的包进行发送,最后在接收方对这些包进行组合。在各个包中有固定的结构,所以接收方可以很清楚的知道到底应该组合多少个包作为最终的结果。 + +那么对于netty来说,channel中传输的是ByteBuf,实际上最最最底层的就是byte数组。对于这种byte数组来说,接收方并不知道到底应该组合多少个byte来合成原来的消息,所以需要在接收端对收到的byte进行组合,从而生成最终的数据。 + +那么对于netty中的byte数据流应该怎么组合呢?我们接下来看两种组合方法。 + +# 手动组合 + +这种组合的方式的基本思路是构造一个目标大小的ByteBuf,然后将接收到的byte通过调用ByteBuf的writeBytes方法写入到ByteBuf中。最后从ByteBuf中读取对应的数据。 + +比如我们想从服务端发送一个int数字给客户端,一般来说int是32bits,然后一个byte是8bits,那么一个int就需要4个bytes组成。 + +在server端,可以建立一个byte的数组,数组中包含4个元素。将4个元素的byte发送给客户端,那么客户端该如何处理呢? + +首先我们需要建立一个clientHander,这个handler应该继承ChannelInboundHandlerAdapter,并且在其handler被添加到ChannelPipeline的时候初始化一个包含4个byte的byteBuf。 + +handler被添加的时候会触发一个handlerAdded事件,所以我们可以这样写: + +```java +private ByteBuf buf; + +@Override +public void handlerAdded(ChannelHandlerContext ctx) { + //创建一个4个byte的缓冲器 + buf = ctx.alloc().buffer(4); +} +``` + +上例中,我们从ctx分配了一个4个字节的缓冲器,并将其赋值给handler中的私有变量buf。 + +当handler执行完毕,从ChannelPipeline中删除的时候,会触发handlerRemoved事件,在这个事件中,我们可以对分配的Bytebuf进行清理,通常来说,可以调用其release方法,如下所示: + +```java +public void handlerRemoved(ChannelHandlerContext ctx) { + buf.release(); // 释放buf + buf = null; +} +``` + +然后最关键的一步就是从channel中读取byte并将其放到4个字节的byteBuf中。在之前的文章中我们提到了,可以在channelRead方法中,处理消息读取的逻辑。 + +```java +public void channelRead(ChannelHandlerContext ctx, Object msg) { + ByteBuf m = (ByteBuf) msg; + buf.writeBytes(m); // 写入一个byte + m.release(); + + if (buf.readableBytes() >= 4) { // 已经凑够4个byte,将4个byte组合称为一个int + long result = buf.readUnsignedInt(); + ctx.close(); + } +} +``` + +每次触发channelRead方法,都会将读取到的一个字节的byte通过调用writeBytes方法写入buf中。当buf的可读byte大于等于4个的时候就说明4个字节已经读满了,可以对其进行操作了。 + +这里我们将4个字节组合成一个unsignedInt,并使用readUnsignedInt方法从buf中读取出来组合称为一个int数字。 + +上面的例子虽然可以解决4个字节的byte问题,但是如果数据结构再负责一点,上面的方式就会力不从心,需要考虑太多的数据组合问题。接下来我们看另外一种方式。 + +# Byte的转换类 + +netty提供了一个ByteToMessageDecoder的转换类,可以方便的对Byte转换为其他的类型。 + +我们只需要重新其中的decode方法,就可以实现对ByteBuf的转换: + +```java +public class SquareDecoder extends ByteToMessageDecoder { + @Override + public void decode(ChannelHandlerContext ctx, ByteBuf in, List out) + throws Exception { + out.add(in.readBytes(in.readableBytes())); + } +} +``` + +上面的例子将byte从input转换到output中,当然,你还可以在上面的方法中进行格式转换,如下所示: + +```java +public class TimeDecoder extends ByteToMessageDecoder { + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) { + if (in.readableBytes() < 4) { + return; + } + + out.add(in.readBytes(4)); + } +} +``` + +上面的例子会先判断in中是否有4个byte,如果有就将其读出来放到out中去。那么有同学会问了,输入不是一个byte一个byte来的吗?为什么这里可以一次读取到4个byte?这是因为ByteToMessageDecoder内置了一个缓存装置,所以这里的in实际上是一个缓存集合。 + +# ReplayingDecoder + +netty还提供了一个更简单的转换ReplayingDecoder,如果使用ReplayingDecoder重新上面的逻辑就是这样的: + +```java +public class TimeDecoder extends ReplayingDecoder { + @Override + protected void decode( + ChannelHandlerContext ctx, ByteBuf in, List out) { + out.add(in.readBytes(4)); + } +} +``` + +只需要一行代码即可。 + +事实上ReplayingDecoder 是ByteToMessageDecoder 的子类,是在ByteToMessageDecoder上丰富了一些功能的结果。 + +他们两的区别在于ByteToMessageDecoder 还需要通过调用readableBytes来判断是否有足够的可以读byte,而使用ReplayingDecoder直接读取即可,它假设的是所有的bytes都已经接受成功了。 + +比如下面使用ByteToMessageDecoder的代码: + +```java +public class IntegerHeaderFrameDecoder extends ByteToMessageDecoder { + + @Override + protected void decode(ChannelHandlerContext ctx, + ByteBuf buf, List out) throws Exception { + + if (buf.readableBytes() < 4) { + return; + } + + buf.markReaderIndex(); + int length = buf.readInt(); + + if (buf.readableBytes() < length) { + buf.resetReaderIndex(); + return; + } + + out.add(buf.readBytes(length)); + } +} +``` + +上例假设在byte的头部是一个int大小的数组,代表着byte数组的长度,需要先读取int值,然后再根据int值来读取对应的byte数据。 + +和下面的代码是等价的: + +```java +public class IntegerHeaderFrameDecoder + extends ReplayingDecoder { + + protected void decode(ChannelHandlerContext ctx, + ByteBuf buf, List out) throws Exception { + + out.add(buf.readBytes(buf.readInt())); + } +} +``` + +上面代码少了判断的步骤。 + +那么这是怎么实现的呢? + +事实上ReplayingDecoder 会传递一个会抛出 Error的 ByteBuf , 当 ByteBuf 读取的byte个数不满足要求的时候,会抛出异常,当ReplayingDecoder 捕获到这个异常之后,会重置buffer的readerIndex到最初的状态,然后等待后续的数据进来,然后再次调用decode方法。 + +所以,ReplayingDecoder的效率会比较低,为了解决这个问题,netty提供了checkpoint() 方法。这是一个保存点,当报错的时候,可以不会退到最初的状态,而是回退到checkpoint() 调用时候保存的状态,从而可以减少不必要的浪费。 + +# 总结 + +本文介绍了在netty中进行stream操作和变换的几种方式,希望大家能够喜欢。 \ No newline at end of file diff --git a/learn-netty文章/70.netty系列之netty中常用的字符串编码解码器.md b/learn-netty文章/70.netty系列之netty中常用的字符串编码解码器.md new file mode 100644 index 0000000..e9a072b --- /dev/null +++ b/learn-netty文章/70.netty系列之netty中常用的字符串编码解码器.md @@ -0,0 +1,152 @@ +# netty系列之:netty中常用的字符串编码解码器 + +文章目录 + + + +[简介](http://www.flydean.com/14-6-netty-codec-string/#简介)[netty中的字符串编码解码器](http://www.flydean.com/14-6-netty-codec-string/#netty中的字符串编码解码器)[不同平台的换行符](http://www.flydean.com/14-6-netty-codec-string/#不同平台的换行符)[字符串编码的实现](http://www.flydean.com/14-6-netty-codec-string/#字符串编码的实现)[总结](http://www.flydean.com/14-6-netty-codec-string/#总结) + +# 简介 + +字符串是我们程序中最常用到的消息格式,也是最简单的消息格式,但是正因为字符串string太过简单,不能附加更多的信息,所以在netty中选择的是使用byteBuf作为最底层的消息传递载体。 + +虽然底层使用的ByteBuf,但是对于程序员来说,还是希望能够使用这种最简单的字符串格式,那么有什么简单的方法吗? + +# netty中的字符串编码解码器 + +为了解决在netty的channel中传递字符串的问题,netty提供了针对于字符串的编码和解码器,分别是StringEncoder和StringDecoder。 + +我们来看下他们是怎么在程序中使用的,首先是将StringDecoder和StringEncoder加入channelPipeline中: + +``` + ChannelPipeline pipeline = ...; + + // Decoders + pipeline.addLast("frameDecoder", new LineBasedFrameDecoder(80)); + pipeline.addLast("stringDecoder", new StringDecoder(CharsetUtil.UTF_8)); + + // Encoder + pipeline.addLast("stringEncoder", new StringEncoder(CharsetUtil.UTF_8)); +``` + +注意,这里我们在使用StringDecoder之前还调用了LineBasedFrameDecoder,先把数据按行进行分割,然后再进行字符串的读取。 + +那么有人要问了,decoder加入了LineBasedFrameDecoder预处理,为什么写入的时候没有添加行的分割符呢? + +事实上这里有两种处理方式,第一种就是在向channel中写入字符串的时候,手动加上行分隔符,如下所示: + +``` + void channelRead(ChannelHandlerContext ctx, String msg) { + ch.write("Did you say '" + msg + "'?\n"); + } +``` + +如果不想每次都在msg后面加上换行符,那么可以将StringEncoder替换成为LineEncoder,上面的pipeline就变成下面这样: + +``` + ChannelPipeline pipeline = ...; + + // Decoders + pipeline.addLast("frameDecoder", new LineBasedFrameDecoder(80)); + pipeline.addLast("stringDecoder", new StringDecoder(CharsetUtil.UTF_8)); + + // Encoder + pipeline.addLast("lineEncoder", new LineEncoder(LineSeparator.UNIX, CharsetUtil.UTF_8)); +``` + +这样,我们在handler中就不需要手动添加换行符了,如下所示: + +``` + void channelRead(ChannelHandlerContext ctx, String msg) { + ch.write("Did you say '" + msg + "'?"); + } +``` + +# 不同平台的换行符 + +在unix和windows平台传递过文本文件的朋友可能会遇到一个问题,就是windows创建的文本文件,如果在unix下面打开的话,会发现每行后面多出了一个特殊字符,这是因为unix和windows平台定义的换行符是不同的。 + +在unix平台通常使用”\n”来换行,而在windows平台则使用””\r\n”来换行。 + +java程序因为是跨平台的,写出的程序可能运行在unix平台,也可能运行在windows平台,所以我们需要有一个办法来获取平台的换行符,netty提供了一个LineSeparator的类来完成这个工作。 + +LineSeparator中有三个换行符的定义,分别是: + +``` + public static final LineSeparator DEFAULT = new LineSeparator(StringUtil.NEWLINE); + + public static final LineSeparator UNIX = new LineSeparator("\n"); + + public static final LineSeparator WINDOWS = new LineSeparator("\r\n"); +``` + +UNIX和WINDOWS很好理解,他们就是我们刚刚讲到的不同的平台。 + +那么什么是DEFAULT呢?DEFAULT中传入的NEWLINE,实际上是从系统属性中获取到的,如果没有获取到,则使用默认的”\n”。 + +``` +public static final String NEWLINE = SystemPropertyUtil.get("line.separator", "\n"); +``` + +# 字符串编码的实现 + +上面我们讲到了和字符串编码解码相关的类分别是StringEncoder,LineEncoder和StringDecoder,我们来详细看下这三个类的实现。 + +首先是StringEncoder,StringEncoder继承了MessageToMessageEncoder: + +``` +public class StringEncoder extends MessageToMessageEncoder +``` + +泛型中的CharSequence表示StringEncoder要encode的对象是CharSequence,也就是字符序列。 + +虽然大家常用String这个类,但是不一定大家都知道String其实是CharSequence的子类,所以StringEncoder也可以编码字符串。 + +StringEncoder的编码逻辑很简单,将传入的字符串msg转换成为CharBuffer,然后调用ByteBufUtil的encodeString方法就可以转换成为ByteBuf,并加入out中去: + +``` + protected void encode(ChannelHandlerContext ctx, CharSequence msg, List out) throws Exception { + if (msg.length() == 0) { + return; + } + out.add(ByteBufUtil.encodeString(ctx.alloc(), CharBuffer.wrap(msg), charset)); + } +``` + +LineEncoder和StringEncoder很类似,它也是继承自MessageToMessageEncoder: + +``` +public class LineEncoder extends MessageToMessageEncoder +``` + +不同之处在于encoder方法: + +``` + protected void encode(ChannelHandlerContext ctx, CharSequence msg, List out) throws Exception { + ByteBuf buffer = ByteBufUtil.encodeString(ctx.alloc(), CharBuffer.wrap(msg), charset, lineSeparator.length); + buffer.writeBytes(lineSeparator); + out.add(buffer); + } +``` + +ByteBufUtil的encodeString多了一个lineSeparator.length参数,用来预留lineSeparator的位置,然后在返回的ByteBuf后面加上lineSeparator作为最终的输出。 + +StringDecoder是和StringEncoder相反的过程: + +``` +public class StringDecoder extends MessageToMessageDecoder +``` + +这里的ByteBuf表示的是要解码的对象是ByteBuf,我们看下他的解码方法: + +``` + protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List out) throws Exception { + out.add(msg.toString(charset)); + } +``` + +直接调用msg.toString方法即可将ByteBuf转换成为字符串。 + +# 总结 + +以上就是netty中对字符串的编码解码器,通过使用这几个编码解码器可以大大简化我们的工作。 \ No newline at end of file diff --git a/learn-netty文章/71.netty系列之netty中常用的xml编码解码器.md b/learn-netty文章/71.netty系列之netty中常用的xml编码解码器.md new file mode 100644 index 0000000..05560df --- /dev/null +++ b/learn-netty文章/71.netty系列之netty中常用的xml编码解码器.md @@ -0,0 +1,133 @@ +# netty系列之:netty中常用的xml编码解码器 + +文章目录 + + + +[简介](http://www.flydean.com/14-7-netty-codec-xml/#简介)[XmlFrameDecoder](http://www.flydean.com/14-7-netty-codec-xml/#XmlFrameDecoder)[XmlDecoder](http://www.flydean.com/14-7-netty-codec-xml/#XmlDecoder)[总结](http://www.flydean.com/14-7-netty-codec-xml/#总结) + +# 简介 + +在json之前,xml是最常用的数据传输格式,虽然xml的冗余数据有点多,但是xml的结构简单清晰,至今仍然运用在程序中的不同地方,对于netty来说自然也提供了对于xml数据的支持。 + +netty对xml的支持表现在两个方面,第一个方面是将编码过后的多个xml数据进行frame拆分,每个frame包含一个完整的xml。另一方面是将分割好的frame进行xml的语义解析。 + +进行frame拆分可以使用XmlFrameDecoder,进行xml文件内容的解析则可以使用XmlDecoder,接下来我们会详细讲解两个decoder实现和使用。 + +# XmlFrameDecoder + +因为我们收到的是数据流,所以不确定收到的数据到底是什么样的,一个正常的xml数据可能会被拆分成多个数据frame。 + +如下所示: + +``` + +-------+-----+--------------+ + | | + +-------+-----+--------------+ +``` + +这是一个正常的xml数据,但是被拆分成为了三个frame,所以我们需要将其合并成为一个frame如下: + +``` + +-----------------+ + | | + +-----------------+ +``` + +还有可能不同的xml数据被分拆在多个frame中的情况,如下所示: + +``` + +-----+-----+-----------+-----+----------------------------------+ + | | content | + +-----+-----+-----------+-----+----------------------------------+ +``` + +上面的数据需要拆分成为两个frame: + +``` + +-----------------+-------------------------------------+ + | | content | + +-----------------+-------------------------------------+ +``` + +拆分的逻辑很简单,主要是通过判断xml的分隔符的位置来判断xml是否开始或者结束。xml中的分隔符有三个,分别是'<‘, ‘>’ 和 ‘/’。 + +在decode方法中只需要判断这三个分隔符即可。 + +另外还有一些额外的判断逻辑,比如是否是有效的xml开始字符: + +``` + private static boolean isValidStartCharForXmlElement(final byte b) { + return b >= 'a' && b <= 'z' || b >= 'A' && b <= 'Z' || b == ':' || b == '_'; + } +``` + +是否是注释: + +``` + private static boolean isCommentBlockStart(final ByteBuf in, final int i) { + return i < in.writerIndex() - 3 + && in.getByte(i + 2) == '-' + && in.getByte(i + 3) == '-'; + } +``` + +是否是CDATA数据: + +``` + private static boolean isCDATABlockStart(final ByteBuf in, final int i) { + return i < in.writerIndex() - 8 + && in.getByte(i + 2) == '[' + && in.getByte(i + 3) == 'C' + && in.getByte(i + 4) == 'D' + && in.getByte(i + 5) == 'A' + && in.getByte(i + 6) == 'T' + && in.getByte(i + 7) == 'A' + && in.getByte(i + 8) == '['; +``` + +通过使用这些方法判断好xml数据的起始位置之后,就可以调用extractFrame方法将要使用的ByteBuf从原始数据中拷贝出来,最后放到out中去: + +``` +final ByteBuf frame = + extractFrame(in, readerIndex + leadingWhiteSpaceCount, xmlElementLength - leadingWhiteSpaceCount); + in.skipBytes(xmlElementLength); + out.add(frame); +``` + +# XmlDecoder + +将xml数据拆分成为一个个frame之后,接下来就是对xml中具体数据的解析了。 + +netty提供了一个xml数据解析的方法叫做XmlDecoder,主要用来对已经是一个单独的xml数据的frame进行实质内容的解析,它的定义如下: + +``` +public class XmlDecoder extends ByteToMessageDecoder +``` + +XmlDecoder根据读取到的xml内容,将xml的部分拆分为XmlElementStart,XmlAttribute,XmlNamespace,XmlElementEnd,XmlProcessingInstruction,XmlCharacters,XmlComment,XmlSpace,XmlDocumentStart,XmlEntityReference,XmlDTD和XmlCdata。 + +这些数据基本上覆盖了xml中所有可能出现的元素。 + +所有的这些元素都是定义在io.netty.handler.codec.xml包中的。 + +但是XmlDecoder对xml的读取解析则是借用了第三方xml工具包:fasterxml。 + +XmlDecoder使用了fasterxml中的AsyncXMLStreamReader和AsyncByteArrayFeeder用来进行xml数据的解析。 + +这两个属性的定义如下: + +``` + private static final AsyncXMLInputFactory XML_INPUT_FACTORY = new InputFactoryImpl(); + private final AsyncXMLStreamReader streamReader; + private final AsyncByteArrayFeeder streamFeeder; + + this.streamReader = XML_INPUT_FACTORY.createAsyncForByteArray(); + this.streamFeeder = (AsyncByteArrayFeeder)this.streamReader.getInputFeeder(); +``` + +decode的逻辑是通过判断xml element的类型来分别进行不同数据的读取,最后将读取到的数据封装成上面我们提到的各种xml对象,最后将xml对象添加到out list中返回。 + +# 总结 + +我们可以借助XmlFrameDecoder和XmlDecoder来实现非常方便的xml数据解析,netty已经为我们造好轮子了,我们就不需要再自行发明了。 \ No newline at end of file diff --git a/learn-netty文章/72.netty系列之netty中常用的对象编码解码器.md b/learn-netty文章/72.netty系列之netty中常用的对象编码解码器.md new file mode 100644 index 0000000..a053a2b --- /dev/null +++ b/learn-netty文章/72.netty系列之netty中常用的对象编码解码器.md @@ -0,0 +1,342 @@ +# netty系列之:netty中常用的对象编码解码器 + +文章目录 + + + +[简介](http://www.flydean.com/14-8-netty-codec-object/#简介)[什么是序列化](http://www.flydean.com/14-8-netty-codec-object/#什么是序列化)[netty中对象的传输](http://www.flydean.com/14-8-netty-codec-object/#netty中对象的传输)[ObjectDecoder](http://www.flydean.com/14-8-netty-codec-object/#ObjectDecoder)[ObjectEncoderOutputStream和ObjectDecoderInputStream](http://www.flydean.com/14-8-netty-codec-object/#ObjectEncoderOutputStream和ObjectDecoderInputStream)[总结](http://www.flydean.com/14-8-netty-codec-object/#总结) + +# 简介 + +我们在程序中除了使用常用的字符串进行数据传递之外,使用最多的还是JAVA对象。在JDK中,对象如果需要在网络中传输,必须实现Serializable接口,表示这个对象是可以被序列化的。这样就可以调用JDK自身的对象对象方法,进行对象的读写。 + +那么在netty中进行对象的传递可不可以直接使用JDK的对象序列化方法呢?如果不能的话,又应该怎么处理呢? + +今天带大家来看看netty中提供的对象编码器。 + +# 什么是序列化 + +序列化就是将java对象按照一定的顺序组织起来,用于在网络上传输或者写入存储中。而反序列化就是从网络中或者存储中读取存储的对象,将其转换成为真正的java对象。 + +所以序列化的目的就是为了传输对象,对于一些复杂的对象,我们可以使用第三方的优秀框架,比如Thrift,Protocol Buffer等,使用起来非常的方便。 + +JDK本身也提供了序列化的功能。要让一个对象可序列化,则可以实现java.io.Serializable接口。 + +java.io.Serializable是从JDK1.1开始就有的接口,它实际上是一个marker interface,因为java.io.Serializable并没有需要实现的接口。继承java.io.Serializable就表明这个class对象是可以被序列化的。 + +```java +@Data +@AllArgsConstructor +public class CustUser implements java.io.Serializable{ + private static final long serialVersionUID = -178469307574906636L; + private String name; + private String address; +} +``` + +上面我们定义了一个CustUser可序列化对象。这个对象有两个属性:name和address。 + +接下看下怎么序列化和反序列化: + +```java +public void testCusUser() throws IOException, ClassNotFoundException { + CustUser custUserA=new CustUser("jack","www.flydean.com"); + CustUser custUserB=new CustUser("mark","www.flydean.com"); + + try(FileOutputStream fileOutputStream = new FileOutputStream("target/custUser.ser")){ + ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); + objectOutputStream.writeObject(custUserA); + objectOutputStream.writeObject(custUserB); + } + + try(FileInputStream fileInputStream = new FileInputStream("target/custUser.ser")){ + ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); + CustUser custUser1 = (CustUser) objectInputStream.readObject(); + CustUser custUser2 = (CustUser) objectInputStream.readObject(); + log.info("{}",custUser1); + log.info("{}",custUser2); + } + } +``` + +上面的例子中,我们实例化了两个CustUser对象,并使用objectOutputStream将对象写入文件中,最后使用ObjectInputStream从文件中读取对象。 + +上面是最基本的使用。需要注意的是CustUser class中有一个serialVersionUID字段。 + +serialVersionUID是序列化对象的唯一标记,如果class中定义的serialVersionUID和序列化存储中的serialVersionUID一致,则表明这两个对象是一个对象,我们可以将存储的对象反序列化。 + +如果我们没有显示的定义serialVersionUID,则JVM会自动根据class中的字段,方法等信息生成。很多时候我在看代码的时候,发现很多人都将serialVersionUID设置为1L,这样做是不对的,因为他们没有理解serialVersionUID的真正含义。 + +## 重构序列化对象 + +假如我们有一个序列化的对象正在使用了,但是突然我们发现这个对象好像少了一个字段,要把他加上去,可不可以加呢?加上去之后原序列化过的对象能不能转换成这个新的对象呢? + +答案是肯定的,前提是两个版本的serialVersionUID必须一样。新加的字段在反序列化之后是空值。 + +## 序列化不是加密 + +有很多同学在使用序列化的过程中可能会这样想,序列化已经将对象变成了二进制文件,是不是说该对象已经被加密了呢? + +这其实是序列化的一个误区,序列化并不是加密,因为即使你序列化了,还是能从序列化之后的数据中知道你的类的结构。比如在RMI远程调用的环境中,即使是class中的private字段也是可以从stream流中解析出来的。 + +如果我们想在序列化的时候对某些字段进行加密操作该怎么办呢? + +这时候可以考虑在序列化对象中添加writeObject和readObject方法: + +```java +private String name; + private String address; + private int age; + + private void writeObject(ObjectOutputStream stream) + throws IOException + { + //给age加密 + age = age + 2; + log.info("age is {}", age); + stream.defaultWriteObject(); + } + + private void readObject(ObjectInputStream stream) + throws IOException, ClassNotFoundException + { + stream.defaultReadObject(); + log.info("age is {}", age); + //给age解密 + age = age - 2; + } +``` + +上面的例子中,我们为CustUser添加了一个age对象,并在writeObject中对age进行了加密(加2),在readObject中对age进行了解密(减2)。 + +注意,writeObject和readObject都是private void的方法。他们的调用是通过反射来实现的。 + +## 使用真正的加密 + +上面的例子, 我们只是对age字段进行了加密,如果我们想对整个对象进行加密有没有什么好的处理办法呢? + +JDK为我们提供了javax.crypto.SealedObject 和java.security.SignedObject来作为对序列化对象的封装。从而将整个序列化对象进行了加密。 + +还是举个例子: + +```java +public void testCusUserSealed() throws IOException, ClassNotFoundException, NoSuchPaddingException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, InvalidKeyException { + CustUser custUserA=new CustUser("jack","www.flydean.com"); + Cipher enCipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + Cipher deCipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + SecretKey secretKey = new SecretKeySpec("saltkey111111111".getBytes(), "AES"); + IvParameterSpec iv = new IvParameterSpec("vectorKey1111111".getBytes()); + enCipher.init(Cipher.ENCRYPT_MODE, secretKey, iv); + deCipher.init(Cipher.DECRYPT_MODE,secretKey,iv); + SealedObject sealedObject= new SealedObject(custUserA, enCipher); + + try(FileOutputStream fileOutputStream = new FileOutputStream("target/custUser.ser")){ + ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); + objectOutputStream.writeObject(sealedObject); + } + + try(FileInputStream fileInputStream = new FileInputStream("target/custUser.ser")){ + ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); + SealedObject custUser1 = (SealedObject) objectInputStream.readObject(); + CustUser custUserV2= (CustUser) custUser1.getObject(deCipher); + log.info("{}",custUserV2); + } + } +``` + +上面的例子中,我们构建了一个SealedObject对象和相应的加密解密算法。 + +SealedObject就像是一个代理,我们写入和读取的都是这个代理的加密对象。从而保证了在数据传输过程中的安全性。 + +## 使用代理 + +上面的SealedObject实际上就是一种代理,考虑这样一种情况,如果class中的字段比较多,而这些字段都可以从其中的某一个字段中自动生成,那么我们其实并不需要序列化所有的字段,我们只把那一个字段序列化就可以了,其他的字段可以从该字段衍生得到。 + +在这个案例中,我们就需要用到序列化对象的代理功能。 + +首先,序列化对象需要实现writeReplace方法,表示替换成真正想要写入的对象: + +```java +public class CustUserV3 implements java.io.Serializable{ + + private String name; + private String address; + + private Object writeReplace() + throws java.io.ObjectStreamException + { + log.info("writeReplace {}",this); + return new CustUserV3Proxy(this); + } +} +``` + +然后在Proxy对象中,需要实现readResolve方法,用于从系列化过的数据中重构序列化对象。如下所示: + +```java +public class CustUserV3Proxy implements java.io.Serializable{ + + private String data; + + public CustUserV3Proxy(CustUserV3 custUserV3){ + data =custUserV3.getName()+ "," + custUserV3.getAddress(); + } + + private Object readResolve() + throws java.io.ObjectStreamException + { + String[] pieces = data.split(","); + CustUserV3 result = new CustUserV3(pieces[0], pieces[1]); + log.info("readResolve {}",result); + return result; + } +} +``` + +我们看下怎么使用: + +```java +public void testCusUserV3() throws IOException, ClassNotFoundException { + CustUserV3 custUserA=new CustUserV3("jack","www.flydean.com"); + + try(FileOutputStream fileOutputStream = new FileOutputStream("target/custUser.ser")){ + ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); + objectOutputStream.writeObject(custUserA); + } + + try(FileInputStream fileInputStream = new FileInputStream("target/custUser.ser")){ + ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); + CustUserV3 custUser1 = (CustUserV3) objectInputStream.readObject(); + log.info("{}",custUser1); + } + } +``` + +注意,我们写入和读出的都是CustUserV3对象。 + +## Serializable和Externalizable的区别 + +最后我们讲下Externalizable和Serializable的区别。Externalizable继承自Serializable,它需要实现两个方法: + +```java + void writeExternal(ObjectOutput out) throws IOException; + void readExternal(ObjectInput in) throws IOException, ClassNotFoundException; +``` + +什么时候需要用到writeExternal和readExternal呢? + +使用Serializable,Java会自动为类的对象和字段进行对象序列化,可能会占用更多空间。而Externalizable则完全需要我们自己来控制如何写/读,比较麻烦,但是如果考虑性能的话,则可以使用Externalizable。 + +另外Serializable进行反序列化不需要执行构造函数。而Externalizable需要执行构造函数构造出对象,然后调用readExternal方法来填充对象。所以Externalizable的对象需要一个无参的构造函数。 + +# netty中对象的传输 + +在上面的序列化一节中,我们已经知道了对于定义好的JAVA对象,我们可以通过使用ObjectOutputStream和ObjectInputStream来实现对象的读写工作,那么在netty中是否也可以使用同样的方式来进行对象的读写呢? + +很遗憾的是,在netty中并不能直接使用JDK中的对象读写方法,我们需要对其进行改造。 + +这是因为我们需要一个通用的对象编码和解码器,如果使用ObjectOutputStream和ObjectInputStream,因为不同对象的结构是不一样的,所以我们在读取对象的时候需要知道读取数据的对象类型才能进行完美的转换。 + +而在netty中我们需要的是一种更加通用的编码解码器,那么应该怎么做呢? + +还记得之前我们在讲解通用的frame decoder中讲过的LengthFieldBasedFrameDecoder? 通过在真实的数据前面加上数据的长度,从而达到根据数据长度进行frame区分的目的。 + +netty中提供的编码解码器名字叫做ObjectEncoder和ObjectDecoder,先来看下他们的定义: + +``` +public class ObjectEncoder extends MessageToByteEncoder { +public class ObjectDecoder extends LengthFieldBasedFrameDecoder { +``` + +可以看到ObjectEncoder继承自MessageToByteEncoder,其中的泛型是Serializable,表示encoder是从可序列化的对象encode成为ByteBuf。 + +而ObjectDecoder正如上面我们所说的继承自LengthFieldBasedFrameDecoder,所以可以通过一个长度字段来区分实际要读取对象的长度。 + +接下来我们详细了解一下这两个类是如何工作的。 + +## ObjectEncoder + +先来看ObjectEncoder是如何将一个对象序列化成为ByteBuf的。 + +根据LengthFieldBasedFrameDecoder的定义,我们需要一个数组来保存真实数据的长度,这里使用的是一个4字节的byte数组叫做LENGTH_PLACEHOLDER,如下所示: + +``` +private static final byte[] LENGTH_PLACEHOLDER = new byte[4]; +``` + +我们看下它的encode方法的实现: + +``` + protected void encode(ChannelHandlerContext ctx, Serializable msg, ByteBuf out) throws Exception { + int startIdx = out.writerIndex(); + + ByteBufOutputStream bout = new ByteBufOutputStream(out); + ObjectOutputStream oout = null; + try { + bout.write(LENGTH_PLACEHOLDER); + oout = new CompactObjectOutputStream(bout); + oout.writeObject(msg); + oout.flush(); + } finally { + if (oout != null) { + oout.close(); + } else { + bout.close(); + } + } + int endIdx = out.writerIndex(); + out.setInt(startIdx, endIdx - startIdx - 4); + } +``` + +这里首先创建了一个ByteBufOutputStream,然后向这个Stream中写入4字节的长度字段,接着将ByteBufOutputStream封装到CompactObjectOutputStream中。 + +CompactObjectOutputStream是ObjectOutputStream的子类,它重写了writeStreamHeader和writeClassDescriptor两个方法。 + +CompactObjectOutputStream将最终的数据msg写入流中,一个encode的过程就差不多完成了。 + +为什么说差不多完成了呢?因为长度字段还是空的。 + +在最开始的时候,我们只是写入了一个长度的placeholder,这个placeholder是空的,并没有任何数据,这个数据是在最后一步out.setInt中写入的: + +``` +out.setInt(startIdx, endIdx - startIdx - 4); +``` + +这种实现也给了我们一种思路,在我们还不知道消息的真实长度的时候,如果希望在消息之前写入消息的长度,可以先占个位置,等消息全部读取完毕,知道真实的长度之后,再替换数据。 + +到此,对象数据已经全部编码完毕,接下来我们看一下如何从编码过后的数据中读取对象。 + +# ObjectDecoder + +之前说过了ObjectDecoder继承自LengthFieldBasedFrameDecoder,它的decode方法是这样的: + +``` + protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception { + ByteBuf frame = (ByteBuf) super.decode(ctx, in); + if (frame == null) { + return null; + } + + ObjectInputStream ois = new CompactObjectInputStream(new ByteBufInputStream(frame, true), classResolver); + try { + return ois.readObject(); + } finally { + ois.close(); + } + } +``` + +首先调用LengthFieldBasedFrameDecoder的decode方法,根据对象的长度,读取到真实的对象数据放到ByteBuf中。 + +然后通过自定义的CompactObjectInputStream从ByteBuf中读取到真实的对象,并返回。 + +CompactObjectInputStream继承自ObjectInputStream,是和CompactObjectOutputStream相反的操作。 + +# ObjectEncoderOutputStream和ObjectDecoderInputStream + +ObjectEncoder和ObjectDecoder是对象和ByteBuf之间的转换,netty还提供了和ObjectEncoder,ObjectDecoder兼容的ObjectEncoderOutputStream和ObjectDecoderInputStream,这两个类可以从stream中对对象编码和解码,并且和ObjectEncoder,ObjectDecoder完全兼容的。 + +# 总结 + +以上就是netty中提供的对象编码和解码器,大家如果希望在netty中传递对象,那么netty提供的这两个编码解码器是最好的选择。 \ No newline at end of file diff --git a/learn-netty文章/73.netty系列之使用Jboss Marshalling来序列化java对象.md b/learn-netty文章/73.netty系列之使用Jboss Marshalling来序列化java对象.md new file mode 100644 index 0000000..bf5cd17 --- /dev/null +++ b/learn-netty文章/73.netty系列之使用Jboss Marshalling来序列化java对象.md @@ -0,0 +1,190 @@ +# netty系列之:使用Jboss Marshalling来序列化java对象 + +文章目录 + + + +[简介](http://www.flydean.com/17-jboss-marshalling/#简介)[添加JBoss Marshalling依赖](http://www.flydean.com/17-jboss-marshalling/#添加JBoss_Marshalling依赖)[JBoss Marshalling的使用](http://www.flydean.com/17-jboss-marshalling/#JBoss_Marshalling的使用)[总结](http://www.flydean.com/17-jboss-marshalling/#总结) + +# 简介 + +在JAVA程序中经常会用到序列化的场景,除了JDK自身提供的Serializable之外,还有一些第三方的产品可以实现对JAVA对象的序列化。其中比较有名的就是Google protobuf。当然,也有其他的比较出名的序列化工具,比如Kryo和JBoss Marshalling。 + +今天想给大家介绍的就是JBoss Marshalling,为什么要介绍JBoss Marshalling呢? + +用过google protobuf的朋友可能都知道,虽然protobuf好用,但是需要先定义序列化对象的结构才能生成对应的protobuf文件。如果怕麻烦的朋友可能就不想考虑了。 + +JBoss Marshalling就是在JDK自带的java.io.Serializable中进行优化的一个序列化工具,用起来非常的简单,并且和java.io.Serializable兼容,所以是居家必备开发程序的好帮手。 + +根据JBoss官方的介绍,JBoss Marshalling和JDK java.io.Serializable相比有两个非常大的优点,第一个优点就是JBoss Marshalling解决了java.io.Serializable中使用的一些不便和问题。第二个优点就是JBoss Marshalling完全是可插拔的,这样就提供了对JBoss Marshalling框架进行扩展的可能,那么一起来看看JBoss Marshalling的使用吧。 + +# 添加JBoss Marshalling依赖 + +如果想用JBoss Marshalling,那么第一步就是添加JBoss Marshalling的依赖。 + +很奇怪的是如果你到JBoss Marshalling的官网上,可能会发现JBoss Marshalling很久都没有更新了,它的最新版本还是2011-04-27年出的1.3.0.CR9版本。 + +但是不要急,如果你去maven仓库搜一下,会发现最新的版本是2021年5月发行的2.0.12.Final版本。 + +所以这里我们就拿最新的2.0.12.Final版本为例进行讲解。 + +如果仔细观察JBoss Marshalling的maven仓库,可以看到JBoss Marshalling包含了4个依赖包,分别是JBoss Marshalling API,JBoss Marshalling River Protocol,JBoss Marshalling Serial Protocol和JBoss Marshalling OSGi Bundle。 + +JBoss Marshalling API是我们在程序中需要调用的API接口,这个是必须要包含的。JBoss Marshalling River Protocol和JBoss Marshalling Serial Protocol是marshalling的两种实现方式,可以根据需要自行取舍。 + +JBoss官网并没有太多关于这两个序列化实现的细节,我只能说,根据我的了解river的压缩程度更高。其他更多细节和实现可能只有具体阅读源码才知道了。 + +JBoss Marshalling OSGi Bundle是一个基于OSGi的可插拔的框架。 + +如果我们只是做对象的序列化,那么只需要使用JBoss Marshalling API和JBoss Marshalling River Protocol就行了。 + +``` + + org.jboss.marshalling + jboss-marshalling + 2.0.12.Final + + + org.jboss.marshalling + jboss-marshalling-river + 2.0.12.Final + +``` + +# JBoss Marshalling的使用 + +添加了依赖之后,我们就可以开始使用JBoss Marshalling了。JBoss Marshalling的使用非常简单,首先我们要根据选择的marshalling方式创建MarshallerFactory: + +``` + // 使用river作为marshalling的方式 + MarshallerFactory marshallerFactory = Marshalling.getProvidedMarshallerFactory("river"); +``` + +这里我们选择使用river作为marshalling的序列化方式。 + +有了MarshallerFactory,我们还需要一个MarshallingConfiguration为MarshallerFactory提供一些必要的序列化参数。 + +一般来说,我们可以控制MarshallingConfiguration的下面一些属性: + +``` +MarshallingConfiguration configuration = new MarshallingConfiguration(); + configuration.setVersion(4); + configuration.setClassCount(10); + configuration.setBufferSize(8096); + configuration.setInstanceCount(100); + configuration.setExceptionListener(new MarshallingException()); + configuration.setClassResolver(new SimpleClassResolver(getClass().getClassLoader())); +``` + +setVersion是设置使用的marshalling protocol的版本号,这个版本号非常重要,因为依赖的protocol实现可能根据会根据需要进行序列化实现的升级,可能产生不兼容的情况。通过设置版本号,可以保证升级之后的protocol也能兼容之前的序列化版本。 + +setClassCount是预设要序列化对象中的class个数。 + +setInstanceCount是预设序列化对象中的class实例个数。 + +setBufferSize设置读取数据的buff大小,通过调节这个属性可以调整序列化的性能。 + +setExceptionListener添加序列化异常的时候的异常监听器。 + +setClassResolver用来设置classloader。 + +JBoss Marshalling的强大之处在于我们在序列化的过程中还可以对对象进行拦截,从而进行日志输出或者其他的业务操作。 + +configuration提供了两个方法,分别是setObjectPreResolver和setObjectResolver。 + +这两个方法接受一个ObjectResolver对象,可以用来对对象进行处理。 + +两个方法的不同在于执行的顺序,preResolver在所有的resolver之前执行。 + +做好上面的配置之后,我们就可以正式开始编码了。 + +``` + final Marshaller marshaller = marshallerFactory.createMarshaller(configuration); + try(FileOutputStream os = new FileOutputStream(fileName)){ + marshaller.start(Marshalling.createByteOutput(os)); + marshaller.writeObject(obj); + marshaller.finish(); + } +``` + +上面的例子中,通过调用marshaller的start方法开启序列化,然后调用marshaller.writeObject写入对象。 + +最后调用marshaller.finish结束序列化。 + +整个序列化的代码如下所示: + +``` + public void marshallingWrite(String fileName, Object obj) throws IOException { + // 使用river作为marshalling的方式 + MarshallerFactory marshallerFactory = Marshalling.getProvidedMarshallerFactory("river"); + // 创建marshalling的配置 + MarshallingConfiguration configuration = new MarshallingConfiguration(); + // 使用版本号4 + configuration.setVersion(4); + + final Marshaller marshaller = marshallerFactory.createMarshaller(configuration); + try(FileOutputStream os = new FileOutputStream(fileName)){ + marshaller.start(Marshalling.createByteOutput(os)); + marshaller.writeObject(obj); + marshaller.finish(); + } + } + + public static void main(String[] args) throws IOException { + MarshallingWriter writer = new MarshallingWriter(); + Student student= new Student("jack", 18, "first grade"); + writer.marshallingWrite("/tmp/marshall.txt",student); + } +``` + +非常的简洁明了。 + +注意,这里我们序列化了一个Student对象,这个对象一定要实现java.io.Serializable接口,否则会抛出类型下面的异常: + +``` +Exception in thread "main" java.io.NotSerializableException: + at org.jboss.marshalling.river.RiverMarshaller.doWriteObject(RiverMarshaller.java:274) + at org.jboss.marshalling.AbstractObjectOutput.writeObject(AbstractObjectOutput.java:58) + at org.jboss.marshalling.AbstractMarshaller.writeObject(AbstractMarshaller.java:111) +``` + +接下来就是序列化的反向动作反序列化了。 + +代码很简单,我们直接上代码: + +``` + public void marshallingRead(String fileName) throws IOException, ClassNotFoundException { + // 使用river协议创建MarshallerFactory + MarshallerFactory marshallerFactory = Marshalling.getProvidedMarshallerFactory("river"); + // 创建配置文件 + MarshallingConfiguration configuration = new MarshallingConfiguration(); + // 使用版本号4 + configuration.setVersion(4); + final Unmarshaller unmarshaller = marshallerFactory.createUnmarshaller(configuration); + try(FileInputStream is = new FileInputStream(fileName)){ + unmarshaller.start(Marshalling.createByteInput(is)); + Student student=(Student)unmarshaller.readObject(); + log.info("student:{}",student); + unmarshaller.finish(); + } + } + + public static void main(String[] args) throws IOException, ClassNotFoundException { + MarshallingReader reader= new MarshallingReader(); + reader.marshallingRead("/tmp/marshall.txt"); + } +``` + +运行上面的代码,我们可能得到下面的输出: + +``` +[main] INFO c.f.marshalling.MarshallingReader - student:Student(name=jack, age=18, className=first grade) +``` + +可见读取序列化的对象已经成功。 + +# 总结 + +以上就是JBoss Marshalling的基本使用。通常对我们程序员来说,这个基本的使用已经足够了。除非你有根据复杂的序列化需求,比如对象中的密码需要在序列化的过程中进行替换,这种需求可以使用我们前面提到的ObjectResolver来实现。 + +本文的例子可以参考:[learn-netty4](https://github.com/ddean2009/learn-netty4) \ No newline at end of file diff --git a/learn-netty文章/74.netty系列之netty对marshalling的支持.md b/learn-netty文章/74.netty系列之netty对marshalling的支持.md new file mode 100644 index 0000000..a93d9db --- /dev/null +++ b/learn-netty文章/74.netty系列之netty对marshalling的支持.md @@ -0,0 +1,309 @@ +# netty系列之:netty对marshalling的支持 + +文章目录 + + + +[简介](http://www.flydean.com/17-1-netty-marshalling/#简介)[netty中的marshalling provider](http://www.flydean.com/17-1-netty-marshalling/#netty中的marshalling_provider)[Marshalling编码器](http://www.flydean.com/17-1-netty-marshalling/#Marshalling编码器)[Marshalling编码的另外一种实现](http://www.flydean.com/17-1-netty-marshalling/#Marshalling编码的另外一种实现)[总结](http://www.flydean.com/17-1-netty-marshalling/#总结) + +# 简介 + +在之前的文章中我们讲过了,jboss marshalling是一种非常优秀的java对象序列化的方式,它可以兼容JDK自带的序列化,同时也提供了性能和使用上的优化。 + +那么这么优秀的序列化工具可不可以用在netty中作为消息传递的方式呢? + +答案当然是肯定的,在netty中一切皆有可能。 + +# netty中的marshalling provider + +回顾一下jboss marshalling的常用用法,我们需要从MarshallerFactory中创建出Marshaller,因为mashaller有不同的实现,所以需要指定具体的实现来创建MarshallerFactory,如下所示: + +``` +MarshallerFactory marshallerFactory = Marshalling.getProvidedMarshallerFactory("river"); +``` + +这个MarshallerFactory实际上就是一个MarshallerProvider。 + +netty中定义了这样的一个接口: + +``` +public interface MarshallerProvider { + + Marshaller getMarshaller(ChannelHandlerContext ctx) throws Exception; +} +``` + +MarshallerProvider实际上就做了和MarshallerFactory等同的工作。 + +既然MarshallerProvider是一个接口,那么它有哪些实现呢? + +在netty中它有两个实现类,分别是DefaultMarshallerProvider和ThreadLocalMarshallerProvider。 + +两者有什么区别呢? + +先来看一下DefaultMarshallerProvider: + +``` +public class DefaultMarshallerProvider implements MarshallerProvider { + + private final MarshallerFactory factory; + private final MarshallingConfiguration config; + + public DefaultMarshallerProvider(MarshallerFactory factory, MarshallingConfiguration config) { + this.factory = factory; + this.config = config; + } + + public Marshaller getMarshaller(ChannelHandlerContext ctx) throws Exception { + return factory.createMarshaller(config); + } + +} +``` + +顾名思义,DefaultMarshallerProvider就是marshallerProvider的默认实现,从具体的实现代码中,我们可以看出,DefaultMarshallerProvider实际上需要传入MarshallerFactory和MarshallingConfiguration作为参数,然后使用传入的MarshallerFactory来创建具体的marshaller Provider,和我们手动创建marshaller的方式是一致的。 + +但是上面的实现中每次getMarshaller都需要重新从factory中创建一个新的,性能上可能会有问题。所以netty又实现了一个新的ThreadLocalMarshallerProvider: + +``` +public class ThreadLocalMarshallerProvider implements MarshallerProvider { + private final FastThreadLocal marshallers = new FastThreadLocal(); + + private final MarshallerFactory factory; + private final MarshallingConfiguration config; + + public ThreadLocalMarshallerProvider(MarshallerFactory factory, MarshallingConfiguration config) { + this.factory = factory; + this.config = config; + } + + @Override + public Marshaller getMarshaller(ChannelHandlerContext ctx) throws Exception { + Marshaller marshaller = marshallers.get(); + if (marshaller == null) { + marshaller = factory.createMarshaller(config); + marshallers.set(marshaller); + } + return marshaller; + } +} +``` + +ThreadLocalMarshallerProvider和DefaultMarshallerProvider的不同之处在于,ThreadLocalMarshallerProvider中保存了一个FastThreadLocal的对象,FastThreadLocal是JDK中ThreadLocal的优化版本,比ThreadLocal更快。 + +在getMarshaller方法中,先从FastThreadLocal中get出Marshaller对象,如果Marshaller对象不存在,才从factory中创建出一个Marshaller对象,最后将Marshaller对象放到ThreadLocal中。 + +有MarshallerProvider就有和他对应的UnMarshallerProvider: + +``` +public interface UnmarshallerProvider { + + Unmarshaller getUnmarshaller(ChannelHandlerContext ctx) throws Exception; +} +``` + +netty中的UnmarshallerProvider有三个实现类,分别是DefaultUnmarshallerProvider,ThreadLocalUnmarshallerProvider和ContextBoundUnmarshallerProvider. + +前面的两个DefaultUnmarshallerProvider,ThreadLocalUnmarshallerProvider跟marshaller的是实现是一样的,这里就不重复讲解了。 + +我们主要来看一下ContextBoundUnmarshallerProvider的实现。 + +从名字上我们可以看出,这个unmarshaller是和ChannelHandlerContext相关的。 + +ChannelHandlerContext表示的是channel的上下文环境,它里面有一个方法叫做attr,可以保存和channel相关的属性: + +``` + Attribute attr(AttributeKey key); +``` + +ContextBoundUnmarshallerProvider的做法就是将Unmarshaller存放到context中,每次使用的时候先从context中获取,如果没有取到再从factroy中获取。 + +我们来看下ContextBoundUnmarshallerProvider的实现: + +``` +public class ContextBoundUnmarshallerProvider extends DefaultUnmarshallerProvider { + + private static final AttributeKey UNMARSHALLER = AttributeKey.valueOf( + ContextBoundUnmarshallerProvider.class, "UNMARSHALLER"); + + public ContextBoundUnmarshallerProvider(MarshallerFactory factory, MarshallingConfiguration config) { + super(factory, config); + } + + @Override + public Unmarshaller getUnmarshaller(ChannelHandlerContext ctx) throws Exception { + Attribute attr = ctx.channel().attr(UNMARSHALLER); + Unmarshaller unmarshaller = attr.get(); + if (unmarshaller == null) { + unmarshaller = super.getUnmarshaller(ctx); + attr.set(unmarshaller); + } + return unmarshaller; + } +} +``` + +ContextBoundUnmarshallerProvider继承自DefaultUnmarshallerProvider,在getUnmarshaller方法首先从ctx取出unmarshaller,如果没有的话,则调用DefaultUnmarshallerProvider中的getUnmarshaller方法取出unmarshaller。 + +# Marshalling编码器 + +上面的章节中我们获取到了marshaller,接下来看一下如何使用marshaller来进行编码和解码操作。 + +首先来看一下编码器MarshallingEncoder,MarshallingEncoder继承自MessageToByteEncoder,接收的泛型是Object: + +``` +public class MarshallingEncoder extends MessageToByteEncoder +``` + +是将Object对象编码成为ByteBuf。回顾一下之前我们讲到的通常对象的编码都需要用到一个对象长度的字段,用来分割对象的数据,同样的MarshallingEncoder也提供了一个4个字节的LENGTH_PLACEHOLDER,用来存储对象的长度。 + +具体的看一下它的encode方法: + +``` + protected void encode(ChannelHandlerContext ctx, Object msg, ByteBuf out) throws Exception { + Marshaller marshaller = provider.getMarshaller(ctx); + int lengthPos = out.writerIndex(); + out.writeBytes(LENGTH_PLACEHOLDER); + ChannelBufferByteOutput output = new ChannelBufferByteOutput(out); + marshaller.start(output); + marshaller.writeObject(msg); + marshaller.finish(); + marshaller.close(); + + out.setInt(lengthPos, out.writerIndex() - lengthPos - 4); + } +``` + +encode的逻辑很简单,首先从provider中拿到marshaller对象,然后先向out中写入4个字节的LENGTH_PLACEHOLDER,接着使用marshaller向 +out中写入编码的对象,最后根据写入对象长度填充out,得到最后的输出。 + +因为encode的数据保存的有长度数据,所以decode的时候就需要用到一个frame decoder叫做LengthFieldBasedFrameDecoder。 + +通常有两种方式来使用LengthFieldBasedFrameDecoder,一种是将LengthFieldBasedFrameDecoder加入到pipline handler中,decoder只需要处理经过frame decoder处理过后的对象即可。 + +还有一种方法就是这个decoder本身就是一个LengthFieldBasedFrameDecoder。 + +这里netty选择的是第二种方法,我们看下MarshallingDecoder的定义: + +``` +public class MarshallingDecoder extends LengthFieldBasedFrameDecoder +``` + +首先需要在构造函数中指定LengthFieldBasedFrameDecoder的字段长度,这里调用了super方法来实现: + +``` + public MarshallingDecoder(UnmarshallerProvider provider, int maxObjectSize) { + super(maxObjectSize, 0, 4, 0, 4); + this.provider = provider; + } +``` + +并且重写了extractFrame方法: + +``` + protected ByteBuf extractFrame(ChannelHandlerContext ctx, ByteBuf buffer, int index, int length) { + return buffer.slice(index, length); + } +``` + +最后再看下decode方法: + +``` + protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception { + ByteBuf frame = (ByteBuf) super.decode(ctx, in); + if (frame == null) { + return null; + } + Unmarshaller unmarshaller = provider.getUnmarshaller(ctx); + ByteInput input = new ChannelBufferByteInput(frame); + try { + unmarshaller.start(input); + Object obj = unmarshaller.readObject(); + unmarshaller.finish(); + return obj; + } finally { + unmarshaller.close(); + } + } +``` + +decode的逻辑也很简单,首先调用super方法decode出frame ByteBuf。然后再调用unmarshaller实现对象的读取,最后将改对象返回。 + +# Marshalling编码的另外一种实现 + +上面我们讲到对对象的编码使用的是LengthFieldBasedFrameDecoder,根据对象实际数据之前的一个length字段来确定字段的长度,从而读取真实的数据。 + +那么可不可以不指定对象长度也能够准确的读取对象呢? + +其实也是可以的,我们可以不断的尝试读取数据,直到找到合适的对象数据为止。 + +看过我之前文章的朋友可能就想到了,ReplayingDecoder不就是做这个事情的吗?在ReplayingDecoder中会不断的重试,直到找到符合条件的消息为止。 + +于是netty基于ReplayingDecoder也有一个marshalling编码解码的实现,叫做CompatibleMarshallingEncoder和CompatibleMarshallingDecoder。 + +CompatibleMarshallingEncoder很简单,因为不需要对象的实际长度,所以直接使用marshalling编码即可。 + +``` +public class CompatibleMarshallingEncoder extends MessageToByteEncoder { + + private final MarshallerProvider provider; + + public CompatibleMarshallingEncoder(MarshallerProvider provider) { + this.provider = provider; + } + + @Override + protected void encode(ChannelHandlerContext ctx, Object msg, ByteBuf out) throws Exception { + Marshaller marshaller = provider.getMarshaller(ctx); + marshaller.start(new ChannelBufferByteOutput(out)); + marshaller.writeObject(msg); + marshaller.finish(); + marshaller.close(); + } +} +``` + +CompatibleMarshallingDecoder继承了ReplayingDecoder: + +``` +public class CompatibleMarshallingDecoder extends ReplayingDecoder +``` + +它的decode方法的核心就是调用unmarshaller的方法: + +``` +Unmarshaller unmarshaller = provider.getUnmarshaller(ctx); + ByteInput input = new ChannelBufferByteInput(buffer); + if (maxObjectSize != Integer.MAX_VALUE) { + input = new LimitingByteInput(input, maxObjectSize); + } + try { + unmarshaller.start(input); + Object obj = unmarshaller.readObject(); + unmarshaller.finish(); + out.add(obj); + } catch (LimitingByteInput.TooBigObjectException ignored) { + discardingTooLongFrame = true; + throw new TooLongFrameException(); + } finally { + unmarshaller.close(); + } +``` + +注意,这里解码的时候会有两种异常,第一种异常就是unmarshaller.readObject时候的异常,这种异常会被ReplayingDecoder捕获从而重试。 + +还有一种就是字段太长的异常,这种异常无法处理只能放弃: + +``` + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + if (cause instanceof TooLongFrameException) { + ctx.close(); + } else { + super.exceptionCaught(ctx, cause); + } + } +``` + +# 总结 + +以上就是在netty中使用marshalling进行编码解码的实现。原理和对象编码解码是很类似的,大家可以对比分析一下。 \ No newline at end of file diff --git a/learn-netty文章/75.netty系列之protobuf在UDP协议中的使用.md b/learn-netty文章/75.netty系列之protobuf在UDP协议中的使用.md new file mode 100644 index 0000000..c89470b --- /dev/null +++ b/learn-netty文章/75.netty系列之protobuf在UDP协议中的使用.md @@ -0,0 +1,161 @@ +# netty系列之:protobuf在UDP协议中的使用 + +文章目录 + + + +[简介](http://www.flydean.com/17-1-netty-protobuf-udp/#简介)[UDP在netty中的表示](http://www.flydean.com/17-1-netty-protobuf-udp/#UDP在netty中的表示)[DatagramPacketEncoder](http://www.flydean.com/17-1-netty-protobuf-udp/#DatagramPacketEncoder)[DatagramPacketDecoder](http://www.flydean.com/17-1-netty-protobuf-udp/#DatagramPacketDecoder)[总结](http://www.flydean.com/17-1-netty-protobuf-udp/#总结) + +# 简介 + +netty中提供的protobuf编码解码器可以让我们直接在netty中传递protobuf对象。同时netty也提供了支持UDP协议的channel叫做NioDatagramChannel。如果直接使用NioDatagramChannel,那么我们可以直接从channel中读写UDP对象:DatagramPacket。 + +但是DatagramPacket中封装的是ByteBuf对象,如果我们想要向UDP channel中写入对象,那么需要一个将对象转换成为ByteBuf的方法,很明显netty提供的protobuf编码解码器就是一个这样的方法。 + +那么可不可以将NioDatagramChannel和ProtobufDecoder,ProtobufEncoder相结合呢? + +NioDatagramChannel中channel读写的对象都是DatagramPacket。而ProtobufDecoder与ProtobufEncoder是将protoBuf对象MessageLiteOrBuilder跟ByteBuf进行转换,所以两者是不能直接结合使用的。 + +怎么才能在UDP中使用protobuf呢?今天要向大家介绍netty专门为UDP创建的编码解码器DatagramPacketEncoder和DatagramPacketDecoder。 + +# UDP在netty中的表示 + +UDP的数据包在netty中是怎么表示呢? + +netty提供了一个类DatagramPacket来表示UDP的数据包。netty中的UDP channel就是使用DatagramPacket来进行数据的传递。先看下DatagramPacket的定义: + +``` +public class DatagramPacket + extends DefaultAddressedEnvelope implements ByteBufHolder +``` + +DatagramPacket继承自DefaultAddressedEnvelope,并且实现了ByteBufHolder接口。 + +其中的ByteBuf是数据包中需要传输的数据,InetSocketAddress是数据包需要发送到的地址。 + +而这个DefaultAddressedEnvelope又是继承自AddressedEnvelope: + +``` +public class DefaultAddressedEnvelope implements AddressedEnvelope +``` + +DefaultAddressedEnvelopee中有三个属性,分别是message,sender和recipient: + +``` + private final M message; + private final A sender; + private final A recipient; +``` + +这三个属性分别代表了要发送的消息,发送方的地址和接收方的地址。 + +# DatagramPacketEncoder + +DatagramPacketEncoder是一个DatagramPacket的编码器,所以要编码的对象就是DatagramPacket。上一节我们也提到了DatagramPacket实际上继承自AddressedEnvelope。所有的DatagramPacket都是一个AddressedEnvelope对象,所以为了通用起见,DatagramPacketEncoder接受的要编码的对象是AddressedEnvelope。 + +我们先来看下DatagramPacketEncoder的定义: + +``` +public class DatagramPacketEncoder extends MessageToMessageEncoder> { +``` + +DatagramPacketEncoder是一个MessageToMessageEncoder,它接受一个AddressedEnvelope的泛型,也就是我们要encoder的对象类型。 + +那么DatagramPacketEncoder会将AddressedEnvelope编码成什么呢? + +DatagramPacketEncoder中定义了一个encoder,这个encoder可以在DatagramPacketEncoder初始化的时候传入: + +``` +private final MessageToMessageEncoder encoder; + + public DatagramPacketEncoder(MessageToMessageEncoder encoder) { + this.encoder = checkNotNull(encoder, "encoder"); + } +``` + +实际上DatagramPacketEncoder中实现的encode方法,底层就是调用encoder的encode方法,我们来看下他的实现: + +``` + protected void encode( + ChannelHandlerContext ctx, AddressedEnvelope msg, List out) throws Exception { + assert out.isEmpty(); + + encoder.encode(ctx, msg.content(), out); + if (out.size() != 1) { + throw new EncoderException( + StringUtil.simpleClassName(encoder) + " must produce only one message."); + } + Object content = out.get(0); + if (content instanceof ByteBuf) { + // Replace the ByteBuf with a DatagramPacket. + out.set(0, new DatagramPacket((ByteBuf) content, msg.recipient(), msg.sender())); + } else { + throw new EncoderException( + StringUtil.simpleClassName(encoder) + " must produce only ByteBuf."); + } + } +``` + +上面的逻辑就是从AddressedEnvelope中调用`msg.content()`方法拿到AddressedEnvelope中的内容,然后调用encoder的encode方法将其编码并写入到out中。 + +最后调用out的get方法拿出编码之后的内容,再封装到DatagramPacket中去。 + +所以不管encoder最后返回的是什么对象,最后都会被封装到DatagramPacket中,并返回。 + +总结一下,DatagramPacketEncoder传入一个AddressedEnvelope对象,调用encoder将AddressedEnvelope的内容进行编码,最后封装成为一个DatagramPacket并返回。 + +鉴于protoBuf的优异对象序列化能力,我们可以将ProtobufEncoder传入到DatagramPacketEncoder中,做为真实的encoder: + +``` + ChannelPipeline pipeline = ...; +pipeline.addLast("udpEncoder", new DatagramPacketEncoder(new ProtobufEncoder(...)); +``` + +这样就把ProtobufEncoder和DatagramPacketEncoder结合起来了。 + +# DatagramPacketDecoder + +DatagramPacketDecoder是和DatagramPacketEncoder相反的操作,它是将接受到的DatagramPacket对象进行解码,至于解码成为什么对象,也是由传入其中的decoder属性来决定的: + +``` +public class DatagramPacketDecoder extends MessageToMessageDecoder { + + private final MessageToMessageDecoder decoder; + + public DatagramPacketDecoder(MessageToMessageDecoder decoder) { + this.decoder = checkNotNull(decoder, "decoder"); + } +``` + +DatagramPacketDecoder要解码的对象是DatagramPacket,而传入的decoder要解码的对象是ByteBuf。 + +所以我们需要一个能够解码ByteBuf的decoder实现,而和protoBuf对应的就是ProtobufDecoder。 + +先来看下DatagramPacketDecoder的decoder方法是怎么实现的: + +``` + protected void decode(ChannelHandlerContext ctx, DatagramPacket msg, List out) throws Exception { + decoder.decode(ctx, msg.content(), out); + } +``` + +可以看到DatagramPacketDecoder的decoder方法很简单,就是从DatagramPacket中拿到content内容,然后交由decoder去decode。 + +如果使用ProtobufDecoder作为内置的decoder,则可以将ByteBuf对象decode成为ProtoBuf对象,刚好和之前讲过的encode相呼应。 + +将ProtobufDecoder传入DatagramPacketDecoder也非常简单,我们可以这样做: + +``` + ChannelPipeline pipeline = ...; + pipeline.addLast("udpDecoder", new DatagramPacketDecoder(new ProtobufDecoder(...)); +``` + +这样一个DatagramPacketDecoder就完成了。 + +# 总结 + +可以看到,如果直接使用DatagramPacketEncoder和DatagramPacketDecoder加上ProtoBufEncoder和ProtoBufDecoder,那么实现的是DatagramPacket和ByteBuf直接的互相转换。 + +当然这里的ProtoBufEncoder和ProtoBufDecoder可以按照用户的需要被替换成为不同的编码解码器。 + +可以自由组合编码解码方式,就是netty编码器的最大魅力。 \ No newline at end of file diff --git a/learn-netty文章/76.netty系列之可能有人听过ThreadLocal,但一定没人听过ThreadLocal对象池.md b/learn-netty文章/76.netty系列之可能有人听过ThreadLocal,但一定没人听过ThreadLocal对象池.md new file mode 100644 index 0000000..167e0ba --- /dev/null +++ b/learn-netty文章/76.netty系列之可能有人听过ThreadLocal,但一定没人听过ThreadLocal对象池.md @@ -0,0 +1,286 @@ +# netty系列之:可能有人听过ThreadLocal,但一定没人听过ThreadLocal对象池 + +文章目录 + + + +[简介](http://www.flydean.com/47-netty-thread-local-object-pool-2/#简介)[ThreadLocal](http://www.flydean.com/47-netty-thread-local-object-pool-2/#ThreadLocal)[ThreadLocalMap](http://www.flydean.com/47-netty-thread-local-object-pool-2/#ThreadLocalMap)[Recycler](http://www.flydean.com/47-netty-thread-local-object-pool-2/#Recycler)[总结](http://www.flydean.com/47-netty-thread-local-object-pool-2/#总结) + +# 简介 + +JDK中的Thread大家肯定用过,只要是用过异步编程的同学肯定都熟悉。为了保存Thread中特有的变量,JDK引入了ThreadLocal类来专门对Thread的本地变量进行管理。 + +# ThreadLocal + +很多新人可能不明白ThreadLocal到底是什么,它和Thread到底有什么关系。 + +其实很简单,ThreadLocal本质上是一个key,它的value就是Thread中一个map中存储的值。 + +每个Thread中都有一个Map, 这个Map的类型是ThreadLocal.ThreadLocalMap。我们先不具体讨论这个ThreadLocalMap到底是怎么实现的。现在就简单将其看做是一个map即可。 + +接下来,我们看下一个ThreadLocal的工作流程。 + +首先来看一下ThreadLocal的使用例子: + +``` + public class ThreadId { + // 一个线程ID的自增器 + private static final AtomicInteger nextId = new AtomicInteger(0); + + // 为每个Thread分配一个线程 + private static final ThreadLocal threadId = + new ThreadLocal() { + @Override protected Integer initialValue() { + return nextId.getAndIncrement(); + } + }; + + // 返回当前线程的ID + public static int get() { + return threadId.get(); + } + } +``` + +上面的类是做什么用的呢? + +当你在不同的线程环境中调用ThreadId的get方法时候,会返回不同的int值。所以可以看做是ThreadId为每个线程生成了一个线程ID。 + +我们来看下它是怎么工作的。 + +首先我们调用了ThreadLocal的get方法。ThreadLocal中的get方法定义如下: + +``` + public T get() { + Thread t = Thread.currentThread(); + ThreadLocalMap map = getMap(t); + if (map != null) { + ThreadLocalMap.Entry e = map.getEntry(this); + if (e != null) { + @SuppressWarnings("unchecked") + T result = (T)e.value; + return result; + } + } + return setInitialValue(); + } +``` + +get方法中,我们第一步获取当前的线程Thread,然后getMap返回当前Thread中的ThreadLocalMap对象。 + +如果Map不为空,则取出以当前ThreadLocal为key对应的值。 + +如果Map为空,则调用初始化方法: + +``` + private T setInitialValue() { + T value = initialValue(); + Thread t = Thread.currentThread(); + ThreadLocalMap map = getMap(t); + if (map != null) + map.set(this, value); + else + createMap(t, value); + return value; + } +``` + +初始化方法首先判断当前Thread中的ThreadLocalMap是否为空,如果不为空则设置初始化的值。 + +如果为空则创建新的Map: + +``` + void createMap(Thread t, T firstValue) { + t.threadLocals = new ThreadLocalMap(this, firstValue); + } +``` + +大家可以看到整个ThreadLocalMap中的Key就是ThreadLocal本身,而Value就是ThreadLocal中定义的泛型的值。 + +现在我们来总结一下ThreadLocal到底是做什么的。 + +每个Thread中都有一个ThreadLocal.ThreadLocalMap的Map对象,我们希望向这个Map中存放一些特定的值,通过一个特定的对象来访问到存放在Thread中的这个值,这样的对象就是ThreadLocal。 + +通过ThreadLocal的get方法,就可以返回绑定到不同Thread对象中的值。 + +# ThreadLocalMap + +上面我们简单的将ThreadLocalMap看做是一个map。事实上ThreadLocalMap是一个对象,它里面存放的每个值都是一个Entry. + +这个Entry不同于Map中的Entry,它是一个static的内部类: + +``` + static class Entry extends WeakReference> { + /** The value associated with this ThreadLocal. */ + Object value; + + Entry(ThreadLocal k, Object v) { + super(k); + value = v; + } + } +``` + +注意,这里的Entry继承自WeakReference,表示这个Entry的key是弱引用对象,如果key没有强引用的情况下,会在gc中被回收。从而保证了Map中数据的有效性。 + +ThreadLocalMap中的值都存放在Entry数组中: + +``` +private Entry[] table; +``` + +我们看一下怎么从ThreadLocalMap中get一个值的: + +``` + private Entry getEntry(ThreadLocal key) { + int i = key.threadLocalHashCode & (table.length - 1); + Entry e = table[i]; + if (e != null && e.get() == key) + return e; + else + return getEntryAfterMiss(key, i, e); + } +``` + +key.threadLocalHashCode 可以简单的看做是ThreadLocal代表的key的值。 + +而 key.threadLocalHashCode & (table.length – 1) 则使用来计算当前key在table中的index。 + +这里使用的是位运算,用来提升计算速度。实际上这个计算等同于: + +``` +key.threadLocalHashCode % table.length +``` + +是一个取模运算。 + +如果按照取模运算的index去查找,找到就直接返回。 + +如果没找到则会遍历调用nextIndex方法,修改index的值,只到查找完毕为止: + +``` + private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) { + Entry[] tab = table; + int len = tab.length; + + while (e != null) { + ThreadLocal k = e.get(); + if (k == key) + return e; + if (k == null) + expungeStaleEntry(i); + else + i = nextIndex(i, len); + e = tab[i]; + } + return null; + } +``` + +# Recycler + +ThreadLocal本质上是将ThreadLocal这个对象和不同的Thread进行绑定,通过ThreadLocal的get方法可以获得存储在不同Thread中的值。 + +归根结底,ThreadLocal和Thread是一对多的关系。因为ThreadLocal在ThreadLocalMap中是弱引用,所以当ThreadLocal被置为空之后,对应的ThreadLocalMap中的对象会在下一个垃圾回收过程中被回收,从而为Thread中的ThreadLocalMap节省一个空间。 + +那么当我们的Thread是一个长时间运行的Thread的时候,如果在这个Thread中分配了很多生命周期很短的对象,那么会生成很多待回收的垃圾对象,给垃圾回收器造成压力。 + +为了解决这个问题,netty为我们提供了Recycler类,用来回收这些短生命周期的对象。接下来,我们来探究一下Recycler到底是怎么工作的。 + +在这之前,我们先看下怎么使用Recycler。 + +``` +public class MyObject { + + private static final Recycler RECYCLER = new Recycler() { + protected MyObject newObject(Recycler.Handle handle) { + return new MyObject(handle); + } + } + + public static MyObject newInstance(int a, String b) { + MyObject obj = RECYCLER.get(); + obj.myFieldA = a; + obj.myFieldB = b; + return obj; + } + + private final Recycler.Handle handle; + private int myFieldA; + private String myFieldB; + + private MyObject(Handle handle) { + this.handle = handle; + } + + public boolean recycle() { + myFieldA = 0; + myFieldB = null; + return handle.recycle(this); + } +} + +MyObject obj = MyObject.newInstance(42, "foo"); +... +obj.recycle(); +``` + +本质上,Recycler就像是一个工厂类,通过它的get方法来生成对应的类对象。当这个对象需要被回收的时候,调用Recycler.Handle中的recycle方法,即可将对象回收。 + +先看一下生成对象的get方法: + +``` + public final T get() { + if (maxCapacityPerThread == 0) { + return newObject((Handle) NOOP_HANDLE); + } + Stack stack = threadLocal.get(); + DefaultHandle handle = stack.pop(); + if (handle == null) { + handle = stack.newHandle(); + handle.value = newObject(handle); + } + return (T) handle.value; + } +``` + +上面代码的含义就是,先判断是否超过了单个线程允许的最大容量,如果是,则返回一个新的对象,绑定一个空的handler,表示这个新创建的对象是不可以被回收的。 + +如果不是,则从threadLocal中拿到当前线程绑定的Stack。然后从Stack中取出最上面的元素,如果Stack中没有对象,则创建新的对象,并绑定handle。 + +最后返回handle绑定的对象。 + +再看一下handle的回收对象方法recycle: + +``` + public void recycle(Object object) { + if (object != value) { + throw new IllegalArgumentException("object does not belong to handle"); + } + + Stack stack = this.stack; + if (lastRecycledId != recycleId || stack == null) { + throw new IllegalStateException("recycled already"); + } + + stack.push(this); + } +``` + +上面的代码先去判断和handle绑定的对象是不是要回收的对象。只有相等的时候才进行回收。 + +而回收的本质就是将对象push到stack中,供后续get的时候取出使用。 + +所以,Recycler能够节约垃圾回收对象个数的原因是,它会将不再使用的对象存储到Stack中,并在下次get的时候返回,重复使用。这也就是我们在回收需要重置对象属性的原因: + +``` + public boolean recycle() { + myFieldA = 0; + myFieldB = null; + return handle.recycle(this); + } +``` + +# 总结 + +如果你在一个线程中会有多个同类型的短生命周期对象,那么不妨试试Recycle吧。 \ No newline at end of file diff --git a/learn-netty文章/77.网络协议之haproxy的Proxy Protocol代理协议.md b/learn-netty文章/77.网络协议之haproxy的Proxy Protocol代理协议.md new file mode 100644 index 0000000..65bce82 --- /dev/null +++ b/learn-netty文章/77.网络协议之haproxy的Proxy Protocol代理协议.md @@ -0,0 +1,279 @@ +# 网络协议之:haproxy的Proxy Protocol代理协议 + +文章目录 + + + +[简介](http://www.flydean.com/20-haproxy-protocol/#简介)[Proxy Protocol的实现细节](http://www.flydean.com/20-haproxy-protocol/#Proxy_Protocol的实现细节)[Proxy Protocol的使用情况](http://www.flydean.com/20-haproxy-protocol/#Proxy_Protocol的使用情况)[总结](http://www.flydean.com/20-haproxy-protocol/#总结) + +# 简介 + +代理大家应该都很熟悉了,比较出名的像是nginx,apache HTTPD,stunnel等。 + +我们知道代理就是代替客户端向服务器端进行消息请求,并且希望在代理的过程中保留初始的TCP连接信息,例如源和目标IP和端口等,以提供一些个性化的操作。 + +一般情况下,为了实现这个目标,有一些现成的解决办法,比如在HTTP协议中,可以使用“X-Forwarded-For”标头,来包含有关原始源地址,还有”X-Original-To”用来携带目的地址的信息。 + +又比如在SMTP协议中,可以特别使用XCLIENT协议来进行邮件交换。 + +或者可以通过编译内核,把你的代理作为你服务器的默认网关。 + +这些方式虽然可用,但是或多或少有一些限制,要么与协议相关,要么修改修改系统架构,从而可扩展性不强。 + +尤其是在多个代理服务器链式调用的情况下,上述方法几乎是不可能完成的。 + +这就需要一个统一的代理协议,通过所有的节点都兼容这个代理协议就可以无缝实现代理的链式调用。这个代理协议就是haproxy在2010年提出的proxy Protocol。 + +这个代理协议的优点是: + +- 它与协议无关(可以与任何7层协议一起使用,即使在加密的情况也可用) +- 它不需要任何基础架构更改 +- 可以穿透NAT防火墙 +- 它是可扩展的 + +而haproxy本身就是一个非常优秀的开源负载均衡和代理软件,提供了高负载能力和优秀的性能,所以在很多公司得以广泛的使用,比如:GoDaddy, GitHub, Bitbucket,Stack Overflow,Reddit, Slack,Speedtest.net, Tumblr, Twitter等。 + +今天要介绍的就是haproxy的Proxy Protocol代理协议的底层细节。 + +# Proxy Protocol的实现细节 + +上面我们提到了Proxy Protocol的目的就是可以携带一些可以标记初始的TCP连接信息的字段,比如IP地址和端口等。 + +如果是客户端和服务器端直连,那么服务器端可以通过getsockname和getpeername获得如下的信息: + +- address family: AF_INET for IPv4, AF_INET6 for IPv6, AF_UNIX +- socket protocol: SOCK_STREAM for TCP, SOCK_DGRAM for UDP +- 网络层的源和目标地址 +- 传输层的源和目标的端口号 + +所以Proxy Protocol的目的就是封装上面的这些信息,然后将上述信息放到请求头中去,这样服务器端就可以正确读取客户端的信息。 + +在Proxy Protocol中,定义了两个版本。 + +在版本1中,头文件信息是文本形式的,也就是人类可读的,采用这种方式,主要是为了在协议应用的早期保证更好的可调试性,从而快速景修正。 + +在版本2中,提供了对头文件的二进制编码功能,在版本1的功能已经基本完善的前提下,提供二进制编码,可以有效的提高应用的传输和处理性能。 + +因为有两个版本,所以在服务器的接收端也需要实现对相应版本的支持。 + +为了更好的应用Proxy Protocol,Proxy Protocol实际只定义了一个header信息,这个请求头会在连接发起者发起连接的时候放在每个连接的开头。并且该协议是无状态的,因为它不期望发送者在发送标头之前等待接收者,也不期望接收者发送回任何内容。 + +接下来,我们具体观察一下两个版本协议的实现。 + +## 版本1 + +在版本1中,proxy header是由一串US-ASCII编码的字符串组成的。这个proxy header将会在客户端和服务器端建立连接,并且发送任何真实数据之前发送。 + +先来看一个使用了proxy header的http请求的例子: + +``` + PROXY TCP4 192.168.0.1 192.168.0.102 12345 443\r\n + GET / HTTP/1.1\r\n + Host: 192.168.0.102\r\n + \r\n +``` + +上面的例子中,\r\n表示的是回车换行,也就是行结束的标记。该代码向host:192.168.0.102发送了一个HTTP请求,第一行的内容就是使用的proxy header。 + +具体什么含义呢? + +首先是字符串”PROXY”,表示这是一个proxy protocol的header,并且是v1版本的。 + +接着是一个空格分隔符。 + +然后是proxy使用的INET protocol 和 family。对于v1版本来说,支持”TCP4″和”TCP6″这两种方式。上面的例子中,我们使用的是TCP4. + +如果要使用其他的协议,那么可以设置为”UNKNOWN”。如果设置为”UNKNOWN”,那么后面到CRLF之前的数据将会被忽略。 + +接着是一个空格分隔符。 + +然后是网络层源的IP地址,根据选的是TCP4还是TCP6,对应的源IP地址也有不同的表示形式。 + +接着是一个空格分隔符。 + +然后是网络层目标地址的IP地址,根据选的是TCP4还是TCP6,对应的源IP地址也有不同的表示形式。 + +接着是一个空格分隔符。 + +然后是TCP源的端口号,取值范围是0-65535。 + +接着是一个空格分隔符。 + +然后是TCP目标地址的端口号,取值范围是0-65535。 + +接着是CRLF结束符。 + +这样一个v1版本的proxy protocol就定义完了,是不是很简单。 + +根据这样的定义,我们很好来计算整个proxy protocol的最大长度,对于TC4来说,最大的长度表示为: + +``` + - TCP/IPv4 : + "PROXY TCP4 255.255.255.255 255.255.255.255 65535 65535\r\n" + => 5 + 1 + 4 + 1 + 15 + 1 + 15 + 1 + 5 + 1 + 5 + 2 = 56 chars +``` + +对于TCP6来说,最大的长度表示为: + +``` + - TCP/IPv6 : + "PROXY TCP6 ffff:f...f:ffff ffff:f...f:ffff 65535 65535\r\n" + => 5 + 1 + 4 + 1 + 39 + 1 + 39 + 1 + 5 + 1 + 5 + 2 = 104 chars +``` + +对于UNKNOWN来说,可能有下面的最小长度和最大长度表示为: + +``` + - unknown connection (short form) : + "PROXY UNKNOWN\r\n" + => 5 + 1 + 7 + 2 = 15 chars + + - worst case (optional fields set to 0xff) : + "PROXY UNKNOWN ffff:f...f:ffff ffff:f...f:ffff 65535 65535\r\n" + => 5 + 1 + 7 + 1 + 39 + 1 + 39 + 1 + 5 + 1 + 5 + 2 = 107 chars +``` + +所以,总体来说108个字符已经足够v1版本使用了。 + +## 版本2 + +版本2主要是实现的二进制编码,虽然对人类可读不友好,但是可以提高传输和解析效率。 + +版本2的header是以下面12 bytes开头的block: + +``` +x0D \x0A \x0D \x0A \x00 \x0D \x0A \x51 \x55 \x49 \x54 \x0A +``` + +接下来的一个byte(13 bytes)是protocol version 和 command。因为一个byte是8个bits,使用一个byte来保存有点太奢侈了。所以将其拆分成两部分。 + +高位的4个bits保存的是版本,这里版本号必须是”\x2″。 + +低位的4个bits保存的是command,有下面几个值: + +- LOCAL(\x0): 表示连接是由代理自己发起的,一般用在代理向服务器发送健康检查时。 + + + +- PROXY(\x1): 代表连接是由另外一个节点发起的,这是一个proxy代理请求。 然后接收者必须使用协议块中提供的信息来获取原始地址。 + +- 其他:其他command都需要被丢弃,因为不可识别。 + +接下来的一个byte(14 bytes)保存的是transport protocol 和 address family。 + +其中高4位保存的是address family,低4位保存的是transport protocol。 + +address family可能有下面的值: + +- AF_UNSPEC(0x0): 表示的是不支持的,或者未定义的protocol。当sender发送LOCAL command或者处理为止protocol families的时候就可以使用这个值。 +- AF_INET(0x1):表示的是IPv4地址,占用4bytes。 +- AF_INET6(0x2):表示的是IPv6地址,占用16bytes。 +- AF_UNIX(0x3):表示的是unix address地址,占用108 bytes。 + +transport protocol可能有下面的值: + +- UNSPEC(0x0): 未知协议类型。 +- STREAM(0x1):使用的是SOCK_STREAM protocol,比如TCP 或者UNIX_STREAM。 +- DGRAM(0x2):使用的是SOCK_DGRAM protocol,比如UDP 或者UNIX_DGRAM。 + +低4位和高4位进行组合,可以得到下面几种值: + +- UNSPEC(\x00) +- TCP over IPv4(\x11) +- UDP over IPv4(\x12) +- TCP over IPv6(\x21) +- UDP over IPv6(\x22) +- UNIX stream(\x31) +- UNIX datagram(\x32) + +第15和16 bytes表示的剩下的字段的长度,综上,16-byte的v2可以用下面的结构体表示: + +``` + struct proxy_hdr_v2 { + uint8_t sig[12]; /* hex 0D 0A 0D 0A 00 0D 0A 51 55 49 54 0A */ + uint8_t ver_cmd; /* protocol version and command */ + uint8_t fam; /* protocol family and address */ + uint16_t len; /* number of following bytes part of the header */ + }; +``` + +从第17个byte开始,就是地址的长度和端口号信息,可以用下面的结构体表示: + +``` + union proxy_addr { + struct { /* for TCP/UDP over IPv4, len = 12 */ + uint32_t src_addr; + uint32_t dst_addr; + uint16_t src_port; + uint16_t dst_port; + } ipv4_addr; + struct { /* for TCP/UDP over IPv6, len = 36 */ + uint8_t src_addr[16]; + uint8_t dst_addr[16]; + uint16_t src_port; + uint16_t dst_port; + } ipv6_addr; + struct { /* for AF_UNIX sockets, len = 216 */ + uint8_t src_addr[108]; + uint8_t dst_addr[108]; + } unix_addr; + }; +``` + +在V2版本中,除了address信息之外,header中还可以包含一些额外的扩展信息,这些信息被称为Type-Length-Value (TLV vectors),格式如下: + +``` + struct pp2_tlv { + uint8_t type; + uint8_t length_hi; + uint8_t length_lo; + uint8_t value[0]; + }; +``` + +字段的含义分别是类型,长度和值。 + +下面是目前支持的类型: + +``` + #define PP2_TYPE_ALPN 0x01 + #define PP2_TYPE_AUTHORITY 0x02 + #define PP2_TYPE_CRC32C 0x03 + #define PP2_TYPE_NOOP 0x04 + #define PP2_TYPE_UNIQUE_ID 0x05 + #define PP2_TYPE_SSL 0x20 + #define PP2_SUBTYPE_SSL_VERSION 0x21 + #define PP2_SUBTYPE_SSL_CN 0x22 + #define PP2_SUBTYPE_SSL_CIPHER 0x23 + #define PP2_SUBTYPE_SSL_SIG_ALG 0x24 + #define PP2_SUBTYPE_SSL_KEY_ALG 0x25 + #define PP2_TYPE_NETNS 0x30 +``` + +# Proxy Protocol的使用情况 + +上面也提到了,一个协议的好坏不仅仅在与这个协议定义的好不好,也在于使用这个协议的软件多不多。 + +如果主流的代理软件都没有使用你这个代理协议,那么协议定义的再好也没有用。相反,如果大家都在使用你这个协议,协议定义的再差也是主流协议。 + +好在Proxy Protocol已经在代理服务器界被广泛的使用了。 + +具体使用该协议的软件如下: + +- Elastic Load Balancing,AWS的负载均衡器,从2013年7月起兼容 +- Dovecot,一个POP/IMAP邮件服务器从2.2.19版本开始兼容 +- exaproxy,一个正向和反向代理服务器,从1.0.0版本开始兼容 +- gunicorn ,python HTTP 服务器,从0.15.0开始兼容 +- haproxy,反向代理负载均衡器,从1.5-dev3开始兼容 +- nginx,正方向代理服务器,http服务器,从1.5.12开始兼容 +- Percona DB,数据库服务器,从5.6.25-73.0开始兼容 +- stud,SSL offloader,从第一个版本开始兼容 +- stunnel,SSL offloader,从4.45开始兼容 +- apache HTTPD,web 服务器,在扩展模块myfixip中使用 +- varnish,HTTP 反向代理缓存,从4.1版开始兼容 + +基本上所有的主流服务器都兼容Proxy Protocol,所以我们可以把Proxy Protocol当做是事实上的标准。 + +# 总结 + +在本文中,我们介绍了Proxy Protocol的底层定义,那么Proxy Protocol具体怎么使用,能不能实现自己的Proxy Protocol服务器呢?敬请期待。 \ No newline at end of file diff --git a/learn-netty文章/78.netty系列之给ThreadLocal插上梦想的翅膀,详解FastThreadLocal.md b/learn-netty文章/78.netty系列之给ThreadLocal插上梦想的翅膀,详解FastThreadLocal.md new file mode 100644 index 0000000..f582ca9 --- /dev/null +++ b/learn-netty文章/78.netty系列之给ThreadLocal插上梦想的翅膀,详解FastThreadLocal.md @@ -0,0 +1,193 @@ +# netty系列之:给ThreadLocal插上梦想的翅膀,详解FastThreadLocal + +文章目录 + + + +[简介](http://www.flydean.com/48-netty-fastthreadlocal/#简介)[从ThreadLocalMap中获取数据](http://www.flydean.com/48-netty-fastthreadlocal/#从ThreadLocalMap中获取数据)[FastThreadLocal](http://www.flydean.com/48-netty-fastthreadlocal/#FastThreadLocal)[总结](http://www.flydean.com/48-netty-fastthreadlocal/#总结) + +# 简介 + +JDK中的ThreadLocal可以通过get方法来获得跟当前线程绑定的值。而这些值是存储在ThreadLocal.ThreadLocalMap中的。而在ThreadLocalMap中底层的数据存储是一个Entry数组中的。 + +那么从ThreadLocalMap中获取数据的速度如何呢?速度有没有可以优化的空间呢? + +一起来看看。 + +# 从ThreadLocalMap中获取数据 + +ThreadLocalMap作为一个Map,它的底层数据存储是一个Entry类型的数组: + +``` +private Entry[] table; +``` + +我们再来回顾一下ThreadLocal是怎么获取数据的: + +``` + private Entry getEntry(ThreadLocal key) { + int i = key.threadLocalHashCode & (table.length - 1); + Entry e = table[i]; + if (e != null && e.get() == key) + return e; + else + return getEntryAfterMiss(key, i, e); + } +``` + +首先根据ThreadLocal对象中的threadLocalHashCode跟table的长度进行取模运算,得到要获取的Entry在table中的位置,然后判断位置Entry的key是否和要获取的ThreadLocal对象一致。 + +如果一致,说明获取到了ThreadLocal绑定的对象,直接返回即可。 + +如果不一致,则需要再次进行查找。 + +我们看下再次查找的逻辑: + +``` + private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) { + Entry[] tab = table; + int len = tab.length; + + while (e != null) { + ThreadLocal k = e.get(); + if (k == key) + return e; + if (k == null) + expungeStaleEntry(i); + else + i = nextIndex(i, len); + e = tab[i]; + } + return null; + } +``` + +getEntryAfterMiss的逻辑是,先判断Entry中的对象是否要获取的对象,如果是则直接返回。 + +如果Entry中的对象为空,则触发清除过期Entry的方法。否则的话计算出下一个要判断的地址,再次进行判断,直到最终找到要找到的对象为止。 + +可以看到,如果第一次没有找到要找到的对象的话,后面则可能会遍历多次,从而造成执行效率变低。 + +那么有没有可以提升这个寻找速度的方法呢?答案是肯定的。 + +# FastThreadLocal + +之前我们提到了,Netty中的本地对象池技术,netty为其创建了一个专门的类叫做Recycler。虽然Recycler中也使用到了ThreadLocal,但是Recycler使用的threadLocal并不是JDK自带的ThreadLocal,而是FastThreadLocal。和它关联的ThreadLocalMap叫做InternalThreadLocalMap,和它关联的Thread叫做FastThreadLocalThread。netty中的类和JDK中的类的对应关系如下: + +| netty中的对象 | JDK中的对象 | +| :--------------------: | :------------------------: | +| FastThreadLocalThread | Thread | +| InternalThreadLocalMap | ThreadLocal.ThreadLocalMap | +| FastThreadLocal | ThreadLocal | + +我们先来看FastThreadLocalThread。不管它到底快不快,既然是Thread,那么自然就要继承自JDK的Thread: + +``` +public class FastThreadLocalThread extends Thread +``` + +和Thread一样,FastThreadLocalThread中也有一个ThreadLocalMap,叫做InternalThreadLocalMap,它是FastThreadLocalThread的private属性: + +``` +private InternalThreadLocalMap threadLocalMap; +``` + +InternalThreadLocalMap中也有一个ThreadLocal对象,叫做slowThreadLocalMap,是在fastThreadLocalMap不生效的时候使用的。 + +接下来我们来看下这个ThreadLocalMap为什么快: + +``` + public static InternalThreadLocalMap get() { + Thread thread = Thread.currentThread(); + if (thread instanceof FastThreadLocalThread) { + return fastGet((FastThreadLocalThread) thread); + } else { + return slowGet(); + } + } +``` + +从get方法可以看到,如果当前thread是FastThreadLocalThread的话,则会去调用fastGet方法,否则调用slowGet方法。 + +slowGet方法就是使用传统的ThreadLocal来get: + +``` + private static InternalThreadLocalMap slowGet() { + InternalThreadLocalMap ret = slowThreadLocalMap.get(); + if (ret == null) { + ret = new InternalThreadLocalMap(); + slowThreadLocalMap.set(ret); + } + return ret; + } +``` + +我们重点关注下fastGet方法: + +``` + private static InternalThreadLocalMap fastGet(FastThreadLocalThread thread) { + InternalThreadLocalMap threadLocalMap = thread.threadLocalMap(); + if (threadLocalMap == null) { + thread.setThreadLocalMap(threadLocalMap = new InternalThreadLocalMap()); + } + return threadLocalMap; + } +``` + +这里fast的效果就出现了,fastGet直接返回了thread中的InternalThreadLocalMap对象,不需要进行任何查找的过程。 + +再看下FastThreadLocal如何使用get方法来获取具体的值: + +``` + public final V get() { + InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get(); + Object v = threadLocalMap.indexedVariable(index); + if (v != InternalThreadLocalMap.UNSET) { + return (V) v; + } + + return initialize(threadLocalMap); + } +``` + +可以看到FastThreadLocal中的get首先调用了InternalThreadLocalMap的get方法,直接返回了FastThreadLocalThread中的InternalThreadLocalMap对象,这个速度是非常快的。 + +然后直接使用FastThreadLocal中的index,来获取threadLocalMap中具体存储数据的数组中的元素: + +``` + public Object indexedVariable(int index) { + Object[] lookup = indexedVariables; + return index < lookup.length? lookup[index] : UNSET; + } +``` + +因为是直接index访问的,所以也非常快。这就是fast的由来。 + +那么有同学会问题了,FastThreadLocal中的index是怎么来的呢? + +``` + private final int index; + + public FastThreadLocal() { + index = InternalThreadLocalMap.nextVariableIndex(); + } +``` + +而InternalThreadLocalMap中的nextVariableIndex方法是一个静态方法: + +``` + public static int nextVariableIndex() { + int index = nextIndex.getAndIncrement(); + if (index < 0) { + nextIndex.decrementAndGet(); + throw new IllegalStateException("too many thread-local indexed variables"); + } + return index; + } +``` + +也就是说,只要new一个FastThreadLocal,该对象中,就会生成一个唯一的index。然后FastThreadLocal使用该index去InternalThreadLocalMap中存取对象。这样就不存在ThreadLocal那种需要多次遍历查找的情况。 + +# 总结 + +FastThreadLocal是和FastThreadLocalThread配套使用才会真正的fast,否则的话就会fallback到ThreadLocal去执行,大家一定要注意这一点。 \ No newline at end of file diff --git a/learn-netty文章/79.netty系列之我有一个可扩展的Enum你要不要看一下.md b/learn-netty文章/79.netty系列之我有一个可扩展的Enum你要不要看一下.md new file mode 100644 index 0000000..71585c6 --- /dev/null +++ b/learn-netty文章/79.netty系列之我有一个可扩展的Enum你要不要看一下.md @@ -0,0 +1,196 @@ +# netty系列之:我有一个可扩展的Enum你要不要看一下? + +文章目录 + + + +[简介](http://www.flydean.com/49-netty-extensible-enum/#简介)[enum和Enum](http://www.flydean.com/49-netty-extensible-enum/#enum和Enum)[netty中可扩展的Enum:ConstantPool](http://www.flydean.com/49-netty-extensible-enum/#netty中可扩展的Enum_ConstantPool)[使用ConstantPool](http://www.flydean.com/49-netty-extensible-enum/#使用ConstantPool)[总结](http://www.flydean.com/49-netty-extensible-enum/#总结) + +# 简介 + +很多人都用过java中的枚举,枚举是JAVA 1.5中引用的一个新的类型,用来表示可以列举的范围,但是可能很少有人知道java中的enum到底是怎么工作的,enum和Enum有什么关系?Enum可不可以扩展? + +一起来看看吧。 + +# enum和Enum + +JAVA1.5中引入了枚举类,我们通常使用enum关键字来定义一个枚举类: + +``` +public enum StatusEnum { + START(1,"start"), + INPROCESS(2,"inprocess"), + END(3,"end"); + + private int code; + private String desc; + + StatusEnum(int code, String desc){ + this.code=code; + this.desc=desc; + } +} +``` + +上面的枚举类中,我们自定义了构造函数,并且定义了3个枚举对象。 + +接下来看下怎么来使用这个枚举类: + +``` + public static void main(String[] args) { + StatusEnum start = START; + System.out.println(start.name()); + System.out.println(start.ordinal()); + System.out.println(start.code); + System.out.println(start.desc); + } +``` + +可以输出code和desc很好理解,因为这是我们自定义的枚举类中的属性,但是name和ordinal是什么呢?他们是哪里来的呢? + +这里就要介绍java.lang.Enum类了,它是JAVA中所有enum枚举类的父类,name()和ordinal()方法就是在这个类中定义的: + +``` +public final int ordinal() { + return ordinal; + } + +public final String name() { + return name; + } +``` + +其中ordinal表示的是枚举类中枚举的位置,那么就是枚举类中枚举的名字。在上面的例子中,START的两个值分别是1和START。 + +我们来看下Enum类的定义: + +``` +public abstract class Enum> + implements Comparable, Serializable +``` + +输入它是一个抽象类,但是编译器是不允许你继承这个类的。如果你强行继承,则会抛错: + +``` +Classes cannot directly extend 'java.lang.Enum' +``` + +所以说,强扭的瓜不甜,大家一定要记住。 + +事实上,不仅仅Enum类本身不能被继承,上面创建的enum类StatusEnum也是不能被继承的。 + +这会造成一个什么问题呢? + +如果这个enum是包含在一个外部jar包中的时候,你就没法对该enum进行扩展,在某些特定的情况下,这样的限制可能会带来一些不便。 + +还好,netty也意识到了这个问题,接下来,我们看下netty是怎么解决的。 + +# netty中可扩展的Enum:ConstantPool + +netty中的表示常量的类叫做Constant,它有两个属性,分别是ID和name: + +``` +public interface Constant> extends Comparable { + + int id(); + + String name(); +} +``` + +存储这些Constant的就叫做ConstantPool。ConstantPool中有一个ConcurrentMap用来保存具体的Constant。 我们看一下ConstantPool +的工厂类方法valueOf: + +``` +public T valueOf(String name) { + return getOrCreate(checkNonEmpty(name, "name")); + } +``` + +valueOf方法传入创建的Constant的名字。然后调用getOrCreate方法来创建新的Constant: + +``` + private T getOrCreate(String name) { + T constant = constants.get(name); + if (constant == null) { + final T tempConstant = newConstant(nextId(), name); + constant = constants.putIfAbsent(name, tempConstant); + if (constant == null) { + return tempConstant; + } + } + + return constant; + } +``` + +可以看到getOrCreate就是向constants Map中创建和获取新创建的constant对象。 + +# 使用ConstantPool + +ConstantPool是一个抽象类,如果我们需要新建一个枚举类池,可以直接继承ConstantPool,然后实现其中的newConstant方法。下面是一个使用的具体例子: + +``` +public final class Foo extends AbstractConstant { + Foo(int id, String name) { + super(id, name); + } +} + +public final class MyConstants { + + private static final ConstantPool pool = new ConstantPool() { + @Override + protected Foo newConstant(int id, String name) { + return new Foo(id, name); + } + }; + + public static Foo valueOf(String name) { + return pool.valueOf(name); + } + + public static final Foo A = valueOf("A"); + public static final Foo B = valueOf("B"); +} + +private final class YourConstants { + public static final Foo C = MyConstants.valueOf("C"); + public static final Foo D = MyConstants.valueOf("D"); +} +``` + +在上面的例子中,我们创建的枚举类继承自AbstractConstant,然后自定义了ConstantPool,从pool中可以返回新创建的Foo对象。 + +实时上,在netty channel中经常使用的ChannelOption就是AbstractConstant的子类,我们简单来看下其中的实现: + +``` +public class ChannelOption extends AbstractConstant> { + + private static final ConstantPool> pool = new ConstantPool>() { + @Override + protected ChannelOption newConstant(int id, String name) { + return new ChannelOption(id, name); + } + }; + public static ChannelOption valueOf(String name) { + return (ChannelOption) pool.valueOf(name); + } + public static ChannelOption valueOf(Class firstNameComponent, String secondNameComponent) { + return (ChannelOption) pool.valueOf(firstNameComponent, secondNameComponent); + } + public static boolean exists(String name) { + return pool.exists(name); + } + public static ChannelOption newInstance(String name) { + return (ChannelOption) pool.newInstance(name); + } +``` + +可以看到,ChannelOption中定义了ConstantPool,然后通过ConstantPool的valueOf和newInstance方法来创建新的ChannelOption对象。 + +# 总结 + +如果你也想要对枚举类进行扩展,不妨使用Constant和ConstantPool试试。 + +本文的例子可以参考:[learn-netty4](https://github.com/ddean2009/learn-netty4) \ No newline at end of file diff --git a/learn-netty文章/8.netty系列之使用POJO替代buf.md b/learn-netty文章/8.netty系列之使用POJO替代buf.md new file mode 100644 index 0000000..d623acb --- /dev/null +++ b/learn-netty文章/8.netty系列之使用POJO替代buf.md @@ -0,0 +1,159 @@ +# netty系列之:使用POJO替代buf + +# 简介 + +在之前的文章中我们提到了,对于NioSocketChannel来说,它不接收最基本的string消息,只接收ByteBuf和FileRegion。但是ByteBuf是以二进制的形式进行处理的,对于程序员来说太不直观了,处理起来也比较麻烦,有没有可能直接处理java简单对象呢?本文将会探讨一下这个问题。 + +# decode和encode + +比如我们需要直接向channel中写入一个字符串,在之前的文章中,我们知道这是不可以的,会报下面的错误: + +```java +DefaultChannelPromise@57f5c075(failure: java.lang.UnsupportedOperationException: unsupported message type: String (expected: ByteBuf, FileRegion)) +``` + +也就说ChannelPromise只接受ByteBuf和FileRegion,那么怎么做呢? + +既然ChannelPromise只接受ByteBuf和FileRegion,那么我们就需要把String对象转换成ByteBuf即可。 + +也就是说在写入String之前把String转换成ByteBuf,当要读取数据的时候,再把ByteBuf转换成String。 + +我们知道ChannelPipeline中可以添加多个handler,并且控制这些handler的顺序。 + +那么我们的思路就出来了,在ChannelPipeline中添加一个encode,用于数据写入的是对数据进行编码成ByteBuf,然后再添加一个decode,用于在数据写出的时候对数据进行解码成对应的对象。 + +encode,decode是不是很熟悉?对了,这就是对象的序列化。 + +# 对象序列化 + +netty中对象序列化是要把传输的对象和ByteBuf直接互相转换,当然我们可以自己实现这个转换对象。但是netty已经为我们提供了方便的两个转换类:ObjectEncoder和ObjectDecoder。 + +先看ObjectEncoder,他的作用就是将对象转换成为ByteBuf。 + +这个类很简单,我们对其分析一下: + +```java +public class ObjectEncoder extends MessageToByteEncoder { + private static final byte[] LENGTH_PLACEHOLDER = new byte[4]; + + @Override + protected void encode(ChannelHandlerContext ctx, Serializable msg, ByteBuf out) throws Exception { + int startIdx = out.writerIndex(); + + ByteBufOutputStream bout = new ByteBufOutputStream(out); + ObjectOutputStream oout = null; + try { + bout.write(LENGTH_PLACEHOLDER); + oout = new CompactObjectOutputStream(bout); + oout.writeObject(msg); + oout.flush(); + } finally { + if (oout != null) { + oout.close(); + } else { + bout.close(); + } + } + + int endIdx = out.writerIndex(); + + out.setInt(startIdx, endIdx - startIdx - 4); + } +} +``` + +ObjectEncoder继承了MessageToByteEncoder,而MessageToByteEncoder又继承了ChannelOutboundHandlerAdapter。为什么是OutBound呢?这是因为我们是要对写入的对象进行转换,所以是outbound。 + +首先使用ByteBufOutputStream对out ByteBuf进行封装,在bout中,首先写入了一个LENGTH_PLACEHOLDER字段,用来表示stream中中Byte的长度。然后用一个CompactObjectOutputStream对bout进行封装,最后就可以用CompactObjectOutputStream写入对象了。 + +对应的,netty还有一个ObjectDecoder对象,用于将ByteBuf转换成对应的对象,ObjectDecoder继承自LengthFieldBasedFrameDecoder,实际上他是一个ByteToMessageDecoder,也是一个ChannelInboundHandlerAdapter,用来对数据读取进行处理。 + +我们看下ObjectDecoder中最重要的decode方法: + +```java +protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception { + ByteBuf frame = (ByteBuf) super.decode(ctx, in); + if (frame == null) { + return null; + } + + ObjectInputStream ois = new CompactObjectInputStream(new ByteBufInputStream(frame, true), classResolver); + try { + return ois.readObject(); + } finally { + ois.close(); + } +} +``` + +上面的代码可以看到,将输入的ByteBuf转换为ByteBufInputStream,最后转换成为CompactObjectInputStream,就可以直接读取对象了。 + +# 使用编码和解码器 + +有了上面两个编码解码器,直接需要将其添加到client和server端的ChannelPipeline中就可以了。 + +对于server端,其核心代码如下: + +```java +//定义bossGroup和workerGroup +EventLoopGroup bossGroup = new NioEventLoopGroup(1); +EventLoopGroup workerGroup = new NioEventLoopGroup(); + +ServerBootstrap b = new ServerBootstrap(); +b.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new ChannelInitializer() { + @Override + public void initChannel(SocketChannel ch) throws Exception { + ChannelPipeline p = ch.pipeline(); + p.addLast( + // 添加encoder和decoder + new ObjectEncoder(), + new ObjectDecoder(ClassResolvers.cacheDisabled(null)), + new PojoServerHandler()); + } + }); + +// 绑定端口,并准备接受连接 +b.bind(PORT).sync().channel().closeFuture().sync(); +``` + +同样的,对于client端,我们其核心代码如下: + +```java +EventLoopGroup group = new NioEventLoopGroup(); +Bootstrap b = new Bootstrap(); +b.group(group) + .channel(NioSocketChannel.class) + .handler(new ChannelInitializer() { + @Override + public void initChannel(SocketChannel ch) throws Exception { + ChannelPipeline p = ch.pipeline(); + p.addLast( + // 添加encoder和decoder + new ObjectEncoder(), + new ObjectDecoder(ClassResolvers.cacheDisabled(null)), + new PojoClientHandler()); + } + }); + +// 建立连接 +b.connect(HOST, PORT).sync().channel().closeFuture().sync(); +``` + +可以看到上面的逻辑就是将ObjectEncoder和ObjectDecoder添加到ChannelPipeline中即可。 + +最后,就可以在客户端和浏览器端通过调用: + +```java +ctx.write("加油!"); +``` + +直接写入字符串对象了。 + +# 总结 + +有了ObjectEncoder和ObjectDecoder,我们就可以不用受限于ByteBuf了,程序的灵活程度得到了大幅提升。 + +本文的例子可以参考:[learn-netty4](https://github.com/ddean2009/learn-netty4) \ No newline at end of file diff --git a/learn-netty文章/80.netty系列之HashedWheelTimer一种定时器的高效实现.md b/learn-netty文章/80.netty系列之HashedWheelTimer一种定时器的高效实现.md new file mode 100644 index 0000000..bf747f3 --- /dev/null +++ b/learn-netty文章/80.netty系列之HashedWheelTimer一种定时器的高效实现.md @@ -0,0 +1,203 @@ +# netty系列之:HashedWheelTimer一种定时器的高效实现 + +文章目录 + + + +[简介](http://www.flydean.com/50-netty-hashed-wheel-timer/#简介)[java.util.Timer](http://www.flydean.com/50-netty-hashed-wheel-timer/#javautilTimer)[java.util.concurrent.ScheduledThreadPoolExecutor](http://www.flydean.com/50-netty-hashed-wheel-timer/#javautilconcurrentScheduledThreadPoolExecutor)[HashedWheelTimer](http://www.flydean.com/50-netty-hashed-wheel-timer/#HashedWheelTimer)[总结](http://www.flydean.com/50-netty-hashed-wheel-timer/#总结) + +# 简介 + +定时器是一种在实际的应用中非常常见和有效的一种工具,其原理就是把要执行的任务按照执行时间的顺序进行排序,然后在特定的时间进行执行。JAVA提供了java.util.Timer和java.util.concurrent.ScheduledThreadPoolExecutor等多种Timer工具,但是这些工具在执行效率上面还是有些缺陷,于是netty提供了HashedWheelTimer,一个优化的Timer类。 + +一起来看看netty的Timer有何不同吧。 + +# java.util.Timer + +Timer是JAVA在1.3中引入的。所有的任务都存储在它里面的TaskQueue中: + +``` +private final TaskQueue queue = new TaskQueue(); +``` + +TaskQueue的底层是一个TimerTask的数组,用于存储要执行的任务。 + +``` +private TimerTask[] queue = new TimerTask[128]; +``` + +看起来TimerTask只是一个数组,但是Timer将这个queue做成了一个平衡二叉堆。 + +当添加一个TimerTask的时候,会插入到Queue的最后面,然后调用fixup方法进行再平衡: + +``` + void add(TimerTask task) { + // Grow backing store if necessary + if (size + 1 == queue.length) + queue = Arrays.copyOf(queue, 2*queue.length); + + queue[++size] = task; + fixUp(size); + } +``` + +当从heap中移出运行的任务时候,会调用fixDown方法进行再平衡: + +``` + void removeMin() { + queue[1] = queue[size]; + queue[size--] = null; // Drop extra reference to prevent memory leak + fixDown(1); + } +``` + +fixup的原理就是将当前的节点和它的父节点进行比较,如果小于父节点就和父节点进行交互,然后遍历进行这个过程: + +``` + private void fixUp(int k) { + while (k > 1) { + int j = k >> 1; + if (queue[j].nextExecutionTime <= queue[k].nextExecutionTime) + break; + TimerTask tmp = queue[j]; queue[j] = queue[k]; queue[k] = tmp; + k = j; + } + } +``` + +fixDown的原理是比较当前节点和它的子节点,如果当前节点大于子节点,则将其降级: + +``` + private void fixDown(int k) { + int j; + while ((j = k << 1) <= size && j > 0) { + if (j < size && + queue[j].nextExecutionTime > queue[j+1].nextExecutionTime) + j++; // j indexes smallest kid + if (queue[k].nextExecutionTime <= queue[j].nextExecutionTime) + break; + TimerTask tmp = queue[j]; queue[j] = queue[k]; queue[k] = tmp; + k = j; + } + } +``` + +二叉平衡堆的算法这里不做详细的介绍。大家可以自行查找相关的文章。 + +# java.util.concurrent.ScheduledThreadPoolExecutor + +虽然Timer已经很好用了,并且是线程安全的,但是对于Timer来说,想要提交任务的话需要创建一个TimerTask类,用来封装具体的任务,不是很通用。 + +所以JDK在5.0中引入了一个更加通用的ScheduledThreadPoolExecutor,这是一个线程池使用多线程来执行具体的任务。当线程池中的线程个数等于1的时候,ScheduledThreadPoolExecutor就等同于Timer。 + +ScheduledThreadPoolExecutor中进行任务保存的是一个DelayedWorkQueue。 + +DelayedWorkQueue和DelayQueue,PriorityQueue一样都是一个基于堆的数据结构。 + +因为堆需要不断的进行siftUp和siftDown再平衡操作,所以它的时间复杂度是O(log n)。 + +下面是DelayedWorkQueue的shiftUp和siftDown的实现代码: + +``` + private void siftUp(int k, RunnableScheduledFuture key) { + while (k > 0) { + int parent = (k - 1) >>> 1; + RunnableScheduledFuture e = queue[parent]; + if (key.compareTo(e) >= 0) + break; + queue[k] = e; + setIndex(e, k); + k = parent; + } + queue[k] = key; + setIndex(key, k); + } + + private void siftDown(int k, RunnableScheduledFuture key) { + int half = size >>> 1; + while (k < half) { + int child = (k << 1) + 1; + RunnableScheduledFuture c = queue[child]; + int right = child + 1; + if (right < size && c.compareTo(queue[right]) > 0) + c = queue[child = right]; + if (key.compareTo(c) <= 0) + break; + queue[k] = c; + setIndex(c, k); + k = child; + } + queue[k] = key; + setIndex(key, k); + } +``` + +# HashedWheelTimer + +因为Timer和ScheduledThreadPoolExecutor底层都是基于堆结构的。虽然ScheduledThreadPoolExecutor对Timer进行了改进,但是他们两个的效率是差不多的。 + +那么有没有更加高效的方法呢?比如O(1)是不是可以达到呢? + +我们知道Hash可以实现高效的O(1)查找,想象一下假如我们有一个无限刻度的钟表,然后把要执行的任务按照间隔时间长短的顺序分配到这些刻度中,每当钟表移动一个刻度,即可以执行这个刻度中对应的任务,如下图所示: + +![img](412a02375baf4cea9cbaec5330442747.png) + +这种算法叫做Simple Timing Wheel算法。 + +但是这种算法是理论上的算法,因为不可能为所有的间隔长度都分配对应的刻度。这样会耗费大量的无效内存空间。 + +所以我们可以做个折中方案,将间隔时间的长度先用hash进行处理。这样就可以缩短间隔时间的基数,如下图所示: + +![img](a844aefb374143e495729c44c9b252d4.png) + +这个例子中,我们选择8作为基数,间隔时间除以8,余数作为hash的位置,商作为节点的值。 + +每次遍历轮询的时候,将节点的值减一。当节点的值为0的时候,就表示该节点可以取出执行了。 + +这种算法就叫做HashedWheelTimer。 + +netty提供了这种算法的实现: + +``` +public class HashedWheelTimer implements Timer +``` + +HashedWheelTimer使用HashedWheelBucket数组来存储具体的TimerTask: + +``` +private final HashedWheelBucket[] wheel; +``` + +首先来看下创建wheel的方法: + +``` + private static HashedWheelBucket[] createWheel(int ticksPerWheel) { + //ticksPerWheel may not be greater than 2^30 + checkInRange(ticksPerWheel, 1, 1073741824, "ticksPerWheel"); + + ticksPerWheel = normalizeTicksPerWheel(ticksPerWheel); + HashedWheelBucket[] wheel = new HashedWheelBucket[ticksPerWheel]; + for (int i = 0; i < wheel.length; i ++) { + wheel[i] = new HashedWheelBucket(); + } + return wheel; + } +``` + +我们可以自定义wheel中ticks的大小,但是ticksPerWheel不能超过2^30。 + +然后将ticksPerWheel的数值进行调整,到2的整数倍。 + +然后创建ticksPerWheel个元素的HashedWheelBucket数组。 + +这里要注意,虽然整体的wheel是一个hash结构,但是wheel中的每个元素,也就是HashedWheelBucket是一个链式结构。 + +HashedWheelBucket中的每个元素都是一个HashedWheelTimeout. HashedWheelTimeout中有一个remainingRounds属性用来记录这个Timeout元素还会在Bucket中保存多久。 + +``` +long remainingRounds; +``` + +# 总结 + +netty中的HashedWheelTimer可以实现更高效的Timer功能,大家用起来吧。 \ No newline at end of file diff --git a/learn-netty文章/81.netty系列之在netty中实现线程和CPU绑定.md b/learn-netty文章/81.netty系列之在netty中实现线程和CPU绑定.md new file mode 100644 index 0000000..044bd56 --- /dev/null +++ b/learn-netty文章/81.netty系列之在netty中实现线程和CPU绑定.md @@ -0,0 +1,189 @@ +# netty系列之:在netty中实现线程和CPU绑定 + +文章目录 + + + +[简介](http://www.flydean.com/51-netty-thread-affinity/#简介)[引入affinity](http://www.flydean.com/51-netty-thread-affinity/#引入affinity)[AffinityThreadFactory](http://www.flydean.com/51-netty-thread-affinity/#AffinityThreadFactory)[在netty中使用AffinityThreadFactory](http://www.flydean.com/51-netty-thread-affinity/#在netty中使用AffinityThreadFactory)[总结](http://www.flydean.com/51-netty-thread-affinity/#总结) + +# 简介 + +之前我们介绍了一个非常优秀的细粒度控制JAVA线程的库:java thread affinity。使用这个库你可以将线程绑定到特定的CPU或者CPU核上,通过减少线程在CPU之间的切换,从而提升线程执行的效率。 + +虽然netty已经够优秀了,但是谁不想更加优秀一点呢?于是一个想法产生了,那就是能不能把affinity库用在netty中呢? + +答案是肯定的,一起来看看吧。 + +# 引入affinity + +affinity是以jar包的形式提供出去的,目前最新的正式版本是3.20.0,所以我们需要这样引入: + +``` + + + net.openhft + affinity + 3.20.0 + +``` + +引入affinity之后,会在项目的依赖库中添加一个affinity的lib包,这样我们就可以在netty中愉快的使用affinity了。 + +# AffinityThreadFactory + +有了affinity,怎么把affinity引入到netty中呢? + +我们知道affinity是用来控制线程的,也就是说affinity是跟线程有关的。而netty中跟线程有关的就是EventLoopGroup,先看一下netty中EventLoopGroup的基本用法,这里以NioEventLoopGroup为例,NioEventLoopGroup有很多构造函数的参数,其中一种是传入一个ThreadFactory: + +``` + public NioEventLoopGroup(ThreadFactory threadFactory) { + this(0, threadFactory, SelectorProvider.provider()); + } +``` + +这个构造函数表示NioEventLoopGroup中使用的线程都是由threadFactory创建而来的。这样以来我们就找到了netty和affinity的对应关系。只需要构造affinity的ThreadFactory即可。 + +刚好affinity中有一个AffinityThreadFactory类,专门用来创建affinity对应的线程。 + +接下来我们来详细了解一下AffinityThreadFactory。 + +AffinityThreadFactory可以根据提供的不同AffinityStrategy来创建对应的线程。 + +AffinityStrategy表示的是线程之间的关系。在affinity中,有5种线程关系,分别是: + +``` + SAME_CORE - 线程会运行在同一个CPU core中。 + SAME_SOCKET - 线程会运行在同一个CPU socket中,但是不在同一个core上。 + DIFFERENT_SOCKET - 线程会运行在不同的socket中。 + DIFFERENT_CORE - 线程会运行在不同的core上。 + ANY - 只要是可用的CPU资源都可以。 +``` + +这些关系是通过AffinityStrategy中的matches方法来实现的: + +``` +boolean matches(int cpuId, int cpuId2); +``` + +matches传入两个参数,分别是传入的两个cpuId。我们以SAME_CORE为例来看看这个mathes方法到底是怎么工作的: + +``` + SAME_CORE { + @Override + public boolean matches(int cpuId, int cpuId2) { + CpuLayout cpuLayout = AffinityLock.cpuLayout(); + return cpuLayout.socketId(cpuId) == cpuLayout.socketId(cpuId2) && + cpuLayout.coreId(cpuId) == cpuLayout.coreId(cpuId2); + } + } +``` + +可以看到它的逻辑是先获取当前CPU的layout,CpuLayout中包含了cpu个数,sockets个数,每个sockets的cpu核数等基本信息。并且提供了三个方法根据给定的cpuId返回对应的socket、core和thread信息: + +``` + int socketId(int cpuId); + + int coreId(int cpuId); + + int threadId(int cpuId); +``` + +matches方法就是根据传入的cpuId找到对应的socket,core信息进行比较,从而生成了5中不同的策略。 + +先看一下AffinityThreadFactory的构造函数: + +``` + public AffinityThreadFactory(String name, boolean daemon, @NotNull AffinityStrategy... strategies) { + this.name = name; + this.daemon = daemon; + this.strategies = strategies.length == 0 ? new AffinityStrategy[]{AffinityStrategies.ANY} : strategies; + } +``` + +可以传入thread的name前缀,和是否是守护线程,最后如果strategies不传的话,默认使用的是AffinityStrategies.ANY策略,也就是说为线程分配任何可以绑定的CPU。 + +接下来看下这个ThreadFactory是怎么创建新线程的: + +``` +public synchronized Thread newThread(@NotNull final Runnable r) { + String name2 = id <= 1 ? name : (name + '-' + id); + id++; + Thread t = new Thread(new Runnable() { + @Override + public void run() { + try (AffinityLock ignored = acquireLockBasedOnLast()) { + r.run(); + } + } + }, name2); + t.setDaemon(daemon); + return t; + } + + private synchronized AffinityLock acquireLockBasedOnLast() { + AffinityLock al = lastAffinityLock == null ? AffinityLock.acquireLock() : lastAffinityLock.acquireLock(strategies); + if (al.cpuId() >= 0) + lastAffinityLock = al; + return al; + } +``` + +从上面的代码可以看出,创建的新线程会以传入的name为前缀,后面添加1,2,3,4这种后缀。并且根据传入的是否是守护线程的标记,将调用对应线程的setDaemon方法。 + +重点是Thread内部运行的Runnable内容,在run方法内部,首先调用acquireLockBasedOnLast方法获取lock,在获得lock的前提下运行对应的线程方法,这样就会将当前运行的Thread和CPU进行绑定。 + +从acquireLockBasedOnLast方法中,我们可以看出AffinityLock实际上是一个链式结构,每次请求的时候都调用的是lastAffinityLock的acquireLock方法,如果获取到lock,则将lastAffinityLock进行替换,用来进行下一个lock的获取。 + +有了AffinityThreadFactory,我们只需要在netty的使用中传入AffinityThreadFactory即可。 + +# 在netty中使用AffinityThreadFactory + +上面讲到了要在netty中使用affinity,可以将AffinityThreadFactory传入EventLoopGroup中。对于netty server来说可以有两个EventLoopGroup,分别是acceptorGroup和workerGroup,在下面的例子中我们将AffinityThreadFactory传入workerGroup,这样后续work中分配的线程都会遵循AffinityThreadFactory中配置的AffinityStrategies策略,来获得对应的CPU: + +``` +//建立两个EventloopGroup用来处理连接和消息 + EventLoopGroup acceptorGroup = new NioEventLoopGroup(acceptorThreads); + //创建AffinityThreadFactory + ThreadFactory threadFactory = new AffinityThreadFactory("affinityWorker", AffinityStrategies.DIFFERENT_CORE,AffinityStrategies.DIFFERENT_SOCKET,AffinityStrategies.ANY); + //将AffinityThreadFactory加入workerGroup + EventLoopGroup workerGroup = new NioEventLoopGroup(workerThreads,threadFactory); + try { + ServerBootstrap b = new ServerBootstrap(); + b.group(acceptorGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .childHandler(new ChannelInitializer() { + @Override + public void initChannel(SocketChannel ch) throws Exception { + ch.pipeline().addLast(new AffinityServerHandler()); + } + }) + .option(ChannelOption.SO_BACKLOG, 128) + .childOption(ChannelOption.SO_KEEPALIVE, true); + + // 绑定端口并开始接收连接 + ChannelFuture f = b.bind(port).sync(); + + // 等待server socket关闭 + f.channel().closeFuture().sync(); + } finally { + //关闭group + workerGroup.shutdownGracefully(); + acceptorGroup.shutdownGracefully(); + } +``` + +为了获取更好的性能,Affinity还可以对CPU进行隔离,被隔离的CPU只允许执行本应用的线程,从而获得更好的性能。 + +要使用这个特性需要用到linux的isolcpus。这个功能主要是将一个或多个CPU独立出来,用来执行特定的Affinity任务。 + +isolcpus命令后面可以接CPU的ID,或者可以修改/boot/grub/grub.conf文件,添加要隔离的CPU信息如下: + +``` +isolcpus=3,4,5 +``` + +# 总结 + +affinity可以对线程进行极致管控,对性能要求严格的朋友可以试试,但是在使用过程中需要选择合适的AffinityStrategies,否则可能会得不到想要的结果。 + +本文的例子可以参考:[learn-netty4](https://github.com/ddean2009/learn-netty4) \ No newline at end of file diff --git a/learn-netty文章/82.netty系列之在netty中使用native传输协议.md b/learn-netty文章/82.netty系列之在netty中使用native传输协议.md new file mode 100644 index 0000000..c3b1efa --- /dev/null +++ b/learn-netty文章/82.netty系列之在netty中使用native传输协议.md @@ -0,0 +1,181 @@ +# netty系列之:在netty中使用native传输协议 + +文章目录 + + + +[简介](http://www.flydean.com/52-netty-native-transport-md/#简介)[native传输协议的依赖](http://www.flydean.com/52-netty-native-transport-md/#native传输协议的依赖)[netty本地传输协议的使用](http://www.flydean.com/52-netty-native-transport-md/#netty本地传输协议的使用)[总结](http://www.flydean.com/52-netty-native-transport-md/#总结) + +# 简介 + +对于IO来说,除了传统的block IO,使用最多的就是NIO了,通常我们在netty程序中最常用到的就是NIO,比如NioEventLoopGroup,NioServerSocketChannel等。 + +我们也知道在IO中有比NIO更快的IO方式,比如kqueue和epoll,但是这两种方式需要native方法的支持,也就是说需要在操作系统层面提供服务。 + +如果我们在支持Kqueue或者epoll的服务器上,netty是否可以提供对这些优秀IO的支持呢? + +答案是肯定的。但是首先kqueue和epoll需要JNI支持,也就是说JAVA程序需要调用本地的native方法。 + +# native传输协议的依赖 + +要想使用kequeue和epoll这种native的传输方式,我们需要额外添加项目的依赖,如果是linux环境,则可以添加如下的maven依赖环境: + +``` + + + io.netty + netty-transport-native-epoll + ${project.version} + linux-x86_64 + + ... + +``` + +其中version需要匹配你所使用的netty版本号,否则可能出现调用异常的情况。 + +classifier表示的是系统架构,它的值可以是linux-x86_64,也可以是linux-aarch_64. + +如果你使用的mac系统,那么可以这样引入: + +``` + + + io.netty + netty-transport-native-kqueue + ${project.version} + osx-x86_64 + + ... + +``` + +netty除了单独的个体包之外,还有一个all in one的netty-all包,如果你使用了这个all in one的包,那么不需要额外添加native的依赖。 + +如果netty提供的系统架构并没有你正在使用的,那么你需要手动进行编译,以下是编译所依赖的程序包, 如果是在RHEL/CentOS/Fedora系统中,则使用: + +``` +sudo yum install autoconf automake libtool make tar \ + glibc-devel \ + libgcc.i686 glibc-devel.i686 +``` + +如果是在Debian/Ubuntu系统中,则使用: + +``` +sudo apt-get install autoconf automake libtool make tar \ + gcc +``` + +如果是在MacOS/BSD系统中,则使用: + +``` +brew install autoconf automake libtool +``` + +# netty本地传输协议的使用 + +安装好依赖包之后,我们就可以在netty中使用这些native传输协议了。 + +native传输协议的使用和NIO的使用基本一致,我们只需要进行下面的替换即可。 + +如果是在liunx系统中,则进行下面的替换: + +``` + NioEventLoopGroup → EpollEventLoopGroup + NioEventLoop → EpollEventLoop + NioServerSocketChannel → EpollServerSocketChannel + NioSocketChannel → EpollSocketChannel +``` + +如果是在mac系统中,则进行下面的替换: + +``` + NioEventLoopGroup → KQueueEventLoopGroup + NioEventLoop → KQueueEventLoop + NioServerSocketChannel → KQueueServerSocketChannel + NioSocketChannel → KQueueSocketChannel +``` + +这里还是使用我们熟悉的聊天服务为例,首先看下基于Kqueue的netty服务器端应该怎么写: + +``` +EventLoopGroup bossGroup = new KQueueEventLoopGroup(1); + EventLoopGroup workerGroup = new KQueueEventLoopGroup(); + try { + ServerBootstrap b = new ServerBootstrap(); + b.group(bossGroup, workerGroup) + .channel(KQueueServerSocketChannel.class) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new NativeChatServerInitializer()); + + Channel channel = b.bind(PORT).sync().channel(); + log.info("server channel:{}", channel); + channel.closeFuture().sync(); +``` + +和NIO一样,在服务器端我们需要使用KQueueEventLoopGroup创建两个EventLoopGroup,一个是bossGroup, 一个是workerGroup。 + +然后将这两个group传入到ServerBootstrap中,并且添加KQueueServerSocketChannel作为channel。 + +其他的内容和NIO server的内容是一样的。 + +接下来我们看下基于Kqueue的netty客户端改如何跟server端建立连接: + +``` +EventLoopGroup group = new KQueueEventLoopGroup(); + try { + Bootstrap b = new Bootstrap(); + b.group(group) + .channel(KQueueSocketChannel.class) + .handler(new NativeChatClientInitializer()); + + // 建立连接 + Channel ch = b.connect(HOST, PORT).sync().channel(); + log.info("client channel: {}", ch); +``` + +这里使用的是KQueueEventLoopGroup,并将KQueueEventLoopGroup放到Bootstrap中,并且为Bootstrap提供了和server端一致的KQueueSocketChannel。 + +然后就是客户端向channel中写消息,这里我们直接从命令行输入: + +``` +// 从命令行输入 + ChannelFuture lastWriteFuture = null; + BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); + for (;;) { + String line = in.readLine(); + if (line == null) { + break; + } + // 将从命令行输入的一行字符写到channel中 + lastWriteFuture = ch.writeAndFlush(line + "\r\n"); + // 如果输入'再见',则等待server端关闭channel + if ("再见".equalsIgnoreCase(line)) { + ch.closeFuture().sync(); + break; + } + } +``` + +上面代码的意思是将命令行收到的消息写入到channel中,如果输入的是’再见’,则关闭channel。 + +为了能够处理字符串,这里用到了三个编码解码器: + +``` + // 添加行分割器 + pipeline.addLast(new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter())); + // 添加String Decoder和String Encoder,用来进行字符串的转换 + pipeline.addLast(new StringEncoder()); + pipeline.addLast(new StringDecoder()); +``` + +分别是行分割器,字符编码器和字符解码器。 + +运行一下看,程序运行没问题,客户端和服务器端可以进行通讯。 + +# 总结 + +这里我们只以Kqueue为例介绍了netty中native传输协议的使用,具体的代码,大家可以参考: + +[learn-netty4](https://github.com/ddean2009/learn-netty4) \ No newline at end of file diff --git a/learn-netty文章/82c99c6dc89c4dd19675b6b2079539f2.png b/learn-netty文章/82c99c6dc89c4dd19675b6b2079539f2.png new file mode 100644 index 0000000..32a2210 Binary files /dev/null and b/learn-netty文章/82c99c6dc89c4dd19675b6b2079539f2.png differ diff --git a/learn-netty文章/83.netty系列之kequeue传输协议详解.md b/learn-netty文章/83.netty系列之kequeue传输协议详解.md new file mode 100644 index 0000000..f8e10df --- /dev/null +++ b/learn-netty文章/83.netty系列之kequeue传输协议详解.md @@ -0,0 +1,312 @@ +# netty系列之:kequeue传输协议详解 + +文章目录 + + + +[简介](http://www.flydean.com/53-1-netty-kqueue-transport/#简介)[KQueueEventLoopGroup](http://www.flydean.com/53-1-netty-kqueue-transport/#KQueueEventLoopGroup)[KQueueEventLoop](http://www.flydean.com/53-1-netty-kqueue-transport/#KQueueEventLoop)[KQueueServerSocketChannel和KQueueSocketChannel](http://www.flydean.com/53-1-netty-kqueue-transport/#KQueueServerSocketChannel和KQueueSocketChannel)[总结](http://www.flydean.com/53-1-netty-kqueue-transport/#总结) + +# 简介 + +在前面的章节中,我们介绍了在netty中可以使用kequeue或者epoll来实现更为高效的native传输方式。那么kequeue和epoll和NIO传输协议有什么不同呢? + +本章将会以kequeue为例进行深入探讨。 + +在上面我们介绍的native的例子中,关于kqueue的类有这样几个,分别是KQueueEventLoopGroup,KQueueServerSocketChannel和KQueueSocketChannel,通过简单的替换和添加对应的依赖包,我们可以轻松的将普通的NIO netty服务替换成为native的Kqueue服务。 + +是时候揭开Kqueue的秘密了。 + +# KQueueEventLoopGroup + +eventLoop和eventLoopGroup是用来接受event和事件处理的。先来看下KQueueEventLoopGroup的定义: + +``` +public final class KQueueEventLoopGroup extends MultithreadEventLoopGroup +``` + +作为一个MultithreadEventLoopGroup,必须实现一个newChild方法,用来创建child EventLoop。在KQueueEventLoopGroup中,除了构造函数之外,额外需要实现的方法就是newChild: + +``` + protected EventLoop newChild(Executor executor, Object... args) throws Exception { + Integer maxEvents = (Integer) args[0]; + SelectStrategyFactory selectStrategyFactory = (SelectStrategyFactory) args[1]; + RejectedExecutionHandler rejectedExecutionHandler = (RejectedExecutionHandler) args[2]; + EventLoopTaskQueueFactory taskQueueFactory = null; + EventLoopTaskQueueFactory tailTaskQueueFactory = null; + + int argsLength = args.length; + if (argsLength > 3) { + taskQueueFactory = (EventLoopTaskQueueFactory) args[3]; + } + if (argsLength > 4) { + tailTaskQueueFactory = (EventLoopTaskQueueFactory) args[4]; + } + return new KQueueEventLoop(this, executor, maxEvents, + selectStrategyFactory.newSelectStrategy(), + rejectedExecutionHandler, taskQueueFactory, tailTaskQueueFactory); + } +``` + +newChild中的所有参数都是从KQueueEventLoopGroup的构造函数中传入的。除了maxEvents,selectStrategyFactory和rejectedExecutionHandler之外,还可以接收taskQueueFactory和tailTaskQueueFactory两个参数,最后把这些参数都传到KQueueEventLoop的构造函数中去,最终返回一个KQueueEventLoop对象。 + +另外在使用KQueueEventLoopGroup之前我们还需要确保Kqueue在系统中是可用的,这个判断是通过调用`KQueue.ensureAvailability();`来实现的。 + +KQueue.ensureAvailability首先判断是否定义了系统属性io.netty.transport.noNative,如果定了,说明native transport被禁用了,后续也就没有必要再进行判断了。 + +如果io.netty.transport.noNative没有被定义,那么会调用`Native.newKQueue()`来尝试从native中获取一个kqueue的FileDescriptor,如果上述的获取过程中没有任何异常,则说明kqueue在native方法中存在,我们可以继续使用了。 + +以下是判断kqueue是否可用的代码: + +``` + static { + Throwable cause = null; + if (SystemPropertyUtil.getBoolean("io.netty.transport.noNative", false)) { + cause = new UnsupportedOperationException( + "Native transport was explicit disabled with -Dio.netty.transport.noNative=true"); + } else { + FileDescriptor kqueueFd = null; + try { + kqueueFd = Native.newKQueue(); + } catch (Throwable t) { + cause = t; + } finally { + if (kqueueFd != null) { + try { + kqueueFd.close(); + } catch (Exception ignore) { + // ignore + } + } + } + } + UNAVAILABILITY_CAUSE = cause; + } +``` + +# KQueueEventLoop + +KQueueEventLoop是从KQueueEventLoopGroup中创建出来的,用来执行具体的IO任务。 + +先来看一下KQueueEventLoop的定义: + +``` +final class KQueueEventLoop extends SingleThreadEventLoop +``` + +不管是NIO还是KQueue或者是Epoll,因为使用了更加高级的IO技术,所以他们使用的EventLoop都是SingleThreadEventLoop,也就是说使用单线程就够了。 + +和KQueueEventLoopGroup一样,KQueueEventLoop也需要判断当前的系统环境是否支持kqueue: + +``` + static { + KQueue.ensureAvailability(); + } +``` + +上一节讲到了,KQueueEventLoopGroup会调用KQueueEventLoop的构造函数来返回一个eventLoop对象, 我们先来看下KQueueEventLoop的构造函数: + +``` + KQueueEventLoop(EventLoopGroup parent, Executor executor, int maxEvents, + SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler, + EventLoopTaskQueueFactory taskQueueFactory, EventLoopTaskQueueFactory tailTaskQueueFactory) { + super(parent, executor, false, newTaskQueue(taskQueueFactory), newTaskQueue(tailTaskQueueFactory), + rejectedExecutionHandler); + this.selectStrategy = ObjectUtil.checkNotNull(strategy, "strategy"); + this.kqueueFd = Native.newKQueue(); + if (maxEvents == 0) { + allowGrowing = true; + maxEvents = 4096; + } else { + allowGrowing = false; + } + this.changeList = new KQueueEventArray(maxEvents); + this.eventList = new KQueueEventArray(maxEvents); + int result = Native.keventAddUserEvent(kqueueFd.intValue(), KQUEUE_WAKE_UP_IDENT); + if (result < 0) { + cleanup(); + throw new IllegalStateException("kevent failed to add user event with errno: " + (-result)); + } + } +``` + +传入的maxEvents表示的是这个KQueueEventLoop能够接受的最大的event个数。如果maxEvents=0,则表示KQueueEventLoop的event容量可以动态扩展,并且最大值是4096。否则的话,KQueueEventLoop的event容量不能扩展。 + +maxEvents是作为数组的大小用来构建changeList和eventList。 + +KQueueEventLoop中还定义了一个map叫做channels,用来保存注册的channels: + +``` +private final IntObjectMap channels = new IntObjectHashMap(4096); +``` + +来看一下channel的add和remote方法: + +``` + void add(AbstractKQueueChannel ch) { + assert inEventLoop(); + AbstractKQueueChannel old = channels.put(ch.fd().intValue(), ch); + assert old == null || !old.isOpen(); + } + + void remove(AbstractKQueueChannel ch) throws Exception { + assert inEventLoop(); + int fd = ch.fd().intValue(); + AbstractKQueueChannel old = channels.remove(fd); + if (old != null && old != ch) { + channels.put(fd, old); + assert !ch.isOpen(); + } else if (ch.isOpen()) { + ch.unregisterFilters(); + } + } +``` + +可以看到添加和删除的都是AbstractKQueueChannel,后面的章节中我们会详细讲解KQueueChannel,这里我们只需要知道channel map中的key是kequeue中特有的FileDescriptor的int值。 + +再来看一下EventLoop中最重要的run方法: + +``` + protected void run() { + for (;;) { + try { + int strategy = selectStrategy.calculateStrategy(selectNowSupplier, hasTasks()); + switch (strategy) { + case SelectStrategy.CONTINUE: + continue; + + case SelectStrategy.BUSY_WAIT: + + case SelectStrategy.SELECT: + strategy = kqueueWait(WAKEN_UP_UPDATER.getAndSet(this, 0) == 1); + if (wakenUp == 1) { + wakeup(); + } + default: + } + + final int ioRatio = this.ioRatio; + if (ioRatio == 100) { + try { + if (strategy > 0) { + processReady(strategy); + } + } finally { + runAllTasks(); + } + } else { + final long ioStartTime = System.nanoTime(); + + try { + if (strategy > 0) { + processReady(strategy); + } + } finally { + final long ioTime = System.nanoTime() - ioStartTime; + runAllTasks(ioTime * (100 - ioRatio) / ioRatio); + } +``` + +它的逻辑是先使用selectStrategy.calculateStrategy获取当前的select strategy,然后根据strategy的值来判断是否需要执行processReady方法,最后执行runAllTasks,从task queue中拿到要执行的任务去执行。 + +selectStrategy.calculateStrategy用来判断当前的select状态,默认情况下有三个状态,分别是:SELECT,CONTINUE,BUSY_WAIT。 这三个状态都是负数: + +``` + int SELECT = -1; + + int CONTINUE = -2; + + int BUSY_WAIT = -3; +``` + +分别表示当前的IO在slect的block状态,或者跳过当前IO的状态,和正在IO loop pull的状态。BUSY_WAIT是一个非阻塞的IO PULL,kqueue并不支持,所以会fallback到SELECT。 + +除了这三个状态之外,calculateStrategy还会返回一个正值,表示当前要执行的任务的个数。 + +在run方法中,如果strategy的结果是SELECT,那么最终会调用Native.keventWait方法返回当前ready的events个数,并且将ready的event放到KQueueEventArray的eventList中去。 + +如果ready的event个数大于零,则会调用processReady方法对这些event进行状态回调处理。 + +怎么处理的呢?下面是处理的核心逻辑: + +``` + AbstractKQueueChannel channel = channels.get(fd); + + AbstractKQueueUnsafe unsafe = (AbstractKQueueUnsafe) channel.unsafe(); + + if (filter == Native.EVFILT_WRITE) { + unsafe.writeReady(); + } else if (filter == Native.EVFILT_READ) { + unsafe.readReady(eventList.data(i)); + } else if (filter == Native.EVFILT_SOCK && (eventList.fflags(i) & Native.NOTE_RDHUP) != 0) { + unsafe.readEOF(); + } +``` + +这里的fd是从eventList中读取到的: + +``` +final int fd = eventList.fd(i); +``` + +根据eventList的fd,我们可以从channels中拿到对应的KQueueChannel,然后根据event的filter状态来决定KQueueChannel的具体操作,是writeReady,readReady或者readEOF。 + +最后就是执行runAllTasks方法了,runAllTasks的逻辑很简单,就是从taskQueue中读取任务然后执行。 + +# KQueueServerSocketChannel和KQueueSocketChannel + +KQueueServerSocketChannel是用在server端的channel: + +``` +public final class KQueueServerSocketChannel extends AbstractKQueueServerChannel implements ServerSocketChannel { +``` + +KQueueServerSocketChannel继承自AbstractKQueueServerChannel,除了构造函数之外,最重要的一个方法就是newChildChannel: + +``` + @Override + protected Channel newChildChannel(int fd, byte[] address, int offset, int len) throws Exception { + return new KQueueSocketChannel(this, new BsdSocket(fd), address(address, offset, len)); + } +``` + +这个方法用来创建一个新的child channel。从上面的代码中,我们可以看到生成的child channel是一个KQueueSocketChannel的实例。 + +它的构造函数接受三个参数,分别是parent channel,BsdSocket和InetSocketAddress。 + +``` + KQueueSocketChannel(Channel parent, BsdSocket fd, InetSocketAddress remoteAddress) { + super(parent, fd, remoteAddress); + config = new KQueueSocketChannelConfig(this); + } +``` + +这里的fd是socket accept acceptedAddress的结果: + +``` +int acceptFd = socket.accept(acceptedAddress); +``` + +下面是KQueueSocketChannel的定义: + +``` +public final class KQueueSocketChannel extends AbstractKQueueStreamChannel implements SocketChannel { +``` + +KQueueSocketChannel和KQueueServerSocketChannel的关系是父子的关系,在KQueueSocketChannel中有一个parent方法,用来返回ServerSocketChannel对象,这也是前面提到的newChildChannel方法中传入KQueueSocketChannel构造函数中的serverChannel: + +``` +public ServerSocketChannel parent() { + return (ServerSocketChannel) super.parent(); + } +``` + +KQueueSocketChannel还有一个特性就是支持tcp fastopen,它的本质是调用BsdSocket的connectx方法,在建立连接的同时传递数据: + +``` +int bytesSent = socket.connectx( + (InetSocketAddress) localAddress, (InetSocketAddress) remoteAddress, iov, true); +``` + +# 总结 + +以上就是KqueueEventLoop和KqueueSocketChannel的详细介绍,基本上和NIO没有太大的区别,只不过性能根据优秀。 \ No newline at end of file diff --git a/learn-netty文章/84.netty系列之epoll传输协议详解.md b/learn-netty文章/84.netty系列之epoll传输协议详解.md new file mode 100644 index 0000000..dcb9eb9 --- /dev/null +++ b/learn-netty文章/84.netty系列之epoll传输协议详解.md @@ -0,0 +1,243 @@ +# netty系列之:epoll传输协议详解 + +文章目录 + + + +[简介](http://www.flydean.com/53-2-netty-epoll-transport/#简介)[epoll的详细使用](http://www.flydean.com/53-2-netty-epoll-transport/#epoll的详细使用)[EpollEventLoopGroup](http://www.flydean.com/53-2-netty-epoll-transport/#EpollEventLoopGroup)[EpollEventLoop](http://www.flydean.com/53-2-netty-epoll-transport/#EpollEventLoop)[EpollServerSocketChannel](http://www.flydean.com/53-2-netty-epoll-transport/#EpollServerSocketChannel)[EpollSocketChannel](http://www.flydean.com/53-2-netty-epoll-transport/#EpollSocketChannel)[总结](http://www.flydean.com/53-2-netty-epoll-transport/#总结) + +# 简介 + +在前面的章节中,我们讲解了kqueue的使用和原理,接下来我们再看一下epoll的使用。两者都是更加高级的IO方式,都需要借助native的方法实现,不同的是Kqueue用在mac系统中,而epoll用在liunx系统中。 + +# epoll的详细使用 + +epoll的使用也很简单,我们还是以常用的聊天室为例来讲解epoll的使用。 + +对于server端来说需要创建bossGroup和workerGroup,在NIO中这两个group是NIOEventLoopGroup,在epoll中则需要使用EpollEventLoopGroup: + +``` + EventLoopGroup bossGroup = new EpollEventLoopGroup(1); + EventLoopGroup workerGroup = new EpollEventLoopGroup(); +``` + +接着需要将bossGroup和workerGroup传入到ServerBootstrap中: + +``` +ServerBootstrap b = new ServerBootstrap(); + b.group(bossGroup, workerGroup) + .channel(EpollServerSocketChannel.class) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new NativeChatServerInitializer()); +``` + +注意,这里传入的channel是EpollServerSocketChannel,专门用来处理epoll的请求。其他的部分和普通的NIO服务是一样的。 + +接下来看下epoll的客户端,对于客户端来说需要创建一个EventLoopGroup,这里使用的是EpollEventLoopGroup: + +``` +EventLoopGroup group = new EpollEventLoopGroup(); +``` + +然后将这个group传入Bootstrap中去: + +``` +Bootstrap b = new Bootstrap(); + b.group(group) + .channel(EpollSocketChannel.class) + .handler(new NativeChatClientInitializer()); +``` + +这里使用的channel是EpollSocketChannel,是和EpollServerSocketChannel对应的客户端的channel。 + +# EpollEventLoopGroup + +先看下EpollEventLoopGroup的定义: + +``` +public final class EpollEventLoopGroup extends MultithreadEventLoopGroup +``` + +和KqueueEventLoopGroup一样,EpollEventLoopGroup也是继承自MultithreadEventLoopGroup,表示它可以开启多个线程。 + +在使用EpollEventLoopGroup之前,需要确保epoll相关的JNI接口都已经准备完毕: + +``` +Epoll.ensureAvailability(); +``` + +newChild方法用来生成EpollEventLoopGroup的子EventLoop: + +``` + protected EventLoop newChild(Executor executor, Object... args) throws Exception { + Integer maxEvents = (Integer) args[0]; + SelectStrategyFactory selectStrategyFactory = (SelectStrategyFactory) args[1]; + RejectedExecutionHandler rejectedExecutionHandler = (RejectedExecutionHandler) args[2]; + EventLoopTaskQueueFactory taskQueueFactory = null; + EventLoopTaskQueueFactory tailTaskQueueFactory = null; + + int argsLength = args.length; + if (argsLength > 3) { + taskQueueFactory = (EventLoopTaskQueueFactory) args[3]; + } + if (argsLength > 4) { + tailTaskQueueFactory = (EventLoopTaskQueueFactory) args[4]; + } + return new EpollEventLoop(this, executor, maxEvents, + selectStrategyFactory.newSelectStrategy(), + rejectedExecutionHandler, taskQueueFactory, tailTaskQueueFactory); + } +``` + +从方法中可以看到,newChild接受一个executor和多个额外的参数,这些参数分别是SelectStrategyFactory,RejectedExecutionHandler,taskQueueFactory和tailTaskQueueFactory,最终将这些参数传入EpollEventLoop中,返回一个新的EpollEventLoop对象。 + +# EpollEventLoop + +EpollEventLoop是由EpollEventLoopGroup通过使用new child方法来创建的。 + +对于EpollEventLoop本身来说,是一个SingleThreadEventLoop: + +``` +class EpollEventLoop extends SingleThreadEventLoop +``` + +借助于native epoll IO的强大功能,EpollEventLoop可以在单线程的情况下快速进行业务处理,十分优秀。 + +和EpollEventLoopGroup一样,EpollEventLoop在初始化的时候需要检测系统是否支持epoll: + +``` + static { + Epoll.ensureAvailability(); + } +``` + +在EpollEventLoopGroup调用的EpollEventLoop的构造函数中,初始化了三个FileDescriptor,分别是epollFd,eventFd和timerFd,这三个FileDescriptor都是调用Native方法创建的: + +``` +this.epollFd = epollFd = Native.newEpollCreate(); +this.eventFd = eventFd = Native.newEventFd(); +this.timerFd = timerFd = Native.newTimerFd(); +``` + +然后调用Native.epollCtlAdd建立FileDescriptor之间的关联关系: + +``` +Native.epollCtlAdd(epollFd.intValue(), eventFd.intValue(), Native.EPOLLIN | Native.EPOLLET); +Native.epollCtlAdd(epollFd.intValue(), timerFd.intValue(), Native.EPOLLIN | Native.EPOLLET); +``` + +在EpollEventLoop的run方法中,首先会调用`selectStrategy.calculateStrategy`方法,拿到当前的select状态,默认情况下有三个状态,分别是: + +``` + int SELECT = -1; + + int CONTINUE = -2; + + int BUSY_WAIT = -3; +``` + +这三个状态我们在kqueue中已经介绍过了,不同的是epoll支持BUSY_WAIT状态,在BUSY_WAIT状态下,会去调用`Native.epollBusyWait(epollFd, events)`方法返回busy wait的event个数。 + +如果是在select状态下,则会去调用`Native.epollWait(epollFd, events, 1000)`方法返回wait状态下的event个数。 + +接下来会分别调用`processReady(events, strategy)`和`runAllTasks`方法,进行event的ready状态回调处理和最终的任务执行。 + +# EpollServerSocketChannel + +先看下EpollServerSocketChannel的定义: + +``` +public final class EpollServerSocketChannel extends AbstractEpollServerChannel implements ServerSocketChannel +``` + +EpollServerSocketChannel继承自AbstractEpollServerChannel并且实现了ServerSocketChannel接口。 + +EpollServerSocketChannel的构造函数需要传入一个LinuxSocket: + +``` + EpollServerSocketChannel(LinuxSocket fd) { + super(fd); + config = new EpollServerSocketChannelConfig(this); + } +``` + +LinuxSocket是一个特殊的socket,用来处理和linux的native socket连接。 + +EpollServerSocketChannelConfig是构建EpollServerSocketChannel的配置,这里用到了4个配置选项,分别是SO_REUSEPORT,IP_FREEBIND,IP_TRANSPARENT,TCP_DEFER_ACCEPT和TCP_MD5SIG。每个配置项都对应着网络协议的特定含义。 + +我们再看一下EpollServerSocketChannel的newChildChannel方法: + +``` + protected Channel newChildChannel(int fd, byte[] address, int offset, int len) throws Exception { + return new EpollSocketChannel(this, new LinuxSocket(fd), address(address, offset, len)); + } +``` + +newChildChannel和KqueueServerSocketChannel方法一样,也是返回一个EpollSocketChannel,并且将传入的fd构造成为LinuxSocket。 + +# EpollSocketChannel + +EpollSocketChannel是由EpollServerSocketChannel创建返回的,先来看下EpollSocketChannel的定义: + +``` +public final class EpollSocketChannel extends AbstractEpollStreamChannel implements SocketChannel { +``` + +可以看到EpollSocketChannel继承自AbstractEpollStreamChannel,并且实现了SocketChannel接口。 + +回到之前EpollServerSocketChannel创建EpollSocketChannel时调用的newChildChannel方法,这个方法会调用EpollSocketChannel的构造函数如下所示: + +``` + EpollSocketChannel(Channel parent, LinuxSocket fd, InetSocketAddress remoteAddress) { + super(parent, fd, remoteAddress); + config = new EpollSocketChannelConfig(this); + + if (parent instanceof EpollServerSocketChannel) { + tcpMd5SigAddresses = ((EpollServerSocketChannel) parent).tcpMd5SigAddresses(); + } + } +``` + +从代码的逻辑可以看到,如果EpollSocketChannel是从EpollServerSocketChannel创建出来的话,那么默认会开启tcpMd5Sig的特性。 + +什么是tcpMd5Sig呢? + +简单点说,tcpMd5Sig就是在TCP的数据报文中添加了MD5 sig,用来进行数据的校验,从而提示数据传输的安全性。 + +TCP MD5是在RFC 2385中提出的,并且只在linux内核中才能开启,也就是说如果你想使用tcpMd5Sig,那么必须使用EpollServerSocketChannel和EpollSocketChannel。 + +所以如果是追求性能或者特殊使用场景的朋友,需要接触这种native transport的时候还是很多的,可以仔细研究其中的配置选项。 + +再看一下EpollSocketChannel中非常重要的doConnect0方法: + +``` + boolean doConnect0(SocketAddress remote) throws Exception { + if (IS_SUPPORTING_TCP_FASTOPEN_CLIENT && config.isTcpFastOpenConnect()) { + ChannelOutboundBuffer outbound = unsafe().outboundBuffer(); + outbound.addFlush(); + Object curr; + if ((curr = outbound.current()) instanceof ByteBuf) { + ByteBuf initialData = (ByteBuf) curr; + long localFlushedAmount = doWriteOrSendBytes( + initialData, (InetSocketAddress) remote, true); + if (localFlushedAmount > 0) { + outbound.removeBytes(localFlushedAmount); + return true; + } + } + } + return super.doConnect0(remote); + } +``` + +在这个方法中会首先判断是否开启了TcpFastOpen选项,如果开启了该选项,那么最终会调用LinuxSocket的write或者sendTo方法,这些方法可以添加初始数据,可以在建立连接的同时传递数据,从而达到Tcp fast open的效果。 + +如果不是tcp fast open,那么需要调用Socket的connect方法去建立传统的连接。 + +# 总结 + +epoll在netty中的实现和kqueue很类似,他们的不同在于运行的平台和具体的功能参数,如果追求高性能的朋友可以深入研究。 + +本文的代码,大家可以参考: + +[learn-netty4](https://github.com/ddean2009/learn-netty4) \ No newline at end of file diff --git a/learn-netty文章/85.netty系列之在netty中使用TCP协议请求DNS服务器.md b/learn-netty文章/85.netty系列之在netty中使用TCP协议请求DNS服务器.md new file mode 100644 index 0000000..80e1538 --- /dev/null +++ b/learn-netty文章/85.netty系列之在netty中使用TCP协议请求DNS服务器.md @@ -0,0 +1,357 @@ +# netty系列之:在netty中使用TCP协议请求DNS服务器 + +文章目录 + + + +[简介](http://www.flydean.com/54-netty-dns-over-tcp/#简介)[DNS传输协议简介](http://www.flydean.com/54-netty-dns-over-tcp/#DNS传输协议简介)[DNS的IP地址](http://www.flydean.com/54-netty-dns-over-tcp/#DNS的IP地址)[Do53/TCP在netty中的使用](http://www.flydean.com/54-netty-dns-over-tcp/#Do53TCP在netty中的使用)[总结](http://www.flydean.com/54-netty-dns-over-tcp/#总结) + +# 简介 + +DNS的全称domain name system,既然是一个系统就有客户端和服务器之分。一般情况来说我们并不需要感知这个DNS客户端的存在,因为我们在浏览器访问某个域名的时候,浏览器作为客户端已经实现了这个工作。 + +但是有时候我们没有使用浏览器,比如在netty环境中,如何构建一个DNS请求呢? + +# DNS传输协议简介 + +在RFC的规范中,DNS传输协议有很多种,如下所示: + +- DNS-over-UDP/53简称”Do53″,是使用UDP进行DNS查询传输的协议。 +- DNS-over-TCP/53简称”Do53/TCP”,是使用TCP进行DNS查询传输的协议。 +- DNSCrypt,对DNS传输协议进行加密的方法。 +- DNS-over-TLS简称”DoT”,使用TLS进行DNS协议传输。 +- DNS-over-HTTPS简称”DoH”,使用HTTPS进行DNS协议传输。 +- DNS-over-TOR,使用VPN或者tunnels连接DNS。 + +这些协议都有对应的实现方式,我们先来看下Do53/TCP,也就是使用TCP进行DNS协议传输。 + +# DNS的IP地址 + +先来考虑一下如何在netty中使用Do53/TCP协议,进行DNS查询。 + +因为DNS是客户端和服务器的模式,我们需要做的是构建一个DNS客户端,向已知的DNS服务器端进行查询。 + +已知的DNS服务器地址有哪些呢? + +除了13个root DNS IP地址以外,还出现了很多免费的公共DNS服务器地址,比如我们常用的阿里DNS,同时提供了IPv4/IPv6 DNS和DoT/DoH服务。 + +``` +IPv4: +223.5.5.5 + +223.6.6.6 + +IPv6: +2400:3200::1 + +2400:3200:baba::1 + +DoH 地址: +https://dns.alidns.com/dns-query + +DoT 地址: +dns.alidns.com +``` + +再比如百度DNS,提供了一组IPv4和IPv6的地址: + +``` +IPv4: +180.76.76.76 + +IPv6: +2400:da00::6666 +``` + +还有114DNS: + +``` +114.114.114.114 +114.114.115.115 +``` + +当然还有很多其他的公共免费DNS,这里我选择使用阿里的IPv4:223.5.5.5为例。 + +有了IP地址,我们还需要指定netty的连接端口号,这里默认的是53。 + +然后就是我们要查询的域名了,这里以www.flydean.com为例。 + +你也可以使用你系统中配置的DNS解析地址,以mac为例,可以通过nslookup进行查看本地的DNS地址: + +``` +nslookup www.flydean.com +Server: 8.8.8.8 +Address: 8.8.8.8#53 + +Non-authoritative answer: +www.flydean.com canonical name = flydean.com. +Name: flydean.com +Address: 47.107.98.187 +``` + +# Do53/TCP在netty中的使用 + +有了DNS Server的IP地址,接下来我们需要做的就是搭建netty client,然后向DNS server端发送DNS查询消息。 + +## 搭建DNS netty client + +因为我们进行的是TCP连接,所以可以借助于netty中的NIO操作来实现,也就是说我们需要使用NioEventLoopGroup和NioSocketChannel来搭建netty客户端: + +``` + final String dnsServer = "223.5.5.5"; + final int dnsPort = 53; + +EventLoopGroup group = new NioEventLoopGroup(); + Bootstrap b = new Bootstrap(); + b.group(group) + .channel(NioSocketChannel.class) + .handler(new Do53ChannelInitializer()); + + final Channel ch = b.connect(dnsServer, dnsPort).sync().channel(); +``` + +netty中的NIO Socket底层使用的就是TCP协议,所以我们只需要像常用的netty客户端服务一样构建客户端即可。 + +然后调用Bootstrap的connect方法连接到DNS服务器,就建立好了channel连接。 + +这里我们在handler中传入了自定义的Do53ChannelInitializer,我们知道handler的作用是对消息进行编码、解码和对消息进行读取。因为目前我们并不知道客户端查询的消息格式,所以Do53ChannelInitializer的实现我们在后面再进行详细讲解。 + +## 发送DNS查询消息 + +netty提供了DNS消息的封装,所有的DNS消息,包括查询和响应都是DnsMessage的子类。 + +每个DnsMessage都有一个唯一标记的ID,还有代表这个message类型的DnsOpCode。 + +对于DNS来说,opCode有下面这几种: + +``` + public static final DnsOpCode QUERY = new DnsOpCode(0, "QUERY"); + public static final DnsOpCode IQUERY = new DnsOpCode(1, "IQUERY"); + public static final DnsOpCode STATUS = new DnsOpCode(2, "STATUS"); + public static final DnsOpCode NOTIFY = new DnsOpCode(4, "NOTIFY"); + public static final DnsOpCode UPDATE = new DnsOpCode(5, "UPDATE"); +``` + +因为每个DnsMessage都可能包含4个sections,每个section都以DnsSection来表示。因为有4个section,所以在DnsSection定义了4个section类型: + +``` + QUESTION, + ANSWER, + AUTHORITY, + ADDITIONAL; +``` + +每个section里面又包含了多个DnsRecord, DnsRecord代表的就是Resource record,简称为RR,RR中有一个CLASS字段,下面是DnsRecord中CLASS字段的定义: + +``` + int CLASS_IN = 1; + int CLASS_CSNET = 2; + int CLASS_CHAOS = 3; + int CLASS_HESIOD = 4; + int CLASS_NONE = 254; + int CLASS_ANY = 255; +``` + +DnsMessage是DNS消息的统一表示,对于查询来说,netty中提供了一个专门的查询类叫做DefaultDnsQuery。 + +先来看下DefaultDnsQuery的定义和构造函数: + +``` +public class DefaultDnsQuery extends AbstractDnsMessage implements DnsQuery { + + public DefaultDnsQuery(int id) { + super(id); + } + + public DefaultDnsQuery(int id, DnsOpCode opCode) { + super(id, opCode); + } +``` + +DefaultDnsQuery的构造函数需要传入id和opCode。 + +我们可以这样定义一个DNS查询: + +``` +int randomID = (int) (System.currentTimeMillis() / 1000); + DnsQuery query = new DefaultDnsQuery(randomID, DnsOpCode.QUERY) +``` + +既然是QEURY,那么还需要设置4个sections中的查询section: + +``` +query.setRecord(DnsSection.QUESTION, new DefaultDnsQuestion(queryDomain, DnsRecordType.A)); +``` + +这里调用的是setRecord方法向section中插入RR数据。 + +这里的RR数据使用的是DefaultDnsQuestion。DefaultDnsQuestion的构造函数有两个,一个是要查询的domain name,这里就是”www.flydean.com”,另外一个参数是dns记录的类型。 + +dns记录的类型有很多种,在netty中有一个专门的类DnsRecordType表示,DnsRecordType中定义了很多个类型,如下所示: + +``` +public class DnsRecordType implements Comparable { + public static final DnsRecordType A = new DnsRecordType(1, "A"); + public static final DnsRecordType NS = new DnsRecordType(2, "NS"); + public static final DnsRecordType CNAME = new DnsRecordType(5, "CNAME"); + public static final DnsRecordType SOA = new DnsRecordType(6, "SOA"); + public static final DnsRecordType PTR = new DnsRecordType(12, "PTR"); + public static final DnsRecordType MX = new DnsRecordType(15, "MX"); + public static final DnsRecordType TXT = new DnsRecordType(16, "TXT"); + ... +``` + +因为类型比较多,我们挑选几个常用的进行讲解。 + +- A类型,是address的缩写,用来指定主机名或者域名对应的ip地址. +- NS类型,是name server的缩写,是域名服务器记录,用来指定域名由哪个DNS服务器来进行解析。 +- MX类型,是mail exchanger的缩写,是一个邮件交换记录,用来根据邮箱的后缀来定位邮件服务器。 +- CNAME类型,是canonical name的缩写,可以将多个名字映射到同一个主机. +- TXT类型,用来表示主机或者域名的说明信息。 + +以上几个是我们经常会用到的dns record类型。 + +这里我们选择使用A,用来查询域名对应的主机IP地址。 + +构建好query之后,我们就可以使用netty client发送query指令到dns服务器了,具体的代码如下: + +``` + DnsQuery query = new DefaultDnsQuery(randomID, DnsOpCode.QUERY) + .setRecord(DnsSection.QUESTION, new DefaultDnsQuestion(queryDomain, DnsRecordType.A)); + ch.writeAndFlush(query).sync(); +``` + +## DNS查询的消息处理 + +DNS的查询消息我们已经发送出去了,接下来就是对消息的处理和解析了。 + +还记得我们自定义的Do53ChannelInitializer吗?看一下它的实现: + +``` +class Do53ChannelInitializer extends ChannelInitializer { + @Override + protected void initChannel(SocketChannel ch) { + ChannelPipeline p = ch.pipeline(); + p.addLast(new TcpDnsQueryEncoder()) + .addLast(new TcpDnsResponseDecoder()) + .addLast(new Do53ChannelInboundHandler()); + } +} +``` + +我们向pipline中添加了两个netty自带的编码解码器TcpDnsQueryEncoder和TcpDnsResponseDecoder,还有一个自定义用来做消息解析的Do53ChannelInboundHandler。 + +因为我们向channel中写入的是DnsQuery,所以需要一个encoder将DnsQuery编码为ByteBuf,这里使用的是netty提供的TcpDnsQueryEncoder: + +``` +public final class TcpDnsQueryEncoder extends MessageToByteEncoder +``` + +TcpDnsQueryEncoder继承自MessageToByteEncoder,表示将DnsQuery编码为ByteBuf。 + +看下他的encode方法: + +``` + protected void encode(ChannelHandlerContext ctx, DnsQuery msg, ByteBuf out) throws Exception { + out.writerIndex(out.writerIndex() + 2); + this.encoder.encode(msg, out); + out.setShort(0, out.readableBytes() - 2); + } +``` + +可以看到TcpDnsQueryEncoder在msg编码之前存储了msg的长度信息,所以是一个基于长度的对象编码器。 + +这里的encoder是一个DnsQueryEncoder对象。 + +看一下它的encoder方法: + +``` + void encode(DnsQuery query, ByteBuf out) throws Exception { + encodeHeader(query, out); + this.encodeQuestions(query, out); + this.encodeRecords(query, DnsSection.ADDITIONAL, out); + } +``` + +DnsQueryEncoder会依次编码header、questions和records。 + +完成编码之后,我们还需要从DNS server的返回中decode出DnsResponse,这里使用的是netty自带的TcpDnsResponseDecoder: + +``` +public final class TcpDnsResponseDecoder extends LengthFieldBasedFrameDecoder +``` + +TcpDnsResponseDecoder继承自LengthFieldBasedFrameDecoder,表示数据是以字段长度来进行分割的,这和我们刚刚将的encoder的格式类似。 + +来看下他的decode方法: + +``` + protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception { + ByteBuf frame = (ByteBuf)super.decode(ctx, in); + if (frame == null) { + return null; + } else { + DnsResponse var4; + try { + var4 = this.responseDecoder.decode(ctx.channel().remoteAddress(), ctx.channel().localAddress(), frame.slice()); + } finally { + frame.release(); + } + return var4; + } + } +``` + +decode方法先调用LengthFieldBasedFrameDecoder的decode方法将要解码的内容提取出来,然后调用responseDecoder的decode方法,最终返回DnsResponse。 + +这里的responseDecoder是一个DnsResponseDecoder。具体decoder的细节这里就不过多阐述了。感兴趣的同学可以自行查阅代码文档。 + +最后,我们得到了DnsResponse对象。 + +接下来就是自定义的InboundHandler对消息进行解析了: + +``` +class Do53ChannelInboundHandler extends SimpleChannelInboundHandler +``` + +在它的channelRead0方法中,我们调用了readMsg方法对消息进行处理: + +``` + private static void readMsg(DefaultDnsResponse msg) { + if (msg.count(DnsSection.QUESTION) > 0) { + DnsQuestion question = msg.recordAt(DnsSection.QUESTION, 0); + log.info("question is :{}",question); + } + int i = 0, count = msg.count(DnsSection.ANSWER); + while (i < count) { + DnsRecord record = msg.recordAt(DnsSection.ANSWER, i); + //A记录用来指定主机名或者域名对应的IP地址 + if (record.type() == DnsRecordType.A) { + DnsRawRecord raw = (DnsRawRecord) record; + log.info("ip address is: {}",NetUtil.bytesToIpAddress(ByteBufUtil.getBytes(raw.content()))); + } + i++; + } + } +``` + +DefaultDnsResponse是DnsResponse的一个实现,首先判断msg中的QUESTION个数是否大于零。 + +如果大于零,则打印出question的信息。 + +然后再解析出msg中的ANSWER并打印出来。 + +最后,我们可能得到这样的输出: + +``` +INFO c.f.dnstcp.Do53ChannelInboundHandler - question is :DefaultDnsQuestion(www.flydean.com. IN A) +INFO c.f.dnstcp.Do53ChannelInboundHandler - ip address is: 47.107.98.187 +``` + +# 总结 + +以上就是使用netty创建DNS client进行TCP查询的讲解。 + +本文的代码,大家可以参考: + +[learn-netty4](https://github.com/ddean2009/learn-netty4) \ No newline at end of file diff --git a/learn-netty文章/86.netty系列之在netty中使用UDP协议请求DNS服务器.md b/learn-netty文章/86.netty系列之在netty中使用UDP协议请求DNS服务器.md new file mode 100644 index 0000000..a8d890d --- /dev/null +++ b/learn-netty文章/86.netty系列之在netty中使用UDP协议请求DNS服务器.md @@ -0,0 +1,238 @@ +# netty系列之:在netty中使用UDP协议请求DNS服务器 + +文章目录 + + + +[简介](http://www.flydean.com/55-netty-dns-over-udp/#简介)[搭建netty客户端](http://www.flydean.com/55-netty-dns-over-udp/#搭建netty客户端)[在netty中发送DNS查询请求](http://www.flydean.com/55-netty-dns-over-udp/#在netty中发送DNS查询请求)[DNS消息的处理](http://www.flydean.com/55-netty-dns-over-udp/#DNS消息的处理)[总结](http://www.flydean.com/55-netty-dns-over-udp/#总结) + +# 简介 + +之前我们讲到了如何在netty中构建client向DNS服务器进行域名解析请求。使用的是最常见的TCP协议,也叫做Do53/TCP。 + +事实上除了TCP协议之外,DNS服务器还接收UDP协议。这个协议叫做DNS-over-UDP/53,简称(“Do53”)。 + +本文将会一步一步带领大家在netty中搭建使用UDP的DNS客户端。 + +# 搭建netty客户端 + +因为这里使用的UDP协议,netty为UDP协议提供了专门的channel叫做NioDatagramChannel。EventLoopGroup还是可以使用常用的NioEventLoopGroup,这样我们搭建netty客户端的代码和常用的NIO UDP代码没有太大的区别,如下所示: + +``` +EventLoopGroup group = new NioEventLoopGroup(); + Bootstrap b = new Bootstrap(); + b.group(group) + .channel(NioDatagramChannel.class) + .handler(new Do53UdpChannelInitializer()); + final Channel ch = b.bind(0).sync().channel(); +``` + +这里的EventLoopGroup使用的是NioEventLoopGroup,作为client端Bootstrap的group。 + +因为要使用UDP协议进行传输,所以这里的channel使用的是NioDatagramChannel。 + +设置好channel之后,传入我们自定义的handler,netty client就搭建完毕了。 + +因为是UDP,所以这里没有使用TCP中的connect方法,而是使用bind方法来获得channel。 + +Do53UdpChannelInitializer中包含了netty提供的UDP DNS的编码解码器,还有自定义的消息处理器,我们会在后面的章节中详细进行介绍。 + +# 在netty中发送DNS查询请求 + +搭建好netty客户端之后,接下来就是使用客户端发送DNS查询消息了。 + +先看具体的查询代码: + +``` +int randomID = (int) (System.currentTimeMillis() / 1000); + DnsQuery query = new DatagramDnsQuery(null, addr, randomID).setRecord( + DnsSection.QUESTION, + new DefaultDnsQuestion(queryDomain, DnsRecordType.A)); + ch.writeAndFlush(query).sync(); + boolean result = ch.closeFuture().await(10, TimeUnit.SECONDS); + if (!result) { + log.error("DNS查询失败"); + ch.close().sync(); + } +``` + +查询的逻辑是先构建UDP的DnsQuery请求包,然后将这请求包写入到channel中,然后等待消息处理完毕。 + +DnsQuery之前我们已经介绍过了,他是netty中所有DNS查询的基础类。 + +``` +public interface DnsQuery extends DnsMessage +``` + +DnsQuery的子类有两个,分别是DatagramDnsQuery和DefaultDnsQuery。这两个实现类一个表示UDP协议的查询,一个表示TCP协议的查询。 + +我们看下UDP协议的DatagramDnsQuery具体定义: + +``` +public class DatagramDnsQuery extends DefaultDnsQuery implements AddressedEnvelope +``` + +可以看到DatagramDnsQuery不仅仅继承自DefaultDnsQuery,还实现了AddressedEnvelope接口。 + +AddressedEnvelope是netty中UDP包的定义,所以要想在netty中发送基于UDP协议的数据包,就必须实现AddressedEnvelope中定义的方法。 + +作为一个UDP数据包,除了基本的DNS查询中所需要的id和opCode之外,还需要提供两个额外的地址,分别是sender和recipient: + +``` + private final InetSocketAddress sender; + private final InetSocketAddress recipient; +``` + +所以DatagramDnsQuery的构造函数可以接收4个参数: + +``` + public DatagramDnsQuery(InetSocketAddress sender, InetSocketAddress recipient, int id, DnsOpCode opCode) { + super(id, opCode); + if (recipient == null && sender == null) { + throw new NullPointerException("recipient and sender"); + } else { + this.sender = sender; + this.recipient = recipient; + } + } +``` + +这里recipient和sender不能同时为空。 + +在上面的代码中,我们构建DatagramDnsQuery时,传入了服务器的InetSocketAddress: + +``` +final String dnsServer = "223.5.5.5"; + final int dnsPort = 53; + InetSocketAddress addr = new InetSocketAddress(dnsServer, dnsPort); +``` + +并且随机生成了一个ID。然后调用setRecord方法填充查询的数据。 + +``` +.setRecord(DnsSection.QUESTION, + new DefaultDnsQuestion(queryDomain, DnsRecordType.A)); +``` + +DnsSection有4个,分别是: + +``` + QUESTION, + ANSWER, + AUTHORITY, + ADDITIONAL; +``` + +这里是查询操作,所以需要设置DnsSection.QUESTION。它的值是一个DnsQuestion: + +``` +public class DefaultDnsQuestion extends AbstractDnsRecord implements DnsQuestion +``` + +在这个查询中,我们传入了要查询的domain值:www.flydean.com,还有查询的类型A:address,表示的是域名的IP地址。 + +# DNS消息的处理 + +在Do53UdpChannelInitializer中为pipline添加了netty提供的UDP编码解码器和自定义的消息处理器: + +``` +class Do53UdpChannelInitializer extends ChannelInitializer { + @Override + protected void initChannel(DatagramChannel ch) throws Exception { + ChannelPipeline p = ch.pipeline(); + p.addLast(new DatagramDnsQueryEncoder()) + .addLast(new DatagramDnsResponseDecoder()) + .addLast(new Do53UdpChannelInboundHandler()); + } +} +``` + +DatagramDnsQueryEncoder负责将DnsQuery编码成为DatagramPacket,从而可以在NioDatagramChannel中进行传输。 + +``` +public class DatagramDnsQueryEncoder extends MessageToMessageEncoder> { +``` + +DatagramDnsQueryEncoder继承自MessageToMessageEncoder,要编码的对象是AddressedEnvelope,也就是我们构建的DatagramDnsQuery。 + +看一下它里面最核心的encode方法: + +``` + protected void encode(ChannelHandlerContext ctx, AddressedEnvelope in, List out) throws Exception { + InetSocketAddress recipient = (InetSocketAddress)in.recipient(); + DnsQuery query = (DnsQuery)in.content(); + ByteBuf buf = this.allocateBuffer(ctx, in); + boolean success = false; + try { + this.encoder.encode(query, buf); + success = true; + } finally { + if (!success) { + buf.release(); + } + } + out.add(new DatagramPacket(buf, recipient, (InetSocketAddress)null)); + } +``` + +基本思路就是从AddressedEnvelope中取出recipient和DnsQuery,然后调用encoder.encode方法将DnsQuery进行编码,最后将这些数据封装到DatagramPacket中。 + +这里的encoder是一个DnsQueryEncoder实例,专门用来编码DnsQuery对象。 + +DatagramDnsResponseDecoder负责将接受到的DatagramPacket对象解码成为DnsResponse供后续的自定义程序读取使用: + +``` +public class DatagramDnsResponseDecoder extends MessageToMessageDecoder +``` + +看一下它的decode方法: + +``` + protected void decode(ChannelHandlerContext ctx, DatagramPacket packet, List out) throws Exception { + try { + out.add(this.decodeResponse(ctx, packet)); + } catch (IndexOutOfBoundsException var5) { + throw new CorruptedFrameException("Unable to decode response", var5); + } + } +``` + +上面的decode方法实际上调用了DnsResponseDecoder的decode方法进行解码操作。 + +最后就是自定义的Do53UdpChannelInboundHandler用来进行消息的读取和解析: + +``` + private static void readMsg(DatagramDnsResponse msg) { + if (msg.count(DnsSection.QUESTION) > 0) { + DnsQuestion question = msg.recordAt(DnsSection.QUESTION, 0); + log.info("question is :{}", question); + } + for (int i = 0, count = msg.count(DnsSection.ANSWER); i < count; i++) { + DnsRecord record = msg.recordAt(DnsSection.ANSWER, i); + if (record.type() == DnsRecordType.A) { + //A记录用来指定主机名或者域名对应的IP地址 + DnsRawRecord raw = (DnsRawRecord) record; + System.out.println(NetUtil.bytesToIpAddress(ByteBufUtil.getBytes(raw.content()))); + } + } + } +``` + +自定义handler接受的是一个DatagramDnsResponse对象,处理逻辑也很简单,首先读取msg中的QUESTION,并打印出来。 + +然后读取msg中的ANSWER字段,如果ANSWER的类型是A address,那么就调用NetUtil.bytesToIpAddress方法将其转换成为IP地址输出。 + +最后我们可能得到下面的输出: + +``` +question is :DefaultDnsQuestion(www.flydean.com. IN A) +49.112.38.167 +``` + +# 总结 + +以上就是在netty中使用UDP协议进行DNS查询的详细讲解。 + +本文的代码,大家可以参考: + +[learn-netty4](https://github.com/ddean2009/learn-netty4) \ No newline at end of file diff --git a/learn-netty文章/87.netty系列之 在netty中使用 tls 协议请求 DNS 服务器.md b/learn-netty文章/87.netty系列之 在netty中使用 tls 协议请求 DNS 服务器.md new file mode 100644 index 0000000..0198bd8 --- /dev/null +++ b/learn-netty文章/87.netty系列之 在netty中使用 tls 协议请求 DNS 服务器.md @@ -0,0 +1,192 @@ +# netty系列之: 在netty中使用 tls 协议请求 DNS 服务器 + +文章目录 + + + +[简介](http://www.flydean.com/56-netty-dns-over-tls/#简介)[支持DoT的DNS服务器](http://www.flydean.com/56-netty-dns-over-tls/#支持DoT的DNS服务器)[搭建支持DoT的netty客户端](http://www.flydean.com/56-netty-dns-over-tls/#搭建支持DoT的netty客户端)[TLS的客户端请求](http://www.flydean.com/56-netty-dns-over-tls/#TLS的客户端请求)[总结](http://www.flydean.com/56-netty-dns-over-tls/#总结) + +# 简介 + +在前面的文章中我们讲过了如何在netty中构造客户端分别使用tcp和udp协议向DNS服务器请求消息。在请求的过程中并没有进行消息的加密,所以这种请求是不安全的。 + +那么有同学会问了,就是请求解析一个域名的IP地址而已,还需要安全通讯吗? + +事实上,不加密的DNS查询消息是很危险的,如果你在访问一个重要的网站时候,DNS查询消息被监听或者篡改,有可能你收到的查询返回IP地址并不是真实的地址,而是被篡改之后的地址,从而打开了钓鱼网站或者其他恶意的网站,从而造成了不必要的损失。 + +所以DNS查询也是需要保证安全的。 + +幸运的是在DNS的传输协议中特意指定了一种加密的传输协议叫做DNS-over-TLS,简称(“DoT”)。 + +那么在netty中可以使用DoT来进行DNS服务查询吗?一起来看看吧。 + +# 支持DoT的DNS服务器 + +因为DNS中有很多传输协议规范,但并不是每个DNS服务器都支持所有的规范,所以我们在使用DoT之前需要找到一个能够支持DoT协议的DNS服务器。 + +这里我还是选择使用阿里DNS服务器: + +``` +223.5.5.5 +``` + +之前使用TCP和UDP协议的时候查询的DNS端口是53,如果换成了DoT,那么端口就需要变成853。 + +# 搭建支持DoT的netty客户端 + +DoT的底层还是TCP协议,也就是说TLS over TCP,所以我们需要使用NioEventLoopGroup和NioSocketChannel来搭建netty客户端,如下所示: + +``` +EventLoopGroup group = new NioEventLoopGroup(); + Bootstrap b = new Bootstrap(); + b.group(group) + .channel(NioSocketChannel.class) + .handler(new DotChannelInitializer(sslContext, dnsServer, dnsPort)); + final Channel ch = b.connect(dnsServer, dnsPort).sync().channel(); +``` + +这里选择的是NioEventLoopGroup和NioSocketChannel。然后向Bootstrap中传入自定义的DotChannelInitializer即可。 + +DotChannelInitializer中包含了自定义的handler和netty自带的handler。 + +我们来看下DotChannelInitializer的定义和他的构造函数: + +``` +class DotChannelInitializer extends ChannelInitializer { + + public DotChannelInitializer(SslContext sslContext, String dnsServer, int dnsPort) { + this.sslContext = sslContext; + this.dnsServer = dnsServer; + this.dnsPort = dnsPort; + } +``` + +DotChannelInitializer需要三个参数分别是sslContext,dnsServer和dnsPort。 + +这三个参数都是在sslContext中使用的: + +``` + protected void initChannel(SocketChannel ch) { + ChannelPipeline p = ch.pipeline(); + p.addLast(sslContext.newHandler(ch.alloc(), dnsServer, dnsPort)) + .addLast(new TcpDnsQueryEncoder()) + .addLast(new TcpDnsResponseDecoder()) + .addLast(new DotChannelInboundHandler()); + } +``` + +SslContext主要用来进行TLS配置,下面是SslContext的定义: + +``` +SslProvider provider = + SslProvider.isAlpnSupported(SslProvider.OPENSSL)? SslProvider.OPENSSL : SslProvider.JDK; + final SslContext sslContext = SslContextBuilder.forClient() + .sslProvider(provider) + .protocols("TLSv1.3", "TLSv1.2") + .build(); +``` + +因为SslProvider有很多种,可以选择openssl,也可以选择JDK自带的。 + +这里我们使用的openssl,要想提供openssl的支持,我们还需要提供openssl的依赖包如下: + +``` + + io.netty + netty-tcnative + 2.0.51.Final + + + io.netty + netty-tcnative-boringssl-static + 2.0.51.Final + +``` + +有了provider之后,就可以调用SslContextBuilder.forClient方法来创建SslContext。 + +这里我们指定SSL的protocol是”TLSv1.3″和”TLSv1.2″。 + +然后再调用sslContext的newHandler方法就创建好了支持ssl的handler: + +``` +sslContext.newHandler(ch.alloc(), dnsServer, dnsPort) +``` + +newHandler还需要指定dnsServer和dnsPort信息。 + +处理完ssl,接下来就是对dns查询和响应的编码解码器,这里使用的是TcpDnsQueryEncoder和TcpDnsResponseDecoder。 + +TcpDnsQueryEncoder和TcpDnsResponseDecoder在之前介绍使用netty搭建tcp客户端的时候就已经详细解说过了,这里就不再进行讲解了。 + +编码解码之后,就是自定义的消息处理器DotChannelInboundHandler: + +``` +class DotChannelInboundHandler extends SimpleChannelInboundHandler +``` + +DotChannelInboundHandler中定义了消息的具体处理方法: + +``` + private static void readMsg(DefaultDnsResponse msg) { + if (msg.count(DnsSection.QUESTION) > 0) { + DnsQuestion question = msg.recordAt(DnsSection.QUESTION, 0); + log.info("question is :{}", question); + } + int i = 0, count = msg.count(DnsSection.ANSWER); + while (i < count) { + DnsRecord record = msg.recordAt(DnsSection.ANSWER, i); + if (record.type() == DnsRecordType.A) { + //A记录用来指定主机名或者域名对应的IP地址 + DnsRawRecord raw = (DnsRawRecord) record; + log.info("ip address is: {}",NetUtil.bytesToIpAddress(ByteBufUtil.getBytes(raw.content()))); + } + i++; + } + } +``` + +读取的逻辑很简单,先从DefaultDnsResponse中读取QUESTION,打印出来,然后再读取它的ANSWER,因为这里是A address,所以调用NetUtil.bytesToIpAddress方法将ANSWER转换为ip地址打印出来。 + +最后我们可能得到这样的输出: + +``` +INFO c.f.dnsdot.DotChannelInboundHandler - question is :DefaultDnsQuestion(www.flydean.com. IN A) +INFO c.f.dnsdot.DotChannelInboundHandler - ip address is: 47.107.98.187 +``` + +# TLS的客户端请求 + +我们创建好channel之后,就需要向DNS server端发送查询请求了。因为是DoT,那么和普通的TCP查询有什么区别呢? + +答案是并没有什么区别,因为TLS的操作SslHandler我们已经在handler中添加了。所以这里的查询和普通查询没什么区别。 + +``` +int randomID = (int) (System.currentTimeMillis() / 1000); + DnsQuery query = new DefaultDnsQuery(randomID, DnsOpCode.QUERY) + .setRecord(DnsSection.QUESTION, new DefaultDnsQuestion(queryDomain, DnsRecordType.A)); + ch.writeAndFlush(query).sync(); + boolean result = ch.closeFuture().await(10, TimeUnit.SECONDS); + if (!result) { + log.error("DNS查询失败"); + ch.close().sync(); + } +``` + +同样我们需要构建一个DnsQuery,这里使用的是DefaultDnsQuery,通过传入一个randomID和opcode即可。 + +因为是查询,所以这里的opcode是DnsOpCode.QUERY。 + +然后需要向QUESTION section中添加一个DefaultDnsQuestion,用来查询具体的域名和类型。 + +这里的queryDomain是www.flydean.com,查询类型是A,表示的是对域名进行IP解析。 + +最后将得到的query,写入到channel中即可。 + +# 总结 + +这里我们使用netty构建了一个基于TLS的DNS查询客户端,除了添加TLS handler之外,其他操作和普通的TCP操作类似。但是要注意的是,要想客户端可以正常工作,我们需要请求支持DoT协议的DNS服务器才可以。 + +本文的代码,大家可以参考: + +[learn-netty4](https://github.com/ddean2009/learn-netty4) \ No newline at end of file diff --git a/learn-netty文章/88.netty系列之来,手把手教你使用netty搭建一个DNS tcp服务器.md b/learn-netty文章/88.netty系列之来,手把手教你使用netty搭建一个DNS tcp服务器.md new file mode 100644 index 0000000..f121b3e --- /dev/null +++ b/learn-netty文章/88.netty系列之来,手把手教你使用netty搭建一个DNS tcp服务器.md @@ -0,0 +1,195 @@ +# netty系列之:来,手把手教你使用netty搭建一个DNS tcp服务器 + +文章目录 + + + +[简介](http://www.flydean.com/57-netty-dns-tcpserver/#简介)[搭建netty服务器](http://www.flydean.com/57-netty-dns-tcpserver/#搭建netty服务器)[DNS服务器的消息处理](http://www.flydean.com/57-netty-dns-tcpserver/#DNS服务器的消息处理)[DNS客户端消息请求](http://www.flydean.com/57-netty-dns-tcpserver/#DNS客户端消息请求)[总结](http://www.flydean.com/57-netty-dns-tcpserver/#总结) + +# 简介 + +在前面的文章中,我们提到了使用netty构建tcp和udp的客户端向已经公布的DNS服务器进行域名请求服务。基本的流程是借助于netty本身的NIO通道,将要查询的信息封装成为DNSMessage,通过netty搭建的channel发送到服务器端,然后从服务器端接受返回数据,将其编码为DNSResponse,进行消息的处理。 + +那么DNS Server是否可以用netty实现呢? + +答案当然是肯定的,但是之前也讲过了DNS中有很多DnsRecordType,所以如果想实现全部的支持类型可能并现实,这里我们就以最简单和最常用的A类型为例,用netty来实现一下DNS的TCP服务器。 + +# 搭建netty服务器 + +因为是TCP请求,所以这里使用基于NIO的netty server服务,也就是NioEventLoopGroup和NioServerSocketChannel,netty服务器的代码如下: + +``` + EventLoopGroup bossGroup = new NioEventLoopGroup(1); + EventLoopGroup workerGroup = new NioEventLoopGroup(); + ServerBootstrap bootstrap = new ServerBootstrap().group(bossGroup, + workerGroup) + .channel(NioServerSocketChannel.class) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new Do53ServerChannelInitializer()); + final Channel channel = bootstrap.bind(dnsServerPort).channel(); + channel.closeFuture().sync(); +``` + +因为是服务器,所以我们需要两个EventLoopGroup,一个是bossGroup,一个是workerGroup。 + +将这两个group传递给ServerBootstrap,并指定channel是NioServerSocketChannel,然后添加自定义的Do53ServerChannelInitializer即可。 + +Do53ServerChannelInitializer中包含了netty自带的tcp编码解码器和自定义的服务器端消息处理方式。 + +这里dnsServerPort=53,也是默认的DNS服务器的端口值。 + +# DNS服务器的消息处理 + +Do53ServerChannelInitializer是我们自定义的initializer,里面为pipline添加了消息的处理handler: + +``` +class Do53ServerChannelInitializer extends ChannelInitializer { + @Override + protected void initChannel(Channel ch) throws Exception { + ch.pipeline().addLast( + new TcpDnsQueryDecoder(), + new TcpDnsResponseEncoder(), + new Do53ServerInboundHandler()); + } +} +``` + +这里我们添加了两个netty自带的编码解码器,分别是TcpDnsQueryDecoder和TcpDnsResponseEncoder。 + +对于netty服务器来说,接收到的是ByteBuf消息,为了方便服务器端的消息读取,需要将ByteBuf解码为DnsQuery,这也就是TcpDnsQueryDecoder在做的事情。 + +``` +public final class TcpDnsQueryDecoder extends LengthFieldBasedFrameDecoder +``` + +TcpDnsQueryDecoder继承自LengthFieldBasedFrameDecoder,也就是以字段长度来区分对象的起始位置。这和TCP查询传过来的数据结构是一致的。 + +下面是它的decode方法: + +``` + protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception { + ByteBuf frame = (ByteBuf)super.decode(ctx, in); + return frame == null ? null : DnsMessageUtil.decodeDnsQuery(this.decoder, frame.slice(), new DnsQueryFactory() { + public DnsQuery newQuery(int id, DnsOpCode dnsOpCode) { + return new DefaultDnsQuery(id, dnsOpCode); + } + }); + } +``` + +decode接受一个ByteBuf对象,首先调用LengthFieldBasedFrameDecoder的decode方法,将真正需要解析的内容解析出来,然后再调用DnsMessageUtil的decodeDnsQuery方法将真正的ByteBuf内容解码成为DnsQuery返回。 + +这样就可以在自定义的handler中处理DnsQuery消息了。 + +上面代码中,自定义的handler叫做Do53ServerInboundHandler: + +``` +class Do53ServerInboundHandler extends SimpleChannelInboundHandler +``` + +从定义看,Do53ServerInboundHandler要处理的消息就是DnsQuery。 + +看一下它的channelRead0方法: + +``` + protected void channelRead0(ChannelHandlerContext ctx, + DnsQuery msg) throws Exception { + DnsQuestion question = msg.recordAt(DnsSection.QUESTION); + log.info("Query is: {}", question); + ctx.writeAndFlush(newResponse(msg, question, 1000, QUERY_RESULT)); + } +``` + +我们从DnsQuery的QUESTION section中拿到DnsQuestion,然后解析DnsQuestion的内容,根据DnsQuestion的内容返回一个response给客户端。 + +这里的respone是我们自定义的: + +``` + private DefaultDnsResponse newResponse(DnsQuery query, + DnsQuestion question, + long ttl, byte[]... addresses) { + DefaultDnsResponse response = new DefaultDnsResponse(query.id()); + response.addRecord(DnsSection.QUESTION, question); + + for (byte[] address : addresses) { + DefaultDnsRawRecord queryAnswer = new DefaultDnsRawRecord( + question.name(), + DnsRecordType.A, ttl, Unpooled.wrappedBuffer(address)); + response.addRecord(DnsSection.ANSWER, queryAnswer); + } + return response; + } +``` + +上面的代码封装了一个新的DefaultDnsResponse对象,并使用query的id作为DefaultDnsResponse的id。并将question作为response的QUESEION section。 + +除了QUESTION section,response中还需要ANSWER section,这个ANSWER section需要填充一个DnsRecord。 + +这里构造了一个DefaultDnsRawRecord,传入了record的name,type,ttl和具体内容。 + +最后将构建好的DefaultDnsResponse返回。 + +因为客户端查询的是A address,按道理我们需要通过QUESTION中传入的domain名字,然后根据DNS服务器中存储的记录进行查找,最终返回对应域名的IP地址。 + +但是因为我们只是模拟的DNS服务器,所以并没有真实的域名IP记录,所以这里我们伪造了一个ip地址: + +``` + private static final byte[] QUERY_RESULT = new byte[]{46, 53, 107, 110}; +``` + +然后调用Unpooled的wrappedBuffer方法,将byte数组转换成为ByteBuf,传入DefaultDnsRawRecord的构造函数中。 + +这样我们的DNS服务器就搭建好了。 + +# DNS客户端消息请求 + +上面我们搭建好了DNS服务器,接下来就可以使用DNS客户端来请求DNS服务器了。 + +这里我们使用之前创建好的netty DNS客户端,只不过进行少许改动,将DNS服务器的域名和IP地址替换成下面的值: + +``` + Do53TcpClient client = new Do53TcpClient(); + final String dnsServer = "127.0.0.1"; + final int dnsPort = 53; + final String queryDomain ="www.flydean.com"; + client.startDnsClient(dnsServer,dnsPort,queryDomain); +``` + +dnsServer就填本机的IP地址,dnsPort就是我们刚刚创建的默认端口53。 + +首先运行DNS服务器: + +``` +INFO i.n.handler.logging.LoggingHandler - [id: 0x021762f2] REGISTERED +INFO i.n.handler.logging.LoggingHandler - [id: 0x021762f2] BIND: 0.0.0.0/0.0.0.0:53 +INFO i.n.handler.logging.LoggingHandler - [id: 0x021762f2, L:/0:0:0:0:0:0:0:0:53] ACTIVE +``` + +可以看到DNS服务器已经准备好了,绑定的端口是53。 + +然后运行上面的客户端,在客户端可以得到下面的结果: + +``` +INFO c.f.d.Do53TcpChannelInboundHandler - question is :DefaultDnsQuestion(www.flydean.com. IN A) +INFO c.f.d.Do53TcpChannelInboundHandler - ip address is: 46.53.107.110 +``` + +可以看到DNS查询成功,并且返回了我们在服务器中预设的值。 + +然后再看一下服务器端的输出: + +``` +INFO i.n.handler.logging.LoggingHandler - [id: 0x021762f2, L:/0:0:0:0:0:0:0:0:53] READ: [id: 0x44d4c761, L:/127.0.0.1:53 - R:/127.0.0.1:65471] +INFO i.n.handler.logging.LoggingHandler - [id: 0x021762f2, L:/0:0:0:0:0:0:0:0:53] READ COMPLETE +INFO c.f.d.Do53ServerInboundHandler - Query is: DefaultDnsQuestion(www.flydean.com. IN A) +``` + +可以看到服务器端成功和客户端建立了连接,并成功接收到了客户端的查询请求。 + +# 总结 + +以上就是使用netty默认DNS服务器端的实现原理和例子。因为篇幅有限,这里只是默认了type为A address的情况,对其他type感兴趣的朋友可以自行探索。 + +本文的代码,大家可以参考: + +[learn-netty4](https://github.com/ddean2009/learn-netty4) \ No newline at end of file diff --git a/learn-netty文章/89.netty系列之在netty中使用proxy protocol.md b/learn-netty文章/89.netty系列之在netty中使用proxy protocol.md new file mode 100644 index 0000000..d29b935 --- /dev/null +++ b/learn-netty文章/89.netty系列之在netty中使用proxy protocol.md @@ -0,0 +1,395 @@ +# netty系列之:在netty中使用proxy protocol + +文章目录 + + + +[简介](http://www.flydean.com/58-netty-haproxy/#简介)[netty对proxy protocol协议的支持](http://www.flydean.com/58-netty-haproxy/#netty对proxy_protocol协议的支持)[HAProxyMessage的编码解码器](http://www.flydean.com/58-netty-haproxy/#HAProxyMessage的编码解码器)[netty中proxy protocol的代码示例](http://www.flydean.com/58-netty-haproxy/#netty中proxy_protocol的代码示例)[总结](http://www.flydean.com/58-netty-haproxy/#总结) + +# 简介 + +我们知道proxy protocol是haproxy提出的一个代理协议,通过这个协议,所有实现这个协议的proxy或者LBS,都可以附带真实客户端的IP地址和端口号,这使得proxy protocol在实际应用中非常有用。 + +这么优秀的协议,没有理由netty不支持。本文将会谈一下netty中对proxy protoco代理协议的支持。 + +# netty对proxy protocol协议的支持 + +proxy protocol协议其实很简单,就是在请求前面带了proxy header信息。 + +在netty中这个header信息叫做HAProxyMessage: + +``` +public final class HAProxyMessage extends AbstractReferenceCounted { +``` + +HAProxyMessage是一个ReferenceCounted,这一点和ByteBuf很类似,说明HAProxyMessage保留着和ByteBuf很类似的特性。 + +根据proxy protocol协议,该协议可以分为两个版本,分别是v1和v2,其中v1版本是文本协议,而v2版本支持二进制的格式。 + +显然从代码编写和调试的角度来看v1更加友好,但是从程序的角度来看,v2可能性能更高。 + +HAProxyMessage中有个专门的HAProxyProtocolVersion类,来表示proxy protocol的版本信息: + +``` +public enum HAProxyProtocolVersion { + + V1(VERSION_ONE_BYTE), + + V2(VERSION_TWO_BYTE); +``` + +HAProxyProtocolVersion是一个枚举类,在它里面定义了和proxy协议相对应的两个版本号。 + +在版本号之后是command,在netty中用HAProxyCommand来表示: + +``` +public enum HAProxyCommand { + + LOCAL(HAProxyConstants.COMMAND_LOCAL_BYTE), + + PROXY(HAProxyConstants.COMMAND_PROXY_BYTE); +``` + +HAProxyCommand也是一个枚举类,里面定义了两个command的值,分别是local和proxy。 + +其中local表示该请求是代理服务器主动发起的,而不是客户端发起的,比如监控检测等请求。 + +proxy表示该请求是一个代理请求。 + +接下来是AddressFamily和TransportProtocol,这两个字段用同一个byte来表示,所以这两个类都是HAProxyProxiedProtocol的内部类。 + +先看下AddressFamily的定义: + +``` + public enum AddressFamily { + + AF_UNSPEC(AF_UNSPEC_BYTE), + + AF_IPv4(AF_IPV4_BYTE), + + AF_IPv6(AF_IPV6_BYTE), + + AF_UNIX(AF_UNIX_BYTE); +``` + +AddressFamily中定义了4个address family类型,分别是unspec,ipv4,ipv6和unix。分别对应未知family,ipv4,ipv6和unix domain socket。 + +再看下TransportProtocol的定义: + +``` + public enum TransportProtocol { + + UNSPEC(TRANSPORT_UNSPEC_BYTE), + + STREAM(TRANSPORT_STREAM_BYTE), + + DGRAM(TRANSPORT_DGRAM_BYTE); +``` + +TransportProtocol有3个值,分别是unspec,stream和dgram。分别对应未知协议,http/https协议,udp/tcp协议。 + +因为AddressFamily和TransportProtocol实际上是同一个byte,所以经过组合之后可以得到下面的几个枚举值: + +``` + UNKNOWN(TPAF_UNKNOWN_BYTE, AddressFamily.AF_UNSPEC, TransportProtocol.UNSPEC), + + TCP4(TPAF_TCP4_BYTE, AddressFamily.AF_IPv4, TransportProtocol.STREAM), + + TCP6(TPAF_TCP6_BYTE, AddressFamily.AF_IPv6, TransportProtocol.STREAM), + + UDP4(TPAF_UDP4_BYTE, AddressFamily.AF_IPv4, TransportProtocol.DGRAM), + + UDP6(TPAF_UDP6_BYTE, AddressFamily.AF_IPv6, TransportProtocol.DGRAM), + + UNIX_STREAM(TPAF_UNIX_STREAM_BYTE, AddressFamily.AF_UNIX, TransportProtocol.STREAM), + + UNIX_DGRAM(TPAF_UNIX_DGRAM_BYTE, AddressFamily.AF_UNIX, TransportProtocol.DGRAM); +``` + +以上的枚举值也是HAProxyProxiedProtocol中定义的值。 + +接下就是源ip地址,目标地ip地址,源端口和目标端口这几个值,定义为属性表示如下: + +``` + private final String sourceAddress; + private final String destinationAddress; + private final int sourcePort; + private final int destinationPort; +``` + +最后,proxy protocol中还可以包含额外的字段tlv,tlv在netty中也是一种byteBuf,使用HAProxyTLV表示: + +``` +public class HAProxyTLV extends DefaultByteBufHolder +``` + +因为tlv是key value结构,所以看下HAProxyTLV的构造函数: + +``` + public HAProxyTLV(Type type, ByteBuf content) { + this(type, Type.byteValueForType(type), content); + } +``` + +HAProxyTLV接受一个type和byteBuf的value。 + +Type是一个枚举类,在netty中可以支持下面的值: + +``` + public enum Type { + PP2_TYPE_ALPN, + PP2_TYPE_AUTHORITY, + PP2_TYPE_SSL, + PP2_TYPE_SSL_VERSION, + PP2_TYPE_SSL_CN, + PP2_TYPE_NETNS, + OTHER; +``` + +在HAProxyMessage中,tlv是一个list来保存的: + +``` +private final List tlvs; +``` + +到此,所有HAProxyMessage所需要的参数都齐了,我们看下HAProxyMessage的构造函数: + +``` + public HAProxyMessage( + HAProxyProtocolVersion protocolVersion, HAProxyCommand command, HAProxyProxiedProtocol proxiedProtocol, + String sourceAddress, String destinationAddress, int sourcePort, int destinationPort, + List tlvs) +``` + +HAProxyMessage会将所有的参数都存储到本地的变量中,供后续使用。 + +因为proxy protocol有两个版本,v1和v2,所以HAProxyMessage中提供了两个将header编码为AProxyMessage对象的方法,分别是: + +``` +static HAProxyMessage decodeHeader(ByteBuf header) +``` + +和: + +``` +static HAProxyMessage decodeHeader(String header) +``` + +有了proxy protocol的java表示之后,我们再来看一下HAProxyMessage的编码解码器。 + +# HAProxyMessage的编码解码器 + +netty对HAProxyMessage对象的支持表现在两个地方,netty提供了两个类分别对HAProxyMessage进行编码和解码,这两个类是HAProxyMessageEncoder和HAProxyMessageDecoder。 + +先看一下HAProxyMessageEncoder: + +``` +public final class HAProxyMessageEncoder extends MessageToByteEncoder +``` + +HAProxyMessageEncoder继承自MessageToByteEncoder,传入的泛型是HAProxyMessage,表示是将HAProxyMessage编码成为ByteBuf。 + +它的encode方法很简单,根据HAProxyMessage传入的message版本信息,分别进行编码: + +``` + protected void encode(ChannelHandlerContext ctx, HAProxyMessage msg, ByteBuf out) throws Exception { + switch (msg.protocolVersion()) { + case V1: + encodeV1(msg, out); + break; + case V2: + encodeV2(msg, out); + break; + default: + throw new HAProxyProtocolException("Unsupported version: " + msg.protocolVersion()); + } + } +``` + +HAProxyMessageDecoder是跟HAProxyMessageEncoder相反的动作,是将接收到的ByteBuf解析成为HAProxyMessage: + +``` +public class HAProxyMessageDecoder extends ByteToMessageDecoder +``` + +因为HAProxyMessage有两个版本,那么怎么判断接收到的ByeBuf是哪个版本呢? + +其实很简单,因为v1版本和v2版本的开始字符是不一样的,v1版本的开头是一个text:”PROXY”, v2版本的开头是一个固定的二进制串,如下所示: + +``` + static final byte[] BINARY_PREFIX = { + (byte) 0x0D, + (byte) 0x0A, + (byte) 0x0D, + (byte) 0x0A, + (byte) 0x00, + (byte) 0x0D, + (byte) 0x0A, + (byte) 0x51, + (byte) 0x55, + (byte) 0x49, + (byte) 0x54, + (byte) 0x0A + }; + + static final byte[] TEXT_PREFIX = { + (byte) 'P', + (byte) 'R', + (byte) 'O', + (byte) 'X', + (byte) 'Y', + }; +``` + +看下它的decode方法实现: + +``` + protected final void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { + if (version == -1) { + if ((version = findVersion(in)) == -1) { + return; + } + } + + ByteBuf decoded; + + if (version == 1) { + decoded = decodeLine(ctx, in); + } else { + decoded = decodeStruct(ctx, in); + } + + if (decoded != null) { + finished = true; + try { + if (version == 1) { + out.add(HAProxyMessage.decodeHeader(decoded.toString(CharsetUtil.US_ASCII))); + } else { + out.add(HAProxyMessage.decodeHeader(decoded)); + } + } catch (HAProxyProtocolException e) { + fail(ctx, null, e); + } + } + } +``` + +上面代码的逻辑是先从ByteBuf中根据版本号decode出header信息放到ByteBuf中。 + +然后再根据版本号的不同,分别调用HAProxyMessage的两个不同版本的decodeHeader方法进行解码。最终得到HAProxyMessage。 + +# netty中proxy protocol的代码示例 + +有了netty对proxy protocol的支持,那么在netty中搭建支持proxy protocol的服务器和客户端就很容易了。 + +先看一下如何搭建支持proxy protocol的服务器: + +``` + private static void startServer(int port) throws InterruptedException { + EventLoopGroup bossGroup = new NioEventLoopGroup(1); + EventLoopGroup workerGroup = new NioEventLoopGroup(); + try { + ServerBootstrap b = new ServerBootstrap(); + b.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new ServerInitializer()); + b.bind(port).sync().channel().closeFuture().sync(); + } finally { + bossGroup.shutdownGracefully(); + workerGroup.shutdownGracefully(); + } + } +``` + +代码和常规的netty server一样,这里使用了NioEventLoopGroup和NioServerSocketChannel,搭建了一个支持TCP协议的netty服务器。 + +ServerInitializer中包含了netty自带的HAProxy编码器和自定义的消息处理器: + +``` +class ServerInitializer extends ChannelInitializer { + @Override + public void initChannel(SocketChannel ch) throws Exception { + ch.pipeline().addLast( + new LoggingHandler(LogLevel.DEBUG), + new HAProxyMessageDecoder(), + new SimpleChannelInboundHandler() { + @Override + protected void channelRead0(ChannelHandlerContext ctx, Object msg) { + if (msg instanceof HAProxyMessage) { + log.info("proxy message is : {}", msg); + } else if (msg instanceof ByteBuf) { + log.info("bytebuf message is : {}", ByteBufUtil.prettyHexDump((ByteBuf) msg)); + } + } + }); + } +} +``` + +这里使用netty自带的HAProxyMessageDecoder,用来将ByteBuf消息解码为HAProxyMessage,然后在自定义的SimpleChannelInboundHandler中对HAProxyMessage进行处理。 + +这里的服务器可以处理两种消息,一种是HAProxyMessage,一种是原始的ByteBuf。处理的结果就是将消息打印出来。 + +然后看下客户端的定义: + +``` +EventLoopGroup group = new NioEventLoopGroup(); + Bootstrap b = new Bootstrap(); + b.group(group) + .channel(NioSocketChannel.class) + .handler(new ClientHander()); + Channel ch = b.connect(host, port).sync().channel(); +``` + +客户端使用的是EventLoopGroup和NioSocketChannel,是基于TCP协议的请求。 + +这里添加了自定义的handler:ClientHander,ClientHander继承自ChannelOutboundHandlerAdapter用来对client发出的消息进行处理。 + +这里看一下它的handlerAdded方法: + +``` + public void handlerAdded(ChannelHandlerContext ctx) throws Exception { + ctx.pipeline().addBefore(ctx.name(), null, HAProxyMessageEncoder.INSTANCE); + super.handlerAdded(ctx); + } +``` + +可以看到handlerAdded方法向channelPipeline中添加了HAProxyMessageEncoder,用于编码HAProxyMessage。 + +因为对于一个connection来说,HAProxyMessage只需要用到一次,后续的正常消息就不需要这个编码器了,所以我们需要在write方法中监听HAProxyMessage的状态,如果写入成功之后,就从pipeline中移出HAProxyMessageEncoder和ClientHander。 + +``` + public void write(final ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + ChannelFuture future1 = ctx.write(msg, promise); + if (msg instanceof HAProxyMessage) { + future1.addListener((ChannelFutureListener) future2 -> { + if (future2.isSuccess()) { + ctx.pipeline().remove(HAProxyMessageEncoder.INSTANCE); + ctx.pipeline().remove(ClientHander.this); + } else { + ctx.close(); + } + }); + } + } +``` + +最后我们构建了一个虚拟的HAProxyMessage,然后通过netty客户端进行发送: + +``` +HAProxyMessage message = new HAProxyMessage( + HAProxyProtocolVersion.V2, HAProxyCommand.PROXY, HAProxyProxiedProtocol.TCP4, + "127.0.0.1", "127.0.0.2", 8000, 9000); + ch.writeAndFlush(message).sync(); + ch.writeAndFlush(Unpooled.copiedBuffer("this is a proxy protocol message!", CharsetUtil.UTF_8)).sync(); + ch.close().sync(); +``` + +# 总结 + +上面的代码只是一个简单的模拟proxy protocol在netty中的使用情况,并不代表上面的代码就可以在实际的项目中应用了。如果你想使用的话,可以在下面的代码上面继续丰富和完善。 + +本文的代码,大家可以参考: + +[learn-netty4](https://github.com/ddean2009/learn-netty4) \ No newline at end of file diff --git a/learn-netty文章/8ed5c09fd5d4443487994e57ad5a8bb8.png b/learn-netty文章/8ed5c09fd5d4443487994e57ad5a8bb8.png new file mode 100644 index 0000000..180bacd Binary files /dev/null and b/learn-netty文章/8ed5c09fd5d4443487994e57ad5a8bb8.png differ diff --git a/learn-netty文章/9.netty系列之自动重连.md b/learn-netty文章/9.netty系列之自动重连.md new file mode 100644 index 0000000..0eb3db5 --- /dev/null +++ b/learn-netty文章/9.netty系列之自动重连.md @@ -0,0 +1,119 @@ +# netty系列之:自动重连 + +# 简介 + +我们在使用客户端和服务器端连接的过程中,可能会因为各种问题导致客户端和服务器的连接发生中断,遇到这种情况,一般情况下我们需要使用监控程序去监听客户端和服务器端的连接,如果第一时间发现连接断开了,就需要手动去重连。比较麻烦,今天给大家介绍一种netty中自动重连的方式。 + +# 使用netty建立连接 + +要使用netty建立连接,首先需要启动服务器,通常来说服务器通过使用ServerBootstrap来启动服务器,如下所示: + +```java +// 绑定端口并启动 +ChannelFuture f = b.bind(PORT).sync(); +``` + +对于客户端来说,可以通过Bootstrap按如下的方式启动: + +```java +// 连接服务器 +ChannelFuture f = b.connect(HOST, PORT).sync(); +``` + +# 自动重连接的原理 + +那么当客户端和服务器端的连接断了之后,如何自动重连呢? + +对于客户端来说,自动重连只需要再次调用Bootstrap的connect方法即可。现在的关键问题在于,如何找到重新调用connect的时机。 + +我们知道,不论server还是client,对于消息的处理都需要注册专门处理消息的handler。 + +对于读取消息来说,一般需要继承ChannelInboundHandlerAdapter,在这个handler中定义了很多和channel生命周期有关的方法,我们可以从这些生命周期的方法入手。 + +一般来说客户端和服务器连接的状态是这的: + +CHANNEL REGISTERED–》CHANNEL ACTIVE –》 READ –》READ COMPLETE –》 CHANNEL INACTIVE –》 CHANNEL UNREGISTERED + +客户端和服务器端的连接如果关闭的话,则会触发CHANNEL INACTIVE 和 CHANNEL UNREGISTERED 两个事件,这样我们在客户端重写下面两个方法,在方法中加入重连的逻辑即可。 + +```java +@Override +public void channelInactive(final ChannelHandlerContext ctx) { + println("连接断开:" + ctx.channel().remoteAddress()); +} + +@Override +public void channelUnregistered(final ChannelHandlerContext ctx) throws Exception { + println("sleep:" + ReconnectClient.RECONNECT_DELAY + 's'); + + ctx.channel().eventLoop().schedule(() -> { + println("重连接: " + ReconnectClient.HOST + ':' + ReconnectClient.PORT); + ReconnectClient.connect(); + }, ReconnectClient.RECONNECT_DELAY, TimeUnit.SECONDS); +} +``` + +在channelInactive方法中,我们只是打印了一些日志。主要逻辑在channelUnregistered方法中,在这个方法中我们首先通过ctx获取到当前的channel,然后拿到channel中的eventLoop,然后调用它的schedule方法,在给定的时间后重新调用connect()方法。 + +connect()方法返回的是一个ChannelFuture,所以可以在ChannelFuture中添加一些listener用来监听connect的执行状态。 + +这里定义的connect方法如下: + +```java +static void connect() { + bs.connect().addListener(future -> { + if (future.cause() != null) { + handler.startTime = -1; + handler.println("建立连接失败: " + future.cause()); + } + }); +} +``` + +# 模拟自动重连 + +上一节我们已经知道怎么自动重连了,本小节将会对自动重连进行一个模拟。 + +这里要介绍一个类,叫做IdleStateHandler,从名字就可以看出来这个类是当 Channel 没有做任何read, write操作的时候,就会触发这个Idle的状态。 + +表示Idle状态的类叫做IdleStateEvent,Idle有6个状态,分别是FIRST_READER_IDLE_STATE_EVENT,READER_IDLE_STATE_EVENT,FIRST_WRITER_IDLE_STATE_EVENT,WRITER_IDLE_STATE_EVENT,FIRST_ALL_IDLE_STATE_EVENT和ALL_IDLE_STATE_EVENT。 + +分别表示读取状态的IDLE,写状态的IDLE和读写状态的IDLE。 + +这样我们在client启动的时候就可以加上IdleStateHandler,当client一段时间没有读取到server端发来的消息的时候,我们就调用ctx.close()将channel关闭,从而出发client端的重连操作。 + +```java +bs.group(group) + .channel(NioSocketChannel.class) + .remoteAddress(HOST, PORT) + .handler(new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel ch) throws Exception { + ch.pipeline().addLast(new IdleStateHandler(READ_TIMEOUT, 0, 0), handler); + } + }); +``` + +IdleStateEvent是一个用户出发的event,要捕获到这个event,需要重写userEventTriggered: + +```java +public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + if (!(evt instanceof IdleStateEvent)) { + return; + } + IdleStateEvent e = (IdleStateEvent) evt; + if (e.state() == IdleState.READER_IDLE) { + // 在Idle状态 + println("Idle状态,关闭连接"); + ctx.close(); + } +} +``` + +上面的例子中,我们捕获了IdleStateEvent,并判断如果IdleState的状态是IdleState.READER_IDLE,那么就将channel关闭。 + +# 总结 + +本文我们介绍了重连的原理和用户触发的Event,希望大家能够喜欢。 + +本文的例子可以参考:[learn-netty4](https://github.com/ddean2009/learn-netty4) \ No newline at end of file diff --git a/learn-netty文章/9613bc4cb9314dc2903a938f80340b47.png b/learn-netty文章/9613bc4cb9314dc2903a938f80340b47.png new file mode 100644 index 0000000..7c4443d Binary files /dev/null and b/learn-netty文章/9613bc4cb9314dc2903a938f80340b47.png differ diff --git a/learn-netty文章/9c642b58f6c248f9bddfbb71799549f9.png b/learn-netty文章/9c642b58f6c248f9bddfbb71799549f9.png new file mode 100644 index 0000000..9ab95c4 Binary files /dev/null and b/learn-netty文章/9c642b58f6c248f9bddfbb71799549f9.png differ diff --git a/learn-netty文章/a844aefb374143e495729c44c9b252d4.png b/learn-netty文章/a844aefb374143e495729c44c9b252d4.png new file mode 100644 index 0000000..eb177ba Binary files /dev/null and b/learn-netty文章/a844aefb374143e495729c44c9b252d4.png differ diff --git a/learn-netty文章/ae242899a8234b668045716daa2eec1a.png b/learn-netty文章/ae242899a8234b668045716daa2eec1a.png new file mode 100644 index 0000000..4315a56 Binary files /dev/null and b/learn-netty文章/ae242899a8234b668045716daa2eec1a.png differ diff --git a/learn-netty文章/b38d484f06a9484992e301f64472a095.png b/learn-netty文章/b38d484f06a9484992e301f64472a095.png new file mode 100644 index 0000000..5c4f223 Binary files /dev/null and b/learn-netty文章/b38d484f06a9484992e301f64472a095.png differ diff --git a/learn-netty文章/c1978325b8844650b66baa7f37036fe1.png b/learn-netty文章/c1978325b8844650b66baa7f37036fe1.png new file mode 100644 index 0000000..839e7e5 Binary files /dev/null and b/learn-netty文章/c1978325b8844650b66baa7f37036fe1.png differ diff --git a/learn-netty文章/c9b27ad3e0644fcc956f5d2cf904f223.png b/learn-netty文章/c9b27ad3e0644fcc956f5d2cf904f223.png new file mode 100644 index 0000000..67701df Binary files /dev/null and b/learn-netty文章/c9b27ad3e0644fcc956f5d2cf904f223.png differ diff --git a/learn-netty文章/dda8e9896b454c6f8c42300c1170a0c3.png b/learn-netty文章/dda8e9896b454c6f8c42300c1170a0c3.png new file mode 100644 index 0000000..30ff35c Binary files /dev/null and b/learn-netty文章/dda8e9896b454c6f8c42300c1170a0c3.png differ diff --git a/learn-netty文章/ee0512f8cab94d47ae8a13fd6a062e92.png b/learn-netty文章/ee0512f8cab94d47ae8a13fd6a062e92.png new file mode 100644 index 0000000..0758640 Binary files /dev/null and b/learn-netty文章/ee0512f8cab94d47ae8a13fd6a062e92.png differ diff --git a/learn-netty文章/f025f1e1e9d94e8db5b0e0979e136116.png b/learn-netty文章/f025f1e1e9d94e8db5b0e0979e136116.png new file mode 100644 index 0000000..76424d2 Binary files /dev/null and b/learn-netty文章/f025f1e1e9d94e8db5b0e0979e136116.png differ diff --git a/learn-netty文章/f5b938e8a753482d86fc792e870b8fe7.png b/learn-netty文章/f5b938e8a753482d86fc792e870b8fe7.png new file mode 100644 index 0000000..843f7f6 Binary files /dev/null and b/learn-netty文章/f5b938e8a753482d86fc792e870b8fe7.png differ diff --git a/learn-netty文章/fe7d545f9a3647fc8383e57c8e3391d8.png b/learn-netty文章/fe7d545f9a3647fc8383e57c8e3391d8.png new file mode 100644 index 0000000..60a4268 Binary files /dev/null and b/learn-netty文章/fe7d545f9a3647fc8383e57c8e3391d8.png differ diff --git a/socks5-netty/LICENSE b/socks5-netty/LICENSE new file mode 100644 index 0000000..578ec46 --- /dev/null +++ b/socks5-netty/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 huchengyi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/socks5-netty/Netty.md b/socks5-netty/Netty.md new file mode 100644 index 0000000..ee96fef --- /dev/null +++ b/socks5-netty/Netty.md @@ -0,0 +1,131 @@ +# netty 学习 + +## 主要构件 + +### channel + +channel表示一个到实体的开放连接。实体是指硬件设备、文件、网络套接字等。channel是传入和传出数据的载体。(连接——实体——运输数据) + +### 回调 + +我的理解就是 + +1. A持有B的引用b。 +2. 当A发生了某事件时,调用引用b的callback()方法。 + +### future + +future是另一种操作完成通知应用程序的方式。 + +jdk中的实现: + +``` + public static void main(String[] args) throws ExecutionException, InterruptedException { + ExecutorService executors=Executors.newFixedThreadPool(20); + TaskCallable task=new TaskCallable(); + Future future=executors.submit(task); + System.out.println(future.isDone()); + String result=future.get(); + System.out.println(future.isDone()); + System.out.println(result); + } +``` + +样例输出 + +``` +false +true +这是callable的运行结果 +``` + +上面代码的get将会等待线程运行结束(阻塞)。 + +netty实现了自己ChannelFuture。用于异步操作(大概意思就是非阻塞的使用)。 + +ChannelFuture可以注册监听器(Listener),当ChannelFuture发生事件时,会调用监听器Listtener对应的方法。ChannelFuture和ChannelFutureListener提供的通知机制不需要程序员手动检查是否操作情况(例如上述的get、isdone等)。 + +``` + Channel channel=...; + ChannelFuture future=channel.connect("127.0.0.1",25); + future.addListener(new ChannelFutureListener(){ + @Override + public void operationComplete(ChannelFuture future){//这里有Channelfuture的引用哦 + //todo: 操作完成时的操作 + } + }); +``` + +### 事件和ChannelHandler(通过回调!) + +事件:通知状态的更改或者操作的状态 + +事件分类——入站相关: + +- 连接被激活或者连接失活(从socket的角度看都是从远端接受了数据) +- 数据读取 +- 用户事件 +- 错误事件 + +出站事件是某个动作的操作结果 + +- 打开或者关闭到远程节点的连接(从socket角度看都是向远端发送数据) +- 将数据写到或者冲刷到套接字 + +每个事件都可以被分发给ChannelHandle中某个用户实现的方法(回调)。 + +Netty提供了很多开箱即用的ChannelHandler,比如用于Http和Tls的,好像还有用于socks5的。 + +### channel和EventLoop + +在java的NIO中有一个Selector在处理套接字的可读可写事件,并且做相关的处理(派发)。 + +不同的是,Netty对每一个Channel分配一个EventLoop,用以处理所有事件!包括: + +- 注册感兴趣的事件 +- 将事件派发给ChannelHandle +- 安排进一步的事件 + +EventLoop本身由一个线程驱动,处理一个Channel的所有IO事件,并且在这个EventLoop生命周期都不会变化。 + +## 致谢 + +感谢作者提供了一个很好的学习 netty socks5 的项目。 + +之前一直想写一个代理,以了解http原理,最终目的是达到大陆到国外的不可描述的目的。作者的项目是很好的sslocal-加密 的项目。 + +## 问题描述 + +拿到这个项目,看了很久之后感觉作者写的很完善,没什么好自己动手修改的地方。 + +然后就看作者的具体实现。用jdk9带的 java misson control 工具查看了一下项目的内存情况。发现当长时间运行,产生多个与远端的连接时,程序占用的内存非常大,而且使用jmc的full gc工具也无法进行垃圾回收。具体情况可以见附图。 + +看jmc中的线程信息时,发现:出现了350多个类nioEventLoopGroup-350-1的runnable的线程。 + +结合学习的netty知识判断和对您源码的阅读,判断是在`Socks5CommandRequestHandler.channelRead0()`方法中的`EventLoopGroup bossGroup=new NioEventLoopGroup();`语句导致的。这句话导致——每次需要代理新建到远程服务器的连接时,都创建了一个NioEventLoopGroup。而且经过上面fullGC的测试,这些NioEventLoopGroup还不能被回收。 + +## 问题解决 + +netty实战告诉我,服务器端有两个NioEventLoopGroup,客户端有一个NioEventLoopGroup。 + +所以我设想,只使用一个NioEventLoopGroup固定的处理所有对远程的channel。 + +我做出了如下修改 +在`Socks5CommandRequestHandler`中增加: + +``` + private EventLoopGroup bossGroup; + public Socks5CommandRequestHandler(ProxyServer proxyServer) { + bossGroup=proxyServer.getBossGroup(); + } +``` + +即在ProxyServer中创建一个单独的NioEventLoopGroup,在Socks5CommandRequestHandler,导入这个NioEventLoopGroup,用以管理与远程的连接。 + +## 效果 + +在JMC的线程页面中显示只有3个NioEventLoopGroup(编号分别位2、3、4)。当执行full gc时,内存占用降低到13.8M。这个数据非常稳定,13.8M。 + +## 谢谢 + +这是第一次在github上提交issue。也是第一次利用自己的知识解决java内存相关的问题。感觉很宝贵。 \ No newline at end of file diff --git a/socks5-netty/README.md b/socks5-netty/README.md new file mode 100644 index 0000000..3e268c6 --- /dev/null +++ b/socks5-netty/README.md @@ -0,0 +1,34 @@ +# socks5-netty +基于netty实现的socks5代理 + +## 安装 + +- 下载git代码 ,mvn install + +- 或者直接下载bin/proxy + +## 运行 +- linux : target/assembler/jsw/proxy/bin/proxy start + +- windows : target/assembler/jsw/proxy/bin/proxy.bat start + +## 配置 + +- config.properties + - port=11080 监听端口 + - auth=true 是否鉴权 + +- password.properties + - user=password 鉴权用户密码,每行一个 + +- log4j.perperties + - log4j.logger.com.geccocrawler.socks5=info 默认级别是info只输出流量日志 + +## 扩展 +- 自定义鉴权方式 + + 实现PasswordAuth接口,通过proxyServer.passwordAuth()方法设置。系统自带的是PropertiesPasswordAuth,基于properties文件的鉴权 + +- 自定义代理日志 + + 实现ProxyFlowLog接口,通过proxyServer.proxyFlowLog()方法设置。系统自带的是ProxyFlowLog4j,基于log4j的日志记录 diff --git a/socks5-netty/bin/proxy/bin/proxy b/socks5-netty/bin/proxy/bin/proxy new file mode 100644 index 0000000..2abf8f0 --- /dev/null +++ b/socks5-netty/bin/proxy/bin/proxy @@ -0,0 +1,574 @@ +#! /bin/sh + +# +# Copyright (c) 1999, 2006 Tanuki Software Inc. +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of the Java Service Wrapper and associated +# documentation files (the "Software"), to deal in the Software +# without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sub-license, +# and/or sell copies of the Software, and to permit persons to +# whom the Software is furnished to do so, subject to the +# following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NON-INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + +# +# Java Service Wrapper sh script. Suitable for starting and stopping +# wrapped Java applications on UNIX platforms. +# +# This file is originally from Java Service Wrapper 3.2.3 distribution +# with alteration to fit the needs of AppAssembler Maven Plugin +# + + +#----------------------------------------------------------------------------- +# These settings can be modified to fit the needs of your application + +# Application +APP_NAME="proxy" +APP_LONG_NAME="socks5-netty" + +# discover BASEDIR +BASEDIR=`dirname "$0"`/.. +BASEDIR=`(cd "$BASEDIR"; pwd)` +ls -l "$0" | grep -e '->' > /dev/null 2>&1 +if [ $? = 0 ]; then + #this is softlink + _PWD=`pwd` + _EXEDIR=`dirname "$0"` + cd "$_EXEDIR" + _BASENAME=`basename "$0"` + _REALFILE=`ls -l "$_BASENAME" | sed 's/.*->\ //g'` + BASEDIR=`dirname "$_REALFILE"`/.. + BASEDIR=`(cd "$BASEDIR"; pwd)` + cd "$_PWD" +fi + + + +# Wrapper +WRAPPER_CMD="./wrapper" +WRAPPER_CONF="$BASEDIR/conf/wrapper.conf" + +# Priority at which to run the wrapper. See "man nice" for valid priorities. +# nice is only used if a priority is specified. +PRIORITY= + +# Location of the pid file. +PIDDIR="$BASEDIR/logs" + +# If uncommented, causes the Wrapper to be shutdown using an anchor file. +# When launched with the 'start' command, it will also ignore all INT and +# TERM signals. +#IGNORE_SIGNALS=true + +# If specified, the Wrapper will be run as the specified user. +# IMPORTANT - Make sure that the user has the required privileges to write +# the PID file and wrapper.log files. Failure to be able to write the log +# file will cause the Wrapper to exit without any way to write out an error +# message. +# NOTE - This will set the user which is used to run the Wrapper as well as +# the JVM and is not useful in situations where a privileged resource or +# port needs to be allocated prior to the user being changed. +#RUN_AS_USER= + +# The following two lines are used by the chkconfig command. Change as is +# appropriate for your application. They should remain commented. +# chkconfig: 2345 20 80 +# description: socks5-netty + +# Do not modify anything beyond this point +#----------------------------------------------------------------------------- + +# Get the fully qualified path to the script +case $0 in + /*) + SCRIPT="$0" + ;; + *) + PWD=`pwd` + SCRIPT="$PWD/$0" + ;; +esac + +# Resolve the true real path without any sym links. +CHANGED=true +while [ "X$CHANGED" != "X" ] +do + # Change spaces to ":" so the tokens can be parsed. + SAFESCRIPT=`echo $SCRIPT | sed -e 's; ;:;g'` + # Get the real path to this script, resolving any symbolic links + TOKENS=`echo $SAFESCRIPT | sed -e 's;/; ;g'` + REALPATH= + for C in $TOKENS; do + # Change any ":" in the token back to a space. + C=`echo $C | sed -e 's;:; ;g'` + REALPATH="$REALPATH/$C" + # If REALPATH is a sym link, resolve it. Loop for nested links. + while [ -h "$REALPATH" ] ; do + LS="`ls -ld "$REALPATH"`" + LINK="`expr "$LS" : '.*-> \(.*\)$'`" + if expr "$LINK" : '/.*' > /dev/null; then + # LINK is absolute. + REALPATH="$LINK" + else + # LINK is relative. + REALPATH="`dirname "$REALPATH"`""/$LINK" + fi + done + done + + if [ "$REALPATH" = "$SCRIPT" ] + then + CHANGED="" + else + SCRIPT="$REALPATH" + fi +done + +# Change the current directory to the location of the script +cd "`dirname "$REALPATH"`" +REALDIR=`pwd` + +# If the PIDDIR is relative, set its value relative to the full REALPATH to avoid problems if +# the working directory is later changed. +FIRST_CHAR=`echo $PIDDIR | cut -c1,1` +if [ "$FIRST_CHAR" != "/" ] +then + PIDDIR=$REALDIR/$PIDDIR +fi +# Same test for WRAPPER_CMD +FIRST_CHAR=`echo $WRAPPER_CMD | cut -c1,1` +if [ "$FIRST_CHAR" != "/" ] +then + WRAPPER_CMD=$REALDIR/$WRAPPER_CMD +fi +# Same test for WRAPPER_CONF +FIRST_CHAR=`echo $WRAPPER_CONF | cut -c1,1` +if [ "$FIRST_CHAR" != "/" ] +then + WRAPPER_CONF=$REALDIR/$WRAPPER_CONF +fi + +# Process ID +ANCHORFILE="$PIDDIR/$APP_NAME.anchor" +PIDFILE="$PIDDIR/$APP_NAME.pid" +LOCKDIR="/var/lock/subsys" +LOCKFILE="$LOCKDIR/$APP_NAME" +pid="" + +# Resolve the location of the 'ps' command +PSEXE="/usr/bin/ps" +if [ ! -x "$PSEXE" ] +then + PSEXE="/bin/ps" + if [ ! -x "$PSEXE" ] + then + echo "Unable to locate 'ps'." + echo "Please report this message along with the location of the command on your system." + exit 1 + fi +fi + +# Resolve the os +DIST_OS=`uname -s | tr "[A-Z]" "[a-z]" | tr -d ' '` +case "$DIST_OS" in + 'sunos') + DIST_OS="solaris" + ;; + 'hp-ux' | 'hp-ux64') + DIST_OS="hpux" + ;; + 'darwin') + DIST_OS="macosx" + ;; + 'unix_sv') + DIST_OS="unixware" + ;; +esac + +# Resolve the architecture +DIST_ARCH=`uname -p | tr "[A-Z]" "[a-z]" | tr -d ' '` +if [ "$DIST_ARCH" = "unknown" ] +then + DIST_ARCH=`uname -m | tr "[A-Z]" "[a-z]" | tr -d ' '` +fi +case "$DIST_ARCH" in + 'amd64' | 'athlon' | 'ia32' | 'ia64' | 'i386' | 'i486' | 'i586' | 'i686' | 'x86_64') + DIST_ARCH="x86" + ;; + 'ip27') + DIST_ARCH="mips" + ;; + 'power' | 'powerpc' | 'power_pc' | 'ppc64') + DIST_ARCH="ppc" + ;; + 'pa_risc' | 'pa-risc') + DIST_ARCH="parisc" + ;; + 'sun4u' | 'sparcv9') + DIST_ARCH="sparc" + ;; + '9000/800') + DIST_ARCH="parisc" + ;; +esac + +outputFile() { + if [ -f "$1" ] + then + echo " $1 (Found but not executable.)"; + else + echo " $1" + fi +} + +# Decide on the wrapper binary to use. +# Start with 64 bit wrapper binary, fall back to 32-bit or the default one as needed + +# For macosx, we also want to look for universal binaries. +if [ "$DIST_OS" = "macosx" ] +then + DIST_ARCH="universal" +fi + +WRAPPER_TEST_CMD="$WRAPPER_CMD-$DIST_OS-$DIST_ARCH-64" +"$WRAPPER_TEST_CMD" -v > /dev/null 2>&1 +if [ "$?" = "0" ] +then + WRAPPER_CMD="$WRAPPER_TEST_CMD" +else + WRAPPER_TEST_CMD="$WRAPPER_CMD-$DIST_OS-$DIST_ARCH-32" + "$WRAPPER_TEST_CMD" -v > /dev/null 2>&1 + if [ "$?" = "0" ] + then + WRAPPER_CMD="$WRAPPER_TEST_CMD" + else + WRAPPER_TEST_CMD="$WRAPPER_CMD" + "$WRAPPER_TEST_CMD" -v > /dev/null 2>&1 + if [ "$?" != "0" ] + then + echo "Unable to locate any of the following operational binaries:" + outputFile "$WRAPPER_CMD-$DIST_OS-$DIST_ARCH-64" + outputFile "$WRAPPER_CMD-$DIST_OS-$DIST_ARCH-32" + outputFile "$WRAPPER_CMD" + exit 1 + fi + fi +fi + +# Build the nice clause +if [ "X$PRIORITY" = "X" ] +then + CMDNICE="" +else + CMDNICE="nice -$PRIORITY" +fi + +# Build the anchor file clause. +if [ "X$IGNORE_SIGNALS" = "X" ] +then + ANCHORPROP= + IGNOREPROP= +else + ANCHORPROP=wrapper.anchorfile=\"$ANCHORFILE\" + IGNOREPROP=wrapper.ignore_signals=TRUE +fi + +# Build the lock file clause. Only create a lock file if the lock directory exists on this platform. +LOCKPROP= +if [ -d $LOCKDIR ] +then + if [ -w $LOCKDIR ] + then + LOCKPROP=wrapper.lockfile=\"$LOCKFILE\" + fi +fi + +checkUser() { + # $1 touchLock flag + # $2 command + + # Check the configured user. If necessary rerun this script as the desired user. + if [ "X$RUN_AS_USER" != "X" ] + then + # Resolve the location of the 'id' command + IDEXE="/usr/xpg4/bin/id" + if [ ! -x "$IDEXE" ] + then + IDEXE="/usr/bin/id" + if [ ! -x "$IDEXE" ] + then + echo "Unable to locate 'id'." + echo "Please report this message along with the location of the command on your system." + exit 1 + fi + fi + + if [ "`$IDEXE -u -n`" = "$RUN_AS_USER" ] + then + # Already running as the configured user. Avoid password prompts by not calling su. + RUN_AS_USER="" + fi + fi + if [ "X$RUN_AS_USER" != "X" ] + then + # If LOCKPROP and $RUN_AS_USER are defined then the new user will most likely not be + # able to create the lock file. The Wrapper will be able to update this file once it + # is created but will not be able to delete it on shutdown. If $2 is defined then + # the lock file should be created for the current command + if [ "X$LOCKPROP" != "X" ] + then + if [ "X$1" != "X" ] + then + # Resolve the primary group + RUN_AS_GROUP=`groups $RUN_AS_USER | awk '{print $3}' | tail -1` + if [ "X$RUN_AS_GROUP" = "X" ] + then + RUN_AS_GROUP=$RUN_AS_USER + fi + touch $LOCKFILE + chown $RUN_AS_USER:$RUN_AS_GROUP $LOCKFILE + fi + fi + + # Still want to change users, recurse. This means that the user will only be + # prompted for a password once. Variables shifted by 1 + su -m $RUN_AS_USER -c "\"$REALPATH\" $2" + RETVAL=$? + + # Now that we are the original user again, we may need to clean up the lock file. + if [ "X$LOCKPROP" != "X" ] + then + getpid + if [ "X$pid" = "X" ] + then + # Wrapper is not running so make sure the lock file is deleted. + if [ -f "$LOCKFILE" ] + then + rm "$LOCKFILE" + fi + fi + fi + + exit $RETVAL + fi +} + +getpid() { + if [ -f "$PIDFILE" ] + then + if [ -r "$PIDFILE" ] + then + pid=`cat "$PIDFILE"` + if [ "X$pid" != "X" ] + then + # It is possible that 'a' process with the pid exists but that it is not the + # correct process. This can happen in a number of cases, but the most + # common is during system startup after an unclean shutdown. + # The ps statement below looks for the specific wrapper command running as + # the pid. If it is not found then the pid file is considered to be stale. + if [ "$DIST_OS" = "macosx" ]; then + pidtest=`$PSEXE -p $pid -o command -ww | grep "$WRAPPER_CMD" | tail -1` + else + pidtest=`$PSEXE -p $pid -o args | grep "$WRAPPER_CMD" | tail -1` + fi + if [ "X$pidtest" = "X" ] + then + # This is a stale pid file. + rm -f "$PIDFILE" + echo "Removed stale pid file: $PIDFILE" + pid="" + fi + fi + else + echo "Cannot read $PIDFILE." + exit 1 + fi + fi +} + +testpid() { + pid=`$PSEXE -p $pid | grep $pid | grep -v grep | awk '{print $1}' | tail -1` + if [ "X$pid" = "X" ] + then + # Process is gone so remove the pid file. + rm -f "$PIDFILE" + pid="" + fi +} + +console() { + echo "Running $APP_LONG_NAME..." + getpid + if [ "X$pid" = "X" ] + then + # The string passed to eval must handles spaces in paths correctly. + COMMAND_LINE="$CMDNICE \"$WRAPPER_CMD\" \"$WRAPPER_CONF\" wrapper.syslog.ident=$APP_NAME wrapper.pidfile=\"$PIDFILE\" $ANCHORPROP $LOCKPROP $WRAPPER_CONF_OVERRIDES" + eval $COMMAND_LINE + else + echo "$APP_LONG_NAME is already running." + exit 1 + fi +} + +start() { + echo "Starting $APP_LONG_NAME..." + getpid + if [ "X$pid" = "X" ] + then + # The string passed to eval must handles spaces in paths correctly. + COMMAND_LINE="$CMDNICE \"$WRAPPER_CMD\" \"$WRAPPER_CONF\" wrapper.syslog.ident=$APP_NAME wrapper.pidfile=\"$PIDFILE\" wrapper.daemonize=TRUE $ANCHORPROP $IGNOREPROP $LOCKPROP $WRAPPER_CONF_OVERRIDES" + eval $COMMAND_LINE + else + echo "$APP_LONG_NAME is already running." + exit 1 + fi +} + +stopit() { + echo "Stopping $APP_LONG_NAME..." + getpid + if [ "X$pid" = "X" ] + then + echo "$APP_LONG_NAME was not running." + else + if [ "X$IGNORE_SIGNALS" = "X" ] + then + # Running so try to stop it. + kill $pid + if [ $? -ne 0 ] + then + # An explanation for the failure should have been given + echo "Unable to stop $APP_LONG_NAME." + exit 1 + fi + else + rm -f "$ANCHORFILE" + if [ -f "$ANCHORFILE" ] + then + # An explanation for the failure should have been given + echo "Unable to stop $APP_LONG_NAME." + exit 1 + fi + fi + + # We can not predict how long it will take for the wrapper to + # actually stop as it depends on settings in wrapper.conf. + # Loop until it does. + savepid=$pid + CNT=0 + TOTCNT=0 + while [ "X$pid" != "X" ] + do + # Show a waiting message every 5 seconds. + if [ "$CNT" -lt "5" ] + then + CNT=`expr $CNT + 1` + else + echo "Waiting for $APP_LONG_NAME to exit..." + CNT=0 + fi + TOTCNT=`expr $TOTCNT + 1` + + sleep 1 + + testpid + done + + pid=$savepid + testpid + if [ "X$pid" != "X" ] + then + echo "Failed to stop $APP_LONG_NAME." + exit 1 + else + echo "Stopped $APP_LONG_NAME." + fi + fi +} + +status() { + getpid + if [ "X$pid" = "X" ] + then + echo "$APP_LONG_NAME is not running." + exit 1 + else + echo "$APP_LONG_NAME is running ($pid)." + exit 0 + fi +} + +dump() { + echo "Dumping $APP_LONG_NAME..." + getpid + if [ "X$pid" = "X" ] + then + echo "$APP_LONG_NAME was not running." + + else + kill -3 $pid + + if [ $? -ne 0 ] + then + echo "Failed to dump $APP_LONG_NAME." + exit 1 + else + echo "Dumped $APP_LONG_NAME." + fi + fi +} + +case "$1" in + + 'console') + checkUser touchlock $1 + console + ;; + + 'start') + checkUser touchlock $1 + start + ;; + + 'stop') + checkUser "" $1 + stopit + ;; + + 'restart') + checkUser touchlock $1 + stopit + start + ;; + + 'status') + checkUser "" $1 + status + ;; + + 'dump') + checkUser "" $1 + dump + ;; + + *) + echo "Usage: $0 { console | start | stop | restart | status | dump }" + exit 1 + ;; +esac + +exit 0 diff --git a/socks5-netty/bin/proxy/bin/proxy.bat b/socks5-netty/bin/proxy/bin/proxy.bat new file mode 100644 index 0000000..869e340 --- /dev/null +++ b/socks5-netty/bin/proxy/bin/proxy.bat @@ -0,0 +1,143 @@ +@echo off +setlocal + +rem +rem Copyright (c) 1999, 2006 Tanuki Software Inc. +rem +rem Permission is hereby granted, free of charge, to any person +rem obtaining a copy of the Java Service Wrapper and associated +rem documentation files (the "Software"), to deal in the Software +rem without restriction, including without limitation the rights +rem to use, copy, modify, merge, publish, distribute, sub-license, +rem and/or sell copies of the Software, and to permit persons to +rem whom the Software is furnished to do so, subject to the +rem following conditions: +rem +rem The above copyright notice and this permission notice shall be +rem included in all copies or substantial portions of the Software. +rem +rem THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +rem EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +rem OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +rem NON-INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +rem HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +rem WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +rem FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +rem OTHER DEALINGS IN THE SOFTWARE. + +rem +rem Java Service Wrapper script. Suitable for starting and stopping +rem wrapped Java applications on Windows platforms. +rem +rem This file is originally from Java Service Wrapper 3.2.3 distribution +rem with alteration to fit the needs of AppAssembler Maven Plugin +rem + +if "%OS%"=="Windows_NT" goto nt +echo This script only works with NT-based versions of Windows. +exit /b 1 + +:nt + +set BASEDIR=%~dp0\.. + + +rem +rem Find the application home. +rem +rem %~dp0 is location of current script under NT +set _REALPATH=%~dp0 + +rem Decide on the wrapper binary. +set _WRAPPER_BASE=wrapper + +if "%PROCESSOR_ARCHITEW6432%"=="AMD64" goto amd64 +if "%PROCESSOR_ARCHITECTURE%"=="AMD64" goto amd64 +if "%PROCESSOR_ARCHITECTURE%"=="IA64" goto ia64 + +set _WRAPPER_EXE=%_REALPATH%%_WRAPPER_BASE%-windows-x86-32.exe +goto validate_wrapper_exe + +:amd64 +set _WRAPPER_EXE=%_REALPATH%%_WRAPPER_BASE%-windows-x86-64.exe +goto validate_wrapper_exe + +:ia64 +set _WRAPPER_EXE=%_REALPATH%%_WRAPPER_BASE%-windows-ia-64.exe +goto validate_wrapper_exe + +:validate_wrapper_exe +if NOT exist "%_WRAPPER_EXE%" set _WRAPPER_EXE=%_REALPATH%%_WRAPPER_BASE%.exe + +if exist "%_WRAPPER_EXE%" goto validate + +echo Unable to locate a Wrapper executable using any of the following names: +echo %_REALPATH%%_WRAPPER_BASE%-windows-x86-32.exe +echo %_REALPATH%%_WRAPPER_BASE%-windows-x86-64.exe +echo %_REALPATH%%_WRAPPER_BASE%.exe +exit /b 1 + +:validate +rem Find the requested command. +for /F %%v in ('echo %1^|findstr "^console$ ^start$ ^pause$ ^resume$ ^stop$ ^restart$ ^install$ ^remove ^status"') do call :exec set COMMAND=%%v + +if "%COMMAND%" == "" ( + echo Usage: %0 { console : start : pause : resume : stop : restart : install : remove : status } + exit /b 1 +) else ( + shift +) + +rem +rem Find the wrapper.conf +rem +:conf +set _WRAPPER_CONF="%_REALPATH%..\conf\wrapper.conf" + +rem +rem Run the application. +rem +call :%COMMAND% +goto :eof + +:console +"%_WRAPPER_EXE%" -c %_WRAPPER_CONF% %WRAPPER_CONF_OVERRIDES% +goto :eof + +:start +"%_WRAPPER_EXE%" -t %_WRAPPER_CONF% %WRAPPER_CONF_OVERRIDES% +goto :eof + +:pause +"%_WRAPPER_EXE%" -a %_WRAPPER_CONF% %WRAPPER_CONF_OVERRIDES% +goto :eof + +:resume +"%_WRAPPER_EXE%" -e %_WRAPPER_CONF% %WRAPPER_CONF_OVERRIDES% +goto :eof + +:stop +"%_WRAPPER_EXE%" -p %_WRAPPER_CONF% %WRAPPER_CONF_OVERRIDES% +goto :eof + +:install +"%_WRAPPER_EXE%" -i %_WRAPPER_CONF% %WRAPPER_CONF_OVERRIDES% +goto :eof + +:remove +"%_WRAPPER_EXE%" -r %_WRAPPER_CONF% %WRAPPER_CONF_OVERRIDES% +goto :eof + +:status +"%_WRAPPER_EXE%" -q %_WRAPPER_CONF% %WRAPPER_CONF_OVERRIDES% +goto :eof + +:restart +call :stop +call :start +goto :eof + +:exec +%* +goto :eof + diff --git a/socks5-netty/bin/proxy/bin/wrapper-linux-x86-64 b/socks5-netty/bin/proxy/bin/wrapper-linux-x86-64 new file mode 100644 index 0000000..3128b95 Binary files /dev/null and b/socks5-netty/bin/proxy/bin/wrapper-linux-x86-64 differ diff --git a/socks5-netty/bin/proxy/bin/wrapper-windows-x86-64.exe b/socks5-netty/bin/proxy/bin/wrapper-windows-x86-64.exe new file mode 100644 index 0000000..db2ddda Binary files /dev/null and b/socks5-netty/bin/proxy/bin/wrapper-windows-x86-64.exe differ diff --git a/socks5-netty/bin/proxy/conf/config.properties b/socks5-netty/bin/proxy/conf/config.properties new file mode 100644 index 0000000..0bb1010 --- /dev/null +++ b/socks5-netty/bin/proxy/conf/config.properties @@ -0,0 +1,2 @@ +port=11080 +auth=true \ No newline at end of file diff --git a/socks5-netty/bin/proxy/conf/log4j.properties b/socks5-netty/bin/proxy/conf/log4j.properties new file mode 100644 index 0000000..ed001bf --- /dev/null +++ b/socks5-netty/bin/proxy/conf/log4j.properties @@ -0,0 +1,10 @@ +log4j.rootLogger=error,stdout + +#stdout console appender +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=[%d{yyyy.MM.dd HH:mm:ss}] %p %C:%M(%L) - %m%n + +#log4j.logger.io.netty=debug +#log4j.logger.io.netty.handler=debug +log4j.logger.com.geccocrawler.socks5=info \ No newline at end of file diff --git a/socks5-netty/bin/proxy/conf/password.properties b/socks5-netty/bin/proxy/conf/password.properties new file mode 100644 index 0000000..51e670a --- /dev/null +++ b/socks5-netty/bin/proxy/conf/password.properties @@ -0,0 +1 @@ +test=test \ No newline at end of file diff --git a/socks5-netty/bin/proxy/conf/wrapper.conf b/socks5-netty/bin/proxy/conf/wrapper.conf new file mode 100644 index 0000000..3228bd6 --- /dev/null +++ b/socks5-netty/bin/proxy/conf/wrapper.conf @@ -0,0 +1,108 @@ +#******************************************************************** +# Wrapper Properties +#******************************************************************** +# Java Application +wrapper.java.command=java +wrapper.working.dir=.. + +# Java Main class. This class must implement the WrapperListener interface +# or guarantee that the WrapperManager class is initialized. Helper +# classes are provided to do this for you. See the Integration section +# of the documentation for details. +wrapper.java.mainclass=org.tanukisoftware.wrapper.WrapperSimpleApp +set.default.REPO_DIR=lib +set.APP_BASE=. + +# Java Classpath (include wrapper.jar) Add class path elements as +# needed starting from 1 +wrapper.java.classpath.1=lib/wrapper.jar +wrapper.java.classpath.2=conf +wrapper.java.classpath.3=%REPO_DIR%/socks5-netty-0.0.1-SNAPSHOT.jar +wrapper.java.classpath.4=%REPO_DIR%/netty-all-4.1.6.Final.jar +wrapper.java.classpath.5=%REPO_DIR%/slf4j-log4j12-1.7.21.jar +wrapper.java.classpath.6=%REPO_DIR%/slf4j-api-1.7.21.jar +wrapper.java.classpath.7=%REPO_DIR%/log4j-1.2.17.jar +wrapper.java.classpath.8=%REPO_DIR%/commons-codec-1.10.jar + +# Java Library Path (location of Wrapper.DLL or libwrapper.so) +wrapper.java.library.path.1=lib + +# Java Additional Parameters +#wrapper.java.additional.1= +wrapper.java.additional.1=-Djava.net.preferIPv4Stack=true + +# Initial Java Heap Size (in MB) +#wrapper.java.initmemory=3 +wrapper.java.initmemory=256M + +# Maximum Java Heap Size (in MB) +#wrapper.java.maxmemory=64 +wrapper.java.maxmemory=512M + +# Application parameters. Add parameters as needed starting from 1 +wrapper.app.parameter.1=com.geccocrawler.socks5.ProxyServer + +#******************************************************************** +# Wrapper Logging Properties +#******************************************************************** +# Format of output for the console. (See docs for formats) +wrapper.console.format=PM + +# Log Level for console output. (See docs for log levels) +wrapper.console.loglevel=INFO + +# Log file to use for wrapper output logging. +wrapper.logfile=log/app.log + +# Format of output for the log file. (See docs for formats) +wrapper.logfile.format=LPTM + +# Log Level for log file output. (See docs for log levels) +wrapper.logfile.loglevel=INFO + +# Maximum size that the log file will be allowed to grow to before +# the log is rolled. Size is specified in bytes. The default value +# of 0, disables log rolling. May abbreviate with the 'k' (kb) or +# 'm' (mb) suffix. For example: 10m = 10 megabytes. +wrapper.logfile.maxsize=0 + +# Maximum number of rolled log files which will be allowed before old +# files are deleted. The default value of 0 implies no limit. +wrapper.logfile.maxfiles=0 + +# Log Level for sys/event log output. (See docs for log levels) +wrapper.syslog.loglevel=NONE + +#******************************************************************** +# Wrapper Windows Properties +#******************************************************************** +# Title to use when running as a console +wrapper.console.title=socks5-netty + +#******************************************************************** +# Wrapper Windows NT/2000/XP Service Properties +#******************************************************************** +# WARNING - Do not modify any of these properties when an application +# using this configuration file has been installed as a service. +# Please uninstall the service before modifying this section. The +# service can then be reinstalled. + +# Name of the service +wrapper.ntservice.name=proxy + +# Display name of the service +wrapper.ntservice.displayname=socks5-netty + +# Description of the service +wrapper.ntservice.description=socks5-netty + +# Service dependencies. Add dependencies as needed starting from 1 +wrapper.ntservice.dependency.1= + +# Mode in which the service is installed. AUTO_START or DEMAND_START +wrapper.ntservice.starttype=AUTO_START + +# Allow the service to interact with the desktop. +wrapper.ntservice.interactive=false + +configuration.directory.in.classpath.first=conf diff --git a/socks5-netty/bin/proxy/lib/commons-codec-1.10.jar b/socks5-netty/bin/proxy/lib/commons-codec-1.10.jar new file mode 100644 index 0000000..1d7417c Binary files /dev/null and b/socks5-netty/bin/proxy/lib/commons-codec-1.10.jar differ diff --git a/socks5-netty/bin/proxy/lib/libwrapper-linux-x86-64.so b/socks5-netty/bin/proxy/lib/libwrapper-linux-x86-64.so new file mode 100644 index 0000000..24197bf Binary files /dev/null and b/socks5-netty/bin/proxy/lib/libwrapper-linux-x86-64.so differ diff --git a/socks5-netty/bin/proxy/lib/log4j-1.2.17.jar b/socks5-netty/bin/proxy/lib/log4j-1.2.17.jar new file mode 100644 index 0000000..1d425cf Binary files /dev/null and b/socks5-netty/bin/proxy/lib/log4j-1.2.17.jar differ diff --git a/socks5-netty/bin/proxy/lib/netty-all-4.1.6.Final.jar b/socks5-netty/bin/proxy/lib/netty-all-4.1.6.Final.jar new file mode 100644 index 0000000..af9fec7 Binary files /dev/null and b/socks5-netty/bin/proxy/lib/netty-all-4.1.6.Final.jar differ diff --git a/socks5-netty/bin/proxy/lib/slf4j-api-1.7.21.jar b/socks5-netty/bin/proxy/lib/slf4j-api-1.7.21.jar new file mode 100644 index 0000000..2a5c33e Binary files /dev/null and b/socks5-netty/bin/proxy/lib/slf4j-api-1.7.21.jar differ diff --git a/socks5-netty/bin/proxy/lib/slf4j-log4j12-1.7.21.jar b/socks5-netty/bin/proxy/lib/slf4j-log4j12-1.7.21.jar new file mode 100644 index 0000000..ff4fddd Binary files /dev/null and b/socks5-netty/bin/proxy/lib/slf4j-log4j12-1.7.21.jar differ diff --git a/socks5-netty/bin/proxy/lib/socks5-netty-0.0.1-SNAPSHOT.jar b/socks5-netty/bin/proxy/lib/socks5-netty-0.0.1-SNAPSHOT.jar new file mode 100644 index 0000000..ea40e62 Binary files /dev/null and b/socks5-netty/bin/proxy/lib/socks5-netty-0.0.1-SNAPSHOT.jar differ diff --git a/socks5-netty/bin/proxy/lib/wrapper-windows-x86-64.dll b/socks5-netty/bin/proxy/lib/wrapper-windows-x86-64.dll new file mode 100644 index 0000000..f07fc9e Binary files /dev/null and b/socks5-netty/bin/proxy/lib/wrapper-windows-x86-64.dll differ diff --git a/socks5-netty/bin/proxy/lib/wrapper.jar b/socks5-netty/bin/proxy/lib/wrapper.jar new file mode 100644 index 0000000..4db355b Binary files /dev/null and b/socks5-netty/bin/proxy/lib/wrapper.jar differ diff --git a/socks5-netty/bin/proxy/wrapper.log b/socks5-netty/bin/proxy/wrapper.log new file mode 100644 index 0000000..e5415ad --- /dev/null +++ b/socks5-netty/bin/proxy/wrapper.log @@ -0,0 +1,6 @@ +STATUS | wrapper | 2016/12/28 16:56:45 | --> Wrapper Started as Console +STATUS | wrapper | 2016/12/28 16:56:45 | Launching a JVM... +INFO | jvm 1 | 2016/12/28 16:56:45 | Wrapper (Version 3.2.3) http://wrapper.tanukisoftware.org +INFO | jvm 1 | 2016/12/28 16:56:45 | Copyright 1999-2006 Tanuki Software, Inc. All Rights Reserved. +INFO | jvm 1 | 2016/12/28 16:56:45 | +INFO | jvm 1 | 2016/12/28 16:57:01 | [2016.12.28 16:57:01] INFO com.geccocrawler.socks5.log.ProxyFlowLog4j:log(29) - test,1482915419446,1482915419829,10.2.17.71:11080,127.0.0.1:57596,514,6779,7293 diff --git a/socks5-netty/pom.xml b/socks5-netty/pom.xml new file mode 100644 index 0000000..fe78614 --- /dev/null +++ b/socks5-netty/pom.xml @@ -0,0 +1,74 @@ + + 4.0.0 + com.geccocrawler + socks5-netty + 0.0.1-SNAPSHOT + + + UTF-8 + 1.9 + 1.9 + + + + io.netty + netty-all + 4.1.6.Final + + + org.slf4j + slf4j-log4j12 + 1.7.21 + + + commons-codec + commons-codec + 1.10 + + + com.squareup.okhttp3 + okhttp + 3.5.0 + test + + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 2.6 + + + + true + + com.geccocrawler.socks5.ProxyServer + + + + + + org.apache.maven.plugins + maven-dependency-plugin + 2.10 + + + copy-dependencies + package + + copy-dependencies + + + ${project.build.directory}/lib + + + + + + + + \ No newline at end of file diff --git a/socks5-netty/socks5-netty.iml b/socks5-netty/socks5-netty.iml new file mode 100644 index 0000000..9e0549d --- /dev/null +++ b/socks5-netty/socks5-netty.iml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/socks5-netty/src/main/java/com/geccocrawler/socks5/ProxyServer.java b/socks5-netty/src/main/java/com/geccocrawler/socks5/ProxyServer.java new file mode 100644 index 0000000..8c882e8 --- /dev/null +++ b/socks5-netty/src/main/java/com/geccocrawler/socks5/ProxyServer.java @@ -0,0 +1,181 @@ +package com.geccocrawler.socks5; + +import java.util.Properties; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.geccocrawler.socks5.auth.PasswordAuth; +import com.geccocrawler.socks5.auth.PropertiesPasswordAuth; +import com.geccocrawler.socks5.handler.ChannelListener; +import com.geccocrawler.socks5.handler.ProxyChannelTrafficShapingHandler; +import com.geccocrawler.socks5.handler.ProxyIdleHandler; +import com.geccocrawler.socks5.handler.ss5.Socks5CommandRequestHandler; +import com.geccocrawler.socks5.handler.ss5.Socks5InitialRequestHandler; +import com.geccocrawler.socks5.handler.ss5.Socks5PasswordAuthRequestHandler; +import com.geccocrawler.socks5.log.ProxyFlowLog; +import com.geccocrawler.socks5.log.ProxyFlowLog4j; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.codec.socksx.v5.Socks5CommandRequestDecoder; +import io.netty.handler.codec.socksx.v5.Socks5InitialRequestDecoder; +import io.netty.handler.codec.socksx.v5.Socks5PasswordAuthRequestDecoder; +import io.netty.handler.codec.socksx.v5.Socks5ServerEncoder; +import io.netty.handler.logging.LoggingHandler; +import io.netty.handler.timeout.IdleStateHandler; + +public class ProxyServer { + + private EventLoopGroup bossGroup=new NioEventLoopGroup(); + + public EventLoopGroup getBossGroup() { + return bossGroup; + } + + private static final Logger logger = LoggerFactory.getLogger(ProxyServer.class); + + private int port; + + private boolean auth; + + private boolean logging; + + private ProxyFlowLog proxyFlowLog; + + private ChannelListener channelListener; + + private PasswordAuth passwordAuth; + + private ProxyServer(int port) { + this.port = port; + } + + public static ProxyServer create(int port) { + return new ProxyServer(port); + } + + public ProxyServer auth(boolean auth) { + this.auth = auth; + return this; + } + + public ProxyServer logging(boolean logging) { + this.logging = logging; + return this; + } + + public ProxyServer proxyFlowLog(ProxyFlowLog proxyFlowLog) { + this.proxyFlowLog = proxyFlowLog; + return this; + } + + public ProxyServer channelListener(ChannelListener channelListener) { + this.channelListener = channelListener; + return this; + } + + public ProxyServer passwordAuth(PasswordAuth passwordAuth) { + this.passwordAuth = passwordAuth; + return this; + } + + public ProxyFlowLog getProxyFlowLog() { + return proxyFlowLog; + } + + public ChannelListener getChannelListener() { + return channelListener; + } + + public PasswordAuth getPasswordAuth() { + return passwordAuth; + } + + public boolean isAuth() { + return auth; + } + + public boolean isLogging() { + return logging; + } + + public void start() throws Exception { + if(proxyFlowLog == null) { + proxyFlowLog = new ProxyFlowLog4j(); + } + if(passwordAuth == null) { + passwordAuth = new PropertiesPasswordAuth(); + } + EventLoopGroup boss = new NioEventLoopGroup(2); + EventLoopGroup worker = new NioEventLoopGroup(); + try { + ServerBootstrap bootstrap = new ServerBootstrap(); + bootstrap.group(boss, worker) + .channel(NioServerSocketChannel.class) + .option(ChannelOption.SO_BACKLOG, 1024) + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 1000) + .childHandler(new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel ch) throws Exception { + //流量统计 + ch.pipeline().addLast( + ProxyChannelTrafficShapingHandler.PROXY_TRAFFIC, + new ProxyChannelTrafficShapingHandler(3000, proxyFlowLog, channelListener) + ); + //channel超时处理 + ch.pipeline().addLast(new IdleStateHandler(3, 30, 0)); + ch.pipeline().addLast(new ProxyIdleHandler()); + //netty日志 + if(logging) { + ch.pipeline().addLast(new LoggingHandler()); + } + //Socks5MessagByteBuf + ch.pipeline().addLast(Socks5ServerEncoder.DEFAULT); + //sock5 init + ch.pipeline().addLast(new Socks5InitialRequestDecoder()); + //sock5 init + ch.pipeline().addLast(new Socks5InitialRequestHandler(ProxyServer.this)); + if(isAuth()) { + //socks auth + ch.pipeline().addLast(new Socks5PasswordAuthRequestDecoder()); + //socks auth + ch.pipeline().addLast(new Socks5PasswordAuthRequestHandler(getPasswordAuth())); + } + //socks connection + ch.pipeline().addLast(new Socks5CommandRequestDecoder()); + //Socks connection + ch.pipeline().addLast(new Socks5CommandRequestHandler(ProxyServer.this.getBossGroup())); + } + }); + + ChannelFuture future = bootstrap.bind(port).sync(); + logger.debug("bind port : " + port); + future.channel().closeFuture().sync(); + } finally { + boss.shutdownGracefully(); + worker.shutdownGracefully(); + } + } + + public static void main(String[] args) throws Exception { + int port = 11080; + boolean auth = false; + Properties properties = new Properties(); + try { + properties.load(ProxyServer.class.getResourceAsStream("/config.properties")); + port = Integer.parseInt(properties.getProperty("port")); + auth = Boolean.parseBoolean(properties.getProperty("auth")); + } catch(Exception e) { + logger.warn("load config.properties error, default port 11080, auth false!"); + } + ProxyServer.create(port).logging(true).auth(auth).start(); + } +} diff --git a/socks5-netty/src/main/java/com/geccocrawler/socks5/auth/PasswordAuth.java b/socks5-netty/src/main/java/com/geccocrawler/socks5/auth/PasswordAuth.java new file mode 100644 index 0000000..ae8d82f --- /dev/null +++ b/socks5-netty/src/main/java/com/geccocrawler/socks5/auth/PasswordAuth.java @@ -0,0 +1,7 @@ +package com.geccocrawler.socks5.auth; + +public interface PasswordAuth { + + public boolean auth(String user, String password); + +} diff --git a/socks5-netty/src/main/java/com/geccocrawler/socks5/auth/PropertiesPasswordAuth.java b/socks5-netty/src/main/java/com/geccocrawler/socks5/auth/PropertiesPasswordAuth.java new file mode 100644 index 0000000..7347514 --- /dev/null +++ b/socks5-netty/src/main/java/com/geccocrawler/socks5/auth/PropertiesPasswordAuth.java @@ -0,0 +1,29 @@ +package com.geccocrawler.socks5.auth; + +import java.io.IOException; +import java.util.Properties; + +public class PropertiesPasswordAuth implements PasswordAuth { + + private static Properties properties; + + static { + properties = new Properties(); + try { + properties.load(PropertiesPasswordAuth.class.getResourceAsStream("/password.properties")); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public boolean auth(String user, String password) { + String configPasword = properties.getProperty(user); + if(configPasword != null) { + if(password.equals(configPasword)) { + return true; + } + } + return false; + } + +} diff --git a/socks5-netty/src/main/java/com/geccocrawler/socks5/handler/ChannelListener.java b/socks5-netty/src/main/java/com/geccocrawler/socks5/handler/ChannelListener.java new file mode 100644 index 0000000..b1b3478 --- /dev/null +++ b/socks5-netty/src/main/java/com/geccocrawler/socks5/handler/ChannelListener.java @@ -0,0 +1,10 @@ +package com.geccocrawler.socks5.handler; + +import io.netty.channel.ChannelHandlerContext; + +public interface ChannelListener { + + public void inActive(ChannelHandlerContext ctx); + + public void active(ChannelHandlerContext ctx); +} diff --git a/socks5-netty/src/main/java/com/geccocrawler/socks5/handler/ProxyChannelTrafficShapingHandler.java b/socks5-netty/src/main/java/com/geccocrawler/socks5/handler/ProxyChannelTrafficShapingHandler.java new file mode 100644 index 0000000..5a4d730 --- /dev/null +++ b/socks5-netty/src/main/java/com/geccocrawler/socks5/handler/ProxyChannelTrafficShapingHandler.java @@ -0,0 +1,67 @@ +package com.geccocrawler.socks5.handler; + +import com.geccocrawler.socks5.log.ProxyFlowLog; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.traffic.ChannelTrafficShapingHandler; + +public class ProxyChannelTrafficShapingHandler extends ChannelTrafficShapingHandler { + + public static final String PROXY_TRAFFIC = "ProxyChannelTrafficShapingHandler"; + + private long beginTime; + + private long endTime; + + private String username = "anonymous"; + + private ProxyFlowLog proxyFlowLog; + + private ChannelListener channelListener; + + public static ProxyChannelTrafficShapingHandler get(ChannelHandlerContext ctx) { + return (ProxyChannelTrafficShapingHandler)ctx.pipeline().get(PROXY_TRAFFIC); + } + + public ProxyChannelTrafficShapingHandler(long checkInterval, ProxyFlowLog proxyFlowLog, ChannelListener channelListener) { + super(checkInterval); + this.proxyFlowLog = proxyFlowLog; + this.channelListener = channelListener; + } + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + beginTime = System.currentTimeMillis(); + if(channelListener != null) { + channelListener.active(ctx); + } + super.channelActive(ctx); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + endTime = System.currentTimeMillis(); + if(channelListener != null) { + channelListener.inActive(ctx); + } + proxyFlowLog.log(ctx); + super.channelInactive(ctx); + } + + public long getBeginTime() { + return beginTime; + } + + public long getEndTime() { + return endTime; + } + + public static void username(ChannelHandlerContext ctx, String username) { + get(ctx).username = username; + } + + public String getUsername() { + return username; + } + +} diff --git a/socks5-netty/src/main/java/com/geccocrawler/socks5/handler/ProxyIdleHandler.java b/socks5-netty/src/main/java/com/geccocrawler/socks5/handler/ProxyIdleHandler.java new file mode 100644 index 0000000..14af24d --- /dev/null +++ b/socks5-netty/src/main/java/com/geccocrawler/socks5/handler/ProxyIdleHandler.java @@ -0,0 +1,18 @@ +package com.geccocrawler.socks5.handler; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.timeout.IdleStateEvent; + +public class ProxyIdleHandler extends ChannelInboundHandlerAdapter { + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt instanceof IdleStateEvent) { + ctx.channel().close(); + } else { + super.userEventTriggered(ctx, evt); + } + } + +} diff --git a/socks5-netty/src/main/java/com/geccocrawler/socks5/handler/ss5/Socks5CommandRequestHandler.java b/socks5-netty/src/main/java/com/geccocrawler/socks5/handler/ss5/Socks5CommandRequestHandler.java new file mode 100644 index 0000000..3063385 --- /dev/null +++ b/socks5-netty/src/main/java/com/geccocrawler/socks5/handler/ss5/Socks5CommandRequestHandler.java @@ -0,0 +1,127 @@ +package com.geccocrawler.socks5.handler.ss5; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.socksx.v5.DefaultSocks5CommandRequest; +import io.netty.handler.codec.socksx.v5.DefaultSocks5CommandResponse; +import io.netty.handler.codec.socksx.v5.Socks5AddressType; +import io.netty.handler.codec.socksx.v5.Socks5CommandResponse; +import io.netty.handler.codec.socksx.v5.Socks5CommandStatus; +import io.netty.handler.codec.socksx.v5.Socks5CommandType; + +public class Socks5CommandRequestHandler extends SimpleChannelInboundHandler{ + EventLoopGroup bossGroup; + + private static final Logger logger = LoggerFactory.getLogger(Socks5CommandRequestHandler.class); + + public Socks5CommandRequestHandler(EventLoopGroup bossGroup) { + this.bossGroup=bossGroup; + } + + @Override + protected void channelRead0(final ChannelHandlerContext clientChannelContext, DefaultSocks5CommandRequest msg) throws Exception { + logger.debug("目标服务器 : " + msg.type() + "," + msg.dstAddr() + "," + msg.dstPort()); + if(msg.type().equals(Socks5CommandType.CONNECT)) { + logger.trace("准备连接目标服务器"); + + Bootstrap bootstrap = new Bootstrap(); + bootstrap.group(bossGroup) + .channel(NioSocketChannel.class) + .option(ChannelOption.TCP_NODELAY, true) + .handler(new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel ch) throws Exception { + //ch.pipeline().addLast(new LoggingHandler());//in out + //将目标服务器信息转发给客户端 + ch.pipeline().addLast(new Dest2ClientHandler(clientChannelContext)); + } + }); + logger.trace("连接目标服务器"); + ChannelFuture future = bootstrap.connect(msg.dstAddr(), msg.dstPort()); + future.addListener(new ChannelFutureListener() { + + public void operationComplete(final ChannelFuture future) throws Exception { + if(future.isSuccess()) { + logger.trace("成功连接目标服务器"); + clientChannelContext.pipeline().addLast(new Client2DestHandler(future)); + Socks5CommandResponse commandResponse = new DefaultSocks5CommandResponse(Socks5CommandStatus.SUCCESS, Socks5AddressType.IPv4); + clientChannelContext.writeAndFlush(commandResponse); + } else { + Socks5CommandResponse commandResponse = new DefaultSocks5CommandResponse(Socks5CommandStatus.FAILURE, Socks5AddressType.IPv4); + clientChannelContext.writeAndFlush(commandResponse); + } + } + + }); + } else { + clientChannelContext.fireChannelRead(msg); + } + } + + /** + * 将目标服务器信息转发给客户端 + * + * @author huchengyi + * + */ + private static class Dest2ClientHandler extends ChannelInboundHandlerAdapter { + + private ChannelHandlerContext clientChannelContext; + + public Dest2ClientHandler(ChannelHandlerContext clientChannelContext) { + this.clientChannelContext = clientChannelContext; + } + + @Override + public void channelRead(ChannelHandlerContext ctx2, Object destMsg) throws Exception { + logger.trace("将目标服务器信息转发给客户端"); + clientChannelContext.writeAndFlush(destMsg); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx2) throws Exception { + logger.trace("目标服务器断开连接"); + clientChannelContext.channel().close(); + } + } + + /** + * 将客户端的消息转发给目标服务器端 + * + * @author huchengyi + * + */ + private static class Client2DestHandler extends ChannelInboundHandlerAdapter { + + private ChannelFuture destChannelFuture; + + public Client2DestHandler(ChannelFuture destChannelFuture) { + this.destChannelFuture = destChannelFuture; + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + logger.trace("将客户端的消息转发给目标服务器端"); + destChannelFuture.channel().writeAndFlush(msg); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + logger.trace("客户端断开连接"); + destChannelFuture.channel().close(); + } + } +} diff --git a/socks5-netty/src/main/java/com/geccocrawler/socks5/handler/ss5/Socks5InitialRequestHandler.java b/socks5-netty/src/main/java/com/geccocrawler/socks5/handler/ss5/Socks5InitialRequestHandler.java new file mode 100644 index 0000000..9388167 --- /dev/null +++ b/socks5-netty/src/main/java/com/geccocrawler/socks5/handler/ss5/Socks5InitialRequestHandler.java @@ -0,0 +1,45 @@ +package com.geccocrawler.socks5.handler.ss5; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.geccocrawler.socks5.ProxyServer; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.socksx.SocksVersion; +import io.netty.handler.codec.socksx.v5.DefaultSocks5InitialRequest; +import io.netty.handler.codec.socksx.v5.DefaultSocks5InitialResponse; +import io.netty.handler.codec.socksx.v5.Socks5AuthMethod; +import io.netty.handler.codec.socksx.v5.Socks5InitialResponse; + +public class Socks5InitialRequestHandler extends SimpleChannelInboundHandler { + + private static final Logger logger = LoggerFactory.getLogger(Socks5InitialRequestHandler.class); + + private ProxyServer proxyServer; + + public Socks5InitialRequestHandler(ProxyServer proxyServer) { + this.proxyServer = proxyServer; + } + + @Override + protected void channelRead0(ChannelHandlerContext ctx, DefaultSocks5InitialRequest msg) throws Exception { + logger.debug("初始化ss5连接 : " + msg); + if(msg.decoderResult().isFailure()) { + logger.debug("不是ss5协议"); + ctx.fireChannelRead(msg); + } else { + if(msg.version().equals(SocksVersion.SOCKS5)) { + if(proxyServer.isAuth()) { + Socks5InitialResponse initialResponse = new DefaultSocks5InitialResponse(Socks5AuthMethod.PASSWORD); + ctx.writeAndFlush(initialResponse); + } else { + Socks5InitialResponse initialResponse = new DefaultSocks5InitialResponse(Socks5AuthMethod.NO_AUTH); + ctx.writeAndFlush(initialResponse); + } + } + } + } + +} diff --git a/socks5-netty/src/main/java/com/geccocrawler/socks5/handler/ss5/Socks5PasswordAuthRequestHandler.java b/socks5-netty/src/main/java/com/geccocrawler/socks5/handler/ss5/Socks5PasswordAuthRequestHandler.java new file mode 100644 index 0000000..3b2dfe3 --- /dev/null +++ b/socks5-netty/src/main/java/com/geccocrawler/socks5/handler/ss5/Socks5PasswordAuthRequestHandler.java @@ -0,0 +1,42 @@ +package com.geccocrawler.socks5.handler.ss5; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.geccocrawler.socks5.auth.PasswordAuth; +import com.geccocrawler.socks5.handler.ProxyChannelTrafficShapingHandler; + +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.socksx.v5.DefaultSocks5PasswordAuthRequest; +import io.netty.handler.codec.socksx.v5.DefaultSocks5PasswordAuthResponse; +import io.netty.handler.codec.socksx.v5.Socks5PasswordAuthResponse; +import io.netty.handler.codec.socksx.v5.Socks5PasswordAuthStatus; + +public class Socks5PasswordAuthRequestHandler extends SimpleChannelInboundHandler { + + private static final Logger logger = LoggerFactory.getLogger(Socks5PasswordAuthRequestHandler.class); + + private PasswordAuth passwordAuth; + + public Socks5PasswordAuthRequestHandler(PasswordAuth passwordAuth) { + this.passwordAuth = passwordAuth; + } + + @Override + protected void channelRead0(ChannelHandlerContext ctx, DefaultSocks5PasswordAuthRequest msg) throws Exception { + logger.debug("用户名密码 : " + msg.username() + "," + msg.password()); + if(passwordAuth.auth(msg.username(), msg.password())) { + ProxyChannelTrafficShapingHandler.username(ctx, msg.username()); + Socks5PasswordAuthResponse passwordAuthResponse = new DefaultSocks5PasswordAuthResponse(Socks5PasswordAuthStatus.SUCCESS); + ctx.writeAndFlush(passwordAuthResponse); + } else { + ProxyChannelTrafficShapingHandler.username(ctx, "unauthorized"); + Socks5PasswordAuthResponse passwordAuthResponse = new DefaultSocks5PasswordAuthResponse(Socks5PasswordAuthStatus.FAILURE); + //发送鉴权失败消息,完成后关闭channel + ctx.writeAndFlush(passwordAuthResponse).addListener(ChannelFutureListener.CLOSE); + } + } + +} diff --git a/socks5-netty/src/main/java/com/geccocrawler/socks5/log/ProxyFlowLog.java b/socks5-netty/src/main/java/com/geccocrawler/socks5/log/ProxyFlowLog.java new file mode 100644 index 0000000..5975706 --- /dev/null +++ b/socks5-netty/src/main/java/com/geccocrawler/socks5/log/ProxyFlowLog.java @@ -0,0 +1,9 @@ +package com.geccocrawler.socks5.log; + +import io.netty.channel.ChannelHandlerContext; + +public interface ProxyFlowLog { + + public void log(ChannelHandlerContext ctx); + +} diff --git a/socks5-netty/src/main/java/com/geccocrawler/socks5/log/ProxyFlowLog4j.java b/socks5-netty/src/main/java/com/geccocrawler/socks5/log/ProxyFlowLog4j.java new file mode 100644 index 0000000..b44d8ae --- /dev/null +++ b/socks5-netty/src/main/java/com/geccocrawler/socks5/log/ProxyFlowLog4j.java @@ -0,0 +1,68 @@ +package com.geccocrawler.socks5.log; + +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.util.Enumeration; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.geccocrawler.socks5.handler.ProxyChannelTrafficShapingHandler; + +import io.netty.channel.ChannelHandlerContext; + +public class ProxyFlowLog4j implements ProxyFlowLog { + + private static final Logger logger = LoggerFactory.getLogger(ProxyFlowLog4j.class); + + public void log(ChannelHandlerContext ctx) { + ProxyChannelTrafficShapingHandler trafficShapingHandler = ProxyChannelTrafficShapingHandler.get(ctx); + InetSocketAddress localAddress = (InetSocketAddress)ctx.channel().localAddress(); + InetSocketAddress remoteAddress = (InetSocketAddress)ctx.channel().remoteAddress(); + + long readByte = trafficShapingHandler.trafficCounter().cumulativeReadBytes(); + long writeByte = trafficShapingHandler.trafficCounter().cumulativeWrittenBytes(); + + logger.info("{},{},{},{}:{},{}:{},{},{},{}", + trafficShapingHandler.getUsername(), + trafficShapingHandler.getBeginTime(), + trafficShapingHandler.getEndTime(), + getLocalAddress(), + localAddress.getPort(), + remoteAddress.getAddress().getHostAddress(), + remoteAddress.getPort(), + readByte, + writeByte, + (readByte + writeByte)); + } + + /** + * 获取本机的IP + * + * @return Ip地址 + */ + private static String getLocalAddress() { + try { + for (Enumeration interfaces = NetworkInterface.getNetworkInterfaces(); interfaces.hasMoreElements();) { + NetworkInterface networkInterface = interfaces.nextElement(); + if (networkInterface.isLoopback() || networkInterface.isVirtual() || !networkInterface.isUp()) { + continue; + } + Enumeration addresses = networkInterface.getInetAddresses(); + if (addresses.hasMoreElements()) { + InetAddress address = addresses.nextElement(); + if(address instanceof Inet4Address) { + return address.getHostAddress(); + } + } + } + } catch (SocketException e) { + logger.debug("Error when getting host ip address: <{}>.", e.getMessage()); + } + return "127.0.0.1"; + } + +} diff --git a/socks5-netty/src/main/resources/config.properties b/socks5-netty/src/main/resources/config.properties new file mode 100644 index 0000000..f516561 --- /dev/null +++ b/socks5-netty/src/main/resources/config.properties @@ -0,0 +1,2 @@ +port=1081 +auth=false \ No newline at end of file diff --git a/socks5-netty/src/main/resources/log4j.properties b/socks5-netty/src/main/resources/log4j.properties new file mode 100644 index 0000000..ed001bf --- /dev/null +++ b/socks5-netty/src/main/resources/log4j.properties @@ -0,0 +1,10 @@ +log4j.rootLogger=error,stdout + +#stdout console appender +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=[%d{yyyy.MM.dd HH:mm:ss}] %p %C:%M(%L) - %m%n + +#log4j.logger.io.netty=debug +#log4j.logger.io.netty.handler=debug +log4j.logger.com.geccocrawler.socks5=info \ No newline at end of file diff --git a/socks5-netty/src/main/resources/password.properties b/socks5-netty/src/main/resources/password.properties new file mode 100644 index 0000000..51e670a --- /dev/null +++ b/socks5-netty/src/main/resources/password.properties @@ -0,0 +1 @@ +test=test \ No newline at end of file diff --git a/socks5-netty/src/test/java/com/geccocrawler/socks5/test/HttpProxyClient.java b/socks5-netty/src/test/java/com/geccocrawler/socks5/test/HttpProxyClient.java new file mode 100644 index 0000000..88d59f7 --- /dev/null +++ b/socks5-netty/src/test/java/com/geccocrawler/socks5/test/HttpProxyClient.java @@ -0,0 +1,41 @@ +package com.geccocrawler.socks5.test; + +import java.net.InetSocketAddress; +import java.net.PasswordAuthentication; +import java.net.Proxy; + +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; + +public class HttpProxyClient { + + public static void main(String[] args) throws Exception { + final String user = "test"; + final String password = "test"; + + Proxy proxyTest = new Proxy(Proxy.Type.SOCKS, new InetSocketAddress("127.0.0.1", 11080)); + + java.net.Authenticator.setDefault(new java.net.Authenticator() + { + private PasswordAuthentication authentication = new PasswordAuthentication(user, password.toCharArray()); + + @Override + protected PasswordAuthentication getPasswordAuthentication() + { + return authentication; + } + }); + + + OkHttpClient client = new OkHttpClient.Builder().proxy(proxyTest).build(); + Request request = new Request.Builder().url("https://www.baidu.com").build(); + Response response = client.newCall(request).execute(); + System.out.println(response.code()); + System.out.println(response.body().string()); + + client.dispatcher().executorService().shutdown(); + client.connectionPool().evictAll(); + } + +}