Unity 위에서 동작하는 Asynchronous Socket 통신

2021년 12월 16일
제작기간 2021년 12월 11일
태그 Unity

Reference 👉👉 .NET Code Example

  • Test 환경: macOS Monterey
  • Unity Editor Version: 2020.3.22f1
  • .NET SDK Version: 5.0.101

로컬에서 함께 즐길 수 있는 가벼운 게임을 만들어보고 싶어져서 dotnet이 제공하는 비동기 소켓 통신을 살펴보았다. 위의 링크에 자세한 코멘트와 함께 설명이 되어있어서 구현이 어렵지는 않았다.

AsynchronousSocketListener.cs

using System;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace Server
{
    public class StateObject
    {
        public const int BufferSize = 1024;
        public byte[] buffer = new byte[BufferSize];
        public StringBuilder sb = new StringBuilder();
        public Socket workSocket = null;
    }

    public class AsynchronousSocketListener : MonoBehaviour
    {
        private static GameManager _gameManager = null;
        private static ManualResetEvent allDone = new ManualResetEvent(false);
        public AsynchronousSocketListener()
        {
        }

        private static void StartListening()
        {
            // Domain Name System
            IPHostEntry ipHostInfo = Dns.GetHostEntry("ipAddress");
            IPAddress ipAddress = ipHostInfo.AddressList[0];
            IPEndPoint localEndPoint = new IPEndPoint(ipAddress, 11000);

            Socket listener = new Socket(ipAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

            try
            {
                listener.Bind(localEndPoint);
                listener.Listen(100);

                while (true)
                {
                    allDone.Reset();
                    Debug.Log("Waiting for a connection....");
                    listener.BeginAccept(new AsyncCallback(AcceptCallback), listener);

                    allDone.WaitOne();
                }
            }
            catch (Exception e)
            {
                Debug.Log((e.ToString()));
            }
            Debug.Log("Enter to continue");
        }

        private static void AcceptCallback(IAsyncResult ar)
        {
            Debug.Log("AcceptCallback");
            allDone.Set();

            Socket listener = (Socket)ar.AsyncState;
            Socket handler = listener.EndAccept(ar);

            Debug.Log("Set listener and handler");

            StateObject state = new StateObject();
            state.workSocket = handler;

            Debug.Log("Set StateObject");

            handler.BeginReceive(state.buffer, 0, StateObject.BufferSize, SocketFlags.None, new AsyncCallback(ReadCallback), state);
        }

        private static void ReadCallback(IAsyncResult ar)
        {
            Debug.Log("ReadCallback");
            String content = String.Empty;

            StateObject state = (StateObject)ar.AsyncState;
            Socket handler = state.workSocket;

            int bytesRead = handler.EndReceive(ar);

            if (bytesRead > 0)
            {
                // 데이터가 더 있을 수도 있기 때문에, 받은 데이터를 저장함
                state.sb.Append(Encoding.ASCII.GetString(state.buffer, 0, bytesRead));

                content = state.sb.ToString();
                if (content.IndexOf("<EOF>") > -1)
                {
                    // 모든 데이터를 읽어왔음
                    _gameManager.Receive(content.Replace("<EOF>", ""));

                    // Echo to client
                    Send(handler, content);
                }
                else
                {
                    // 아직 읽을 데이터가 남았기에 마저 Receive
                    handler.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0, new AsyncCallback(ReadCallback), state);
                }
            }
        }

        private static void Send(Socket handler, String data)
        {
            byte[] byteData = Encoding.ASCII.GetBytes(data);

            handler.BeginSend(byteData, 0, byteData.Length, SocketFlags.None, new AsyncCallback(SendCallback), handler);
        }

        private static void SendCallback(IAsyncResult ar)
        {
            try
            {
                Socket handler = (Socket)ar.AsyncState;

                int bytesSent = handler.EndSend(ar);
                Debug.LogFormat("Sent {0} bytes to client.", bytesSent);

                handler.Shutdown(SocketShutdown.Both);
                handler.Close();
            }
            catch (Exception e)
            {
                Debug.Log(e.ToString());
            }
        }

        public void ActivateServer(GameManager manager)
        {
            _gameManager = manager;
            Thread thread = new Thread(new ThreadStart(StartListening));
            thread.IsBackground = true;
            thread.Start();
        }
    }
}

ipAddress에는 사용 중인 네트워크의 ip 주소를 적었다.

AsynchronousClient.cs

using System;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using UnityEngine;

namespace Client
{
    public class StateObject
    {
        public const int BufferSize = 256;
        public byte[] buffer = new byte[BufferSize];
        public StringBuilder sb = new StringBuilder();
        public Socket workSocket = null;
    }

    public class AsynchronousClient : MonoBehaviour
    {
        private const int port = 11000;

        public static ManualResetEvent connectDone = new ManualResetEvent(false);
        public static ManualResetEvent sendDone = new ManualResetEvent(false);
        public static ManualResetEvent receiveDone = new ManualResetEvent(false);

        public static string response = String.Empty;

        private static void StartClient(string message)
        {
            connectDone.Reset();
            sendDone.Reset();
            receiveDone.Reset();

            try
            {
                IPHostEntry ipHostEntry = Dns.GetHostEntry("ipAddress");
                IPAddress ipAddress = ipHostEntry.AddressList[0];
                IPEndPoint remoteEP = new IPEndPoint(ipAddress, port);

                Socket client = new Socket(ipAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

                client.BeginConnect(remoteEP, new AsyncCallback(ConnectCallback), client);
                connectDone.WaitOne();

                Send(client, string.Format("{0} <EOF>", message));
                sendDone.WaitOne();

                Receive(client);
                receiveDone.WaitOne();

                client.Shutdown(SocketShutdown.Both);
                client.Close();
            }
            catch (Exception e)
            {
                Debug.Log(e.ToString());
            }
        }

        private static void ConnectCallback(IAsyncResult ar)
        {
            try
            {
                Socket client = (Socket)ar.AsyncState;

                client.EndConnect(ar);
                Debug.Log(string.Format("Socket connected to {0}", client.RemoteEndPoint.ToString()));

                connectDone.Set();
            }
            catch (Exception e)
            {
                Debug.Log(e.ToString());
            }
        }

        private static void Receive(Socket client)
        {
            try
            {
                StateObject state = new StateObject();
                state.workSocket = client;

                client.BeginReceive(state.buffer, 0, StateObject.BufferSize, SocketFlags.None, new AsyncCallback(ReceiveCallback), state);
            }
            catch (Exception e)
            {
                Debug.Log(e.ToString());
            }
        }

        private static void ReceiveCallback(IAsyncResult ar)
        {
            try
            {
                StateObject state = (StateObject)ar.AsyncState;
                Socket client = state.workSocket;

                int bytesRead = client.EndReceive(ar);

                if (bytesRead > 0)
                {
                    state.sb.Append(Encoding.ASCII.GetString(state.buffer), 0, bytesRead);

                    client.BeginReceive(state.buffer, 0, StateObject.BufferSize, SocketFlags.None, new AsyncCallback(ReceiveCallback), state);
                }
                else
                {
                    if (state.sb.Length > 1)
                        response = state.sb.ToString();

                    receiveDone.Set();
                }
            }
            catch (Exception e)
            {
                Debug.Log(e.ToString());
            }
            Debug.LogFormat("Client Get Response: {0}", response);
        }

        private static void Send(Socket client, String data)
        {
            byte[] bytesData = Encoding.ASCII.GetBytes(data);

            client.BeginSend(bytesData, 0, bytesData.Length, SocketFlags.None, new AsyncCallback(SendCallback), client);
        }

        private static void SendCallback(IAsyncResult ar)
        {
            try
            {
                Socket client = (Socket)ar.AsyncState;
                int bytesSent = client.EndSend(ar);

                Debug.Log(string.Format("Sent {0} bytes to server.", bytesSent));

                sendDone.Set();
            }
            catch (Exception e)
            {
                Debug.Log(e.ToString());
            }
        }

        Thread thread;
        public void ActivateClient()
        {
            thread = new Thread(new ThreadStart(() => StartClient("Activate")));
            thread.Start();
        }

        public void SendMessages(string message)
        {
            thread = new Thread(new ThreadStart(() => StartClient(message)));
            thread.Start();
        }
    }
}

유니티에서 사용하기 위해서 약간 수정하였지만, 제공된 Sample Code와 거의 동일하다. 모든 통신 작업은 게임의 플레이에 큰 영향을 주면 안되기 때문에 thread를 사용하여 처리하였다. 특히, Server의 경우, While문을 돌며 요청을 기다리기 때문에 thread를 이용하지 않는 경우 게임 진행이 불가하다.

Sample code는 1회용으로 제공되는 것인지 event 상태를 reset해주지 않아, 2회 이상 메세지를 보내면 오류가 발생한다.

connectDone.Reset();
sendDone.Reset();
receiveDone.Reset();

다시 정상적으로 thread를 차단하기 위해서 위와 같이 Reset을 해줄 필요가 있다.🤩

아래는 서버와 클라이언트를 호출하기 위해 작성한 GameManager의 Update문이다. 간편하게 처리하고 테스트하고 싶어서 커맨드를 받아 작동하게 하였다.


private void Update()
{
    if (this._message != String.Empty)
    {
        this._chatUIController.Receive(this._message);
        this._message = String.Empty;
    }

    if (Input.GetButtonUp("Submit"))
    {
        if (this._inputField.text.Equals("Server"))
        {
            this._server.ActivateServer(this);
        }
        else if (this._inputField.text.Equals("Client"))
        {
            this._client.ActivateClient();
        }
        else
        {
            this._client.SendMessages(this._inputField.text);
        }
        this._inputField.text = String.Empty;
    }
}

스크린샷

Client에서 보낸 메세지가 뜨는 모습