[소켓프로그래밍/ComputerNetwork/SocketAPI] Send,Recv,Accept

SHark·2025년 2월 23일

ComputerNetwork

목록 보기
7/7
post-thumbnail

Blocking과 NonBlocking

소켓은 Blocking모드와 NonBlocking모드 2가지 모드를 지원합니다. 기본은 Blocking모드 입니다.

Blocking

  • 함수가 일을 완료할 때 까지 기다리는 것을 의미합니다. 코드의 흐름이 멈추게 됩니다.
  • 1:1 통신이거나, 서버/클라에서 네트워크적인 행동외에 할 것이 없다면 Blocking이어도 상관없습니다.

NonBlocking

  • 함수가 일을 할 수 없다면 바로 Return 합니다.
  • 함수가 일을 할 수 없다는걸 알면, 내 코드에서 다른 일을 할 수 있습니다.
  • 즉, 서버/클라에서 각자가 해야할 일(ex.게임 프레임 로직)을 처리할 수 있게 됩니다.
  • 에러가 아닌 경우, 일을 할게 없어서 바로 Return을 하는 경우 WinSock에서는 에러코드 WSAEWOULDBLOCK을 반환하게 만들어졌습니다.

여기서 동기와 비동기에 대한 이야기가 나올 수 있는데, I/O은 태생적으로 비동기적입니다. 왜냐하면, I/O작업은 코드에서 직접하는게 아니라, Kernel Code와 Ethernet장치가 하게됩니다.

ioctlsocket

  • 성공시 0을 반환합니다.
  • 실패시, SOCKET_ERROR를 반환합니다.
  • Listen전에 해야합니다.
ULONG uMode;
ioctlsocket(_ListenSocket, FIONBIO, &uMode);

Accpet

연결이 성립된(handShake가 끝난) 대상에 소켓을 매핑시키는 작업을 합니다. 시그니처는 아래와 같습니다.

SOCKET WSAAPI accept(
  [in]      SOCKET   s,
  [out]     sockaddr *addr,
  [in, out] int      *addrlen
);

example

SOCKAADR_IN connectInfo;
ZeroMemory(&connectInfo);

int connectLen = sizeof(connectionInfo);
SOCKET connectSocket = ::accpet(_ListenSocket, reinterpret_cast<SOCKADDR*>(&connectInfo), &connectLen);
  • _ListenSocket : 리슨 소켓(Server의 소켓)을 집어넣는 자리입니다.
  • connectInfo : 연결된 소켓(Client)의 정보를 집어넣는 SOCKADDR_IN 구조체입니다.

Blocking Socket인 경우

  • 백로그 큐에 대기 중인 연결이 있다면, Accept()를 호출하고 Socket을 반환합니다.
  • 백로그 큐가 비어있다면(대기중인 연결이 없다면), 계속 Blocking상태로 있습니다.
  • 성공시, SOCKET을 반환하고 실패시 INVALID_SOCKET을 반환합니다.

Blocking Socket인데, Accept에 실패했다고?

BackLogQueue에는 연결이 다 되어서 들어오는데, 어떻게 실패할 수 있을까는 생각이 직관적으로 들 수 있다.

Accpet실패를 상대방이 끊어서 실패하는 경우지 않을까라는 어떻게보면 직관적으로 무책임하게(?) (최적화를 하고싶은데, 캐시를 제일 먼저 의심하는 것처럼)생각할 수 있는데 절대 아니다.

Accpet는 BackLogQueue에 있는 연결정보를 그대로 가져올 뿐이다. 설령 상대방이 connect을 한 뒤, 바로 끊는다고해도 TCP입장에서는 그 순간에는 그걸 모른다. Send(),Recv()와 같이 동작을 취해야 해당 소켓이 닫혔는지 어떤 상태인지 알 수 있는 것이다.

실제로 MS공식문서에서 에러코드도 연결이 실패했다거나, 적극적으로 거부했다거나 이런게 없다.
Accpet MSDN

따라서, Blocking일 때, Accept에 실패했다는건 SOCKET을 만들 메모리가 없거나, 인자가 잘못되었거나, Listen을 그전에 안해줬거나 등 프로그래머가 실수한 케이스 밖에 없다는 이야기이다.

NonBlocking 모드인 경우

  • 백로그 큐에 대기중인 연결이 있다면, Socket을 반환합니다.
  • 백로그 큐가 비어있다면, 바로 Return을 합니다.
  • 성공시 SOCKET을 반환하고, 실패시 INVALID_SOCKET을 반환합니다.
    • 당장 할일이 없어서, Accpet의 경우는 BacklogQueue가 비어있어서겠죠? ErrorCode를 반환한 경우 WSAEWOULDBLOCK을 반환합니다.

Send

연결된 소켓(Established된 소켓)에 데이터를 보내는 함수입니다. TCP는 Send, UDP는 sendto 함수를 이용합니다.

int WSAAPI send(
  [in] SOCKET     s,
  [in] const char *buf,
  [in] int        len,
  [in] int        flags
);

Blocking Socket인 경우

  • Send로 요청된 Len을 송신버퍼에 복사할 때 까지 Blocking 하게 됩니다.
  • 송신버퍼에 복사했을 때, 성공했다면 해당 길이만큼 Return합니다.
  • 송신에 실패하는 경우, SOCKET_ERROR를 반환하게 됩니다.
    - 송신에 실패하는 케이스는 매우 다양하기 때문에, 그때 그때 WSAGetLastError()로 에러코드를 확인하는게 원칙이며, 예외적으로 나중에 자주 뜨거나 무시가능한 경우들이 확정이 될때는 제외할 수 있습니다.
  • 따라서, Return값은 SOCKET_ERROR,요청한 길이 2가지 케이스밖에 없습니다.

NonBlocking Socket인 경우

  • Send로 요청된 Len을 송신버퍼에 복사를 시도합니다.
  • 송신버퍼에 모두 복사가 될 수도 있고, 복사가 안될 수도 있습니다. (Len이 1~요청한 길이)
  • 송신에 실패한 경우, SOCKET_ERROR를 반환하게 됩니다.

NonBlocking Socket에서 Send 요청시, 요청한 길이 미만이거나 WOULDBLOCK이 발생한 경우

  • Send 요청을 했으나, 송신버퍼에 해당 길이만큼의 공간이 없다는 의미입니다.
    • 이 의미는 꽤 심각할 수 있는데, 송신버퍼에 send요청한 만큼 공간이 없는 경우라면, 먼저 상대방의 수신 윈도우가 가득찼다는게 전제가 된 상황입니다.
    • 상대방의 수신 윈도우가 가득차서(데이터를 읽어가지 않아서) 송신버퍼에 쌓아놓는 상황이 일어날 때, 위와 같은 상황이 발생하게 됩니다.

따라서, NonBlocking으로 Send했을 때 WOULDBLOCK 혹은 Len가 작다면 연결을 끊을 생각도 해봐야합니다.(애초에 데이터를 읽지 못하는 상황)

Recv

데이터를 수신하는 함수입니다.

int recv(
  [in]  SOCKET s,
  [out] char   *buf,
  [in]  int    len,
  [in]  int    flags
);
  • 정상종료인 경우 0을 반환합니다. (수신버퍼가 비었을 때를 의미하지 않습니다.)
  • 수신에 실패한 경우 SOCKET_ERROR를 반환합니다.
  • Blocking이든, NonBlocking이든 수신에 성공한다면 Return값으로 1~Len의 길이를 받습니다.
  • Blocking이라면, 수신할 게 있다면(수신버퍼에 데이터가 1개라도 있다면) 수신을 하고 아니라면 계속 Blocking상태에 있습니다.
  • NonBlocking이라면, 한번 시도해보고 수신할게 없다면 WSAEWOULDBLOCK을 반환할겁니다.

수신 시에, 1~Len까지인 이유에 대해서

  • TCP는 Stream형태이기 때문에, 패킷(세그먼트)의 경계가 명확하지 않습니다.
    따라서, 패킷이 짤려올 수도 있고 뭉쳐져서 보내질수도 있습니다.

  • 예를들어, 수신윈도우의 공간이 조금밖에 없어서 상대방이 원하는 데이터를 모두 못보낼 수도 있습니다.

  • 혼잡제어 때문에 송신하는 과정에서 일부의 데이터만 전송될 수도 있습니다.

  • 네이글 과정에서 MSS에 딱 도달해서, 데이터가 짤려서 보내질 수 있습니다.

  • 네이글 과정에서 여러 PayLoad가 하나의 TCP Header에 뭉쳐져서 올 수도 있습니다.

따라서, Recv는 위같은 TCP의 구현을 대응할 수 있도록 만들어졌기 때문에 Recv는 1~Len까지 받는게 기본동작이 되었다고 알고 있습니다.

SocketOption

소켓에는 소켓 옵션을 설정할 수 있습니다.

int WSAAPI setsockopt(
  [in] SOCKET     s,
  [in] int        level,
  [in] int        optname,
  [in] const char *optval,
  [in] int        optlen
);

다양한 옵션이 있지만, 자주 쓰게되는 옵션만 따로 정리하겠습니다.

LingerOption

Linger Option은 closesocket을 언제 반환할지에 대한 내용입니다.
closesocket은 1.FIN 송신(종료절차수립),2.Socket Handle반환 2가지 역할을 하는 함수입니다.

이때, FIN 송신을 주고 4wayHandShake과정으로 들어갔을 때 아래와 같은 선택지가 있습니다.
1. 즉시 closeSocket리턴, 커널이 자신의 송신버퍼에 남아있는 데이터를 전부 보내고 소켓을 정리하기
2. 즉시 CloseSocket리턴 ,송신버퍼에 있는 데이터를 전부 버리고 소켓을 강제종료시키기.
3. 일정 시간 동안 closeSocket리턴하지않고, 일정 시간(TimeOut)시간을 두고
리턴, 그 동안 커널이 자신의 송신버퍼에 있는 데이터를 전부 보내고, 남은건 버리기(혹은 전부 다 보내기)

기본동작은 1번 입니다. 그리고, 2번은 꽤 활용도가 있을 수 있습니다. 왜냐하면, 4wayHandshake를 타지않는 상황을 바랄 때 RST를 보내도록 할 수 있습니다.

LINGER option

/*default*/
option.l_onOff = 0;

/* RST */
option.l_onOff = 1;
option.l_linger = 0;

/* ??? */
option.l_onOff = 1;
option.l_linger = 10;

LINGER에 l_linger가 양수인게 의미가 있을까?

흔히하는 오해가, l_linger값을 10으로 셋팅한다면, 10초뒤에 RST를 쏴주지 않을까?라는 의미로 해석될 수 있습니다. 하지만, RST를 쏴주지 않습니다.

LINGER는 closeSocket()의 함수리턴시점을 정해주는 옵션입니다.

  • 정해진 시간 내에 성공적으로 송신버퍼에 있는 데이터를 모두 보냈어도 0을 반환.
  • 정해진 시간 내에 송신버퍼에 있는 데이터를 모두 보내지못해 타임아웃을 당했어도 0을 반환.

SNDBUF, RCVBUF

송/수신버퍼의 크기를 설정할 수 있다고 나와있지만, RCVBUFF는 애초에 정해진 크기보다 크게 잡기 때문에, 잘 동작을 하지 않을 수도 있고 송신버퍼는 0을 줬을 때는 다른 의미를 가지기 때문에 꽤 흥미로울 수 있습니다.

https://learn.microsoft.com/en-us/previous-versions/troubleshoot/windows/win32/data-segment-tcp-winsock

BROAD_CAST

  • UDP에서 사용합니다.

KeepAlive

  • TCP가 연결을 무한히 유지될 수 있는 상황(서로 데이터를 안보내고 연결만 해놓으면 그냥 계속 유지됨)이 있기 때문에, TCP의 구현들은 대부분 KeepAlive 기능을 자체적으로 만듭니다.
    • 연결이 지속되는 시간을 설정할 수 있습니다. OS마다 지속되는 기본 시간이 다릅니다.
    • 더미 테스트 같은걸 할 때, 여러 더미들이 한꺼번에 들어갔다가 한꺼번에 나가는 경우 RST가 유실되는 상황이 있는데, 이때 TCP에서 연결이 제대로 끊기지 않는 상황을 대비해서 설정하는 경우가 종종 있다고 합니다.

0개의 댓글