Q1. DummyClient와 Server에서 ServerCore를 이용해서 동작하는 프로세스를 설명해 보세요.
Q2. Connector를 만들어 주는 이유를 두가지 말해 보세요.
Q3. 작업한 내용을 나열해 보세요.
Q4. DummyClient, Server, ServerCore Connector, ServerCoreListener, ServerCore Session이 어떻게 상호작용하며 동작하는지 설명해주세요
답
1.
- ServerCore에서는 Listener를 이용하여 클라이언트의 접속을 대기합니다.
- 클라이언트가 접속하면, Listener에서 Accept 함수를 호출하여 클라이언트와 연결된 소켓(Socket)을 생성합니다.
- 생성된 소켓은 Connector를 이용하여 데이터 송수신을 처리합니다.
- 데이터 송수신 과정에서는 Send 함수를 이용하여 데이터를 전송하고, Receive 함수를 이용하여 데이터를 수신합니다.
- 수신된 데이터는 처리 후, 다시 Send 함수를 이용하여 클라이언트에게 전송됩니다.
- 클라이언트와의 연결이 끊어지면, 해당 소켓은 Close 함수를 호출하여 종료됩니다.
2.
Connect, Recieve, Send는 서버 클라 공용으로 쓰면 좋기 떄문에, 다른 서버에 Connect 방식으로 연결하기 위해
3.
ServerCore project에 Connector 클래스 파일을 추가한다 -> IPEndPoint를 매개변수로 받는 Connect 함수를 만들어 준다 소켓 만드는 부분을 넣어주는데 DummyClient의 휴대폰 설정 부분(new Socket)을 복붙한다 SocketAsyncEventArgs를 받아주는 RegisterConnect 함수를 만든다. sender와 SocketAsyncEventArgs를 받는 OnConnectCompleted 함수를 만든다. Connect 안에서 SocketAsyncEventArgs args를 인스턴스화하고 args.Completed 에 +=로 OnConnectCompleted를 연결한다. args.RemoteEndPoint에 endPoint를 넣어준다. args.UserToken을 이용해서 socket을 RegisterConnect에 socket을 넘겨준다. -> 그 다음에 RegisterConnect를 호출해 매개변수로 args를 넘겨줄거야. 이제 RegisterConnect에서는 소켓을 추출해야 한다. Connect에서 UserToken에 넘겨줬는데 UserToken은 Object 타입이니까 Socket으로 변형을 해줘서 Socket변수로 받는다. 제대로 받았는지 socket 변수 null 체크를 하고, socket.ConnectAsync(args)를 해주고 반환값을 pending에 넣어준다. pending이 false면 OnConnectCompleted를 호출해 args 이벤트를 넘겨주면 된다. -> OnConnectCompleted에서는 SocketError를 체크하고, 실패했으면 로그를 출력해준다. -> Func<Session> _sessionFactory;를 Connector클래스에 선언해준다. -> Connect 함수의 매개 변수에 Func<Session> sessionFactory를 추가해준다. 그걸 멤버변수 _sessionFactory에 넣어주고, -> OnConnectCompleted 함수에서 _sessionFacotry.Invoke();를 해 return 값을 Session session에 넣어준다. 그리고 session.Start를 args.ConnectSocket을 매개변수로 넣어 실행한다. 그리고 session.OnConnected에 args.RemoteEndPoint를 넣어 호출해준다.
ServerCore 프로젝트의 속성으로 가서 OutputType을 classLibrary로 바꿔준다. Server와 DummyClient 프로젝트에서 ServerCore를 참조하게 하자. ServerCore의 Program.cs의 코드를 Server로 이전시키자 Session, Listner, Connector 클래스에 public을 붙여주자 Solution의 속성에 가서 DummyClient, Server를 Start로 설정해주자. ->Server의 GameSession 클래스를 DummyClient에 복붙한다. DummyClient의 Main에 있던 [보낸다] 부분을 GameSession의 OnConnected로 옮겨준다. OnConnected의 버퍼를 보내는 코드는 삭제한다. Send함수를 Socket이 아닌 Session 클래스의 인터페이스를 사용하게 수정한다. DummyClient의 Main에 있던 버퍼를 만들어주고 Recieve해주던 [받는다] 부분도 Connector의 OnConnectComplted의 session.Start를 호출할 때 Start에서 SetBuffer를 해주기 때문에 삭제한다. OnRecv의 메시지를 From Server로 바꿔준다.
DummyClient에서 Connector 사용하기 -> DummyClient의 Main에서 connector를 생성하고, connector.Connect에 GameSession을 만드는 람다식 () => {return new GameSession();}을 Connect의 매개변수 sessionFactory에 연결해준다. -> Main의 소켓 만들고, 입장하고 나가는 부분 지워준다.
4.
- DummyClient는 Connector를 이용하여 Server에 접속합니다.
- Server는 Listener를 이용하여 클라이언트의 접속을 대기합니다.
- 클라이언트가 접속하면, Listener에서 Accept 함수를 호출하여 클라이언트와 연결된 소켓(Socket)을 생성합니다.
- 생성된 소켓은 Connector를 이용하여 데이터 송수신을 처리합니다.
- 데이터 송수신 과정에서는 Send 함수를 이용하여 데이터를 전송하고, Receive 함수를 이용하여 데이터를 수신합니다.
- 수신된 데이터는 처리 후, 다시 Send 함수를 이용하여 클라이언트에게 전송됩니다.
- 서버에서는 ServerCore Listener가 클라이언트의 접속을 대기하고 있습니다.
- 클라이언트가 접속하면, Listener에서 Accept 함수를 호출하여 클라이언트와 연결된 소켓(Socket)을 생성합니다.
- 생성된 소켓은 ServerCore Connector를 이용하여 데이터 송수신을 처리합니다.
- 데이터 송수신 과정에서는 Send 함수를 이용하여 데이터를 전송하고, Receive 함수를 이용하여 데이터를 수신합니다.
- 수신된 데이터는 처리 후, 다시 Send 함수를 이용하여 클라이언트에게 전송됩니다.
- ServerCore Session은 클라이언트와의 연결을 담당하며, 클라이언트로부터 수신된 데이터를 처리합니다.
- 처리된 데이터는 ServerCore Connector를 이용하여 클라이언트에게 전송됩니다.
위와 같은 과정을 통해 DummyClient, Server, ServerCore Connector, ServerCore Listener, ServerCore Session 간의 상호작용 및 동작이 이루어집니다.
이전 시간 코드
DummyClient
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace DummyClient
{
internal class Program
{
static void Main(string[] args)
{
// DNS ( Domain Name System )
string host = Dns.GetHostName();
IPHostEntry ipHost = Dns.GetHostEntry(host);
IPAddress ipAddr = ipHost.AddressList[0];
IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);
// ipAddre는 식당 주소, 7777은 식당 정문인지 후문인지 문의 번호 }
// 식당 주소 찾는 부분은 똑같을 거야.
while (true)
{
// 휴대폰 설정
Socket socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
try
{
// 문지기한테 입장 문의
**socket.Connect(endPoint);**
Console.WriteLine($"Connected To {socket.RemoteEndPoint.ToString()}");
// 보낸다
for (int i = 0; i < 5; i++)
{
byte[] sendBuff = Encoding.UTF8.GetBytes($"Hello World! {i}");
int sendBytes = 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();
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
Thread.Sleep(100);
}
}
}
}
Listener의 반대 역할을 하는 Connector를 만들어 볼거야. (공식 명칭은 아냐)
DummyClient에서 보면 Connect 부분을 블로킹 함수로 사용하고 있었어.
try
{
// 문지기한테 입장 문의
socket.Connect(endPoint);
그런데 게임에서는 블로킹 함수를 쓰는 걸 지양해야 하기 때문에 Connector는 이 부분을 담당하게 된다.
Listener는 서버 입장에서 연결을 기다리는 애였고, Connector라는 애는 반대 역할을 하게 될거야.
ServerCore project에 추가→new Item Connector라는 이름으로 추가한다.
서버에 왜 Connector가 필요한 걸까? Listener만 있으면 되는거 아닌가?
두가지 이유 있어.
- ServerCorer는 서버를 메인 용도로 만들고 있지만 Connect하고 Recieve, Send하는 부분은 공용으로 사용하면 좋을거야.
- 이 보다 더 큰 이유는 서버를 콘텐츠로 올릴 때 MMO 같은 경우는 서버를 하나 짜리로 만들 것인지, 분산 처리 해서 어떤 서버는 NPC, AI만 담당하는 역할만 하고, 어떤 서버는 Monster, 아이템, 필드 등 나머지 컨텐츠를 관리하는 분할해서 만드는 경우가 있다. 그런 경우를 생각해 보면 메인 서버로 작동하는 애 하나 있긴 하겠지만 걔가 반대로 다른 서버와 통신 하기 위해서는 다른 서버에 connect 방식으로 연결하긴 해야 한다. 분산 서버를 구현 한다는 건 서버가 여러개 있을 때 걔네들끼리 연결하기 위해서는 한쪽은 Listener역할을 해야 하고 다른쪽은 커넥트를 해서 한번은 연결을 해주긴 해야한다. 그래서 서버끼리 통신하기 위해서는 Connector가 필수적으로 들어가야 하니까 굳이 더미 클라이언트나 유니티에서 얘를 사용하지 않을 예정이라 해도 이런 식으로 반대쪽으로 연결을 해줘야 할 애는 하나씩 꼭 만들어줘야 할 필요성이 있게 된다.
DummyClient의 코드를 옆에 옮겨서 커닝을 하면 만들어 보도록 하자.
DummyClient의 Main을 보면
static void Main(string[] args)
{
string host = Dns.GetHostName();
IPHostEntry ipHost = Dns.GetHostEntry(host);
IPAddress ipAddr = ipHost.AddressList[0];
**// IPEndPoint라는 주소를 이용해서**
**IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);**
while (true)
{
**// 소켓을 만들어준 다음에**
**Socket socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);**
try
{
**// 소켓에서 뭔가 연결을 해줬었어.**
**socket.Connect(endPoint);**
이게 전부였어.
Connector도 인터페이스를 만들어 줄 것인데
Listener 처럼 `Init이라 만들어 줘도 되는데, 그냥 Connect라는 이름으로 만들어 줄거야.
class Connector
{
Func<Session> _sessionFactory;
public void Connect(IPEndPoint endPoint, Func<Session> sessionFactory) // 처음에 받아야 할 매개변수는 endPoint야.
{
// 소켓을 만들어 줘야 하는데 DummyClient의 코드를 복사해주자.
// 휴대폰 설정
Socket socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
_sessionFactory = sessionFactory;
SocketAsyncEventArgs args = new SocketAsyncEventArgs();
args.Completed += OnConnectCompleted;
args.RemoteEndPoint = endPoint; // 연결하는 애는 상대방애를 연결해 줘야해
args.UserToken = socket; // 소켓 넘겨주는 방법 class 멤버변수 선언하거나 여러 방법 있었지만 여기선 UserToken 사용해본다.
RegisterConnect(args);
}
// Listener, Send, Recieve 에서 했던 것 처럼 RegisterConnect를 만들어 준다.
void RegisterConnect(SocketAsyncEventArgs args) // 매개 변수로 이걸 받아 줄거고
{
// 소켓을 추출해야 하는데 Connect에서 UserToken에 넘겨 줬어. UserToken을 Socket으로 변환해야해.
Socket socket = args.UserToken as Socket; // 멤버 변수로 받지 않은 이유는 Connect를 하나만 하는 경우도 있겠지만 Listener에서 계속 돌면서 1000명이건 10000명이건 받을 수 있던 것처럼 Connector도 여러명 받을 수 있으니까 Event로 인자를 넘겨주고 있는거.
if(socket == null)
return;
bool pending = socket.ConnectAsync(args); // 여기 Connect도 Async 버전이 있는 걸 볼 수 있다.
if(pending == false)
OnConnectCompleted(null, args); // 바로 연결 됐으면 이벤트를 넘겨주면 된다.
}
void OnConnectCompleted(object sender, SocketAsyncEventArgs args) // 인자는 안봐도 이거니 그냥 만들어 준다.
{
// 늘 하던 거를 하면 된다.
if(args.SocketError == SocketError.Success)
{
Session session = _session.Invoke(); // 여기서 세션을 만들어 줘야 하는데 지난 시간에 Session을 만든걸 보면 abstract로 만들어 놨었어.
// 어떤 세선이 필요한지 모르니까 Engine단에서는 직접 new Session을 할 수 없었다. 그래서 지난 시간에 했던 방법을 다시 보면
// Func<Session> _sessionFactory를 받아가지고, 어떤 세션을 만들어 줄까를 인자로 받았어. Connect도 같은 인터페이스로 맞춰주자.
// Connector 클래스에도 _sessionFactory 변수를 선언하고, Connect의 인자로 Func<Session> sessionFactory를 받아서 Connect 하는 순간에 어떤 식으로 만들어줄까를 넘겨주게 된다.
// 여기서 _sessionFactory.Invoke()를 해서 컨텐츠단에서 요구한 방식대로 Session을 만들어 줄거야.
// DummyClient로 치면 이제 소켓 파주고, Connect까지 한 다음에, [보낸다,받는다,나간다] 단계로 오게 된다.
// 만약 연결이 되었으면 Start를 해가지고 args.ConnectSocket이 있을거야.
session.Start(args.ConnectSocket);
// 애당초 session을 처음에 Start하기 위해서는 Session클래스에 가보면 Socket socket을 매개 변수로 넣어줘야 한다.
// Socket이 있어야만 RegisterRecv를 할 때 _socket을 기반으로 _socket.ReceiveAsync(_recvArgs); ReccAsync를 한번 예약을 해준것을 알 수 있다.
// 기본적으로 session을 사용하려면 결국 Socket이 연결이 되어 있어야 하는데 인자로 넘겨준 ConnectSocket으로 뱉어주게 될거야.
// 일단 Start를 하면 자동으로 recv 이벤트가 등록이 된 상태 일거고,
session.OnConnected(args.RemoteEndPoint); // 이렇게 연결해 주면 된다.
// 결국 Listener과 대칭적인 방식으로 만들어 줬다는 걸 알 수 있다.
}
else
{
Console.WriteLine($"OnConnectCompleted Fail: {args.SocketError}");
}
}
참고로 session.Start(args.ConnectSocket);의 설명이 나온다.
새로 만들어준 소켓이 여기 ConnectAsync가 끝난 다음에 ConnectSocket 여기에다가 Socket이 붙어 주는 거고,
물론 UserToken에다 socket을 넣긴 했어. 얘를 통해서 똑같이 여기 session.Start()에 넘겨줘도 똑같이 작동 할거야.
어쨌든 이게 더 세련된 거 같으니까 ConnectSocket을 이용하기로 할거야.
여기까지 만들었으면 테스트 해봐야 해.
우리가 작업하고 있던 ServerCore같은 경우는 서버 용도로 사용하고 있었고, 클라 용도는 DummyClient에 있었어.
Connector를 DummyClient 에서 사용할 수 없어.
그렇다고 Connector 코드를 DummyClient에 복사 하는 건 세련되지 않아. 이제 새로운 방법을 사용해 보자.
ServerCore는 라이브러리로만 사용하기로 했으니 세팅해줄 거야.
ServerCore의 속성으로 가면 설정을 변경할 수 있다.
Class Library로 바꿔준다.
ServerCore를 시작 프로젝트로 설정한 다음에 시작을 해본다.
이제는 에러가 나는 것을 볼 수 있다.
얘는 이제 라이브러리 형식으로만 사용되는 애라서 독립적으로 사용할 수 없다는 말이다. 다른 애한테 기생해서 간접적으로 실행해야 된다는 말이다.
Server랑 DummyClient에서 ServerCore를 참조하도록 해주자.
Server에서 우클릭해서 추가→참조를 누른다.
이렇게 ServerCore를 체크해 주면 된다.
마찬가지로 DummyClient에서도 ServerCore를 참조 해주면 된다.
이제 ServerCore의 Program의 코드를 Server쪽에다가 이전을 시켜줄 거야.
전부 ctrl+x해서 Server의 Program에 붙여넣기를 한다. using도 이전을 시켜준다.
using ServerCore;도 추가해준다.
보호수준 문제로 이렇게 뜨니까 Session으로 가서
public abstract class Session
지금까지는 같은 프로젝트에서 사용하고 있었지만 이제 아니니 이렇게 public을 붙여준다. Listener와 Connector에도 public을 붙여준다.
그리고 ServerCore의 Program.cs는 더이상 사용 안하니 삭제한다.
이제 빨간줄이 사라졌다.
Server가 컨텐츠 단, ServerCore가 엔진단이 된거다. 컨텐츠 단에서는 안에서는 뭘 하는지 모르겠지만 Session 인터페이스만 갖다가 사용을 하고, Event시리즈들만 재정의 해서 사용하고 있다.
그러면 아까와 똑같이 작동을 할거야.
Solution의 속성으로 가서
이렇게 해준다.
이제 DummyClient로 가서 방금 작업한 Connector를 여기에 연결시켜 줄거야.
기존에 작업하던 블로킹 방식을 새로 만든 Connector 방식으로 바꿔 준다는 얘기.
Connector를 사용하려면 Connect 매개변수로 SessionFactory를 지정해줘야 하고, SessionFactory에서는 어떤 식으로든 Session을 만들어줘야 한다.
서버에서 사용했던 GameSession을 비슷하게 사용하는 거니, GameSession 클래스 복사 해서 DummyClient에다 붙여 넣는다. 이전한 코드에서 달라지는 부분은 DummyClient의 Main에 있던 [보낸다] 작업을 GameSession의 OnConnected로 옮긴다.
public override void OnConnected(EndPoint endPoint)
{
Console.WriteLine($"OnConnected : {endPoint}");
//byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to MMORPG Server !!");
//Send(sendBuff);
//Thread.Sleep(5000);
//Disconnect();
// 이 코드들은 삭제한다.
**// 보낸다
for (int i = 0; i < 5; i++)
{
byte[] sendBuff = Encoding.UTF8.GetBytes($"Hello World! {i}");
// int sendBytes =** socket.Send(sendBuff);
**Send(sendBuff);
}**
}
그리고 Send 하는 경우는 이제 socket을 사용해 socket.Send(sendBuff)를 하는게 아니라 GameSession 자체가 Send라는 인터페이스가 있으니까 Send를 사용한다.
다시 DummyClient의 Main을 보면 보내고 받고 나가고 하고 있었어.
받을 때 보면 블로킹 계열의 Receive를 하고 있었는데
// 받는다
byte[] recvBuff = new byte[1024];
int recvBytes = socket.**Receive**(recvBuff);
string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytes);
Console.WriteLine($"[From Server] {recvData}");
이런식으로 버퍼를 실시간으로 만들어주고 있었어. 이건 삭제를 해도 되는게 우리가 사용하는 새로운 Session 코드에서는
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();
}
recv버퍼를 이렇게 연결하고 있엇어. 그래서 받는다 부분은 필요가 없다.
다만 OnRecv만 FromServer로 바꿔줘야 한다.
public override void OnRecv(ArraySegment<byte> buffer)
{
string recvData = Encoding.UTF8.GetString(buffer.Array, buffer.Offset, buffer.Count);
Console.WriteLine($"[From Server] {recvData}");
}
이제 GameSession은 만들어 줬으니까 Listener를 사용했던 거 처럼 Connector를 사용해줄거야.
static void Main(string[] args)
{
// DNS ( Domain Name System )
string host = Dns.GetHostName();
IPHostEntry ipHost = Dns.GetHostEntry(host);
IPAddress ipAddr = ipHost.AddressList[0];
IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);
**Connector connector = new Connector();
connector.Connect(endPoint, () => { return new GameSession(); });**
어떤 세션을 만들어 줄지 GameSession을 만드는 람다식을 Connect로 sessionFactory에 연결해준다.
그럼 얘는 Connect를 하면서 Connect에서는 RegisterConnect를 해줄거고, 만약 성공적으로 ConnectAsync가 완료가 되면 OnConnectCompleted가 호출이 될거야. 그럼 OnConnected로 다시 갈거고, 결국에는 처음에 매핑했던 OnConnected의 보낸다 부분이 실행이 될거야.
public override void OnConnected(EndPoint endPoint)
{
Console.WriteLine($"OnConnected : {endPoint}");
// 보낸다
for (int i = 0; i < 5; i++)
{
byte[] sendBuff = Encoding.UTF8.GetBytes($"Hello World! {i}");
**Send(sendBuff);**
}
}
이렇게만 하면 동작은 하긴 할텐데 try catch 구문을 빼놨어서 네트워크 연결 실패하면 크래쉬 나긴 할거야.
DummyClient Main의
소켓을 만들고 입장하고 이런 부분은 삭제해 줘도 된다.
while (true)
{
// 휴대폰 설정
// Socket socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
try
{
// 문지기한테 입장 문의
// socket.Connect(endPoint);
// Console.WriteLine($"Connected To {socket.RemoteEndPoint.ToString()}");
// 나간다
// socket.Shutdown(SocketShutdown.Both);
// socket.Close();
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
Thread.Sleep(100);
}
여기까지 작업을 했으니
static void Main(string[] args)
{
// DNS ( Domain Name System )
string host = Dns.GetHostName();
IPHostEntry ipHost = Dns.GetHostEntry(host);
IPAddress ipAddr = ipHost.AddressList[0];
IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);
Connector connector = new Connector();
**connector.Connect(endPoint, () => { return new GameSession(); });**
이제 세션 하나만 만든 채로 잘 동작을 하는지 테스트를 해 보자.
나중엔 OnConnected 한 다음에 SessionManager 같은데다가 이 GameSession을 기억하게 해줘야 한다.
지금은 일단 Send랑 Recieve가 동작하는지만 보고 싶으니까
일단 실행을 해보면
보내는 부분, 받는 부분 정상적으로 동작하는 거 테스트 할 수 있었다.
나중에 후반에 가면 스트레스 테스트도 하고 튜닝도 하며 안정성이 보장 되는지를 봐야겠지만 일단은
이렇게 우리가 작업하던 이 ServerCore 라이브러리에서 Server와 Client를 각각 라이브러리를 참조하 만드는 부분까지 수정을 했다.
아울러 Connector도 작업을 해놓은 상태다.
작업한 코드
DummyClient
using ServerCore;
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace DummyClient
{
class GameSession : Session
{
public override void OnConnected(EndPoint endPoint)
{
Console.WriteLine($"OnConnected : {endPoint}");
// 보낸다
for (int i = 0; i < 5; i++)
{
byte[] sendBuff = Encoding.UTF8.GetBytes($"Hello World! {i}");
Send(sendBuff);
}
}
public override void OnDisconnected(EndPoint endPoint)
{
Console.WriteLine($"OnDisconnected : {endPoint}");
}
public override void OnRecv(ArraySegment<byte> buffer)
{
string recvData = Encoding.UTF8.GetString(buffer.Array, buffer.Offset, buffer.Count);
Console.WriteLine($"[From Server] {recvData}");
}
public override void OnSend(int numOfBytes)
{
Console.WriteLine($"Transferred bytes: {numOfBytes}");
}
}
internal class Program
{
static void Main(string[] args)
{
// DNS ( Domain Name System )
string host = Dns.GetHostName();
IPHostEntry ipHost = Dns.GetHostEntry(host);
IPAddress ipAddr = ipHost.AddressList[0];
IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);
Connector connector = new Connector();
connector.Connect(endPoint, () => { return new GameSession(); });
while (true)
{
try
{
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
Thread.Sleep(100);
}
}
}
}
Server
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using ServerCore;
namespace Server
{
class GameSession : Session
{
public override void OnConnected(EndPoint endPoint)
{
Console.WriteLine($"OnConnected : {endPoint}");
byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to MMORPG Server !!");
Send(sendBuff);
Thread.Sleep(5000);
Disconnect();
}
public override void OnDisconnected(EndPoint endPoint)
{
Console.WriteLine($"OnDisconnected : {endPoint}");
}
public override void OnRecv(ArraySegment<byte> buffer)
{
string recvData = Encoding.UTF8.GetString(buffer.Array, buffer.Offset, buffer.Count);
Console.WriteLine($"[FromClient] {recvData}");
}
public override void OnSend(int numOfBytes)
{
Console.WriteLine($"Transferred bytes: {numOfBytes}");
}
}
class Program
{
static Listener _listener = new Listener();
static void Main(string[] args)
{
// DNS ( Domain Name System )
string host = Dns.GetHostName();
IPHostEntry ipHost = Dns.GetHostEntry(host);
IPAddress ipAddr = ipHost.AddressList[0];
IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);
// ipAddre는 식당 주소, 7777은 식당 정문인지 후문인지 문의 번호
//_listener.Init(endPoint, OnAcceptHandler);
_listener.Init(endPoint, () => { return new GameSession(); });
Console.WriteLine("Listening...");
while (true)
{
;
}
}
}
}
ServerCore Connector
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace ServerCore
{
public class Connector
{
Func<Session> _sessionFactory;
public void Connect(IPEndPoint endPoint, Func<Session> sessionFactory) // Connect하는 순간에 어떤 Session을 만들어줄까를 sessionFactory에 넘겨준다
{
// 휴대폰 설정
Socket socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
_sessionFactory = sessionFactory; // 넘겨 받은 어떤 세션 만들까에 대한 걸 변수에 넣어준다.
SocketAsyncEventArgs args = new SocketAsyncEventArgs();
args.Completed += OnConnectCompleted;
args.RemoteEndPoint = endPoint; // 연결하는 부분은 결국 상대방의 주소 endPoint를 넣어준다.
args.UserToken = socket; // socket을 RegisterConnect에 넘겨주고 싶은데 여러 방법이 있을텐데, UserToken이라 해서 원하는 정보를 넘겨줄 수가 있다.
RegisterConnect(args); // args를 넘겨준다.
}
void RegisterConnect(SocketAsyncEventArgs args)
{
Socket socket = args.UserToken as Socket; // UserToken을 Socket형으로 받는다.
// Socket _socket;을 class에 선언해서 받게 하지 않는 이유는 Connect를 하나만 받는 경우도 있겠지만 Listener처럼 뺑뻉이 돌면서 1000명이건 10000명이건 받을 수 있는 것처럼
// Connector도 한명만 받고 끝내는게 아닐 수 있으니까 굳이 Socket 변수로 받기 보다는 이벤트를 통해서 인자를 넘겨주고있는 것이다.
if (socket == null)
return;
bool pending = socket.ConnectAsync(args); // 얘도 Async 버전이 있었어.
if (pending == false)
OnConnectCompleted(null, args);
}
void OnConnectCompleted(object sender, SocketAsyncEventArgs args)
{
if (args.SocketError == SocketError.Success)
{
Session session = _sessionFactory.Invoke(); // Session을 abstract로 만들어 놨었어. 직접 new를 못하기 때문에
/// 지난 시간에 어떻게 했는지 컨닝을 해보면 Func<Session> _sessionFactory를 받아서 어떤 세션을 만들어 줄까를 인자로 받았었어.
// Connector도 같은 인터페이스로 맞춰주자.
// 컨텐츠 단에서 요구한 방식대로 세션을 만들어 줄거야.
session.Start(args.ConnectSocket); // 연결이 되었으면 Start를 한다
// Session.Start를 하려면 Socket을 넣어 줘야 해. Session의 Start코드를 보면 socket이 있어야만 나중에 RegisterRecv를 할 때 _socket을 기반으로 ReceiveAsync를 예약 해준 걸 알 수 있다.
// 기본적으로 Session을 사용할 때는 Socket이 연결되어 있어야 하는데 그걸 인자로 넘겨준 ConnectSocket으로 뱉어주게 될거야. 얘를 통해서 하면 된다.
// 이렇게 Start를 하면 Recv 이벤트가 자동으로 등록이 된 상태 거고,
session.OnConnected(args.RemoteEndPoint);
}
else
{
Console.WriteLine($"OnConnectCompletedFail: {args.SocketError}");
}
}
}
}
ServerCore listener
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace ServerCore
{
public class Listener
{
Socket _listenSocket;
//Action<Socket> _OnAcceptHandler;
Func<Session> _sessionFactory;
public void Init(IPEndPoint endPoint, Func<Session> sessionFactory)
{
// 문지기(가 들고있는 휴대폰)
_listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); // TCP로 할 때 설정
//_OnAcceptHandler += onAcceptHandler;
_sessionFactory += sessionFactory;
// 문지기 교육
_listenSocket.Bind(endPoint); // 식당 주소와 후문인지 정문인지 기입을 해준 것
// 영업 시작
// backlog : 최대 대기수
_listenSocket.Listen(10);
for (int i = 0; i < 10; i++)
{
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 sender, SocketAsyncEventArgs args)
{
if (args.SocketError == SocketError.Success) // 모든 게 잘 처리 됐다는 뜻
{
// 유저가 커넥트 요청 해서 Accept 했으면 여기서 뭔가를 해주고
// TODO
// _OnAcceptHandler.Invoke(args.AcceptSocket);
// GameSession session = new GameSession();
Session session = _sessionFactory.Invoke();
session.Start(args.AcceptSocket);
session.OnConnected(args.AcceptSocket.RemoteEndPoint);
}
else
Console.WriteLine(args.SocketError.ToString());
RegisterAccept(args); // 다음 아이를 위해서 또 한번 등록을 해주는 거
}
public Socket Accept()
{
return _listenSocket.Accept();
}
}
}
ServerCore Session
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace ServerCore
{
public abstract class Session
{
Socket _socket;
int _disconnect = 0;
object _lock = new object();
Queue<byte[]> _sendQueue = new Queue<byte[]>();
List<ArraySegment<byte>> _pendingList = new List<ArraySegment<byte>>();
SocketAsyncEventArgs _sendArgs = new SocketAsyncEventArgs();
SocketAsyncEventArgs _recvArgs = new SocketAsyncEventArgs();
public abstract void OnConnected(EndPoint endPoint);
public abstract void OnRecv(ArraySegment<byte> buffer);
public abstract void OnSend(int numOfBytes);
public abstract void OnDisconnected(EndPoint endPoint);
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 역할을 얘가 대신 해주는 거.
OnSend(_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;
OnDisconnected(_socket.RemoteEndPoint);
_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}");
OnRecv(new ArraySegment<byte>(args.Buffer, args.Offset, args.BytesTransferred));
RegisterRecv();
}
catch(Exception e)
{
Console.WriteLine($"OnRecvCompletedFailed {e}");
}
}
else
{
Disconnect();
}
}
#endregion
}
}
'Server programming' 카테고리의 다른 글
02_12_네트워크 프로그래밍_RecvBuffer (0) | 2023.04.06 |
---|---|
02_11_네트워크 프로그래밍_TCP vs UDP (0) | 2023.04.06 |
02_09_네트워크 프로그래밍_Session #4_엔진단과 컨텐츠단 분리 (0) | 2023.04.04 |
02_08_네트워크 프로그래밍_Session #3_BufferList (0) | 2023.04.04 |
02_07_네트워크 프로그래밍_Session #2_Send (0) | 2023.04.04 |
댓글