grep

Culture

TOAST Haste 소개

NHN

2016년 12월 26일

원문에서 보기 ↗

목차

들어가기에 앞서 TOAST Haste Let's haste 결론

들어가기에 앞서

우리는 오랜 시간 동안 TCP(Transmission Control Protocol)를 이용해서 많은 애플리케이션을 개발해왔습니다. TCP는 패킷 순서 보장, 흐름 제어, 혼잡 제어 등 많은 역할을 해주어 개발자들이 좀 더 추상적인 수준에서 네트워크를 바라볼 수 있게 도와줍니다. 그래서 개발자들은 단말 간의 네트워크를 바이트 스트림으로 바라보고 손쉽게 개발을 할 수 있습니다.

하지만 특정 분야(예를 들면 실시간성이 필요한)에서는 TCP가 해주는 일들이 과하게 느껴질 때가 있고, 제 성능을 내지 못하기도 합니다. 가장 대표적으로는 Head of line blocking (이하 HOL blocking) 이 있습니다.

그럼 TCP 대신 UDP를 이용하면 어떨까요? UDP는 Best effort로 동작하며, 단말의 애플리케이션 간의 포트를 구분해주는 역할만 합니다. 다르게 보자면, UDP는 새하얀 도화지 같아서, 그 위에 얼마든지 기능을 추가할 수 있습니다. 실제로 Google은 대부분 서비스에 UDP를 기반으로 한 QUIC 프로토콜을 선택적으로 사용하고 있습니다.

UDP에 대해서 좀 더 다양한 내용을 알고 싶으시면 권오범 님이 작성해주신 UDP를 사용할 때 고려해야 할 것들 을 읽어보시면 많은 도움이 될 것 같습니다.

게임은 어떤가요? 한국의 멀티플레이 게임들은 대부분 TCP를 이용해서 개발되고 있습니다. 앞서 설명해 드렸다시피 TCP는 굉장히 좋은 전송 프로토콜입니다. 하지만 UDP를 이용하면 멀티플렉싱, Wi-Fi Cellular handover 등의 부분에서 이득을 얻을 수 있습니다. 이런 장점을 얻기 위해 UDP를 이용해서 게임을 개발하기 시작하면 많은 고민을 마주하게 됩니다. 그리고 이런 고민을 해결하다 보면 콘텐츠 개발보다 네트워크 레이어 개발에 많은 시간을 투자할 수밖에 없게 되고, 일정에 쫓기다가 결국 다시 TCP로 돌아가게 됩니다. 그럼 빠르게 UDP를 이용한 게임 서버 개발은 불가능한 것일까요?

우리도 위와 같은 고민을 했습니다. 매번 네트워크 레이어의 개발은 할 수 없으니 재사용 가능하게 만들어두면 다음엔 더 쉽게 만들 수 있지 않을까? 라는 생각이 들었습니다. 그리고 그렇게 만들어진 프레임워크가 바로 TOAST Haste 입니다.

TOAST Haste

Toasthaste.png

TOAST Haste 는 비동기 게임서버 프레임워크 입니다. TOAST Haste 를 이용해서 개발하면 아래와 같은 장점을 얻을 수 있습니다.

그럼 하나하나 자세히 살펴보겠습니다.

TOAST Haste의 소스는 https://github.com/nhnent/toast-haste.framework 에서 확인할 수 있습니다. TOAST Haste .NET SDK는 https://github.com/nhnent/toast-haste.sdk.dotnet 에서 확인할 수 있습니다.

다양한 QoS 제공

UDP는 기본적으로 Best effort로 동작합니다. 즉, Port로 애플리케이션을 구분해주는 것 이외는 아무런 일을 하지 않습니다. 따라서 패킷 순서 보장 및 재송신을 구현해야 합니다. TOAST Haste는 패킷 순서를 보장 및 재송신에 대한 구현이 되어 있으며, 아래와 같은 QoS를 제공하고 있습니다.

TOAST Haste 의 QoS 구현은 ENet(MIT License)과 TCP 구현 일부를 바탕으로 개발되었습니다.

Multiplexing

TOAST Haste 는 논리적인 Channel을 지정해서 송수신할 수 있도록 설계되었습니다. Channel 별로 처리가 수행되기 때문에, 데이터를 역할에 따라 적절하게 Channel 별로 나눠서 송수신할 수 있습니다. 적절한 Channel 분산은 HOL blocking 문제를 최소화시킬 수 있습니다.

Reliable sequenced(혹은 Reliable fragmented) QoS로 송수신할 경우, 패킷 유실시 Channel 별로 HOL blocking 문제가 발생합니다. 따라서 도메인별로 적절한 채널 분배가 필요합니다.

Wi-Fi Cellular handover

TCP의 경우 연결기반이기 때문에 Source IP, Source Port, Destination IP, Destination Port 4가지 정보를 이용해서 하나의 연결을 식별합니다. 따라서, Wi-Fi와 Cellular 이동 시 망이 변경되어 IP가 바뀌면 새로운 연결을 시도해야 합니다. TOAST Haste는 자체 식별번호로 연결을 관리하고 있으므로 IP가 변경되어도 재연결 없이 연결이 유지됩니다.

선택적 암호화

게임뿐만 아니라 모든 애플리케이션에서 중요한 데이터들의 암호화는 필수입니다. 하지만 게임의 경우, 모든 패킷을 암호화하면 실시간성을 보장하기 힘들어지기 때문에 필요에 따라 암호화를 수행해야 합니다. TOAST Haste 는 연결 성립 시, 암호화에 대한 사전정보를 주고받습니다. 이후에는 사용자가 필요할 때마다 암호화를 수행할 수 있습니다. 참고로 TOAST Haste는 키교환에 Diffie-Hellman Algorithm을, 데이터 암호화에는 AES256을 사용 중입니다.

Let's haste

간단한 Echo 서버와 클라이언트를 만들어보면서 TOAST Haste의 사용법을 익혀보도록 합시다.

Echo server

1. GameServerBootstrap을 통한 서버 설정

public class EchoServer {
    private static final int PORT = 5056;

    public static void main(String[] args) {
        GameServerBootstrap bootstrap = new GameServerBootstrap();

        bootstrap.application(new EchoServerApplication())
                .option(UDPOption.THREAD_COUNT, 2)
                .bind(PORT).start();
    }
}

2. EchoServerApplication 에서 ClientPeer를 생성하는 코드를 추가합니다.

public class EchoServerApplication extends ServerApplication {
    @Override
    protected void setup() {
    }

    @Override
    protected void tearDown() {
    }

    @Override
    protected ClientPeer createPeer(InitialRequest initialRequest, NetworkPeer networkPeer) {
        return new EchoPeer(initialRequest, networkPeer);
    }
}

3. 실제 주고받는 데이터인 EchoMessage 클래스를 구현합니다.

public class EchoMessage extends MessageBridge {
    public static final short MESSAGE = 0;

    public EchoMessage(RequestMessage request) {
        super(request);
    }

    @FieldParameter(Code = MESSAGE)
    public String message;
}

4. 실제 클라이언트와 데이터 송수신을 하는 EchoPeer 클래스를 구현합니다.

public class EchoPeer extends ClientPeer {
    private static final Logger logger = LoggerFactory.getLogger(EchoPeer.class);

    public EchoPeer(InitialRequest initialRequest, NetworkPeer networkPeer) {
        super(initialRequest, networkPeer);
    }

    @Override
    protected void onReceive(RequestMessage request, SendOptions options) {
        EchoMessage message = new EchoMessage(request);

        logger.info(MessageFormat.format("Client message is \"{0}\"", message.message));

        ResponseMessage response = message.toResponse();
        this.send(response, options);
    }

    @Override
    protected void onDisconnect(DisconnectReason reason, String detail) {
    }
}

Echo client

1. NetworkConnection 객체 생성 및 설정 객체 만들기

_connection = new NetworkConnection();
_config = new ConnectionConfig
{
    ChannelCount = 5,
    DisconnectionTimeout = 3000,
    IsCrcEnabled = false,
    MaxUnreliableCommands = 0,
    MTUSize = 1350,
    PingInterval = 1000,
    SentCountAllowance = 3,
    WarningQueueSize = 500,
};
_connection.Configure(_config);

2. NetworkConnection 객체에 응답 이벤트 등록하기

_connection.ResponseReceived += OnResponseReceived;
_connection.StatusChanged += OnStatusChanged;
...

const int MESSAGE_CODE = 0;

private static void OnResponseReceived(ResponseMessage response)
{
    if (response.Code == MESSAGE_CODE)
    {
        string message = string.Empty;
        if (response.Data.GetValue(MESSAGE_PARAM_CODE, out message))
        {
            Console.WriteLine("[OnResponseReceived] Server message is \"{0}\"", message);
        }
    }
}

3. 연결하기 및 응답 쓰레드 시작하기

IPEndPoint remoteEndPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 5056);
_connection.Connect(remoteEndPoint, new Version(0, 1, 0), null);

Thread receiveThread = new Thread(() =>
{
    while (true)
    {
        _connection.NetworkUpdate();
    }
});
receiveThread.Start();

4. 데이터 송신을 위한 메서드 만들기

const int MESSAGE_PARAM_CODE = 0;

public void Send(string input)
{
    DataObject data = new DataObject();
    data.SetString(MESSAGE_PARAM_CODE, input);
    _connection.SendRequestMessage(MESSAGE_CODE, data, SendOptions.ReliableSend);
}

결론

TCP는 오랜 시간 동안, 그리고 지금도 좋은 전송 프로토콜이며 이 점에 대해선 조금의 의심도 없습니다. 하지만 애플리케이션에 따라서 TCP의 장점들이 가끔은 한계로 다가올 때가 있습니다. 대표적인 예로 실시간성이 있어야 하는 서비스들이며, 그 서비스 중에서 대표적인 것이 게임입니다. 게임 서버 프레임워크가 가져야 하는 가치는 네트워크가 전부는 아닙니다. TOAST Haste는 다양한 QoS의 네트워크 구현뿐만 아니라, 최적의 Thread 모델 선택과 Thread-safe한 객체 재활용 등 다양한 기법들을 담아내고 있습니다. 아직은 갈 길이 멀고, 부족한 부분이 많습니다. 좀 더 많은 사람에게 지식과 코드를 공유하고 채워나가는 것이 지금부터 해야 할 일이라고 생각합니다.

그런 의미에서 TOAST Haste의 구현 전체를 공개합니다. 이상한 구현부가 있거나, 좋은 아이디어가 있다면 함께 얘기하면서 모두에게 도움이 되는 프레임워크로 발전할 수 있으면 좋겠습니다. 참고로 TOAST Haste는 TOASTCloud Real-time multiplayer 서비스의 뼈대입니다. 앞으로도 서비스 중에 발생하는 버그나 추가 기능들을 TOAST Haste에 꾸준히 업데이트할 예정입니다.

관심 있으신 분들은 https://github.com/nhnent/toast-haste.framework 에서 프레임워크 소스코드를, https://github.com/nhnent/toast-haste.sdk.dotnet 에서 닷넷 SDK 소스코드를 확인할 수 있습니다.