본문 바로가기
cs공부/네트워크프로그래밍

네트워크 프로그래밍 - C# Non - blocking을 사용한 Server 소켓프로그래밍의 Send

by 코딩하는 돼징 2023. 7. 7.
반응형

Receive코드 알아보러 가기

 

네트워크 프로그래밍 - C# Non - blocking을 사용한 Server 소켓프로그래밍의 Receive

Listener코드 알아보러 가기 네트워크프로그래밍 - C# 간단한 Non - blocking 사용한 Server 소켓프로그래밍 구현 blocking방식 이용한 Server코드 네트워크 프로그래밍 - C# 간단한 Server 소켓프로그래밍 구현

code-piggy.tistory.com


void RegisterSend(SocketAsyncEventArgs args)
{
    bool pending = _socket.SendAsync(args);
    if (pending == false)
        OnSendCompleted(null, args);
}
void OnSendCompleted(object sender, SocketAsyncEventArgs args)
{
    if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
    {
        try
        {
                    
        }

        catch (Exception e)
        {
            Console.WriteLine($"OnSendCompleted Faield {e}");
        }
     }
    else
    {
        Disconnect();
    }
}

코드의 틀은 Receive와 비슷하다.하지만 Send하는 시점은 정해져 있지 않다.

 

Receive같은 경우에는 Start할때 한 번만 예약 해놓고 Client측에서 메시지를 전송하면은 Receive가 완료되면서 OnRecvCompleted가 자동으로 호출된다.

public void Start(Socket socket)
{
    RegisterRecv(recvArgs);
}

Send같은 경우에는 보내줄 버퍼와 메시지를 같이 설정해주어야하는데 미래에 어떤 메시지를 보낼지 알고 예약을 할 수가 없으니 불가능하다.

 

그러므로 Receive와 다르게 접근해야 한다.


원래 있었던 Send부분을 수정하자

원래 있었던 Send코드

public void Send(byte[] sendBuff)
{
   _socket.Send(sendBuff);
}

01 Send안에서 RegisterSend을 실행하도록 수정

public void Send(byte[] sendBuff)
{ 
    SocketAsyncEventArgs sendArgs = new SocketAsyncEventArgs();
    sendArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnSendCompleted);
    sendArgs.SetBuffer(sendBuff,0,sendBuff.Length);
    RegisterSend(sendArgs);
 }

Receive와 비교를 해보자면 OnRecvCompleted이 호출이 되면 RegisterRecv(args)를 통해 다시 등록을 해주었다..

하지만 Send같은 경우 OnSendCompleted이 호출되고 다시 등록하는게 불필요한데 그 이유는 Send에서 sendBuff(보내고 싶은 정보)를 보내게 되면 똑같은 정보를 다시 보내게 되는 것이기 때문이다.  그러므로 재사용이 불가능하다.


문제1 이벤트가 생길때마다 다시 만들면서 보내고있으니 재사용 불가 상태

SocketAsyncEventArgs sendArgs = new SocketAsyncEventArgs();
sendArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnSendCompleted);
sendArgs.SetBuffer(sendBuff,0,sendBuff.Length);

멀티쓰레드 환경에서 동시다발적으로 send를 한다고 가정하면 SendAysnc함수가 아파지는것은 아닌데 재사용을 할 수 없다는 문제가 발생한다.

void OnSendCompleted(object sender, SocketAsyncEventArgs args)
{
    if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
    {
        try
        {
                    
        }
    }
}

일반적으로 OnSendCompleted가 됐다고 해서  receive 같은 경우 따로 등록을 해줬는데 Send 같은 경우 성공한 경우에는 사실 설정할 이벤트가 마땅히 없다.


문제2 RegisterSend를 매번 다시하고 있다.

RegisterSend(sendArgs);

만약에 send를 1000명의 유저가 있다고 하면 어떤 A라는 유저가 움직였다고하면 주변의 모든 유저에게 loop를 돌면서 모든 유저에게 A라는 유저가 움직였다는 정보를 보내준다. 더 큰 문제는 A유저만 움직이는 것이 아니라 모든 유저가 움직이므로 Send가 호출되는것이 되게 많아질 것이다.

그러면 SendAsync가 매번 호출될텐데 그러면 네트워크 과부하가 걸릴것이다. 운영체제가 커널에서 처리하기 때문에 막 쓰는것이 큰 문제가 된다.


이를 해결하기위해 매번 보내는 것이아니라 뭉쳐서 보내는 방법은 없을까?

 

해결책1 이벤트 핸들러 재사용

Session 클래스 내에서 이벤트 핸들러를 등록하면 여러 번 Send메서드를 호출할 때마다 매번 이벤트를 등록할 필요가 없어진다. 그러므로 재사용할 수 있게 만들 수 있다.

class Session
{
    Socket _socket;
    int _disconnected = 0;
    SocketAsyncEventArgs _sendArgs = new SocketAsyncEventArgs();
}

public void Start(Socket socket)
{
    _sendArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnSendCompleted);
    RegisterSend();
}
          
void RegisterSend()
{
    bool pending = _socket.SendAsync(_sendArgs);
    if (pending == false)
        OnSendCompleted(null, _sendArgs);
}

_sendArgs변수가 이미 Session클래스의 멤버로 선어되었기 때문에 RegisterSend에서 별도의 매개변수가 필요하지 않게 되었다.


해결책2 queue에 차곡차곡 모아서 어느 정도 모았으면 그때 보내기

01 Queue만들기

Queue<byte[]> _sendQueue = new Queue<byte[]>();
bool _pending = false;

pending이 false인 경우

Queue에 아무것도 등록이 되지 않은 상태

pending이 true인 경우

Queue에 등록이 되어있는 상태


02 Quene에 등록하기

싱글쓰레드 버젼

public void Send(byte[] sendBuff)
        {
            _sendQueue.Enqueue(sendBuff);
            if (_pending == false)
                RegisterSend();
        }

멀티쓰레드 버젼

lock을 이용해 한번에 한번씩만 들어오게 하기

object _lock = new object();
public void Send(byte[] sendBuff)
{
    lock (_lock)
    {
        _sendQueue.Enqueue(sendBuff);
        if (_pending == false)
            RegisterSend();
    }
}

03 Queue에 순차적으로 담고 다 찼으면 OnSendCompleted 호출하기

void RegisterSend()
{
    _pending = true; 
    byte[] buff = _sendQueue.Dequeue();
    _sendArgs.SetBuffer(buff, 0, buff.Length);
    bool pending = _socket.SendAsync(_sendArgs);
    if (pending == false)
        OnSendCompleted(null, _sendArgs);
}

1) 다음에 Send되는애가 Register안돼고 바로 queue에 담겨지기 위해서 _pending = true로 설정한다.

_pending = true;

 

2) _sendQueue에서 큐에서 첫 번째로 들어온 데이터를 가져온다.

byte[] buff = _sendQueue.Dequeue();

3) _sendArgs에 전송할 데이터의 정보 설정

_sendArgs.SetBuffer(buff, 0, buff.Length);

4) 작업의 완료 여부를 pending에 저장

bool pending = _socket.SendAsync(_sendArgs);
if (pending == false)
    OnSendCompleted(null, _sendArgs);

Send.Async메서드가 호출되면 비동기작업이 시작된다.

pending이 true인 경우

작업이 완료되지 않아서 현재 대기 중인 상태

pending이 false인 경우

작업이 즉시 완료된 상태


04 전송완료결과 처리

void OnSendCompleted(object sender, SocketAsyncEventArgs args)
{
    lock (_lock)
    {
        if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
        {
            try
            {
                if(_sendQueue.Count > 0)
                {
                    RegisterSend();
                }
                else
                    _pending = false;
            }

            catch (Exception e)
            {
                Console.WriteLine($"OnSendCompleted Faield {e}");
            }
        }
        else
        {
            Disconnect();
        }
    }
}

Queue에 보내야할 데이터가 남아 있는 경우

if(_sendQueue.Count > 0)
{
    RegisterSend(); // 다음 전송을 위해서 RegisterSend을 호출한다
}

Queue에 보내야할 데이터가 남아 있지 않은 경우

else
    _pending = false; // 작업이 완료되었음을 나타낸다.

개선된 Send코드 보러가기

 

네트워크 프로그래밍 -Send코드 개선하기

Send 코드 참조 네트워크 프로그래밍 - C# Non - blocking을 사용한 Server 소켓프로그래밍의 Send Receive코드 알아보러 가기 네트워크 프로그래밍 - C# Non - blocking을 사용한 Server 소켓프로그래밍의 Receive Li

code-piggy.tistory.com

 

 

 

 

 

 

참고 :  본 내용은 MMORPG  PART4 강의를 수강하여 작성하였습니다.

https://www.inflearn.com/course/%EC%9C%A0%EB%8B%88%ED%8B%B0-mmorpg-%EA%B0%9C%EB%B0%9C-part4

 

 

 

 

 

 

반응형

댓글