728x90

3주차에는 주소체계와 TCP 기반 서버/클라이언트에 대하여 공부한다.

 

  • 인터넷 주소

인터넷 주소는 당연히 인터넷 상에서 컴퓨터끼리 구분하기 위해 사용되는 주소이다.

4바이트 주소체계인 IPv4와 16바이트 주소체계인 IPv6가 존재한다.

네트워크 주소와 호스트 주소로 나뉘는데, 이름 그대로 네트워크 주소를 이용해서 네트워크를 찾고 호스트 주소를 이용해서 해당 네트워크에서 호스트를 검색한다.

클래스 A 1 Byte (네트워크 ID) 1 Byte(호스트 ID) 1 Byte(호스트 ID) 1 Byte(호스트 ID)
클래스 B 1 Byte (네트워크 ID) 1 Byte (네트워크 ID) 1 Byte(호스트 ID) 1 Byte(호스트 ID)
클래스 C 1 Byte (네트워크 ID) 1 Byte (네트워크 ID) 1 Byte (네트워크 ID) 1 Byte(호스트 ID)
클래스 D 멀티캐스트 IP 주소

 

클래스 A의 첫 번재 바이트 범위는 0이상     127이하  -> 클래스 A의 첫 번째 비트는 항상 0으로 시작

클래스 B의 첫 번째 바이트 범위는 128이상 191이하  -> 클래스 B의 첫 두 비트는 항상 10으로 시작

클래스 C의 첫 번째 바이트 범위는 192이상 223이하 -> 클래스 C의 첫 세 비트는 항상 110으로 시작

 

첫 번째 바이트 정보만 참조해도 IP주소의 클래스 구분이 가능하므로, 네트워크 주소와 호스트 주소의 경계를 구분할 수 있다.

 

  • Port

IP는 컴퓨터끼리 구분하기 위해 사용된다.

그러면 한 컴퓨터 내의 프로그램끼리는 어떻게 구분을 할까?

바로 Port를 사용해서 구분한다.

Port 번호는 소켓을 구분하는 용도로 사용이 된다.

둘 이상의 포트가 하나의 프로그램에 할당될 수도 있기는 하다.

2의 16제곱인 0 ~ 65535의 수를 포트로 지정을 할 수 있지만, 그 중에서 0 ~ 1023은 이미 용도가 지정되어 있는 포트들이라 사용을 피하는 것이 좋다.

 

  • 주소 표현을 위한 구조체
struct sockaddr_in {
	__uint8_t       sin_len;
	sa_family_t     sin_family;
	in_port_t       sin_port;
	struct  in_addr sin_addr;
	char            sin_zero[8];
};

sin_family: 주소체계 정보를 저장한다.

주소체계 의미
AF_INET IPv4 인터넷 프로토콜
AF_INET6 IPv6 인터넷 프로토콜
AF_LOCAL 로컬 통신을 위한 유닉스 프로토콜의 주소체계

sin_port: 16비트 Port 번호를 저장한다.

 

sin_addr: 32비트의 IP주소 정보를 저장한다.

in_addr 구조체는 그냥 32비트 정수 자료형이다.

 

sin_zero: 특별한 의미를 지니지 않으며, 0으로 채운다.

struct sockaddr_in의 크기를 struct_sockaddr에 맞춰주기 위해 만든 멤버이다.

 

이 sockaddr_in은 bind 함수의 인자로 전달을 하는데 bind 함수의 매개변수 타입은 sockaddr이기 때문에 형 변환이 필요하다.

sockaddr도 같은 16바이트이다.

구조체를 살펴보면

struct sockaddr {
	__uint8_t       sa_len;         /* total length */
	sa_family_t     sa_family;      /* [XSI] address family */
	char            sa_data[14];
};

이렇게 작성이 되어 있다.

sockaddr에 IPv4의 정보를 담기가 불편해서 동일한 바이트 크기를 가지는 sockaddr_in 구조체를 정의하고 sin_zero를 통해서 구조체의 크기를 맞춘 것이다.

 

  • 바이트 순서와 네트워크 바이트 순서

컴퓨터구조 시간에 빅 엔디안과 리틀 엔디안에 대해서 공부 했을 것이다.

 

- 빅 엔디안(Big Endian)

상위 바이트의 값을 작은 번지수에 저장하는 방식

 

- 리틀 엔디안(Little Endian)

상위 바이트의 값을 큰 번지수에 저장하는 방식

 

우리가 쓰는 컴퓨터가 빅 엔디안을 쓸 수도 있고, 리틀 엔디안을 쓸 수도 있다.

하지만 네트워크 바이트 순서는 빅 엔디안이 기준이다.

데이터를 전송 할 때는 바이트 단위로 데이터를 전송하기 때문에 상관 없지만 sockaddr_in 구조체의 내용을 전송할 때는 빅 엔디안으로 바꾸어 주어야 한다.

 

그럴 때는 아래의 함수들을 사용한다.

#include <stdint.h>
#include <netinet/in.h>

unsigned short htons(unsigned short);
unsigned short ntohs(unsigned short);
unsigned long htonl(unsigned long);
unsigned long ntohl(unsigned long);

h는 호스트를 의미하고, n은 네트워크를 의미한다.

htons를 예로 설명하면 호스트에서 네트워크로 short를 변경한다는 의미이다.

만약 현재 내 컴퓨터가 Big Endian 컴퓨터라면 원래의 값을 리턴해주기 때문에 본인의 컴퓨터를 신경쓰지 않고 저 코드들을 사용해주면 된다.

 

#include <stdio.h>
#include <arpa/inet.h>

int main(int argc, char *argv[]){
    
    unsigned short host_port = 0x1234;
    unsigned short net_port;
    unsigned long host_addr = 0x12345678;
    unsigned long net_addr;

    net_port = htons(host_port);
    net_addr = htons(host_addr);

    printf("Host ordered port: %#x \n", host_port);
    printf("Network ordered port : %#x \n", net_port);
    printf("Host ordered address: %#lx \n", host_addr);
    printf("Network ordered address: %#lx \n", net_addr);

    return 0;
}

해당 코드를 실행해보았을 때

Host ordered port: 0x1234 
Network ordered port : 0x3412 
Host ordered address: 0x12345678 
Network ordered address: 0x7856

이런 식으로 순서가 바뀌어 출력되게 된다면 해당 컴퓨터는 리틀 엔디안임을 의미한다.

 

우리는 IP주소를 적을 때 111.111.111.111 처럼 문자열로 작성을 하게 된다.

이렇게만 작성을 하고 우리는 수로 변환해주지 않았는 데, 이 때 inet_addr() 함수를 사용하게 된다.

#include <arpa/inet.h>

in_addr_t inet_addr(const char * string);

해당 문자열을 32비트 정수형으로 변환해준다.

 

#include <stdio.h>
#include <arpa/inet.h>

int main(int argc, char *argv[]){
    char *addr1 = "111.111.111.111";
    char *addr2 = "123.123.1.1";

    unsigned long conv_addr = inet_addr(addr1);
    if(conv_addr == INADDR_NONE) printf("Error!!!\n");
    else printf("Network ordered integer addr:  %#lx\n", conv_addr);

    conv_addr = inet_addr(addr2);
    if(conv_addr == INADDR_NONE) printf("Error!!!\n");
    else printf("Network ordered integer addr:  %#lx\n", conv_addr);

    return 0;
}

이렇게 코드를 실행해보면

Network ordered integer addr:  0x6f6f6f6f
Network ordered integer addr:  0x1017b7b

우리가 필요로 하는 32비트로 값을 얻게 된다.

 

이번에는 반환하는 것이 아니라 구조체에 저장하는 함수이다.

#include <arpa/inet.h>

int inet_aton(const char * string, struct in_addr * addr);

-string

변환할 IP 주소 정보를 담고 있는 문자열

- addr

변환된 정보를 저장할 in_addr 구조체 변수의 주소 값 전달

return: 1(success), 0(fail)

 

#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>

void error_handling(char *message);

int main(int argc, char *argv[]){
    struct sockaddr_in addr_inet;

    if(inet_aton(argv[1], &addr_inet.sin_addr)) printf("Network ordered integer addr: %#x\n", addr_inet.sin_addr.s_addr);
    else error_handling("Conversion error");

    return 0;
}

void error_handling(char *message){
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

이 코드를 사용해보면 &addr_inet에 데이터가 들어가게 된다.

 

이번에는 반대로 정수형태를 문자열로 바꾸는 함수이다.

#include <arpa/inet.h>

char *inet_ntoa(struct in_addr adr);

- adr

변환할 정수 값

return: 변환된 문자열의 주소 값, -1(fail)

#include <stdio.h>
#include <string.h>
#include <arpa/inet.h>

int main(int argc, char *argv[]){
    struct sockaddr_in addr1, addr2;
    char *str_ptr;
    char str_arr[20];

    addr1.sin_addr.s_addr=htonl(0x1020304);
    addr2.sin_addr.s_addr=htonl(0x1010101);

    str_ptr = inet_ntoa(addr1.sin_addr);
    strcpy(str_arr, str_ptr);
    printf("Dotted-Decimal notation1: %s \n", str_ptr);

    str_ptr = inet_ntoa(addr2.sin_addr);
    strcpy(str_arr, str_ptr);
    printf("Dotted-Decimal notation2: %s \n", str_ptr);

    return 0;
}

실행해보면 문자열로 출력이 되는 것을 볼 수 있다.

 

이제 우리가 인터넷 주소를 초기화 했던 코드들을 살펴보도록 하자.

struct sockaddr_in addr;
//IP 주소를 문자열로 선언, 보통은 받아옴
char *serv_ip = "111.111.111.111";
//PORT 번호를 문자열로 선언, 이것도 보통은 받아옴
char *serv_port = "1204";
//memset을 이용하여 현재 구조체의 모든 값을 0으로 초기화
memset(&addr, 0, sizeof(addr));
//주소 체계를 지정, 보통은 IPV4로 지정
addr.sin_family = AF_INET;
//문자열로 작성된 IP 주소를 입력
addr.sin_addr.s_addr = inet_addr(serv_ip);
//문자열로 작성된 port 번호를 입력
addr.sin_port=htons(atoi(serv_port));

이제 주소 초기화 한 함수들을 이해할 수 있을 것이다.

 

728x90

HTTP 1일 차로 인터넷 네트워크에 대해서 공부한다.

 

  • 인터넷 통신

만약 컴퓨터 두 대가 직접적으로 연결이 되어 있다면

이렇게 직접 통신을 하면 될 것이다.

 

하지만 수많을 컴퓨터들이 있는 인터넷 내에서는 어떻게 통신을 할까?

이렇게 수많은 노드들을 거친 후에 서버에 도착해야 할 것이다.

과연 어떤 방법으로 서버를 찾아가게 될까?

  • IP

그 방법으로는 컴퓨터마다 고유의 번호를 부여하고 그 번호로 찾아가게 하는 것이다.

고유의 번호를 IP 주소라고 한다.

지정한 IP 주소에 패킷이라는 통신 단위로 데이터를 저장하게 된다.

이렇게 전송 데이터에 IP 관련된 정보를 넣어서 감싼 단위를 패킷이라고 한다.

패킷 안에는 IP 정보가 들어가 있기 때문에 해당 정보를 이용하여 목적지까지 패킷을 운반한다.

하지만 이 IP 프로토콜에도 한계가 있다. 

- 비연결성

만약 패킷을 받을 대상이 없거나 서비스 불가 상태여도 데이터를 전송한다.

 

- 비신뢰성

데이터가 중간에 소실될 수 있고, 패킷이 순서대로 오지 않을 수도 있다.

 

- 프로그램 구분

같은 IP를 사용하는 서버에서 통신하는 애플리케이션이 둘 이상이면 어느 애플리케이션으로 가야 하는지 알 수 없다.

  • TCP, UDP

해당 방법을 TCP에서 해결할 수 있다.

 

프로토콜 계층을 살펴보면

인터넷 프로토콜 스택의 4계층이다.

애플리케이션 계층 - HTTP, FTP
전송 계층- TCP, UDP
인터넷 계층 - IP
네트워크 인터페이스 계층

 

    이거를 사용하는 단계까지 같이 표현을 해보자면

이렇게 나타내진다.

데이터를 전송할 때에는 이 모든 과정을 거쳐 전송하게 된다.

IP 패킷으로 포장하기 전에 TCP 하나를 추가하게 되는 것인데

TCP는 이전에 IP 프로토콜에서 생겼던 문제들을 해결할 수 있다.

TCP 특징

- 연결지향(TCP 3 way handshake, 가상연결)

데이터를 전송하기 전에 서버에 접속 요청을 보낸 후, 서버에서 요청 수락을 하고 서버에서 클라이언트로 접속 요청을 보내면 그것에 응답하여 서로 연결이 된 것을 확인한 후 데이터를 전송하는 방법이다.

클라이언트와 서버 간에는 연결이 된 것을 확인할 수 있지만, 그 중간의 노드들은 접속이 된 것을 알지 못하기 때문에 가상 연결이라고도 한다.

 

- 데이터 전달 보증

데이터를 수신하면 데이터를 수신한 곳에서 송신한 곳으로 수신이 완료되었음을 알리는 신호를 보낸다.

이렇게 하면 데이터가 전달되었다는 것을 보증할 수 있다.

 

- 순서 보장

순서가 맞지 않는다면, 맞지 않는 곳에서부터 재전송을 요청하여 순서대로 도착함을 보장한다.

이렇게 TCP에 대해 알아보았다.

 

UDP는 이와 정반대의 특징을 가지는데

연결지향, 데이터 전달 보증, 순서 보장의 특징을 가지지 않아 IP와 거의 비슷하지만 속도가 훨씬 빠르다.

  • PORT

이렇게 한 번에 둘 이상 연결할 경우가 있다.

이럴 때에는 어떤 데이터가 어디에 쓰이는지 어떻게 구별할 수 있을까?

 

이럴 때에는 위에 같이 포장되어 있는 PORT의 정보를 이용한다.

 

이렇게 포트 번호를 보고 어디에서 온 데이터인지 구별하여 사용한다.

  • DNS

IP는 숫자로만 구성되어 있어 기억하기 어렵다.

우리가 웹 사이트에 접속할 때마다 저 번호를 기억해서 치고 들어가면 상당히 힘들 것이다.

그렇기 때문에 우리는 DNS(Domain Name System)을 사용한다.

도메인 명을 IP 주소로 변환해 주는 방법이다.

우리가 google에 접속하려고 하면 구글의 google.com을 치고 접속한다.

그러면 도메인 서버에서 해당 도메인명에 일치하는 IP주소를 찾아 접속을 해준다.

그러면 우리는 IP 주소를 몰라도 google에 접속할 수 있게 되는 것이다.

'백엔드 > HTTP' 카테고리의 다른 글

HTTP 6일차  (0) 2023.03.09
HTTP 5일차  (1) 2023.03.09
HTTP 4일차  (0) 2023.03.08
HTTP 3일차  (0) 2023.03.06
HTTP 2일차  (0) 2023.03.02

+ Recent posts