using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace ServerCore
{
class Listener
{
Socket _listenSocket;
public void Init(IPEndPoint endPoint)
{
// AddressFamily : 네트워크 주소 체계, SoketType.Stream : 소켓의 유형설정, ProtocolType.Tcp : 연결 방식
_listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
// 소켓 설정하고 바인딩하고 리슨으로 열어주고
_listenSocket.Bind(endPoint);
_listenSocket.Listen(10);
SocketAsyncEventArgs args = new SocketAsyncEventArgs();
args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted);
RegisterAccept(args);
}
void RegisterAccept(SocketAsyncEventArgs args)
{
// 재사용하므로 초기화해줌
args.AcceptSocket = null;
bool pending = _listenSocket.AcceptAsync(args);
if (pending == false)
OnAcceptCompleted(null, args);
}
void OnAcceptCompleted(object obj, SocketAsyncEventArgs args)
{
// 연결에 성공한 경우
if(args.SocketError == SocketError.Success)
{
Session session = new Session();
session.OnConnected(args.AcceptSocket.RemoteEndPoint);
session.Start(args.AcceptSocket);
}
else
{
Console.WriteLine(args.SocketError.ToString());
}
RegisterAccept(args);
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace ServerCore
{
class Session
{
Socket _clientSocket;
byte[] _recvBuffer = new byte[1024];
public void OnConnected(EndPoint endPoint)
{
Console.WriteLine($"Connected : {endPoint}");
}
public void OnDisconnectd()
{
}
public void OnRecv()
{
}
public void OnSend()
{
}
public void Start(Socket socket)
{
// 비동기 recv
_clientSocket = socket;
SocketAsyncEventArgs recvArgs = new SocketAsyncEventArgs();
recvArgs.SetBuffer(_recvBuffer, 0, _recvBuffer.Length);
recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);
RegisterRecev(recvArgs);
}
void RegisterRecev(SocketAsyncEventArgs args)
{
// 받을 때 마다 버퍼 안비워줘도 됨?
bool pending = _clientSocket.ReceiveAsync(args);
if (pending == false)
OnRecvCompleted(null, args);
}
void OnRecvCompleted(object obj, SocketAsyncEventArgs args)
{
if(args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
{
string chat = Encoding.UTF8.GetString(args.Buffer);
Console.WriteLine(chat);
RegisterRecev(args);
}
else
{
// OnDisconnect
// tcp의 경우 연결이 끊기지 않은 이상 보낼 때 오류나면 재전송하는데
// 바로 끊어버리면 문제 생기지 않나?
Console.WriteLine(args.SocketError.ToString());
}
}
}
}
강의를 본 뒤 강의와 다른 점 몇개를 수정 하였다.
1) string chat = Encoding.UTF8.GetString의 매개변수를 args.Buffer에서
(args.Buffer, args.Offset, args.BytesTransferred)로 변경
2) Disconnect 메서드 추가
: 왜 disconnect 메서드가 필요한지는 아래 기술
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace ServerCore
{
class Session
{
Socket _clientSocket;
int _disconnected = 0;
byte[] _recvBuffer = new byte[1024];
public void OnConnected(EndPoint endPoint)
{
Console.WriteLine($"Connected : {endPoint}");
}
public void OnDisconnectd()
{
}
public void OnRecv()
{
}
public void OnSend()
{
}
public void Disconnect()
{
// _disconnect를 1로 바꾼다.
// 만약 이미 1 였다면(이미 disconnect한 경우) return;
if (Interlocked.Exchange(ref _disconnected, 1) == 1)
return;
_clientSocket.Shutdown(SocketShutdown.Both);
_clientSocket.Close();
}
public void Start(Socket socket)
{
// 비동기 recv
_clientSocket = socket;
SocketAsyncEventArgs recvArgs = new SocketAsyncEventArgs();
recvArgs.SetBuffer(_recvBuffer, 0, _recvBuffer.Length);
recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);
RegisterRecev(recvArgs);
}
void RegisterRecev(SocketAsyncEventArgs args)
{
// 받을 때 마다 버퍼 안비워줘도 됨?
bool pending = _clientSocket.ReceiveAsync(args);
if (pending == false)
OnRecvCompleted(null, args);
}
void OnRecvCompleted(object obj, SocketAsyncEventArgs args)
{
if(args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
{
string chat = Encoding.UTF8.GetString(args.Buffer, args.Offset, args.BytesTransferred);
Console.WriteLine(chat);
RegisterRecev(args);
}
else
{
// OnDisconnect
// tcp의 경우 연결이 끊기지 않은 이상 보낼 때 오류나면 재전송하는데
// 바로 끊어버리면 문제 생기지 않나?
Console.WriteLine(args.SocketError.ToString());
}
}
}
}
해결해야할 의문점들
탐구 후에 답변을 작성하였습니다
1) Receive나 Send 도중 Disconnect가 발생하면 어떻게 되나요?
: 만약 적절한 연결 끊김 확인(args.BytesTransferred, args.SocketError 확인) 및 Disconnect에 관한 처리가 제대로 이루어지지 않는다면 좀비pc(네트워크 연결이 끊어졌으나 소켓이 열린 채로 남아 있는 상태)가 될 수 있으므로 주의가 필요합니다.
만약 Socket.Shutdown()을 통해 Disconnect 처리를 한다면 송수신 버퍼에 남아있는 데이터 까지는 처리가 가능합니다. socket.Close()를 사용해 연결을 바로 끊어버리게 된다면 송수신 버퍼에 남아있는 데이터는 버려지게됩니다.
위 코드에선 그냥 강의에서 있어다는 이유만으로 추가 했었는데 추가로 공부하며 Disconnect가 필요한 이유에 대해서 알 수 있었습니다.
2) TCP 전송 중 오류가나서 전송이 실패한다면 args.BytesTransferred가 0 이거나 SocketError가 발생하여 if문에 의해 Disconnect 처리가 된다면 문제가 되지 않나요? tcp는 데이터 전송을 보장하니까요
: 비동기 Send의 경우 네트워크 상태가 좋지 않거나 일시적으로 전송이 지연되면 자동으로 SendAsync()가 호출되어 전송작업을 진행하고 비동기 Receive 같은 경우는 ReceiveAsync가 차단되지 않으며 계속 대기합니다. 즉, 송신 및 수신이 실패했다고 하더라도 바로 송수신 완료처리가 이루어지는 것이 아니라 내부적으로 송수신이 완료될 때 까지 처리를 진행하기 때문에 문제 없습니다.
물론 네트워크 지연이 너무나 심하다거나 계속해서 오류가 난다면 아마 timeout 처리를 통해 disconnect하게 될 것 입니다. 실제 게임에서도 네트워크 문제가 생기면 연결이 끊겨버리니까요.
3) Receive 받을 때 마다 계속 args.Buffer를 통해 값을 받아들여 처리하게 되는데 버퍼를 비워주지 않아도 괜찮은가요? 데이터가 겹치거나 그러는건 아닌지..
: 사실 지금 코드 자체가 문제가 있는 편입니다. _recvBuffer가 전역변수이기 때문에 OnReceiveCompleted()가 여러 스레드에서 동시에 실행 된다면 Send되어 온 데이터를 정확히 처리할 수 없을 것 입니다. 이를 해결하기 위해선 RegisterReceive() 부분에 args.Buffer를 args.SetBuffer(new byte[1024], 0, 1024); 식으로 계속 새로 할당해줘야만 문제가 발생하지 않습니다. (이 경우 OnReceiveCopleted()가 동시 실행된다고 하더라도 매개변수로 넘어오는 args마다 buffer가 따로 설정되어 있기 때문에 값이 겹쳐버리는 문제점이 해결됩니다.)
그러나 위에 해결방법 자체도 문제가 존재합니다. RegisterReceive(args) 재호출을 반복하면서 계속 new byte를 통해 버퍼를 만들어주니 메모리가 점점 쌓이는 문제점이 발생합니다. RegisterReceive(args)가 호출되고 new byte를 하는 시점에서 기존 바이트 배열의 참조가 끊어지면서 Garbage Collector에 의해 회수 될 수 있으나 그 회수 시점을 정확하게 알 수 없기 때문입니다.
따라서 이 문제를 해결하기 위해선 아주 큰 버퍼를 만들어 잘라쓴다거나 버퍼를 잘라쓰고 다시 회수시키는 버퍼풀 방식으로 개선할 수 있을 것 입니다.
위 문제를 토대로 다시 코드를 수정하였습니다.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace ServerCore
{
class Session
{
Socket _clientSocket;
int _disconnected = 0;
public void OnConnected(EndPoint endPoint)
{
Console.WriteLine($"Connected : {endPoint}");
}
public void OnDisconnectd()
{
}
public void OnRecv()
{
}
public void OnSend()
{
}
public void Disconnect()
{
// _disconnect를 1로 바꾼다.
// 만약 이미 1 였다면(이미 disconnect한 경우) return;
if (Interlocked.Exchange(ref _disconnected, 1) == 1)
return;
_clientSocket.Shutdown(SocketShutdown.Both);
_clientSocket.Close();
}
public void Start(Socket socket)
{
// 비동기 recv
_clientSocket = socket;
SocketAsyncEventArgs recvArgs = new SocketAsyncEventArgs();
recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);
RegisterRecev(recvArgs);
}
void RegisterRecev(SocketAsyncEventArgs args)
{
args.SetBuffer(new byte[1024], 0, 1024);
bool pending = _clientSocket.ReceiveAsync(args);
if (pending == false)
OnRecvCompleted(null, args);
}
void OnRecvCompleted(object obj, SocketAsyncEventArgs args)
{
if(args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
{
string chat = Encoding.UTF8.GetString(args.Buffer, args.Offset, args.BytesTransferred);
Console.WriteLine(chat);
RegisterRecev(args);
}
else
{
// OnDisconnect
Console.WriteLine(args.SocketError.ToString());
Disconnect();
}
}
}
}
recvBuffer의 경우 Send 구현 이후 SendBuffer와 같이 추가할 예정.
'C# > 네트워크 관련' 카테고리의 다른 글
스스로 네트워크 프로그래밍 #5 - ReceiveBuffer (0) | 2025.02.24 |
---|---|
스스로 네트워크 프로그래밍 #4 - 비동기 Send (0) | 2025.02.13 |
스스로 네트워크 프로그래밍 #2 - 비동기 Accept, Connect (1) | 2025.01.20 |
스스로 네트워크 프로그래밍 #1 - 동기 socket 연결 (0) | 2025.01.19 |
JobTimer (0) | 2025.01.16 |