응용 프로그램 프로토콜이란 응용 프로그램 수준에서 주고받는 데이터의 형식과 의미, 처리 방식을 정의한 프로토콜입니다. 이는 표준화되어 있지 않으며, 응용 프로그램 프로토콜이 결정되면, 데이터를 정해진 형식과 절차에 따라 주고받도록 소켓 함수를 이용하여 구현하면 됩니다.
통신 양단이 주고받을 데이터를 C 언어의 구조체로 정의해야 하는 것 뿐만 아니라, 데이터를 전송해야 할 때 추가로 고려해야 할 사항이 있습니다.
TCP처럼 메시지 경계를 구분하지 않는 프로토콜을 사용할 때는 응용 프로그램 수준에서 메시지 경계를 구분하기 위한 추가 작업을 해야 합니다. 그 방법으로는 4가지가 존재하며, 자세한 방법은 다음 절인 '다양한 데이터 전송 방식'에서 다루겠습니다.
특별한 전제가 없다면 빅 엔디언 방식으로 통일하는 것이 좋습니다.
구조체 멤버 맞춤은 C 언어의 구조체(C++ 언어의 클래스 포함) 멤버의 메모리 시작 주소를 결정하는 컴파일러 규칙입니다. 구조체의 각 멤버(필드)가 메모리에서 배치되는 방식이 메모리 정렬 규칙에 맞도록 조정하는 것을 말합니다. CPU는 메모리에 접근할 때, 특정 바이트 경계에서 데이터를 읽고 쓰는 것이 효율적이므로, 이를 맞추기 위해 패딩(Padding)이 삽입됩니다. 예를 들어, 4바이트 정수형(int)은 4바이트 단위의 주소에서 시작하는 것이 CPU의 처리 효율에 유리합니다. 따라서 2바이트 short 타입 뒤에 4바이트 int 타입이 오면, 중간에 2바이트 패딩이 들어가게 됩니다. 이처럼 C/C++ 컴파일러는 구조체 멤버에 대한 접근 속도를 빠르게 하기 위해 메모리를 약간 낭비하는 방식을 취합니다.
만약 중간에 비어 있는 부분 없이 정확한 바이트를 보내고자 한다면 구조체 패킹 옵션을 주면 됩니다. 윈도우에서는 #pragema pack 지시자를, 리눅스에서는 __attribute__((packed)) 지시자를 사용하면 됩니다.
버퍼를 선언할 때 유의할 점은, 1바이트 널 문자를 고려하여 BUFSIZE+1 크기의 버퍼를 선언해야 합니다.
크기가 같은 버퍼를 정의해두고 데이터를 주고받으면 됩니다. 그리고 recv() 함수에 MSG_WAITALL 옵션을 주어 항상 BUFSIZE만큼 데이터를 읽습니다. 구현이 쉽지만, 미래에 사용할 가장 긴 데이터를 고려해야 하고 길이가 짧은 데이터를 주고받을 때 낭비하는 부분이 생깁니다.
EOR(End Of Record)로 사용할 데이터 패턴을 정해야 하는데, 흔히 '\n'이나 '\r\n'를 사용합니다. 그러나 소켓 수신 버퍼에서 1바이트 데이터를 읽는 부분 때문에 성능이 떨어질 수 있습니다. 이를 해결하기 위해 소켓 수신 버퍼에서 데이터를 한꺼번에 읽어서 내부에 저장해두고, 읽기 요청이 있을 때마다 1바이트씩 리턴해주는 사용자 함수를 만들어서 사용해야 합니다. 마지막으로 배열에 있는 데이터를 문자열로 간주하여 출력하는데, 데이터 자체에 '\n\0'이 포함되어 있으므로 줄바꿈을 따로 해주지 않아도 됩니다. 생성할 데이터의 길이를 미리 알 수 없을 때 적합합니다.
recv() 함수에 MSG_WAITALL 옵션을 주어 두 번(한 번은 고정 길이, 한 번은 가변 길이) 데이터를 읽는 방식입니다. 생성할 데이터의 길이를 미리 알 수 있을 때 적합합니다.
EOR로 특별한 데이터 패턴 대신 연결 종료를 사용하는 일종의 가변 길이 데이터 전송 방식입니다. recv() 함수에 MSG_WAITALL 옵션을 주어 데이터를 최대한 읽습니다. 'TCP 연결 설정 -> 데이터 전송 -> TCP 연결 종료'를 반복해야 하므로, 데이터를 자주 전송하는 경우에는 비효율적입니다.