Web Socket

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();
    }
}

Last updated