지우너

[C# 게임서버] 네트워크 프로그래밍(1) 본문

Programming/Server

[C# 게임서버] 네트워크 프로그래밍(1)

지옹 2024. 1. 16. 22:32

앞에서 그랬던 것처럼 내용을 정리한 PPT로 내용을 대체한다... 

 

1. 소켓 프로그래밍

1.1. 개념

클라이언트

클라이언트가 서버에 접속을 하면 서버에서 이런저런 처리를 한 다음 세션을 하나 만들어주는데,

그 후 모든 통신들은 세션을 통해서 이루어진다.

 

서버

일반적으로 연락을 주고받는 용도가 아니라 그냥 문지기 역할을 하는 Listener 소켓을 하나 준비한다.

bind라는 함수를 통해 식당주소(서버주소)와 번호(port)가 무엇인지 Listener 소켓에 연동시킴

listen이라는 함수를 통해 클라에게서 접속요청을 받을 수 있도록 준비

접속 요청이 오면 accept 함수를 이용해서 안내해줌

accept가 끝나면 클라 세션 하나가 만들어지는데, 그 세션에도 소켓이 있다.

클라이언트와 대화할 때는 클라이언트를 대표하는 소켓과 메시지를 주고 받음

 

1.2. 구현

1.2.1. 서버 구현

 

 

 

[전체 코드]

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

namespace ServerCore
{
	class Program
	{
		static void Main(string[] args)
		{
			// 내 로컬 컴퓨터의 호스트 네임을 host 변수로 받아온다.
			string host = Dns.GetHostName();
			IPHostEntry ipHost = Dns.GetHostEntry(host);
			// 배열 0번 인덱스에 있는 주소만 사용할 예정
			IPAddress iPAddress = ipHost.AddressList[0];
			// 최종 주소. 식당으로 비유하자면 iPAddress는 식당 주소이고, port 7777은 문의 번호에 해당한다.
			IPEndPoint endPoint = new IPEndPoint(iPAddress, 7777);


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

			try
			{
				listenSocket.Bind(endPoint);

				// backLog: 최대 대기수
				listenSocket.Listen(10);

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

					Socket clientSocket = listenSocket.Accept();

					// 받는다!!!
					// clientSocket이 보낸 데이터를 receiveBuff에 저장해준다.
					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!!");
					clientSocket.Send(sendBuff);

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

			}
			catch(Exception e)
			{
                Console.WriteLine(e.ToString());
            }
		}
	}
}

 

1.2.2. 클라이언트 구현

서버와 유사한 부분이 많았기 때문에 필요한 부분이 아니라면 별도의 설명은 적지 않았다.

 

[전체 코드]

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

namespace DummyClient
{
	class Program
	{
		static void Main(string[] args)
		{
			// 내 로컬 컴퓨터의 호스트 네임을 host 변수로 받아온다.
			string host = Dns.GetHostName();
			IPHostEntry ipHost = Dns.GetHostEntry(host);
			// 배열 0번 인덱스에 있는 주소만 사용할 예정
			IPAddress iPAddress = ipHost.AddressList[0];
			// 최종 주소. 식당으로 비유하자면 iPAddress는 식당 주소이고, port 7777은 문의 번호에 해당한다.
			IPEndPoint endPoint = new IPEndPoint(iPAddress, 7777);
			
			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 Server");
				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());
            }	
        }
	}
}

 

 

1.3. 실행

 

2. Listener

지금 모든 코드가 main함수 안에 다 들어가 있는데, 코드가 점점 커지면 관리하기 어려워지기 때문에 처음부터 분리하는 습관을 들여야 한다.

2.1. Listener Class 분리

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

namespace ServerCore
{
	class Listener
	{
		Socket _listenSocket;

		// endPoint를 이용해서 bind도 하고 listen도 하니까 초기화할 때 인자로 필요할 것이다.
		public void Init(IPEndPoint endPoint)
		{
			_listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
			
			_listenSocket.Bind(endPoint);
			// backLog: 최대 대기수
			_listenSocket.Listen(10);
		}
		public Socket Accept()
		{
			return _listenSocket.Accept();
		}
	}
}

 

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

namespace ServerCore
{
	class Program
	{
		static Listener _listener = new Listener();
		static void Main(string[] args)
		{
			// 내 로컬 컴퓨터의 호스트 네임을 host 변수로 받아온다.
			string host = Dns.GetHostName();
			IPHostEntry ipHost = Dns.GetHostEntry(host);
			// 배열 0번 인덱스에 있는 주소만 사용할 예정
			IPAddress iPAddress = ipHost.AddressList[0];
			// 최종 주소. 식당으로 비유하자면 iPAddress는 식당 주소이고, port 7777은 문의 번호에 해당한다.
			IPEndPoint endPoint = new IPEndPoint(iPAddress, 7777);

			try
			{
				_listener.Init(endPoint);
				while (true)
				{
					Console.WriteLine("Listening...");
					Socket clientSocket = _listener.Accept();
   					// ...이하 동일
				}
			}
			catch(Exception e)
			{
                Console.WriteLine(e.ToString());
            }
		}
	}
}

 

2.2.  Server 코드 수정

2.2.1. Blocking 방식의 문제점

 

2.2.2. Accept()를 RegisterAccept()와 OnAcceptCompleted() 두 파트로 나누기

 

 

 

 

 

[전체 코드]

Listener class

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

namespace ServerCore
{
	class Listener
	{
		Socket _listenSocket;
		Action<Socket> _onAcceptHandler;

		// endPoint를 이용해서 bind도 하고 listen도 하니까 초기화할 때 인자로 필요할 것이다.
		public void Init(IPEndPoint endPoint, Action<Socket> onAcceptHandler)
		{
			_listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
			_onAcceptHandler += onAcceptHandler;
			
			_listenSocket.Bind(endPoint);
			// backLog: 최대 대기수
			_listenSocket.Listen(10);

			// 한 번만 만들어주면 다음에 계속 재사용할 수 있다
			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)
			{
				// Todo
				_onAcceptHandler.Invoke(args.AcceptSocket);
			}
			else
                Console.WriteLine(args.SocketError.ToString());
			// 여기까지 왔으면 모든 일이 다 끝났다는 의미가 되므로, 다음 클라이언트를 위해 다시 등록해준다.
			RegisterAccept(args);
        }
	}
}

 

 

Program class

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

namespace ServerCore
{
	class Program
	{
		static Listener _listener = new Listener();
		static void OnAcceptHandler(Socket clientSocket)
		{
			try
			{
				// 받는다!!!
				// clientSocket이 보낸 데이터를 receiveBuff에 저장해준다.
				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!!");
				clientSocket.Send(sendBuff);

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

		static void Main(string[] args)
		{
			// 내 로컬 컴퓨터의 호스트 네임을 host 변수로 받아온다.
			string host = Dns.GetHostName();
			IPHostEntry ipHost = Dns.GetHostEntry(host);
			// 배열 0번 인덱스에 있는 주소만 사용할 예정
			IPAddress iPAddress = ipHost.AddressList[0];
			// 최종 주소. 식당으로 비유하자면 iPAddress는 식당 주소이고, port 7777은 문의 번호에 해당한다.
			IPEndPoint endPoint = new IPEndPoint(iPAddress, 7777);


			_listener.Init(endPoint, OnAcceptHandler);
			Console.WriteLine("Listening...");

			while (true)
			{
				//Socket clientSocket = _listener.Accept();
			}
		}
	}
}

 

 

[참고 사이트]

[C#과 유니티로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버

@livelyjuseok

 

SocketAsyncEventArgs.Completed Event

 

소켓 프로그래밍. (Socket Programming)

‍완벽히 이해하는 동기/비동기 & 블로킹/논블로킹 - Inpa Dev

[network] 동기vs비동기, 블로킹vs논블로킹의 차이

C# Non - blocking을 사용한 Server 소켓프로그래밍의 Connector

Sync/Async, Blocking/Non-Blocking 무슨 차이일까?

 

[이것이 C#이다] 13. 대리자와 이벤트

대리자) 델리게이트 & 이벤트