1. 문제 상황
서버와 클라이언트가 데이터를 주고받는 과정에서 recv()나 send() 함수가 예상대로 작동하지 않을 때가 있다.
데이터가 정상적으로 수신되지 않거나, 데이터 크기가 다르다는 오류가 발생하는 것이 대표적이다.
이 문제는 주로 서버와 클라이언트가 서로 다른 데이터 타입 크기를 사용하기 때문인데, 특히 윈도우와 리눅스처럼 다른 운영체제에서 통신할 때 자주 발생한다.
리눅스에서 서버를 열고, 윈도우에서 클라이언트가 cmd로 SSH를 통해 접속하여 데이터를 주고받는 상황을 가정하자.
서버와 클라이언트 모두 C 언어로 작성되었으며, 동일한 구조체를 사용하고 있다고 생각할 수 있다.
하지만 데이터 타입의 크기가 운영체제마다 다르기 때문에 실제로는 구조체 크기가 달라지고, 이로 인해 통신 중 오류가 발생할 수 있다.
2. 데이터 타입 불일치의 원인
1. 운영체제와 컴파일러의 차이
리눅스(64비트 환경)의 경우 int는 4바이트, long은 8바이트로 정의된다. 반면, 윈도우(64비트 환경)에서는 int가 4바이트로 동일하지만, long은 32비트 시스템과 마찬가지로 4바이트로 정의된다.
예를 들어, 리눅스 서버와 윈도우 클라이언트가 다음과 같은 구조체를 사용한다고 하자. 운영체제나 컴파일러에 따라 데이터 타입의 크기와 메모리 정렬 방식이 달라질 수 있다.
struct Data {
int id; // 사용자 ID
long timestamp; // 타임스탬프
};
리눅스에서 이 구조체의 크기는 int 4바이트와 long 8바이트를 더해 총 12바이트가 된다. 하지만 메모리 정렬 규칙에 따라 4바이트의 패딩이 추가되어 최종적으로 16바이트가 된다.
반면, 윈도우에서는 int 4바이트와 long 4바이트를 더해 총 8바이트가 된다. 윈도우의 정렬 규칙에 따라 추가 패딩이 없으므로 구조체 크기는 그대로 8바이트다.
이 차이로 인해 리눅스 서버가 구조체 데이터를 전송하면 16바이트를 보내지만, 윈도우 클라이언트는 동일한 구조체를 사용한다고 믿고 8바이트만 읽으려 하기 때문에 데이터가 손실되거나 잘못된 결과가 발생하게 된다.
2. 메모리 정렬(Padding) 문제
컴파일러는 성능 최적화를 위해 데이터를 메모리에서 정렬한다.
즉, 데이터 크기를 기준으로 특정 위치를 비워 두는 패딩(Padding) 처리가 이루어진다.
struct Sample {
char a; // 1바이트
int b; // 4바이트
};
위 구조체는 5바이트만 필요할 것 같지만, 메모리 정렬 규칙에 따라 8바이트가 된다.
- a는 1바이트를 차지하지만, b는 4바이트 정렬 규칙에 따라 시작 주소가 4의 배수가 되어야 한다.
- 따라서 중간에 3바이트의 패딩이 추가된다.
서버와 클라이언트가 동일한 정렬 규칙을 따르지 않으면, 구조체 크기가 달라져 오류가 발생한다.
3. 데이터 타입을 맞추는 방법
1. 고정 크기 데이터 타입 사용
stdint.h 라이브러리에서 제공하는 고정 크기 데이터 타입을 사용하면, 데이터 타입의 크기를 명확히 지정할 수 있다.
이 방법은 운영체제나 컴파일러에 관계없이 일관된 데이터 크기를 보장한다.
주요 타입과 크기
타입 | 크기 | 설명 |
int8_t | 1바이트 | 부호 있는 정수 |
uint8_t | 1바이트 | 부호 없는 정수 |
int16_t | 2바이트 | 부호 있는 정수 |
uint16_t | 2바이트 | 부호 없는 정수 |
int32_t | 4바이트 | 부호 있는 정수 |
uint32_t | 4바이트 | 부호 없는 정수 |
int64_t | 8바이트 | 부호 있는 정수 |
uint64_t | 8바이트 | 부호 없는 정수 |
EX.
#include <stdint.h>
typedef struct {
int32_t id; // 4바이트 고정
uint8_t flag; // 1바이트 고정
int64_t timestamp; // 8바이트 고정
} Packet;
위와 같이 고정 크기 데이터 타입을 사용하면, 서버와 클라이언트가 동일한 크기의 구조체를 사용하게 되어 오류를 방지할 수 있다.
2. #pragma pack을 활용한 메모리 정렬 강제
메모리 정렬 문제를 방지하기 위해 #pragma pack을 사용하여 구조체를 강제로 1바이트 정렬로 설정할 수 있다.
이렇게 하면 구조체의 크기가 패딩 없이 정확히 계산된 크기를 갖게 된다.
#pragma pack(push, 1)
typedef struct {
int32_t id; // 4바이트
uint8_t flag; // 1바이트
char name[32]; // 32바이트
} Packet;
#pragma pack(pop)
정렬 전후 구조체 크기 비교
기본 정렬 (패딩 O) | 강제 정렬 (패딩 X) |
40바이트 | 37바이트 |
패딩이 제거되면, 서버와 클라이언트가 동일한 구조체 크기를 갖게 되어 통신 오류가 방지된다.
3. 네트워크 바이트 순서 변환
서버와 클라이언트가 서로 다른 아키텍처(빅엔디안, 리틀엔디안)를 사용한다면, 바이트 순서를 맞추는 작업도 필요하다.
- 네트워크에서는 빅엔디안 방식으로 데이터를 전송하는 것이 표준이다.
- 따라서 데이터를 전송하기 전에 htonl, ntohl 함수로 변환해야 한다.
#include <arpa/inet.h>
typedef struct {
uint32_t id;
uint32_t score;
} GameData;
void sendData(int socket, GameData* data) {
data->id = htonl(data->id);
data->score = htonl(data->score);
send(socket, data, sizeof(GameData), 0);
}
void recvData(int socket, GameData* data) {
recv(socket, data, sizeof(GameData), 0);
data->id = ntohl(data->id);
data->score = ntohl(data->score);
}
4. 윈도우와 리눅스 통신에서의 고려사항
서버와 클라이언트가 서로 다른 운영체제를 사용하는 경우, 데이터 타입뿐만 아니라 구조체의 메모리 정렬 방식과 바이트 순서에서도 차이가 발생할 수 있다.
이런 문제를 해결하기 위해 각 운영체제에 맞는 정렬 옵션과 통신 과정에서의 표준화 작업이 필요하다.
1. 데이터 정렬 옵션
윈도우와 리눅스에서는 구조체의 데이터 정렬 방식을 지정하거나 패딩을 제거하기 위한 방법이 다르다.
윈도우에서의 정렬 방식
윈도우에서는 __declspec(align(N)) 지시어를 사용하여 구조체 멤버의 정렬 기준을 설정할 수 있다.
예를 들어, align(1)을 사용하면 모든 멤버를 1바이트 정렬 기준으로 배치하며, 이로 인해 패딩이 제거된다.
struct __declspec(align(1)) Packet {
int id; // 4바이트
char name[32]; // 32바이트
};
위 코드에서는 모든 멤버가 1바이트 단위로 정렬되므로 구조체 크기가 정확히 36바이트로 설정된다.
리눅스에서의 정렬 방식
리눅스에서는 __attribute__((packed)) 속성을 사용해 패딩을 제거할 수 있다. 이 속성은 구조체 멤버를 가능한 한 밀집되게 배치하며, 결과적으로 크기를 줄인다.
struct __attribute__((packed)) Packet {
int id; // 4바이트
char name[32]; // 32바이트
};
위 코드에서도 모든 멤버가 패딩 없이 배치되며, 구조체 크기가 36바이트가 된다.
이처럼 두 운영체제에서 다른 정렬 옵션을 사용하지만, 목표는 동일하다. 구조체의 크기를 고정하고, 추가적인 패딩으로 인한 데이터 손실을 방지하는 것이다.
2. 네트워크 통신 과정
서버와 클라이언트 간 통신을 안정적으로 구현하기 위해 고정 크기 데이터타입 정의와, 구조체의 크기 고정, 네트워크 바이트 순서로 변환 중 어떤 것을 사용하고 고려할지 미리 염두에 두어야 한다.
고정 크기 데이터 타입 정의
먼저, 구조체를 설계할 때 반드시 int32_t, uint8_t와 같은 고정 크기 데이터 타입을 사용해야 한다. 이렇게 하면 데이터의 크기가 운영체제나 컴파일러에 관계없이 일정하게 유지된다.
구조체의 크기 고정
패딩을 방지하기 위해 #pragma pack 지시어 또는 운영체제별 정렬 옵션을 사용하여 구조체의 크기를 고정해야 한다. 예를 들어, #pragma pack(push, 1)을 사용하면 모든 멤버가 1바이트 단위로 정렬되며, 서버와 클라이언트가 동일한 구조체 크기를 공유할 수 있다.
네트워크 바이트 순서로 변환
운영체제와 아키텍처에 따라 데이터가 저장되는 방식(빅엔디안, 리틀엔디안)이 다를 수 있다. 네트워크 통신에서는 빅엔디안이 표준이므로, 데이터를 전송하기 전에 htonl(host to network long) 또는 htons(host to network short)와 같은 함수를 사용해 데이터를 변환해야 한다.
수신한 데이터는 다시 ntohl(network to host long)이나 ntohs(network to host short)를 통해 복원해야 한다.
'Dog-Honey-Tips' 카테고리의 다른 글
[Linux] 리눅스에서 기존 사용자 계정을 새로운 사용자로 복제하는 방법 (0) | 2024.12.01 |
---|---|
[Ubuntu] ssh로 다른 컴퓨터에서 접속할 수 있도록 서버 설정하기(우분투 + 윈도우/ WSL) (1) | 2024.11.22 |
[AWS] IAM 사용자 생성하기 (3) | 2024.10.13 |
[Obsidian] 코드 복붙 한 줄 생김/ 코드 복붙 줄 띄어짐 (해결방법) (4) | 2024.09.22 |