티스토리 뷰

하게 된 이유

- 프로젝트들에 앞서 기둥이 되는 서버와 클라이언트 네트워크 코드를 매번 직접 작성했었는데, 이번 기회에 기본이 되는 서버단을 구현해놓고 사용하기 위해서 C++ IOCP 서버와 Select 서버, C# 클라이언트 서버를 구성하였습니다. 그 와중에 이 파트는 C# 비동기 클라이언트 서버를 구축하는 이야기입니다.

먼저 들어가기에 앞서, 비동기란?

https://jiyun-hong.tistory.com/107

 

소켓 통신의 종류 - 동기, 비동기, 블록킹, 논 블록킹

소켓 통신 종류에는 물론 TCP와 UDP가 존재하지만, 그 두 가지에서도 여러 방향으로 나뉘게 된다. 크게 네 가지로 나뉘는데 먼저 동기인가 비동기인가, 두 번째로 블록킹이냐 논 블록킹이냐 두가

jiyun-hong.tistory.com

 

C#의 비동기 통신에는 여러 구조가 있는데 저는 여기서 APM 모델을 사용하여 구현하였습니다.

 

먼저 Connect의 경우, 사용할 소켓을 하나 만듭니다.(TCP), 그리고 소켓의 BeginConnect에 연결할 주소와, 콜백 함수, 소켓을 인자로 넘겨줍니다. 그런 후, Connect가 호출이 되면 소켓의 EndConnect로 비동기 호출을 종료해줍니다.

콜백함수의 형태는 void 함수이름(IAsyncResult result) 형태로 만들면 됩니다.

public void Start()
{
	IPEndPoint endPoint; // Address
    _socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    _socket.BeginConnect(endPoint, ConnectComplete, _socket);
}

void ConnectComplete(IAsyncResult result)
{
	// 비동기 대기 종료
    _socket.EndConnect(result);

    OnConnected();

    // 비동기 Recv 등록
    RegisterRecv(_recvBuffer.WriteSegment);
}

 

이후 Recv의 경우, 비슷하게 BeginReceive 함수를 통해 등록하고, 콜백 함수를 통해 호출이 되면 실행하게 됩니다.

socket.BeginReceive(bytes.Array, bytes.Offset, bytes.Count, SocketFlags.None, RecvEventComplete, _socket);

일단 보시면, ArraySegment<byte>를 인자로 받아 Array 주소와, Offset, count(크기)를 입력하고 flag와 콜백함수, 소켓을 넘겨주면 됩니다. 단 버퍼 인자는 다르게 생성해도 됩니다.

void RegisterRecv(ArraySegment<byte> bytes)
{
    if (_socket.Connected)
    {
    	// 비동기 Recv 등록
        _socket.BeginReceive(bytes.Array, bytes.Offset, bytes.Count, SocketFlags.None, RecvEventComplete, _socket);
    }
}

void RecvEventComplete(IAsyncResult result)
{
 	if (result.IsCompleted)
    {
        try
        {
            int size = _socket.EndReceive(result);
            // TODO
            
            // 비동기 재등록
            RegisterRecv(버퍼);
        }
        catch (SocketException e)
        {
            Console.WriteLine($"Recv Failed{e}");
        }
        catch (Exception e)
        {
            Console.WriteLine($"Recv Failed{e}");
        }
    }
    else
    {
        // Error TODO
    }
}

 

Send도 똑같이 작동합니다. 단 다른 점은 버퍼를 List형태로 만들어서 모아서 보내거나, byte[] 형식으로 한개의 버퍼만 보내거나 할 수 있습니다. 인자에 따라 다르게 작동하니 참고해주세요!

그리고 Lock을 쓰고 있는데, Lock을 쓰는 이유는 Send 특성 상, 한번만 호출이 되는 것이 아니다보니 여러 스레드가 동시에 Send를 호출할 수 있습니다. 그렇다 보니 Lock을 걸어서 SendBuffer에 동시에 접근하는 것을 막아주는 것입니다.

 

_socket.BeginSend(bytes, 0(offset), 10(count), SocketFlags.None, SendEventComplete, _socket);

void RegisterSend()
{
    if (_socket.Connected)
    {
    	// TODO 데이터 저장
        byte[] bytes = new byte[10];
        
        // 비동기 등록
        _socket.BeginSend(bytes, 0, 10, SocketFlags.None, SendEventComplete, _socket);
    }
}

void SendEventComplete(IAsyncResult result)
{
    lock (_lock)
    {
        if (result.IsCompleted)
        {
            try
            {
                int size = _socket.EndSend(result);

                // TODO

            }
            catch (SocketException e)
            {
                Console.WriteLine($"Send Failed{e}");
            }
            catch (Exception e)
            {
                Console.WriteLine($"Send Failed{e}");
            }
        }
        else
        {
            //TODO
        }
    }
}

 

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
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
글 보관함