Neste protocolo, vou mostrar como você pode facilmente fazer uma conexão TCP entre Erlang e Unity3D (C #).
Este protocolo pressupõe que o leitor esteja familiarizado com os conceitos básicos do Erlang, como supervisor ou gen_server, bem como com os conceitos básicos de script do Unity em C #. Além disso, alguns códigos serão deixados de fora. Mostro apenas as partes necessárias.
Fundamentos
A coisa mais importante a entender aqui é como o protocolo TCP é projetado e como queremos (ab) usá-lo. O TCP é um protocolo de streaming, o que significa que, se enviarmos N bytes de dados, dependendo de muitas coisas, podemos recebê-los em um pacote ou em vários pacotes. Isso é lamentável para nós, já que normalmente queremos enviar uma mensagem (como mover o player para (X, Y)) e não um monte de bytes. O método que queremos usar é chamado de enquadramento. Isto é realmente simples: primeiro enviamos qual é o tamanho da mensagem (normalmente 1, 2 ou 4 bytes inteiros), e vem a mensagem real.
Como a mensagem média é bastante curta, vamos escolher 2 bytes para indicar o comprimento da mensagem.
Servidor
Supervisor
Vamos começar com a parte de Erlang. A chave aqui é usar o sistema de enquadramento integrado de Erlang.
Primeiro crie um supervisor. O método deve ser semelhante a este:init/1
init([]) ->
case gen_tcp:listen(Port, [{active, false}, binary, {packet, 2}]) of
{ok, Socket} -> {ok, SupervisorFlags, [ChildDefinition]};
{error, Error} -> {error, Error}
end.
Eu recomendo usar um simple_one_for_one
tipo de criança aqui. Além disso, certifique-se de que algo gere alguns filhos após o supervisor iniciar, para que haja processos realmente ouvindo a porta de rede.
A parte importante aqui é a opção:{packet, 2}
Os pacotes consistem em um cabeçalho que especifica o número de bytes no pacote, seguido por esse número de bytes. O comprimento do cabeçalho pode ser um, dois ou quatro bytes; contendo um inteiro sem sinal na ordem de bytes big-endian. Cada operação de envio gerará o cabeçalho, e o cabeçalho será retirado em cada operação de recebimento.
(veja: inet: setopts / 2 )
Isso é exatamente o que queremos. Observe a ordem de bytes big-endian, teremos que lidar com isso no código C # mais tarde.
Servidor TCP
Em segundo lugar, vem o processo do servidor TCP. Isso deve usar o gen_server
comportamento.
-record(state, {socket=nil}).
init(Socket) ->
gen_server:cast(self(), accept),
{ok, #state{socket=Socket}}.
handle_cast(accept, S = #state{socket=ListenSocket}) ->
{ok, AcceptSocket} = gen_tcp:accept(ListenSocket),
ok = inet:setopts(AcceptSocket, [{active, once}]),
{noreply, S#state{socket=AcceptSocket};
handle_cast({tcp, Socket, Data}, S = #state{socket=Socket}) ->
Reply = some_module:calculate_reply(Data),
gen_tcp:send(Socket, Reply),
inet:setopts(Socket, {active, once}),
{noreply, S}.
handle_info({tcp, Socket, Data}, State) ->
gen_server:cast(self(), {tcp, Socket, Data}),
{noreply, State};
handle_info({tcp_closed, _Socket}, State) ->
{stop, tcp_closed, State}.
É isso. Vamos ver o que está acontecendo aqui. Em e , basicamente redirecionamos as mensagens para , para fazer as coisas da maneira gen_server. Com ativamos o socket para receber uma mensagem.init/1
handle_info/2
handle_cast/2
inet:setopts/2
Cliente
O cliente C # tem duas classes. O NetworkController (que é um MonoBehaviour) e a Message, que é uma classe simples.
Dê uma olhada no NetworkController.
public class NetworkController : MonoBehaviour {
void Awake() {
DontDestroyOnLoad(this);
}
// Use this for initialization
void Start() {
startServer();
}
// Update is called once per frame
void Update() {
processMessage();
}
static TcpClient client = null;
static BinaryReader reader = null;
static BinaryWriter writer = null;
static Thread networkThread = null;
private static Queue<Message> messageQueue = new Queue<Message>();
static void addItemToQueue(Message item) {
lock(messageQueue) {
messageQueue.Enqueue(item);
}
}
static Message getItemFromQueue() {
lock(messageQueue) {
if (messageQueue.Count > 0) {
return messageQueue.Dequeue();
} else {
return null;
}
}
}
static void processMessage() {
Message msg = getItemFromQueue();
if (msg != null) {
// do some processing here, like update the player state
}
}
static void startServer() {
if (networkThread == null) {
connect();
networkThread = new Thread(() => {
while (reader != null) {
Message msg = Message.ReadFromStream(reader);
addItemToQueue(msg);
}
lock(networkThread) {
networkThread = null;
}
});
networkThread.Start();
}
}
static void connect() {
if (client == null) {
string server = "localhost";
int port = 12345;
client = new TcpClient(server, port);
Stream stream = client.GetStream();
reader = new BinaryReader(stream);
writer = new BinaryWriter(stream);
}
}
public static void send(Message msg) {
msg.WriteToStream(writer);
writer.Flush();
}
}
Esta classe faz várias coisas.
- O método garante que essa classe não seja destruída.
Awake()
- O método inicia a conexão TCP e define o fluxo de leitura e gravação. Como o protocolo usa dados binários (comprimento do quadro), é importante usar e aqui.
connect()
BinaryReader
BinaryWriter
- O método inicia um thread em segundo plano, que lê as mensagens e as coloca em uma fila de maneira segura para thread ( ).
startServer()
addItemToQueue()
- Em cada renderização de quadro, o método chama , o que retira da fila uma mensagem por vez (isso também é seguro para thread). Com esta técnica é possível usar threads de fundo reais no Unity.
Update()
processMessage()
- O método público envia uma mensagem. Isso provavelmente será chamado por outro controlador.
send()
E para colocar todas as peças juntas, aqui vem a classe Message:
public class Message {
public ushort length { get; set; }
public byte[] content { get; set; }
public static Message ReadFromStream(BinaryReader reader) {
ushort len;
byte[] len_buf;
byte[] buffer;
len_buf = reader.ReadBytes(2);
if (BitConverter.IsLittleEndian) {
Array.Reverse(len_buf);
}
len = BitConverter.ToUInt16(len_buf, 0);
buffer = reader.ReadBytes(len);
return new Message(buffer);
}
public void WriteToStream(BinaryWriter writer) {
byte[] len_bytes = BitConverter.GetBytes(length);
if (BitConverter.IsLittleEndian) {
Array.Reverse(len_bytes);
}
writer.Write(len_bytes);
writer.Write(content);
}
public Message(byte[] data) {
content = data;
}
}
Nada complicado está acontecendo aqui. Há um método estático que pode criar uma instância lendo a partir do fio e há um método que grava o conteúdo da mensagem no fluxo de saída. A única coisa a notar aqui é a conversão endian. Pode haver uma maneira melhor de fazer isso em C #, mas esta foi a primeira que encontrei e faz o trabalho.WriteToStream()
Espero que você goste desse post. Sinta-se a vontade para deixar comentários.