http 서버에서 정상적인 응답 메세지 전송 후 'java.io.IOException: 현재 연결은 원격 호스트에 의해 강제로 끊겼습니다' 에러가 발생하는 현상

18,906 views
Skip to first unread message

JIHYE LEE

unread,
Feb 15, 2017, 4:36:14 AM2/15/17
to Netty Korean User Group
안녕하세요.
Netty 기반의 http 요청을 처리하는 서버를 작성하였는데, client 에서 connection 종료시 exceptionCaught 이벤트 핸들러가 호출 되고 있습니다.  

테스트를 해보면, 
client 에서 get 요청 전송시 SimpleChannelInboundHandler의 channelRead0 이벤트가 2번 호출됩니다.
1번째는 HttpRequest, 2번째는 LastHttpContent 메세지가 전달되어 정상적으로 내부 로직 처리후 client로 응답 메세지를 보냅니다.

그리고 나서  exceptionCaught 이벤트가 호출되며  'java.io.IOException: 현재 연결은 원격 호스트에 의해 강제로 끊겼습니다' 에러가 발생하고 있습니다.

<Error>
ERROR 02.15 11:50:34.517 [nioEventLoopGroup-4-1 ] [HttpListenerHandler   .exceptionCaught            ]: 105 - 
java.io.IOException: 현재 연결은 원격 호스트에 의해 강제로 끊겼습니다
at sun.nio.ch.SocketDispatcher.read0(Native Method)
at sun.nio.ch.SocketDispatcher.read(SocketDispatcher.java:43)
at sun.nio.ch.IOUtil.readIntoNativeBuffer(IOUtil.java:223)
at sun.nio.ch.IOUtil.read(IOUtil.java:192)
at sun.nio.ch.SocketChannelImpl.read(SocketChannelImpl.java:379)
at io.netty.buffer.UnpooledUnsafeDirectByteBuf.setBytes(UnpooledUnsafeDirectByteBuf.java:447)
at io.netty.buffer.AbstractByteBuf.writeBytes(AbstractByteBuf.java:881)
at io.netty.channel.socket.nio.NioSocketChannel.doReadBytes(NioSocketChannel.java:242)
at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:119)
at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:511)
at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:468)
at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:382)
at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:354)
at io.netty.util.concurrent.SingleThreadEventExecutor$2.run(SingleThreadEventExecutor.java:111)
at io.netty.util.concurrent.DefaultThreadFactory$DefaultRunnableDecorator.run(DefaultThreadFactory.java:137)
at java.lang.Thread.run(Thread.java:744)

< http 요청 / 응답 내용>
GET /testtest/users/TestUser3432124 HTTP/1.1
Host: 
Connection: Keep-Alive, TE
TE: trailers
User-Agent: RPT-HTTPClient/0.3-3E

HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
Content-Length: 226
Connection: keep-alive

{
  "result" : 393222,
  "loginId" : "TestUser3432124",
}
 

<구현 코드>
public class TestHttpListenerHandler extends SimpleChannelInboundHandler<Object> {
private static final Logger logger = LoggerFactory.getLogger(TestHttpListenerHandler.class);

private HttpRequest request;
private ByteBuf byteBufContent = PooledByteBufAllocator.DEFAULT.heapBuffer();// Unpooled.buffer();
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
   logger.info("channelReadComplete.");
ctx.flush();
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) {

if (msg instanceof HttpRequest) {
request = (HttpRequest) msg;

if (HttpHeaders.is100ContinueExpected(request))
ctx.write(new DefaultFullHttpResponse(HTTP_1_1, CONTINUE));
}

if (msg instanceof HttpContent) {
HttpContent httpContent = (HttpContent) msg;
try {
byteBufContent = byteBufContent.writeBytes(httpContent.content());
} catch (IndexOutOfBoundsException e) {
logger.error(e.getMessage(), e);
}

if (httpContent instanceof LastHttpContent) {
FullHttpRequest fullHttpRequest = new DefaultFullHttpRequest(request.getProtocolVersion(), request.getMethod(), request.getUri(), byteBufContent);
fullHttpRequest.headers().set(request.headers());
FullHttpResponse fullHttpResponse = httpDispatcher.handleHttpRequestMessage(ctx, fullHttpRequest);
if (!writeResponse(ctx, fullHttpRequest, fullHttpResponse)) {
// If keep-alive is off, close the connection once the content is fully written.
ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
}
request = null;
byteBufContent.clear();
}
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
logger.error("", cause);
ctx.close();
}


private boolean writeResponse(ChannelHandlerContext ctx, FullHttpRequest request, FullHttpResponse response) {
     // Decide whether to close the connection or not.
     boolean keepAlive = HttpHeaders.isKeepAlive(request);

     if (keepAlive) {
 // Add 'Content-Length' header only for a keep-alive connection.
 response.headers().set(CONTENT_LENGTH, response.content().readableBytes());
 // Add keep alive header as per:
 response.headers().set(CONNECTION, HttpHeaders.Values.KEEP_ALIVE);
     }

     // Write the response.
     ctx.write(response);

     return keepAlive;
}
}


확인 결과 client 에서 connection 종료시 FIN 패킷이 아닌 RST 패킷이 전달되고 있는데요, 이럴경우 위 에러가 발생할 수 있는지 궁금합니다.
( client 에서 보내는 요청의 http 헤더에는 Keep-Alive 설정이 되어 있습니다. )
코드상 잘 못 구현한 부분이 있다면 조언 부탁드리겠습니다. 

감사합니다. 

이희승 (Trustin Lee)

unread,
Feb 23, 2017, 8:58:38 AM2/23/17
to nett...@googlegroups.com
안녕하세요.

특별히 잘못된 부분은 없어 보입니다.

'현재 연결은 원격 호스트에 의해 강제로 끊겼습니다'는 소위 말하는 'connection reset by peer' 에러로, 상대방이 정상적인 handshake 절차를 거치지 않고 RST 패킷으로 연결을 끊을 때 발생합니다.

이 현상은 실제 서비스를 운영하다 보면 네트워크 상황 등의 다양한 이유로 인해 빈번히 발생하게 됩니다. 따라서 크게 신경 쓸 것은 없다고 생각합니다.

다만, 말씀하신 것처럼 클라이언트가 항상 RST 패킷으로 커넥션을 마무리한다면, 클라이언트 측 소켓의 SO_LINGER 설정이 0으로 되어 있을 가능성이 있으므로, 클라이언트 측 개발자 분과 상의하실 필요가 있을 것 같습니다. 보통 다음과 같은 이유 (커넥션을 닫는 측의 TIME_WAIT 상태를 억제하기 위해) 로 일부러 그렇게 설정하는 경우가 있습니다:


이희승 드림
--
이 메일은 Google 그룹스 'Netty Korean User Group' 그룹에 가입한 분들에게 전송되는 메시지입니다.
이 그룹에서 탈퇴하고 더 이상 이메일을 받지 않으려면 netty-ko+u...@googlegroups.com에 이메일을 보내세요.
더 많은 옵션을 보려면 https://groups.google.com/d/optout을(를) 방문하세요.

JIHYE LEE

unread,
Mar 2, 2017, 2:09:30 AM3/2/17
to Netty Korean User Group
안녕하세요. 답변 감사합니다.
    
    네 맞습니다. 
    현재 nGrinder를 사용해서 부하 테스트를 진행하는데요, 연결 종료시 RST 패킷으로 TCP 연결 종료를 하고 있어서 해당 exception 이 발생하고 있습니다. 
   
    서버에서 계속 exception 이 올라오다 보니 성능 체크하는데 영향이 있어서 해당 exception 을 발생하지 않게 할 수 없을까? 라는 생각으로 질문을 드렸었는데, 다른 방법은 없을것 같네요. 
    소켓연결 종료시 FIN 으로 종료될 수 있도록 클라이언트를 작성해서 테스트를 진행해 봐야 할 것 같습니다.

    감사합니다. 

 
 
Reply all
Reply to author
Forward
0 new messages