Q1. 개선을 위해 작업한 내용들을 나열해 보세요
Q2. 더 개선할 점은 뭐가 있을까?
답
1.
recvArgs도 _sendArgs처럼 Session클래스로 옮기고 인터페이스를 맞춰 준다. 그로 인한 변화를 수정해 준다.
한번에 많은 _sendArgs를 보내기 위해 SetBuffer대신 BufferList를 사용한다.
List<ArraySegment<byte>> _pendingList = new List<ArraySegment<byte>>();를 class Session에 만들어 주고, RegisterSend에서 _pendingList를 Clear해준 다음, while문으로 _sendQueue.Count가 0보다 클동안 _sendQueue를 Dequeue해서 하나씩 buff에 넣어주고, 그걸 _pendingList에 하나하나 Add(new ArraySegment<byte>()인터페이스로 넣어준 후, 마지막에 _sendArgs.BufferList에 _pendingList를 대입해준다.
-> Send에서 _pending 대신 _pendingList.Count로 대기중인 정보가 있는지 판별하게 한다.
-> OnSendCompleted에서 try 부분에 _sendArgs.BufferList를 null로 해주고, _pendingList도 Clear 해주고, _sendArgs.BytesTransferred의 값을 로그로 찍어서 전송된 바이트 수를 출력해준다.
->RegisterSend에서 _pending = true를 삭제해 준다. class Session의 _pending 선언도 삭제. RegisterSend의 _pendingList.Clear도 삭제해준다.
2.
- 일정한 시간 동안 몇 byte를 보냈는지를 추적해서 너무 심하게 많이 보낸다 싶으면 쉬면서 하게 한다. 악의적으로 많이 보낸다 싶으면 Disconnect 하는 식으로 관리한다
- 존에 있는 모든 행동들을 기록해서 한번에 보내는 방식도 컨텐츠 작업할 때 추가할 수 있다
요약: 네트워크 프로그래밍 세션에서 다룬 내용 중 패킷을 모아서 한번에 보내는 방식으로 개선하는 과정을 다루고 있다. 이를 위해 예약된 패킷들을 _pendingList에 넣어두고, _SendArgs.BufferList에 연결한 다음, 한번에 SendAsync로 보내는 방식을 사용하였다.
이전 시간 Session 코드
using System;
using System.Collections.Generic;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace ServerCore
{
class Session
{
Socket _socket;
int _disconnect = 0;
object _lock = new object();
Queue<byte[]> _sendQueue = new Queue<byte[]>();
bool _pending = false;
SocketAsyncEventArgs _sendArgs = new SocketAsyncEventArgs();
public void Start(Socket socket)
{
_socket = socket;
SocketAsyncEventArgs recvArgs = new SocketAsyncEventArgs();
recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);
recvArgs.SetBuffer(new byte[1024], 0, 1024);
_sendArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnSendCompleted);
RegisterRecv(recvArgs);
}
public void Send(byte[] sendBuff)
{
lock (_lock)
{
_sendQueue.Enqueue(sendBuff);
if (_pending == false)
RegisterSend();
//_sendArgs.SetBuffer(sendBuff, 0, sendBuff.Length);
}
}
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 (_lock)
{
if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
{
try
{
if (_sendQueue.Count > 0)
RegisterSend();
else
_pending = false;
}
catch (Exception e)
{
Console.WriteLine($"OnRecvCompletedFailed {e}");
}
}
else
{
Disconnect();
}
}
}
public void Disconnect()
{
if (Interlocked.Exchange(ref _disconnect, 1) == 1)
return;
_socket.Shutdown(SocketShutdown.Both);
_socket.Close();
}
#region 네트워크 통신
void RegisterRecv(SocketAsyncEventArgs args)
{
bool pending = _socket.ReceiveAsync(args);
if (pending == false)
OnRecvCompleted(null, args);
}
void OnRecvCompleted(object sender, SocketAsyncEventArgs args)
{
if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success) // 모든 게 잘 처리 됐다는 뜻
{
// TODO
try
{
string recvData = Encoding.UTF8.GetString(args.Buffer, args.Offset, args.BytesTransferred);
Console.WriteLine($"[FromClient] {recvData}");
RegisterRecv(args);
}
catch(Exception e)
{
Console.WriteLine($"OnRecvCompletedFailed {e}");
}
}
else
{
Disconnect();
}
}
#endregion
}
}
지난 시간에 했던 것 중에 recieveArgs, sendArgs를 사용하는게 중요했어. C++ 서버 만들 때도 비슷하게 만들게 될거야.
_recvArgs와 _sendArgs간에 SetBuffer하는 거 차이가 있다.
public void Start(Socket socket)
{
_socket = socket;
_recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);
**_recvArgs.SetBuffer(new byte[1024], 0, 1024);**
recieveArg는 빈 버퍼로 연결만 해 주었다. 나중에 클라이언트 쪽에서 data를 쏴주면 그 data가 buffer에 저장이 될 거야.
반대로 send하는 경우는 달랐어. 같은 인터페이스인 SetBuffer를 하긴 하는데 보낼 데이터가 있는 버퍼에 길이를 입력해서 연결해 줬다.
void RegisterSend()
{
_pending = true;
byte[] buff = _sendQueue.Dequeue();
**_sendArgs.SetBuffer(buff, 0, buff.Length);**
이게 중요했다.
_sendArgs는 재사용을 위해 class Session에 선언하고 저장하고 있었다.
_recvArgs 의 경우도 Session 클래스로 옮겨도 상관 없다. 그렇게 하면 C++서버랑 인터페이스를 비슷하게 맞춰주기 위해 이렇게 하는 편이야.
class Session
{
Socket _socket;
int _disconnect = 0;
object _lock = new object();
Queue<byte[]> _sendQueue = new Queue<byte[]>();
bool _pending = false;
SocketAsyncEventArgs _sendArgs = new SocketAsyncEventArgs();
**SocketAsyncEventArgs _recvArgs = new SocketAsyncEventArgs();**
RegisterSend() 할 때 argument를 안받았는데 RegisterRecv도 argument 안받게 인터페이스 통일해도 마찬가지로 잘 작동한다.
void RegisterRecv()
{
bool pending = _socket.ReceiveAsync(_recvArgs);
if (pending == false)
OnRecvCompleted(null, _recvArgs);
}
일단은
SocketAsyncEventArgs _sendArgs = new SocketAsyncEventArgs();
SocketAsyncEventArgs _recvArgs = new SocketAsyncEventArgs();
이렇게 하나씩 들고 있게 했어.
_sendArgs는 계속 얘를 재사용 하면서 실제로 완료 될 때 까지는 _sendQueue에다가 넣어주고, Send가 완료가 됐으면 _sendQueue를 확인해서 만약에 그 동안에 보내야 할 정보를 넣어 줬으면 걔를 이어서 처리하는 방식까지 지난 시간에 해봤다.
여기까지 해도 되긴 된다. 이어서 서버를 구현해도 된다.
경우에 따라 더 최적화를 하자면 RegisterSend를 보면
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);
}
지금은 Dequeue를 한번씩 해가지고 sendQueue에 있는 정보 하나당 SendAsync를 한번씩 해주고 있는데 조금 더 우아한 인터페이스가 있어. _sendArgs에 .을 찍으서 살펴보면 지금 사용하는 SetBuffer를 하면 Buffer를 채워주는 애고 하나짜리를 사용하는 애는데 아래에 보면
_sendArgs.BufferList라는 애가 있어. SetBuffer와 똑같은 기능을 하는데 보낼 정보들을 리스트로 쭉 _sendArgs에 넣어줘서 연결을 해주면, 그 상태에서 SendAsync해주면 리스트에 연결되어 있는 모든 애들을 다 한번에 쫙 보내주는 그런 기능을 한다.
그래서 이렇게 한번 한번씩 SetBuffer를 해서 보내기 보다는 BufferList를 만들어서 한번에 보내면 조금 더 효율적이라는 생각이 들거야.
조심해야 할 점은 SetBuffer와 BufferList 둘 다 동시에 세팅하면 에러난다. 둘중 하나만 골라서 사용해야 한다는 의미.
이 부분을 수정해 보도록 할거야.
Dequeue를 하는 부분을 수정해야 하는데
void RegisterSend()
{
_pending = true;
**while(_sendQueue.Count > 0)
{**
**byte[] buff = _sendQueue.Dequeue();
_sendArgs.BufferList.Add(new ArraySegment<byte>(buff, 0, buff.Length));
}**
bool pending = _socket.SendAsync(_sendArgs);
if (pending == false)
OnSendCompleted(null, _sendArgs);
}
_sendArgs.BufferList.Add(buff, 0, buff.Length);이렇게 하면 안된다. **new ArraySegment<byte>**로 인터페이스를 맞춰 줘야 한다.
ArraySegment즉 array의 일부라는 표현을 하는 거고,
실제로 얘가 받는 인자를 보면 어떤 배열의 일부를 나타내는 구조체임을 알 수 있어. f12로 들어가서 보면 class가 아니 struct로 되어 있어. heap 영이 아니라 stack 영역에 할당 되어서 실제로 Add를 할 때는 값이 복사되는 형태로 작동을 하고, 왜 굳이 사용하냐면 c#같은 경우는 배열 사용할 때
[ ][ ][ ][ ][ ][ ][ ][ ][ ][ ]
10바이트의 경우 C++의경우 5번째 지점부터 3개를 사용할 거라고 넘겨줄 경우 포인터를 이용해서 시작 주소를 옮겨서 건내주면 된다.
C#의 경우는 정상적인 상황이면 포인터를 사용할 수 없으니까 무조건 첫 주소만 알 수 있다. 그게 buff 라는 주소, 그래서 이렇게 인덱스를 넘겨줘서 몇 번째 부터 시작하는지 따로 넘겨준거.
5번 인덱스 부터 사용하고 싶으면 5를 넘겨주는 거, 몇개 사용할지 Length로 넘겨주는 거.
기본적으로 어떤 버퍼의 범위를 나타내고 싶으면 이렇게 배열, 시작 인덱스, 크기를 3종 세트로 넘겨주게 된다.
조심해야 할 건 리스트에다가 Add를 해서 넘겨주면안되고, 리스트를 일단 만들어 준 다음에 마지막으로 BufferList에다가 이퀄(=)을 해서 넘겨줘야 한다.
_sendArgs.BufferList **=** list;
이건 딱히 이유는 없고 smdn 문서에도 안나와 있지만 BufferList를 만들 때 이렇게 만들어져 놨어.
void RegisterSend()
{
_pending = true;
**List<ArraySegment<byte>> list = new List<ArraySegment<byte>>();
while(_sendQueue.Count > 0)
{
byte[] buff = _sendQueue.Dequeue();
list.Add(new ArraySegment<byte>(buff, 0, buff.Length));
}
_sendArgs.BufferList = list;**
bool pending = _socket.SendAsync(_sendArgs);
if (pending == false)
OnSendCompleted(null, _sendArgs);
}
해결책은 List<ArraySegment<byte>> list = new List<ArraySegment<byte>>(); 이렇게 list를 만들어서
list에다가 _sendQueue의 내용을 하나씩 다 추가를 해 준 다음에
마지막으로 list를 _sendArgs.BufferList에 연결해 줘야 한다.
list도 매번 만드는게 낭비 같다는 생각이 드니까 다른 애들과 같이 함수 밖 클래스로 옮겨주고 이름을 _pendingList로 한다.
List<ArraySegment<byte>> _pendingList = new List<ArraySegment<byte>>();
void RegisterSend()
{
_pending = true;
**_pendingList.Clear();
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을 사용하고 있었는데 _pendingList를 이용해서 대기 중인 정보가 있는지 없는지 판별할 수 있겠다는 생각이 든다.
public void Send(byte[] sendBuff)
{
lock (_lock)
{
_sendQueue.Enqueue(sendBuff);
**if (_pending == false)**
RegisterSend();
//_sendArgs.SetBuffer(sendBuff, 0, sendBuff.Length);
}
}
이 부분에서
public void Send(byte[] sendBuff)
{
lock (_lock)
{
_sendQueue.Enqueue(sendBuff);
**if (_pendingList.Count == 0) // 대기중인 애가 없다는**
RegisterSend();
}
}
대기중인 애가 없으면 RegisterSend를 해라. 이렇게 넘어오면 된다.
OnSendCompleted에 가서도 코드 흐름을 좀 수정해 준다.
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($"OnRecvCompletedFailed {e}");
}
}
else
{
Disconnect();
}
}
}
이 코드에서
void OnSendCompleted(object sender, SocketAsyncEventArgs args)
{
lock(_lock)
{
if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
{
try
{
**_sendArgs.BufferList = null; // 더이상 굳이 pendingList를 갖고 있을 필요 없으니까
_pendingList.Clear(); // OnSendCompleted라는 건 예약한 pendingList가 완료되었다는 거니까 Clear 해주면 된다.
// 몇 바이트 보냈는
Console.WriteLine($"Transferred bytes: {_sendArgs.BytesTransferred}");**
if(_sendQueue.Count > 0)
RegisterSend();
else
_pending = false;
}
catch (Exception e)
{
Console.WriteLine($"OnSendCompletedFailed {e}");
}
}
else
{
Disconnect();
}
}
}
_pendingList가 bool _pending 역할 대신해 주고 있기 때문에 RegisterSend의 _pending = true;와 class Session에서 _pending 선언도 삭제해 준다.
OnSendCompleted에서 해주고 있으므로 RegisterSend의 _pendingList.Clear()도 삭제해준다.
이제 코드 흐름을 다시 살펴보면 아까와는 조금 다르다는 걸 알 수 있다. 처음부터 살펴 보면 Send를 했다 가정을 해보면
public void Send(byte[] sendBuff)
{
lock (_lock)
{
_sendQueue.Enqueue(sendBuff);
if (_pendingList.Count == 0)
RegisterSend();
}
}
락을 잡고 _sendQueue에 일감 sendBuffer를 넣어주고,
_pendingList가 없다는 가정이 true라고 하면 RegisterSend를 호출을 하고, 이미 예약된 목록이 있다고 하면 그냥 스킵하고 Queue에다가 넣어주기만 하고 나가는 상태가 된다.
그다음에 RegistSend에 들어왔다면
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);
}
일단 _pendingList는 null인 상태야.
_sendQueue가 비어있지 않는 동아 _sendQueue에 있는 애들을 하나씩 다 빼가지고, _pendingList에 넣어 놓은 다음에 걔를 마지막으로 BufferList에 연결을 해줄 것이다.
그럼 BufferList는 예약된 모든 목록들을 들고 있을 것이고,
걔를 sendAsync를 하면 이전에 했던 버전에서는 버퍼 하나가 있으니까 걔만 보내줬었는데 지금은 _sendArgs의 버퍼는 null인 상태고 BufferList는 _pendingList로 연결이 되어 있으니까
SendAsynce로 한번에 쫙 보낼 줄 것이다.
바로 보낼 수 있으면 pending이 false로 리턴을 할테니까 OnSendCompleted를 바로 실행할 것이고
그게 아니면 조금 이따가 OnSendCompleted가 완료가 될 텐데, 어느 순간에는 완료가 될 거고
이 상태에서 성공적으로 보냈다고 하면 다음 단계로 보낼 준비를 해야한다.
void OnSendCompleted(object sender, SocketAsyncEventArgs args)
{
lock(_lock)
{
if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
{
try
{
_sendArgs.BufferList = null; // 더이상 굳이 pendingList를 갖고 있을 필요 없으니까
_pendingList.Clear(); // bool 역할을 얘가 대신 해주는 거.
Console.WriteLine($"Transferred bytes: {_sendArgs.BytesTransferred}");
if(_sendQueue.Count > 0)
RegisterSend();
}
catch (Exception e)
{
Console.WriteLine($"OnSendCompletedFailed {e}");
}
}
else
{
Disconnect();
}
}
}
_pendingList는 이제 Clear를 해주는 거. 왜냐하면 예약한 건 다 성공적으로 보냈다는 의미가 되는 거니까. BufferList도 null로 밀어준다. null을 굳이 해줄 필요는 없긴 한데 그냥 깔끔하게 하기 위해.
_sendQueue의 Count가 0이 아니다 즉 열심히 보내는 동안에 누군가가 _sendQueue에 패킷을 넣어 줬다고 하면 다시 한번 RegisterSend를 호출한다.
다시 _sendQueue의 Count가 0이 될 때 까지 계속 _pendingList에 넣어주고 반복하게 될 것이다.
이전 버전과 비슷하지만 한방에 뭉쳐서 모든 패킷을 꺼내서 한방에 보내주는게 달라졌어.
이 상태에서 실행을 해보면 아까와 마찬가지로 잘 보내지고 있는 걸 볼 수 있다.
진짜로 되는 건지는 맨 마지막에 테스트를 하기 전까지는 알 수 없다. 멀티 스레드 환경에서 테스트 해야 하기 때문
이렇게 1차적인 개선 사항을 넣어 줬다.
아직 완벽하게 모든 문제가 해결 되진 않았다.
- RegisterSend를 할 때 SendQueue를 무조건 비워서 모든 정보를 보내고 있는데 사실, Recieve도 마찬가지지만 무조건 받고 보내면 안되고, 순식같에 일정한 시간 동안 몇 byte를 보냈는지를 추적해서 너무 심하게 많이 보낸다 싶으면 쉬면서 하는게 좋긴 하다. 동시 다발적으로 많은 패킷이 몰릴 때 상대방이 받을 수 없는 데 너무 많이 보내면 문제가 있다. 상대방이 악의적으로 의미 없는 정보를 막 뿌리면 그런 부분은 recv를 할 때 체크를 해서 비정상이다 싶으면 바로 Disconnect해서 쫒아 내야 한다. 이런 부분들이 추가 되긴 해야 한다.
- 지금 sendQueue에 예약된 작업을 뭉쳐서 보내긴 했는데 패킷 모아 보내는 건 더 많은 작업이 필요하다. 어떤 존에서 1000명의 유저들이 몰려 있다 가정하면 각각의 유저들이 움직이고 스킬을 쏘고 그럴 거야. 그렇다는 건 하나의 유저가 하는 모든 행동들을 다른 999명 유저들에게도 다 뿌려가지고 정보를 서로 공유하면서 진행이 될텐데 만약에 유저가 옆자리로 움직였다는 패킷을 send로 넣어주게 되면 어떤 방식이든 RegisterSend로 가서 보내주게 될거야. 근데 경우에 따라서는 패킷 자체를 작은 패킷을 하나 짜리로 만들어서 보내는 게 아니라 걔네들 자체를 뭉쳐서 보내는 경우가 생긴다. 어떤 유저가 움직였다는 걸 바로 보내는 게 아니라 1000명의 유저들이 아줄 짧은 시간 동안 서로 움직이고 스킬 쏘는 모든 행동들을 다 기록한 어마어마하게 큰 버퍼를 하나를 만든 다음에 걔를 돌아가면서 send를 하게 되면 훨씬 성능 개선을 할 수 있을 거 같다는 생각이 든다. 많은 패킷을 모아 보내는 걸 서버 엔진에서 해줄 것인지 아니면 컨텐츠 단에서 모아서 send를 한번만 하게 요청을 하게 할 것인지는 길이 갈리게 된다. 선생님은 개인적으로 엔진은 패킷 모아 보내기는 하지 않고 이정도 선에서 끝내고 나중에 컨텐츠를 만들 때 존에 있는 모든 행동들을 기록 했다가 한번에 모아서 보내는 게 좋다고 생각한다고 한다.
이번시간에 한 건 패킷이 동시 다발적으로 몰렸을 때 예약된 애들을 _sendQueue에 있는 애들을 어떤 식으로든 한번에 SendAsync로 보낼 수 있게 개선을 한거다.
ServerCore의 Session.cs
using System;
using System.Collections.Generic;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace ServerCore
{
class Session
{
Socket _socket;
int _disconnect = 0;
object _lock = new object();
Queue<byte[]> _sendQueue = new Queue<byte[]>();
SocketAsyncEventArgs _sendArgs = new SocketAsyncEventArgs();
SocketAsyncEventArgs _recvArgs = new SocketAsyncEventArgs();
List<ArraySegment<byte>> _pendingList = new List<ArraySegment<byte>>();
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();
}
public void Send(byte[] sendBuff)
{
lock (_lock)
{
_sendQueue.Enqueue(sendBuff);
if (_pendingList.Count == 0)
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);
}
void OnSendCompleted(object sender, SocketAsyncEventArgs args)
{
lock (_lock)
{
if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
{
try
{
_sendArgs.BufferList = null; // 더이상 굳이 pendingList를 갖고 있을 필요 없으니까
_pendingList.Clear(); // bool 역할을 얘가 대신 해주는 거.
Console.WriteLine($"Transferred bytes: {_sendArgs.BytesTransferred}");
if (_sendQueue.Count > 0)
RegisterSend();
}
catch (Exception e)
{
Console.WriteLine($"OnRecvCompletedFailed {e}");
}
}
else
{
Disconnect();
}
}
}
public void Disconnect()
{
if (Interlocked.Exchange(ref _disconnect, 1) == 1)
return;
_socket.Shutdown(SocketShutdown.Both);
_socket.Close();
}
#region 네트워크 통신
void RegisterRecv()
{
bool pending = _socket.ReceiveAsync(_recvArgs);
if (pending == false)
OnRecvCompleted(null, _recvArgs);
}
void OnRecvCompleted(object sender, SocketAsyncEventArgs args)
{
if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success) // 모든 게 잘 처리 됐다는 뜻
{
// TODO
try
{
string recvData = Encoding.UTF8.GetString(args.Buffer, args.Offset, args.BytesTransferred);
Console.WriteLine($"[FromClient] {recvData}");
RegisterRecv();
}
catch(Exception e)
{
Console.WriteLine($"OnRecvCompletedFailed {e}");
}
}
else
{
Disconnect();
}
}
#endregion
}
}
'Server programming' 카테고리의 다른 글
02_10_네트워크 프로그래밍_Connector (0) | 2023.04.06 |
---|---|
02_09_네트워크 프로그래밍_Session #4_엔진단과 컨텐츠단 분리 (0) | 2023.04.04 |
02_07_네트워크 프로그래밍_Session #2_Send (0) | 2023.04.04 |
02_06_네트워크 프로그래밍_Session #1_Recieve (0) | 2023.04.04 |
02_05_네트워크 프로그래밍_Listener (0) | 2023.04.04 |
댓글