Q1. SocketAsyncEventArgs 를 재사용하기 위해 어떻게 수정 했는가?
Q2. OnSendCompleted에서 _sendQueue에 쌓여있으면 어떻게 했는가?
Q3. 아직 부족한 점은 무엇인가?
Q4. Send 함수에서 사용된 SocketAsyncEventArgs와 OnSendCompleted 함수는 어떤 역할을 하나요?
Q5. 이번 시간에 한 작업을 요약해 보세요.
Q1. SocketAsyncEventArgs를 재사용하기 위해서는 RegisterSend나 RegisterRecv를 호출할 때마다 새로운 SocketAsyncEventArgs 객체를 생성하는 것이 아니라, 미리 만들어 놓은 SocketAsyncEventArgs 객체를 사용하면 됩니다. 이렇게 하면 객체 생성에 따른 오버헤드가 줄어들어 성능이 향상됩니다.
Q2. OnSendCompleted에서 sendQueue에 쌓여있을 때는, RegisterSend 함수를 호출합니다. 만약 sendQueue가 비어있다면 _pending 변수를 false로 설정합니다.
Q3. Send를 한번 할 때 마다 SendAsync를 한번씩 해줘야 하는 거. Send를 한번 할 때마다 SendAsync를 한번씩 호출하는 것은 비효율적인 방법입니다. 이는 SendAsync가 비동기 방식으로 동작하기 때문에, SendAsync 함수가 호출될 때마다 새로운 스레드가 생성되어 작업을 처리하게 됩니다. 이렇게 되면 스레드 생성에 따른 오버헤드가 발생하고, 시스템 자원을 낭비하게 됩니다.
Q4.
Send 함수에서 사용된 SocketAsyncEventArgs 객체는 비동기 송신 작업을 수행하는 데 사용됩니다. 이 객체는 송신할 데이터를 담고 있으며, 송신 작업이 완료되면 OnSendCompleted 함수가 호출됩니다.
OnSendCompleted 함수는 비동기 송신 작업이 완료되었을 때 호출되는 콜백 함수입니다. 이 함수는 SocketAsyncEventArgs 객체를 매개변수로 받아서 해당 객체를 사용하여 송신 결과를 처리합니다. 이 함수가 없으면 비동기 송신 작업이 완료되었을 때 어떤 동작도 수행하지 않게 됩니다.
Q5.
Send, RegisterSend, OnSendCompleted의 기본 뼈대를 Recieve를 쪽을 참고해서 작성한다
-> Session의 Send함수에 기존 블로킹 함수를 삭제하고 Start에 있는 처음에 실행되는 부분을 복붙해서 sendArgs가 생성되고, OnSendCompleted가 sendArgs.Completed에 연동되고, sendArgs.SetBuffer도 sendBuffer로 세팅되게 수정해준다. 그리고 ResgisterSend(sendArgs)를 호출한다
-> SocketAsyncEventArgs _sendArgs = new SocketAsyncEventArgs();를 재사용하기 위해 Send 함수 밖으로 옮겨 줬다
-> _sendArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnSendCompleted); 는 Start로 옮긴다
-> _sendArgs가 클래스의 멤버 변수로 되었으니 매개변수로 사용하던 부분을 수정해 준다.
-> Session 클래스에 byte의 배열을 받는 Queue를 생성하고, bool _pending을 선언한다.
-> Send에서 _sendQueue에다 Enqueue로 sendBuff를 넣어준다. _pending으로 테스트해서 false면 RegisterSend를 호출하는 걸로 수정한다.
-> 멀티스레드 환경에서도 Send가 호출 될 수 있게 Lock을 걸어줘야 한다. class Session에 lock에 쓰기 위한 object를 하나 만든다.
-> Send 내부의 코드가 lock을 잡고 실행되게 한다.
-> RegisterSend로 가서 _pending을 true로 켜준다. _sendQueue에서 Dequeue로 하나를 뽑아 byte[] buff 변수에 넣어준다.그리고 _sendArgs의 SetBuffer를 이용해서 buff를 연결시켜 준다.
-> OnSendCompleted의 try에서 _pending = false로 꺼준다.
-> 콜백으로 RegisterSend가 아닌 곳에서 호출될 것을 대비해 OnSendCompleted에 락을 걸어준다.
-> 다른 스레드에서 Send할 때 _pending이 true여서 _sendQueue에 넣어 놓은 것을 OnSendCompleted에서 처리해준다. try에서 _sendQueue.count>0이면 RegisterSend를 호출한다.
지난 시간 Session 코드
using System;
using System.Collections.Generic;
using System.Net.Sockets;
using System.Text;
namespace ServerCore
{
internal class Session
{
Socket _socket;
int _disconnect = 0;
public void Start(Socket socket)
{
_socket = socket;
SocketAsyncEventArgs recvArgs = new SocketAsyncEventArgs();
recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);
recvArgs.SetBuffer(new byte[1024], 0, 1024);
// Listener에선 AcceptAsync가 완료가 된면 args.AcceptSocket에 연결된 새로 만들어준 소켓을 뱉어 주고 있었는데 Recive의 경우는 SetBuffer를 이용해 Buffer를 만들어줘야 한다.
// 3번째 버전 사용. 바이트 배열, offset 0부터, 사이즈는 1024. 경우에 따라 쪼개서 사용할 경우 offset 0 아닐 수 있어.
//recvArgs.UserToken = this; 이렇게 정보를 넣어 줄 수도 있는데 굳이 안해도 된다.
RegisterRecv(recvArgs);
}
public void Send(byte[] sendBuff) // 일단은 블로킹 버전으로 인터페이스만 맞춰준
{
_socket.Send(sendBuff);
}
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)
{
// 상대방이 연결 끊거나 할 때 0으로 올 수도 있어.
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
}
}
public void Start(Socket socket)
{
_socket = socket;
SocketAsyncEventArgs recvArgs = new SocketAsyncEventArgs();
recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);
recvArgs.SetBuffer(new byte[1024], 0, 1024);
RegisterRecv(recvArgs);
}
RegisterRecv를 살펴보면 이벤트를 만든 다음에 RegisterRecv를 하나만 실행하고 있었어.
RegisterRecv, OnRecvCompleted가 비동기로 돌아기는 하지만 애시당초 스레드가 동시에 OnRecvCompleted에 들어오는 경우는 없을거야. 완료 될 때 마다 다른 스레드에서 호출 될 수 있을지언정 동시 다발적으로 두개가 들어올 수는 없어. 낚시대가 하나밖에 없으니까.
Send같은 경우는 얘기가 완전 달라진다.
Send도 일단 Async계열 함수를 쓴다 가정하고 비슷하게 만들어 보자.
Session.cs에
public void Send(byte[] sendBuff)
{
_socket.Send(sendBuff);
}
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($"OnRecvCompletedFailed {e}");
}
}
else
{
Disconnect();
~~~~}
}
이렇게 해주면 될 거 같은데 애당초 Recieve와 다르게 Send하는 시점이 정해져 있지 않다는 얘기다.
Recieve 같은 경우에는 초반에 Start를 할 때 한번만 RegisterRecv(recvArgs); 로 예약만 해 다음에 실제로 클라이언트에서 메시지를 전송을 하면 그제서야 Recieve가 완료가 되면서 OnRecieveCompleted가 자동으로 호출이 될거야. 그리고 다시 예약하기 위해 RegisterRecv(args); 를 호출하고 있었어.
그런데 Send 같은 경우는 해당이 안되는게 Start에서 RegisterSend를 하는 순간
recvArgs.SetBuffer(new byte[1024], 0, 1024);
이런 식으 보내줄 버퍼랑 메시지를 설정해줘야 하는데 미래에 어떤 메시지를 보낼 줄 알고 예약한다는 건 말이 안된다. Send를 할 때는 다른 방식으로 해야 한다는 생각이 든다.
public void Send(byte[] sendBuff) // 일단은 블로킹 버전으로 인터페이스만 맞춰준
{
//_socket.Send(sendBuff);
}
Send를 하는 시점에다가 RegisterSend를 하게끔 만들어주자. 이게 생각나는 첫번쨰 방법.
Send에 SocketAsyncEvetnArgs 이벤트를 넣어줘야 한다.
public void Start(Socket socket)
{
_socket = socket;
**SocketAsyncEventArgs recvArgs = new SocketAsyncEventArgs();
recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);
recvArgs.SetBuffer(new byte[1024], 0, 1024);**
// Listener에선 AcceptAsync가 완료가 된면 args.AcceptSocket에 연결된 새로 만들어준 소켓을 뱉어 주고 있었는데 Recive의 경우는 SetBuffer를 이용해 Buffer를 만들어줘야 한다.
// 3번째 버전 사용. 바이트 배열, offset 0부터, 사이즈는 1024. 경우에 따라 쪼개서 사용할 경우 offset 0 아닐 수 있어.
//recvArgs.UserToken = this; 이렇게 정보를 넣어 줄 수도 있는데 굳이 안해도 된다.
RegisterRecv(recvArgs);
}
Start의 처음에 실행하는 부분의 코드를 Send에 복사해 준 다음
public void Send(byte[] sendBuff) // 일단은 블로킹 버전으로
{
//_socket.Send(sendBuff);
SocketAsyncEventArgs recvArgs = new SocketAsyncEventArgs();
recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);
recvArgs.SetBuffer(new byte[1024], 0, 1024);
}
수정해 준다.
Completed의 경우는 OnSendCompleted가 될 거고, recvArgs를 sendArgs로 바꿔준다.
버퍼의 경우는 새로운 버퍼를 만들어 주는게 아니라 애당초 보낼 줄 애를 매개변수로 받고 있었으니까 sendBuff를 넣어주면 된다.
public void Send(byte[] **sendBuff**)
{
//_socket.Send(sendBuff);
SocketAsyncEventArgs **sendArgs** = new SocketAsyncEventArgs();
**sendArgs**.Completed += new EventHandler<SocketAsyncEventArgs>(**OnSendCompleted**);
**sendArgs**.SetBuffer(**sendBuff**, 0, **sendBuff**.**Length**);
RegisterSend(**sendArgs**);
}
이렇게 sendBuff를 새로운 소켓 이벤트로 연동을 해준 다음에 걔를 RegisterSend를 해줄거고,
void RegisterSend(SocketAsyncEventArgs args)
{
bool pending = _socket.SendAsync(args);
if (pending == false)
OnSendCompleted(null, args);
}
그리고 완료 했으면 OnSendCompleted도 완료가 될거야.
Recv의 경우는 OnRecvCompleted에서 다시 한번 RegisterRecv를 해줬어. 예약을 해줬었는데
Send같은 경우는 문제가 일어난다. OnSendCompleted에서 RegisterSend(args)를 다시 하는게 말이 안되는게
void OnSendCompleted(object sender, SocketAsyncEventArgs **args**)
{
if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
{
try
{
RegisterSend(**args**);
}
catch (Exception e)
{
Console.WriteLine($"OnSendCompletedFailed {e}");
}
}
else
{
Disconnect();
}
}
여기 들어있는 args 버퍼는
public void **Send(byte[] sendBuff)**
{
//_socket.Send(sendBuff);
SocketAsyncEventArgs sendArgs = new SocketAsyncEventArgs();
sendArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnSendCompleted);
**sendArgs**.SetBuffer(sendBuff, 0, sendBuff.Length);
RegisterSend(**sendArgs**);
}
void RegisterSend(SocketAsyncEventArgs **args**)
{
bool pending = _socket.SendAsync(args);
if (pending == false)
OnSendCompleted(null, **args**);
}
Send에서 연결해준 byte[] sendBuff 이 아이였어. 여기서 보내고 싶은 정보가 있었는데 한번 보낸 다음에 또 같은 정보로 예약을 할 필요는 없다. 애당초 재사용을 할 수 없다는 얘기가 된다.
public void **Send(byte[] sendBuff)**
{
//_socket.Send(sendBuff);
SocketAsyncEventArgs **sendArgs** = new SocketAsyncEventArgs();
sendArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnSendCompleted);
**sendArgs**.SetBuffer(sendBuff, 0, sendBuff.Length);
**RegisterSend**(**sendArgs**);
}
문제는 여기서 이벤트 보낼 때 마다 sendArgs를 한번씩 다시 만들고있고, 사용을 전혀 못하는 상태인데다가 멀티 스레드 환경에서 동시에 Send를 한다고 하면 다행히 SendAsync가 멀티스레드 환경에서 호출된다고 뻑나는 그런 함수는 아니다. 그건 다행이긴 한데 재사용을 할 수 없다는 게 마음에 안든다.
일단 실행을 해보면 간단한 코드라 실행이 된다는 걸 수 있다.
void OnSendCompleted(object sender, SocketAsyncEventArgs **args**)
{
if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
{
try
{
//RegisterSend(**args**);
}
catch (Exception e)
{
Console.WriteLine($"OnSendCompletedFailed {e}");
}
}
else
{
Disconnect();
}
}
Recieve이벤트가 완료 되었으면 새로운 Recieve 이벤트를 다시 한번 예약을 해야 하니까 try 부분에서 뭔가를 해줬는데 보내는 거 에 성공하는 경우에는 딱히 해줄 게 없는 거도 맞아.
그럼에도 Send에서 sendArgs이벤트 만드는 부분을 재사용 못하는게 첫번 문제고, 더 치명적인 문제는 RegisterSend를 매번마 다시 하는 것도 문제야.
public void Send(byte[] sendBuff)
{
//_socket.Send(sendBuff);
SocketAsyncEventArgs sendArgs = new SocketAsyncEventArgs();
sendArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnSendCompleted);
sendArgs.SetBuffer(sendBuff, 0, sendBuff.Length);
RegisterSend(sendArgs);
}
void RegisterSend(SocketAsyncEventArgs args)
{
bool pending = _socket.**SendAsync**(args);
if (pending == false)
OnSendCompleted(null, args);
}
만약 MMORPG 에서 1000명의 유저가 동일한 존에 모여 있다고 가정해 보자. A 유저가 한발짝 움직이면 움직였단 정보 루프 돌면서 주변 유저들에게 한번씩 정보를 보내준다. 그걸 A뿐만 아니라 나머지 애들도 움직이고 스킬 쓰고 난리 치고 있으니까 Send를 호출하는 횟수가 엄청나게 늘게 된다.
그럴 때 마다 registerSend를 해서 SendAsync를 매번 호출하는 건 문제가 있다. 나중에 MMO 성능 테스트를 해보면 대부분 Send와 Recieve 하는 네트워크 쪽 송수신을 하는 부분에서 부하가 많이 걸리고 느리다.
유저모드에서 네트워크 패킷을 보내는 건 불가능 하고, 이런 건 운영체제가 커널단에서 다 처리해주는 것이기 때문에 SendAsync얘를 쿨하게 아무 때나 막 쓰는 건 문제가 있다.
그렇기 때문에 이런 식으로 이벤트를 매번 만들어서 보내는 게 아니라 어떻게 해서든 뭉쳐서 보내면 좋겠다는 생각이 든다. 이왕이면 우리가 만들어준 SocketAsyncEventArgs sendArgs이 Async이벤트도 재사용하면 좋을 거 같다는 생각이 든다.
그래서 재사용하기 위해
abstract class Session
{
Socket _socket;
int _disconnect = 0;
**SocketAsyncEventArgs _sendArgs = new SocketAsyncEventArgs();**
SocketAsyncEventArgs sendArgs를 Send 안에다가 만드는게 아니라 밖에다가 만들어 보자. sendArgs앞에 _를 붙였다.
그리고 초기화 하는 부분 중 Completed에 연결하는 부분은 여러번 할 필요 없으니 Start로 올겨주자.
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)
{_
sendArgs.SetBuffer(sendBuff, 0, sendBuff.Length);
은 그냥 Send에서 하게 놔두자.
RegisterSend(sendArgs);
_sendArgs가 클래스의 멤버 변수로 되었으니 매개변수로 사용하던 부분을 수정해 준다. sendArgs를 굳이 넣을 필요가 없게 되니까 매개변수를 삭제하고 RegiseterSend의 정의부도 매개변수 없게 바꾸자.
public void Send(byte[] sendBuff)
{
//_socket.Send(sendBuff);
_sendArgs.SetBuffer(sendBuff, 0, sendBuff.Length);
RegisterSend();
}
void RegisterSend()
{
bool pending = _socket.SendAsync(**_sendArgs**);
if (pending == false)
OnSendCompleted(null, **_sendArgs**);
}
이런 식으로 SendAsync와 OnSendCompleted에 매개 변수로 RegisterSend로 전달되었던 args대신 클래스 멤버 변수인 _sendArgs를 넣어주면 된다.
이렇게 재사용 되게 쿨하게 바꾼다고 해결 되는 건 아니다.
지금 Send를 호출하면 지금 고친게 문제가 될 것이다.
public void Send(byte[] sendBuff)
{
//_socket.Send(sendBuff);
_sendArgs.SetBuffer(sendBuff, 0, sendBuff.Length);
RegisterSend();
}
별도의 새로운 이벤트로 만들어준게 아니라 동일한 이벤트를 계속 사용하고 있는데 기존에 넣어준 애가 완료 되지도 않았는데 멋대로 버퍼를 다른 애로 바꿔주면 에러가 날거야.
그 다음에 하고 싶은 건
Send에서 보내는 byte[] sendBuff 이걸 매번마다 RegisterSend하는 게 아니라 큐에다가 쌓아서 한번에 보내게 할거야.
RegisterSend의 SendAsync가 끝나서 OnSendCompleted가 완료되기 전 까지는 보내지 않고 큐에다가만 쌓아 놓다가 모든 보내는 게 완료되었으면 다시 Send로 돌아와서 큐를 비우는 형식으로 고쳐 볼거야.
결국 RegisterRecv를 할 때 낙시대를 던져서 물고기를 낙아챈 다음에 올려서 물고기를 떼어낸 다음에 다시 한번 낙시대를 던졌는데 그걸 반복하는 것과 마찬가지로 얘도 _sendArgs 하나만 사용하니까 OnSendCompleted할 때 다시 한번 낚시대를 다시 던질지 try에서 체크하는 걸로 비유를 할 수 있다.
말로 하기 힘드니 코드를 보면 분석을 해보자.
나중엔 byte[] sendBuff 이런 식으로 byte배열을 Send 하는 게 아니라 패킷라는 개념으로 보낼 것이긴 한데 그건 나중에 가서 고치면 되는 거니까 일단은 실험하고 있는 byte 배열 버전으로 만들어 보자.
이제는 Send를 호출 했다고 무조건 SendRegister를 호출 해서 SendAsync를 하는게 아니라 일단은 Queue에다가 차곡차곡 쌓아 놓는다고 했어.
class Session
{
Socket _socket;
int _disconnect = 0;
**Queue<byte[]> _sendQueue = new Queue<byte[]>();
bool _pending = false;**
SocketAsyncEventArgs _sendArgs = new SocketAsyncEventArgs();
이런 식으로 byte배열을 받는 Queue를 만들어 준다.
bool _pending은 pending 여부를 저장한다. _pending은 만약 여기서 한번이라도 RegisterSend를 했으면 _pending은 true상태로 될거고, 뭔가를 보내고 있다. OnSendComplete에서 모든 작업이 끝났으면 _pending을 false로 꺼줄 거야.
void RegisterSend()
{
bool pending = _socket.SendAsync(_sendArgs);
if (pending == false)
OnSendCompleted(null, _sendArgs);
}
만약에 누군가 이미 registerSend하고 있으면 Send하는 부분에서는 RegisterSend를 실행하는게 아니라 Queue에다가만 byte[] sendBuff 이 바이트 배열을 넣어 놓고 종료를 한다는 얘기가 된다.
일단 코드를 만들어 보자.
public void Send(byte[] sendBuff)
{
**_sendQueue.Enqueue(sendBuff);**
if (_pending == false) // 1빠로 Send 호출한 경우, 그게 아니라면 여기서 끝내는
RegisterSend();
}
싱글 스레드면 이정도로 충분한데 멀티스레드에서 쓸 수 있게 해줘야 하니 락의 개념이 들어갈거야.
class Session
{
Socket _socket;
int _disconnect = 0;
**object _lock = new object();**
락을 쓰기 위해 오브젝트를 하나 만들고
public void Send(byte[] sendBuff)
{
lock (_lock)
{
_sendQueue.Enqueue(sendBuff);
if (_pending == false)
RegisterSend();
}
}
이렇게 락을 잡으면 한번에 한명씩만 들어온다는 게 된다.
흐름상 RegisterSend 쪽 코드를 이어서 만들어 주면 된다.
void RegisterSend()
{
**_pending = true;**
**byte[] buff = _sendQueue.Dequeue(); // 얘를 뽑아서
_sendArgs.SetBuffer(buff, 0, buff.Length); // _sendArgs에 연결시켜 준다.**
// pending이 false면 OnSendCompleted가 호출되고, 그게 아니면 예약한게 나중에 호출 될거야.
bool pending = _socket.SendAsync(_sendArgs);
if (pending == false)
OnSendCompleted(null, _sendArgs);
}
void OnSendCompleted(object sender, SocketAsyncEventArgs args)
{
if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
{
try
{
**_pending = false;**
}
catch (Exception e)
{
Console.WriteLine($"OnSendCompletedFailed {e}");
}
}
else
{
Disconnect();
}
}
이렇게 _pending을 켜주고 꺼주면
public void Send(byte[] sendBuff)
{
lock (_lock)
{
_sendQueue.Enqueue(sendBuff);
if (_pending == false)
RegisterSend();
}
}
Send를 할 때 _pending 상태를 봐서 true면 RegisterSend를 스킵하고 안하게 될거야. 이걸로 누군가가 스레드에 Send를 예약했는지 안했는지 판별하게 한 거.
RegisterSend는 애당초 Send에서 락을 건 상태에서 호출을 해주는 거라 별도의 락 처리를 안해줘도 된다.
OnSendCompleted같은 경우는 RegisterSend를 통해 호출 됐으면 락이 필요 없겠지만 또 다른 경우가 있었어.
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);
}
이런 식으로 콜백 방식으로 나중에 다른 스레드에서 호출될 수 있는데 그 경우를 챙겨주기 위해 락이 들어가야 한다.
void OnSendCompleted(object sender, SocketAsyncEventArgs args)
{
**lock(_lock)
{**
if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
{
try
{
_pending = false;
}
catch (Exception e)
{
Console.WriteLine($"OnSendCompletedFailed {e}");
}
}
else
{
Disconnect();
}
**}**
}
이렇게 되면 안전한 상황이 된 거 .
멀티스레드 프로그래밍은 크래쉬 내보면서 연습하는 수 밖에 없다.
여기서 _pending을 false로 하고 하나 더 체크해줘야 한다.
만약에
public void Send(byte[] sendBuff)
{
lock (_lock)
{
_sendQueue.Enqueue(sendBuff);
if (_pending == false)
RegisterSend();
}
}
청음으로 Send에 들어와서 RegisterSend를 했다고 가정을 해보자.
void RegisterSend()
{
_pending = true;
byte[] buff = _sendQueue.Dequeue(); // 얘를 뽑아서
_sendArgs.SetBuffer(buff, 0, buff.Length); // _sendArgs에 연결시켜 준다.
// pending이 false면 OnSendCompleted가 호출되고, 그게 아니면 예약한게 나중에 호출 될거야.
bool pending = _socket.**SendAsync**(_sendArgs);
if (pending == false)
OnSendCompleted(null, _sendArgs);
}
근데 SendAsync가 바로 완료되지 않아가지고, pending이 true인 상태가 아닌게 되가지고 OnSendCompleted가 조금 있다가 호출이 됐는데 그 상태에서 다른 스레드가 Send를 했다고 가정을 하면 걔는 _pending 이 true인 상태이기 때문에
public void Send(byte[] sendBuff)
{
lock (_lock)
{
_sendQueue.Enqueue(sendBuff);
if (_pending == false)
RegisterSend();
}
}
RegisterSend 얘를 스킵하고, _sendQueue에만 넣어준 상태가 된다. 그렇다는 건 나중에라도 누군가가 _sendQueue에 있는 애를 처리해야 된다는 말이 된다.
그걸 여기서 처리하면 좋아.
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($"OnSendCompletedFailed {e}");
}
}
else
{
Disconnect();
}
}
}
SendAsync를 하고 있는 동안에 누군가가 또 예약을 했으면 다시 한번 걔를 처리해준다는 얘기다.
여기서 한 것의 주 목적중 하나는 SocketAsyncEventArgs _sendArgs를 재사용 하는 거였어.
매번마다 할 때 마다 new를 하는 방식이 아니라 _sendArgs를 하나만 만들어 두고, send를 언제 할지 예측할 수 없으니까 _sendArgs가 필요할 때 RegisterSend안에서 사용하고 있는 거.
만약 누군가가 보내고 있는 작업이 완료되지 않았다면 _sendQueue에만 넣어놓고 안보낸 상태로 종료를 할거야. 그럼 pending한 애가 완료 되면 OnSendCompleted에 들어오게 될 건데 걔가 다시 체크를 해서 혹시라도 SendAsync가 지연이 되는 동안에 누군가가 Queue에 넣어 놨으면 그걸 처리하러 갈거야. 결국에는 recieve 하는 동작과 유사하게 동작을 할거야.
다만 SendArgs를 재사용할 수 없는게 아쉬워서 고치긴 했지만 주요 문제는 이게 아니었어. 가장 큰 문제는 패킷을 한번 보낼 때 마다, Send를 한번 할 때 마다 SendAsync를 한번씩 해줘야 하는 거야. Queue에 넣어 주는 걸로 수정을 했지만 그럼에도 완벽하진 않아. Send를 100번 호출하면 SendAsync도 언젠가는 결국엔 100번 호출이 될테니까 완전한 해결책은 아니다.
일단은 1차적으로 Async 계열로 바꿨다는 성과만 있었던 것이고 다음시간에 추가로 바꿜 거야.
실행을 해보면 잘 되지만 큰 의미는 없다. 테스트 하고 싶으면 클라에서 세션 몇 백개씩 만들어서 계속 패킷을 쏘는 작업을 해야 하고 서버 쪽에서도 동시 다발적으로 멀티스레드 환경에서 구성해야 하기 때문에 버그 있는지 아직 알 수는 없다.
나중에 테스트 하면서 고치면 된다.
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[]>();
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
}
}
'Server programming' 카테고리의 다른 글
02_09_네트워크 프로그래밍_Session #4_엔진단과 컨텐츠단 분리 (0) | 2023.04.04 |
---|---|
02_08_네트워크 프로그래밍_Session #3_BufferList (0) | 2023.04.04 |
02_06_네트워크 프로그래밍_Session #1_Recieve (0) | 2023.04.04 |
02_05_네트워크 프로그래밍_Listener (0) | 2023.04.04 |
02_04_네트워크 프로그래밍_소켓 프로그래밍 입문 #2 (0) | 2023.04.03 |
댓글