TCP & UDP

Date:     Updated:

카테고리:

태그:

인프런에 있는 루키스님의 게임 서버 강의를 듣고 정리한 내용입니다.


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 해주기 때문에 이를 이용하면 크기를 알 수 있다.


thread21


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;

//}

thread22

만약 위와 같이 클라에서 데이터를 send 하는데, 서버에서 receive 하지 않는다면 어떻게 될까? 신기하게도 send 함수는 성공한 상태를 리턴한다. 이는 TCP가 작동하는 방식 때문이다. 아래와 같이 클라에서 서버로 “Hello” 라는 데이터가 보내진다고 해보자.


thread26


send Network… recv
thread23 thread24 thread25

실제로 클라에서 서버로 데이터가 넘어가는 과정은 다음과 같다.

  • 클라 커널 영역에 존재하는 clientSocket-SendBuffer에 지정한 데이터 담기 (send 성공)
  • 서버 커널 영역에 존재하는 clientSocket-RecvBuffer에 데이터 전송
  • RecvBuffer에 담긴 데이터를 지정한 버퍼로 가져오기 (recv 성공)

참고로 sendBuffer에는 성공적으로 담겼지만 성공적으로 서버로 전송되었느지의 여부는 타임아웃이나 서버로부터 성공 메시지를 받아와서 확인하면 된다.

case1 case2
thread27 thread28

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;
}

thread29

thread33

TCP 통신의 또 다른 특징은 데이터간의 경계가 없다는 것이다. 위 코드와 같이 클라에서 char 100의 데이터를 10번 보냈을 때, 서버는 100의 데이터를 10번을 받는 것이 아니라 1000의 데이터로 한 번에 받게 된다(Hello World가 한 번만 출력되는 것은 줄바꿈 때문).

thread31

또는 위와 같이 데이터가 중간에 짤려서 전송되기도 한다. 따라서 서버에서 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
  • 중간에 분실되면 데이터 증발
  • 서버가 받던 말던 일단 보내고 생각



맨 위로 이동하기

Server 카테고리 내 다른 글 보러가기

댓글 남기기