I have recently started experimenting with Netty for the purpose of creating a video game server/client relationship. I am using the Netty "Object Echo" example provided with Netty 4.1.X source. The client and server are contained in separate projects. The class "VersionInfo" is available in both the server and client projects, but with different package names.
The problem I am having, when I send a custom object from the client to the server, it is not received. However, if I send a String object, it works perfectly.
Server:
public class Server {
private int port;
public Server(int port) {
this.port = port;
}
public void run() throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ServerChannelInitializer())
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true);
// Bind and start to accept incoming connections.
ChannelFuture channelFuture = serverBootstrap.bind(port).sync();
// Wait until the server socket is closed.
// In this example, this does not happen, but you can do that to gracefully
// shut down your server.
channelFuture.channel().closeFuture().sync();
} finally {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
public static void main(String[] args) {
try {
new Server(1337).run();
} catch (Exception e) {
e.printStackTrace();
}
}
}
Server Channel Initializer:
public class ServerChannelInitializer extends ChannelInitializer<SocketChannel> {
public void initChannel(SocketChannel channel) throws Exception {
ChannelPipeline pipeline = channel.pipeline();
pipeline.addLast(new ObjectEncoder());
pipeline.addLast(new ObjectDecoder(ClassResolvers.cacheDisabled(null)));
pipeline.addLast(new ObjectEchoServerHandler());
}
}
Object Echo Server Handler:
public class ObjectEchoServerHandler extends ChannelInboundHandlerAdapter {
public static final String VERSION = "1.0.0";
@Override
public void channelActive(ChannelHandlerContext ctx) {
System.out.println("[Server] Channel Active");
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
// business logic here.
System.out.println("[Server] channelRead()");
if (msg instanceof VersionInfo) {
VersionInfo versionInfo = (VersionInfo) msg;
String clientVersion = versionInfo.version;
System.out.println("[Server] Version Info Received! Client Version: " + clientVersion + " Local Version: " + VERSION);
if (clientVersion.equals(VERSION)) {
VersionInfo success = new VersionInfo();
success.versionChecked = true;
ctx.write(success);
System.out.println("[Server] Version Check Passed!");
} else {
// send client version info with false boolean, meaning server version not the same.
ctx.write(msg);
System.out.println("[Server] Version Check Failed!");
}
} else if (msg instanceof String) {
ctx.write("[Server] " + msg);
} else {
System.out.println("ERROR! Received unknown object! Class name: " + msg.getClass().getSimpleName());
}
//ctx.write(msg);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
System.out.println("ExceptionCaught");
cause.printStackTrace();
ctx.close();
}
}
VersionInfo Class (on both Client & Server, but different package names):
public class VersionInfo {
public String version = "1.0.0";
public boolean versionChecked = false;
}
Now on to the client code:
public class Client {
public static void main(String[] args) throws Exception {
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(workerGroup);
bootstrap.channel(NioSocketChannel.class);
bootstrap.option(ChannelOption.TCP_NODELAY, true);
bootstrap.option(ChannelOption.SO_KEEPALIVE, true);
bootstrap.handler(new ClientChannelInitializer());
// Start the client.
ChannelFuture channelFuture = bootstrap.connect("localhost", 1337).sync(); // (5)
// Wait until the connection is closed.
channelFuture.channel().closeFuture().sync();
} finally {
workerGroup.shutdownGracefully();
}
}
}
ClientChannelInitializer:
public class ClientChannelInitializer extends ChannelInitializer<SocketChannel> {
@Override
public void initChannel(SocketChannel channel) throws Exception {
ChannelPipeline pipeline = channel.pipeline();
pipeline.addLast(new ObjectEncoder());
pipeline.addLast(new ObjectDecoder(ClassResolvers.cacheDisabled(null)));
pipeline.addLast(new ObjectEchoClientHandler());
}
}
Finally ObjectEchoClientHandler:
public class ObjectEchoClientHandler extends ChannelInboundHandlerAdapter {
private final String string = "This is a test message! :)";
private final VersionInfo versionInfo = new VersionInfo();
@Override
public void channelActive(ChannelHandlerContext ctx) {
// Send the first message if this handler is a client-side handler.
System.out.println("[Client] Sending Version Info");
ctx.writeAndFlush(versionInfo); // this.. does nothing?
//ctx.writeAndFlush(string); // this works
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
System.out.println("[Client] channelRead()");
// Check client version
if (msg instanceof VersionInfo) {
VersionInfo versionInfo = (VersionInfo) msg;
System.out.println("[Client] Received Version Info");
if (versionInfo.versionChecked) {
System.out.println("[Client] Version Check Passed!");
} else {
ctx.close(); // try closing the connection
System.out.println("[Client] Version Check Failed!");
}
} else if (msg instanceof String) {
System.out.println(msg.toString());
// Echo back the received object to the server.
ctx.write(string);
} else {
System.out.println("ERROR! Received unknown object! Class name: " + msg.getClass().getSimpleName());
}
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
Sending a string in ObjectEchoClientHandler channelActive method works fine. Sending a custom POJO/Object does not invoke ObjectEchoServerHandler channelRead method.
What could be causing this?
EDIT #1: Adding logging text below as suggested by Nicholas. Sorry for the blockquote around the logging debug. Stack Overflow editor would not let me submit it without it.
VersionInfo class changed to reflect same package names. New class:
package com.nettytest.iocommon;
public class VersionInfo {
public String version = "1.0.0";
public boolean versionChecked = false;
}
Client output after sending custom Pojo.
Mar 22, 2017 1:09:43 PM io.netty.handler.logging.LoggingHandler channelRegistered SEVERE: [id: 0x237af0b8] REGISTERED Mar 22, 2017 1:09:43 PM io.netty.handler.logging.LoggingHandler connect SEVERE: [id: 0x237af0b8] CONNECT: localhost/127.0.0.1:1337 [Client] Sending Version Info Mar 22, 2017 1:09:43 PM io.netty.handler.logging.LoggingHandler channelActive SEVERE: [id: 0x237af0b8, L:/127.0.0.1:52830 - R:localhost/127.0.0.1:1337] ACTIVE
Mar 22, 2017 1:09:43 PM io.netty.handler.logging.LoggingHandler write SEVERE: [id: 0x237af0b8, L:/127.0.0.1:52830 - R:localhost/127.0.0.1:1337] WRITE: com.nettytest.iocommon.VersionInfo@7544bac Mar 22, 2017 1:09:43 PM io.netty.handler.logging.LoggingHandler flush SEVERE: [id: 0x237af0b8, L:/127.0.0.1:52830 - R:localhost/127.0.0.1:1337] FLUSH
Server output after receiving custom Pojo.
Mar 22, 2017 1:09:43 PM io.netty.handler.logging.LoggingHandler channelRegistered [Server] Channel Active SEVERE: [id: 0xcdffaece, L:/127.0.0.1:1337 - R:/127.0.0.1:52830] REGISTERED Mar 22, 2017 1:09:43 PM io.netty.handler.logging.LoggingHandler channelActive
SEVERE: [id: 0xcdffaece, L:/127.0.0.1:1337 - R:/127.0.0.1:52830] ACTIVE
One way of debugging this would be to add a LoggingHandler to your server pipeline. Something like this:
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.logging.LogLevel;
....
final LoggingHandler loggingHandler = new LoggingHandler(getClass(),
LogLevel.ERROR);
....
pipeline.addLast(new ObjectDecoder(ClassResolvers.cacheDisabled(null)));
pipeline.addLast("logger", loggingHandler);
pipeline.addLast(new ObjectEchoServerHandler());
....
My initial suspicion is that since the pojo type you are passing has different package names on the client and the server (meaning they are not the same class) that the ObjectDecoder on the client is failing since it cannot find the class in its classpath. For this to work correctly, the class must be present in the server and client classpaths.