46. Creating bots for server testing

This chapter will introduce how to test with bots rather than using game clients.

46.1. Method 1: Using iFun Engine’s bot component

iFun Engine provides a component that imitates client-server sessions by accessing like a client for test purposes. You can include this component in the game code to launch separate server instances that operate like a large number of bots.

Note

Sequence number validation is not supported.

To use the test function, include funapi/test/network.h, and use the supported funtest::Network and funtest::Session classes. (funtest for C#.Network, funtest.Session)

46.1.1. funtest Network classes

The Network class handles resetting and support the following methods.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
namespace funtest {

class Network {
 public:
  struct Option {
    struct Compression {
      Compression() : type("none"), threshold(128) {
      }
      string type;
      string dictionary;
      size_t threshold;
    };

    Option(size_t io_threads_size = 4);

    // 네트워크 스레드 수를 입력합니다. 기본 값은 4 입니다.
    size_t io_threads_size;

    // 아래 항목들은 서버의 MANIFEST/SessionService 에 입력된 값과
    // 같아야합니다.

    bool use_session_reliability;
    bool send_session_id_only_once;
    bool disable_tcp_nagle;

    Compression tcp_compression;
    Compression udp_compression;
    Compression websocket_compression;

    string server_encryption_public_key;
  };

  // session 이 열리고 닫힐 때 호출될 callback 과 네트워크 설정을
  // 입력합니다.. 이 함수는 반드시 가장먼저 호출해야 합니다.
  static void Install(const SessionOpenedHandler &opened_handler,
                      const SessionClosedHandler &closed_handler,
                      const Option &option);

  // 필요할 경우 TCP 연결이 맺어지고, 끊길 때 호출될 callback 을 지정합니다.
  static void RegisterTcpTransportHandler(
      const TcpTransportAttachedHandler &tcp_attached_handler,
      const TcpTransportDetachedHandler &tcp_detached_handler);

  // JSON 타입의 메시지 핸들러를 등록합니다.
  static void Register(const string &message_type,
                       const MessageHandler &message_handler);

  // Protobuf 타입의 메시지 핸들러를 등록합니다.
  static void Register2(const string &message_type,
                        const MessageHandler2 &message_handler);
};

}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
namespace funtest

public static class Network
{
  public struct Option
  {
    public struct Compression
    {
      public string Type;
      public string Dictionary;
      public ulong Threshold;
    }

    // 네트워크 스레드 수를 입력합니다. 기본 값은 4 입니다.
    public ulong IoThreadsSize;

    // 아래 항목들은 서버의 MANIFEST/SessionService 에 입력된 값과
    // 같아야합니다.

    public bool UseSessionReliability;
    public bool SendSessionIdOnlyOnce;
    public bool DisableTcpNagle;

    public Compression TcpCompression;
    public Compression UdpCompression;
    public Compression WebSocketCompression;

    public string ServerEncryptionPublicKey;
  }

  // session 이 열리고 닫힐 때 호출될 callback 과 io thread 의 수를
  // 설정합니다. 이 함수는 반드시 가장먼저 호출해야 합니다.
  public static void Install(
      SessionOpenedHandler session_opened_handler,
      SessionClosedHandler session_closed_handler,
      Option option);

  // 필요할 경우 TCP 연결이 맺어지고, 끊길 때 호출될 callback 을 지정합니다.
  public static void RegisterTcpTransportHandler(
      TcpTransportAttachedHandler tcp_attached_handler,
      TcpTransportDetachedHandler tcp_detached_handler);

  // JSON 타입의 메시지 핸들러를 등록합니다.
  public static void RegisterMessageHandler(
      string message_type, JsonMessageHandler message_handler);

  // Protobuf 타입의 메시지 핸들러를 등록합니다.
  public static void RegisterMessageHandler(
      string message_type, ProtobufMessageHandler message_handler);
}

}

Note

서버에서 AccountManager::RedirectClient*() 로 클라이언트를 다른 서버로 이동시킬 경우 기존 세션이 닫히고 새로 연결된 서버에 새로운 세션이 열립니다. 이 때 Plugin 에서는 세션 닫힘, 세션 열림 핸들러가 불리지 않지만 funtest::Network 에서는 불립니다. 세션 닫힘 핸들러에서는 Reason 이 kClosedForRedirection 인지 확인하여 구분할 수 있으며, 세션 열림 핸들러에서는 Session::IsRedirected() 로 구분할 수 있습니다.

46.1.2. funtest Session classes

The Session class handles connection to a single client.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
namespace funtest {

class Session {
 public:
  DECLARE_CLASS_PTR(Session);

  enum State {
    kOpening,
    kOpened,
    kClosed
  };

  // 세션 객체를 생성합니다. 이 함수를 호출한다고 세션이 맺어지는 것은
  // 아닙니다. TCP 연결을 위해서는 아래 ConnectTcp(...) 함수를 이용해야됩니다.
  static Ptr<Session> Create();

  virtual ~Session();

  // TCP 로 서버에 접속합니다. 만약 첫 연결이면 세션이 열리게 됩니다.
  // 그렇지 않은 경우에는 이미 존재하던 세션을 계속 재사용하게 됩니다.
  virtual void ConnectTcp(const string &ip, uint16_t port,
                          EncodingScheme encoding) = 0;
  virtual void ConnectTcp(const boost::asio::ip::tcp::endpoint &endpoint,
                          EncodingScheme encoding) = 0;

  // UDP 로 서버에 접속합니다. 만약 첫 연결이면 세션이 열리게 됩니다.
  // 그렇지 않은 경우에는 이미 존재하던 세션을 계속 재사용하게 됩니다.
  virtual void ConnectUdp(const string &ip, uint16_t port,
                          EncodingScheme encoding) = 0;
  virtual void ConnectUdp(const boost::asio::ip::tcp::endpoint &endpoint,
                          EncodingScheme encoding) = 0;

  // 현재 버전에선 작동되지 않습니다.
  // HTTP 로 서버에 접속합니다. 만약 첫 연결이면 세션이 열리게 됩니다.
  virtual void ConnectHttp(const string &url, EncodingScheme encoding) = 0;
  virtual void ConnectHttp(const string &ip, uint16_t port,
                           EncodingScheme encoding) = 0;
  virtual void ConnectHttp(const boost::asio::ip::tcp::endpoint &endpoint,
                           EncodingScheme encoding) = 0;

  // 세션 아이디를 반환합니다.
  virtual const SessionId &id() const = 0;
  // 세션 상태를 반환합니다.
  virtual State state() const = 0;

  // Transport 의 연결 상태를 확인합니다.
  virtual bool IsTransportAttached() const = 0;
  virtual bool IsTransportAttached(TransportProtocol protocol) const = 0;

  // 서버로 JSON 메시지를 보냅니다.
  virtual void SendMessage(const string &message_type, const Json &message,
                           TransportProtocol protocol) = 0;

  virtual void SendMessage(const string &message_type,
                           const Ptr<FunMessage> &message,
                           TransportProtocol protocol) = 0;

  // Transport 연결을 닫습니다.
  // 새로운 트랜스포트가 붙어서 세션을 계속 쓸 수 있기 때문에 세션은 계속 유효한 상태로 남습니다.
  virtual void CloseTransport() = 0;
  virtual void CloseTransport(TransportProtocol protocol) = 0;

  // 세션을 닫습니다.
  virtual void Close() = 0;

  // 세션 별 Context 를 다루는 인터페이스입니다. 이 컨텍스트는 세션이 살아있는 동안 유효합니다.
  // 예)
  //   boost::mutex::scoped_lock lock(*session);  // 필요할 경우 lock 으로 보호
  //   if (session->GetContext()["my_state"] == ...) {
  //     ...
  //     session->GetContext()["my_state"] = ...;
  //   }
  virtual void SetContext(const Json &ctxt) = 0;
  virtual Json &GetContext() = 0;
  virtual const Json &GetContext() const = 0;
  virtual boost::mutex &GetContextMutex() const = 0;
  virtual operator boost::mutex &() const = 0;
};

}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
namespace funtest {

public class Session
{
  public enum SessionState
  {
    kOpening = 0,
    kOpened,
    kClosed
  }

  public enum EncodingScheme
  {
    kUnknownEncoding = 0,
    kJsonEncoding,
    kProtobufEncoding
  }

  // 세션 객체를 생성합니다. 이 함수를 호출한다고 세션이 맺어지는 것은
  // 아닙니다. TCP 연결을 위해서는 아래 ConnectTcp(...) 함수를 이용해야됩니다.
  public Session();

  ~Session();

  // TCP 로 서버에 접속합니다. 만약 첫 연결이면 세션이 열리게 됩니다.
  // 그렇지 않은 경우에는 이미 존재하던 세션을 계속 재사용하게 됩니다.
  public void ConnectTcp(string ip, ushort port, EncodingScheme encoding);
  public void ConnectTcp(System.Net.IPEndPoint address,
                         EncodingScheme encoding);

  // UDP 로 서버에 접속합니다. 만약 첫 연결이면 세션이 열리게 됩니다.
  // 그렇지 않은 경우에는 이미 존재하던 세션을 계속 재사용하게 됩니다.
  public void ConnectUdp(string ip, ushort port, EncodingScheme encoding);
  public void ConnectUdp(System.Net.IPEndPoint address,
                         EncodingScheme encoding);

   // 현재 버전에선 작동되지 않습니다.
   // HTTP 로 서버에 접속합니다. 만약 첫 연결이면 세션이 열리게 됩니다.
  public void ConnectHttp(string ip, ushort port, EncodingScheme encoding);
  public void ConnectHttp(System.Net.IPEndPoint address,
                          EncodingScheme encoding);
  public void ConnectHttp(string url, EncodingScheme encoding);

  // 세션 아이디를 반환합니다.
  public System.Guid Id;
  // 세션 상태를 반환합니다.
  public SessionState State;

  // Transport 의 연결 상태를 확인합니다.
  public bool IsTransportAttached();
  public bool IsTransportAttached(funapi.Session.Transport transport);

  // 서버로 JSON 메시지를 보냅니다.
  public void SendMessage(string message_type, JObject message,
                          funapi.Session.Transport transport);

  // 서버로 Protocol buffer 메시지를 보냅니다.
  public void SendMessage(string message_type, FunMessage message,
                          funapi.Session.Transport transport);

  // Transport 연결을 닫습니다.
  // 새로운 트랜스포트가 붙어서 세션을 계속 쓸 수 있기 때문에 세션은 계속 유효한 상태로 남습니다.
  public void CloseTransport();
  public void CloseTransport(funapi.Session.Transport transport);

  // 세션을 닫습니다.
  public void Close();

  // 세션 별 Context 를 다루는 인터페이스입니다. 이 컨텍스트는 세션이 살아있는 동안 유효합니다.
  // 예)
  //   lock (session) // 필요할 경우 lock 으로 보호
  //   {
  //     if (session.Context ["my_state"] == ...) {
  //       ...
  //       session.Context ["my_state"] = ...;
  //     }
  //   }
  public JObject Context;
}

}

46.1.3. Example: 300 bots each send 5000 echoes

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#include <funapi/test/network.h>

static bool Start() {
  funtest::Network::Option option;
  funtest::Network::Install(OnSessionOpened, OnSessionClosed, option);

  funtest::Network::RegisterTcpTransportHandler(OnTcpAttached, OnTcpDetached);

  funtest::Network::Register2("pbuf_echo", OnEcho);

  // Created 300 clients.
  for (size_t i = 0; i < 300; ++i) {
    Ptr<funtest::Session> session = funtest::Session::Create();
    session->GetContext()["count"] = 5000;
    session->ConnectTcp("127.0.0.1", 8013, kProtobufEncoding);
  }

  return true;
}


void OnSessionOpened(const Ptr<funtest::Session> &session) {
  LOG(INFO) << "[test_client] session created: sid=" << session->id();

  string message = RandomGenerator::GenerateAlphanumeric(5, 50);

  session->GetContext()["sent_message"] = message;

  Ptr<FunMessage> msg(new FunMessage);
  PbufEchoMessage *echo_msg = msg->MutableExtension(pbuf_echo);
  echo_msg->set_msg(message);
  session->SendMessage("pbuf_echo", msg, kTcp);
};


void OnSessionClosed(const Ptr<funtest::Session> &session, SessionCloseReason reason) {
  LOG(INFO) << "[test_client] session closed: sid=" << session->id();
};


void OnTcpAttached(const Ptr<funtest::Session> &session, bool connected) {
  if (not connected) {
    LOG(ERROR) << "[test_client] failed to connect to the server";
  }
};


void OnTcpDetached(const Ptr<funtest::Session> &session) {
  LOG(INFO) << "[test_client] tcp transport disconnected: sid=" << session->id();
};


void OnEcho(const Ptr<funtest::Session> &session, const Ptr<FunMessage> &msg) {
  BOOST_ASSERT(msg->HasExtension(pbuf_echo));

  const PbufEchoMessage &echo_msg = msg->GetExtension(pbuf_echo);
  const string &message = echo_msg.msg();

  string sent_message = session->GetContext()["sent_message"].GetString();
  BOOST_ASSERT(sent_message == message);

  Json &count_ctxt = session->GetContext()["count"];
  count_ctxt.SetInteger(count_ctxt.GetInteger() - 1);
  if (count_ctxt.GetInteger() == 0) {
    LOG(INFO) << "[test_client] complete: sid=" << session->id();
    return;
  }

  {
    string message = RandomGenerator::GenerateAlphanumeric(5, 50);
    session->GetContext()["sent_message"] = message;

    Ptr<FunMessage> msg(new FunMessage);
    PbufEchoMessage *echo_msg = msg->MutableExtension(pbuf_echo);
    echo_msg->set_msg(message);
    session->SendMessage("pbuf_echo", msg, kTcp);
  }
};
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
using funapi;

public static void Start()
{
  funapi.funtest.Network.Option option;
  funapi.funtest.Network.Install (
      new funapi.funtest.Network.SessionOpenedHandler (OnSessionOpened),
      new funapi.funtest.Network.SessionClosedHandler (OnSessionClosed),
      option);

  funapi.funtest.Network.RegisterTcpTransportHandler(OnTcpAttached,
                                                     OnTcpDetached);

  funapi.funtest.Network.RegisterMessageHandler ("pbuf_echo", OnEcho));

  for (int i = 0; i < 300; ++i)
  {
    funapi.funtest.Session session = new funapi.funtest.Session ();
    session.Context["count"] = 5000;
    session.ConnectTcp (
        "127.0.0.1",
        8013,
        funapi.funtest.Session.EncodingScheme.kProtobufEncoding);
  }
}


public static void OnSessionOpened(funapi.funtest.Session session)
{
  Log.Info ("[test client] Session opened: session_id={0}", session.Id);

  string message = RandomGenerator.GenerateAlphanumeric(5, 50);

  session.Context ["sent_message"] = message;

  FunMessage msg = new FunMessage();
  PbufEchoMessage echo_msg = new PbufEchoMessage();
  echo_msg.msg = message;
  msg.AppendExtension_pbuf_echo (echo_msg);

  session.SendMessage("pbuf_echo", msg, funapi.Session.Transport.kTcp);
}


public static void OnSessionClosed(funapi.funtest.Session session,
                                   Session.CloseReason reason)
{
  Log.Info ("[test client] Session closed: session_id={0}, reason={1}",
            session.Id, reason);
}


public static void OnTcpAttached(funapi.funtest.Session session,
                   bool connected)
{
  if (!connected) {
    Log.Error ("[test_client] failed to connect to the server");
  }
}


public static void OnTcpDetached(funapi.funtest.Session session)
{
  Log.Info ("[test_client] tcp transport disconnected: sid={0}",
            session.Id);
}


public static void OnEcho(funapi.funtest.Session session,
                          FunMessage msg) {
  PbufEchoMessage echo;
  if (!msg.TryGetExtension_pbuf_echo (out echo))
  {
    Log.Error ("OnEchoPbuf: Wrong message.");
    return;
  }
  string message = echo.msg;
  Log.Info ("client recv echo.msg. {0}", echo.msg);

  string sent_message = (string) session.Context ["sent_message"];
  Log.Assert (sent_message == message);

  int count  = (int) session.Context ["count"];
  session.Context ["count"] = count - 1;

  if ((int) session.Context ["count"] == 0)
  {
    Log.Info("[test_client] complete: sid={0}",
             session.Id.ToString());
    return;
  }

  {
    string message2 = RandomGenerator.GenerateAlphanumeric(5, 50);
    session.Context ["sent_message"] = message2;

    FunMessage fun_message = new FunMessage ();
    PbufEchoMessage echo2 = new PbufEchoMessage();
    echo_msg.msg = message2;
    fun_message.AppendExtension_pbuf_echo(echo2);
    session.SendMessage("pbuf_echo", fun_message,
                        funapi.Session.Transport.kTcp);
  }
}

46.2. Method 2: Using client plugins

When making test bots, it is best to run in Unity or Unreal without UI, and the client plugin includes test code for this.

46.2.1. Using C# Runtime

C# Runtime tests run the client from the terminal. Test code for C# Runtime using the Unity plugin is in the plugin-test/src folder.

Important

Although this code uses the Unity plugin, you cannot use UnityEngine libraries as it was made for use with C# Runtime and so there may be slight differences from the actual code used in Unity when invoking updates. Note that you cannot make C# Runtime test bots by importing client code made in Unity as is.

Operation for one client is in the src/client.cs file. The client has one FunapiSession object and uses TCP, UDP, and HTTP protocols. The client handles sending and receiving of test echo messages.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Client
{
    public Client (int id, string server_ip)
    {
        // 클라이언트 고유 아이디
        id_ = id;

        // 서버 주소
        server_ip_ = server_ip;
    }

    public void Connect (bool session_reliability)
    {
        // FunapiSession 객체 생성 및 콜백 등록
        session_ = FunapiSession.Create(server_ip_, session_reliability);
        session_.SessionEventCallback += onSessionEvent;
        session_.TransportEventCallback += onTransportEvent;
        session_.TransportErrorCallback += onTransportError;
        session_.ReceivedMessageCallback += onReceivedMessage;

        ...

        // 서버에 연결
        ushort port = getPort(protocols[i], encodings[i]);
        session_.Connect(protocols[i], encodings[i], port, option);
    }

    ...
}

src/main.cs is the file to conduct the test. Generate dummy clients to connect, close, and send and receive messages.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
class TesterMain
{
    // 더미 클라이언트 개수
    const int kClientMax = 3;
    // 서버 주소
    const string kServerIp = "127.0.0.1";

    // 클라이언트 목록
    List<Client> list_ = new List<Client>();

    // Main 함수
    public static void Main ()
    {
        // 테스트 시작
        new TesterMain().Start();
    }

    void start ()
    {
        // 더미 클라이언트를 생성합니다.
        for (int i = 0; i < kClientMax; ++i)
        {
            Client client = new Client(i, kServerIp);
            list_.Add(client);
        }

        // 업데이트를 위한 스레드를 생성합니다.
        Thread t = new Thread(new ThreadStart(onUpdate));
        t.Start();

        // Connect 테스트
        testConnect();

        // 시작/종료 테스트
        testStartStop();

        // 메시지 테스트
        testSendReceive();

        // 스레드 종료
        t.Abort();
    }

    void onUpdate ()
    {
        // 스레드가 종료될 때까지 33 milliseconds 간격으로 업데이트를 수행합니다.
        while (true)
        {
            foreach (Client c in list_)
            {
                c.Update();
            }

            Thread.Sleep(33);
        }
    }

    void connect ()
    {
        // 클라이언트 연결을 시작합니다.
        foreach (Client c in list_)
        {
            c.Connect(reliableSession);
        }

        // 모든 클라이언트의 연결이 완료될 때까지 대기합니다.
        while (waitForConnect())
        {
            Thread.Sleep(100);
        }
    }

    ...
}

Note

All code is in the plugin-test/src folder. If you need to modify the file path or add files, you can modify the plugin-test/makefile file.

When your test code is implemented, you only need to build and run it. Go to the folder containing makefile on the terminal and then enter the make command to build.

$ make
$ mono tester.exe

46.2.2. Using C# VS 2015

You can use a Unity plugin in Visual Studio to test with bots. Test code is in the vs2015 folder. Sample tests were written in VS 2015.

46.2.2.1. Configuring a project

When you open the funapi-plugin.sln file in the`vs2015` folder, two projects are loaded. funapi-plugin is a project made with the Unity plugin file as a class library, while funapi-plugin-tester is a unit test project made with the.NET framework.

To run the Unity plugin code in VS, you need to declare one preprocessor. Add NO_UNITY to the conditional compile flavor in the funapi-plugin project’s build attributes.

46.2.2.2. Writing unit test code

Sample test code is in the Tester.cs file in the funapi-plugin-tester folder. The function declaring [TestMethod] is a test function, and this function’s name is indicated in the test explorer. You can modify or add functions in this file to write test code or create a new test class.

Note

As with C# Runtime tests, be aware that you cannot use UnityEngine libraries.

46.2.2.3. Building and running unit tests

Run the solution build from the build menu. If the build is successful, UnitTestExplorer appears. If this window does not appear, go to Test > Window > Test explorer in the menu.

A list of test methods previously created in the explorer is displayed. If you run all, each unit test function will run in order, or you can choose the method you want to run only one.

You cannot immediately check test logs. When testing is finished, the results are displayed in the [Output] window. Click Output to see the log window and view all logs.