├── .gitignore ├── .travis.yml ├── LICENSE ├── NOTICE.txt ├── README.md ├── pom.xml └── src ├── main └── java │ └── com │ └── spotify │ └── netty4 │ ├── handler │ └── codec │ │ └── zmtp │ │ ├── ZMTP10Protocol.java │ │ ├── ZMTP10WireFormat.java │ │ ├── ZMTP20Protocol.java │ │ ├── ZMTP20WireFormat.java │ │ ├── ZMTPCodec.java │ │ ├── ZMTPConfig.java │ │ ├── ZMTPDecoder.java │ │ ├── ZMTPEncoder.java │ │ ├── ZMTPEstimator.java │ │ ├── ZMTPException.java │ │ ├── ZMTPFramingDecoder.java │ │ ├── ZMTPFramingEncoder.java │ │ ├── ZMTPHandshake.java │ │ ├── ZMTPHandshakeFailure.java │ │ ├── ZMTPHandshakeSuccess.java │ │ ├── ZMTPHandshaker.java │ │ ├── ZMTPIdentityGenerator.java │ │ ├── ZMTPLongIdentityGenerator.java │ │ ├── ZMTPMessage.java │ │ ├── ZMTPMessageDecoder.java │ │ ├── ZMTPMessageEncoder.java │ │ ├── ZMTPParsingException.java │ │ ├── ZMTPProtocol.java │ │ ├── ZMTPProtocols.java │ │ ├── ZMTPSession.java │ │ ├── ZMTPSocketType.java │ │ ├── ZMTPUtils.java │ │ ├── ZMTPVersion.java │ │ ├── ZMTPWireFormat.java │ │ ├── ZMTPWireFormats.java │ │ └── ZMTPWriter.java │ └── util │ └── BatchFlusher.java └── test ├── java └── com │ └── spotify │ └── netty4 │ └── handler │ └── codec │ └── zmtp │ ├── Buffers.java │ ├── CodecBenchmark.java │ ├── EndToEndTest.java │ ├── Fragmenter.java │ ├── FragmenterTest.java │ ├── HandshakeTest.java │ ├── ListenableFutureAdapter.java │ ├── PipelineTester.java │ ├── PipelineTests.java │ ├── ProtocolViolationTests.java │ ├── VerifyingDecoder.java │ ├── ZMQIntegrationTest.java │ ├── ZMTP10WireFormatTest.java │ ├── ZMTPFramingEncoderTest.java │ ├── ZMTPMessageDecoderTest.java │ ├── ZMTPMessageTest.java │ ├── ZMTPParserTest.java │ ├── ZMTPSocket.java │ ├── ZMTPWriterTest.java │ └── benchmarks │ ├── AsciiString.java │ ├── CustomReqRepBenchmark.java │ ├── ProgressMeter.java │ ├── ReqRepBenchmark.java │ └── ThroughputBenchmark.java └── resources └── logback.xml /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .idea 3 | netty-zmtp.iml 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | 3 | jdk: 4 | - oraclejdk8 5 | - oraclejdk7 6 | - openjdk6 7 | - openjdk7 8 | 9 | sudo: false 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2012 Spotify AB 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | Netty ZMTP 2 | Copyright (c) 2012-2014 Spotify AB 3 | 4 | This product includes software developed at Spotify AB. 5 | http://www.spotify.com/ 6 | 7 | This product includes modified software from and depends on 'Netty', a network 8 | communication framework in Java, which can be obtained at: http://netty.io/ 9 | 10 | This product depends on 'JeroMQ', a communications framework, which can be 11 | obtained at: https://github.com/zeromq/jeromq 12 | 13 | This product depends on 'Junit', a testing framework for Java, which can be 14 | obtained at: http://junit.org/ 15 | 16 | This product depends on 'mockito', a mocking framework for Java, which can be 17 | obtained at: https://code.google.com/p/mockito/ 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Netty-zmtp 2 | 3 | **Note** This project has been discontinued. 4 | 5 | [![Build Status](https://travis-ci.org/spotify/netty-zmtp.png?branch=master)](https://travis-ci.org/spotify/netty-zmtp) 6 | 7 | This is a ZeroMQ codec for Netty that aims to implement ZMTP, the ZeroMQ 8 | Message Transport Protocol versions 1.0 and 2.0 as specified in 9 | http://rfc.zeromq.org/spec:13 and http://rfc.zeromq.org/spec:15. 10 | 11 | This project is hosted on https://github.com/spotify/netty-zmtp/ 12 | 13 | At Spotify we use ZeroMQ for a lot of the internal communication between 14 | services in the backend. As we implement more services on top of the JVM we 15 | felt the need for more control over the state of TCP connections, routing, 16 | message queue management, etc as well as getting better performance than seems 17 | to be currently possible with the JNI based JZMQ library. 18 | 19 | This project implements the ZMTP wire protocol but not the ZeroMQ API, meaning 20 | that it can be used to communicate with other peers using e.g. ZeroMQ (libzmq) 21 | but it's not a drop-in replacement for JZMQ like e.g. JeroMQ attempts to be. 22 | For an example of how a ZeroMQ socket equivalent might be implemented using 23 | the netty-zmtp codecs, see the `ZMTPSocket` class in the tests. 24 | 25 | We have successfully used these handlers to implement services capable of 26 | processing millions of messages per second. 27 | 28 | Currently this project targets Java 6+ and Netty 4.x. It does not have any 29 | native dependency on e.g. libzmq. 30 | 31 | ## Usage 32 | 33 | To use netty-zmtp, insert a `ZMTPCodec` instance into your channel pipeline. 34 | 35 | ```java 36 | ch.pipeline().addLast(ZMTPCodec.of(ROUTER)); 37 | ``` 38 | 39 | Upstream handlers will receive `ZMTPMessage` instances. 40 | 41 | ```java 42 | @Override 43 | public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { 44 | final ZMTPMessage message = (ZMTPMessage) msg; 45 | // ... 46 | } 47 | ``` 48 | 49 | Wait for the ZMTP handshake to complete before sending messages. 50 | 51 | ```java 52 | @Override 53 | public void userEventTriggered(final ChannelHandlerContext ctx, final Object evt) 54 | throws Exception { 55 | if (evt instanceof ZMTPHandshakeSuccess) { 56 | // ... 57 | } 58 | } 59 | ``` 60 | 61 | ### `pom.xml` 62 | 63 | ```xml 64 | 65 | com.spotify 66 | netty4-zmtp 67 | 0.4.1 68 | 69 | ``` 70 | 71 | ## Performance 72 | 73 | Performance seems decent, with the throughput benchmark producing 7M+ messages/s throughput numbers 74 | on a recent laptop. 75 | 76 | For maximum throughput, look into using the `BatchFlusher` to opportunistically gather writes into 77 | fewer syscalls. 78 | 79 | Truly overhead conscientious users might want to look into implementing the `ZMTPEncoder` and 80 | `ZMTPDecoder` interfaces for eliminating the `ZMTPMessage` intermediary when reading/writing 81 | application messages. 82 | 83 | ## Benchmarks 84 | 85 | ### Preparation 86 | 87 | First compile and run tests: 88 | 89 | ``` 90 | mvn clean test 91 | ``` 92 | 93 | Fetch dependencies: 94 | 95 | ``` 96 | mvn dependency:copy-dependencies 97 | ``` 98 | 99 | Now benchmarks are ready to run. 100 | 101 | ### One-Way Throughput 102 | 103 | ``` 104 | java -cp 'target/classes:target/test-classes:target/dependency/*' \ 105 | com.spotify.netty4.handler.codec.zmtp.benchmarks.ThroughputBenchmark 106 | ``` 107 | 108 | ``` 109 | 1s: 3,393,219 messages/s. (total: 3,399,036) 110 | 2s: 6,595,711 messages/s. (total: 10,007,250) 111 | 3s: 7,158,176 messages/s. (total: 17,154,830) 112 | 4s: 7,313,554 messages/s. (total: 24,461,521) 113 | 5s: 7,294,709 messages/s. (total: 31,790,260) 114 | 6s: 7,282,308 messages/s. (total: 39,064,152) 115 | 7s: 7,294,591 messages/s. (total: 46,336,682) 116 | ``` 117 | 118 | 119 | ### Req/Rep Throughput 120 | 121 | 122 | ``` 123 | java -cp 'target/classes:target/test-classes:target/dependency/*' \ 124 | com.spotify.netty4.handler.codec.zmtp.benchmarks.ReqRepBenchmark 125 | ``` 126 | 127 | ``` 128 | 1s: 444,848 requests/s. (total: 446,249) 129 | 2s: 1,251,304 requests/s. (total: 1,699,916) 130 | 3s: 1,241,569 requests/s. (total: 2,941,709) 131 | 4s: 1,365,408 requests/s. (total: 4,307,949) 132 | 5s: 1,379,640 requests/s. (total: 5,681,522) 133 | 6s: 1,379,048 requests/s. (total: 7,064,183) 134 | 7s: 1,377,180 requests/s. (total: 8,438,673) 135 | ``` 136 | 137 | ### Req/Rep With Custom Encoder/Decoder Throughput 138 | 139 | ``` 140 | java -cp 'target/classes:target/test-classes:target/dependency/*' \ 141 | com.spotify.netty4.handler.codec.zmtp.benchmarks.CustomReqRepBenchmark 142 | ``` 143 | 144 | ``` 145 | 1s: 443,337 requests/s. 1.470 ms avg latency. (total: 445,512) 146 | 2s: 1,306,539 requests/s. 0.765 ms avg latency. (total: 1,747,153) 147 | 3s: 1,549,594 requests/s. 0.645 ms avg latency. (total: 3,303,268) 148 | 4s: 1,557,397 requests/s. 0.642 ms avg latency. (total: 4,859,727) 149 | 5s: 1,618,137 requests/s. 0.618 ms avg latency. (total: 6,472,114) 150 | 6s: 1,609,406 requests/s. 0.621 ms avg latency. (total: 8,084,958) 151 | 7s: 1,611,349 requests/s. 0.621 ms avg latency. (total: 9,692,777) 152 | 8s: 1,611,672 requests/s. 0.620 ms avg latency. (total: 11,306,988) 153 | ``` 154 | 155 | ## Feedback 156 | 157 | There is an open Google group for general development and usage discussion 158 | available at https://groups.google.com/group/netty-zmtp 159 | 160 | We use the github issue tracker at https://github.com/spotify/netty-zmtp/issues 161 | 162 | ## License 163 | 164 | This software is licensed using the Apache 2.0 license. Details in the file 165 | LICENSE 166 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | com.spotify 5 | netty4-zmtp 6 | 0.4.2-SNAPSHOT 7 | jar 8 | 9 | netty-zmtp 10 | ZMTP, the ZeroMQ Message Transport Protocol for Netty 11 | https://github.com/spotify/netty-zmtp 12 | 13 | 14 | org.sonatype.oss 15 | oss-parent 16 | 9 17 | 18 | 19 | 20 | 21 | The Apache Software License, Version 2.0 22 | http://www.apache.org/licenses/LICENSE-2.0.txt 23 | repo 24 | 25 | 26 | 27 | 28 | scm:git:https://github.com/spotify/netty-zmtp 29 | scm:git:git@github.com:spotify/netty-zmtp 30 | https://github.com/spotify/netty-zmtp 31 | HEAD 32 | 33 | 34 | 35 | UTF-8 36 | 1.6 37 | 1.6 38 | 39 | 40 | 41 | 42 | dano 43 | dano@spotify.com 44 | Daniel Norberg 45 | 46 | 47 | 48 | 49 | 50 | io.netty 51 | netty-all 52 | 4.0.34.Final 53 | 54 | provided 55 | 56 | 57 | com.google.code.findbugs 58 | jsr305 59 | 3.0.0 60 | provided 61 | 62 | 63 | 64 | 65 | org.zeromq 66 | jeromq 67 | 0.3.4 68 | test 69 | 70 | 71 | junit 72 | junit 73 | 4.12 74 | test 75 | 76 | 77 | org.mockito 78 | mockito-all 79 | 1.10.19 80 | test 81 | 82 | 83 | org.openjdk.jmh 84 | jmh-core 85 | 1.9.3 86 | test 87 | 88 | 89 | org.openjdk.jmh 90 | jmh-generator-annprocess 91 | 1.9.3 92 | test 93 | 94 | 95 | org.openjdk.jmh 96 | jmh-generator-reflection 97 | 1.9.3 98 | test 99 | 100 | 101 | com.google.guava 102 | guava 103 | 18.0 104 | test 105 | 106 | 107 | ch.qos.logback 108 | logback-classic 109 | 1.1.3 110 | test 111 | 112 | 113 | org.hamcrest 114 | hamcrest-library 115 | 1.3 116 | test 117 | 118 | 119 | 120 | 121 | 122 | 123 | org.apache.maven.plugins 124 | maven-compiler-plugin 125 | 3.3 126 | 127 | false 128 | 1.6 129 | 1.6 130 | 131 | 132 | 133 | org.apache.maven.plugins 134 | maven-release-plugin 135 | 2.5.2 136 | 137 | v@{project.version} 138 | 139 | 140 | 141 | org.apache.maven.scm 142 | maven-scm-provider-gitexe 143 | 1.9 144 | 145 | 146 | 147 | 148 | org.sonatype.plugins 149 | nexus-staging-maven-plugin 150 | 1.6.5 151 | true 152 | 153 | ossrh 154 | https://oss.sonatype.org/ 155 | true 156 | 157 | 158 | 159 | org.apache.maven.plugins 160 | maven-enforcer-plugin 161 | 1.4 162 | 163 | 164 | enforce 165 | 166 | 167 | 168 | 169 | 170 | 171 | enforce 172 | 173 | 174 | 175 | 176 | 177 | org.apache.maven.plugins 178 | maven-surefire-plugin 179 | 2.18.1 180 | 181 | -Xmx1g -Dio.netty.leakDetectionLevel=paranoid 182 | 183 | 184 | 185 | 186 | 187 | 188 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/netty4/handler/codec/zmtp/ZMTP10Protocol.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.spotify.netty4.handler.codec.zmtp; 18 | 19 | import java.nio.ByteBuffer; 20 | 21 | import io.netty.buffer.ByteBuf; 22 | import io.netty.buffer.Unpooled; 23 | import io.netty.channel.ChannelHandlerContext; 24 | 25 | import static com.spotify.netty4.handler.codec.zmtp.ZMTP10WireFormat.readIdentity; 26 | import static com.spotify.netty4.handler.codec.zmtp.ZMTP10WireFormat.writeGreeting; 27 | import static com.spotify.netty4.handler.codec.zmtp.ZMTPVersion.ZMTP10; 28 | 29 | class ZMTP10Protocol implements ZMTPProtocol { 30 | 31 | @Override 32 | public ZMTPHandshaker handshaker(final ZMTPConfig config) { 33 | return new Handshaker(config.localIdentity()); 34 | } 35 | 36 | static class Handshaker implements ZMTPHandshaker { 37 | 38 | private final ByteBuffer localIdentity; 39 | 40 | Handshaker(final ByteBuffer localIdentity) { 41 | this.localIdentity = localIdentity; 42 | } 43 | 44 | @Override 45 | public ByteBuf greeting() { 46 | final ByteBuf out = Unpooled.buffer(); 47 | writeGreeting(out, localIdentity); 48 | return out; 49 | } 50 | 51 | @Override 52 | public ZMTPHandshake handshake(final ByteBuf in, final ChannelHandlerContext ctx) 53 | throws ZMTPException { 54 | final ByteBuffer remoteIdentity = readIdentity(in); 55 | assert remoteIdentity != null; 56 | return ZMTPHandshake.of(ZMTP10, remoteIdentity); 57 | } 58 | } 59 | 60 | @Override 61 | public String toString() { 62 | return "ZMTP/1.0"; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/netty4/handler/codec/zmtp/ZMTP10WireFormat.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.spotify.netty4.handler.codec.zmtp; 18 | 19 | import java.nio.ByteBuffer; 20 | 21 | import io.netty.buffer.ByteBuf; 22 | 23 | class ZMTP10WireFormat implements ZMTPWireFormat { 24 | 25 | private static final byte FINAL_FLAG = 0x0; 26 | private static final byte MORE_FLAG = 0x1; 27 | 28 | /** 29 | * Read the remote identity octets from a ZMTP/1.0 greeting. 30 | */ 31 | static ByteBuffer readIdentity(final ByteBuf buffer) throws ZMTPParsingException { 32 | final long length = readLength(buffer); 33 | if (length == -1) { 34 | return null; 35 | } 36 | final long identityLength = length - 1; 37 | if (identityLength < 0 || identityLength > 255) { 38 | throw new ZMTPParsingException("Bad remote identity length: " + length); 39 | } 40 | 41 | // skip the flags byte 42 | buffer.skipBytes(1); 43 | 44 | final byte[] identity = new byte[(int) identityLength]; 45 | buffer.readBytes(identity); 46 | return ByteBuffer.wrap(identity); 47 | } 48 | 49 | /** 50 | * Read a ZMTP/1.0 frame length. 51 | */ 52 | static long readLength(final ByteBuf in) { 53 | if (in.readableBytes() < 1) { 54 | return -1; 55 | } 56 | long size = in.readByte() & 0xFF; 57 | if (size == 0xFF) { 58 | if (in.readableBytes() < 8) { 59 | return -1; 60 | } 61 | size = in.readLong(); 62 | } 63 | 64 | return size; 65 | } 66 | 67 | /** 68 | * Write a ZMTP/1.0 frame length. 69 | * 70 | * @param length The length. 71 | * @param out Target buffer. 72 | */ 73 | static void writeLength(final long length, final ByteBuf out) { 74 | writeLength(out, length, length); 75 | } 76 | 77 | /** 78 | * Write a ZMTP/1.0 frame length. 79 | * 80 | * @param out Target buffer. 81 | * @param maxLength The maximum length of the field. 82 | * @param length The length. 83 | */ 84 | static void writeLength(final ByteBuf out, final long maxLength, final long length) { 85 | if (maxLength < 255) { 86 | out.writeByte((byte) length); 87 | } else { 88 | out.writeByte(0xFF); 89 | out.writeLong(length); 90 | } 91 | } 92 | 93 | /** 94 | * Write a ZMTP/1.0 greeting. 95 | * 96 | * @param out Target buffer. 97 | * @param identity Socket identity. 98 | */ 99 | static void writeGreeting(final ByteBuf out, final ByteBuffer identity) { 100 | writeLength(identity.remaining() + 1, out); 101 | out.writeByte(0x00); 102 | out.writeBytes(identity.duplicate()); 103 | } 104 | 105 | @Override 106 | public Header header() { 107 | return new ZMTP10Header(); 108 | } 109 | 110 | @Override 111 | public int frameLength(final int content) { 112 | if (content + 1 < 255) { 113 | return 1 + 1 + content; 114 | } else { 115 | return 1 + 8 + 1 + content; 116 | } 117 | } 118 | 119 | static class ZMTP10Header implements Header { 120 | 121 | int maxLength; 122 | int length; 123 | boolean more; 124 | 125 | @Override 126 | public void set(final int maxLength, final int length, final boolean more) { 127 | this.maxLength = maxLength; 128 | this.length = length; 129 | this.more = more; 130 | } 131 | 132 | @Override 133 | public void write(final ByteBuf out) { 134 | writeLength(out, maxLength + 1, length + 1); 135 | out.writeByte(more ? MORE_FLAG : FINAL_FLAG); 136 | } 137 | 138 | @Override 139 | public boolean read(final ByteBuf in) throws ZMTPParsingException { 140 | final long len = readLength(in); 141 | if (len == -1) { 142 | // Wait for more data 143 | return false; 144 | } 145 | 146 | if (len == 0) { 147 | throw new ZMTPParsingException("Received frame with zero length"); 148 | } 149 | 150 | if (in.readableBytes() < 1) { 151 | // Wait for more data 152 | return false; 153 | } 154 | 155 | length = (int) len - 1; 156 | more = (in.readByte() & MORE_FLAG) == MORE_FLAG; 157 | 158 | return true; 159 | } 160 | 161 | @Override 162 | public long length() { 163 | return length; 164 | } 165 | 166 | @Override 167 | public boolean more() { 168 | return more; 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/netty4/handler/codec/zmtp/ZMTP20Protocol.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.spotify.netty4.handler.codec.zmtp; 18 | 19 | import com.spotify.netty4.handler.codec.zmtp.ZMTP20WireFormat.Greeting; 20 | 21 | import java.nio.ByteBuffer; 22 | 23 | import io.netty.buffer.ByteBuf; 24 | import io.netty.buffer.Unpooled; 25 | import io.netty.channel.ChannelHandlerContext; 26 | 27 | import static com.spotify.netty4.handler.codec.zmtp.ZMTP20WireFormat.detectProtocolVersion; 28 | import static com.spotify.netty4.handler.codec.zmtp.ZMTP20WireFormat.readGreeting; 29 | import static com.spotify.netty4.handler.codec.zmtp.ZMTP20WireFormat.readGreetingBody; 30 | import static com.spotify.netty4.handler.codec.zmtp.ZMTP20WireFormat.writeGreetingBody; 31 | import static com.spotify.netty4.handler.codec.zmtp.ZMTPUtils.checkNotNull; 32 | import static com.spotify.netty4.handler.codec.zmtp.ZMTPVersion.ZMTP10; 33 | import static com.spotify.netty4.handler.codec.zmtp.ZMTPVersion.ZMTP20; 34 | 35 | class ZMTP20Protocol implements ZMTPProtocol { 36 | 37 | @Override 38 | public ZMTPHandshaker handshaker(final ZMTPConfig config) { 39 | return new Handshaker(config.socketType(), config.localIdentity(), config.interop()); 40 | } 41 | 42 | static class Handshaker implements ZMTPHandshaker { 43 | 44 | private final ZMTPSocketType socketType; 45 | private final ByteBuffer identity; 46 | private final boolean interop; 47 | 48 | private boolean splitHandshake; 49 | 50 | Handshaker(final ZMTPSocketType socketType, final ByteBuffer identity, final boolean interop) { 51 | this.socketType = checkNotNull(socketType, "ZMTP/2.0 requires a socket type"); 52 | this.identity = checkNotNull(identity, "identity"); 53 | this.interop = interop; 54 | } 55 | 56 | @Override 57 | public ByteBuf greeting() { 58 | final ByteBuf out = Unpooled.buffer(); 59 | if (interop) { 60 | ZMTP20WireFormat.writeCompatSignature(out, identity); 61 | } else { 62 | ZMTP20WireFormat.writeGreeting(out, socketType, identity); 63 | } 64 | return out; 65 | } 66 | 67 | @Override 68 | public ZMTPHandshake handshake(final ByteBuf in, final ChannelHandlerContext ctx) 69 | throws ZMTPException { 70 | if (splitHandshake) { 71 | final Greeting remoteGreeting = readGreetingBody(in); 72 | if (remoteGreeting.revision() < 1) { 73 | throw new ZMTPException("Bad ZMTP revision: " + remoteGreeting.revision()); 74 | } 75 | return ZMTPHandshake.of(ZMTP20, remoteGreeting.identity(), remoteGreeting.socketType()); 76 | } 77 | 78 | if (interop) { 79 | final int mark = in.readerIndex(); 80 | final ZMTPVersion version = detectProtocolVersion(in); 81 | switch (version) { 82 | case ZMTP10: 83 | in.readerIndex(mark); 84 | // when a ZMTP/1.0 peer is detected, just send the identity bytes. Together 85 | // with the compatibility signature it makes for a valid ZMTP/1.0 greeting. 86 | ctx.writeAndFlush(Unpooled.wrappedBuffer(identity)); 87 | final ByteBuffer remoteIdentity = ZMTP10WireFormat.readIdentity(in); 88 | assert remoteIdentity != null; 89 | return ZMTPHandshake.of(ZMTP10, remoteIdentity); 90 | case ZMTP20: 91 | splitHandshake = true; 92 | final ByteBuf out = Unpooled.buffer(); 93 | writeGreetingBody(out, socketType, identity); 94 | ctx.writeAndFlush(out); 95 | return null; 96 | default: 97 | throw new ZMTPException("Unknown ZMTP version: " + version); 98 | } 99 | } else { 100 | final Greeting remoteGreeting = readGreeting(in); 101 | return ZMTPHandshake.of(ZMTP20, remoteGreeting.identity(), remoteGreeting.socketType()); 102 | } 103 | } 104 | 105 | } 106 | 107 | @Override 108 | public String toString() { 109 | return "ZMTP/2.0"; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/netty4/handler/codec/zmtp/ZMTPCodec.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.spotify.netty4.handler.codec.zmtp; 18 | 19 | 20 | import java.nio.ByteBuffer; 21 | import java.nio.channels.ClosedChannelException; 22 | import java.util.List; 23 | 24 | import io.netty.buffer.ByteBuf; 25 | import io.netty.channel.Channel; 26 | import io.netty.channel.ChannelHandler; 27 | import io.netty.channel.ChannelHandlerContext; 28 | import io.netty.channel.CombinedChannelDuplexHandler; 29 | import io.netty.handler.codec.ReplayingDecoder; 30 | 31 | import static com.spotify.netty4.handler.codec.zmtp.ZMTPUtils.checkNotNull; 32 | 33 | /** 34 | * A ZMTP codec for Netty. 35 | * 36 | * Note: A single codec instance is not {@link Sharable} among multiple {@link Channel} instances. 37 | */ 38 | public class ZMTPCodec extends ReplayingDecoder { 39 | 40 | private final ZMTPSession session; 41 | private final ZMTPHandshaker handshaker; 42 | 43 | private final ZMTPConfig config; 44 | 45 | public ZMTPCodec(final ZMTPSession session) { 46 | this.config = session.config(); 47 | this.session = checkNotNull(session, "session"); 48 | this.handshaker = config.protocol().handshaker(config); 49 | } 50 | 51 | /** 52 | * Get the {@link ZMTPSession} for this codec. 53 | */ 54 | public ZMTPSession session() { 55 | return session; 56 | } 57 | 58 | @Override 59 | public void channelActive(final ChannelHandlerContext ctx) throws Exception { 60 | super.channelActive(ctx); 61 | ctx.writeAndFlush(handshaker.greeting()); 62 | } 63 | 64 | @Override 65 | public void channelInactive(final ChannelHandlerContext ctx) throws Exception { 66 | super.channelInactive(ctx); 67 | if (!session.handshakeFuture().isDone()) { 68 | session.handshakeFailure(new ClosedChannelException()); 69 | ctx.fireUserEventTriggered(new ZMTPHandshakeFailure(session)); 70 | } 71 | } 72 | 73 | @Override 74 | protected void decode(final ChannelHandlerContext ctx, final ByteBuf in, final List out) 75 | throws Exception { 76 | 77 | // Discard input if handshake failed. It is expected that the user will close the channel. 78 | if (session.handshakeFuture().isDone()) { 79 | assert !session.handshakeFuture().isSuccess(); 80 | in.skipBytes(in.readableBytes()); 81 | } 82 | 83 | // Shake hands 84 | final ZMTPHandshake handshake; 85 | try { 86 | handshake = handshaker.handshake(in, ctx); 87 | if (handshake == null) { 88 | // Handshake is not yet done. Await more input. 89 | return; 90 | } 91 | } catch (Exception e) { 92 | session.handshakeFailure(e); 93 | ctx.fireUserEventTriggered(new ZMTPHandshakeFailure(session)); 94 | throw e; 95 | } 96 | 97 | // Handshake is done. 98 | session.handshakeSuccess(handshake); 99 | 100 | // Replace this handler with the framing encoder and decoder 101 | if (actualReadableBytes() > 0) { 102 | out.add(in.readBytes(actualReadableBytes())); 103 | } 104 | final ZMTPDecoder decoder = config.decoder().decoder(session); 105 | final ZMTPEncoder encoder = config.encoder().encoder(session); 106 | final ZMTPWireFormat wireFormat = ZMTPWireFormats.wireFormat(session.negotiatedVersion()); 107 | final ChannelHandler handler = 108 | new CombinedChannelDuplexHandler( 109 | new ZMTPFramingDecoder(wireFormat, decoder), 110 | new ZMTPFramingEncoder(wireFormat, encoder)); 111 | ctx.pipeline().replace(this, ctx.name(), handler); 112 | 113 | // Tell the user that the handshake is complete 114 | ctx.fireUserEventTriggered(new ZMTPHandshakeSuccess(session, handshake)); 115 | } 116 | 117 | public static Builder builder() { 118 | return new Builder(); 119 | } 120 | 121 | public static ZMTPCodec from(final ZMTPConfig config) { 122 | return new ZMTPCodec(ZMTPSession.from(config)); 123 | } 124 | 125 | public static ZMTPCodec of(final ZMTPSocketType socketType) { 126 | return builder().socketType(socketType).build(); 127 | } 128 | 129 | public static ZMTPCodec from(final ZMTPSession session) { 130 | return new ZMTPCodec(session); 131 | } 132 | 133 | public static class Builder { 134 | 135 | private final ZMTPConfig.Builder config = ZMTPConfig.builder(); 136 | 137 | private Builder() { 138 | } 139 | 140 | public Builder protocol(final ZMTPProtocol protocol) { 141 | config.protocol(protocol); 142 | return this; 143 | } 144 | 145 | public Builder interop(final boolean interop) { 146 | config.interop(interop); 147 | return this; 148 | } 149 | 150 | public Builder socketType(final ZMTPSocketType socketType) { 151 | config.socketType(socketType); 152 | return this; 153 | } 154 | 155 | public Builder localIdentity(final CharSequence localIdentity) { 156 | config.localIdentity(localIdentity); 157 | return this; 158 | } 159 | 160 | public Builder localIdentity(final byte[] localIdentity) { 161 | config.localIdentity(localIdentity); 162 | return this; 163 | } 164 | 165 | public Builder localIdentity(final ByteBuffer localIdentity) { 166 | config.localIdentity(localIdentity); 167 | return this; 168 | } 169 | 170 | public Builder encoder(final ZMTPEncoder.Factory encoder) { 171 | config.encoder(encoder); 172 | return this; 173 | } 174 | 175 | public Builder encoder(final Class encoder) { 176 | config.encoder(encoder); 177 | return this; 178 | } 179 | 180 | public Builder decoder(final ZMTPDecoder.Factory decoder) { 181 | config.decoder(decoder); 182 | return this; 183 | } 184 | 185 | public Builder decoder(final Class decoder) { 186 | config.decoder(decoder); 187 | return this; 188 | } 189 | 190 | public Builder identityGenerator(final ZMTPIdentityGenerator identityGenerator) { 191 | config.identityGenerator(identityGenerator); 192 | return this; 193 | } 194 | 195 | public ZMTPCodec build() { 196 | return ZMTPCodec.from(config.build()); 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/netty4/handler/codec/zmtp/ZMTPConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.spotify.netty4.handler.codec.zmtp; 18 | 19 | import java.lang.reflect.Constructor; 20 | import java.lang.reflect.InvocationTargetException; 21 | import java.nio.ByteBuffer; 22 | 23 | import static com.spotify.netty4.handler.codec.zmtp.ZMTPUtils.checkNotNull; 24 | import static io.netty.util.CharsetUtil.UTF_8; 25 | 26 | /** 27 | * Configuration for a ZMTP session and {@link ZMTPCodec}. Can be reused and shared across multiple 28 | * channel instances. 29 | */ 30 | public class ZMTPConfig { 31 | 32 | public static final ByteBuffer ANONYMOUS = ByteBuffer.allocate(0).asReadOnlyBuffer(); 33 | 34 | private final ZMTPProtocol protocol; 35 | private final boolean interop; 36 | private final ZMTPSocketType socketType; 37 | private final ByteBuffer localIdentity; 38 | private final ZMTPEncoder.Factory encoder; 39 | private final ZMTPDecoder.Factory decoder; 40 | private final ZMTPIdentityGenerator identityGenerator; 41 | 42 | private ZMTPConfig(final Builder builder) { 43 | this.protocol = checkNotNull(builder.protocol, "protocol"); 44 | this.interop = checkNotNull(builder.interop, "interop"); 45 | this.socketType = checkNotNull(builder.socketType, "socketType"); 46 | this.localIdentity = checkNotNull(builder.localIdentity, "localIdentity"); 47 | this.encoder = checkNotNull(builder.encoder, "encoder"); 48 | this.decoder = checkNotNull(builder.decoder, "decoder"); 49 | this.identityGenerator = checkNotNull(builder.identityGenerator, "identityGenerator"); 50 | } 51 | 52 | public ZMTPProtocol protocol() { 53 | return protocol; 54 | } 55 | 56 | public boolean interop() { 57 | return interop; 58 | } 59 | 60 | public ZMTPSocketType socketType() { 61 | return socketType; 62 | } 63 | 64 | public ByteBuffer localIdentity() { 65 | return localIdentity; 66 | } 67 | 68 | public ZMTPEncoder.Factory encoder() { 69 | return encoder; 70 | } 71 | 72 | public ZMTPDecoder.Factory decoder() { 73 | return decoder; 74 | } 75 | 76 | public ZMTPIdentityGenerator identityGenerator() { 77 | return identityGenerator; 78 | } 79 | 80 | public Builder toBuilder() { 81 | return new Builder(this); 82 | } 83 | 84 | public static Builder builder() { 85 | return new Builder(); 86 | } 87 | 88 | public static class Builder { 89 | 90 | private ZMTPProtocol protocol = ZMTPProtocols.ZMTP20; 91 | private boolean interop = true; 92 | private ZMTPSocketType socketType; 93 | private ByteBuffer localIdentity = ANONYMOUS; 94 | private ZMTPEncoder.Factory encoder = ZMTPMessageEncoder.FACTORY; 95 | private ZMTPDecoder.Factory decoder = ZMTPMessageDecoder.FACTORY; 96 | private ZMTPIdentityGenerator identityGenerator = ZMTPLongIdentityGenerator.GLOBAL; 97 | 98 | private Builder() { 99 | } 100 | 101 | private Builder(final ZMTPConfig config) { 102 | this.protocol = config.protocol; 103 | this.interop = config.interop; 104 | this.socketType = config.socketType; 105 | this.localIdentity = config.localIdentity; 106 | this.encoder = config.encoder; 107 | this.decoder = config.decoder; 108 | 109 | } 110 | 111 | public Builder protocol(final ZMTPProtocol protocol) { 112 | this.protocol = protocol; 113 | return this; 114 | } 115 | 116 | public Builder interop(final boolean interop) { 117 | this.interop = interop; 118 | return this; 119 | } 120 | 121 | public Builder socketType(final ZMTPSocketType socketType) { 122 | this.socketType = socketType; 123 | return this; 124 | } 125 | 126 | public Builder localIdentity(final CharSequence localIdentity) { 127 | return localIdentity(UTF_8.encode(localIdentity.toString())); 128 | } 129 | 130 | public Builder localIdentity(final byte[] localIdentity) { 131 | return localIdentity(ByteBuffer.wrap(localIdentity)); 132 | } 133 | 134 | public Builder localIdentity(final ByteBuffer localIdentity) { 135 | this.localIdentity = localIdentity; 136 | return this; 137 | } 138 | 139 | public Builder encoder(final ZMTPEncoder.Factory encoder) { 140 | this.encoder = encoder; 141 | return this; 142 | } 143 | 144 | public Builder encoder(final Class encoder) { 145 | return encoder(new ZMTPEncoderClassFactory(encoder)); 146 | } 147 | 148 | public Builder decoder(final ZMTPDecoder.Factory decoder) { 149 | this.decoder = decoder; 150 | return this; 151 | } 152 | 153 | public Builder decoder(final Class decoder) { 154 | return decoder(new ZMTPDecoderClassFactory(decoder)); 155 | } 156 | 157 | public Builder identityGenerator(final ZMTPIdentityGenerator identityGenerator) { 158 | this.identityGenerator = identityGenerator; 159 | return this; 160 | } 161 | 162 | public ZMTPConfig build() { 163 | return new ZMTPConfig(this); 164 | } 165 | 166 | } 167 | 168 | @Override 169 | public String toString() { 170 | return "ZMTPConfig{" + 171 | "protocol=" + protocol + 172 | ", interop=" + interop + 173 | ", socketType=" + socketType + 174 | ", localIdentity=" + localIdentity + 175 | ", encoder=" + encoder + 176 | ", decoder=" + decoder + 177 | '}'; 178 | } 179 | 180 | private static class ZMTPEncoderClassFactory implements ZMTPEncoder.Factory { 181 | 182 | private final Constructor constructor; 183 | 184 | public ZMTPEncoderClassFactory(final Class encoder) { 185 | checkNotNull(encoder, "encoder"); 186 | try { 187 | constructor = encoder.getDeclaredConstructor(); 188 | } catch (NoSuchMethodException e) { 189 | throw new IllegalArgumentException("Class must have default constructor: " + encoder); 190 | } 191 | if (!constructor.isAccessible()) { 192 | constructor.setAccessible(true); 193 | } 194 | } 195 | 196 | @Override 197 | public ZMTPEncoder encoder(final ZMTPSession session) { 198 | try { 199 | return constructor.newInstance(); 200 | } catch (InstantiationException e) { 201 | throw new RuntimeException(e); 202 | } catch (IllegalAccessException e) { 203 | throw new RuntimeException(e); 204 | } catch (InvocationTargetException e) { 205 | throw new RuntimeException(e); 206 | } 207 | } 208 | 209 | @Override 210 | public String toString() { 211 | return "ZMTPEncoderClassFactory{" + 212 | "constructor=" + constructor + 213 | '}'; 214 | } 215 | } 216 | 217 | private static class ZMTPDecoderClassFactory implements ZMTPDecoder.Factory { 218 | 219 | private final Constructor constructor; 220 | 221 | public ZMTPDecoderClassFactory(final Class decoder) { 222 | checkNotNull(decoder, "decoder"); 223 | try { 224 | constructor = decoder.getDeclaredConstructor(); 225 | } catch (NoSuchMethodException e) { 226 | throw new IllegalArgumentException("Class must have default constructor: " + decoder); 227 | } 228 | if (!constructor.isAccessible()) { 229 | constructor.setAccessible(true); 230 | } 231 | } 232 | 233 | @Override 234 | public ZMTPDecoder decoder(final ZMTPSession session) { 235 | try { 236 | return constructor.newInstance(); 237 | } catch (InstantiationException e) { 238 | throw new RuntimeException(e); 239 | } catch (IllegalAccessException e) { 240 | throw new RuntimeException(e); 241 | } catch (InvocationTargetException e) { 242 | throw new RuntimeException(e); 243 | } 244 | } 245 | 246 | @Override 247 | public String toString() { 248 | return "ZMTPDecoderClassFactory{" + 249 | "constructor=" + constructor + 250 | '}'; 251 | } 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/netty4/handler/codec/zmtp/ZMTPDecoder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.spotify.netty4.handler.codec.zmtp; 18 | 19 | import java.io.Closeable; 20 | import java.util.List; 21 | 22 | import io.netty.buffer.ByteBuf; 23 | import io.netty.channel.ChannelHandlerContext; 24 | 25 | /** 26 | * A streaming decoder that takes parsed ZMTP frame headers and raw content and (optionally) 27 | * produces some output. 28 | */ 29 | public interface ZMTPDecoder extends Closeable { 30 | 31 | /** 32 | * Start a new ZMTP frame. 33 | * 34 | * @param ctx The {@link ChannelHandlerContext} where this decoder is used. 35 | * @param length The total length in bytes of the frame content. 36 | * @param more {@code true} if there are additional frames following this one in the current 37 | * ZMTP message, {@code false otherwise.} 38 | * @param out {@link List} to which decoded messages should be added. 39 | */ 40 | void header(final ChannelHandlerContext ctx, final long length, boolean more, 41 | final List out); 42 | 43 | /** 44 | * Read ZMTP frame content. Called repeatedly, at least once, per frame until all of the frame 45 | * content data has been read. 46 | * 47 | * @param ctx The {@link ChannelHandlerContext} where this decoder is used. 48 | * @param data The raw ZMTP frame content. 49 | * @param out {@link List} to which decoded messages should be added. 50 | */ 51 | void content(final ChannelHandlerContext ctx, ByteBuf data, final List out); 52 | 53 | /** 54 | * End the ZMTP message. Called once after {@link #header} has been called with {@code more == 55 | * false}. 56 | * 57 | * @param ctx The {@link ChannelHandlerContext} where this decoder is used. 58 | * @param out {@link List} to which decoded messages should be added. 59 | */ 60 | void finish(final ChannelHandlerContext ctx, final List out); 61 | 62 | /** 63 | * Tear down the decoder and release e.g. retained {@link ByteBuf}s. May be called mid-message. 64 | */ 65 | @Override 66 | void close(); 67 | 68 | /** 69 | * Creates {@link ZMTPDecoder} instances. 70 | */ 71 | interface Factory { 72 | 73 | /** 74 | * Create a {@link ZMTPDecoder} for a {@link ZMTPSession}; 75 | */ 76 | ZMTPDecoder decoder(ZMTPSession session); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/netty4/handler/codec/zmtp/ZMTPEncoder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2014 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.spotify.netty4.handler.codec.zmtp; 18 | 19 | import java.io.Closeable; 20 | 21 | import io.netty.buffer.ByteBuf; 22 | 23 | /** 24 | * An encoder that takes implementation defined messages and writes a stream of ZMTP frames. 25 | */ 26 | public interface ZMTPEncoder extends Closeable { 27 | 28 | /** 29 | * Estimate ZMTP output for the {@code message} using a {@link ZMTPEstimator}. Called before 30 | * {@link #encode}. 31 | * 32 | * @param message The message to be estimated. 33 | * @param estimator The {@link ZMTPEstimator} to use. 34 | */ 35 | void estimate(Object message, ZMTPEstimator estimator); 36 | 37 | /** 38 | * Write ZMTP output for the {@code message} using the {@link ZMTPWriter}. Called after {@link 39 | * #estimate}. 40 | * 41 | * @param message The message to write. 42 | * @param writer The {@link ZMTPWriter} to use. 43 | */ 44 | void encode(Object message, ZMTPWriter writer); 45 | 46 | /** 47 | * Tear down the encoder and release e.g. retained {@link ByteBuf}s. 48 | */ 49 | @Override 50 | void close(); 51 | 52 | /** 53 | * Creates {@link ZMTPEncoder} instances. 54 | */ 55 | interface Factory { 56 | 57 | /** 58 | * Create a {@link ZMTPEncoder} for a {@link ZMTPSession}; 59 | */ 60 | ZMTPEncoder encoder(ZMTPSession session); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/netty4/handler/codec/zmtp/ZMTPEstimator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.spotify.netty4.handler.codec.zmtp; 18 | 19 | public class ZMTPEstimator { 20 | 21 | private int size; 22 | 23 | private ZMTPWireFormat wireFormat; 24 | 25 | ZMTPEstimator(final ZMTPWireFormat wireFormat) { 26 | this.wireFormat = wireFormat; 27 | } 28 | 29 | public void reset() { 30 | size = 0; 31 | } 32 | 33 | public void frame(final int size) { 34 | this.size += wireFormat.frameLength(size); 35 | } 36 | 37 | public int size() { 38 | return size; 39 | } 40 | 41 | static ZMTPEstimator create(final ZMTPVersion version) { 42 | return new ZMTPEstimator(ZMTPWireFormats.wireFormat(version)); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/netty4/handler/codec/zmtp/ZMTPException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2013 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.spotify.netty4.handler.codec.zmtp; 18 | 19 | /** 20 | * Base for all checked Netty ZMTP exceptions. 21 | */ 22 | public class ZMTPException extends Exception { 23 | 24 | public ZMTPException() { 25 | } 26 | 27 | public ZMTPException(final String message) { 28 | super(message); 29 | } 30 | 31 | public ZMTPException(final String message, final Throwable exception) { 32 | super(message, exception); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/netty4/handler/codec/zmtp/ZMTPFramingDecoder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2013 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.spotify.netty4.handler.codec.zmtp; 18 | 19 | import java.util.List; 20 | 21 | import io.netty.buffer.ByteBuf; 22 | import io.netty.channel.ChannelHandlerContext; 23 | import io.netty.handler.codec.ByteToMessageDecoder; 24 | 25 | import static java.lang.Math.min; 26 | 27 | /** 28 | * Netty ZMTP decoder. 29 | */ 30 | class ZMTPFramingDecoder extends ByteToMessageDecoder { 31 | 32 | private final ZMTPDecoder decoder; 33 | private final ZMTPWireFormat.Header header; 34 | 35 | private long remaining; 36 | private boolean headerParsed; 37 | 38 | public ZMTPFramingDecoder(final ZMTPWireFormat wireFormat, final ZMTPDecoder decoder) { 39 | this.header = wireFormat.header(); 40 | this.decoder = decoder; 41 | } 42 | 43 | @Override 44 | protected void handlerRemoved0(final ChannelHandlerContext ctx) { 45 | decoder.close(); 46 | } 47 | 48 | @Override 49 | protected void decode(final ChannelHandlerContext ctx, final ByteBuf in, final List out) 50 | throws ZMTPParsingException { 51 | while (in.isReadable()) { 52 | if (!headerParsed) { 53 | final int mark = in.readerIndex(); 54 | headerParsed = header.read(in); 55 | if (!headerParsed) { 56 | // Wait for more data 57 | in.readerIndex(mark); 58 | return; 59 | } 60 | decoder.header(ctx, header.length(), header.more(), out); 61 | remaining = header.length(); 62 | } 63 | 64 | final int writerMark = in.writerIndex(); 65 | final int n = (int) min(remaining, in.readableBytes()); 66 | final int readerMark = in.readerIndex(); 67 | in.writerIndex(readerMark + n); 68 | decoder.content(ctx, in, out); 69 | in.writerIndex(writerMark); 70 | final int read = in.readerIndex() - readerMark; 71 | remaining -= read; 72 | if (remaining > 0) { 73 | // Wait for more data 74 | return; 75 | } 76 | if (!header.more()) { 77 | decoder.finish(ctx, out); 78 | } 79 | headerParsed = false; 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/netty4/handler/codec/zmtp/ZMTPFramingEncoder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2013 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.spotify.netty4.handler.codec.zmtp; 18 | 19 | 20 | import java.util.ArrayList; 21 | import java.util.List; 22 | 23 | import io.netty.buffer.ByteBuf; 24 | import io.netty.channel.Channel; 25 | import io.netty.channel.ChannelHandlerContext; 26 | import io.netty.channel.ChannelOutboundHandlerAdapter; 27 | import io.netty.channel.ChannelPromise; 28 | import io.netty.channel.DefaultChannelPromise; 29 | import io.netty.util.ReferenceCountUtil; 30 | 31 | /** 32 | * Netty ZMTP encoder. 33 | */ 34 | class ZMTPFramingEncoder extends ChannelOutboundHandlerAdapter { 35 | 36 | private final ZMTPEncoder encoder; 37 | 38 | private final List messages = new ArrayList(); 39 | private final List promises = new ArrayList(); 40 | private ZMTPWriter writer; 41 | private ZMTPEstimator estimator; 42 | 43 | ZMTPFramingEncoder(final ZMTPSession session, final ZMTPEncoder encoder) { 44 | if (session == null) { 45 | throw new NullPointerException("session"); 46 | } 47 | if (encoder == null) { 48 | throw new NullPointerException("encoder"); 49 | } 50 | this.encoder = encoder; 51 | this.writer = ZMTPWriter.create(session.negotiatedVersion()); 52 | this.estimator = ZMTPEstimator.create(session.negotiatedVersion()); 53 | } 54 | 55 | public ZMTPFramingEncoder(final ZMTPWireFormat wireFormat, final ZMTPEncoder encoder) { 56 | if (wireFormat == null) { 57 | throw new NullPointerException("wireFormat"); 58 | } 59 | if (encoder == null) { 60 | throw new NullPointerException("encoder"); 61 | } 62 | this.encoder = encoder; 63 | this.writer = new ZMTPWriter(wireFormat); 64 | this.estimator = new ZMTPEstimator(wireFormat); 65 | } 66 | 67 | @Override 68 | public void handlerRemoved(final ChannelHandlerContext ctx) { 69 | encoder.close(); 70 | } 71 | 72 | @Override 73 | public void write(final ChannelHandlerContext ctx, final Object msg, 74 | final ChannelPromise promise) { 75 | messages.add(msg); 76 | promises.add(promise); 77 | } 78 | 79 | @Override 80 | public void flush(final ChannelHandlerContext ctx) throws Exception { 81 | if (messages == null) { 82 | return; 83 | } 84 | estimator.reset(); 85 | for (final Object message : messages) { 86 | encoder.estimate(message, estimator); 87 | } 88 | final ByteBuf output = ctx.alloc().buffer(estimator.size()); 89 | writer.reset(output); 90 | for (final Object message : messages) { 91 | encoder.encode(message, writer); 92 | ReferenceCountUtil.release(message); 93 | } 94 | final ChannelPromise aggregate = new AggregatePromise(ctx.channel(), promises); 95 | messages.clear(); 96 | promises.clear(); 97 | ctx.write(output, aggregate); 98 | ctx.flush(); 99 | } 100 | 101 | private static class AggregatePromise extends DefaultChannelPromise { 102 | 103 | private final ChannelPromise[] promises; 104 | 105 | private AggregatePromise(final Channel channel, 106 | final List promises) { 107 | super(channel); 108 | this.promises = promises.toArray(new ChannelPromise[promises.size()]); 109 | } 110 | 111 | @Override 112 | public ChannelPromise setSuccess(final Void result) { 113 | super.setSuccess(result); 114 | for (final ChannelPromise promise : promises) { 115 | promise.setSuccess(result); 116 | } 117 | return this; 118 | } 119 | 120 | @Override 121 | public boolean trySuccess() { 122 | final boolean result = super.trySuccess(); 123 | for (final ChannelPromise promise : promises) { 124 | promise.trySuccess(); 125 | } 126 | return result; 127 | } 128 | 129 | @Override 130 | public ChannelPromise setFailure(final Throwable cause) { 131 | super.setFailure(cause); 132 | for (final ChannelPromise promise : promises) { 133 | promise.setFailure(cause); 134 | } 135 | return this; 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/netty4/handler/codec/zmtp/ZMTPHandshake.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.spotify.netty4.handler.codec.zmtp; 18 | 19 | import java.nio.ByteBuffer; 20 | 21 | import javax.annotation.Nullable; 22 | 23 | import static com.spotify.netty4.handler.codec.zmtp.ZMTPUtils.checkNotNull; 24 | 25 | public class ZMTPHandshake { 26 | 27 | private final ZMTPVersion negotiatedVersion; 28 | private final ByteBuffer remoteIdentity; 29 | private final ZMTPSocketType remoteSocketType; 30 | 31 | private ZMTPHandshake(final ZMTPVersion negotiatedVersion, 32 | final ByteBuffer remoteIdentity, final ZMTPSocketType remoteSocketType) { 33 | this.negotiatedVersion = checkNotNull(negotiatedVersion, "negotiatedVersion"); 34 | this.remoteIdentity = checkNotNull(remoteIdentity, "remoteIdentity"); 35 | this.remoteSocketType = remoteSocketType; 36 | } 37 | 38 | public ZMTPVersion negotiatedVersion() { 39 | return negotiatedVersion; 40 | } 41 | 42 | public ByteBuffer remoteIdentity() { 43 | return remoteIdentity.asReadOnlyBuffer(); 44 | } 45 | 46 | @Nullable 47 | public ZMTPSocketType remoteSocketType() { 48 | return remoteSocketType; 49 | } 50 | 51 | @Override 52 | public boolean equals(final Object o) { 53 | if (this == o) { return true; } 54 | if (o == null || getClass() != o.getClass()) { return false; } 55 | 56 | final ZMTPHandshake that = (ZMTPHandshake) o; 57 | 58 | if (negotiatedVersion != that.negotiatedVersion) { return false; } 59 | if (remoteIdentity != null ? !remoteIdentity.equals(that.remoteIdentity) 60 | : that.remoteIdentity != null) { return false; } 61 | if (remoteSocketType != that.remoteSocketType) { return false; } 62 | 63 | return true; 64 | } 65 | 66 | @Override 67 | public int hashCode() { 68 | int result = negotiatedVersion != null ? negotiatedVersion.hashCode() : 0; 69 | result = 31 * result + (remoteSocketType != null ? remoteSocketType.hashCode() : 0); 70 | result = 31 * result + (remoteIdentity != null ? remoteIdentity.hashCode() : 0); 71 | return result; 72 | } 73 | 74 | @Override 75 | public String toString() { 76 | return "ZMTPHandshake{" + 77 | "negotiatedVersion=" + negotiatedVersion + 78 | ", remoteSocketType=" + remoteSocketType + 79 | '}'; 80 | } 81 | 82 | static ZMTPHandshake of(final ZMTPVersion negotiatedVersion, 83 | final ByteBuffer remoteIdentity) { 84 | return new ZMTPHandshake(negotiatedVersion, remoteIdentity, null); 85 | } 86 | 87 | static ZMTPHandshake of(final ZMTPVersion negotiatedVersion, 88 | final ByteBuffer remoteIdentity, final ZMTPSocketType remoteSocketType) { 89 | return new ZMTPHandshake(negotiatedVersion, remoteIdentity, remoteSocketType); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/netty4/handler/codec/zmtp/ZMTPHandshakeFailure.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.spotify.netty4.handler.codec.zmtp; 18 | 19 | public class ZMTPHandshakeFailure { 20 | 21 | private final ZMTPSession session; 22 | 23 | ZMTPHandshakeFailure(final ZMTPSession session) { 24 | this.session = session; 25 | } 26 | 27 | public ZMTPSession session() { 28 | return session; 29 | } 30 | 31 | @Override 32 | public String toString() { 33 | return "ZMTPHandshakeFailure{" + 34 | "session=" + session + 35 | '}'; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/netty4/handler/codec/zmtp/ZMTPHandshakeSuccess.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.spotify.netty4.handler.codec.zmtp; 18 | 19 | public class ZMTPHandshakeSuccess { 20 | 21 | private final ZMTPSession session; 22 | private final ZMTPHandshake handshake; 23 | 24 | ZMTPHandshakeSuccess(final ZMTPSession session, 25 | final ZMTPHandshake handshake) { 26 | this.session = session; 27 | this.handshake = handshake; 28 | } 29 | 30 | public ZMTPSession session() { 31 | return session; 32 | } 33 | 34 | public ZMTPHandshake handshake() { 35 | return handshake; 36 | } 37 | 38 | @Override 39 | public String toString() { 40 | return "ZMTPHandshakeSuccess{" + 41 | "session=" + session + 42 | ", handshake=" + handshake + 43 | '}'; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/netty4/handler/codec/zmtp/ZMTPHandshaker.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.spotify.netty4.handler.codec.zmtp; 18 | 19 | import io.netty.buffer.ByteBuf; 20 | import io.netty.channel.ChannelHandlerContext; 21 | 22 | interface ZMTPHandshaker { 23 | 24 | /** 25 | * Get a greeting to send immediately when a connection is established. 26 | */ 27 | ByteBuf greeting(); 28 | 29 | /** 30 | * Continue handshake in response to receiving data from the remote peer. This method is called 31 | * repeatedly until it returns a non-null {@link ZMTPHandshake} result. 32 | * 33 | * @param in Data from the remote peer. 34 | * @param ctx The channel handler context. 35 | * @return A {@link ZMTPHandshake} if the handshake is complete, null otherwise. 36 | * @throws ZMTPException for protocol errors. 37 | */ 38 | ZMTPHandshake handshake(ByteBuf in, ChannelHandlerContext ctx) throws ZMTPException; 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/netty4/handler/codec/zmtp/ZMTPIdentityGenerator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.spotify.netty4.handler.codec.zmtp; 18 | 19 | import java.nio.ByteBuffer; 20 | 21 | public interface ZMTPIdentityGenerator { 22 | 23 | ByteBuffer generateIdentity(ZMTPSession session); 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/netty4/handler/codec/zmtp/ZMTPLongIdentityGenerator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.spotify.netty4.handler.codec.zmtp; 18 | 19 | import java.nio.ByteBuffer; 20 | import java.security.SecureRandom; 21 | import java.util.concurrent.atomic.AtomicLong; 22 | 23 | /** 24 | * A {@link ZMTPIdentityGenerator} that generates identities using a {@code long} counter. 25 | */ 26 | public class ZMTPLongIdentityGenerator implements ZMTPIdentityGenerator { 27 | 28 | public static ZMTPLongIdentityGenerator GLOBAL = new ZMTPLongIdentityGenerator(); 29 | 30 | private static final AtomicLong peerIdCounter = new AtomicLong(new SecureRandom().nextLong()); 31 | 32 | @Override 33 | public ByteBuffer generateIdentity(final ZMTPSession session) { 34 | final ByteBuffer generated = ByteBuffer.allocate(9); 35 | generated.put((byte) 0); 36 | generated.putLong(peerIdCounter.incrementAndGet()); 37 | generated.flip(); 38 | return generated; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/netty4/handler/codec/zmtp/ZMTPMessage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2013 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.spotify.netty4.handler.codec.zmtp; 18 | 19 | import java.nio.CharBuffer; 20 | import java.nio.charset.Charset; 21 | import java.util.ArrayList; 22 | import java.util.Arrays; 23 | import java.util.Collection; 24 | import java.util.Iterator; 25 | import java.util.List; 26 | 27 | import io.netty.buffer.ByteBuf; 28 | import io.netty.buffer.ByteBufAllocator; 29 | import io.netty.util.AbstractReferenceCounted; 30 | import io.netty.util.internal.RecyclableArrayList; 31 | 32 | import static com.spotify.netty4.handler.codec.zmtp.ZMTPUtils.checkNotNull; 33 | import static io.netty.buffer.ByteBufUtil.encodeString; 34 | import static io.netty.util.CharsetUtil.UTF_8; 35 | import static java.util.Arrays.asList; 36 | 37 | public class ZMTPMessage extends AbstractReferenceCounted implements Iterable { 38 | 39 | private final ByteBuf[] frames; 40 | 41 | private ZMTPMessage(final ByteBuf[] frames) { 42 | this.frames = checkNotNull(frames, "frames"); 43 | } 44 | 45 | @Override 46 | public ZMTPMessage retain() { 47 | super.retain(); 48 | return this; 49 | } 50 | 51 | @Override 52 | public ZMTPMessage retain(final int increment) { 53 | super.retain(increment); 54 | return this; 55 | } 56 | 57 | /** 58 | * Create a new message from a string frames, using UTF-8 encoding. 59 | */ 60 | public static ZMTPMessage fromUTF8(final CharSequence... strings) { 61 | return from(ByteBufAllocator.DEFAULT, UTF_8, strings); 62 | } 63 | 64 | /** 65 | * Create a new message from a string frames, using UTF-8 encoding. 66 | */ 67 | public static ZMTPMessage fromUTF8(final ByteBufAllocator alloc, final CharSequence... strings) { 68 | return from(alloc, UTF_8, strings); 69 | } 70 | 71 | /** 72 | * Create a new message from a list of string frames, using UTF-8 encoding. 73 | */ 74 | public static ZMTPMessage fromUTF8(final Iterable strings) { 75 | return from(UTF_8, strings); 76 | } 77 | 78 | /** 79 | * Create a new message from a list of string frames, using UTF-8 encoding. 80 | */ 81 | public static ZMTPMessage fromUTF8(final ByteBufAllocator alloc, 82 | final Iterable strings) { 83 | return from(alloc, UTF_8, strings); 84 | } 85 | 86 | /** 87 | * Create a new message from a list of string frames, using a specified encoding. 88 | */ 89 | public static ZMTPMessage from(final Charset charset, final CharSequence... strings) { 90 | return from(charset, asList(strings)); 91 | } 92 | 93 | /** 94 | * Create a new message from a list of string frames, using a specified encoding. 95 | */ 96 | public static ZMTPMessage from(final ByteBufAllocator alloc, final Charset charset, 97 | final CharSequence... strings) { 98 | return from(alloc, charset, asList(strings)); 99 | } 100 | 101 | /** 102 | * Create a new message from a list of string frames, using a specified encoding. 103 | */ 104 | public static ZMTPMessage from(final Charset charset, 105 | final Iterable strings) { 106 | return from(ByteBufAllocator.DEFAULT, charset, strings); 107 | } 108 | 109 | /** 110 | * Create a new message from a list of string frames, using a specified encoding. 111 | */ 112 | public static ZMTPMessage from(final ByteBufAllocator alloc, final Charset charset, 113 | final Iterable strings) { 114 | final List frames = new ArrayList(); 115 | for (final CharSequence string : strings) { 116 | frames.add(encodeString(alloc, CharBuffer.wrap(string), charset)); 117 | } 118 | return from(frames); 119 | } 120 | 121 | /** 122 | * Create a new message from a list of frames. 123 | */ 124 | public static ZMTPMessage from(final Collection frames) { 125 | checkNotNull(frames, "frames"); 126 | return new ZMTPMessage(frames.toArray(new ByteBuf[frames.size()])); 127 | } 128 | 129 | /** 130 | * Create a new message from a list of frames. 131 | */ 132 | public static ZMTPMessage from(final ByteBuf[] frames) { 133 | return new ZMTPMessage(frames.clone()); 134 | } 135 | 136 | public int size() { 137 | return frames.length; 138 | } 139 | 140 | @Override 141 | public Iterator iterator() { 142 | return new FrameIterator(); 143 | } 144 | 145 | /** 146 | * Get a specific frame. 147 | */ 148 | public ByteBuf frame(final int i) { 149 | return frames[i]; 150 | } 151 | 152 | @Override 153 | protected void deallocate() { 154 | for (final ByteBuf frame : frames) { 155 | frame.release(); 156 | } 157 | } 158 | 159 | @Override 160 | public boolean equals(final Object o) { 161 | if (this == o) { return true; } 162 | if (o == null || getClass() != o.getClass()) { return false; } 163 | 164 | final ZMTPMessage byteBufs = (ZMTPMessage) o; 165 | 166 | return Arrays.equals(frames, byteBufs.frames); 167 | 168 | } 169 | 170 | @Override 171 | public int hashCode() { 172 | return frames != null ? Arrays.hashCode(frames) : 0; 173 | } 174 | 175 | @Override 176 | public String toString() { 177 | return "ZMTPMessage{" + toString(frames) + '}'; 178 | } 179 | 180 | /** 181 | * Create a human readable string representation of binary data, keeping printable ascii and hex 182 | * encoding everything else. 183 | * 184 | * @param data The data 185 | * @return A human readable string representation of the data. 186 | */ 187 | private static String toString(final ByteBuf data) { 188 | if (data == null) { 189 | return null; 190 | } 191 | final StringBuilder sb = new StringBuilder(); 192 | for (int i = data.readerIndex(); i < data.writerIndex(); i++) { 193 | final byte b = data.getByte(i); 194 | if (b > 31 && b < 127) { 195 | if (b == '%') { 196 | sb.append('%'); 197 | } 198 | sb.append((char) b); 199 | } else { 200 | sb.append('%'); 201 | sb.append(String.format("%02X", b)); 202 | } 203 | } 204 | return sb.toString(); 205 | } 206 | 207 | /** 208 | * Create a human readable string representation of a list of ZMTP frames, keeping printable ascii 209 | * and hex encoding everything else. 210 | * 211 | * @param frames The ZMTP frames. 212 | * @return A human readable string representation of the frames. 213 | */ 214 | private static String toString(final ByteBuf[] frames) { 215 | final StringBuilder builder = new StringBuilder("["); 216 | for (int i = 0; i < frames.length; i++) { 217 | final ByteBuf frame = frames[i]; 218 | builder.append('"'); 219 | builder.append(toString(frame)); 220 | builder.append('"'); 221 | if (i < frames.length - 1) { 222 | builder.append(','); 223 | } 224 | } 225 | builder.append(']'); 226 | return builder.toString(); 227 | } 228 | 229 | /** 230 | * Convenience method for reading a {@link ZMTPMessage} from a {@link ByteBuf}. 231 | */ 232 | public static ZMTPMessage read(final ByteBuf in, final ZMTPVersion version) 233 | throws ZMTPParsingException { 234 | final int mark = in.readerIndex(); 235 | final ZMTPWireFormat wireFormat = ZMTPWireFormats.wireFormat(version); 236 | final ZMTPWireFormat.Header header = wireFormat.header(); 237 | final RecyclableArrayList frames = RecyclableArrayList.newInstance(); 238 | while (true) { 239 | final boolean read = header.read(in); 240 | if (!read) { 241 | frames.recycle(); 242 | in.readerIndex(mark); 243 | return null; 244 | } 245 | if (in.readableBytes() < header.length()) { 246 | frames.recycle(); 247 | in.readerIndex(mark); 248 | return null; 249 | } 250 | if (header.length() > Integer.MAX_VALUE) { 251 | throw new ZMTPParsingException("frame is too large: " + header.length()); 252 | } 253 | final ByteBuf frame = in.readSlice((int) header.length()); 254 | frame.retain(); 255 | frames.add(frame); 256 | if (!header.more()) { 257 | @SuppressWarnings("unchecked") final ZMTPMessage message = 258 | ZMTPMessage.from((List) (Object) frames); 259 | frames.recycle(); 260 | return message; 261 | } 262 | } 263 | } 264 | 265 | /** 266 | * Convenience method for writing a {@link ZMTPMessage} to a {@link ByteBuf}. 267 | */ 268 | public ByteBuf write(final ZMTPVersion version) { 269 | return write(ByteBufAllocator.DEFAULT, version); 270 | } 271 | 272 | /** 273 | * Convenience method for writing a {@link ZMTPMessage} to a {@link ByteBuf}. 274 | */ 275 | public ByteBuf write(final ByteBufAllocator alloc, final ZMTPVersion version) { 276 | final ZMTPMessageEncoder encoder = new ZMTPMessageEncoder(); 277 | final ZMTPEstimator estimator = ZMTPEstimator.create(version); 278 | encoder.estimate(this, estimator); 279 | final ByteBuf out = alloc.buffer(estimator.size()); 280 | final ZMTPWriter writer = ZMTPWriter.create(version); 281 | writer.reset(out); 282 | encoder.encode(this, writer); 283 | return out; 284 | } 285 | 286 | /** 287 | * Convenience method for writing a {@link ZMTPMessage} to a {@link ByteBuf}. 288 | */ 289 | public void write(final ByteBuf out, final ZMTPVersion version) { 290 | final ZMTPWriter writer = ZMTPWriter.create(version); 291 | final ZMTPMessageEncoder encoder = new ZMTPMessageEncoder(); 292 | writer.reset(out); 293 | encoder.encode(this, writer); 294 | } 295 | 296 | /** 297 | * Create a new {@link ZMTPMessage} with a frame added at the front. 298 | */ 299 | public ZMTPMessage push(final ByteBuf frame) { 300 | for (final ByteBuf f : frames) { 301 | f.retain(); 302 | } 303 | final ByteBuf[] frames = new ByteBuf[this.frames.length + 1]; 304 | frames[0] = frame; 305 | System.arraycopy(this.frames, 0, frames, 1, this.frames.length); 306 | return new ZMTPMessage(frames); 307 | } 308 | 309 | /** 310 | * Create a new {@link ZMTPMessage} with the front frame removed. 311 | */ 312 | public ZMTPMessage pop() { 313 | if (this.frames.length == 0) { 314 | throw new IllegalStateException("empty message"); 315 | } 316 | final ByteBuf[] frames = new ByteBuf[this.frames.length - 1]; 317 | System.arraycopy(this.frames, 1, frames, 0, frames.length); 318 | for (final ByteBuf f : frames) { 319 | f.retain(); 320 | } 321 | return new ZMTPMessage(frames); 322 | } 323 | 324 | /** 325 | * Iterates over the frames of the {@link ZMTPMessage}. 326 | */ 327 | private class FrameIterator implements Iterator { 328 | 329 | int i; 330 | 331 | @Override 332 | public boolean hasNext() { 333 | return i < frames.length; 334 | } 335 | 336 | @Override 337 | public ByteBuf next() { 338 | return frames[i++]; 339 | } 340 | 341 | @Override 342 | public void remove() { 343 | throw new UnsupportedOperationException("remove"); 344 | } 345 | } 346 | } 347 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/netty4/handler/codec/zmtp/ZMTPMessageDecoder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.spotify.netty4.handler.codec.zmtp; 18 | 19 | import java.util.ArrayList; 20 | import java.util.List; 21 | 22 | import io.netty.buffer.ByteBuf; 23 | import io.netty.buffer.Unpooled; 24 | import io.netty.channel.ChannelHandlerContext; 25 | 26 | public class ZMTPMessageDecoder implements ZMTPDecoder { 27 | 28 | public static final Factory FACTORY = new Factory() { 29 | @Override 30 | public ZMTPDecoder decoder(final ZMTPSession session) { 31 | return new ZMTPMessageDecoder(); 32 | } 33 | }; 34 | 35 | private static final ByteBuf DELIMITER = Unpooled.EMPTY_BUFFER; 36 | 37 | private final List frames = new ArrayList(); 38 | private int frameLength; 39 | 40 | /** 41 | * Reset parser in preparation for the next message. 42 | */ 43 | private void reset() { 44 | frames.clear(); 45 | frameLength = 0; 46 | } 47 | 48 | @Override 49 | public void header(final ChannelHandlerContext ctx, final long length, final boolean more, 50 | final List out) { 51 | frameLength = (int) length; 52 | } 53 | 54 | @Override 55 | public void content(final ChannelHandlerContext ctx, final ByteBuf data, final List out) { 56 | // Wait for more data? 57 | if (data.readableBytes() < frameLength) { 58 | return; 59 | } 60 | 61 | if (frameLength == 0) { 62 | frames.add(DELIMITER); 63 | return; 64 | } 65 | 66 | final ByteBuf frame = data.readSlice(frameLength); 67 | frame.retain(); 68 | frames.add(frame); 69 | } 70 | 71 | @Override 72 | public void finish(final ChannelHandlerContext ctx, final List out) { 73 | final ZMTPMessage message = ZMTPMessage.from(frames); 74 | reset(); 75 | out.add(message); 76 | } 77 | 78 | @Override 79 | public void close() { 80 | for (final ByteBuf frame : frames) { 81 | frame.release(); 82 | } 83 | frames.clear(); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/netty4/handler/codec/zmtp/ZMTPMessageEncoder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2014 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.spotify.netty4.handler.codec.zmtp; 18 | 19 | import io.netty.buffer.ByteBuf; 20 | 21 | public class ZMTPMessageEncoder implements ZMTPEncoder { 22 | 23 | public static final Factory FACTORY = new Factory() { 24 | @Override 25 | public ZMTPEncoder encoder(final ZMTPSession session) { 26 | return new ZMTPMessageEncoder(); 27 | } 28 | }; 29 | 30 | @Override 31 | public void estimate(final Object msg, final ZMTPEstimator estimator) { 32 | final ZMTPMessage message = (ZMTPMessage) msg; 33 | for (int i = 0; i < message.size(); i++) { 34 | final ByteBuf frame = message.frame(i); 35 | estimator.frame(frame.readableBytes()); 36 | } 37 | } 38 | 39 | @Override 40 | public void encode(final Object msg, final ZMTPWriter writer) { 41 | final ZMTPMessage message = (ZMTPMessage) msg; 42 | for (int i = 0; i < message.size(); i++) { 43 | final ByteBuf frame = message.frame(i); 44 | final boolean more = i < message.size() - 1; 45 | final ByteBuf dst = writer.frame(frame.readableBytes(), more); 46 | dst.writeBytes(frame, frame.readerIndex(), frame.readableBytes()); 47 | } 48 | } 49 | 50 | @Override 51 | public void close() { 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/netty4/handler/codec/zmtp/ZMTPParsingException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2013 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.spotify.netty4.handler.codec.zmtp; 18 | 19 | public class ZMTPParsingException extends ZMTPException { 20 | 21 | public ZMTPParsingException(final String message) { 22 | super(message); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/netty4/handler/codec/zmtp/ZMTPProtocol.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.spotify.netty4.handler.codec.zmtp; 18 | 19 | public interface ZMTPProtocol { 20 | 21 | ZMTPHandshaker handshaker(ZMTPConfig config); 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/netty4/handler/codec/zmtp/ZMTPProtocols.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.spotify.netty4.handler.codec.zmtp; 18 | 19 | public class ZMTPProtocols { 20 | 21 | public static final ZMTPProtocol ZMTP10 = new ZMTP10Protocol(); 22 | public static final ZMTPProtocol ZMTP20 = new ZMTP20Protocol(); 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/netty4/handler/codec/zmtp/ZMTPSession.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2013 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.spotify.netty4.handler.codec.zmtp; 18 | 19 | import java.nio.ByteBuffer; 20 | 21 | import io.netty.util.concurrent.DefaultPromise; 22 | import io.netty.util.concurrent.Future; 23 | import io.netty.util.concurrent.GlobalEventExecutor; 24 | 25 | import static com.spotify.netty4.handler.codec.zmtp.ZMTPUtils.checkNotNull; 26 | 27 | /** 28 | * Represents one end of a single ZMTP connection. 29 | */ 30 | public class ZMTPSession { 31 | 32 | private final DefaultPromise handshake = new DefaultPromise( 33 | GlobalEventExecutor.INSTANCE); 34 | 35 | private final ZMTPConfig config; 36 | 37 | private volatile ByteBuffer peerIdentity; 38 | 39 | ZMTPSession(final ZMTPConfig config) { 40 | this.config = checkNotNull(config, "config"); 41 | } 42 | 43 | /** 44 | * The configuration of this ZMTP session. 45 | */ 46 | public ZMTPConfig config() { 47 | return config; 48 | } 49 | 50 | /** 51 | * Get the peer identity. 52 | */ 53 | public ByteBuffer peerIdentity() { 54 | if (!handshake.isDone()) { 55 | throw new IllegalStateException("handshake not complete"); 56 | } 57 | return peerIdentity.asReadOnlyBuffer(); 58 | } 59 | 60 | /** 61 | * Check whether the peer is anonymous and the peer identity is generated. 62 | */ 63 | public boolean isPeerAnonymous() { 64 | return !handshake().remoteIdentity().hasRemaining(); 65 | } 66 | 67 | /** 68 | * The ZMTP framing version negotiated as part of the handshake on connection establishment. 69 | */ 70 | public ZMTPVersion negotiatedVersion() { 71 | return handshake().negotiatedVersion(); 72 | } 73 | 74 | /** 75 | * Get a future that will be notified when the ZMTP handshake is complete. 76 | */ 77 | public Future handshakeFuture() { 78 | return handshake; 79 | } 80 | 81 | /** 82 | * Signal ZMTP handshake success. 83 | */ 84 | void handshakeSuccess(final ZMTPHandshake handshake) { 85 | peerIdentity = handshake.remoteIdentity().hasRemaining() 86 | ? handshake.remoteIdentity() 87 | : config.identityGenerator().generateIdentity(this); 88 | this.handshake.setSuccess(handshake); 89 | } 90 | 91 | /** 92 | * Signal ZMTP handshake failure. 93 | */ 94 | void handshakeFailure(final Throwable cause) { 95 | this.handshake.tryFailure(cause); 96 | } 97 | 98 | private ZMTPHandshake handshake() { 99 | if (!handshake.isDone()) { 100 | throw new IllegalStateException("handshake not complete"); 101 | } 102 | final ZMTPHandshake handshake = this.handshake.getNow(); 103 | assert handshake != null; 104 | return handshake; 105 | } 106 | 107 | @Override 108 | public String toString() { 109 | return "ZMTPSession{" + 110 | "config=" + config + 111 | ", handshake=" + handshake + 112 | '}'; 113 | } 114 | 115 | public static ZMTPSession from(final ZMTPConfig config) { 116 | return new ZMTPSession(config); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/netty4/handler/codec/zmtp/ZMTPSocketType.java: -------------------------------------------------------------------------------- 1 | package com.spotify.netty4.handler.codec.zmtp; 2 | 3 | /** 4 | * Enumerates the different socket types, used to make sure that connecting both peers in a socket 5 | * pair has compatible socket types. 6 | */ 7 | public enum ZMTPSocketType { 8 | PAIR, 9 | PUB, 10 | SUB, 11 | REQ, 12 | REP, 13 | DEALER, 14 | ROUTER, 15 | PULL, 16 | PUSH 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/netty4/handler/codec/zmtp/ZMTPUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2013 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.spotify.netty4.handler.codec.zmtp; 18 | 19 | import static java.lang.String.format; 20 | 21 | class ZMTPUtils { 22 | 23 | static T checkNotNull(final T obj, final String message) { 24 | if (obj == null) { 25 | throw new NullPointerException(message); 26 | } 27 | return obj; 28 | } 29 | 30 | static void checkArgument(final boolean expression, final String message, final Object... args) { 31 | if (!expression) { 32 | throw new IllegalArgumentException(format(message, args)); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/netty4/handler/codec/zmtp/ZMTPVersion.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.spotify.netty4.handler.codec.zmtp; 18 | 19 | import java.util.List; 20 | 21 | import static java.util.Arrays.asList; 22 | import static java.util.Collections.unmodifiableList; 23 | 24 | public enum ZMTPVersion { 25 | ZMTP10(0, true), 26 | ZMTP20(1, true); 27 | 28 | private final int revision; 29 | private final boolean supported; 30 | 31 | ZMTPVersion(final int revision, final boolean supported) { 32 | this.revision = revision; 33 | this.supported = supported; 34 | } 35 | 36 | public int revision() { 37 | return revision; 38 | } 39 | 40 | public boolean supported() { 41 | return supported; 42 | } 43 | 44 | private static final List SUPPORTED = unmodifiableList(asList(ZMTP10, ZMTP20)); 45 | 46 | public static boolean isSupported(final ZMTPVersion version) { 47 | return SUPPORTED.contains(version); 48 | } 49 | 50 | public static boolean isSupported(final int revision) { 51 | for (final ZMTPVersion version : SUPPORTED) { 52 | if (version.revision == revision) { 53 | return version.supported; 54 | } 55 | } 56 | return false; 57 | } 58 | 59 | public static List supportedVersions() { 60 | return SUPPORTED; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/netty4/handler/codec/zmtp/ZMTPWireFormat.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.spotify.netty4.handler.codec.zmtp; 18 | 19 | import io.netty.buffer.ByteBuf; 20 | 21 | interface ZMTPWireFormat { 22 | 23 | int frameLength(int content); 24 | 25 | Header header(); 26 | 27 | interface Header { 28 | 29 | void set(int maxLength, int length, boolean more); 30 | 31 | void write(ByteBuf out); 32 | 33 | boolean read(ByteBuf in) throws ZMTPParsingException; 34 | 35 | long length(); 36 | 37 | boolean more(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/netty4/handler/codec/zmtp/ZMTPWireFormats.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.spotify.netty4.handler.codec.zmtp; 18 | 19 | class ZMTPWireFormats { 20 | 21 | static ZMTPWireFormat wireFormat(final ZMTPVersion version) { 22 | switch (version) { 23 | case ZMTP10: 24 | return new ZMTP10WireFormat(); 25 | case ZMTP20: 26 | return new ZMTP20WireFormat(); 27 | default: 28 | throw new IllegalArgumentException("Unsupported version: " + version); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/netty4/handler/codec/zmtp/ZMTPWriter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2014 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.spotify.netty4.handler.codec.zmtp; 18 | 19 | import io.netty.buffer.ByteBuf; 20 | 21 | import static java.lang.Math.min; 22 | 23 | /** 24 | * A writer for encoding ZMTP frames onto a {@link ByteBuf}. 25 | */ 26 | public class ZMTPWriter { 27 | 28 | private final ZMTPWireFormat.Header header; 29 | 30 | private ByteBuf buf; 31 | private int frameSize; 32 | private int headerIndex; 33 | private int contentIndex; 34 | 35 | ZMTPWriter(final ZMTPWireFormat wireFormat) { 36 | this(wireFormat.header()); 37 | } 38 | 39 | ZMTPWriter(final ZMTPWireFormat.Header header) { 40 | this.header = header; 41 | } 42 | 43 | void reset(final ByteBuf buf) { 44 | this.buf = buf; 45 | } 46 | 47 | /** 48 | * Start a new ZMTP frame. 49 | * 50 | * @param size Payload size. 51 | * @param more true if more frames will be written, false if this is the last frame. 52 | * @return A {@link ByteBuf} for writing the frame payload. 53 | */ 54 | public ByteBuf frame(final int size, final boolean more) { 55 | frameSize = size; 56 | headerIndex = buf.writerIndex(); 57 | header.set(size, size, more); 58 | header.write(buf); 59 | contentIndex = buf.writerIndex(); 60 | return buf; 61 | } 62 | 63 | /** 64 | * Rewrite the ZMTP frame header, optionally writing a different size or changing the MORE flag. 65 | * This can be useful when writing a payload where estimating the exact size is expensive but an 66 | * upper bound can be cheaply computed. E.g. when writing UTF8. 67 | * 68 | * @param size New size. This must not be greater than the size provided in the call to {@link 69 | * #frame}. 70 | * @param more true if more frames will be written, false if this is the last frame. 71 | * @return A {@link ByteBuf} for writing the remainder of the frame payload, if any. The {@link 72 | * ByteBuf#writerIndex()} will be set to directly after the already written payload, or truncated 73 | * down to the end of the new smaller payload, if the written payload exceeds the new frame size. 74 | */ 75 | public ByteBuf reframe(final int size, final boolean more) { 76 | if (size > frameSize) { 77 | // Although ByteBufs grow (reallocate) dynamically, the header might end up taking more space, 78 | // forcing us to move the already written payload. We currently do not implement this. 79 | throw new IllegalArgumentException("new frame size is greater than original size"); 80 | } 81 | final int mark = buf.writerIndex(); 82 | final int written = mark - contentIndex; 83 | if (written < 0) { 84 | throw new IllegalStateException("written < 0"); 85 | } 86 | final int newIndex = contentIndex + min(written, size); 87 | buf.writerIndex(headerIndex); 88 | header.set(frameSize, size, more); 89 | header.write(buf); 90 | buf.writerIndex(newIndex); 91 | return buf; 92 | } 93 | 94 | static ZMTPWriter create(final ZMTPVersion version) { 95 | return new ZMTPWriter(ZMTPWireFormats.wireFormat(version)); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/netty4/util/BatchFlusher.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2014 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.spotify.netty4.util; 18 | 19 | import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; 20 | 21 | import io.netty.channel.Channel; 22 | import io.netty.channel.EventLoop; 23 | 24 | /** 25 | * A helper for doing opportunistic batching of netty channel flushes, allowing for a gathering 26 | * write to an underlying {@link java.nio.channels.GatheringByteChannel}, collapsing multiple writes 27 | * into fewer syscalls. 28 | */ 29 | public class BatchFlusher { 30 | 31 | private static final int DEFAULT_MAX_PENDING = 64; 32 | 33 | private final Channel channel; 34 | private final EventLoop eventLoop; 35 | private final int maxPending; 36 | 37 | private final AtomicIntegerFieldUpdater WOKEN = 38 | AtomicIntegerFieldUpdater.newUpdater(BatchFlusher.class, "woken"); 39 | @SuppressWarnings("UnusedDeclaration") private volatile int woken; 40 | 41 | private int pending; 42 | 43 | /** 44 | * Used to flush all outstanding writes in the outbound channel buffer. 45 | */ 46 | private final Runnable flush = new Runnable() { 47 | @Override 48 | public void run() { 49 | pending = 0; 50 | channel.flush(); 51 | } 52 | }; 53 | 54 | /** 55 | * Used to wake up the event loop and schedule a flush to be performed after all outstanding write 56 | * tasks are run. The outstanding write tasks must be allowed to run before performing the actual 57 | * flush in order to ensure that their payloads have been written to the outbound buffer. 58 | */ 59 | private final Runnable wakeup = new Runnable() { 60 | @Override 61 | public void run() { 62 | woken = 0; 63 | eventLoop.execute(flush); 64 | } 65 | }; 66 | 67 | public BatchFlusher(final Channel channel) { 68 | this(channel, DEFAULT_MAX_PENDING); 69 | } 70 | 71 | public BatchFlusher(final Channel channel, final int maxPending) { 72 | this.channel = channel; 73 | this.maxPending = maxPending; 74 | this.eventLoop = channel.eventLoop(); 75 | } 76 | 77 | /** 78 | * Schedule an asynchronous opportunistically batching flush. 79 | */ 80 | public void flush() { 81 | if (eventLoop.inEventLoop()) { 82 | pending++; 83 | if (pending >= maxPending) { 84 | pending = 0; 85 | channel.flush(); 86 | } 87 | } 88 | if (woken == 0 && WOKEN.compareAndSet(this, 0, 1)) { 89 | woken = 1; 90 | eventLoop.execute(wakeup); 91 | } 92 | } 93 | } -------------------------------------------------------------------------------- /src/test/java/com/spotify/netty4/handler/codec/zmtp/Buffers.java: -------------------------------------------------------------------------------- 1 | package com.spotify.netty4.handler.codec.zmtp; 2 | 3 | import io.netty.buffer.ByteBuf; 4 | import io.netty.buffer.Unpooled; 5 | 6 | class Buffers { 7 | 8 | public static byte[] bytes(int... bytes) { 9 | byte[] bs = new byte[bytes.length]; 10 | for (int i = 0; i < bytes.length; i++) { 11 | bs[i] = (byte) bytes[i]; 12 | } 13 | return bs; 14 | } 15 | 16 | public static ByteBuf buf(int... bytes) { 17 | return buf(bytes(bytes)); 18 | } 19 | 20 | public static ByteBuf buf(byte[] data) { 21 | return Unpooled.copiedBuffer(data); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/test/java/com/spotify/netty4/handler/codec/zmtp/CodecBenchmark.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2013 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.spotify.netty4.handler.codec.zmtp; 18 | 19 | import com.google.common.collect.Lists; 20 | 21 | import org.openjdk.jmh.annotations.Benchmark; 22 | import org.openjdk.jmh.annotations.Scope; 23 | import org.openjdk.jmh.annotations.State; 24 | import org.openjdk.jmh.infra.Blackhole; 25 | import org.openjdk.jmh.runner.Runner; 26 | import org.openjdk.jmh.runner.RunnerException; 27 | import org.openjdk.jmh.runner.options.Options; 28 | import org.openjdk.jmh.runner.options.OptionsBuilder; 29 | 30 | import java.util.List; 31 | 32 | import io.netty.buffer.ByteBuf; 33 | import io.netty.buffer.PooledByteBufAllocator; 34 | import io.netty.channel.ChannelHandlerContext; 35 | import io.netty.util.ReferenceCountUtil; 36 | 37 | import static com.spotify.netty4.handler.codec.zmtp.ZMTPVersion.ZMTP10; 38 | import static com.spotify.netty4.handler.codec.zmtp.ZMTPVersion.ZMTP20; 39 | import static com.spotify.netty4.handler.codec.zmtp.ZMTPWireFormats.wireFormat; 40 | 41 | // FIXME (dano): this benchmark needs to be in this package because it uses some internals 42 | 43 | @State(Scope.Benchmark) 44 | public class CodecBenchmark { 45 | 46 | private final List out = Lists.newArrayList(); 47 | 48 | private final ZMTPMessage message = ZMTPMessage.fromUTF8( 49 | "first identity frame", 50 | "second identity frame", 51 | "", 52 | "datadatadatadatadatadatadatadatadatadata", 53 | "datadatadatadatadatadatadatadatadatadata", 54 | "datadatadatadatadatadatadatadatadatadata", 55 | "datadatadatadatadatadatadatadatadatadata"); 56 | 57 | private final ZMTPFramingDecoder messageDecoderZMTP10 = 58 | new ZMTPFramingDecoder(wireFormat(ZMTP10), new ZMTPMessageDecoder()); 59 | 60 | private final ZMTPFramingDecoder messageDecoderZMTP20 = 61 | new ZMTPFramingDecoder(wireFormat(ZMTP20), new ZMTPMessageDecoder()); 62 | 63 | private final ZMTPFramingDecoder discardingDecoderZMTP10 = 64 | new ZMTPFramingDecoder(wireFormat(ZMTP10), new Discarder()); 65 | 66 | private final ZMTPFramingDecoder discardingDecoderZMTP20 = 67 | new ZMTPFramingDecoder(wireFormat(ZMTP20), new Discarder()); 68 | 69 | private final ByteBuf incomingZMTP10; 70 | private final ByteBuf incomingZMTP20; 71 | 72 | private final ZMTPMessageEncoder encoder = new ZMTPMessageEncoder(); 73 | private final ZMTPWriter writerZMTP10 = ZMTPWriter.create(ZMTP10); 74 | private final ZMTPWriter writerZMTP20 = ZMTPWriter.create(ZMTP20); 75 | 76 | private final ByteBuf tmp = PooledByteBufAllocator.DEFAULT.buffer(4096); 77 | 78 | { 79 | incomingZMTP10 = message.write(PooledByteBufAllocator.DEFAULT, ZMTP10); 80 | incomingZMTP20 = message.write(PooledByteBufAllocator.DEFAULT, ZMTP20); 81 | } 82 | 83 | @SuppressWarnings("ForLoopReplaceableByForEach") 84 | private void consumeAndRelease(final Blackhole bh, final List out) { 85 | for (int i = 0; i < out.size(); i++) { 86 | final Object o = out.get(i); 87 | bh.consume(o); 88 | ReferenceCountUtil.release(o); 89 | } 90 | out.clear(); 91 | } 92 | 93 | @Benchmark 94 | public void parsingToMessageZMTP10(final Blackhole bh) throws ZMTPParsingException { 95 | messageDecoderZMTP10.decode(null, incomingZMTP10.resetReaderIndex(), out); 96 | consumeAndRelease(bh, out); 97 | } 98 | 99 | @Benchmark 100 | public void parsingToMessageZMTP20(final Blackhole bh) throws ZMTPParsingException { 101 | messageDecoderZMTP20.decode(null, incomingZMTP20.resetReaderIndex(), out); 102 | consumeAndRelease(bh, out); 103 | } 104 | 105 | @Benchmark 106 | public void discardingZMTP10(final Blackhole bh) throws ZMTPParsingException { 107 | discardingDecoderZMTP10.decode(null, incomingZMTP10.resetReaderIndex(), out); 108 | consumeAndRelease(bh, out); 109 | } 110 | 111 | @Benchmark 112 | public void discardingZMTP20(final Blackhole bh) throws ZMTPParsingException { 113 | discardingDecoderZMTP20.decode(null, incomingZMTP20.resetReaderIndex(), out); 114 | consumeAndRelease(bh, out); 115 | } 116 | 117 | @Benchmark 118 | public Object encodingZMTP10() { 119 | writerZMTP10.reset(tmp.setIndex(0, 0)); 120 | encoder.encode(message, writerZMTP10); 121 | return tmp; 122 | } 123 | 124 | @Benchmark 125 | public Object encodingZMTP20() { 126 | writerZMTP20.reset(tmp.setIndex(0, 0)); 127 | encoder.encode(message, writerZMTP20); 128 | return tmp; 129 | } 130 | 131 | public static void main(final String... args) throws RunnerException, InterruptedException { 132 | Options opt = new OptionsBuilder() 133 | .include(".*") 134 | .forks(1) 135 | .build(); 136 | 137 | new Runner(opt).run(); 138 | } 139 | 140 | 141 | private class Discarder implements ZMTPDecoder { 142 | 143 | private int size; 144 | 145 | 146 | @Override 147 | public void header(final ChannelHandlerContext ctx, final long length, final boolean more, 148 | final List out) { 149 | this.size += size; 150 | } 151 | 152 | @Override 153 | public void content(final ChannelHandlerContext ctx, final ByteBuf data, 154 | final List out) { 155 | data.skipBytes(data.readableBytes()); 156 | } 157 | 158 | @Override 159 | public void finish(final ChannelHandlerContext ctx, final List out) { 160 | } 161 | 162 | @Override 163 | public void close() { 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/test/java/com/spotify/netty4/handler/codec/zmtp/EndToEndTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2014 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.spotify.netty4.handler.codec.zmtp; 18 | 19 | import com.google.common.collect.Queues; 20 | 21 | import org.junit.Test; 22 | 23 | import java.net.InetSocketAddress; 24 | import java.net.SocketAddress; 25 | import java.util.concurrent.BlockingQueue; 26 | 27 | import io.netty.bootstrap.Bootstrap; 28 | import io.netty.bootstrap.ServerBootstrap; 29 | import io.netty.channel.Channel; 30 | import io.netty.channel.ChannelHandler; 31 | import io.netty.channel.ChannelHandlerContext; 32 | import io.netty.channel.ChannelInboundHandlerAdapter; 33 | import io.netty.channel.ChannelInitializer; 34 | import io.netty.channel.nio.NioEventLoopGroup; 35 | import io.netty.channel.socket.nio.NioServerSocketChannel; 36 | import io.netty.channel.socket.nio.NioSocketChannel; 37 | import io.netty.util.ReferenceCountUtil; 38 | 39 | import static com.spotify.netty4.handler.codec.zmtp.ZMTPProtocols.ZMTP10; 40 | import static com.spotify.netty4.handler.codec.zmtp.ZMTPProtocols.ZMTP20; 41 | import static com.spotify.netty4.handler.codec.zmtp.ZMTPSocketType.DEALER; 42 | import static com.spotify.netty4.handler.codec.zmtp.ZMTPSocketType.ROUTER; 43 | import static java.util.concurrent.TimeUnit.SECONDS; 44 | import static org.hamcrest.core.Is.is; 45 | import static org.hamcrest.core.IsNull.notNullValue; 46 | import static org.hamcrest.core.IsNull.nullValue; 47 | import static org.junit.Assert.assertThat; 48 | 49 | public class EndToEndTest { 50 | 51 | private static final InetSocketAddress ANY_PORT = new InetSocketAddress("127.0.0.1", 0); 52 | 53 | private Channel bind(final SocketAddress address, final ChannelHandler codec, 54 | final ChannelHandler handler) { 55 | final ServerBootstrap bootstrap = new ServerBootstrap(); 56 | bootstrap.group(new NioEventLoopGroup(1), new NioEventLoopGroup()); 57 | bootstrap.channel(NioServerSocketChannel.class); 58 | bootstrap.childHandler(new ChannelInitializer() { 59 | @Override 60 | protected void initChannel(final NioSocketChannel ch) throws Exception { 61 | ch.pipeline().addLast(codec, handler); 62 | } 63 | }); 64 | return bootstrap.bind(address).awaitUninterruptibly().channel(); 65 | } 66 | 67 | private Channel connect(final SocketAddress address, final ChannelHandler codec, 68 | final ChannelHandler handler) { 69 | final Bootstrap bootstrap = new Bootstrap(); 70 | bootstrap.group(new NioEventLoopGroup()); 71 | bootstrap.channel(NioSocketChannel.class); 72 | bootstrap.handler(new ChannelInitializer() { 73 | @Override 74 | protected void initChannel(final NioSocketChannel ch) throws Exception { 75 | ch.pipeline().addLast(codec, handler); 76 | } 77 | }); 78 | return bootstrap.connect(address).awaitUninterruptibly().channel(); 79 | } 80 | 81 | private void testRequestReply(final ChannelHandler serverCodec, final ChannelHandler clientCodec) 82 | throws InterruptedException { 83 | 84 | // Set up server & client 85 | final Handler server = new Handler(); 86 | final Handler client = new Handler(); 87 | final SocketAddress address = bind(ANY_PORT, serverCodec, server).localAddress(); 88 | final Channel clientChannel = connect(address, clientCodec, client); 89 | final Channel serverConnectedChannel = server.connected.poll(5, SECONDS); 90 | assertThat(serverConnectedChannel, is(notNullValue())); 91 | 92 | // Make sure there's no left over messages/connections on the wires 93 | Thread.sleep(100); 94 | assertThat("unexpected server message", server.messages.poll(), is(nullValue())); 95 | assertThat("unexpected client message", client.messages.poll(), is(nullValue())); 96 | assertThat("unexpected server connection", server.connected.poll(), is(nullValue())); 97 | 98 | // Send and receive request 99 | final ZMTPMessage helloWorldMessage = ZMTPMessage.fromUTF8("", "hello", "world"); 100 | clientChannel.writeAndFlush(helloWorldMessage.retain()); 101 | final ZMTPMessage receivedRequest = server.messages.poll(5, SECONDS); 102 | assertThat(receivedRequest, is(notNullValue())); 103 | assertThat(receivedRequest, is(helloWorldMessage)); 104 | helloWorldMessage.release(); 105 | 106 | // Send and receive reply 107 | final ZMTPMessage fooBarMessage = ZMTPMessage.fromUTF8("", "foo", "bar"); 108 | serverConnectedChannel.writeAndFlush(fooBarMessage.retain()); 109 | final ZMTPMessage receivedReply = client.messages.poll(5, SECONDS); 110 | assertThat(receivedReply, is(notNullValue())); 111 | assertThat(receivedReply, is(fooBarMessage)); 112 | fooBarMessage.release(); 113 | 114 | // Make sure there's no left over messages/connections on the wires 115 | Thread.sleep(100); 116 | assertThat("unexpected server message", server.messages.poll(), is(nullValue())); 117 | assertThat("unexpected client message", client.messages.poll(), is(nullValue())); 118 | assertThat("unexpected server connection", server.connected.poll(), is(nullValue())); 119 | } 120 | 121 | @Test 122 | public void test_ZMTP10_Router_VS_ZMTP10_Dealer() throws InterruptedException { 123 | final ZMTPCodec server = ZMTPCodec.builder() 124 | .protocol(ZMTP10) 125 | .socketType(ROUTER) 126 | .build(); 127 | 128 | final ZMTPCodec client = ZMTPCodec.builder() 129 | .protocol(ZMTP10) 130 | .socketType(DEALER) 131 | .build(); 132 | 133 | testRequestReply(server, client); 134 | } 135 | 136 | @Test 137 | public void test_ZMTP20_Interop_Router_VS_ZMTP20_Interop_Dealer() throws InterruptedException { 138 | final ZMTPCodec server = ZMTPCodec.builder() 139 | .protocol(ZMTP20) 140 | .interop(true) 141 | .socketType(ROUTER) 142 | .build(); 143 | 144 | final ZMTPCodec client = ZMTPCodec.builder() 145 | .protocol(ZMTP20) 146 | .interop(true) 147 | .socketType(DEALER) 148 | .build(); 149 | 150 | testRequestReply(server, client); 151 | } 152 | 153 | @Test 154 | public void test_ZMTP20_Interop_Router_VS_ZMTP10_Dealer() throws InterruptedException { 155 | final ZMTPCodec server = ZMTPCodec.builder() 156 | .protocol(ZMTP20) 157 | .interop(true) 158 | .socketType(ROUTER) 159 | .build(); 160 | 161 | final ZMTPCodec client = ZMTPCodec.builder() 162 | .protocol(ZMTP10) 163 | .socketType(DEALER) 164 | .build(); 165 | 166 | testRequestReply(server, client); 167 | } 168 | 169 | @Test 170 | public void test_ZMTP20_NoInterop_Router_VS_ZMTP20_NoInterop_Dealer() throws InterruptedException { 171 | final ZMTPCodec server = ZMTPCodec.builder() 172 | .protocol(ZMTP20) 173 | .interop(false) 174 | .socketType(ROUTER) 175 | .build(); 176 | 177 | final ZMTPCodec client = ZMTPCodec.builder() 178 | .protocol(ZMTP20) 179 | .interop(false) 180 | .socketType(DEALER) 181 | .build(); 182 | 183 | testRequestReply(server, client); 184 | } 185 | 186 | private static class Handler extends ChannelInboundHandlerAdapter { 187 | 188 | private final BlockingQueue connected = Queues.newLinkedBlockingQueue(); 189 | private final BlockingQueue messages = Queues.newLinkedBlockingQueue(); 190 | 191 | @Override 192 | public void channelActive(final ChannelHandlerContext ctx) throws Exception { 193 | super.channelActive(ctx); 194 | connected.add(ctx.channel()); 195 | } 196 | 197 | @Override 198 | public void channelRead(final ChannelHandlerContext ctx, final Object msg) throws Exception { 199 | ReferenceCountUtil.releaseLater(msg); 200 | messages.put((ZMTPMessage) msg); 201 | } 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/test/java/com/spotify/netty4/handler/codec/zmtp/Fragmenter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2013 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.spotify.netty4.handler.codec.zmtp; 18 | 19 | class Fragmenter { 20 | 21 | interface Consumer { 22 | void fragments(int[] limits, int count) throws Exception; 23 | } 24 | 25 | private final int[] limits; 26 | private final int length; 27 | 28 | public Fragmenter(final int length) { 29 | this.limits = new int[length]; 30 | this.length = length; 31 | } 32 | 33 | public void fragment(final Consumer consumer) throws Exception { 34 | fragment(consumer, 0, 0); 35 | } 36 | 37 | private void fragment(final Consumer consumer, final int count, final int limit) 38 | throws Exception { 39 | if (limit == length) { 40 | consumer.fragments(limits, count); 41 | return; 42 | } 43 | 44 | for (int o = limit + 1; o <= length; o++) { 45 | limits[count] = o; 46 | fragment(consumer, count + 1, o); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/test/java/com/spotify/netty4/handler/codec/zmtp/FragmenterTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2013 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.spotify.netty4.handler.codec.zmtp; 18 | 19 | import com.google.common.collect.Lists; 20 | 21 | import org.junit.Test; 22 | 23 | import java.util.Arrays; 24 | import java.util.List; 25 | 26 | import static org.junit.Assert.assertArrayEquals; 27 | import static org.junit.Assert.assertEquals; 28 | 29 | public class FragmenterTest { 30 | 31 | private static final int SIZE = 4; 32 | 33 | private static final int[][] EXPECTED = { 34 | {1, 2, 3, 4}, 35 | {1, 2, 4}, 36 | {1, 3, 4}, 37 | {1, 4}, 38 | {2, 3, 4}, 39 | {2, 4}, 40 | {3, 4}, 41 | {4}, 42 | }; 43 | 44 | @Test 45 | public void test() throws Exception { 46 | final List output = Lists.newArrayList(); 47 | final Fragmenter fragmenter = new Fragmenter(SIZE); 48 | fragmenter.fragment(new Fragmenter.Consumer() { 49 | @Override 50 | public void fragments(final int[] limits, final int count) { 51 | output.add(Arrays.copyOf(limits, count)); 52 | } 53 | }); 54 | 55 | assertEquals(EXPECTED.length, output.size()); 56 | for (int i = 0; i < EXPECTED.length; i++) { 57 | assertArrayEquals(EXPECTED[i], output.get(i)); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/test/java/com/spotify/netty4/handler/codec/zmtp/HandshakeTest.java: -------------------------------------------------------------------------------- 1 | package com.spotify.netty4.handler.codec.zmtp; 2 | 3 | 4 | import org.junit.Test; 5 | import org.junit.runner.RunWith; 6 | import org.mockito.Mock; 7 | import org.mockito.runners.MockitoJUnitRunner; 8 | 9 | import java.nio.ByteBuffer; 10 | 11 | import io.netty.buffer.ByteBuf; 12 | import io.netty.buffer.Unpooled; 13 | import io.netty.channel.ChannelHandlerContext; 14 | 15 | import static com.spotify.netty4.handler.codec.zmtp.Buffers.buf; 16 | import static com.spotify.netty4.handler.codec.zmtp.ZMTPSocketType.PUB; 17 | import static com.spotify.netty4.handler.codec.zmtp.ZMTPSocketType.REQ; 18 | import static com.spotify.netty4.handler.codec.zmtp.ZMTPSocketType.ROUTER; 19 | import static com.spotify.netty4.handler.codec.zmtp.ZMTPSocketType.SUB; 20 | import static com.spotify.netty4.handler.codec.zmtp.ZMTPVersion.ZMTP10; 21 | import static com.spotify.netty4.handler.codec.zmtp.ZMTPVersion.ZMTP20; 22 | import static io.netty.util.CharsetUtil.UTF_8; 23 | import static org.hamcrest.Matchers.is; 24 | import static org.hamcrest.Matchers.notNullValue; 25 | import static org.hamcrest.Matchers.nullValue; 26 | import static org.junit.Assert.assertEquals; 27 | import static org.junit.Assert.assertThat; 28 | import static org.junit.Assert.fail; 29 | import static org.mockito.Mockito.verify; 30 | import static org.mockito.Mockito.verifyNoMoreInteractions; 31 | import static org.mockito.Mockito.verifyZeroInteractions; 32 | 33 | /** 34 | * Tests the handshake protocol 35 | */ 36 | @RunWith(MockitoJUnitRunner.class) 37 | public class HandshakeTest { 38 | 39 | private static final ByteBuffer FOO = UTF_8.encode("foo"); 40 | private static final ByteBuffer BAR = UTF_8.encode("bar"); 41 | 42 | @Mock ChannelHandlerContext ctx; 43 | 44 | @Test 45 | public void testGreeting() { 46 | ZMTPHandshaker h = new ZMTP10Protocol.Handshaker(FOO); 47 | assertThat(h.greeting(), is(buf(0x04, 0x00, 0x66, 0x6f, 0x6f))); 48 | 49 | h = new ZMTP10Protocol.Handshaker(ByteBuffer.allocate(0)); 50 | assertThat(h.greeting(), is(buf(0x01, 0x00))); 51 | 52 | h = new ZMTP20Protocol.Handshaker(SUB, FOO, true); 53 | assertThat(h.greeting(), is(buf(0xff, 0, 0, 0, 0, 0, 0, 0, 4, 0x7f))); 54 | 55 | h = new ZMTP20Protocol.Handshaker(REQ, FOO, false); 56 | assertThat(h.greeting(), is(buf(0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0x7f, 57 | 0x01, 0x03, 0x00, 3, 0x66, 0x6f, 0x6f))); 58 | } 59 | 60 | @Test 61 | public void test1to1Handshake() throws Exception { 62 | final ZMTP10Protocol.Handshaker h = new ZMTP10Protocol.Handshaker(FOO); 63 | assertThat(h.greeting(), is(buf(0x04, 0x00, 0x66, 0x6f, 0x6f))); 64 | final ZMTPHandshake handshake = h.handshake(buf(0x04, 0x00, 0x62, 0x61, 0x72), ctx); 65 | assertThat(handshake, is(notNullValue())); 66 | verifyZeroInteractions(ctx); 67 | assertEquals(ZMTPHandshake.of(ZMTP10, BAR, null), handshake); 68 | } 69 | 70 | @Test 71 | public void test2InteropTo1Handshake() throws Exception { 72 | ZMTPHandshaker h = new ZMTP20Protocol.Handshaker(ROUTER, FOO, true); 73 | assertThat(h.greeting(), is(buf(0xff, 0, 0, 0, 0, 0, 0, 0, 0x04, 0x7f))); 74 | ZMTPHandshake handshake = h.handshake(buf(0x04, 0x00, 0x62, 0x61, 0x72), ctx); 75 | assertThat(handshake, is(notNullValue())); 76 | verify(ctx).writeAndFlush(buf(0x66, 0x6f, 0x6f)); 77 | assertEquals(ZMTPHandshake.of(ZMTP10, BAR, null), handshake); 78 | } 79 | 80 | @Test 81 | public void test2InteropTo2InteropHandshake() throws Exception { 82 | ZMTPHandshaker h = new ZMTP20Protocol.Handshaker(PUB, FOO, true); 83 | assertThat(h.greeting(), is(buf(0xff, 0, 0, 0, 0, 0, 0, 0, 0x04, 0x7f))); 84 | ZMTPHandshake handshake; 85 | handshake = h.handshake(buf(0xff, 0, 0, 0, 0, 0, 0, 0, 0x04, 0x7f), ctx); 86 | assertThat(handshake, is(nullValue())); 87 | verify(ctx).writeAndFlush(buf(0x01, 0x01, 0x00, 0x03, 0x66, 0x6f, 0x6f)); 88 | handshake = h.handshake(buf(0x01, 0x01, 0x00, 0x03, 0x62, 0x61, 0x72), ctx); 89 | assertThat(handshake, is(notNullValue())); 90 | verifyNoMoreInteractions(ctx); 91 | assertEquals(ZMTPHandshake.of(ZMTPVersion.ZMTP20, BAR, PUB), handshake); 92 | } 93 | 94 | @Test 95 | public void test2InteropTo2Handshake() throws Exception { 96 | ZMTPHandshaker h = new ZMTP20Protocol.Handshaker(PUB, FOO, true); 97 | assertThat(h.greeting(), is(buf(0xff, 0, 0, 0, 0, 0, 0, 0, 0x04, 0x7f))); 98 | ByteBuf cb = buf(0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0x7f, 0x01, 0x01, 0x00, 0x03, 0x62, 0x61, 0x72); 99 | ZMTPHandshake handshake; 100 | handshake = h.handshake(cb, ctx); 101 | assertThat(handshake, is(nullValue())); 102 | verify(ctx).writeAndFlush(buf(0x01, 0x01, 0x00, 0x03, 0x66, 0x6f, 0x6f)); 103 | handshake = h.handshake(cb, ctx); 104 | assertThat(handshake, is(notNullValue())); 105 | verifyNoMoreInteractions(ctx); 106 | assertEquals(ZMTPHandshake.of(ZMTPVersion.ZMTP20, BAR, PUB), handshake); 107 | } 108 | 109 | @Test 110 | public void test2To2InteropHandshake() throws Exception { 111 | ZMTPHandshaker h = new ZMTP20Protocol.Handshaker(PUB, FOO, false); 112 | assertThat(h.greeting(), is(buf(0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0x7f, 0x1, 0x1, 0, 0x3, 0x66, 0x6f, 0x6f))); 113 | 114 | try { 115 | h.handshake(buf(0xff, 0, 0, 0, 0, 0, 0, 0, 0x4, 0x7f), ctx); 116 | fail("not enough data in greeting (because compat mode) should have thrown exception"); 117 | } catch (IndexOutOfBoundsException e) { 118 | // expected 119 | } 120 | ZMTPHandshake handshake = h.handshake( 121 | buf(0xff, 0, 0, 0, 0, 0, 0, 0, 0x4, 0x7f, 0x1, 0x1, 0, 0x03, 0x62, 0x61, 0x72), ctx); 122 | assertThat(handshake, is(notNullValue())); 123 | assertEquals(ZMTPHandshake.of(ZMTPVersion.ZMTP20, BAR, PUB), handshake); 124 | } 125 | 126 | @Test 127 | public void test2To2Handshake() throws Exception { 128 | ZMTPHandshaker h = new ZMTP20Protocol.Handshaker(PUB, FOO, false); 129 | assertThat(h.greeting(), is(buf(0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0x7f, 0x1, 0x1, 0, 0x3, 0x66, 0x6f, 0x6f))); 130 | ZMTPHandshake handshake = h.handshake(buf( 131 | 0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0x7f, 0x1, 0x1, 0, 0x03, 0x62, 0x61, 0x72), ctx); 132 | assertThat(handshake, is(notNullValue())); 133 | assertEquals(ZMTPHandshake.of(ZMTPVersion.ZMTP20, BAR, PUB), handshake); 134 | } 135 | 136 | 137 | @Test 138 | public void test2To1Handshake() { 139 | ZMTPHandshaker h = new ZMTP20Protocol.Handshaker(PUB, FOO, false); 140 | assertThat(h.greeting(), is(buf(0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0x7f, 0x1, 0x1, 0, 0x3, 0x66, 0x6f, 0x6f))); 141 | try { 142 | assertThat(h.handshake(buf(0x04, 0, 0x62, 0x61, 0x72), ctx), is(nullValue())); 143 | fail("An ZMTP/1 greeting is invalid in plain ZMTP/2. Should have thrown exception"); 144 | } catch (ZMTPException e) { 145 | // pass 146 | } 147 | } 148 | 149 | @Test 150 | public void test2To2CompatTruncated() throws Exception { 151 | final ByteBuffer identity = UTF_8.encode("identity"); 152 | ZMTP20Protocol.Handshaker h = new ZMTP20Protocol.Handshaker(PUB, identity, true); 153 | assertThat(h.greeting(), is(buf(0xff, 0, 0, 0, 0, 0, 0, 0, 9, 0x7f))); 154 | ZMTPHandshake handshake = h.handshake(buf(0xff, 0, 0, 0, 0, 0, 0, 0, 1, 0x7f, 1, 5), ctx); 155 | assertThat(handshake, is(nullValue())); 156 | verify(ctx).writeAndFlush(buf(1, 1, 0, 8, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79)); 157 | } 158 | 159 | @Test 160 | public void testReadZMTP2Greeting() throws Exception { 161 | final ByteBuf in = buf(0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0x7f, 0x01, 0x02, 0x00, 0x01, 0x61); 162 | final ZMTP20WireFormat.Greeting greeting = ZMTP20WireFormat.readGreeting(in); 163 | assertThat(greeting.identity(), is(UTF_8.encode("a"))); 164 | } 165 | 166 | @Test 167 | public void testReadZMTP1RemoteIdentity() throws Exception { 168 | ByteBuffer identity = ZMTP10WireFormat.readIdentity(buf(0x04, 0x00, 0x62, 0x61, 0x72)); 169 | assertThat(identity, is(notNullValue())); 170 | assertEquals(BAR, identity); 171 | 172 | // anonymous handshake 173 | identity = ZMTP10WireFormat.readIdentity(buf(0x01, 0x00)); 174 | assertThat(identity, is(notNullValue())); 175 | assert identity != null; 176 | assertThat(identity.remaining(), is(0)); 177 | } 178 | 179 | @Test 180 | public void testTypeToConst() { 181 | assertEquals(8, ZMTPSocketType.PUSH.ordinal()); 182 | } 183 | 184 | @Test 185 | public void testDetectProtocolVersion() { 186 | try { 187 | ZMTP20WireFormat.detectProtocolVersion(Unpooled.wrappedBuffer(new byte[0])); 188 | fail("Should have thrown IndexOutOfBoundsException"); 189 | } catch (IndexOutOfBoundsException e) { 190 | // ignore 191 | } 192 | try { 193 | ZMTP20WireFormat.detectProtocolVersion(buf(0xff, 0, 0, 0)); 194 | fail("Should have thrown IndexOutOfBoundsException"); 195 | } catch (IndexOutOfBoundsException e) { 196 | // ignore 197 | } 198 | 199 | assertEquals(ZMTP10, ZMTP20WireFormat.detectProtocolVersion(buf(0x07))); 200 | assertEquals(ZMTP10, ZMTP20WireFormat 201 | .detectProtocolVersion(buf(0xff, 0, 0, 0, 0, 0, 0, 0, 1, 0))); 202 | assertEquals(ZMTP20, ZMTP20WireFormat 203 | .detectProtocolVersion(buf(0xff, 0, 0, 0, 0, 0, 0, 0, 1, 1))); 204 | } 205 | 206 | @Test 207 | public void testReadZMTP2GreetingMalformed() { 208 | try { 209 | ByteBuf in = buf(0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0x7f, 0x01, 0x02, 0xf0, 0x01, 0x61); 210 | ZMTP20WireFormat.readGreeting(in); 211 | fail("13th byte is not 0x00, should throw exception"); 212 | } catch (ZMTPException e) { 213 | // pass 214 | } 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /src/test/java/com/spotify/netty4/handler/codec/zmtp/ListenableFutureAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.spotify.netty4.handler.codec.zmtp; 18 | 19 | import com.google.common.util.concurrent.AbstractFuture; 20 | import com.google.common.util.concurrent.ListenableFuture; 21 | 22 | import io.netty.util.concurrent.Future; 23 | import io.netty.util.concurrent.GenericFutureListener; 24 | 25 | public class ListenableFutureAdapter extends AbstractFuture implements GenericFutureListener> { 26 | 27 | static ListenableFuture listenable(final Future future) { 28 | final ListenableFutureAdapter adapter = new ListenableFutureAdapter(); 29 | future.addListener(adapter); 30 | return adapter; 31 | } 32 | 33 | @Override 34 | public void operationComplete(final Future future) throws Exception { 35 | if (future.isSuccess()) { 36 | set(future.getNow()); 37 | } else { 38 | setException(future.cause()); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/test/java/com/spotify/netty4/handler/codec/zmtp/PipelineTester.java: -------------------------------------------------------------------------------- 1 | package com.spotify.netty4.handler.codec.zmtp; 2 | 3 | import java.util.concurrent.BlockingQueue; 4 | import java.util.concurrent.LinkedBlockingQueue; 5 | import java.util.concurrent.atomic.AtomicInteger; 6 | 7 | import io.netty.bootstrap.Bootstrap; 8 | import io.netty.bootstrap.ServerBootstrap; 9 | import io.netty.buffer.ByteBuf; 10 | import io.netty.channel.Channel; 11 | import io.netty.channel.ChannelHandler; 12 | import io.netty.channel.ChannelHandlerContext; 13 | import io.netty.channel.ChannelInboundHandlerAdapter; 14 | import io.netty.channel.ChannelInitializer; 15 | import io.netty.channel.local.LocalAddress; 16 | import io.netty.channel.local.LocalChannel; 17 | import io.netty.channel.local.LocalEventLoopGroup; 18 | import io.netty.channel.local.LocalServerChannel; 19 | import io.netty.util.ReferenceCountUtil; 20 | 21 | /** 22 | * Tests the behaviours of ChannelPipelines, using the local transport method. 23 | * A PipelineTester instance has two ends named server and client where the server is the 24 | * position furthest upstream in the pipeline, and client is outside of the pipeline reading from 25 | * and writing to it. 26 | */ 27 | class PipelineTester { 28 | private final BlockingQueue emittedOutside = new LinkedBlockingQueue(); 29 | private final BlockingQueue emittedInside = new LinkedBlockingQueue(); 30 | private Channel outerChannel = null; 31 | private Channel innerChannel = null; 32 | 33 | private static final AtomicInteger port = new AtomicInteger(); 34 | 35 | /** 36 | * Constructs a server using pipeline and a client communicating with it. 37 | * 38 | * @param handlers Server channel handlers. 39 | */ 40 | public PipelineTester(final ChannelHandler... handlers) { 41 | final LocalAddress address = new LocalAddress("pipeline-tester-" + port.incrementAndGet()); 42 | 43 | final ServerBootstrap sb = new ServerBootstrap(); 44 | sb.group(new LocalEventLoopGroup(1), new LocalEventLoopGroup()); 45 | sb.channel(LocalServerChannel.class); 46 | sb.childHandler(new ChannelInitializer() { 47 | @Override 48 | protected void initChannel(final LocalChannel ch) throws Exception { 49 | ch.pipeline().addLast(handlers); 50 | ch.pipeline().addLast("pipelineTesterEndpoint", new ChannelInboundHandlerAdapter() { 51 | 52 | @Override 53 | public void channelRead(final ChannelHandlerContext ctx, final Object msg) 54 | throws Exception { 55 | ReferenceCountUtil.releaseLater(msg); 56 | emittedInside.put(msg); 57 | } 58 | 59 | @Override 60 | public void channelActive(final ChannelHandlerContext ctx) throws Exception { 61 | innerChannel = ctx.channel(); 62 | } 63 | }); 64 | } 65 | }); 66 | sb.bind(address).awaitUninterruptibly(); 67 | 68 | final Bootstrap cb = new Bootstrap(); 69 | cb.group(new LocalEventLoopGroup()); 70 | cb.channel(LocalChannel.class); 71 | cb.handler(new ChannelInitializer() { 72 | @Override 73 | protected void initChannel(final LocalChannel ch) throws Exception { 74 | ch.pipeline().addLast("1", new ChannelInboundHandlerAdapter() { 75 | @Override 76 | public void channelActive(final ChannelHandlerContext ctx) throws Exception { 77 | outerChannel = ctx.channel(); 78 | } 79 | 80 | @Override 81 | public void channelRead(final ChannelHandlerContext ctx, final Object msg) 82 | throws Exception { 83 | ReferenceCountUtil.releaseLater(msg); 84 | emittedOutside.put((ByteBuf) msg); 85 | } 86 | }); 87 | } 88 | }); 89 | 90 | cb.connect(address).awaitUninterruptibly(); 91 | } 92 | 93 | /** 94 | * Read the ChannelBuffers emitted from this pipeline in the client end. 95 | * 96 | * @return a ByteBuf from the the client FIFO 97 | */ 98 | public ByteBuf readClient() { 99 | try { 100 | return emittedOutside.take(); 101 | } catch (InterruptedException e) { 102 | throw new Error(e); 103 | } 104 | } 105 | 106 | /** 107 | * Write a ByteBuf to the pipeline from the client end. 108 | * 109 | * @param buf the ByteBuf to write 110 | */ 111 | public void writeClient(ByteBuf buf) { 112 | outerChannel.writeAndFlush(buf); 113 | } 114 | 115 | /** 116 | * Read an Object from the server end of the pipeline. This can be a ByteBuf, or, if 117 | * there is a FrameDecoder some sort of pojo. 118 | * 119 | * @return an Object read from the server end. 120 | */ 121 | public Object readServer() { 122 | try { 123 | return emittedInside.take(); 124 | } catch (InterruptedException e) { 125 | throw new Error(e); 126 | } 127 | } 128 | 129 | /** 130 | * Write a Object to the server end of the pipeline. 131 | * 132 | * @param message the Object to be written 133 | */ 134 | public void writeServer(Object message) { 135 | innerChannel.writeAndFlush(message); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/test/java/com/spotify/netty4/handler/codec/zmtp/PipelineTests.java: -------------------------------------------------------------------------------- 1 | package com.spotify.netty4.handler.codec.zmtp; 2 | 3 | import org.junit.Test; 4 | 5 | import io.netty.buffer.ByteBuf; 6 | import io.netty.buffer.Unpooled; 7 | import io.netty.channel.ChannelHandlerContext; 8 | import io.netty.channel.ChannelInboundHandlerAdapter; 9 | 10 | import static com.google.common.base.Strings.repeat; 11 | import static com.spotify.netty4.handler.codec.zmtp.Buffers.buf; 12 | import static com.spotify.netty4.handler.codec.zmtp.Buffers.bytes; 13 | import static com.spotify.netty4.handler.codec.zmtp.ZMTPProtocols.ZMTP10; 14 | import static com.spotify.netty4.handler.codec.zmtp.ZMTPProtocols.ZMTP20; 15 | import static com.spotify.netty4.handler.codec.zmtp.ZMTPSocketType.REQ; 16 | import static io.netty.util.CharsetUtil.UTF_8; 17 | import static org.hamcrest.Matchers.is; 18 | import static org.junit.Assert.assertThat; 19 | 20 | /** 21 | * These tests has a full pipeline setup. 22 | */ 23 | public class PipelineTests { 24 | 25 | // FIXME (dano): Tests are hardcoded to use a message 260 bytes long 26 | private static final byte[] LONG_MSG = repeat("data", 100).substring(0, 260).getBytes(UTF_8); 27 | 28 | /** 29 | * First let's just exercise the PipelineTester a bit. 30 | */ 31 | @Test 32 | public void testPipelineTester() { 33 | final ByteBuf buf = Unpooled.copiedBuffer("Hello, world", UTF_8); 34 | 35 | final PipelineTester pipelineTester = new PipelineTester(new ChannelInboundHandlerAdapter() { 36 | 37 | @Override 38 | public void channelActive(final ChannelHandlerContext ctx) throws Exception { 39 | super.channelActive(ctx); 40 | ctx.channel().writeAndFlush(buf); 41 | } 42 | }); 43 | assertThat(pipelineTester.readClient(), is(buf)); 44 | 45 | final ByteBuf foo = Unpooled.copiedBuffer("foo", UTF_8); 46 | pipelineTester.writeClient(foo.retain()); 47 | 48 | assertThat(foo, is(pipelineTester.readServer())); 49 | 50 | final ByteBuf bar = Unpooled.copiedBuffer("bar", UTF_8); 51 | pipelineTester.writeServer(bar.retain()); 52 | assertThat(bar, is(pipelineTester.readClient())); 53 | } 54 | 55 | @Test 56 | public void testZMTPPipeline() { 57 | 58 | final PipelineTester pt = new PipelineTester( 59 | ZMTPCodec.builder() 60 | .protocol(ZMTP20) 61 | .socketType(REQ) 62 | .localIdentity("foo") 63 | .build()); 64 | assertThat(buf(0xff, 0, 0, 0, 0, 0, 0, 0, 4, 0x7f), is(pt.readClient())); 65 | pt.writeClient(buf(0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0x7f, 1, 4, 0, 1, 0x63)); 66 | assertThat(buf(1, 3, 0, 3, 0x66, 0x6f, 0x6f), is(pt.readClient())); 67 | 68 | pt.writeClient(buf(1, 1, 0x65, 1, 0, 0, 1, 0x62)); 69 | ZMTPMessage m = (ZMTPMessage) pt.readServer(); 70 | 71 | assertThat(m.size(), is(3)); 72 | assertThat(buf(0x65), is(m.frame(0))); 73 | assertThat(buf(), is(m.frame(1))); 74 | assertThat(buf(0x62), is(m.frame(2))); 75 | } 76 | 77 | @Test 78 | public void testZMTPPipelineFragmented() { 79 | final PipelineTester pt = new PipelineTester( 80 | ZMTPCodec.builder() 81 | .protocol(ZMTP20) 82 | .socketType(REQ) 83 | .localIdentity("foo") 84 | .build()); 85 | 86 | assertThat(buf(0xff, 0, 0, 0, 0, 0, 0, 0, 4, 0x7f), is(pt.readClient())); 87 | pt.writeClient(buf(0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0x7f, 1, 4, 0, 1, 0x63, 1, 1, 0x65, 1)); 88 | assertThat(buf(1, 3, 0, 3, 0x66, 0x6f, 0x6f), is(pt.readClient())); 89 | 90 | pt.writeClient(buf(0, 0, 1, 0x62)); 91 | ZMTPMessage m = (ZMTPMessage) pt.readServer(); 92 | 93 | assertThat(m.size(), is(3)); 94 | assertThat(buf(0x65), is(m.frame(0))); 95 | assertThat(buf(), is(m.frame(1))); 96 | assertThat(buf(0x62), is(m.frame(2))); 97 | } 98 | 99 | @Test 100 | public void testZMTP1PipelineLongMessage() { 101 | final PipelineTester pt = new PipelineTester( 102 | ZMTPCodec.builder() 103 | .protocol(ZMTP10) 104 | .socketType(REQ) 105 | .localIdentity("foo") 106 | .build()); 107 | 108 | assertThat(buf(0x04, 0, 0x66, 0x6f, 0x6f), is(pt.readClient())); 109 | 110 | ByteBuf cb = Unpooled.buffer(); 111 | // handshake: length + flag + client identity octets "BAR" 112 | cb.writeBytes(buf(4, 0, 0x62, 0x61, 0x72)); 113 | // two octet envelope delimiter 114 | cb.writeBytes(bytes(0x01, 0x01)); 115 | // content frame size + flag octet 116 | cb.writeBytes(bytes(0x0ff, 0, 0, 0, 0, 0, 0, 0x01, 0x05, 0)); 117 | // payload 118 | cb.writeBytes(LONG_MSG); 119 | 120 | pt.writeClient(cb); 121 | ZMTPMessage m = (ZMTPMessage) pt.readServer(); 122 | 123 | assertThat(m.size(), is(2)); 124 | assertThat(buf(), is(m.frame(0))); 125 | assertThat(buf(LONG_MSG), is(m.frame(1))); 126 | } 127 | 128 | @Test 129 | public void testZMTP1PipelineFragmentedHandshake() { 130 | doTestZMTP1PipelineFragmentedHandshake(buf(4), buf(0, 0x62, 0x61, 0x72)); 131 | doTestZMTP1PipelineFragmentedHandshake(buf(4, 0), buf(0x62, 0x61, 0x72)); 132 | doTestZMTP1PipelineFragmentedHandshake(buf(4, 0, 0x62), buf(0x61, 0x72)); 133 | doTestZMTP1PipelineFragmentedHandshake(buf(4, 0, 0x62, 0x61), buf(0x72)); 134 | } 135 | 136 | private void doTestZMTP1PipelineFragmentedHandshake(ByteBuf first, ByteBuf second) { 137 | final PipelineTester pt = new PipelineTester( 138 | ZMTPCodec.builder() 139 | .protocol(ZMTP10) 140 | .socketType(REQ) 141 | .localIdentity("foo") 142 | .build()); 143 | 144 | assertThat(buf(0x04, 0, 0x66, 0x6f, 0x6f), is(pt.readClient())); 145 | 146 | // write both fragments of client handshake 147 | pt.writeClient(first); 148 | pt.writeClient(second); 149 | 150 | ByteBuf cb = Unpooled.buffer(); 151 | // two octet envelope delimiter 152 | cb.writeBytes(bytes(0x01, 0x01)); 153 | // content frame size + flag octet 154 | cb.writeBytes(bytes(0x0ff, 0, 0, 0, 0, 0, 0, 0x01, 0x05, 0)); 155 | // payload 156 | cb.writeBytes(LONG_MSG); 157 | 158 | pt.writeClient(cb); 159 | ZMTPMessage m = (ZMTPMessage) pt.readServer(); 160 | 161 | assertThat(m.size(), is(2)); 162 | assertThat(buf(), is(m.frame(0))); 163 | assertThat(buf(LONG_MSG), is(m.frame(1))); 164 | } 165 | 166 | 167 | @Test 168 | // tests the case when the message to be parsed is fragmented inside the long long size field 169 | public void testZMTP1PipelineLongMessageFragmentedLong() { 170 | final PipelineTester pt = new PipelineTester( 171 | ZMTPCodec.builder() 172 | .protocol(ZMTP10) 173 | .socketType(REQ) 174 | .localIdentity("foo") 175 | .build()); 176 | 177 | assertThat(buf(0x04, 0, 0x66, 0x6f, 0x6f), is(pt.readClient())); 178 | 179 | ByteBuf cb = Unpooled.buffer(); 180 | // handshake: length + flag + client identity octets "BAR" 181 | cb.writeBytes(buf(4, 0, 0x62, 0x61, 0x72)); 182 | // two octet envelope delimiter 183 | cb.writeBytes(bytes(0x01, 0x01)); 184 | // fragmented first part of frame size 185 | cb.writeBytes(bytes(0x0ff, 0, 0)); 186 | 187 | pt.writeClient(cb); 188 | 189 | cb = Unpooled.buffer(); 190 | // fragmented second part of frame size 191 | cb.writeBytes(bytes(0, 0, 0, 0, 0x01, 0x05, 0)); 192 | // payload 193 | cb.writeBytes(LONG_MSG); 194 | 195 | pt.writeClient(cb); 196 | 197 | ZMTPMessage m = (ZMTPMessage) pt.readServer(); 198 | 199 | assertThat(m.size(), is(2)); 200 | assertThat(buf(), is(m.frame(0))); 201 | assertThat(buf(LONG_MSG), is(m.frame(1))); 202 | } 203 | 204 | @Test 205 | // tests the case when the message to be parsed is fragmented between 0xff flag and 8 octet length 206 | public void testZMTP1PipelineLongMessageFragmentedSize() { 207 | final PipelineTester pt = new PipelineTester( 208 | ZMTPCodec.builder() 209 | .protocol(ZMTP10) 210 | .socketType(REQ) 211 | .localIdentity("foo") 212 | .build()); 213 | 214 | assertThat(buf(0x04, 0, 0x66, 0x6f, 0x6f), is(pt.readClient())); 215 | 216 | ByteBuf cb = Unpooled.buffer(); 217 | // handshake: length + flag + client identity octets "BAR" 218 | cb.writeBytes(buf(4, 0, 0x62, 0x61, 0x72)); 219 | // two octet envelope delimiter 220 | cb.writeBytes(bytes(0x01, 0x01)); 221 | // fragmented first part of frame size 222 | cb.writeBytes(bytes(0x0ff)); 223 | 224 | pt.writeClient(cb); 225 | 226 | cb = Unpooled.buffer(); 227 | // fragmented second part of frame size 228 | cb.writeBytes(bytes(0, 0, 0, 0, 0, 0, 0x01, 0x05, 0)); 229 | // payload 230 | cb.writeBytes(LONG_MSG); 231 | 232 | pt.writeClient(cb); 233 | 234 | ZMTPMessage m = (ZMTPMessage) pt.readServer(); 235 | 236 | assertThat(m.size(), is(2)); 237 | assertThat(buf(), is(m.frame(0))); 238 | assertThat(buf(LONG_MSG), is(m.frame(1))); 239 | } 240 | 241 | 242 | @Test 243 | // tests fragmentation in the size field of the second message 244 | public void testZMTP1PipelineMultiMessage() { 245 | final PipelineTester pt = new PipelineTester( 246 | ZMTPCodec.builder() 247 | .protocol(ZMTP10) 248 | .socketType(REQ) 249 | .localIdentity("foo") 250 | .build()); 251 | 252 | assertThat(buf(0x04, 0, 0x66, 0x6f, 0x6f), is(pt.readClient())); 253 | 254 | ByteBuf cb = Unpooled.buffer(); 255 | // handshake: length + flag + client identity octets "BAR" 256 | cb.writeBytes(buf(4, 0, 0x62, 0x61, 0x72)); 257 | // two octet envelope delimiter 258 | cb.writeBytes(bytes(0x01, 0x01)); 259 | // content frame size + flag octet 260 | cb.writeBytes(bytes(0x0ff, 0, 0, 0, 0, 0, 0, 0x01, 0x05, 0)); 261 | // payload 262 | cb.writeBytes(LONG_MSG); 263 | // second message, first fragment 264 | // fragmented first part of frame size 265 | cb.writeBytes(bytes(1, 1, 0x0ff, 0, 0)); 266 | 267 | pt.writeClient(cb); 268 | ZMTPMessage m = (ZMTPMessage) pt.readServer(); 269 | 270 | assertThat(m.size(), is(2)); 271 | assertThat(buf(), is(m.frame(0))); 272 | assertThat(buf(LONG_MSG), is(m.frame(1))); 273 | 274 | // send the rest of the second message 275 | cb = Unpooled.buffer(); 276 | // fragmented second part of frame size 277 | cb.writeBytes(bytes(0, 0, 0, 0, 0x01, 0x05, 0)); 278 | // payload 279 | cb.writeBytes(LONG_MSG); 280 | pt.writeClient(cb); 281 | 282 | m = (ZMTPMessage) pt.readServer(); 283 | 284 | assertThat(m.size(), is(2)); 285 | assertThat(buf(), is(m.frame(0))); 286 | assertThat(buf(LONG_MSG), is(m.frame(1))); 287 | } 288 | 289 | } 290 | -------------------------------------------------------------------------------- /src/test/java/com/spotify/netty4/handler/codec/zmtp/ProtocolViolationTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2013 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.spotify.netty4.handler.codec.zmtp; 18 | 19 | import com.google.common.util.concurrent.SettableFuture; 20 | 21 | import org.junit.After; 22 | import org.junit.Before; 23 | import org.junit.experimental.theories.Theories; 24 | import org.junit.experimental.theories.Theory; 25 | import org.junit.experimental.theories.suppliers.TestedOn; 26 | import org.junit.runner.RunWith; 27 | 28 | import java.net.InetSocketAddress; 29 | 30 | import io.netty.bootstrap.Bootstrap; 31 | import io.netty.bootstrap.ServerBootstrap; 32 | import io.netty.buffer.ByteBuf; 33 | import io.netty.buffer.Unpooled; 34 | import io.netty.channel.Channel; 35 | import io.netty.channel.ChannelHandler; 36 | import io.netty.channel.ChannelHandlerContext; 37 | import io.netty.channel.ChannelInboundHandlerAdapter; 38 | import io.netty.channel.ChannelInitializer; 39 | import io.netty.channel.nio.NioEventLoopGroup; 40 | import io.netty.channel.socket.nio.NioServerSocketChannel; 41 | import io.netty.channel.socket.nio.NioSocketChannel; 42 | import io.netty.util.ReferenceCountUtil; 43 | 44 | import static com.spotify.netty4.handler.codec.zmtp.ZMTPProtocols.ZMTP20; 45 | import static com.spotify.netty4.handler.codec.zmtp.ZMTPSocketType.ROUTER; 46 | import static java.util.concurrent.TimeUnit.SECONDS; 47 | import static org.junit.Assert.assertFalse; 48 | 49 | @RunWith(Theories.class) 50 | public class ProtocolViolationTests { 51 | 52 | private Channel serverChannel; 53 | private InetSocketAddress serverAddress; 54 | 55 | private final String identity = "identity"; 56 | private NioEventLoopGroup bossGroup; 57 | private NioEventLoopGroup group; 58 | 59 | @ChannelHandler.Sharable 60 | private static class MockHandler extends ChannelInboundHandlerAdapter { 61 | 62 | private SettableFuture active = SettableFuture.create(); 63 | private SettableFuture exception = SettableFuture.create(); 64 | private SettableFuture inactive = SettableFuture.create(); 65 | 66 | private volatile boolean handshaked; 67 | private volatile boolean read; 68 | 69 | @Override 70 | public void channelActive(final ChannelHandlerContext ctx) throws Exception { 71 | active.set(null); 72 | } 73 | 74 | @Override 75 | public void channelInactive(final ChannelHandlerContext ctx) throws Exception { 76 | inactive.set(null); 77 | } 78 | 79 | @Override 80 | public void userEventTriggered(final ChannelHandlerContext ctx, final Object evt) throws Exception { 81 | if (evt instanceof ZMTPHandshakeSuccess) { 82 | handshaked = true; 83 | } 84 | } 85 | 86 | @Override 87 | public void channelRead(final ChannelHandlerContext ctx, final Object msg) throws Exception { 88 | ReferenceCountUtil.release(msg); 89 | read = true; 90 | } 91 | 92 | @Override 93 | public void exceptionCaught(final ChannelHandlerContext ctx, final Throwable cause) 94 | throws Exception { 95 | exception.set(cause); 96 | ctx.close(); 97 | } 98 | } 99 | 100 | private final MockHandler mockHandler = new MockHandler(); 101 | 102 | @Before 103 | public void setup() { 104 | final ServerBootstrap serverBootstrap = new ServerBootstrap(); 105 | serverBootstrap.channel(NioServerSocketChannel.class); 106 | bossGroup = new NioEventLoopGroup(1); 107 | group = new NioEventLoopGroup(); 108 | serverBootstrap.group(bossGroup, group); 109 | serverBootstrap.childHandler(new ChannelInitializer() { 110 | @Override 111 | protected void initChannel(final NioSocketChannel ch) throws Exception { 112 | ch.pipeline().addLast( 113 | ZMTPCodec.builder() 114 | .protocol(ZMTP20) 115 | .socketType(ROUTER) 116 | .localIdentity(identity) 117 | .build(), 118 | mockHandler); 119 | } 120 | }); 121 | 122 | serverChannel = serverBootstrap.bind(new InetSocketAddress("localhost", 0)) 123 | .awaitUninterruptibly().channel(); 124 | serverAddress = (InetSocketAddress) serverChannel.localAddress(); 125 | } 126 | 127 | @After 128 | public void teardown() { 129 | if (serverChannel != null) { 130 | serverChannel.close(); 131 | } 132 | if (bossGroup != null) { 133 | bossGroup.shutdownGracefully(); 134 | } 135 | if (group != null) { 136 | group.shutdownGracefully(); 137 | } 138 | } 139 | 140 | @Theory 141 | public void protocolErrorsCauseException( 142 | @TestedOn(ints = {16, 17, 27, 32, 48, 53}) final int payloadSize) throws Exception { 143 | final Bootstrap b = new Bootstrap(); 144 | b.group(new NioEventLoopGroup()); 145 | b.channel(NioSocketChannel.class); 146 | b.handler(new ChannelInitializer() { 147 | @Override 148 | protected void initChannel(final NioSocketChannel ch) throws Exception { 149 | ch.pipeline().addLast(new MockHandler()); 150 | } 151 | }); 152 | 153 | final Channel channel = b.connect(serverAddress).awaitUninterruptibly().channel(); 154 | 155 | final ByteBuf payload = Unpooled.buffer(payloadSize); 156 | for (int i = 0; i < payloadSize; i++) { 157 | payload.writeByte(0); 158 | } 159 | channel.writeAndFlush(payload); 160 | 161 | mockHandler.active.get(5, SECONDS); 162 | mockHandler.exception.get(5, SECONDS); 163 | mockHandler.inactive.get(5, SECONDS); 164 | assertFalse(mockHandler.handshaked); 165 | assertFalse(mockHandler.read); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/test/java/com/spotify/netty4/handler/codec/zmtp/VerifyingDecoder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.spotify.netty4.handler.codec.zmtp; 18 | 19 | import java.util.List; 20 | 21 | import io.netty.buffer.ByteBuf; 22 | import io.netty.channel.ChannelHandlerContext; 23 | 24 | public class VerifyingDecoder implements ZMTPDecoder { 25 | 26 | private ExpectedOutput expected; 27 | 28 | private int readIndex; 29 | private boolean finished; 30 | private long frameSize; 31 | 32 | public VerifyingDecoder(final ExpectedOutput expected) { 33 | this.expected = expected; 34 | } 35 | 36 | public VerifyingDecoder() { 37 | } 38 | 39 | public void expect(ExpectedOutput expected) { 40 | this.expected = expected; 41 | } 42 | 43 | @Override 44 | public void header(final ChannelHandlerContext ctx, final long length, final boolean more, 45 | final List out) { 46 | if (finished) { 47 | throw new IllegalStateException("already finished"); 48 | } 49 | if (readIndex >= expected.message.size()) { 50 | throw new IllegalStateException( 51 | "more frames than expected: " + 52 | "readIndex=" + readIndex + ", " + 53 | "expected=" + expected + 54 | ", frame(size=" + length + 55 | ", more=" + more + ")"); 56 | } 57 | frameSize = length; 58 | } 59 | 60 | @Override 61 | public void content(final ChannelHandlerContext ctx, final ByteBuf data, final List out) { 62 | if (data.readableBytes() < frameSize) { 63 | return; 64 | } 65 | final ByteBuf expectedFrame = expected.message.frame(readIndex); 66 | final ByteBuf frame = data.readBytes((int) frameSize); 67 | if (!expectedFrame.equals(frame)) { 68 | throw new IllegalStateException( 69 | "read frame did not match expected frame: " + 70 | "readIndex=" + readIndex + ", " + 71 | "expected frame=" + expectedFrame + 72 | "read frame=" + frame); 73 | } 74 | readIndex++; 75 | } 76 | 77 | @Override 78 | public void finish(final ChannelHandlerContext ctx, final List out) { 79 | if (finished) { 80 | throw new IllegalStateException("already finished"); 81 | } 82 | if (readIndex != expected.message.size()) { 83 | throw new IllegalStateException( 84 | "less than expected frames read: " + 85 | "readIndex=" + readIndex + ", " + 86 | "expected=" + expected); 87 | } 88 | readIndex = 0; 89 | finished = true; 90 | } 91 | 92 | @Override 93 | public void close() { 94 | } 95 | 96 | public void assertFinished() { 97 | if (!finished) { 98 | throw new AssertionError("not finished"); 99 | } 100 | finished = false; 101 | } 102 | 103 | static class ExpectedOutput { 104 | 105 | private final ZMTPMessage message; 106 | 107 | public ExpectedOutput(final ZMTPMessage message) { 108 | this.message = message; 109 | } 110 | 111 | @Override 112 | public String toString() { 113 | return message.toString(); 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/test/java/com/spotify/netty4/handler/codec/zmtp/ZMQIntegrationTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.spotify.netty4.handler.codec.zmtp; 18 | 19 | import com.google.common.base.Strings; 20 | import com.google.common.util.concurrent.ListenableFuture; 21 | 22 | import org.junit.After; 23 | import org.junit.Before; 24 | import org.junit.experimental.theories.DataPoints; 25 | import org.junit.experimental.theories.FromDataPoints; 26 | import org.junit.experimental.theories.Theories; 27 | import org.junit.experimental.theories.Theory; 28 | import org.junit.runner.RunWith; 29 | import org.mockito.ArgumentCaptor; 30 | import org.mockito.Mockito; 31 | import org.zeromq.ZMQ; 32 | import org.zeromq.ZMsg; 33 | 34 | import java.net.InetSocketAddress; 35 | import java.util.List; 36 | import java.util.concurrent.ExecutionException; 37 | import java.util.concurrent.TimeUnit; 38 | import java.util.concurrent.TimeoutException; 39 | 40 | import io.netty.util.ReferenceCountUtil; 41 | 42 | import static com.spotify.netty4.handler.codec.zmtp.ZMTPProtocols.ZMTP10; 43 | import static com.spotify.netty4.handler.codec.zmtp.ZMTPProtocols.ZMTP20; 44 | import static com.spotify.netty4.handler.codec.zmtp.ZMTPSocketType.DEALER; 45 | import static com.spotify.netty4.handler.codec.zmtp.ZMTPSocketType.ROUTER; 46 | import static io.netty.util.CharsetUtil.UTF_8; 47 | import static org.hamcrest.Matchers.is; 48 | import static org.hamcrest.Matchers.isEmptyString; 49 | import static org.hamcrest.Matchers.not; 50 | import static org.junit.Assert.assertEquals; 51 | import static org.junit.Assert.assertThat; 52 | import static org.junit.Assume.assumeFalse; 53 | import static org.mockito.Matchers.any; 54 | import static org.mockito.Matchers.eq; 55 | import static org.mockito.Mockito.timeout; 56 | import static org.mockito.Mockito.verify; 57 | 58 | @RunWith(Theories.class) 59 | public class ZMQIntegrationTest { 60 | 61 | private static final String ANONYMOUS = ""; 62 | private static final String IDENTITY = "zmq-integration-test"; 63 | private static final String MIN_IDENTITY = "z"; 64 | private static final String MAX_IDENTITY = Strings.repeat("z", 255); 65 | 66 | @DataPoints("identities") 67 | public static final String[] IDENTITIES = {IDENTITY, MIN_IDENTITY, MAX_IDENTITY, ANONYMOUS}; 68 | 69 | @DataPoints("versions") 70 | public static final ZMTPProtocol[] PROTOCOLS = {ZMTP10, ZMTP20}; 71 | 72 | private final ZMTPSocket.Handler handler = 73 | Mockito.mock(ZMTPSocket.Handler.class); 74 | private final ArgumentCaptor messageCaptor = 75 | ArgumentCaptor.forClass(ZMTPMessage.class); 76 | 77 | private ZMQ.Context context; 78 | 79 | private ZMTPSocket zmtpSocket; 80 | 81 | private int port; 82 | private ZMQ.Socket zmqSocket; 83 | 84 | @Before 85 | public void setUp() { 86 | context = ZMQ.context(1); 87 | } 88 | 89 | @After 90 | public void tearDown() { 91 | if (zmtpSocket != null) { 92 | zmtpSocket.close(); 93 | } 94 | if (zmqSocket != null) { 95 | zmqSocket.close(); 96 | } 97 | if (context != null) { 98 | context.close(); 99 | } 100 | } 101 | 102 | @Theory 103 | public void test_NettyBindRouter_ZmqConnectDealer( 104 | @FromDataPoints("identities") final String zmqIdentity, 105 | @FromDataPoints("identities") final String nettyIdentity, 106 | @FromDataPoints("versions") final ZMTPProtocol nettyProtocol 107 | ) 108 | throws TimeoutException, InterruptedException, ExecutionException { 109 | // XXX (dano): jeromq fails on identities longer than 127 bytes due to a signedness issue 110 | assumeFalse(MAX_IDENTITY.equals(zmqIdentity)); 111 | 112 | final ZMTPSocket router = nettyBind(ROUTER, nettyIdentity, nettyProtocol); 113 | final ZMQ.Socket dealer = zmqConnect(ZMQ.DEALER, zmqIdentity); 114 | 115 | testReqRep(dealer, router, zmqIdentity); 116 | } 117 | 118 | @Theory 119 | public void test_NettyBindDealer_ZmqConnectRouter( 120 | @FromDataPoints("identities") final String zmqIdentity, 121 | @FromDataPoints("identities") final String nettyIdentity, 122 | @FromDataPoints("versions") final ZMTPProtocol nettyProtocol 123 | ) 124 | throws InterruptedException, TimeoutException, ExecutionException { 125 | // XXX (dano): jeromq fails on identities longer than 127 bytes due to a signedness issue 126 | assumeFalse(MAX_IDENTITY.equals(zmqIdentity)); 127 | 128 | final ZMTPSocket dealer = nettyBind(DEALER, nettyIdentity, nettyProtocol); 129 | final ZMQ.Socket router = zmqConnect(ZMQ.ROUTER, zmqIdentity); 130 | testReqRep(dealer, router, zmqIdentity); 131 | } 132 | 133 | @Theory 134 | public void test_ZmqBindRouter_NettyConnectDealer( 135 | @FromDataPoints("identities") final String zmqIdentity, 136 | @FromDataPoints("identities") final String nettyIdentity, 137 | @FromDataPoints("versions") final ZMTPProtocol nettyProtocol 138 | ) 139 | throws InterruptedException, TimeoutException, ExecutionException { 140 | // XXX (dano): jeromq fails on identities longer than 127 bytes due to a signedness issue 141 | assumeFalse(MAX_IDENTITY.equals(zmqIdentity)); 142 | 143 | final ZMQ.Socket router = zmqBind(ZMQ.ROUTER, zmqIdentity); 144 | final ZMTPSocket dealer = nettyConnect(DEALER, nettyIdentity, nettyProtocol); 145 | testReqRep(dealer, router, zmqIdentity); 146 | } 147 | 148 | @Theory 149 | public void test_ZmqBindDealer_NettyConnectRouter( 150 | @FromDataPoints("identities") final String zmqIdentity, 151 | @FromDataPoints("identities") final String nettyIdentity, 152 | @FromDataPoints("versions") final ZMTPProtocol nettyProtocol 153 | ) 154 | throws TimeoutException, InterruptedException, ExecutionException { 155 | // XXX (dano): jeromq fails on identities longer than 127 bytes due to a signedness issue 156 | assumeFalse(MAX_IDENTITY.equals(zmqIdentity)); 157 | 158 | final ZMQ.Socket dealer = zmqBind(ZMQ.DEALER, zmqIdentity); 159 | final ZMTPSocket router = nettyConnect(ROUTER, nettyIdentity, nettyProtocol); 160 | testReqRep(dealer, router, zmqIdentity); 161 | } 162 | 163 | private void testReqRep(final ZMQ.Socket req, final ZMTPSocket rep, final String zmqIdentity) 164 | throws InterruptedException, TimeoutException { 165 | 166 | // Verify that sockets are connected 167 | verify(handler, timeout(5000)).connected(eq(rep), any(ZMTPSocket.ZMTPPeer.class)); 168 | 169 | // Verify that the peer identity was correctly received 170 | verifyPeerIdentity(rep, zmqIdentity); 171 | 172 | // Send request 173 | final ZMsg request = ZMsg.newStringMsg("envelope", "", "hello", "world"); 174 | request.send(req, false); 175 | 176 | // Receive request 177 | verify(handler, timeout(5000)).message( 178 | eq(rep), any(ZMTPSocket.ZMTPPeer.class), messageCaptor.capture()); 179 | final ZMTPMessage receivedRequest = messageCaptor.getValue(); 180 | 181 | // Send reply 182 | rep.send(receivedRequest); 183 | 184 | // Receive reply 185 | final ZMsg reply = ZMsg.recvMsg(req); 186 | 187 | // Verify echo 188 | assertEquals(request, reply); 189 | } 190 | 191 | private void testReqRep(final ZMTPSocket req, final ZMQ.Socket rep, final String zmqIdentity) 192 | throws InterruptedException, TimeoutException { 193 | 194 | // Verify that sockets are connected 195 | verify(handler, timeout(5000)).connected(eq(req), any(ZMTPSocket.ZMTPPeer.class)); 196 | 197 | // Verify that the peer identity was correctly received 198 | verifyPeerIdentity(req, zmqIdentity); 199 | 200 | // Send request 201 | final ZMTPMessage request = ZMTPMessage.fromUTF8("envelope", "", "hello", "world"); 202 | request.retain(); 203 | req.send(request); 204 | 205 | // Receive request 206 | final ZMsg receivedRequest = ZMsg.recvMsg(rep); 207 | 208 | // Send reply 209 | receivedRequest.send(rep, false); 210 | 211 | // Receive reply 212 | verify(handler, timeout(5000)).message( 213 | eq(req), any(ZMTPSocket.ZMTPPeer.class), messageCaptor.capture()); 214 | final ZMTPMessage reply = messageCaptor.getValue(); 215 | ReferenceCountUtil.releaseLater(reply); 216 | 217 | // Verify echo 218 | assertEquals(request, reply); 219 | request.release(); 220 | } 221 | 222 | private ZMQ.Socket zmqBind(final int zmqType, final String identity) { 223 | zmqSocket = context.socket(zmqType); 224 | setIdentity(zmqSocket, identity); 225 | port = zmqSocket.bindToRandomPort("tcp://127.0.0.1"); 226 | return zmqSocket; 227 | } 228 | 229 | private ZMQ.Socket zmqConnect(final int zmqType, final String identity) { 230 | zmqSocket = context.socket(zmqType); 231 | setIdentity(zmqSocket, identity); 232 | zmqSocket.connect("tcp://127.0.0.1:" + port); 233 | return zmqSocket; 234 | } 235 | 236 | private ZMTPSocket nettyConnect(final ZMTPSocketType socketType, final String identity, 237 | final ZMTPProtocol protocol) 238 | throws InterruptedException, ExecutionException, TimeoutException { 239 | zmtpSocket = ZMTPSocket.builder() 240 | .handler(handler) 241 | .protocol(protocol) 242 | .type(socketType) 243 | .identity(identity) 244 | .build(); 245 | 246 | final ListenableFuture f = zmtpSocket.connect("tcp://127.0.0.1:" + port); 247 | f.get(5, TimeUnit.SECONDS); 248 | 249 | return zmtpSocket; 250 | } 251 | 252 | private ZMTPSocket nettyBind(final ZMTPSocketType socketType, final String identity, 253 | final ZMTPProtocol protocol) 254 | throws InterruptedException, ExecutionException, TimeoutException { 255 | zmtpSocket = ZMTPSocket.builder() 256 | .handler(handler) 257 | .protocol(protocol) 258 | .type(socketType) 259 | .identity(identity) 260 | .build(); 261 | 262 | final ListenableFuture f = zmtpSocket.bind("tcp://127.0.0.1:*"); 263 | final InetSocketAddress address = f.get(5, TimeUnit.SECONDS); 264 | port = address.getPort(); 265 | 266 | return zmtpSocket; 267 | } 268 | 269 | private void setIdentity(final ZMQ.Socket socket, final String identity) { 270 | if (!identity.equals(ANONYMOUS)) { 271 | socket.setIdentity(identity.getBytes(UTF_8)); 272 | } 273 | } 274 | 275 | private void verifyPeerIdentity(final ZMTPSocket socket, final String zmqIdentity) throws InterruptedException { 276 | final List peers = socket.peers(); 277 | assertThat(peers.size(), is(1)); 278 | final ZMTPSession session = peers.get(0).session(); 279 | if (ANONYMOUS.equals(zmqIdentity)) { 280 | assertThat(session.isPeerAnonymous(), is(true)); 281 | assertThat(UTF_8.decode(session.peerIdentity()).toString(), not(isEmptyString())); 282 | } else { 283 | assertThat(session.isPeerAnonymous(), is(false)); 284 | assertThat(session.peerIdentity(), is(UTF_8.encode(zmqIdentity))); 285 | } 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /src/test/java/com/spotify/netty4/handler/codec/zmtp/ZMTP10WireFormatTest.java: -------------------------------------------------------------------------------- 1 | package com.spotify.netty4.handler.codec.zmtp; 2 | 3 | import org.junit.Rule; 4 | import org.junit.Test; 5 | import org.junit.rules.ExpectedException; 6 | 7 | import io.netty.buffer.ByteBuf; 8 | import io.netty.buffer.Unpooled; 9 | 10 | import static org.hamcrest.Matchers.is; 11 | import static org.junit.Assert.assertThat; 12 | 13 | public class ZMTP10WireFormatTest { 14 | 15 | @Rule public final ExpectedException expectedException = ExpectedException.none(); 16 | 17 | @Test 18 | public void testTooLongIdentity() throws Exception { 19 | final ByteBuf buffer = Unpooled.buffer(); 20 | buffer.writeByte(0xFF); 21 | buffer.writeLong(256 + 1); 22 | buffer.writeByte(0); 23 | buffer.writeBytes(new byte[256]); 24 | 25 | expectedException.expect(ZMTPException.class); 26 | ZMTP10WireFormat.readIdentity(buffer); 27 | } 28 | 29 | @Test 30 | public void testLongFrameLengthMissingLong() { 31 | final ByteBuf buffer = Unpooled.buffer(); 32 | buffer.writeByte(0xFF); 33 | final long size = ZMTP10WireFormat.readLength(buffer); 34 | assertThat("Length shouldn't have been determined", size, is(-1L)); 35 | } 36 | 37 | @Test 38 | public void testLongFrameLengthWithLong() { 39 | final ByteBuf buffer = Unpooled.buffer(); 40 | buffer.writeByte(0xFF); 41 | buffer.writeLong(4); 42 | final long size = ZMTP10WireFormat.readLength(buffer); 43 | assertThat("Frame length should be after the first byte", size, is(4L)); 44 | } 45 | 46 | @Test 47 | public void testFrameLengthEmptyBuffer() { 48 | final ByteBuf buffer = Unpooled.buffer(); 49 | final long size = ZMTP10WireFormat.readLength(buffer); 50 | assertThat("Empty buffer should return -1 frame length", size, is(-1L)); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/test/java/com/spotify/netty4/handler/codec/zmtp/ZMTPFramingEncoderTest.java: -------------------------------------------------------------------------------- 1 | package com.spotify.netty4.handler.codec.zmtp; 2 | 3 | import com.google.common.base.Strings; 4 | 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | import org.junit.runner.RunWith; 8 | import org.mockito.ArgumentCaptor; 9 | import org.mockito.Captor; 10 | import org.mockito.Mock; 11 | import org.mockito.runners.MockitoJUnitRunner; 12 | 13 | import io.netty.buffer.ByteBuf; 14 | import io.netty.buffer.ByteBufAllocator; 15 | import io.netty.buffer.Unpooled; 16 | import io.netty.buffer.UnpooledByteBufAllocator; 17 | import io.netty.channel.ChannelHandlerContext; 18 | import io.netty.channel.ChannelPromise; 19 | import io.netty.util.concurrent.EventExecutor; 20 | 21 | import static com.spotify.netty4.handler.codec.zmtp.Buffers.buf; 22 | import static com.spotify.netty4.handler.codec.zmtp.Buffers.bytes; 23 | import static com.spotify.netty4.handler.codec.zmtp.ZMTPConfig.ANONYMOUS; 24 | import static com.spotify.netty4.handler.codec.zmtp.ZMTPProtocols.ZMTP10; 25 | import static com.spotify.netty4.handler.codec.zmtp.ZMTPProtocols.ZMTP20; 26 | import static com.spotify.netty4.handler.codec.zmtp.ZMTPSocketType.DEALER; 27 | import static io.netty.util.CharsetUtil.UTF_8; 28 | import static org.hamcrest.Matchers.is; 29 | import static org.junit.Assert.assertThat; 30 | import static org.mockito.Matchers.any; 31 | import static org.mockito.Mockito.when; 32 | 33 | @RunWith(MockitoJUnitRunner.class) 34 | public class ZMTPFramingEncoderTest { 35 | 36 | private final static ByteBufAllocator ALLOC = new UnpooledByteBufAllocator(false); 37 | 38 | @Mock ChannelHandlerContext ctx; 39 | @Mock ChannelPromise promise; 40 | @Mock EventExecutor executor; 41 | 42 | @Captor ArgumentCaptor bufCaptor; 43 | 44 | private static final String LARGE_FILL = Strings.repeat("a", 500); 45 | 46 | @Before 47 | public void setUp() { 48 | when(ctx.write(bufCaptor.capture(), any(ChannelPromise.class))).thenReturn(promise); 49 | when(ctx.alloc()).thenReturn(ByteBufAllocator.DEFAULT); 50 | when(ctx.executor()).thenReturn(executor); 51 | } 52 | 53 | @Test 54 | public void testEncodeZMTP1() throws Exception { 55 | 56 | ZMTPConfig config = ZMTPConfig.builder() 57 | .protocol(ZMTP10) 58 | .socketType(DEALER) 59 | .build(); 60 | ZMTPSession session = new ZMTPSession(config); 61 | session.handshakeSuccess(ZMTPHandshake.of(ZMTPVersion.ZMTP10, ANONYMOUS)); 62 | 63 | ZMTPFramingEncoder enc = new ZMTPFramingEncoder(session, new ZMTPMessageEncoder()); 64 | 65 | ZMTPMessage message = ZMTPMessage.fromUTF8(ALLOC, "id0", "id1", "", "f0"); 66 | 67 | enc.write(ctx, message, promise); 68 | enc.flush(ctx); 69 | final ByteBuf buf = bufCaptor.getValue(); 70 | assertThat(buf, is(buf(4, 1, 0x69, 0x64, 0x30, 71 | 4, 1, 0x69, 0x64, 0x31, 72 | 1, 1, 73 | 3, 0, 0x66, 0x30))); 74 | buf.release(); 75 | } 76 | 77 | @Test 78 | public void testEncodeZMTP2() throws Exception { 79 | 80 | ZMTPMessage message = ZMTPMessage.fromUTF8(ALLOC, "id0", "id1", "", "f0"); 81 | 82 | ZMTPConfig config = ZMTPConfig.builder() 83 | .protocol(ZMTP20) 84 | .socketType(DEALER) 85 | .build(); 86 | ZMTPSession session = new ZMTPSession(config); 87 | session.handshakeSuccess(ZMTPHandshake.of(ZMTPVersion.ZMTP20, ANONYMOUS)); 88 | 89 | ZMTPFramingEncoder enc = new ZMTPFramingEncoder(session, new ZMTPMessageEncoder()); 90 | 91 | enc.write(ctx, message, promise); 92 | enc.flush(ctx); 93 | final ByteBuf buf = bufCaptor.getValue(); 94 | assertThat(buf, is(buf(1, 3, 0x69, 0x64, 0x30, 95 | 1, 3, 0x69, 0x64, 0x31, 96 | 1, 0, 97 | 0, 2, 0x66, 0x30))); 98 | buf.release(); 99 | } 100 | 101 | @Test 102 | public void testEncodeZMTP2Long() throws Exception { 103 | ZMTPMessage message = ZMTPMessage.fromUTF8(ALLOC, "id0", "", LARGE_FILL); 104 | ByteBuf buf = Unpooled.buffer(); 105 | buf.writeBytes(bytes(1, 3, 0x69, 0x64, 0x30, 106 | 1, 0, 107 | 2, 0, 0, 0, 0, 0, 0, 0x01, 0xf4)); 108 | buf.writeBytes(LARGE_FILL.getBytes(UTF_8)); 109 | 110 | ZMTPConfig config = ZMTPConfig.builder() 111 | .protocol(ZMTP20) 112 | .socketType(DEALER) 113 | .build(); 114 | ZMTPSession session = new ZMTPSession(config); 115 | 116 | session.handshakeSuccess(ZMTPHandshake.of(ZMTPVersion.ZMTP20, ANONYMOUS)); 117 | 118 | ZMTPFramingEncoder enc = new ZMTPFramingEncoder(session, new ZMTPMessageEncoder()); 119 | 120 | enc.write(ctx, message, promise); 121 | enc.flush(ctx); 122 | final ByteBuf buf2 = bufCaptor.getValue(); 123 | 124 | assertThat(buf, is(buf2)); 125 | 126 | buf.release(); 127 | buf2.release(); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/test/java/com/spotify/netty4/handler/codec/zmtp/ZMTPMessageDecoderTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.spotify.netty4.handler.codec.zmtp; 18 | 19 | import com.google.common.collect.Lists; 20 | 21 | import org.junit.Test; 22 | import org.junit.runner.RunWith; 23 | import org.mockito.Mock; 24 | import org.mockito.runners.MockitoJUnitRunner; 25 | 26 | import java.util.List; 27 | 28 | import io.netty.buffer.ByteBuf; 29 | import io.netty.buffer.ByteBufAllocator; 30 | import io.netty.buffer.Unpooled; 31 | import io.netty.buffer.UnpooledByteBufAllocator; 32 | import io.netty.channel.ChannelHandlerContext; 33 | 34 | import static io.netty.util.CharsetUtil.UTF_8; 35 | import static org.hamcrest.Matchers.contains; 36 | import static org.hamcrest.Matchers.hasSize; 37 | import static org.junit.Assert.assertThat; 38 | 39 | @RunWith(MockitoJUnitRunner.class) 40 | public class ZMTPMessageDecoderTest { 41 | 42 | @Mock ChannelHandlerContext ctx; 43 | 44 | private final static ByteBufAllocator ALLOC = new UnpooledByteBufAllocator(false); 45 | 46 | @Test 47 | public void testSingleFrame() throws Exception { 48 | final ZMTPMessageDecoder decoder = new ZMTPMessageDecoder(); 49 | 50 | final ByteBuf content = Unpooled.copiedBuffer("hello", UTF_8); 51 | 52 | final List out = Lists.newArrayList(); 53 | decoder.header(ctx, content.readableBytes(), false, out); 54 | decoder.content(ctx, content, out); 55 | decoder.finish(ctx, out); 56 | 57 | final Object expected = ZMTPMessage.fromUTF8(ALLOC, "hello"); 58 | assertThat(out, hasSize(1)); 59 | assertThat(out, contains(expected)); 60 | } 61 | 62 | @Test 63 | public void testTwoFrames() throws Exception { 64 | final ZMTPMessageDecoder decoder = new ZMTPMessageDecoder(); 65 | 66 | final ByteBuf f0 = Unpooled.copiedBuffer("hello", UTF_8); 67 | final ByteBuf f1 = Unpooled.copiedBuffer("world", UTF_8); 68 | 69 | final List out = Lists.newArrayList(); 70 | decoder.header(ctx, f0.readableBytes(), true, out); 71 | decoder.content(ctx, f0, out); 72 | decoder.header(ctx, f1.readableBytes(), false, out); 73 | decoder.content(ctx, f1, out); 74 | decoder.finish(ctx, out); 75 | 76 | final Object expected = ZMTPMessage.fromUTF8(ALLOC, "hello", "world"); 77 | assertThat(out, hasSize(1)); 78 | assertThat(out, contains(expected)); 79 | } 80 | } -------------------------------------------------------------------------------- /src/test/java/com/spotify/netty4/handler/codec/zmtp/ZMTPMessageTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2014 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.spotify.netty4.handler.codec.zmtp; 18 | 19 | import com.google.common.base.Function; 20 | import com.google.common.collect.Lists; 21 | 22 | import org.junit.Test; 23 | import org.junit.runner.RunWith; 24 | import org.junit.runners.Parameterized; 25 | 26 | import java.nio.CharBuffer; 27 | import java.util.ArrayList; 28 | import java.util.List; 29 | 30 | import io.netty.buffer.ByteBuf; 31 | import io.netty.buffer.ByteBufAllocator; 32 | import io.netty.buffer.ByteBufUtil; 33 | import io.netty.buffer.Unpooled; 34 | import io.netty.buffer.UnpooledByteBufAllocator; 35 | 36 | import static io.netty.util.CharsetUtil.UTF_8; 37 | import static java.util.Arrays.asList; 38 | import static org.hamcrest.Matchers.is; 39 | import static org.hamcrest.Matchers.not; 40 | import static org.junit.Assert.assertEquals; 41 | import static org.junit.Assert.assertThat; 42 | 43 | @RunWith(Parameterized.class) 44 | public class ZMTPMessageTest { 45 | 46 | private final static ByteBufAllocator ALLOC = new UnpooledByteBufAllocator(false); 47 | 48 | @Parameterized.Parameters(name = "{0}") 49 | public static Iterable versions() { 50 | final List versions = new ArrayList(); 51 | for (final ZMTPVersion version : ZMTPVersion.supportedVersions()) { 52 | versions.add(new Object[]{version}); 53 | } 54 | return versions; 55 | } 56 | 57 | @Parameterized.Parameter(0) 58 | public ZMTPVersion version; 59 | 60 | @Test 61 | public void testNotEquals() { 62 | final ZMTPMessage m1 = ZMTPMessage.fromUTF8(ALLOC, "hello", "world"); 63 | final ZMTPMessage m2 = ZMTPMessage.fromUTF8(ALLOC, "foo", "bar"); 64 | assertThat(m1, is(not(m2))); 65 | } 66 | 67 | @Test 68 | public void testEquals() { 69 | final ZMTPMessage m1 = ZMTPMessage.fromUTF8(ALLOC, "hello", "world"); 70 | final ZMTPMessage m2 = ZMTPMessage.fromUTF8(ALLOC, "hello", "world"); 71 | assertThat(m1, is(m2)); 72 | } 73 | 74 | @Test 75 | public void testIdentityEquals() { 76 | final ZMTPMessage m = ZMTPMessage.fromUTF8(ALLOC, "hello", "world"); 77 | assertThat(m, is(m)); 78 | } 79 | 80 | @Test 81 | public void testWriteAndRead() throws ZMTPParsingException { 82 | final ZMTPMessage message = ZMTPMessage.fromUTF8(ALLOC, "hello", "world"); 83 | final ByteBuf buffer = message.write(ALLOC, version); 84 | final ZMTPMessage read = ZMTPMessage.read(buffer, version); 85 | assertThat(read, is(message)); 86 | } 87 | 88 | @Test 89 | public void testWriteAndReadTwoMessages() throws ZMTPParsingException { 90 | final ZMTPMessage m1 = ZMTPMessage.fromUTF8(ALLOC, "hello", "world"); 91 | final ZMTPMessage m2 = ZMTPMessage.fromUTF8(ALLOC, "foo", "bar"); 92 | final ByteBuf buffer = Unpooled.buffer(); 93 | m1.write(buffer, version); 94 | m2.write(buffer, version); 95 | final ZMTPMessage r1 = ZMTPMessage.read(buffer, version); 96 | final ZMTPMessage r2 = ZMTPMessage.read(buffer, version); 97 | assertThat(r1, is(m1)); 98 | assertThat(r2, is(m2)); 99 | } 100 | 101 | @Test 102 | public void testFromStringsUTF8() { 103 | assertEquals(ZMTPMessage.fromUTF8(ALLOC, ""), message("")); 104 | assertEquals(ZMTPMessage.fromUTF8(ALLOC, "a"), message("a")); 105 | assertEquals(ZMTPMessage.fromUTF8(ALLOC, "aa"), message("aa")); 106 | assertEquals(ZMTPMessage.fromUTF8(ALLOC, "aa", "bb"), message("aa", "bb")); 107 | assertEquals(ZMTPMessage.fromUTF8(ALLOC, "aa", "", "bb"), message("aa", "", "bb")); 108 | } 109 | 110 | private ZMTPMessage message(final String... frames) { 111 | return ZMTPMessage.from(frames(asList(frames))); 112 | } 113 | 114 | private static List frames(final List frames) { 115 | return Lists.transform(frames, new Function() { 116 | @Override 117 | public ByteBuf apply(final String input) { 118 | return ByteBufUtil.encodeString(ALLOC, CharBuffer.wrap(input), UTF_8); 119 | } 120 | }); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/test/java/com/spotify/netty4/handler/codec/zmtp/ZMTPParserTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.spotify.netty4.handler.codec.zmtp; 18 | 19 | import com.spotify.netty4.handler.codec.zmtp.VerifyingDecoder.ExpectedOutput; 20 | 21 | import org.junit.experimental.theories.DataPoints; 22 | import org.junit.experimental.theories.FromDataPoints; 23 | import org.junit.experimental.theories.Theories; 24 | import org.junit.experimental.theories.Theory; 25 | import org.junit.runner.RunWith; 26 | 27 | import java.util.List; 28 | 29 | import io.netty.buffer.ByteBuf; 30 | import io.netty.buffer.ByteBufAllocator; 31 | import io.netty.buffer.UnpooledByteBufAllocator; 32 | import io.netty.channel.ChannelHandlerContext; 33 | 34 | import static com.spotify.netty4.handler.codec.zmtp.ZMTPWireFormats.wireFormat; 35 | import static java.util.Arrays.asList; 36 | import static org.mockito.Mockito.mock; 37 | 38 | /** 39 | * This test attempts to thoroughly exercise the {@link ZMTPFramingDecoder} by feeding it input 40 | * fragmented in every possible way using {@link Fragmenter}. Everything from whole un-fragmented 41 | * message parsing to each byte being fragmented in a separate buffer is tested. Generating all 42 | * possible message fragmentations takes some time, so running this test can typically take a few 43 | * minutes. 44 | */ 45 | @RunWith(Theories.class) 46 | public class ZMTPParserTest { 47 | 48 | private final static ByteBufAllocator ALLOC = new UnpooledByteBufAllocator(false); 49 | private final ChannelHandlerContext ctx = mock(ChannelHandlerContext.class); 50 | 51 | @DataPoints("frames") 52 | public static String[][] FRAMES = { 53 | {"1"}, 54 | {"2", ""}, 55 | {"3", "aa"}, 56 | {"4", "", "a"}, 57 | {"5", "", "a", "bb"}, 58 | {"6", "aa", "", "b", "cc"}, 59 | {"7", "", "a"}, 60 | {"8", "", "b", "cc"}, 61 | {"9", "aa", "", "b", "cc"}, 62 | }; 63 | 64 | @DataPoints("versions") 65 | public static final List VERSIONS = ZMTPVersion.supportedVersions(); 66 | 67 | @Theory 68 | public void testParse(@FromDataPoints("frames") final String[] frames, 69 | @FromDataPoints("versions") final ZMTPVersion version) throws Exception { 70 | final List input = asList(frames); 71 | final ZMTPWireFormat wireFormat = wireFormat(version); 72 | 73 | final ZMTPMessage inputMessage = ZMTPMessage.fromUTF8(ALLOC, input); 74 | 75 | final ExpectedOutput expected = new ExpectedOutput(inputMessage); 76 | 77 | final ByteBuf serialized = inputMessage.write(ALLOC, version); 78 | final int serializedLength = serialized.readableBytes(); 79 | 80 | // Test parsing the whole message 81 | { 82 | final VerifyingDecoder verifier = new VerifyingDecoder(expected); 83 | final ZMTPFramingDecoder decoder = new ZMTPFramingDecoder(wireFormat, verifier); 84 | decoder.decode(ctx, serialized, null); 85 | verifier.assertFinished(); 86 | serialized.setIndex(0, serializedLength); 87 | } 88 | 89 | // Prepare for trivial message parsing test 90 | final ZMTPMessage trivial = ZMTPMessage.fromUTF8(ALLOC, "e", "", "a", "b", "c"); 91 | final ByteBuf trivialSerialized = trivial.write(ALLOC, version); 92 | final int trivialLength = trivialSerialized.readableBytes(); 93 | final ExpectedOutput trivialExpected = new ExpectedOutput(trivial); 94 | 95 | // Test parsing fragmented input 96 | final VerifyingDecoder verifier = new VerifyingDecoder(); 97 | final ZMTPFramingDecoder decoder = new ZMTPFramingDecoder(wireFormat, verifier); 98 | new Fragmenter(serialized.readableBytes()).fragment(new Fragmenter.Consumer() { 99 | @Override 100 | public void fragments(final int[] limits, final int count) throws Exception { 101 | verifier.expect(expected); 102 | serialized.setIndex(0, serializedLength); 103 | for (int i = 0; i < count; i++) { 104 | final int limit = limits[i]; 105 | serialized.writerIndex(limit); 106 | decoder.decode(ctx, serialized, null); 107 | } 108 | verifier.assertFinished(); 109 | 110 | // Verify that the parser can be reused to parse the same message 111 | serialized.setIndex(0, serializedLength); 112 | decoder.decode(ctx, serialized, null); 113 | verifier.assertFinished(); 114 | 115 | // Verify that the parser can be reused to parse a well-behaved message 116 | verifier.expect(trivialExpected); 117 | trivialSerialized.setIndex(0, trivialLength); 118 | decoder.decode(ctx, trivialSerialized, null); 119 | verifier.assertFinished(); 120 | } 121 | }); 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /src/test/java/com/spotify/netty4/handler/codec/zmtp/ZMTPWriterTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.spotify.netty4.handler.codec.zmtp; 18 | 19 | import com.google.common.collect.Lists; 20 | 21 | import org.junit.Test; 22 | import org.junit.runner.RunWith; 23 | import org.mockito.runners.MockitoJUnitRunner; 24 | 25 | import java.util.List; 26 | 27 | import io.netty.buffer.ByteBuf; 28 | import io.netty.buffer.Unpooled; 29 | import io.netty.channel.ChannelHandlerContext; 30 | 31 | import static com.spotify.netty4.handler.codec.zmtp.ZMTPVersion.ZMTP10; 32 | import static com.spotify.netty4.handler.codec.zmtp.ZMTPWireFormats.wireFormat; 33 | import static io.netty.buffer.Unpooled.copiedBuffer; 34 | import static io.netty.util.CharsetUtil.UTF_8; 35 | import static java.util.Arrays.asList; 36 | import static java.util.Collections.singletonList; 37 | import static org.hamcrest.Matchers.hasSize; 38 | import static org.hamcrest.Matchers.is; 39 | import static org.hamcrest.Matchers.sameInstance; 40 | import static org.hamcrest.collection.IsIterableContainingInOrder.contains; 41 | import static org.junit.Assert.assertThat; 42 | 43 | @RunWith(MockitoJUnitRunner.class) 44 | public class ZMTPWriterTest { 45 | 46 | private final List out = Lists.newArrayList(); 47 | 48 | @Test 49 | public void testOneFrame() throws Exception { 50 | final ZMTPWriter writer = ZMTPWriter.create(ZMTP10); 51 | final ByteBuf buf = Unpooled.buffer(); 52 | writer.reset(buf); 53 | 54 | ByteBuf frame = writer.frame(11, false); 55 | assertThat(frame, is(sameInstance(buf))); 56 | 57 | final ByteBuf content = copiedBuffer("hello world", UTF_8); 58 | 59 | frame.writeBytes(content.duplicate()); 60 | 61 | final ZMTPFramingDecoder decoder = new ZMTPFramingDecoder(wireFormat(ZMTP10), new RawDecoder()); 62 | decoder.decode(null, buf, out); 63 | 64 | assertThat(out, hasSize(1)); 65 | assertThat(out, contains((Object) singletonList(content))); 66 | } 67 | 68 | @Test 69 | public void testTwoFrames() throws Exception { 70 | final ZMTPWriter writer = ZMTPWriter.create(ZMTP10); 71 | final ByteBuf buf = Unpooled.buffer(); 72 | writer.reset(buf); 73 | 74 | final ByteBuf f0 = copiedBuffer("hello ", UTF_8); 75 | final ByteBuf f1 = copiedBuffer("hello ", UTF_8); 76 | 77 | writer.frame(f0.readableBytes(), true).writeBytes(f0.duplicate()); 78 | writer.frame(f1.readableBytes(), false).writeBytes(f1.duplicate()); 79 | 80 | final ZMTPFramingDecoder decoder = new ZMTPFramingDecoder(wireFormat(ZMTP10), new RawDecoder()); 81 | decoder.decode(null, buf, out); 82 | 83 | assertThat(out, hasSize(1)); 84 | assertThat(out, contains((Object) asList(f0, f1))); 85 | } 86 | 87 | @Test 88 | public void testReframe() throws Exception { 89 | final ZMTPFramingDecoder decoder = new ZMTPFramingDecoder(wireFormat(ZMTP10), new RawDecoder()); 90 | final ZMTPWriter writer = ZMTPWriter.create(ZMTP10); 91 | final ByteBuf buf = Unpooled.buffer(); 92 | 93 | writer.reset(buf); 94 | 95 | // Request a frame with margin in anticipation of a larger payload... 96 | // ... but write a smaller payload 97 | final ByteBuf content = copiedBuffer("hello world", UTF_8); 98 | writer.frame(content.readableBytes() * 2, true).writeBytes(content.duplicate()); 99 | 100 | // And rewrite the frame accordingly 101 | writer.reframe(content.readableBytes(), false); 102 | 103 | // Verify that the message can be parsed 104 | decoder.decode(null, buf, out); 105 | assertThat(out, hasSize(1)); 106 | assertThat(out, contains((Object) singletonList(content))); 107 | 108 | // Write and verify another message 109 | final ByteBuf next = copiedBuffer("next", UTF_8); 110 | writer.frame(next.readableBytes(), false).writeBytes(next.duplicate()); 111 | 112 | out.clear(); 113 | decoder.decode(null, buf, out); 114 | assertThat(out, hasSize(1)); 115 | assertThat(out, contains((Object) singletonList(next))); 116 | } 117 | 118 | 119 | private class RawDecoder implements ZMTPDecoder { 120 | 121 | private long length; 122 | 123 | private List frames = Lists.newArrayList(); 124 | 125 | public void header(final ChannelHandlerContext ctx, final long length, final boolean more, 126 | final List out) { 127 | this.length = length; 128 | } 129 | 130 | @Override 131 | public void content(final ChannelHandlerContext ctx, final ByteBuf data, 132 | final List out) { 133 | if (data.readableBytes() < length) { 134 | return; 135 | } 136 | frames.add(data.readBytes((int) length)); 137 | } 138 | 139 | @Override 140 | public void finish(final ChannelHandlerContext ctx, final List out) { 141 | out.add(frames); 142 | frames = Lists.newArrayList(); 143 | } 144 | 145 | @Override 146 | public void close() { 147 | for (final ByteBuf frame : frames) { 148 | frame.release(); 149 | } 150 | frames.clear(); 151 | } 152 | } 153 | } -------------------------------------------------------------------------------- /src/test/java/com/spotify/netty4/handler/codec/zmtp/benchmarks/AsciiString.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2014 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.spotify.netty4.handler.codec.zmtp.benchmarks; 18 | 19 | import com.google.common.base.Function; 20 | 21 | import java.util.Arrays; 22 | 23 | import io.netty.buffer.ByteBuf; 24 | 25 | import static com.google.common.base.Preconditions.checkNotNull; 26 | 27 | public class AsciiString implements CharSequence { 28 | 29 | public static final Function 30 | ASCII_STRING_FROM_STRING = 31 | new Function() { 32 | @Override 33 | public AsciiString apply(final String input) { 34 | return from(input); 35 | } 36 | }; 37 | 38 | private final byte[] chars; 39 | 40 | public AsciiString(final byte[] chars) { 41 | this.chars = checkNotNull(chars, "chars"); 42 | } 43 | 44 | @Override 45 | public int length() { 46 | return chars.length; 47 | } 48 | 49 | @Override 50 | public char charAt(final int index) { 51 | return (char) chars[index]; 52 | } 53 | 54 | @Override 55 | public CharSequence subSequence(final int start, final int end) { 56 | final byte[] chars = new byte[end - start]; 57 | System.arraycopy(this.chars, start, chars, 0, end - start); 58 | return new AsciiString(chars); 59 | } 60 | 61 | @Override 62 | public boolean equals(final Object o) { 63 | if (this == o) { return true; } 64 | if (o == null || getClass() != o.getClass()) { return false; } 65 | 66 | final AsciiString that = (AsciiString) o; 67 | 68 | return Arrays.equals(chars, that.chars); 69 | } 70 | 71 | @Override 72 | public int hashCode() { 73 | return chars != null ? Arrays.hashCode(chars) : 0; 74 | } 75 | 76 | @Override 77 | public String toString() { 78 | final char[] chars = new char[this.chars.length]; 79 | for (int i = 0; i < this.chars.length; i++) { 80 | chars[i] = (char) this.chars[i]; 81 | } 82 | return new String(chars); 83 | } 84 | 85 | public static AsciiString from(final String s) { 86 | final byte[] chars = new byte[s.length()]; 87 | for (int i = 0; i < s.length(); i++) { 88 | chars[i] = (byte) s.charAt(i); 89 | } 90 | return new AsciiString(chars); 91 | } 92 | 93 | public void write(final ByteBuf buf) { 94 | buf.writeBytes(chars); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/test/java/com/spotify/netty4/handler/codec/zmtp/benchmarks/ProgressMeter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2013 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.spotify.netty4.handler.codec.zmtp.benchmarks; 18 | 19 | import com.google.common.util.concurrent.AbstractScheduledService; 20 | 21 | import java.util.concurrent.TimeUnit; 22 | import java.util.concurrent.atomic.AtomicLong; 23 | 24 | import static java.lang.System.out; 25 | import static java.util.concurrent.TimeUnit.NANOSECONDS; 26 | import static java.util.concurrent.TimeUnit.SECONDS; 27 | 28 | class ProgressMeter extends AbstractScheduledService { 29 | 30 | private static final double NANOS_PER_MS = TimeUnit.MILLISECONDS.toNanos(1); 31 | private static final long NANOS_PER_S = TimeUnit.SECONDS.toNanos(1); 32 | 33 | private final AtomicLong totalLatency = new AtomicLong(); 34 | private final AtomicLong totalOperations = new AtomicLong(); 35 | 36 | private final String unit; 37 | private final boolean reportLatency; 38 | 39 | private long startTime; 40 | private long lastRows; 41 | private long lastTime; 42 | private long lastLatency; 43 | 44 | public ProgressMeter(final String unit) { 45 | this(unit, false); 46 | } 47 | 48 | public ProgressMeter(final String unit, final boolean reportLatency) { 49 | this.unit = unit; 50 | this.reportLatency = reportLatency; 51 | startAsync(); 52 | } 53 | 54 | public void inc() { 55 | this.totalOperations.incrementAndGet(); 56 | } 57 | 58 | public void inc(final long ops) { 59 | this.totalOperations.addAndGet(ops); 60 | } 61 | 62 | public void inc(final long ops, final long latency) { 63 | this.totalOperations.addAndGet(ops); 64 | this.totalLatency.addAndGet(latency); 65 | } 66 | 67 | @Override 68 | protected void runOneIteration() throws Exception { 69 | final long now = System.nanoTime(); 70 | final long totalOperations = this.totalOperations.get(); 71 | final long totalLatency = this.totalLatency.get(); 72 | 73 | final long deltaOps = totalOperations - lastRows; 74 | final long deltaTime = now - lastTime; 75 | final long deltaLatency = totalLatency - lastLatency; 76 | 77 | lastRows = totalOperations; 78 | lastTime = now; 79 | lastLatency = totalLatency; 80 | 81 | // TODO (dano): use HdrHistogram to compute latency percentiles 82 | 83 | final long operations = (deltaTime == 0) ? 0 : (NANOS_PER_S * deltaOps) / deltaTime; 84 | final double avgLatency = (deltaOps == 0) ? 0 : deltaLatency / (NANOS_PER_MS * deltaOps); 85 | final long seconds = NANOSECONDS.toSeconds(now - startTime); 86 | 87 | out.printf("%,4ds: %,12d %s/s.", seconds, operations, unit); 88 | if (reportLatency) { 89 | out.printf(" %,10.3f ms avg latency.", avgLatency); 90 | } 91 | out.printf(" (total: %,12d)\n", totalOperations); 92 | out.flush(); 93 | } 94 | 95 | @Override 96 | protected void startUp() throws Exception { 97 | startTime = System.nanoTime(); 98 | lastTime = startTime; 99 | } 100 | 101 | @Override 102 | protected Scheduler scheduler() { 103 | return Scheduler.newFixedRateSchedule(1, 1, SECONDS); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/test/java/com/spotify/netty4/handler/codec/zmtp/benchmarks/ReqRepBenchmark.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2014 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.spotify.netty4.handler.codec.zmtp.benchmarks; 18 | 19 | import com.google.common.base.Strings; 20 | 21 | import com.spotify.netty4.handler.codec.zmtp.ZMTPCodec; 22 | import com.spotify.netty4.handler.codec.zmtp.ZMTPHandshakeSuccess; 23 | import com.spotify.netty4.handler.codec.zmtp.ZMTPMessage; 24 | import com.spotify.netty4.util.BatchFlusher; 25 | 26 | import java.net.InetSocketAddress; 27 | import java.net.SocketAddress; 28 | 29 | import io.netty.bootstrap.Bootstrap; 30 | import io.netty.bootstrap.ServerBootstrap; 31 | import io.netty.buffer.PooledByteBufAllocator; 32 | import io.netty.channel.Channel; 33 | import io.netty.channel.ChannelHandlerContext; 34 | import io.netty.channel.ChannelInboundHandlerAdapter; 35 | import io.netty.channel.ChannelInitializer; 36 | import io.netty.channel.ChannelOption; 37 | import io.netty.channel.nio.NioEventLoopGroup; 38 | import io.netty.channel.socket.nio.NioServerSocketChannel; 39 | import io.netty.channel.socket.nio.NioSocketChannel; 40 | 41 | import static com.spotify.netty4.handler.codec.zmtp.ZMTPSocketType.DEALER; 42 | import static com.spotify.netty4.handler.codec.zmtp.ZMTPSocketType.ROUTER; 43 | 44 | public class ReqRepBenchmark { 45 | 46 | private static final InetSocketAddress ANY_PORT = new InetSocketAddress("127.0.0.1", 0); 47 | 48 | public static void main(final String... args) throws InterruptedException { 49 | final ProgressMeter meter = new ProgressMeter("requests"); 50 | 51 | // Codecs 52 | 53 | // Server 54 | final ServerBootstrap serverBootstrap = new ServerBootstrap() 55 | .group(new NioEventLoopGroup(1), new NioEventLoopGroup()) 56 | .channel(NioServerSocketChannel.class) 57 | .childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT) 58 | .childHandler(new ChannelInitializer() { 59 | @Override 60 | protected void initChannel(final NioSocketChannel ch) throws Exception { 61 | ch.pipeline().addLast(ZMTPCodec.builder() 62 | .socketType(ROUTER) 63 | .build()); 64 | ch.pipeline().addLast(new ServerHandler()); 65 | } 66 | }); 67 | final Channel server = serverBootstrap.bind(ANY_PORT).awaitUninterruptibly().channel(); 68 | 69 | // Client 70 | final SocketAddress address = server.localAddress(); 71 | final Bootstrap clientBootstrap = new Bootstrap() 72 | .group(new NioEventLoopGroup()) 73 | .channel(NioSocketChannel.class) 74 | .option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT) 75 | .handler(new ChannelInitializer() { 76 | @Override 77 | protected void initChannel(final NioSocketChannel ch) throws Exception { 78 | ch.pipeline().addLast( 79 | ZMTPCodec.builder() 80 | .socketType(DEALER) 81 | .build()); 82 | ch.pipeline().addLast(new ClientHandler(meter)); 83 | } 84 | }); 85 | final Channel client = clientBootstrap.connect(address).awaitUninterruptibly().channel(); 86 | 87 | // Run until client is closed 88 | client.closeFuture().await(); 89 | } 90 | 91 | private static class ServerHandler extends ChannelInboundHandlerAdapter { 92 | 93 | private BatchFlusher flusher; 94 | 95 | @Override 96 | public void channelRegistered(final ChannelHandlerContext ctx) throws Exception { 97 | this.flusher = new BatchFlusher(ctx.channel()); 98 | } 99 | 100 | @Override 101 | public void channelRead(final ChannelHandlerContext ctx, final Object msg) throws Exception { 102 | final ZMTPMessage message = (ZMTPMessage) msg; 103 | ctx.write(message); 104 | flusher.flush(); 105 | } 106 | } 107 | 108 | private static class ClientHandler extends ChannelInboundHandlerAdapter { 109 | 110 | private static final int CONCURRENCY = 1000; 111 | 112 | private static final ZMTPMessage REQUEST = ZMTPMessage.fromUTF8( 113 | "envelope1", "envelope2", 114 | "", 115 | Strings.repeat("d", 20), 116 | Strings.repeat("d", 40), 117 | Strings.repeat("d", 100)); 118 | 119 | private final ProgressMeter meter; 120 | 121 | private BatchFlusher flusher; 122 | 123 | public ClientHandler(final ProgressMeter meter) { 124 | this.meter = meter; 125 | } 126 | 127 | @Override 128 | public void channelRegistered(final ChannelHandlerContext ctx) throws Exception { 129 | this.flusher = new BatchFlusher(ctx.channel()); 130 | } 131 | 132 | @Override 133 | public void userEventTriggered(final ChannelHandlerContext ctx, final Object evt) 134 | throws Exception { 135 | if (evt instanceof ZMTPHandshakeSuccess) { 136 | for (int i = 0; i < CONCURRENCY; i++) { 137 | ctx.write(req()); 138 | } 139 | flusher.flush(); 140 | } 141 | } 142 | 143 | private ZMTPMessage req() { 144 | return REQUEST.retain(); 145 | } 146 | 147 | @Override 148 | public void channelRead(final ChannelHandlerContext ctx, final Object msg) throws Exception { 149 | final ZMTPMessage message = (ZMTPMessage) msg; 150 | meter.inc(); 151 | message.release(); 152 | ctx.write(req()); 153 | flusher.flush(); 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/test/java/com/spotify/netty4/handler/codec/zmtp/benchmarks/ThroughputBenchmark.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.spotify.netty4.handler.codec.zmtp.benchmarks; 18 | 19 | import com.spotify.netty4.handler.codec.zmtp.ZMTPCodec; 20 | import com.spotify.netty4.handler.codec.zmtp.ZMTPHandshakeSuccess; 21 | import com.spotify.netty4.handler.codec.zmtp.ZMTPMessage; 22 | import com.spotify.netty4.util.BatchFlusher; 23 | 24 | import java.net.InetSocketAddress; 25 | import java.net.SocketAddress; 26 | 27 | import io.netty.bootstrap.Bootstrap; 28 | import io.netty.bootstrap.ServerBootstrap; 29 | import io.netty.buffer.ByteBuf; 30 | import io.netty.buffer.PooledByteBufAllocator; 31 | import io.netty.channel.Channel; 32 | import io.netty.channel.ChannelHandlerContext; 33 | import io.netty.channel.ChannelInboundHandlerAdapter; 34 | import io.netty.channel.ChannelInitializer; 35 | import io.netty.channel.ChannelOption; 36 | import io.netty.channel.MessageSizeEstimator; 37 | import io.netty.channel.nio.NioEventLoopGroup; 38 | import io.netty.channel.socket.nio.NioServerSocketChannel; 39 | import io.netty.channel.socket.nio.NioSocketChannel; 40 | import io.netty.util.ReferenceCountUtil; 41 | 42 | import static com.google.common.base.Strings.repeat; 43 | import static com.spotify.netty4.handler.codec.zmtp.ZMTPSocketType.DEALER; 44 | import static com.spotify.netty4.handler.codec.zmtp.ZMTPSocketType.ROUTER; 45 | import static io.netty.channel.ChannelOption.ALLOCATOR; 46 | 47 | /** 48 | * A raw one-way throughput benchmark. 49 | */ 50 | public class ThroughputBenchmark { 51 | 52 | private static final InetSocketAddress ANY_PORT = new InetSocketAddress("127.0.0.1", 0); 53 | 54 | public static void main(final String... args) throws InterruptedException { 55 | final ProgressMeter meter = new ProgressMeter("messages"); 56 | 57 | // Server 58 | final ServerBootstrap serverBootstrap = new ServerBootstrap() 59 | .group(new NioEventLoopGroup(1), new NioEventLoopGroup(1)) 60 | .channel(NioServerSocketChannel.class) 61 | .childOption(ALLOCATOR, PooledByteBufAllocator.DEFAULT) 62 | .childHandler(new ChannelInitializer() { 63 | @Override 64 | protected void initChannel(final NioSocketChannel ch) throws Exception { 65 | ch.pipeline().addLast(ZMTPCodec.of(ROUTER)); 66 | ch.pipeline().addLast(new ServerHandler(meter)); 67 | } 68 | }); 69 | final Channel server = serverBootstrap.bind(ANY_PORT).awaitUninterruptibly().channel(); 70 | 71 | // Client 72 | final SocketAddress address = server.localAddress(); 73 | final Bootstrap clientBootstrap = new Bootstrap() 74 | .group(new NioEventLoopGroup(1)) 75 | .channel(NioSocketChannel.class) 76 | .option(ALLOCATOR, PooledByteBufAllocator.DEFAULT) 77 | .option(ChannelOption.MESSAGE_SIZE_ESTIMATOR, ByteBufSizeEstimator.INSTANCE) 78 | .handler(new ChannelInitializer() { 79 | @Override 80 | protected void initChannel(final NioSocketChannel ch) throws Exception { 81 | ch.pipeline().addLast(ZMTPCodec.of(DEALER)); 82 | ch.pipeline().addLast(new ClientHandler()); 83 | } 84 | }); 85 | final Channel client = clientBootstrap.connect(address).awaitUninterruptibly().channel(); 86 | 87 | // Run until client is closed 88 | client.closeFuture().await(); 89 | } 90 | 91 | private static class ServerHandler extends ChannelInboundHandlerAdapter { 92 | 93 | private final ProgressMeter meter; 94 | 95 | public ServerHandler(final ProgressMeter meter) { 96 | this.meter = meter; 97 | } 98 | 99 | @Override 100 | public void channelRead(final ChannelHandlerContext ctx, final Object msg) throws Exception { 101 | ReferenceCountUtil.release(msg); 102 | meter.inc(); 103 | } 104 | } 105 | 106 | private static class ClientHandler extends ChannelInboundHandlerAdapter { 107 | 108 | private static final int BATCH_SIZE = 128; 109 | 110 | private static final ZMTPMessage MESSAGE = ZMTPMessage.fromUTF8(repeat(".", 100)); 111 | 112 | private BatchFlusher flusher; 113 | 114 | @Override 115 | public void channelRegistered(final ChannelHandlerContext ctx) throws Exception { 116 | this.flusher = new BatchFlusher(ctx.channel()); 117 | } 118 | 119 | @Override 120 | public void userEventTriggered(final ChannelHandlerContext ctx, final Object evt) 121 | throws Exception { 122 | if (evt instanceof ZMTPHandshakeSuccess) { 123 | send(ctx); 124 | } 125 | } 126 | 127 | @Override 128 | public void channelWritabilityChanged(final ChannelHandlerContext ctx) throws Exception { 129 | send(ctx); 130 | } 131 | 132 | private void send(final ChannelHandlerContext ctx) { 133 | while(ctx.channel().isWritable()) { 134 | for (int i = 0; i < BATCH_SIZE; i++) { 135 | ctx.write(MESSAGE.retain()); 136 | } 137 | flusher.flush(); 138 | } 139 | } 140 | } 141 | 142 | private static class ByteBufSizeEstimator implements MessageSizeEstimator, 143 | MessageSizeEstimator.Handle { 144 | 145 | public static final ByteBufSizeEstimator INSTANCE = new ByteBufSizeEstimator(); 146 | 147 | @Override 148 | public Handle newHandle() { 149 | return this; 150 | } 151 | 152 | @Override 153 | public int size(final Object msg) { 154 | if (msg instanceof ByteBuf) { 155 | return ((ByteBuf) msg).readableBytes(); 156 | } 157 | return 0; 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | %d{HH:mm:ss.SSS} [%thread] %level %logger{20-}: %msg%n 22 | 23 | System.err 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | --------------------------------------------------------------------------------