원문 http://cafe.naver.com/newchany/86


1절. endian 에 대한 기초지식

아마 네트웍 프로그래밍을 조금 해보았다면, Little-Endian, Big-Endian 이라는 말을 들어 보았을것이다. 때로 order byte, byte order 라고 말하기도 하며, 작은 끝돌이, 큰 끝돌이(--;) 라고 말하기도 한다. 작은 끝돌이, 큰 끝돌이는 김치하 교수의 번역서에서 볼수 있는 단어들이다.

결론부터 말하자면 엔디안 이란 컴퓨터에서 데이타가 저장되는 순서를 말하는 것이다. 컴퓨터에서 데이타 저장은 여러분도 잘 아시다 시피 byte 단위로 저장이 된다. 그런데 이 단위 저장을 할때 각 제조업체(CPU)에 따라서 저장이 되는 순서가 서로 다르다. 예를들어 우리가 자주 사용하는 32bit 정수의 경우 한번에 4byte 의 데이타가 저장이 되는데, 이때 가장 낮은 바이트부터 저장을 하는 방식이 있고, 가장 높은 바이트부터 저장을 하는 방식이 존재한다. 전자를 Little Endian 이라고 하며, 후자를 Big Endian 이라고 한다.


1.1절. 자세한 데이타 방식

즉 Little Endian 저장방식이 적용될경우 다음과 같이 저장되게 된다.

    I : 32 bit int 형정수


    |   1byte   |
    +-----------+-----------+-----------+-----------+
    |    I1     |    I2     |    I3     |    I4     |   
    +-----------+-----------+-----------+-----------+

 addr A   addr A+1      addr A+2     addr A+3
			
반면 Big Endian 은 다음과 같이 데이타가 저장될 것이다.
    I : 32 bit int 형정수


    |   1byte   |
    +-----------+-----------+-----------+-----------+
    |    I4     |    I3     |    I2     |    I1     |   
    +-----------+-----------+-----------+-----------+

 addr A   addr A+1      addr A+2     addr A+3
			
보면 알겠지만 서로 반대되는 순서로 데이타가 저장이 됨을 알수 있을것이다.

그럼 좀더 실제적인 예를들어 보도록 하겠다. Little Endian 을 적용하는 가장 대표적인 CPU는 Intel 계열 CPU 이며, Big Endian 을 적용하는 가장 대표적인 CPU 는 Sparc 계열 CPU 이다.

우리는 Endian 의 테스트를 위해서 간단한 쏘쓰를 하나 제작할 것이다.

예제 : endian.c

#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(int argc, char **argv)
{
    int fd;
    int data=123456789;
    char c[4];
    fd = open("test_bin", O_CREAT|O_WRONLY);

    write(fd, (void *)&data, sizeof(int));
    memcpy(c, (void *)&data, sizeof(int));

    close(fd);
}
			
이 프로그램은 test_bin 이라는 이름의 파일을 open 한다음 int 형 정수를 쓰는 일을한다. 우리는 위의 프로그램을 Intel Linux 와, Sparc Solaris 에서 각각 실행시켜서 그 결과를 알아볼것이다. Sparck Solaris 에서 위의 프로그램을 컴파일,실행한후 생성되는 test_bin 파일을 ftp 등을 통해서 리눅스로 가져와서 테스트한다(동일한 환경에서 테스트하는게 혼동을 피할수 있음으로).

Linux 에서 위의 프로그램을 실행후 만들어진 파일을 test_bin_linux 로 이름을 바꾸고, Solaris 에서 만들어진 파일은 test_bin_solaris 로 이름을 바꾸고 나서 od(1) 프로그램을 이용해서 그값을 확인해 보았다.

[root@coco endian]# od -x test_bin_linux 
0000000 cd15 075b
0000004
[root@coco endian]# od -x test_bin_solaris 
0000000 5b07 15cd
			
위의 결과를 보면 Little Endian 과 Big Endian 의 차이점을 쉽게 이해할수 있을것이다. 저장방식이 바이트 단위로 서로 전혀 반대임을 알수 있다.


2절. Endian 이 실제 프로그래밍 환경에서 중요한가

Endian 에 의한 byte order 은 해당 시스템의 CPU 에서 신경을 쓰므로, 단지 하나의 시스템에서만 프로그래밍 작업을 한다면 Endian 에 대해서 전혀 신경 쓸필요가 없다.

그러나 네트웍프로그래밍을 할경우 이기종간의 (예를 들어 Sparc 과 Intel) 통신을 염두에 두어야만한다. 이럴경우 Endian 에 신경을 써주지 않으면 전혀 엉뚱한 결과를 가지고 오게 된다.


2.1절. 그럼 네트웍 상에서 어떤 문제가 발생하는가

이번에는 서버 클라이언트 프로그램을 만들어 보도록 하자. 서버 프로그램은 Sparc 솔라리스에, 클라이언트 프로그램은 Inten Linux 에서 각각 작동하도록 할것이다.

다음은 서버 프로그램이다.

server.c

	
#include <sys/time.h>
#include <sys/socket.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main(int argc, char **argv)
{
    int server_sockfd, client_sockfd, sockfd;
    struct sockaddr_in clientaddr, serveraddr;

    int fd_num;

    int state, client_len;
    int i, maxi, maxfd;
    int data;

    server_sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_sockfd == -1)
    {
        perror("socket error : ");
        exit(0);
    }

    bzero(&serveraddr, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serveraddr.sin_port = htons(atoi(argv[1]));

    if(bind (server_sockfd, (struct sockaddr
             *)&serveraddr, sizeof(serveraddr)) < 0)
    {
        perror("bind error ");
        exit(0);
    }

    if (listen(server_sockfd, 5) < 0)
    {
        perror("listen error : ");
        exit(0);
    }

    client_sockfd = accept(server_sockfd, (struct
                    sockaddr *)&clientaddr, &client_len);
    read(client_sockfd, (void *)&data, sizeof(int));
    printf("%d\n", data);
    close(client_sockfd);
}
			

다음은 클라이언트 프로그램이다.

client.c

#include <sys/time.h>
#include <sys/socket.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main(int argc, char **argv)
{
    int client_sockfd;
    struct sockaddr_in clientaddr;
    int data = 123456789;
    int client_len;

    client_sockfd = socket(AF_INET, SOCK_STREAM, 0);
    clientaddr.sin_family = AF_INET;
    clientaddr.sin_addr.s_addr = inet_addr("192.168.100.190");
    clientaddr.sin_port = htons(atoi(argv[1]));

    client_len = sizeof(clientaddr);

    if (connect(client_sockfd, (struct sockaddr *)&clientaddr, client_len) < 0)
    {
        perror("Connect error : ");
        exit(0);
    }

    write(client_sockfd, (void *)&data, sizeof(int));
    close(client_sockfd);
}
			

클라이언트는 123456789 를 보내고 서버는 이값을 받아서 출력하는 단순한 일을한다. 그럼 각각의 쏘쓰를 솔라리스와 리눅스로 옮겨서 컴파일후 작동시켜보도록 하자. 서버측에서는 다음과 같은 결과를 보여줄것이다.

[root@solaris test]# ./server 12345
365779719
			
분명 클라이언트는 123456789 를 보냈는데, 서버측에서는 전혀 다른 값을 출력시킨다. 이유는 간단한다 Endian 이 달라서 데이타의 byte 가 역전되어 있기 때문이다.

그럼 이문제를 어떻게 해결해야 할까. 인테넷 상에는 그야말로 다양한 시스템이 존재하고 시스템마다 Endian 차이가 존재할건데,


2.2절. 그럼 Endian 문제의 해결방법은 ?

가장 간단한 해결방법은 Endian 이 Big 이든 Little 이든 하나로 통일시키는 거겠지만 이건 사실 불가능하다. 그럼 생각할수 있는 방법이 공통되는 Endian 으로 변환 시킨다음에 자신의 데이타를 전송하는 방법이 될것이다. 그러면 전송하는 측이나 전송받는 측이나 이 공통되는 Endian 을 알고 있을 것임으로 byte order 를 시킬수 있을것이다.

그래서 network byte order 이란것이 존재한다. 즉 network 로 데이타를 보낼때는 무조건 하나의 Endian 으로 통일을 시키는 것이다. network byte order 는 Big Endian 을 따른다. 그러므로 우리는 시스템에 관계없이 무조건 byte 를 Big Endian 에 맞도록 byte order 를 시킨다음에 네트웍을 통해서 외부로 보내면 된다. 받는 측에서는 자기에게 들어오는 Endian 이 Big 으로 통일되어 있음으로 Big Endian 을 자신의 Endian 에 맞도록 byte order 시켜주면 될것이다.

C 는 이러한 byte order 를 위한 함수를 제공한다. 함수가 하는 일은 자신의 host byte order 을 network byte order 에 맞게 변경시켜주는 것과, network byte order 을 자신의 host byte order 에 맞게 변경시켜주는 2가지 군의 함수를 제공한다.

#include <netinet/in.h>

unsigned long int htonl(unsigned long int hostlong);
unsigned short int htons(unsigned short int hostshort);

unsigned long int ntohl(unsigned long int netlong);
unsigned short int ntohs(unsigned short int netshort);
			
htonl 과 htons 는 host to network 즉 host byte order 를 network byte order 로 변경시켜주며, ntohs 는 network to host 로 그 반대로 변경 작업을 한다.

그렇다면 위의 쏘스들에서 port 지정을 위해서 왜 htons 를 사용하는지를 알수 있을것이다.

serveraddr.sin_port = htons(atoi(argv[1]));
			
자신의 host byte order 를 network byte order 로 변경 시키는 작업이다. linux 의 경우 little endian 을 따르는 byte order 를 하는데 만약 nework byte order 로 변경시키지 않는다면, 자기가 입력한것과 전혀 다른 port 가 전달될것이다. 예를 들어 349 번 포트를 htons 없이 지정했다면 34049 가 지정된것으로 전달될것이다. 결국 원하는 포트를 찾지 못하는 결과를 가져올 것이다.

이제 위의 코드를 어떻게 변경시켜야 할지 감을 잡았을 것이다. data 를 보내는 측에서는 htonl 을 이용해서 host byte order 로 변경시켜주고, 받는 측(서버) 에서는 ntohl 을 이용해서 network byte order 를 자신에게 맞도록 변경시켜주면 된다. 다음과 같이 코드를 각각 변경시키면 된다.

client.c
========
data = htonl(data);
write(client_sockfd, (void *)&data, sizeof(int));

server.c
========
printf("%d\n", ntohl(data));
close(client_sockfd);
			


2.3절. 또 다른 해결책은 없나요

byte order 함수를 사용해서 Little/Big Endian 문제의 해결은 했지만, 조금만 생각해보면 이 방법이 꽤 불편할수 있다는걸 알수 있다. 데이타를 보낼적엔 반드시 Endian 변환을 해줘야 한다. 꽤 귀찮은 작업이 될수 있다. 혹시라도 실수로 변환을 하지 않을경우에는 어떻게 될지 알수 없다.

그렇다면 byte order 에 신경쓸필요 없이 byte 단위로 데이타를 보내면 될것이다. char 를 사용하는 것이다. char 은 1byte 의 크기를 가짐으로 byte order 에 신경쓸 필요가 없이 사용가능하다. 그래서 많은 경우 네트웍 통신 프로그래밍을 할경우 char 만을 이용해서 통신을 하도록 하는 경우도 있다. 예를 들어 위의 12345678 을 int 형으로 보내는 대신 문자열로 변환시켜서 전송하는 것이다. 이럴경우에는 Endian 문제에 신경쓸필요가 없을것이다.


2.4절. Endian 체크하기

다음과 같은 간단한 코드를 이용해서 시스템의 Endian 을 체크할수 있다.

예제 : endian_check.c

int main()
{
    int i = 0x00000001;
    if( ((char *)&i)[0] )
        printf( "Littile Endian\n" );
    else
        printf( "Big Endian\n" );
}
			


3절. 결론

이상 Endian 에 대해서 간단하게 알아보았다. 보통 int 형과 같이 byte order 가 필요한 데이타 통신을 "binary 통신을 한다" 라고 말한다. 최근에는 binary 통신을 사용하지 않고 단지 text(char) 만을 이용해서 통신을 하는게 좀더 일반적인 추세이다. 그러나 어느 방식을 채택하든지 사용하기에 간편한 방식을 택하면 될것이다.


MSDN에 정의 되어 있는 내용 입니다.


It is possible to split the definition of a class or a struct, an interface or a method over two or more source files. Each source file contains a section of the type or method definition, and all parts are combined when the application is compiled.



둘 이상의 소스 파일에 대해 클래스 또는 구조체, 인터페이스 또는 메소드의 정의를 분할 할 수 있습니다. 각 소스 파일에는 형식 또는 메서드 정의의 섹션이 포함되어 있으며 응용 프로그램이 컴파일 될 때 모든 파트가 결합됩니다.


C# 2.0부터 적용되어 있는 것으로 알고 있습니다.

사용하는 이유는 여러가지가 있습니다만 제가 생각하는 이유로는
1. 프로젝트에 여러사람이 접근해서 작업을 할 경우에 소스 merge에 이점을 두거나 해당 부분을 나눠서 작업을 할 수 있는 이점이 있습니다.
 - 예를 들어서 몬스터의 AI 부분 작업을 여러사람이 나눠서 하게 된다면 AI의 해당 부분들( 플레이어 공격, 휴식, 죽음 등등 )을 각각의 파일로 나눠서 작업하게 되면 나중에 접근하기도 쉽고, 나눠서 작업하기도 편합니다. 이후에 예를 설명 드리겠습니다.

2. MSDN에서 설명되어 있기를... Visual Studio에서는 Windows Forms, 웹 서비스 래퍼 코드 등을 만들 때이 방법을 사용합니다. Visual Studio로 만든 파일을 수정하지 않고 이러한 클래스를 사용하는 코드를 만들 수 있습니다.
- 뭐 대충 자동으로 생성되는 코드랑 작업자가 사용하는 코드를 분류해서 사용하면 실수를 미연에 방지할 수 있다는 내용입니다만 전 이런거 사용을 하지 않기 때문에 1번의 이유가 더 확실하죠.

< 간단한 코드를 예를 들어보이겠습니다. >


EnemyAI.cs

public partial class EnemyAI {

    public string Init() {

        return "Enemy AI Init";

    }

}


EnemyAI_Attack.cs

public partial class EnemyAI {

    public string Attack() {

        return "Enemy Attack!!!!";

    }

}


EnemyAI_Death.cs

public partial class EnemyAI {

    public string Death() 

    {

        if( HP > 0 )

            return "No Die";


        return "Enemy Death";

    }

}


Program.cs

public class Program {

    public static void Main() {

        EnemyAI enemy_ai = new EnemyAI();

        Console.WriteLine(enemy_ai.Init());

        Console.WriteLine(enemy_ai.Attack());

        Console.WriteLine(enemy_ai.Death());

    }

}


이런식으로 파일을 나눠서 작업을 하면 좀 더 명확하게 작업을 분류할 수 있고 나중에 특정 작업에 대해서 접근 하기도 쉬워집니다. 다만 너무 많아지면 나중에 오히려 분류하기 힘들어지니 너무 많은 분류는 자제하시는게 좋을 것 같네요.

< 정리 >

partial class는 CLR 수준이 아닌 C# Compiler 수준에서 처리되므로 많은 partial class를 만들어도 실제 컴파일시에 하나의 클래스로 수집하여 인식하므로 단일 코드로 취합합니다.

소스코드 수준에서만 허용됩니다. binary 시엔 허용되지 않습니다.

무분별한 partial class 의 사용은 더욱 복잡해질 소지가 있습니다.

여러 파일에 partial 클래스로 선언시 그 클래스의 멤버들을 보는 것은 복잡하겠지만 클래스뷰를 이용하면 partial을 모아서 한 클래스처럼 보실 수 있습니다.



자세한 사항은 https://msdn.microsoft.com/en-us/library/wa80x488.aspx 참고 하시길 바랍니다.

'프로그래밍 > C#' 카테고리의 다른 글

Command Line Reader 클래스  (0) 2016.11.21
JsonFx 파일 가독성 높게 저장하기  (0) 2016.11.21

+ Recent posts