본 포스팅은 2편으로 구성되어 있습니다.
Netty
네티는 비동기 이벤트 기반 네트워크 애플리케이션 프레임워크로써 유지보수를 고려한 고성능 프로토콜 서버와 클라이언트를 빠르게 개발할 수 있다.
네티를 사용하면 네티가 이벤트를 사용하여 데이터를 처리해주기 때문에 순수 자바코드로 짠 네트워크프로그램보다 훨씬 쉽게 개발할 수 있다.
소켓
소켓은 데이터 송수신을 위한 네트워크 추상화단위인데, 일반적으로 네트워크 프로그램에서 소켓은 ip와 port를 가지고있으며 양방향 네트워크 통신이 가능한 객체를 의미.
동기 & 비동기
- 동기 : 서비스처리가 완료된 이후에 처리 결과를 확인하는 방식을 동기식 호출(결과가 올때까지 대기해야함)
- 비동기 : 서비스처리가 완료되기전에 우선 응답을 전달하는 방식(대기하는것을 방지)
블로킹 & 논블로킹
소켓의 동작방식은 블로킹 & 논블로킹으로 나뉜다.
- 블로킹 : 요청한 작업이 성공하거나 에러가 발생하기 전까지는 응답을 돌려주지않는 것을 의미 (ServerSocket, Socket 클래스)
- 블로킹 소켓은 데이터입출력에서 스레드의 블로킹이 발생하기 때문에 동시에 여러 클라이언트에 대한 처리가 불가능. 그렇기때문에 다중 클라이언트 접속처리를 위하여 연결된 클라이언트별로 각각 스레드를 할당함. → 다만 이방법은 ServerSocket 클래스의 accept()가 블로킹모드로 동작하기때문에 여러 쓰레드가 접근하면 대기시간이 길어지고 쓰레드를 무한정 생성하기때문에 OOM발생가능성 → 그러므로 쓰레드풀을 만들어서 OOM을 방지하도록 하지만 동시 접속수를 늘리기 위해서 스레드풀은 크게 늘리는것은 많은 이유로 제약이 있음. → 이러한 단점을 개선한 방식이 논블로킹 소켓방식
- 논블로킹 : 요청한 작업의 성공여부와 상관없이 바로 결과를 돌려줌 (ServerSocketChannel, SocketChannel 클래스)
블로킹에서 논블로킹으로 변환할때 많은 부분을 새롭게 개발하지 않기위해서 자바는 추상화된 api를 제공하고있다.
논블로킹 소켓은 구조적으로 소켓으로부터 읽은 데이터를 바로 소켓에 쓸수없다.
이를 위해서 각 이벤트가 공유하는 데이터 객체를 생성하고 그 객체를 통해서 각 소켓 채널로 데이터를 전송한다.
그러므로 논블로킹으로 데이터를 구성하면 필연적으로 프로그램이 복잡해지는데, 네티는 개발자가 기능구현에 집중할수있도록 이와 같은 기능을 프레임워크 레벨에서 API로 제공하고있다.
부트스트랩
ServerBootStrap API (서버)
부트스트랩은 네티로 작성한 네트워크 애플리케이션이 시작할 때 가장 처음 수행되는 부분으로, 애플리케이션이 수행할 동작과 환경을 설정하는 도우미 클래스.
부트스트랩은 크게 3가지로 구분
- 이벤트루프 - 소켓 채널에서 발생한 이벤트를 처리하는 스레드 모델에 대한 구현(group 메서드)
- 채널전송모드 - 블로킹, 논블로킹, epoll(입출력다중화기법, 대신 리눅스에서만 가능) (channel메서드) (NioServerSocketChannel, OioServerSocketChannel..)
- 채널파이프라인 - 네티채널과 이벤트 핸들러 사이의 연결통로. 소켓채널로 수신된 데이터를 처리할 데이터핸들러를 지정(handler, childHandler메서드)
- handler - 서버 소켓 채널의 이벤트 핸들러 설정
- childHandler - 클라이언트 소켓 채널로 송수신되는 데이터 가공 핸들러 설정
- option메서드 - 서버 소컷채널의 옵션 설정 / 커널에서 사용되는 값 변경 (SO_SNDBUF, SO_RCVBUF, SO_BACKLOG ..)
- TCP_NODELAY (Nagle알고리즘 비활성화여부 지정, 디폴트:비활성화) - 한번에 모아서 보내서 해더를 줄이자. 대신 ack가 올때까지 다음문자를 안보내기때문에 빠른 응답불가
- SO_KEEPALIVE - 운영체제에서 지정된 시간에 한번씩 keepalive 패킷을 상대방에게 전송, 디폴트:false
- SO_REUSEADDR (TIME_WAIT 상태의 포트라도 서버 소켓에 바인드함, 디폴트: 비활성화 ) - 상대방으로부터 끊겠다는 연락을 받고 내가 상대방으로 ACK를 보내는데 그게 상대방에게 도달할때까지 일정시간기다리는게 TIME_WAIT. 이시점에서 다시 서버를 키면 TIME_WAIT로 포트가 물고있기때문에 서버가 기동이 안되는데, 그걸 무시하고 포트를 사용할수있음
- SO_BACKLOG (동시에 수용 가능한 소켓 연결 요청 수) - 동시 연결된 수가 아니라 동시 연결요청 수임
- .option(ChannelOption.SO_BACKLOG, 1) // 서버가 동시에 하나의 연결 요청만 수용
- cilldOption메서드 - 서버에 접속한 클라이언트 소켓 채널에 대한 옵션 설정
- SO_LINGER - close후에 커널 버퍼에 아직 전송되지 않는 데이터가 남아있을때의 처리, 디폴트:false
- .childOption(ChannelOption.SO_LINGER, 0(타임아웃값)) // close시, 커널 버퍼의 데이터를 상대방에게 모두 전송, 타임아웃0초 // 0이면 TIME_WAIT로 전환안됨 (남은 데이터를 상대방 소켓채널로 모두 전송하고 즉시 연결끊음), TIME_WAIT가 안되는 장점은 있지만, 마지막으로 전송한 데이터가 잘 전송되어있는지는 확인불가
public class EchoServer {
public static void main(String[] args) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(1); // 클라이언트의 연결을 수락하는 부모쓰레드, 스레드 그룹내에서 생성할 최대 쓰레드 1개.
EventLoopGroup workerGroup = new NioEventLoopGroup(); // 연결된 클라이언트의 소켓으로부터 데이터 입출력 및 이벤트 처리를 담당하는 자식쓰레드. 인수없으면 하드웨어코어수를 기준.
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class) // 서버 소켓 채널에서 발생한 이벤트만 처리
.childHandler(new ChannelInitializer<SocketChannel>() { // 서버에 연결된 클라이언트 소켓 채널에서 발생한 이벤트를 수신하여 처리
@Override
public void initChannel(SocketChannel ch) {
ChannelPipeline p = ch.pipeline();
p.addLast(new EchoServerHandler());
}
});
ChannelFuture f = b.bind(8888).sync();
f.channel().closeFuture().sync();
}
finally {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
}
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
String readMessage = ((ByteBuf) msg).toString(Charset.defaultCharset());
StringBuilder builder = new StringBuilder();
builder.append("수신한 문자열 [");
builder.append(readMessage);
builder.append("]");
System.out.println(builder.toString());
ctx.write(msg);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
// Close the connection when an exception is raised.
cause.printStackTrace();
ctx.close();
}
}
BootStrap API (클라이언트)
기본적으로 서버부트스트랩과 같다. 단, 클라이언트는 단일 소켓 채널 설정이므로 부모 자식관계의 api가 없다. childOption() 이런 메서드는 없고 option만 제공
- group - 클라이언트는 한개의 이벤트 루프객채를 설정(서버는 부모, 자식)
- channel - 클라이언트 소켓 채널 설정 (NioSocketChannel, OioSocketChannel..)
- handler - 클라이언트 소켓 채널의 이벤트 핸들러 설정 (서버와 다르게 childHandler가 아님)
- option - 클라이언트 소켓 채널의 소켓 옵션 설정 (서버와 다르게 childOption이 아님)
public final class EchoClient {
public static void main(String[] args) throws Exception {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
p.addLast(new EchoClientHandler());
}
});
ChannelFuture f = b.connect("localhost", 8888)
.sync(); // sync - 채널요청이 완료될때까지 대기 / 요청실패하면 예외
f.channel().closeFuture().sync();
} finally {
group.shutdownGracefully();
}
}
}
public class EchoClientHandler extends ChannelInboundHandlerAdapter {
// 소켓채널이 최초 활성화 되었을때 실행되는 메서드
@Override
public void channelActive(ChannelHandlerContext ctx) {
String sendMessage = "Hello netty";
ByteBuf messageBuffer = Unpooled.buffer();
messageBuffer.writeBytes(sendMessage.getBytes());
StringBuilder builder = new StringBuilder();
builder.append("전송한 문자열 [");
builder.append(sendMessage);
builder.append("]");
System.out.println(builder.toString());
ctx.writeAndFlush(messageBuffer);
}
// 서버로 부터 수신된 데이터가 있을 떄 호출되는 메서드
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
String readMessage = ((ByteBuf) msg).toString(Charset.defaultCharset());
StringBuilder builder = new StringBuilder();
builder.append("수신한 문자열 [");
builder.append(readMessage);
builder.append("]");
System.out.println(builder.toString());
}
// 수신된 데이터를 모두 읽었을 떄 호출되는 이벤트 메서드. channelRead() 수행이 완료되고 자동 포출
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
ctx.close();
}
// 수신된 데이터를 모두 읽은 후 서버와 연결된 채널을 닫음
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
채널 파이프라인
채널 파이프라인은 네티의 채널과 이벤트 핸들러 사이에서 연결통로 역할 수행.
- 클라이언트 연결에 대응하는 소켓 채널 객체를 생성하고, 빈 채널 파이프라인 객체를 생성하여 소켓채널에 할당.
- 소켓 채널에 등록된 ChannelInitializer 인터페이스의 구현체를 가져와서 initChannel 메서드 호출.
- 소켓 채널 참조로부터 1에서 등록한 파이프라인 객체를 가져오고 채널 파이프라인에 입력된 이벤트 핸들러 객체를 등록.
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
// 연결된 클라이언트 소켓채널이 사용할 채널 파이프라인 설정
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
// ch는 연결된 클라이언트 소켓채널을 의미
public void initChannel(SocketChannel ch) { // 클라이언트 소켓 채널이 생성될때 자동호출
ChannelPipeline p = ch.pipeline(); // 네티 내부에서 클라이언트 소켓 채널을 생성할때
// 빈채널파이프라인객체를 생성및 할당하기때문에 꺼내쓸수있는것
p.addLast(new EchoServerV1Handler()); // 이벤트를 다룰 핸들러를 연결
}
});
ChannelFuture f = b.bind(8888).sync();
f.channel().closeFuture().sync();
이벤트 핸들러
- 소켓채널의 이벤트를 인터페이스로 정의
- 이 인터페이스를 상속받은 이벤트 핸들러를 작성하여 채널 파이프라인에 등록
- 채널 파이프라인으로 입력되는 이벤트를 이벤트루프가 가로채어 첫번째 핸들러부터 이벤트에 해당하는 메서드있는지를 찾고 실행. 있다면 다음핸들러를 실행하지않음
- 해당하는 메서드가 없다면 다음 핸들러로 넘겨서 해당하는 메서드가 있는지 찾고 실행..
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
ChannelPipeline p = ch.pipeline();
p.addLast(new EchoServerV4FirstHandler());
p.addLast(new EchoServerV4SecondHandler());
}
});
public class EchoServerV4FirstHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf readMessage = (ByteBuf) msg;
System.out.println("FirstHandler channelRead : " + readMessage.toString(Charset.defaultCharset()));
ctx.write(msg);
ctx.fireChannelRead(msg); // 다음핸들러의 channelRead메서드도 실행하고싶을때 설정. 이게 없다면 해당이벤트는 소비되어서 다음핸들러로 넘어가지않음
}
}
네티는 소켓채널에서 발생하는 이벤트를 인바운드(ChannelInboundHandler), 아웃바운드(ChannelOutboundHandler) 이벤트로 구분하고있다.
채널인바운드 이벤트
인바운드 이벤트는 소켓 채널에서 발생한 이벤트 중에서 연결 상대방이 어떤 동작을 취했을 때 발생. ex) 채널활성화, 데이터 수신
- channelRegistered - 서버와 클라이언트에 상관없이 새로운 채널이 생성되는 시점에 발생 (서버는 본인꺼, 클라이언트꺼해서 2번발생, 클라이언트는 1번)
- channelActive - channelRegistered이후에 발생. 채널 입출력을 수행할 상태가 되었음을 알려줌. 상대방에 연결한 직후에 한번 수행할 작업을 처리하기 적절.
- channelRead - 데이터가 수신되었음을 알려줌. 수신된 데이터는 네티의 ByteBuf 객체에 저장되어 있으며, 이벤트 메서드의 두번째 인자인 msg를 통해 접근 가능.
public class EchoServerV1Handler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf readMessage = (ByteBuf) msg;
System.out.println("channelRead : " + readMessage.toString(Charset.defaultCharset()));
ctx.write(msg); // flush가 없으므로 전송을 미룸.
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
System.out.println("channelReadComplete 발생");
ctx.flush(); // flush() 네티채널 버퍼에 저장된 데이터를 상대방으로 즉시 전송. channelReadComplete()이므로 소켓채널에서 더이상 읽어들이 데이터가 없을때 데이터를 전송함
}
- channelReadComplete - channelRead이벤트는 채널에 데이터가 있을때 발생하는 것과 달라, 채널에 데이터를 다읽어서 더이상 데이터가 없을때 발생
- channelInactive - 채널이 비활성화되었을때 발생. 이 이후에는 채널에대한 입출력 불가능
- channelUnregistered - 채널이 이벤트루프에서 제거되었을때 발생
채널아웃바운드 이벤트
아웃바운드 이벤트는 소켓 채널에서 발생한 이벤트 중에서 네티사용자가 요청한 동작에 해당하는 이벤트. ex) 연결 요청, 데이터 전송, 소켓 닫기
- bind - 서버소켓채널이 클라이언트의 연결을 대기하는 ip와 포트가 설정되었을때 발생. 서버소켓채널이 사용중인 SocketAddress객체가 인수로 입력됨.
- connect - 클라이언트 소켓채널이 서버에 연결되었을때 발생. 원격지SocketAddress정보와 로컬SocketAddress정보가 인수로 입력됨
- disconnect - 클라이언트 소켓채널의 연결이 끊어졌을때 발생. 별도의 인수없음
- close - 클라이언트 소캣채널의 연결이 닫혔을때 발생. 별도의 인수없음
- wirte - 소켓채널에 데이터가 기록되었을때 발생. 소켓채널에 기록된 데이터 버퍼가 인수로 입력됨.
- flush - 소켓채널에 대한 flush메서드가 호출되었을떄 발생. 별도의 인수없음
코덱
네티에서 인코더는 전송할 데이터를 전송프로토콜에 맞추어 변환 작업을 수행하고, 디코더는 반대의 작업을 수행
ChannelOutboundHandler = 인코더 / ChannelInboundHandler = 디코더
데이터를 전송할때는 (서버가 읽을때는) 인코더를 사용하여 패킷으로 변환하고, 데이터를 수신할때는(서버가 보낼때는) 디코더를 사용하여 패킷을 데이터로 변환한다.
네티는 자주사용되는 포로토콜의 인코더와 디코더를 기본으로 제공한다. (io.netty.handler.codec 패키지에 포함)
- base64 코덱 - 8비트 이진 데이터를 ASCII 영역의 문자열로 바꾸는 인코딩을 의미
- bytes 코덱 - 바이트 배열 데이터에 대한 송수신을 지원하는 코덱
- http 코덱 - http 프로토콜을 지원하는 코덱으로 하위 패키지에서 다양한 데이터 송수신 방법을 지원.
- marshalling 코덱 - 객체를 송신가능한 형태로 변환하는과정(json, xml..)
- string 코덱 - 문자열 송수신을 지원하는코덱. 주로 텔넷 프로토콜이나 채팅서버 프로토콜에 사용
- serialization - 자바의 객체를 네트워크로 전송할 수 있도록 직렬화& 역직렬화를 지원하는 코딕
이벤트루프
이벤트를 실행하기 위한 무한루프 스레드.
이벤트루프가 처리한 이벤트 결과를 돌려주는 방식에 따라서 콜백패턴, 퓨처패턴으로 나뉨. (네티는 둘다 지원)
이벤트 기반 애플리케이션이 이벤트를 처리하는 방법은 크게 2가지.
- 이벤트 처리하는 로직을 가진 이벤트 메서드를 이벤트 리스너에 등록하고 객체에 이벤트가 발생했을때 이벤트 처리 쓰레드에서 등록된 메서드를 수행(단일스레드)
- 이벤트 큐에 이벤트를 등록하고 이벤트 루프가 이벤트 큐에 접근하여 처리(단일,다중스레드)
다중쓰레드 이벤트는 처리시간을 단축할수있지만, cpu사용률이 100%에 근접한 상태에서 쓰레드 개수를 늘리면 오히려 성능이 줄어들게되므로 적절한 개수가 중요하다(cpu경합, 컨택스트 스위치 때문)
다중쓰레드는 이벤트 큐의 발생순서와 실행순서가 일치하지않는게 기본원칙이지만, 네티에서는 다중쓰레드 이벤트라도 실행순서를 일치를 보장한다. (이벤트 큐를 공유하지 않기 때문에 순서가 보장됨)
네티는 비동기호출을 위한 두가지 패턴을 제공.
- 리액터 패턴의 구현체인 이벤트 핸들러
- 퓨처패턴(케이크 주문을하면 주문서를 바로 주듯이, 메서드를 호출하는 즉시 퓨처 객체를 돌려줌)
ChannelFuture future = b.bind(8888); // 포트 바인딩이 완료되기전에 ChannelFuture객체를 돌려줌 (케이트주문서)
future.sync(); // sync메서드는 주어진 ChannelFuture 객체의 작업이 완료될때까지 블로킹하는 메서드. bind메서드 처리가 완료되면 sync도 같이완료됨
Channel channel = future.channel(); // future를 통해서 채널을 얻어온다. 여기서 얻어진 채널은 8888번 포트에 바인딩된 서버채널
ChannelFuture closeFuture = channel.closeFuture(); // 바인드가 완료된 서버채널의 ChannelFuture객체를 돌려줌
closeFuture.sync(); // 채널의 연결이 종료될 때 연결 종료 이벤트를 받는다. 이벤트만 받을때는 아무일도 하지않음
// 비동기 I/O 메서드 호출의 결과로 ChannelFuture객체를 돌려받고 이 객체로 작업완료 유무를 확인한다.
// 퓨처객체가 while문으로 작업의 완료유무를 계속 확인하지않고(비효율적이니까) **네티는 작업이 완료되면 수행할 리스너를 설정할 수 있다.**
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ChannelFuture channelFuture = ctx.writeAndFlush(msg);
// ChannelFuture 객체가 완료 이벤트를 수신할때 채널을 닫음(성공유무 관계없음)
**channelFuture.addListener(ChannelFutureListener.CLOSE);**
}
네티가 제공하는 기본 채널리스너(ChannelFutureListener 인터페이스 구현)
- ChannelFutureListener.CLOSE - ChannelFuture 객체가 작업 완료 이벤트를 수신했을때 ChannelFuture 객체에 포함된 채널을 닫는다.(작업성공유무 관계없이)
- ChannelFutureListener.CLOSE_ON_FAILURE - ChannelFuture 객체가 완료 이벤트를 수신하고 결과가 실패일 때 ChannelFuture 객체에 포함된 채널을 닫는다.
- ChannelFuture.FIRE_EXCEPTION_ON_FAILURE - 객체가 완료 이벤트를 수신하고 결과가 실패일때 채널 예외 이벤트를 발생시킨다.
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ChannelFuture channelFuture = ctx.writeAndFlush(msg); // 클라이언트 소켓버퍼에 데이터 기록완료하고 채널로 전송(클라이언트에게 되돌려줌)
final int writeMessageSize = ((ByteBuf) msg).readableBytes(); // 네티가 수신한 msg객체는 ByteBuf객체
channelFuture.addListener(new ChannelFutureListener() { // 사용자 정의 리스너사용
@Override
// ChannelFuture 객체에서 발생하는 작업 완료 이벤트 메서드
public void operationComplete(ChannelFuture future) throws Exception {
System.out.println("전송한 Byte : " + writeMessageSize);
future.channel().close(); // 채널 닫기 이벤트 발생
}
});
}
위와 같은 식으로 직접구현하면 더많은 기능을 사용할수있음.
바이트버퍼
자바 버퍼는 바이트 데이터를 저장하는 저장소로써 저장된 데이터를 읽고 쓰는 메서드를 제공한다. 저장되는 데이터 형에 따라서 읽고 쓰는 데이터의 크기가 달라지는데 이를 버퍼 추상 클래스를 사용하여 처리한다.
자바는 기본적으로 자체 버퍼 패키지를 제공한다. 그러나 하나의 바이트 버퍼에 대하여 읽기 작업 또는 쓰기 작업의 완료를 의미하는 filp메서드를 호출해야만 반대작업을 수행할 수 있는 불편함이(그래서 자바바이트 버퍼를 사용할때는 읽기와 쓰기를 분리해야 생각해야하며 특히 다중 스레드 환경에서는 바이트 버퍼를 공유하지 않아야한다) 존재하기 때문에 네티는 자바 퍼버를 쓰지않고 네티 자체의 바이트버퍼를 사용한다(네티는 위와 같은 문제점을 해결하기위해 읽기인덱스, 쓰기인덱스를 별도로 제공). 또한 아래와 같은 장점제공
네티 바이트 버퍼
- 네티 바이트 버퍼만 사용하고자한다면 netty-buffer-4.0.31.Final.jar파일을 넣으면된다.
- 네티 바이트 버퍼는 별도의 읽기 인덱스와 쓰기 인덱스를 제공하므로 flip메서드 없이도 읽기쓰기 병행이 가능하다.
- 가변바이트 버퍼이다. (자바 바이트 버퍼는 생성시 지정한 크기를 못바꿈)
ByteBuf buf1 = Unpooled.buffer(11); // 버퍼풀 사용하지않는 11바이트 크기의 힙 버퍼 생성 (jvm 힙영역에 생성)
ByteBuf buf2 = Unpooled.directBuffer(11); // 버퍼풀 사용하지않는 11바이트 크기의 다이렉트 버퍼 생성(운영체제 커널영역에 생성)
ByteBuf buf3 = PooledByteBufAllocator.DEFAULT.heapBuffer(11); // 버퍼풀 사용하는 11바이트 크기의 힙 버퍼 생성
ByteBuf buf4 = PooledByteBufAllocator.DEFAULT.directBuffer(11); // 버퍼풀 사용하는 11바이트 크기의 다이렉트 버퍼 생성
// 바이트 인수를 지정해주지않으면 기본값인 256바이트 크기의 버퍼가 생성된다.
System.out.println(buf.capacity());
System.out.println(buf.isDirect());
buf.writeInt(1234); // 정수형 4바이트 쓰기
System.out.println(buf.readableBytes());
System.out.println(buf.writableBytes());
System.out.println(buf.readShort()); // 2바이트를 읽고
System.out.println(buf.readableBytes()); // 4바이트중 2바이트를 읽었으니 2바이트가 남음
System.out.println(buf.writableBytes());
buf.capacity(20); // 동적으로 크기 변경
System.out.println(buf.capacity());
buf.writeGBytes("abc".getBytes()); // 문자열 쓰기
buf.clear(); // 읽기 쓰기 인덱스 모두 0으로 초기화
- 바이트 버퍼풀 제공하여 가비지 컬렉션 횟수 감소(자바는 자체적으로 바이트 버퍼풀을 제공하지않아서 객체풀링을 사용하려면 서드파티 라브러리를 사용하거나 직접구현해야함)
- 네티 바이트 버퍼의 기본 엔디안은 자바와 동일하게 빅엔디안이다. 특별한 상황에서 리틀엔디안 바이트 버퍼가 필요할때는 order메서드를 사용하여 변환
ByteBuf a = buf.order(ByteOrder.LITTLE_ENDIAN);
- 자바 바이트버퍼와 상호변환이 가능하다.
public void convertNettyBufferToJavaBuffer() {
ByteBuf buf = Unpooled.buffer(11);
buf.writeBytes("abcd".getBytes());
ByteBuffer nioByteBuffer = buf.nioBuffer(); // 네티 -> 자바
}
public void convertJavaBufferToNettyBuffer() {
ByteBuffer byteBuffer = ByteBuffer.wrap("abcd".getBytes());
ByteBuf buf = Unpooled.wrappedBuffer(byteBuffer); // 자바 -> 네티
}
- 채널과 바이트풀
channelRead메서드의 인수로 사용되는 바이트 버퍼는 네티 바이트 버퍼다. 풀에 관리되며 작업이 끝난후에 풀로 돌아간다.
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf readMessage = (ByteBuf) msg; // 네티 바이트버퍼. 풀에 관리되므로 channelRead가 실행된이후 풀로 돌아감
System.out.println("channelRead : " + readMessage.toString(Charset.defaultCharset()));
// alloc메서드를 통하여 네티 프레임워크에서 초기화된 ByteBufAllocator로 접근가능. 다이렉트 버퍼풀이 디폴트. 개발자의 필요에따라 힙버퍼 생성가능
ByteBufAllocator byteBufAllocator = ctx.alloc();
// buffer메서드로 풀에서 네티 바이트버퍼를 생성할수있음. release메서드가 호출되거나 버이트 버퍼를 채널에 기록하면 버퍼풀로 돌아감
ByteBuf newBuffer = byteBufAllocator.buffer();
ctx.write(msg); // 바이트 버퍼가 입력되면, 채널에 기록하고 버퍼풀로 돌아감
}
참조
네트워크 소녀 netty 책 참조
'IT > 오픈소스' 카테고리의 다른 글
JPA - 기본 개념 (1) | 2024.01.30 |
---|---|
카프카(Kafka) - 활용정리 (0) | 2023.09.12 |
카프카(Kakfa) 개념정리 (1) | 2023.09.11 |
Docker란? (1) | 2023.03.26 |
네티(Netty) 기본정리2 (0) | 2022.02.22 |
댓글