리눅스 혹은 유닉스 계열의 시스템에서 프로세스가 파일을 다룰 때 사용하는 것으로, 운영체제가 특정 파일에 할당해주는 정수값 이다.
유닉스 시스템에서는 일반적인 파일부터 디렉토리, 소켓, 파이프, 블록 디바이스 등 모든 객체들을 파일로 관리하는데, 유닉스 시스템에서 프로세스가 이 파일들을 접근할 때 파일 디스크럽터를 이용한다. 디스크럽터는 네트워크 프로그래밍을 할 때 클라이언트와 서버의 데이터 교환도 용이하게 해 준다.
/proc/<pid>/fd
파일 디스크럽터 확인 예시
우리가 자주 사용하는 표준 입출력에도 파일 디스크럽터 값이 할당 되어 있다.
0 - 2까지는 이미 파일 디스크립터 값이 할당되어 있어서 우리가 사용 할 수 있는 건 3 이상부터다.
open()함수를 이용하면 현재 사용 가능한 디스크립터 값을 할당해준다.
flags 인자는 프로세스가 파일을 어떻게 접근 할지 명시할 수 있다.
O_RDONLY
: Read Only 읽기O_WRONLY
: Write Only 쓰기O_RDWR
: Read and Write 읽기 & 쓰기flags 인자에 쓰기 작업을 위한 추가적인 명령을 제공하도록 한 개 이상의 비트마스크들을 OR 형태로 작성할 수도 있다.
O_CREAT.
: 만약 파일이 없다면 빈 파일을 만든다.O_TRUNC.
: 만약 파일이 이미 있다면 파일 내용을 다 지우고 다시 쓴다.O_APPEND.
: 쓰기가 일어날 때 마다 파일 포인터가 파일 끝에 위치하게 한다. (이어쓴다) mode 인자는 새 파일들의 접근 권한 비트들을 명시한다. (sys/stat.h에 정의되어있다)
리눅스 커널의 관점에서 보면, 소켓은 네트워크 상의 두 프로그램 간 양방향 통신을 위한 엔드포인트다. 그리고 UNIX 프로그램의 관점에서 본다면 소켓은 해당 식별자를 가지는 열린 파일이다. 애플리케이션의 입장에서 소켓은 네트워크에서 파일을 읽어오고/쓰게 해 주는 File Descriptor이다.
소켓 인터페이스는 네트워크 애플리케이션을 설계하기 위해 UNIX 입출력 함수와 연결하는데 쓰이는 함수의 집합이다(set of functions that are used in conjunction with the UNIX I/O functions to build network applications). 클라이언트에서 TCP/IP로 넘어가기 전에 시스템 콜을 하는 영역으로, 클라이언트에서 연결을 요청하면 클라이언트 소켓 주소에 들어가는 포트는 커널에서 자동으로 배정한다.
그림 : 소켓 인터페이스 기반 네트워크 응용프로그램의 개요
소켓 인터페이스를 기반으로 하는 클라이언트-서버 트랜잭션을 나타낸 그림이다. 위 그림에 있는 함수들을 살펴보면 다음과 같다.
클라이언트 / 서버가 소켓 판별자(socket descriptor)를 만들기 위해 사용하는 함수다.
clientfd = socket(AF_INET, SOCK_STREAM, 0);
AF_INET
: 네트워크 주소 체계를 알려준다. 'AF_INET'은 32비트 주소를 사용한다는 뜻이다(IPv4)type
: 소켓이 어떤 타입(TCP/UDP 등)인지 알려준다. 'SOCK_STREAM'은 TCP 프로토콜을 사용한 통신 소켓이라는 뜻이다.protocol
: 프로토콜 정보; 앞의 두개 인자를 이용해 프로토콜을 특정할 수 없을 때에만 따로 명시해준다. 그 이외에는 예시에서 처럼 0으로 처리해줘도 된다.socket() 함수로 socket descriptor를 할당받고 소켓 유형을 지정한 후, IP주소와 포트 번호를 할당하기 위해 위에 소켓 구조체를 정의해야한다.
/* Generic socket address structure (for connect, bind, and accept) */
struct sockaddr
{
uint16_t sa_family; /* Protocol family */
char sa_data[14]; /* Address data */
};
이 소켓의 종류가 무엇인지 담고있는, 말 그대로 일반적인 형태의 구조체다. IPv4는 sockaddr_in 이라는 전용 구조체를 따로 가지고있다.
/* IP socket address structure */
struct sockaddr_in {
uint16_t sin_family; /* Protocol family (always AF_INET) */
uint16_t sin_port; /* Port number in network byte order */
struct in_addr sin_addr; /* IP address in network byte order */
unsigned char sin_zero[8]; /* Pad to sizeof(struct sockaddr) */
};
sin_family
: AF_INET(32비트 IPv4 주소를 뜻한다) 이 구조체는 IPv4 전용 구조체이므로 항상 AF_INET이다.sin_port
: 16비트 포트 번호sin_addr
: 32비트 IP주소- IP주소와 포트 번호는 항상 네트워크 바이트 주소 (빅 엔디안)으로 저장된다.
- _in은 internet에 대한 축약이며, input과는 연관이 없다
socket() 함수는 자신이 클라이언트든 서버든 상관없이 항상 자신이 클라이언트라고 가정 하고 그에 따라 소켓을 생성한다. 그래서 서버 측에서는 이를 서버 역할로 바꾸기 위해 시스템을 통해 아래 세 가지 함수 bind(), listen(), accept()를 호출한다.
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
// Returns 0 if OK, -1 on error
bind 함수는 커널에게 addr에 있는 서버의 소켓 주소를 소켓 식별자 socketfd 와 연결 해달라고 한 후, 성공시 0, 에러시 -1을 리턴한다.
sockfd
: socket() 함수를 통해 배정받은 디스크립터 번호*addr
: IP주소와 PORT 번호를 지정한 sockaddr 구조체를 가리키는 포인터addrlen
: 주소 정보를 담은 변수의 길이서버는 커널에게 해당 디스크립터가 클라이언트가 아닌 서버에 의해 사용 될 것이라고 명시해 주기위해 listen()을 호출한다.
#include <sys/socket.h>
int listen(int sockfd, int backlog);
// Returns: 0 if OK, −1 on error
listen() 함수는 활성화된 소켓의 sockfd를 listening socket (클라이언트에게 연결 요청을 받을 수 있는 소켓)으로 바꿔준다. backlog는 연결 요청 시 대기 할 수 있는 큐(대기열)의 크기를 지정해 주는 인자다.
sockfd
: 소켓 디스크립터 번호backlog
: 연결 요청을 대기하는 큐의 크기#include <sys/socket.h>
int accept(int listenfd, struct sockaddr *addr, int *addrlen);
// Returns: nonnegative connected descriptor if OK, -1 on error
마지막으로 대기 중인 클라이언트의 요청을 accept하면서 데이터를 주고받을 수 있게 된다.
listenfd
: 리스닝 소켓의 디스크립터*addr
: 대기 큐를 참조해 얻은 클라이언트의 주소 정보addrlen
: addr변수의 크기듣기 식별자와 연결 식별자(listendfd, connectedfd)
- listen desc.와 connected desc.는 소켓이 서버와 연결되었는지 아닌지 여부를 나타내며, 소켓의 상태를 구분한다.
- listen desc. 는 클라이언트의 접속을 기다리는 상태의 네트워크 소켓. 클라이언트 연결 요청에 대한 끝점으로 한번 생성되며 서버가 살아있는 동안 계속 존재한다.
- connected desc. 는 이미 서버와 연결되어 데이터를 주고받는 상태의 네트워크 소켓. 클라이언트와 서버 사이에 성립된 연결의 끝점으로, 서버가 연결 요청을 수락할 때 마다 생성되며, 서버가 클라이언트에 서비스하는 동안에만 존재한다.
- 이 두 디스크립터를 구분함으로써 많은 클라이언트 연결을 동시에 처리 할 수 있는 동시성 서버 를 만들 수 있다.
bind(), listen(), accept() 세 가지 함수를 순차적으로 호출함으로써 서버 측에서 데이터 송수신 준비를 마칠 수 있다. 최종적으로 클라이언트 측에서 connect() 함수를 통해 연결한 뒤, write() 함수로 실제 데이터를 출력한다. 데이터 송수신이 완료 되면, close() 시스템 콜을 통해 소켓의 자원을 모두 제거 해 준다.
서버에서는 socket() 함수로 소켓 디스크립터를 할당 받은 뒤 bind(), listen(), accept() 함수를 순차적으로 호출해 클라이언트와 연결 할 수 있었다. 클라이언트에서는 대기중인 서버에 연결 요청(connect)을 통해 클라이언트-서버 간의 연결을 생성 할 수 있다.
서버에게 연결 요청을 보낸다
#include <sys/socket.h>
int connect(int clientfd, const struct sockaddr *addr,
socklen_t addrlen);
// Returns: 0 if OK, −1 on error
clientfd
: 클라이언트의 소켓 디스크립터 번호*addr
: 연결 요청할 서버의 주소 구조체addrlen
: 서버 주소 구조체 변수의 길이TCP의 3 Way Handshake
클라이언트에서 connect()를 호출하면 listen() - 대기중이던 서버에 연결이 요청되고 서버는 accept() 함수를 불러 서버와 클라이언트 간의 연결이 생긴다.
리눅스의 getaddrinfo
와 getnameinfo
함수
getaddrinfo
: 호스트 이름, 호스트 주소, 서비스 이름, 포트번호의 스트링 표시를 소켓 주소 구조체로 변환
getnameinfo
: getaddrinfo의 반대. 소켓 주소 구조체를 대응되는 호스트와 서비스이름 스트링으로 변환.
클라이언트와 서버가 이용할 수 있는 상위수준의 도움함수
open_clientfd
,open_listenfd
open_clientfd
: 클라이언트는 open_clientfd를 호출해 서버와 연결을 설정함open_listenfd
: 서버는 open_listenfd 함수를 호출해 연결 요청을 받을 준비가 된 듣기 식별자 생성