프로그래밍/2024 네트워크 프로그래밍

9. 구조체 헤더를 이용한 소켓통신 - 클라이언트와 헤더파일 살펴보기

데일리 백수 2025. 1. 14. 20:53

 

헤더파일 calc.c

먼저 헤더파일 분석입니다.

구조체를 이용하여 계산 요청 및 응답 데이터를 정의하였으며, 데이터 크기와 정렬을 정확히 맞추기 위해 패킹을 사용했습니다.


#pragma pack(1)을 사용하여 1바이트 단위로 정렬하여, 구조체의 멤버간의 패딩이 생기지 않게 강제했습니다. 이를 통해 데이터 크기를 줄이고, 일관적으로 내용을 표시할 수 있습니다. 

먼저 클라이언트에서 서버로 계산 요청을 정의하는 구조체인 CALC_REQ_HDR_t입니다. 

멤버는 연산에 사용되는 피연산자의 개수인 numOperand와 연산을 수행할 연산자를 나타내는 operator 변수로 이루어져 있습니다. 이를 통해, 서버에 자신이 수행할 연산의 내용을 전달합니다. 

다음으로 서버에서 클라이언트로 계산 결과를 정의하는 구조체인 CALC_RESP_t 입니다. 

멤버는 계산결과값을 저장하는 answer 변수 하나입니다. 

이 커스텀 헤더파일에 정의되어 있는 내용을 통해 패킹을 통한 효율적인 메모리 사용과, (패킹을 하지  않으면, 구조체의 크기가 멤버 변수 크기와 일치하지 않음.) 효율적인 데이터 전송을 할 수있습니다.

아쉬운 점은 연산자 값을 define이나 enum 이용해서 정의했으면 가독성에서 좀더 좋지 않았을 까라는 생각입니다. 네트워크 전송에 문제가 발생해 사용하지 않나? 라는 생각이 들었지만 깊게 조사하지는 않았습니다. 

 

클라이언트 client.c

#include "calc.h"
#include "mylib2.h"
#include <stdint.h>

#define BUFFER_SIZE 1024

int main(int argc, char *argv[]) {

    int sockFd;
    unsigned char buffer[BUFFER_SIZE];
    CALC_REQ_HDR_t calcReqHdr;
    CALC_RESP_t *calcRespPtr;
    int numOperand, operator;
    int32_t *operandPtr;
    int32_t operandTmp;
    int messageLen;
    unsigned char *message;
    int32_t answer;
    int i;

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

    sockFd = generate_tcp_client_socket(argv[1], atoi(argv[2]));

    printf("Connected to the calculating server.\n");

    printf("Number of operands: ");
    scanf("%d", &numOperand);
    calcReqHdr.numOperand = (int8_t)numOperand;
    fgetc(stdin);

    printf("Operator (+, -, *): ");
    scanf(" %c", (char*)&operator);  
    calcReqHdr.operator = (int8_t)operator;

    printf("calcReqHdr.numOperand: %d\n", calcReqHdr.numOperand);
    printf("calcReqHdr.operator: %d %c\n", calcReqHdr.operator, calcReqHdr.operator);
    printf("sizeof(CALC_REQ_HDR_t): %ld\n", sizeof(CALC_REQ_HDR_t));

    operandPtr = (int32_t *)malloc(numOperand * sizeof(int32_t));
    for (i = 0; i < numOperand; i++) {
        printf("Operand %d: ", i);
        scanf("%d", &operandTmp);
        operandPtr[i] = htonl(operandTmp);
    }

    messageLen = sizeof(CALC_REQ_HDR_t) + numOperand * sizeof(int32_t);
    message = (unsigned char *)malloc(messageLen);

    memcpy(message, (unsigned char *)&calcReqHdr, sizeof(CALC_REQ_HDR_t));
    memcpy(message + sizeof(CALC_REQ_HDR_t), (unsigned char *)operandPtr, numOperand * sizeof(int32_t));

    printf("Message to send: ");
    for (i = 0; i < messageLen; i++)
        printf("%02X ", message[i]);
    printf("\n");

    if (write_n(sockFd, message, messageLen) < 0) {
        error_handling("write_n() error.");
    }

    if (read_n(sockFd, buffer, sizeof(CALC_RESP_t)) < 0) {
        error_handling("read_n() error.");
    }

    calcRespPtr = (CALC_RESP_t *)buffer;
    answer = ntohl(calcRespPtr->answer);

    printf("Answer: %d\n", answer);

    free(operandPtr);
    free(message);
    close(sockFd);

    return 0;
}

 

다음으로 클라이언트 코드 분석입니다.

변수가 상당히 많지만 하나하나 살펴보겠습니다.

서버와 연결을 위한 소켓인 sockFd와 서버로부터 데이터를 수신하기 위한 임시버퍼인 buffer, 계산 요청의 헤더를 저장하기 위한 calcReqHdr, 서버로부터 수신한 계산 응답 데이터를 저장하는 구조체 포인터인 calcResPtr ,사용자에게 피연산자의 개수와 연산자를 입력받는 numOperand,operator와 피연산자 배열에 대한 포인터인 operandPtr(포인터로 선언한 이유는 동적 할당을 위함), 사용자 입력을 임시 저장하는 operandTmp, 서버로 전송할 메시지의 길이와 버퍼인 messageLen과 message, 서버로부터 수신한 계산 결과를 저장할 answer이 있습니다.

사용자에게 ip주소와 포트번호를 받았는지 확인 후,(개수로) 커스텀 헤더파일에 있는 generate_tcp_client 메소드를 이용하여 서버와의 connect를 진행합니다.

그후 사용자에게 입력한 피연산자 개수를 numOperand에 받고, 서버에 요청하기 위한 구조체인 calcReqHdr의 numOperand에 저장합니다.

그후 연산을 진행할 연산자를 입력받은 후, operator에 저장한 뒤. 역시 calcReqHdr의 operator에 저장합니다. 

그 후 사용자에게 입력한 내용을 확인 시켜줍니다(피연산자 개수와 연산에 사용할 연산자) 

그 후, 몇 개의 피연산자를 사용할 것인지 알고 있기 때문에 malloc을 이용하여 numOperand에 저장된 피연산자의 개수만큼 크기를 가지는 동적 배열을 생성합니다.

그 후 for 루프를 이용하여 사용자에게 피연산자 값을 입력 받고, (여기서 임시변수가 있는 이유는 , 네트워크 바이트 오더로 변환을 하기 위해서) 그 후 사용자가 입력한 내용을 네트워크 바이트 순서로 변환하여 배열에 저장합니다. 굳이? 란생각을 할 수도 있지만 호환성을 위해서는 네트워크로 전송하기 전에 표준 규격인 빅 앤디안으로 변환해주는 것이 필요합니다.

 

그 후, 서버로 전송할 요청 메시지를 생성합니다. 이를 위해 헤더와 피연산자 데이터를 하나의 메시지로 결합합니다. 

Sizeof(CALC_REQ_HDR_t)를 통해 요청 해더의 크기를 가져오고, 피연산자의 개수에 각 피연산자의 데이터 크기를 곱해줍니다. 즉 messageLen은 헤더(피연산자 개수,연산자)와 데이터(피연산자 배열)의 사이즈가 될 것입니다. 

그 후, 계산한 messageLen을 통하여 메시지를 저장할 공간을 동적으로 할당합니다. 

그 후 실제 메시지에 헤더 데이터와 피연산자 배열 데이터를 복사합니다. 조금 살펴봐야 할 부분이 있는데. 헤더를 복사하는 부분은 직관적이지만 피연산자 배열 데이터를 복사하는 부분은 직관적이지 않아 자세히 살펴봐야 합니다. 

먼저 message+sizeof(CALC_REQ_HDR_t)를 통해 복사를 시작할 위치(헤더가 끝난 바로 다음 위치)를 정해준 후 , 피연산자 배열인 operandPtr에서 numOperand *변수의 크기 만큼 데이터를 복사하여, 메시지에 피연산자 배열 데이터를 추가해줍니다. 

그 후, 실제 생성된 message 배열의 내용을 출력해줍니다. 

실제 생성한 message 배열을 write_n을 통하여 내 messageLen만큼 전송합니다. 여기서 write_n을 통하여 messageLen만큼 전송을 보장할 수 있습니다.  

그 후 서버는 송신 결과에 대한 result값을 재전송할 것입니다. 이를 read_n을 통하여 buffer에 저장해줍니다. 여기서 서버가 보내는 데이터의 크기는 응답 구조체인 CALC_RESP_t의 사이즈만큼이 될 것입니다. 

그후 buffer에서 응답받은 내용을 CALC_RESP_t로 캐스팅 시켜 calcRespPtr에 저장해 준 후, 빅 엔디안인 데이터를 리틀 앤디안으로 변환시켜 answer에 저장합니다. 

여기서 화살표를 사용하는 이유는 내가 할당하지 않은 메모리변수에 접근하기 위해서는 ->를 사용해야 합니다. 그 후 최종적으로 결과를 출력 합니다. 

또한 동적할당을 진행한 메모리와 소켓을 꼭 해제 시켜주어야 합니다.(메모리 누수 방지)

피연산자 개수 3으로 지정하고, 덧셈 연산을 지정한 후 , 1 2 3 을 순서대로 입력했습니다 결과는 6이 나왔습니다. 

잘 작동되는 모습을 볼 수 있습니다.