Server programming

02_04_네트워크 프로그래밍_소켓 프로그래밍 입문 #2

devRiripong 2023. 4. 3. 17:37
반응형

Q1. 실제 게임에서는 블로킹 함수를 쓰면 안되는 이유는?

Q2. 간단한 소켓 프로그램을 구현해 보세요.

 

 

네트워크프로그래밍을 처음으로 시작해보자.

지난시간에 했던 코드와 lock 도 삭제한다.

 

solution에서 우클릭해서 속성으로 가서

 

이렇게 맞춰주면 된다.

일단 문지기를 만들어 줘야 한다. Listen 소켓을 만들어 줘야 한다.

namespace ServerCore
{
    class Program
    {              
        static void Main(string[] args)
        {
            // DNS ( Domain Name System )          
            Socket listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

Socket의 첫번째 인자가 네트워크 주소를 넣어줘야 하는데 DNS를 사용할거야. 얘가 뭘 하는 건지 보자면

cmd를 열어서 ping www.google.com을 입력해보자. 

주소를 적었지만 내부적으로는 해당 아이피를 찾아서 요청을 보내는 것을 볼 수 있다.

DNS는 결국 이런 역할을 해주는 건데 실제 IP 주소를 코드에 적으면 문제가 되는게 나중에 서버를 딴 데로 이전을 한다고 해보면 경우에 따라서 아이피 주소가 바뀔 수 있는데 도메인을 등록하고 그 도메인으로 아이피를 찾아 갈 수 있게 한다면 이름으로 주소를 찾게끔 해주면 관리가 쉽게 된다는 것을 알 수 있다. DNS는 나중에 알아일단은 그냥 사용해 보자.

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);
// DNS가 어떻게 www.google.com 같은 이름을 통해 IP주소를 찾았냐 하면 어려운 주제야. DNS 서버라는 애가 우리 네트워크 망 안에 있을거야. 걔가 일단 해준다고 생각하자.   
            IPAddress ipAddr = ipHost.AddressList[0];  
// IP주소를 뱉어준다. www.google.com을 했을 때 주소에 해당하는 IP가 여러개 있을 수도 있어서 배열. 
            
			IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);  // 얘가 최종 주소
            // ipAddr는 식당 주소, 7777은 식당 정문인지 후문인지 문 번호를 나타낸다. 
// 7777에다가 문지기 배치 했는데 나중에 클라이언트가 엉뚱한 번호로 접근 시도하려 하면 입장 못한다. 
// 이 번호랑 클라이언트가 접근할 주소랑 같게 맞춰줘야 한다.  

            // 문지기(정확하게 말하면 문지가 들고있는 휴대폰)
            Socket listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); // TCP로 할 때 설정
// 첫번째 인자는 IP버전 4를 쓸지, 6를 쓸지 넣어주는 거. DNS에서 만들어 줬기 때문에 사용하던 AddressFaily를 넣어줬다. 
// 그다음에 TCP를 사용할지 UDP를 사용할지 골라줘야 해. TCP로 작업할 거라 두번째, 세번째 인자 넣어줬다. 다음에 이론 알아볼거. 

            try  // 혹시 에러 나나 확인 위해 try catch로 감쌌다. 
            {
                // 문지기 교육
// 문지기가 들고 있는 핸드폰에 위 코드에서 찾은 주소를 연동해줘야 한다. 
                listenSocket.Bind(endPoint); // 식당 주소와 후문인지 정문인지 이런거를 같 기입을 해준 것

                // 영업 시작
                // backlog : 최대 대기수  
// 너무 흥행해서 1000명이 접속하면 문지기가 안내 하기 전까지 대기 인원. 이 숫자 초과하면 나머지는 입장 문의 하면 fail뜬다. 
                listenSocket.Listen(10);

// 서버 입장인데 영업을 하고 손님 한번 받고 끝낼게 아니니까 무한루프
                while (true)
                {
                    Console.WriteLine("Listening...");  // 지금 영업중이야. 

                    // 손님을 입장시킨다. 
                    Socket clientSocket = listenSocket.Accept();
// Aceept에 마우스 대면 Socket을 뱉어 주는 걸 볼 수 있다. Socket은 아까 얘기한 대리인. Session의 Socket이 된다. 
// 아까 입장한 손님이랑 대화를 하고 싶으면 무조건 이 clientSocket을 통해서 대화를 하면 된다. 

                    // 받는다
                    byte[] recvBuff = new byte[1024];  // Recieve에 받을 버퍼를 넣어줘야 한다. 보내준 데이터는 여기에 저장된다.  
                    int recvBytes = clientSocket.Receive(recvBuff); // 몇 바이트를 받았는지 뱉어준다. 
                    string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytes);  // 테스트 할 때는 문자열을 왔다 갔다 할 거니까.
// 일단 규약을 UFT8로 했다. "안녕하세요" 문자열 보낼 때 규약이 여러가지 있다. 어떻게 인코딩 할지. 나중에 이론으로 다룰거야. 
// GetString으로 recvBuff에 받은 애를 string으로 변환 해줄거야. 지금은 문자열을 받는다 가정하니 간단하게. 실제 게임은 문자열로 못한다. 
                    Console.WriteLine($"[FromClient] {recvData}");  // 출력 

                    // 보낸다
// 클라이언트에 메시지를 보내보자. 
// 몇개 보낼질 아니까 Buff 바로 만들어서 보낸다.
                    byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to MMORPG Server !");
                    clientSocket.Send(sendBuff); // 아까 clientSocket에 Send하면 된다. 

                    // 쫒아낸다 
                    clientSocket.Shutdown(SocketShutdown.Both);  // 끊어지기 전에 예고. 할말 없어. TCP 이론에서 알아볼 거. 안 넣어도 동작 한다.
                    clientSocket.Close();  // 핸드폰 연결 끊어진다.
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e.ToString());
            }       
        
        }
    }
}

이렇게 서버는 완료.

 

여기서 알 수 있었던 건

Socket clientSocket = listenSocket.Accept(); 를 호출 했는데 손님이 입장 안하면 어떻게 될까? 지금은 기초중의 기초라 이렇게 하는 거지 실제로는 이렇게 안할거야. 블로킹 함수라 입장을 안하면 딱 여기서 멈추게 된다. 클라이언트가 접속을 하면 반환을 하고 다음 부분이 실행이 된다.

블로킹, 논블로킹에 대한 얘기도 나중에 계속 하게 될거다.

clientSocket.Send(sendBuff);도 마찬가지로 내가 보냈는데 다른 쪽에서 안받으면 계속 대기를 탈거야.

 

이제 Client 쪽. 손님처리하는 건 간단하다고 했어. DummyClient로 가서

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

이 부분을 DummyClient에 복붙하고 시작을 하자.

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

여기까지 해서 서버, 클라가 완성이 되었다. 동시에 실행하면 된다. 설정에서 두 프로젝트 동시에 실행하게 만들어 줬으니. ctrl + f5누르면

 

클라이언트

서버

서버는 Listening을 하다가 클라이언트가 Hello World를 보낸 거.

서버에서도 클라이언트에 Welcome to MMORPG Server라고 메시지를 보냈다.

클라이언트 보내고 바로 종료한 상태고, 서버는 while문을 통해 다른 클라가 접속하는지 받고 있는거다.

이 상태에서 Client를 다시 켜 볼건데

DummyCllient에서 우클릭해서 파일탐색기에서 열기를 눌러보면

 

DummyClient.exe를 실행해보면

이렇게 받는것을 볼 수 있다.

 

이렇게 굉장히 간단하게 소켓 프로그램을 구현해봤다. 앞으로 이걸 발전시켜 나가면 되는건데.

특히 위험한 부분중 하나는 게임에서는 socket.Connect(endPoint); 같은 블로킹 계열을 쓰면 안된다. 서버에서 못 받아 주면 계속 대기하게 되고

마찬가지로 Client의 socket.Send, socket.Recieve도 서버에서 안받아주거나 안보내주면 기다리게 될 거야.

어쨌든 간단하게 서버와 클라를 구현해 보았다.

 

 

반응형