BSD Socket 사용법 정리
FreeBSD(+*nix) 버전 클라이언트측 코드
본 포스팅에서는 FreeBSD를 포함하여 *nix를 기준으로 Socket 함수들을 정리한다.
1. socket
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
socket 객체(enpoint)를 생성하는 함수이다. 생성에 성공하면 file descriptor를 반환한다. 그러나 오류가 발생하면 -1
를 반환하고, 자세한 사유는 errno
를 통해 확인할 수 있다.
parameter: domain
sys/socket.h
에 정의된 여러개의 상수들 중 하나를 선택할 수 있다. 이번 예제에서 쓰인 상수는 IPC를 구현할 때 사용된 AF_UNIX
및 TCP/UDP 통신에 사용된 AF_INET
이다. 참고로 AF_LOCAL
도 있는데 이는 AF_UNIX
와 같은 값이다. IPv6 기반의 인터넷 통신에 대해서는 AF_INET6
라는 별도의 상수가 선언되어 있는데 이를 활용한 IPv6 Socket 통신은 추후에 확인한다.
parameter: type
이번 예제에서 쓰인 상수는 TCP 및 IPC 통신에 쓰이는 상수인 SOCK_STREAM
, UDP 통신에 쓰이는 상수인 SOCK_DGRAM
이다. 패킷의 내용을 직접 다룰 수 있는 상수인 SOCK_RAW
도 있는데 이는 보안을 위해 운영체제에 따라서 지원을 안 할 수도 있다.
parameter: protocol
socket
함수가 반환하는 file descripter도 일종의 파일처럼 취급되는데, 이 파일에 적용될 옵션을 설정할 수 있다. 이번 예제에서는 0
을 설정하였다. 즉 특수한 옵션은 부여하지 않았다.
정리하면 다음과 같이 정형화시킬 수 있다.
TCP (IPv4): int fd = socket(AF_INET, SOCK_STREAM, 0);
TCP (IPv6): int fd = socket(AF_INET6, SOCK_STREAM, 0);
UDP (IPv4): int fd = socket(AF_INET, SOCK_DGRAM, 0);
UDP (IPv6): int fd = socket(AF_INET6, SOCK_DGRAM, 0);
IPC: int fd = socket(AF_UNIX, SOCK_STREAM, 0);
2. bind와 connect
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
TCP, IPC와 같이 송신과 수신을 확실히 확인하는 프로토콜에서는 서버 측에서 호출하는 bind
와 클라이언트 측에서 호출하는 connect
를 사용한다. 두 함수는 모두 접속할 주소가 적혀있는 inaddr_XX
구조체를 필요로 한다.
parameter: sockfd
앞의 socket
함수로 얻은 file descriptor를 전달한다.
parameter: addr
sockaddr_in
는 인터넷으로 접속하려는 주소를 구성하고 전달하는 구조체이다. sockaddr_un
은 IPC로 접속할 때 그 위치를 구성하고 전달하는 구조체이다. 그 외에도 다른 구조체들이 사용될 수 있으나, 이들 구조체의 기본 형태는 모두 공통적으로 아래의 sockaddr
구조체의 변형이다.
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
}
sa_family
는 주소의 종류를 결정한다. AF_INET
, AF_INET6
, AF_UNIX
, AF_LOCAL
등을 지정할 수 있다. 상수의 종류에 따라 sa_addr[14]
에 들어갈 주소의 구조는 달라진다.
TCP, UDP 등의 인터넷(IPv4) 기반 통신은 다음과 같이 sin_port
와 sin_addr
로 세분화된다.
struct sockaddr_in {
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
};
struct in_addr {
uint32_t s_addr;
};
sin_port
는 포트 번호이고, sin_addr
는 IPv4 주소이며, IPv4는 in_addr
이라는 별도의 구조체 내의 32비트 정수로 전달된다. 두 변수의 endian은 모두 Big Endian이다. sin_addr.s_addr
의 몇몇 특수한 IP 주소는 다음과 같은 상수로 정의되어 있다.
INADDR_LOOPBACK
127.0.0.1
이다.INADDR_ANY
0.0.0.0
이며 아무 단말기로부터 연결을 수용할 때 사용된다.INADDR_BROADCAST
255.255.255.255
이며 모든 단말기로 브로드캐스트할 때 사용된다.
IPC 기반의 통신은 다음과 같이 sun_path
로 분화된다.
struct sockaddr_un {
sa_family_t sun_family;
char sun_path[108];
};
여기서 sun_path
의 길이는 POSIX 표준에서는 정의되지 않았다. 꼭 108문자가 아니어도 되고, 구현에 따라 다른 크기로 선언될 수도 있다. 문자열의 실제 길이가 얼마가 되었든, 항상 NULL
문자로 끝나야 하며 런타임 시 동적으로 문자열 길이를 측정한다.
parameter: addrlen
addr
로 지정한 구조체의 길이를 전달한다.
예를 들어, TCP, UDP에 기반한 통신을 하고자 할 때 IP 주소와 포트 번호를 구조체에 다음과 같이 정형화할 수 있다. IPv4 주소를 Big Endian 정수로 변환할 때 inet_pton
함수를 사용하고, 포트 번호를 나타내는 시스템 정수를 Big Endian 정수로 변환할 때는 htons
를 사용한다. 구조체의 내용을 전부 0x00
으로 리셋하기 위하여 bzero
함수를 사용한다.
struct sockaddr_in addr_server;
bzero(&addr_server, sizeof(struct sockaddr_in)); // #include <strings.h>
addr_server.sin_family = AF_INET;
addr_server.sin_port = htons(포트번호); // #include <arpa/inet.h>
switch (inet_pton(AF_INET, "000.000.000.000", &addr_server.sin_addr)) { // #include <arpa/inet.h>
case 1:
// 정상인 경우
break;
case 0:
// IP 주소가 잘못된 경우
break;
case -1:
// AF_ 상수가 잘못 지정된 경우
break;
}
IPC에 기반한 통신을 하고자 할 때 연결 주소를 구조체에 다음과 같이 정형화할 수 있다.
struct sockaddr_un addr_server;
bzero(&addr_server, sizeof(struct sockaddr_un)); // #include <strings.h>
addr_server.sun_family = AF_UNIX;
strcpy(addr_server.sun_path, "주소");
3. listen과 accept
#include <sys/types.h>
#include <sys/socket.h>
int listen(int sockfd, int backlog);
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
listen
과 accept
는 TCP와 IPC 서버 프로그램과 같이 상대방의 연결 여부를 확인하는 프로토콜로 소켓을 사용할 때 서버가 클라이언트를 기다렸다가 연결을 허용하는 함수이다. listen
은 연결 대기열에 클라이언트가 추가되기를 기다리는 함수이고, accept
는 연결 대기열에서 클라이언트를 하나 꺼내와서 통신을 시작하는 함수이다. 사용 예는 다음과 같이 정형화할 수 있다.
struct sockaddr addr_client;
socklen_t addr_client_len;
if (listen(fd, SOMAXCONN) < 0) {
// 오류 처리
}
while (1) {
if (accept(fd, &addr_client, &addr_client_len) < 0) {
// 오류 처리
}
// 클라이언트와 통신할 내용
}
parameter: sockfd
socket
함수로 얻은 파일 디스크립터이다.
parameter: backlog
클라이언트 대기열의 최대 크기이다. 시스템에서 기본으로 제공되는 상수로는 SOMAXCONN
이 있다.
parameter: addr, addrlen
클라이언트 측 주소를 담게 될 구조체 및 그 구조체의 길이이다.
3. read, write와 recv, send 및 recvfrom, sendto
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
모두 클라이언트와 무언가를 주고 받는데 사용되는 함수이다. 여기서 read
/write
/recv
/send
는 TCP 및 UDP와 같이 송수신 여부를 확인하는 방식의 프로토콜로 통신할 때 사용하는 함수이고, recvfrom
/sendto
는 UDP와 같이 송수신여부를 확인하지 않고 일방적으로 전달하는 방식의 프로토콜로 통신할 때 사용하는 함수이다.
특히 read
/write
는 소켓함수라기 보다는 운영체제의 입출력 함수인데, socket
에서 반환한 객체가 파일 디스크립터이기 때문에 이들 함수의 사용이 가능하다. 이 때는 내부적으로 recv
, send
가 호출되며 flags = 0
으로 처리된다.