38. 客户端支持Part 1: 插件¶
使用客户端插件后,可以便利地使用网络通信、公告事项确认、资源下载等功能。 目前支持可以在Unity、Unreal Engine 4和Cocos2d-x中使用的客户端插件。 客户端插件可以在 GitHub 中获取。
本章介绍了客户端插件的使用方法、注意事项、使用技巧等。下面的说明是以 Unity插件为基准编制的。
38.1. 概要¶
38.1.1. 什么是Session?¶
服务器和客户端之间的网络连接通过Session来管理。Session除了直接Close或 网络通信失败超出了设定时间范围而发生Timeout的情况以外,会一直保持。 即,即使网络连接已断线,服务器仍会保持Session,重连时如果有相同Id的Session, 会继续收发消息,就像没有断线一样。
一个Session最多可拥有3个Transport连接,即TCP、UDP、HTTP各一个。 当想要同时创建多个具有相同协议的连接时,须要创建多个session。
Tip
与Session的内部实现有关的具体内容已在 (高级)iFun引擎的网络堆栈 中介绍。
38.1.1.1. Session Id¶
访问服务器后,先从服务器获取Session Id,然后将该Session Id包含在与服务器之间收发的所有消息中。
使用JSON时,Session Id为36 bytes字符串。使用Protobuf时,可以使用 16 bytes Array,或是与JSON相同的36 bytes字符串。因为Session Id包含在与服务器之间收发 的所有消息中,所以使用16 bytes Array可 减少网络使用量。
若想要在Protobuf中使用Byte Array作为Session Id,将MANIFEST文件的
send_session_id_as_string
值设置为false即可。该选项的默认值为true。
在使用JSON时,不对该选项进行任何操作,继续使用36 bytes字符串。
Tip
为了便于开发,插件的初始设置会在每次连接时请求新的Session Id。如果想要在客户端上也继续使用Session Id,请使用 Session Reliability 选项。
38.1.1.2. FunapiSession类¶
为了更加简单方便地使用插件,目前已经添加了用于替代原FunapiNetwork的FunapiSession 类。
FunapiNetwork以后不会再更新。 (插件版本158以后)
FunapiSession类拥有FunapiNetwork类的大部分功能, 接口和回调函数等变得更加简洁。使用FunapiNetwork的用户只要掌握了几点 变更事项,即可轻松转换为FunapiSession。
FunapiNetwork的帮助内容请参考 이전 클라이언트 플러그인 설명 文档。
38.1.1.3. Session Reliability选项¶
Session reliability是创建FunapiSession类时所传输的Boolean类型参数。
它是在与服务器的连接断线后重连时也可以通过相同的会话进行通信的选项。 它可以省略重新访问服务器后须要经历的认证过程,服务器也可以原封不动地保持 用户的数据值。建议在移动环境等会经常断线的环境中使用该选项。
Tip
Session reliability功能 仅能在TCP协议中使用 。
Session reliability须要确保服务器和客户端一直使用相同的设置。如果已将MANIFEST
文件的 use_session_reliability
选项设置为true,那么在客户端创建FunapiSession
类时,对 session_reliability
参数也应传输true。当该值不同
时,服务器将输出如下日志。
// 서버는 Session reliability 옵션이 꺼져 있고 클라이언트는 켜져 있을 경우...
message with seq number. but seq number validation disabled: sid=..., transport_protocol=Tcp
// 서버는 Session reliability 옵션이 켜져 있고 클라이언트는 꺼져 있을 경우...
message without seq number: session_id=...
38.2. 连接到服务器¶
下面通过代码示例简略介绍应用插件的实现方法。所有代码可在插件中所包含的 示例项目中查看。
38.2.1. 创建FunapiSession对象¶
下面是为了与服务器进行通信而创建并设置FunapiSession类的方法。
// FunapiSession 객체를 생성합니다.
// 전달하는 파라미터는 서버 주소와 Session reliability 옵션 사용 여부입니다.
FunapiSession session = FunapiSession.Create("127.0.0.1", true);
// 받은 메시지를 처리하기 위한 콜백을 등록합니다.
session.ReceivedMessageCallback += onReceivedMessage;
// 이벤트 콜백을 등록합니다.
session.SessionEventCallback += onSessionEvent;
session.TransportEventCallback += onTransportEvent;
session.TransportErrorCallback += onTransportError;
// Transport 옵션과 port를 지정한 후 연결을 시도합니다.
ushort port = getPort(protocol, encoding);
TransportOption option = makeOption(protocol);
session.Connect(protocol, encoding, port, option);
插件中所使用的回调函数运用了Unity的event对象。 通过事件注册回调函数 的方法如示例所示,使用’+=’运算符。一个事件也可能注册多个回调函数。
最后一行的 FunapiSession.Connect 函数运用已指定的参数来创建Transport,并试图进行连接。
如果已经存在相应 protocol
的Transport,将不会重新连接,而是再次利用相应Trnasport。此时 option
函数会被忽略,所以当通过特定Transport重新连接时,
最好使用仅指定了 protocol
的FunapiSession.Connect
函数。
下面是声明了可以在 FunapiSession.Connect 函数中使用的 protocol
和 encoding
种类的代码。
// 프로토콜은 TCP, UDP, HTTP 3가지 타입을 지원합니다.
public enum TransportProtocol
{
kDefault = 0,
kTcp,
kUdp,
kHttp
};
// 메시지 인코딩 방식은 JSON과 Protobuf 2가지 방법이 있습니다.
public enum FunEncoding
{
kNone,
kJson,
kProtobuf
}
38.2.2. 事件回调函数¶
FunapiSession中有显示Session状态变化的回调函数和显示Transport状态变化的回调 函数。除此以外,还有显示Transport的内部错误的回调函数。 当Transport内部发生错误时,Error回调函数和Transport的事件回调函数可能 会被同时调用。
38.2.2.1. Session Event¶
将回调函数注册到 SessionEventCallback 中后,每当会话状态发生变更时, 会收到如下所示的会话相关事件通知。
public enum SessionEventType
{
kOpened, // 세션이 처음 연결되면 호출됩니다. 같은 세션으로 재연결시에는 호출되지 않습니다.
kChanged, // 세션 Id가 변경되면 호출됩니다. 정상적인 상황이라면 이 이벤트는 발생되지 않습니다.
kStopped, // 세션에 연결된 모든 Transport의 연결이 끊기면 호출됩니다.
kClosed, // 세션이 닫히면 호출됩니다. Transport의 연결이 끊겼다고 세션이 닫히지는 않습니다.
// 세션은 서버에서 명시적으로 Close가 호출되거나 세션 타임아웃이 되었을 때 종료됩니다.
kRedirectStarted, // Redirect 관련 이벤트는 아래 [서버간 이동] 항목을 참고해주세요.
kRedirectSucceeded,
kRedirectFailed
};
FunapiSession中的SessionEventCallback已按如下所示进行声明。
void SessionEventHandler (SessionEventType type, string session_id);
38.2.2.2. Transport Event¶
与Transport有关的事件如下所示。除了 kStarted
以外,其他所有事件都是连接失败或
连接断线时发生的事件。 kStopped
是连接未正常结束或因错误而
导致连接断线或Transport的连接断线时发生的事件。
public enum TransportEventType
{
kStarted, // Transport가 시작되면 호출됩니다.
kStopped, // Transport가 종료되면 호출됩니다.
kConnectionFailed, // 연결에 실패하면 호출됩니다.
kConnectionTimedOut, // ConnectionTimeout 시간 초과로 연결에 실패하면 호출됩니다.
kDisconnected // 의도치 않게 연결이 끊겼을 때 호출됩니다.
};
kDisconnected
在物理(套接口)连接断线或PING超时时调用,而且因设备的种类
或通信网的状态的不同,感测到物理连接断线的时点也会有很大差异。使用PING
可以通过 Disconnected 事件判断出连接已断线;但在不使用PING时,请勿仅仅通过
Disconnected 事件来判断连接断线,建议通过与服务器之间的定期通信
来确认。
FunapiSession中的TransportEventCallback已按如下所示进行声明。
void TransportEventHandler (TransportProtocol protocol, TransportEventType type);
38.2.2.3. Transport Error¶
对于Tranpsort的Error回调函数,Error类型和发生Error的消息类型也会被一同传输。
当发生Transport错误时,如果已经在重新尝试连接,那么会从内部结束尝试性连接,
并调用 kConnectionFailed
事件。当在已与服务器连接的状态下发生错误时,
将仅传输错误,且连接不会结束。但是大部分错误都是在无法连接的状态下或是
无法与服务器收发消息的情况下发生的,所以如果在Transport中发生错误,最好断开相应连接,并尝试
重新连接。
public class TransportError
{
public enum Type
{
kNone,
kStartingFailed, // Transport 초기화에 실패했을 때 호출됩니다.
kConnectingFailed, // TCP 연결에 실패했을 때 호출됩니다.
kInvalidSequence, // 서버와 메시지 동기화에 실패했을 때 호출됩니다.
kEncryptionFailed, // 메시지 암호화에 실패했을 때 호출됩니다.
kSendingFailed, // 메시지를 보내는 과정에서 오류가 발생했을 때 호출됩니다.
kReceivingFailed, // 메시지를 받는 과정에서 오류가 발생했을 때 호출됩니다.
kRequestFailed, // HTTP의 요청에 실패했을 때 호출됩니다.
kDisconnected // 소켓 연결이 끊겼을 때 호출됩니다.
}
public Type type = Type.kNone;
public string message = null;
}
FunapiSession中的TransportErrorCallback已按如下所示进行声明。
void TransportErrorHandler (TransportProtocol protocol, TransportError type);
38.2.3. Transport选项¶
调用FunapiSession的Connect函数时,可以通过参数来传输 Transport的选项。当该值为null时,将使用default值。须根据要使用的协议 (TCP、UDP、HTTP)来创建TransportOption类,进行指定。 例如TCP,要创建TcpTransportOption进行传输。
下面是定义了TransportOption类的代码。
// Base class to specify options for Transport classes.
// Can be used for UDP transport.
public class TransportOption
{
// Specify the encryption algorithm.
// Should use the same value with the game server.
public EncryptionType Encryption = EncryptionType.kDefaultEncryption;
// Specify the compression algorithm.
// Should use the same value with the game server.
public FunCompressionType CompressionType = FunCompressionType.kNone;
// Adds a sequence number to each message to validate messages.
// If you want to validate the message without the session reliability option,
// set SequenceValidation to true.
// If you use the session reliability, it verifies the message without
// SequenceValidation option.
// This option can be specified for HTTP or TCP.
public bool SequenceValidation = false;
// 서버와 연결할 때 Timeout 될 시간을 지정합니다.
// 기본 값은 10초이며, 0을 입력할 경우 Timeout 처리를 하지 않고 계속 연결을 시도합니다.
// 이 경우, 서버로부터 응답이 오지 않으면 무한히 대기하는 상황이 발생할 수 있기 때문에
// 디버깅 목적으로만 사용하기를 권장합니다.
// Timeout에 설정한 시간을 초과하면 연결 시도를 중단하고, kStopped 이벤트를 발생시킵니다.
public float ConnectionTimeout = 10f;
}
// Transport option for TCP.
// Note that options in TransportOption class can be used.
public class TcpTransportOption : TransportOption
{
// If set to be true, it will automatically retry to connect to the server.
// (Up to 3 times.)
public bool AutoReconnect = false;
// If set to true, Nagle's algorithm will be disabled.
public bool DisableNagle = false;
// Enables ping.
public bool EnablePing = false;
// Enables log for ping values.
public bool EnablePingLog = false;
// Determines the interval between ping messages.
public int PingIntervalSeconds = 0;
// Clinet will wait for ping response from the server for PingTimeoutSeconds.
// 이 시간 내에 Ping 응답이 오지 않는다면 서버와의 연결이 끊긴 것으로 보고 Disconnect 처리됩니다.
// 인터벌 내에 핑 응답을 받지 못하면 핑을 재전송 하지 않고 핑 응답을 받은 후에 핑을 전송합니다.
public float PingTimeoutSeconds = 0f;
// Helper function to set ping-related options.
public void SetPing (int interval, float timeout, bool enable_log = false);
}
// Transport option class for HTTP.
public class HttpTransportOption : TransportOption
{
// If you want to use HTTPS, set the option.
public bool HTTPS = false;
// Determine whether to use UnityEngine.WWW class.
public bool UseWWW = false;
}
在插件中处理HTTP请求时,使用HttpWebRequest类。 当在Windows中通过Unity Editor使用该功能时,偶尔会出现编辑器不响应的 情况。(因没有明确关闭套接口而导致的问题)为了避免该问题,已添加了UseWWW 选项, 以便能够使用UnityEngine的WWW类。由于该现象仅在编辑器中出现,所以建议将该选项默认设置为false, 仅在运行编辑器时使用WWW类。
指定Transport选项的模板代码位于示例项目Tester.cs文档的makeOption函数中。
38.3. 传送及接收消息¶
38.3.1. 传送消息¶
如想要发送消息,调用FunapiSession类的SendMessage函数即可。
public void SendMessage (string msg_type, object message,
TransportProtocol protocol = TransportProtocol.kDefault,
EncryptionType enc_type = EncryptionType.kDefaultEncryption);
msg_type
是表示消息类型的string型参数。当通过Protobuf发送消息时,也可以使用enum MessageType型来代替string型。
message
是包含实际数据的消息,类型为object。JSON、Protobuf消息 均可以发送。
当未指定 protocol
时, 消息将通过首次注册(Connect)到FunapiSession中的协议
传输。当已注册了多个Transport但却想要通过特定协议传输消息时,指定protocol值即可。
当FunapiSession上已连接了多个协议但却想指定发送消息时默认使用的协议, 修改FunapiSession的DefaultProtocol属性值即可。
session.DefaultProtocol = TransportProtocol.kHttp;
enc_type
为加密型参数。当使用加密功能但未指定加密类型时,将通过默认
类型进行加密,若在不使用加密的情况下输入该值,将会发生错误。
当服务器允许一个协议采用多种加密类型时,在发送消息时,可以选择加密类型后 发送。与加密有关的详细内容请参考 消息加密 。
38.3.1.1. 发送JSON消息¶
Dictionary<string, object> message = new Dictionary<string, object>();
message["message"] = "hello world";
session.SendMessage("echo", message);
为了在插件中使用JSON,我们运用MiniJSON库。MiniJSON的数据类型为 Dictionary<string, object> 必要时,也可以使用其他JSON库。 与此有关的具体说明请参考 JSON Helper类 。
38.3.1.2. 发送Protobuf消息¶
PbufEchoMessage echo = new PbufEchoMessage();
echo.msg = "hello proto";
FunMessage message = FunapiMessage.CreateFunMessage(echo, MessageType.pbuf_echo);
session.SendMessage(MessageType.pbuf_echo, message);
Protobuf消息的基本形式为 FunMessage 。用户消息为extend形式。 extend消息中第0~15条为已预约的消息。用户消息从第16条开始可以使用。
MessageType 是在服务器中构建.proto文件创建消息DLL时同时生成的 enum列表。通过它,可以使用消息名称来代替extend消息的数字。
Important
如果想要在Unity中使用基于Protobuf-net的消息,将须要额外的构建过程。 请参考 在Unity中使用protobuf 的说明。
38.3.2. 接收消息¶
为了从服务器接收消息,须要注册消息回调函数。ReceivedMessageCallback 已按如下所示进行声明。
void ReceivedMessageHandler (string msg_type, object message);
之所以数据类型为object,是因为接收的消息有可能是JSON消息,也有可能是Protobuf消息。
在已注册的处理器中,根据消息类型,将 message
对象转型为JSON或Protobuf后使用
即可。
下面是消息处理器的示例。所有代码可以在Tester.Session.cs文件中查看。
// 받은 메시지를 처리할 콜백을 등록합니다.
session.ReceivedMessageCallback += onReceivedMessage;
// 기다리는 메시지에 timeout을 정해서 알림을 받고 싶다면 이 콜백을 등록합니다.
// 메시지가 지정한 시간내에 오지 않으면 이 콜백 함수가 호출됩니다.
session.ResponseTimeoutCallback += onResponseTimedOut;
// 메시지를 처리하는 방식은 정해진 형식이 없으며 사용하기 편한 형태로 쓰시면 됩니다.
// 여기에서는 받은 메시지를 처리하기 위해 각각의 메시지를 Key-Value 방식으로 저장하겠습니다.
message_handler_["echo"] = onEcho;
message_handler_["pbuf_echo"] = onEchoWithProtobuf;
由于在插件内部通过ReceivedMessageCallback函数传输已Deserialize的消息 ,所以无需再重新对消息进行Deserialize。仅对接收的object消息转型使用即可。
下面是对已注册到 message_handler_
中的消息回调函数的实现示例。
// JSON 메시지에 대한 처리 함수입니다.
void onEcho (object message)
{
// 예제에서는 MiniJSON을 사용합니다.
// 다른 JSON 라이브러리를 사용하고 싶다면 아래 JSON Helper 관련 설명을 참고해주세요.
FunDebug.Assert(message is Dictionary<string, object>);
// 이미 Deserialize 된 메시지를 문자열로 출력하기 위해 다시 Serialize 합니다.
string strJson = Json.Serialize(message);
FunDebug.Log("Received an echo message: {0}", strJson);
}
// Protobuf 메시지에 대한 처리 함수입니다.
void onEchoWithProtobuf (object message)
{
// iFun Engine에서 사용하는 protobuf 메시지는 FunMessage를 기본으로 하고
// 사용자 메시지는 FunMessage 안에 extend 형태로 추가해서 사용합니다.
FunDebug.Assert(message is FunMessage);
FunMessage msg = message as FunMessage;
// extend 된 echo 메시지를 가져옵니다.
object obj = FunapiMessage.GetMessage(msg, MessageType.pbuf_echo);
if (obj == null)
return;
PbufEchoMessage echo = obj as PbufEchoMessage;
FunDebug.Log("Received an echo message: {0}", echo.msg);
}
38.3.3. Response Timeout¶
如果想对正在等待的服务器传输消息定义Timeout,可使用FunapiSession类中的 SetResponseTimeout 函数,对任意消息指定超时时间。
// 기다리는 메시지 타입과 시간을 지정합니다. 등록되는 순간부터 시간이 적용됩니다.
// 'sc_login' 메시지가 10초 내에 오지 않을 경우 ResponseTimeoutCallback 함수가 호출됩니다.
session.SetResponseTimeout("sc_login", 10f);
Tip
不可对相同的消息类型进行重复注册。
由于它是一次性事件,所以在想要对同一消息再次设置Timeout时,须在通过ReceivedMessageCallback 函数收到等待消息后,或因暂停时间结束而调用ResponseTimeoutCallback函数后 ,通过SetResponseTimeout函数重新指定Timeout。
ResponseTimeoutCallback已按如下所示进行声明。已通过SetResponseTimeout函数传输的消息类型将通过参数接收。 若在Timeout的时间内从服务器收到相应消息,将不调用 该回调函数。
public delegate void ResponseTimeoutHandler (string msg_type);
38.3.4. JSON Helper类¶
默认使用MiniJSON插件。如想使用其他JSON库,可继承 JsonAccessor接口类,并创建对JSON库进行处理的类后 ,更改FunapiMessage的JsonHelper即可。
如果需要使用JsonAccessor类中没有的函数,在JsonAccessor衍生类中添加使用 即可。
新创建的JsonAccessor类按如下所示方法注册即可。发送的消息和 接收的消息将全部通过新注册的JSON类进行Serialize或Deserialize。
NewJsonAccessor json_helper = NewJsonAccessor();
FunapiMessage.JsonHelper = json_helper;
Note
在164插件版本中JsonAccessor类的接口有变动。(2016年8月更新) 若已通过继承之前的JsonAccessor类来实现JsonHelper,须要对更新插件时 所新添加的接口进行追加实现。
38.4. 消息加密¶
在与服务器收发消息时,可会对消息进行加密。
38.4.1. 加密类型¶
加密类型有如下种类。
public enum EncryptionType
{
// 암호화를 사용하지만 특정 메시지를 암호화하지 않은 상태로 보내고 싶을 때
// SendMessage의 파라미터로 이 값을 전달하면 됩니다.
kDummyEncryption,
// iFun Engine에서 제공하는 암호화 타입입니다.
// 메시지를 주고 받을 때마다 키 값이 변경되어 안정적인 암호화 방식입니다.
// Tcp 프로토콜에서만 사용 가능합니다.
kIFunEngine1Encryption,
// iFun Engine에서 제공하는 암호화 타입입니다.
// 고정된 암호화 키를 사용합니다. 프로토콜에 상관없이 사용 가능합니다.
kIFunEngine2Encryption,
// ChaCha20 암호화 타입입니다.
// Tcp 프로토콜에서만 사용 가능합니다.
kChaCha20Encryption,
// Aes 128 암호화 타입입니다.
// Tcp 프로토콜에서만 사용 가능합니다.
kAes128Encryption
}
设置加密后,所有消息将在加密状态下发送,但部分消息也可以在未加密的状态下 直接发送。当想要使用加密功能但又想对特定消息不加密时,将SendMessage的 加密类型设置为 kDummyEncryption 即可。
kIFunEngine1Encryption 类型在每次收发消息时加密密钥都会变更。 即使发送的消息相同,每次的密钥值也不相同,所以它是一种稳定的加密方式。 而**kIFunEngine2Encryption**使用固定的密钥,所以与 kIFunEngine1Encryption 相比,它是一种安全性相对较弱的加密方式。
Tip
IFunEngine1、ChaCha20、Aes128加密方式仅可在TCP协议中使用。
ChaCha20 和 Aes128 使用了外部库(sodium)。该库 文件位于 Plugins 文件夹中。
38.4.2. 使用加密¶
对于 TCP ,即使不做任何其他设置,如果已在服务器中设置加密,则在客户端也将随之 进行加密。首次访问服务器时,会对加密进行同步,当服务器上传输了协议的加密类型 列表后,客户端就会使用服务器上规定的加密方式。
若加密类型为多个,则会使用首个加密类型作为默认加密类型。如果在发送 消息时未指定特定的加密类型,将会通过已指定的默认类型进行加密。如果不想单独指定默认加密 类型,在调用FunapiSession.Connect函数时,指定TransportOption的 Encryption值即可。
TcpTransportOption option = new TcpTransportOption();
option.Encryption = EncryptionType.kIFunEngine1Encryption;
当客户端的加密类型为服务器上没有使用的加密类型时,会发生错误, 且无法收发消息。
对于 UDP和HTTP ,在访问后没有和服务器同步加密类型的过程,所以须要在客户端 指定要使用的加密类型。在iFun引擎提供的加密类型中,UDP和HTTP 可以使用的加密类型只有IFunEngine2Encryption。如果想在UDP和HTTP中使用加密, 在TransportOption的Encryption中指定该类型即可。
TransportOption option = new TransportOption();
option.Encryption = EncryptionType.kIFunEngine2Encryption;
Note
GitHub 上发布的客户端插件是包含了IFunEngine1Encryption和IFunEngine2Encryption 类型的加密功能的免费版本。可以使用ChaCha20和Aes128类型。 对于 付费用户 ,请发送邮件至 Funapi support ,我们将为您发送包含所有加密类型 的插件源代码。
38.5. Message Compression¶
Message sent between client and server can be compressed. Currently, following algorithms are supported.
FunCompressionType.kNone
: Default value. Does not compress messages.FunCompressionType.kZstd
: Zstadnard. Recommended for real-time messages.FunCompressionType.kDeflate
. Deflate. It offers better compression ratio, but it may incur additional latency.
38.6. 结束连接与重连¶
38.6.1. 结束连接¶
为了结束和服务器之间的连接,调用FunapiSession的Stop函数即可。
session.Stop();
没有参数的Stop函数会断开已连接的所有Transport。如想要断开特定Transport的连接 ,用参数传输协议类型即可。
session.Stop(TransportProtocol.kTcp);
Stop函数将通过非同步方式断开Transport的连接。Stop函数被调用后不会立即 结束连接,这是因为在Transport试图连接服务器时,若存在等待发送的消息或 无法发送的消息,会发送缓冲区中的消息后再断开连接。
Note
之所以Transport在试图连接服务器时会等待,是因为在连接途中调用Stop时 由于IL2CPP相关漏洞,iOS中会发生崩溃。
只有TCP的条件下才会发送结束连接前未传输的消息后再结束连接。在TCP连接中, 当SendMessage已被调用但仍有未传输的消息时,会将所有未传输的消息 都发送完毕后再断开连接。通过这种方式,即使在调用Stop之前发送logout等消息 ,也能保证消息的传输。
有时会直接结束连接而无需等待,例如已在服务器中关闭Session时、为了跨服移动而须要 结束连接时,以及App关闭时,这些情况均不会等待,会直接断开连接。
38.6.2. 连接结束通知¶
38.6.2.1. Transport Event¶
当Transport的连接结束后,将通过Transport Event传输 kStopped 。 该事件无论是在因错误而断开连接,或是已正常结束连接,只要Transport的连接 断线,就会发生。
当Transport意外断线时, kDisconnected 将通过Transport Event进行传输。当使用Ping时,会检查服务器的连接, 若超出一定时间后服务器仍未响应,也会发生kDisconnected事件。当不使用Ping时,仅通过该事件去检查不同 移动环境的连接断线问题是较为困难的。因为根据设备种类或通信网种类的不同,在检测到连接 断线之前有时需要花费很长时间。若想通过kDisconnected来检查服务器的连接 状态,请使用Ping。
Tip
对于HTTP,由于协议特性问题,不会发生kDisconnected事件,所以Ping仅可在TCP协议中使用。
Transport的事件可通过注册FunapiSession的TransportEventCallback获取。
38.6.2.2. Session Event¶
当Session上的所有Transport连接断线时,将通过Session Event 传输 kStopped 事件。
Session kClosed事件仅在Session Timeout 或服务器已明确关闭Session时发生,而不是在Transport连接断线时发生。 如果从服务器上收到消息显示Session已关闭,则将断开所有Transport连接,并传输 kClosed事件。
Session事件可通过注册FunapiSession的SessionEventCallback获取。
38.6.3. 重连¶
结束服务器连接后重新连接时,使用FunapiSession.Connect函数。 仅将要连接的Transport协议类型通过参数传输即可。
public void Connect (TransportProtocol protocol)
可以使用包含了TransportOption参数的Connect函数进行重连,但Transport选项一旦 注册后便无法更改,即使传输了新的TransportOption,也会被忽略。 因此,最好在重连时仅指定协议后,调用Connect函数。
对于TCP,即使 AutoReconnect
选项已设置为 true
,在重连时若连接失败
也不会尝试重连。 AutoReconnect
选项仅在首次连接时启用。
38.7. 使用Ping¶
为了检查网络连接状态,有一个在频道中使用的名为ping的命令语。与此 类似,插件中也有Ping函数。它可用于检查当前是否保持与服务器的连接。
在难以预测的移动网络环境中,使用ping函数可以持续检查连接状态。 通过它可以更加快速地掌握连接是否断线,以作出应对。
Tip
该功能仅可在TCP协议中使用。
38.7.1. 设置Ping¶
Ping在调用FunapiSession.Connect函数之前,可通过Transport选项进行设置。 如想使用Ping功能,按如下所示调用 SetPing 函数或输入各个参数值即可。
TcpTransportOption tcp_option = new TcpTransportOption();
// 이 함수만 호출하면 핑 설정이 완료됩니다.
tcp_option.SetPing(3, 20, true);
...
// 각각의 값을 따로 입력하고 싶다면 아래와 같이 하면 됩니다.
// 이 값을 true로 주면 Ping 기능을 사용할 수 있습니다.
// EnablePing의 기본 값은 false 입니다.
tcp_option.EnablePing = true;
// 핑 값을 로그로 보고 싶다면 이 값을 true로 주면 됩니다.
tcp_option.EnablePingLog = true;
// 핑 메시지를 보내는 간격을 지정합니다. (초단위)
tcp_option.PingIntervalSeconds = 3;
// 서버로부터 핑 응답을 기다리는 최대 시간을 지정합니다. (초단위)
// 이 시간 이내에 응답이 없을 경우 연결을 끊고 Disconnect 처리를 하게 됩니다.
tcp_option.PingTimeoutSeconds = 20;
当在Ping超时的时间内服务器未给出任何响应时,将断开连接,做Disconnect处理。 Disconnect后,将通过FunapiSession的TransportEventCallback传输kDisconnected。
在服务器中激活Ping功能的方法可以在 会话Ping(RTT) 中查看。
38.8. 跨服移动¶
有时须要根据服务器请求,断开当前已访问的服务器后连接到新的服务器上。 该过程是在插件内部进行的,是通过服务器端的请求进行的动作,所以客户端无法向服务器 请求移动。
38.8.1. 查看跨服移动状态¶
跨服移动开始后,将通过 SessionEventCallback 传输与跨服移动有关的进行状态。 下面是通过回调函数接收的事件类型种类。
public enum SessionEventType
{
...
kRedirectStarted, // 서버간 이동을 시작합니다.
kRedirectSucceeded, // 서버간 이동을 완료했습니다.
kRedirectFailed // 서버간 이동에 실패했습니다.
};
跨服移动开始后,先传输 kRedirectStarted
事件,然后结束与当前服务器之间的连接,
再开始连接新的服务器。直至完成跨服移动前,与Redirect相关事件以外的其他
Session或Transport相关事件都不会被触发。即使在连接的途中发生错误,也仅会输出调试日志
,不会触发任何事件。跨服移动完成后,将传输 kRedirectSucceeded
或
kRedirectFailed
事件。
38.8.2. 서버 이동에 사용할 옵션 설정¶
서버를 이동할 때 접속 중인 서버와 이동하는 서버 간의 설정이 다를 수 있습니다. 이럴 경우 클라이언트에서 Session / Transport 의 옵션을 직접 설정해야 합니다. 옵션 값을 설정하지 않은 경우 이동 전 Session 과 Transport 의 옵션을 그대로 사용하게 됩니다. 옵션 값을 전달하는 콜백 함수의 원형은 아래와 같습니다.
typedef std::function<std::shared_ptr<FunapiSessionOption>(
const fun::string& /*flavor*/)> SessionOptionHandler;
void SetSessionOptionCallback(const SessionOptionHandler &handler);
typedef std::function<std::shared_ptr<FunapiTransportOption>(
const TransportProtocol,
const fun::string& /*flavor*/ )> TransportOptionHandler;
void SetTransportOptionCallback(const TransportOptionHandler &handler);
public delegate SessionOption SessionOptionHandler (string flavor);
public event SessionOptionHandler SessionOptionCallback;
public delegate TransportOption TransportOptionHandler (string flavor, TransportProtocol protocol);
public event TransportOptionHandler TransportOptionCallback;
flavor
는 서버의 종류를 나타냅니다. 이 타입은 서버에서 정해주는 문자열 값입니다. 서버로부터
이동 메시지를 받으면 이동할 서버의 Transport를 새로 만들기 전에 이 콜백 함수들을 호출합니다. 해당 서버의
종류와 프로토콜 타입에 따라 아래와 같이 옵션을 구성해서 반환하면 됩니다.
Note
이동 전 서버에 동일한 프로토콜의 Transport 가 없을 경우에는 기본 TransportOption 을 사용합니다.
// "lobby" flavor 를 사용하는 서버에 접속 할때 세션 신뢰성 옵션을 활성화 합니다.
session_->SetSessionOptionCallback([](const fun::string &flavor) -> std::shared_ptr<fun::FunapiSessionOption>{
if (flavor.compare("lobby") == 0) {
auto session_option = fun::FunapiSessionOption::Create();
session_option->SetSessionReliability(true);
return session_option;
}
// 서버 이동전 옵션을 그대로 사용합니다.
return nullptr;
});
// 새로 만들어질 Tcp 프로토콜의 옵션을 설정합니다.
session_->SetTransportOptionCallback([](const fun::TransportProtocol protocol,
const fun::string &flavor) -> std::shared_ptr<fun::FunapiTransportOption> {
if (flavor == "lobby" &&
protocol == fun::TransportProtocol::kTcp) {
auto option = fun::FunapiTcpTransportOption::Create();
option->SetDisableNagle(true);
return option;
}
// 서버 이동전 옵션을 그대로 사용합니다.
return nullptr;
});
session_.SessionOptionCallback += onSessionOption;
session_.TransportOptionCallback += onTransportOption;
SessionOption onSessionOption (string flavor)
{
if (flavor == "lobby")
{
SessionOption option = new SessionOption();
option.sessionReliability = true;
return option;
}
// 서버 이동전 옵션을 그대로 사용합니다.
return null;
}
TransportOption onTransportOption (string flavor, TransportProtocol protocol)
{
if (flavor == "lobby" && protocol == TransportProtocol.kTcp)
{
TcpTransportOption tcp_option = new TcpTransportOption();
tcp_option.DisableNagle = true;
return tcp_option;
}
// 서버 이동전 옵션을 그대로 사용합니다.
return null;
}
이미 사용중이던 옵션과 동일한 옵션을 사용하거나 기본 옵션으로 연결해도 되는 경우에는 굳이 이 이벤트 콜백을 등록할 필요는 없습니다.
38.9. 组播和聊天¶
使用插件的组播功能,可访问任意频道,和通道的所有用户收发 消息。利用FunapiSession对象,与负责组播的服务器进行连接, 传输消息。可创建FunapiMulticastClient对象,传输FunapiSession对象, 使用组播功能。
FunapiMulticastClient可在欲向位于同一个空间(频道)中的所有用户发送消息时使用。
将 _bounce
选项设置为ture,即使消息接收对象为’自己’,也可以收到相应消息。
专业的聊天类,即FunapiChatClient是继承了FunapiMulticastClient而创建的衍生 类。FunapiChatClient是仅收发文本的类,适合在玩家聊天中使用。
组播 使用TCP ,根据所连接的FunapiSession中使用的方式进行Encoding。 可以使用JSON和Protobuf。
Important
组播功能的每一个Session仅能连接一个对象。若连接两个以上组播对象, 在Session收到组播消息时,将无法获知应该向哪个对象传输消息。 FunapiMulticastClient类和FunapiChatClient类也同样无法同时 通过两个相同的Session来进行连接。
38.9.1. 组播接口¶
FunapiMulticastClient中存在如下接口。
// FunapiMulticastClient 생성자입니다.
// FunapiSession와 주고 받을 메시지의 Encoding을 입력받습니다.
public FunapiMulticastClient (FunapiSession session, FunEncoding encoding)
// 내 id(name)입니다.
// sender를 지정하면 채널 입/퇴장이나 메시지를 보낼 때 sender를 함께 전송합니다.
// 나를 포함한 채널의 모든 유저가 해당 메시지를 보낸 이를 확인할 수 있습니다.
public string sender { set; }
// 서버와 연결되어 있는지 확인하기 위한 property 입니다.
// 서버와 연결이 되어 있다면 true를 반환합니다.
public bool Connected;
// 채널 목록을 요청하는 함수입니다.
// 채널 목록과 함께 채널에 있는 유저 수도 함께 전달됩니다.
// 요청에 성공하면 ChannelListCallback에 등록한 함수가 호출됩니다.
public void RequestChannelList ();
// channel_id 가 입장한 채널의 id 인지 확인하는 property 입니다.
// 입장한 채널이면 true를 반환합니다.
public bool InChannel (string channel_id);
// 채널에 입장할 때 사용하는 함수입니다.
// channel_id 는 입장할 channel id 입니다. 존재하지 않는 채널이면 서버에서 생성합니다.
// handler는 메시지를 전달받으면 호출되는 함수입니다.
// 이미 channel_id에 해당하는 채널에 입장했거나 서버와 연결되어 있지 않으면 false 를 반환합니다.
public bool JoinChannel (string channel_id, ChannelMessage handler);
// 채널을 나갈 때 사용하는 함수입니다.
// channel_id는 나갈 channel id 입니다.
// 입장한 채널이 없거나 서버와 연결되어 있지 않으면 false 를 반환합니다.
public bool LeaveChannel (string channel_id);
// 채널에 메시지를 전송할 때 사용하는 함수입니다.
// 이 함수는 protobuf로 메시지를 전송할 때 사용합니다.
public bool SendToChannel (FunMulticastMessage mcast_msg);
// 이 함수는 json으로 메시지를 전송할 때 사용합니다.
public bool SendToChannel (object json_msg);
// 채널에 입/퇴장을 알려주는 Handler의 delegate 입니다.
// 채널의 입/퇴장 알림을 받고 싶다면 ChannelNotify의 event 객체인
// JoinedCallback이나 LeftCallback을 등록하면 됩니다.
public delegate void ChannelNotify(string channel_id, string sender);
// 채널로부터 메시지를 받으면 호출되는 Handler의 delegate 입니다.
// 이 delegate와 동일한 타입으로 함수를 만들고 JoinChannel() 함수의 두 번째 인자로 전달합니다.
// channel_id 는 메시지를 전달받은 채널 id 입니다.
// body는 전달받은 메시지이며 Encoding에 따라서 JSON이나 Protobuf인 FunMulticastMessage가 전달됩니다.
public delegate void ChannelMessage(string channel_id, string sender, object body);
38.9.2. 组播示例。¶
首先创建FunapiSession并 通过Tcp连接,为通过FunapiMulticastClient处理组播进行事前准备。
string kServerIp = "127.0.0.1";
// 우선 FunapiSession 을 만듭니다.
// 멀티캐스팅은 session reliability 와 상관없이 동작합니다.
// 예제의 단순함을 위해 여기서는 이 기능을 사용하지 않겠습니다.
session = FunapiSession.Create(kServerIp, false);
// Transport의 이벤트를 받기 위한 콜백함수를 등록합니다.
session.TransportEventCallback += onTransportEvent;
// 서버에 접속합니다.
// Transport의 옵션 설정은 생략하겠습니다.
session.Connect(TransportProtocol.kTcp, FunEncoding.kJson, 8012);
为了传输消息,现在创建FunapiMulticastClient。
可以注册对频道的入场和退场进行通知的回调函数。可以对包含自身在内的 所有出入频道的用户入场和退场情况进行通知。
JoinedCallback |
用户进入频道时所调用的函数 |
LeftCallback |
用户退出频道时所调用的函数 |
ErrorCallback |
发生错误时传输错误代码的函数 |
// FunapiMulticastClient를 만듭니다.
// 위에서 만든 session과 session과 같은 encoding 타입을 전달합니다.
FunapiMulticastClient multicast = new FunapiMulticastClient(session, FunEncoding.kJson);
// 내 아이디를 입력합니다.
multicast_.sender = "my name";
// 채널 목록을 받았을 때 호출되는 콜백입니다.
multicast_.ChannelListCallback += delegate (object channel_list) {
onMulticastChannelList(encoding, channel_list);
};
// Player가 채널에 입장하면 호출되는 콜백입니다.
multicast_.JoinedCallback += delegate(string channel_id, string sender) {
DebugUtils.Log("JoinedCallback called. player:{0}", sender);
};
// Player가 채널에서 퇴장하면 호출되는 콜백입니다.
multicast_.LeftCallback += delegate(string channel_id, string sender) {
DebugUtils.Log("LeftCallback called. player:{0}", sender);
};
// 에러가 발생했을 때 알림을 받는 콜백입니다.
// 에러 종류는 enum FunMulticastMessage.ErrorCode 타입을 참고해주세요.
multicast_.ErrorCallback += delegate(FunMulticastMessage.ErrorCode code) {
// error
};
为进入频道,会调用 JoinChannel()
函数。若相应频道有消息传输,将调用
onMulticastChannelReceived函数。
若想同时进入多个频道,在每个想要进入的频道
调用 JoinChannel()
函数即可。
// 입장할 채널 id 를 입력합니다.
// 채널이 존재하지 않으면 JoinChannel() 함수를 호출할 때 채널이 생성됩니다.
string channel_id = "test_channel";
// 채널에 입장하기 위해 JoinChannel() 함수를 호출합니다.
// 해당 채널로부터 메시지가 전송되면 onMulticastChannelReceived 함수가 호출됩니다.
multicast.JoinChannel(channel_id, onMulticastChannelReceived);
// 채널에 메시지 전송
PbufHelloMessage hello_msg = new PbufHelloMessage();
hello_msg.message = "multicasting message";
PbufHelloMessage
存在于服务器的{project}_messages.proto文件中。
首次通过funapi_initiator创建项目时,默认已经添加。PbufHelloMessage
是为示例默认生成的,在 MulticastServer
中proto message的名字或field
不受限制。因此,通过必要的名字来创建proto message,自由定义要使用的field
后,iFun引擎即可自主对其确认并传输给客户端。这对于JSON也一样。
下面是{project}_messages.proto文件中的基本内容。
message PbufAnotherMessage {
optional string msg = 1;
}
extend FunMessage {
optional PbufEchoMessage pbuf_echo = 16;
optional PbufAnotherMessage pbuf_another = 17;
}
import "funapi/service/multicast_message.proto";
message PbufHelloMessage
{
optional string message = 1;
}
extend FunMulticastMessage
{
optional PbufHelloMessage pbuf_hello = 9;
}
在以下onMulticastChannelReceived的protobuf示例代码中, MulticastMessageType.pbuf_hello
消息未被默认添加到插件中。构建服务器时,proto文件也会一同
被构建,此时将自动识别已在 extend FunMulticastMessage
中输入的PBufHelloMessage的field名和extend编号,并创建enum。 与此有关的具体说明请参考 在Unity中使用protobuf 。
private void onMulticastChannelReceived (string channel_id, string sender, object body)
{
if (multicast_.encoding == FunEncoding.kJson)
{
// 채널이 맞는지 확인합니다.
string channel = FunapiMessage.JsonHelper.GetStringField(body, "_channel");
FunDebug.Assert(channel != null && channel == channel_id);
// 메시지를 전송할 때 "message" 필드에 전송할 메시지를 담았습니다.
// 따라서 수신한 메시지는 mcast_msg["message"] 에 있습니다.
string message = FunapiMessage.JsonHelper.GetStringField(body, "_message");
// 여기서는 간단하게 로그를 출력하겠습니다.
DebugUtils.Log("Received a multicast message from the '{0}' channel.\nMessage: {1}",
channel_id, message);
}
else
{
// 수신한 메시지를 Protobuf인 FunMulticastMessage로 처리합니다.
FunMulticastMessage mcast_msg = body as FunMulticastMessage;
// MulticastMessageType 은 서버를 빌드할 때 proto 파일도 함께 빌드하면 자동 생성됩니다.
// 이 예제에서는 PbufHelloMessage를 사용하므로 이것의 field 이름인 pbuf_hello를 사용합니다.
object obj = FunapiMessage.GetMulticastMessage(mcast_msg, MulticastMessageType.pbuf_hello);
PbufHelloMessage hello_msg = obj as PbufHelloMessage;
if (hello_msg == null)
return;
// 메시지를 전송할 때 PbufHelloMessage의 message field에 메시지를 저장합니다.
// 따라서 hello_msg.message 에 전달된 메시지가 담겨 있습니다.
DebugUtils.Log("Received a multicast message from the '{0}' channel.\nMessage: {1}",
channel_id, hello_msg.message);
}
}
想传输消息时,调用 SendToChannel()
函数。
// channel id를 입력합니다.
string channel_id = "test_channel";
if (multicast_.encoding == FunEncoding.kJson)
{
// JSON으로 메시지를 전송합니다.
Dictionary<string, object> mcast_msg = new Dictionary<string, object>();
// _channel 필드에 channel id를 입력합니다.
mcast_msg["_channel"] = channel_id;
// 보낸 메시지를 나도 받고 싶다면 _bounce 필드값을 true로 설정합니다.
mcast_msg["_bounce"] = true;
// 메시지를 mcast_msg["message"] 에 담겠습니다.
// 이는 자유롭게 변경하셔도 되며 필요하다면 다른 필드를 추가하셔도 됩니다.
mcast_msg["message"] = "multicast test message";
// 다른 필드 추가
// mcast_msg["something"] = "...";
// 멀티캐스팅 역할을 하는 서버에 메시지를 전송합니다.
multicast.SendToChannel(mcast_msg);
}
else
{
// protobuf로 메시지를 전송합니다.
// PbufHelloMessage 파일은 서버의 {project}_messages.proto 파일에 정의 되어 있습니다.
// 기본적으로 포함된 proto message인데 자유롭게 이름과 field 등을 정의하셔도 됩니다.
PbufHelloMessage hello_msg = new PbufHelloMessage();
// message field에 전송할 메시지를 담겠습니다.
hello_msg.message = "multicast test message";
// 멀티캐스팅 메시지를 전송하기 위해서 FunMulticastMessage 를 생성합니다.
// 이 부분은 멀티캐스팅 역할을 하는 서버에서 필요한 proto message 이므로 반드시 필요한 부분입니다.
FunMulticastMessage mcast_msg = new FunMulticastMessage();
// channel field 에 channel id 를 입력합니다.
mcast_msg.channel = channel_id;
// 보낸 메시지를 나도 받고 싶다면 bounce 필드 값을 true로 설정합니다.
mcast_msg.bounce = true;
// protobuf 에서 제공하는 Extensible 인터페이스를 이용하여 PbufHelloMessage를 추가합니다.
Extensible.AppendValue(mcast_msg, (int)MulticastMessageType.pbuf_hello, hello_msg);
// 멀티캐스팅 역할을 하는 서버에 메시지를 전송합니다.
multicast_.SendToChannel(mcast_msg);
}
退出频道时,调用 LeaveChannel()
函数。
// 채널 나가기
string channel_id = "test_channel";
multicast.LeaveChannel(channel_id);
38.9.3. 聊天接口¶
FunapiChatClient中存在如下接口,与FunapiMulticastClient的差别
在于它不通过 object
对象接收消息,而是仅接收 string
。
// FunapiChatClient 생성자입니다.
// FunapiSession과 주고 받을 메시지의 Encoding을 입력받습니다.
public FunapiChatClient (FunapiSession session, FunEncoding encoding);
// 채널에 입장할 때 사용하는 함수입니다.
// chat_channel은 입장할 channel id 입니다. 존재하지 않는 채널이면 서버에서 생성합니다.
// my_name은 채팅에서 사용할 나의 이름입니다. FunapiMulticastClient의 sender에 값을 저장하게 됩니다.
// handler는 메시지를 전달받으면 호출되는 함수입니다.
// 이미 channel_id 에 해당하는 채널에 입장했거나 서버와 연결되어 있지 않으면 false를 반환합니다.
public bool JoinChannel (string chat_channel, string my_name, OnChatMessage handler);
// my_name 파라미터가 없는 채널에 입장할 때 사용하는 함수입니다.
// FunapiMulticastClient의 sender 값을 지정했다면 채널에 입장할 때 이 함수를 사용해도 좋습니다.
public bool JoinChannel (string channel_id, OnChatMessage handler);
// 채널을 나갈 때 사용하는 함수입니다.
// 채널에서 나갈 때는 FunapiMulticastClient의 LeaveChannel 함수를 호출하면 됩니다.
public bool LeaveChannel (string channel_id);
// 채널에 메시지를 전송할 때 사용하는 함수입니다.
// chat_channel은 메시지를 전송할 channel 의 id 입니다.
// text는 전송할 메시지입니다.
public bool SendText (string chat_channel, string text);
// 채널로부터 메시지를 받으면 호출되는 Handler의 delegate 입니다.
// 이 delegate와 동일한 타입으로 함수를 만들고 JoinChannel() 함수의 인자로 전달합니다.
public delegate void OnChatMessage(string channel_id, string sender, string text);
38.9.4. 聊天示例¶
FunapiChatClient是运用组播功能创建的类,其使用方法和FunapiMulticastClient 类似。因此,此处将简单介绍FunapiChatClient的使用方法。
示例中将通过 Json
进行Encoding。
// FunapiSession을 생성합니다.
FunapiSession session = FunapiSession.Create(kServerIp, false);
session.TransportEventCallback += onTransportEvent;
// 서버에 연결합니다.
session.Connect(TransportProtocol.kTcp, FunEncoding.kJson, 8012);
...
// FunapiChatClient 객체를 생성합니다.
FunapiChatClient chat = new FunapiChatClient(session, FunEncoding.kJson);
// 입장하는 플레이어의 이름을 입력합니다.
chat_.sender = "my name";
// 채널 입장
string channel_id = "test_channel";
chat.JoinChannel(channel_id, onMulticastChannelReceived);
接收到消息后,将调用 onMulticastChannelReceived()
函数。
// 메시지를 전달받으면 호출됩니다.
// FunapiMulticastClient 와는 달리 encoding에 따라 message를 처리하지 않아도 됩니다.
// 메시지 보낸 사람인 sender와 전송된 메시지인 text를 처리하시면 됩니다.
private void onMulticastChannelReceived (string chat_channel, string sender, string text)
{
// 여기서는 간단하게 로그를 출력하겠습니다.
DebugUtils.Log("Received a chat channel message.\nChannel={0}, sender={1}, text={2}",
chat_channel, sender, text);
}
运用 SendText()
函数向频道传输消息。
// 채널에 메시지 전송
string channel_id = "test_channel";
string message = "Hello World";
chat.SendText(channel_id, message);
从频道中退出时,使用 LeaveChannel()
函数。
// 채널 나가기
string channel_id = "test_channel";
chat.LeaveChannel(channel_id);
38.10. 查看公告事项¶
如果已经使用了引擎提供的公告服务器,则可以通过客户端插件从公告服务器中获取公告事项列表,随时更新公告事项。
可通过FunapiAnnouncement类向服务器请求公告事项列表。下面是 使用FunapiAnnouncement类向服务器请求公告事项,并在回调函数中解析公告事项列表 的示例代码。消息类型为JSON。
std::shared_ptr<fun::FunapiAnnouncement> announcement_ = nullptr;
// 공지사항은 웹 서비스입니다.
// 서버에서 공지사항 용도로 열어 둔 ip와 port를 주소로 요청합니다.
// 두번째 인자로 다운로드될 파일들의 저장위치를 지정합니다.
announcement_ = fun::FunapiAnnouncement::Create("http://127.0.0.1:8080",
TCHAR_TO_UTF8(*(FPaths::ProjectSavedDir())));
// 완료 결과를 받을 콜백을 등록합니다.
announcement_->AddCompletionCallback([this](const std::shared_ptr<fun::FunapiAnnouncement> &announcement,
const fun::vector<std::shared_ptr<fun::FunapiAnnouncementInfo>>&info,
const fun::FunapiAnnouncement::ResultCode result){
if (result == fun::FunapiAnnouncement::ResultCode::kSucceed)
{
// 각각의 목록에 대해 정보를 가져올 수 있습니다.
for (auto i : info)
{
UE_LOG(LogFunapiExample,
Log,
TEXT("date=%s message=%s subject=%s file_path=%s"),
*FString(i->GetDate().c_str()),
*FString(i->GetMessageText().c_str()),
*FString(i->GetSubject().c_str()),
*FString(i->GetFilePath().c_str()));
}
}
});
// 서버로 부터 받을 공지사항 개수를 등록합니다.
announcement_->RequestList(5);
// 목록을 가져오거나 이미 있을경우 갱신해줍니다.
fun::FunapiAnnouncement::UpdateAll();
FunapiAnnouncement announcement = new FunapiAnnouncement();
void Init ()
{
// 공지사항은 웹 서비스입니다.
// 서버에서 공지사항 용도로 열어 둔 ip와 port를 주소로 요청합니다.
announcement.Init("http://127.0.0.1:8080");
announcement.ResultCallback += OnAnnouncementResult;
// 이 함수를 호출하면 목록을 가져오거나 이미 있을경우 갱신해줍니다.
// 가져올 공지사항 최대 개수를 인자로 넘깁니다.
announcement.UpdateList(5);
}
void OnAnnouncementResult (AnnounceResult result)
{
// 결과가 Success가 아닐경우
if (result != AnnounceResult.kSuccess)
return;
// 메시지는 JSON 타입이며 MiniJSON을 사용합니다.
if (announcement.ListCount > 0)
{
for (int i = 0; i < announcement.ListCount; ++i)
{
// 각각의 목록에 대해 정보를 가져올 수 있습니다.
Dictionary<string, object> list = announcement.GetAnnouncement(i);
string buffer = "";
foreach (var item in list)
buffer += item.Key + ": " + item.Value + "\n";
Debug.Log("announcement >> " + buffer);
}
}
}
Note
공지사항에 이미지가 포함되어 있을 경우 공지사항을 다 받은 후에 순차적으로 다운로드 하여 image_url
에 들어 있는 파일을 플랫폼 별로 다음과 같은 위치에 저장합니다.
FunapiUtils.GetLocalDataPath/
+ "announce/"
경로 아래에 저장합니다.FunapiAnnouncement::Create(url, path)
함수의 path
인자로 지정된 경로 아래에 저장합니다.Note
모든 다운로드가 끝나면 콜백 함수가 호출됩니다. 다운로드 할 이미지가 많을 경우 콜백 함수 호출까지 시간이 오래걸릴 수도 있습니다.
公告事项中的项目未被指定。服务器管理员对必要的项目进行定义并使用即可。 但在想要更改原来使用的字段时,有时须要同时修改插件代码,所以在须要修改原字段 时,请咨询 Funapi support 。
38.11. 服务器维护消息。¶
当服务器正在维护时,客户端发送的消息会被忽略,服务器在每次收到消息时将向客户端发送维护说明消息。 连接服务器后,若不向服务器发送任何消息,将无法从服务器收到维护说明消息。
维护说明消息并不是通过用于接收消息的回调函数传输,而是通过FunapiSession的MaintenanceCallback函数传输。 如想处理服务器维护消息,可注册MaintenanceCallback函数,并在回调函数中 进行必要的操作即可。
// 서버로부터 점검 안내 메시지를 받았을 때 이 콜백함수가 호출됩니다.
session.MaintenanceCallback += onMaintenanceMessage;
若收到服务器维护消息,按如下方式解析后使用即可。消息内容由维护开始日期和时间、 结束日期和时间、字符串消息组成。
date_start |
string |
服务器维护开始日期和时间 |
date_end |
string |
服务器维护结束日期和时间 |
messages |
string |
消息 |
if (encoding == FunEncoding.kJson)
{
JsonAccessor json_helper = FunapiMessage.JsonHelper;
FunDebug.Log("Maintenance message\nstart: {0}\nend: {1}\nmessage: {2}",
json_helper.GetStringField(message, "date_start"),
json_helper.GetStringField(message, "date_end"),
json_helper.GetStringField(message, "messages"));
}
else if (encoding == FunEncoding.kProtobuf)
{
FunMessage msg = message as FunMessage;
object obj = FunapiMessage.GetMessage(msg, MessageType.pbuf_maintenance);
if (obj == null)
return;
MaintenanceMessage maintenance = obj as MaintenanceMessage;
FunDebug.Log("Maintenance message\nstart: {0}\nend: {1}\nmessage: {2}",
maintenance.date_start, maintenance.date_end, maintenance.messages);
}
所有代码可以在Tester.Session.cs文件中查看。
38.12. 下载资源文件¶
如果在服务器中使用客户端资源服务,那么在客户端也可以更新客户端的 资源,无需额外的二进制更新。
如想使用该功能,须要使用FunapiHttpDownloader类。 下面是创建FunapiHttpDownloader注册回调函数的示例代码。 所有代码可以在Tester.Download.cs文件中查看。
// FunapiHttpDownloader 객체를 생성합니다.
FunapiHttpDownloader downloader = new FunapiHttpDownloader();
// 필요한 콜백을 등록합니다.
// ReadyCallback과 FinishedCallback은 필수로 등록해야 합니다.
downloader.VerifyCallback += OnDownloadVerify;
downloader.ReadyCallback += OnDownloadReady;
downloader.UpdateCallback += OnDownloadUpdate;
downloader.FinishedCallback += OnDownloadFinished;
// 다운로드할 파일의 목록을 요청합니다.
// 파라미터로 서버 주소(포트를 포함한 URL)와 파일이 저장될 경로를 넘겨줍니다.
downloader.GetDownloadList("http://127.0.0.1:8020", "target/path", "file/path");
回调函数中 ReadyCallback
和 FinishedCallback
函数是必需的回调函数,其他函数
根据需求注册使用即可。
从收到文件列表时开始下载。调用GetDownloadList函数,获取资源 目录,确认本地文件。在GetDownloadList函数的参数中指定服务器URL和要保存文件的 文件夹路径。 file/path 是包含了列表文件名的路径,并且对除了作为第一个参数的服务器URL 地址以外的后面路径部分进行传输即可。当向游戏服务器请求时,可以不指定该值,但在 直接通过CDN请求资源列表文件时,务必要输入该值。
VerifyCallback |
各文件的有效性检验结束时调用。 |
ReadyCallback |
收到资源列表,结束有效性检验后,完成下载准备工作时 调用。 |
UpdateCallback |
显示正在下载的文件的进行状态。最好用于 UI更新。 |
FinishedCallback |
资源下载结束后调用。 |
调用GetDownloadList函数后,若下载准备工作完毕,将调用ReadyCallback函数。 调用ReadyCallback函数后,可调用StartDownload函数,并开始下载。 下载完成后,将调用FinishedCallback函数。
downloader.StartDownload();
如果没有新下载的文件,将不调用ReadyCallback函数,仅调用FinishedCallback 函数。
FunapiHttpDownloader类中有几个可以在下载开始前确认的信息。
TotalDownloadFileCount |
要下载的所有文件个数 |
TotalDownloadFileSize |
要下载的所有文件大小(bytes) |
CurrentDownloadFileCount |
已下载的文件个数 |
CurDownloadFileSize |
已下载的文件大小(bytes) |
这些Property须要在ReadyCallback函数被调用后使用。
38.13. 社交网络插件¶
使用Funapi Social plugin可轻松通过Facebook和Twitter账户登录或
发帖等。安装包文件位于插件根目录下 additional-plugins/Packages
文件夹中。
Scripts
文件夹是在Import安装包时,对附加文件进行解压后的文件夹。
若已经通过安装包安装,也可以不复制 Scripts
文件夹中的文件。示例代码位于 Tester
文件夹中。
Tip
社交网络插件当前仅提供Unity版本。
38.13.1. 接口类¶
Funapi Social Plugin中有共同使用的接口类。名为SocialNetwork,拥有如下所示接口。
public abstract class SocialNetwork : MonoBehaviour
{
// 친구 정보를 담고 있는 클래스입니다.
public class UserInfo
{
public string id; // 계정 아이디
public string name; // 계정 이름
public string url; // 프로필 사진 url
public Texture2D picture; // 프로필 사진 이미지
}
// 초기화 함수입니다.
// 페이스북의 경우 파라미터가 필요 없지만 트위터는 consumer key 와 consumer secret 값을 전달해야 합니다.
public abstract void Init (params object[] param);
// 메시지를 포스팅하는 함수입니다.
public virtual void Post (string message)
// 메시지와 이미지를 함께 포스팅하는 함수입니다.
public virtual void PostWithImage (string message, byte[] image)
// 메시지와 함수가 호출되는 시점의 화면을 함께 포스팅하는 함수입니다.
public virtual void PostWithScreenshot (string message)
// 친구 목록에서 해당하는 아이디의 친구 정보를 반환합니다.
public UserInfo FindFriendInfo (string id)
// 친구 목록에서 해당하는 인덱스의 친구 정보를 반환합니다. (인덱스는 0부터 시작)
public UserInfo FindFriendInfo (int index)
// 초대 목록에서 해당하는 아이디의 친구 정보를 반환합니다.
public UserInfo FindInviteFriendInfo (string id)
// 초대 목록에서 해당하는 인덱스의 친구 정보를 반환합니다. (인덱스는 0부터 시작)
public UserInfo FindInviteFriendInfo (int index)
// 내 계정의 아이디를 반환합니다.
public string my_id;
// 내 계정의 이름을 반환합니다.
public string my_name;
// 내 계정의 프로필 사진 Texture2D를 반환합니다.
public Texture2D my_picture;
// 친구 목록의 전체 개수를 반환합니다.
public int friend_list_count;
// 초대 목록의 전체 개수를 반환합니다.
public int invite_list_count;
// 이벤트 함수 원형
public delegate void EventHandler (SNResultCode code);
public delegate void PictureDownloaded (UserInfo user);
// 초기화, 로그인, 친구 목록 가져오기, 포스팅 등의 응답에 대한 이벤트 함수입니다.
// 콜백 호출시 파라미터로 enum SNResultCode 값이 전달됩니다.
// 요청에 실패할 경우 SNResultCode.kError가 전달됩니다.
public event EventHandler OnEventCallback;
// 프로필 사진을 받으면 호출되는 이벤트 함수입니다.
// 해당 프로필 사진의 UserInfo가 호출과 함께 전달됩니다.
public event PictureDownloaded OnPictureDownloaded;
}
使用继承了该SocialNetwork类创建的FacebookConnector类和TwitterConnector 类即可。
Note
在开始联动插件之前,须创建相应的社交App。 Facebook请创建 Facebook App 。 Twitter请创建 Twitter App 。 对于Facebook,须要在安装我公司提供的 facebook-plugin 之前, 先安装 Facebook SDK for Unity 。
38.13.2. Facebook¶
插件功能
有对Facebook SDK的功能进行了Wrapping,故仅提供几种功能。如除了以下 功能以外,还需使用其他功能,可以向 Funapi support 申请。
通过Facebook账户登录
请求个人信息和头像
请求好友列表和头像
FacebookConnector属于MonoBehaviour对象,所以须要提前注册到Scene。下面 是从已注册的Object中调取FacebookConnector对象进行初始化的代码。
// FacebookConnector 컴포넌트를 가져옵니다.
FacebookConnector facebook = GameObject.Find("object name").GetComponent<FacebookConnector>();
// 초기화, 로그인 등의 요청에 대한 응답을 알려주는 이벤트 핸들러를 등록합니다.
facebook.OnEventCallback += OnEventHandler;
// 프로필 사진을 다운받으면 호출되는 이벤트 핸들러를 등록합니다.
facebook.OnPictureDownloaded += delegate(SocialNetwork.UserInfo user) {
// do something with profile picture
};
// 초기화 함수를 호출합니다.
facebook.Init();
Facebook登录
登录函数有两种,当仅需要读取权限时,可通过 LogInWithRead
函数登录,当同时还需要
写入权限时,须通过 LogInWithPublish
函数登录。
须用登录函数的参数来传输权限列表。该权限可以在Facebook App的Status & Review 页面查看,默认提供了几种功能,发帖子等大部分功能可在请求权限 并获得Facebook的批准后在App中使用。在Unity编辑器中测试时可通过 Graph API Explorer 获取用于测试的Access Token。
// 사용자 정보와 친구 목록을 읽어올 수 있도록 LogInWithRead 함수로 로그인합니다.
facebook.LogInWithRead(new List<string>() {
"public_profile", "email", "user_friends"});
// 친구 목록을 요청합니다. 최대 100명의 친구 목록을 가져옵니다.
facebook.RequestFriendList(100);
请求好友列表时,会自动同时请求图片,若暂时不需要所有好友的图片,也可选择性 下载图片。
// 이 옵션을 false로 주면 친구 목록을 요청할 때 프로필 사진을 같이 요청하지 않습니다.
// 이 값은 친구 목록을 요청하기 전에 설정해야 합니다. FacebookConnector 객체를 초기화 할 때 설정하는 것이 좋습니다.
facebook.auto_request_picture = false;
// 친구 목록에 있는 유저들의 프로필 사진을 요청합니다.
// 시작 인덱스 값과 최대 개수를 전달합니다.
facebook.RequestFriendPictures(0, 20);
若请求图片,将依次通过非同步的方式下载图片。完成头像图片的下载后,将调用 PictureCallback事件函数。如果想在图片下载完成时收到通知,须在初始化 FacebookConnector对象时,在PictureCallback事件中注册回调函数。
38.13.3. Twitter¶
插件功能
Twitter插件提供的功能如下所示。如除了以下功能以外,还需使用其他功能,可以向 Funapi support 申请。
通过Twitter账户登录
请求个人信息和头像
请求好友列表和头像
发帖到我的Twitter
TwitterConnector属于MonoBehaviour对象,所以须要提前注册到Scene。下面 是从已注册的Object中调取TwitterConnector对象进行初始化的代码。
// TwitterConnector 컴포넌트를 가져옵니다.
TwitterConnector twitter = GameObject.Find("object name").GetComponent<TwitterConnector>();
// 초기화, 로그인, 포스팅 등의 요청에 대한 응답을 알려주는 이벤트 핸들러를 등록합니다.
twitter.OnEventCallback += OnEventHandler;
// 프로필 사진을 다운받으면 호출되는 이벤트 핸들러를 등록합니다.
twitter.OnPictureDownloaded += delegate(SocialNetwork.UserInfo user) {
// do something with profile picture
};
// 초기화 함수를 호출합니다.
// 트위터 앱의 Consumer Key와 Consumer Secret 값을 전달합니다.
twitter.Init("4RnU4YDXmu8vmwKW5Lgpej3Xc",
"voDDAoaTNXj8VjuWRDhfrnCpa9pnVgpRhBJuKwjJpkg62dtEhd");
Twitter初始化时,须要添加 Consumer Key
和 Consumer Secret
值作为传输参数,并调用 Init()
函数。该值可在 Twitter App 的
[Keys and Access Tokens]标签下查看。
Twitter登录
Twitter的登录方法相对较为复杂,但一旦登录并获得App批准后,将继续使用首次登录
时获得的 Access Token
,所以只要经历这样一次过程即可。
// 로그인 함수를 호출해 앱을 승인하고 PIN Code를 얻습니다.
// 웹을 통해서 얻은 PIN Code를 입력받는 UI는 클라이언트에서 구현해야 합니다.
twitter.Login();
// Access Token을 요청합니다.
// 로그인 함수를 통해 얻은 PIN Code를 파라미터로 전달합니다.
twitter.RequestAccess("pin code");
通过调用 RequestAccess()
函数而获取的 Access Token
是可永久使用的认证值,
只要有该值即可随时在Twitter发帖。登录一次后,该值会在
插件中加密保存,在下次调用 Init()
函数时进行确认,且无需认证过程,就会通过回调函数
通知已登录成功。
登录后即可发布消息。如下所示,发送消息即可发布帖子到我的Twitter。
twitter.Post("Funapi plugin test message~");
38.14. Unity插件¶
Unity client plugin可通过 GitHub 获取。 如您需要包含加密功能的版本,须向 Funapi support 申请。
在从 GitHub 获取的源代码中,将 Assets
文件夹中的Funapi、Plugins文件夹复制到使
用插件的项目的 Assets
文件夹中即可。若使用HTTPS,请一同复制Editor和
Resources文件夹。 Tester
文件夹内含有插件的示例文件。
38.14.1. 在Unity中使用protobuf¶
由于iOS / Android的限制规定,通过protobuf-net所建的C#代码 目前还无法直接使用。当直接使用
protobuf.NET
所创建的C#文件时,
将在读取消息的 some_protobuf_message.GetExtension(...)
类型的代码中留下如下
类似错误消息,并在 libmono
的JIT编译器中发生崩溃。
F/mono (1021): * Assertion at mini-arm.c:2595, condition `pdata.found == 1' not met
F/libc (1021): Fatal signal 11 (SIGSEGV) at 0x0000600d (code=-6), thread 1033 (UnityMain)
因此,消息的serialize/deserialize部分须更改为使用AOT构建的.dll ,而不是C#代码。
设置成构建Protobuf专用.dll
在游戏服务器的最上级目录的 CMakeLists.txt
中,有如下所示的
GENERATE_UNITY_PROTOBUF_DLL
设置。将该值更改为 true
,即可创建
Build目录中所需的.dll文件。
# Source managed by git can append a build number from git commit id.
set(PACKAGE_WITH_BUILD_NUMBER_FROM_GIT false)
# Source managed by svn can append a build number from svn info.
set(PACKAGE_WITH_BUILD_NUMBER_FROM_SVN false)
# Generate .DLL for Unity Engine
set(GENERATE_UNITY_PROTOBUF_DLL true)
复制已创建的.dll文件
当已在构建游戏服务器Build目录时,例如已构建名为 hello
的项目的调试设置时,将在
hello-build/debug/unity_dll
目录下生成如下.dll文件。
protobuf-net.dll
: Unity引擎专用protobuf-net
文件
messages.dll
: 引擎中生成的protobuf消息定义
FunMessageSerializer.dll
: 引擎中生成的protobuf的读取写入routine
复制该文件到客户端的 Assets
目录下,在Unity编辑器中查看即可。
如果是Windows环境,可使用 https://winscp.net/eng
中提供的程序,会更加便利。
Tip
个别消息和消息的 enum
的源代码在服务器Build目录下的 unity_cs
目录中会生成为 messages.cs
、 messages_enum.cs
文件。
38.14.2. C# Runtime Test Code¶
插件文件夹内有用于Bot Test的C# Runtime测试代码。在Unity中 可以同时打开的套接口的最大数量是受限制的,所以建议直接编写用于测试的Bot程序。 更具体的内容可在 方法2:利用客户端插件 文档中查看。
38.14.3. 查看Unity Plugin日志¶
与插件有关的日志仅在Unity编辑器或C# Runtime运行时显示。保存日志有可能会对游戏的
运行产生影响,因此除了通过编辑器运行的情况以外不会保存日志。
如果为了测试而想要时常查看日志,将 Funapi/DebugUtils.cs 文件上方的
ENABLE_LOG
修改为可时常定义即可。除了默认日志以外,如想
查看更具体的日志,请激活 ENABLE_DEBUG
。
//#define ENABLE_DEBUG // 디버그 로그 표시
// 유니티 에디터 or C# Runtime일 경우 로그 표시
#if ENABLE_DEBUG || UNITY_EDITOR || NO_UNITY
#define ENABLE_LOG
#endif
ENABLE_DEBUG
define可以在 Funapi/DebugUtils.cs 文件中取消注释,也可在Unity
Build设置中添加define symbol。在Build设置的 Other Settings 标签页
为 Scripting Define Symbols 项添加ENABLE_DEBUG即可。
38.14.4. 保存日志¶
可将插件的日志保存为字符串或文件。如想使用该功能,在
Funapi/DebugUtils.cs 文件中激活 ENABLE_SAVE_LOG
define即可。
下面是与FunDebug类中的日志保存有关的函数和属性值。与此相关的示例代码位于 Tester/Tester.cs 文件中。
// 이 옵션을 활성화하면 로그가 버퍼에 저장됩니다.
#define ENABLE_SAVE_LOG
public class FunDebug
{
...
// 버퍼에 저장된 내용을 파일로 저장합니다. 버퍼의 크기는 1MBytes 입니다.
// 버퍼가 꽉 차면 자동으로 파일로 저장되고 버퍼가 초기화됩니다.
// 파일은 로컬 저장 경로의 'Data/Logs/' 폴더에 저장됩니다.
public static void SaveLogs();
// 저장된 로그의 크기를 반환합니다.
public static int GetLogLength();
// 저장된 전체 로그를 반환합니다.
public static string GetLogString();
// 버퍼를 초기화합니다.
public static void ClearLogBuffer();
// 저장된 모든 로그 파일을 삭제합니다.
public static void RemoveAllLogFiles();
}
38.14.5. 创建调试日志文件¶
在使用Unity插件进行开发的过程中,若判断插件有问题,可通过如下所示方法保存日志, 并将文件和再现步骤通知给我们,我们将深表感谢。
在 Funapi/DebugUtils.cs 文件中激活以下define。
#define ENABLE_DEBUG
#define ENABLE_SAVE_LOG
在游戏结束时或再现情况发生后调用以下函数,保存日志。
// 로그를 파일로 저장
FunDebug.SaveLogs();
当在编辑器中运行时,通过这种方式保存的日志可在与项目文件夹的 Assets
所在文件夹
相同路径下的 Data/Logs
文件夹中查看。当在移动设备上进行测试时,在设备中访问App所在
文件夹是受到限制的,所以有可能难以访问文件。
38.15. Unreal Engine 4插件¶
Unreal Engine 4 plugin可通过 GitHub 获取。 如您需要包含加密功能的版本,请发送邮件至 Funapi support 。
38.15.1. 在Editor中运行¶
插件中包含了用于进行 FunapiSession
测试的简单示例代码。只有该actor类的
对象在Unreal编辑器中显示,tester
sample map才能稳定地Play。首次运行项目
时,编辑器中可能不显示funapi_tester对象。此时若编译一次项目,
即可创建类对象。如果已经编译但也仍未在编辑器中显示,则须要关闭Editor后
重新加载项目。如果 funapi_tester
对象在编辑器的Contents浏览器(C++ Class > funapi_plugin_ue4)中
显示,加载完 tester
map后按下Play键,即可运行sample程序。
38.15.2. 编译外部库¶
Unreal插件中可使用如下外部库。
libcurl |
使用HTTP通信中所需的必要功能。 |
libcrypto |
Openssl库中包含的加密库。使用了MD5。 |
libprotobuf |
Google protocol buffer库。 |
libsodium |
ChaCha20用于进行AES-128加密的库。 |
上述库的编译脚本和已编译的库文件已一同发布。在插件 文件夹中的 ThirdParty 文件夹下,Build脚本和库文件已按照不同平台进行了 区分。
ThirdParty
ㄴ build // 라이브러리 빌드 스크립트 파일들
ㄴ include // 헤더 파일들
ㄴ lib // 라이브러리 파일들
ㄴ proto // .proto 파일과 proto 파일용 컴파일 스크립트
直接通过已发布的编译脚本来构建库即可,除了须要 新构建库的特殊情况以外,建议原封不同地使用已发布的库文件。
38.15.3. 构建Protobuf文件¶
如果想构建 .proto
文件,运行 ThirdParty/proto/make-win.bat
文件或 make-mac.sh
文件即可。
如想要将 .proto
文件添加到Build列表中,打开批处理文件,在下方的文件列表中添加即可。运行批处理文件后,结果文件将在
Source
文件夹下的 funapi
文件夹中添加或修改。
38.16. Cocos2d-x插件¶
Cocos2d-x client plugin可通过 GitHub 获取。 如您需要包含加密功能的版本,请发送邮件至 Funapi support 。
38.17. 提交漏洞¶
Client plugin的建议事项或漏洞请发送邮件至 Funapi support 。 암호화가 포함된 버전이 필요하시면 Funapi support 로 요청 메일을 보내주십시오.
38.18. 버그 신고¶
Client plugin 에 대한 건의사항이나 버그 신고는 Funapi support 로 메일을 보내주십시오.