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

 

소켓에서 버퍼링을 하는 이유는 성능 최적화와 데이터 무결성 보장이다.

 

시스템 호출 최소화

소켓을 통해 데이터를 주고받을 때, 시스템 호출( send(), recv() )은 매우 비용이 크다

  • 네트워크 입출력은 커널 공간과 유저 공간을 오가야 하는데, 시스템 호출이 발생할 때마다 문맥교환이 일어난다.
  • 문맥교환은 오버헤드가 크기 때문에, 자주 호출하는 것보다 한 번에 큰 덩어리로 처리하는 것이 훨씬 효율적이다.
  • 버퍼를 사용하면 소량의 데이터를 보낼 때마다 시스템 호출을 하는 것이 아닌, 일정 크기만큼 모아서 (=버퍼링) 한 번에 보냄으로써 성능 향상을 기대할 수 있다.

 

네트워크 트래픽 감소

  • 네트워크는 패킷 단위로 데이터를 전송하는데, 너무 작은 패킷을 자주 전송하면 오버헤드와 지연이 발생한다.
  • 버퍼링을 사용하면 작은 데이터 여러 개를 하나의 패킷으로 묶어서 전송하여 네트워크 부하를 줄일 수 있다.

 

데이터 손실 방지

  • 송신 측과 수신 측의 속도가 다를 수 있다. ( 예: 빠른 서버 <-> 느린 클라 )
  • 만약 버퍼가 없다면, 수신 속도가 느릴 때 송신된 데이터가 사라질 수 있다.
  • 이를 방지하기 위해 수신 버퍼를 사용하여 데이터가 안정적으로 도착할 때까지 저장해야한다.

 

패킷 순서 보장 및 데이터 무결성 유지

  • TCP와 같은 프로토콜에서는 패킷이 순서대로 도착하도록 버퍼를 활용하여 정렬 및 재조합을 한다.
  • 예를 들어, 네트워크 상태가 불안정할 경우 패킷이 순서대로 도착하지 않을 수도 있는데, 버퍼를 사용하여 이를 재조립한다.

 

위와 같은 이유로 버퍼링이 꼭 필요하다.

소켓 입/출력 버퍼의 크기를 확인하려면 다음과 같은 함수를 사용하면 된다.

 

getsockopt
//윈속 초기화
WSADATA wsa = { 0 };
if (::WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
{
	puts("ERROR: 윈속을 초기화 할 수 없습니다.");
	return 0;
}

//소켓 생성
SOCKET hSocket = ::socket(AF_INET, SOCK_STREAM, 0);
if (hSocket == INVALID_SOCKET)
{
	puts("ERROR: 소켓을 생성할 수 없습니다.");
	return 0;
}

//소켓의 '송신' 버퍼의 크기를 확인하고 출력한다.
int nBufSize = 0, nLen = sizeof(nBufSize);
if (::getsockopt(hSocket, SOL_SOCKET,
	SO_SNDBUF, (char*)&nBufSize, &nLen) != SOCKET_ERROR)
	printf("Send buffer size: %d\n", nBufSize);

//소켓의 '수신' 버퍼의 크기를 확인하고 출력한다.
nBufSize = 0;
nLen = sizeof(nBufSize);
if (::getsockopt(hSocket, SOL_SOCKET,
	SO_RCVBUF, (char*)&nBufSize, &nLen) != SOCKET_ERROR)
	printf("Receive buffer size: %d\n", nBufSize);

//소켓을 닫고 종료.
::closesocket(hSocket);
::WSACleanup();

 

위 코드 처럼 getsockopt 함수를 이용해 소켓의 입력/출력 버퍼의 크기를 확인할 수 있다.

 

https://learn.microsoft.com/ko-kr/windows/win32/api/winsock/nf-winsock-getsockopt

 

getsockopt 함수(winsock.h) - Win32 apps

getsockopt 함수(winsock.h)는 소켓 옵션을 검색합니다.

learn.microsoft.com

 

'강의 정리 > 인프런' 카테고리의 다른 글

[인프런] 시큐어 코딩  (0) 2025.03.31
[인프런] 쉘 코드  (0) 2025.03.31
[인프런] 쓰레드 생성 및 실행  (0) 2025.03.31
[인프런] 함수 포인터  (0) 2025.03.22
[인프런] inline 함수와 컴파일러 최적화  (0) 2025.03.17

+ Recent posts