기본 개념
단말기 = 고객
스위치 = 아파트 경비실
라우터 = 택배 배송 센터
상황 : 고객 a가 고객 b에게 택배(데이터)를 보내고 싶다.
고객 a는 현대 아파트에 살고, 고객 b는 자이 아파트에 산다.
a가 b에게 택배를 보내기 위해서 거쳐야 하는 단계는?
1. 아파트 경비실에서는 해당 아파트에 거주하는 고객들의 정보에 대해 알고 있다.
2. 따라서 고객 a는 현대 아파트 경비실에 택배를 전달하고 경비실은 택배 배송 센터에 택배를 전달한다.
3. 택배 배송 센터는 택배를 고객 b가 거주하는 자이 아파트 경비실에 전달한다.
4. 자이 아파트 경비실은 최종적으로 고객 b에게 택배를 전달한다.
- 생략된 부분은 택배 배송 센터에서 a아파트와 b아파트를 배송하는 이동 구간이 '인터넷'이라는 점이다.
TCP 5계층
5_어플리케이션 계층 (상품)
- http, dns, ftp 등 유저 인터페이스와 관련된 부분
4_트랜스포트 (배송 정책)
- tcp, udp 등 전송 확인, 문제 해결... 택배로 보낼지 퀵으로 보낼지에 대한 정책을 결정
3_네트워크 (최종 주소)
- IPv4, IPv6 네트워크 간 경로 설정 (라우터가 담당한다)
2_데이터 링크 (아파트 단지까지의 경로)
- 네트워크 내 경로 설정, 이더넷 등 (스위치가 담당한다)
1_피지컬 (택배 운송)
- 신호 처리 (케이블, 허브)
위 순서를 통해 데이터가 유저에게 어떻게 전달되는지 개략적으로 알 수 있다.
이렇게 나눔으로써 책임이 명확해지고, 어느 부분이 잘못되었는지 확인이 가능해진다.
3계층 주소를 담당하는 것은 IP이며, 장치는 라우터를 사용한다.
2계층 주소를 담당하는 것은 MAC이며, 장치는 스위치를 사용한다.
TCP와 UDP가 다른 점
- TCP는 안전한 트럭과도 같다. 조금 느릴지언정 안전하다
- UDP는 위험한 총알 배송 오토바이를 연상하면 된다.
1. 연결 지향성
1.1. TCP
- 연결형 서비스
- 연결을 위해 할당되는 논리적 경로가 있다.
- 전송 순서가 보장된다. (코드에서 본 것처럼 클라이언트가 서버에 접속하면 소켓을 할당하고 해당 소켓을 통해 긴밀하게 연락을 주고 받음)
- TCP는 전화에 비유되는데 전화를 걸었을 때 받는 쪽에서 받아야만 통신이 가능하기 때문.
- 클라쪽에서 connect를 요청하면 서버쪽에서는 accept를 통해 받아줘야 한다
(이 부분은 이후 UDP를 살펴보며 TCP와 비교해서 어느 부분들이 코드에서 관여하는지 확인할 수 있다)
- 데이터가 분실되면 다시 보낸다.
1.2. UDP
- TCP와 대칭되는 내용이라고 생각하면 이해하기 쉽다.
- 비연결형 서비스, 연결이라는 개념이 없다보니 전송 순서가 보장되지 않는다.
- 중간에 데이터가 분실되어도 책임지지 않는다.
2. 속도와 신뢰성
2.1. TCP
- TCP는 신뢰성이 좋지만 UDP에 비해 상대적으로 느리다. (이유는 아래 서술)
- 분실이 일어나면 책임지고 다시 전송
- 받는 쪽이 바쁘면 일부만 보낸다
2.2. UDP
- 분실에 대한 책임이 없고 그만큼 단순하기 때문에 빠르다.
3. 데이터의 경계
3.1. TCP
- TCP는 데이터의 경계가 없다.
- HELLOWORLD를 보냈으면 HELLOWOR / LD처럼 어디서든 잘릴 수 있음
3.2. UDP
- UDP는 택배 상자에 담아 보내는 개념이라 데이터가 각각의 경계를 가지고 있다.
그렇다면 게임 서버에선 어떤 방식을 써야 할까?
- MMO는 TCP와 궁합이 잘 맞다.
- 스킬을 쓰고 이동을 하는 순서로 움직여야 하는데 UDP는 순서 보장이 되지 않으므로 이동을 하고 스킬을 쓰는 등의 문제가 발생할 수 있다.
TCP 방식에서 데이터의 경계가 없다고 했다. 그렇다면 지금 온 패킷이 완성된 패킷인지 어떻게 판별하는가?
- 그래서 패킷 포맷을 만들어서 전달하게 되는데, 이 내용은 회사마다 다르다.
아래는 UDP 코드인데 TCP 코드를 완전히 삭제하지 않고 주석처리 해놓았으므로 어느 부분이 변경되었는지 확인할 수 있다.
/* Server.cpp */
#include "pch.h"
#include "ThreadManager.h"
int main()
{
/*소켓 초기화*/
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData))
return 0;
/*Handle의 개념*/
//SOCKET listenSocket = socket(AF_INET/*IPv4*/, SOCK_STREAM/*TCP 방식 사용*/, 0);
SOCKET listenSocket = socket(AF_INET/*IPv4*/, SOCK_DGRAM/*UDP 방식 사용*/, IPPROTO_UDP);
if (listenSocket == INVALID_SOCKET)
return 0;
/*-----------직원 고용------------*/
SOCKADDR_IN serverAddr;
memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
serverAddr.sin_port = htons(7777);
// 서버 소켓에 ip 주소와 포트 번호를 할당
if (::bind(listenSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
return 0;
// 서버 소켓을 대기 상태로 전환 -> 이 과정을 거쳐야지만 accept에서 대기할 수 있다
//if (::listen(listenSocket, SOMAXCONN) == SOCKET_ERROR)
// return 0;
while (true)
{
SOCKADDR_IN clientAddr;
memset(&clientAddr, 0, sizeof(clientAddr));
int32 addrLen = sizeof(clientAddr);
//SOCKET clientSocket = ::accept(listenSocket, (SOCKADDR*)&clientAddr, &addrLen);
//if (clientSocket == INVALID_SOCKET)
// return 0;
//char ip[16];
//::inet_ntop(AF_INET, &clientAddr.sin_addr, ip, sizeof(ip));
//cout << "Client connected! IP = " << ip << endl;
char recvBuffer[100];
int32 recvLen = recvfrom(listenSocket, recvBuffer, sizeof(recvBuffer), 0, (SOCKADDR*)&clientAddr, &addrLen);
if (recvLen <= 0)
return 0;
cout << "Recv Data : " << recvBuffer << endl;
cout << "Recv Data Len : " << recvLen << endl;
//this_thread::sleep_for(1s);
//int32 resultCode = ::send(clientSocket, recvBuffer, recvLen, 0);
//if (resultCode == SOCKET_ERROR)
// return 0;
}
closesocket(listenSocket);
WSACleanup();
}
/* DummyClient.cpp */
#include "pch.h"
// 클라
// 1. 소켓 생성
// 2. 서버에 연결 요청
// 3. 통신
int main()
{
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
return 0;
// 'TCP' SOCKET clientSocket = ::socket(AF_INET, SOCK_STREAM, 0);
SOCKET clientSocket = ::socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (clientSocket == INVALID_SOCKET)
return 0;
SOCKADDR_IN serverAddr;
memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
// serverAddr.sin_addr.s_addr = ::htonl(INADDR_ANY);
inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);
serverAddr.sin_port = htons(7777);
//if (connect(clientSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
// return 0;
cout << "Connected To Server!" << endl;
while (true)
{
// clientSocket의 sendBuffer(커널단)에 sendBuffer(유저단)의 데이터를 sizeof(sendBuffer)만큼 전달하겠다.
char sendBuffer[100] = "Hello ! I am Client!";
sendto(clientSocket, sendBuffer, sizeof(sendBuffer), 0, (SOCKADDR*)&serverAddr, sizeof(serverAddr));
//if (resultCode == SOCKET_ERROR)
// return 0; // 커널단의 sendBuffer에 잘 전달되었는지를 확인
///*-----------------------------------------------------------------------------------------------------*/
//// clientSocket의 recvBuffer(커널단)으로부터 recvBuffer(유저단)에 sizeof(recvBuffer)만큼의 데이터를 받겠다
//char recvBuffer[100];
//int32 recvLen = recv(clientSocket, recvBuffer, sizeof(recvBuffer), 0);
//if (recvLen <= 0)
// return 0;
//cout << "Echo Data : " << recvBuffer << endl;
this_thread::sleep_for(1s);
}
closesocket(clientSocket);
WSACleanup();
}
'게임 서버 > [Inflearn_rookiss]올인원_클라&서버 연동' 카테고리의 다른 글
12. Select 방식 (0) | 2023.12.11 |
---|---|
11. 논 블로킹 소켓 (1) | 2023.12.11 |
9. 소켓 프로그래밍 (0) | 2023.12.11 |
8. 스마트 포인터 (2) | 2023.12.11 |
7. 이벤트와 조건변수 (0) | 2023.12.11 |
댓글