In earlier article, we went through basic concepts of Netty & a very simple example of Chat application.
Example in this article
In this article, we will enhance that same chat application & add a custom codec to encode/decode custom object with file read as explained below.
- Use same chat application from earlier article.
- Introduce special chat message (or command) which client can enter in format “file:<file-name>”.
- Create custom POJO object with that command & name of client who ran command. Write that POJO object to channel.
- Create custom codec which will take the custom POJO object & encode/decode. It will take file name from command, read the content of the file. Then it will create a String with client name, file name, file content & start-end marking of content. Then write this String to server. Decoding will be done as plain text.
- This way other client will receive the content of the file.
Lets code
Here is the custom POJO object which we will use in client.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
package com.itsallbinary.netty.chat.enhanced; public class FileMessage { // Command format = file:<file-name> private String fileCommand; private String clientName; public FileMessage(String fileCommand, String clientName) { this.fileCommand = fileCommand; this.clientName = clientName; } String getClientName() { return clientName; } String getFileCommand() { return fileCommand; } } |
Here is the custom codec which will take above custom POJO object & encode it as per above requirements of this example.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
package com.itsallbinary.netty.chat.enhanced; import java.io.File; import java.nio.charset.Charset; import java.nio.file.Files; import java.util.List; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.ByteToMessageCodec; public class FileMessageCodec extends ByteToMessageCodec<FileMessage> { /* * In case of message of type FileMessage, read file & create a String message * with the text inside file. Write final string to buffer. * * File command format file:<file-name> */ @Override protected void encode(ChannelHandlerContext ctx, FileMessage msg, ByteBuf out) throws Exception { File file = new File(msg.getFileCommand().replace("file:", "")); String fileData = Files.readString(file.toPath()); String message = "[" + msg.getClientName() + "]: \n-------------------" + file.getName() + "-------------------\n" + fileData + "\n-------------------End Of File-------------------\n"; out.writeCharSequence(message, Charset.defaultCharset()); } /* * For decoding convert message to String. */ @Override protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception { out.add(msg.toString(Charset.defaultCharset())); } } |
Now we will modify Chat client from earlier article to accept special command & based on that command create custom object & write that object to channel.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
package com.itsallbinary.netty.chat.enhanced; import java.io.File; import java.util.Scanner; import io.netty.bootstrap.Bootstrap; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.codec.string.StringDecoder; import io.netty.handler.codec.string.StringEncoder; public class EnhancedChatClient { static final String HOST = "127.0.0.1"; static final int PORT = 8007; static String clientName; public static void main(String[] args) throws Exception { /* * Get name of the user for this chat session. */ Scanner scanner = new Scanner(System.in); System.out.println("Please enter your name: "); if (scanner.hasNext()) { clientName = scanner.nextLine(); System.out.println("Welcome " + clientName); } /* * Configure the client. */ // Since this is client, it doesn't need boss group. Create single group. EventLoopGroup group = new NioEventLoopGroup(); try { Bootstrap b = new Bootstrap(); b.group(group) // Set EventLoopGroup to handle all eventsf for client. .channel(NioSocketChannel.class)// Use NIO to accept new connections. .handler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throws Exception { ChannelPipeline p = ch.pipeline(); /* * Socket/channel communication happens in byte streams. String decoder & * encoder helps conversion between bytes & String. */ p.addLast(new StringDecoder()); p.addLast(new StringEncoder()); /* * Add FileMessage codec. If message has format file:<file-name>, then this * codec will read file & send content of file to other clients. */ p.addLast(new FileMessageCodec()); // This is our custom client handler which will have logic for chat. p.addLast(new EnhancedChatClientHandler()); } }); // Start the client. ChannelFuture f = b.connect(HOST, PORT).sync(); /* * Iterate & take chat message inputs from user & then send to server. */ while (scanner.hasNext()) { String input = scanner.nextLine(); Channel channel = f.sync().channel(); if (input.startsWith("file:")) { channel.writeAndFlush(new FileMessage(input, clientName)); } else { channel.writeAndFlush("[" + clientName + "]: " + input); } channel.flush(); } // Wait until the connection is closed. f.channel().closeFuture().sync(); } finally { // Shut down the event loop to terminate all threads. group.shutdownGracefully(); } } } |
In order to test, also add below 2 text files at the root of the project.
1 |
This is content of RaviFile.txt. This text file belongs to Ravi. |
1 |
This is content of JohnFile.txt. This text file belongs to John. |
Rest of the code can remain as it is. Here is the remaining code files.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
package com.itsallbinary.netty.chat.enhanced; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; public class EnhancedChatClientHandler extends SimpleChannelInboundHandler<String> { /* * Print chat message received from server. */ @Override protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception { System.out.println("Message: " + msg); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
package com.itsallbinary.netty.chat.enhanced; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.codec.string.StringDecoder; import io.netty.handler.codec.string.StringEncoder; public final class EnhancedChatServer { // Port where chat server will listen for connections. static final int PORT = 8007; public static void main(String[] args) throws Exception { /* * Configure the server. */ // Create boss & worker groups. Boss accepts connections from client. Worker // handles further communication through connections. EventLoopGroup bossGroup = new NioEventLoopGroup(1); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup) // Set boss & worker groups .channel(NioServerSocketChannel.class)// Use NIO to accept new connections. .childHandler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throws Exception { ChannelPipeline p = ch.pipeline(); /* * Socket/channel communication happens in byte streams. String decoder & * encoder helps conversion between bytes & String. */ p.addLast(new StringDecoder()); p.addLast(new StringEncoder()); // This is our custom server handler which will have logic for chat. p.addLast(new EnhancedChatServerHandler()); } }); // Start the server. ChannelFuture f = b.bind(PORT).sync(); System.out.println("Chat Server started. Ready to accept chat clients."); // Wait until the server socket is closed. f.channel().closeFuture().sync(); } finally { // Shut down all event loops to terminate all threads. bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
package com.itsallbinary.netty.chat.enhanced; import java.util.ArrayList; import java.util.List; import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; /** * Handles a server-side channel. */ public class EnhancedChatServerHandler extends SimpleChannelInboundHandler<String> { // List of connected client channels. static final List<Channel> channels = new ArrayList<Channel>(); /* * Whenever client connects to server through channel, add his channel to the * list of channels. */ @Override public void channelActive(final ChannelHandlerContext ctx) { System.out.println("Client joined - " + ctx); channels.add(ctx.channel()); } /* * When a message is received from client, send that message to all channels. * FOr the sake of simplicity, currently we will send received chat message to * all clients instead of one specific client. This code has scope to improve to * send message to specific client as per senders choice. */ @Override public void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception { System.out.println("Server received - " + msg); for (Channel c : channels) { c.writeAndFlush("-> " + msg + '\n'); } } /* * In case of exception, close channel. One may chose to custom handle exception * & have alternative logical flows. */ @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { System.out.println("Closing connection for client - " + ctx); ctx.close(); } } |
Executing the enhanced chat application
Steps for execution are same as earlier article. Here are the console logs.
ChatServer console logs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
Chat Server started. Ready to accept chat clients. Client joined - ChannelHandlerContext(EnhancedChatServerHandler#0, [id: 0x04383fc2, L:/127.0.0.1:8007 - R:/127.0.0.1:51704]) Client joined - ChannelHandlerContext(EnhancedChatServerHandler#0, [id: 0xf7c75f20, L:/127.0.0.1:8007 - R:/127.0.0.1:51731]) Server received - [John]: Hi Ravi Server received - [Ravi]: Hi John Server received - [John]: -------------------JohnFile.txt------------------- This is content of JohnFile.txt. This text file belongs to John. -------------------End Of File------------------- Server received - [Ravi]: -------------------RaviFile.txt------------------- This is content of RaviFile.txt. This text file belongs to Ravi. -------------------End Of File------------------- Closing connection for client - ChannelHandlerContext(EnhancedChatServerHandler#0, [id: 0x04383fc2, L:/127.0.0.1:8007 - R:/127.0.0.1:51704]) Closing connection for client - ChannelHandlerContext(EnhancedChatServerHandler#0, [id: 0xf7c75f20, L:/127.0.0.1:8007 - R:/127.0.0.1:51731]) |
First ChatClient console logs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
Please enter your name: Ravi Welcome Ravi Message: -> [John]: Hi Ravi Hi John Message: -> [Ravi]: Hi John Message: -> [John]: -------------------JohnFile.txt------------------- This is content of JohnFile.txt. This text file belongs to John. -------------------End Of File------------------- file:RaviFile.txt Message: -> [Ravi]: -------------------RaviFile.txt------------------- This is content of RaviFile.txt. This text file belongs to Ravi. -------------------End Of File------------------- |
Second ChatClient console logs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
Please enter your name: John Welcome John Hi Ravi Message: -> [John]: Hi Ravi Message: -> [Ravi]: Hi John file:JohnFile.txt Message: -> [John]: -------------------JohnFile.txt------------------- This is content of JohnFile.txt. This text file belongs to John. -------------------End Of File------------------- Message: -> [Ravi]: -------------------RaviFile.txt------------------- This is content of RaviFile.txt. This text file belongs to Ravi. -------------------End Of File------------------- |
You can find complete code in GIT Repository.