Game Development, 게임개발/게임네트워크

TCP,UDP 소켓프로그래밍, C# - 소켓 생성부터 종료까지

게임이 더 좋아 2021. 9. 28. 18:36
반응형
728x170

 

게임 네트워크를 알아보는데 직접 해보는 것만큼 도움되는 것이 없다.

소켓 프로그래밍을 해보자.

 


 

우선 소켓이란 TCP와 UDP를 간단하게 다루기 위한 통신 API이다.

IP 주소와 포트 번호를 같이 묶어서 상대를 지정하고 통신하는 것이다.

통신 상대로는 네트워크로 연결된 단말이나 같은 단말 내의 어플리케이션이 가능하다.

**같은 네트워크 안에 있는 다른 컴퓨터나 컴퓨터 안에서의 프로세스라고 하면 되겠다.

 

네트워크로 연결된 단말을 지정하여 통신하려면 어느 단말의 어플리케이션과 통신할 것인지 정해야 한다.

실제로 통신할 때는 물리적으로 케이블을 포트에 연결하여 단말끼리 접속한다.

 

소켓은 말 그대로 어플리케이션에 가상으로 존재하는 이더넷 포트를 말한다.

소켓으로 통신할 때는 소켓이 지정한 포트에 전용 이더넷 포트를 만들고

통신 상대의 가상 이더넷 포트와 가상 케이블로 연결된 것 같은 상태로 통신하는 것이다.

 

또한 소켓을 이용하면 같은 단말에 있는 다른 어플리케이션과도 통신이 가능하다.

 

소켓을 사용해 접속, 접속 종료, 데이터 전송을 하려면 프로그램을 작성해줘야 한다.

한 번 진짜로 해보자

 


 

먼저 우리가 할 것은 TCP로 프로그래밍을 해볼 것이다.

TCP로 접속하는 단말은 접속을 기다리는 서버접속을 요청하는 클라이언트로 나눌 수 있다.

 

TCP 서버의 대기를 시작으로 해보자.

처음에는 소켓에서 대기해야 하는데

소켓을 사용하려면 소켓을 생성해서 사용할 포트 번호를 할당하고

클라이언트가 접속할 수 있도록 대기하는 것이다.

 

TCP 서버에서 대기하는 코드를 보자.

	// 대기
	void StartListener()
	{
		Debug.Log("Start server communication.");
		
		// Create Socket, 소켓 생성 -> 대기 전용 소켓으로 listening socket이라고 함
		m_listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
		// 소켓의 종류는 Stream, 프로토콜 타입은 TCP로 지정한다.
        
        
        // Assign port number to use, 포트 번호 할당-> Bind함수로 end point를 지정한다.
		m_listener.Bind(new IPEndPoint(IPAddress.Any, m_port));
        
        
		// Wait for Client, 클라이언트 접속 대기 -> Listen(1)은 대기상태가 True라고 생각하면 된다.
		m_listener.Listen(1);

		
		m_state = State.AcceptClient;
	}

 

 

클라이언트의 접속 요청을 받는다.

	// 클라이언트의 접속 대기.
	void AcceptClient()
	{
    	//무한대기를 할 수 없으니 null이 아니면서 클라이언트에서 데이터를 감지하면 아래 내용을 수행한다.
		if (m_listener != null && m_listener.Poll(0, SelectMode.SelectRead)) {
			
            // 클라이언트가 접속했습니다. has connection with server
            // Accept()는 blocking 상태이기 때문에 Accept를 하는 조건을 위에서 준다.
			m_socket = m_listener.Accept(); 
            
			m_isConnected = true;
		}
	}

 

서버에서는 Accept 함수를 통해 클라이언트 접속 요청을 받고

Socket 클래스의 Accept 함수는 클라이언트가 접속을 요청할 때까지 블로킹, blocking을 한다.

** 호출한 함수를 처리할 때까지 제어가 돌아오지 않음을 말하는데

이는 Accept 함수가 수행될 때까지 main routine으로 돌아오지 않는 것과 비슷하다.

 

blocking 상태에서는 클라이언트 접속이 들어올 때까지 어플리케이션의 작동을 막는다.

 

하지만 실제로 온라인 게임에서 이렇게 구현하면 게임이 돌아가질 않으니

Poll 함수로 클라이언트가 보내는 데이터를 감시해서 

데이터를 수신했을 때만 Accept 함수를 호출하게끔 한다.

그 이후

새로 생성한 소켓 인스턴스로 클라이언트와 통신하면 된다.

 

 


 

TCP 클라이언트가 대기 중인 서버에 접속해보자.

서버일 때와 마찬가지로 클라이언트도 통신을 담당하는 소켓이 필요하다.

통신 상대가 될 단말의 IP 주소와 포트 번호를 지정해서 접속을 해야 한다.

 

통신할 서버의 리스닝 소켓이 대기 상태라면 접속할 수 있다.

소켓을 이용해서 서버에 접속해보자.

 

void ClientProcess()
	{

		// 서버에 접속.
		m_socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
		m_socket.NoDelay = true;
		m_socket.SendBufferSize = 0;
		m_socket.Connect(m_address, m_port);


	}

소켓을 생성한다.

서버의 소켓의 종류와 프로토콜타입이 같다.

 

또한 생성한 소켓에는 작은 패킷을 버퍼링하지 않도록 Socket.Nodelay = True로 해준다.

또한 TCP buffer의 크기를 0으로 해준다.

** 작은 패킷을 전송할 때는 TCP버퍼에 패킷을 담아서 꽉차거나 시간이 지나고 나서 보내기 때문에

delay가 생긴다.

 

클라이언트의 소켓이 접속할 소켓의 IP 주소와 포트 번호를 지정해서 Connect 함수로 요청을 한다.

**Connect 함수도 블로킹함수이기 때문에 접속이 될 때까지 해당 어플리케이션이 다른 행동을 할 수 없다.

++접속에 실패하면 SocketException throw 함

 

그래서 blocking을 false로 해놓는다면 블로킹하지는 않지만

상대와 통신 접속이 완료되었는지 어플리케이션에서 확인을 해줘야 한다.

 

 

위와 같이 프로그램을 한다면

서버와 클라이언트 간의 접속이 가능하고 통신할 수 있다.

하지만 아직 데이터의 송수신에 관해서를 프로그램을 작성하지 않았다.

 

알아보자

 


 

데이터 송수신을 해보자

서버와 클라이언트가 연결된 뒤에는

서버는 Accept 함수로 가져온 Socket 클래스의 인스턴스를 사용하고

클라이언트는 Connect 함수로 가져온 Socket의 인스턴스를 사용해서 통신한다.

 

그리고 소켓의 Send, Receive 함수로 데이터를 송수신할 수 있다.

Send 함수로 보낸 데이터는 수신 단말의 버퍼에 저장되고

버퍼에 저장된 데이터는 Receive 함수로 가져올 수 있다.

 

위에 이어서

ClientProcess()를 보자

void ClientProcess()
	{
		Debug.Log("[TCP]Start client communication.");

		/*
		m_socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
		m_socket.NoDelay = true;
		m_socket.SendBufferSize = 0;
		m_socket.Connect(m_address, m_port);
		*/
        
		// 메시지 송신.
		byte[] buffer = System.Text.Encoding.UTF8.GetBytes("Hello, this is client.");	
		m_socket.Send(buffer, buffer.Length, SocketFlags.None);
        
}

 

메세지를 송신하는 부분이 있다.

buffer에 해당하는 부분을 인코딩해서 집어넣고

해당 buffer를 보낸다.

 

서버에서의 수신은 아래와 같이 한다.

	// 클라이언트의 메시지 수신.
	void ServerCommunication()
	{
		byte[] buffer = new byte[1400];
		int recvSize = m_socket.Receive(buffer, buffer.Length, SocketFlags.None);
		if (recvSize > 0) {
			string message = System.Text.Encoding.UTF8.GetString(buffer);
			Debug.Log(message);
			m_state = State.StopListener;
		}
	}

어플리케이션이 Receive 함수를 호출할 때까지 데이터는 시스템의 버퍼에 저장되어있다.

**Receive를 하지 않을 경우 시스템의 수신 버퍼가 꽉차서 수신이 불가능한 상태가 될 수 있다.

서버는 버퍼를 가지고 있고 Receive로 클라이언트가 보낸 버퍼를 가져온다.

 

다음은 통신 종료이다.

알아보자

 


클라이언트는 아래와 같이 종료한다.

	void ClientProcess()
	{
    	/*

		// 서버에 접속.
		m_socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
		m_socket.NoDelay = true;
		m_socket.SendBufferSize = 0;
		m_socket.Connect(m_address, m_port);

		// 메시지 송신.
		byte[] buffer = System.Text.Encoding.UTF8.GetBytes("Hello, this is client.");	
		m_socket.Send(buffer, buffer.Length, SocketFlags.None);
		*/
		// 접속 해제. 
		m_socket.Shutdown(SocketShutdown.Both);
		m_socket.Close();

		Debug.Log("[TCP]End client communication.");
	}

 

Shutdown 함수를 이용하여 패킷에 대한 송신을 차단한 다음

수신이 끝난 뒤 Close로 접속을 끊는다.

 

서버에서의 대기 상태를 종료할 때도 Close를 호출하여 리스닝 소켓을 닫는다.

	// 대기 종료.
	void StopListener()
	{	
		// 대기를 종료합니다.
		if (m_listener != null) {
			m_listener.Close();
			m_listener = null;
		}

		m_state = State.Endcommunication;

		Debug.Log("[TCP]End server communication.");
	}

 

여기서 UDP로 프로토콜을 바꾸고 싶다?

UDP는 서버와 클라이언트 구별도 없고

대기도 필요 없다. 프로토콜 타입하고 소켓 종류만 바꿔서 소켓 생성후 IP주소, 포트번호에 바로 보내면 된다.

 

UDP에서는 Send -> SendTo, Receive -> ReceiveFrom 을 쓴다.접속이 되어있지 않으므로보낼 때마다 IP주소와 포트번호를 지정해서 보내면 된다.

 

** 하지만 눈치챈사람이 있겠지만 UDP에서 Connect를 써도 된다. 

 

반응형
그리드형