• Packet Loss ( 패킷이 유실된 경우 )
  • TCP Out of order ( 패킷의 순서가 뒤바뀐 경우 )
  • Retransmission과 Dup ACK
  • Zero - window ( 수신측 버퍼에 여유 공간이 하나도 없는 경우 )

TCP 계층 즉, 4계층 부터 아래 1계층 까지 따져봤을 때 발생할 수 있는 장애 유형이 있다.

 

📘 Packet Loss ( 패킷이 유실된 경우 )

인터넷이라는 네트워크 환경은 기본적으로 불안정하다.

그래서 인터넷 수준, L3 혹은 그 이하에서 발생하는 것 중에 가장 흔한 게 패킷이 아예 유실되어 버릴 수 있다.

자주 일어나는 일은 아니지만 간간히 일어난다.

 

📘 TCP Out of order ( 패킷의 순서가 뒤바뀐 경우 ).

수신 측에서 예상한 순서와 다른 TCP 세그먼트가 도착하는 현상을 말한다.

 

TCP는 연결이라는 개념을 포함하고 있는데, 데이터들이 날아올 때 순서를 따진다.

연결이 있고 순서 개념이 있다 보니 데이터가 왔을 때, 1번이 오면 그다음은 2번 그다음은 3번이 와야하는데

가끔 순서가 뒤바뀔 수 있다.

 

발생하는 주요 원인

  • 멀티패스 라우팅
    • 하나의 TCP 연결이 여러 네트워크 경로를 통해 전송될 경우
    • 각 경로마다 지연이 다르기 때문에 순서가 바뀌기 쉽다.
  • 네트워크 혼잡
    • 일부 패킷이 네트워크 장비에서 큐잉되거나 드롭되면서 재전송이 발생
    • 나중에 보낸 패킷이 먼저 도착하는 상황이 생길 수 있다.
  • 리트랜스미션
    • ACK 손실 또는 지연으로 인해 송신 측이 재전송하게 되어, 그 패킷이 원래 패킷보다 먼저 도착할 수 있다.

 

📘 Dup ACK (  Duplicate ACK )

Dup ACK는 말 그대로 중복된 ACK 패킷을 의미한다.

 

TCP 수신 측이 같은 ACK 번호를 반복해서 보낼 때, 이를 송신 측에서 중복 ACK로 인식한다.

이는 일반적으로 데이터 손실 또는 Out of order 상황을 의미한다.

 

특정 패킷이 안와서 재전송을 요청했는데 해당 패킷이 도착하고, 재전송한 패킷이 도착하면 

Dup ACK가 발생할 수 있는 것이다.

 

발생하는 주요 원인

  • 패킷 손실 
    • 가장 흔한 원인. 특정 시퀀스의 세그먼트가 손실되었을 때 발생
  • 패킷 재정렬
    • 순서가 어긋난 패킷이 먼저 도착하여, 이전 시퀀스 번호에 대한 ACK가 반복되는 경우

 

📘 Zero-window ( 수신측 버퍼에 여유 공간이 하나도 없는 경우 )

윈도우 사이즈가 0이 된다는 것은 수신 측에서 더 이상 데이터를 받을 수 없는 상태를 의미한다.
다시 말해 수신 측 버퍼가 꽉 찼다는 의미

 

소켓 수신 버퍼에 데이터가 차곡차곡 쌓일 때, 

recv를 이용해 소켓 수신 버퍼에 있는 데이터를 L7에 있는 버퍼에 데이터를 담는 속도보다

소켓 수신 버퍼에 데이터가 담기는 속도보다 빠르지 않으면 윈도우 사이즈가 0이 된다.

 

이는 위 3가지의 오류 사항과는 성격이 다르다.

위 3가지의 오류 사항은 네트워크 인프라 수준에서의 오류라고 생각 할 수 있는 반면

Zero-window는 개발자가 구현하는 코드에 따라 해결이 가능하기 때문이다.

'네트워크' 카테고리의 다른 글

[네트워크] 파일 수신  (0) 2025.04.20
[네트워크] 파일 송신  (0) 2025.04.20
[네트워크] TCP/IP 연결의 의미  (0) 2025.04.20
[네트워크] select  (0) 2025.04.17
[네트워크] I / O 멀티플렉싱  (0) 2025.04.17

TCP/IP에서의 클라이언트와 연결은 3 way handshake라는 규칙이 만족되면

연결이 되었다고 서버와 클라가 주관적으로 판단하는 것으로, 단순히 논리적인 통신 채널이 확보된 상태라고 정의 할 수 있다.

 

다시 말해 서버 입장에서는 accept 함수가 리턴되고, 클라 입장에서는 connect 함수가 리턴을 하면 연결됐다라고 할 수 있는 것이다.

 

물리적인 통신 채널이 아닌 논리적인 통신 채널이 확보된 상태이기 때문에

TCP 연결이 끊어졌다는 것을 인지할 때 상황에 따라 다르다.

 

일반적인 정상적인 종료는 클라에서 FIN 패킷을 보내 연결증 종료하면 서버가 이를 수신하고,

4 way handshake를 우아한 종료를 진행하며 이를 통해 연결 해제를 명확히 감지할 수 있다.

하지만 클라가 랜케이블을 뽑거나 블루스크린처럼 갑자기 다운되는 경우, FIN이나 RST 신호를 보내지 못하므로

서버는 클라가 연결이 끊겼는지 즉시 알 수 없다.

 

이를 보완하기 위해 서버에서 아래와 같은 방법들을 사용한다.

  • TCP Keep-Alive 설정 : 일정시간 동안 응답이 없으면 OS 수준에서 연결을 끊는다.
  • Heartbeat / ping : L7 단계에서 주기적으로 메시지를 보내, 응답이 없으면 연결 해제가 되었다고 판단한다.
    • 데이터 통신이 원활히 이루어지고 있을 경우에는 굳이 Heartbeat를 주고 받을 필요 없기 때문에 이를 고려해 코드를 작성해야한다.

 

앞서 언급한 내용처럼 TCP에서의 연결은 착각이라고도 생각할 수 있겠다.

서버에서는 클라와 연결중이라고 생각 할 수 있지만,

클라가 랜케이블을 뽑았는지, 블루스크린이 떳는지 알 방법이 없기 때문이다.

 

따라서 클라이언트와의 연결은 단순한 통신 가능 여부를 넘어서, 식별 가능성, 상태 추적 까지 포함된 개념이라고 봐야한다.

'네트워크' 카테고리의 다른 글

[네트워크] 파일 송신  (0) 2025.04.20
[네트워크] TCP 장애 유형  (0) 2025.04.20
[네트워크] select  (0) 2025.04.17
[네트워크] I / O 멀티플렉싱  (0) 2025.04.17
[네트워크] 우아하지 않은 비정상 종료  (0) 2025.04.14
select()는 리눅스 / 유닉스 시스템에서 여러 파일 디스크립터를 동시에 감시하여, 읽기/쓰기/예외 이벤트가 발생한 파일 디스크립터를 감지할 수 있게 해주는 함수다.

 

  • 여러 클라이언트의 요청을 동시에 처리할 수 있게 해준다.
  • 여러 소켓을 동시에 감시할 수 있게 해준다.
  • 단일 스레드 기반의 네트워크 서버에서 효율적으로 동시성 처리가 가능하다.
  • 하지만 최대로 감지할 수 있는 클라의 수가 윈도우는 64개, 리눅스는 1024개 이기 때문에 제한이 있다.

 

✅ 예제

 

UINT nCount
FD_SET fdRead;
std::list<SOCKET>::iterator it;

// 클라 접속 및 정보 수신 변화 감시셋을 초기화 한다.
FD_ZERO(&fdRead);
for(it = g_listClient.begin(); it != g_listClient.end(); ++it)
{
    FD_SET(*it, &fdRead);
}

// 변화가 발생할 때까지 대기한다.
::select(0, &fdRead, NULL, NULL, NULL);

// 변화가 감지된 소켓을 확인한다.
nCount = fdRead.fd_count;
for(int nIndex = 0; nIndex < nCount; ++nIndex)
{
    // 소켓에 변화 감시 플래그가 설정 되었는지 확인한다.
    if(!FD_ISSET(fdRead.fd_array[nIndex], &fdRead))
    {
        continue;
    }
    
    // 감지된 소켓이 listen 소켓인지 확인한다.
    // 다시 말해, 클라가 연결을 시도했는지 여부를 확인한다.
    if(fdRead.fd_array[nIndex] == g_hSocket)
    {
        // 새 클라이언트의 접속을 받는다.
        SOCKADDR_IN clientaddr = { 0 };
        int nAddrLen = sizeof(clientaddr);
        SOCKET hClient = ::accept(g_hSocket, (SOCKADDR*)&clientaddr, &nAddrLen);
        if(hClient != INVALID_SOCKET)
        {
            FD_SET(hClient, &fdRead);
            g_listClient.push_back(hClient);
        }
    }
    else // 클라이언트가 전송한 데이터가 있을 경우
    {
        char szBuffer[1024] = { 0 };
        int nReceive = ::recv(fdRead.fd_array[nIndex], (char*)szBuffer, sizeof(szBuffer), 0);
    }
}
  • 여러 입 / 출력 요청이 한 채널에 동시에 혼재 할 수 있는 입/출력
  • 개발자 관점에서 보면 송신과 수신이 동시에 발생하는 것 처럼 보인다.
  • 모든 입 / 출력은 프로세스가 아니라 OS가 주도한다는 것이 핵심이다.

예를 들어 파일에 fwrite를 하면, 우리가 작성한 프로그램이 fwrite를 했다라고 생각할 수 있지만,

본질적으로는 OS한테 파일에 write 해달라고 요청한 것에 불과하다.

 

이처럼 I/O 멀티플렉싱 역시 OS가 처리해준다.

 

네트워크에서 굉장히 중요한 관점 중 하나는 송신하고 수신이 동시에 일어난다는 점인데,

단순히 생각해보면 위 작업을 멀티쓰레드로 하면 가능하다는 것을 생각할 수 있다.

 

송수신이 동시에 일어난다면, 이를 감시 할 수 있는 작업 또한 필요한데,

송수신이 완료되면 OS가 알려주는 방식이 어떤 특정 함수를 호출하는 방식이라면 callback 방식이 되는 것이고,

신호를 주는 방식이라면 Event 방식이 되는 것이다.

 

Bias는 부동소수점에서 지수를 양수로 표현하기 위해 사용하는 보정값을 말한다.

 

📌 Bias가 필요한 이유

컴퓨터는 보통 부호 없는 이진수를 쉽게 다룬다.

하지만 부동소수점의 지수는 양수도 되고 음수도 되어야 한다.

예를 들어

이처럼 지수가 음수, 0, 양수 모두를 포함해야하므로, 이를 컴퓨터가 쉽게 표현할 수 있도록 모든 지수에 일정한 수를 더해서

양수로 만들어서 저장해야한다.

 

📌 32비트 ( float ) 기준

  • 지수는 9비트 -> 표현 범위 : 0 ~ 255
  • 이 중 실제 지수로 사용되는 범위는 -126 ~ +127 이다
  • 그래서 Bias를 127로 설정한다.

'CS' 카테고리의 다른 글

[CS] 부동소수점  (0) 2025.04.15
[CS] 힙 단편화  (0) 2025.04.09
[CS] 스택과 힙  (0) 2025.04.09
[CS] 메모리 보호 기법 ( ASLR )  (0) 2025.03.11
[cs] 문자열 상수  (0) 2025.02.23
부동 소수점은 컴퓨터에서 실수를 표현하는 방식 중 하나다.
정수만으로 표현할 수 없는 소수점이 있는 수들을 다룰 수 있도록 설계된 방식을 말한다.


📌 개요

컴퓨터는 이진수 체계를 사용하므로 실수도 이진 소수로 표현해야한다.

하지만 실수는 무한히 많은 수가 존재해서 모든 실수를 정확하게 표현할 수 없다.

따라서 일정한 근사치로 표현하게 되는데, 이때 사용하는 대표적인 방식이 바로 부동소수점 방식이다.

 

📌 구조

IEEE 754 표준에 따라 부동소수점 수는 다음과 같은 세 부분으로 구성된다.

일반적인 구조( 32비트 단정도 )

부호(S) 지수(E) 가수(M)
1비트 8비트 23비트
  • 부호(S) : 0이면 양수, 1이면 음수
  • 지수(E) : 2의 몇 제곱인지를 나타낸다
  • 가수(M) : 실제 유효숫자( 정규화된 소수 부분 )

 

📌 정규화 표현

 

부동소수점 수는 일반적으로 다음과 같은 형식으로 표현된다.

  • Bias( 지수 편향 ) : 표현 가능한 음수 지수를 양수로 바꾸기 위해 사용된다.
    • 32비트 단정도 : Bias = 127
    • 64비트 배정도 : Bias = 1023

 

📌 예시

 

실수 10.375를 IEEE 754 32비트 부동소수점으로 변환해 보자.

  1. 부호 비트(S)를 구한다.
    • 10.375는 양수 -> 부호 비트 = 0
  2. 정수부와 소수부를 이진수로 바꾼다.
    • 10 = 1010 ( 2진수 )
    • 0.375 =
      • 0.375 * 2 = 0.75 -> 정수부 0
      • 0.75 * 2 = 1.5 -> 정수부 1
      • 0.5 * 2 = 1.0 -> 정수부 1
        • 따라서 0.375 = 0.011
    • ▶️ 전체 이진 표현 : 1010.011
  3. 정규화
    • 1010.011 -> 1.010011 * 2^3 형태로 변환 ( 소수점 뒤에만 남기기 )
  4. 지수 구하기
    • 지수 = 3 ( 위에서 2^3 에서 3인 것을 확인 )
    • Bias = 127 ( IEEE 754 단정도 기준 )
    • E = 3 + 127 = 130
    • 130 = 10000010 ( 8비트 )
  5. 가수 구하기
    • 정규화된 수의 소수 부분 : 010011
    • 23비트로 채움: 01001100000000000000000
  6. 최종 32비트 구성
부호(S) 지수(E) 가수(M)
0 10000010 01001100000000000000000

 

최종 결과 (2진수) : 0 10000010 01001100000000000000000

16진수로 변환 : 0x41260000

 

'CS' 카테고리의 다른 글

[CS] Bias ( 부동소수점 보정값 )  (0) 2025.04.15
[CS] 힙 단편화  (0) 2025.04.09
[CS] 스택과 힙  (0) 2025.04.09
[CS] 메모리 보호 기법 ( ASLR )  (0) 2025.03.11
[cs] 문자열 상수  (0) 2025.02.23
  • 연결된 클라이언트 혹은 서버 프로세스가 비정상 종료되면 발생하는 일 ( 메모리 폴트 등 )
  • 프로세스 종료 시 OS는 할당해준 자원을 강제 회수하며 여기에는 개방한 파일과 소켓이 포함된다.
  • 프로세스 강제 종료보다 더 심각한 경우

 

와이어 샤크로 비정상 종료 되면 어떤 패킷이 전송되는지 확인해보자.

 

클라를 강제로 종료하면 위 그림처럼 RST 를 전송하는 것을 확인할 수 있다.

RST을 받으면 그 즉시 TCP 연결은 끊어진다.

 

📌 프로세스 강제 종료보다 심각한 경우

  • 상대방이 랜 케이블을 뽑은 경우
  • 상대방이 블루스크린이 발생한 경우

위 2가지 모두 연결은 끊어졌지만 서버쪽은 상대방이 끊겨 있는지 확인할 방법이 없다.

위같은 연결 상태를 좀비세션이라고 부른다.

 

위 경우 말고도 라우터에 문제가 생겨서 좀비 세션이 생기기도 한다.

 

위와 같은 경우를 해결하기 위해 보통 하트비트라는 기법을 사용해 좀비세션을 탐지한다.

하트비트는 주기적으로 서버가 클라에게 특정 패킷을 전송하고 응답이 없을 경우 해당 클라와 연결을 끊어버리는 것을 말한다.

.

  • 채팅 메세지 사용자 입력 및 송신처리 전담 쓰레드를 만든다.
  • 서버가 보내주는 채팅 메세지 수신 및 처리 전담 쓰레드 분리

채팅을 받을 쓰레드를 따로 생성

// 채팅 메세지 수신 쓰레드 생성
DWORD dwThreadID = 0;
HANDLE hTread = ::CreateThread(NULL,
    0,
    ThreadReceive,
    (LPVOID)hSocket,
    0,
    &dwThreadID);
::CloseHandle(hTread);

 

ThreadReceive

// 서버가 보낸 메세지를 수신하고 화면에 출력하는 쓰레드 함수
DWORD WINAPI ThreadReceive(LPVOID pParam)
{
    SOCKET hSocket = (SOCKET)pParam;
    char szBuffer[128] = { 0 };
    while(::recv(hSocket, szBuffer, sizeof(szBuffer), 0) > 0)
    {
        printf("-> %s\n", szBuffer);
        memset(szBuffer, 0, sizeof(szBuffer));
    }
    
    puts("수신 쓰레드가 끝났습니다.");
    return 0;
}

 

서버로부터 FIN( 0 )을 받으면 끝낸다.

앞서 만든 Echo 멀티 쓰레드 코드에 약간의 변형을 주어 채팅 서버로 변환할 수 있다.
  • 클라이언트 TCP 세션을 관리하는 개별 쓰레드가 클라이언트 수 만큼 존재한다.
  • 연결된 모든 클라이언트를 관리하기 위한 자료구조가 필요하다.
  • 쓰레드에서 자료구조에 삭제, 추가, 접근 해야하기 때문에 쓰레드 동기화 작업이 필요하다.

 

서버에 접속하는 유저를 리스트로 관리한다.

// 새로 연결된 클라의 소켓을 리스트에 저장한다.
BOOL AddUser(SOCKET hSocket)
{
    // CriticalSection을 이용해 동기화를 시작한다.
    ::EnterCriticalSection(&g_cs); // 임계영역 시작
    g_listClient.push_back(hSocket);
    ::LeaveCriticalSection(&g_cs); // 임계영역 끝
    
    return TRUE;
}

 

CriticalSection을 이용해 동기화를 시작하면 쓰레드가 진입해 LeaveCriticalSection을 호출하기 전까지

다른 쓰레드는 g_listClient에 socket을 저장할 수 없다.

이처럼 다른 쓰레드의 임계영역이 끝나야만 다른 쓰레드가 작업할 수 있으므로 임계구간은 최소화하는것이 좋다.

 

 

연결된 클라들에게 메세지를 전송한다.

void SendChattingMessage(char *pszParam)
{
    int nLength = strlen(pszParam);
    std::list<SOCKET>::iterator it;
    
    ::EnterCriticalSection(&g_cs); // 임계영역 시작
    for(it = g_listClient.begin(); it != g_listClient.end(); ++it)
    {
        ::send(*it, pszParam, sizeof(char)* (nLength + 1), 0);
    }
    ::LeaveCriticalSection(&g_cs); // 임계영역 끝    
}

 

멀티쓰레드 기반 채팅 클라이언트 7분 강의

앞서 작성했었던 Echo Server를 멀티 쓰레드 환경으로 변경해보자.

 

main.c 

accept를 담당하고, 클라와 연결 되면 해당 클라와 통신하기 위한 쓰레드를 생성해준다.
// 클라 연결을 받고 새로운 소켓을 생성한다.
while((hClient = ::accept(hSocket,
    (SOCKADDR*)&clientaddr,
    &nAddrLen)) != INVALID_SOCKET)
{
    // 새 클라와 통신하기 위한 쓰레드를 생성한다.
    // 클라가 접속할 때마다 쓰레드가 하나씩 생성된다.
    hThread = ::CreateThread(
        NULL, // 쓰레드를 생성한 프로세스의 보안속성을 상속받는다.
        0,   // 스택 메모리를 기본 크기 값으로 가진다. ( 1MB )
        ThreadFunction, // 쓰레드로 실행할 함수의 이름을 적는다.
        (LPVOID)hClient, // 새로 생성된 클라 소켓을 넘긴다.
        0, // 생성 플래그는 기본값을 사용한다.
        &dwThreadID); // 생성된 쓰레드 ID가 저장될 변수의 주소를 담는다.
        
    ::CloseHandle(hTread);
}

 

 

ThreadFunction

클라의 recv, send를 담당할 쓰레드
DWORD WINAPI ThreadFunction(LPVOID pParam)
{
    char szBuffer[128] = { 0 };
    int nReceive = 0;
    SOCKET hClient = (SOCKET)pParam;
    
    puts("새 클라이언트가 연결되었습니다.");
    
    while((nReceive = ::recv(
                hClient, szBuffer, sizeof(szBuffer), 0)) > 0)
    {
        ::send(hClient, szBuffer, sizeof(szBuffer), 0);
        puts(szBuffer);
        memset(szBuffer, 0, sizeof(szBuffer));
    }
    
    puts("클라이언트 연결이 끊어졌습니다.");
    ::closesocket(hClient);
    return 0;
}

 

+ Recent posts