[Part 4 게임 서버] 네트워크 프로그래밍 1(네트워크 기초 이론 ~ 소켓 프로그래밍 입문 #2

<네트워크 기초 이론>

1. 해킹 공격에서 안전해야함 

악성 유저가 의도적으로 굉장히 많은 패킷을 보내어 서버를 마비 시키거나 패킷을 보내지 않는 등 다양한 네트워크 공격이 있을 수 있다. 

 

2. 네트워크를 보내는 방식과 택배를 보내는 방식이 매우 유사

아파트 단지마다 경비실이 있고 택배 배송 센터가 있다. 

1) 아파트 단지 내에서 택배를 주고 받을 때, 경비실을 거쳐서 배송받아야 한다면, 보내는 사람과 받는 사람의 호수만 알면 보낼 수 있다 

2) 다른 아파트로 택배를 보내야할 때는, 경비실 -> 택배 배송 센터를 거쳐서 보내야한다.

-> 이때는 호수뿐만 아니라 아파트 이름까지 다 알아야 보낼 수 있다. 

 

3. 네트워크 기초 

각각의 아파트 호수: 단말기

경비실: 스위치

택배 배송 센터: 라우터로 이해

1) 위의 1번의 경우는 같은 네트워크내에서 스위치를 통해 보내는 상황을 말한다. 

204.123.155.1 -> 204.123.155.4 와 같이 앞의 ip 주소가 동일 => 동일 네트워크를 의미

2) 위의 2번의 경우에는 다른 네트워크로 라우터를 거쳐서 보내는 상황을 말한다. 

204.123.155.1 -> 201.231.152.3 앞의 ip 주소가 동일하지 않음 

* 외부로 보낼 경우 세상에 하나밖에 없는 주소여야 가능 

 

<통신 모델>

1. 네트워크 계층(Network Layer)

1) 애플리케이션(Application) - 5~ 7계층 (도메인 주소)

- 유저 인터페이스를 결정: HTTP, FTP, DNS 등의 프로토콜을 이용

2) 트랜스포트(Transport) - 4계층

- 전송 확인/ 오류 해결 정책을 정함: TCP, UDP 

* 보통 MMO에서는 TCP를 주로 사용

3) 네트워크(Network) -> 3계층(IP 주소)

- 네트워크 간 경로 설정: IPv4, IPv6

- 관련 장치: 라우터

4) 데이터 링크(Data Link) -> 2계층(MAC 주소)

- 네트워크 내 경로 설정: 이더넷, PPP

- 관련 장치: 스위치 

5) 피지컬(Physical) -> 1계층

- 신호 처리: 케이블/허브

 

* OSI 7계층 모델에서는 애플리케이션 모델이 세션, 프레젠테이션, 애플리케이션으로 나눠져있는 모델을 말한다. 

* 도메인 주소: 주소를 숫자로 표현하면 어렵기 때문에 문자열로 표현 

 

<소켓 프로그래밍 입문 #1>

1. 식당 예제로 설명

a) 식당 입장

 1> 식당 입구의 문지기를 고용 

 2> 문지기 교육: 식당에 대한 주소를 알아야함, 어떤 문인지

 3> 영업 시작: 실제로 손님이 왔을 시, 입장 안내까지 가능한 상태

 4> 안내: 손님이 연락이 왔을 때, 문지기는 식당의 상황을 봐서 입장을 시켜줌. 이때, 모든 손님이 앉지 않고 한 명만 대리인의 자격으로 앉을 수 있다고 가정 

 5>  고객 유치 성공: 문지기가 대리인에게 핸드폰을 건네줘서 손님과 연락 가능하도록 한다.

b) 손님 관점

 1> 입장 문의: 손님이 핸드폰을 가지고 해당 식당의 주소로 입장이 가능한지 문지기에게 연락

 2> 손님의 대리인 입장: 손님과 대리인 연락하다록 문지기가 연결해준다. 

 

2. 실제 상황 적용

a) 서버의 관점: 식당 관점

 1> Listener 소켓 준비: 문지기 고용

 2> Bind(서버 주소/ Port를 소켓에 연동): 문지기 교육

 3> Listen: 손님이 연락이 오는지 듣기 위해 준비

 4> Accept: 클라이언트한테 접속 요청이 오면 받아들여서 안내를 해줌

=> Accept를 하면 클라이언트 세션이 만들어짐(똑같이 소켓이 존재) : 클라이언트 대표 소켓과 메세지를 주고 받으면 대화 가능 

b) 클라이언트: 손님 관점

 1> 소켓 준비: 위에서 핸드폰이 소켓으로 

 - 소켓: 네트워크 통신을 위한 추상적인 장치 

 2> 서버주소로 Connect: 식당 번호 문의

 - 입장이 가능한지 묻는 것: 해당 주소로 연결 가능한지?

=> 소켓을 통해 Sesssion 소켓과 패킷 송수신이 가능하다. 

*위에서 대리인이 Session

 

<소켓 프로그래밍 입문 #2>

솔루션 우클릭 -> 속성 -> 여러개의 시작 프로젝트에서 DummyClient와 ServerCore를 시작으로 바꿈

 

1. 서버 구현

1) 서버에서 Listener 소켓을 준비하기위해 System.Net.Sockets에 Socket을 사용하여 구현한다. 

  a) 해당 Socket 클래스는 세 가지인자를 받음: AddressFamily, SocketType, ProtocolType

* 강의에서는 AddressFaily로 DNS 주소를 받고 있지만, 나한테는 SocketType과 ProtocolType 2가지 인자밖에 안받고 있다. 

  b) 첫번째 인자에는 네트워크 주소를 넣어야함 -> DNS 사용

 - cmd에서 ping www.google.com을을 하면 아래와 같이 142.250.207.68의 주소로 connect를 함

 - 외부적으로 www.google.com으로  로 연결했지만 내부적으로 해당 IP를 찾아서 연결해주었다. 

 - 서버의 IP 주소를 그대로 사용하지 않고 사용자가 알기 쉽게 고정해놓은 DNS 주소로 접근하도록 한다. 

  c) 위의 AddressFamily를 가져오기 위해 endPoint(전달 받아야할 IP주소)주소를 이용해야한다. 

  - Dns.GetHostName을 통해 로컬 컴퓨터의 호스트 이름을 가져온다 

  - Dns.GetHostEntry 함수에 호스트 이름을 인자로 주어 IPHostEntry를 가져온다.

  - ipHost.AddressList를 통해 호스트와 연결된 IP 주소의 목록을 가져옴 

* 로컬 컴퓨터에서는 IP 주소가 하나뿐이지만, 구글과 같은 거대한 회사에서는 여러개의 IP 주소가 연결되어 있을 수 있다. 

  - IPEndPoint는 위에서 가져온 ipAddr의 7777 port 로 설정한다. 

  * EndPoint: 메세지 도착 IP 주소 

  - 강의에서는 Socket의 첫번째 인자에 endPoint.AddressFamily를 넣어줬다 

  - ProtocolType을 TCP, SocketType을 Stream으로 해준다. 

static void Main(string[] args)
        {
            // DNS (Donmain Name System)
            string host = Dns.GetHostName();
            IPHostEntry ipHost = Dns.GetHostEntry(host);
            IPAddress ipAddr = ipHost.AddressList[0];
            // IPEndPoint: 도착 IP 주소 
            IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777); // ip주소, port 번호 

            // 문지기 주소 
            Socket listenSocket = new Socket(SocketType.Stream, ProtocolType.Tcp);
        }

2) 위에서 찾은 주소와 Listener 소켓을 연동해줘야한다 -> 문지기 교육

 - Bind를 통해 EndPoint 주소와 연결

3) listenSocket.Listen을 통해 클라이언트의 연결을 들도록 한다. 

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

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

4) 이때, 한번만 듣는 것이 아닌 계속 클라이언트의 연결 신호를 들어야하기 때문에 While문으로 작성한다

 - 손님을 입장 시키기위해 listenSocket.Accept으로 클라이언트의 연결을 받아들인다. 

 -> 이때, 세션의 소켓(대리인)이 만들어져 이 소켓과 클라이언트가 통신한다. 

 - 클라이언트와 연결 후, 클라이언트가 보내는 메세지를 받기 위해 아래의 코드와 같이 코딩한다. 

  a) clientSocket.Receive로 클라이언트가 보낸 메세지를 recvBuff에 저장한 후, 저장된 메세지의 바이트수를 반환한다.

  b) recvBuff에 저장된 바이트를 Encoding하여 recvData에 저장한 후 Console에 출력한다. 

  이때 GetString의 두번째 인자와 세번째 인자는 각각 처음 접근할 인덱스와 가져올 바이트의 개수이다. 

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

                // 보낸다.
            }

 c) 클라이언트에게 메세지를 보내기 위해 다음과 같이 코딩한다.

  - 문자열 코드의 Bytes를 받아온 후 해당 Byte를 Send 함수로 보냄

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

d) 메세지를 보냈으면, 연결을 종료하겠다고 미리 말한 후 연결을 종료한다. 

  - clientSocket.Shutdown으로 미리 연결을 종료하겠다고 알린다. -> SocketShutdown.Both로 Recieve와 Send 둘다 안하겠다.

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

f) 혹시 모를 에러를 대비하여 Try Catch문으로 다음과 같이 감싸준다. 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace SeverCore
{
    class Program
    {
        static void Main(string[] args)
        {
            // DNS (Donmain Name System)
            string host = Dns.GetHostName();
            IPHostEntry ipHost = Dns.GetHostEntry(host);
            IPAddress ipAddr = ipHost.AddressList[0];
            // IPEndPoint: 도착 IP 주소 
            IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777); // ip주소, port 번호 

            // 문지기 주소 
            Socket listenSocket = new Socket(SocketType.Stream, ProtocolType.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($"[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());
            }


        }
    }
}

 

* 기본적으로 Accept는 Blocking 함수로 클라이언트가 접속을 안하면 아래의 코드는 실행되지 않는다.

 

2. Client 부분

DummyClient 프로젝트의 Program.cs에서 클라이언트 부분을 작성한다. 

 

1) EndPoint(데이터를 전달하고싶은 Ip 주소)는 위와 동일하기 때문에 똑같이 코드를 복사하여 가져온다.

	// DNS (Donmain Name System)
            string host = Dns.GetHostName();
            IPHostEntry ipHost = Dns.GetHostEntry(host);
            IPAddress ipAddr = ipHost.AddressList[0];
            // IPEndPoint: 도착 IP 주소 
            IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777); // ip주소, port 번호 

            // 휴대폰 설정
            Socket socket = new Socket(SocketType.Stream, ProtocolType.Tcp);

2) 클라이언트의 소켓이 서버로 연결한다. 

                // 문지기에게 문의
                socket.Connect(endPoint);
                Console.WriteLine($"Connect To {socket.RemoteEndPoint.ToString()}");

3) 서버에서 Send & Recieve & Shutdown & Close 했던것과 같이 코드를 짠다

* 클라이언트는 보내고 받는 것이기 때문에 순서가 바뀜

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;

namespace DummyClient
{
    class Program
    {
        static void Main(string[] args)
        {
            // DNS (Donmain Name System)
            string host = Dns.GetHostName();
            IPHostEntry ipHost = Dns.GetHostEntry(host);
            IPAddress ipAddr = ipHost.AddressList[0];
            // IPEndPoint: 도착 IP 주소 
            IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777); // ip주소, port 번호 

            // 휴대폰 설정
            Socket socket = new Socket(SocketType.Stream, ProtocolType.Tcp);

            try
            {

                // 문지기에게 문의
                socket.Connect(endPoint);
                Console.WriteLine($"Connect 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());
            }
        }
    }
}

3. 실행 결과

: 아래와 같이 서로 메세지를 주고 받은 것을 확인할 수 있다. 

-> 서버는 계속 Listen 하는 중 

DummyClient 프로젝트의 파일 -> bin -> Debug -> DummyClient.exe 파일을 통해 강제로 실행하면 계속 서버가 클라이언트 메세지를 받음

 

* 참고 자료: 아래의 블로그에 네트워크 통신과 소켓 통신에 대해 잘 설명되어있다. 

https://itmining.tistory.com/127

 

[C#] TCP/IP 프로토콜과 소켓 프로그래밍

경고!! 제가 아는 부분에 대해서 공유드리는 목적의 발표입니다. 이 부분에 대해 빠삭하지 않기 때문에 질문에 대한 답을 당장은 드릴 수 없을 수 있지만, 추후에 공부를 통해서 답을 드리겠습니

itmining.tistory.com