This repository hosts a complete workshop on Reactor Netty.
When finishing the workshop one will know how to create and utilize:
492 |-
495 |
-
496 |
497 |UDPserver and client
498 | -
499 |
500 |TCPserver and client
501 | -
502 |
503 |HTTPserver and client
504 |
Reference documentation can be useful while creating those servers and clients:
508 |-
511 |
- 512 | 513 | 514 |
- 515 | 516 | 517 |
-
518 |
Netty documentation (Reactor Netty uses Netty 4.2.x)
519 |
520 |
Prerequisites
526 |-
529 |
-
530 |
531 |Java 8
532 | -
533 |
534 |Java IDEinstalled withMavensupport
535 |
How to start
541 |-
544 |
-
545 |
Clone/fork this repository https://github.com/violetagg/reactor-netty-workshop
546 |
547 | -
548 |
Import the project as
549 |Mavenone in yourIDE
550 | -
551 |
Make sure that the language level is set to
552 |Java 8in yourIDEproject settings
553 | -
554 |
Follow this script to create
555 |HTTP/TCP/UDPserver and client
556 | -
557 |
Fix the
559 |TODOone by one in the tests underio.spring.workshop.reactornetty558 | package in order to make the unit tests green
560 | -
561 |
The solution is available in the
563 |completebranch for comparison. Each step of this workshop 562 | has its companion commit in the git history with a detailed commit message.
564 |
UDP server and client
570 |If one wants to utilize the UDP protocol one will need to create a UDP servers that can send packages
573 | to each other or even UDP clients that can connect to specific UDP servers.
574 | Reactor Netty provides easy to use and configure UdpServer and UdpClient, they hide most of the
575 | Netty functionality that is needed in order to create UDP server and client, and in addition add
576 | Reactive Streams backpressure.
Creating UDP server
580 |UdpServer allows to build, configure and materialize a UDP server.
582 | Invoking UdpServer.create() one can prepare the UDP server for configuration. Having already a UdpServer
583 | instance, one can start configuring the host, port, the IO handler etc.
584 | For configuration purposes, an immutable builder pattern is used.
585 | In the example below the UDP server will be bound on port 8080 and will not use a random port.
UdpServer.create() // Prepares a UDP server for configuration.
590 | .port(0) // Configures the port number as zero, this will let the system
591 | // pick up an ephemeral port when binding the server.
592 | .port(8080)// Configures the port number as 8080.
593 | .bind() // Binds the UDP server and returns a Mono<Connection>.
594 | .block(); // Blocks and waits the server to finish initialising.
595 | When finished with the server configuration, invoking UdpServer.bind(), one will bind the server
599 | and Mono<Connection> will be return, subscribing to this Publisher one can react
600 | on a successfully finished operation or handle issues that might happen. On the other hand
601 | cancelling this Mono, the underlying binding will be aborted. If one do not need to interact with the Mono,
602 | there is UdpServer.bindNow(Duration) which is a convenient method for binding the server and obtaining the
603 | Connection.
604 | The Connection that is emitted as a result of a successfully bound server, holds contextual information
605 | for the underlying channel and provides a non-blocking resource disposing API.
606 | Disposing resource is very important so once the server is not necessary anymore, it must be disposed
607 | by the user via Connection.dispose() or Connection.disposeNow(). The difference between these two methods is
608 | that the second one will release the resources and close the underlying channel in a blocking fashion.
Configuring UDP server
613 |Configuring the Wire Logger
615 |There are use case when one wants to enable a wire logger in order to understand what’s wrong with the server.
617 | For that purposes UdpServer.wiretap can be used and this will use reactor.netty.udp.UdpServer category
618 | with DEBUG level. There are also UdpServer.wiretap(String) and UdpServer.wiretap(String, LogLevel) so that
619 | one can change the category and the log level.
Configuring the Event Loop Group
624 |By default the UDP server will use Event Loop Group where the number of the worker threads will be
626 | the number of processors available to the runtime on init (but with a minimum value of 4). When
627 | one needs a different configuration, one of the LoopResource.create methods can be used in order to configure a new
628 | Event Loop Group. Once the Event Loop Group is configured, the new configuration can be provided using one of the
629 | UdpServer.runOn.
Add UDP server IO handler that will echo the received package
635 |UdpServer.handle(BiFunction<UdpInbound, UdpOutbound, Publisher<Void>>) should be used if one wants to attach IO
637 | handler that will process the incoming packages and will eventually send packages as reply.
638 | UdpInbound is used to receive bytes from the peer where UdpInbound.receiveObject() returns the pure inbound
639 | Flux, while UdpInbound.receive() returns ByteBufFlux which provides an extra API to handle the incoming traffic.
640 | UdpOutbound is used to send bytes to the peer, listen for any error returned by the write operation
641 | and close on terminal signal (complete|error). If more than one Publisher is attached
642 | (multiple calls to UdpOutbound.send* methods), completion occurs when all publishers complete.
643 | The Publisher<Void> that has to be returned as a result represents the sequence of the operations that will be
644 | applied for the incoming and outgoing traffic.
645 | The packages that will be received and that will be send are handled by io.netty.channel.socket.DatagramPacket.
646 | For example if one wants to transform an incoming package and then to prepare a new one for sending, the snippet bellow
647 | can be used:
if (o instanceof DatagramPacket) {
652 | // Incoming DatagramPacket
653 | DatagramPacket p = (DatagramPacket) o;
654 | ByteBuf buf1 = Unpooled.copiedBuffer("Hello ", CharsetUtil.UTF_8);
655 | // Creates a new ByteBuf using the incoming DatagramPacket content.
656 | ByteBuf buf2 = Unpooled.copiedBuffer(buf1, p.content()
657 | .retain());
658 | // Creates a new DatagramPacket with the ByteBuf and the sender
659 | // information from the incoming DatagramPacket.
660 | return new DatagramPacket(buf2, p.sender());
661 | }
662 | Creating UDP client
667 |UdpClient allows to build, configure and materialize a UDP client.
669 | Invoking UdpClient.create() one can prepare the UDP client for configuration. Having already a UdpClient
670 | instance, one can start configuring the host, port, the IO handler etc.
671 | For configuration purposes, the same immutable builder pattern is used as in UdpServer.
When finished with the client configuration, invoking UdpClient.connect(), one will connect the client
675 | and Mono<Connection> will be return, subscribing to this Publisher one can react
676 | on a successfully finished operation or handle issues that might happen. On the other hand
677 | cancelling this Mono, the underlying connecting operation will be aborted. If one do not need to interact with the
678 | Mono, there is UdpClient.connectNow(Duration) which is a convenient method for connecting the client and obtaining
679 | the Connection.
680 | As already described in the UDP server section, disposing the resources can be done via Connection.dispose()
681 | or Connection.disposeNow().
Configuring UDP client
686 |Configuring the Wire Logger
688 |UdpClient.wiretap can be used for wire logging and this will use reactor.netty.udp.UdpClient category
690 | with DEBUG level. There are also UdpClient.wiretap(String) and UdpClient.wiretap(String, LogLevel) so that
691 | one can change the category and the log level.
Add UDP client IO handler that will send a package and will react on an incoming packages
704 |UdpClient.handle(BiFunction<UdpInbound, UdpOutbound, Publisher<Void>>) should be used if one wants to attach IO
706 | handler that will process the incoming packages and will eventually send packages as reply.
707 | Here as a convenience UdpOutbound.send* (e.g. UdpOutbound.sendString) methods can be used instead of
708 | UdpOutbound.sendObject as the client is connected to exactly one UDP server. The same is also for
709 | using UdpInbound.receive() instead of UdpInbound.receiveObject().
Utilize Mono<Connection>
714 |In the section for the UDP server creation was described that as a result of UdpServer.bind one will
716 | receive Mono<Connection> which will emit (complete|error) signals.
717 | Utilizing this Mono one can send a datagram package (the snippet below) as soon as the UDP server is
718 | bound successfully.
DatagramChannel udp = DatagramChannel.open();
723 | udp.configureBlocking(true);
724 | udp.connect(new InetSocketAddress(server1.address().getPort()));
725 |
726 | byte[] data = new byte[1024];
727 | new Random().nextBytes(data);
728 | for (int i = 0; i < 4; i++) {
729 | udp.write(ByteBuffer.wrap(data));
730 | }
731 |
732 | udp.close();
733 | TCP server and client
740 |If one wants to utilize the TCP protocol one will need to create a TCP servers that can send packages
743 | to the connected clients or TCP clients that can connect to specific TCP servers.
744 | Reactor Netty provides easy to use and configure TcpServer and TcpClient, they hide most of the
745 | Netty functionality that is needed in order to create with TCP server and client, and in addition add
746 | Reactive Streams backpressure.
Creating TCP server
750 |TcpServer allows to build, configure and materialize a TCP server.
752 | Invoking TcpServer.create() one can prepare the TCP server for configuration. Having already a TcpServer
753 | instance, one can start configuring the host, port, the IO handler etc.
754 | For configuration purposes, the same immutable builder pattern is used as in UdpServer.
When finished with the server configuration, invoking TcpServer.bind, one will bind the server
758 | and Mono<DisposableServer> will be return, subscribing to this Publisher one can react
759 | on a successfully finished operation or handle issues that might happen. On the other hand
760 | cancelling this Mono, the underlying connecting operation will be aborted. If one do not need to interact with the
761 | Mono, there is TcpServer.bindNow(Duration) which is a convenient method for binding the server and obtaining
762 | the DisposableServer.
763 | DisposableServer holds contextual information for the underlying server.
764 | Disposing the resources can be done via DisposableServer.dispose() or DisposableServer.disposeNow().
Enabling SSL support for TCP server
769 |TcpServer provides several convenient methods for configuring SSL:
-
774 |
-
775 |
776 |TcpServer.secure(SslContext)where SslContext is already configured
777 | -
778 |
780 |TcpServer.secure(Consumer<? super SslProvider.SslContextSpec>)where the SSL configuration customization 779 | can be done via the passed builder.
781 |
Add TCP server IO handler that will send a file
786 |TcpServer.handle(BiFunction<NettyInbound, NettyOutbound, Publisher<Void>>) should be used if one wants to attach IO
788 | handler that will process the incoming messages and will eventually send messages as a reply.
789 | NettyInbound is used to receive bytes from the peer where NettyInbound.receiveObject() returns the pure inbound
790 | Flux, while NettyInbound.receive() returns ByteBufFlux which provides an extra API to handle the incoming traffic.
791 | NettyOutbound is used to send bytes to the peer, listen for any error returned by the write operation
792 | and close on terminal signal (complete|error). If more than one Publisher is attached
793 | (multiple calls to NettyOutbound.send* methods), completion occurs when all publishers complete.
794 | The Publisher<Void> that has to be returned as a result represents the sequence of the operations that will be
795 | applied for the incoming and outgoing traffic.
796 | For example if one wants to send a file to the client where the file name is received as an incoming package,
797 | the snippet bellow can be used:
.handle((in, out) ->
802 | in.receive()
803 | .asString()
804 | .flatMap(s -> {
805 | try {
806 | Path file = Paths.get(getClass().getResource(s).toURI());
807 | return out.sendFile(file)
808 | .then();
809 | } catch (URISyntaxException e) {
810 | return Mono.error(e);
811 | }
812 | }))
813 | Creating TCP client
818 |TcpClient allows to build, configure and materialize a TCP client.
820 | Invoking TcpClient.create() one can prepare the TCP client for configuration. Having already a TcpClient
821 | instance, one can start configuring the host, port, the IO handler etc.
822 | For configuration purposes, the same immutable builder pattern is used as in UdpServer.
When finished with the client configuration, invoking TcpClient.connect(), one will connect the client
826 | and Mono<Connection> will be return, subscribing to this Publisher one can react
827 | on a successfully finished operation or handle issues that might happen. On the other hand
828 | cancelling this Mono, the underlying connecting operation will be aborted. If one do not need to interact with the
829 | Mono, there is TcpClient.connectNow(Duration) which is a convenient method for connecting the client and obtaining
830 | the Connection.
831 | As already described in the UDP server section, disposing the resources can be done via Connection.dispose()
832 | or Connection.disposeNow().
Enabling SSL support for TCP client
837 |TcpClient provides several convenient methods for configuring SSL.
839 | When one wants to use the default SSL configuration provided by Reactor Netty TcpClient.secure() can be used.
840 | If additional configuration is necessary then one of the following methods can be used:
-
844 |
-
845 |
846 |TcpClient.secure(SslContext)where SslContext is already configured
847 | -
848 |
850 |TcpClient.secure(Consumer<? super SslProvider.SslContextSpec>)where the SSL configuration customization 849 | can be done via the passed builder.
851 |
Add TCP client IO handler
856 |TcpClient.handle(BiFunction<NettyInbound, NettyOutbound, Publisher<Void>>) should be used if one wants to attach IO
858 | handler that will process the incoming messages and will eventually send messages as reply.
859 | Here as a convenience NettyOutbound.send* (e.g. NettyOutbound.sendString) methods can be used instead of
860 | NettyOutbound.sendObject. The same is also for using NettyInbound.receive() instead of
861 | NettyInbound.receiveObject().
HTTP server and client
868 |Creating HTTP server
871 |HttpServer allows to build, configure and materialize a HTTP server.
873 | Invoking HttpServer.create() one can prepare the HTTP server for configuration. Having already a HttpServer
874 | instance, one can start configuring the host, port, the IO handler, compression etc.
875 | For configuration purposes, the same immutable builder pattern is used as in UdpServer.
When finished with the server configuration, invoking HttpServer.bind, one will bind the server
879 | and Mono<DisposableServer> will be return, subscribing to this Publisher one can react
880 | on a successfully finished operation or handle issues that might happen. On the other hand
881 | cancelling this Mono, the underlying connecting operation will be aborted. If one do not need to interact with the
882 | Mono, there is HttpServer.bindNow(Duration) which is a convenient method for binding the server and obtaining
883 | the DisposableServer.
884 | Disposing the resources can be done via DisposableServer.dispose() or DisposableServer.disposeNow().
Defining routes for the HTTP server
889 |In HttpServer one can handle the incoming requests and outgoing responses using
891 | HttpServer.handle(BiFunction<HttpServerRequest, HttpServerResponse, Publisher<Void>>) which is similar to the
892 | mechanism that was already described for UdpServer/TcpServer. However there is also a possibility to specify
893 | concrete routes and HTTP methods that the server will respond. This can be done using
894 | HttpServer.route(Consumer<HttpServerRoutes>). Using HttpServerRoutes one can specify the HTTP method, paths etc.
895 | For example the snippet below specifies that the server will respond only on POST method, where the path starts with
896 | /test and has a path parameter.
.route(routes ->
901 | routes.post("/test/{param}", (req, res) ->
902 | res.sendString(req.receive()
903 | .asString()
904 | .map(s -> s + ' ' + req.param("param") + '!'))))
905 | HttpServerRequest provides API for accessing http request attributes as method, path, headers, path parameters etc.
909 | as well as to receive the request body.
910 | HttpServerResponse provides API for accessing http response attributes as status code, headers, compression etc.
911 | as well as to send the response body.
Creating HTTP client
916 |HttpClient allows to build, configure and materialize a HTTP client.
918 | Invoking HttpClient.create() one can prepare the HTTP client for configuration. Having already a HttpClient
919 | instance, one can start configuring the host, port, headers, compression etc.
920 | For configuration purposes, the same immutable builder pattern is used as in UdpServer.
When finished with the client configuration, invoking HttpClient.get|post|… methods, one will receive
924 | HttpClient.RequestSender and will be able start configuring the HTTP request such as the uri and the request body.
925 | HttpClient.RequestSender.send* will end the HTTP request’s configuration and one can start discribing the actions
926 | on the HTTP response when it is received on the returned HttpClient.ResponseReceiver, the response body can be obtained via the provided
927 | HttpClient.ResponseReceiver.response* methods. As HttpClient.ResponseReceiver API always returns Publisher,
928 | the request and response executions are always deferred to the moment when there is a Subscriber
929 | that subscribes to the defined sequence. For example in the snippet below block() will subscribe to the defined
930 | sequence and in fact will trigger the execution.
In the snippet below can be used to send POST request with a body and received the answer from the server:
934 |HttpClient.create() // Prepares a HTTP client for configuration.
938 | .port(server.port()) // Obtain the server's port and provide it as a port to which this
939 | // client should connect.
940 | .wiretap(true) // Applies a wire logger configuration.
941 | .headers(h -> h.add("Content-Type", "text/plain")) // Adds headers to the HTTP request.
942 | .post() // Specifies that POST method will be used.
943 | .uri("/test/World") // Specifies the path.
944 | .send(ByteBufFlux.fromString(Flux.just("Hello"))) // Sends the request body.
945 | .responseContent() // Receives the response body.
946 | .aggregate()
947 | .asString()
948 | .block();
949 |