Coding Memo

Socket Programming - TCP 본문

Game Server (C++)

Socket Programming - TCP

minttea25 2022. 10. 31. 20:59

본 포스팅은 인프런에 등록되어 있는 Rockiss 님의 강의를 보고 간단하게 정리한 글입니다.


TCP (Transmission Control Protocol)은 네트워크 5계층에서 Transport Layer에 해당하는 송수신 간 전송 방식 중 하나이다. 보통은 UDP와 대조되어 사용된다.

 

TCP는 다음과 같은 특징을 가지고 있다.

 

1. 데이터 전송 경로 외에도, 호스트 간 연결이 따로 필요하다.

2. 송수신데이터에 대한 데이터의 경계(Boundary)가 없다.

3. 전송 순서가 보장된다.

4. 전송 시, 데이터 손실이 발생할 경우 다시 전송한다. (신뢰성이 있다.)

5. 데이터를 받을 호스트가 여건이 안될 경우 데이터를 일부만 전송한다. (흐름/혼잡제어)

6. 위 특징에 따라 고려할 것이 많으니 속도가 상대적으로 느리다. (UDP에 비해)

 

좀 더 자세히 보면...

1. 서버 측에서 TCP 연결은 binding하고 listening할 socket과 실제 데이터를 주고 받을 (클라이언트)소켓이 따로 필요하다.

 

2. 100바이트가 송신이 되었어도 수신하는 측에서 10바이트를 읽으면 데이터가 끊길 수 있다.

 

3. TCP 송수신 시 송신자와 송신자의 데이터에 sequence number을 붙여 순서를 확인할 수 있다.

 

4. 수신자는 TCP Header의 Checksum 값으로 오류를 확인하고 이에 대한 결과를 송신자에게 응답 패킷을 보낸다. 만약 수신자측에서 오류가 확인될 경우 송신자는 다시 송신하게 된다. (응답 패킷에 대한 정책은 여러가지가 있다.)

 

5. 송신자와 수신자 사이의 데이터 처리 속도가 많이 차이가 나게 될 경우 흐름 제어를 하게 된다. 흐름제어에는 Stop and Wait와 Sliding Window 방식이 있다.

혼잡제어도 흐름제어와 비슷한 이유로 필요한데, 혼잡제어는 AIMD, Slow Start, 혼잡 회피 등의 알고리즘으로 손신자의 데이터 전송 속도를 제어하게 된다.

 

(아래 링크에 깔끔하게 잘 설명되어 있는 것 같다.)

https://velog.io/@jsj3282/TCP-%ED%9D%90%EB%A6%84%EC%A0%9C%EC%96%B4%ED%98%BC%EC%9E%A1%EC%A0%9C%EC%96%B4-%EC%98%A4%EB%A5%98%EC%A0%9C%EC%96%B4

 

 

 

이전 포스트: https://minttea25.tistory.com/81

 

Socket Programming Basic

본 포스팅은 인프런에 등록되어 있는 Rockiss 님의 강의를 보고 간단하게 정리한 글입니다. 소켓을 이용해서 간단하게 서버와 클라이언트 접속을 해보도록 하자. 소켓 프로그래밍을 위해서는 다

minttea25.tistory.com

 

위 포스트도 TCP로 간단하게 연결한 것이다. 이에 데이터 송수신까지 테스트를 해본다.


TCP 연결은 위 포스트를 참고하고 이미 코드가 작성되어 있다고 하자.

확인할 점은 TCP 연결이기 때문에 socket을 SOCK_STREAM으로 설정해 주어야 한다는 점이다.

 

TCP 통신에는 send() 함수와 recv() 함수를 이용해 데이터를 송수신 처리 한다.

 

1. send

데이터를 전송할 socket과 데이터가 담겨있는 버퍼, 데이터의 사이즈, 정책(flag)를 인자로 넣어준다.

return value는 보내진 전체 데이터의 바이트 수를 반환한다.

 

(자세한 내용은 이쪽...)

https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-send

 

send function (winsock2.h) - Win32 apps

Sends data on a connected socket. (send)

learn.microsoft.com

 

아래 코드에서는 버퍼의 사이즈를 100바이트로 하고 전송해주는 코드이다.

 

한 가지 알아두어야 할 점이 있는데, 실제로 send가 성공하고 받는 쪽에서 받는데 성공했다는 의미가 아니다.

sendBuffer에 데이터 버퍼를 넣었고, 이를 전송했다는 의미이다.

char sendBuffer[100] = "Hello, world!";

int resultCode = ::send(clientSocket, sendBuffer, sizeof(sendBuffer), 0);

// 오류 확인
if (resultCode == SOCKET_ERROR)
{
	int errCode = ::WSAGetLastError();
	cout << "Send ErrorCode : " << errCode << endl;
	return 0;
}

 

2. recv

데이터를 받는 함수이다. 좀 더 자세히 말하자면 전송된 데이터가 담겨있는 recvBuffer에서 값을 읽어들이는 함수이다.

send와 마찬가지로, 소켓과 데이터를 받을 버퍼, 읽을 사이즈(길이), flag를 인자로 넣어준다.

return value는 받은 데이터의 바이트 수이다.

 

(자세한 내용은 이쪽...)

https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-recv

 

recv function (winsock2.h) - Win32 apps

The recv function (winsock2.h) receives data from a connected socket or a bound connectionless socket.

learn.microsoft.com

 

읽을 버퍼는 넉넉하게 1000바이트로 할당하였다.

char recvBuffer[1000];

int recvLen = ::recv(clientSocket, recvBuffer, sizeof(recvBuffer), 0);

if (clientSocket <= 0)
{
	int errCode = ::WSAGetLastError();
	cout << "Recv ErrorCode : " << errCode << endl;
	return 0;
}

클라이언트에서 데이터 전송 - 서버에서 응답 - 서버에서 같은 데이터를 클라이언트로 전송 - 클라이언트 수신

방식의 에코서버 형태로 테스트 하였다.

 

Server

더보기
int main()
{
    WSAData wsaData;
    if (::WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
    {
        // fail
        return 0;
    }

    SOCKET listenSocket = ::socket(AF_INET, SOCK_STREAM, 0);
    if (listenSocket == INVALID_SOCKET)
    {
        int errCode = ::WSAGetLastError();
        cout << "Socket ErrorCode : " << errCode << endl;
        return 0;
    }

    SOCKADDR_IN serverAddr; // IPv4 사용시 이용
    ::memset(&serverAddr, 0, sizeof(serverAddr)); // clear serverAddr
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_addr.s_addr = ::htonl(INADDR_ANY); // 알아서 정하도록 하기
    serverAddr.sin_port = ::htons(7777);

    if (::bind(listenSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
    {
        int errCode = ::WSAGetLastError();
        cout << "Bind ErrorCode : " << errCode << endl;
        return 0;
    }

    if (::listen(listenSocket, 10) == SOCKET_ERROR)
    {
        int errCode = ::WSAGetLastError();
        cout << "Listen ErrorCode : " << errCode << endl;
        return 0;
    }
   
    while (true)
    {
        SOCKADDR_IN clientAddr; // IPv4
        ::memset(&clientAddr, 0, sizeof(clientAddr));

        int32 addrLen = sizeof(clientAddr);
        // 실시간으로 연결하여 패킷을 주고받을 소켓
        SOCKET clientSocket = ::accept(listenSocket, (SOCKADDR*)&serverAddr, &addrLen);
        if (clientSocket == INVALID_SOCKET)
        {
            int errCode = ::WSAGetLastError();
            cout << "Accept ErrorCode : " << errCode << endl;
            return 0;
        }

        while (true)
        {
            char recvBuffer[1000];

            this_thread::sleep_for(1s);

            int recvLen = ::recv(clientSocket, recvBuffer, sizeof(recvBuffer), 0);
            if (clientSocket <= 0)
            {
                int errCode = ::WSAGetLastError();
                cout << "Recv ErrorCode : " << errCode << endl;
                return 0;
            }

            cout << "Recv Data! Data: " << recvBuffer << endl;
            cout << "Recv Data! Len=: " << recvLen << endl;


            int resultCode = ::send(clientSocket, recvBuffer, recvLen, 0);
            if (resultCode == SOCKET_ERROR)
            {
                int errCode = ::WSAGetLastError();
                cout << "Send ErrorCode : " << errCode << endl;
                return 0;
            }
        }
    }

    // 윈속 종료
    ::WSACleanup();
}

 

Client

더보기
int main()
{
    WSAData wsaData;
    if (::WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
    {
        return 0;
    }

    SOCKET clientSocket =  ::socket(AF_INET, SOCK_STREAM, 0);

    if (clientSocket == INVALID_SOCKET)
    {
        int errCode = ::WSAGetLastError();
        cout << "Socket ErrorCode : " << errCode << endl;
        return 0;
    }

    SOCKADDR_IN serverAddr; // IPv4 사용시 이용
    ::memset(&serverAddr, 0, sizeof(serverAddr)); // clear serverAddr
    serverAddr.sin_family = AF_INET;
    ::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)
    {
        int errCode = ::WSAGetLastError();
        cout << "Connect ErrorCode : " << errCode << endl;
        return 0;
    }

    // connect 성공! 이제부터 데이터 송수신 가능
    cout << "Connected To Server !" << endl;

    while (true)
    {
        char sendBuffer[100] = "Hello, world!";
        int resultCode = ::send(clientSocket, sendBuffer, sizeof(sendBuffer), 0);
        if (resultCode == SOCKET_ERROR)
        {
            int errCode = ::WSAGetLastError();
            cout << "Send ErrorCode : " << errCode << endl;
            return 0;
        }

        cout << "Send Data! Len: = " << sizeof(sendBuffer) << endl;

        char recvBuffer[1000];
        int recvLen = ::recv(clientSocket, recvBuffer, sizeof(recvBuffer), 0);
        if (clientSocket <= 0)
        {
            int errCode = ::WSAGetLastError();
            cout << "Recv ErrorCode : " << errCode << endl;
            return 0;
        }

        cout << "Recv Data! Data: " << recvBuffer << endl;
        cout << "Recv Data! Len=: " << recvLen << endl;

        this_thread::sleep_for(1s);
    }
    
    // 소켓 리소스 반환
    ::closesocket(clientSocket);

    // 윈속 종료
    ::WSACleanup();
}

TCP 설명의 2번 관련해서 테스트!

 

client가 100바이트씩 10번 보내고 server가 1000바이트씩 읽으면 어떻게 될까?

 

<code>

더보기

server

클라이언트에서 먼저 10번 실행되어야 하므로 1초 sleep 한후에 시작하자.

char recvBuffer[1000];

this_thread::sleep_for(1s);

int recvLen = ::recv(clientSocket, recvBuffer, sizeof(recvBuffer), 0);
if (clientSocket <= 0)
{
	int errCode = ::WSAGetLastError();
	cout << "Recv ErrorCode : " << errCode << endl;
	return 0;
}

cout << "Recv Data! Data: " << recvBuffer << endl;
cout << "Recv Data! Len=: " << recvLen << endl;

 

client

for문을 이용해 100바이트씩 10번 send하자.

char sendBuffer[100] = "Hello, world!";

for (int i = 0; i < 10; i++)
{
	cout << i << "번째 DATA" << endl;
	int resultCode = ::send(clientSocket, sendBuffer, sizeof(sendBuffer), 0);
	if (resultCode == SOCKET_ERROR)
	{
		int errCode = ::WSAGetLastError();
		cout << "Send ErrorCode : " << errCode << endl;
		return 0;
	}
}

결과

 

이미지로만 봐서는 잘 안보일 수있지만, 클라이언트쪽에서 10번 데이터를 보낸 후에 서버에서 data를 1000바이트를 읽는 것을 확인할 수 있다.

 

데이터가 100바이트로 도착해도 수신하는 쪽은 같은 단위로 데이터를 받아들이는 것이 아니라 고정된 버퍼의 사이즈만큼 데이터를 읽게 된다. 따라서 실제 recvBuffer에는 hello world가 10번 입력이 되있을 것이다. (출력은 string의 null값까지만 되어 있어서 하나처럼 보이지만 말이다.)

 

결론적으로 TCP에서는 데이터의 Boundary가 정해져 있지 않기 때문에, 송신하는 쪽의 데이터의 Boundary와 수신하는 쪽의 데이터 Boundary가 일치하지 않으면 데이터에 문제가 생길 수 있다.

'Game Server (C++)' 카테고리의 다른 글

Socket Option - (get/set)sockopt()  (0) 2022.11.07
Socket Programming - UDP  (0) 2022.11.03
Socket Programming Basic  (0) 2022.10.18
Type Cast  (1) 2022.10.11
Object Pool  (0) 2022.10.11