45. 编写服务器测试用Bot¶
本章介绍了不使用游戏客户端的情况下,进行Bot测试的方法。
45.1. 方法1:利用iFun引擎的Bot组件¶
iFun引擎提供了用于进行测试的类似于客户端的组件,访问后会模拟客户端-服务器会话。将该组件包含在游戏服务器代码中,通过单独的服务器实例运行,即可像多数的bot那样运行。
Note
暂不支持加密、Session Reliability、Sequence Number Validation。
为了使用测试功能,可以包含 funapi/test/network.h ,
使用其中支持的 funtest::Network
类和 funtest::Session
类。(对于C#,则为funtest.Network, funtest.Session)
45.1.1. 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 | namespace funtest {
class Network {
public:
// session 이 열리고 닫힐 때 호출될 callback 과 io thread 의 수를
// 설정합니다. 이 함수는 반드시 가장먼저 호출해야 합니다.
static void Install(const SessionOpenedHandler &opened_handler,
const SessionClosedHandler &closed_handler,
size_t io_threads_size);
// 필요할 경우 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 | namespace funtest
public static class Network
{
// session 이 열리고 닫힐 때 호출될 callback 과 io thread 의 수를
// 설정합니다. 이 함수는 반드시 가장먼저 호출해야 합니다.
public static void Install(
SessionOpenedHandler session_opened_handler,
SessionClosedHandler session_closed_handler,
uint io_threads_size);
// 필요할 경우 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);
}
}
|
45.1.2. 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;
}
}
|
45.1.3. 示例: 300个Bot各传输5000次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 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 | #include <funapi/test/network.h>
static bool Start() {
funtest::Network::Install(OnSessionOpened, OnSessionClosed, 4);
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 | using funapi;
public static void Start()
{
funapi.funtest.Network.Install (
new funapi.funtest.Network.SessionOpenedHandler (OnSessionOpened),
new funapi.funtest.Network.SessionClosedHandler (OnSessionClosed),
4);
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);
}
}
|
45.2. 方法2:利用客户端插件¶
创建测试Bot时,最好在Unity或Unreal中直接运行,无需UI。客户端插件中已包含相关的测试代码。
45.2.1. 使用C# Runtime¶
C# Runtime测试是一种在 终端 上运行客户端的方法。利用Unity插件进行C# Runtime 测试的代码位于 plugin-test/src 文件夹中。
Important
虽然是通过Unity创建,但它的创建是为了进行C# Runtime测试,所以无法使用UnityEngine库, 因此它在调用Update等方面,与实际Unity中所使用的代码稍有不同。注意,无法直接导入已通过Unity创建好的 客户端代码来创建C# Runtime测试Bot。
src/client.cs 文件中包含了对一个客户端进行的action。Client对于含有一个 FunapiSession对象的协议,TCP、UDP、HTTP三者均可使用。 Client负责收发用于测试的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 | 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 文件是执行测试的文件。它会创建虚拟客户端,负责连接、结束、 收发消息。
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
所有代码位于 plugin-test/src 文件夹中。如需更改路径或添加文件,修改 plugin-test/makefile 文件即可。
若已实现测试代码,后续只要构建并运行即可。在终端上移动至 makefile 所在文件夹后,
通过 make
命令语句构建。
$ make
$ mono tester.exe
45.2.2. 使用C# VS 2015¶
可在Visual Studio中运用Unity插件进行Bot测试。测试代码位于 vs2015 文件夹中。测试示例已在VS 2015中编写。
45.2.2.1. 设置项目¶
运行 vs2015 文件夹下的 funapi-plugin.sln 文件时,会加载2个项目,其中 funapi-plugin
是将Unity的插件文件变成类库的项目, funapi-plugin-tester
是
.NET框架的单元测试项目。
为了在VS中运行Unity插件的代码,须要先声明一个Preprocessor。
在 funapi-plugin
项目的Build属性中,为附条件编译符号添加 NO_UNITY 。
45.2.2.2. 编写单元测试代码¶
funapi-plugin-tester 文件夹的 Tester.cs 文件中有示例测试代码。
已声明 [TestMethod]
的函数是测试函数,该函数的名字会显示在测试资源管理器中。
可以通过在该文件中修改或添加函数来编写测试代码,也可以创建新的测试类
来使用。
Note
注意,它和C# Runtime测试一样,也无法使用UnityEngine库。
45.2.2.3. 构建及执行单元测试¶
在Build菜单中执行solution build。Build成功后,将显示UnitTestExplorer,如该窗口 未显示,可在菜单中选择 测试>窗口>测试资源管理器 。
此时将显示已在资源管理中创建的测试方法列表。选择全部执行,将依次执行各个单元测试 函数,也可仅选择任意一个方法执行。
无法在测试时直接查看日志,待测试完毕后,结果窗口上会显示 [输出] 字样,点击后将显示单独的日志窗口,从中查看全部日志。