C언어 - 구조체 크기
// 5명의 학생의 정보를 입력받아 “d9-4.dat” 파일에 기록하는 프로그램
#include<stdio.h>
typedef struct
{
char name[15];
char country[20];
int age;
}STUDENT;
int main()
{
FILE *fp;
STUDENT st[5];
int i;
if((fp = fopen ("d9-4.dat", "w")) == NULL)
{
printf("File open error...\n");
return -100;
}
for(i=0;i<5;++i)
{
printf("Enter name & country & age : ");
scanf("%s %s %d", st[i].name, st[i].country, &st[i].age);
// 메모리로 저장
fprintf(fp, "%s %s %d\n", st[i].name, st[i].country, st[i].age);
// 데이터를 쓴다.
}
fclose (fp);
return 0;
}
⇒ FILE *fp;
→ FILE을 연다는 것은 장치를 여는 것이고, 실제로는 하드드라이브를 여는 것이다.
→ 고수준인 fopen을 사용했으므로 포인터를 반환(File stream)
→ 저수준 open을 사용했다면 int형 반환(File descriptor)
// 데이터 파일에서 몸무게와 키를 읽어 비만을 체크하는 프로그램
#include<math.h> // linux에서 컴파일 할때 -lm 옵션을 붙여 주어야 한다.
#include<stdio.h>
typedef struct
{
char name[20];
float height;
float weight;
}STUDENT;
int main()
{
FILE *fp;
STUDENT st, *sp;
int i = 0;
if((fp = fopen ("d9-5.dat", "r")) == NULL)
{
printf("File Open Error!\n");
exit(-1);
}
sp = &st;
while(!feof(fp))
{
fscanf(fp, "%s %f %f", sp->name, &sp->height, &sp->weight);
printf("student name : %s\n", sp -> name);
printf("Height : %3.1f \nWeight: %3.1f\n", sp->height, sp->weight);
if((sp->height - 109) + 10 <= sp->weight)
{
printf("You are fat.. You need diet!!!\n\n");
}
else if((sp ->height - 109) - 10 >= sp->weight)
{
printf("You are thin.. You need more food!!!\n\n");
}
else
{
printf("You are normal... Keep you shape!!!\n\n");
}
}
fclose(fp);
return 0;
}
⇒ feof
→ fp가 가리키는 파일이 EOF(End Of File)인지를 검사하고 만일 EOF라면 0을 반환한다. (즉, 0이 되면 위 while문은 종료한다.)
⇒ (*sp).name 가 sp -> name 로 바뀌었다.
→ ‘->’는 ‘&’보다 연산자 우선순위가 높으므로 height를 가리키고 다음으로 주소를 가리킨다.
// 두 점 사이의 거리를 구하는 프로그램
#include<stdio.h>
#include<math.h>
typedef struct
{
int x;
int y;
}POINTER;
double distance(int, int, int, int);
int main()
{
POINTER p1, p2;
double dist;
printf("please enter x & y coordinate: ");
scanf("%d %d", &p1.x, &p1.y);
printf("please enter x' & y' coordinate: ");
scanf("%d %d", &p2.x, &p2.y);
dist = distance(p1.x, p1.y, p2.x, p2.y);
// dist = distance (p1, p2) → 인자가 2개로 줄었으며 각각의 인자는 8byte
printf("distance: %6.3f\n", dist);
// printf("distance: %6.3f\n", distance(&p1, &p2)); → 각각의 인자가 4byte
return 0;
}
double distance(int a, int b, int c, int d)
// double distance(POINTER p1, POINTER p2) → dist = distance (p1, p2) 일때
/* double distance(*POINTER p1, *POINTER p2) →
printf("distance: %6.3f\n", distance(&p1, &p2)); 일때 */
{
int square1;
int square2;
square1 = (c-a) * (c-a);
square2 = (d-b) * (d-b);
return (sqrt (square1 + square2));
}
⇒ 구조체는 용량의 상관이 있다.(우리가 쓰는 보통의 변수형들은 상관이 없다.)
※ 원본값의 수정이 가능하다. → 포인터(주소)
원본값의 수정이 불가능하다. → 값
⇒ 값전달에 있어 구조체인 경우 수십 byte 이상도 나올수 있다. 따라서 값이 인자로 왔다갔다 함에 따라 메모리를 많이 차지하게 된다.
⇒ 따라서 구조체의 주소를 넘기면 위의 문제를 해결할 수 있다. 대부분의 구조체는 주소를 넘긴다. (물론 주소를 넘김으로써 원본 값의 수정이 가능한 점도 있다.)
⇒ dist = distance(p1.x, p1.y, p2.x, p2.y);
→ dist = distance (p1, p2);로 바꾸면 인자가 2개로 줄어든다. 각각의 인자는 8byte이다.
→ dist = distance(&p1, &p2)); 로 바꾸면 주소값이 전달되므로 각각의 인자는 4byte로 위의 소스 보다 크기가 줄어든다.
- IPv4 TCP 클라이언트
⇒ 소켓 인터페이스를 사용할 때 통신의 단계에 따라 서버와 클라이언트의 사용 방법이 각각 다르므로 클라이언트 및 서버의 구분은 매우 중요하다. 일단 클라이언트로부터 알아보자. 클라이언트의 임무는 수동적으로 접속을 기다리고 있는 서버에게 통신을 개시하는 것이다. 전형적인 TCP 클라이언트의 통신은 다음의 4가지 단계를 거친다.
1. socket()를 이용하여 TCP 소켓을 생성
2. connect()를 이용하여 서버와의 연결을 설정
3. send(), recv()를 이용하여 통신을 수행
4. close()를 이용하여 연결을 종료
// TCPEchoClient4.c (IPv4 기반의 TCP 에코 클라이언트를 구현한 코드)
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include"Practical.h"
int main(int argc, char *argv[]) // 명령어 인자의 정확한 개수 확인
{
char *servIP = argv[1]; // 첫번째 인자: 서버 IP 주소(dotted 형식)
char *echoString = argv[2]; // 두번째 인자 : 보내려는 에코 문자열
int sock;
struct sockaddr_in servAddr; // 서버 주소
int rtnVal;
in_port_t servPort; // 세번째 인자(선택):
size_t echoStringLen; // 입력받은 문자열의 길이를 확인
ssize_t numBytes; // 서버에 에코 문자열 전송
unsigned int totalBytesRcvd = 0; // 수신한 문자 개수
if(argc<3 || argc>4)
{
DieWithUserMessage("Parameter(s)",
"<Server Address> <Echo Word> [<Server Port>]");
}
servPort = (argc == 4) ? atoi(argv[3]) : 7;
// 서버포트(숫자형식). 7이 잘 알려진 포트로 생략시 7을 사용
sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if(sock < 0)
{
DieWithSystemMessage("socket() failed");
}
memset(&servAddr, 0, sizeof(servAddr)); // 0으로 구조체 초기화
servAddr.sin_family = AF_INET; // IPv4 주소 패밀리
rtnVal = inet_pton(AF_INET, servIP, &servAddr.sin_addr.s_addr);
if(rtnVal == 0)
{
DieWithUserMessage("inet_pton() failed", "invalid address string");
}
else if(rtnVal < 0)
{
DieWithSystemMessage("inet_pton() failed");
}
servAddr.sin_port = htons(servPort); // 서버 포트
if(connect(sock, (struct sockaddr *) &servAddr, sizeof(servAddr))<0)
{
DieWithSystemMessage("connect() failed");
}
echoStringLen = strlen(echoString); // 입력받은 문자열의 길이를 확인
numBytes = send(sock, echoString, echoStringLen, 0);
// 서버에 에코 문자열 전송
if(numBytes < 0)
{
DieWithSystemMessage("send() failed");
}
else if(numBytes != echoStringLen)
{
DieWithUserMessage("send()", "sent unexpected number of bytes");
}
fputs("Received: ", stdout); // 돌려받은 에코 문자열 출력을 위한 설정
while(totalBytesRcvd < echoStringLen)
{
char buffer[BUFSIZE]; // 입출력 버퍼
/* 버퍼 크기(byte) 만큼 서버로 부터 수신(널 문자를 위해 1바이트 남겨놓음) */
numBytes = recv(sock, buffer, BUFSIZE -1, 0);
if(numBytes <0)
{
DieWithSystemMessage("recv() failed");
}
else if(numBytes == 0)
{
DieWithUserMessage("recv()", "connection closed prematurely");
}
totalBytesRcvd += numBytes; // 총 받은 크기를 기록
buffer[numBytes] = '\0'; // 널 문자를 추가하여 문자열 완성!
fputs(buffer, stdout); // 에코 버퍼를 출력
}
fputc('\n', stdout);
close(sock);
exit(0);
}
1. 응용 프로그램 초기 설정 및 파라미터 파싱(parsing, 분석)
⇒ 헤더파일
→ 이 헤더파일들은 API의 표준함수들과 상수 값들을 선언한다. 소켓 함수와 자료 구조에 대한 올바른 헤더 파일을 위해서는 시스템 문서를 참고한다.(예 man 페이지),
→ 이 예제 코드에서 사용자 정의 함수의 프로토타입(prototype)을 포함하는 Practical.h라는 헤더 파일을 사용하였다.
⇒ 전형적인 파라미터 파싱과 적정성 검사
→ IPv4 주소와 에코를 위한 문자열은 명령어의 첫 번째와 두 번째 파라미터로부터 전달이 된다. 클라이언트는 선택적으로 서버의 포트 번호를 얻기 위해 명령어의 세 번째 파라미터를 취할 수도 있다. 포트 번호인 세 번재 파라미터가 주어지지 않게 되면 클라이언트는 잘 알려진 에코 프로토콜 포트인 7번 포트를 기본적으로 사용하게 된다.
2. tcp 소켓 생성
⇒ socket() 함수를 사용하여 소켓을 생성한다. 여기서 소켓은 TCP(IPPROTP_TCP)라 불리는 스트림 기반의 프로토콜(SOCK_STREAM)을 이용하여 IPv4(AF_INET)환경에서 동작하도록 만들어졌다. socket() 함수는 성공적으로 호출되었을 때 정수형 값을 취하는 식별자(descriptor)(또는 ‘핸들러(handle)') 를 반환하는데, 이는 생성된 소켓을 다루는데 쓰인다. 만약 소켓호출이 실패하면 -1의 값을 반환한다. 예제 코드에서는 DieWithSystemMessage()라는 사용자 정의 에러처리 함수를 사용하여 오류가 암시하는 내용을 출력하고 프로그램을 종료하도록 하였다.
3. 주소 준비 및 연결 설정
⇒ 서버의 주소를 담기 위한 sockaddr_in 구조체 준비
→ 소켓을 연결하려면 연결 대상의 IP 주소와 포트 번호를 명시해야 한다. sockaddr_in 이라는 구조체는 이러한 주소 정보를 담기 위한 일종의 ‘그릇’이다. memset()는 구조체에 값을 채울 때 명시적으로 전체를 0으로 초기화하여 잘못된 값으로 인한 오동작을 예방한다.
⇒ sockaddr_in 주소 구조체를 채움
→ 주소 구조체에 주소 패밀리(AF_INET), IP 주소, 포트 번호를 채워야 한다. inet_pton()이라는 함수는 명령어 인자에서 dotted-quad 형식의 문자열로 넘어온 서버의 IP 주소를 32비트의 이진 형식으로 변환한다. 명령어 행에서 넘어온 서버의 포트 번호가 있다면 atoi()에 의해서 문자열 형태가 이전 형태로 변환이 된다. hton()('host to network short')는 변환된 이진 값이 API에 알맞은 형태로 되어있는지 확인한다.
⇒ 연결 설정 과정
→ connect() 함수는 클라이언트에서 생성된 소켓을 sockaddr_in 주소 구조체에 포함된 IP 주소와 포트 번호가 가리키는 서버 소켓으로 연결을 시도한다. 소켓 API가 다양한 프로토콜 집합체에 범용으로 쓰이므로 IPv4 주소를 위해 사용되는 sockaddr_in 주소 구조체에 대한 포인터는 일반 주소 구조체 형식을 가리키도록 sockaddr*로 형 변환(type casting)을 해야 하며, 주소 구조체의 실제 크기도 동시에 알려주어야 한다.
4. 에코 문자열을 서버에 보냄
⇒ 에코 문자열의 길이를 파악하고 추후에 사용하기 위해 변수 공간에 저장한다. 에코 문자열을 가리키는 포인터는 send()로 전달된다. 문자열 자체는 프로그램이 시작될 때 어딘가에 저장되지만 이것은 중요한 것이 아니다. 우리가 실제로 필요한 것은 문자열의 첫 번째 바이트가 위치한 주소와 문자열의 크기이다. 예제 코드에서는 일반적인 문자열의 종료를 나타내는 null 문자를 보내지 않는다는 사실에 주목하자. 성공적으로 호출되었다면 send()는 전송한 바이트 수를 반환하며, 그렇지 않다면 -1을 반환한다. 만약 send()의 호출이 실패하거나 잘못된 수의 바이트를 전송하게 되면 에러로 처리를 해야 한다. 실제로 예제 코드에서는 잘못된 수의 바이트를 전송할 경우가 생기지 않지만, 그럼에도 불구하고 에러는 언제 어디서나 발생할 수 있기 때문에 에러 처리를 위한 코드를 포함하는 것이 좋다.
5. 에코 서버의 응답 수신
⇒ TCP는 바이트-스트림 프로토콜이다. 이러한 종류의 프로토콜이 의미하는 바는 send() 호출 시 전송된 데이터의 경계가 없다는 점이다. 어느 한쪽에서 단 한번의 send() 호출로 데이터를 보내더라도 상대방은 단 한번의 recv() 호출로 데이터를 모두 받지 못할 수도 있다. 따라서 보낸 데이터의 정확한 바이트 수만큼 받을 때까지 recv()를 반복하여 데이터를 수신해야 한다. 예제 코드의 경우 서버에서 전송된 데이터는 한번에 읽히기 때문에 수신을 위한 반복문(loop)은 십중팔구 한번만 실행이 될 것이다. 하지만 이와 같은 결과는 항상 보장될 수 없으며 반복해서 읽어 들이는 환경을 고려해야만 하고 소켓을 사용하는 응용 프로그램을 개발할 때 생각해야 하는 기본 원칙이기도 하다. 절대로 네트워크 자체와 통신 상대방의 프로그램이 진행하려는 상황을 예상하거나 가정해서는 안 된다.
⇒ 바이트 블록 수신
→ recv()는 수신할 데이터가 있을 때까지 더 이상 진행이 되지 않고 실행을 잠시 멈추는 블록(block) 함수이다. 데이터가 도착하면 데이터를 버퍼에 복사하고 읽은 바이트의 수를 반환하며, 실패시 -1을 반환한다. 반환값이 0이라면 상대편의 응용 프로그램이 TCP 연결을 종료한 것이다. 이번 예제에서 recv()로 전달된 size 파라미터는 null 문자를 추가하기 위한 공간을 남겨두고 전달한 것임을 주목하자.
⇒ 버퍼 출력
→ 서버가 전송한 데이터를 수신하는 그대로 출력한다. null 문자로 끝나는 일반 문자열을 다루는 fputs()를 사용하기 위해 수신된 각각의 데이터 끝 부분에 null 문자를 추가하였다. 예제 코드에서는 송신한 바이트와 수신한 바이트의 내용이 같은지를 확인하지 않는다. 서버는 클라이언트가 보낸 문자열의 길이에 따라서 완전히 별개의 내용을 보낼 수도 있으며, 이 내용은 화면에 그대로 출력된다.
⇒ 줄 바꿈
→ 송신한 바이트 수와 동일한 바이트 수를 수신하게 되면 반복문을 빠져 나오고 새로운 줄 바꿈 문자를 출력한다.
6. 연결 종료와 응용 프로그램 종료
⇒ close() 함수는 상대방의 소켓에 연결의 종료를 알리고 소켓에 할당된 자원을 회수한다.
'내장형 하드웨어 > C언어' 카테고리의 다른 글
C언어 - 연결리스트2 (0) | 2011.07.11 |
---|---|
C언어 - 동적 자료형, 연결리스트 (0) | 2011.07.08 |
C언어 - 함수 포인터, 다중 포인터, 구조체 (0) | 2011.07.05 |
C언어 - 배열의 값과 주소 표시법, 함수 포인터 배열 (0) | 2011.07.04 |
C언어 - text mode, binary mode, fprintf, freed, 배열과 포인터 (0) | 2011.06.30 |