서버 테스트용 Bot 작성

이 챕터에서는 게임 클라이언트를 쓰지 않고 Bot 테스트를 할 수 있는 방법들을 소개합니다.

방법1: 아이펀 엔진의 Bot 콤포넌트 이용

아이펀엔진은 테스트 목적으로 마치 클라이언트인 것처럼 접속해서 클라이언트-서버 세션을 흉내내는 콤포넌트를 제공합니다. 게임 서버 코드에 이 콤포넌트를 포함시켜 별도의 서버 인스턴스로 띄우게 되면 다수의 bot 처럼 동작시킬 수 있습니다.

Note

Sequence Number Validation 은 지원하지 않습니다.

테스트 기능을 이용하기 위해서는 funapi/test/network.h 를 include 하고, 거기서 지원하는 funtest::Network 클래스와 funtest::Session 클래스를 활용합니다. (C# 의 경우 funtest.Network, funtest.Session)

funtest 의 Network 클래스

Network 클래스는 초기화를 담당하며 다음과 같은 메소드를 지원합니다.

 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() 로 구분할 수 있습니다.

funtest 의 Session 클래스

Session 클래스는 하나의 클라이언트 연결을 담당합니다.

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

}

예제: 300 개의 Bot 이 각각 5000 번씩 echo 전송

아래 예제는 funtest 를 이용하여 서버 실행 시 Start() 함수에서 funtest 세션을 300개 생성하여 세션별로 PbufEcho 메시지를 서버로 5000번 전송하는 예제입니다.

 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
#include <funapi/test/network.h>

static bool Install(const ArgumentMap &arguments) {
  // 생략...

  // funtest 세션이 열리고 닫힐 때 불릴 콜백함수를 등록합니다.
  funtest::Network::Option option;
  funtest::Network::Install(OnSessionOpened, OnSessionClosed, option);

  // TCP 연결이 맺어지고 끊어질 때 불릴 콜백함수를 등록합니다.
  funtest::Network::RegisterTcpTransportHandler(OnTcpAttached, OnTcpDetached);

  // 서버로부터 전달받을 protobuf 메시지 핸들러를 등록합니다.
  funtest::Network::Register2("pbuf_echo", OnEcho);

  return true;
}

static bool Start() {
  // 생략...

  // 서버에게 메시지를 보낼 funtest 세션을 300개 생성합니다.
  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;
}

// funtest 세션이 열릴 때 호출되는 콜백함수 입니다.
static 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);
};

// funtest 세션이 닫힐 때 호출되는 콜백함수 입니다.
static void OnSessionClosed(const Ptr<funtest::Session> &session, SessionCloseReason reason) {
  LOG(INFO) << "[test_client] session closed: sid=" << session->id();
};

// TCP 연결이 맺어질 떄 호출되는 콜백함수 입니다.
static void OnTcpAttached(const Ptr<funtest::Session> &session, bool connected) {
  if (not connected) {
    LOG(ERROR) << "[test_client] failed to connect to the server";
  }
};

// TCP 연결이 끊어질 때 호출되는 콜백함수 입니다.
static void OnTcpDetached(const Ptr<funtest::Session> &session) {
  LOG(INFO) << "[test_client] tcp transport disconnected: sid=" << session->id();
};

// 서버로부터 PbufEcho 메시지를 받았을 때 불리는 핸들러입니다.
static 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);

  // funtest 세션 생성 시 지정한 횟수만큼 메시지 전송을 반복합니다.
  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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
using funapi;

public static bool Install(ArgumentMap arguments)
{
  // 생략...

  // funtest 세션이 열리고 닫힐 때 불릴 콜백함수를 등록합니다.
  funapi.funtest.Network.Option option;
  funapi.funtest.Network.Install (
      new funapi.funtest.Network.SessionOpenedHandler (OnSessionOpened),
      new funapi.funtest.Network.SessionClosedHandler (OnSessionClosed),
      option);

  // TCP 연결이 맺어지고 끊어질 때 불릴 콜백함수를 등록합니다.
  funapi.funtest.Network.RegisterTcpTransportHandler (OnTcpAttached,
                                                      OnTcpDetached);

  // 서버로부터 전달받을 protobuf 메시지 핸들러를 등록합니다.
  funapi.funtest.Network.RegisterMessageHandler ("pbuf_echo", OnEcho));
}

public static void Start()
{
  // 서버에게 메시지를 보낼 funtest 세션을 300개 생성합니다.
  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);
  }
}


// funtest 세션이 열릴 때 호출되는 콜백함수 입니다.
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);
}


// funtest 세션이 닫힐 때 호출되는 콜백함수 입니다.
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);
}


// TCP 연결이 맺어질 때 호출되는 콜백함수 입니다.
public static void OnTcpAttached(funapi.funtest.Session session,
                   bool connected)
{
  if (!connected) {
    Log.Error ("[test_client] failed to connect to the server");
  }
}


// TCP 연결이 끊어질 때 호출되는 콜백함수 입니다.
public static void OnTcpDetached(funapi.funtest.Session session)
{
  Log.Info ("[test_client] tcp transport disconnected: sid={0}",
            session.Id);
}


// 서버로부터 PbufEcho 메시지를 받았을 때 불리는 핸들러입니다.
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;

  // funtest 세션 생성 시 지정한 횟수만큼 메시지 전송을 반복합니다.
  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);
  }
}

방법2: 유니티 플러그인을 이용

서버를 테스트하기 위한 봇 클라이언트는 하나의 호스트에서 동시에 여러개를 실행시킬 수 있도록 적은 리소스로 에디터 없이 실행할 수 있도록 만드는 것이 좋습니다.

아이펀 엔진 의 클라이언트 플러그인은 에디터 없이도 봇 클라이언트를 제작할 수 있도록 샘플 프로젝트를 제공합니다.

Mono C# 라이브러리 이용

Mono 라이브러리가 설치되어 있는 리눅스의 커맨드라인에서 Mono 의 C# 컴파일러를 이용해서 클라이언트를 컴파일하고 실행하는 방법에 대해서 설명합니다.

유니티 클라이언트 플러그인은 여기 에서 받으실 수 있으며, 샘플 코드는 유니티 클라이언트 플러그인 내부의 csharp-samples 폴더에 있습니다.

Important

아이펀 엔진 유니티 플러그인을 사용해서 만들었지만 Mono 로 컴파일해야 하므로 유니티 에디터에서 제공하는 라이브러리는 사용할 수 없습니다. 그러므로 실제 유니티 프로젝트의 소스코드를 그대로 사용할 수 없는 것에 주의해 주시기 바랍니다.

Note

샘플 코드에서 미리 정의된 PbufEcho 메시지 외에 사용자가 정의한 Protobuf 메시지를 사용할 수 있습니다. 서버 빌드로 생성된 dll 파일을 csharp-samples 디렉터리 밑에 넣어주시면 샘플 코드에서 사용자가 정의한 Protobuf 메시지를 사용할 수 있습니다.

샘플 코드에서 src/client.cs 파일은 클라이언트 하나에 대한 동작을 정의합니다. 샘플 코드에서 ClientTCP 연결을 맺고 미리 정의된 echo 메시지를 보내고 받는 일을 수행합니다.

 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
class Client
{
    public Client (int id)
    {
        // 클라이언트 고유 아이디
        id_ = id;
    }

    // Connect 함수를 통해 서버와 연결을 시도합니다.
    public void Connect (TransportProtocol protocol, FunEncoding encoding)
    {
        if (session == null)
        {
            SessionOption option = new SessionOption();
            option.sessionReliability = false;
            option.sendSessionIdOnlyOnce = false;

            // 세션이 없는 경우 서버와 통신할 세션을 생성하고 연결과 관련된 콜백함수를 등록합니다.
            session = FunapiSession.Create(address, option);
            session.SessionEventCallback += onSessionEvent;
            session.TransportEventCallback += onTransportEvent;
            session.TransportErrorCallback += onTransportError;
            session.ReceivedMessageCallback += onReceivedMessage;
        }

        session.Connect(protocol, encoding, getPort(protocol, encoding));
    }

    ...
}

src/main.cs 파일은 테스트 프로젝트 자체를 실행하는 파일입니다. Thread 를 생성하여 더미 클라이언트를 만들고, 서버에게 메시지를 보내는 함수를 호출합니다.

 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
class TesterMain
{
    // 테스트를 진행할 횟수
    const int kTestCount = 10;
    // 더미 클라이언트 개수
    const int kClientMax = 100;
    // 서버 주소
    const string kServerIp = "127.0.0.1";

    // Main 함수
    public static void Main ()
    {
        Client.address = kServerIp;

        // 테스트 시작
        new TesterMain().start();
    }

    void start ()
    {
        int testCount = 0;
        // 지정한 테스트 횟수만큼 반복합니다.
        while (testCount < kTestCount)
        {
            // onTest 함수를 수행할 Thread를 더미 클라이언트 개수만큼 생성합니다.
            for (int i = 0; i < kClientMax; ++i)
            {
                Thread t = new Thread(new ThreadStart(onTest));
                t.IsBackground = true;
                threads_.Add(t);

                t.Start();
            }

            foreach (Thread t in threads_)
            {
                t.Join();
            }

            threads_.Clear();
            Thread.Sleep(1000);
        }

        // 플러그인 업데이터 종료
        FunapiMono.Stop();

        // 테스트 프로세스 종료
        Process.GetCurrentProcess().Kill();
    }

    void onTest()
    {
        // 더미 클라이언트를 생성합니다.
        Client client = new Client(++client_id_);

        // TCP + Protobuf 타입으로 서버와 연결합니다.
        client.Connect(TransportProtocol.kTcp, FunEncoding.kProtobuf);
        while (!client.Connected)
            Thread.Sleep(10);

        // 서버에게 Echo Message를 전송합니다.
        client.SendEchoMessageWithCount(TransportProtocol.kTcp, 1000);
        while (!client.IsDone)
            Thread.Sleep(10);

        client.Stop();
        client = null;
    }

    ...

    // 클라이언트 아이디
    int client_id_ = 0;
    // 클라이언트 목록
    List<Thread> threads_ = new List<Thread>();
}

Note

전체 코드는 csharp-samples/src 폴더에 있습니다. 경로 수정이나 파일 추가가 필요하다면 csharp-samples/makefile 파일을 수정하면 됩니다.

테스트 코드 구현이 끝났다면 이제 빌드하고 실행하기만 하면 됩니다. 터미널에서 makefile 파일이 있는 폴더로 이동한 후에 다음 명령을 실행하면 빌드 후 실행됩니다.

$ make
$ mono tester.exe

Visual Studio 이용

Visual Studio 에서 유니티 플러그인을 이용해 봇 테스트를 할 수 있습니다. 플러그인 폴더 아래 csharp-samples/vs2015 폴더에 Visual Studio 용 솔루션 파일이 있습니다.

Note

샘플 프로젝트는 VS 2015 Community 버전에서 작성되었으나, VS 2019 Community 버전에서도 동작합니다.

프로젝트 설정

Visual Studiofunapi-plugin-unity.sln 파일을 열면 funapi-plugin-unity 프로젝트가 로드되는데 포함되어 있는 모든 파일은 상위 폴더의 파일을 참조로 링크되어 있습니다.

샘플 코드도 상위 src 폴더 내의 파일을 그대로 사용하므로 샘플 코드에 대한 설명은 앞서 설명한 Mono C# 라이브러리 이용 을 참고해 주시기 바랍니다.

Note

유니티 플러그인의 코드를 Visual Studio 에서 실행하기 위해서는 컴파일 옵션이 필요합니다.

프로젝트 속성->빌드->조건부 컴파일 기호에 NO_UNITY 가 설정되어 있는지 확인해 주세요.

Note

C# Mono와 마찬가지로 UnityEngine 라이브러리는 사용할 수 없음에 주의해주세요.

빌드 및 실행

빌드 후 실행하면 콘솔창으로 로그를 확인 할 수 있는데 테스트가 끝나면 창이 자동으로 닫힙니다.

콘솔창이 닫히지 않게 싶은 경우에는 Ctrl + F5 로 실행하면 테스트가 끝나도 창이 닫히지 않습니다.

콘솔의 경우 버퍼 크기가 정해져 있어 최근 로그만 남습니다. 버퍼 크기는 콘솔의 속성창에서 변경할 수 있습니다.