SocketAsyncEventArgs 객체를 이용해 데이터를 송수신 할 때, Buffer를 사용하게 된다. 문제는 이 Buffer를 사용하고 조치를 취하지 않으면, 가비지 컬렉션이 알아서 가져가지 않는다.
맨 처음 서버를 실행시키면, 100메가 정도 사용하던 메모리가 좀 오래 되면, 1기가 넘게 쌓여있다. 대충 계속 살펴보면 1~2기가 사이를 왔다갔다한다.
SocketAsyncEventArgs 메모리 릭에 관련해 검색 해보니, SocketAsyncEventArgs객체를 풀링해서 사용해야 한다고 돼 있고, 또한, 사용 후 Dispose()함수를 호출 해, 리소스를 날려줘야 가비지 컬렉션이 수거 해 간다고 한다. 정리 해보면
- SocketAsyncEventArgs 사용 후, Completed에 [-=]으로 콜백함수 제거
- SetBuffer(null, 0,0)으로 버퍼사용 해제
- SocketAsyncEventArgs 객체 소멸 시, Dispose()함수 호출
위 세가지는 반드시 해줘야 하는 일이다. 하지만 이렇게 하더라도, 메모리 사용량이 메우 크다(쌓이고 쌓이다, 다시 줄어들긴하는데, 또 다시 커짐). GC가 돌아도 바로 수거해 가지 않는 느낌?? 해당 서버를 같은 물리서버에서 여러개 띄울 경우(포트만 다르게 해서 채널링 하는 경우) , 더 많이 쌓인다.
위 문제를 해결하기 위해, Buffer사용도 풀링해서 사용해야 한다. 다시 말해, SocketAsyncEventArgs객체를 풀링하고, Buffer도 풀링해야 한다. 아래 코드는 SocketAsyncEventArgs객체 풀링과 Buffer풀링하는 예제이다. 모두 MSDN에 올라와있는 예제를 내꺼에 맞게 쬐금 바꿨다.
SocketAsyncEventArgs 풀링
using System;
using System.Collections.Generic;
using System.Net.Sockets;
public class SocketAsyncEventArgsPool : Singleton<SocketAsyncEventArgsPool>
{
Stack<SocketAsyncEventArgs> m_pool;
override protected void init()
{
//Constants.MAX_CONNECTION 최대 연결 수
m_pool = new Stack<SocketAsyncEventArgs>(Constants.MAX_CONNECTION);
for (int i = 0; i < Constants.MAX_CONNECTION; i++)
{
SocketAsyncEventArgs args = new SocketAsyncEventArgs();
m_pool.Push(args);
}
}
public void Push(SocketAsyncEventArgs item)
{
if(item == null)
{
throw new ArgumentNullException("item is null");
}
lock (m_pool)
{
if (m_pool.Count >= Constants.MAX_CONNECTION)
{
item.Dispose();
return;
}
m_pool.Push(item);
}
}
public SocketAsyncEventArgs Pop()
{
lock(m_pool)
{
if (m_pool.Count > 0)
return m_pool.Pop();
else
{
SocketAsyncEventArgs args = new SocketAsyncEventArgs();
return args;
}
}
}
public int Count
{
get{
return m_pool.Count;
}
}
}
Buffer 풀링
using System;
using System.Collections.Generic;
using System.Net.Sockets;
public class BufferManager : Singleton<BufferManager>
{
int m_num_bytes; // the total number of bytes controlled by the buffer pool
byte[] m_buffer; // the underlying byte array maintained by the Buffer Manager
Stack<int> m_free_index_pool;
int m_current_index;
int m_buffer_size;
public BufferManager()
{}
override protected void init()
{
//최대 유저 수 x2(송신용,수신용)
m_num_bytes = Constants.MAX_CONNECTION*jdCommonDefine.SOCKET_BUFFER_SIZE*2;
m_current_index = 0;
m_buffer_size = jdCommonDefine.SOCKET_BUFFER_SIZE;
m_free_index_pool = new Stack<int>();
m_buffer = new byte[m_num_bytes];
}
public bool SetBuffer(SocketAsyncEventArgs args)
{
if (m_free_index_pool.Count > 0)
{
args.SetBuffer(m_buffer, m_free_index_pool.Pop(), m_buffer_size);
}
else
{
if (m_num_bytes < (m_current_index + m_buffer_size))
{
return false;
}
args.SetBuffer(m_buffer, m_current_index, m_buffer_size);
m_current_index += m_buffer_size;
}
return true;
}
/// <summary>
/// Removes the buffer from a SocketAsyncEventArg object. This frees the buffer back to the
/// buffer pool
/// </summary>
public void FreeBuffer(SocketAsyncEventArgs args)
{
if (args == null)
return;
m_free_index_pool.Push(args.Offset);
//args.SetBuffer(null, 0, 0); //가끔 SocketAsyncEventArgs에서 사용중이라고 이셉션 발생가능하기 때문에,이 함수 밖에서 처리함.
}
}
이제 서버에서 송, 수신하는 부분을 살펴보자. jdUserToken객체를 유저가 접속되고나면 생성되는 객체이다.
2019.12.22 - [게임을 만들자/게임 서버(C#)] - c# 실시간 게임 서버 만들기 3 - 서버
using System;
using System.IO;
using System.Net.Sockets;
using System.Text;
using System.Runtime.Serialization.Formatters.Binary;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Diagnostics;
public class jdUserToken
{
.....
SocketAsyncEventArgs m_receive_event_args;
jdMessageResolver m_message_resolver;
Socket m_socket;
//요청 받은 패킷 리스트
List<jdPacket> m_packet_list = new List<jdPacket>(5);
object m_mutext_packet_list = new object();
SocketAsyncEventArgs m_send_event_args;
//보낼 패킷 리스트
Queue<jdPacket> m_send_packet_queue = new Queue<jdPacket>(100);
object m_mutext_send_list = new object();
public jdUserToken()
{
m_message_resolver = new jdMessageResolver();
}
public void Init()
{
//수신용 객체 pop
m_receive_event_args = SocketAsyncEventArgsPool.Instance.Pop();
m_receive_event_args.Completed += onReceiveComplected;
m_receive_event_args.UserToken = this;
/송신용 객체 pop
m_send_event_args = SocketAsyncEventArgsPool.Instance.Pop();
m_send_event_args.Completed += onSendComplected;
m_send_event_args.UserToken = this;
//송 수신용 객체 모두 버퍼풀을 이용해 버퍼 세팅
BufferManager.Instance.SetBuffer(m_receive_event_args);
BufferManager.Instance.SetBuffer(m_send_event_args);
}
public void Update()
{
if(m_packet_list.Count > 0)
{
lock (m_mutext_packet_list)
{
try
{
//수신 패킷 처리
foreach (jdPacket packet in m_packet_list)
user.ProcessPacket(packet);
m_packet_list.Clear();
}
catch (Exception e)
{
예외처리
}
}
}
}
public void AddPacket(jdPacket packet)
{
lock(m_mutext_packet_list)
{
m_packet_list.Add(packet);
}
}
public void StartReceive()
{
bool pending = Socket.ReceiveAsync(m_receive_event_args);
if (!pending)
onReceiveComplected(this, m_receive_event_args);
}
public void Send(jdPacket packet)
{
if (Socket == null)
return;
lock(m_mutext_send_list)
{
//수신 중인 패킷이 없으면 바로, 전송
if (m_send_packet_queue.Count < 1)
{
m_send_packet_queue.Enqueue(packet);
sendProcess();
return;
}
//수신 중인 패킷이 있으면, 큐에 넣고 나감.
//쌓인 패킷이 100개가 넘으면 그 다음부터는 무시함. 제 겜은 그래도 됨..
if(m_send_packet_queue.Count < 100)
m_send_packet_queue.Enqueue(packet);
}
}
private void sendProcess()
{
if (Socket == null)
return;
jdPacket packet = m_send_packet_queue.Peek();
byte[] send_data = packet.GetSendBytes();
int data_len = send_data.Length;
if(data_len > jdCommonDefine.SOCKET_BUFFER_SIZE)
{
//버퍼풀에서 설정한 크기보다 작은 경우(4K 초과)
//SocketAsyncEventArgsPool에서 새 객체를 pop해서 사용
//접속 초기에 데이터 큰 것을 보낼 때만 상용됨.
//큰 데이터를 보낼 일이 없으면 무시 가능.
SocketAsyncEventArgs send_event_args = SocketAsyncEventArgsPool.Instance.Pop();
if (send_event_args == null)
{
LogManager.Error("SocketAsyncEventArgsPool::Pop() result is null");
return;
}
//LogManager.Debug("Send Thread ID: " + Thread.CurrentThread.ManagedThreadId);
send_event_args.Completed += onSendComplectedPooling;
send_event_args.UserToken = this;
send_event_args.SetBuffer(send_data, 0, send_data.Length);
bool pending = Socket.SendAsync(send_event_args);
if (!pending)
onSendComplectedPooling(null, send_event_args);
}
else
{
//버퍼풀에서 설정한 크기보다 작은 경우(4K 이하)
//99% 이 경우가 많습니다.
//버퍼를 설정
m_send_event_args.SetBuffer(m_send_event_args.Offset, send_data.Length);
//버퍼에 데이터 복사
Array.Copy(send_data,0, m_send_event_args.Buffer, m_send_event_args.Offset, send_data.Length);
bool pending = Socket.SendAsync(m_send_event_args);
if (!pending)
onSendComplected(null, m_send_event_args);
}
}
void onSendComplected(object sender, SocketAsyncEventArgs e)
{
if (e.SocketError == SocketError.Success)
{
lock (m_mutext_send_list)
{
if(m_send_packet_queue.Count > 0)
m_send_packet_queue.Dequeue();
if (m_send_packet_queue.Count > 0)
sendProcess();
}
}
else
{
}
}
void onSendComplectedPooling(object sender, SocketAsyncEventArgs e)
{
if (e.BufferList != null)
{
e.BufferList = null;
}
e.SetBuffer(null, 0, 0);
e.UserToken = null;
e.RemoteEndPoint = null;
e.Completed -= onSendComplectedPooling;
SocketAsyncEventArgsPool.Instance.Push(e);
if (e.SocketError == SocketError.Success)
{
//LogManager.Debug("onSendComplected Thread ID: " + Thread.CurrentThread.ManagedThreadId);
lock (m_mutext_send_list)
{
if (m_send_packet_queue.Count > 0)
m_send_packet_queue.Dequeue();
if (m_send_packet_queue.Count > 0)
sendProcess();
}
}
else
{
}
}
void onReceiveComplected(object sender, SocketAsyncEventArgs e)
{
// check if the remote host closed the connection
if (e.BytesTransferred > 0 && e.SocketError == SocketError.Success)
{
//read message
m_message_resolver.OnReceive(e.Buffer, e.Offset, e.BytesTransferred, onMessageComplected);
//receive message
StartReceive();
}
else
{
if(e.SocketError == SocketError.Success)
{
LogManager.Info("SocketError.Success, but BytesTransferred is 0");
jdPacket packet = new jdPacket();
packet.m_type = (Int16)jdPacketType.PACKET_USER_CLOSED;
AddPacket(packet);
}
else
{
LogManager.Error(string.Format("SocketError is {0}", e.SocketError.ToString()));
jdPacket packet = new jdPacket();
packet.m_type = (Int16)jdPacketType.PACKET_USER_CLOSED;
AddPacket(packet);
}
}
}
void onMessageComplected(jdPacket packet)
{
AddPacket(packet);
}
public void Close()
{
try{
if (Socket != null)
Socket.Shutdown(SocketShutdown.Send);
}catch(Exception e)
{
LogManager.Debug(e.ToString());
}
finally
{
if(Socket != null)
Socket.Close();
}
Socket = null;
User = null;
m_message_resolver.ClearBuffer();
lock(m_mutext_packet_list)
{
m_packet_list.Clear();
}
lock(m_mutext_send_list)
{
m_send_packet_queue.Clear();
}
//수신 객체 해제
{
BufferManager.Instance.FreeBuffer(m_receive_event_args);
m_receive_event_args.SetBuffer(null, 0, 0);
if (m_receive_event_args.BufferList != null)
m_receive_event_args.BufferList = null;
m_receive_event_args.UserToken = null;
m_receive_event_args.RemoteEndPoint = null;
m_receive_event_args.Completed -= onReceiveComplected;
SocketAsyncEventArgsPool.Instance.Push(m_receive_event_args);
//풀링하지 않는 경우엔 반드시, m_send_event_args.Dispose();
m_receive_event_args = null;
}
//송신 객체 해제
{
BufferManager.Instance.FreeBuffer(m_send_event_args);
if (m_send_event_args.BufferList != null)
m_send_event_args.BufferList = null;
m_send_event_args.UserToken = null;
m_send_event_args.RemoteEndPoint = null;
m_send_event_args.Completed -= onSendComplected;
SocketAsyncEventArgsPool.Instance.Push(m_send_event_args);
//풀링하지 않는 경우엔 반드시, m_send_event_args.Dispose();
m_send_event_args = null;
}
}
}
이전 하고 달라진 점은, 보낼 때, 큐에 넣어서, 순차적으로 보내는 점이다. Buffer를 풀링해서 사용하려면 이렇게 해야한다.
놀랍게도 Buffer풀링 방식으로 바꾼 후, 더 이상 SafeNativeOverlapped로 인한 릭은 사라지고, 메모리 사용량도 1기가가 넘어가건게 지금은 60~70메가 사이로 유지된다.
'게임을 만들자 > 게임 서버(C#)' 카테고리의 다른 글
Firebase Functions, 인앱 검증하기 (0) | 2022.05.26 |
---|---|
개인 PC 서버, 내부 IP, 외부 IP 확인하기.(IPTIME) (0) | 2021.03.29 |
C#, 외부 프로세스 실행 (0) | 2021.03.16 |
c# 구글 인앱 iap 서버 검증 코드 (1) | 2021.01.08 |
윈도우 서버 TCPNoDelay, TcpAckFrequency 설정 (1) | 2020.12.23 |