Server programming

02_05_네트워크 프로그래밍_Listener

devRiripong 2023. 4. 4.
반응형

Q1. Listener를 만들었는데 Accept만 논블로킹 방식(비동기 방식)으로 만들었을 때 작동하는 흐름을 설명해 보세요.

Q2. Accept 함수가 논블로킹 방식으로 작동해야 하는 이유에 대해 설명해 보세요

Q3. 클라이언트가 연결되면 Listener가 수행하는 작업에 대해 말해 보세요

Q4. 서버를 구현할 때 Listener를 사용하는 방법에 대해 말해 보세요.

Q5. 작업한 과정을 말해보세요. 

 

1.Listener가 비동기 방식으로 작동할 때의 흐름: Listener는 클라이언트의 연결 요청을 받아들이는 역할을 합니다. 비동기 방식으로 작동할 때에는, Listener가 연결 요청을 받으면 해당 요청을 처리하는 새로운 스레드를 생성하여 처리합니다. 이렇게 하면, 다수의 클라이언트 요청을 동시에 처리할 수 있습니다.

2.Accept 함수가 논블로킹 방식으로 작동하는 이유: Accept 함수는 클라이언트의 연결 요청을 받아들이는 함수입니다. 이 함수가 블로킹 방식으로 작동하면, 다른 클라이언트의 연결 요청을 처리하지 못하고 대기 상태에 머무르게 됩니다. 따라서, Accept 함수는 논블로킹 방식으로 작동하여 다른 클라이언트의 연결 요청도 처리할 수 있도록 합니다

3.클라이언트가 연결되면 Listener가 수행하는 작업: 클라이언트가 연결되면, Listener는 해당 클라이언트와 통신하기 위한 소켓(Socket) 객체를 생성합니다. 이 소켓 객체를 이용하여 서버와 클라이언트 간의 데이터 통신을 수행합니다.

4.서버를 구현할 때 Listener를 사용하는 방법: 서버를 구현할 때에는, Listener를 사용하여 클라이언트의 연결 요청을 받아들이고, 해당 클라이언트와 통신하기 위한 소켓 객체를 생성합니다. 이후에는, 생성된 소켓 객체를 이용하여 서버와 클라이언트 간의 데이터 통신을 수행합니다.

5.새 클래스 Listener에 _listenSocket 변수를 선언해 주고, Init에 listen socket(문지기),  Bind(문지기 교육), Listen(영업 시작) 초기화 코드 넣음. -> Accept 함수 생성후 _listenSocket.Accept() 실행 후 결과 Socket return -> 임시로 class Program에 Listener 클래스 변수 생성 -> Main에서 _listner.Init 호출 -> Main에서 선언한 _listener.Accept로 변경 후 논 블로킹으로 변경 시작 -> Listener로 가서 RegisterAccept, OnAcceptCompleted 함수 생성 -> RegisterAccept의 매개변수로 SocketAsyncEventArgs를 넣어주고 _listenSocket.AcceptAsync(args)를 실행 후 return 값으로 pending 판정 후 아니면 OnAcceptCompleted 호출 -> Init에 SocketAsyncEvenrtArsgs args 변수 선언 후 instance화 하고 args.Completed에 OnAcceptCompleted 연결 -> Init에 RegisterAccept(args)로 최초 한번 등록 ->  유저가 왔으면 해야할 일 Init 매개 변수onAcceptHandler로 받아서 Action 변수 _OnAcceptHandler에 연결시켜 주고, OnAcceptCompleted에서 Invoke 하기, Invoke인자로 args.AcceptSocket 넣기 ->  Main 위에 OnAcceptHandler 함수 만들고 거기에 손님 오면 할 행동인 [받는다, 보낸다, 쫒아낸다] 넣기. -> Main의 _listener.Init에 OnAcceptHandler 함수 전달하기 -> Main의 while문은 계속 돌아가는 걸로 빈걸로 두기 -> RegisterAccept에서 args.AcceptSocket을 null로 해주기 

 

 

지난시간 DummyClient.cs

using System;
using System.Net;
using System.Net.Sockets;
using System.Text;

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);
            // 식당 주소 찾는 부분은 똑같아 . 

            // 고객 입장에서 일단 휴대폰 부터 설정했어.
// 아까 Server에서 했던거랑 마찬가지로 TCP로 할 거면 두번째, 세번째 인자는 세트라 생각하면 된다.
            Socket socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
// 애는 고객이 들고 있는 핸드폰 이니까 문지기에게 연락을 해야해. 

            try  // 혹시 문제 생길까봐
            {
                // 문지기한테 입장 문의
                socket.Connect(endPoint); // 인자로 상대방의 주소를 넣어준다. 문지기가 받으면 다음코드로 이어지거 안받으면 대기 할거야. 
                Console.WriteLine($"Connected To {socket.RemoteEndPoint.ToString()}");  // 서버의 정보를 출력 

// 서버의 경우는 받은 다음에 보냈으니까 클라는 순서를 거꾸로 해야 해. 
                // 보낸다
                byte[] sendBuff = Encoding.UTF8.GetBytes("Hello World!");  // sendBuff를 먼저 만들어 주고. 
                int sendBytes = socket.Send(sendBuff);  // 통째로 보낸다.

                // 받는다
                byte[] recvBuff = new byte[1024];  // 서버가 몇 바이트 보낼지 모르니까 일단 큰 숫자로. 
                int recvBytes = socket.Receive(recvBuff);  // socket을 통해 Receive 해준다. 몇 바이트 받았는지 뱉어준다. 
                string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytes);  // byte를 string으로 변환하는 자겅ㅂ
                Console.WriteLine($"[From Server] {recvData}");  // 서버가 뭐라 했는지 출력 

                // 나간다
                socket.Shutdown(SocketShutdown.Both); // 나 볼일 다 봤으니까 나갈래. 
                socket.Close();  
            }
            catch (Exception e)
            {
                Console.WriteLine(e.ToString());
            }
        }
    }
}

코드가 커지면 관리하기 힘들어지니 관리하는 습관을 길러야 하는데

지금 따로 빼고 싶은 아이는 문지기에 관한 아이야.

이 애를 지금처럼 여기서 만드는 게 아니라 새로운 클래스에서 만들어 볼거야. 지난 시간 SercerCore의 Program.cs

using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ServerCore
{

    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);
            // ipAddr는 식당 주소, 7777은 식당 정문인지 후문인지 문의 번호

            // 문지기(가 들고있는 휴대폰)
            **Socket listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); // TCP로 할 때 설정**

            try
            {

                // 문지기 교육
                listenSocket.Bind(endPoint); // 식당 주소와 후문인지 정문인지 기입을 해준 것

                // 영업 시작
                // backlog : 최대 대기수  
                listenSocket.Listen(10);

                while (true)
                {
                    Console.WriteLine("Listening...");

                    // 손님을 입장시킨다.
                    Socket clientSocket = listenSocket.Accept();

                    // 받는다
                    byte[] recvBuff = new byte[1024];
                    int recvBytes = clientSocket.Receive(recvBuff);
                    string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytes);
                    Console.WriteLine($"[FromClient] {recvData}");

                    // 보낸다
                    byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to MMORPG Server !");
                    clientSocket.Send(sendBuff);

                    // 쫒아낸다 
                    clientSocket.Shutdown(SocketShutdown.Both);
                    clientSocket.Close();
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e.ToString());
            }       
        
        }
    }
}

이 코드에서 Linstener를 분리해서 옮기기

ServerCore에서 우클릭 새항목 추가로 Listener라는 이름으로 클래스를 추가해준다.

Program.cs에 있는 애들을 하나씩 옮겨와야해.

serverCore의 Program에서 보면

// 문지기(가 들고있는 휴대폰)
Socket listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); // TCP로 할 때 설정

소켓이라는 개념이었어.

Socket을 이용해서 bind도 해주고, Listen, Accept도 해주고 있으니까 일단 Linstener에 Socket을 선언해준다.

class Listener
{
		Socket _listenSocket;

일단 Init을 만들어 초기화 하는 작업을 해준다.

public void Init()
{
}

EndPoint를 받아줘야 그걸 이용해서 Bind도 하고 listen도 할거야. 일단 endPoint만 받아 올거야.

public void Init(IPEndPoint endPoint)
{
}

그리고

// 문지기(가 들고있는 휴대폰)
Socket listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); // TCP로 할 때 설정

이 부분을 잘라온다.

public void Init(IPEndPoint endPoint)
{
    _listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); // TCP로 할 때 설정
}

Bind, Listen을 해야 하는데 이 부분도 Program에서 Listener로 옮겨온다.

public void Init(IPEndPoint endPoint)
{
    // 문지기(가 들고있는 휴대폰)
    _listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); // TCP로 할 때 설정

    // 문지기 교육
    _listenSocket.Bind(endPoint); // 식당 주소와 후문인지 정문인지 기입을 해준 것

    // 영업 시작
    // backlog : 최대 대기수  
    _listenSocket.Listen(10);
}

초기화 하는 부분은 이렇게 bind랑 listen까지 하면 거의 준비가 끝난 거고,

Accept는 별도의 함수에서 하게 만들어 주자. listenSocket.Accept()를 하면 Socket clientSocket을 뱉어 주고 있었어. 대리인의 휴대폰이라 비유를 했었어. 얘도 Socket을 뱉어주게 하면 된다.

public Socket Accept()
{
    return _listenSocket.Accept(); 
}

그리고 기존 Progams.cs코드를 살펴 보면

Listener를 하나 넣어 줄건데 문지기 역할을 하는 애였어.

namespace ServerCore
{
    class Program
    {
        static Listener _listener = new Listener();

				static void Main(string[] args)
        {

이걸 추가해준다.

참고로 나중 가면 main에서 다 만들어 주는게 아니라 만들 게임에서 Server project 쪽에서 이런 작업들을 하게 될거야. 지금 하는 건 임시로만 넣어 놓는다 생각하면 된다.

Main에서 Init으로 초기화 작업을 해주면 된다.

try
{
		_listener.Init(endPoint);

이러면 LIstener에 있는 init이 실행 되면서 세팅이 된다.

Main의

while (true)
{
    Console.WriteLine("Listening...");

    // 손님을 입장시킨다.
    Socket clientSocket = listenSocket.Accept();

이 부분은

	// 손님을 입장시킨다.
	Socket clientSocket = _listener.Accept();

listenSocket은 Listener로 옮겼으니 선언해 준 _listener를 이용해 Accept를 하게 해줬어.

실행해보면 아까와 마찬가지로 잘 동작하는 것을 볼 수 있다.

아직 고칠점은 많다. 보내는 부분, 받는 부분도 Session이라는 클래스로 빼서 관리하기는 할거야.

지금 _Listener에서 마음에 안드는 부분은 Accept 부분을 블로킹 계열 함수를 사용한다는게 문제다.

while문에서 _listener.Accept가 실행이 되면 무한 대기를 한다. 실제로 클라이언트 쪽에서 Connect라는 함수를 사용해서 입장 요청을 주기 전까지는 Accept는 리턴을 하지 않고 영영 기다리게 된다.

Accept는 눈감아 줄 수 있다 하더라도

Recieve나 Send를 할 때 블로킹 계열 함수를 사용한다면 어떤 일이 일어날까? Recieve, Send로 왔다갔다 할 텐데 그 때마다 무한정 대기하면 문제가 될거야. 누가 메시지를 보내는지, 듣고 있는지 모르는데 이런 식으로 대기 하는 건 말도 안되는 행동이다.

Accept, Send, Recieve 이런 모든 입출력 계열의 함수들은 논블로킹(비동기) 방식으로 채택을 해야 한다.

Listener의 Accept부터 논블로킹으로 바꿔주면

_listneSocket.을 찍어 보면 함수들이 나오는데

_listenSocket.AcceptAsyc가 있어. Async 가 붙어 있으면 비동기라 생각하면 된다. 동시에 처리되지 않고 나중에 처리 될 수 있다는 의미. 영어에서 A가 붙으면 반대의 개념이 된다.

Async의 장점은 성공을 하든 안하든 바로 리턴을 하고 본다.

문제는 유저가 접속을 안해도 리턴을 하는 거니까 유저가 접속 요청을 하면 어떤 식으로든 서버에 알려줘야 한다.

그래서 비동기 방식이 블로킹 방식보다 조금 더 어려워진다. 블로킹인 지금은 직관적이지만.

Async는 일단 바로 다음줄로 내려 온 다음에 한참 있다 누군가가 들어오면 그 때서야 clientSocket을 콜백 방식으로 나중에 연락을 줄거야. 그 부분을 만들어 보자.

참고로 비동기는 C#뿐만 아니라 C++에도 개념이 들어간다.

Accept가 무작정 기다렸다가 처리가 되는 방식이 아니기 때문에 Accept를 요청하는 부분과 실제로 처리가 되어 완료가 되는 부분으로 둘로 쪼개서 나눠서 만들어야 한다.

Listener로 가서

void RegisterAccept()     // 등록을 한다는 의미
{

}

void OnAcceptCompleted()    // 완료 되었을 때 
{

}

AcceptAsync를 다시 봤을 때 여러 버전 중에서 3번을 사용할거야. SocketAsyncEventArgs를 받고 있는 걸 볼 수 있다.

void RegisterAccept(SocketAsyncEventArgs args)

SocketAsyncEventArgs 를 받는 이 버전으로 만든다. Accept() 함수에 있던 _listenSocket.AcceptAsync()를 여기에 옮겨준다.

return 값이 bool인데 얘는 pending 여부를 나타낸다. C++버전과 다른 점.

void RegisterAccept(SocketAsyncEventArgs args)
{
    bool pending = _listenSocket.AcceptAsync(args); // 당장 완료 한다 보장 없고 요청 하는거. 등록하는거. 
    if (pending == false)    // 운 좋게 바로 클라이언트가 접속했을 경우
        OnAcceptCompleted(); 
}

pending이 false가 된다면 바로 완료가 된다. 운좋게 실행하는 동시에 client 접속 요청이 와서 pending 없이 완료 되었다는 얘기. 그러면 OnAcceptComplete를 호출하면 된다.

근데 pending이 true면 나중에 거꾸로 어떻게로든 통보가 온다는 얘기가 된다.

RegisterAccept를 할 떄 SocketAsyncEventArgs로 뭔가를 만들 줘가지고, _listenSocket.AcceptAsync(args)가 완료가 되었을 때 우리한테 메시지를 거꾸로 보내주도록 맞춰줘야 한다.

일단 Init에 이 묘한 SocketAsyncEventArgs를 만들어 준다. 이건 한번만 만들면 재사용 할 수 있다는 장점이 있다.

SocketAsyncEventArgs args = new SocketAsyncEventArgs();

그리고 args.을 눌러보면 Completed가 있는데

args.Completed

를 보면

EventHandler 방식이야. 한마디로 이벤트 방식으로 callback으로 전달해주는 방식이다. += 문법으로 이벤트를 달아 줄거야. 여기에 OnAcceptConpleted라는 함수를 Callback 함수로 넣어줬다.

args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted);

이렇게 해주고

TEventArgs에 SocketAsyncEventArgs를 넣어줬으니까

void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
{

}

이렇게 형식을 맞춰준다.

void RegisterAccept(SocketAsyncEventArgs args)
{
    bool pending = _listenSocket.AcceptAsync(args);
    if (pending == false)    // 운 좋게 바로 클라이언트가 접속했을 경우
        **OnAcceptCompleted(null, args);**  
}

여기서도 형식에 맞게 이런 식으로 매개 변수를 전달해주면 된다.

여기까지 이벤트를 만들어 준거고

RegisterAccept(args);

최초 한번은 이런 식으로 등록을 해줄거야.

아까 같은 경우는 Accept를 시작하고 끝날 때 까지 기다렸다면

지금은 Init에서 초기화를 하는 부분에서

public void Init(IPEndPoint endPoint)
{
    // 문지기(가 들고있는 휴대폰)
    _listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); // TCP로 할 때 설정

    // 문지기 교육
    _listenSocket.Bind(endPoint); // 식당 주소와 후문인지 정문인지 기입을 해준 것

    // 영업 시작
    // backlog : 최대 대기수  
    _listenSocket.Listen(10);

    **SocketAsyncEventArgs args = new SocketAsyncEventArgs();
    args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted);** 
    RegisterAccept**(args);** 
}

이렇게 등록을 해줄거야.

그리고 이 상태에서 클라이언트가 connect 요청이 왔다면

callback 형식으로 OnAcceptCompleted가 호출이 된다는 걸 알 수 있다.

void RegisterAccept(SocketAsyncEventArgs args)
{
    bool pending = **_listenSocket.AcceptAsync(args);**
    if (pending == false)    // 운 좋게 바로 클라이언트가 접속했을 경우
        **OnAcceptCompleted(null, args);**  
}

void **OnAcceptCompleted**(object sender, SocketAsyncEventArgs args)
{

}

**_listenSocket.AcceptAsync(args);**얘는 100프로 처리 하고 끝내는 게 아니라 비동기 방식으로 일단 예약만 한다는 게 된다. 그래서 이름을 RegistgerAccept로 지은 거.

결국 흐름을 두가지로 봐야 한다.

**RegisterAccept(args)**를 할 때

pending이 false면 직접 OnAcceptCompleted를 불러 줄거고,

pending이 true면 OnAcceptCompleted(null, args);는 호출되지 않겠지만

나중에라도 진짜로 완료 됐으면

    **args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted);** 

이쪽에서 OnAcceptCompleted를 호출을 해줄 것이다.

결국엔 어떤 식으로든 OnAcceptCompleted에 들어오게 된다는 얘기가 된다.

여기 안에 들어오는 거 부터는 블로킹 버전 Accept()에서 리턴을 한 다음에 들어 오는 부분을 넣어주면 된다.

void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
{
    if(args.SocketError == SocketError.Success) // 모든 게 잘 처리 됐다는 뜻
    {
        // 유저가 커넥트 요청 해서 Accept 했으면 여기서 뭔가를 해주고 
        // TODO
    }
    else
        Console.WriteLine(args.SocketError.ToString());

// 다 끝나
    RegisterAccept(args); // 다음 턴, 다음 아이를 위해서 또 한번 등록을 해주는 거
}

이 흐름이 처음에는 선생님도 어려웠다

로직을 따라 가면서 보면 Acccept 부분 뿐만 아니라 Recieve나 Send하는 부분도 다 이런 식으로 되어 있다.

public void Init(IPEndPoint endPoint)
{
    // 문지기(가 들고있는 휴대폰)
    _listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); // TCP로 할 때 설정

    // 문지기 교육
    _listenSocket.Bind(endPoint); // 식당 주소와 후문인지 정문인지 기입을 해준 것

    // 영업 시작
    // backlog : 최대 대기수  
    _listenSocket.Listen(10);

    SocketAsyncEventArgs args = new SocketAsyncEventArgs();
    args.Completed += new EventHandler<SocketAsyncEventArgs>(**OnAcceptCompleted**); 
    **RegistterAccept(args);** 
}

Init에서 처음에 RegisterAccept한건 최초로 낚시대를 강물에 휙 던진거라 할 수 있고

그리고

RegisterAccept에서

void RegistterAccept(SocketAsyncEventArgs args)
{
    bool pending = _listenSocket.**AcceptAsync**(args);
    if (pending == false)    // 운 좋게 바로 클라이언트가 접속했을 경우
        OnAcceptCompleted(null, args);  
}

Async 계열 함수를 호출 했다는 건 랜덤성이 있는 거. 바로 처리 될 수도 있고 아닐 수도 있고.

pending이 false면 낚시대 던지자마자 바로 물고기가 잡혔다 하고 바로 완료된 부분 처리하는 거고,

아니면 나중에 나중에 자동으로 물고기가 입질하면 낚아 채는 거라고 보면 된다.

void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
{
    if(args.SocketError == SocketError.Success) // 모든 게 잘 처리 됐다는 뜻
    {
        // 유저가 커넥트 요청 해서 Accept 했으면 여기서 뭔가를 해주고 
        // TODO
    }
    else
        Console.WriteLine(args.SocketError.ToString());

    **RegistterAccept**(args); // 다음 아이를 위해서 또 한번 등록을 해주는 거
}

물고기를 잡았으면 낚시대를 다시 바다로 던져 주는 거.

낚시대를 던진다→물고기가 잡혔으니 끌어 올렸다→낚시대를 다시 한번 던진다.

이 흐름이 계속 반복되는 것이다.

Register→OnAcceptCompleted→Regeter

이렇게 계속 뺑뺑이를 돌게 될거다.

처음에만 Init에서 인위적으로 만들어 준거고 그 다음에는 알아서 뺑뺑이를 돈다고 보면 된다.

이 부분이 중요하다.

결국 이렇게

public Socket Accept()
{ 
    return _listenSocket.Accept(); 
}

한줄에 처리 됐던게 조금 더 복잡하게 단계를 나눠서 시작 단계과 끝단계로 나뉘기는 했는데

장점은 명확하다.

기다리지 않고 바로 처리를 할 수 있다는게 장점이 된다.

이제 OnAcceptCompleted의 TODO를 채우면 된다. 유저가 왔으면 무엇을 해야 하는지를 넣으면 된다.

Program.cs로 가서 기존의 Accept했던 부분 보면

                // 손님을 입장시킨다.
        Socket clientSocket = listenSocket.Accept();

				// 받는다
        byte[] recvBuff = new byte[1024];
        int recvBytes = clientSocket.Receive(recvBuff);
        string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytes);
        Console.WriteLine($"[FromClient] {recvData}");

        // 보낸다
        byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to MMORPG Server !");
        clientSocket.Send(sendBuff);

        // 쫒아낸다 
        clientSocket.Shutdown(SocketShutdown.Both);
        clientSocket.Close();

ClientSocket을 받은 다음에 “받는다, 보낸다, 쫒아낸다.” 이 부분을 실행을 하고 있었어.

얘를 실행하도록 어떻게 맞춰줄지 곰곰히 생각을 해보면 Callback 방식을 이용한다.

Action이든 delegate든 Init에서 인자로 하나 받아서 요청한 아이를 OnAceceptComplted의 //TODO에서 실행시키면 된다.

코드를 써보면

Listener에서

Action<Socket> _OnAcceptHandler;

Accept가 완료가 됐으면 어떻게 처리할 것이냐 얘기하는 거고,

Init에서 초기활 할 때 얘를 받아주게 하자.

public void Init(IPEndPoint endPoint, **Action<Socket> onAcceptHandler**)
{
    // 문지기(가 들고있는 휴대폰)
    _listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); // TCP로 할 때 설정
    **_OnAcceptHandler += onAcceptHandler;**

    // 문지기 교육
    _listenSocket.Bind(endPoint); // 식당 주소와 후문인지 정문인지 기입을 해준 것

    // 영업 시작
    // backlog : 최대 대기수  
    _listenSocket.Listen(10);

    SocketAsyncEventArgs args = new SocketAsyncEventArgs();
    args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted); 
    RegistterAccept(args); 
}

그리고 +=를 이용해서 연결을 해주자.

그리고 OnAcceptCompleted의 //TODO에서 호출을 해줘야 한다.

void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
{
    if(args.SocketError == SocketError.Success) // 모든 게 잘 처리 됐다는 뜻
    {
        // 유저가 커넥트 요청 해서 Accept 했으면 여기서 뭔가를 해주고 
        // TODO
        **_OnAcceptHandler.Invoke(args.AcceptSocket);**
    }
    **else
        Console.WriteLine(args.SocketError.ToString());

    RegistterAccept(args); // 다음 아이를 위해서 또 한번 등록을 해주는 거
}

인자로 socket을 넣어줘야 하는데 args.찍어 보면 AcceptSocket을 뱉어주고 있어.

SocketAsyncEventArgs얘가 일꾼 같은 느낌이야. 당장 _listenSocket.AcceptAsync(args)했을 때 당장 값을 추출할 수 없었으니까 이런 저런 정보들을 다 SocketAsyncEventArgs를 통해서 전달해 주는 것이다.

여기다가 args.AcceptSocket을 하면 수정 전 코드에서

            Socket clientSocket = listenSocket.Accept();

clientSocket을 뱉어주는 부분을 여기서 해주고 있는 것을 볼 수 있다. 그걸 다시 전달해 주면 된다. _onAcceptHandler한테 Invoke해서 전달을 해주고 있다는 얘기가 된다. 늦어서 왔는데 그 애가 AcceptSocket이라는 얘였어라고 전달 해준 거.

다시 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);
            // ipAddr는 식당 주소, 7777은 식당 정문인지 후문인지 문의 번호

            // 문지기(가 들고있는 휴대폰)
            Socket listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); // TCP로 할 때 설정

            **try**
            {

                // 문지기 교육
                listenSocket.Bind(endPoint); // 식당 주소와 후문인지 정문인지 기입을 해준 것

                // 영업 시작
                // backlog : 최대 대기수  
                listenSocket.Listen(10);

                while (true)
                {
                    Console.WriteLine("Listening...");

                    // 손님을 입장시킨다.
                    Socket clientSocket = listenSocket.Accept();

                    **// 받는다
                    byte[] recvBuff = new byte[1024];
                    int recvBytes = clientSocket.Receive(recvBuff);
                    string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytes);
                    Console.WriteLine($"[FromClient] {recvData}");

                    // 보낸다
                    byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to MMORPG Server !");
                    clientSocket.Send(sendBuff);

                    // 쫒아낸다 
                    clientSocket.Shutdown(SocketShutdown.Both);
                    clientSocket.Close();**
                }
            }
            **catch** (Exception e)
            {
                Console.WriteLine(e.ToString());
            }       
        
        }

Main에서 만들었던 받는다, 보낸다, 쫒아낸다 이 부분을 OnAcceptHandler로 이사를 시켜줘야 한다.

static void OnAcceptHandler(Socket clientSocket)
{
    // 받는다
    byte[] recvBuff = new byte[1024];
    int recvBytes = clientSocket.Receive(recvBuff);
    string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytes);
    Console.WriteLine($"[FromClient] {recvData}");

    // 보낸다
    byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to MMORPG Server !");
    clientSocket.Send(sendBuff);

    // 쫒아낸다 
    clientSocket.Shutdown(SocketShutdown.Both);
    clientSocket.Close();
}

try, catch도 없애고

OnAcceptHandler에 넣어준다.

static void OnAcceptHandler(Socket clientSocket)
{
    try
    {
        // 받는다
        byte[] recvBuff = new byte[1024];
        int recvBytes = clientSocket.Receive(recvBuff);
        string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytes);
        Console.WriteLine($"[FromClient] {recvData}");

        // 보낸다
        byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to MMORPG Server !");
        clientSocket.Send(sendBuff);

        // 쫒아낸다 
        clientSocket.Shutdown(SocketShutdown.Both);
        clientSocket.Close();
    }
    catch(Exception e)
    {
        Console.WriteLine(e);
    }        
}

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);**
    Console.WriteLine("Listening...");

    **while (true)**
    {            
				;
    }          
}

결국 _listener를 Init할 때 “문지기야 우리의 endPoint는”이 아니고, 혹시라고 누가 들어오면 OnAcceptHandler로 알려줘. 라고 명령을 한 셈이 된다.

그러면 다음에 while문에는 아무것도 안해줘도 이 부분 때문 계속 돌아갈거야. 의미 없지만 프로그램이 종료되지 않게만 임시로 넣어 둔 거.

콜백 방식으로 사용하고 있었으니까 이런 식으로 코드가 바뀌었다는 결론을 내릴 수 있다.

Listener로 가서 하나만 더 연구를 해볼건데

public void Init(IPEndPoint endPoint, **Action<Socket> onAcceptHandler**)
{
    // 문지기(가 들고있는 휴대폰)
    _listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); // TCP로 할 때 설정
    _OnAcceptHandler += onAcceptHandler;

    // 문지기 교육
    _listenSocket.Bind(endPoint); // 식당 주소와 후문인지 정문인지 기입을 해준 것

    // 영업 시작
    // backlog : 최대 대기수  
    _listenSocket.Listen(10);

    **SocketAsyncEventArgs args = new SocketAsyncEventArgs();**
    args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted); 
    RegistterAccept(args); 
}

일단 시작할 때 new로 args를 만들어 줬어. 필요할 때 마다 메시지를 전해주는 소켓도 알려주는 요정같은 아이였는데

OnAcceptCompleted에서 사용한다음에 날린 다음에 RegisterAccept에서 새로 만드는게 아니라 RegisterAccept에 같은 애를 다시 넣어줬어. 성능은 좋지만 조심해야 할 게 args를 재사용할 때 기존의 잔재들을 없애고 사용해야 한다.

OnAcceptCompleted에 들어오면

  void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
        {
            if(args.SocketError == SocketError.Success) // 모든 게 잘 처리 됐다는 뜻
            {
                _OnAcceptHandler.Invoke(**args.AcceptSocket**);
            }
            else
                Console.WriteLine(args.SocketError.ToString());

            RegistterAccept(args); // 다음 아이를 위해서 또 한번 등록을 해주는 거
        }

args.AcceptSocket에다가 클라이언트에 연결된 클라이언트의 대리인의 소켓이 AcceptSocket에 만들어지는 거였어.

근데 얘를 안지우고 RegisterAccept에 넣어주면

두번쨰 바퀴를 돌면 args가 null이 아닌 이미 들어가 있는 값으로 있을 거야. 그러면 에러가 난다.

args가 초기화 된 값이 아니라 이미 엉뚱한 값을 가지고 있다는게 되니까 밀어줘야 한다.

RegisterAccept에서 한번 null로 밀어주자.

void RegistterAccept(SocketAsyncEventArgs args)
{
    **args.AcceptSocket = null;**

    bool pending = _listenSocket.AcceptAsync(args);
    if (pending == false)    // 운 좋게 바로 클라이언트가 접속했을 경우
        OnAcceptCompleted(null, args);  
}

생소하면 이해가 잘 안갈 수 있으니 이해가 갈 때 까지 분석을 해보자.

실행을 하면 잘 되는데

DummyClient에서 계속 반복하게 테스트 해보자.

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()}");

                    // 보낸다
                    byte[] sendBuff = Encoding.UTF8.GetBytes("Hello World!");
                    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);// 너무 많이 보내도 문제 있으니
            }**          
        }
    }
}

잘 처리 된다. 0.1초 마다 보낸다.

더미클라이언트도 많은 유저 접속하는 거 실험하려고 만든거야.

Listener를 만들었는데 Accept만 논블로킹 방식(비동기 방식)으로 만들었다.

작업결과 

Listener

using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;

namespace ServerCore
{
    internal class Listener
    {
        Socket _listenSocket;
        Action<Socket> _OnAcceptHandler; 

        public void Init(IPEndPoint endPoint, Action<Socket> onAcceptHandler)
        {
            // 문지기(가 들고있는 휴대폰)
            _listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); // TCP로 할 때 설정
            _OnAcceptHandler += onAcceptHandler;

            // 문지기 교육
            _listenSocket.Bind(endPoint); // 식당 주소와 후문인지 정문인지 기입을 해준 것

            // 영업 시작
            // backlog : 최대 대기수  
            _listenSocket.Listen(10);

            SocketAsyncEventArgs args = new SocketAsyncEventArgs();
            args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted); 
            RegistterAccept(args); 
        }

        void RegistterAccept(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);
            }
            else
                Console.WriteLine(args.SocketError.ToString());

            RegistterAccept(args); // 다음 아이를 위해서 또 한번 등록을 해주는 거
        }

        public Socket Accept()
        { 
            return _listenSocket.Accept(); 
        }

    }
}

Program

using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ServerCore
{

    class Program
    {
        static Listener _listener = new Listener(); 

        static void OnAcceptHandler(Socket clientSocket)
        {
            try
            {
                // 받는다
                byte[] recvBuff = new byte[1024];
                int recvBytes = clientSocket.Receive(recvBuff);
                string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytes);
                Console.WriteLine($"[FromClient] {recvData}");

                // 보낸다
                byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to MMORPG Server !");
                clientSocket.Send(sendBuff);

                // 쫒아낸다 
                clientSocket.Shutdown(SocketShutdown.Both);
                clientSocket.Close();
            }
            catch(Exception e)
            {
                Console.WriteLine(e);
            }        
        }
              
        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);
            Console.WriteLine("Listening...");

            while (true)
            {
                ;
            }          
        }
    }
}

출처: https://inf.run/P4nt

반응형

댓글