응용 프로그램 프로토콜(application protocol)
응용 프로그램 프로토콜이 결정되면 데이터를 정해진 형시과 절차에 따라 주고받아 처리하도록 소켓 함수를 이용해 구현하면 된다.
통신 양단이 주고받을 데이터를 정의하는 것만으로는 아직 충분하지 않다. 데이터를 전송할 때 고려할 사항을 알아보자
TCP 처럼 메세지 경계를 구분하지 안흔ㄴ 프로토콜을 사용할 경우 응용 프로그램 수준에서 메세지 경계를 구분하기 위한 추가 작업을 해야한다. 다음과 같이 4가지 방법을 고려할 수 있다.
송신자는 항상 고정 길이 데이터를 보내고 수신자는 항상 고정 길이 데이터를 읽는다.
장점
단점
송신자는 가변 길이 데이터를 보내고 끝 부분에 특별한 표시(EOR, End Of Record)를 붙인다. 그리고 수신자는 EOR이 나올 때까지 데이터를 읽는다.
장점
단점
송신자는 보낼 데이터 크기를 고정 길이 데이터로 보내고, 이어서 가변 길이 데이터를 보낸다. 수신자의 경우 고정 길이 데이터를 읽어서 뒤따라올 가변 데이터의 길이를 알아내고, 이 길이 만큼 데이터를 읽는다.
장점
해당 방법을 사용할 경우 메세지 구조
(5-2 그림)
송신자는 가변 길이 데이터 전송 후 접속을 정상 종료한다. 수신자는 recv()
함수의 리턴값이 '0'(=정상종료)이 될 때까지 데이터를 읽는다.
장점
단점
서로 다른 바이트 정렬 방식을 사용하는 시스템 사이에 데이터를 교환할 때는 바이트 정렬 방식을 통일해야 한다. 그렇지 않으면 데이터 해석에 문제가 생긴다.
특별한 전제가 없으면 빅 엔디안(네트워크 바이트 정렬) 방식으로 통일하는 것이 좋다.
구조체 멤버 맞춤(structure member alignment)
- 구조체(C++의 클래스도 포함) 멤버의 메모리 시작 주소를 결정하는 컴파일러의 규칙
간단한 예를 들어 다음과 같이 구조체를 이용해 메세지를 정의하면 10(=4+1+4+1)바이트가 아닌 16바이트가 전송된다.
struct MyMessage
{
int a; // 4 바이트
char b; // 1 바이트
int c; // 4 바이트
char d; // 1 바이트
};
MyMessage msg;
...
send(sock, (char *) &msg, sizeof(msg), 0);
(5-3 그림)
위 그림은 변수 msg의 메모리 구조인데, 기본적으로 C/C++ 컴파일러는 구조체 멤버에 대한 접근 속도를 빠르게 하려고 메모리를 약간 낭비하는 방식을 취하기 때문이다.
양쪽으로 프로그램이 동일한 구조체 멤버 맞춤을 사용한다면 이와 같이 메세지를 전송해도 문제가 되지 않는다. 만약 정확히 10바이트를 보내고자 한다면 다음과 같이 #pragma pack
지시자를 사용하면 된다.
#pragma pack(1) // 구조체 멤버 맞춤을 1바이트 경계로 변경
struct MyMessage
{
int a; // 4 바이트
char b; // 1 바이트
int c; // 4 바이트
char d; // 1 바이트
};
#pragma pack() // 구조체 멤버 맞춤을 기본값으로 환원
MyMessage msg;
...
send(sock, (char *) &msg, sizeof(msg), 0);
이때 변수 msg의 메모리 구조는 아래와 같다.
(5-4 그림)
참고 자료
김성우 저, "TCP/IP 윈도우 소켓 프로그래밍", 한빛아카데미, 2018