네트워크 프로그래밍
지난 시간에서 우리는 새로운 클래스인 Session을 만들어 소켓 프로그래밍의 Receive 부분과 Send 부분을 분리하여 작업을 진행했다. 오늘은 이어서 Send 부분의 코드 로직을 조금 더 우아하게 사용하기 위해 개선할 것이다.
Send 개선
class Session
{
Socket _socket;
int _disconnected = 0;
object _lock = new object();
Queue<byte[]> _sendQueue = new Queue<byte[]>();
List<ArraySegment<byte>> _pendinglist = new List<ArraySegment<byte>>();
SocketAsyncEventArgs _recvArgs = new SocketAsyncEventArgs();
SocketAsyncEventArgs _sendArgs = new SocketAsyncEventArgs();
public void Start(Socket socket)
{
_socket = socket;
_recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);
_recvArgs.SetBuffer(new byte[1024], 0, 1024);
_sendArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnSendCompleted);
RegisterRecv();
}
...
}
_sendArgs는 보낼 때 버퍼 사이즈가 정해지기 때문에 Receive 처럼 재사용할 수가 없다. 따라서 세션 클래스 내부에 멤버 변수로 변경해주어 사용한다. 이 때 C++ 서버 구조와 유사하기 맞추기 위해 _recvArgs 또한 같이 멤버 변수로 빼주어 사용한다. 이렇게 처리하게 되면 기존의 RegisterRecv() 메서드에서 매개 변수 사용하지 않고 처리할 수 있으며 내부 로직은 아래와 같이 변경하여 진행한다.
void RegisterRecv()
{
// 전역변수로 선언한 _recvArgs의 값을 이용한다.
bool pending = _socket.ReceiveAsync(_recvArgs);
if (pending == false)
{
OnRecvCompleted(null, _recvArgs);
}
}
List를 이용한 일괄처리
이전 시간에 작성한 RegisterSend() 내부 코드를 보면 SetBuffer 1개. 즉 버퍼라는 통짜 하나를 사용하는 방식으로 이루어져 있다. 그러나 BufferList() 메서드 기능을 이용하면 리스트에 버퍼 정보를 담아 전달해 줄 수 있다. 즉 Buffer를 한 번에 한 개, 또 다른 Buffer를 한 번에 한 개, 이런 방식으로 동작하던 로직을 리스트를 이용하여 한번에 슈루룽~ 보내는 것이다.
하지만 BufferList의 사용법은 일단 리스트와는 조금 다르다. 아래 이미지만 봐도 알 수 있듯이 배열의 일부분을 사용하는 것을 볼 수 있으며, 내부 인자 값으로는 사용하고자 하는 배열, 시작 인덱스, 길이를 설정해주어 사용자가 원하는 배열의 시작 위치, 길이만큼을 복사해서 사용할 수 있게 해준다.
즉, 아래와 같이 사용해야 한다.
// _sendArgs.BufferList.Add(new ArraySegment<byte>(buff, 0, buff.Length));
// ❌
_pendinglist.Add(new ArraySegment<byte>(buff, 0, buff.Length));
// ⭕
다만 위와같이 리스트에 직접적으로 데이터를 넣을 경우 문제가 생길 수 있으므로 List에 정보를 담고, 리스트를 또 버퍼 리스트에 담아서 사용해야 한다. 조금은 복잡한 편 😩.. 따라서 전체 코드는 아래와 같다.
List<ArraySegment<byte>> _pendinglist = new List<ArraySegment<byte>>();
void RegisterSend()
{
while (_sendQueue.Count > 0)
{
byte[] buff = _sendQueue.Dequeue();
_pendinglist.Add(new ArraySegment<byte>(buff, 0, buff.Length));
}
_sendArgs.BufferList = _pendinglist;
bool pending = _socket.SendAsync(_sendArgs);
if (pending == false)
{
OnSendCompleted(null, _sendArgs);
}
}
즉, 위에 설명한 것과 같이 리스트를 따로 제작하고... 해당 리스트에 Add를 통해 데이터를 넣어주고... 이 리스트를 또 버퍼리스트에 담아주는 일련의 과정을 거쳐야 한다. 보다 자세한 사용법은 아래 MSDN 링크를 참고한다. 😎
생성한 _pendinglist를 통한 pending 관리
이전까지는 _pending 이라는 bool 값을 전역 변수를 통해 관리하였다. 하지만 이제 리스트를 이용하여 해당 값을 처리하고 관리한다.
Send
public void Send(byte[] sendBuff)
{
lock (_lock)
{
_sendQueue.Enqueue(sendBuff);
if (_pendinglist.Count == 0)
{
RegisterSend();
}
}
}
카운트를 통해 로직을 처리한다.
RegisterSend
void RegisterSend()
{
while (_sendQueue.Count > 0)
{
byte[] buff = _sendQueue.Dequeue();
_pendinglist.Add(new ArraySegment<byte>(buff, 0, buff.Length));
}
_sendArgs.BufferList = _pendinglist;
bool pending = _socket.SendAsync(_sendArgs);
if (pending == false)
{
OnSendCompleted(null, _sendArgs);
}
}
일전 로직에서는 _pending의 값을 true 처리하여 작업을 진행하였지만, 이제 큐를 통해 처리를 진행한다. 👍
OnSendCompleted
void OnSendCompleted(object sender, SocketAsyncEventArgs args)
{
lock (_lock)
{
if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
{
try
{
_sendArgs.BufferList = null;
// 성공적으로 데이터를 모두 전송했으므로 리스트를 비워준다.
_pendinglist.Clear();
// 로그
Console.WriteLine($"Transferred bytes: {_sendArgs.BytesTransferred}");
if (_sendQueue.Count > 0)
{
RegisterSend();
}
}
catch (Exception e)
{
Console.WriteLine($"OnSendCompleted Fail {e}");
}
}
else
{
Disconnect();
}
}
}
마지막으로 성공적으로 데이터를 모두 전송했으면 리스트를 비워주고, 버퍼 리스트도 초기화한다. 끝! 😤.. 인줄 알았지만 또 개선할 부분이 필요하다. 🤯..
무엇을 또 개선해야할까?
첫째. 바로 패킷이 비정상적으로 몰리는 상황이다. 현재 RegisterSend 메서드에서 while (_sendQueue.Count > 0) 큐를 모두 비울 때 까지 정보를 보내고 있는데, 이는 Receive할 때와 마찬가지로 동일한 문제를 가지고 있다. 즉. 누군가 악의적으로 의미 없는 정보들을 마치 디도스 공격과 같이 계속 보낼 경우 큐에 있는 정보를 계속 비워낼 떄 까지 while 문이 돌기 때문에 문제가 과부하 문제가 생길 수 있다. 이럴 때는 Receive 할 때 체크하여 Disconnect 해주어야 한다.
둘째. 특정 상황에서는 패킷을 모아보내는 상황이다. 이전 포스팅에서 메이플스토리에 천 명의 유저가 몰려 있고, 움직이거나 스킬을 쓴다고 가정했는데, 만약 이 때 각 사용자들의 패킷을 모두 모아서 보내어 한 개씩 처리하게 되면 모든 작업을 하나, 하나 일련의 과정으로 처리하기 때문에 비 합리적이므로 처리가 된다. 따라서 이 때에는 천 명의 유저가 사용한 움직임, 스킬과 같은 정보를 어마어마하게 큰 1개의 버퍼를 만들어 패킷을 처리하는 것이 더 바람직하다.
'공부 > 인프런 - Rookiss' 카테고리의 다른 글
Part 4-3-9. 네트워크 프로그래밍 : Connector (1) | 2023.10.30 |
---|---|
Part 4-3-8. 네트워크 프로그래밍 : Session #4 (Event Handler) (0) | 2023.10.29 |
Part 4-3-6. 네트워크 프로그래밍 : Session #2 (Send 분리) (0) | 2023.09.22 |
Part 4-3-5. 네트워크 프로그래밍 : Session #1 (Receive 분리) (0) | 2023.09.21 |
Part 4-3-4. 네트워크 프로그래밍 : Listener (0) | 2023.09.21 |