JavaのNettyとC++のBoost.AsioでTCP通信してみた

JavaのNettyとC++のBoost.AsioでTCP接続を試してみたときのメモになります。

Nettyとは

Netty は非同期のイベント駆動型ネットワークアプリケーションフレームワークで、FTP,SMTP,HTTPなどのレガシープロトコルから得られた経験をもとに開発の容易さ、パフォーマンス、安定性、柔軟性の実現に成功しているらしいです。

netty.io

通信のプロトコルが何か特別というわけではなく、メッセージの受信に対してイベント駆動でプログラミングを行いやすくなるものなので、TCP接続が行えれば良いのJavaのオブジェクトをシリアライズして送るなど言語依存のメッセージを行わないのであれば通信先の言語は何でも良いかと思います。

Nettyだからというわけではなく、ストリームを扱うプログラミングに言えるのですが以下のリンク先の"Dealing with a Stream-based Transport"にあるように3つのメッセージを連続を送ったとしてもパケットはメッセージ単位ではなくバイトサイズにより分割して送られるので、一つのメッセージの終端は判断できるようにする必要があります。

netty.io

今回はJava側でチュートリアルにあったエコーサーバをコピペして確認したのですが、実装は以下のようになりました。

EchoHandler .java

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

public class EchoHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ctx.write(msg);
        ctx.flush();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        // Close the connection when an exception is raised.
        cause.printStackTrace();
        ctx.close();
    }
}

EchoServer.java

package jp.co.teruuu.echo;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;

public class EchoServer {

    private int port;

    public EchoServer(int port) {
        this.port = port;
    }

    public void run() throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup(); // (1)
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap(); // (2)
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class) // (3)
                    .childHandler(new ChannelInitializer<SocketChannel>() { // (4)
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new EchoHandler());
                        }
                    })
                    .option(ChannelOption.SO_BACKLOG, 128)          // (5)
                    .childOption(ChannelOption.SO_KEEPALIVE, true); // (6)

            // Bind and start to accept incoming connections.
            ChannelFuture f = b.bind(port).sync(); // (7)

            // Wait until the server socket is closed.
            // In this example, this does not happen, but you can do that to gracefully
            // shut down your server.
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws Exception {
        int port = 8080;
        if (args.length > 0) {
            port = Integer.parseInt(args[0]);
        }

        new EchoServer(port).run();
    }
}

EchoHandlerのchannelReadにあるように受け取ったメッセージをそのまま送り返してみます。EchoServerはサーバー側の立ち上げを行っていまして、NIOのサーバでイベントハンドラとしてEchoHandlerを指定しています。Nettyは単純にTCP接続のプログラミングを行いやすくするものなので、EchoServerを立ち上げた後はtelnetで接続して動きを確認することもできます。

Boost.Asio

次にC++のライブラリのBoost.Asioですが、これ自体はネットワーク専用というよりは非同期プログラミングのライブラリでネットワークプログラミングの機能も含まれているようです。

www.boost.org

WindowsのWSL2上で動作確認をしたのですが、sudo apt install libboost-asio-devでインストール後以下のCMakeLists.txtで使えるようになりました。

# set minimum cmake version
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)

# project name and language
project(try-boost LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

add_executable(try-boost src/try-boost.cpp)

FIND_PACKAGE (Threads)
target_link_libraries(try-boost PRIVATE Threads::Threads)

find_package(Boost 1.71.0 REQUIRED system)
target_include_directories(try-boost PUBLIC ${Boost_INCLUDE_DIRS})

入れてから気づいたのですが、Boost.Asioはヘッダオンリーライブラリになるのでtarget_include_directoriesでインクルードで見に行くディレクトリを追加するだけでよいようです。また、cmake後のcmake --buildでpthread周りのライブラリがリンクされていないとエラーメッセージが表示されていたのでtarget_link_libraries(try-boost PRIVATE Threads::Threads)しています。

それから、C++側はJavaのエコーサーバにつなぎに行くクライアントになるのですが、実装は以下のようになりました。

#include <iostream>
#include <boost/asio.hpp>

namespace asio = boost::asio;
using asio::ip::tcp;

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cerr << "Usage: <host> <port>" << std::endl;
        return 1;
    }

    asio::io_service io_service;
    tcp::socket socket(io_service);

    // 接続
    socket.connect(tcp::endpoint(asio::ip::address::from_string(argv[1]), atoi(argv[2])));

    std::string send_msg;
    boost::asio::streambuf read_buffer;

    for (;;)
    {

        std::cout << "send message: ";
        std::cin >> send_msg;

        // メッセージ送信
        boost::system::error_code error;
        asio::write(socket, asio::buffer(send_msg), error);

        if (error)
        {
            std::cout << "send failed: " << error.message() << std::endl;
        }
        else
        {
            std::cout << "send correct" << std::endl;
        }

        // メッセージ受信
        asio::read(socket, read_buffer, boost::asio::transfer_exactly(send_msg.length()), error);
        if (error)
        {
            std::cout << "read failed: " << error.message() << std::endl;
        }
        else
        {
            std::string data = boost::asio::buffer_cast<const char *>(read_buffer.data());
            std::cout << "read correct: " << data << std::endl;
            read_buffer.consume(send_msg.length());
        }
    }
}

socket.connect(tcp::endpoint(asio::ip::address::from_string(argv[1]), atoi(argv[2])));でつなぎに行って、asio::write(socket, asio::buffer(send_msg), error);でメッセージを送って、asio::read(socket, read_buffer, boost::asio::transfer_exactly(send_msg.length()), error);で書き込んだバイトサイズ分読み込むというものになります。

動きを確認すると以下のようにちゃんとエコーサーバからのレスポンスを受け取れることが確認できました。

$ ./try-boost 172.30.208.1 8080
send message: hello,world
send correct
read correct: hello,world
send message: へろーわーるど
send correct
read correct: へろーわーるど
send message: