공부/인프런 - Rookiss

Part 4-3-3. 네트워크 프로그래밍 : 소켓 프로그래밍 입문

셩잇님 2023. 9. 19. 20:39
반응형

 

 

네트워크 프로그래밍

 

이론

 

 오늘은 소켓 프로그래밍에 대해서 알아보는 시간을 가져보도록 하자. 그렇지만 다짜고자 바로 코딩을 시작하면 어렵기 떄문에 프로그램의 어떻게 동작하는지 전체적인 흐름을 알아보자. 😋

 

손님 가게
가게에 방문하고 싶은 손님 입구 만들기, 문지기 고용
- 문지기 교육
입장 문의 영업 시작
소통 (직원) 안내 (소통)

 

 손님은 본인이 가게에 방문하는 것이 아닌 대리자를 통해 가게를 방문한다. 핸드폰을 통해 식당 번호로 입장 문의를 하고, 가게는 문지기를 통해 손님의 요청을 받을지 말지 결정하고, 만약 받게 되다면 가게 내부로 안내하게 되며 서로 소통이 가능한 상태가 된다. 이러한 상황을 소켓 프로그래밍에 대입하면 아래와 같은 구조가 된다.

 

소켓 프로그래밍 비유 예제 및 실제 소켓 프로그래밍의 단계
출처 :  https://recipes4dev.tistory.com/153

 

 즉 클라이언트가 손님의 역할을 맡고, 가게가 서버의 역할을 맡아 담당한다. 이러한 과정에서 손님은 비교적으로 간단하게 소통을 요청할 순 있지만, 서버에서는 주소, Port 등 다양한 정보를 결합하거나 요청, 대기, 승인들의 절차가 있기 때문에 손님에 비해 비교적 많은 일을 담당하여 처리한다. 위 사진만 봐도 알 수 있듯이.. 😅

 

 


 

 

구현

 코드를 통해 소켓 프로그래밍을 구현해보자!

 

서버

class Program
{
    static void Main(string[] args)
    {
        string host = Dns.GetHostName();
        IPHostEntry ipHost = Dns.GetHostEntry(host);
        IPAddress ipAddr = ipHost.AddressList[0];
        IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);

        // 문지기 구현
        Socket listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

        try
        {
            listenSocket.Bind(endPoint);
            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($"[From Client] {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());
        }
    }
}

 

 각각의 구문을 나누어서 살펴보자. 먼저 기본으로 내 로컬 컴퓨터 호스트의 이름을 DNS 프로토콜을 이용해 세팅을 진행한다. 아이피 주소, 포트번호 등을 여기에서 정의한다.

 

            string host = Dns.GetHostName();
            IPHostEntry ipHost = Dns.GetHostEntry(host);
            IPAddress ipAddr = ipHost.AddressList[0];
            IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);

 

 먼저, string 클래스와 Dns 클래스를 이용해 내 로컬 컴퓨터의 호스트 이름을 가져온다.  이후 IPAddress 클래스를 통해 내 IP를 가져오는데 이 때 IPAddress의 반환 값이 배열로 선언된 것을 볼 수 있다. 왜 배열을 사용하는지 곰곰히 생각해보면 구글과 네이버 등과 같은 엄청난 트래픽이 발생하는 곳에 동일 IP를 통해 사용자들이 접근하게 된다면 부하가 생길 수 있으므로 IP 주소를 여러 개를 사용하여 이러한 부하를 줄여준다.

 

 마지막으로 endPoint를 설정해주는데 이 때 사용되는 인자 값은 ipAddr과 같은 식당의 주소와 7777과 같은 식당의 문 번호 정보이다. (앞, 뒷문) 이 때 중요한 점은 식당 입장 시 사용되는 문 번호(=포트)가 식당(=서버), 손님(=클라) 모두 동일한 번호를 사용해야 된다는 점이다. 

 

다음은 문지기 구현이다. 

 

Socket listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

 

 이 때에 사용되는 인자값은 1. 네트워크 주소, 2. 소켓 타입, 3. 프로토콜 타입이다. 네트워크 주소의 경우 DNS 기능을 이용하는데 DNS란 IP 주소를 도메인 주소로 변경해주는 기능이다. 조금 의아할 수 있겠지만 네이버, 다음, 네이트 같은 사이트들을 도메인 대신 IP로 외운다고 생각해보면 이해하기 쉬울 것이다. 또한 이렇게 사용할 경우 서버의 주소가 변경되는 문제가 발생하도 보다 유연하게 처리할 수 있다. 소켓타입의 경우 우리는 Tcp를 사용하기 위해 Stream을 이용하고 프로토콜타입의 경우 Tcp를 이용하기 떄문에 Tcp와 같이 적어준다.

 

 다음은 문지기 교육과 영업 시작을 나타낸다. (=결합 및 연결 대기)

 

                // 문지기 교육
                listenSocket.Bind(endPoint);

                // 영업 시작
                listenSocket.Listen(10);

 

 Listen의 인자 값은 backlog인데 이는 최대 대기수를 의미한다. 해당 값을 초과할 경우 클라이언트에서 접속 불가가 일어난다. 즉 메이프르토리 채널이 꽉 차 다른 채널을 이용해야한다는 의미와 같다. 

 

 마지막으로 손님 입장 및 입장 후 처리 로직이다.

 

// 접속 할 때 까지 대기하기 위한 while
while (true)
{
    Console.WriteLine("Listening...");

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

    // 손님의 하고 싶은 말을 듣는다.

    // 얼마나 많은 데이터를 받을 지 모르기에 큰 배열로 설정한다.
    byte[] recvBuff = new byte[1024];
    // clientSocket.Receive를 통해 recvBuff에 실제 정보가 얼마나 담겼는지 확인한다.
    int recvBytes = clientSocket.Receive(recvBuff);
    // 문자열을 이용할 것이므로 Encoding 한다.
    string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytes);
    Console.WriteLine($"[From Client] {recvData}");

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

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

 

 우리의 서버는 사용자가 언제, 어디서든 들어 올 수 있어야하므로 while 문과 무한루프를 이용해 항상 사용자를 받아준다.  이 후의 로직은 손님(=클라이언트)의 요청을 받는 행위이다. 먼저 Accept() 메서드를 이용하여 손님을 입장시키고, 손님의 말을 듣는다. 이 때 손님이 얼마나 많은 말을 할지 모르므로 1024 byte 배열을 선언 후 Receive 통해 손님의 실제 정보가 얼마나 담겼는지 recvBytes에 저장한다. 이 후 손님의 말을 문자열을 이용해 Encoding 해준다.

 

 이후 손님에게 식당(=서버)에 온 것을 환영한다는 의미로 회신 메시지를 보내주고, 손님의 모든 말을 들었으므로 Shutdown과 Close를 통해 손님을 내쫓는다. 🤣.. 마지막으로 이 모든 내용을 혹시 모를 사고를 대비하여 try~catch문으로 구현한다.

 


 

클라

 클라이언트는 비교적 서버보다 구현이 간단하다.

 

static void Main(string[] args)
{
    string host = Dns.GetHostName();
    IPHostEntry ipHost = Dns.GetHostEntry(host);
    IPAddress ipAddr = ipHost.AddressList[0];
    IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);

    // 휴대폰 설정(=손님 소켓 생성)
    Socket socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

    try
    {
        // 문지기에게 연결한다
        // endPoint로 입장을 요청 문의한다.
        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());
    }
}

 

            string host = Dns.GetHostName();
            IPHostEntry ipHost = Dns.GetHostEntry(host);
            IPAddress ipAddr = ipHost.AddressList[0];
            IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);
            
            Socket socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

 

 먼저 기존에 서버에서 사용하는 DNS의 코드와 소켓을 설정하는 부분은 그대로 가져와서 사용해준다. 해당 내용의 설명은 위와 같으므로 생략한다.

 

try
{
    // 문지기에게 연결한다
    // endPoint로 입장을 요청 문의한다.
    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());
}

 

 이후 서버 입장 및 메시지 발신/수신 로직이다. 우리는 Connect() 메서드를 통해 endPoint 주소로 전화(=연결)하여 입장 요청을 문의한다. 이후 서버에서 인코딩하였던 것과 동일하게 서버에게 보낼 메시지를 인코딩하여 Send() 메서드를 이용해 보낸다.

 

 마찬가지로 서버에서 메시지를 받을 수 있으므로 서버에서 설정한 것과 동일하게 크기를 설정하고, 받은 데이터를 문자열로 변환(Encoding)하는 작업을 거친다. 현재로서는 이 기능 외에는 따로 구현한 것이 없어서 메시지를 받고 난 이후에는 닫아준다. 또한 마지막으로 서버와 동일하게 혹시 모르는 문제 처리를 위해 try~catch문으로 내용을 감싸준다.

 


 

실행 결과

 

 

 프로그램을 실행해보면 서버에게서 메시지가 온 것과 클라이언트에서 메시지 온 내용을 확인해 볼 수 있다. 빌드하면서 생성된 더미 클라이언트 exe 파일을 파일 탐색기를 통해 프로젝트 폴더로 이동해 여러 번 추가적으로 실행하면 서버에 계속 들어오는 로그를 추가적으로 확인할 수 있다. 👍

 

 

 

반응형