How should one retry HTTP requests in a Netty-based HTTP client?
Consider the following handler, which attempts to retry the HTTP request after 1 second if HTTP response code 503 is received:
public class RetryChannelHandler extends ChannelDuplexHandler {
List<HttpObject> requestParts;
#Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
if (msg instanceof HttpRequest) {
requestParts = new ArrayList<>();
requestParts.add((HttpRequest)msg);
} else if (msg instanceof HttpObject) {
requestParts.add((HttpObject)msg);
}
super.write(ctx, msg, promise);
}
#Override
public void channelRead(final ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof HttpResponse) {
HttpResponse res = (HttpResponse)msg;
if (res.status().code() == 503) {
ctx.executor().schedule(new Runnable() {
#Override
public void run() {
for (HttpObject obj : requestParts) {
ctx.channel().write(obj);
}
}
}, 1000, TimeUnit.MILLISECONDS);
} else {
super.channelRead(ctx, msg);
}
} else {
super.channelRead(ctx, msg);
}
}
}
When I write to the channel in this example, the other handlers in the pipeline see the HttpObjects, but the HttpRequest is not actually performed again-- only one HttpResponse is ever received.
I think I'm simply misusing Channel in this case, and I need to create a new Channel (representing a new connection to the server) to perform the retry. What's not clear to me is how to create that new Channel from the context of the Handler, and whether I'm really in the right layer of Netty to be doing this kind of logic.
Any guidance on how to achieve the kind of behavior I'm describing would be appreciated.
You will also need to call flush() after the you called write(...) otherwise it will not be flushed the the Channel. Also you will need to ensure you probably retain() and duplicate() HttpContent as otherwise you may will end up trying to write an already released HttpContent object.
Something like this (not tested):
public class RetryChannelHandler extends ChannelDuplexHandler {
Queue<HttpObject> requestParts;
#Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
if (msg instanceof HttpRequest) {
requestParts = new ArrayDeque<>();
requestParts.add((HttpRequest)msg);
} else if (msg instanceof HttpContent) {
requestParts.add(((HttpContent)msg).duplicate().retain());
}
super.write(ctx, msg, promise);
}
#Override
public void channelRead(final ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof HttpResponse) {
HttpResponse res = (HttpResponse)msg;
if (res.status().code() == 503) {
ctx.executor().schedule(new Runnable() {
#Override
public void run() {
HttpObject obj;
while ((obj = requestParts.poll()) != null) {
ctx.write(obj);
}
ctx.flush();
}
}, 1000, TimeUnit.MILLISECONDS);
} else {
HttpObject obj;
while ((obj = requestParts.poll()) != null) {
ReferenceCountUtil.release(obj);
}
super.channelRead(ctx, msg);
}
} else {
super.channelRead(ctx, msg);
}
}
}
Related
I want to calculate the compressed size before HttpDecompressor.
I try to call connection.addHandlerFirst, but does not work.
HttpClient.create()
.mapConnect((connection, bootstrap) -> connection.map(
conn -> {
conn.addHandlerFirst(new ChannelInboundHandlerAdapter() {
#Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof HttpContent) {
System.out.println("received:" + msg);
}
super.channelRead(ctx, msg);
}
});
return conn;
}
))
.compress(true);
Using Connection#addHandlerFirst won't help because the handler will be added after reactor codecs. More information here
You can add this handler directly to the Netty pipeline like this:
HttpClient.create()
.mapConnect((connection, bootstrap) -> connection.map(
conn -> {
conn.channel().pipeline().addBefore(NettyPipeline.HttpDecompressor, "myhandler",new ChannelInboundHandlerAdapter() {
#Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof HttpContent) {
System.out.println("received:" + msg);
}
super.channelRead(ctx, msg);
}
});
return conn;
}
))
.compress(true);
However you should remember that once you add this directly to the pipeline, if you use connection pooling this handler will stay also for the next requests (which is not the case with Connection#addHandlerFirst). So if you need this only for particular requests, then you should remove it after the response is received.
Something like this:
HttpClient.create()
.doOnResponse((res, conn) ->
conn.channel().pipeline().addBefore(NettyPipeline.HttpDecompressor, "myhandler",new ChannelInboundHandlerAdapter() {
#Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof HttpContent) {
System.out.println("received:" + msg);
}
super.channelRead(ctx, msg);
}
}))
.doAfterResponse((res, conn) ->
conn.channel().pipeline().remove("myhandler"))
.compress(true)
I want to close the channel when it hasn't received any data after certain seconds. I tried IdleHandler, but it isn't working. My main handler is clientHandler which extends SimpleChannelInboundHandler. This sends data in string and receives data in String format. Sometimes, I don't get the data during that time I want my channel to close after certain timeout, but currently it is waiting for the data from the server.
One more observation, When I check in the packet sender to verify for the same request. I get empty response from the server but this response is not received by my ClientHandler.
Following is the code.
clientBootstrap.handler(new ChannelInitializer<SocketChannel>() {
#Override
public void initChannel(SocketChannel ch){
ch.pipeline()
.addLast(new IdleStateHandler(5, 5, 10))
.addLast(new MyHandler())
.addLast(new ClientHandler(cardIssueRequest,promise));
}
});
MyHandler:
public class MyHandler extends ChannelDuplexHandler {
#Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
if (evt instanceof IdleStateEvent) {
IdleStateEvent e = (IdleStateEvent) evt;
if (e.state() == IdleState.READER_IDLE) {
ctx.close();
} else if (e.state() == IdleState.WRITER_IDLE) {
ctx.close();
}
}
}
}
ClientHandler:
public class ClientHandler extends SimpleChannelInboundHandler {
RequestModel request;
private final Promise<String> promise;
public ClientHandler(RequestModel request, Promise<String> promise) {
this.request = request;
this.promise = promise;
}
#Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, Object o) {
String response = ((ByteBuf) o).toString(CharsetUtil.UTF_8);
log.info("Client received: " + response);
promise.trySuccess(response);
}
#Override
public void channelActive(ChannelHandlerContext channelHandlerContext) {
log.info("Client sent: " + request);
channelHandlerContext.writeAndFlush(Unpooled.copiedBuffer((request.toString()), CharsetUtil.UTF_8));
}
#Override
public void exceptionCaught(ChannelHandlerContext channelHandlerContext, Throwable cause) {
cause.printStackTrace();
channelHandlerContext.close();
promise.setFailure(cause);
}
}
After taking the thread dump, I found the issue was that my program was waiting in the promise statement. So, after setting timeout for the promise, my issue got solved.
promise.get(60, TimeUnit.SECONDS)
I am trying to implement a TCP server in java using Netty. I am able to handle message of length < 1024 correctly but when I receive message more than 1024, I am only able to see partial message.
I did some research, found that I should implement replayingdecoder but I am unable to understand how to implement the decode method
My message uses JSON
Netty version 4.1.27
protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception
My Server setup
EventLoopGroup group;
group = new NioEventLoopGroup(this.numThreads);
try {
ServerBootstrap serverBootstrap;
RequestHandler requestHandler;
ChannelFuture channelFuture;
serverBootstrap = new ServerBootstrap();
serverBootstrap.group(group);
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.localAddress(new InetSocketAddress("::", this.port));
requestHandler = new RequestHandler(this.responseManager, this.logger);
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(requestHandler);
}
});
channelFuture = serverBootstrap.bind().sync();
channelFuture.channel().closeFuture().sync();
}
catch(Exception e){
this.logger.info(String.format("Unknown failure %s", e.getMessage()));
}
finally {
try {
group.shutdownGracefully().sync();
}
catch (InterruptedException e) {
this.logger.info(String.format("Error shutting down %s", e.getMessage()));
}
}
My current request handler
package me.chirag7jain.Response;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;
import org.apache.logging.log4j.Logger;
import java.net.InetSocketAddress;
public class RequestHandler extends ChannelInboundHandlerAdapter {
private ResponseManager responseManager;
private Logger logger;
public RequestHandler(ResponseManager responseManager, Logger logger) {
this.responseManager = responseManager;
this.logger = logger;
}
#Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf byteBuf;
String data, hostAddress;
byteBuf = (ByteBuf) msg;
data = byteBuf.toString(CharsetUtil.UTF_8);
hostAddress = ((InetSocketAddress) ctx.channel().remoteAddress()).getAddress().getHostAddress();
if (!data.isEmpty()) {
String reply;
this.logger.info(String.format("Data received %s from %s", data, hostAddress));
reply = this.responseManager.reply(data);
if (reply != null) {
ctx.write(Unpooled.copiedBuffer(reply, CharsetUtil.UTF_8));
}
}
else {
logger.info(String.format("NO Data received from %s", hostAddress));
}
}
#Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
}
#Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
this.logger.info(String.format("Received Exception %s", cause.getMessage()));
ctx.close();
}
I would accept data in channelRead() and accumulate it in a buffer. Before return from channelRead() I would invoke read() on a Channel. You may need to record other data, as per your needs.
When netty invokes channelReadComplete(), there is a moment to send whole buffer to your ResponseManager.
Channel read(): Request to Read data from the Channel into the first
inbound buffer, triggers an
ChannelInboundHandler.channelRead(ChannelHandlerContext, Object) event
if data was read, and triggers a channelReadComplete event so the
handler can decide to continue reading.
Your Channel object is accessible by ctx.channel().
Try this code:
private final AttributeKey<StringBuffer> dataKey = AttributeKey.valueOf("dataBuf");
#Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf byteBuf;
String data, hostAddress;
StringBuffer dataBuf = ctx.attr(dataKey).get();
boolean allocBuf = dataBuf == null;
if (allocBuf) dataBuf = new StringBuffer();
byteBuf = (ByteBuf) msg;
data = byteBuf.toString(CharsetUtil.UTF_8);
hostAddress = ((InetSocketAddress) ctx.channel().remoteAddress()).getAddress().getHostAddress();
if (!data.isEmpty()) {
this.logger.info(String.format("Data received %s from %s", data, hostAddress));
}
else {
logger.info(String.format("NO Data received from %s", hostAddress));
}
dataBuf.append(data);
if (allocBuf) ctx.attr(dataKey).set(dataBuf);
ctx.channel().read();
}
#Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
StringBuffer dataBuf = ctx.attr(dataKey).get();
if (dataBuf != null) {
String reply;
reply = this.responseManager.reply(dataBuf.toString());
if (reply != null) {
ctx.write(Unpooled.copiedBuffer(reply, CharsetUtil.UTF_8));
}
}
ctx.attr(dataKey).set(null);
ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
}
An application protocol with variable length messages must have:
a length word
a terminator character or sequence, which in turn implies an escape character in case the data contains the terminator
a self-describing protocol such as XML.
I read the documentation of IdleStateHandlerand from my Server I implemented it same as from the documentation,
but I don't understand on how can I exactly tell if the Client become disconnected for example the Client loses the Wifi Connectivity.
from my understanding, inside my Handler, the method channelInactive() was trigger when the client become disconnected,
then using IdleStateHandler, the IdleState.READER_IDLE will be triggered when no read was performed for the specified period of time,
then after 3 seconds of no read from the client I closed the channel and was expecting that the channelInactive will be trigger but it's not, why?.
Initializer
public class ServerInitializer extends ChannelInitializer<SocketChannel> {
String TAG = "LOG: ";
#Override
protected void initChannel(SocketChannel ch) throws Exception {
System.out.println(TAG + "Starting ServerInitializer class...");
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("decoder", new ObjectDecoder(ClassResolvers.cacheDisabled(null)));
pipeline.addLast("encoder", new ObjectEncoder());
pipeline.addLast("idleStateHandler", new IdleStateHandler(6, 3, 0, TimeUnit.SECONDS));
pipeline.addLast("handler", new ServerHandler());
}
}
Handler
public class ServerHandler extends ChannelInboundHandlerAdapter {
private String TAG = "LOG: ";
public ServerHandler(){}
#Override
public void channelActive(ChannelHandlerContext ctx) {
Log.w(TAG,"New Client become connected, Sending a message to the Client. Client Socket is: " + ctx.channel().remoteAddress().toString());
List<String> msg = new ArrayList<>();
msg.add(0,"sample message 1");
msg.add(1,"sample message 2");
sendMessage(ctx, msg);
}
public void sendMessage(ChannelHandlerContext ctx, List message){
ctx.write(message);
ctx.flush();
}
#Override
public void channelInactive(ChannelHandlerContext ctx) {
Log.w(TAG,"A Client become disconnected. Client Socket is: " + ctx.channel().remoteAddress().toString() + " id: " + (String.valueOf(ctx.channel().hashCode())));
//COnnection id dead, do something here...
}
#Override
public void channelRead(ChannelHandlerContext ctx, Object object) { // (2)
Log.w(TAG, "CLIENT: "+ ctx.channel().remoteAddress().toString() + " SAYS: " + object);
}
#Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { // (4)
// Close the connection for that client when an exception is raised.
Log.e(TAG,"Something's wrong, CLIENT: "+ ctx.channel().remoteAddress().toString() + " CAUSE: " + cause.toString());
ctx.close();
}
#Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
Log.w(TAG,"LOLO");
if (evt instanceof IdleStateEvent) {
IdleStateEvent e = (IdleStateEvent) evt;
if (e.state() == IdleState.READER_IDLE) {
ctx.close(); //Closed the Channel so that the `channelInactive` will be trigger
} else if (e.state() == IdleState.WRITER_IDLE) {
ctx.writeAndFlush("ping\n"); //Send ping to client
}
}
}
}
Anyone can help me out
IdleStateHandler should always be the first handler in your pipeline.
Use the ReadTimeoutHandler instead of IdleStateHandler and override the exceptionCaught method.
I am making a Curl post curl -X POST -d "dsds" 10.0.0.211:5201 to my Netty socket server but in my ChannelRead when I try to cast Object msg into FullHttpRequest It throws following exception.
java.lang.ClassCastException: io.netty.buffer.SimpleLeakAwareByteBuf cannot be cast to io.netty.handler.codec.http.FullHttpRequest
at edu.clemson.openflow.sos.host.netty.HostPacketHandler.channelRead(HostPacketHandler.java:42)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:334)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:326)
at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1320)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:334)
at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:905)
at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:123)
at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:563)
at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:504)
at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:418)
at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:390)
at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:742)
at io.netty.util.concurrent.DefaultThreadFactory$DefaultRunnableDecorator.run(DefaultThreadFactory.java:145)
at java.lang.Thread.run(Thread.java:748)
Following is my Socket Handler class
#ChannelHandler.Sharable
public class HostPacketHandler extends ChannelInboundHandlerAdapter {
private static final Logger log = LoggerFactory.getLogger(HostPacketHandler.class);
private RequestParser request;
public HostPacketHandler(RequestParser request) {
this.request = request;
log.info("Expecting Host at IP {} Port {}",
request.getClientIP(), request.getClientPort());
}
public void setRequestObject(RequestParser requestObject) {
this.request = requestObject;
}
#Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
// Discard the received data silently.
InetSocketAddress socketAddress = (InetSocketAddress) ctx.channel().remoteAddress();
log.info("Got Message from {} at Port {}",
socketAddress.getHostName(),
socketAddress.getPort());
//FullHttpRequest request = (FullHttpRequest) msg;
log.info(msg.getClass().getSimpleName());
//((ByteBuf) msg).release();
}
#Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
// Close the connection when an exception is raised.
cause.printStackTrace();
ctx.close();
}
}
Pipeline:
public class NettyHostSocketServer implements IClientSocketServer {
protected static boolean isClientHandlerRunning = false;
private static final Logger log = LoggerFactory.getLogger(SocketManager.class);
private static final int CLIENT_DATA_PORT = 9877;
private static final int MAX_CLIENTS = 5;
private HostPacketHandler hostPacketHandler;
public NettyHostSocketServer(RequestParser request) {
hostPacketHandler = new HostPacketHandler(request);
}
private boolean startSocket(int port) {
NioEventLoopGroup group = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(group)
.channel(NioServerSocketChannel.class)
.localAddress(new InetSocketAddress(port))
.childHandler(new ChannelInitializer<SocketChannel>() {
#Override
public void initChannel(SocketChannel ch)
throws Exception {
ch.pipeline().addLast(
hostPacketHandler);
}
});
ChannelFuture f = b.bind().sync();
log.info("Started host-side socket server at Port {}",CLIENT_DATA_PORT);
return true;
// Need to do socket closing handling. close all the remaining open sockets
//System.out.println(EchoServer.class.getName() + " started and listen on " + f.channel().localAddress());
//f.channel().closeFuture().sync();
} catch (InterruptedException e) {
log.error("Error starting host-side socket");
e.printStackTrace();
return false;
} finally {
//group.shutdownGracefully().sync();
}
}
#Override
public boolean start() {
if (!isClientHandlerRunning) {
isClientHandlerRunning = true;
return startSocket(CLIENT_DATA_PORT);
}
return true;
}
#Override
public int getActiveConnections() {
return 0;
}
}
I also used wireshark to check If I am getting valid packets or not. Below is the screenshot of Wireshark dump.
Your problem is that you never decode the ByteBuf into an actual HttpRequest object which is why you get an error. You can't cast a ByteBuf to a FullHttpRequest object.
You should do something like this:
#Override
public void initChannel(Channel channel) throws Exception {
channel.pipeline().addLast(new HttpRequestDecoder()) // Decodes the ByteBuf into a HttpMessage and HttpContent (1)
.addLast(new HttpObjectAggregator(1048576)) // Aggregates the HttpMessage with its following HttpContent into a FullHttpRequest
.addLast(hostPacketHandler);
}
(1) If you also want to send HttpResponse use this handler HttpServerCodec which adds the HttpRequestDecoder and HttpResponseEncoder.