프로그래밍/C#, .NET

[.NET] 'SocketAsyncEventArgs'사용시 'System.InvalidOperationException' 에러

당근천국 2023. 6. 2. 15:30

'System.InvalidOperationException'에러는 이미 'SocketAsyncEventArgs'개체가 사용 중인 경우 발생합니다.

 

 

1. 원인 분석

이 라이브러리로 만든 프로그램이 몇 개인데 이런 에러가 있었다고??

라는 생각을 하며 클라이언트의 상세 로그를 봤더니.....

개발 중인 프로그램의 정보가 많이 들어가 있어 요만큼만 공개

 

강조된 로그가 3줄인데 첫 줄과 마지막 줄은 각각 다른 요청입니다.

 

첫 번째 줄은 주기적으로 서버에 보내는 내용이고

세 번째 줄은 사람이 직접 서버에 요청하는 내용입니다....

 

우연의 결과로 밀리초까지 일치해버린것입니다!!!!!!

같은 증상으로 죽어버린 다른 클라이언트의 로그도 동일합니다.

(아래 로그)

 

동시에 하나의 'SocketAsyncEventArgs' 개체를 사용하여 보내기(Send)를 하려고  해서 발생한 오류였던 것입니다.

 

 

나는 억울하다

데이터양이 적어서 'SocketAsyncEventArgs' 처리 중에 다음 요청이 올 확률이 거의  없는데도 이런 사례가 있는 거 보면

대수의 법칙은 정말 위대합니다 ㅎㅎㅎㅎ 

 

'SocketAsyncEventArgs' 관련 샘플에서도 언급이 없는 내용이라 좀 억울하네요 ㅎㅎㅎ

 

 

2. 해결 방법

이런 문제의 해결 방법은 결국 동일합니다.

큐를 만들어서 순차 처리를 하는 것이죠.

 

1) 큐를 만들어 놓고

2) '보내기(Send)'를 할 때 큐에 넣어두고

3) 큐에서 값을 하나씩 뽑아서

4) '보내기' 한 후 

5) 완료되면 '3)'부터 반복

하는 것입니다.

 

저는 아래와 같이 큐를 관리하는 클래스를 만들어 사용했습니다.

/// <summary>
/// SocketAsyncEventArgs가 사용중이라면 대기할 요청
/// </summary>
public class SendQueue
{
    /// <summary>
    /// 샌드 요청이 끝났는지 여부
    /// </summary>
    /// <remarks>
    /// 외부용이다.<br />
    /// 샌드 요청이 시작될때 true,
    /// 끝날때 false로 넣어준다.
    /// </remarks>
    public bool Used { get; set; } = false;

    /// <summary>
    /// 다음 요청이 들어있는 큐
    /// </summary>
    private Queue<byte[]> Send = new Queue<byte[]>();

    /// <summary>
    /// 큐에 남아있는 데이터 수
    /// </summary>
    public int Count 
    { 
        get
        { 
            return Send.Count; 
        }
    }

    /// <summary>
    /// 큐에 추가할 패턴. 완성된 패턴을 넣는다.
    /// </summary>
    /// <param name="byteSendData"></param>
    public void Add(byte[] byteSendData)
    {
        if (0 < byteSendData.Length)
        {//데이터가 있다.
            this.Send.Enqueue(byteSendData);
        }
    }

    /// <summary>
    /// 큐의 맨앞에 잇는 데이터를 추출한다. 없으면 byte[0]가 리턴됨
    /// </summary>
    /// <returns></returns>
    public byte[] Get()
    {
        byte[] byteReturn = new byte[0];

        if (0 < Send.Count)
        {
            byteReturn = this.Send.Dequeue();
        }

        return byteReturn;
    }
}

 

36번 줄 : '보내기' 요청을 하면 일단 이 함수를 호출하여 보낼 데이터를 큐에 저장해 둡니다.

 

48번 줄 : 큐에 남아있는 데이터 한 개를 추출하여 리턴합니다.

 

 

'SocketAsyncEventArgs' 요청이 완료되면

다시 큐에 남아있는 내용이 있는지 확인하고

남아있으면 바로 'Get()'를 호출한 데이터를 이용하여 '보내기' 작업을 해줍니다.

 

아래 코드는 예제 코드입니다.

/// <summary>
/// 전송 완료 이벤트 연결됨.
/// <para>'Send'에서 전송이 시작되면 이 곳에서 마무리 한다.</para>
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void SaeaSend_Completed(object sender, SocketAsyncEventArgs e)
{
    this.m_SendQueue.Used = false;
    if (0 < this.m_SendQueue.Count)
    {//큐에 남아있는게 있다

        //남은 요청 추출
        byte[] byteSendData = this.m_SendQueue.Get();
        if (0 < byteSendData.Length)
        {
            //남은 요청을 보낸다.
            this.Send(byteSendData);
        }
    }
}

 

 

마무리

완전 동일한 타이밍에 '보내기' 작업을 할 일이 많지 않아서 전혀 몰랐던 버그였습니다 ㅎㅎㅎㅎ

역시 공용으로 쓰려고 만든 라이브러리는 극한 상황에서 돌려보지 않으면 에러를 예측하기 힘드네요.