728x90
게임 서버 프로그래밍 교과서
2장. 컴퓨터 네트워크
2.1 OSI 모델
- 계층 1 : 물리 계층
- 계층 2 : 데이터 링크 계층
- 계층 3 : 네트워크 계층
- 계층 4 : 전송 계층
- 계층 5 : 세션 계층, 6 : 표현 계층, 7 : 응용 계층
데이터 링크 계층
- 각 단말기는 고유한 주소를 갖는다.
- 단말기는 데이터를 프레임이라는 단위로 주고 받는다.
네트워크 계층
- 라우터는 서로 다른 LAN 사이에 컴퓨터 통신을 할 수 있게 하는 장비이다.
- 3계층에서 흔히 접하는 것은인터넷 프로토콜(Internet Protocol)혹은 IP라고 하는 것이다. 인터넷 프로토콜에서는 주소 형식 8비트 숫자 4개로 구성되어 있다. 우리가 흔히 IP 주소라고 칭하는 것이 이것이다.
- 데이터는 IP에 의해 IP 패킷 형태로 포장된다.
2.2 인터넷
- 서로 다른 종류의 많은 스위치와 라우터가 연결되어 있다. 우리는 이것을 인터넷이라고 한다.
2.3 컴퓨터 네트워크 데이터
스트림 형식
- 스트림(stream)이란 데이터의 흐름이다. 두 단말기를 연결한 후 그 연결을 끊기 전까지는 한쪽에서 다른 한쪽으로 연결된 데이터 흐름 하나를 일컫는다. 스트림 안에 있는 데이터를 중간에 끊는 것은 여러분의 몫이며, 스트림 자체는 데이터를 중간에 구별하지 않는다.
- 컴퓨터 네트워크에서 스트림 개념은 보낸 개수와 받는 개수가 다를 수 있다. 또 보낸 데이터의 시작과 받는 데이터의 시작이 다를 수도 있다. 보낸 데이터의 끝도 마찬가지다. 그러나 결국 받은 데이터를 모두 이어붙이면 보낸 데이터를 이어 붙인 것과 같다.
- 따라서 스트림 형식으로 데이터를 송수신 할 때, 데이터가 여러 부분으로 나뉘어있다면 여러분이 이것을 따로 정의해주어야 한다. 예를 들어 어떤 데이터를 보내기 전에, 보낼 데이터 크기를 먼저 보낸다든지 데이터 시작이나 끝을 알리는 특정 기호를 추가한다든지 한다. 전자는 헤더를 붙이는 방식이라고 하며, 후자는 구분자(delimiter)를 이용하는 방식이라고 한다.
메시지 형식
- 스트림과 달리 메시지는 시작과 끝을 구별할 수 있다. 각 데이터가 정확히 구별되는 것을 메시지 형식 이라고 한다. IP 패킷에는 주고 받는 데이터, 즉 페이로드의 크기와 송신자 주소, 수신자 주소, 체크섬등이 들어있다.
- 수신자 주소란 수신 측 단말기까지 전달할 때 라우터에서 필요한 정보이다. 송신자 주소는 수신 측 단말기까지 도착한 후 수신자 측에서 송신자를 식별하는 용도로 쓴다.
- IP 패킷의 크기는 제한적이지만, 스트림이나 메시지에서는 이러한 제한이 없다. 운영체제에 내장된 네트워크 모듈인 네트워크 스택에서 프로그램이 보내고 받는 스트림과 메시지를 관리해주기 때문이다.
- 프로그램이 매우 긴 스트림을 송신할 때 운영체제는 이를 IP패킷의 크기 제한에 맞춰 여러 조각을 낸다. 이를 단편화라고 한다. 각 조각은 IP패킷 하나하나가 되어 받는 쪽에 송신한다. 받는 쪽에서는 이 조각들을 받아 조립한 후 스트림 형태로 복원한다. 이 과정도 운영체제에서 진행한다.
2.4 컴퓨터 네트워크 식별자
- 컴퓨터 네트워크 식별자 즉 주소는 인터넷에서 모두 고유하다.
- IP 주소는 크게 버전4 주소형식(IPv4), 버전 6 주소 형식(IPv6)이 있다. IPv4 주소 형식은 1바이트 숫자 4개 조합으로 32비트이다.
- 한 IP주소 안에서 누가 주고 받는 것인지 식별하는 역할을 하는 것이 포트(port)이다. IP 주소와 포트를 같이 표현하는 방법은 <IP주소 : 포트> 형식이다. 이렇게 IP 주소와 포트를 한데 묶어 끝점(endpoint)라고 한다.
2.5 컴퓨터 네트워크의 품질과 특성
- 네트워크의 품질을 저해하는 것들
- 스위치나 라우터에 자신이 처리할 수 있는 한계를 넘는 데이터가 수신되면 크게 둘 중 하나의 방식을 사용한다.
- 자기가 처리할 수 있는 것 이상을 그냥 버린다.
- 아직 처리하지 못한 것들을 메모리에 누적한다.
- 네트워크 기기가 처리하지 못한 패킷을 버리는 것을 패킷 드롭(drop)이라고 한다. 결과적으로 패킷 유실이 일어난다.
- 네트워크 기기가 처리할 수 있는 한계를 넘어가면 패킷 유실이 발생할 수 있다.
- 회선 신호가 약하거나 잡음이 섞이면 패킷 유실이 발생할 수 있다.
전송 속도와 전송 지연 시간
- 네트워크 품질을 거론할 때는 전송속도(스루풋)와 레이턴시(전송 지연 시간) 현상도 거론한다. 여기서 전송속도란 두 기기 간에 초당 전송될 수 있는 최대 데이터 양을 의미한다. 보통 초당 비트수나 혹은 바이트 수로 표현한다.
- 레이턴시는 두 기기간에 데이터를 최소량 전송할 때 걸리는 시간을 의미하며, 많이 쓰는 단위는 밀리초(ms)이다.
- 송신자와 수신자간 네트워크 기기 안 하드웨어와 소프트웨어의 처리속도도 네트워크 레이턴시의 원인이 된다.
- 두 단말기 사이의 레이턴시 = 두 단말기 사이에 있는 네트워크 기기의 레이턴시 총합
- 두 단말기 사이의 스루풋 = 두 단말기 사이의 네트워크 기기 중 최소 스루풋
네트워크 품질 기준 세 가지
- 전송속도(스루풋)
- 전송될 수 있는 데이터의 단위시간 당 총량을 의미한다. 전송속도는 회선의 종류가 좋을수록, 네트워크 장비의 처리 속도가 빠를수록 향상된다. 당연히 전송속도는 높을수록 좋다.
- 패킷 유실률
- 전송되는 데이터가 중간에 버려지는 비율이다. 회선 품질이 좋을수록, 경로에 있는 라우터 개수가 적을수록, 라우터의 처리성능이 좋을수록 낮다. 패킷 유실율은 낮을수록 좋다.
- 레이턴시
- 전송되는 데이터가 목적지에 도착하는데 걸리는 시간이다. 회선의 길이가 길 수록, 경로에 있는 라우터 개수가 많을수록, 라우터의 처리 성능이 낮을수록 높다. 레이턴시는 낮을수록 좋다.
2.6 컴퓨터 네트워크에서 데이터 보내기와 받기
UDP 네트워킹
- UDP(user datagram protocol), 사용자가 정의한 데이터 그램을 상대방에게 보낼 수 있게하는 통신 규약이다. 데이터그램은 64KB 이하의 이진 데이터로, 스트림이 아닌 메시지 성질을 갖는다. 즉, 데이터 일부가 뭉치거나 쪼개지지 않는다.
- UDP는 패킷 유실 현상이 발생할 수 있다. 데이터그램을 하나 이상 못받거나 같은 데이터그램을 두 번 이상 받을 가능성이 있으며, 심지어 보낸 순서와 다르게 데이터그램을 받을 수도 있다.
- 따라서 받는 쪽에서 데이터그램 유실이나, 순서 뒤바뀜 혹은 중복 수신 현상이 발생해도 괜찮을 때만 UDP를 사용하는 것이 좋다.
- UDP로 데이터를 주고 받으려면 소켓을 생성해야 한다. 소켓은 단말기 사이에 통실할 수 있게 운영체제에서 제공하는 자원이다. 파일을 읽고 쓰려면 파일 핸들 혹은 파일 디스크립터를 생성해야 하듯이, 통신을 하려면 소켓을 생성해야 한다. 소켓을 생성하면 소켓 핸들값이 반환되며, 이는 대부분 운영체제에서 int32 타입이다.
- UDP 소켓을 이용해서 데이터를 보내는 것 뿐만 아니라 받는 것도 가능하다. 수신용 소켓, 송신용 소켓을 따로 만들지 않아도 된다. 보통은 수신용 소켓과 송신용 소켓을 따로 만드는 것 자체를 권장하지 않는다.
- UDP의 또 다른 특징은 다대다 통신이 가능하다는 것이다. 상대방의 끝점만 알면 데이터를 보내면 된다. UDP의 사용법이 간단한 반면, 데이터 유실 또는 순서 뒤바뀜 같은 문제가 생긴다는 단점이 있다. 따라서 데이터를 보낸 순서와 똑같이 받으려면 데이터가 정확하게 왔는지 검사하고, 잘못되었으면 다시 보내는 대책이 필요하다.
TCP 네트워킹
- 이러한 문제는 TCP를 이용해서 해결할 수 있다. TCP는 보내는 쪽 데이터가 받는 쪽에서 완전히 동일함을 보장해 주는 프로토콜이다. UDP는 sendto(), recvfront() 함수만 있으면 데이터를 주고 받을 수 있다. 하지만 TCP는 데이터를 주고 받기 전에 ‘연결’이라는 과정을 먼저 해야한다. 이를 보고 연결지향형 이라고 한다. 또 여연글은 일대일만 가능하다.
- 연결한 후에야 데이터를 주고 받을 수 있다는 불편함이 있지만, 그 대신 보낸 데이터가 받는 쪽에서 정확히 모두 도착한다는 것을 보장한다. 이러한 중요 특징 때문에 인터넷 프로그램 대부분은 TCP를 활용한다.
- TCP는 메시지 형태가 아니라 스트림 형태다. 즉, 데이터를 뭉치거나 쪼갤 수 있다.
- UDP는 IP패킷 유실이 발생할 경우 UDP 데이터그램도 덩달아 드롭된다. 하지만 TCP는 똑같은 상황에서도 데이터가 상대방에게 정확히 전송된다. 이것이 가능한 이유는 TCP가 갖고 있는 흐름 제어 기능(data flow control) 때문이다.
- TCP에서 보낼 스트림 데이터는 세그먼트라는 IP 패킷에 넣을 수 있는 크기의 단위로 쪼개진다. 그러고나서 IP 패킷안에 세그먼트를 넣어서 수신자에게 전송된다. 그리고 수신자는 IP패킷을 받으면 여기서 세그먼트를 꺼내 받은 세그먼트 응답을 송신자에게 반송한다. 이 응답을 ack혹은 acknowledge라고 한다.
- 보낸 쪽에서는 일정 시간 안에 세그먼트에 대한 ack가 회신되지 않으면, 상대방이 받았다는 응답이 올때까지 다시 세그먼트를 보낸다.
2.7 패킷 유실 시 UDP와 TCP에서 현상
- UDP에서는 데이터그램이 유실된다.
- TCP에서는 중간에 지연 시간이 발생한다.
- UDP는 주로 레이턴시가 민감하거나 패킷 유실이 있어도 괜찮은 곳에서 주로 사용한다. 그외의 모든 경우에서는 TCP를 사용한다.
- 네트워크 게임 개발에서 거의 모든 메시지 종류에는 TCP를 사용한다. 앞서 언급했듯이 캐릭터 이동같은 몇몇 상황에서만 UDP를 사용한다.
게임 서버 최적화
megayuchi님의 (socket + C/C++기반의)실시간 게임서버 최적화 전략 를 보고 정리 중 이다.
실시간 게임 최적화 전략
- 수동적인 서버
- 겨로가만 저장해주는 서버
- 패킷 중계
- 능동적인 서버 - 일반적인 게임서버
- 생태를 서버의 메모리에 저장하고 있다.
- DB는 보조 수단이다.
- 클라이언트의 시간 차 0을 목표로 한다.(불가능 하지만)
- 게임 루프가 있다.
- 서버가 하는 일 in Voxel Horizon
- 유저로부터의 패킷에 의한 이벤트처리
- 플레이어 or NPC에 대한 이동처리
처리량과 응답성의 차이
- 어느 부분이 문제인지 생각할 필요가 있다.
응답성 향상을 위한 주요 기법
- 비동기 처리
- 특정 작업 때문에 전체 응답성이 떨어지는 상황을 방지
- 멀티 스레드 - 별도의 worker thread가 처리
- 싱글 스레드 - 작업을 쪼개서 원래의 작업 스레드에서 여러번에 걸쳐서 처리
- thread pool을 이용한 병렬처리
- 서로 의존성을 가지지 않는 n개의 항목에 대해 M개의 worker thread가 병렬로 처리한다.
- thread pool이 작업을 완료 할 때 까지 원래의 작업 스레드는 대기한다.
- 코드 최적화 - SIMD 등
- 서로 조합해서 사용 가능하다.
비동기 처리
- 로그인/로그아웃
- 급하지 않은 작업
- 특정 작업 때문에 전체 응답성이 떨어지는 상황을 방지한다.
- DB 종속적인 작업들
- 저장소에 대한 I/O
비동기 처리 - 싱글 스레드
- 별도의 worker thread를 생성하지 않는다.
- 긴 시간이 걸리는 작업을 쪼개서 메인 스레드의 타임 라인에 분산해서 처리한다.
- 시간 전체 처리 소요 시간은 그대로, 처리까지 걸리는 시간은 상승, 응답성은 상승
- 스레드 간 동기화가 복잡해질 경우 선택한다.
- 스레드간 동기화 비용 0
- 메인 스레드의 응답성은 약간 저하 될 수 있다.
비동기 처리 - 멀티 스레드
- 별도의 worker thread에 작업을 할당하여 완전히 백그라운드로 작업
- 남는 CPU자원을 조금이라도 더 사용할 수 있다.
- worker thread로 작업을 넘겼을 때 필요한 자원도 완전히 분리할 수 있을 경우(동기화 부담이 없을 경우)에 적합하다.
- 메인 스레드 응답성에 거의 영향을 끼치지 않는다.
- 스레드 동기화 비용이 아주 적을 경우만 사용해야 한다.
Thread Pool을 이용한 병렬처리
- 1차선 도로를 N차선 도로로 확장
- 처리량 때문에 지연이 발생하는 경우 사용
- 처리해야 할 항목(원소) 각각이 서로 의존성이 거의 없어야 함
- thread per core일 때,(이론상) 거의 선형적으로 처리량 증가
- 비동기 처리와 병행 가능
코드 최적화 - SIMD등
- 당장 수행되어야 할 코드
- 컨텍스트 스위칭 없이 현재 컨텍스트 상에서 당장 빠르게 코드에 실행되어야 하는 경우
- thread pool을 쓰는 경우 thread pool이 작동하기 시작해서 결과를 동기화할 때 까지의 비용이 더 클 수 있다.
- 그냥 정직하게 빠른 코드를 짜야한다.
- 수학 연산이 많다면 SIMD는 반드시 사용한다.
- STL을 걷어내고 직접 짜게되면 보통 빨라진다.
응답성 향상을 위한 코딩
- 범용 Heap 보다는 Stack을 사용
- 범용 Heap보다는 고정 사이즈 memory pool(병합기능 x)
- 적절한 동기화 객체 선택
- SIMD 최적화의 경우 전체적으로 최대 30%까지 성능 향상을 기대할 수 있다.
실제 게임서버에 적용
VOXEL Horizon에서의 사례
기본전략
- 응답성 최우선
- 1개의 메인스레드 + n개의 보조 스레드 풀
- 동기화로 인한 성능 저하 방지
- 유지보수 용이(디버깅, 후임자 인수인계)
- 클라이언트와 서버가 대부분의 코드를 공유할 것
- 동일 입력 동일 결과 보장
- 유지보수 용이
- 멀티 스레드 남용 금지!
- 가능하면 SIMD 사용 → 그러나 큰 기대는 금물
서버와 클라이언트의 코드 공유
- 클라이언트와 서버는 최대한 코드를 공유한다.
- 서버와 클라이언트가 동일한 입력에 대해 동일한 결과를 보장
- 엔진 코드는 서버에서도 그대로 사용하므로 서버에서 필요한 기능을 지원할 것
- 다수의 인스턴스 생성
- 멀티 스레드를 이용한 병렬 처리
- 비동기 처리
서버의 작업 스케쥴링 전략
- 1개의 메인 스레드 + N개의 보조 스레드 풀
- 동기화로 인한 성능 저하 방지
- 유지보수 용이
- 메인 스레드의 명령에 의해 다수의 워커 스레드가 작업
- 작업을 마치면 워커 스레드가 메인 스레드에 통보
패킷 수신 → 메시지 처리
- 최소 패킷 구조 = SIZE(4 bytes) + Body(N bytes)
- I/O 워커 스레드의 메시지 수집과 메인 스레드의 경쟁 상태를 줄인다.
- Double buffering
- 하나 이상의 패킷이 수집되면 network 측 worker thread가 쓰기 버퍼에 수집된 패킷을 써넣는다.
- main thread는 패킷 수신이 통보되면 쓰기 버퍼와 읽기 버퍼의 포인터를 swap한다(가벼운 lock사용)
- main thread는 읽기 버퍼의 쌓인 패킷을 처리한다.
- 처리가 완료되면 쓰기 버퍼와 읽기 버퍼의 포인터를 swap한다.
백준
1269 대칭 차집합
#include <bits/stdc++.h>
using namespace std;
map<int, bool> _map;
int n, m;
int main()
{
cin >> n >> m;
for (int i = 0; i < n + m; ++i)
{
int a;
cin >> a;
if (_map[a] == true)
_map.erase(a);
else
_map[a] = true;
}
cout << _map.size();
return 0;
}
- map 자료구조를 사용했다.
- 넣으려고 하는 값을 가지고 있다면 삭제해주고 없다면 추가해주는식으로 구현했다.
728x90
'Study > TIL(Today I Learned)' 카테고리의 다른 글
24.04.08 서버 프로그래밍, 백준 (0) | 2024.04.09 |
---|---|
24.04.07 서버 최적화 (1) | 2024.04.07 |
24.04.05 서버 프로그래밍 (1) | 2024.04.06 |
24.04.04 회고 (0) | 2024.04.05 |
24.04.03 PintOS (0) | 2024.04.03 |