[네트워크] 소켓의 생성과 주소할당

minji·2021년 8월 13일
0

앞서 파일 디스크립터에 대해 알아보면서, 데이터 파일과 마찬가지로 소켓에서도 fd값을 이용해 네트워크간 통신을 할 수 있다 고 했다.

따라서 이번에는, fd값을 할당받을 수 있는 socket() 함수와 이 소켓을 통해 서버와 클라이언트가 통신하기 위한 정보에는 어떤 것들이 있는지, 어떻게 지정할 수 있는지 알아보려고 한다.

가장 간단한 예시로, 클라이언트에게 "서버와 연결되었습니다." 문자열을 전송하는 서버 소켓을 만들고, 어떤 식으로 주소를 할당하는지 한 문장씩 해석해보려고 한다.

전체 서버 프로그램(C언어)

1. 소켓의 생성

int serv_sock;
serv_sock=socket(PF_INET,SOCK_STREAM,0);

위의 두 문장이 소켓을 생성하는 문장이다. 정확히는 socket 함수를 통해 소켓을 생성할 수 있다.
이 때, serv_sock 값이 파일 디스크립터 번호에 해당하며, 운영체제가 이를 할당 (실패시 -1) 한다.
socket 함수의 3가지 형식 매개변수는 순서대로 (프로토콜 체계, 데이터 전송방식, 프로토콜 정보) 에 해당하며, 아래와 같다.

  • PF_INET : IPv4를 의미

    IPv4 는 인터넷 프로토콜로, 현재 대부분의 IP 주소는 IPv4이다., IPv6를 사용하는 경우, PF_INET6로 대체해주면 된다.
    cf) v4와 v6의 차이는 주소를 표현하는 바이트수로, v4는 4바이트, v6는 16바이트의 주소를 가진다.

  • SOCK_STREAM : TCP 소켓임을 의미

    두번째 인자의 경우, 데이터의 전송 방식에 따라 TCP 와 UDP로 나눌 수 있다.
    TCP는 연결지향형 방식으로, 신뢰성이 높다는 특징을 가진다. 데이터가 소실없이, 순서대로 전달되어야만 하는 응용프로그램들에 주로 이용된다.
    UDP는 비 연결지향형 방식으로, 빠른 전송을 지향한다. 실시간 화상과 같은 속도가 중요한 응용 프로그램들에 주로 사용된다.
    UDP 소켓을 원할 경우 SOCK_DGRAM으로 대체해준다.

  • 0 (프로토콜 정보)

    세번째 인자인 프로토콜 정보의 경우, 앞선 1,2번째 인자만으로 프로토콜을 정확히 특정할 수 없을때만 이를 따로 명시해주면 된다. 그렇지 않은 경우에는 위와 같이 0으로 대체해도 무관하다.
    예시의 경우, "IPv4 프로토콜 체계에서 동작하는 TCP 소켓" 의 프로토콜이 IPROTO_TCP 하나로 특정되므로, 0을 적어준 것이다.

2. 소켓의 주소 할당

socket() 함수를 통해 fd값은 할당받았고, 소켓의 유형까지 지정 완료했다.

이제 이 서버 소켓은 다른 네트워크 어딘가의 클라이언트와 통신을 해야한다 .
그렇다면 이 소켓에 추가적으로 정의되어야하는 정보는 무엇이 있을까?

바로 IP 주소와 port번호이다. 즉, 소켓에는 두 주소가 할당되어야한다.
IP주소로 컴퓨터를 특정, Port번호로 프로세스를 특정할 수 있어야 하기 때문이다.

struct sockaddr_in serv_addr;

serv_addr 변수가 바로 소켓의 주소가 할당될 변수이다.
그 구조체인 sockaddr_in의 선언문을 보면, 다음과 같다.

struct sockaddr_in {
	sa_family_t     sin_family;
	in_port_t       sin_port;
	struct  in_addr sin_addr;
	char            sin_zero[8];
};
  • sin_family : 주소 체계 (IPv4, IPv6, 로컬통신 등)

  • sin_port : port 번호

    port 번호의 자료형인 in_port_t를 확인해보면, __uint16_t 으로 선언되어있는 것을 볼 수 있다.
    이는 port번호가 16비트임을 의미한다.
  • sin_addr : ip주소

    ip주소 역시, 구조체를 타고 들어가보면 __uint32_t로 선언되어 있음을 확인할 수 있고, 이는 ip주소가 32비트에 해당함을 알 수 있다. 즉, 32비트 = 4바이트 이므로 IPv4에 해당한다.
  • sin_zero :

    이와 관련해서는 바로 아래에서 자세히 알아보자. 우선은 해당 8비트가 모두 0으로 채워진다는 것만 알면 된다.


구조체의 각 변수들의 의미를 알았으니, 이제 이를 토대로 주소를 부여해주면 된다.
해당 문장들을 하나씩 살펴보자.

memset(&serv_addr,0,sizeof(serv_addr));
serv_addr.sin_family=AF_INET;
serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);
serv_addr.sin_port=htons(atoi(argv[1]));
  • memset(&serv_addr,0,sizeof(serv_addr));

    이 문장을 파악하기 위해서는 실제로 bind 함수에 할당되는 변수가 sockaddr_in 형이 아닌 sockaddr형인 것에 주목해야 한다.
bind(serv_sock,(struct sockaddr*) &serv_addr,sizeof(serv_addr))

sockaddr_in 구조체는IPv4의 주소정보를 담기 위한 구조체이다. 반면, 실제로 bind에 할당되는 sockaddr 형은 다음과 같이 이루어져 있다.

struct sockaddr {
	__uint8_t       sa_len;         /* total length */
	sa_family_t     sa_family;      /* [XSI] address family */
	char            sa_data[14];    /* [XSI] addr value (actually larger) */
};

즉, sa_data에 해당하는 부분을 통해 ip주소와 port번호를 특정할 수 있어야 하는데, IPv4의 경우, 14바이트를 채우지 못한다. 따라서 나머지 바이트들을 0으로 채우기로 약속한 것이다.
앞에서 확인했듯이, IPv4의 port번호는 2바이트 + ip주소는 4바이트 = 6바이트이기 때문에, 14-6=8바이트를 sin_zero로 채워준 것을 알 수 있다.

  • serv_addr.sin_family=AF_INET;

    IPv4에 해당하는 주소체계이다.
  • serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);

    IP주소를 할당하는 문장이다.
    우선, htonl함수는 host to network (long) 으로 해석할 수 있다.
    즉, long 자료형의(4바이트이므로 ip주소일 것을 예측할 수 있다.) 변수를 호스트 바이트 순서에서 네트워크 바이트순서로 변환하는 함수이다.

    참고로 바이트 순서란, CPU가 메모리에 데이터를 저장하는 방식으로, 빅엔디안이냐, 리틀엔디안이냐는 의미이다.
    예를들어 리틀엔디안 방식을 이용하는 환경에서 빅엔디안 방식을 이용하는 환경으로 데이터를 전송하려면 그 저장 방식에 맞게 순서를 미리 바꾸어야 할 것이다.


    따라서, 원하는 순서대로 데이터를 저장하도록 하기 위해 htonl 함수를 이용해야한다.

    다음으로, INADDR_ANY운영체제에서 임의로 IP주소를 할당해달라는 의미이다.
    대부분의 IP주소를 하나만 갖는 서버의 경우, 매번 그 주소를 입력하는 것은 번거로운 일일 것이다. 따라서 위와 같은 상수를 통해 간편하게 할당할 수 있도록 한 것이다
  • serv_addr.sin_port=htons(atoi(argv[1]));

    port번호를 할당하는 문장이다.
    위의 예시를 참고해 알 수 있듯, htons 는 short형(2바이트)의 port번호를 host 바이트 순서에서 network 바이트 순서로 변환함을 알 수 있다.
    그 변수에 해당하는 atoi는 a(char) to i(integer)으로, 문자열로 입력된 포트번호를 정수형으로 바꾸어주는 함수이다.

여기까지, 소켓을 생성하고 그 주소를 초기화하는 과정까지 완료했다.
이제 해당 소켓을 클라이언트와 통신할 수 있도록 실제로 열어놓고, 대기하는 과정을 알아보자.

** 윤성우의 열혈 TCP/IP 소켓 프로그래밍을 토대로 공부하며 정리한 글입니다

profile
SW Engineer

0개의 댓글