본문 바로가기
Computer Science/UNIX & Linux

[UNIX/Linux] ep11-1) 소켓 프로그래밍 기초

by 클레어몬트 2024. 11. 26.

https://claremont.tistory.com/entry/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-IPCInter-Process-Communication

[운영체제] IPC(Inter-Process Communication)

https://claremont.tistory.com/entry/ep2-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4process [운영체제] ep2) 프로세스(process)ㅁ프로세스(process): 실행 중인 프로그램 (실행/스케줄링의 단위 및 자료구조)보조기억장치에 저장

claremont.tistory.com

 
[ep11-1, ep11-2의 학습목표]
1. TCP/IP 프로토콜의 기본 개념을 이해한다
2. IP 주소와 포트 번호의 개념을 이해한다
3. 소켓 관련 구조체와 함수를 이해한다
4. 소켓을 이용한 통신 프로그램을 작성할 수 있다
 
 
ㅁTCP/IP 프로토콜의 5계층
• 네트워크 계층 구조의 기준이라고 할 수 있는 ISO의 OSI 7계층과 달리 TCP/IP는 5계층
(하드웨어 계층과 네트워크 접속 계층을 묶어서 4계층으로 구분하기도 함)

 
1. 응용 계층: 사용자에게 서비스를 제공하기 위한 계층
e.g. Telnet, FTP, HTTP, SMTP, DNS 등
 
2. 전송 계층: "패킷의 전송"을 담당하는 계층
e.g. TCP, UDP 등
 
3. 네트워크 계층: 인터넷 계층이라고도 하며 "패킷이 전달되는 경로"를 담당
e.g. IP, ICMP, ARP 등
 
4, 5. 네트워크 접속 계층, 하드웨어 계층: 물리적인 네트워크와의 연결을 담당
e.g. 이더넷 카드, 랜 카드
 
ㅇTCP(Transmission Control Protocol)
전화를 걸 때처럼 데이터를 주고받기 전에 송신 측과 수신 측이 연결되어 있어야 함
• 송신한 데이터가 수신 측에 도착했는지 확인하는 과정을 거침
• 데이터를 주고받는 속도를 조절해 통신 효율이 떨어지거나 데이터가 손실되는 일을 방지할 수도 있음
 
ㅇUDP(User Datagram Protocol)
• 사전에 목적지와 연결을 설정하지 않고 상대방이 데이터를 제대로 수신했는지 확인하지 않음
• 단순히 목적지 주소를 지정해 네트워크로 전송
• 중도에 데이터가 분실되어도 신경 쓰지 않음
• 신뢰성보다는 속도가 중요한 서비스에 주로 이용 e.g. OTT, 게임
 
소켓 인터페이스: 응용 계층에서 전송 계층의 기능을 사용할 수 있도록 제공하는 API(응용 프로그래밍 인터페이스)
응용 프로그램과 TCP 계층을 연결하는 역할 (전송 계층이나 네트워크 계층의 복잡한 구조를 몰라도 네트워크 프로그램을 쉽게 작성할 수 있다)

 
 
IP 주소: 인터넷을 이용할 때 사용하는 주소
점(.)으로 구분된 32비트 숫자로 표시
+ IP 주소를 네트워크 주소(인터넷 주소)라고도 한다
 
호스트명: 시스템은 주소를 숫자로 구분하는 것이 효율적이지만 사람은 이름으로 구분하는 것이 편리하여 IP주소 외에 이름을 지정
인터넷에서 사용하는 호스트 명은 ‘호스트명+도메인명’ 형태로 구성
호스트명은 같은 도메인 안에서 중복되지 않게 시스템 관리자가 정해서 사용한다
- DNS(Domain Name System): 호스트명과 도메인명을 관리하는 시스템
※ 국내에서는 한국인터넷진흥원에서 kr 도메인을 관리
 
[호스트명과 IP 주소 변환] 호스트명과 IP 주소를 등록해 놓은 파일이나 DB를 검색
- 이와 관련된 파일은 /etc/hosts며, DB로는 제공하는 서비스에 따라 DNS일 수도 있고 NIS일 수도 있음
- /etc/nsswitch.conf 파일에 어떤 데이터베이스를 어떤 순서로 활용하는지 지정하고 있음
 
(예시)

hosts: files dns # 의미: 호스트명과 IP 주소를 먼저 파일에서 찾고, 파일에서 찾지 못하면 DNS 서비스를 이용함

 
Linux에서는 호스트명과 IP 주소를 변환하는 여러 가지 함수를 제공
[호스트명과 IP 주소 읽어오기]
ㅇ struct hostent* gethostent(void): 호스트명과 IP 주소를 읽어서 hostent 구조체에 저장하고 그 주소를 반환
DB의 끝을 만나면 null을 반환
 
(hostent 구조체)

struct hostent {
    char* h_name; // 호스트명을 저장
    char** h_aliases; // 호스트를 가리키는 다른 이름들을 저장
    int h_addrtype; // 호스트 주소의 형식을 지정
    int h_length; // 주소의 길이를 지정
    char** h_addr_list; // 해당 호스트의 주소 목록을 저장 (이 항목의 값을 해석하려면 2절에서 배우는 함수들이 필요)
};

*ent는 entry의 줄임말이다(데이터베이스의 그 entry)
 
ㅇ void sethostent(int stayopen): DB의 현재 읽기 위치를 시작 부분으로 재설정
- stayopen : IP 주소 DB를 열어둘 것인지 여부를 나타내는 값
stayopen 값이 0이 아니면 DB가 열린 채로 유지
 
 
ㅇ void endhostent(void): DB를 닫음
수행에 성공하면 0을 반환
 

#include <netdb.h>
#include <stdio.h>

int main() {
    struct hostent* hent;

    sethostent(0); // 호스트 파일의 처음으로 읽기 위치를 설정

    // 호스트 파일에서 차례로 읽은 호스트명(h_name)을 출력
    while ((hent = gethostent()) != NULL) {
        printf("Name=%s\n", hent->h_name);
    }

    endhostent(); // 호스트 파일을 닫음

    return 0;
}

 
/etc/hosts 파일의 내용이 다음과 같을 때 위의 코드를 실행하면 다음과 같이 호스트명 부분이 출력

 
※ 네트워크 라이브러리 지정
위의 코드와 같은 네트워크 프로그램을 그냥 컴파일하면 OS에 따라 네트워크 라이브러리를 포함하지 않아 오류가 발생할 수 있으므로 네트워크 라이브러리를 지정해줘야 함

gcc -o ch12_1.out ch12_1.c -lnsl

 
 
ㅇ struct hostent* gethostbyname(const char* name): 호스트명으로 정보 검색
- name : 검색하려는 호스트명
호스트명을 인자로 받아 DB에서 해당 항목을 검색해 hostent 구조체에 저장하고 그 주소를 반환
 
 
IP 주소를 인자로 받아 DB에서 해당 항목을 검색해 hostent 구조체에 저장하고 그 주소를 반환
ㅇ struct hostent* gethostbyaddr(const void* addr, socklen_t len, int type): IP 주소로 정보 검색
- addr : 검색하려는 IP 주소 (addr에 저장되는 주소는 변환을 수행한 것)
- len : addr 길이
- type : IP 주소 형식
 
(type 인자에 사용하는 주소 형식)

이 중에서 주로 AF_UNIX와 AF_INET 을 사용한다

 
ㅁIP 주소 구분의 필요성
• IP 주소는 데이터가 전송될 목적지 호스트를 알려주는 역할을 수행
• 목적지 호스트에는 여러 가지 기능을 수행하는 서비스 프로세스들이 동시에 동작하고 있을 수 있음 e.g. 웹 서비스, 메일 서비스, FTP 서비스, 텔넷 서비스 등을 수행하는 프로세스가 동시에 동작
• 전송되어오는 데이터를 어느 서비스 프로세스에 전달할 것인지 구분할 수 있어야 함
• 인터넷에서도 IP 주소 외에 서비스를 구분하는 다른 정보가 필요
 
포트 번호
• 2바이트 정수로 되어 있으므로 0~65535까지 사용할 수 있음
• 잘 알려진 포트는 인터넷에서 자주 사용하는 서비스에 미리 지정된 포트 번호이다 (0~1023까지 사용)
e.g. SSH 프로토콜: 22, FTP: 21, HTTP: 80 등
• 일반 프로그램에서는 0~1023을 제외한 1024~65535를 사용
• 이미 정해진 포트 번호는 /etc/services 파일에 등록
• 물론 시스템은 /etc/service 파일에서 정보를 검색하는 함수를 제공
 
 
[포트 정보 읽어오기]
ㅇ struct servent* getservent(void): 포트 정보를 읽어서 servent 구조체에 저장하고 그 주소를 반환
DB의 끝을 만나면 null을 리턴
 
(servent 구조체)

struct servent {
    char* s_name; // 포트명을 저장
    char** s_aliases; // 해당 서비스를 가리키는 다른 이름들을 저장
    int s_port; // 포트 번호를 저장
    char* s_proto; // 서비스에 사용하는 프로토콜의 종류를 나타냄
};

*servent는 service entry의 줄임말이다 

getservent() 함수는 socket 라이브러리에 정의되어 있다
만약 컴파일에서 함수 오류가 발생하면 다음과 같이 -lsocket으로 지정해줘야 한다

gcc -o ch12_1.out ch12_1.c -lnsl

 
 
ㅇ void setservent(int stayopen): DB의 현재 읽기 위치를 시작 부분으로 재설정
- stayopen : 포트 정보 DB를 열어둘 것인지 여부를 나타내는 값
setservent() 함수는 getservent() 함수를 처음 사용하기 전에 호출해야 함
stayopen 값이 0이 아니면 DB가 열린 채로 유지
성공하면 0을 반환
 
 
ㅇ void endservent(void): DB를 닫음
성공하면 0을 반환
 

#include <netdb.h>
#include <stdio.h>

int main() {
    struct servent* port;
    int n;

    setservent(0); // 포트 정보 DB에서 현재 읽기 위치를 시작으로 이동

    // 처음 5개의 포트 정보를 차례로 읽어서 출력
    for (n = 0; n < 5; n++) {
        port = getservent();
        printf("Name=%s, Port=%d\n", port->s_name, port->s_port);
    }

    endservent(); // 포트 정보 DB를 닫음

    return 0;
}
// 실행 결과: /etc/services 파일의 내용과 실행 결과를 비교해보면 포트 번호가 다름
// 이는 servent 구조체에 저장되는 포트 번호의 바이트 순서가 다르기 때문

 
 
포트명을 인자로 받아 DB에서 해당 항목을 검색해 servent 구조체에 저장하고 그 주소를 반환
ㅇ struct servent* getservbyname(const char* name, const char* proto): 서비스명으로 정보 검색
- name : 검색할 포트명
- proto : “tcp” 또는 “udp” 또는 NULL을 지정 (같은 서비스 포트가 TCP 서비스를 위한 번호와 UDP 서비스를 위한 번호로 구분되기 때문)
 
 
포트 번호를 인자로 받아 DB에서 해당 항목을 검색해 servent 구조체에 저장하고 그 주소를 반환
ㅇ struct servent* getservbyport(int port, const char* proto): 포트 번호로 정보 검색
- port : 검색할 포트 번호
- proto : “tcp” 또는 “udp” 또는 NULL을 지정 (같은 서비스 포트가 TCP 서비스를 위한 번호와 UDP 서비스를 위한 번호로 구분되기 때문)
 
 
ㅁ소켓의 종류
1. UNIX 도메인 소켓: 같은 호스트에서 프로세스 사이에 통신할 때 사용
- 패밀리명: AF_UNIX
 
2. 인터넷 소켓: 인터넷을 통해 다른 호스트와 통신할 때 사용
- 패밀리명: AF_INET
 
ㅁ소켓의 통신 방식
• 프로토콜: 소켓을 이용할 때도 하부 프로토콜로 TCP를 사용할 것인지 UDP를 사용할 것인지 지정해야 하는데 이때 정의된 상수를 사용
- SOCK_STREAM : TCP 사용
- SOCK_DGRAM : UDP 사용
 
• 소켓의 네 가지 통신 유형
1. AF_UNIX: SOCK_STREAM
2. AF_UNIX: SOCK_DGRAM
3. AF_INET: SOCK_STREAM
4. AF_INET: SOCK_DGRAM
 
ㅁ소켓 주소 구조체
(UNIX 도메인 소켓의 주소 구조체)

struct sockaddr_un {
    __kernel_sa_family_t sun_family; // AF_UNIX
    char s sun_path[UNIX_PATH_MAX]; // 경로명
};

 
(인터넷 소켓의 주소 구조체)

struct sockaddr_in {
    __kernel_sa_family_t sin_family; // 주소 패밀리명
    __be16 sin_port; // 포트 번호
    struct in_addr sin_addr; // 인터넷 주소
};

struct in_addr {
    __be32 s_addr; // 32비트 주소
};

 
 
바이트 순서 함수
1. 빅 엔디언 방식: 썬 SPARC에서 사용
각각의 바이트를 순서대로 저장
메모리의 낮은 주소에 정수의 첫 바이트를 위치시킴
e.g. 0x1234를 저장할 경우 빅 엔디언은 0x12, 0x34의 순서로 저장 (최상위 바이트 우선)
 
2. 리틀 엔디언 방식: Intel 계열 CPU에서 사용
각각의 바이트를 거꾸로 저장
메모리의 높은 주소에 정수의 첫 바이트를 위치시킴
e.g. 0x1234를 저장할 경우 리틀 엔디언은 0x34, 0x12의 순서로 저장 (최하위 바이트 우선)
 
ㅇ네트워크 바이트 순서(NBO), 호스트 바이트 순서(HBO)
• 컴퓨터마다 바이트를 저장하는 순서가 다르기 때문에 네트워크를 이용한 통신에서는 바이트 순서가 중요한 문제
• 데이터를 보내는 컴퓨터와 받는 컴퓨터의 정수 저장 방식이 다르면 같은 값을 서로 다르게 해석하기 때문
• 따라서 TCP/IP에서는 데이터를 전송할 때 무조건 빅 엔디언을 사용하기로 결정
• 시스템에서 통신을 통해 데이터를 내보낼 때는 HBO에서 NBO로 데이터 순서를 바꿔서 전송하고, 데이터를 받으면 NBO에서 HBO로 변환한 후 처리해야 함
• NBO와 HBO 간에 바이트 순서를 변환해 주는 함수를 사용해 이 작업을 수행할 수 있음
 
 
[바이트 순서 함수] - socket 라이브러리에 정의되어 있음
ㅇ uint32_t htonl(uint32_t hostlong)
- hostlong : 호스트 바이트 순서로 저장된 값
32비트 HBO를 32비트 NBO로 변환
 
ㅇ uint16_t htons(uint16_t hostshort)
- hostshort : 호스트 바이트 순서로 저장된 값
16비트 HBO를 16비트 NBO로 변환
 
ㅇ uint32_t ntohl(uint32_t netlong)
- netlong : 네트워크 바이트 순서로 저장된 값
32비트 NBO를 32비트 HBO로 변환
 
ㅇ uint16_t ntohs(uint16_t netshort)
netshort : 네트워크 바이트 순서로 저장된 값
16비트 NBO를 16비트 HBO로 변환
 

#include <netdb.h>
#include <stdio.h>

int main() {
    struct servent* port;
    int n;

    setservent(0);

    // s_port의 값을 ntohs() 함수로 변환해 출력
    for (n = 0; n < 5; n++) {
        port = getservent();
        printf("Name=%s, Port=%d\n", port->s_name, ntohs(port->s_port));
    }

    endservent();
    
    return 0;
}
// 실행 결과: /etc/services 파일에 정의된 값과 동일함을 알 수 있음

 
 

#include <netdb.h>
#include <stdio.h>

int main() {
    struct servent* port;

    // 포트명을 검색하려면 인자를 서비스명으로 지정
    port = getservbyname("ssh", "tcp");
    printf("Name=%s, Port=%d\n", port->s_name, ntohs(port->s_port)); // ntohs() 함수를 사용해 검색 결과를 출력

    // 포트 번호로 검색하려면 포트 번호를 NBO로 지정해야 함
    // 첫 번째 인자를 htons() 함수를 사용해 포트 번호를 HBO에서 NBO로 변환해 지정
    port = getservbyport(htons(21), "tcp"); 
    printf("Name=%s, Port=%d\n", port->s_name, ntohs(port->s_port)); // ntohs() 함수를 사용해 검색 결과를 출력

    return 0;
}
// 실행 결과: ssh 서비스는 22번 포트를, FTP 서비스는 21번 포트를 사용하고 있음을 알 수 있음

 
 
IP 주소를 문자열로 받아 이를 이진값으로 바꿔서 반환
ㅇ in_addr_t inet_addr(const char* cp): 문자열 IP 주소를 숫자로 변환
- cp : 문자열 형태의 IP 주소
in_addr_t 는 long형
 
 
IP 주소를 in_addr 구조체 형태로 받아 점으로 구분된 문자열로 반환
ㅇ char* inet_ntoa(const struct in_addr in): 구조체 IP 주소를 문자열로 변환
- in : in_addr 구조체 형태의 IP 주소
 

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

int main() {
    in_addr_t addr;
    struct hostent* hp;
    struct in_addr in;

    // 문자열로 된 IP 주소를 이진 형태로 변환
    if ((addr = inet_addr("8.8.8.8")) == (in_addr_t) - 1) {
        printf("Error : inet_addr(8.8.8.8\n");
        exit(1);
    }

    // 정수형으로 변환된 IP 주소를 인자로 지정하고 호스트 정보를 검색
    // 첫 번째 인자는 char* 이므로 형 변환을 수행해 지정
    // 주소는 4바이트 크기고 인터넷 주소이므로 AF_INET를 지정
    hp = gethostbyaddr((char*)&addr, 4, AF_INET);
    if (hp == NULL) {
        printf("Host information not found\n");
        exit(2);
    }

    printf("Name=%s\n", hp->h_name); // 검색된 호스트명을 출력

    // hostent 구조체의 항목으로 리턴된 IP 주소를 출력
    // 이 IP 주소는 이진값으로 hostent 구조체 항목에서 in_addr 구조체로 복사한 후 문자열 형태로 변환해야 함
    // in_addr 구조체에 값을 지정
    memcpy(&in.s_addr, *hp->h_addr_list, sizeof(in.s_addr));
    printf("IP=%s\n", inet_ntoa(in)); // inet_ntoa() 함수로 in_addr 구조체를 인자로 받아 문자열로 변환하고 출력
    
    return 0;
}
// 실행 결과: 호스트명과 IP 주소가 출력

 
 
 
 
 
 
 
 
참고 및 출처: 시스템 프로그래밍 리눅스&유닉스(이종원)