using System.Net;
using System.Net.Sockets;
using System.Text;
namespace DummyClient
{
internal class Program
{
static void Main(string[] args)
{
// DNS
string host = Dns.GetHostName();
IPHostEntry ipHost = Dns.GetHostEntry(host);
IPAddress iPAddr = ipHost.AddressList[0];
IPEndPoint endPoint = new IPEndPoint(iPAddr, 7777);
while (true)
{
// 휴대폰 설정
Socket socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
// 문지기한테 입장 문의
socket.Connect(endPoint);
Console.WriteLine($"Connected to {socket.RemoteEndPoint.ToString()}");
for(int i = 1; i <= 5; i++)
{
// 보낸다
byte[] sendBuff = Encoding.UTF8.GetBytes($"Hello World! {i}");
socket.Send(sendBuff);
}
// 받는다
byte[] recvBuff = new Byte[1024];
int recvBytes = socket.Receive(recvBuff);
string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytes);
Console.WriteLine($"[From Server] {recvData}");
//나간다
socket.Shutdown(SocketShutdown.Both);
socket.Close();
Thread.Sleep(1000);
}
}
}
}
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace ServerCore
{
internal class Program
{
static Listener _listener = new Listener();
static void OnAcceptHandler(Socket clientSocket)
{
Session session = new Session();
session.Start(clientSocket);
byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to MMORPG Server !");
session.Send(sendBuff);
Thread.Sleep(1000);
session.Disconnect();
}
static void Main(string[] args)
{
// DNS
string host = Dns.GetHostName();
IPHostEntry ipHost = Dns.GetHostEntry(host);
IPAddress iPAddr = ipHost.AddressList[0];
IPEndPoint endPoint = new IPEndPoint(iPAddr, 7777);
_listener.init(endPoint, OnAcceptHandler);
Console.WriteLine("Listening.... ");
while (true)
{
}
}
}
}
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
{
internal class Listener
{
Socket _listenSocket;
Action<Socket> _onAceeptHandler;
public void init(IPEndPoint endPoint, Action<Socket> onAceeptHandler)
{
_listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
_onAceeptHandler += onAceeptHandler;
// 문지기 교육
_listenSocket.Bind(endPoint);
// 영업 시작
// Backlog : 최대 대기 수
_listenSocket.Listen(10);
// callback 방식으로 동작시키기 위해 SocketAsyncEventArgs 객체 생성
SocketAsyncEventArgs args = new SocketAsyncEventArgs();
// args.Completed 이벤트에 OnAccpetComplete 메서드 등록
// args.Completed는 EventHandler라는 delegate의 이벤트로
// EventHandler는 object와 SocketAysncEventArgs를 매개변수로 받음
// 따라서 똑같은 매개변수를 갖는 OnAccpetComplete 메서드를 등록 가능
// 만약 비동기 Accpet가 완료된다면 신호를 보내 OnAccpetComplete 함수가
// 실행되게 됨.
args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAccpetComplete);
// 직접 등록해줘서 펜딩이 일어나지 않는다면 if문에서 처리될거고
// 펜딩이 일어난다면 바로 위에서 콜백방식으로 실행됨
RegisterAccept(args);
}
void RegisterAccept(SocketAsyncEventArgs args)
{
// 재사용하기때문에 이전것이 남아있으면 안되므로 null로 초기화
args.AcceptSocket = null;
bool pending = _listenSocket.AcceptAsync(args);
// 빨리 처리되어 펜딩이 일어나지 않는다면
// 펜딩이 일어난다면 callback 방식으로 if문 내 구문이 실행되게됨
if (pending == false)
OnAccpetComplete(null, args);
}
void OnAccpetComplete(object obj, SocketAsyncEventArgs args)
{
// 에러없이 성공 되었다면
if(args.SocketError == SocketError.Success)
{
// 클라이언트 소켓을 전달받는 부분과 동치
// _OnAceeptHandler에 등록된 함수가 args.AcceptSocket을
// 매개변수로 받아 실행된다.
// 이떄 args.AccetSocket은 동기 연결 방식에서 Accpet()함수로
// 만들어졌던 클라이언트소켓과 동일함
_onAceeptHandler.Invoke(args.AcceptSocket);
}
else
Console.WriteLine(args.SocketError.ToString());
// 다시 연결을 비동기로 받아줌 -> 연결 반복
RegisterAccept(args);
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http.Headers;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace ServerCore
{
internal class Session
{
Socket _socket;
int _disconnectd = 0;
public void Start(Socket socket)
{
_socket = socket;
SocketAsyncEventArgs recvArgs = new SocketAsyncEventArgs();
recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnReceiveCompleted);
// Accept는 받으면 받은 socket 정보가 args에 남는데
// recv는 없어서 이렇게 따로 설정해주는듯
recvArgs.SetBuffer(new byte[1024], 0, 1024);
RegisterReceive(recvArgs);
}
public void Send(byte[] sendBuff)
{
_socket.Send(sendBuff);
}
public void Disconnect()
{
if (Interlocked.Exchange(ref _disconnectd, 1) == 1)
return;
_socket.Shutdown(SocketShutdown.Both);
_socket.Close();
}
#region 네트워크 통신
void RegisterReceive(SocketAsyncEventArgs args)
{
bool pending = _socket.ReceiveAsync(args);
if (pending == false)
OnReceiveCompleted(null, args);
}
void OnReceiveCompleted(object sender, SocketAsyncEventArgs args)
{
// BytesTransferred -> 몇 byte를 전송 받았는가
// 0인 경우 상대방이 연결을 끊은 것
if(args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
{
string recvData = Encoding.UTF8.GetString(args.Buffer, args.Offset, args.BytesTransferred);
Console.WriteLine($"[From Data] {recvData}");
// accept와 다르게 if문 밖에서 호출하지 않는 이유는
// accpet는 연결 안되면 걍 버리고 다른 연결 기다리면 되는데
// 얘는 연결 상태로 에러 난거니까 다시 receive하면 안됨
RegisterReceive(args);
}
else
{
}
}
#endregion
}
}
+ Disconnect()의 Interlocked 처리에 관하여
주어진 코드에서는 여러 스레드가 동시에 Disconnect() 함수를 실행하는 상황이 발생하지 않을 가능성이 높습니다. 현재 코드 구조를 보면, 각 클라이언트에 대해 하나의 스레드 또는 비동기 작업이 사용되고 있기 때문입니다. 이를 좀 더 구체적으로 살펴보겠습니다.
코드 구조 분석
- Listener 클래스:
- Listener는 서버 소켓을 생성하고 클라이언트의 연결 요청을 비동기적으로 처리합니다. 클라이언트가 연결되면 SocketAsyncEventArgs 콜백으로 OnAccpetComplete가 호출되며, 여기서 클라이언트 소켓을 받아서 OnAcceptHandler로 전달합니다.
- 중요한 점은 클라이언트 연결은 순차적으로 처리되며, 각 클라이언트는 OnAcceptHandler로 독립된 Session 객체를 통해 처리된다는 것입니다.
- Program 클래스:
- OnAcceptHandler가 클라이언트 소켓을 전달받고, 새로운 Session 객체를 생성한 후 해당 클라이언트 소켓을 처리합니다. 이때, Send()와 Disconnect()가 호출되는데, 동일한 소켓에 대해 하나의 Session 객체만 처리하므로, 다른 스레드가 동일한 클라이언트 소켓에 대해 Disconnect()를 동시에 호출하는 일이 발생하지 않습니다.
- Session 클래스:
- Session 클래스는 각 클라이언트와의 통신을 담당합니다. Start() 메서드에서 클라이언트 소켓을 전달받고, 비동기적으로 데이터를 수신하거나 송신합니다.
- Disconnect()는 주로 하나의 스레드에서 실행되며, 각 클라이언트에 대해 독립적으로 실행됩니다.
- 멀티스레드 환경에서는 여러 클라이언트가 동시에 서버에 접속할 수 있지만, 각 클라이언트에 대해 독립된 Session 인스턴스가 생성되므로, 여러 클라이언트의 소켓이 서로 간섭할 일은 없습니다.
멀티스레드 환경에서 Disconnect() 충돌 가능성
현재 구조에서는 클라이언트 소켓을 처리하는 스레드는 하나이고, 각 소켓은 하나의 Session에서만 관리됩니다. 따라서, 동일한 소켓에 대해 여러 스레드가 Disconnect()를 동시에 호출하는 상황은 사실상 없습니다. 각 클라이언트 소켓에 대해 독립적으로 처리되기 때문에, 동시 호출을 방지하기 위한 별도의 처리(예: Interlocked.Exchange)는 필요하지 않을 수 있습니다.
하지만, 다음과 같은 상황에서는 충돌 가능성이 생길 수 있습니다:
- 예외 처리 중 복수의 경로에서 Disconnect()를 호출: 예를 들어, 통신 중에 예외가 발생하고, 예외 처리에서 Disconnect()를 호출하는 동시에 다른 코드에서 수동으로 Disconnect()를 호출하는 경우.
- 타이머나 별도의 관리 스레드에서 연결을 강제로 종료하려는 경우: 서버에서 별도의 타이머 스레드가 타임아웃된 클라이언트 소켓을 닫으려 할 때, 이미 다른 곳에서 Disconnect()가 호출되고 있을 수 있습니다.
결론
현재 코드에서는 여러 스레드가 동시에 같은 클라이언트 소켓에 대해 Disconnect()를 호출하는 상황이 발생하지 않을 가능성이 높습니다. 각 클라이언트 소켓은 독립된 Session 객체에서 처리되므로, 중복 호출이 일어날 가능성은 낮습니다.
그러나, 잠재적인 예외 상황이나 타이머 기반 관리 로직이 추가될 경우에는 여러 스레드에서 같은 클라이언트 소켓에 대해 동시에 Disconnect()를 호출하는 위험이 있을 수 있습니다. 이런 경우에 대비해 Interlocked.Exchange와 같은 동기화 기법을 적용해 중복 호출을 방지하는 것은 좋은 방어적 프로그래밍 기법입니다.
'C# > 네트워크 관련' 카테고리의 다른 글
Session #5 (send 방식 수정) (0) | 2024.10.27 |
---|---|
Socket 실습 #4 (non-blocking Send) (0) | 2024.10.25 |
Socket 연결 실습 - 2 (0) | 2024.10.01 |
Socket 연결 실습 (0) | 2024.09.29 |
Thread Local Storage 예시 (2) | 2024.09.23 |