[RTMP] 서버와 클라이언트의 소켓 실행 과정

4 minute read

소켓 프로그래밍 시작하기

서버 소켓의 실행 과정은 크게 6단계로 진행됩니다.

단계 목적 코드
1 소켓 생성 socket()
2 IP주소와 PORT번호 할당 bind()
3 연결요청 가능 상태로 변경 listen()
4 연결요청에 대한 수락 accept()
5 데이터 송수신 read(), write()
6 연결 종료 close()

반면에 클라이언트 소켓의 실행 과정은 크게 4단계로 진행됩니다.

단계 목적 코드
1 소켓 생성 socket()
3 연결요청 connect()
5 데이터 송수신 reac(), write()
6 연결 종료 close()

클라이언트 소켓의 단계를 1,3,5,6으로 표기한 이유는 후술하겠습니다.

소켓 생성, socket()

#include <sys/socket.h>
/**
* @param domain 소켓이 사용할 프로토콜 체계 정보 전달
*   - PF_INET : IPv4 프로토콜
*   - PF_INET6 : IPv6 프로토콜
* @param type 소켓의 데이터 전송방식에 대한 정보 전달
*   - SOCK_STREAM : 연결지향형 소켓
*   - SOCK_DGRAM : 비 연결지향형 소켓
* @param protocol 통신에 사용되는 프로토콜 정보 전달
*   - domain에서 지정한 범위 내에서 protocol이 지정된다.
*/
int socket(int domain, int type, int protocol);

//IPv4 인터넷 프로토콜 체계에서 동작하는 연결지향형 데이터 전송 소켓
int tcp_socket=socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
int tcp_socket=socket(PF_INET, SOCK_STREAM);
int tcp_socket=socket(PF_INET, SOCK_STREAM, 0);

IPv4 체계에서는 연결지향형 데이터 전송 프로토콜이 TCP만 존재합니다. 마지막 인자를 다양한 방법으로 전달해도 됩니다.

tcp_server는 char형 메시지를 write() 1번에 전송했고, tcp_client는 read()를 1바이트씩 13번에 나누어서 전송받았습니다. 이와 같은 특징은 뒤에서 RTMP 패킷을 주고 받을 때 중요하게 사용됩니다.

주소와 PORT번호 할당, bind()

소켓을 생성한 뒤에는 사용하는 주소체계, IP주소, PORT 번호를 지정해서 소켓에 알려주어야 합니다. 어떤 경로로 들어온는 정보를 수신할지 알려주는 과정입니다.

예전에는 sockaddr를 사용해서 IP주소 정보와 PORT정보를 sa_data에 넣는 과정이 불편했습니다.

struct sockaddr{
  sa_family_t    sin_family;  //주소체계
  char           sa_data[14]; //나머지 정보
}

그래서 최근에는 sockaddr_in을 정의해서 좀 더 편하게 IP주소와 PORT 번호를 전달합니다.

struct sockaddr_in{
  sa_family_t    sin_family;  //주소체계
  uint16_t       sin_port;    //16비트 TCP PORT번호. 네트워크 바이트 순서
  struct in_addr sin_addr;    //32비트 IP주소. 네트워크 바이트 순서
  char           sin_zero[8]; //sockaddr_in의 크기를 socketaddr과 일치시키기 위함
}

struct in_addr{
  in_addr_t      s_addr;       //32비트 IPv4 인터넷 주소
}

struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family=AF_INET;
//host to network short(uint16_t, 2byte 데이터를 변환해라)
serv_addr.sin_port=htons(atoi(argv[1]));     
//host to network long(uint32_t, 4byte 데이터를 변환해라)
//INADDR_ANY : 127.0.0.1 (localhost)
serv_addr.sin_addr.s_addr=htonl(INADDR_ANY); 

if(bind(serv_sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr))==-1)
  error_handling("bind() error"); 

결국 들어가야하는 데이터 타입은 sockaddr이기 때문에 sockaddr_in 타입으로 정보를 저장하고, socketaddr타입으로 형변환을 해서 전달합니다.

바이트 순서와 네트워크 바이트 순서

- 빅 엔디안 : 상위 바이트의 값을 작은 번지수에 저장
  - 정수 0x12345678
  - 메모리 주소 : 0x20 0x21 0x22 0x23
  - 저장된 정수 : 0x12 0x34 0x56 0x78
- 리틀 엔디안 : 상위 바이트의 값을 큰 번지수에 저장
  - 정수 0x12345678
  - 메모리 주소 : 0x20 0x21 0x22 0x23
  - 저장된 정수 : 0x78 0x56 0x34 0x12

인텔 CPU는 리틀 엔디안 형식을 사용합니다. 네트워크를 전송하 때는 빅 엔디안으로 전송해야하는 규칙이 있기 때문에 리틀 엔디안 형식의 데이터를 빅 엔디안 형식의 데이터로 변환해서 전송해야 합니다.

바이트 순서는 소켓을 bind()할 때에만 명시적으로 바꾸어줍니다. 실제 데이터를 송수신하는 과정에서는 내부적으로 처리를 해주기 때문에 신경쓰지 않아도 됩니다.

localhost가 아닌 다른 주소와의 연결을 할 때에는 inet_aton()를 사용하면 됩니다.

#include <arpa/inet.h>
struct sockaddr_in serv_addr;
char *addr="1.2.3.4";

//위에서 INADDR_ANY을 사용한 방법
// serv_addr.sin_addr.s_addr=htonl(INADDR_ANY); 

//dotted nation의 주소를 32비트 정수형으로 변환
// unsigned long conv_addr = inet_addr(addr);

//inet_addr() 이후 대입하는 과정을 한 번에 수행해줌
if(inet_aton(addr, &addr_inet.sin_addr))
  error_handling("Conversion Error");

지금까지의 내용을 종합해보면 아래와 같이 동작합니다.

int serv_sock;
struct sockaddr_in serv_addr;
char *serv_ip="1.2.3.4";
char *serv_port="9190";
serv_sock=socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family=AF_INET;
serv_addr.sin_addr.s_addr=inet_addr(addr);
serv_addr.sin_port=htons(atoi(serv_port));
if(bind(serv_sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr))==-1 )
  error_handling("bind() error"); 

터미널에서 IP주소와 port정보를 전달받아서 사용하기 위해서는 아래와 같이 작성합니다.

int sock;
struct sockaddr_in serv_addr;

if(argc!=3){
  printf("Usage : %s <IP> <port>\n", argv[0]);
  exit(1);
}

sock=socket(PF_INET, SOCK_STREAM, 0);
if(sock == -1)
  error_handling("socket() error");

memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family=AF_INET;
serv_addr.sin_addr.s_addr=inet_addr(argv[1]);
serv_addr.sin_port=htons(atoi(argv[2]));

서버에 “127.0.0.1”이 아닌 다른 주소를 전달하는 경우는 컴퓨터에 두 개 이상의 IP를 할당 받아서 사용하는 경우입니다. 서버에 특정 주소와 포트로 들어오는 정보만을 받기 위해서는 특정 주소를 전달하면 됩니다. 주소에 INADDR_ANY를 적으면 컴퓨터에 할당된 IP주소와 상관없이 port번호만 일치하면 데이터를 수신할 수 있습니다.

연결요청 가능 상태로 변경, listen()

listen() 함수를 호출하면 연결요청 대기상태로 들어갑니다. listen()이 호출되어야 클라이언트가 연결 요청을 할 수 있게됩니다. 이 때문에 채팅 서버와 채팅 클라이언트를 실행할 때, 항상 서버 먼저 실행해야합니다.

#include <sys/socekt.h>

int listen(int sock, int backlog);

if(listen(serv_sock, 5)==-1)
		error_handling("listen() error");

서버 소켓을 첫 번째 인자로 전달하고, 두 번째 인자로는 연결요청 대기 큐의 사이즈를 전달합니다. 두 번재 인자의 값은 서버의 성격마다 다르지만, 웹 서버와 같이 잦은 연결요청을 받는 서버의 경우 최소 15 이상을 전달해야한다. 이 크기는 실험적 결과에 의존해서 결정된다.

연결요청에 대한 수락, accept()

#include <sys/socket.h>

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

struct sockaddr_in clnt_addr;
clnt_addr_size=sizeof(clnt_addr);  
clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_addr,&clnt_addr_size);
if(clnt_sock==-1)
  error_handling("accept() error");  

연결요청 대기 큐에서 대기중인 클라이언트의 연결요청을 수락하는 기능을 수행합니다. accept()함수를 호출할 때 전달하는 서버 소켓과 별개로, accept()에 의해서 return되는 소켓fd가 별도로 존재합니다. 이 소켓은 내부적으로 만들어진 소켓으로, 연결 요청을 한 클라이언트 소켓이랑 연결까지 이루어집니다.

정리하자면, accept()함수에 인자로 전달하는 serv_sock은 연결요청을 받아서 연결요청 대기 큐에 전달을하는 역할, clnt_sock은 클라이언트와 연결되어 데이터를 주고 받는 역할을 수행합니다.

클라이언트의 연결 요청, connect()

클라이언트 소켓은 socket()으로 생성한 이후 bind()의 과정 없이 바로 connect()를 진행합니다. 서버 소켓의 bind()는 어떤 주소와 포트로 들어오는 데이터 정보를 받을지 결정하기 위해서 필요했었습니다. 클라이언트의 입장에서는 connect()를 호출하고 connect()가 리턴되면 connect()에 전달했던 소켓이 서버와 연결됩니다. 따라서 클라이언트는 connect()에 연결하고 싶은 소켓, 서버의 주소와 포트 정보를 전달합니다.

#include <sys/socket.h>

int connect(int sock, struct sockaddr* servaddr, socklen_t addrlen);

정확히 말하면, 클라이언트의 연결요청 connect()가 호출된 이후 반환되는 시점은 ‘서버의 연결 요청 대기 큐에 추가가 된 상태’, 또는 ‘네트워크 연결이 끊기는 경우’ 둘 중 하나입니다. 이를 표현하기 위해서 클라이언트 소켓의 단계를 1,3,5,6으로 서버 소켓의 과정 중 어떤 단계와 대응되는 것인지 표현던 것입니다.

클라이언트의 IP와 port번호도 할당되기는 합니다. 클라이언트의 IP는 컴퓨터에 할당된 IP로, port는 임의로 선택해서 할당이 됩니다.

TCP 특징 : 전송되는 데이터의 경계가 존재하지 않는다.

전송되는 데이터의 경계가 존재하지 않는다는 말은 통신을 하는 두 host가 호출하는 write()와 read()의 횟수가 일치하지 않아도 됨을 의미합니다. feature/tcp 브랜치의 tcp_server.c, tcp_client.c에 예시가 있습니다.

$ git clone https://github.com/niklasjang/cpp-rtmp-relay
$ git checkout feature/tcp

Tags:

Categories:

Updated: