Network Programming : Soket - BeAPro
Soket이란?
소켓은 프로세스가 네트워크로 데이터를 주고 받는 창구 역할을 하며 Unix, Window, Max 등 대부분의 현대 시스템에 내제되어있다.
리눅스 커널의 관점에서 본다면 소켓은 통신을 위한 끝점(endpoint)이다.
Endpoint는 아이피 주소와 포트 번호의 조합으로 구성되어져 있다. 소켓은 endpoint를 통해 유일하게 식별되어질 수 있다.
두 소켓이 연결되면 서로 다른 프로세스끼리 데이터를 주고 받을 수 있다.
Soket Address Structures
/* 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) */
};
/* Generic socket address structure (for connect, bind, and accept) */
struct sockaddr {
uint16_t sa_family; /* Protocol family */
char sa_data[14]; /* Address data */
};
위는 소켓 주소의 구조체이다.
먼저 typedef struct sockaddr SA;를 선언하여 타입을 정의한다.
그 다음 sockaddr_in 구조체는 선언될 때마다 sockaddr 구조체로 부터 상속받는다.
인터넷 소켓 주소는 sockaddr_in 타입의 16바이트 구조체(sin_addr)에 저장되는 것을 볼 수 있다.
sin_family의 값은 항상 AF_INET여야 하는데 이는 우리가 32bit IP주소를 사용하고 있다는 것을 나타낸다.
sin_port는 16비트 포트 번호이다.
sin_addr은 32bit IP주소이다.
IP주소와 포트 번호는 항상 빅 에디안 순서로 저장된다.
soket Function
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
소켓을 생성하고 싶다면 soket함수를 다음과 같이 이용해야 한다.
clientfd = Socket(AF_INET, SOCK_STREAM, 0);
전에 명시하였듯이 AF_INET은 32bit IP 주소를 사용하고 있다는 것을 나타낸다.
SOCK_STREAM은 소켓이 인터넨 연결의 끝점이 될 것이라는 것을 나타낸다.
getaddrinfo함수를 이용하여 한다면 Protocol Independent 한 소켓을 생성할 수 있다.
위 함수를 이용하여 생성된 소켓을 부분만 만들어졌고 아직 읽거나 쓸 수 없다.
connect Function
#include <sys/socket.h>
int connect(int clientfd, const struct sockaddr *addr, socklen_t addrlen);
connect 함수는 소켓 주소 addr의 서버와 인터넷 연결을 시도한다.
addrlen은 sizeof(sockaddr_in)이 된다. connect 함수는 연결이 성공할 때까지 block되어 있거나 에러가 발생한다. 성공하였다면 clientfd는 이제 읽거나 쓸 준비가 되며 다음과 같이 표현할 수 있다.
(x:y, addr.sin_addr:addr.sin_port)
x는 클라이언트의 IP주소이고 y는 host가 클라이언트의 프로세스를 구별할 수 있게 해주는 단기 포트 넘버이다.
soket함수에서와 마찬가지로 getaddrinfo를 사용하여 connect함수에 인자를 제공하는 것이 가장 좋다.
bind Function
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
bind함수는 커널에게 addr에 있는 서버의 소켓 주소를 sockfd와 연결 할 수 있는지를 확인한다.
addrlen 인자는 sizeof(sockaddr_in)이다. soket과 connect에서 마찬가지로 getaddrinfo를 사용하여 bind에게 인자를 제공하는 것이 좋다.
listen Function
#include <sys/socket.h>
int listen(int sockfd, int backlog);
서버는 listen함수를 호출해서 descriptor가 클라이언트 대신 사용될 것을 알려준다.
listen함수는 sockfd를 듣기 소켓으로 변환하며, 듣기 소켓은 클라이언트로부터 연결 요청을 승락할 수 있다.
backlog 인자는 커널이 요청들을 거절하기 전에 큐에 저장해야 하는 연결 수에 대한 정보를 제공한다.
accept Function
#include <sys/socket.h>
int accept(int listenfd, struct sockaddr *addr, int *addrlen);
서버는 accept 함수를 호출하여 클라이언트로부터 연결요청을 기다린다.
accept 함수는 클라이언트로부터 연결 요청이 listenfd에 도달하기를 기다린다.
요청이 왔다면 addr에 클라이언트의 소켓 주소를 넣어주어 클라이언트와 통신할 수 있는 connfd를 찾아 리턴한다.
listenfd와 connfd는 많이 헷갈리는 개념이다.
listenfd는 클라이언트 연결 요청에 대해 끝점으로서 역할을 하며 한 번 생성되면 서버가 살아있는 동안 계속 존재한다.
connfd는 클라이언트와 서버 사이에 연결이 이루어졌을 경우 끝점이다. 서버가 연결 요청을 수락할 때마다 생성되며 서버가 클라이언트에 서비스 하는 동안에만 존재한다.
위를 통해 listenfd와 connfd에 차이를 명확하게 알 수 있다.
1단계 : 서버는 accept를 호출하여 listenfd(3)에 연결 요청이 오기를 기다린다. (fd 0~2는 표준 파일들을 위해 배정되어 있다)
2단계 : 클라이언트는 connect 함수를 호출하여 listenfd에 요청을 보낸다.
3단계 : accept 함수는 새로운 연결 식별자 connfd(4)를 열고 clientfd와 connfd 사이를 연결시킨다.
Host and Service Conversion
리눅스는 getaddrinfo와 getnameinfo함수를 이용하여 소켓 주소 구조체를 호스트이름, 호스트주소, 서비스이름, 포트번호로 혹은 그 역으로 변환시켜준다.
이 함수들은 소켓을 특정 IP 프로토콜의 버전에 의존하지 않도록, 즉 Protocol Independent하도록 프로그래밍 해준다.
getaddrinfo function
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
int getaddrinfo(const char *host, const char *service,const struct addrinfo *hints,
struct addrinfo **result);
void freeaddrinfo(struct addrinfo *result);
const char *gai_strerror(int errcode);
getaddrinfo 함수는 호스트이름, 호스트주소, 서비스이름, 포트번호를 소켓 주소 구조체로 변환한다.
host와 service가 주어지면 getaddrinfo는 소켓 주소 구조체를 가리키는 addrinfo 구조체의 연결리스트를 가리키는 포인터 result를 반환한다.
클라이언트는 getaddrinfo를 호출 후에 리스트를 방문한다. soket 함수와 connect함수를 호출하면서 연결이 성립할 때까지 리스트를 탐색한다.
서버는 리스트를 방문할 때마다 soket과 bind 함수를 호출하면서 연결이 성립할 때까지 탐색을 진행한다.
freeaddrinfo 함수를 호출하여 메모리 누수를 방지해야 한다. 만약 getaddrinfo 함수가 에러코드를 리턴하면 gai_strerror를 호출한다.
getaddr을 사용하여 addrinfo 구조체를 생성하면 ai_flags를 제외한 모든 필드를 채워준다.
getnameinfo function
#include <sys/socket.h>
#include <netdb.h>
int getnameinfo(const struct sockaddr *sa, socklen_t salen, char *host, size_t hostlen,
char *service, size_t servlen, int flags);
getnameinfo함수는 소켓 주소 구조체를 호스트와 서비스이름으로 바꿔준다.
sa는 길이 salen 바이트의 소켓 주소 구조체를 가리키고, host는 hostlen 바이트 길이의 버퍼, service는 길이 servlen 바이트의 버퍼를 가리킨다. sa를 대응되는 호스트와 서비스이름으로 바꾸고 이를 host와 service에 복사한다.
getnameinfo함수 또한 에러 발생시 gai_strerror 함수를 호출한다.