[BSD Socket] FreeBSD(+*nix) 버전 소켓함수 정리

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_portsin_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);

listenaccept는 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으로 처리된다.