├── .github ├── dependabot.yml ├── mergify.yml └── workflows │ ├── build-test.yml │ └── release-drafter.yml ├── .gitignore ├── LICENSE ├── README.md ├── netty-reactive-streams-http ├── pom.xml └── src │ ├── main │ └── java │ │ └── org │ │ └── playframework │ │ └── netty │ │ └── http │ │ ├── DefaultStreamedHttpRequest.java │ │ ├── DefaultStreamedHttpResponse.java │ │ ├── DefaultWebSocketHttpResponse.java │ │ ├── DelegateHttpMessage.java │ │ ├── DelegateHttpRequest.java │ │ ├── DelegateHttpResponse.java │ │ ├── DelegateStreamedHttpRequest.java │ │ ├── DelegateStreamedHttpResponse.java │ │ ├── EmptyHttpRequest.java │ │ ├── EmptyHttpResponse.java │ │ ├── HttpStreamsClientHandler.java │ │ ├── HttpStreamsHandler.java │ │ ├── HttpStreamsServerHandler.java │ │ ├── StreamedHttpMessage.java │ │ ├── StreamedHttpRequest.java │ │ ├── StreamedHttpResponse.java │ │ └── WebSocketHttpResponse.java │ └── test │ └── java │ └── org │ └── playframework │ └── netty │ └── http │ ├── DelegateProcessor.java │ ├── FullStackHttpIdentityProcessorVerificationTest.java │ ├── HttpHelper.java │ ├── HttpStreamsTest.java │ ├── PekkoStreamsUtil.java │ ├── ProcessorHttpClient.java │ ├── ProcessorHttpServer.java │ └── WebSocketsTest.java ├── netty-reactive-streams ├── pom.xml └── src │ ├── main │ └── java │ │ └── org │ │ └── playframework │ │ └── netty │ │ ├── CancelledSubscriber.java │ │ ├── HandlerPublisher.java │ │ └── HandlerSubscriber.java │ └── test │ └── java │ └── org │ └── playframework │ └── netty │ ├── BatchedProducer.java │ ├── ChannelPublisherTest.java │ ├── ClosedLoopChannel.java │ ├── HandlerPublisherVerificationTest.java │ ├── HandlerSubscriberBlackboxVerificationTest.java │ ├── HandlerSubscriberWhiteboxVerificationTest.java │ ├── ProbeHandler.java │ ├── ScheduledBatchedProducer.java │ └── probe │ ├── Probe.java │ ├── PublisherProbe.java │ └── SubscriberProbe.java └── pom.xml /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "maven" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: "maven" 8 | directory: "/netty-reactive-streams" 9 | schedule: 10 | interval: "weekly" 11 | - package-ecosystem: "maven" 12 | directory: "/netty-reactive-streams-http" 13 | schedule: 14 | interval: "weekly" 15 | - package-ecosystem: "maven" 16 | directory: "/" 17 | schedule: 18 | interval: "weekly" 19 | target-branch: "2.0.x" 20 | commit-message: 21 | prefix: "[2.0.x] " 22 | - package-ecosystem: "maven" 23 | directory: "/netty-reactive-streams" 24 | schedule: 25 | interval: "weekly" 26 | target-branch: "2.0.x" 27 | commit-message: 28 | prefix: "[2.0.x] " 29 | - package-ecosystem: "maven" 30 | directory: "/netty-reactive-streams-http" 31 | schedule: 32 | interval: "weekly" 33 | target-branch: "2.0.x" 34 | commit-message: 35 | prefix: "[2.0.x] " 36 | - package-ecosystem: "github-actions" 37 | directory: "/" 38 | schedule: 39 | interval: "weekly" 40 | - package-ecosystem: "github-actions" 41 | directory: "/" 42 | schedule: 43 | interval: "weekly" 44 | target-branch: "2.0.x" 45 | commit-message: 46 | prefix: "[2.0.x] " 47 | -------------------------------------------------------------------------------- /.github/mergify.yml: -------------------------------------------------------------------------------- 1 | extends: .github 2 | -------------------------------------------------------------------------------- /.github/workflows/build-test.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | push: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build: 13 | strategy: 14 | matrix: 15 | java: [ '8', '11', '17', '21' ] 16 | os: [ 'ubuntu-latest' ] 17 | runs-on: ${{ matrix.os }} 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Set up JDK 21 | uses: actions/setup-java@v4 22 | with: 23 | java-version: ${{ matrix.java }} 24 | distribution: 'temurin' 25 | cache: 'maven' 26 | - name: Build 27 | run: mvn --no-transfer-progress -B clean package 28 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | update_release_draft: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: release-drafter/release-drafter@v6 13 | with: 14 | name: "Netty Reactive Streams $RESOLVED_VERSION" 15 | config-name: release-drafts/increasing-minor-version.yml # located in .github/ in the default branch within this or the .github repo 16 | commitish: ${{ github.ref_name }} 17 | env: 18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | 3 | *.iml 4 | .idea/ 5 | .project 6 | .settings/ 7 | netty-reactive-streams-http/.classpath 8 | netty-reactive-streams-http/.project 9 | netty-reactive-streams-http/.settings/ 10 | netty-reactive-streams/.classpath 11 | netty-reactive-streams/.project 12 | netty-reactive-streams/.settings/ 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Netty Reactive Streams 2 | 3 | [![Twitter Follow](https://img.shields.io/twitter/follow/playframework?label=follow&style=flat&logo=twitter&color=brightgreen)](https://twitter.com/playframework) 4 | [![Discord](https://img.shields.io/discord/931647755942776882?logo=discord&logoColor=white)](https://discord.gg/g5s2vtZ4Fa) 5 | [![GitHub Discussions](https://img.shields.io/github/discussions/playframework/playframework?&logo=github&color=brightgreen)](https://github.com/playframework/playframework/discussions) 6 | [![StackOverflow](https://img.shields.io/static/v1?label=stackoverflow&logo=stackoverflow&logoColor=fe7a16&color=brightgreen&message=playframework)](https://stackoverflow.com/tags/playframework) 7 | [![YouTube](https://img.shields.io/youtube/channel/views/UCRp6QDm5SDjbIuisUpxV9cg?label=watch&logo=youtube&style=flat&color=brightgreen&logoColor=ff0000)](https://www.youtube.com/channel/UCRp6QDm5SDjbIuisUpxV9cg) 8 | [![Twitch Status](https://img.shields.io/twitch/status/playframework?logo=twitch&logoColor=white&color=brightgreen&label=live%20stream)](https://www.twitch.tv/playframework) 9 | [![OpenCollective](https://img.shields.io/opencollective/all/playframework?label=financial%20contributors&logo=open-collective)](https://opencollective.com/playframework) 10 | 11 | [![Build Status](https://github.com/playframework/netty-reactive-streams/actions/workflows/build-test.yml/badge.svg)](https://github.com/playframework/netty-reactive-streams/actions/workflows/build-test.yml) 12 | [![Maven](https://img.shields.io/maven-central/v/org.playframework.netty/netty-reactive-streams.svg?logo=apache-maven)](https://mvnrepository.com/artifact/org.playframework.netty/netty-reactive-streams) 13 | [![Repository size](https://img.shields.io/github/repo-size/playframework/netty-reactive-streams.svg?logo=git)](https://github.com/playframework/netty-reactive-streams) 14 | 15 | This provides a reactive streams implementation for Netty. Essentially it comes in the form of two channel handlers, one that publishes inbound messages received on a channel to a `Publisher`, and another that writes messages received by a `Subscriber` outbound. 16 | 17 | Features include: 18 | 19 | * Full backpressure support, as long as the `AUTO_READ` channel option is disabled. 20 | * Publishers/subscribers can be dynamically added and removed from the pipeline. 21 | * Multiple publishers/subscribers can be inserted into the pipeline. 22 | * Customisable cancel/complete/failure handling. 23 | 24 | ## Releasing a new version 25 | 26 | This project is released and published through Sonatype. You must have Sonatype credentials installed, preferably in `$HOME/.m2/settings.xml` and a GPG key that is available to the standard GPG keyservers. If your key is not on the server, you should add it to the keyserver as follows: 27 | 28 | ``` 29 | gpg --send-key --keyserver pgp.mit.edu 30 | ``` 31 | 32 | After that, you can perform a release through the following commands: 33 | 34 | ``` 35 | mvn release:prepare -Darguments=-Dgpg.passphrase=thephrase 36 | mvn release:perform -Darguments=-Dgpg.passphrase=thephrase 37 | ``` 38 | 39 | This will push a release to the staging repository and automatically close and publish the staging repository to production. 40 | 41 | ## Using the publisher/subscriber 42 | 43 | Here's an example of creating a client channel that publishes/subscribes to `ByteBuf`'s: 44 | 45 | ```java 46 | EventLoopGroup workerGroup = new NioEventLoopGroup(); 47 | 48 | Bootstrap bootstrap = new Bootstrap() 49 | .group(workerGroup) 50 | .channel(NioSocketChannel.class) 51 | .option(ChannelOption.SO_KEEPALIVE, true); 52 | .handler(new ChannelInitializer() { 53 | @Override 54 | public void initChannel(SocketChannel ch) throws Exception { 55 | HandlerPublisher publisher = new HandlerPublisher<>(ch.executor(), 56 | ByteBuf.class); 57 | HandlerSubscriber subscriber = new HandlerSubscriber<>(ch.executor()); 58 | 59 | // Here you can subscriber to the publisher, and pass the subscriber to a publisher. 60 | 61 | ch.pipeline().addLast(publisher, subscriber); 62 | } 63 | }); 64 | ``` 65 | 66 | Notice that the channels executor is explicitly passed to the publisher and subscriber constructors. `HandlerPublisher` and `HandlerSubscriber` use Netty's happens before guarantees made by its `EventExecutor` interface in order to handle asynchronous events from reactive streams, hence they need to have an executor to do this with. This executor must also be the same executor that the handler is registered to use with the channel, so that all channel events are fired from the same context. The publisher/subscriber will throw an exception if the handler is registered with a different executor. 67 | 68 | ## A word of caution with ByteBuf's 69 | 70 | Reactive streams allows implementations to drop messages when a stream is cancelled or failed - Netty reactive streams does this itself. This introduces a problem when those messages hold resources and must be cleaned up, for example, if they are `ByteBuf's` that need to be released. While Netty reactive streams will call `ReferenceCountUtil.release()` on every message that it drops, other implementations that it's interacting with likely won't do this. 71 | 72 | For this reason, you should make sure you do one of the following: 73 | 74 | * Insert a handler in the pipeline that converts incoming `ByteBuf`'s to some non reference counted structure, for example `byte[]`, or some high level immutable message structure, and have the `HandlerPublisher` publish those. Similarly, the `HandlerSubscriber` should subscribe to some non reference counted structure, and a handler should be placed in the pipeline to convert these structures to `ByteBuf`'s. 75 | * Write a wrapping `Publisher` and `Subscriber` that synchronously converts `ByteBuf`'s to/from a non reference counted structure. 76 | 77 | ## Netty Reactive Streams HTTP 78 | 79 | In addition to raw reactive streams support, the `netty-reactive-streams-http` library provides some HTTP model abstractions and a handle for implementing HTTP servers/clients that use reactive streams to stream HTTP message bodies. The `HttpStreamsServerHandler` and `HttpStreamsClientHandler` ensure that every `HttpRequest` and `HttpResponse` in the pipeline is either a `FullHttpRequest` or `FullHttpResponse`, for when the body can be worked with in memory, or `StreamedHttpRequest` or `StreamedHttpResponse`, which are messages that double as `Publisher` for handling the request/response body. 80 | 81 | -------------------------------------------------------------------------------- /netty-reactive-streams-http/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | org.playframework.netty 7 | netty-reactive-streams-parent 8 | 3.0.5-SNAPSHOT 9 | 10 | 11 | netty-reactive-streams-http 12 | 13 | Netty Reactive Streams HTTP support 14 | jar 15 | 16 | 17 | 18 | ${project.groupId} 19 | netty-reactive-streams 20 | ${project.version} 21 | 22 | 23 | io.netty 24 | netty-codec-http 25 | 26 | 27 | org.reactivestreams 28 | reactive-streams-tck 29 | 30 | 31 | org.testng 32 | testng 33 | 34 | 35 | org.apache.pekko 36 | pekko-stream_2.12 37 | 38 | 39 | 40 | 41 | 42 | 43 | maven-resources-plugin 44 | 3.3.1 45 | 46 | 47 | copy-resources 48 | validate 49 | 50 | copy-resources 51 | 52 | 53 | ${project.build.resources[0].directory}/META-INF 54 | 55 | 56 | ../ 57 | true 58 | 59 | README.md 60 | LICENSE 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | org.apache.maven.plugins 70 | maven-jar-plugin 71 | 72 | 73 | default-jar 74 | package 75 | 76 | jar 77 | 78 | 79 | 80 | 81 | org.playframework.netty.http 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /netty-reactive-streams-http/src/main/java/org/playframework/netty/http/DefaultStreamedHttpRequest.java: -------------------------------------------------------------------------------- 1 | package org.playframework.netty.http; 2 | 3 | import io.netty.handler.codec.http.DefaultHttpRequest; 4 | import io.netty.handler.codec.http.HttpContent; 5 | import io.netty.handler.codec.http.HttpMethod; 6 | import io.netty.handler.codec.http.HttpVersion; 7 | import org.reactivestreams.Publisher; 8 | import org.reactivestreams.Subscriber; 9 | 10 | /** 11 | * A default streamed HTTP request. 12 | */ 13 | public class DefaultStreamedHttpRequest extends DefaultHttpRequest implements StreamedHttpRequest{ 14 | 15 | private final Publisher stream; 16 | 17 | public DefaultStreamedHttpRequest(HttpVersion httpVersion, HttpMethod method, String uri, Publisher stream) { 18 | super(httpVersion, method, uri); 19 | this.stream = stream; 20 | } 21 | 22 | public DefaultStreamedHttpRequest(HttpVersion httpVersion, HttpMethod method, String uri, boolean validateHeaders, Publisher stream) { 23 | super(httpVersion, method, uri, validateHeaders); 24 | this.stream = stream; 25 | } 26 | 27 | @Override 28 | public void subscribe(Subscriber subscriber) { 29 | stream.subscribe(subscriber); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /netty-reactive-streams-http/src/main/java/org/playframework/netty/http/DefaultStreamedHttpResponse.java: -------------------------------------------------------------------------------- 1 | package org.playframework.netty.http; 2 | 3 | import io.netty.handler.codec.http.DefaultHttpResponse; 4 | import io.netty.handler.codec.http.HttpContent; 5 | import io.netty.handler.codec.http.HttpResponseStatus; 6 | import io.netty.handler.codec.http.HttpVersion; 7 | import org.reactivestreams.Publisher; 8 | import org.reactivestreams.Subscriber; 9 | 10 | /** 11 | * A default streamed HTTP response. 12 | */ 13 | public class DefaultStreamedHttpResponse extends DefaultHttpResponse implements StreamedHttpResponse { 14 | 15 | private final Publisher stream; 16 | 17 | public DefaultStreamedHttpResponse(HttpVersion version, HttpResponseStatus status, Publisher stream) { 18 | super(version, status); 19 | this.stream = stream; 20 | } 21 | 22 | public DefaultStreamedHttpResponse(HttpVersion version, HttpResponseStatus status, boolean validateHeaders, Publisher stream) { 23 | super(version, status, validateHeaders); 24 | this.stream = stream; 25 | } 26 | 27 | @Override 28 | public void subscribe(Subscriber subscriber) { 29 | stream.subscribe(subscriber); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /netty-reactive-streams-http/src/main/java/org/playframework/netty/http/DefaultWebSocketHttpResponse.java: -------------------------------------------------------------------------------- 1 | package org.playframework.netty.http; 2 | 3 | import io.netty.handler.codec.http.DefaultHttpResponse; 4 | import io.netty.handler.codec.http.HttpResponseStatus; 5 | import io.netty.handler.codec.http.HttpVersion; 6 | import io.netty.handler.codec.http.websocketx.WebSocketFrame; 7 | import io.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory; 8 | import org.reactivestreams.Processor; 9 | import org.reactivestreams.Subscriber; 10 | import org.reactivestreams.Subscription; 11 | 12 | /** 13 | * A default WebSocket HTTP response. 14 | */ 15 | public class DefaultWebSocketHttpResponse extends DefaultHttpResponse implements WebSocketHttpResponse { 16 | 17 | private final Processor processor; 18 | private final WebSocketServerHandshakerFactory handshakerFactory; 19 | 20 | public DefaultWebSocketHttpResponse(HttpVersion version, HttpResponseStatus status, 21 | Processor processor, 22 | WebSocketServerHandshakerFactory handshakerFactory) { 23 | super(version, status); 24 | this.processor = processor; 25 | this.handshakerFactory = handshakerFactory; 26 | } 27 | 28 | public DefaultWebSocketHttpResponse(HttpVersion version, HttpResponseStatus status, boolean validateHeaders, 29 | Processor processor, 30 | WebSocketServerHandshakerFactory handshakerFactory) { 31 | super(version, status, validateHeaders); 32 | this.processor = processor; 33 | this.handshakerFactory = handshakerFactory; 34 | } 35 | 36 | @Override 37 | public WebSocketServerHandshakerFactory handshakerFactory() { 38 | return handshakerFactory; 39 | } 40 | 41 | @Override 42 | public void subscribe(Subscriber subscriber) { 43 | processor.subscribe(subscriber); 44 | } 45 | 46 | @Override 47 | public void onSubscribe(Subscription subscription) { 48 | processor.onSubscribe(subscription); 49 | } 50 | 51 | @Override 52 | public void onNext(WebSocketFrame webSocketFrame) { 53 | processor.onNext(webSocketFrame); 54 | } 55 | 56 | @Override 57 | public void onError(Throwable error) { 58 | processor.onError(error); 59 | } 60 | 61 | @Override 62 | public void onComplete() { 63 | processor.onComplete(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /netty-reactive-streams-http/src/main/java/org/playframework/netty/http/DelegateHttpMessage.java: -------------------------------------------------------------------------------- 1 | package org.playframework.netty.http; 2 | 3 | import io.netty.handler.codec.DecoderResult; 4 | import io.netty.handler.codec.http.*; 5 | 6 | class DelegateHttpMessage implements HttpMessage { 7 | protected final HttpMessage message; 8 | 9 | public DelegateHttpMessage(HttpMessage message) { 10 | this.message = message; 11 | } 12 | 13 | @Override 14 | @Deprecated 15 | public HttpVersion getProtocolVersion() { 16 | return message.protocolVersion(); 17 | } 18 | 19 | @Override 20 | public HttpVersion protocolVersion() { 21 | return message.protocolVersion(); 22 | } 23 | 24 | @Override 25 | public HttpMessage setProtocolVersion(HttpVersion version) { 26 | message.setProtocolVersion(version); 27 | return this; 28 | } 29 | 30 | @Override 31 | public HttpHeaders headers() { 32 | return message.headers(); 33 | } 34 | 35 | @Override 36 | @Deprecated 37 | public DecoderResult getDecoderResult() { 38 | return message.decoderResult(); 39 | } 40 | 41 | @Override 42 | public DecoderResult decoderResult() { 43 | return message.decoderResult(); 44 | } 45 | 46 | @Override 47 | public void setDecoderResult(DecoderResult result) { 48 | message.setDecoderResult(result); 49 | } 50 | 51 | @Override 52 | public String toString() { 53 | return this.getClass().getName() + "(" + message.toString() + ")"; 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /netty-reactive-streams-http/src/main/java/org/playframework/netty/http/DelegateHttpRequest.java: -------------------------------------------------------------------------------- 1 | package org.playframework.netty.http; 2 | 3 | import io.netty.handler.codec.http.HttpMethod; 4 | import io.netty.handler.codec.http.HttpRequest; 5 | import io.netty.handler.codec.http.HttpVersion; 6 | 7 | class DelegateHttpRequest extends DelegateHttpMessage implements HttpRequest { 8 | 9 | protected final HttpRequest request; 10 | 11 | public DelegateHttpRequest(HttpRequest request) { 12 | super(request); 13 | this.request = request; 14 | } 15 | 16 | @Override 17 | public HttpRequest setMethod(HttpMethod method) { 18 | request.setMethod(method); 19 | return this; 20 | } 21 | 22 | @Override 23 | public HttpRequest setUri(String uri) { 24 | request.setUri(uri); 25 | return this; 26 | } 27 | 28 | @Override 29 | @Deprecated 30 | public HttpMethod getMethod() { 31 | return request.method(); 32 | } 33 | 34 | @Override 35 | public HttpMethod method() { 36 | return request.method(); 37 | } 38 | 39 | @Override 40 | @Deprecated 41 | public String getUri() { 42 | return request.uri(); 43 | } 44 | 45 | @Override 46 | public String uri() { 47 | return request.uri(); 48 | } 49 | 50 | @Override 51 | public HttpRequest setProtocolVersion(HttpVersion version) { 52 | super.setProtocolVersion(version); 53 | return this; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /netty-reactive-streams-http/src/main/java/org/playframework/netty/http/DelegateHttpResponse.java: -------------------------------------------------------------------------------- 1 | package org.playframework.netty.http; 2 | 3 | import io.netty.handler.codec.http.HttpResponse; 4 | import io.netty.handler.codec.http.HttpResponseStatus; 5 | import io.netty.handler.codec.http.HttpVersion; 6 | 7 | class DelegateHttpResponse extends DelegateHttpMessage implements HttpResponse { 8 | 9 | protected final HttpResponse response; 10 | 11 | public DelegateHttpResponse(HttpResponse response) { 12 | super(response); 13 | this.response = response; 14 | } 15 | 16 | @Override 17 | public HttpResponse setStatus(HttpResponseStatus status) { 18 | response.setStatus(status); 19 | return this; 20 | } 21 | 22 | @Override 23 | @Deprecated 24 | public HttpResponseStatus getStatus() { 25 | return response.status(); 26 | } 27 | 28 | @Override 29 | public HttpResponseStatus status() { 30 | return response.status(); 31 | } 32 | 33 | @Override 34 | public HttpResponse setProtocolVersion(HttpVersion version) { 35 | super.setProtocolVersion(version); 36 | return this; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /netty-reactive-streams-http/src/main/java/org/playframework/netty/http/DelegateStreamedHttpRequest.java: -------------------------------------------------------------------------------- 1 | package org.playframework.netty.http; 2 | 3 | import io.netty.handler.codec.http.HttpContent; 4 | import io.netty.handler.codec.http.HttpRequest; 5 | import org.reactivestreams.Publisher; 6 | import org.reactivestreams.Subscriber; 7 | 8 | final class DelegateStreamedHttpRequest extends DelegateHttpRequest implements StreamedHttpRequest { 9 | 10 | private final Publisher stream; 11 | 12 | public DelegateStreamedHttpRequest(HttpRequest request, Publisher stream) { 13 | super(request); 14 | this.stream = stream; 15 | } 16 | 17 | @Override 18 | public void subscribe(Subscriber subscriber) { 19 | stream.subscribe(subscriber); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /netty-reactive-streams-http/src/main/java/org/playframework/netty/http/DelegateStreamedHttpResponse.java: -------------------------------------------------------------------------------- 1 | package org.playframework.netty.http; 2 | 3 | import io.netty.handler.codec.http.HttpContent; 4 | import io.netty.handler.codec.http.HttpResponse; 5 | import org.reactivestreams.Publisher; 6 | import org.reactivestreams.Subscriber; 7 | 8 | final class DelegateStreamedHttpResponse extends DelegateHttpResponse implements StreamedHttpResponse { 9 | 10 | private final Publisher stream; 11 | 12 | public DelegateStreamedHttpResponse(HttpResponse response, Publisher stream) { 13 | super(response); 14 | this.stream = stream; 15 | } 16 | 17 | @Override 18 | public void subscribe(Subscriber subscriber) { 19 | stream.subscribe(subscriber); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /netty-reactive-streams-http/src/main/java/org/playframework/netty/http/EmptyHttpRequest.java: -------------------------------------------------------------------------------- 1 | package org.playframework.netty.http; 2 | 3 | import io.netty.buffer.ByteBuf; 4 | import io.netty.buffer.Unpooled; 5 | import io.netty.handler.codec.http.*; 6 | import io.netty.util.ReferenceCountUtil; 7 | import io.netty.util.ReferenceCounted; 8 | 9 | class EmptyHttpRequest extends DelegateHttpRequest implements FullHttpRequest { 10 | 11 | public EmptyHttpRequest(HttpRequest request) { 12 | super(request); 13 | } 14 | 15 | @Override 16 | public FullHttpRequest setUri(String uri) { 17 | super.setUri(uri); 18 | return this; 19 | } 20 | 21 | @Override 22 | public FullHttpRequest setMethod(HttpMethod method) { 23 | super.setMethod(method); 24 | return this; 25 | } 26 | 27 | @Override 28 | public FullHttpRequest setProtocolVersion(HttpVersion version) { 29 | super.setProtocolVersion(version); 30 | return this; 31 | } 32 | 33 | @Override 34 | public FullHttpRequest copy() { 35 | if (request instanceof FullHttpRequest) { 36 | return new EmptyHttpRequest(((FullHttpRequest) request).copy()); 37 | } else { 38 | DefaultHttpRequest copy = new DefaultHttpRequest(protocolVersion(), method(), uri()); 39 | copy.headers().set(headers()); 40 | return new EmptyHttpRequest(copy); 41 | } 42 | } 43 | 44 | @Override 45 | public FullHttpRequest retain(int increment) { 46 | ReferenceCountUtil.retain(message, increment); 47 | return this; 48 | } 49 | 50 | @Override 51 | public FullHttpRequest retain() { 52 | ReferenceCountUtil.retain(message); 53 | return this; 54 | } 55 | 56 | @Override 57 | public FullHttpRequest touch() { 58 | if (request instanceof FullHttpRequest) { 59 | return ((FullHttpRequest) request).touch(); 60 | } else { 61 | return this; 62 | } 63 | } 64 | 65 | @Override 66 | public FullHttpRequest touch(Object o) { 67 | if (request instanceof FullHttpRequest) { 68 | return ((FullHttpRequest) request).touch(o); 69 | } else { 70 | return this; 71 | } 72 | } 73 | 74 | @Override 75 | public HttpHeaders trailingHeaders() { 76 | return new DefaultHttpHeaders(); 77 | } 78 | 79 | @Override 80 | public FullHttpRequest duplicate() { 81 | if (request instanceof FullHttpRequest) { 82 | return ((FullHttpRequest) request).duplicate(); 83 | } else { 84 | return this; 85 | } 86 | } 87 | 88 | @Override 89 | public FullHttpRequest retainedDuplicate() { 90 | if (request instanceof FullHttpRequest) { 91 | return ((FullHttpRequest) request).retainedDuplicate(); 92 | } else { 93 | return this; 94 | } 95 | } 96 | 97 | @Override 98 | public FullHttpRequest replace(ByteBuf byteBuf) { 99 | if (message instanceof FullHttpRequest) { 100 | return ((FullHttpRequest) request).replace(byteBuf); 101 | } else { 102 | return this; 103 | } 104 | } 105 | 106 | @Override 107 | public ByteBuf content() { 108 | return Unpooled.EMPTY_BUFFER; 109 | } 110 | 111 | @Override 112 | public int refCnt() { 113 | if (message instanceof ReferenceCounted) { 114 | return ((ReferenceCounted) message).refCnt(); 115 | } else { 116 | return 1; 117 | } 118 | } 119 | 120 | @Override 121 | public boolean release() { 122 | return ReferenceCountUtil.release(message); 123 | } 124 | 125 | @Override 126 | public boolean release(int decrement) { 127 | return ReferenceCountUtil.release(message, decrement); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /netty-reactive-streams-http/src/main/java/org/playframework/netty/http/EmptyHttpResponse.java: -------------------------------------------------------------------------------- 1 | package org.playframework.netty.http; 2 | 3 | import io.netty.buffer.ByteBuf; 4 | import io.netty.buffer.Unpooled; 5 | import io.netty.handler.codec.http.*; 6 | import io.netty.util.ReferenceCountUtil; 7 | import io.netty.util.ReferenceCounted; 8 | 9 | class EmptyHttpResponse extends DelegateHttpResponse implements FullHttpResponse { 10 | 11 | public EmptyHttpResponse(HttpResponse response) { 12 | super(response); 13 | } 14 | 15 | @Override 16 | public FullHttpResponse setStatus(HttpResponseStatus status) { 17 | super.setStatus(status); 18 | return this; 19 | } 20 | 21 | @Override 22 | public FullHttpResponse setProtocolVersion(HttpVersion version) { 23 | super.setProtocolVersion(version); 24 | return this; 25 | } 26 | 27 | @Override 28 | public FullHttpResponse copy() { 29 | if (response instanceof FullHttpResponse) { 30 | return new EmptyHttpResponse(((FullHttpResponse) response).copy()); 31 | } else { 32 | DefaultHttpResponse copy = new DefaultHttpResponse(protocolVersion(), status()); 33 | copy.headers().set(headers()); 34 | return new EmptyHttpResponse(copy); 35 | } 36 | } 37 | 38 | @Override 39 | public FullHttpResponse retain(int increment) { 40 | ReferenceCountUtil.retain(message, increment); 41 | return this; 42 | } 43 | 44 | @Override 45 | public FullHttpResponse retain() { 46 | ReferenceCountUtil.retain(message); 47 | return this; 48 | } 49 | 50 | @Override 51 | public FullHttpResponse touch() { 52 | if (response instanceof FullHttpResponse) { 53 | return ((FullHttpResponse) response).touch(); 54 | } else { 55 | return this; 56 | } 57 | } 58 | 59 | @Override 60 | public FullHttpResponse touch(Object o) { 61 | if (response instanceof FullHttpResponse) { 62 | return ((FullHttpResponse) response).touch(o); 63 | } else { 64 | return this; 65 | } 66 | } 67 | 68 | @Override 69 | public HttpHeaders trailingHeaders() { 70 | return new DefaultHttpHeaders(); 71 | } 72 | 73 | @Override 74 | public FullHttpResponse duplicate() { 75 | if (response instanceof FullHttpResponse) { 76 | return ((FullHttpResponse) response).duplicate(); 77 | } else { 78 | return this; 79 | } 80 | } 81 | 82 | @Override 83 | public FullHttpResponse retainedDuplicate() { 84 | if (response instanceof FullHttpResponse) { 85 | return ((FullHttpResponse) response).retainedDuplicate(); 86 | } else { 87 | return this; 88 | } 89 | } 90 | 91 | @Override 92 | public FullHttpResponse replace(ByteBuf byteBuf) { 93 | if (response instanceof FullHttpResponse) { 94 | return ((FullHttpResponse) response).replace(byteBuf); 95 | } else { 96 | return this; 97 | } 98 | } 99 | 100 | @Override 101 | public ByteBuf content() { 102 | return Unpooled.EMPTY_BUFFER; 103 | } 104 | 105 | @Override 106 | public int refCnt() { 107 | if (message instanceof ReferenceCounted) { 108 | return ((ReferenceCounted) message).refCnt(); 109 | } else { 110 | return 1; 111 | } 112 | } 113 | 114 | @Override 115 | public boolean release() { 116 | return ReferenceCountUtil.release(message); 117 | } 118 | 119 | @Override 120 | public boolean release(int decrement) { 121 | return ReferenceCountUtil.release(message, decrement); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /netty-reactive-streams-http/src/main/java/org/playframework/netty/http/HttpStreamsClientHandler.java: -------------------------------------------------------------------------------- 1 | package org.playframework.netty.http; 2 | 3 | import org.playframework.netty.CancelledSubscriber; 4 | import io.netty.channel.ChannelHandlerContext; 5 | import io.netty.channel.ChannelPromise; 6 | import io.netty.handler.codec.http.*; 7 | import io.netty.util.ReferenceCountUtil; 8 | import org.reactivestreams.Publisher; 9 | import org.reactivestreams.Subscriber; 10 | import org.reactivestreams.Subscription; 11 | 12 | /** 13 | * Handler that converts written {@link StreamedHttpRequest} messages into {@link HttpRequest} messages 14 | * followed by {@link HttpContent} messages and reads {@link HttpResponse} messages followed by 15 | * {@link HttpContent} messages and produces {@link StreamedHttpResponse} messages. 16 | * 17 | * This allows request and response bodies to be handled using reactive streams. 18 | * 19 | * There are two types of messages that this handler accepts for writing, {@link StreamedHttpRequest} and 20 | * {@link FullHttpRequest}. Writing any other messages may potentially lead to HTTP message mangling. 21 | * 22 | * There are two types of messages that this handler will send down the chain, {@link StreamedHttpResponse}, 23 | * and {@link FullHttpResponse}. If {@link io.netty.channel.ChannelOption#AUTO_READ} is false for the channel, 24 | * then any {@link StreamedHttpResponse} messages must be subscribed to consume the body, otherwise 25 | * it's possible that no read will be done of the messages. 26 | * 27 | * As long as messages are returned in the order that they arrive, this handler implicitly supports HTTP 28 | * pipelining. 29 | */ 30 | public class HttpStreamsClientHandler extends HttpStreamsHandler { 31 | 32 | private int inFlight = 0; 33 | private int withServer = 0; 34 | private ChannelPromise closeOnZeroInFlight = null; 35 | private Subscriber awaiting100Continue; 36 | private StreamedHttpMessage awaiting100ContinueMessage; 37 | private boolean ignoreResponseBody = false; 38 | 39 | public HttpStreamsClientHandler() { 40 | super(HttpResponse.class, HttpRequest.class); 41 | } 42 | 43 | @Override 44 | protected boolean hasBody(HttpResponse response) { 45 | if (response.status().code() >= 100 && response.status().code() < 200) { 46 | return false; 47 | } 48 | 49 | if (response.status().equals(HttpResponseStatus.NO_CONTENT) || 50 | response.status().equals(HttpResponseStatus.NOT_MODIFIED)) { 51 | return false; 52 | } 53 | 54 | if (HttpUtil.isTransferEncodingChunked(response)) { 55 | return true; 56 | } 57 | 58 | 59 | if (HttpUtil.isContentLengthSet(response)) { 60 | return HttpUtil.getContentLength(response) > 0; 61 | } 62 | 63 | return true; 64 | } 65 | 66 | @Override 67 | public void close(ChannelHandlerContext ctx, ChannelPromise future) throws Exception { 68 | if (inFlight == 0) { 69 | ctx.close(future); 70 | } else { 71 | closeOnZeroInFlight = future; 72 | } 73 | } 74 | 75 | @Override 76 | protected void consumedInMessage(ChannelHandlerContext ctx) { 77 | inFlight--; 78 | withServer--; 79 | if (inFlight == 0 && closeOnZeroInFlight != null) { 80 | ctx.close(closeOnZeroInFlight); 81 | } 82 | } 83 | 84 | @Override 85 | protected void receivedOutMessage(ChannelHandlerContext ctx) { 86 | inFlight++; 87 | } 88 | 89 | @Override 90 | protected void sentOutMessage(ChannelHandlerContext ctx) { 91 | withServer++; 92 | } 93 | 94 | @Override 95 | protected HttpResponse createEmptyMessage(HttpResponse response) { 96 | return new EmptyHttpResponse(response); 97 | } 98 | 99 | @Override 100 | protected HttpResponse createStreamedMessage(HttpResponse response, Publisher stream) { 101 | return new DelegateStreamedHttpResponse(response, stream); 102 | } 103 | 104 | @Override 105 | protected void subscribeSubscriberToStream(StreamedHttpMessage msg, Subscriber subscriber) { 106 | if (HttpUtil.is100ContinueExpected(msg)) { 107 | awaiting100Continue = subscriber; 108 | awaiting100ContinueMessage = msg; 109 | } else { 110 | super.subscribeSubscriberToStream(msg, subscriber); 111 | } 112 | } 113 | 114 | @Override 115 | public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { 116 | 117 | if (msg instanceof HttpResponse && awaiting100Continue != null && withServer == 0) { 118 | HttpResponse response = (HttpResponse) msg; 119 | if (response.status().equals(HttpResponseStatus.CONTINUE)) { 120 | super.subscribeSubscriberToStream(awaiting100ContinueMessage, awaiting100Continue); 121 | awaiting100Continue = null; 122 | awaiting100ContinueMessage = null; 123 | if (msg instanceof FullHttpResponse) { 124 | ReferenceCountUtil.release(msg); 125 | } else { 126 | ignoreResponseBody = true; 127 | } 128 | } else { 129 | awaiting100ContinueMessage.subscribe(new CancelledSubscriber()); 130 | awaiting100ContinueMessage = null; 131 | awaiting100Continue.onSubscribe(new Subscription() { 132 | public void request(long n) { 133 | } 134 | public void cancel() { 135 | } 136 | }); 137 | awaiting100Continue.onComplete(); 138 | awaiting100Continue = null; 139 | super.channelRead(ctx, msg); 140 | } 141 | } else if (ignoreResponseBody && msg instanceof HttpContent) { 142 | 143 | ReferenceCountUtil.release(msg); 144 | if (msg instanceof LastHttpContent) { 145 | ignoreResponseBody = false; 146 | } 147 | } else { 148 | super.channelRead(ctx, msg); 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /netty-reactive-streams-http/src/main/java/org/playframework/netty/http/HttpStreamsHandler.java: -------------------------------------------------------------------------------- 1 | package org.playframework.netty.http; 2 | 3 | import org.playframework.netty.HandlerPublisher; 4 | import org.playframework.netty.HandlerSubscriber; 5 | import io.netty.channel.*; 6 | import io.netty.handler.codec.http.*; 7 | import io.netty.util.ReferenceCountUtil; 8 | import org.reactivestreams.Publisher; 9 | import org.reactivestreams.Subscriber; 10 | 11 | import java.util.LinkedList; 12 | import java.util.Queue; 13 | 14 | abstract class HttpStreamsHandler extends ChannelDuplexHandler { 15 | 16 | private final Queue outgoing = new LinkedList<>(); 17 | private final Class inClass; 18 | private final Class outClass; 19 | 20 | public HttpStreamsHandler(Class inClass, Class outClass) { 21 | this.inClass = inClass; 22 | this.outClass = outClass; 23 | } 24 | 25 | /** 26 | * The incoming message that is currently being streamed out to a subscriber. 27 | * 28 | * This is tracked so that if its subscriber cancels, we can go into a mode where we ignore the rest of the body. 29 | * Since subscribers may cancel as many times as they like, including well after they've received all their content, 30 | * we need to track what the current message that's being streamed out is so that we can ignore it if it's not 31 | * currently being streamed out. 32 | */ 33 | private In currentlyStreamedMessage; 34 | 35 | /** 36 | * Ignore the remaining reads for the incoming message. 37 | * 38 | * This is used in conjunction with currentlyStreamedMessage, as well as in situations where we have received the 39 | * full body, but still might be expecting a last http content message. 40 | */ 41 | private boolean ignoreBodyRead; 42 | 43 | /** 44 | * Whether a LastHttpContent message needs to be written once the incoming publisher completes. 45 | * 46 | * Since the publisher may itself publish a LastHttpContent message, we need to track this fact, because if it 47 | * doesn't, then we need to write one ourselves. 48 | */ 49 | private boolean sendLastHttpContent; 50 | 51 | /** 52 | * Whether the given incoming message has a body. 53 | */ 54 | protected abstract boolean hasBody(In in); 55 | 56 | /** 57 | * Create an empty incoming message. This must be of type FullHttpMessage, and is invoked when we've determined 58 | * that an incoming message can't have a body, so we send it on as a FullHttpMessage. 59 | */ 60 | protected abstract In createEmptyMessage(In in); 61 | 62 | /** 63 | * Create a streamed incoming message with the given stream. 64 | */ 65 | protected abstract In createStreamedMessage(In in, Publisher stream); 66 | 67 | /** 68 | * Invoked when an incoming message is first received. 69 | * 70 | * Overridden by sub classes for state tracking. 71 | */ 72 | protected void receivedInMessage(ChannelHandlerContext ctx) {} 73 | 74 | /** 75 | * Invoked when an incoming message is fully consumed. 76 | * 77 | * Overridden by sub classes for state tracking. 78 | */ 79 | protected void consumedInMessage(ChannelHandlerContext ctx) {} 80 | 81 | /** 82 | * Invoked when an outgoing message is first received. 83 | * 84 | * Overridden by sub classes for state tracking. 85 | */ 86 | protected void receivedOutMessage(ChannelHandlerContext ctx) {} 87 | 88 | /** 89 | * Invoked when an outgoing message is fully sent. 90 | * 91 | * Overridden by sub classes for state tracking. 92 | */ 93 | protected void sentOutMessage(ChannelHandlerContext ctx) {} 94 | 95 | /** 96 | * Subscribe the given subscriber to the given streamed message. 97 | * 98 | * Provided so that the client subclass can intercept this to hold off sending the body of an expect 100 continue 99 | * request. 100 | */ 101 | protected void subscribeSubscriberToStream(StreamedHttpMessage msg, Subscriber subscriber) { 102 | msg.subscribe(subscriber); 103 | } 104 | 105 | /** 106 | * Invoked every time a read of the incoming body is requested by the subscriber. 107 | * 108 | * Provided so that the server subclass can intercept this to send a 100 continue response. 109 | */ 110 | protected void bodyRequested(ChannelHandlerContext ctx) {} 111 | 112 | @Override 113 | public void channelRead(final ChannelHandlerContext ctx, Object msg) throws Exception { 114 | 115 | if (inClass.isInstance(msg)) { 116 | 117 | receivedInMessage(ctx); 118 | final In inMsg = inClass.cast(msg); 119 | 120 | if (inMsg instanceof FullHttpMessage) { 121 | 122 | // Forward as is 123 | ctx.fireChannelRead(inMsg); 124 | consumedInMessage(ctx); 125 | 126 | } else if (!hasBody(inMsg)) { 127 | 128 | // Wrap in empty message 129 | ctx.fireChannelRead(createEmptyMessage(inMsg)); 130 | consumedInMessage(ctx); 131 | 132 | // There will be a LastHttpContent message coming after this, ignore it 133 | ignoreBodyRead = true; 134 | 135 | } else { 136 | 137 | currentlyStreamedMessage = inMsg; 138 | // It has a body, stream it 139 | HandlerPublisher publisher = new HandlerPublisher(ctx.executor(), HttpContent.class) { 140 | @Override 141 | protected void cancelled() { 142 | if (ctx.executor().inEventLoop()) { 143 | handleCancelled(ctx, inMsg); 144 | } else { 145 | ctx.executor().execute(new Runnable() { 146 | @Override 147 | public void run() { 148 | handleCancelled(ctx, inMsg); 149 | } 150 | }); 151 | } 152 | } 153 | 154 | @Override 155 | protected void requestDemand() { 156 | bodyRequested(ctx); 157 | super.requestDemand(); 158 | } 159 | }; 160 | 161 | ctx.channel().pipeline().addAfter(ctx.name(), ctx.name() + "-body-publisher", publisher); 162 | ctx.fireChannelRead(createStreamedMessage(inMsg, publisher)); 163 | } 164 | } else if (msg instanceof HttpContent) { 165 | handleReadHttpContent(ctx, (HttpContent) msg); 166 | } 167 | } 168 | 169 | private void handleCancelled(ChannelHandlerContext ctx, In msg) { 170 | if (currentlyStreamedMessage == msg) { 171 | ignoreBodyRead = true; 172 | // Need to do a read in case the subscriber ignored a read completed. 173 | ctx.read(); 174 | } 175 | } 176 | 177 | private void handleReadHttpContent(ChannelHandlerContext ctx, HttpContent content) { 178 | if (!ignoreBodyRead) { 179 | if (content instanceof LastHttpContent) { 180 | 181 | if (content.content().readableBytes() > 0 || 182 | !((LastHttpContent) content).trailingHeaders().isEmpty()) { 183 | // It has data or trailing headers, send them 184 | ctx.fireChannelRead(content); 185 | } else { 186 | ReferenceCountUtil.release(content); 187 | } 188 | 189 | removeHandlerIfActive(ctx, ctx.name() + "-body-publisher"); 190 | currentlyStreamedMessage = null; 191 | consumedInMessage(ctx); 192 | 193 | } else { 194 | ctx.fireChannelRead(content); 195 | } 196 | 197 | } else { 198 | ReferenceCountUtil.release(content); 199 | if (content instanceof LastHttpContent) { 200 | ignoreBodyRead = false; 201 | if (currentlyStreamedMessage != null) { 202 | removeHandlerIfActive(ctx, ctx.name() + "-body-publisher"); 203 | } 204 | currentlyStreamedMessage = null; 205 | } 206 | } 207 | } 208 | 209 | @Override 210 | public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { 211 | if (ignoreBodyRead) { 212 | ctx.read(); 213 | } else { 214 | ctx.fireChannelReadComplete(); 215 | } 216 | } 217 | 218 | @Override 219 | public void write(final ChannelHandlerContext ctx, Object msg, final ChannelPromise promise) throws Exception { 220 | if (outClass.isInstance(msg)) { 221 | 222 | Outgoing out = new Outgoing(outClass.cast(msg), promise); 223 | receivedOutMessage(ctx); 224 | 225 | if (outgoing.isEmpty()) { 226 | outgoing.add(out); 227 | flushNext(ctx); 228 | } else { 229 | outgoing.add(out); 230 | } 231 | 232 | } else if (msg instanceof LastHttpContent) { 233 | 234 | sendLastHttpContent = false; 235 | ctx.write(msg, promise); 236 | } else { 237 | 238 | ctx.write(msg, promise); 239 | } 240 | } 241 | 242 | protected void unbufferedWrite(final ChannelHandlerContext ctx, final Outgoing out) { 243 | 244 | if (out.message instanceof FullHttpMessage) { 245 | // Forward as is 246 | ctx.writeAndFlush(out.message, out.promise); 247 | out.promise.addListener(new ChannelFutureListener() { 248 | @Override 249 | public void operationComplete(ChannelFuture channelFuture) throws Exception { 250 | executeInEventLoop(ctx, new Runnable() { 251 | @Override 252 | public void run() { 253 | sentOutMessage(ctx); 254 | outgoing.remove(); 255 | flushNext(ctx); 256 | } 257 | }); 258 | } 259 | }); 260 | 261 | } else if (out.message instanceof StreamedHttpMessage) { 262 | 263 | StreamedHttpMessage streamed = (StreamedHttpMessage) out.message; 264 | HandlerSubscriber subscriber = new HandlerSubscriber(ctx.executor()) { 265 | @Override 266 | protected void error(Throwable error) { 267 | out.promise.tryFailure(error); 268 | ctx.close(); 269 | } 270 | 271 | @Override 272 | protected void complete() { 273 | executeInEventLoop(ctx, new Runnable() { 274 | @Override 275 | public void run() { 276 | completeBody(ctx); 277 | } 278 | }); 279 | } 280 | }; 281 | 282 | sendLastHttpContent = true; 283 | 284 | // DON'T pass the promise through, create a new promise instead. 285 | ctx.writeAndFlush(out.message); 286 | 287 | ctx.pipeline().addAfter(ctx.name(), ctx.name() + "-body-subscriber", subscriber); 288 | subscribeSubscriberToStream(streamed, subscriber); 289 | } 290 | 291 | } 292 | 293 | private void completeBody(final ChannelHandlerContext ctx) { 294 | removeHandlerIfActive(ctx, ctx.name() + "-body-subscriber"); 295 | 296 | if (sendLastHttpContent) { 297 | ChannelPromise promise = outgoing.peek().promise; 298 | ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT, promise).addListener( 299 | new ChannelFutureListener() { 300 | @Override 301 | public void operationComplete(ChannelFuture channelFuture) throws Exception { 302 | executeInEventLoop(ctx, new Runnable() { 303 | @Override 304 | public void run() { 305 | outgoing.remove(); 306 | sentOutMessage(ctx); 307 | flushNext(ctx); 308 | } 309 | }); 310 | } 311 | } 312 | ); 313 | } else { 314 | outgoing.remove().promise.setSuccess(); 315 | sentOutMessage(ctx); 316 | flushNext(ctx); 317 | } 318 | } 319 | 320 | /** 321 | * Most operations we want to do even if the channel is not active, because if it's not, then we want to encounter 322 | * the error that occurs when that operation happens and so that it can be passed up to the user. However, removing 323 | * handlers should only be done if the channel is active, because the error that is encountered when they aren't 324 | * makes no sense to the user (NoSuchElementException). 325 | */ 326 | private void removeHandlerIfActive(ChannelHandlerContext ctx, String name) { 327 | if (ctx.channel().isActive()) { 328 | ctx.pipeline().remove(name); 329 | } 330 | } 331 | 332 | private void flushNext(ChannelHandlerContext ctx) { 333 | if (!outgoing.isEmpty()) { 334 | unbufferedWrite(ctx, outgoing.element()); 335 | } else { 336 | ctx.fireChannelWritabilityChanged(); 337 | } 338 | } 339 | 340 | private void executeInEventLoop(ChannelHandlerContext ctx, Runnable runnable) { 341 | if (ctx.executor().inEventLoop()) { 342 | runnable.run(); 343 | } else { 344 | ctx.executor().execute(runnable); 345 | } 346 | } 347 | 348 | class Outgoing { 349 | final Out message; 350 | final ChannelPromise promise; 351 | 352 | public Outgoing(Out message, ChannelPromise promise) { 353 | this.message = message; 354 | this.promise = promise; 355 | } 356 | } 357 | } 358 | -------------------------------------------------------------------------------- /netty-reactive-streams-http/src/main/java/org/playframework/netty/http/HttpStreamsServerHandler.java: -------------------------------------------------------------------------------- 1 | package org.playframework.netty.http; 2 | 3 | import org.playframework.netty.CancelledSubscriber; 4 | import org.playframework.netty.HandlerPublisher; 5 | import org.playframework.netty.HandlerSubscriber; 6 | import io.netty.channel.*; 7 | import io.netty.handler.codec.http.*; 8 | import io.netty.handler.codec.http.websocketx.WebSocketFrame; 9 | import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker; 10 | import io.netty.handler.codec.http.websocketx.WebSocketVersion; 11 | import org.reactivestreams.Publisher; 12 | 13 | import java.util.Collections; 14 | import java.util.List; 15 | import java.util.NoSuchElementException; 16 | 17 | /** 18 | * Handler that reads {@link HttpRequest} messages followed by {@link HttpContent} messages and produces 19 | * {@link StreamedHttpRequest} messages, and converts written {@link StreamedHttpResponse} messages into 20 | * {@link HttpResponse} messages followed by {@link HttpContent} messages. 21 | * 22 | * This allows request and response bodies to be handled using reactive streams. 23 | * 24 | * There are two types of messages that this handler will send down the chain, {@link StreamedHttpRequest}, 25 | * and {@link FullHttpRequest}. If {@link io.netty.channel.ChannelOption#AUTO_READ} is false for the channel, 26 | * then any {@link StreamedHttpRequest} messages must be subscribed to consume the body, otherwise 27 | * it's possible that no read will be done of the messages. 28 | * 29 | * There are three types of messages that this handler accepts for writing, {@link StreamedHttpResponse}, 30 | * {@link WebSocketHttpResponse} and {@link FullHttpResponse}. Writing any other messages may potentially 31 | * lead to HTTP message mangling. 32 | * 33 | * As long as messages are returned in the order that they arrive, this handler implicitly supports HTTP 34 | * pipelining. 35 | */ 36 | public class HttpStreamsServerHandler extends HttpStreamsHandler { 37 | 38 | private HttpRequest lastRequest = null; 39 | private Outgoing webSocketResponse = null; 40 | private int inFlight = 0; 41 | private boolean continueExpected = true; 42 | private boolean sendContinue = false; 43 | private boolean close = false; 44 | 45 | private final List dependentHandlers; 46 | 47 | public HttpStreamsServerHandler() { 48 | this(Collections.emptyList()); 49 | } 50 | 51 | /** 52 | * Create a new handler that is depended on by the given handlers. 53 | * 54 | * The list of dependent handlers will be removed from the chain when this handler is removed from the chain, 55 | * for example, when the connection is upgraded to use websockets. This is useful, for example, for removing 56 | * the reactive streams publisher/subscriber from the chain in that event. 57 | * 58 | * @param dependentHandlers The handlers that depend on this handler. 59 | */ 60 | public HttpStreamsServerHandler(List dependentHandlers) { 61 | super(HttpRequest.class, HttpResponse.class); 62 | this.dependentHandlers = dependentHandlers; 63 | } 64 | 65 | @Override 66 | protected boolean hasBody(HttpRequest request) { 67 | // Http requests don't have a body if they define 0 content length, or no content length and no transfer 68 | // encoding 69 | return HttpUtil.getContentLength(request, 0) != 0 || HttpUtil.isTransferEncodingChunked(request); 70 | } 71 | 72 | @Override 73 | protected HttpRequest createEmptyMessage(HttpRequest request) { 74 | return new EmptyHttpRequest(request); 75 | } 76 | 77 | @Override 78 | protected HttpRequest createStreamedMessage(HttpRequest httpRequest, Publisher stream) { 79 | return new DelegateStreamedHttpRequest(httpRequest, stream); 80 | } 81 | 82 | @Override 83 | public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { 84 | // Set to false, since if it was true, and the client is sending data, then the 85 | // client must no longer be expecting it (due to a timeout, for example). 86 | continueExpected = false; 87 | sendContinue = false; 88 | 89 | if (msg instanceof HttpRequest) { 90 | HttpRequest request = (HttpRequest) msg; 91 | lastRequest = request; 92 | if (HttpUtil.is100ContinueExpected(request)) { 93 | continueExpected = true; 94 | } 95 | } 96 | super.channelRead(ctx, msg); 97 | } 98 | 99 | @Override 100 | protected void receivedInMessage(ChannelHandlerContext ctx) { 101 | inFlight++; 102 | } 103 | 104 | @Override 105 | protected void sentOutMessage(ChannelHandlerContext ctx) { 106 | inFlight--; 107 | if (inFlight == 1 && continueExpected && sendContinue) { 108 | ctx.writeAndFlush(new DefaultFullHttpResponse(lastRequest.protocolVersion(), HttpResponseStatus.CONTINUE)); 109 | sendContinue = false; 110 | continueExpected = false; 111 | } 112 | 113 | if (close) { 114 | ctx.close(); 115 | } 116 | 117 | // release reference to lastRequest when there are no in flight requests so it can be GC'd 118 | if (inFlight == 0) { 119 | lastRequest = null; 120 | } 121 | } 122 | 123 | @Override 124 | protected void unbufferedWrite(ChannelHandlerContext ctx, HttpStreamsHandler.Outgoing out) { 125 | 126 | if (out.message instanceof WebSocketHttpResponse) { 127 | if ((lastRequest instanceof FullHttpRequest) || !hasBody(lastRequest)) { 128 | handleWebSocketResponse(ctx, out); 129 | } else { 130 | // If the response has a streamed body, then we can't send the WebSocket response until we've received 131 | // the body. 132 | webSocketResponse = out; 133 | } 134 | } else { 135 | String connection = out.message.headers().get(HttpHeaderNames.CONNECTION); 136 | if (lastRequest.protocolVersion().isKeepAliveDefault()) { 137 | if ("close".equalsIgnoreCase(connection)) { 138 | close = true; 139 | } 140 | } else { 141 | if (!"keep-alive".equalsIgnoreCase(connection)) { 142 | close = true; 143 | } 144 | } 145 | if (inFlight == 1 && continueExpected) { 146 | HttpUtil.setKeepAlive(out.message, false); 147 | close = true; 148 | continueExpected = false; 149 | } 150 | // According to RFC 7230 a server MUST NOT send a Content-Length or a Transfer-Encoding when the status 151 | // code is 1xx or 204, also a status code 304 may not have a Content-Length or Transfer-Encoding set. 152 | if (!HttpUtil.isContentLengthSet(out.message) && !HttpUtil.isTransferEncodingChunked(out.message) 153 | && canHaveBody(out.message)) { 154 | HttpUtil.setKeepAlive(out.message, false); 155 | close = true; 156 | } 157 | super.unbufferedWrite(ctx, out); 158 | } 159 | } 160 | 161 | private boolean canHaveBody(HttpResponse message) { 162 | HttpResponseStatus status = message.status(); 163 | // All 1xx (Informational), 204 (No Content), and 304 (Not Modified) 164 | // responses do not include a message body 165 | return !(status == HttpResponseStatus.CONTINUE || status == HttpResponseStatus.SWITCHING_PROTOCOLS || 166 | status == HttpResponseStatus.PROCESSING || status == HttpResponseStatus.NO_CONTENT || 167 | status == HttpResponseStatus.NOT_MODIFIED); 168 | } 169 | 170 | @Override 171 | protected void consumedInMessage(ChannelHandlerContext ctx) { 172 | if (webSocketResponse != null) { 173 | handleWebSocketResponse(ctx, webSocketResponse); 174 | webSocketResponse = null; 175 | } 176 | } 177 | 178 | private void handleWebSocketResponse(ChannelHandlerContext ctx, Outgoing out) { 179 | WebSocketHttpResponse response = (WebSocketHttpResponse) out.message; 180 | WebSocketServerHandshaker handshaker = response.handshakerFactory().newHandshaker(lastRequest); 181 | 182 | if (handshaker == null) { 183 | HttpResponse res = new DefaultFullHttpResponse( 184 | HttpVersion.HTTP_1_1, 185 | HttpResponseStatus.UPGRADE_REQUIRED); 186 | res.headers().set(HttpHeaderNames.SEC_WEBSOCKET_VERSION, WebSocketVersion.V13.toHttpHeaderValue()); 187 | HttpUtil.setContentLength(res, 0); 188 | super.unbufferedWrite(ctx, new Outgoing(res, out.promise)); 189 | response.subscribe(new CancelledSubscriber<>()); 190 | } else { 191 | // First, insert new handlers in the chain after us for handling the websocket 192 | ChannelPipeline pipeline = ctx.pipeline(); 193 | HandlerPublisher publisher = new HandlerPublisher<>(ctx.executor(), WebSocketFrame.class); 194 | HandlerSubscriber subscriber = new HandlerSubscriber<>(ctx.executor()); 195 | pipeline.addAfter(ctx.executor(), ctx.name(), "websocket-subscriber", subscriber); 196 | pipeline.addAfter(ctx.executor(), ctx.name(), "websocket-publisher", publisher); 197 | 198 | // Now remove ourselves from the chain 199 | ctx.pipeline().remove(ctx.name()); 200 | 201 | // Now do the handshake 202 | // Wrap the request in an empty request because we don't need the WebSocket handshaker ignoring the body, 203 | // we already have handled the body. 204 | handshaker.handshake(ctx.channel(), new EmptyHttpRequest(lastRequest)); 205 | 206 | // And hook up the subscriber/publishers 207 | response.subscribe(subscriber); 208 | publisher.subscribe(response); 209 | } 210 | 211 | } 212 | 213 | @Override 214 | protected void bodyRequested(ChannelHandlerContext ctx) { 215 | if (continueExpected) { 216 | if (inFlight == 1) { 217 | ctx.writeAndFlush(new DefaultFullHttpResponse(lastRequest.protocolVersion(), HttpResponseStatus.CONTINUE)); 218 | continueExpected = false; 219 | } else { 220 | sendContinue = true; 221 | } 222 | } 223 | } 224 | 225 | @Override 226 | public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { 227 | super.handlerRemoved(ctx); 228 | for (ChannelHandler dependent: dependentHandlers) { 229 | try { 230 | ctx.pipeline().remove(dependent); 231 | } catch (NoSuchElementException e) { 232 | // Ignore, maybe something else removed it 233 | } 234 | } 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /netty-reactive-streams-http/src/main/java/org/playframework/netty/http/StreamedHttpMessage.java: -------------------------------------------------------------------------------- 1 | package org.playframework.netty.http; 2 | 3 | import io.netty.handler.codec.http.HttpContent; 4 | import io.netty.handler.codec.http.HttpMessage; 5 | import org.reactivestreams.Publisher; 6 | 7 | /** 8 | * Combines {@link HttpMessage} and {@link Publisher} into one 9 | * message. So it represents an http message with a stream of {@link HttpContent} 10 | * messages that can be subscribed to. 11 | * 12 | * Note that receivers of this message must consume the publisher, 13 | * since the publisher will exert back pressure up the stream if not consumed. 14 | */ 15 | public interface StreamedHttpMessage extends HttpMessage, Publisher { 16 | } 17 | -------------------------------------------------------------------------------- /netty-reactive-streams-http/src/main/java/org/playframework/netty/http/StreamedHttpRequest.java: -------------------------------------------------------------------------------- 1 | package org.playframework.netty.http; 2 | 3 | import io.netty.handler.codec.http.HttpRequest; 4 | 5 | /** 6 | * Combines {@link HttpRequest} and {@link StreamedHttpMessage} into one 7 | * message. So it represents an http request with a stream of 8 | * {@link io.netty.handler.codec.http.HttpContent} messages that can be subscribed to. 9 | */ 10 | public interface StreamedHttpRequest extends HttpRequest, StreamedHttpMessage { 11 | } 12 | -------------------------------------------------------------------------------- /netty-reactive-streams-http/src/main/java/org/playframework/netty/http/StreamedHttpResponse.java: -------------------------------------------------------------------------------- 1 | package org.playframework.netty.http; 2 | 3 | import io.netty.handler.codec.http.HttpResponse; 4 | 5 | /** 6 | * Combines {@link HttpResponse} and {@link StreamedHttpMessage} into one 7 | * message. So it represents an http response with a stream of 8 | * {@link io.netty.handler.codec.http.HttpContent} messages that can be subscribed to. 9 | */ 10 | public interface StreamedHttpResponse extends HttpResponse, StreamedHttpMessage { 11 | } 12 | -------------------------------------------------------------------------------- /netty-reactive-streams-http/src/main/java/org/playframework/netty/http/WebSocketHttpResponse.java: -------------------------------------------------------------------------------- 1 | package org.playframework.netty.http; 2 | 3 | import io.netty.handler.codec.http.HttpResponse; 4 | import io.netty.handler.codec.http.websocketx.WebSocketFrame; 5 | import io.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory; 6 | import org.reactivestreams.Processor; 7 | 8 | 9 | /** 10 | * Combines {@link HttpResponse} and {@link Processor} 11 | * into one message. So it represents an http response with a processor that can handle 12 | * a WebSocket. 13 | * 14 | * This is only used for server side responses. For client side websocket requests, it's 15 | * better to configure the reactive streams pipeline directly. 16 | */ 17 | public interface WebSocketHttpResponse extends HttpResponse, Processor { 18 | /** 19 | * Get the handshaker factory to use to reconfigure the channel. 20 | * 21 | * @return The handshaker factory. 22 | */ 23 | WebSocketServerHandshakerFactory handshakerFactory(); 24 | } 25 | -------------------------------------------------------------------------------- /netty-reactive-streams-http/src/test/java/org/playframework/netty/http/DelegateProcessor.java: -------------------------------------------------------------------------------- 1 | package org.playframework.netty.http; 2 | 3 | import org.reactivestreams.Processor; 4 | import org.reactivestreams.Publisher; 5 | import org.reactivestreams.Subscriber; 6 | import org.reactivestreams.Subscription; 7 | 8 | public class DelegateProcessor implements Processor { 9 | 10 | private final Subscriber subscriber; 11 | private final Publisher publisher; 12 | 13 | public DelegateProcessor(Subscriber subscriber, Publisher publisher) { 14 | this.subscriber = subscriber; 15 | this.publisher = publisher; 16 | } 17 | 18 | @Override 19 | public void subscribe(Subscriber s) { 20 | publisher.subscribe(s); 21 | } 22 | 23 | @Override 24 | public void onSubscribe(Subscription s) { 25 | subscriber.onSubscribe(s); 26 | } 27 | 28 | @Override 29 | public void onNext(In in) { 30 | subscriber.onNext(in); 31 | } 32 | 33 | @Override 34 | public void onError(Throwable t) { 35 | subscriber.onError(t); 36 | } 37 | 38 | @Override 39 | public void onComplete() { 40 | subscriber.onComplete(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /netty-reactive-streams-http/src/test/java/org/playframework/netty/http/FullStackHttpIdentityProcessorVerificationTest.java: -------------------------------------------------------------------------------- 1 | package org.playframework.netty.http; 2 | 3 | import org.apache.pekko.NotUsed; 4 | import org.apache.pekko.actor.ActorSystem; 5 | import org.apache.pekko.japi.function.Function; 6 | import org.apache.pekko.stream.Materializer; 7 | import org.apache.pekko.stream.javadsl.*; 8 | import io.netty.channel.*; 9 | import io.netty.channel.nio.NioEventLoopGroup; 10 | import io.netty.handler.codec.http.*; 11 | import org.reactivestreams.Processor; 12 | import org.reactivestreams.Publisher; 13 | import org.reactivestreams.tck.IdentityProcessorVerification; 14 | import org.reactivestreams.tck.TestEnvironment; 15 | import org.testng.annotations.*; 16 | import scala.concurrent.Await; 17 | import scala.concurrent.duration.Duration; 18 | 19 | import java.net.InetSocketAddress; 20 | import java.util.ArrayList; 21 | import java.util.List; 22 | import java.util.concurrent.Callable; 23 | import java.util.concurrent.CompletionStage; 24 | import java.util.concurrent.ExecutorService; 25 | import java.util.concurrent.Executors; 26 | import java.util.concurrent.TimeUnit; 27 | 28 | /** 29 | * This identity processor verification verifies a client making requests to a server, that echos the 30 | * body back. 31 | * 32 | * The server uses the {@link HttpStreamsServerHandler}, and then exposes the messages sent/received by 33 | * that using reactive streams. So it effectively uses streams of streams. It then uses Pekko streams 34 | * to actually handle the requests, echoing the bodies back in the responses as is. 35 | * 36 | * The client uses the {@link HttpStreamsClientHandler}, and then exposes the messages sent/received by 37 | * that using reactive streams, so it too is effectively a stream of streams. Here Pekko streams is used 38 | * to split the String bodies into many chunks, for more interesting verification of the bodies, and then 39 | * combines all the chunks together back into a String at the end. 40 | */ 41 | public class FullStackHttpIdentityProcessorVerificationTest extends IdentityProcessorVerification { 42 | 43 | private NioEventLoopGroup eventLoop; 44 | private Channel serverBindChannel; 45 | private ActorSystem actorSystem; 46 | private Materializer materializer; 47 | private HttpHelper helper; 48 | private ExecutorService executorService; 49 | 50 | public FullStackHttpIdentityProcessorVerificationTest() { 51 | super(new TestEnvironment(1000)); 52 | } 53 | 54 | @BeforeClass 55 | public void start() throws Exception { 56 | executorService = Executors.newCachedThreadPool(); 57 | actorSystem = ActorSystem.create(); 58 | materializer = Materializer.matFromSystem(actorSystem); 59 | helper = new HttpHelper(materializer); 60 | eventLoop = new NioEventLoopGroup(); 61 | ProcessorHttpServer server = new ProcessorHttpServer(eventLoop); 62 | 63 | // A flow that echos HttpRequest bodies in HttpResponse bodies 64 | final Flow flow = Flow.create().map( 65 | new Function() { 66 | public HttpResponse apply(HttpRequest request) throws Exception { 67 | return helper.echo(request); 68 | } 69 | } 70 | ); 71 | 72 | serverBindChannel = server.bind(new InetSocketAddress("127.0.0.1", 0), new Callable>() { 73 | @Override 74 | public Processor call() throws Exception { 75 | return PekkoStreamsUtil.flowToProcessor(flow, materializer); 76 | } 77 | }).await().channel(); 78 | } 79 | 80 | @AfterClass 81 | public void stop() throws Exception { 82 | serverBindChannel.close().await(); 83 | executorService.shutdown(); 84 | Await.ready(actorSystem.terminate(), Duration.create(10000, TimeUnit.MILLISECONDS)); 85 | eventLoop.shutdownGracefully(100, 10000, TimeUnit.MILLISECONDS).await(); 86 | } 87 | 88 | @Override 89 | public Processor createIdentityProcessor(int bufferSize) { 90 | 91 | ProcessorHttpClient client = new ProcessorHttpClient(eventLoop); 92 | Processor connection = getProcessor(client); 93 | 94 | Flow flow = Flow.create() 95 | // Convert the Strings to HttpRequests 96 | .map(new Function() { 97 | @Override 98 | public HttpRequest apply(String body) throws Exception { 99 | List content = new ArrayList<>(); 100 | String[] chunks = body.split(":"); 101 | for (String chunk: chunks) { 102 | // Make sure we put the ":" back into the body 103 | String c; 104 | if (content.isEmpty()) { 105 | c = chunk; 106 | } else { 107 | c = ":" + chunk; 108 | } 109 | content.add(c); 110 | } 111 | return helper.createChunkedRequest("POST", "/" + chunks[0], content); 112 | } 113 | }) 114 | // Send the flow via the HTTP client connection 115 | .via(PekkoStreamsUtil.processorToFlow(connection)) 116 | // Convert the responses to Strings 117 | .mapAsync(4, new Function>() { 118 | @Override 119 | public CompletionStage apply(HttpResponse response) throws Exception { 120 | return helper.extractBodyAsync(response); 121 | } 122 | }); 123 | 124 | return PekkoStreamsUtil.flowToProcessor(flow, materializer); 125 | } 126 | 127 | private Processor getProcessor(ProcessorHttpClient client) { 128 | try { 129 | return client.connect(serverBindChannel.localAddress()); 130 | } catch (Exception ex) { 131 | throw new RuntimeException(ex); 132 | } 133 | } 134 | 135 | @Override 136 | public Publisher createFailedPublisher() { 137 | return Source.failed(new RuntimeException("failed")) 138 | .toMat(Sink.asPublisher(AsPublisher.WITH_FANOUT), Keep.>right()).run(materializer); 139 | } 140 | 141 | @Override 142 | public ExecutorService publisherExecutorService() { 143 | return executorService; 144 | } 145 | 146 | /** 147 | * We want to send a list of chunks, but the problem is, Netty may (and does) dechunk things in certain 148 | * circumstances. So, we create Strings, and we split it into chunks it in the flow, then combine all 149 | * chunks when it gets back, then split again, that way if netty combines the chunks, it doesn't matter. 150 | */ 151 | @Override 152 | public String createElement(int element) { 153 | StringBuilder sb = new StringBuilder(); 154 | // Make the first element the number, then we set the URL to that number when we make the request, 155 | // making debugging easier. 156 | sb.append(element); 157 | for (int i = 0; i < 20; i++) { 158 | sb.append(":this is a very cool element, it is element number ").append(i); 159 | } 160 | return sb.toString(); 161 | } 162 | 163 | @Override 164 | public long maxSupportedSubscribers() { 165 | return 1; 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /netty-reactive-streams-http/src/test/java/org/playframework/netty/http/HttpHelper.java: -------------------------------------------------------------------------------- 1 | package org.playframework.netty.http; 2 | 3 | import org.apache.pekko.japi.function.Function2; 4 | import org.apache.pekko.stream.Materializer; 5 | import org.apache.pekko.stream.javadsl.AsPublisher; 6 | import org.apache.pekko.stream.javadsl.Sink; 7 | import org.apache.pekko.stream.javadsl.Source; 8 | import io.netty.buffer.ByteBuf; 9 | import io.netty.buffer.Unpooled; 10 | import io.netty.handler.codec.http.*; 11 | import io.netty.util.ReferenceCountUtil; 12 | import org.reactivestreams.Publisher; 13 | 14 | import java.nio.charset.Charset; 15 | import java.util.ArrayList; 16 | import java.util.List; 17 | import java.util.concurrent.CompletableFuture; 18 | import java.util.concurrent.CompletionStage; 19 | import java.util.concurrent.TimeUnit; 20 | 21 | import static org.testng.Assert.assertNotNull; 22 | import static org.testng.AssertJUnit.assertEquals; 23 | 24 | /** 25 | * Helpers for building HTTP test servers 26 | */ 27 | public class HttpHelper { 28 | 29 | protected final Materializer materializer; 30 | 31 | public HttpHelper(Materializer materializer) { 32 | this.materializer = materializer; 33 | } 34 | 35 | /** 36 | * An echo HTTP server 37 | */ 38 | public HttpResponse echo(Object msg) { 39 | HttpResponse response; 40 | if (msg instanceof HttpRequest) { 41 | 42 | HttpRequest request = (HttpRequest) msg; 43 | if (request instanceof FullHttpRequest) { 44 | response = new DefaultFullHttpResponse(request.protocolVersion(), HttpResponseStatus.OK, 45 | ((FullHttpRequest) msg).content()); 46 | response.headers().set("Request-Type", "Full"); 47 | } else if (request instanceof StreamedHttpRequest) { 48 | response = new DefaultStreamedHttpResponse(request.protocolVersion(), HttpResponseStatus.OK, 49 | ((StreamedHttpRequest) msg)); 50 | response.headers().set("Request-Type", "Streamed"); 51 | } else { 52 | throw new IllegalArgumentException("Unsupported HTTP request: " + request); 53 | } 54 | 55 | if (HttpUtil.isTransferEncodingChunked(request)) { 56 | HttpUtil.setTransferEncodingChunked(response, true); 57 | } else if (HttpUtil.isContentLengthSet(request)) { 58 | long contentLength = HttpUtil.getContentLength(request); 59 | response.headers().set("Request-Content-Length", contentLength); 60 | HttpUtil.setContentLength(response, contentLength); 61 | } else { 62 | HttpUtil.setContentLength(response, 0); 63 | } 64 | 65 | response.headers().set("Request-Uri", request.uri()); 66 | } else { 67 | throw new IllegalArgumentException("Unsupported message: " + msg); 68 | } 69 | 70 | return response; 71 | } 72 | 73 | public StreamedHttpRequest createStreamedRequest(String method, String uri, List body) { 74 | List content = new ArrayList<>(); 75 | for (String chunk: body) { 76 | content.add(new DefaultHttpContent(Unpooled.copiedBuffer(chunk, Charset.forName("utf-8")))); 77 | } 78 | Publisher publisher = Source.from(content).runWith(Sink.asPublisher(AsPublisher.WITH_FANOUT), materializer); 79 | return new DefaultStreamedHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.valueOf(method), uri, 80 | publisher); 81 | } 82 | 83 | public StreamedHttpRequest createStreamedRequest(String method, String uri, List body, long contentLength) { 84 | StreamedHttpRequest request = createStreamedRequest(method, uri, body); 85 | HttpUtil.setContentLength(request, contentLength); 86 | return request; 87 | } 88 | 89 | public StreamedHttpRequest createChunkedRequest(String method, String uri, List body) { 90 | StreamedHttpRequest request = createStreamedRequest(method, uri, body); 91 | HttpUtil.setTransferEncodingChunked(request, true); 92 | return request; 93 | } 94 | 95 | public FullHttpResponse createFullResponse(String body) { 96 | ByteBuf content = Unpooled.copiedBuffer(body, Charset.forName("utf-8")); 97 | FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, content); 98 | HttpUtil.setContentLength(response, content.readableBytes()); 99 | return response; 100 | } 101 | 102 | public StreamedHttpResponse createStreamedResponse(HttpVersion version, List body, long contentLength) { 103 | List content = new ArrayList<>(); 104 | for (String chunk: body) { 105 | content.add(new DefaultHttpContent(Unpooled.copiedBuffer(chunk, Charset.forName("utf-8")))); 106 | } 107 | Publisher publisher = Source.from(content).runWith(Sink.asPublisher(AsPublisher.WITH_FANOUT), materializer); 108 | StreamedHttpResponse response = new DefaultStreamedHttpResponse(version, HttpResponseStatus.OK, publisher); 109 | HttpUtil.setContentLength(response, contentLength); 110 | return response; 111 | } 112 | 113 | public String extractBody(Object msg) throws Exception { 114 | return extractBodyAsync(msg).toCompletableFuture().get(1, TimeUnit.SECONDS); 115 | } 116 | 117 | public CompletionStage extractBodyAsync(Object msg) { 118 | if (msg instanceof FullHttpMessage) { 119 | String body = contentAsString((FullHttpMessage) msg); 120 | return CompletableFuture.completedFuture(body); 121 | } else if (msg instanceof StreamedHttpMessage) { 122 | return Source.fromPublisher((StreamedHttpMessage) msg).runFold("", new Function2() { 123 | @Override 124 | public String apply(String body, HttpContent content) throws Exception { 125 | return body + contentAsString(content); 126 | } 127 | }, materializer); 128 | } else { 129 | throw new IllegalArgumentException("Unknown message type: " + msg); 130 | } 131 | } 132 | 133 | private String contentAsString(HttpContent content) { 134 | String body = content.content().toString(Charset.forName("utf-8")); 135 | ReferenceCountUtil.release(content); 136 | return body; 137 | } 138 | 139 | public void assertRequestTypeStreamed(HttpResponse response) { 140 | assertEquals(response.headers().get("Request-Type"), "Streamed"); 141 | } 142 | 143 | public void assertRequestTypeFull(HttpResponse response) { 144 | assertEquals(response.headers().get("Request-Type"), "Full"); 145 | } 146 | 147 | public long getRequestContentLength(HttpResponse response) { 148 | String contentLength = response.headers().get("Request-Content-Length"); 149 | assertNotNull(contentLength, "Expected the request to have a content length"); 150 | return Long.parseLong(contentLength); 151 | } 152 | 153 | public boolean hasRequestContentLength(HttpResponse response) { 154 | return response.headers().contains("Request-Content-Length"); 155 | } 156 | 157 | public void cancelStreamedMessage(Object msg) { 158 | if (msg instanceof StreamedHttpMessage) { 159 | Source.fromPublisher((StreamedHttpMessage) msg).runWith(Sink.cancelled(), materializer); 160 | } else { 161 | throw new IllegalArgumentException("Unknown message type: " + msg); 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /netty-reactive-streams-http/src/test/java/org/playframework/netty/http/HttpStreamsTest.java: -------------------------------------------------------------------------------- 1 | package org.playframework.netty.http; 2 | 3 | import org.apache.pekko.actor.ActorSystem; 4 | import org.apache.pekko.japi.function.Function; 5 | import org.apache.pekko.stream.Materializer; 6 | import org.apache.pekko.stream.javadsl.Flow; 7 | import org.playframework.netty.HandlerPublisher; 8 | import org.playframework.netty.HandlerSubscriber; 9 | import io.netty.bootstrap.Bootstrap; 10 | import io.netty.bootstrap.ServerBootstrap; 11 | import io.netty.buffer.Unpooled; 12 | import io.netty.channel.*; 13 | import io.netty.channel.nio.NioEventLoopGroup; 14 | import io.netty.channel.socket.SocketChannel; 15 | import io.netty.channel.socket.nio.NioServerSocketChannel; 16 | import io.netty.channel.socket.nio.NioSocketChannel; 17 | import io.netty.handler.codec.http.*; 18 | import org.reactivestreams.Processor; 19 | import org.testng.annotations.AfterClass; 20 | import org.testng.annotations.AfterMethod; 21 | import org.testng.annotations.BeforeClass; 22 | import org.testng.annotations.Test; 23 | import scala.concurrent.Await; 24 | import scala.concurrent.duration.Duration; 25 | 26 | import java.nio.charset.Charset; 27 | import java.util.Arrays; 28 | import java.util.Collections; 29 | import java.util.concurrent.BlockingQueue; 30 | import java.util.concurrent.CompletionStage; 31 | import java.util.concurrent.LinkedBlockingQueue; 32 | import java.util.concurrent.TimeUnit; 33 | 34 | import static org.testng.Assert.*; 35 | 36 | public class HttpStreamsTest { 37 | 38 | private NioEventLoopGroup eventLoop; 39 | private ActorSystem actorSystem; 40 | private Materializer materializer; 41 | private HttpHelper helper; 42 | private Channel serverBindChannel; 43 | private Channel client; 44 | private final BlockingQueue clientEvents = new LinkedBlockingQueue<>(); 45 | 46 | private static final long TIMEOUT_MS = 5000; 47 | 48 | @Test 49 | public void streamedRequestResponse() throws Exception { 50 | createEchoServer(); 51 | client.writeAndFlush(helper.createStreamedRequest("POST", "/", Arrays.asList("hello", " ", "world"), 11)); 52 | StreamedHttpResponse response = receiveStreamedResponse(); 53 | 54 | helper.assertRequestTypeStreamed(response); 55 | assertEquals(helper.getRequestContentLength(response), 11); 56 | 57 | assertEquals(helper.extractBody(response), "hello world"); 58 | assertEquals(HttpUtil.getContentLength(response), 11); 59 | } 60 | 61 | @Test 62 | public void chunkedRequestResponse() throws Exception { 63 | createEchoServer(); 64 | client.writeAndFlush(helper.createChunkedRequest("POST", "/", Arrays.asList("hello", " ", "world"))); 65 | StreamedHttpResponse response = receiveStreamedResponse(); 66 | 67 | helper.assertRequestTypeStreamed(response); 68 | assertFalse(helper.hasRequestContentLength(response)); 69 | 70 | assertEquals(helper.extractBody(response), "hello world"); 71 | assertTrue(HttpUtil.isTransferEncodingChunked(response)); 72 | } 73 | 74 | @Test 75 | public void emptyRequestResponse() throws Exception { 76 | createEchoServer(); 77 | client.writeAndFlush(helper.createStreamedRequest("POST", "/", Collections.emptyList())); 78 | FullHttpResponse response = receiveFullResponse(); 79 | 80 | helper.assertRequestTypeFull(response); 81 | assertFalse(helper.hasRequestContentLength(response)); 82 | 83 | assertEquals(helper.extractBody(response), ""); 84 | assertEquals(HttpUtil.getContentLength(response), 0); 85 | } 86 | 87 | @Test 88 | public void zeroLengthRequestResponse() throws Exception { 89 | createEchoServer(); 90 | client.writeAndFlush(helper.createStreamedRequest("POST", "/", Collections.emptyList(), 0)); 91 | FullHttpResponse response = receiveFullResponse(); 92 | 93 | helper.assertRequestTypeFull(response); 94 | assertEquals(helper.getRequestContentLength(response), 0); 95 | 96 | assertEquals(helper.extractBody(response), ""); 97 | assertEquals(HttpUtil.getContentLength(response), 0); 98 | } 99 | 100 | @Test 101 | public void noContentLengthResponse() throws Exception { 102 | responseServer(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, 103 | Unpooled.copiedBuffer("hello world", Charset.forName("utf-8")))); 104 | client.writeAndFlush(helper.createStreamedRequest("GET", "/", Collections.emptyList())); 105 | StreamedHttpResponse response = receiveStreamedResponse(); 106 | 107 | assertFalse(HttpUtil.isContentLengthSet(response)); 108 | assertEquals(helper.extractBody(response), "hello world"); 109 | 110 | client.closeFuture().await(TIMEOUT_MS, TimeUnit.MILLISECONDS); 111 | assertFalse(client.isOpen()); 112 | } 113 | 114 | @Test 115 | public void noContentLength204Response() throws Exception { 116 | responseServer(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NO_CONTENT, 117 | Unpooled.EMPTY_BUFFER)); 118 | client.writeAndFlush(helper.createStreamedRequest("GET", "/", Collections.emptyList())); 119 | FullHttpResponse response = receiveFullResponse(); 120 | 121 | assertFalse(HttpUtil.isContentLengthSet(response)); 122 | assertEquals(helper.extractBody(response), ""); 123 | 124 | client.closeFuture().await(TIMEOUT_MS, TimeUnit.MILLISECONDS); 125 | assertTrue(client.isOpen()); 126 | } 127 | 128 | @Test 129 | public void cancelRequestBody() throws Exception { 130 | createEchoServer(); 131 | start(new AutoReadHandler() { 132 | public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { 133 | if (msg instanceof StreamedHttpRequest) { 134 | StreamedHttpRequest request = (StreamedHttpRequest) msg; 135 | if (request.uri().equals("/cancel")) { 136 | helper.cancelStreamedMessage(request); 137 | HttpResponse response = helper.createFullResponse(""); 138 | response.headers().set("Cancelled", true); 139 | ctx.writeAndFlush(response); 140 | } else { 141 | ctx.writeAndFlush(helper.echo(msg)); 142 | } 143 | } else { 144 | ctx.writeAndFlush(helper.echo(msg)); 145 | } 146 | ctx.read(); 147 | } 148 | }); 149 | 150 | client.writeAndFlush(helper.createStreamedRequest("POST", "/cancel", Arrays.asList("hello", " ", "world"), 11)); 151 | FullHttpResponse response = receiveFullResponse(); 152 | 153 | assertEquals(response.headers().get("Cancelled"), "true"); 154 | assertEquals(helper.extractBody(response), ""); 155 | 156 | // Ensure that the connection is still usable 157 | client.writeAndFlush(helper.createStreamedRequest("POST", "/", Arrays.asList("Hello", " ", "World"), 11)); 158 | StreamedHttpResponse response2 = receiveStreamedResponse(); 159 | assertEquals(helper.extractBody(response2), "Hello World"); 160 | } 161 | 162 | @Test 163 | public void cancelResponseBody() throws Exception { 164 | createEchoServer(); 165 | client.writeAndFlush(helper.createStreamedRequest("POST", "/", Arrays.asList("hello", " ", "world"), 11)); 166 | StreamedHttpResponse response = receiveStreamedResponse(); 167 | helper.cancelStreamedMessage(response); 168 | 169 | // Ensure that the connection is still usable 170 | client.writeAndFlush(helper.createStreamedRequest("POST", "/", Arrays.asList("Hello", " ", "World"), 11)); 171 | StreamedHttpResponse response2 = receiveStreamedResponse(); 172 | assertEquals(helper.extractBody(response2), "Hello World"); 173 | } 174 | 175 | @Test 176 | public void expect100ContinueAccepted() throws Exception { 177 | start(new AutoReadHandler() { 178 | @Override 179 | public void channelRead(final ChannelHandlerContext ctx, Object msg) throws Exception { 180 | helper.extractBodyAsync(msg).thenApply(new java.util.function.Function() { 181 | @Override 182 | public Object apply(String body) { 183 | ctx.writeAndFlush(helper.createFullResponse(body)); 184 | return null; 185 | } 186 | }); 187 | } 188 | }); 189 | StreamedHttpRequest request = helper.createStreamedRequest("POST", "/", Arrays.asList("hello", " ", "world"), 11); 190 | HttpUtil.set100ContinueExpected(request, true); 191 | client.writeAndFlush(request); 192 | 193 | StreamedHttpResponse response = receiveStreamedResponse(); 194 | assertEquals(helper.extractBody(response), "hello world"); 195 | 196 | // Ensure that the connection is still usable 197 | client.writeAndFlush(helper.createStreamedRequest("POST", "/", Arrays.asList("Hello", " ", "World"), 11)); 198 | StreamedHttpResponse response2 = receiveStreamedResponse(); 199 | assertEquals(helper.extractBody(response2), "Hello World"); 200 | } 201 | 202 | @Test 203 | public void expect100ContinueRejected() throws Exception { 204 | start(new AutoReadHandler() { 205 | @Override 206 | public void channelRead(final ChannelHandlerContext ctx, Object msg) throws Exception { 207 | ctx.writeAndFlush(helper.createFullResponse("rejected")); 208 | } 209 | }); 210 | StreamedHttpRequest request = helper.createStreamedRequest("POST", "/", Arrays.asList("hello", " ", "world"), 11); 211 | HttpUtil.set100ContinueExpected(request, true); 212 | client.writeAndFlush(request); 213 | 214 | StreamedHttpResponse response = receiveStreamedResponse(); 215 | assertEquals(helper.extractBody(response), "rejected"); 216 | 217 | // Ensure that the connection was closed 218 | client.closeFuture().await(); 219 | assertFalse(client.isOpen()); 220 | } 221 | 222 | @Test 223 | public void expect100ContinuePipelining() throws Exception { 224 | start(new ChannelHandlerAdapter() { 225 | public void handlerAdded(ChannelHandlerContext ctx) throws Exception { 226 | HandlerPublisher publisher = new HandlerPublisher<>(ctx.executor(), HttpRequest.class); 227 | HandlerSubscriber subscriber = new HandlerSubscriber<>(ctx.executor()); 228 | ctx.pipeline().addLast(publisher, subscriber); 229 | Processor processor = PekkoStreamsUtil.flowToProcessor(Flow.create() 230 | .mapAsync(4, new Function>() { 231 | public CompletionStage apply(HttpRequest request) throws Exception { 232 | return helper.extractBodyAsync(request); 233 | } 234 | }).map(new Function() { 235 | public HttpResponse apply(String body) throws Exception { 236 | HttpResponse response = helper.createFullResponse(body); 237 | response.headers().set("Body", body); 238 | return response; 239 | } 240 | }), 241 | materializer 242 | ); 243 | publisher.subscribe(processor); 244 | processor.subscribe(subscriber); 245 | } 246 | }); 247 | client.writeAndFlush(helper.createStreamedRequest("POST", "/", Collections.singletonList("request 1"), 9)); 248 | client.writeAndFlush(helper.createStreamedRequest("POST", "/", Collections.singletonList("request 2"), 9)); 249 | StreamedHttpRequest request3 = helper.createStreamedRequest("POST", "/", Collections.singletonList("request 3"), 9); 250 | HttpUtil.set100ContinueExpected(request3, true); 251 | client.writeAndFlush(request3); 252 | client.writeAndFlush(helper.createStreamedRequest("POST", "/", Collections.singletonList("request 4"), 9)); 253 | client.writeAndFlush(helper.createStreamedRequest("POST", "/", Collections.singletonList("request 5"), 9)); 254 | StreamedHttpRequest request6 = helper.createStreamedRequest("POST", "/", Collections.singletonList("request 6"), 9); 255 | HttpUtil.set100ContinueExpected(request6, true); 256 | client.writeAndFlush(request6); 257 | 258 | assertEquals(helper.extractBody(receiveStreamedResponse()), "request 1"); 259 | assertEquals(helper.extractBody(receiveStreamedResponse()), "request 2"); 260 | assertEquals(helper.extractBody(receiveStreamedResponse()), "request 3"); 261 | assertEquals(helper.extractBody(receiveStreamedResponse()), "request 4"); 262 | assertEquals(helper.extractBody(receiveStreamedResponse()), "request 5"); 263 | assertEquals(helper.extractBody(receiveStreamedResponse()), "request 6"); 264 | } 265 | 266 | @Test 267 | public void closeAHttp11ConnectionWhenRequestedByFullResponse() throws Exception { 268 | start(new AutoReadHandler() { 269 | @Override 270 | public void channelRead(final ChannelHandlerContext ctx, Object msg) throws Exception { 271 | HttpResponse response = helper.createFullResponse(""); 272 | response.headers().set(HttpHeaderNames.CONNECTION, "close"); 273 | ctx.writeAndFlush(response); 274 | } 275 | }); 276 | client.writeAndFlush(new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/")); 277 | 278 | FullHttpResponse response = receiveFullResponse(); 279 | assertEquals(helper.extractBody(response), ""); 280 | 281 | client.closeFuture().await(TIMEOUT_MS, TimeUnit.MILLISECONDS); 282 | assertFalse(client.isOpen()); 283 | } 284 | 285 | @Test 286 | public void closeAHttp11ConnectionWhenRequestedByStreamedResponse() throws Exception { 287 | start(new AutoReadHandler() { 288 | @Override 289 | public void channelRead(final ChannelHandlerContext ctx, Object msg) throws Exception { 290 | HttpResponse response = helper.createStreamedResponse(HttpVersion.HTTP_1_1, Arrays.asList("hello", " ", "world"), 11); 291 | response.headers().set(HttpHeaderNames.CONNECTION, "close"); 292 | ctx.writeAndFlush(response); 293 | } 294 | }); 295 | client.writeAndFlush(new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/")); 296 | 297 | StreamedHttpResponse response = receiveStreamedResponse(); 298 | assertEquals(helper.extractBody(response), "hello world"); 299 | 300 | client.closeFuture().await(TIMEOUT_MS, TimeUnit.MILLISECONDS); 301 | assertFalse(client.isOpen()); 302 | } 303 | 304 | @Test 305 | public void closeAHttp10ConnectionWhenRequestedByFullResponse() throws Exception { 306 | start(new AutoReadHandler() { 307 | @Override 308 | public void channelRead(final ChannelHandlerContext ctx, Object msg) throws Exception { 309 | HttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_0, HttpResponseStatus.OK); 310 | HttpUtil.setContentLength(response, 0); 311 | ctx.writeAndFlush(response); 312 | } 313 | }); 314 | client.writeAndFlush(new DefaultFullHttpRequest(HttpVersion.HTTP_1_0, HttpMethod.GET, "/")); 315 | 316 | FullHttpResponse response = receiveFullResponse(); 317 | assertEquals(helper.extractBody(response), ""); 318 | 319 | client.closeFuture().await(TIMEOUT_MS, TimeUnit.MILLISECONDS); 320 | assertFalse(client.isOpen()); 321 | } 322 | 323 | @Test 324 | public void closeAHttp10ConnectionWhenRequestedByStreamedResponse() throws Exception { 325 | start(new AutoReadHandler() { 326 | @Override 327 | public void channelRead(final ChannelHandlerContext ctx, Object msg) throws Exception { 328 | HttpResponse response = helper.createStreamedResponse(HttpVersion.HTTP_1_0, Arrays.asList("hello", " ", "world"), 11); 329 | ctx.writeAndFlush(response); 330 | } 331 | }); 332 | client.writeAndFlush(new DefaultFullHttpRequest(HttpVersion.HTTP_1_0, HttpMethod.GET, "/")); 333 | 334 | StreamedHttpResponse response = receiveStreamedResponse(); 335 | assertEquals(helper.extractBody(response), "hello world"); 336 | 337 | client.closeFuture().await(TIMEOUT_MS, TimeUnit.MILLISECONDS); 338 | assertFalse(client.isOpen()); 339 | } 340 | 341 | @BeforeClass 342 | public void startEventLoop() { 343 | eventLoop = new NioEventLoopGroup(); 344 | actorSystem = ActorSystem.create(); 345 | materializer = Materializer.matFromSystem(actorSystem); 346 | helper = new HttpHelper(materializer); 347 | } 348 | 349 | @AfterClass 350 | public void stopEventLoop() throws Exception { 351 | clientEvents.clear(); 352 | Await.ready(actorSystem.terminate(), Duration.create(10000, TimeUnit.MILLISECONDS)); 353 | eventLoop.shutdownGracefully(100, 10000, TimeUnit.MILLISECONDS).await(); 354 | } 355 | 356 | @AfterMethod 357 | public void closeChannels() { 358 | if (serverBindChannel != null) { 359 | serverBindChannel.close(); 360 | } 361 | if (client != null) { 362 | client.close(); 363 | } 364 | } 365 | 366 | private void start(final ChannelHandler handler) throws InterruptedException { 367 | ServerBootstrap bootstrap = new ServerBootstrap(); 368 | bootstrap.group(eventLoop) 369 | .channel(NioServerSocketChannel.class) 370 | .childOption(ChannelOption.AUTO_READ, false) 371 | .localAddress("127.0.0.1", 0) 372 | .childHandler(new ChannelInitializer() { 373 | @Override 374 | protected void initChannel(SocketChannel ch) throws Exception { 375 | ChannelPipeline pipeline = ch.pipeline(); 376 | 377 | pipeline.addLast( 378 | new HttpRequestDecoder(), 379 | new HttpResponseEncoder() 380 | ).addLast("serverStreamsHandler", new HttpStreamsServerHandler()) 381 | .addLast(handler); 382 | } 383 | }); 384 | 385 | serverBindChannel = bootstrap.bind().await().channel(); 386 | 387 | Bootstrap client = new Bootstrap() 388 | .group(eventLoop) 389 | .option(ChannelOption.AUTO_READ, false) 390 | .channel(NioSocketChannel.class) 391 | .handler(new ChannelInitializer() { 392 | @Override 393 | protected void initChannel(SocketChannel ch) throws Exception { 394 | final ChannelPipeline pipeline = ch.pipeline(); 395 | 396 | pipeline.addLast(new HttpClientCodec()) 397 | .addLast("clientStreamsHandler", new HttpStreamsClientHandler()) 398 | .addLast(new AutoReadHandler() { 399 | public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { 400 | clientEvents.add(msg); 401 | } 402 | }); 403 | } 404 | }); 405 | 406 | this.client = client.remoteAddress(serverBindChannel.localAddress()).connect().await().channel(); 407 | } 408 | 409 | private StreamedHttpResponse receiveStreamedResponse() throws InterruptedException { 410 | Object msg = clientEvents.poll(TIMEOUT_MS, TimeUnit.MILLISECONDS); 411 | assertNotNull(msg, "Read response timed out"); 412 | if (msg instanceof StreamedHttpResponse) { 413 | return (StreamedHttpResponse) msg; 414 | } else { 415 | throw new AssertionError("Expected StreamedHttpResponse, got " + msg); 416 | } 417 | } 418 | 419 | private FullHttpResponse receiveFullResponse() throws InterruptedException { 420 | Object msg = clientEvents.poll(TIMEOUT_MS, TimeUnit.MILLISECONDS); 421 | assertNotNull(msg); 422 | if (msg instanceof FullHttpResponse) { 423 | return (FullHttpResponse) msg; 424 | } else { 425 | throw new AssertionError("Expected FullHttpResponse, got " + msg); 426 | } 427 | } 428 | 429 | private void createEchoServer() throws InterruptedException { 430 | start(new AutoReadHandler() { 431 | public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { 432 | ctx.writeAndFlush(helper.echo(msg)); 433 | } 434 | }); 435 | } 436 | 437 | private void responseServer(final HttpResponse response) throws InterruptedException { 438 | start(new AutoReadHandler() { 439 | public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { 440 | if (msg instanceof HttpRequest) { 441 | ctx.writeAndFlush(response); 442 | } else { 443 | throw new IllegalArgumentException("Unknown incoming message type: " + msg); 444 | } 445 | } 446 | }); 447 | } 448 | 449 | private class AutoReadHandler extends ChannelInboundHandlerAdapter { 450 | public void channelActive(ChannelHandlerContext ctx) throws Exception { 451 | ctx.read(); 452 | } 453 | 454 | public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { 455 | ctx.read(); 456 | } 457 | } 458 | } 459 | -------------------------------------------------------------------------------- /netty-reactive-streams-http/src/test/java/org/playframework/netty/http/PekkoStreamsUtil.java: -------------------------------------------------------------------------------- 1 | package org.playframework.netty.http; 2 | 3 | import org.apache.pekko.japi.Pair; 4 | import org.apache.pekko.japi.function.Creator; 5 | import org.apache.pekko.stream.Materializer; 6 | import org.apache.pekko.stream.javadsl.*; 7 | import org.reactivestreams.Processor; 8 | import org.reactivestreams.Publisher; 9 | import org.reactivestreams.Subscriber; 10 | 11 | public class PekkoStreamsUtil { 12 | 13 | public static Processor flowToProcessor(Flow flow, Materializer materializer) { 14 | Pair, Publisher> pair = 15 | Source.asSubscriber() 16 | .via(flow) 17 | .toMat(Sink.asPublisher(AsPublisher.WITH_FANOUT), Keep., Publisher>both()) 18 | .run(materializer); 19 | 20 | return new DelegateProcessor<>(pair.first(), pair.second()); 21 | } 22 | 23 | public static Flow processorToFlow(final Processor processor) { 24 | return Flow.fromProcessor(new Creator>() { 25 | @Override 26 | public Processor create() throws Exception { 27 | return processor; 28 | } 29 | }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /netty-reactive-streams-http/src/test/java/org/playframework/netty/http/ProcessorHttpClient.java: -------------------------------------------------------------------------------- 1 | package org.playframework.netty.http; 2 | 3 | import org.playframework.netty.HandlerPublisher; 4 | import org.playframework.netty.HandlerSubscriber; 5 | import io.netty.bootstrap.Bootstrap; 6 | import io.netty.channel.*; 7 | import io.netty.channel.socket.SocketChannel; 8 | import io.netty.channel.socket.nio.NioSocketChannel; 9 | import io.netty.handler.codec.http.*; 10 | import io.netty.util.concurrent.*; 11 | import org.reactivestreams.Processor; 12 | 13 | import java.net.SocketAddress; 14 | 15 | public class ProcessorHttpClient { 16 | 17 | private final EventLoopGroup eventLoop; 18 | 19 | public ProcessorHttpClient(EventLoopGroup eventLoop) { 20 | this.eventLoop = eventLoop; 21 | } 22 | 23 | public Processor connect(SocketAddress address) { 24 | 25 | final EventExecutor executor = eventLoop.next(); 26 | final Promise promise = new DefaultPromise<>(executor); 27 | final HandlerPublisher publisherHandler = new HandlerPublisher<>(executor, 28 | HttpResponse.class); 29 | final HandlerSubscriber subscriberHandler = new HandlerSubscriber( 30 | executor, 2, 4) { 31 | @Override 32 | protected void error(final Throwable error) { 33 | 34 | promise.addListener(new GenericFutureListener>() { 35 | @Override 36 | public void operationComplete(Future future) throws Exception { 37 | ChannelPipeline pipeline = future.getNow(); 38 | pipeline.fireExceptionCaught(error); 39 | pipeline.close(); 40 | } 41 | }); 42 | } 43 | }; 44 | 45 | Bootstrap client = new Bootstrap() 46 | .group(eventLoop) 47 | .option(ChannelOption.AUTO_READ, false) 48 | .channel(NioSocketChannel.class) 49 | .handler(new ChannelInitializer() { 50 | @Override 51 | protected void initChannel(SocketChannel ch) throws Exception { 52 | 53 | final ChannelPipeline pipeline = ch.pipeline(); 54 | 55 | 56 | pipeline 57 | .addLast(new HttpClientCodec()) 58 | .addLast("clientStreamsHandler", new HttpStreamsClientHandler()) 59 | .addLast(executor, "clientPublisher", publisherHandler) 60 | .addLast(executor, "clientSubscriber", subscriberHandler); 61 | 62 | promise.setSuccess(pipeline); 63 | } 64 | }); 65 | 66 | client.remoteAddress(address).connect(); 67 | return new DelegateProcessor<>(subscriberHandler, publisherHandler); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /netty-reactive-streams-http/src/test/java/org/playframework/netty/http/ProcessorHttpServer.java: -------------------------------------------------------------------------------- 1 | package org.playframework.netty.http; 2 | 3 | import org.playframework.netty.HandlerPublisher; 4 | import org.playframework.netty.HandlerSubscriber; 5 | import io.netty.bootstrap.ServerBootstrap; 6 | import io.netty.channel.*; 7 | import io.netty.channel.socket.SocketChannel; 8 | import io.netty.channel.socket.nio.NioServerSocketChannel; 9 | import io.netty.handler.codec.http.HttpRequest; 10 | import io.netty.handler.codec.http.HttpRequestDecoder; 11 | import io.netty.handler.codec.http.HttpResponse; 12 | import io.netty.handler.codec.http.HttpResponseEncoder; 13 | import org.reactivestreams.Processor; 14 | 15 | import java.net.SocketAddress; 16 | import java.util.concurrent.Callable; 17 | 18 | public class ProcessorHttpServer { 19 | 20 | private final EventLoopGroup eventLoop; 21 | 22 | public ProcessorHttpServer(EventLoopGroup eventLoop) { 23 | this.eventLoop = eventLoop; 24 | } 25 | 26 | public ChannelFuture bind(SocketAddress address, final Callable> handler) { 27 | ServerBootstrap bootstrap = new ServerBootstrap(); 28 | bootstrap.group(eventLoop) 29 | .channel(NioServerSocketChannel.class) 30 | .childOption(ChannelOption.AUTO_READ, false) 31 | .localAddress(address) 32 | .childHandler(new ChannelInitializer() { 33 | @Override 34 | protected void initChannel(SocketChannel ch) throws Exception { 35 | ChannelPipeline pipeline = ch.pipeline(); 36 | 37 | pipeline.addLast( 38 | new HttpRequestDecoder(), 39 | new HttpResponseEncoder() 40 | ).addLast("serverStreamsHandler", new HttpStreamsServerHandler()); 41 | 42 | HandlerSubscriber subscriber = new HandlerSubscriber<>(ch.eventLoop(), 2, 4); 43 | HandlerPublisher publisher = new HandlerPublisher<>(ch.eventLoop(), HttpRequest.class); 44 | 45 | pipeline.addLast("serverSubscriber", subscriber); 46 | pipeline.addLast("serverPublisher", publisher); 47 | 48 | Processor processor = handler.call(); 49 | processor.subscribe(subscriber); 50 | publisher.subscribe(processor); 51 | } 52 | }); 53 | 54 | return bootstrap.bind(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /netty-reactive-streams-http/src/test/java/org/playframework/netty/http/WebSocketsTest.java: -------------------------------------------------------------------------------- 1 | package org.playframework.netty.http; 2 | 3 | import org.apache.pekko.actor.ActorSystem; 4 | import org.apache.pekko.japi.function.Function; 5 | import org.apache.pekko.stream.Materializer; 6 | import org.apache.pekko.stream.javadsl.Flow; 7 | import io.netty.bootstrap.Bootstrap; 8 | import io.netty.bootstrap.ServerBootstrap; 9 | import io.netty.buffer.ByteBuf; 10 | import io.netty.buffer.Unpooled; 11 | import io.netty.channel.*; 12 | import io.netty.channel.nio.NioEventLoopGroup; 13 | import io.netty.channel.socket.SocketChannel; 14 | import io.netty.channel.socket.nio.NioServerSocketChannel; 15 | import io.netty.channel.socket.nio.NioSocketChannel; 16 | import io.netty.handler.codec.http.*; 17 | import io.netty.handler.codec.http.websocketx.*; 18 | import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketClientCompressionHandler; 19 | import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketServerCompressionHandler; 20 | import io.netty.util.ReferenceCountUtil; 21 | import org.reactivestreams.Processor; 22 | import org.testng.annotations.AfterClass; 23 | import org.testng.annotations.AfterMethod; 24 | import org.testng.annotations.BeforeClass; 25 | import org.testng.annotations.Test; 26 | 27 | import java.net.InetSocketAddress; 28 | import java.net.URI; 29 | import java.nio.charset.Charset; 30 | import java.util.concurrent.BlockingQueue; 31 | import java.util.concurrent.LinkedBlockingQueue; 32 | import java.util.concurrent.TimeUnit; 33 | 34 | import static org.testng.Assert.*; 35 | 36 | public class WebSocketsTest { 37 | 38 | private NioEventLoopGroup eventLoop; 39 | private ActorSystem actorSystem; 40 | private Materializer materializer; 41 | private Channel serverBindChannel; 42 | private Channel client; 43 | private BlockingQueue clientEvents = new LinkedBlockingQueue<>(); 44 | private int port; 45 | 46 | /** 47 | * Note: withCompression and withoutExtensions will not work as compression requires Extensions. 48 | * @param withCompression Enable Compression for this test 49 | * @param withExtensions Enable WebSocket Extensions on the handshaker 50 | */ 51 | private void simpleWebSocket(final boolean withCompression, final boolean withExtensions) throws Exception { 52 | start(new AutoReadHandler() { 53 | @Override 54 | public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { 55 | if (msg instanceof HttpRequest) { 56 | HttpRequest request = (HttpRequest) msg; 57 | ReferenceCountUtil.release(msg); 58 | 59 | Processor processor = Flow.create().map(new Function() { 60 | public WebSocketFrame apply(WebSocketFrame msg) throws Exception { 61 | if (msg instanceof TextWebSocketFrame) { 62 | TextWebSocketFrame echo = new TextWebSocketFrame("echo " + ((TextWebSocketFrame) msg).text()); 63 | ReferenceCountUtil.release(msg); 64 | return echo; 65 | } else if (msg instanceof PingWebSocketFrame) { 66 | return new PongWebSocketFrame(msg.content()); 67 | } else if (msg instanceof CloseWebSocketFrame) { 68 | return msg; 69 | } else { 70 | throw new IllegalArgumentException("Unexpected websocket frame: " + msg); 71 | } 72 | } 73 | }).toProcessor().run(materializer); 74 | 75 | ctx.writeAndFlush(new DefaultWebSocketHttpResponse(request.protocolVersion(), 76 | HttpResponseStatus.valueOf(200), processor, 77 | new WebSocketServerHandshakerFactory("ws://127.0.0.1/" + port + "/", null, withExtensions) 78 | )); 79 | } 80 | } 81 | }, withCompression); 82 | 83 | makeWebSocketRequest(withCompression, withExtensions); 84 | assertNoMessages(); 85 | client.writeAndFlush(new TextWebSocketFrame("hello")); 86 | assertEquals(readTextFrame(), "echo hello"); 87 | 88 | ByteBuf ping = Unpooled.wrappedBuffer("hello".getBytes()); 89 | client.writeAndFlush(new PingWebSocketFrame(ping)); 90 | Object pong = pollClient(); 91 | assertNotNull(pong); 92 | if (pong instanceof PongWebSocketFrame) { 93 | assertEquals(((PongWebSocketFrame) pong).content().toString(Charset.defaultCharset()), "hello"); 94 | } else { 95 | fail("Expected pong reply but got " + pong); 96 | } 97 | ReferenceCountUtil.release(pong); 98 | 99 | client.writeAndFlush(new CloseWebSocketFrame(1000, "no reason")); 100 | Object close = pollClient(); 101 | assertNotNull(close); 102 | if (close instanceof CloseWebSocketFrame) { 103 | CloseWebSocketFrame cl = (CloseWebSocketFrame) close; 104 | assertEquals(cl.statusCode(), 1000); 105 | assertEquals(cl.reasonText(), "no reason"); 106 | } else { 107 | fail("Expected close but got " + close); 108 | } 109 | ReferenceCountUtil.release(close); 110 | 111 | client.close(); 112 | assertNoMessages(); 113 | } 114 | 115 | @Test 116 | public void simpleWebSocketWithCompressionAndExtensions() throws Exception { 117 | simpleWebSocket(true, true); 118 | } 119 | 120 | @Test 121 | public void simpleWebSocketWithoutCompressionWithoutExtensions() throws Exception { 122 | simpleWebSocket(false, false); 123 | } 124 | 125 | @Test 126 | public void simpleWebSocketWithoutCompressionWithExtensions() throws Exception { 127 | simpleWebSocket(false, true); 128 | } 129 | 130 | @Test 131 | public void rejectWebSocket() throws Exception { 132 | start(new AutoReadHandler() { 133 | @Override 134 | public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { 135 | if (msg instanceof HttpRequest) { 136 | HttpRequest request = (HttpRequest) msg; 137 | ReferenceCountUtil.release(msg); 138 | 139 | Processor processor = Flow.create().toProcessor().run(materializer); 140 | 141 | ctx.writeAndFlush(new DefaultWebSocketHttpResponse(request.protocolVersion(), 142 | HttpResponseStatus.valueOf(200), processor, 143 | new WebSocketServerHandshakerFactory("ws://127.0.0.1/" + port + "/", null, false) 144 | )); 145 | } 146 | } 147 | }); 148 | 149 | FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/"); 150 | HttpHeaders headers = request.headers(); 151 | headers.add(HttpHeaderNames.UPGRADE, HttpHeaderValues.WEBSOCKET.toLowerCase()) 152 | .add(HttpHeaderNames.CONNECTION, HttpHeaderValues.UPGRADE) 153 | .add(HttpHeaderNames.SEC_WEBSOCKET_KEY, "foobar") 154 | .add(HttpHeaderNames.HOST, "http://127.0.0.1:" + port) 155 | .add(HttpHeaderNames.SEC_WEBSOCKET_ORIGIN, "http://127.0.0.1:" + port) 156 | .add(HttpHeaderNames.SEC_WEBSOCKET_VERSION, "1"); 157 | client.writeAndFlush(request); 158 | 159 | FullHttpResponse response = receiveFullResponse(); 160 | assertEquals(response.status(), HttpResponseStatus.UPGRADE_REQUIRED); 161 | assertEquals(response.headers().get(HttpHeaderNames.SEC_WEBSOCKET_VERSION), "13"); 162 | ReferenceCountUtil.release(response); 163 | } 164 | 165 | @BeforeClass 166 | public void startEventLoop() { 167 | eventLoop = new NioEventLoopGroup(); 168 | actorSystem = ActorSystem.create(); 169 | materializer = Materializer.matFromSystem(actorSystem); 170 | } 171 | 172 | @AfterClass 173 | public void stopEventLoop() { 174 | actorSystem.terminate(); 175 | eventLoop.shutdownGracefully(); 176 | } 177 | 178 | @AfterMethod 179 | public void closeChannels() throws InterruptedException { 180 | if (serverBindChannel != null) { 181 | serverBindChannel.close(); 182 | } 183 | if (client != null) { 184 | client.close(); 185 | } 186 | clientEvents = null; 187 | } 188 | 189 | private void start(final ChannelHandler handler) throws InterruptedException { 190 | start(handler, false); 191 | } 192 | 193 | private void start(final ChannelHandler handler, final boolean enableCompression) throws InterruptedException { 194 | ServerBootstrap bootstrap = new ServerBootstrap(); 195 | bootstrap.group(eventLoop) 196 | .channel(NioServerSocketChannel.class) 197 | .childOption(ChannelOption.AUTO_READ, false) 198 | .localAddress("127.0.0.1", 0) 199 | .childHandler(new ChannelInitializer() { 200 | @Override 201 | protected void initChannel(SocketChannel ch) throws Exception { 202 | ChannelPipeline pipeline = ch.pipeline(); 203 | 204 | pipeline.addLast( 205 | new HttpRequestDecoder(), 206 | new HttpResponseEncoder() 207 | ); 208 | 209 | if (enableCompression) { 210 | pipeline.addLast(new WebSocketServerCompressionHandler()); 211 | } 212 | pipeline 213 | .addLast("serverStreamsHandler", new HttpStreamsServerHandler()) 214 | .addLast(handler); 215 | } 216 | }); 217 | 218 | serverBindChannel = bootstrap.bind().await().channel(); 219 | port = ((InetSocketAddress) serverBindChannel.localAddress()).getPort(); 220 | 221 | clientEvents = new LinkedBlockingQueue<>(); 222 | Bootstrap client = new Bootstrap() 223 | .group(eventLoop) 224 | .option(ChannelOption.AUTO_READ, false) 225 | .channel(NioSocketChannel.class) 226 | .handler(new ChannelInitializer() { 227 | @Override 228 | protected void initChannel(SocketChannel ch) throws Exception { 229 | final ChannelPipeline pipeline = ch.pipeline(); 230 | 231 | pipeline.addLast(new HttpClientCodec(), new HttpObjectAggregator(8192)); 232 | 233 | if (enableCompression) pipeline.addLast(WebSocketClientCompressionHandler.INSTANCE); 234 | 235 | pipeline.addLast(new AutoReadHandler() { 236 | // Store a reference to the current client events 237 | BlockingQueue events = clientEvents; 238 | public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { 239 | events.add(msg); 240 | } 241 | }); 242 | } 243 | }); 244 | 245 | this.client = client.remoteAddress(serverBindChannel.localAddress()).connect().await().channel(); 246 | } 247 | 248 | private void makeWebSocketRequest(final boolean withCompression, final boolean withExtensions) throws InterruptedException { 249 | WebSocketClientHandshaker handshaker = WebSocketClientHandshakerFactory.newHandshaker( 250 | URI.create("ws://127.0.0.1:" + port + "/"), 251 | WebSocketVersion.V13, null, withExtensions, new DefaultHttpHeaders()); 252 | handshaker.handshake(client); 253 | FullHttpResponse response = receiveFullResponse(); 254 | HttpHeaders headers = response.headers(); 255 | if (withCompression) { 256 | assertTrue(headers.contains("sec-websocket-extensions")); 257 | assertEquals(headers.get("sec-websocket-extensions"), "permessage-deflate"); 258 | } else { 259 | assertTrue(!headers.contains("sec-websocket-extensions") || 260 | !headers.get("sec-websocket-extensions").contains("permessage-deflate")); 261 | } 262 | handshaker.finishHandshake(client, response); 263 | } 264 | 265 | private FullHttpResponse receiveFullResponse() throws InterruptedException { 266 | Object msg = pollClient(); 267 | assertNotNull(msg); 268 | if (msg instanceof FullHttpResponse) { 269 | return (FullHttpResponse) msg; 270 | } else { 271 | throw new AssertionError("Expected FullHttpResponse, got " + msg); 272 | } 273 | } 274 | 275 | private String readTextFrame() throws InterruptedException { 276 | Object msg = pollClient(); 277 | assertNotNull(msg); 278 | if (msg instanceof TextWebSocketFrame) { 279 | String text = ((TextWebSocketFrame) msg).text(); 280 | ReferenceCountUtil.release(msg); 281 | return text; 282 | } else { 283 | throw new AssertionError("Expected text web socket frame, got " + msg); 284 | } 285 | } 286 | 287 | private void assertNoMessages() throws InterruptedException { 288 | assertNull(pollClient()); 289 | } 290 | 291 | private Object pollClient() throws InterruptedException { 292 | return clientEvents.poll(500, TimeUnit.MILLISECONDS); 293 | } 294 | 295 | private class AutoReadHandler extends ChannelInboundHandlerAdapter { 296 | public void channelActive(ChannelHandlerContext ctx) throws Exception { 297 | ctx.read(); 298 | } 299 | 300 | public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { 301 | ctx.read(); 302 | } 303 | } 304 | 305 | } 306 | -------------------------------------------------------------------------------- /netty-reactive-streams/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | org.playframework.netty 7 | netty-reactive-streams-parent 8 | 3.0.5-SNAPSHOT 9 | 10 | 11 | netty-reactive-streams 12 | 13 | Netty Reactive Streams Implementation 14 | jar 15 | 16 | 17 | 18 | io.netty 19 | netty-handler 20 | 21 | 22 | org.reactivestreams 23 | reactive-streams 24 | 25 | 26 | org.reactivestreams 27 | reactive-streams-tck 28 | 29 | 30 | org.testng 31 | testng 32 | 33 | 34 | 35 | 36 | 37 | 38 | maven-resources-plugin 39 | 3.3.1 40 | 41 | 42 | copy-resources 43 | validate 44 | 45 | copy-resources 46 | 47 | 48 | ${project.build.resources[0].directory}/META-INF 49 | 50 | 51 | ../ 52 | true 53 | 54 | README.md 55 | LICENSE 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | org.apache.maven.plugins 65 | maven-jar-plugin 66 | 67 | 68 | default-jar 69 | package 70 | 71 | jar 72 | 73 | 74 | 75 | 76 | org.playframework.netty.core 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /netty-reactive-streams/src/main/java/org/playframework/netty/CancelledSubscriber.java: -------------------------------------------------------------------------------- 1 | package org.playframework.netty; 2 | 3 | import org.reactivestreams.Subscriber; 4 | import org.reactivestreams.Subscription; 5 | 6 | /** 7 | * A cancelled subscriber. 8 | */ 9 | public final class CancelledSubscriber implements Subscriber { 10 | 11 | @Override 12 | public void onSubscribe(Subscription subscription) { 13 | if (subscription == null) { 14 | throw new NullPointerException("Null subscription"); 15 | } else { 16 | subscription.cancel(); 17 | } 18 | } 19 | 20 | @Override 21 | public void onNext(T t) { 22 | } 23 | 24 | @Override 25 | public void onError(Throwable error) { 26 | if (error == null) { 27 | throw new NullPointerException("Null error published"); 28 | } 29 | } 30 | 31 | @Override 32 | public void onComplete() { 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /netty-reactive-streams/src/main/java/org/playframework/netty/HandlerPublisher.java: -------------------------------------------------------------------------------- 1 | package org.playframework.netty; 2 | 3 | import io.netty.channel.ChannelDuplexHandler; 4 | import io.netty.channel.ChannelHandlerContext; 5 | import io.netty.channel.ChannelInboundHandler; 6 | import io.netty.channel.ChannelPipeline; 7 | import io.netty.util.ReferenceCountUtil; 8 | import io.netty.util.concurrent.EventExecutor; 9 | import io.netty.util.internal.TypeParameterMatcher; 10 | import org.reactivestreams.Publisher; 11 | import org.reactivestreams.Subscriber; 12 | import org.reactivestreams.Subscription; 13 | import static org.playframework.netty.HandlerPublisher.State.*; 14 | 15 | import java.util.*; 16 | import java.util.concurrent.atomic.AtomicBoolean; 17 | 18 | /** 19 | * Publisher for a Netty Handler. 20 | * 21 | * This publisher supports only one subscriber. 22 | * 23 | * All interactions with the subscriber are done from the handlers executor, hence, they provide the same happens before 24 | * semantics that Netty provides. 25 | * 26 | * The handler publishes all messages that match the type as specified by the passed in class. Any non matching messages 27 | * are forwarded to the next handler. 28 | * 29 | * The publisher will signal complete if it receives a channel inactive event. 30 | * 31 | * The publisher will release any messages that it drops (for example, messages that are buffered when the subscriber 32 | * cancels), but other than that, it does not release any messages. It is up to the subscriber to release messages. 33 | * 34 | * If the subscriber cancels, the publisher will send a close event up the channel pipeline. 35 | * 36 | * All errors will short circuit the buffer, and cause publisher to immediately call the subscribers onError method, 37 | * dropping the buffer. 38 | * 39 | * The publisher can be subscribed to or placed in a handler chain in any order. 40 | */ 41 | public class HandlerPublisher extends ChannelDuplexHandler implements Publisher { 42 | 43 | private final EventExecutor executor; 44 | private final TypeParameterMatcher matcher; 45 | 46 | /** 47 | * Create a handler publisher. 48 | * 49 | * The supplied executor must be the same event loop as the event loop that this handler is eventually registered 50 | * with, if not, an exception will be thrown when the handler is registered. 51 | * 52 | * @param executor The executor to execute asynchronous events from the subscriber on. 53 | * @param subscriberMessageType The type of message this publisher accepts. 54 | */ 55 | public HandlerPublisher(EventExecutor executor, Class subscriberMessageType) { 56 | this.executor = executor; 57 | this.matcher = TypeParameterMatcher.get(subscriberMessageType); 58 | } 59 | 60 | /** 61 | * Returns {@code true} if the given message should be handled. If {@code false} it will be passed to the next 62 | * {@link ChannelInboundHandler} in the {@link ChannelPipeline}. 63 | * 64 | * @param msg The message to check. 65 | * @return True if the message should be accepted. 66 | */ 67 | protected boolean acceptInboundMessage(Object msg) throws Exception { 68 | return matcher.match(msg); 69 | } 70 | 71 | /** 72 | * Override to handle when a subscriber cancels the subscription. 73 | * 74 | * By default, this method will simply close the channel. 75 | */ 76 | protected void cancelled() { 77 | ctx.close(); 78 | } 79 | 80 | /** 81 | * Override to intercept when demand is requested. 82 | * 83 | * By default, a channel read is invoked. 84 | */ 85 | protected void requestDemand() { 86 | ctx.read(); 87 | } 88 | 89 | enum State { 90 | /** 91 | * Initial state. There's no subscriber, and no context. 92 | */ 93 | NO_SUBSCRIBER_OR_CONTEXT, 94 | 95 | /** 96 | * A subscriber has been provided, but no context has been provided. 97 | */ 98 | NO_CONTEXT, 99 | 100 | /** 101 | * A context has been provided, but no subscriber has been provided. 102 | */ 103 | NO_SUBSCRIBER, 104 | 105 | /** 106 | * An error has been received, but there's no subscriber to receive it. 107 | */ 108 | NO_SUBSCRIBER_ERROR, 109 | 110 | /** 111 | * There is no demand, and we have nothing buffered. 112 | */ 113 | IDLE, 114 | 115 | /** 116 | * There is no demand, and we're buffering elements. 117 | */ 118 | BUFFERING, 119 | 120 | /** 121 | * We have nothing buffered, but there is demand. 122 | */ 123 | DEMANDING, 124 | 125 | /** 126 | * The stream is complete, however there are still elements buffered for which no demand has come from the subscriber. 127 | */ 128 | DRAINING, 129 | 130 | /** 131 | * We're done, in the terminal state. 132 | */ 133 | DONE 134 | } 135 | 136 | private final Queue buffer = new LinkedList<>(); 137 | 138 | /** 139 | * Whether a subscriber has been provided. This is used to detect whether two subscribers are subscribing 140 | * simultaneously. 141 | */ 142 | private final AtomicBoolean hasSubscriber = new AtomicBoolean(); 143 | 144 | private State state = NO_SUBSCRIBER_OR_CONTEXT; 145 | 146 | private volatile Subscriber subscriber; 147 | private ChannelHandlerContext ctx; 148 | private long outstandingDemand = 0; 149 | private Throwable noSubscriberError; 150 | 151 | @Override 152 | public void subscribe(final Subscriber subscriber) { 153 | if (subscriber == null) { 154 | throw new NullPointerException("Null subscriber"); 155 | } 156 | 157 | if (!hasSubscriber.compareAndSet(false, true)) { 158 | // Must call onSubscribe first. 159 | subscriber.onSubscribe(new Subscription() { 160 | @Override 161 | public void request(long n) { 162 | } 163 | @Override 164 | public void cancel() { 165 | } 166 | }); 167 | subscriber.onError(new IllegalStateException("This publisher only supports one subscriber")); 168 | } else { 169 | executor.execute(new Runnable() { 170 | @Override 171 | public void run() { 172 | provideSubscriber(subscriber); 173 | } 174 | }); 175 | } 176 | } 177 | 178 | private void provideSubscriber(Subscriber subscriber) { 179 | this.subscriber = subscriber; 180 | switch (state) { 181 | case NO_SUBSCRIBER_OR_CONTEXT: 182 | state = NO_CONTEXT; 183 | break; 184 | case NO_SUBSCRIBER: 185 | if (buffer.isEmpty()) { 186 | state = IDLE; 187 | } else { 188 | state = BUFFERING; 189 | } 190 | subscriber.onSubscribe(new ChannelSubscription()); 191 | break; 192 | case DRAINING: 193 | subscriber.onSubscribe(new ChannelSubscription()); 194 | break; 195 | case NO_SUBSCRIBER_ERROR: 196 | cleanup(); 197 | state = DONE; 198 | subscriber.onSubscribe(new ChannelSubscription()); 199 | subscriber.onError(noSubscriberError); 200 | break; 201 | } 202 | } 203 | 204 | @Override 205 | public void handlerAdded(ChannelHandlerContext ctx) throws Exception { 206 | // If the channel is not yet registered, then it's not safe to invoke any methods on it, eg read() or close() 207 | // So don't provide the context until it is registered. 208 | if (ctx.channel().isRegistered()) { 209 | provideChannelContext(ctx); 210 | } 211 | } 212 | 213 | @Override 214 | public void channelRegistered(ChannelHandlerContext ctx) throws Exception { 215 | provideChannelContext(ctx); 216 | ctx.fireChannelRegistered(); 217 | } 218 | 219 | private void provideChannelContext(ChannelHandlerContext ctx) { 220 | switch(state) { 221 | case NO_SUBSCRIBER_OR_CONTEXT: 222 | verifyRegisteredWithRightExecutor(ctx); 223 | this.ctx = ctx; 224 | // It's set, we don't have a subscriber 225 | state = NO_SUBSCRIBER; 226 | break; 227 | case NO_CONTEXT: 228 | verifyRegisteredWithRightExecutor(ctx); 229 | this.ctx = ctx; 230 | state = IDLE; 231 | subscriber.onSubscribe(new ChannelSubscription()); 232 | break; 233 | default: 234 | // Ignore, this could be invoked twice by both handlerAdded and channelRegistered. 235 | } 236 | } 237 | 238 | private void verifyRegisteredWithRightExecutor(ChannelHandlerContext ctx) { 239 | if (!executor.inEventLoop()) { 240 | throw new IllegalArgumentException("Channel handler MUST be registered with the same EventExecutor that it is created with."); 241 | } 242 | } 243 | 244 | @Override 245 | public void channelActive(ChannelHandlerContext ctx) throws Exception { 246 | // If we subscribed before the channel was active, then our read would have been ignored. 247 | if (state == DEMANDING) { 248 | requestDemand(); 249 | } 250 | ctx.fireChannelActive(); 251 | } 252 | 253 | private void receivedDemand(long demand) { 254 | switch (state) { 255 | case BUFFERING: 256 | case DRAINING: 257 | if (addDemand(demand)) { 258 | flushBuffer(); 259 | } 260 | break; 261 | 262 | case DEMANDING: 263 | addDemand(demand); 264 | break; 265 | 266 | case IDLE: 267 | if (addDemand(demand)) { 268 | // Important to change state to demanding before doing a read, in case we get a synchronous 269 | // read back. 270 | state = DEMANDING; 271 | requestDemand(); 272 | } 273 | break; 274 | default: 275 | 276 | } 277 | } 278 | 279 | private boolean addDemand(long demand) { 280 | 281 | if (demand <= 0) { 282 | illegalDemand(); 283 | return false; 284 | } else { 285 | if (outstandingDemand < Long.MAX_VALUE) { 286 | outstandingDemand += demand; 287 | if (outstandingDemand < 0) { 288 | outstandingDemand = Long.MAX_VALUE; 289 | } 290 | } 291 | return true; 292 | } 293 | } 294 | 295 | private void illegalDemand() { 296 | cleanup(); 297 | subscriber.onError(new IllegalArgumentException("Request for 0 or negative elements in violation of Section 3.9 of the Reactive Streams specification")); 298 | ctx.close(); 299 | state = DONE; 300 | } 301 | 302 | private void flushBuffer() { 303 | while (!buffer.isEmpty() && (outstandingDemand > 0 || outstandingDemand == Long.MAX_VALUE)) { 304 | publishMessage(buffer.remove()); 305 | } 306 | if (buffer.isEmpty()) { 307 | if (outstandingDemand > 0) { 308 | if (state == BUFFERING) { 309 | state = DEMANDING; 310 | } // otherwise we're draining 311 | requestDemand(); 312 | } else if (state == BUFFERING) { 313 | state = IDLE; 314 | } 315 | } 316 | } 317 | 318 | private void receivedCancel() { 319 | switch (state) { 320 | case BUFFERING: 321 | case DEMANDING: 322 | case IDLE: 323 | cancelled(); 324 | case DRAINING: 325 | state = DONE; 326 | break; 327 | } 328 | cleanup(); 329 | subscriber = null; 330 | } 331 | 332 | @Override 333 | public void channelRead(ChannelHandlerContext ctx, Object message) throws Exception { 334 | if (acceptInboundMessage(message)) { 335 | switch (state) { 336 | case IDLE: 337 | buffer.add(message); 338 | state = BUFFERING; 339 | break; 340 | case NO_SUBSCRIBER: 341 | case BUFFERING: 342 | buffer.add(message); 343 | break; 344 | case DEMANDING: 345 | publishMessage(message); 346 | break; 347 | case DRAINING: 348 | case DONE: 349 | ReferenceCountUtil.release(message); 350 | break; 351 | case NO_CONTEXT: 352 | case NO_SUBSCRIBER_OR_CONTEXT: 353 | throw new IllegalStateException("Message received before added to the channel context"); 354 | } 355 | } else { 356 | ctx.fireChannelRead(message); 357 | } 358 | } 359 | 360 | private void publishMessage(Object message) { 361 | if (COMPLETE.equals(message)) { 362 | subscriber.onComplete(); 363 | state = DONE; 364 | } else { 365 | @SuppressWarnings("unchecked") 366 | T next = (T) message; 367 | subscriber.onNext(next); 368 | if (outstandingDemand < Long.MAX_VALUE) { 369 | outstandingDemand--; 370 | if (outstandingDemand == 0 && state != DRAINING) { 371 | if (buffer.isEmpty()) { 372 | state = IDLE; 373 | } else { 374 | state = BUFFERING; 375 | } 376 | } 377 | } 378 | } 379 | } 380 | 381 | @Override 382 | public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { 383 | if (state == DEMANDING) { 384 | requestDemand(); 385 | } 386 | } 387 | 388 | @Override 389 | public void channelInactive(ChannelHandlerContext ctx) throws Exception { 390 | complete(); 391 | } 392 | 393 | @Override 394 | public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { 395 | complete(); 396 | } 397 | 398 | private void complete() { 399 | 400 | switch (state) { 401 | case NO_SUBSCRIBER: 402 | case BUFFERING: 403 | buffer.add(COMPLETE); 404 | state = DRAINING; 405 | break; 406 | case DEMANDING: 407 | case IDLE: 408 | if (subscriber != null) { 409 | subscriber.onComplete(); 410 | } 411 | state = DONE; 412 | break; 413 | case NO_SUBSCRIBER_ERROR: 414 | // Ignore, we're already going to complete the stream with an error 415 | // when the subscriber subscribes. 416 | break; 417 | } 418 | } 419 | 420 | @Override 421 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { 422 | switch (state) { 423 | case NO_SUBSCRIBER: 424 | noSubscriberError = cause; 425 | state = NO_SUBSCRIBER_ERROR; 426 | cleanup(); 427 | break; 428 | case BUFFERING: 429 | case DEMANDING: 430 | case IDLE: 431 | case DRAINING: 432 | state = DONE; 433 | cleanup(); 434 | subscriber.onError(cause); 435 | break; 436 | } 437 | } 438 | 439 | /** 440 | * Release all elements from the buffer. 441 | */ 442 | private void cleanup() { 443 | while (!buffer.isEmpty()) { 444 | ReferenceCountUtil.release(buffer.remove()); 445 | } 446 | } 447 | 448 | private class ChannelSubscription implements Subscription { 449 | @Override 450 | public void request(final long demand) { 451 | executor.execute(new Runnable() { 452 | @Override 453 | public void run() { 454 | receivedDemand(demand); 455 | } 456 | }); 457 | } 458 | 459 | @Override 460 | public void cancel() { 461 | executor.execute(new Runnable() { 462 | @Override 463 | public void run() { 464 | receivedCancel(); 465 | } 466 | }); 467 | } 468 | } 469 | 470 | /** 471 | * Used for buffering a completion signal. 472 | */ 473 | private static final Object COMPLETE = new Object() { 474 | @Override 475 | public String toString() { 476 | return "COMPLETE"; 477 | } 478 | }; 479 | } 480 | -------------------------------------------------------------------------------- /netty-reactive-streams/src/main/java/org/playframework/netty/HandlerSubscriber.java: -------------------------------------------------------------------------------- 1 | package org.playframework.netty; 2 | 3 | import io.netty.channel.ChannelDuplexHandler; 4 | import io.netty.channel.ChannelFuture; 5 | import io.netty.channel.ChannelFutureListener; 6 | import io.netty.channel.ChannelHandlerContext; 7 | import io.netty.util.concurrent.EventExecutor; 8 | import org.reactivestreams.Subscriber; 9 | import org.reactivestreams.Subscription; 10 | 11 | import java.util.concurrent.atomic.AtomicBoolean; 12 | 13 | import static org.playframework.netty.HandlerSubscriber.State.*; 14 | 15 | /** 16 | * Subscriber that publishes received messages to the handler pipeline. 17 | */ 18 | public class HandlerSubscriber extends ChannelDuplexHandler implements Subscriber { 19 | 20 | static final long DEFAULT_LOW_WATERMARK = 4; 21 | static final long DEFAULT_HIGH_WATERMARK = 16; 22 | 23 | /** 24 | * Create a new handler subscriber. 25 | * 26 | * The supplied executor must be the same event loop as the event loop that this handler is eventually registered 27 | * with, if not, an exception will be thrown when the handler is registered. 28 | * 29 | * @param executor The executor to execute asynchronous events from the publisher on. 30 | * @param demandLowWatermark The low watermark for demand. When demand drops below this, more will be requested. 31 | * @param demandHighWatermark The high watermark for demand. This is the maximum that will be requested. 32 | */ 33 | public HandlerSubscriber(EventExecutor executor, long demandLowWatermark, long demandHighWatermark) { 34 | this.executor = executor; 35 | this.demandLowWatermark = demandLowWatermark; 36 | this.demandHighWatermark = demandHighWatermark; 37 | } 38 | 39 | /** 40 | * Create a new handler subscriber with the default low and high watermarks. 41 | * 42 | * The supplied executor must be the same event loop as the event loop that this handler is eventually registered 43 | * with, if not, an exception will be thrown when the handler is registered. 44 | * 45 | * @param executor The executor to execute asynchronous events from the publisher on. 46 | * @see #HandlerSubscriber(EventExecutor, long, long) 47 | */ 48 | public HandlerSubscriber(EventExecutor executor) { 49 | this(executor, DEFAULT_LOW_WATERMARK, DEFAULT_HIGH_WATERMARK); 50 | } 51 | 52 | /** 53 | * Override for custom error handling. By default, it closes the channel. 54 | * 55 | * @param error The error to handle. 56 | */ 57 | protected void error(Throwable error) { 58 | doClose(); 59 | } 60 | 61 | /** 62 | * Override for custom completion handling. By default, it closes the channel. 63 | */ 64 | protected void complete() { 65 | doClose(); 66 | } 67 | 68 | private final EventExecutor executor; 69 | private final long demandLowWatermark; 70 | private final long demandHighWatermark; 71 | 72 | enum State { 73 | NO_SUBSCRIPTION_OR_CONTEXT, 74 | NO_SUBSCRIPTION, 75 | NO_CONTEXT, 76 | INACTIVE, 77 | RUNNING, 78 | CANCELLED, 79 | COMPLETE 80 | } 81 | 82 | private final AtomicBoolean hasSubscription = new AtomicBoolean(); 83 | 84 | private volatile Subscription subscription; 85 | private volatile ChannelHandlerContext ctx; 86 | 87 | private State state = NO_SUBSCRIPTION_OR_CONTEXT; 88 | private long outstandingDemand = 0; 89 | private ChannelFuture lastWriteFuture; 90 | 91 | @Override 92 | public void handlerAdded(ChannelHandlerContext ctx) throws Exception { 93 | verifyRegisteredWithRightExecutor(ctx); 94 | 95 | switch (state) { 96 | case NO_SUBSCRIPTION_OR_CONTEXT: 97 | this.ctx = ctx; 98 | // We were in no subscription or context, now we just don't have a subscription. 99 | state = NO_SUBSCRIPTION; 100 | break; 101 | case NO_CONTEXT: 102 | this.ctx = ctx; 103 | // We were in no context, we're now fully initialised 104 | maybeStart(); 105 | break; 106 | case COMPLETE: 107 | // We are complete, close 108 | state = COMPLETE; 109 | ctx.close(); 110 | break; 111 | default: 112 | throw new IllegalStateException("This handler must only be added to a pipeline once " + state); 113 | } 114 | } 115 | 116 | @Override 117 | public void channelRegistered(ChannelHandlerContext ctx) throws Exception { 118 | verifyRegisteredWithRightExecutor(ctx); 119 | ctx.fireChannelRegistered(); 120 | } 121 | 122 | private void verifyRegisteredWithRightExecutor(ChannelHandlerContext ctx) { 123 | if (ctx.channel().isRegistered() && !executor.inEventLoop()) { 124 | throw new IllegalArgumentException("Channel handler MUST be registered with the same EventExecutor that it is created with."); 125 | } 126 | } 127 | 128 | @Override 129 | public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception { 130 | maybeRequestMore(); 131 | ctx.fireChannelWritabilityChanged(); 132 | } 133 | 134 | @Override 135 | public void channelActive(ChannelHandlerContext ctx) throws Exception { 136 | if (state == INACTIVE) { 137 | state = RUNNING; 138 | maybeRequestMore(); 139 | } 140 | ctx.fireChannelActive(); 141 | } 142 | 143 | @Override 144 | public void channelInactive(ChannelHandlerContext ctx) throws Exception { 145 | cancel(); 146 | ctx.fireChannelInactive(); 147 | } 148 | 149 | @Override 150 | public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { 151 | cancel(); 152 | } 153 | 154 | @Override 155 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { 156 | cancel(); 157 | ctx.fireExceptionCaught(cause); 158 | } 159 | 160 | private void cancel() { 161 | switch (state) { 162 | case NO_SUBSCRIPTION: 163 | state = CANCELLED; 164 | break; 165 | case RUNNING: 166 | case INACTIVE: 167 | subscription.cancel(); 168 | state = CANCELLED; 169 | break; 170 | } 171 | } 172 | 173 | @Override 174 | public void onSubscribe(final Subscription subscription) { 175 | if (subscription == null) { 176 | throw new NullPointerException("Null subscription"); 177 | } else if (!hasSubscription.compareAndSet(false, true)) { 178 | subscription.cancel(); 179 | } else { 180 | this.subscription = subscription; 181 | executor.execute(new Runnable() { 182 | @Override 183 | public void run() { 184 | provideSubscription(); 185 | } 186 | }); 187 | } 188 | } 189 | 190 | private void provideSubscription() { 191 | switch (state) { 192 | case NO_SUBSCRIPTION_OR_CONTEXT: 193 | state = NO_CONTEXT; 194 | break; 195 | case NO_SUBSCRIPTION: 196 | maybeStart(); 197 | break; 198 | case CANCELLED: 199 | subscription.cancel(); 200 | break; 201 | } 202 | } 203 | 204 | private void maybeStart() { 205 | if (ctx.channel().isActive()) { 206 | state = RUNNING; 207 | maybeRequestMore(); 208 | } else { 209 | state = INACTIVE; 210 | } 211 | } 212 | 213 | @Override 214 | public void onNext(T t) { 215 | 216 | // Publish straight to the context. 217 | lastWriteFuture = ctx.writeAndFlush(t); 218 | lastWriteFuture.addListener(new ChannelFutureListener() { 219 | @Override 220 | public void operationComplete(ChannelFuture future) throws Exception { 221 | 222 | outstandingDemand--; 223 | maybeRequestMore(); 224 | } 225 | }); 226 | } 227 | 228 | @Override 229 | public void onError(final Throwable error) { 230 | if (error == null) { 231 | throw new NullPointerException("Null error published"); 232 | } 233 | error(error); 234 | } 235 | 236 | @Override 237 | public void onComplete() { 238 | if (lastWriteFuture == null) { 239 | complete(); 240 | } else { 241 | lastWriteFuture.addListener(new ChannelFutureListener() { 242 | @Override 243 | public void operationComplete(ChannelFuture channelFuture) throws Exception { 244 | complete(); 245 | } 246 | }); 247 | } 248 | } 249 | 250 | private void doClose() { 251 | executor.execute(new Runnable() { 252 | @Override 253 | public void run() { 254 | switch (state) { 255 | case NO_SUBSCRIPTION: 256 | case INACTIVE: 257 | case RUNNING: 258 | ctx.close(); 259 | state = COMPLETE; 260 | break; 261 | } 262 | } 263 | }); 264 | } 265 | 266 | private void maybeRequestMore() { 267 | if (outstandingDemand <= demandLowWatermark && ctx.channel().isWritable()) { 268 | long toRequest = demandHighWatermark - outstandingDemand; 269 | 270 | outstandingDemand = demandHighWatermark; 271 | subscription.request(toRequest); 272 | } 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /netty-reactive-streams/src/test/java/org/playframework/netty/BatchedProducer.java: -------------------------------------------------------------------------------- 1 | package org.playframework.netty; 2 | 3 | import io.netty.channel.ChannelHandlerContext; 4 | import io.netty.channel.ChannelOutboundHandlerAdapter; 5 | import io.netty.channel.ChannelPromise; 6 | 7 | import java.util.concurrent.atomic.AtomicLong; 8 | 9 | /** 10 | * A batched producer. 11 | * 12 | * Responds to read requests with batches of elements according to batch size. When eofOn is reached, it closes the 13 | * channel. 14 | */ 15 | public class BatchedProducer extends ChannelOutboundHandlerAdapter { 16 | 17 | protected final long eofOn; 18 | protected final int batchSize; 19 | protected final AtomicLong sequence; 20 | 21 | public BatchedProducer(long eofOn, int batchSize, long sequence) { 22 | this.eofOn = eofOn; 23 | this.batchSize = batchSize; 24 | this.sequence = new AtomicLong(sequence); 25 | } 26 | 27 | private boolean cancelled = false; 28 | 29 | 30 | @Override 31 | public void read(final ChannelHandlerContext ctx) throws Exception { 32 | if (cancelled) { 33 | throw new IllegalStateException("Received demand after being cancelled"); 34 | } 35 | ctx.pipeline().channel().eventLoop().parent().execute(new Runnable() { 36 | @Override 37 | public void run() { 38 | for (int i = 0; i < batchSize && sequence.get() != eofOn; i++) { 39 | ctx.fireChannelRead(sequence.getAndIncrement()); 40 | } 41 | if (eofOn == sequence.get()) { 42 | ctx.fireChannelInactive(); 43 | } else { 44 | ctx.fireChannelReadComplete(); 45 | } 46 | } 47 | }); 48 | } 49 | 50 | @Override 51 | public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception { 52 | if (cancelled) { 53 | throw new IllegalStateException("Cancelled twice"); 54 | } 55 | cancelled = true; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /netty-reactive-streams/src/test/java/org/playframework/netty/ChannelPublisherTest.java: -------------------------------------------------------------------------------- 1 | package org.playframework.netty; 2 | 3 | import io.netty.bootstrap.Bootstrap; 4 | import io.netty.channel.*; 5 | import io.netty.channel.nio.NioEventLoopGroup; 6 | import io.netty.channel.socket.nio.NioServerSocketChannel; 7 | import io.netty.util.concurrent.DefaultPromise; 8 | import io.netty.util.concurrent.Promise; 9 | import org.reactivestreams.Publisher; 10 | import org.reactivestreams.Subscriber; 11 | import org.reactivestreams.Subscription; 12 | import org.testng.annotations.AfterMethod; 13 | import org.testng.annotations.BeforeMethod; 14 | import org.testng.annotations.Test; 15 | 16 | import java.io.InputStream; 17 | import java.io.OutputStream; 18 | import java.net.InetSocketAddress; 19 | import java.net.Socket; 20 | import java.util.concurrent.BlockingQueue; 21 | import java.util.concurrent.LinkedBlockingQueue; 22 | import java.util.concurrent.TimeUnit; 23 | 24 | import static org.testng.Assert.assertEquals; 25 | import static org.testng.Assert.assertNotNull; 26 | import static org.testng.Assert.assertNull; 27 | 28 | public class ChannelPublisherTest { 29 | 30 | private EventLoopGroup group; 31 | private Channel channel; 32 | private Publisher publisher; 33 | private SubscriberProbe subscriber; 34 | 35 | @BeforeMethod 36 | public void start() throws Exception { 37 | group = new NioEventLoopGroup(); 38 | EventLoop eventLoop = group.next(); 39 | 40 | HandlerPublisher handlerPublisher = new HandlerPublisher<>(eventLoop, Channel.class); 41 | Bootstrap bootstrap = new Bootstrap(); 42 | 43 | bootstrap 44 | .channel(NioServerSocketChannel.class) 45 | .group(eventLoop) 46 | .option(ChannelOption.AUTO_READ, false) 47 | .handler(handlerPublisher) 48 | .localAddress("127.0.0.1", 0); 49 | 50 | channel = bootstrap.bind().await().channel(); 51 | this.publisher = handlerPublisher; 52 | 53 | subscriber = new SubscriberProbe<>(); 54 | } 55 | 56 | @AfterMethod 57 | public void stop() throws Exception { 58 | channel.unsafe().closeForcibly(); 59 | group.shutdownGracefully(); 60 | } 61 | 62 | @Test 63 | public void test() throws Exception { 64 | publisher.subscribe(subscriber); 65 | Subscription sub = subscriber.takeSubscription(); 66 | 67 | // Try one cycle 68 | sub.request(1); 69 | Socket socket1 = connect(); 70 | receiveConnection(); 71 | readWriteData(socket1, 1); 72 | 73 | // Check back pressure 74 | Socket socket2 = connect(); 75 | subscriber.expectNoElements(); 76 | 77 | // Now request the next connection 78 | sub.request(1); 79 | receiveConnection(); 80 | readWriteData(socket2, 2); 81 | 82 | // Close the channel 83 | channel.close(); 84 | subscriber.expectNoElements(); 85 | subscriber.expectComplete(); 86 | } 87 | 88 | private Socket connect() throws Exception { 89 | InetSocketAddress address = (InetSocketAddress) channel.localAddress(); 90 | return new Socket(address.getAddress(), address.getPort()); 91 | } 92 | 93 | private void readWriteData(Socket socket, int data) throws Exception { 94 | OutputStream os = socket.getOutputStream(); 95 | os.write(data); 96 | os.flush(); 97 | InputStream is = socket.getInputStream(); 98 | int received = is.read(); 99 | socket.close(); 100 | assertEquals(received, data); 101 | } 102 | 103 | private void receiveConnection() throws Exception { 104 | Channel channel = subscriber.take(); 105 | channel.pipeline().addLast(new ChannelInboundHandlerAdapter() { 106 | public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { 107 | ctx.writeAndFlush(msg); 108 | } 109 | }); 110 | group.register(channel); 111 | } 112 | 113 | private class SubscriberProbe implements Subscriber { 114 | final BlockingQueue subscriptions = new LinkedBlockingQueue<>(); 115 | final BlockingQueue elements = new LinkedBlockingQueue<>(); 116 | final Promise promise = new DefaultPromise<>(group.next()); 117 | 118 | public void onSubscribe(Subscription s) { 119 | subscriptions.add(s); 120 | } 121 | 122 | public void onNext(T t) { 123 | elements.add(t); 124 | } 125 | 126 | public void onError(Throwable t) { 127 | promise.setFailure(t); 128 | } 129 | 130 | public void onComplete() { 131 | promise.setSuccess(null); 132 | } 133 | 134 | Subscription takeSubscription() throws Exception { 135 | Subscription sub = subscriptions.poll(100, TimeUnit.MILLISECONDS); 136 | assertNotNull(sub); 137 | return sub; 138 | } 139 | 140 | T take() throws Exception { 141 | T t = elements.poll(1000, TimeUnit.MILLISECONDS); 142 | assertNotNull(t); 143 | return t; 144 | } 145 | 146 | void expectNoElements() throws Exception { 147 | T t = elements.poll(100, TimeUnit.MILLISECONDS); 148 | assertNull(t); 149 | } 150 | 151 | void expectComplete() throws Exception { 152 | promise.get(100, TimeUnit.MILLISECONDS); 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /netty-reactive-streams/src/test/java/org/playframework/netty/ClosedLoopChannel.java: -------------------------------------------------------------------------------- 1 | package org.playframework.netty; 2 | 3 | import io.netty.channel.*; 4 | 5 | import java.net.SocketAddress; 6 | 7 | /** 8 | * A closed loop channel that sends no events and receives no events, for testing purposes. 9 | * 10 | * Any outgoing events that reach the channel will throw an exception. All events should be caught 11 | * be inserting a handler that catches them and responds accordingly. 12 | */ 13 | public class ClosedLoopChannel extends AbstractChannel { 14 | 15 | private final ChannelConfig config = new DefaultChannelConfig(this); 16 | private static final ChannelMetadata metadata = new ChannelMetadata(false); 17 | 18 | private volatile boolean open = true; 19 | private volatile boolean active = true; 20 | 21 | public ClosedLoopChannel() { 22 | super(null); 23 | } 24 | 25 | public void setOpen(boolean open) { 26 | this.open = open; 27 | } 28 | 29 | public void setActive(boolean active) { 30 | this.active = active; 31 | } 32 | 33 | @Override 34 | protected AbstractUnsafe newUnsafe() { 35 | return new AbstractUnsafe() { 36 | @Override 37 | public void connect(SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) { 38 | throw new UnsupportedOperationException(); 39 | } 40 | }; 41 | } 42 | 43 | @Override 44 | protected boolean isCompatible(EventLoop loop) { 45 | return true; 46 | } 47 | 48 | @Override 49 | protected SocketAddress localAddress0() { 50 | throw new UnsupportedOperationException(); 51 | } 52 | 53 | @Override 54 | protected SocketAddress remoteAddress0() { 55 | throw new UnsupportedOperationException(); 56 | } 57 | 58 | @Override 59 | protected void doBind(SocketAddress localAddress) throws Exception { 60 | throw new UnsupportedOperationException(); 61 | } 62 | 63 | @Override 64 | protected void doDisconnect() throws Exception { 65 | throw new UnsupportedOperationException(); 66 | } 67 | 68 | @Override 69 | protected void doClose() throws Exception { 70 | this.open = false; 71 | } 72 | 73 | @Override 74 | protected void doBeginRead() throws Exception { 75 | throw new UnsupportedOperationException(); 76 | } 77 | 78 | @Override 79 | protected void doWrite(ChannelOutboundBuffer in) throws Exception { 80 | throw new UnsupportedOperationException(); 81 | } 82 | 83 | @Override 84 | public ChannelConfig config() { 85 | return config; 86 | } 87 | 88 | @Override 89 | public boolean isOpen() { 90 | return open; 91 | } 92 | 93 | @Override 94 | public boolean isActive() { 95 | return active; 96 | } 97 | 98 | @Override 99 | public ChannelMetadata metadata() { 100 | return metadata; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /netty-reactive-streams/src/test/java/org/playframework/netty/HandlerPublisherVerificationTest.java: -------------------------------------------------------------------------------- 1 | package org.playframework.netty; 2 | 3 | import io.netty.channel.ChannelFuture; 4 | import io.netty.channel.ChannelFutureListener; 5 | import io.netty.channel.DefaultEventLoopGroup; 6 | import io.netty.channel.local.LocalChannel; 7 | import org.reactivestreams.Publisher; 8 | import org.reactivestreams.tck.PublisherVerification; 9 | import org.reactivestreams.tck.TestEnvironment; 10 | import org.testng.annotations.*; 11 | 12 | import java.util.ArrayList; 13 | import java.util.Arrays; 14 | import java.util.List; 15 | import java.util.concurrent.Executors; 16 | import java.util.concurrent.ScheduledExecutorService; 17 | 18 | public class HandlerPublisherVerificationTest extends PublisherVerification { 19 | 20 | private final int batchSize; 21 | // The number of elements to publish initially, before the subscriber is received 22 | private final int publishInitial; 23 | // Whether we should use scheduled publishing (with a small delay) 24 | private final boolean scheduled; 25 | 26 | private ScheduledExecutorService executor; 27 | private DefaultEventLoopGroup eventLoop; 28 | 29 | // For debugging, change the data provider to simple, and adjust the parameters below 30 | @Factory(dataProvider = "noScheduled") 31 | public HandlerPublisherVerificationTest(int batchSize, int publishInitial, boolean scheduled) { 32 | super(new TestEnvironment(200)); 33 | this.batchSize = batchSize; 34 | this.publishInitial = publishInitial; 35 | this.scheduled = scheduled; 36 | } 37 | 38 | @DataProvider 39 | public static Object[][] simple() { 40 | boolean scheduled = false; 41 | int batchSize = 2; 42 | int publishInitial = 0; 43 | return new Object[][] { 44 | new Object[] {batchSize, publishInitial, scheduled} 45 | }; 46 | } 47 | 48 | @DataProvider 49 | public static Object[][] full() { 50 | List data = new ArrayList<>(); 51 | for (Boolean scheduled : Arrays.asList(false, true)) { 52 | for (int batchSize : Arrays.asList(1, 3)) { 53 | for (int publishInitial : Arrays.asList(0, 3)) { 54 | data.add(new Object[]{batchSize, publishInitial, scheduled}); 55 | } 56 | } 57 | } 58 | return data.toArray(new Object[][]{}); 59 | } 60 | 61 | @DataProvider 62 | public static Object[][] noScheduled() { 63 | List data = new ArrayList<>(); 64 | for (int batchSize : Arrays.asList(1, 3)) { 65 | for (int publishInitial : Arrays.asList(0, 3)) { 66 | data.add(new Object[]{batchSize, publishInitial, false}); 67 | } 68 | } 69 | return data.toArray(new Object[][]{}); 70 | } 71 | 72 | // I tried making this before/after class, but encountered a strange error where after 32 publishers were created, 73 | // the following tests complained about the executor being shut down when I registered the channel. Though, it 74 | // doesn't happen if you create 32 publishers in a single test. 75 | @BeforeMethod 76 | public void startEventLoop() { 77 | eventLoop = new DefaultEventLoopGroup(); 78 | } 79 | 80 | @AfterMethod 81 | public void stopEventLoop() { 82 | eventLoop.shutdownGracefully(); 83 | eventLoop = null; 84 | } 85 | 86 | @BeforeClass 87 | public void startExecutor() { 88 | if (scheduled) { 89 | executor = Executors.newSingleThreadScheduledExecutor(); 90 | } 91 | } 92 | 93 | @AfterClass 94 | public void stopExecutor() { 95 | if (scheduled) { 96 | executor.shutdown(); 97 | } 98 | } 99 | 100 | @Override 101 | public Publisher createPublisher(final long elements) { 102 | final BatchedProducer out; 103 | if (scheduled) { 104 | out = new ScheduledBatchedProducer(elements, batchSize, publishInitial, executor, 5); 105 | } else { 106 | out = new BatchedProducer(elements, batchSize, publishInitial); 107 | } 108 | 109 | final ClosedLoopChannel channel = new ClosedLoopChannel(); 110 | channel.config().setAutoRead(false); 111 | ChannelFuture registered = eventLoop.register(channel); 112 | 113 | final HandlerPublisher publisher = new HandlerPublisher<>(registered.channel().eventLoop(), Long.class); 114 | 115 | registered.addListener(new ChannelFutureListener() { 116 | @Override 117 | public void operationComplete(ChannelFuture future) throws Exception { 118 | channel.pipeline().addLast("out", out); 119 | channel.pipeline().addLast("publisher", publisher); 120 | 121 | for (long i = 0; i < publishInitial && i < elements; i++) { 122 | channel.pipeline().fireChannelRead(i); 123 | } 124 | if (elements <= publishInitial) { 125 | channel.pipeline().fireChannelInactive(); 126 | } 127 | } 128 | }); 129 | 130 | return publisher; 131 | } 132 | 133 | @Override 134 | public Publisher createFailedPublisher() { 135 | LocalChannel channel = new LocalChannel(); 136 | eventLoop.register(channel); 137 | HandlerPublisher publisher = new HandlerPublisher<>(channel.eventLoop(), Long.class); 138 | channel.pipeline().addLast("publisher", publisher); 139 | channel.pipeline().fireExceptionCaught(new RuntimeException("failed")); 140 | 141 | return publisher; 142 | } 143 | 144 | @Override 145 | public void stochastic_spec103_mustSignalOnMethodsSequentially() throws Throwable { 146 | try { 147 | super.stochastic_spec103_mustSignalOnMethodsSequentially(); 148 | } catch (Throwable t) { 149 | // CI is failing here, but maven doesn't tell us which parameters failed 150 | System.out.println("Stochastic test failed with parameters batchSize=" + batchSize + 151 | " publishInitial=" + publishInitial + " scheduled=" + scheduled); 152 | throw t; 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /netty-reactive-streams/src/test/java/org/playframework/netty/HandlerSubscriberBlackboxVerificationTest.java: -------------------------------------------------------------------------------- 1 | package org.playframework.netty; 2 | 3 | import io.netty.channel.ChannelDuplexHandler; 4 | import io.netty.channel.ChannelHandler; 5 | import io.netty.channel.embedded.EmbeddedChannel; 6 | import org.reactivestreams.Subscriber; 7 | import org.reactivestreams.Subscription; 8 | import org.reactivestreams.tck.SubscriberBlackboxVerification; 9 | import org.reactivestreams.tck.TestEnvironment; 10 | 11 | public class HandlerSubscriberBlackboxVerificationTest extends SubscriberBlackboxVerification { 12 | 13 | public HandlerSubscriberBlackboxVerificationTest() { 14 | super(new TestEnvironment()); 15 | } 16 | 17 | @Override 18 | public Subscriber createSubscriber() { 19 | // Embedded channel requires at least one handler when it's created, but HandlerSubscriber 20 | // needs the channels event loop in order to be created, so start with a dummy, then replace. 21 | ChannelHandler dummy = new ChannelDuplexHandler(); 22 | EmbeddedChannel channel = new EmbeddedChannel(dummy); 23 | HandlerSubscriber subscriber = new HandlerSubscriber<>(channel.eventLoop(), 2, 4); 24 | channel.pipeline().replace(dummy, "subscriber", subscriber); 25 | 26 | return new SubscriberWithChannel<>(channel, subscriber); 27 | } 28 | 29 | @Override 30 | public Long createElement(int element) { 31 | return (long) element; 32 | } 33 | 34 | @Override 35 | public void triggerRequest(Subscriber subscriber) { 36 | EmbeddedChannel channel = ((SubscriberWithChannel) subscriber).channel; 37 | 38 | channel.runPendingTasks(); 39 | while (channel.readOutbound() != null) { 40 | channel.runPendingTasks(); 41 | } 42 | channel.runPendingTasks(); 43 | } 44 | 45 | /** 46 | * Delegate subscriber that makes the embedded channel available so we can talk to it to trigger a request. 47 | */ 48 | private static class SubscriberWithChannel implements Subscriber { 49 | final EmbeddedChannel channel; 50 | final HandlerSubscriber subscriber; 51 | 52 | public SubscriberWithChannel(EmbeddedChannel channel, HandlerSubscriber subscriber) { 53 | this.channel = channel; 54 | this.subscriber = subscriber; 55 | } 56 | 57 | public void onSubscribe(Subscription s) { 58 | subscriber.onSubscribe(s); 59 | } 60 | 61 | public void onNext(T t) { 62 | subscriber.onNext(t); 63 | } 64 | 65 | public void onError(Throwable t) { 66 | subscriber.onError(t); 67 | } 68 | 69 | public void onComplete() { 70 | subscriber.onComplete(); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /netty-reactive-streams/src/test/java/org/playframework/netty/HandlerSubscriberWhiteboxVerificationTest.java: -------------------------------------------------------------------------------- 1 | package org.playframework.netty; 2 | 3 | import io.netty.channel.*; 4 | import io.netty.util.concurrent.DefaultPromise; 5 | import io.netty.util.concurrent.Promise; 6 | import org.reactivestreams.Subscriber; 7 | import org.reactivestreams.tck.SubscriberWhiteboxVerification; 8 | import org.reactivestreams.tck.TestEnvironment; 9 | import org.testng.annotations.AfterMethod; 10 | import org.testng.annotations.BeforeMethod; 11 | 12 | public class HandlerSubscriberWhiteboxVerificationTest extends SubscriberWhiteboxVerification { 13 | 14 | private boolean workAroundIssue277; 15 | 16 | public HandlerSubscriberWhiteboxVerificationTest() { 17 | super(new TestEnvironment()); 18 | } 19 | 20 | private DefaultEventLoopGroup eventLoop; 21 | 22 | // I tried making this before/after class, but encountered a strange error where after 32 publishers were created, 23 | // the following tests complained about the executor being shut down when I registered the channel. Though, it 24 | // doesn't happen if you create 32 publishers in a single test. 25 | @BeforeMethod 26 | public void startEventLoop() { 27 | workAroundIssue277 = false; 28 | eventLoop = new DefaultEventLoopGroup(); 29 | } 30 | 31 | @AfterMethod 32 | public void stopEventLoop() { 33 | eventLoop.shutdownGracefully(); 34 | eventLoop = null; 35 | } 36 | 37 | @Override 38 | public Subscriber createSubscriber(WhiteboxSubscriberProbe probe) { 39 | 40 | 41 | final ClosedLoopChannel channel = new ClosedLoopChannel(); 42 | channel.config().setAutoRead(false); 43 | ChannelFuture registered = eventLoop.register(channel); 44 | 45 | final HandlerSubscriber subscriber = new HandlerSubscriber<>(registered.channel().eventLoop(), 2, 4); 46 | final ProbeHandler probeHandler = new ProbeHandler<>(probe, Long.class); 47 | final Promise handlersInPlace = new DefaultPromise<>(eventLoop.next()); 48 | 49 | registered.addListener(new ChannelFutureListener() { 50 | @Override 51 | public void operationComplete(ChannelFuture future) throws Exception { 52 | channel.pipeline().addLast("probe", probeHandler); 53 | channel.pipeline().addLast("subscriber", subscriber); 54 | handlersInPlace.setSuccess(null); 55 | // Channel needs to be active before the subscriber starts responding to demand 56 | channel.pipeline().fireChannelActive(); 57 | } 58 | }); 59 | 60 | if (workAroundIssue277) { 61 | try { 62 | // Wait for the pipeline to be setup, so we're ready to receive elements even if they aren't requested, 63 | // because https://github.com/reactive-streams/reactive-streams-jvm/issues/277 64 | handlersInPlace.await(); 65 | } catch (InterruptedException e) { 66 | throw new RuntimeException(e); 67 | } 68 | } 69 | 70 | return probeHandler.wrap(subscriber); 71 | } 72 | 73 | @Override 74 | public void required_spec208_mustBePreparedToReceiveOnNextSignalsAfterHavingCalledSubscriptionCancel() throws Throwable { 75 | // See https://github.com/reactive-streams/reactive-streams-jvm/issues/277 76 | workAroundIssue277 = true; 77 | super.required_spec208_mustBePreparedToReceiveOnNextSignalsAfterHavingCalledSubscriptionCancel(); 78 | } 79 | 80 | @Override 81 | public void required_spec308_requestMustRegisterGivenNumberElementsToBeProduced() throws Throwable { 82 | workAroundIssue277 = true; 83 | super.required_spec308_requestMustRegisterGivenNumberElementsToBeProduced(); 84 | } 85 | 86 | @Override 87 | public Long createElement(int element) { 88 | return (long) element; 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /netty-reactive-streams/src/test/java/org/playframework/netty/ProbeHandler.java: -------------------------------------------------------------------------------- 1 | package org.playframework.netty; 2 | 3 | import io.netty.channel.ChannelDuplexHandler; 4 | import io.netty.channel.ChannelHandlerContext; 5 | import io.netty.channel.ChannelPromise; 6 | import org.reactivestreams.Subscriber; 7 | import org.reactivestreams.Subscription; 8 | import org.reactivestreams.tck.SubscriberWhiteboxVerification; 9 | 10 | import java.util.LinkedList; 11 | import java.util.Queue; 12 | import java.util.concurrent.atomic.AtomicInteger; 13 | 14 | public class ProbeHandler extends ChannelDuplexHandler implements SubscriberWhiteboxVerification.SubscriberPuppet { 15 | 16 | private static final int NO_CONTEXT = 0; 17 | private static final int RUN = 1; 18 | private static final int CANCEL = 2; 19 | 20 | private final SubscriberWhiteboxVerification.WhiteboxSubscriberProbe probe; 21 | private final Class clazz; 22 | private final Queue queue = new LinkedList<>(); 23 | private final AtomicInteger state = new AtomicInteger(NO_CONTEXT); 24 | private volatile ChannelHandlerContext ctx; 25 | // Netty doesn't provide a way to send errors out, so we capture whether it was an error or complete here 26 | private volatile Throwable receivedError = null; 27 | 28 | public ProbeHandler(SubscriberWhiteboxVerification.WhiteboxSubscriberProbe probe, Class clazz) { 29 | this.probe = probe; 30 | this.clazz = clazz; 31 | } 32 | 33 | @Override 34 | public void handlerAdded(ChannelHandlerContext ctx) throws Exception { 35 | this.ctx = ctx; 36 | if (!state.compareAndSet(NO_CONTEXT, RUN)) { 37 | ctx.fireChannelInactive(); 38 | } 39 | } 40 | 41 | @Override 42 | public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { 43 | queue.add(new WriteEvent(clazz.cast(msg), promise)); 44 | } 45 | 46 | @Override 47 | public void close(ChannelHandlerContext ctx, ChannelPromise future) throws Exception { 48 | if (receivedError == null) { 49 | probe.registerOnComplete(); 50 | } else { 51 | probe.registerOnError(receivedError); 52 | } 53 | } 54 | 55 | @Override 56 | public void flush(ChannelHandlerContext ctx) throws Exception { 57 | while (!queue.isEmpty()) { 58 | WriteEvent event = queue.remove(); 59 | probe.registerOnNext(event.msg); 60 | event.future.setSuccess(); 61 | } 62 | } 63 | 64 | @Override 65 | public void triggerRequest(long elements) { 66 | // No need, the channel automatically requests 67 | } 68 | 69 | @Override 70 | public void signalCancel() { 71 | if (!state.compareAndSet(NO_CONTEXT, CANCEL)) { 72 | ctx.fireChannelInactive(); 73 | } 74 | } 75 | 76 | private class WriteEvent { 77 | final T msg; 78 | final ChannelPromise future; 79 | 80 | private WriteEvent(T msg, ChannelPromise future) { 81 | this.msg = msg; 82 | this.future = future; 83 | } 84 | } 85 | 86 | public Subscriber wrap(final Subscriber subscriber) { 87 | return new Subscriber() { 88 | public void onSubscribe(Subscription s) { 89 | probe.registerOnSubscribe(ProbeHandler.this); 90 | subscriber.onSubscribe(s); 91 | } 92 | public void onNext(T t) { 93 | subscriber.onNext(t); 94 | } 95 | public void onError(Throwable t) { 96 | receivedError = t; 97 | subscriber.onError(t); 98 | } 99 | public void onComplete() { 100 | subscriber.onComplete(); 101 | } 102 | }; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /netty-reactive-streams/src/test/java/org/playframework/netty/ScheduledBatchedProducer.java: -------------------------------------------------------------------------------- 1 | package org.playframework.netty; 2 | 3 | import io.netty.channel.ChannelHandlerContext; 4 | 5 | import java.util.concurrent.ScheduledExecutorService; 6 | import java.util.concurrent.TimeUnit; 7 | 8 | /** 9 | * A batched producer. 10 | * 11 | * Responds to read requests with batches of elements according to batch size. When eofOn is reached, it closes the 12 | * channel. 13 | */ 14 | public class ScheduledBatchedProducer extends BatchedProducer { 15 | 16 | private final ScheduledExecutorService executor; 17 | private final long delay; 18 | 19 | public ScheduledBatchedProducer(long eofOn, int batchSize, long sequence, ScheduledExecutorService executor, long delay) { 20 | super(eofOn, batchSize, sequence); 21 | this.executor = executor; 22 | this.delay = delay; 23 | } 24 | 25 | protected boolean complete; 26 | 27 | @Override 28 | public void read(final ChannelHandlerContext ctx) throws Exception { 29 | executor.schedule(new Runnable() { 30 | @Override 31 | public void run() { 32 | for (int i = 0; i < batchSize && sequence.get() != eofOn; i++) { 33 | ctx.fireChannelRead(sequence.getAndIncrement()); 34 | } 35 | complete = eofOn == sequence.get(); 36 | executor.schedule(new Runnable() { 37 | @Override 38 | public void run() { 39 | if (complete) { 40 | ctx.fireChannelInactive(); 41 | } else { 42 | ctx.fireChannelReadComplete(); 43 | } 44 | } 45 | }, delay, TimeUnit.MILLISECONDS); 46 | } 47 | }, delay, TimeUnit.MILLISECONDS); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /netty-reactive-streams/src/test/java/org/playframework/netty/probe/Probe.java: -------------------------------------------------------------------------------- 1 | package org.playframework.netty.probe; 2 | 3 | import java.util.Date; 4 | 5 | public class Probe { 6 | 7 | protected final String name; 8 | protected final Long start; 9 | 10 | /** 11 | * Create a new probe and log that it started. 12 | */ 13 | protected Probe(String name) { 14 | this.name = name; 15 | start = System.nanoTime(); 16 | log("Probe created at " + new Date()); 17 | } 18 | 19 | /** 20 | * Create a new probe with the start time from another probe. 21 | */ 22 | protected Probe(String name, long start) { 23 | this.name = name; 24 | this.start = start; 25 | } 26 | 27 | protected void log(String message) { 28 | System.out.println(String.format("%10d %-5s %-15s %s", (System.nanoTime() - start) / 1000, name, Thread.currentThread().getName(), message)); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /netty-reactive-streams/src/test/java/org/playframework/netty/probe/PublisherProbe.java: -------------------------------------------------------------------------------- 1 | package org.playframework.netty.probe; 2 | 3 | import org.reactivestreams.Publisher; 4 | import org.reactivestreams.Subscriber; 5 | 6 | public class PublisherProbe extends Probe implements Publisher { 7 | 8 | private final Publisher publisher; 9 | 10 | public PublisherProbe(Publisher publisher, String name) { 11 | super(name); 12 | this.publisher = publisher; 13 | } 14 | 15 | @Override 16 | public void subscribe(Subscriber s) { 17 | String sName = s == null ? "null" : s.getClass().getName(); 18 | log("invoke subscribe with subscriber " + sName); 19 | publisher.subscribe(new SubscriberProbe<>(s, name, start)); 20 | log("finish subscribe"); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /netty-reactive-streams/src/test/java/org/playframework/netty/probe/SubscriberProbe.java: -------------------------------------------------------------------------------- 1 | package org.playframework.netty.probe; 2 | 3 | import org.reactivestreams.Subscriber; 4 | import org.reactivestreams.Subscription; 5 | 6 | public class SubscriberProbe extends Probe implements Subscriber { 7 | 8 | private final Subscriber subscriber; 9 | 10 | public SubscriberProbe(Subscriber subscriber, String name) { 11 | super(name); 12 | this.subscriber = subscriber; 13 | } 14 | 15 | SubscriberProbe(Subscriber subscriber, String name, long start) { 16 | super(name, start); 17 | this.subscriber = subscriber; 18 | } 19 | 20 | @Override 21 | public void onSubscribe(final Subscription s) { 22 | String sName = s == null ? "null" : s.getClass().getName(); 23 | log("invoke onSubscribe with subscription " + sName); 24 | subscriber.onSubscribe(new Subscription() { 25 | @Override 26 | public void request(long n) { 27 | log("invoke request " + n); 28 | s.request(n); 29 | log("finish request"); 30 | } 31 | 32 | @Override 33 | public void cancel() { 34 | log("invoke cancel"); 35 | s.cancel(); 36 | log("finish cancel"); 37 | } 38 | }); 39 | log("finish onSubscribe"); 40 | } 41 | 42 | @Override 43 | public void onNext(T t) { 44 | log("invoke onNext with message " + t); 45 | subscriber.onNext(t); 46 | log("finish onNext"); 47 | } 48 | 49 | @Override 50 | public void onError(Throwable t) { 51 | String tName = t == null ? "null" : t.getClass().getName(); 52 | log("invoke onError with " + tName); 53 | subscriber.onError(t); 54 | log("finish onError"); 55 | } 56 | 57 | @Override 58 | public void onComplete() { 59 | log("invoke onComplete"); 60 | subscriber.onComplete(); 61 | log("finish onComplete"); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | org.playframework.netty 6 | netty-reactive-streams-parent 7 | 3.0.5-SNAPSHOT 8 | 9 | Netty Reactive Streams Parent POM 10 | Reactive streams implementation for Netty. 11 | 2015 12 | https://github.com/playframework/netty-reactive-streams 13 | 14 | pom 15 | 16 | 17 | GitHub Issues 18 | https://github.com/playframework/netty-reactive-streams/issues 19 | 20 | 21 | 22 | 23 | Apache License, Version 2.0 24 | http://www.apache.org/licenses/LICENSE-2.0.txt 25 | repo 26 | 27 | 28 | 29 | 30 | 31 | playframework 32 | The Play Framework Contributors 33 | https://github.com/playframework 34 | contact@playframework.com 35 | 36 | 37 | 38 | 39 | The Play Framework Project 40 | https://playframework.com 41 | 42 | 43 | 44 | netty-reactive-streams 45 | netty-reactive-streams-http 46 | 47 | 48 | 49 | 50 | 51 | io.netty 52 | netty-handler 53 | ${netty.version} 54 | 55 | 56 | io.netty 57 | netty-codec-http 58 | ${netty.version} 59 | 60 | 61 | org.reactivestreams 62 | reactive-streams 63 | ${reactive-streams.version} 64 | 65 | 66 | org.reactivestreams 67 | reactive-streams-tck 68 | ${reactive-streams.version} 69 | test 70 | 71 | 72 | org.testng 73 | testng 74 | 7.5.1 75 | test 76 | 77 | 78 | org.apache.pekko 79 | pekko-stream_2.12 80 | ${pekko-stream.version} 81 | test 82 | 83 | 84 | 85 | 86 | 87 | 4.2.1.Final 88 | 1.0.4 89 | 1.1.3 90 | 5.1.9 91 | 3.4.2 92 | 93 | 94 | 95 | 96 | 97 | org.apache.maven.plugins 98 | maven-compiler-plugin 99 | 3.14.0 100 | 101 | 8 102 | 8 103 | 104 | 105 | 106 | org.sonatype.plugins 107 | nexus-staging-maven-plugin 108 | 1.7.0 109 | true 110 | 111 | ossrh 112 | https://oss.sonatype.org/ 113 | true 114 | 115 | 116 | 117 | org.apache.maven.plugins 118 | maven-release-plugin 119 | 3.1.1 120 | 121 | true 122 | false 123 | release 124 | deploy 125 | 126 | 127 | 128 | org.apache.maven.plugins 129 | maven-jar-plugin 130 | ${maven-jar-plugin.version} 131 | 132 | 133 | ${project.build.outputDirectory}/META-INF/MANIFEST.MF 134 | 135 | 136 | 137 | 138 | org.apache.felix 139 | maven-bundle-plugin 140 | ${maven-bundle-plugin.version} 141 | 142 | 143 | bundle-manifest 144 | process-classes 145 | 146 | manifest 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | release 157 | 158 | 159 | 160 | org.apache.maven.plugins 161 | maven-source-plugin 162 | 3.3.1 163 | 164 | 165 | attach-sources 166 | 167 | jar-no-fork 168 | 169 | 170 | 171 | 172 | 173 | org.apache.maven.plugins 174 | maven-javadoc-plugin 175 | 3.11.2 176 | 177 | 178 | attach-javadocs 179 | 180 | jar 181 | 182 | 183 | 184 | 185 | 186 | org.apache.maven.plugins 187 | maven-gpg-plugin 188 | 3.2.7 189 | 190 | 191 | sign-artifacts 192 | verify 193 | 194 | sign 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | scm:git:https://github.com/playframework/netty-reactive-streams.git 206 | scm:git:git@github.com:playframework/netty-reactive-streams.git 207 | https://github.com/playframework/netty-reactive-streams 208 | netty-reactive-streams-parent-3.0.3 209 | 210 | 211 | 212 | 213 | ossrh 214 | https://oss.sonatype.org/content/repositories/snapshots 215 | 216 | 217 | 218 | --------------------------------------------------------------------------------