게임개발_이야기/WalkieTalkie

WalkieTalkie - 0

조규현15 2023. 4. 16. 01:03
반응형

안녕하세요.

가제  ( WalkieTalkie ) 로 voip 가능한 chat program 을 구성해보려합니다.

 

구조는 dotnet 으로 서버를 구성하고 .NET MAUI 으로 클라이언트로 생각하고 있습니다.

 

첫 시작으로 server 를 만들겠는데요.

 

VS IDE 를 실행하고 dotnet console program 으로 프로젝트를 시작하면 됩니다.

 

Core 의 첫 단추는 socket 을 생성해야 합니다.

 

1. listen socket 을 만들어봅니다.

listen socket 은 외부의 connection 요청을 받아 줄 수 있는 수신기라 생각하면 됩니다.

 

인터넷을 통해 통신을 하기 위해서는 발신자가 수신자의 주소를 알아야 합니다.

현재는 ip 와 port 로 그 주소를 지칭할 수 있습니다.

ip 는 server 의 주소를 의미하고 port 는 server 가 받아 들일 수 있는 개별 주소라고 이해하면 됩니다.

( port 를 지칭하는 이유는 모든 connection 을 열어 둘 수 없기 때문에 특정 목적을 가진 요청을 구분하기 위함 입니다 )

이 주소 역시 private 하게 관리되어야 불필요한 요청 ( ddos ) 와 같은 불상사를 방지할 수 있습니다.

 

dotnet 에서 listen socket 나아가 socket 을 생성하는 방법은 매우 간단합니다.

 

물론 여기서 선택을 할 수 있습니다. 동기와 비동기로 또는 raw api 와 wrapping 된 api 무엇이든 큰 차이는 없습니다.

지금은 잘 만들어진 libray socket api 를 사용하고 기술적으로 더 자세한 parameter 설정이 필요한 경우 교체하면 됩니다.

그러기 위해서는 구조 설계 ( oop ) 를 잘 해두는 것이 좋겠네요.

 

저는 큰 모듈 ( service ) 를 두고 listen socket 을 만든 다음 클라이언트 개별 요청에 따라 socket 이 생성되면 session 을 만들어 binding 하고 connection 을 따로 관리할 생각입니다. 이 seesion 에서는 send, recv 를 할 수 있는 api ( method ) 를 열어두고 외부에서 이벤트를 핸들링하는 방향으로 진행하고자 합니다.

 

var listener = new TcpListener(IPAddress.Any, PORT);
listener.Start();

 

 

program 의 entry point ( main method ) 에서 위 listener 만 생성하고 종료하면 프로그램이 종료될테니 적절한 시점까지 프로그램이 동작하도록 loop 를 생성하겠습니다.

 

while (true)
{
    // 비동기 Accept                
    var tc = await listener.AcceptTcpClientAsync().ConfigureAwait(false);

    var session = new Session(tc);
    _sessions.Add(session);

    await session.AsyncReceiveMessage();
}

위 방식은 async 이긴한데 한번에 하나의 listen 요청만 받도록 되어있는 한계가 있습니다.

그래도 진행하는데 문제는 없습니다.

 

session 에는 클라이언트와 통신할 socket ( 이는 listen socket 과 다릅니다 ) 을 넘겨주고 sessions ( list ) 에 넣어서 관리합니다. ConfigureAwait(false) 를 붙였기 때문에 await 을 만나고 trigger 되는 시점에는 다른 task 가 별도로 생성되니 sessions 는 멀티쓰레딩이 가능한 구조로 접근해야 합니다. dotnet 에서는 concurrent 를 보장하는 DS 가 제공되니 적당한 DS 를 사용해도 되고, lock 을 사용해서 동시성만 보장해주면 됩니다.

 

이후에는 session 내부의 recv 처리를 위한 method 를 호출하는데 위와 같이 await 호출이후 리턴되는 task 를 적절하게 처리하거나 FireAndForget 을 호출하면 됩니다.

 

task 를 추가로 생성하여 코드를 작성하는 경우 항상 try-catch 를 잘 보장하여 unobserved exception 이 발생하지 않도록 주의해야 합니다.

( await 을 사용하면 task 내부에서 발생하는 exception 을 적절하게 상위 호출 스택에서 catch 할 수 있습니다 )

 

2. AsyncReceiveMessage 처리

recv 처리에는 많은 방법이 있습니다. 우선 통신하기 위한 적절한 프로토콜을 디자인할 수 있습니다.

현재는 별도의 프로토콜을 생성하지 않고 json 포맷으로 데이터를 주고 받고자 합니다.

 

recv 역시 끊임없이 처리되어야 하기 떄문에 loop 가 필요하겠네요.

 

while(true)
{
    var buffer = ArrayPool<Byte>.Shared.Rent(BUFFER_MAX_SIZE);

    var stream = _tc.GetStream();

    // 비동기 수신            
    var nbytes = await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
    if (nbytes > 0)
    {
        var msg = UTF8Encoding.GetString(buffer, 0, nbytes);

        // TODO:
    }

    stream.Close();

    ArrayPool<Byte>.Shared.Return(buffer);
}

 

socket 에서 recv buffer 에 채워진 데이터를 읽기 위해서는 많은 방법이 있겠지만 여기서는 dotnet 에서 제안하는 pool 을 사용하겠습니다. pool 은 매번 객체를 memory 를 할당하지 않고 미리 큰 규모의 memory 를 할당 받고, 사용할 때는 rent ( buffer size ) 를 받아 사용하고 다 쓴 다음에 return 하는 구조로 이해하면 됩니다.

 

rent 한 buffer 에 tc ( socket ) 의 stream 을 받아와 stream 내 데이터를 rent 된 buffer 에 copy 하면 됩니다.

socket 내부와 rent 된 buffer 의 크기는 적절하게 처리하면 되는데 여기서는 tcp 를 쓸 예정이라 buffer 가 가져온 Chunk 가 전송되는 데이터의 크기보다 작은 경우 여러번 read 가 필요할 수 있어 예외 처리가 필요합니다 ( TODO )
이 케이스는 이후 buffer 보다 큰 사이즈의 데이터를 전송하면서 발생할 때 다루도록 하겠습니다.

 

전송된 data 는 byte 이기 때문에 적절한 encoding 이 필요합니다. 저는 UTF8 을 사용할 것이고 저렇게 얻어진 string ( msg ) 는 이후 json 으로 parsing 하고 내부 시스템에 적절하게 event 를 발생하여 로직을 처리하면 됩니다.

 

recv 가 끝나면 후처리를 해주면 됩니다.

반응형