using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http.Headers;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace ServerCore
{
internal class Session
{
Socket _socket;
int _disconnectd = 0;
// Aysnc 횟수가 늘어날수록 시간이 오래걸리므로(부하가 큼) Queue를 통해 쌓아놨다가 한번에 처리
object _obj = new object();
Queue<byte[]> _sendQueue = new Queue<byte[]>();
// send를 처리하고 있는 중(_pending == true)이라면 큐에 넣음
bool _pending = false;
// send 할때마다 _sendArgs를 만들면 매우 부담이 높기에 따로 뺌(재사용)
SocketAsyncEventArgs _sendArgs = new SocketAsyncEventArgs();
public void Start(Socket socket)
{
_socket = socket;
SocketAsyncEventArgs recvArgs = new SocketAsyncEventArgs();
recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnReceiveCompleted);
// Accept는 받으면 받은 socket 정보가 args에 남는데
// recv는 없어서 이렇게 따로 설정해주는듯
recvArgs.SetBuffer(new byte[1024], 0, 1024);
_sendArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnSendCompleted);
RegisterReceive(recvArgs);
}
public void Send(byte[] sendBuff)
{
// 여러 스레드에서 동시에 호출 될 수 있으므로 lock 걺
lock (_obj)
{
_sendQueue.Enqueue(sendBuff);
// 이미 send가 진행 중이라면 RegisterSend 하지 않음
if (_pending == false)
RegisterSend();
}
}
public void Disconnect()
{
if (Interlocked.Exchange(ref _disconnectd, 1) == 1)
return;
_socket.Shutdown(SocketShutdown.Both);
_socket.Close();
}
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);
}
void OnSendCompleted(object sender, SocketAsyncEventArgs args)
{
lock (_obj)
{
if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
{
// SendAysnc가 처리되는 사이에 또 다른 Send()가 있었다면
// 다시 RegisterSend() 실행
// 만약 도중에 Send가 두번 들어와서 큐가 두번 찬다고 하더라도
// 처음 들어온걸 처리한 후 다시 이 함수로 돌아왔을 때
// Queue Count가 0보다 크므로 다시 RegisterSend() 실행될 것
// _pending이 false라면 즉각 SendAysnc로 처리되나
// _peding이 true라면 Queue에 모여있다가
// send가 완료된 후 if문에 들어와서 차례차례 처리되고
// _pending이 false로 바뀌면서 초기상태로 돌아간다.
// Recv는 RegisterReceive 계속 실행시켜서 recv를 기다리지만
// Send는 딱히 들어오는 걸 기다리는 건 아니니
// Send() 함수가 실행되면 처리가 시작됨
if (_sendQueue.Count > 0)
RegisterSend();
else
_pending = false;
}
else
{
Disconnect();
}
}
}
#region 네트워크 통신
void RegisterReceive(SocketAsyncEventArgs args)
{
bool pending = _socket.ReceiveAsync(args);
if (pending == false)
OnReceiveCompleted(null, args);
}
void OnReceiveCompleted(object sender, SocketAsyncEventArgs args)
{
// BytesTransferred -> 몇 byte를 전송 받았는가
// 0인 경우 상대방이 연결을 끊은 것
if(args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
{
string recvData = Encoding.UTF8.GetString(args.Buffer, args.Offset, args.BytesTransferred);
Console.WriteLine($"[From Data] {recvData}");
// accept와 다르게 if문 밖에서 호출하지 않는 이유는
// accpet는 연결 안되면 걍 버리고 다른 연결 기다리면 되는데
// 얘는 연결 상태로 에러 난거니까 다시 receive하면 안됨
RegisterReceive(args);
}
else
{
Disconnect();
}
}
#endregion
}
}
나머지 코드는 동일하므로 생략
Queue의 사용 이유와 Send, Recv시 스레드의 생성과 소멸 관계에 대해 더 많은 이해가 필요함
-> 왜 lock을 거는지에 대한 이해도가 부족.
Queue 사용에 대한 gpt 답변
좋은 질문입니다. 여기서 Queue를 사용하는 주된 이유는 비동기 전송의 안정성과 효율성을 보장하기 위함입니다. 만약 Queue가 없다면, Send()가 여러 번 호출될 때 모든 데이터가 한꺼번에 전송되지 않고, 각각의 SendAsync 호출이 중첩될 위험이 있기 때문입니다. Queue는 이런 문제를 해결해 주며, 중요한 역할을 합니다.
Queue를 사용하는 의미는 다음과 같습니다:
- 비동기 전송의 순서 보장: 큐는 FIFO(First-In-First-Out) 방식으로 동작하므로, Send()가 여러 번 호출되어도 데이터가 전송된 순서를 보장할 수 있습니다. 각 데이터가 입력된 순서대로 정확하게 처리되기 때문에 전송 순서의 일관성을 유지할 수 있습니다.
- 과도한 SendAsync 호출 방지: Queue와 _pending 플래그는 SendAsync 호출을 제어하는 데 사용됩니다. SendAsync가 하나의 전송을 처리하는 동안 추가적인 SendAsync 호출이 발생하는 것을 방지하기 위해 _pending 플래그가 true일 때는 새로운 전송을 시작하지 않고 데이터를 큐에 저장해 둡니다. 이렇게 하면 동시에 다수의 SendAsync 호출로 인한 리소스 낭비를 줄일 수 있습니다.
- 비동기 전송의 안정성: 비동기 환경에서는 Send()가 여러 스레드에서 동시에 호출될 수 있습니다. 이때, Queue와 lock을 이용하면 여러 스레드가 전송 요청을 하더라도 동기화된 방식으로 데이터를 순차 처리할 수 있어 안정성을 높일 수 있습니다.
따라서, Queue를 사용하는 것은 전송 순서와 전송 안정성을 유지하면서, 과도한 SendAsync 호출로 인한 성능 저하를 막기 위한 핵심적인 방법입니다.
SendAsync가 중첩될 경우 발생할 수 있는 주요 위험은 전송 데이터의 순서가 엉킬 가능성과 리소스 낭비로 인한 성능 저하입니다. 비동기 소켓 통신에서 이런 상황은 시스템의 동작에 큰 문제를 야기할 수 있습니다. 자세히 설명하면 다음과 같습니다:
- 전송 순서의 문제:
- SendAsync 호출이 중첩되어 동시에 실행되면, 각 호출의 완료 시점이 달라 데이터가 전송된 순서가 엉킬 수 있습니다. 이 경우 클라이언트는 데이터를 원래 순서대로 받지 못할 수 있으며, 전송 순서가 중요한 데이터를 다루는 경우 큰 문제가 됩니다.
- TCP 프로토콜의 경우, 패킷 순서가 자동으로 보장되지만, 응용 계층에서는 여전히 올바른 순서로 SendAsync를 호출하는 것이 중요합니다. 중첩 호출로 순서가 엉키면, 응용 프로그램에서 데이터 무결성을 유지하기 어렵습니다.
- 불필요한 리소스 낭비:
- 중첩된 SendAsync 호출은 각기 다른 스레드에서 실행되며, 동일한 네트워크 소켓을 두고 경쟁하게 됩니다. 이는 CPU와 메모리 리소스를 과도하게 소모하여 성능 저하를 초래할 수 있습니다.
- SocketAsyncEventArgs 객체는 비교적 무거운 객체라서 많은 수의 SendAsync가 중첩될 경우 가비지 컬렉션이 늘어나고 시스템 성능이 급격히 저하될 수 있습니다.
- 경합으로 인한 데이터 손실 위험:
- 중첩 호출이 동기화되지 않으면, 다른 스레드가 동일한 소켓에서 동시에 전송을 시도하게 되어 충돌이 발생할 수 있습니다. 특히 멀티스레드 환경에서 SocketAsyncEventArgs 버퍼가 올바르게 설정되지 않거나, 예상치 못한 오류가 발생하면 데이터 손실로 이어질 위험이 있습니다.
- 전송 실패 및 오류 증가:
- SocketAsyncEventArgs가 제대로 설정되지 않은 상태에서 여러 SendAsync 호출이 충돌하면 SocketError가 증가할 가능성이 큽니다. 이는 연결이 끊어지거나 전송이 실패하는 상황을 야기할 수 있습니다.
이러한 위험을 막기 위해 _pending 플래그와 Queue를 사용하여 중첩된 SendAsync 호출을 방지하고, 이전 전송이 완료된 후에 다음 전송이 시작되도록 순차적 처리 방식을 사용합니다.
위 Queue 관련 질문도 사실 SendAysnc가 중첩되는 경우
즉, 멀티스레드 환경과 관련이 있는데 이에 대한 이해를 좀 더 진행하여야 정확히 알 수 있을 듯
'C# > 네트워크 관련' 카테고리의 다른 글
Connector 및 기타 (0) | 2024.11.09 |
---|---|
Session #5 (send 방식 수정) (0) | 2024.10.27 |
Socket 연결 실습 - 3 (2) | 2024.10.05 |
Socket 연결 실습 - 2 (0) | 2024.10.01 |
Socket 연결 실습 (0) | 2024.09.29 |