TCP & UDP
카테고리: Server
인프런에 있는 루키스님의 게임 서버 강의를 듣고 정리한 내용입니다.
TCP
Client
if (::connect(clientSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
{
int32 errorCode = ::WSAGetLastError();
cout << "Connect ErrorCode : " << errorCode << endl;
return 0;
}
// 연결 성공! 이제부터 데이터 송수신 가능
cout << "Connected To Server!" << endl;
while (true)
{
// TODO
char sendBuffer[100] = "Hello World!";
int32 resultCode = ::send(clientSocket, sendBuffer, sizeof(sendBuffer), 0);
if (resultCode == SOCKET_ERROR)
{
int32 errCode = ::WSAGetLastError();
cout << "Send ErrorCode : " << errCode << endl;
return 0;
}
cout << "Send Data! Len = " << sizeof(sendBuffer) << endl;
this_thread::sleep_for(10s);
}
- 클라에서는 버퍼에 데이터를 담고, 클라 소켓과 함께
send
함수로 보내면 된다.
Server
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)
{
int32 errCode = ::WSAGetLastError();
cout << "Accept ErrorCode : " << errCode << endl;
return 0;
}
char ipAddress[16];
::inet_ntop(AF_INET, &clientAddr.sin_addr, ipAddress, sizeof(ipAddress));
cout << "Client Connected! IP = " << ipAddress << endl;
// TODO
while (true)
{
char recvBuffer[1000];
int32 recvLen = ::recv(clientSocket, recvBuffer, sizeof(recvBuffer), 0);
if (recvLen <= 0)
{
int32 errCode = ::WSAGetLastError();
cout << "Recv ErrorCode : " << endl;
return 0;
}
cout << "Recv Data! Data = " << recvBuffer << endl;
cout << "Recv Data! Len = " << recvLen << endl;
}
}
accept
로 연결 요청을 수락한 소켓에서 데이터가 날아오면,recv
함수를 통해 데이터를 받을 수 있다.- recvBuffer의 경우 클라에서 어떤 크기로 데이터를 보낸지 모르므로 넉넉하게 잡아 놓는다.
recv
함수는 클라에서 넘어온 버퍼의 사이즈를 return 해주기 때문에 이를 이용하면 크기를 알 수 있다.
Blocking
//while (true)
//{
// char recvBuffer[1000];
// int32 recvLen = ::recv(clientSocket, recvBuffer, sizeof(recvBuffer), 0);
// if (recvLen <= 0)
// {
// int32 errCode = ::WSAGetLastError();
// cout << "Recv ErrorCode : " << endl;
// return 0;
// }
// cout << "Recv Data! Data = " << recvBuffer << endl;
// cout << "Recv Data! Len = " << recvLen << endl;
//}
만약 위와 같이 클라에서 데이터를 send 하는데, 서버에서 receive 하지 않는다면 어떻게 될까? 신기하게도 send
함수는 성공한 상태를 리턴한다. 이는 TCP가 작동하는 방식 때문이다. 아래와 같이 클라에서 서버로 “Hello” 라는 데이터가 보내진다고 해보자.
send | Network… | recv |
---|---|---|
실제로 클라에서 서버로 데이터가 넘어가는 과정은 다음과 같다.
- 클라 커널 영역에 존재하는 clientSocket-SendBuffer에 지정한 데이터 담기 (
send
성공) - 서버 커널 영역에 존재하는 clientSocket-RecvBuffer에 데이터 전송
- RecvBuffer에 담긴 데이터를 지정한 버퍼로 가져오기 (
recv
성공)
참고로 sendBuffer에는 성공적으로 담겼지만 성공적으로 서버로 전송되었느지의 여부는 타임아웃이나 서버로부터 성공 메시지를 받아와서 확인하면 된다.
case1 | case2 |
---|---|
send
, recv
함수의 성공 여부를 위와 같이 정의한 이유가 있다. 만약 왼쪽 그림처럼 sendBuffer가 가득 찾을 때 특정 쓰레드가 send
함수를 수행하면 어떻게 될까? 버퍼가 비워질 때까지 Sleep 상태에 들어가(send 실패) 해당 쓰레드는 Blocking 된다. 마찬가지로 서버에서 recvBuffer에 데이터가 없는데 recv
함수를 수행하면 recv가 성공할 때까지 Sleep 상태에 들어간다.
char sendBuffer[100] = "Hello World!";
for (int32 i = 0; i < 10; i++)
{
int32 resultCode = ::send(clientSocket, sendBuffer, sizeof(sendBuffer), 0);
if (resultCode == SOCKET_ERROR)
{
int32 errCode = ::WSAGetLastError();
cout << "Send ErrorCode : " << errCode << endl;
return 0;
}
cout << "Send Data! Len = " << sizeof(sendBuffer) << endl;
}
TCP 통신의 또 다른 특징은 데이터간의 경계가 없다는 것이다. 위 코드와 같이 클라에서 char 100의 데이터를 10번 보냈을 때, 서버는 100의 데이터를 10번을 받는 것이 아니라 1000의 데이터로 한 번에 받게 된다(Hello World가 한 번만 출력되는 것은 줄바꿈 때문).
또는 위와 같이 데이터가 중간에 짤려서 전송되기도 한다. 따라서 서버에서 recv로 받은 데이터를 다시 분류하는 작업이 필요하게 된다. 이는 천천히 알아보자..
UDP
UDP는 간단하게 말하면 신뢰성이 떨어지는 대신 속도가 빠른 통신 프로토콜이다. 자세한 차이는 밑에서 알아보고, 가장 큰 차이로는 서버가 받던 말던, 중간에 소실되던 말던, 클라는 그냥 마구잡이로 보낸다.
Client
WSAData wsaData;
if (::WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
return 0;
SOCKET clientSocket = ::socket(AF_INET, SOCK_DGRAM, 0);
if (clientSocket == INVALID_SOCKET)
{
HandleError("Socket");
return 0;
}
SOCKADDR_IN serverAddr;
::memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
::inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);
serverAddr.sin_port = ::htons(7777);
while (true)
{
char sendBuffer[100] = "Hello World!";
// Unconnected UDP
int32 resultCode = ::sendto(clientSocket, sendBuffer, sizeof(sendBuffer), 0,
(SOCKADDR*)&serverAddr, sizeof(serverAddr));
if (resultCode == SOCKET_ERROR)
{
HandleError("SendTo");
return 0;
}
cout << "Send Data! Len = " << sizeof(sendBuffer) << endl;
}
- TCP 통신과의 차이점은 크게 없다.
- 소켓 생성할 때
SOCK_DGRAM
옵션 지정 - 서버와
connect
과정이 필요 없음 - TCP에서는 소켓마다 목적지 주소가 지정되어
send
함수로 패킷을 전송했지만 - UDP에서는
send
대신sendto
함수로 데이터를 보낼 때 마다 소켓의 목적지 지정
Server
WSAData wsaData;
if (::WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
return 0;
SOCKET serverSocket = ::socket(AF_INET, SOCK_DGRAM, 0);
if (serverSocket == INVALID_SOCKET)
{
HandleError("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);
if (::bind(serverSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
{
HandleError("Bind");
return 0;
}
while (true)
{
SOCKADDR_IN clientAddr;
::memset(&clientAddr, 0, sizeof(clientAddr));
int32 addrLen = sizeof(clientAddr);
this_thread::sleep_for(1s);
char recvBuffer[1000];
int32 recvLen = ::recvfrom(serverSocket, recvBuffer, sizeof(recvBuffer), 0,
(SOCKADDR*)&clientAddr, &addrLen);
}
- 서버에서도 마찬가지로 소켓 생성할 때
SOCK_DGRAM
옵션 지정 - 특정 클라의 소켓을
accept
할 필요 없음 recv
대신recvfrom
으로 소켓마다 클라의 주소를 함께 지정해서 받아옴
참고로 위 방식을 UnConnected-UDP 방식이라 한다. Connected-UDP는 특정 클라/서버를 지정해두고(connect, accept), TCP와 마찬가지로
send
,recv
함수를 이용한다.
TCP vs UDP
TCP (신뢰성 up, 속도 down)
- 연결을 위해 할당되는 논리적인 경로가 있음
- 데이터의 경계가 없음
- 전송 순서 보장
- 전송이 실패하면 책임지고 다시 전송
- 서버가 받을 상황이 아니면 일부만 보냄 (흐름/혼잡 제어)
UDP (신뢰성 down, 속도 up)
- 연결이라는 개념이 없음
- 데이터의 경계가 있음
- 전송 순서 보장 X
- 중간에 분실되면 데이터 증발
- 서버가 받던 말던 일단 보내고 생각
댓글 남기기