게임 서버 프로그래밍 교과서(배현직 저)를 보고 공부하면서 정리한 내용입니다. 제 뇌피셜도 있고, 도서에서 알기 힘든 부분은 검색을 통해 보충한 부분도 있으므로 잘못된 정보가 있다면 언제든지 댓글 남겨주세요!!
컴퓨터 네트워크는 크게 단말기(terminal, endpoint)와 네트워크 기기로 구성됨.
네트워크는 연결 형태에 따라 다음과 같이 나눌 수 있음
어떠한 형태의 네트워크 구조라도 OSI 모델 표준만 지켜주면 네트워킹이 가능하다. LAN을 구축하고 싶은 경우, OSI 모델의 2계층을 지켜서 구축하면 된다.
OSI 모델에는 1계층 ~ 7계층이 존재한다.
2계층에서 데이터는 프레임(헤더 + 페이로드) 단위로 보내짐. 헤더에는 송수신 주소 정보가 있고, 페이로드에는 보내고자 하는 데이터가 담겨 있음.
Preamble | SFD | DA | SA | Len/Type | Data(+padding) | FCS |
---|---|---|---|---|---|---|
7 bytes | 1 byte | 6 bytes | 6 bytes | 2 bytes | 45 ~ 1500 bytes | 4 bytes |
이렇게 구축한 LAN은
1. 모든 단말기마다 고유 주소를 만들기 어려움
2. 스위치 당 연결 가능한 단말기 수가 정해져 있음
때문에 LAN과 LAN을 연결하여 광역 통신망; WAN을 구축하는 것
LAN 안에서 각 단말은 서로 구별되는 고유 주소를 담고 있지만, 다른 LAN에 속한 단말기와는 효과적으로 통신할 수 없음. 이에 대한 규약을 정의한 것이 3계층이다.
3계층은 IP(Internet protocol)와 라우터(계층적 구성)를 이용해 데이터를 패킷 단위로 전송한다.
참고) IPv4, IPv6, ICMP
무수히 많은 라우터와 스위치, 다른 종류의 통신 회선(랜선, 광섬유, 무선, 전화선 등)이 3계층의 IP에 따라 온 지구를 덮고 있다.
이것을 인터넷이라 한다.
2계층은 프레임, 3계층은 패킷단위로 데이터를 송수신한다. 그러나 응용프로그래머 입장에서는 이를 직접 다루는 일은 흔하지 않다. 응용프로그래머는 다음과 같은 두 가지의 데이터 전송 형식(스트림, 메시지)을 주로 사용한다.
데이터를 일련의 연속된 바이트 스트림으로 취급하고 이를 보내는 형식. 데이터의 크기나 경계에 대한 제한이 없다.(데이터를 중간에 구분하지 않는다.) 따라서 이 형식으로 데이터를 주고받을 때에는 다음과 같은 방식을 사용하는 경우가 많다.
메시지 단위로 데이터를 주고받은 형식. 크기와 경계에 제한이 있기에 각각의 패킷은 독립적으로 처리된다.
게임 서버에서는 매번 스트림을 열고 닫고 수신대기하는 것보다 여러 클라이언트에게 정해진 길이의 메시지를 뿌리는 일이 많기 때문에 보통 메시지 형식으로 데이터를 전달한다.
앞서 살펴본 스트림과 메시지에서는 데이터의 최대 크기가 정해져있지 않다. 그러나 3계층 네트워크 레이어에서는 IP(인터넷 프로토콜)에 따라 패킷의 최대 크기가 결정되어 있는데, OS는 이를 프레그멘테이션(Fragmentation)하여 (데이터를 MTU;최대전송단위에 맞게 나누고 헤더 붙이기) IP 패킷들을 만들고 이를 보낸다.
한편 IP 패킷(v4)의 구조는 다음과 같다.
위와 같이 IP 패킷에는 패킷의 총 길이, 체크섬, 송수신지의 ip 주소, 실제로 보낼 데이터들이 포함되어 있다.
255.255.255.255
형태로 나타나는 IP 주소 형식. 최대 256^4 ≈ 43억개의 주소를 나타낼 수 있다.(1바이트 숫자 4개)
ffff:ffff:ffff:ffff:0000:0000:0000:0000
형태로 나타나는 IP 주소 형식(연속으로 이어지는 0의 경우 생략 가능). 2바이트 숫자 8개로 이루어져 있는 주소체계로, 최대 2(2 * 8 * 8)개의 주소를 나타낼 수 있다.
출처
한 단말 안에서 여러 프로세스 간 구별하기 위해 사용하는 주소. 2바이트 정수 공간을 가지며, IP주소 뒤에 ':'과 함께 붙여 사용한다.
윈도우에서 사용 중인 포트들을 보고싶다면 netstat -ano 명령어를 입력하면 된다.
일반적으로 IP주소:포트로 다른 엔드포인트에 접속하는 것은 사용자 입장에서는 매우 불편하다. 이때 사용되는 것이 host name과 DNS 서버이다.
라우터는 한번에 처리할 수 있는 수 이상의 패킷이 들어오면
후자의 경우 장시간 지속 시 멈춰버리거나 재부팅되는 경우가 있기 때문에, 대부분의 라우터는 전자의 전략을 택한다. 모든 유저의 네트워크 품질을 저하시키는 것보다 일부 사용자에 대해서만 품질을 떨어뜨리는 것이 낫기 때문.
라우터 말고 유/무선 회선에서 일어나는 일련의 과정 때문에 품질이 저하되기도 한다. 수신측에서의 디지털 신호는 OSI 1계층에 의해 아날로그 신호로 바뀌며, 이후 다시 송신측에서 디지털 신호로 바뀐다. 이 과정에서 잡음(노이즈)가 섞이거나, 신호가 약해질 수 있다. 2계층과 3계층에서는 체크섬 검사 등의 방법을 통해 데이터를 수정할 수 있는데, 수정조차 어려운 경우에는 프레임 혹은 패킷을 버린다.
정리하자면
스루풋과 레이턴시 역시 네트워크의 성능을 판단할 때 사용될 수 있다.
위 설명에 따라 다음 두 명제가 성립한다.
따라서 좋은 네트워크란,
1. 스루풋이 우수해야한다.
2. 패킷 유실률이 적어야 한다.
3. 레이턴시가 낮아야 한다.
일반적인 상황에서 무선네트워크를 통한 데이터 송수신을 하면 레이턴시가 불균형하게 나타난다. 그 이유는 CSMA(Carrier Sense Multiple Access) 프로토콜을 따르기 때문이다.
한편 CSMA 프로토콜은 여러 변형이 있으며, 그중 CSMA/CD와 CSMA/CA 두 프로토콜이 자주 사용된다.
User Datagram Protocol은 사용자가 정의한 데이터그램을 상대방에게 보낼 수 있도록 하는 프로토콜이다. Connectionless하기 때문에 스트림을 열지 않으며, 이에 따라 전송 순서가 뒤바뀌거나 일부 패킷이(데이터그램이) 유실될 수도 있다.
게임 서버에서는 캐릭터의 이동 정보(위치 등)를 보낼 때 UDP를 사용할 수 있다. 유실이 되더라도 이후에 들어온 정보로 쉽게 보정할 수 있기 때문이다.
UDP 소켓은 ip 주소(혹은 도메인 네임)과 포트 번호만 있으면 쉽게 데이터를 송수신할 수 있다. 또한 다대다 연결이 가능하다.
다만 데이터 유실 또는 순서 뒤바뀜 문제가 생길 수 있기에 이러한 문제가 중요한 곳에는 적용하기 힘들다는 단점이 있다.
// 송신 측 예시
main()
{
s = socket(udp)
s.bind(any_port)
s.sendTo("수신 ip addr:1212", "data")
s.close()
}
// 수신 측 예시
main()
{
s = socket(udp)
s.bind(1212)
result = s.recv()
s.close()
}
Transmission Control Protocol은 송신 측 데이터와 수신 측 데이터가 완전 동일함을 보장해주는 프로토콜이다. UDP와 달리 연결이 필요하기에 데이터를 stream 형태로 송수신한다.
송신 데이터는 세그먼트 단위로 쪼개져 3계층 IP 패킷에 담기게 된다. 수신측에서는 IP 패킷에서 세그먼트를 꺼낸 후 세그먼트를 받았다는 응답을 송신자에게 반송한다. ack 응답이 없을 경우, 수신자는 세그먼트를 재전송한다.(OS 단)
TCP 소캣은 UDP와 달리 일대일 연결만 가능하다.
// 송신 측 예시
main()
{
s = socket(tcp)
s.bind(any_port)
s.connect("수신 ip addr:1212")
s.send("data")
s.close()
}
// 수신 측 예시
main()
{
conn_s = socket(tcp)
conn_s.listen(1212)
s = conn_s.accept()
while(true)
{
result = s.recv()
if(r.length == 0) break
}
s.close()
}
앞서 설명했듯, UDP의 데이터그램과 TCP의 세그먼트는 3계층 Network Layer의 IP MTU(Maximun Transmission Unit)에 맞춰 패킷으로 분할되어 전달되는데,
1~3계층에서 패킷이 유실될 수 있음에 따라 UDP와 TCP는 이에 각각 다른 전략을 취한다.
UDP는 분할된 패킷 중 1개의 패킷만 유실되어도 수신자 측은 전체 데이터그램이 유실된 것으로 판단한다(따라서 보내고자 하는 데이터그램의 크기가 클수록 유실될 확률도 높다). 반면 TCP는 유실된 패킷의 ack이 도착하지 않기 때문에 해당 패킷을 재전송한다. 수신측에서는 다른 패킷들이 다 도착하더라도 재전송된 패킷이 도착하기 전까지 세그먼트를 재조립하지 못한다.
정리하자면
따라서 네트워크 게임에서는 레이턴시에 민감하고, 패킷 유실이 있어도 괜찮은 곳에서 UDP를 주로 사용하고, 그 외의 모든 경우에는 TCP를 사용한다.
UDP를 사용하기에 적합한 예시에는 캐릭터 이동, 음성 및 화상데이터 전송 등이 있다.
게임 서버에서의 메시지 형식은 크게 사람이 이해할 수 있는 텍스트 형식과 이해하기 힘든 바이너리 형식으로 나눌 수 있다.
텍스트 형식은 다음과 같이 사람이 이해할 수 있는 메시지 형식이다.
BuyItem<LF>
Sword<LF>
1<LF>
<0x00>
예전에는 위와 같이 게임 개발자가 자체적으로 정의하는 경우가 많았지만, 요즘에는 JSON, XML 등 표준화된 형식을 쓰는 것이 일반적이다. WAS로 게임서버를 구현하고자 하는 경우, HTTP restful api를 사용하기도 한다.
정해져 있는 형식에 맞춰 Parser를 동원하는 것이 일반적이다.
이진 형식은 다음과 같이 사람이 이해하기 힘든 형식이다.
0x01 | 0x0023 | 0x0001
// 각각의 필드는 BuyItem, Sword, 1을 의미한다고 가정
별도의 Parser가 필요 없기 때문에 성능 면에서 우수하고 통신량도 적다. 하지만 디버깅이 까다롭다는 단점이 존재한다.
Network Address Translation이란 다른 단말로 전송되던 패킷의 송수신 주소를 다른 것으로 변환하는 것을 말한다. 이를 담당하는 기기를 NAT 라우터라고 하며, 공유기가 이 역할을 수행하기도 한다.
4G, LTE, 5G같은 모바일 셀룰러 통신은 가정집 공유기보다 매우 많은 수의 기기를 지원해야 한다. 따라서 대부분의 ISP는 대규모 사용자용 NAT 라우터인 Carrier-grade NAT를 사용하고 있다.
Data communications and networking 도서 네트워크 교과서
IPv4와 IPv6 모두를 잘 지원하는 프로그램 개발
IPv4와 IPv6 모두를 잘 지원하는 프로그램 개발
Dual Stack과 NAT64/DNS64에 대한 이해
RUDP 구현 예시 : 프라우드넷, RakNet
홀펀칭 기술 : Full cone NAT, uPNP
클라우드 서비스에서의 NAT : L4 스위치와 로드밸런서
원격 프로시저 호출, 원격 메서드 호출(Remote Procedure Call, Remote Method Invocation)
Game Programming Gems 도서