What is htons and When is it used?

정수는 LSB(Least Significant Byte)를 먼저 표현하거나 MSB(Most Significant Byte)를 먼저 표현하는 방식으로 나타낼 수 있습니다. 머신이 내부적으로 어떻게 구성되어 있나에 따라 두 방식 모두 합리적일 수 있습니다. 하지만 네트워크 통신을 위해서는 합의된 형식으로 통일할 필요가 있습니다.

 

htons(xyz)는 16비트의 부호가 없는 정수 short의 값 xyz를 Network Byte Order로 반환해줍니다.

htonl(xyz)는 32비트의 부호가 없는 정수 long의 값 xyz를 Network Byte Order로 반환해줍니다.

 

이러한 함수는 host to netwok로 읽을 수 있습니다. 이 반대의 함수(ntohs, ntohl)은 network byte order를 host order로 바꿔줍니다. 그렇다면 host는 little-endian일까요, big endian일까요?? 정답은 기기가 무엇인가에 따라 다르다는 것입니다. 호스트가 코드를 동작하는 것은 실제 구조에 달렸습니다. 만약 아키텍쳐가 network byte order와 같다면 이 함수들의 리턴값은 인자와 같습니다. x86 머신에서는 host와 network order는 서로 다릅니다.

 

요약:low level C network structure(port와 address information)을 읽거나 쓸 때, 위의 함수를 사용하는 것을 잊지 마세요. 그렇지 않으면 특정 값들이 이상하게 출력되는 것을 볼 수 있을 것입니다.

 

What are the 'big 4' network calls used to create a server?

TCP 서버를 만들기 위해서 4가지의 시스템 콜이 필요합니다. :socket, bind, listen, accept

각각의 시스템 콜은 특정한 목적이 있고 위의 순서대로 호출되어야 합니다. bind를 사용해 port 정보를 직접 정하거나 getaddrinfo를 사용해 생성될 수도 있습니다.  

 

What is the purpose of calling socket?

네트워크 커뮤니케이션의 끝점을 생성하기 위해 socket을 사용합니다. 새로운 소켓은 그 자체로는 특별히 쓸모가 없습니다. 패킷이나 stream based connection에 특정하지만 특정 네트워크 인터페이스나 포트에 의존하지는 않습니다. 대신 socket은 network descriptor를 반환합니다. 이 decriptor는 나중에 bind, listen, accept를 호출하는 데 사용됩니다.

 

What is the purpose of calling bind?

bind는 추상적인 소켓을 실제 네트워크 인터페이스와 포트에 연관시켜줍니다. TCP client에서도 bind를 호출하는 것이 가능하지만, 일반적으로 나가는 port를 특정하는 것은 불필요합니다.

 

What is the purpose of calling listen?

listen은 수신되었지만 아직 처리되지 않은 연결에 대한 큐 사이즈를 특정합니다. 즉, accept에 의해 network descriptor가 아직 할당되지 않은 경우입니다. 높은 서버 효율을 위한 일반적인 값으로 128이나 그 이상의 값을 사용합니다.

 

Why are server socket passive?

서버 소켓은 다른 호스트와 동적으로 연결을 시도하지 않습니다. 대신 들어오는 연결을 기다립니다. 추가로, 서버 소켓은 peer가 연결을 끊을 때 닫히지 않습니다. 대신 클라이언트는 서버의 그 연결과 관련된 별도의 활성화된 소켓과 통신합니다.

 

고유한 TCP 연결은 tuple에 의해 특정됩니다.(source ip, source port, destination ip, destination port). 이것은 웹 브라우저에서 같은 서버 포트를 사용해 여러개의 연결을 가질 수 있도록 해줍니다. 왜냐하면 도착하는 패킷의 각각의 source port가 고유하기 때문입니다. 특정한 서버 포트에서 하나의 passive 서버 소켓이 존재하지만 여러개의 active socket이 존재할 수 있습니다.(현재 열려있는 각각의 연결에 대해) 그리고 서버의 OS는 lookup table을 가지고 있어 특정한 tuple을 활성화된 소켓에 연관지어줍니다. 이러한 과정을 통해 들어오는 패킷이 적절한 소켓으로 전달될 수 있습니다.

 

What is the purpose of calling accept?

일단 서버소켓이 초기화되면 서버는 accept를 호출해 새로운 연결을 기다립니다. socket, bind, listen과 달리 accept 호출은 블록됩니다. 즉, 새로운 연결이 없다면 accept 호출은 블록되고 새로운 클라이언트가 연결되었을 때 반환됩니다. 반환되는 TCP 소켓은 특정한 튜플(클라이언트 IP, 클라이언트 포트, 서버 IP, 서버 포트)와 연관되어 있습니다. 이 TCP 소켓은 이 튜플에 매칭되는 들어오고 나가는 TCP 패킷에 사용됩니다.

 

accept 호출이 새로운 file descriptor를 반환한다는 것을 잘 기억해두세요. 이 file descriptor는 특정한 클라이언트에 한정되어 사용됩니다. 기존의 서버 소켓을 사용해 server I/O를 처리하는 실수를 저지르기 쉽습니다. 

 

What are the gotchas of creating a TCP-server?

TCP 서버에서는 아래와 같은 실수를 저지르기 쉽습니다.

  • 위에서 기술했듯이 서버 소켓 file descriptor를 사용하는 것
  • getaddrinfo에 필요한 SOCK_STREAM을 특정하지 않는 것
  • 존재하는 포트를 다시 사용하는 것
  • 사용되지 않은 구조체 항목을 초기화하지 않는 것
  • 포트가 사용중일 때 bind 호출이 실패하는 것

포트는 각각의 기계에 할당됩니다. 프로세스나 사용자에게 할당되는 것이 아닙니다. 즉, 다른 프로세스가 1234 포트를 사용중이라면 1234 포트를 사용할 수 없습니다. 또한  프로세스가 끝난 다음에 기본적으로 포트는 연결된 상태입니다.

Server code example

아래의 코드는 간단한 TCP 서버의 예입니다. 이 예는 불완전합니다. 예를 들어 socket descriptor를 닫지 않았거나, getaddrinfo에 의해 생성된 메모리를 해제해주지 않았습니다.

 

#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <unistd.h>
#include <arpa/inet.h>

int main(int argc, char **argv) {
    int s;
    int sock_fd = socket(AF_INET, SOCK_STREAM, 0);

    struct addrinfo hints, *result;
    memset(&hints, 0, sizeof (struct addrinfo));
    hints.ai_family = AF_INET;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_flags = AI_PASSIVE;

    s = getaddrinfo(NULL, "1234", &hints, &result);
    if (s != 0) {
        fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(s));
        exit(1);
    }

    if (bind(sock_fd, result->ai_addr, result->ai_addrlen) != 0) {
        perror("bind()");
        exit(1);
    }

    if (listen(sock_fd, 10) != 0) {
        perror("listen()");
        exit(1);
    }
    
    struct sockaddr_in *result_addr = (struct sockaddr_in *) result->ai_addr;
    printf("Listening on file descriptor %d, port %d\n", sock_fd, ntohs(result_addr->sin_port));

    printf("Waiting for connection...\n");
    int client_fd = accept(sock_fd, NULL, NULL);
    printf("Connection made: client_fd=%d\n", client_fd);

    char buffer[1000];
    int len = read(client_fd, buffer, sizeof (buffer) - 1);
    buffer[len] = '\0';

    printf("Read %d chars\n", len);
    printf("===\n");
    printf("%s\n", buffer);

    return 0;
}

 

Why can'y my server re-use the port?

기본적으로 포트는 서버 소켓이 닫히고 나서 즉시 해제되지 않습니다. 대신 포트는 TIMED-WAIT 상태로 들어갑니다. 이러한 경우 timeout이 유효한 네트워크 코드를 실패하도록 보이게 하여 개발에 심각한 혼란을 줄수 있습니다.

 

포트를 즉지 재사용 가능하게 하고 싶다면 포트에 바인딩하기 전에 SO_REUSEPORT를 설정해야 합니다.

int optval = 1;
setsockopt(sfd, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof (optval));

bind(....
Posted by 몰랑&봉봉
,