WebSocket ์๊ฐ
WebSocket ํ๋กํ ์ฝ์ ํด๋ผ์ด์ธํธ์ ์๋ฒ๊ฐ ์ธ์ ๋ ์ง ๋ฉ์์ง๋ฅผ ์ ์กํ ์ ์๋๋ก ์๋ฐฉํฅ ๋ฐ์ดํฐ ์ ์ก ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ์ค๊ณ๋์์ต๋๋ค.
Netty๋ WebSocket์ ์๋ฒฝํ ์ง์ํ๋ฉฐ, ์ด๋ฅผ ์ฌ์ฉํ์ฌ ์ค์๊ฐ ์ฑํ
์ ํ๋ฆฌ์ผ์ด์
์ ๊ตฌํํ ์ ์์ต๋๋ค.
์์ WebSocket ์ ํ๋ฆฌ์ผ์ด์
์ด๋ฒ ์์ ์ ํ๋ฆฌ์ผ์ด์
์ WebSocket ํ๋กํ ์ฝ์ ์ฌ์ฉํ์ฌ ๋ธ๋ผ์ฐ์ ๊ธฐ๋ฐ ์ฑํ
์ ํ๋ฆฌ์ผ์ด์
์ ๊ตฌํํฉ๋๋ค.
๋ค์์ ์ฌ์ฉ์๊ฐ ๋์์ ํต์ ํ ์ ์๋๋ก ํ ๊ฒ์
๋๋ค.
WebSocket ์ง์ ์ถ๊ฐ
WebSocket์ผ๋ก ์ ํํ๊ธฐ ์ํด ์
๊ทธ๋ ์ด๋ ํธ๋์
ฐ์ดํฌ ๋ฉ์ปค๋์ฆ์ด ์ฌ์ฉ๋ฉ๋๋ค.
/ws๋ก ๋๋๋ URL์ด ์์ฒญ๋๋ฉด WebSocket์ผ๋ก ํ๋กํ ์ฝ์ ์
๊ทธ๋ ์ด๋ํฉ๋๋ค.
HTTP ์์ฒญ ์ฒ๋ฆฌ
๋จผ์ HTTP ์์ฒญ์ ์ฒ๋ฆฌํ๋ ๊ตฌ์ฑ ์์๋ฅผ ๊ตฌํํฉ๋๋ค. ์ด ๊ตฌ์ฑ ์์๋ ์ฑํ
๋ฐฉ์ ์ ๊ทผํ ์ ์๋ ํ์ด์ง๋ฅผ ์ ๊ณตํ๊ณ , ์ฐ๊ฒฐ๋ ํด๋ผ์ด์ธํธ๊ฐ ๋ณด๋ธ ๋ฉ์์ง๋ฅผ ํ์ํฉ๋๋ค.
public class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
private final String wsUri;
private static final File INDEX;
static {
URL location = HttpRequestHandler.class.getProtectionDomain().getCodeSource().getLocation();
try {
String path = location.toURI() + "index.html";
path = !path.contains("file:") ? path : path.substring(5);
INDEX = new File(path);
} catch (URISyntaxException e) {
throw new IllegalStateException("Unable to locate index.html", e);
}
}
public HttpRequestHandler(String wsUri) {
this.wsUri = wsUri;
}
@Override
public void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
if (wsUri.equalsIgnoreCase(request.getUri())) {
ctx.fireChannelRead(request.retain());
} else {
if (HttpHeaders.is100ContinueExpected(request)) {
send100Continue(ctx);
}
RandomAccessFile file = new RandomAccessFile(INDEX, "r");
HttpResponse response = new DefaultHttpResponse(request.getProtocolVersion(), HttpResponseStatus.OK);
response.headers().set(HttpHeaders.Names.CONTENT_TYPE, "text/plain; charset=UTF-8");
boolean keepAlive = HttpHeaders.isKeepAlive(request);
if (keepAlive) {
response.headers().set(HttpHeaders.Names.CONTENT_LENGTH, file.length());
response.headers().set(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.KEEP_ALIVE);
}
ctx.write(response);
if (ctx.pipeline().get(SslHandler.class) == null) {
ctx.write(new DefaultFileRegion(file.getChannel(), 0, file.length()));
} else {
ctx.write(new ChunkedNioFile(file.getChannel()));
}
ChannelFuture future = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
if (!keepAlive) {
future.addListener(ChannelFutureListener.CLOSE);
}
}
}
private static void send100Continue(ChannelHandlerContext ctx) {
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE);
ctx.writeAndFlush(response);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
WebSocket ํ๋ ์ ์ฒ๋ฆฌ
WebSocket์ ๋ฐ์ดํฐ๋ฅผ ํ๋ ์ ๋จ์๋ก ์ ์กํฉ๋๋ค. Netty๋ RFC์ ์ ์๋ ์ฌ์ฏ ๊ฐ์ง ํ๋ ์ ์ ํ์ ์ง์ํฉ๋๋ค. ์ฌ๊ธฐ์๋ ํ
์คํธ ๋ฐ์ดํฐ๊ฐ ํฌํจ๋ TextWebSocketFrame
๋ ํฌํจ๋ฉ๋๋ค.
public class TextWebSocketFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
private final ChannelGroup group;
public TextWebSocketFrameHandler(ChannelGroup group) {
this.group = group;
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt == WebSocketServerProtocolHandler.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE) {
ctx.pipeline().remove(HttpRequestHandler.class);
group.writeAndFlush(new TextWebSocketFrame("Client " + ctx.channel() + " joined"));
group.add(ctx.channel());
} else {
super.userEventTriggered(ctx, evt);
}
}
@Override
public void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
group.writeAndFlush(msg.retain());
}
}
ChannelPipeline ์ด๊ธฐํ
์ฑ๋์ด ์์ฑ๋ ๋๋ง๋ค ChannelPipeline
์ ์ด๊ธฐํํ๊ธฐ ์ํด ChannelInitializer
๋ฅผ ํ์ฅํฉ๋๋ค.
public class ChatServerInitializer extends ChannelInitializer<Channel> {
private final ChannelGroup group;
public ChatServerInitializer(ChannelGroup group) {
this.group = group;
}
@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(new ChunkedWriteHandler());
pipeline.addLast(new HttpObjectAggregator(64 * 1024));
pipeline.addLast(new HttpRequestHandler("/ws"));
pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
pipeline.addLast(new TextWebSocketFrameHandler(group));
}
}
์๋ฒ ๋ถํธ์คํธ๋ํ
์๋ฒ๋ฅผ ๋ถํธ์คํธ๋ํํ๊ณ ChatServerInitializer
๋ฅผ ์ค์นํฉ๋๋ค.
public class ChatServer {
private final ChannelGroup channelGroup = new DefaultChannelGroup(ImmediateEventExecutor.INSTANCE);
private final EventLoopGroup group = new NioEventLoopGroup();
private Channel channel;
public ChannelFuture start(InetSocketAddress address) {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(group).channel(NioServerSocketChannel.class).childHandler(createInitializer(channelGroup));
ChannelFuture future = bootstrap.bind(address);
future.syncUninterruptibly();
channel = future.channel();
return future;
}
protected ChannelInitializer<Channel> createInitializer(ChannelGroup group) {
return new ChatServerInitializer(group);
}
public void destroy() {
if (channel != null) {
channel.close();
}
channelGroup.close();
group.shutdownGracefully();
}
public static void main(String[] args) throws Exception {
if (args.length != 1) {
System.err.println("Please give port as argument");
System.exit(1);
}
int port = Integer.parseInt(args[0]);
final ChatServer endpoint = new ChatServer();
ChannelFuture future = endpoint.start(new InetSocketAddress(port));
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
endpoint.destroy();
}
});
future.channel().closeFuture().syncUninterruptibly();
}
}
์ ํ๋ฆฌ์ผ์ด์
ํ
์คํธ
์๋ฒ๋ฅผ ๋น๋ํ๊ณ ์์ํ๊ธฐ ์ํด ๋ค์ Maven ๋ช
๋ น์ ์ฌ์ฉํฉ๋๋ค.
mvn -PChatServer clean package exec:exec
๋ค์๊ณผ ๊ฐ์ URL์ ํตํด ์ ํ๋ฆฌ์ผ์ด์
์ ์ ๊ทผํ ์ ์์ต๋๋ค
์ค์ ์๋๋ฆฌ์ค์์๋ ์๋ฒ์ ์ํธํ๋ฅผ ์ถ๊ฐํด์ผ ํ ์ ์์ต๋๋ค. Netty๋ฅผ ์ฌ์ฉํ๋ฉด ChannelPipeline
์ SslHandler
๋ฅผ ์ถ๊ฐํ๊ณ ๊ตฌ์ฑํ๊ธฐ๋ง ํ๋ฉด ๋ฉ๋๋ค.
public class SecureChatServerInitializer extends ChatServerInitializer {
private final SslContext context;
public SecureChatServerInitializer(ChannelGroup group, SslContext context) {
super(group);
this.context = context;
}
@Override
protected void initChannel(Channel ch) throws Exception {
super.initChannel(ch);
SSLEngine engine = context.newEngine(ch.alloc());
ch.pipeline().addFirst(new SslHandler(engine));
}
}
๋ค์์ผ๋ก, ChatServer
๋ฅผ SecureChatServerInitializer
๋ฅผ ์ฌ์ฉํ๋๋ก ์์ ํ์ฌ SslHandler
๋ฅผ ์ค์นํฉ๋๋ค.
public class SecureChatServer extends ChatServer {
private final SslContext context;
public SecureChatServer(SslContext context) {
this.context = context;
}
@Override
protected ChannelInitializer<Channel> createInitializer(ChannelGroup group) {
return new Secure
ChatServerInitializer(group, context);
}
public static void main(String[] args) throws Exception {
if (args.length != 1) {
System.err.println("Please give port as argument");
System.exit(1);
}
int port = Integer.parseInt(args[0]);
SelfSignedCertificate cert = new SelfSignedCertificate();
SslContext context = SslContext.newServerContext(cert.certificate(), cert.privateKey());
final SecureChatServer endpoint = new SecureChatServer(context);
ChannelFuture future = endpoint.start(new InetSocketAddress(port));
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
endpoint.destroy();
}
});
future.channel().closeFuture().syncUninterruptibly();
}
}