2015년 12월 18일 금요일

C 언어 - 네트워크 프로그래밍 (Socket API)

Socket API


Socket API 를 이용해 네트워크 프로그램을 구현할 수 있다.

설명할 API 종류

  • socket() : socket handler 생성
  • close() : socket handle 해제
  • bind() : IP 및 port 할당
  • listen() : accept 에서 사용될 connection 대기열 설정
  • accept() : connection 대기열에서 connection 을 가져옴
  • connect() : server 로 연결
  • setsockopt() : 다양한 소켓 설정
  • getsockopt() : 소켓 설정 정보를 취득

socket


int socket(int domain, int type, int protocol);

http://man7.org/linux/man-pages/man2/socket.2.html

파라메터:
  • domain : 우선 AF_INET 와 AF_INET6 만 알아두자
  • type : 우선 SOCK_STREAM 과 SOCK_DGRAM 만 알아두자
  • protocol : 우선 IPPROTO_TCP 와 IPPROTO_UDP 만 알아주자

설명:
  • socket handle 을 하나 할당 받는다.
  • socket 옵션들도 많지만 우선 TCP/UDP 와 IPv4/IPv6 만 고려하자.
    domain 에 AF_INET 를 입력하면 IPv4 socket, AF_INET6 를 입력하면 IPv6 socket 이 생성된다.
  • type 에 SOCK_STREAM 을 입력하면 TCP socket, SOCK_DGRAM 을 입력하면 UDP socket 이 생성된다.
  • protocol 에 TCP/UDP 관계없이 0 을 입력하여도 무방하고 IPPROTO_TCP/IPPROTO_UDP 를 TCP/UDP 구분하여 입력하여도 된다.

예) IPv4 TCP socket 생성
int sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sock < 0) {
    // error
}

close


int close(int fd);

http://man7.org/linux/man-pages/man2/close.2.html

파라메터:
  • fd : socket() 으로 생성된 핸들
설명:
  • socket() 함수로 얻은 handle 을 해소

예)
close(sock);

bind


int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

http://man7.org/linux/man-pages/man2/bind.2.html

파라메터:
  • sockfd : socket() 으로 생성된 핸들
  • addr : bind 할 주소 구조체
  • addrlen : 주소 구조체의 크기
설명:
  • socket handle 에 network interface 와 port 를 할당 한다.
  • addr 파라메터에 bind 원하는 IP 주소 및 port 정보를 채운 구조체를 입력한다.
  • struct sockaddr 은 IPv4 와 IPv6 에 따라 구조체가 확장된다.
    • IPv4 는 struct sockaddr_in 구조체를 사용
    • IPv6 는 struct sockaddr_in6 구조체를 사용


예) IPv4 주소에 할당
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(port);

if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) != 0) {
    // error
}

    • INADDR_ANY : 기본 network interface 의 IP 주소 (개발자가 고려할 필요없이 운영체제 기본 network interface 에 할당됨)
    • htonl() : host to network long 이라 풀어 읽을 수 있겠다.
    • htons() : host to network short 이라 풀어 읽을 수 있겠다.

listen


int listen(int sockfd, int backlog); 

http://man7.org/linux/man-pages/man2/listen.2.html

파라메터:
  • sockfd : socket() 으로 생성된 핸들
  • backlog : connection 대기열 크기
설명:
  • accept() 를 하기 위해 client 접속 요청 대기열의 크기를 정한다.

예)
if (listen(sock, 5) != 0) {
    // error
}

accept


int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); 

http://man7.org/linux/man-pages/man2/accept.2.html

  • sockfd : socket() 으로 생성된 핸들
  • addr : client 의 주소가 담길 구조체
  • addrlen : client 주소 구조체의 크기 
설명:
  • client 접속 요청 대기열에서 client 정보를 하나 가져온다.
  • 대기열에 client 접속 요청이 없으면 요청이 있을 때까지 기다린다.

예) IPv4 주소로 bind 한 경우
struct sockaddr_in client_addr;
socklen_t client_addr_len;
int client_sock = accept(server_sock, (struct sockaddr*)&client_addr, &client_addr_len);
if (client_sock < 0) {
    // error
}

    • client_addr 에 client 의 IP 및 port 주소가 담겨있다.

connect


int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 

http://man7.org/linux/man-pages/man2/connect.2.html

파라메터:
  • sockfd : socket() 으로 생성된 핸들
  • addr : 연결하고자 하는 remote 주소 구조체
  • addrlen : remote 주소 구조체 크기 
설명:

  • Server 소켓은 bind(), listen(), accept() 함수의 호출로 client 접속 요청을 대기하는 반면 client 소켓은 connect() 로 대기하고 있는 server 에 접속 한다.


예) IPv4 server 로 접속 요청
struct sockaddr_in remote_addr;
memset(&remote_addr, 0, sizeof(remote_addr));
remote_addr.sin_family = AF_INET;
remote_addr.sin_addr.s_addr = inet_addr("192.168.0.1");
remote_addr.sin_port = htons(port);
if (connect(sock, (struct sockaddr*)&remote_addr, sizeof(remote_addr)) != 0) {
    // error
}


setsockopt


int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen); 

http://man7.org/linux/man-pages/man2/setsockopt.2.html

  • sockfd : socket() 으로 생성된 핸들
  • level : 우선 SOL_SOCKET 만 알아두자 
  • optname : 설정 이름
  • optval : 설정 값
  • optlen : 설정 값 데이터의 크기 (e.g. sizeof(int))
설명:
  • 예제로 제일 많이 사용하는 옵션인 SO_REUSEADDR 을 설명
  • bind() 하려는 port 가 이미 다른 프로세스 등에서 bind() 했다면 해당 프로세스가 port 를 반환하기 전까지는 사용할 수 없다.
  • 때로는 port 반환이 시간이 걸리거나 프로세스가 비정상 종료되어 close() 를 제대로 해주지 못하는 경우가 있다.
  • 이런 경우 socket 에 SO_REUSEADDR 옵션을 주면 해당 port 가 실제로 소유주가 없는 상태라면 할당 받을 수 있도록 한다.
  • 이 외에도 여러 옵션들이 있으며 상황에 따라 구글에서 사용법 및 구동 방식을 참고하면 되겠다.

예)
int reuse = 1;
if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR,  (void*)&reuse, sizeof(reuse)) != 0) {
    // error
}

getsockopt


int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen); 

http://man7.org/linux/man-pages/man2/getsockopt.2.html

파라메터:
  • sockfd : socket() 으로 생성된 핸들
  • level : 우선 SOL_SOCKET 만 알아두자 
  • optname : 설정 이름
  • optval : 설정 값이 담길 데이터
  • optlen : 설정 값 데이터의 크기 (e.g. sizeof(int))
설명:
  • setsockopt() 과 반대로 현재 설정된 값을 가져온다.
  • Socket option 에는 set/get 모두 가능한 option 이 있고 set/get 중 하나만 가능한 option 도 있다.
  • 필요한 경우 구글에서 검색해 보도록 하자.

참고사항


Socket API 의 공통점이라면 socket() 과 accept() 함수를 제외하고 성공시 모두 0 을 return 한다.


Socket API 데이터 타입


  • struct sockaddr : 기본 주소 구조체
  • struct sockaddr_in : IPv4 주소 구조체
  • struct sockaddr_in6 : IPv6 주소 구조체
  • socklen_t : socket API 들이 사용하는 기본적인 크기 표시용 데이터 타입


IPv4 와 IPv6 대응


sockaddr 구조체는 이렇게 생겼다.

struct sockaddr
{
    __SOCKADDR_COMMON (sa_);    /* Common data: address family and length.  */
    char sa_data[14];           /* Address data.  */
};

출처: http://repo-genesis3.cbi.utsa.edu/crossref/ns-sli/usr/include/bits/socket.h.html


sockaddr_in 과 sockaddr_in6 는 이렇게 생겼다.

/* Structure describing an Internet socket address.  */
struct sockaddr_in
{
    __SOCKADDR_COMMON (sin_);
    in_port_t sin_port;                 /* Port number.  */
    struct in_addr sin_addr;            /* Internet address.  */

    /* Pad to size of `struct sockaddr'.  */
    unsigned char sin_zero[sizeof (struct sockaddr) -
                           __SOCKADDR_COMMON_SIZE -
                           sizeof (in_port_t) -
                           sizeof (struct in_addr)];
};

/* Ditto, for IPv6.  */
struct sockaddr_in6
{
    __SOCKADDR_COMMON (sin6_);
    in_port_t sin6_port;        /* Transport layer port # */
    uint32_t sin6_flowinfo;     /* IPv6 flow information */
    struct in6_addr sin6_addr;  /* IPv6 address */
    uint32_t sin6_scope_id;     /* IPv6 scope-id */
};

출처: http://repo-genesis3.cbi.utsa.edu/crossref/ns-sli/usr/include/netinet/in.h.html

모두 __SOCKADDR_COMMON 라는 macro 로 시작한다.

__SOCKADDR_COMMON macro 는 이렇게 생겼다.


#define __SOCKADDR_COMMON(sa_prefix) \
    sa_family_t sa_prefix##family

http://repo-genesis3.cbi.utsa.edu/crossref/ns-sli/usr/include/netinet/in.h.html


sockaddr_in 와 sockaddr_in6 구조체의 처음 인자가 동일하게 sa_family_t 형태의 변수다.
family 멤버 변수는 AF_INET 또는 AF_INET6 가 입력되었던 부분이다.
sockaddr_in 또는 sockaddr_in6 의 첫번째 인자인 family 값이 AF_INET 또는 AF_INET6 인지 확인 후 type casting 을 이용해 다른 처리를 할 수 있다.

이후 예제를 통해 다시 살펴 보겠다.

Cross Platform


위 API 들은 Winsock API 에도 동일하게 존재함

Winsock API 에 다른 점

  • socket 변수 타입 : int 대신 SOCKET 을 사용
  • Socket API 에러 리턴 값 : socket() 이나 accept() 에러 리턴 값은 INVALID_SOCKET, 이외 API 함수들은 SOCKET_ERROR 를 리턴함

댓글 없음:

댓글 쓰기