• Synchronous Blocking I/O
    : 지금까지의 서버는 하나의 클라이언트에 대해서만 요청을 처리할 수 있다.
    : 하나의 클라이언트 요청이 끝나기 전까지 다음 요청을 처리할 수 없다.
    => Iteration Server를 도입해야한다.

  • Multi Process Model


    - 7_server
     : fork()를 통한 프로세스 생성
     : 독립된 주소를 갖으므로 안정적이지만, 프로세스 복제에 대한 오버헤드가 크고, Context Switching 비용이 높다.
     : 데이터를 공유하기 위해 별도의 작업이 필요하다. (IPC)

    #include <stdio.h>
    #include <unistd.h>
    
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <linux/tcp.h>
    
    #include <arpa/inet.h>
    #include <string.h>
    
    #include <signal.h>    // signal
    #include <sys/wait.h>  // wait()
    
    // 4. 시그널은 중첩되서 발생하지 않는다. - 루프 도입
    // 5. 시그널 핸들러는 비동기적으로 동작합니다.
    //    핸들러 내부에서 블록킹 연산을 절대 수행하면 안됩니다.
    //  - waitpid
    
    void handleClient(int csock);
    void handleChildProcess(int signo)
    {
    	printf("Cleanup child Prcess!\n");
    	// while (wait(0) > 0)
    	while (waitpid(-1, 0, WNOHANG) > 0) 
    		;
    }
    
    int main() {
    	// 3. 자식 프로세스의 상태를 수거해야 한다.
    	//  -> 좀비 프로세스로 인한 메모리 누수를 방지할 수 있다.
    	// signal handler 등록
    
    	// sigaction
    	signal(SIGCHLD, handleChildProcess);
    
    
    	int sock = socket(PF_INET, SOCK_STREAM, 0);
    
    	struct sockaddr_in saddr;
    	saddr.sin_family = AF_INET;
    	saddr.sin_port = htons(4000);
    	saddr.sin_addr.s_addr = INADDR_ANY;
    
    	int option = true;
    	socklen_t optlen = sizeof(option);
    	setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &option, optlen);
    
    	int ret = bind(sock, (struct sockaddr *)&saddr, sizeof saddr);
    	if (ret != 0) {
    		perror("bind()");
    		return -1;
    	}
    
    	listen(sock, 5);
    
    	// 하나의 클라이언트만 요청을 처리할 수 있다.
    	//  => Iteration Server를 도입해야 한다.
    
    	while (1)
    	{
    		struct sockaddr_in caddr;
    		socklen_t size = sizeof(caddr);
    
    		int csock = accept(sock, (struct sockaddr *)&caddr, &size);
    
    		char *client_ip = inet_ntoa(caddr.sin_addr);
    		printf("클라이언트 %s 가 접속되었습니다.\n", client_ip);
    
    
    		// 하나의 클라이언트 요청이 끝나기 전까지 다음 요청을 처리할 수 없다.
    		//  1) pid = fork() - 리턴값을 통한 부모 / 자식 분기
    		//   - 자식 프로세스 : 0
    		//   - 부모 프로세스 : 자식 프로세스 pid 
    
    		//  2) fork()가 수행되었을 경우, 사용하지 않는 핸들을 닫아야 한다.
    
    		pid_t pid = fork();
    		if (pid == 0)
    			handleClient(csock);
    
    		close(csock);
    	}
    	
    	close(sock);
    }
    
    #include <stdlib.h>
    
    void handleClient(int csock)
    {
    	int n;
    	char buf[1024];
    
    	while (1) {
    		n = read(csock, buf, sizeof(buf));
    
    		if (n <= 0) {
    			printf("연결 종료!\n");
    			break;
    		}
    
    		n = write(csock, buf, n);
    	}
    
    	// 연결 종료
    	close(csock);
    	exit(0);
    } 

  • Multi Thread Model
    - 8_server
     : pthread 도입
     : kernel 영역을 제외한 복제로 스레드 생성에 대한 오버헤드가 낮고, Context Switching 비용도 낮다. 또 주소를 공유하기 때문에 자원공유가 편리하다.
     :하나의 Thread가 문제가 발생할 경우 전체 Process가 죽을 수 있다.

    // # g++ 8_server.cc -o server -lpthread
    #include <stdio.h>
    #include <unistd.h>
    
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <linux/tcp.h>
    
    #include <arpa/inet.h>
    #include <string.h>
    
    #include <signal.h>    // signal
    #include <sys/wait.h>  // wait()
    
    #include <pthread.h>
    // 멀티 프로세스 모델은 안전성은 뛰어나지만, 반면에 
    // 주소 공간의 분리에 의한 데이터 공유가 힘듭니다.
    // - IPC를 사용해야 한다.
    
    // => 스레드 모델을 도입하자. (pthread)
    void* handleClient(void* arg);
    
    int main() {
    	int sock = socket(PF_INET, SOCK_STREAM, 0);
    
    	struct sockaddr_in saddr;
    	saddr.sin_family = AF_INET;
    	saddr.sin_port = htons(4000);
    	saddr.sin_addr.s_addr = INADDR_ANY;
    
    	int option = true;
    	socklen_t optlen = sizeof(option);
    	setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &option, optlen);
    
    	int ret = bind(sock, (struct sockaddr *)&saddr, sizeof saddr);
    	if (ret != 0) {
    		perror("bind()");
    		return -1;
    	}
    
    	listen(sock, 5);
    
    	while (1)
    	{
    		struct sockaddr_in caddr;
    		socklen_t size = sizeof(caddr);
    
    		int csock = accept(sock, (struct sockaddr *)&caddr, &size);
    
    		char *client_ip = inet_ntoa(caddr.sin_addr);
    		printf("클라이언트 %s 가 접속되었습니다.\n", client_ip);
    
    		pthread_t thread;
    		pthread_create(&thread, 0, handleClient, &csock);
    
    		// 종료 처리를 해야 한다. - join, detach
    		// exit(0); 는 프로세스의 종료 -> return 으로 변경
    		// ★ 스레드가 온전히 종료할수 있도록 만드는 것이 중요
    		// 스레드도 결과적으로 fork 를 사용하므로 스레드에 관한 종료가 필요 join 또는 detach
    
    		// join vs detach?
    		// pthread_join을 통하여 정상적으로 종료된 보조스레드의 자원을 정상적으로 반환시켜주며,
    		// ptrhead_detach함수를 통하여 스레드가 종료시 자원을 정상적으로 반환시켜주도록 하는 함수로
    		// 메모리릭을 방지할 수 있다.
    
    		// - pthread_detach
    		// 생성된 thread에서 pthread_detach를 하면 join할 필요없이 생성된 thread가 할일을 다 마치고
    		// pthread_exit를 하는 순간 자원이 OS에 반납(보장됨)
    		// --> detach(떼어내다), 생성된 스레드를 메인 스레드에서 분리시킨다.
    		// - 호출방법 : 선호출, 후호출 2가지 방식
    		// 선호출 :  int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
    		// 후호출 : pthread_detach(p_thread);
    		// --> detach를 하려면 선호출로 하는것을 추천한다.
    		// why? 후호출을 하기전에 thread가 종료된다면 메모리릭 발생(희박한 가능성이지만...)
    
    		// - pthread_join
    		// 보조 스레드의 종료까지 대기해주는 함수(즉, 메인스레드의 종료 대기)
    		// int pthread_join(pthread_t th, void **thread_return);
    		// 첫번째 아규먼트 th는 기다릴(join)할 쓰레드 식별자이며, 두번째 아규먼트 thread_return은 쓰레드의 리턴(return) 값이다. thread_return 이 NULL 이 아닐경우 해다 포인터로 쓰레드 리턴 값을 받아올수 있다.
    		pthread_detach(thread);
    
    	}
    	
    	close(sock);
    }
    
    #include <stdlib.h>
    
    void* handleClient(void* arg)
    {
    	int csock = *(int*)arg;
    
    	int n;
    	char buf[1024];
    	while (1) {
    		n = read(csock, buf, sizeof(buf));
    
    		if (n <= 0) {
    			printf("연결 종료!\n");
    			break;
    		}
    
    		n = write(csock, buf, n);
    	}
    
    	// 연결 종료
    	close(csock);
    
    	return 0;
    }
    

    - 7_client ~ 8_client
    #include <unistd.h>
    
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <arpa/inet.h>
    
    #include <stdio.h>
    #include <string.h>
    
    #include <linux/tcp.h>
    
    int main()
    {
    	int sock = socket(PF_INET, SOCK_STREAM, 0);
    
    	struct sockaddr_in addr = {0, };
    	addr.sin_family = AF_INET;
    	addr.sin_port = htons(4000);
    	addr.sin_addr.s_addr   = inet_addr("0.0.0.0");
    
    	int option = true;
    	socklen_t optlen = sizeof(option);
    	setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &option, optlen);
    
    	int ret = connect(sock, (struct sockaddr*)&addr, sizeof addr);
    
    	if (ret != 0) {
    		printf("접속 실패!!\n");
    		perror("connect()");
    		return -1;
    	}
    
    	while (1) {
    		char buf[1024];
    		scanf("%s", buf);
    
    		ret = write(sock, buf, strlen(buf));
    		if (ret <= 0)
    			break;
    
    		ret = read(sock, buf, sizeof buf);
    		if (ret <= 0)
    			break;
    
    		write(1, buf, ret);
    		putchar('\n');
    	}
    
    	close(sock);
    }
    


  • I/O Models
    - blocking I/O vs. nonblocking I/O
     ■ blocking I/O
      : I/O 작업은 유저 영역에서 직접 수행할 수 없고, 커널 영역에서만 가능하다. 따라서 I/O 작업을 하기 위해서는 커널에 I/O 작업을 요청해야한다.
      : I/O 작업이 진행되는 동안 유저 영역 (process)의 작업은 중단한채 대기해야한다. 이처럼 I/O 동작 (데이터를 받을 준비, 데이터를 커널 영역에서 유저 영역으로 복사 등)으로 인해 프로세스가 block 상태가 되는 것을 blocking 된다고 한다.




     ■ nonblocking I/O
      : I/O 작업을 진행하는 동안 유저 프로세스의 작업을 중단시키지 않는다. 
      : 유저 프로세스가 커널의 read를 기다리는 것이 아니라 system call을 이용하여 반복적으로 요청하고, 이에 대한 버퍼값이 존재하면 유저 영역으로 복사해준다.
      : 반복적인 system call로 리소스가 남용




    - I/O multiplexing (select, poll) 

     : non-blocking 모델의 문제를 해결하기 위해 고안

     : I/O 처리가 필요한 파일 디스크립터 등을 가려내서 알려준다.




    - signal driven I/O (epoll, SIGIO), RTS(Real-Time Signal)
       Reactor Pattern
        : synchronous, non-blocking I/O handling and relies on an event notification interface

      
     Proactor Pattern
        : 
    asynchronous, non-blocking I/O operations, as provided by interfaces such as POSIX AIO




    synchronous I/O vs. asynchronous I/O (POSIX aio_functions)
     : 동기(synchronous)와 비동기(asynchronous)는 서로 메시지를 주고받는 상대방이 어떤 방식으로 통신을 하는가에 대한 개념이다.
     : I/O 통지모델에서 대화하는 주체들은 커널과 프로세스이다. 프로세스는 커널에게 I/O처리를 요청하고, 커널은 프로세스에게 I/O 상황을 통지한다. 우선 I/O 요청은 반드시 동일하게 처리될 수 밖에 없는 부분이고, 결국에 커널이 프로세스에게 어떤 방식으로 통지하느냐에 따라 동기형이냐 비동기형이 결정될 것이다.
     

      ■ synchronous I/O
       : 동기형 통지모델의 프로세스는 커널에게 지속적으로 현재 I/O 준비 상황을 체크한다.
       : 즉 커널이 준비되었는지를 계속 확인하여 동기화 하는 것이다. 따라서 동기형 통지모델에서 Notify를 적극적으로 진행하는 주체는 유저의 프로세스가 되며 커널은 수동적으로 유저 프로세스의 요청에 따라 현재의 상황을 보고한다.
     

      ■ asynchronous I/O

       : 비동기형 통지모델은 일단 커널에게 I/O작업을 맡기면 커널의 작업 진행사항에 대해서 프로세스가 인지할 필요가 없는 상황을 말한다.
       : 유저의 프로세스가 I/O 동기화를 신경쓸 필요가 없기에 비동기형이라고 부를 수 있다. 따라서 비동기형 통지모델에서 Notify의 적극적인 주체는 커널이 되며, 유저 프로세스는 수동적인 입장에서 자신이 할일을 하다가 통지가 오면 그때 I/O 처리를 하게 된다. 




  • I/O Model 비교


참고: http://ozt88.tistory.com/20


  • Server
    - 5_server
     : 클래스로 설계

     
    #include <stdio.h>
    #include <string.h>
    
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    
    #include <arpa/inet.h>
    #include <netinet/in.h>
    
    #include "Packet.h"
    
    int calculate(char op, int value1, int value2)
    {
    	switch (op) {
    		case '+': return value1 + value2;
    		case '-': return value1 - value2;
    		case '*': return value1 * value2;
    
    		default:
    			return -1;
    	}
    }
    
    // TCP의 경계가 모호하거나, 데이터가 도달하지 않는 문제를
    // 해결하기 위해서는 readn을 도입하면 된다.
    int readn(int sd, char* buf, int len)
    {
    	int n;
    	int ret;
    
    	n = len;
    	while (n > 0) 
    	{
    		ret = read(sd, buf, len);
    		if (ret < 0) return -1;
    		if (ret == 0) return len - n;
    
    		buf += ret;
    		n -= ret;
    	}
    
    	return len;
    }
    
    
    
    // 클래스를 설계할 때 지켜야하는 원칙 : SRP
    
    // 객체 지향 5대 원칙 - 로버트 C 마틴(클린 코드)
    // SRP: 단일 책임 원칙
    // OCP: 개방 폐쇄 원칙
    // LSP: 리스코프 치환 원칙
    // ISP: 인터페이스 분리 원칙
    // DIP: 의존관계 역전 원칙
    
    
    class NetAddress
    {
    	sockaddr_in addr;
    public:
    	NetAddress(const char* ip, short port)
    	{
    		memset(&addr, 0, sizeof addr);
    
    		addr.sin_family = AF_INET;
    		addr.sin_port = htons(port);
    		addr.sin_addr.s_addr = inet_addr(ip);
    	}
    
    	sockaddr* getRawAddress()
    	{
    		return (sockaddr*)&addr;
    	}
    
    };
    
    class Socket
    {
    	int sock;
    public:
    	Socket() { sock = socket(PF_INET, SOCK_STREAM, 0); }
    
    	explicit Socket(int sockFd) : sock(sockFd) {}
    
    	void bind(NetAddress* address)
    	{
    		// 다시 C 함수롤 호출해야 한다.	
    		::bind(sock, address->getRawAddress(), sizeof(sockaddr_in));
    	}
    
    
    	void setReuseAddress(bool b)
    	{
    		int option = b;
    		socklen_t optlen = sizeof(option);
    		setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &option, optlen);
    	}
    
    	void listen(int backlog)
    	{
    		::listen(sock, backlog);
    	}
    
    	Socket accept()
    	{
    		struct sockaddr_in caddr = {0, };
    		socklen_t clen = sizeof(caddr);
    		int csock = ::accept(sock, (struct sockaddr*)&caddr, &clen);
    
    		return Socket(csock);
    	}
    
    	inline int read(char* buf, size_t size)
    	{ return ::read(sock, buf, size); }
    
    	inline int write(char* buf, size_t len)
    	{ return ::write(sock, buf, len); }
    
    	inline int readn(char* buf, int len)
    	{ return ::readn(sock, buf, len); }
    
    	inline int close()
    	{ return ::close(sock); }
    
    };
    
    
    int main()
    {
    	// int sock = socket(PF_INET, SOCK_STREAM, 0);
    	Socket sock;
    
    	NetAddress address("0.0.0.0", 4000); 
    	// struct sockaddr_in saddr = {0, };
    	// saddr.sin_family = AF_INET;
    	// saddr.sin_port = htons(4000);
    	// saddr.sin_addr.s_addr = INADDR_ANY; // 0.0.0.0
    
    	sock.setReuseAddress(true);
    	// int option = true;
    	// socklen_t optlen = sizeof(option);
    	// setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &option, optlen);
    
    	sock.bind(&address);
    	// bind(sock, (struct sockaddr*)&saddr, sizeof saddr);
    
    	sock.listen(5);
    	// listen(sock, 5);
    
    	while (1) 
    	{
    
    		Socket csock = sock.accept();
    		// struct sockaddr_in caddr = {0, };
    		// socklen_t clen = sizeof(caddr);
    		// int csock = accept(sock, (struct sockaddr*)&caddr, &clen);
    
    		// char* cip = inet_ntoa(caddr.sin_addr);
    		// printf("Connected from %s\n", cip);
    
    		while (1) 
    		{
    			int len;
    			int ret = csock.readn((char*)&len, sizeof(len));
    
    			printf("len : %d\n", len);
    			if (ret != sizeof len) {
    				break;
    			} else if (ret == -1) {
    				perror("readn()");
    				break;
    			}
    
    			char buf[4096];
    			ret = csock.readn(buf, len);
    			if (ret != len) {
    				break;
    			} else if (ret == -1) {
    				perror("readn()");
    				break;
    			}
    
    			// 이제 데이터를 파싱하면 됩니다.
    			Packet packet(buf);
    			char op = packet.getByte();
    			int value1 = packet.getInt32();
    			int value2 = packet.getInt32();
    
    			printf("%2d%2c%-2d\n", value1, op, value2);
    			int result = calculate(op, value1, value2);
    
    			csock.write((char*)&result, sizeof(result));
    		}
    
    		//-----------------
    		csock.close();
    	}
    
    	sock.close();
    }
    


    - 6_server
     : SRP - Facade Pattern 적용

     
    #include <stdio.h>
    #include <string.h>
    
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    
    #include <arpa/inet.h>
    #include <netinet/in.h>
    
    #include "Packet.h"
    
    int calculate(char op, int value1, int value2)
    {
    	switch (op) {
    		case '+': return value1 + value2;
    		case '-': return value1 - value2;
    		case '*': return value1 * value2;
    
    		default:
    			return -1;
    	}
    }
    
    // TCP의 경계가 모호하거나, 데이터가 도달하지 않는 문제를
    // 해결하기 위해서는 readn을 도입하면 된다.
    int readn(int sd, char* buf, int len)
    {
    	int n;
    	int ret;
    
    	n = len;
    	while (n > 0) 
    	{
    		ret = read(sd, buf, len);
    		if (ret < 0) return -1;
    		if (ret == 0) return len - n;
    
    		buf += ret;
    		n -= ret;
    	}
    
    	return len;
    }
    
    
    
    // 클래스를 설계할 때 지켜야하는 원칙 : SRP
    
    // 객체 지향 5대 원칙 - 로버트 C 마틴(클린 코드)
    // SRP: 단일 책임 원칙
    // OCP: 개방 폐쇄 원칙
    // LSP: 리스코프 치환 원칙
    // ISP: 인터페이스 분리 원칙
    // DIP: 의존관계 역전 원칙
    
    
    class NetAddress
    {
    	sockaddr_in addr;
    public:
    	NetAddress(const char* ip, short port)
    	{
    		memset(&addr, 0, sizeof addr);
    
    		addr.sin_family = AF_INET;
    		addr.sin_port = htons(port);
    		addr.sin_addr.s_addr = inet_addr(ip);
    	}
    
    	sockaddr* getRawAddress()
    	{
    		return (sockaddr*)&addr;
    	}
    
    };
    
    class Socket
    {
    	int sock;
    public:
    	Socket() { sock = socket(PF_INET, SOCK_STREAM, 0); }
    
    	explicit Socket(int sockFd) : sock(sockFd) {} // explicit로 지정함으로써 암시적 형변환을 막는다.
    
    	void bind(NetAddress* address)
    	{
    		// 다시 C 함수롤 호출해야 한다.	
    		::bind(sock, address->getRawAddress(), sizeof(sockaddr_in));
    	}
    
    
    	void setReuseAddress(bool b)
    	{
    		int option = b;
    		socklen_t optlen = sizeof(option);
    		setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &option, optlen);
    	}
    
    	void listen(int backlog)
    	{
    		::listen(sock, backlog);
    	}
    
    	Socket accept()
    	{
    		struct sockaddr_in caddr = {0, };
    		socklen_t clen = sizeof(caddr);
    		int csock = ::accept(sock, (struct sockaddr*)&caddr, &clen);
    
    		return Socket(csock);
    	}
    
    	inline int read(char* buf, size_t size)
    	{ return ::read(sock, buf, size); }
    
    	inline int write(char* buf, size_t len)
    	{ return ::write(sock, buf, len); }
    
    	inline int readn(char* buf, int len)
    	{ return ::readn(sock, buf, len); }
    
    	inline int close()
    	{ return ::close(sock); }
    
    };
    
    // TCP 서버 프로그래밍을 하는데 있어서 필요한 클래스와 절차를
    // 단순화 시켜주는 상위 개념의 클래스를 제공하자.
    // -> Facade Pattern
    
    class TCPServer
    {
    	Socket sock;
    public:
    	void start(short port)
    	{
    		NetAddress address("0.0.0.0", port); 
    		sock.setReuseAddress(true);
    		sock.bind(&address);
    		sock.listen(5);
    
    		while (1) 
    		{
    
    			Socket csock = sock.accept();
    
    			while (1) 
    			{
    				int len;
    				int ret = csock.readn((char*)&len, sizeof(len));
    
    				printf("len : %d\n", len);
    				if (ret != sizeof len) {
    					break;
    				} else if (ret == -1) {
    					perror("readn()");
    					break;
    				}
    
    				char buf[4096];
    				ret = csock.readn(buf, len);
    				if (ret != len) {
    					break;
    				} else if (ret == -1) {
    					perror("readn()");
    					break;
    				}
    
    				// 이제 데이터를 파싱하면 됩니다.
    				Packet packet(buf);
    				char op = packet.getByte();
    				int value1 = packet.getInt32();
    				int value2 = packet.getInt32();
    
    				printf("%2d%2c%-2d\n", value1, op, value2);
    				int result = calculate(op, value1, value2);
    
    				csock.write((char*)&result, sizeof(result));
    			}
    
    			//-----------------
    			csock.close();
    		}
    
    		sock.close();
    
    
    	}
    
    };
    
    
    int main()
    {
    	TCPServer server;
    	server.start(4000);
    }
    


  • 계산기 Server - Client 만들기


  • Server
    - 1_server
     : 계산기 서버 초기 구조
     : log 메세지를 확인할 때, \n 빼먹으면 문제가 생길 가능성이 크다.
     : 프로토콜 설계
     : 데이터 파싱

    #include <stdio.h>
    
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    
    #include <arpa/inet.h>
    #include <netinet/in.h>
     
    int calculate(char op, int value1, int value2)
    {
    	switch (op) {
    		case '+': return value1 + value2;
    		case '-': return value1 - value2;
    		case '*': return value1 * value2;
    
    		default:
    			return -1;
    	}
    }
    
    int main()
    {
    	int sock = socket(PF_INET, SOCK_STREAM, 0);
    
    	struct sockaddr_in saddr = {0, };
    	saddr.sin_family = AF_INET;
    	saddr.sin_port = htons(4000);
    	saddr.sin_addr.s_addr = INADDR_ANY; // 0.0.0.0
    
    	int option = true;
    	socklen_t optlen = sizeof(option);
    	setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &option, optlen);
    
    	bind(sock, (struct sockaddr*)&saddr, sizeof saddr);
    	listen(sock, 5);
    
    	while (1) 
    	{
    		struct sockaddr_in caddr = {0, };
    		socklen_t clen = sizeof(caddr);
    		int csock = accept(sock, (struct sockaddr*)&caddr, &clen);
    
    		char* cip = inet_ntoa(caddr.sin_addr);
    		printf("Connected from %s\n", cip);
    
    
    		// TCP 는 데이터의 경계가 존재하지 않는다.
    		//-------------
    		char buf[1024];
    		int ret = read(csock, buf, 1024);
    		printf("ret : %d\n", ret);
    		if (ret == 0) {
    			close(csock);
    			break;
    		} else if (ret == -1) {
    			perror("read()");
    			close(csock);
    			break;
    		}
    
    		char* p = buf;
    		char op = *p;
    		++p;
    
    		int value1 = *(int*)p;
    		p += sizeof(int);
    
    		int value2 = *(int*)p;
    		p += sizeof(int);
    
    		printf("%2d%2c%2d\n", value1, op, value2);
    		int result = calculate(op, value1, value2);
    
    		write(csock, &result, sizeof result);
    
    		//-----------------
    		close(csock);
    	}
    
    	close(sock);
    }
    


    - 2_server
     : 수식 여러번 받아서 계산을 할 수 있는 구조
     : tcp는 write와 read가 병합되는 형태의 스트림 단위의 처리방식, 데이터의 경계가 존재하지 않는다.(반면 udp는 패킷 덩어리로 보낸만큼 간다.)
     : 100번 보낸다고해서 100번을 받는것이 아니다. -> 패킷을 분리해야한다. -> 무조건 패킷의 길이가 들어가야한다.
     : 길이는 약 3배정도 여유있게 잡는 것이 좋다.
     : 서버는 온전하게 동작하게 만든 뒤, 고성능 서버로 바꾸는 것!

    #include <stdio.h>
    
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    
    #include <arpa/inet.h>
    #include <netinet/in.h>
    
    int calculate(char op, int value1, int value2)
    {
    	switch (op) {
    		case '+': return value1 + value2;
    		case '-': return value1 - value2;
    		case '*': return value1 * value2;
    
    		default:
    			return -1;
    	}
    }
    
    int main()
    {
    	int sock = socket(PF_INET, SOCK_STREAM, 0);
    
    	struct sockaddr_in saddr = {0, };
    	saddr.sin_family = AF_INET;
    	saddr.sin_port = htons(4000);
    	saddr.sin_addr.s_addr = INADDR_ANY; // 0.0.0.0
    
    	int option = true;
    	socklen_t optlen = sizeof(option);
    	setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &option, optlen);
    
    	bind(sock, (struct sockaddr*)&saddr, sizeof saddr);
    	listen(sock, 5);
    
    	while (1) 
    	{
    		struct sockaddr_in caddr = {0, };
    		socklen_t clen = sizeof(caddr);
    		int csock = accept(sock, (struct sockaddr*)&caddr, &clen);
    
    		char* cip = inet_ntoa(caddr.sin_addr);
    		printf("Connected from %s\n", cip);
    
    
    		// TCP 는 데이터의 경계가 존재하지 않는다.
    		//-------------
    
    		// int len;
    		// int ret = read(csock, &len, sizeof len);
    
    		char buf[4096];
    		int ret = read(csock, buf, sizeof buf);
    		printf("ret : %d\n", ret);
    		if (ret == 0) {
    			close(csock);
    			break;
    		} else if (ret == -1) {
    			perror("read()");
    			close(csock);
    			break;
    		}
    
    
    		while (ret > 0) {
    			char* p = buf;
    
    			int len = *(int*)p;
    			p += sizeof(int);
    
    			char op = *p;
    			++p;
    
    			int value1 = *(int*)p;
    			p += sizeof(int);
    
    			int value2 = *(int*)p;
    			p += sizeof(int);
    
    			ret -= len;
    
    			printf("%2d%2c%2d\n", value1, op, value2);
    			int result = calculate(op, value1, value2);
    
    			write(csock, &result, sizeof result);
    		}
    
    		//-----------------
    		close(csock);
    	}
    
    	close(sock);
    }
    


    - 3_server

    9

    '10'

    '20'

    '-'


     
    : 2_server은 위의 패킷(스트림)이 올 경우 9에서 끊어질 수가 있다. -> readn 구현이 필요 -> block이 필요
    : TCP의 경계가 모호하거나, 데이터가 도달하지 않는 문제를 해결하기 위해서는 readn을 도입하면 된다.

    : ★★readn, tcp 패킷자체에 대한 프로토콜이 잘못될 가능성은 거의 없어진다. 즉, 길이 base로 만들어야한다.

    #include <stdio.h>
    
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    
    #include <arpa/inet.h>
    #include <netinet/in.h>
    
    int calculate(char op, int value1, int value2)
    {
    	switch (op) {
    		case '+': return value1 + value2;
    		case '-': return value1 - value2;
    		case '*': return value1 * value2;
    
    		default:
    			return -1;
    	}
    }
    
    // TCP의 경계가 모호하거나, 데이터가 도달하지 않는 문제를
    // 해결하기 위해서는 readn을 도입하면 된다.
    int readn(int sd, char* buf, int len)
    {
    	int n;
    	int ret;
    
    	n = len;
    	while (n > 0) 
    	{
    		ret = read(sd, buf, len);
    		if (ret < 0) return -1;
    		if (ret == 0) return len - n;
    
    		buf += ret;
    		n -= ret;
    	}
    
    	return len;
    }
    
    
    
    int main()
    {
    	int sock = socket(PF_INET, SOCK_STREAM, 0);
    
    	struct sockaddr_in saddr = {0, };
    	saddr.sin_family = AF_INET;
    	saddr.sin_port = htons(4000);
    	saddr.sin_addr.s_addr = INADDR_ANY; // 0.0.0.0
    
    	int option = true;
    	socklen_t optlen = sizeof(option);
    	setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &option, optlen);
    
    	bind(sock, (struct sockaddr*)&saddr, sizeof saddr);
    	listen(sock, 5);
    
    	while (1) 
    	{
    		struct sockaddr_in caddr = {0, };
    		socklen_t clen = sizeof(caddr);
    		int csock = accept(sock, (struct sockaddr*)&caddr, &clen);
    
    		char* cip = inet_ntoa(caddr.sin_addr);
    		printf("Connected from %s\n", cip);
    
    
    		while (1) 
    		{
    			int len;
    			int ret = readn(csock, (char*)&len, sizeof(len));
    			if (ret == 0) {
    				close(csock);
    				break;
    			} else if (ret == -1) {
    				perror("readn()");
    				close(csock);
    				break;
    			}
    
    			char buf[4096];
    			ret = readn(csock, buf, len);
    			if (ret == 0) {
    				break;
    			} else if (ret == -1) {
    				perror("readn()");
    				break;
    			}
    
    			// 이제 데이터를 파싱하면 됩니다.
    			char* p = buf;
    			char op = *p;
    			++p;
    
    			int value1 = *(int*)p;
    			p += sizeof(int);
    
    			int value2 = *(int*)p;
    			p += sizeof(int);
    
    			printf("%2d%2c%-2d\n", value1, op, value2);
    			int result = calculate(op, value1, value2);
    
    			write(csock, &result, sizeof result);
    		}
    	
    		//-----------------
    		close(csock);
    	}
    
    	close(sock);
    }
    

    - 4_server
     : 패킷 객체화

    #include <stdio.h>
    
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    
    #include <arpa/inet.h>
    #include <netinet/in.h>
    
    #include "Packet.h"
    
    int calculate(char op, int value1, int value2)
    {
    	switch (op) {
    		case '+': return value1 + value2;
    		case '-': return value1 - value2;
    		case '*': return value1 * value2;
    
    		default:
    			return -1;
    	}
    }
    
    // TCP의 경계가 모호하거나, 데이터가 도달하지 않는 문제를
    // 해결하기 위해서는 readn을 도입하면 된다.
    int readn(int sd, char* buf, int len)
    {
    	int n;
    	int ret;
    
    	n = len;
    	while (n > 0) 
    	{
    		ret = read(sd, buf, len);
    		if (ret < 0) return -1;
    		if (ret == 0) return len - n;
    
    		buf += ret;
    		n -= ret;
    	}
    
    	return len;
    }
    
    
    
    int main()
    {
    	int sock = socket(PF_INET, SOCK_STREAM, 0);
    
    	struct sockaddr_in saddr = {0, };
    	saddr.sin_family = AF_INET;
    	saddr.sin_port = htons(4000);
    	saddr.sin_addr.s_addr = INADDR_ANY; // 0.0.0.0
    
    	int option = true;
    	socklen_t optlen = sizeof(option);
    	setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &option, optlen);
    
    	bind(sock, (struct sockaddr*)&saddr, sizeof saddr);
    	listen(sock, 5);
    
    	while (1) 
    	{
    		struct sockaddr_in caddr = {0, };
    		socklen_t clen = sizeof(caddr);
    		int csock = accept(sock, (struct sockaddr*)&caddr, &clen);
    
    		char* cip = inet_ntoa(caddr.sin_addr);
    		printf("Connected from %s\n", cip);
    
    
    		while (1) 
    		{
    			int len;
    			int ret = readn(csock, (char*)&len, sizeof(len));
    
    			printf("len : %d\n", len);
    			if (ret != sizeof len) {
    				close(csock);
    				break;
    			} else if (ret == -1) {
    				perror("readn()");
    				close(csock);
    				break;
    			}
    
    			char buf[4096];
    			ret = readn(csock, buf, len);
    			if (ret != len) {
    				break;
    			} else if (ret == -1) {
    				perror("readn()");
    				break;
    			}
    
    			// 이제 데이터를 파싱하면 됩니다.
    			Packet packet(buf);
    			char op = packet.getByte();
    			int value1 = packet.getInt32();
    			int value2 = packet.getInt32();
    
    			printf("%2d%2c%-2d\n", value1, op, value2);
    			int result = calculate(op, value1, value2);
    
    			write(csock, &result, sizeof result);
    		}
    	
    		//-----------------
    		close(csock);
    	}
    
    	close(sock);
    }
    

    ■ 패킷 클래스 

     ∴ 게임에서는? 

      : 패킷을 만드는 클래스를 하나 둔다.
      : Builder pattern? 객체가 만들어지는 공정은 동일하지만, 내부적으로 표현이 달라지는 형태의 객체를 만들 때 사용(ex. StringBuilder), 코드 반복을 줄일 수 있다.

    #ifndef PACKET_H
    #define PACKET_H
    
    class Packet
    {
    public:
    	inline Packet(char* buf) : start(buf), pos(buf) {}
    
    	inline int getInt32() const
    	{
    		int ret = *(int*)pos;
    		pos += sizeof(int);
    
    		return ret;
    	}
    
    	inline char getByte() const 
    	{
    		char ret = *(char*)pos;
    		++pos;
    
    		return ret;
    	}
    
    	inline Packet& putByte(char value)
    	{
    		*pos = value;
    		++pos;
    
    		return *this;
    	}
    
    	inline Packet& putInt32(int value)
    	{
    		*(int*)pos = value;
    		pos += sizeof(int);
    
    		return *this;
    	}
    
    	inline int build()
    	{
    		int len = pos - start;
    		return len;
    	}
    
    
    private:
    	char* start;
    	mutable char* pos;
    };
    
    
    #endif
    
    


  • Client
    - 1_client
     : 계산기 클라이언트 초기 구조
     : 패킷 만들기

     
     #include <unistd.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    
    #include <errno.h>
    #include <stdio.h>
    #include <string.h>
    
    #include <iostream>
    using namespace std;
    
    // 계산기 클라이언트를 만들어보자.
    // 1. 프로토콜을 설계해야 한다.
    //  [op:1][lvalue:4][rvalue:4]
    int main()
    {
    	int sd = socket(PF_INET,
    			SOCK_STREAM,  
    			IPPROTO_TCP);
    
    	struct sockaddr_in addr = {0, };
    	addr.sin_family = AF_INET; 
    	addr.sin_port = htons(4000);
    	addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    
    	int ret = connect(sd, (struct sockaddr*)&addr, sizeof(addr)); 
    	if (ret == -1) {
    		perror("connect()");
    		return -1;
    	}
    
    	printf("Connection succeed!!\n");
    	//-------------
    	char op = '+';
    	int value1 = 10, value2 = 20;
    	// cout << "value 1: "; cin >> value1;
    	// cout << "value 2: "; cin >> value2;
    	// cout << "op : ";     cin >> op;
    
    	// 패킷을 만들어야 한다.
    	char buf[1024];
    	char* p = buf;
    
    	*p = op;
    	++p;
    
    	*(int*)p = value1;
    	p += sizeof(int);
    
    	*(int*)p = value2;
    	p += sizeof(int);
    	int size = p - buf;
    
    	for (int i = 0 ; i < 100; i++) {
    		write(sd, buf, size);
    	}
    
    	for (int i = 0 ; i < 100; i++) {
    		int result;
    		int n = read(sd, &result, sizeof result);
    		cout << i+1 << "- result : " << result << endl;
    	}
    
    	close(sd);
    }
    


    - 2_client ~ 3_client
     : 수식을 여러번 보내서, 여러 결과값을 받을 수 있는 구조
     : 패킷의 길이까지 붙여서 패킷 만들기

     
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    
    #include <errno.h>
    #include <stdio.h>
    #include <string.h>
    
    #include <iostream>
    using namespace std;
    
    // 계산기 클라이언트를 만들어보자.
    // 1. 프로토콜을 설계해야 한다.
    //  [op:1][lvalue:4][rvalue:4]
    int main()
    {
    	int sd = socket(PF_INET,
    			SOCK_STREAM,  
    			IPPROTO_TCP);
    
    	struct sockaddr_in addr = {0, };
    	addr.sin_family = AF_INET; 
    	addr.sin_port = htons(4000);
    	addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    
    	int ret = connect(sd, (struct sockaddr*)&addr, sizeof(addr)); 
    	if (ret == -1) {
    		perror("connect()");
    		return -1;
    	}
    
    	printf("Connection succeed!!\n");
    	//-------------
    	char op = '+';
    	int value1 = 10, value2 = 20;
    	// cout << "value 1: "; cin >> value1;
    	// cout << "value 2: "; cin >> value2;
    	// cout << "op : ";     cin >> op;
    
    	// 패킷을 만들어야 한다.
    	char buf[1024];
    	char* p = buf;
    
    	*p = op;
    	++p;
    
    	*(int*)p = value1;
    	p += sizeof(int);
    
    	*(int*)p = value2;
    	p += sizeof(int);
    	int size = p - buf;
    	for (int i = 0 ; i < 1000; i++) {
    		write(sd, &size, sizeof size);
    		write(sd, buf, size);
    	}
    
    	for (int i = 0 ; i < 1000; i++) {
    		int result;
    		int n = read(sd, &result, sizeof result);
    		cout << i+1 << "- result : " << result << endl;
    	}
    
    	close(sd);
    }
    

    - 4_client
     : 패킷 클래스로 만들기
     : 체이닝 방식 권장

     
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    
    #include <errno.h>
    #include <stdio.h>
    #include <string.h>
    
    #include <iostream>
    using namespace std;
    
    #include "Packet.h"
    
    
    int main()
    {
    	int sd = socket(PF_INET,
    			SOCK_STREAM,  
    			IPPROTO_TCP);
    
    	struct sockaddr_in addr = {0, };
    	addr.sin_family = AF_INET; 
    	addr.sin_port = htons(4000);
    	addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    
    	int ret = connect(sd, (struct sockaddr*)&addr, sizeof(addr)); 
    	if (ret == -1) {
    		perror("connect()");
    		return -1;
    	}
    
    	printf("Connection succeed!!\n");
    	//-------------
    	char op = '+';
    	int value1 = 10, value2 = 20;
    
    	// 패킷을 만들어야 한다.
    	char buf[1024];
    
    	// 체이닝 방식 - 완전한 객체로
    	Packet packet = Packet(buf).putByte(op)
    								.putInt32(value1)
    								.putInt32(value2);
    	// vs
    	// 불완전한 객체로 존재
    	// Packet packet(buf);
    	// packet.putByte(op);
    	// packet.putInt32(value1);
    	// packet.putInt32(value2);
    
    	int size = packet.build();
    
    	/*
    	char* p = buf;
    	*p = op;
    	++p;
    
    	*(int*)p = value1;
    	p += sizeof(int);
    
    	*(int*)p = value2;
    	p += sizeof(int);
    	int size = p - buf;
    	*/
    	for (int i = 0 ; i < 10000; i++) {
    		write(sd, &size, sizeof size);
    		write(sd, buf, size);
    	}
    
    	for (int i = 0 ; i < 10000; i++) {
    		int result;
    		int n = read(sd, &result, sizeof result);
    		cout << i+1 << "- result : " << result << endl;
    	}
    
    	close(sd);
    }
    


  • Server
    - 1_server
     : 기본 서버 구조

     
    #include <stdio.h>
    
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    
    #include <arpa/inet.h>
    #include <netinet/in.h>
    
    int main()
    {
    	// 1. socket 생성		
    	int sock = socket(PF_INET, SOCK_STREAM, 0);
    
    	// 2. bind() : 소켓 주소 지정
    	struct sockaddr_in saddr = {0, };
    	saddr.sin_family = AF_INET;
    	saddr.sin_port = htons(4000);
    
    	// 연결을 받을 주소
    	saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    	bind(sock, (struct sockaddr*)&saddr, sizeof saddr);
    
    	// 3. 연결 대기 상태로 만든다. : listen
    	listen(sock, 5);
    
    
    	// 4. 클라이언트 요청 수락 : accept
    	struct sockaddr_in caddr = {0, };
    	socklen_t clen = sizeof(caddr);
    	int csock = accept(sock, (struct sockaddr*)&caddr, &clen);
    
    	
    	printf("연결이 수립되었습니다.\n");
    
    	close(csock);
    	close(sock);
    }
    


    - 2_server
     : 여러 클라이언트 받아들이기

     
    #include <stdio.h>
    
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    
    #include <arpa/inet.h>
    #include <netinet/in.h>
    
    int main()
    {
    	int sock = socket(PF_INET, SOCK_STREAM, 0);
    
    	struct sockaddr_in saddr = {0, };
    	saddr.sin_family = AF_INET;
    	saddr.sin_port = htons(4000);
    	// saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    	// 문제점 : 127.0.0.1로 오는 연결만 받을 수 있다.
    
    	// 해결방법 : INADDR_ANY(0.0.0.0);
    	saddr.sin_addr.s_addr = INADDR_ANY; // 0.0.0.0
    
    	bind(sock, (struct sockaddr*)&saddr, sizeof saddr);
    	listen(sock, 5);
    
    	while (1) 
    	{
    		struct sockaddr_in caddr = {0, };
    		socklen_t clen = sizeof(caddr);
    		int csock = accept(sock, (struct sockaddr*)&caddr, &clen);
    
    		char* cip = inet_ntoa(caddr.sin_addr);
    
    		printf("Connected from %s\n", cip);
    		close(csock);
    	}
    
    	close(sock);
    }
    

  • - 3_server
     : bind(): Address already in use 해결
     
    #include <stdio.h>
    
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    
    #include <arpa/inet.h>
    #include <netinet/in.h>
    
    // 1. bind() : Adress already in use
    //  먼저 연결을 종료한 측의 소켓은 유지되어야 한다. - TIME_WAIT
    //  => TIME_WAIT 상태가 종료 되기 이전에 해당 포트로 서버를 바인드
    //     하는 것이 불가능하다.
    
    // 2. TIME_WAIT : 같은 포트의 연결을 손상 시킬 수 있다.
    // 3. 클라이언트 포트는 동일한 포트로 바인딩 되지 않는다.
    // 4. 최신 네트워크 커널의 구현은 SYN의 시퀀스 번호가 TIME-WAIT 상태의
    //    연결로부터 마지막 시퀀스 번호보다 클 경우에만 연결을 허용한다.
    // 5. TIME_WAIT은 먼저 연결을 종료한 쪽에서 발생한다.
    //    클라이언트는 포트가 변경되기 때문에 문제가 없다.
    
    int main()
    {
    	int sock = socket(PF_INET, SOCK_STREAM, 0);
    
    	struct sockaddr_in saddr = {0, };
    	saddr.sin_family = AF_INET;
    	saddr.sin_port = htons(4000);
    	saddr.sin_addr.s_addr = INADDR_ANY; // 0.0.0.0
    
    	//----------------
    	// 6. 해결방법
    	// 서버는 약속된 포트로부터 연결을 받아야 하므로
    	// 반드시 주소를 재할당 할 수 있도록 변경해주어야 한다.
    	int option = true;
    	socklen_t optlen = sizeof(option);
    	setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &option, optlen);
    	//-----------------
    
    	int ret = bind(sock, (struct sockaddr*)&saddr, sizeof saddr);
    	if (ret == -1) {
    		perror("bind()");
    		return -1;
    	}
    
    
    
    	listen(sock, 5);
    
    	struct sockaddr_in caddr = {0, };
    	socklen_t clen = sizeof(caddr);
    	int csock = accept(sock, (struct sockaddr*)&caddr, &clen);
    
    	close(csock);
    	close(sock);
    }
    

  • Client
    - 1_client ~ 2_client
     : 기본 클라이언트 구조

     
    #include <stdio.h>
    
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    
    #include <errno.h>
    #include <string.h>
    
    int main()
    {
    	int sd = socket(PF_INET,
    			SOCK_STREAM,  
    			IPPROTO_TCP);
    
    	struct sockaddr_in addr = {0, };
    	addr.sin_family = AF_INET; 
    	addr.sin_port = htons(4000);
    	addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    
    	int ret = connect(sd, (struct sockaddr*)&addr, sizeof(addr)); 
    	if (ret == -1) {
    		perror("connect()");
    		return -1;
    	}
    
    	printf("Connection succeed!!\n");
    	close(sd);
    }
    


    - 3_client
     : TIME_WAIT 문제 확인

     
    #include <stdio.h>
    
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    
    #include <errno.h>
    #include <string.h>
    
    int main()
    {
    	int sd = socket(PF_INET,
    			SOCK_STREAM,  
    			IPPROTO_TCP);
    
    	struct sockaddr_in addr = {0, };
    	addr.sin_family = AF_INET; 
    	addr.sin_port = htons(4000);
    	addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    
    	int ret = connect(sd, (struct sockaddr*)&addr, sizeof(addr)); 
    	if (ret == -1) {
    		perror("connect()");
    		return -1;
    	}
    
    	printf("Connection succeed!!\n");
    
    	sleep(5);
    	close(sd);
    }
    
    


  • 리눅스 vs. 윈도우
    : 편견을 갖을 필요 없다. 각각의 장단점이 있다.
    : 시스템콜 기반과 표준라이브러리 IO와 차이
    : 시스템콜 메커니즘 자체는 리눅스와 윈도우가 같다.

    : 리눅스는 무료 오픈소스로 파일 디스크립터, 오픈파일 등의 세팅을 바꾸어줄수 있지만 윈도우즈는 변경하기가 힘들고, 가격이 비싸지만 (windows server 같은 경우 cpu당 라이센스 비용 지불), 개발이 쉽다.
    : 바로 함수를 호출하는 메커니즘 vs 하나를 거쳐 호출하는 메커니즘
    : mono vs micro
    : 커널을 다시 빌드해야함 vs 모듈처럼 사용 가능하지만 성능 저하
    : android 코드를 분석해보면 bionic -> 각각의 CPU에 대해 존재하는 이유는 시스템콜 때문이다.



  • 시스템 콜

    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    
    #include <stdio.h >
    
    // stdin, stdout, stderr : FILE*
    // STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO : int
    
    // task_struct
    
    // 1. socket programming 저수준 IO를 사용합니다.
    // 2. 저수준 IO : 운영체제에서 제공하는 IO 관련 API
    //   Windows : Win32/64 API
    //   Linux   : System Call
    
    // 3. 프로세스를 통해 파일을 관리한다.
    //   ulimit -a
    
    int main()
    {
    	// FILE* fp = fopen("sample", "r");
    	int fd = open("sample", O_RDONLY);
    
    	printf("fd : %d\n", STDIN_FILENO);
    	printf("fd : %d\n", STDOUT_FILENO);
    	printf("fd : %d\n", STDERR_FILENO);
    	printf("fd : %d\n", fd);
            // fd 출력시 3인 이유는 표준 입출력, 에러에 대한 fd가 열려있기 때문이다
            // VFS라고 부른다 -> 다형성
            // 내부적으로 파일, 디바이스를 다루는 모든 방식은 파일로 되어있다. 이를 사용하기 위해서는 각각을 모두 열어야한다. -> 동적바인딩을 통해서
            // 리눅스는 array로 파일디스크립터 관리, 윈도우즈는 파일디스크립터 핸들
    }
    

    버퍼링 정책 3가지

     1. Full Buffering : Buffer가 가득 차면 화면에 출력    → File I/O
     2. Line Buffering : ‘\n’이 오면 화면에 출력          → printf의 경우에 해당
     3. Non Bufering : 버퍼링을 하지 않음.              → fprintf(stderr,””);에 해당

      pirntf("error");

      while(1); 

      //출력하지 않는다.


      pirntf("error\n");

      while(1); 

      //출력한다.

    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    
    #include <stdio.h>
    
    int main()
    {
    	int fd = open("sample", O_RDONLY);
    	printf("fd : %d\n", fd);
    
    
    	// 문제점 
    	// 1. 에러 발생시 -1 리턴
    	// 2. 버퍼의 크기가 너무 작을 경우, 빈번한 컨텍스트 스위칭으로 인한
    	//    성능 저하가 있다.
    #if 0
    	char ch;
    	while (read(fd, &ch, sizeof ch) > 0)
    		putchar(ch);
    #endif
    
    	// 3. read(fd, buf, size)
    	//	size : 버퍼의 최대 크기
    	// 4. write(fd, buf, size)
    	//  size : 쓰고자 하는 데이터의 실제 양
    
    	char buf[128];
    	int ret;
    	while ((ret = read(fd, buf, sizeof buf)) > 0) {
    		write(1, buf, ret);
    
    		// buf[ret] = '\0';
    		// printf("%s", buf);
    	}
    
    	close(fd);
            // close를 안해도 문제가 없었던 이유
            // 프로세스가 종료되면서 소유하고 있던 자원을 모두 해지
    
            // app영역에서는 프로세스 종료로 OS가 메모리를 수거함. 대부분 커널의 누수
            // 정적분석은 new와 delete쌍을 찾아서 싱글톤은 메모리 누수로 인식함
            // 동적분석은 비쌈
    
    }
    
    
    
    
    


  • 클라이언트 작성하기
    - http 통신

    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    
    #include <errno.h>
    #include <string.h>
    
    int main()
    {
    	// 1. 소켓 생성 : socket()
    	int sd = socket(PF_INET,      //  프로토콜
    									SOCK_STREAM,  //  TCP(SOCK_STREAM), UDP(SOCK_DGRAM)
    									IPPROTO_TCP); //  하위 프로토콜(생략, 0 으로 지정 가능)
    	
    	// 2. 서버 주소 지정 : 초기화 이전에 0으로 초기화해야 한다.
    	// 문자열 ip -> 32 bit : inet_addr
    	struct sockaddr_in addr = {0, };
    	addr.sin_family = AF_INET; 
    	addr.sin_port = htons(80);
    	addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    
    	// 3. 접속 수행 
    	int ret = connect(sd, (struct sockaddr*)&addr, sizeof(addr)); 
    	if (ret == -1) {
    		// 에러 처리 방법 3가지
    		// 1. printf("connect() error : %d\n", errno);
    
    		// 2. char* msg = strerror(errno);
    		//    printf("connect() error : %s\n", msg);
    		
    		// 3.
    		perror("connect()");
    		goto err;
    	}
    
    	{
    		printf("Connection succeed!!\n");
    
    		// "GET \n\n"
    		char buf[1024] = "GET /\n\n";
    		write(sd, buf, strlen(buf));
    
    		int len;
    		while ((len = read(sd, buf, sizeof(buf))) > 0) {
    			write(1, buf, len);
    		}
    	}
    
    err:
    	// 4. 접속 종료
    	close(sd);
    }


    - DNS

    #include <stdio.h>
    // domain -> ip address
    // : gethostbyname
    
    // gethostbyname
    #include <netdb.h>    
    
    // inet_ntoa: 네트워크로부터 온 패킷을 아스키코드로 바꾸어 주는 것
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    
    int main()
    {
    	struct hostent* host = gethostbyname("www.google.com");
    
    	// name
    	printf("host name : %s\n", host->h_name);
    
    	// alias
    	for (int i = 0 ; host->h_aliases[i] ; i++) {
    		printf("Alias[%d] : %s\n", i + 1, host->h_aliases[i]);
    	}
    
    	for (int i = 0 ; host->h_addr_list[i] ; i++) {
    		printf("Address[%d] : %s\n", 
    				i+1,
    				inet_ntoa(*(struct in_addr*)host->h_addr_list[i]) );
    	}
    }
    


  • Multi Process vs. Multi Thread
    - Multi Process Model
     ■ 장점
      : 독립된 주소공간을 갖고 있기 때문에 안정적이다.
     ■ 단점
      : Context Switching 비용이 높다.
      : 데이터를 공유하기 위해 작업이 필요하다.

    - Multi Thread Model

     ■ 장점
      : Context Switching 비용이 낮다.
      : 주소를 공유하기 때문에 자원공유가 좋다.
     ■ 단점
      : 하나의 Thread가 문제가 발생할 경우 전체 Process가 죽을 수 있다.

    : 다중 스레드와 다중 프로세스가 전체 시스템의 성능에 영향을 미치는 것은 아니지만 어느정도 영향이 있다. (진짜로 영향이 있는 것은 엔진 문제)
    : Linux의 경우 fork 모델이 잘 되어 있어서 다중 프로세스 생성 및 관리가 편하지만 Windows의 경우 fork 모델이 적용되어있지 않아 멀티 스레드를 선호한다.

  • C#, C++, JAVA 서버
    : 구현 방법이 언어가 다를 뿐 거의 차이가 없지만, 그 언어의 철학을 이해할 필요가 있다.
    ex)
    이벤트 처리를 할 때 이벤트 핸들러를 전달할 때 차이
    C++, C: 함수 포인터
    C# : 델리게이트
    JAVA: Listener 인터페이스 구현

    - 왜 게임 Server는 C++을 주로 사용하여 만드는가? 
     : java의 경우 nio를 통해 C++과의 io 접근 속도 차이를 많이 줄였다. 조금은 C++이 빠르겠지만, 성능 결정에 큰 요소가 아니다. 그럼
    도 C++을 사용하는 이유는? GC(가비지 컬렉터) 때문이다. 서버는 객체가 생성,소멸이 빈번하게 일어날 수 밖에 없다. C++ 경우는 사용자가 메모리 해지하고 관리할 수 있지만, C#, JAVA의 언어의 경우 GC에 의한 메모리 관리로 성능저하가 발생할 수 있다.

    - C로 Server? C++ Server 만 답인가? 
     : C 같은 경우는 코드의 양이 많아져서 관리하기가 힘들다. C++은 문법의 양이 너무 많고, 계속 나오고 있다. 그러므로 꼭 C++를 사용하여 서버를 구현하라는 법은 없으며 서버의 특성에 따라 유동적으로 언어를 선택하여 구현하면 된다.


  • 구조체 정렬
      : CPU가 데이터를 처리하는 방식에 효율적인 얼라이먼트를 사용
      : 얼라이먼트를 1byte로 변경?
      -> 리눅스는 각각의 구조체마다 __attribute__((packed))
      -> 윈도우즈는 #pragma pack(push, 1)

    #include <stdio.h>
    
    // 구조체 정렬 : 내부의 필드 중 가장 큰 크기에 기반하여 정렬한다.
    struct AAA {
      int a;
      char b;
    };
    // 5byte가 아닌 8byte로
    // 본래 시스템에 맞게 5byte가 아닌 8byte로 패딩이 채워져 나와야 하지만
    // 네트워크 개념에서 빈 패딩은 오버패킷을 발생하므로 packed를 채워 5바이트로 만들수 있다.
    
    struct AAA {
      int a;
      char b;
    } __attribute__((packed));
    // 5byte
    
    #if 0
    // Windows
    struct BBB {
      int a;
      char b;
      double c;
    };
    // 16byte
    
    #pragma pack(push, 1)
    struct BBB {
      int a;
      char b;
      double c;
    };
    #pragma pack(pop)
    // 13byte
    
    #endif
    
    int main()
    {
      printf("sizeof(AAA) : %ld\n", sizeof(struct AAA));
    }


  • Little Endian vs. Big Endian
    : network byte order는 항상 고려해야한다.
    -> Network 에서는 Big Endian을 사용하기로 규약하였다. 그렇기 때문에 Little Endian을 사용하는 시스템(리눅스)의 경우 Big Endian으로 변환해야한다.
         

    #include <stdio.h>
    // 엔디안(endian) : CPU가 데이터를 메모리에 저장하는 방식
    //                  CPU가 데이터를 해석하는 방식
    
    // 1) Big Endian: 상위 바이트의 값을 하위 번지수에 저장
    // 2) Little Endian: 하위 바이트의 값을 하위 번지수에 저장
    
    // socket API
    // htons(short)
    // htonl(long)
    
    /* int int32ToBigEndian(int n) {
      return ((n & 0xff000000) >> 24) |
             ((n & 0xff0000) >> 8)    |
             ((n & 0xff00)   << 8)    |
             ((n & 0xff)     << 24);
    }
    */
    
    int int32ToBigEndian(unsigned int n) {
      return ((n & 0xff000000) >> 24) |
             ((n & 0xff0000) >> 8)    |
             ((n & 0xff00)   << 8)    |
             ((n & 0xff)     << 24);
    }
    // 16진수 표현: 0x
    // 1byte: 0x00 ~ 0xff
    // 2byte: 0x0000 ~ 0xffff
    //        0x00 0x00
    
    // n = 0x12345678 
    //    = (0001 0010) (0011 0100) (0101 0110) (0111 1000)  
    // 0xff000000 
    //    = (1111 1111) (0000 0000) (0000 0000) (0000 0000)
    // n & 0xff000000 
    //    = (0001 0010) (0000 0000) (0000 0000) (0000 0000)
    //    = 0x12
    // 0x12 >> 24
    //    = (0000 0000) (0000 0000) (0000 0000) (0001 0010)
    //    = 0x00000012
    
    void printIntByteOrder(int n) {
      char *p = (char*)&n;
    
      for (int i = 0 ; i < sizeof(n) ; ++i) {
        printf("%x ", p[i]);
      }
      printf("\n");
    }
    
    int main()
    {
      int ip = 0x12345678;
      printf("%x\n", ip);
    
      printIntByteOrder(ip);
      ip = int32ToBigEndian(ip);
    
      printIntByteOrder(ip);
    }
    
    
    - int32ToBigEndian의 argument가 unsigned인 이유?
     : logic shift가 이루어 져야 하기 때문에... 
     -> arithmatic shift - 앞의 부호 비트가 채워짐. vs. logic shift - 앞이 무조건 0으로 채워짐. 
     + 윈도우와 리눅스가 long을 처리하는 방식이 다르다 (64 비트 기준, linux의 long: 8byte, windows의 long 4byte) -> 그렇기 때문에 기본적으로 4byte를 기준으로 하고 때문에 int형을 사용한다.
     : java의 char은 unsigned 
     -> arithmatic shift - >>, logic shift - >>>

    - htons(), htonl() / ntohs(), ntohl()
     : htons(host to networks) 와 같은 함수가 내부적으로 조건부 컴파일이 되어있기 때문에 little endian일 경우 big endian으로 바뀌지만 big endian의 경우 아무것도 수행하지 않는다.

    // 1. 데이터를 네트워크로 보낼 때
    //  htons, htonl
    
    // 2. 네트워크 데이터를 읽을 때
    //  ntohs, ntohl
    
    #include <arpa/inet.h> 
    #include <stdio.h>
    
    template <typename T>
    void printIntByteOrder(T n) {
      char *p = (char*)&n;
    
      for (int i = 0 ; i < sizeof(n) ; ++i) {
        printf("%x ", p[i]);
      }
      printf("\n");
    }
    
    int main()
    {
      short s = 0x1234;
      s = htons(s);
      s = ntohs(s);
      printIntByteOrder(s);
    
      int n = 0x12345678;
      n = htonl(n);
      n = ntohl(n);
    
      printIntByteOrder(n);
    }


  • OSI 7 Layer Model vs. BSD TCP/IP Protocol Suite 

    OSI(Open Systems Interconnection Reference Model)
     7 Layer는 사용되고 있지 않다. 실질적인 구현은 생각하지 않고 개념적으로 설계되어, 현실적으로 개방형 시스템은 여러개의 시스템에 적용되기 때문에 공통으로 구현하기 힘들고 그에 따른 비용이 증가한다.

    : BSD 에서 만든 TCP suite의 경우도 마찬가지로 학술적인 목적으로 나왔지만, 실질적인 모델을 기반으로 개발을 하고 구축하였기 때문에 사실상 표준이 되었다.

    - Ethernet
     : C:\> ipconfig /all
           0A-00-27    00-00-00
           회사명      고유번호
     : Ethernet의 mac address 를 보면 00-00-00-00-00-00과 같이 6byte인데 이중 vendor 3byte, unique no 3byte인데 이는 요즘 추세로 볼 때 너무 수가 적다. 이를 해결 하고자 로컬 네트워크내에서만 mac address가 겹치지 않으면 되므로 임의적으로 공유기에서 mac address를 바꾸는 식으로 늘릴 수 있다.

      ■ header

    #define ETH_ALEN    6
    #define ETH_HLEN    14 
    
    struct ethhdr {
        unsigned char   h_dest[ETH_ALEN];   /* destination eth addr */
        unsigned char   h_source[ETH_ALEN]; /* source ether addr    */
        __be16      h_proto;        /* packet type ID field, 어떤 상위 프로토콜로 갈지에 대한 방향성 */
    } __attribute__((packed));
    
    # ifconfig
    HWaddr 08:00:27:1f:2d:6c

     

      ■ CSMA (Carrier Sense Multiple Access) / CD (Collision Detect) 방식
       : 브로드캐스트 방식 (모두에게 보내고 자신것이 아니면 버림)

        


    - ARP (Address Resolution Protocol)

     : 이더넷의 상위 프로토콜
     : IP주소와 하드웨어 주소 매핑

     : ARP는 캐시 되어있다.

     : arp 명령어 존재 arp -a, arp -d로 제거 가능, 관리자 권한 필요
       
    - IP (Internet Protocol)
     : 4byte 얼라이먼트 기반으로 작동
     : ip는 host, port는 프로세스 구분
     fragmetation 왜 발생? 라우터마다 MTU가 다르므로... ip는 offset 기반으로 쪼개진다. 이때, id 필요
     : MTU(max transmission unity) 최대 전송 단위가 쪼개질 때 fragment와 ipheader의 id를 기준으로 다시 조합해야 되는데 시퀀스를 통해 조합한다.

     

     

     

     


      ■ header

    struct iphdr {
        __u8    ihl:4,         // 헤더의 길이( 옵션 체크 때문에 존재 ) : 4byte정렬
            version:4;         // version 정보 : ipv4 => 4 , ipv6 => 6
        __u8    tos;           // 현재는 사용하지 않는다. 
        __be16  tot_len;       // 패킷전체길이( 유효 데이터 길이 때문에 존재 ) : 60 
        __be16  id;            // 패킷의 고유번호 ( 조각난 패킷을 조립 할때 필요 )
        __be16  frag_off;      // 데이터의 떨어진 거리 ( 원본 데이터로 부터의 거리 )
                               // 단위가 : 8byte 
                               // MF : 1 중간 패킷  MF=0 마지막 패킷 
        __u8    ttl;           // 잘못된 경로의 패킷의 자동 파괴 기능 때문에 존재
        __u8    protocol;      // 6 : TCP,   17 : UDP
        __sum16 check;         // 수신측에서 해당 데이터가 정확한지 알려줌.
        __be32  saddr;
        __be32  daddr;	
    };
    


    - ICMP (Internet Control Message Protocol)
     : ttl(time to live)은 같은 네트워크안에 존재하면 바뀌지 않음. 라우터(1홉)마다 1씩 감소하는 형태 
     -> ping www.google.com 확인 가능
     : ttl이 58이면? 64 - 58 = 4번의 라우터를 거쳤다.
     : tts는 현재 사용하지 않는다.
     
     

    - RIP (Routing Information Protocol)
     : 라우팅 정보, 홉의 정보
     : 인접한 라우터와의 router table을 교환함.
     

     

     

     ■ UDP Hole Punching
      : 서로 다른 망에서 사설 IP로 잡고 있을 때 서로 어떻게 데이터를 보낼 것인가?
      : A - 192.168.1.2

        B - 192.168.10.3
      : 다른 방법도 존재하지만 Real Time Server를 구축해야 할 경우 이 방법이 가장 효율적.
      : 많은 라우터에서 이 방법을 실제로 사용.( A와 B를 내부적인 라우팅 테이블 조작으로...)

  • TCP vs. UDP
     - TCP
      : udp에 비해 느림, 하지만 무시할만큼은 아니다 -> tcp의 성능을 간과하지마라
      : 신뢰성있는 통신을 보장, 데이터 에러 처리, 버퍼 overflow 방지, 순서 제어 등
      : 기본적으로 tcp는 데이터를 보내고 ack 신호를 기다리는 방식으로 동작한다.
      : 기존 연결에서 적당한 응답시간을 분석해서 그 시간의 2배 이상 동안 ack 신호가 돌아오지 않는다면, 재전송한다.
     

       three way hand shaking
       : Listen
       : TCP를 다시 말하면 read, write의 channel이 생성되는 것이다. -> sequence no를 동기화 하는 역할을 함.
       : clinet의 seq와 server의 seq를 서로 ack_seq 전송을 통해 확인

       1. 최초에 클라이언트가 seq 번호 1000을 보내면 server 에서는 seq 1000 에 1 을 더해서 ack 로 보낸다. 이때, 자신의 seq 번호 2000 도 포함해서 보낸다.
       2. 그러면 클라이언트에서는 server 부터 넘어온 패킷의 ack가 자신의 seq 번호와 일치하는지 확인하고, 확인이 되면 server 의 seq 번호인 2000 에 1을 더해서 ack로 보낸다.
       3. server 에서는 client 가 보낸 ack 의 번호와 자신의 seq 번호가 일치하는지 확인해서 일치하면 연결이 제대로 되었다는것을 인증하고 데이터 통신에 들어가게 된다.


     

      ■ sliding window
       : 송신측과 수신측의 버퍼의 사이즈, 클라이언트의 IO가 느리면 오버플로우 발생 가능성이 있다.
       : 기본적으로 buffer가 한정되어 있기 때문에 window를 통해 제어한다. 만약 server측에서 window가 0으로 ack 신호가 온다면 client는 data를 보내지 않고 기다린다. 이 후 server측에서 window의 크기를 늘리는 ack 신호가 온다면 다시 data를 보낸다.

       
       
       



       four way hand shaking
       : connection을 끊을 때 사용
       : 비정상 종료로 연결상태가 남아있다면 타이머를 통해 처리한다
       1. 
    클라이언트가 연결을 종료하겠다는 FIN플래그를 전송한다.

       2. 서버는 일단 확인메시지를 보내고 자신의 통신이 끝날때까지 기다리는데 이것이 클라이언트에서는 TIME_WAIT, 서버에서는 CLOSE_WAIT 상태다.

       3. 서버가 통신이 끝났으면 연결이 종료되었다고 클라이언트에게 FIN플래그를 전송한다.

       4. 클라이언트는 확인했다는 메시지를 보낸다.
       
       
       

    ∴ Nagle 알고리즘
     : 상대방이 받을 수 있는 사이즈(window size)와 내가 보낼 데이터가 MSS보다 크다면 문제없이 바로 전송한다. 
     : 더 이상 SEND 처리할 데이터가 없다면 현재 있는것 그대로 보낸다.

     : 둘다 아니라면, 아래의 그림 (Nagle On) 처럼 동작한다.
     
      : Nagle은 이런 작은 패킷을 가능한 지연시키기 위하여, ACK가 올때까지 전송을 중지하고 ACK가 도착한 시점에, 지금까지 버퍼에 모인 데이터를 패킷으로 만들어서 전송한다.  ACK를 기다리는 지연방식으로 네이글은 작은 패킷을 연속해서 보내는 네트워크의 비효율을 극복하였다. 하지만 이런 지연방식을 사용하면 위 그림처럼 데이터 전송속도가 더 늦다. 

      : 패스트핑 같은 프로그램은 이 알고리즘을 OFF 하는 것이다. 어떤 데이터를 보내도 ack신호를 받아야 다음 데이터를 전송할 수 있다. 그러나 보내는 데이터가 작고 빈번히 보내는 경우에는 nagle 알고리즘을 끊어 ack신호를 안보냄으로써 실시간성을 보장할 수 있다.
      : 소켓 통신은 기본적으로 이 옵션이 ON 되어 있지만 파라미터 옵션을 통해 OFF 할 수 있다.


     - UDP
      : connection missing...
      : 데이터 유실 가능성과 비순서대로 들어옴
      : udp의 대부분의 문제점은 connection missing에서 발생하기 때문에 항상 connect를 확보하고 보냄으로써 문제점을 해결할 수 있음
      -> RUDP


  • Module Programming


    - 절차


    - 특징



    - Module 적재 및 제거


    - 기본 구조


    - Module 관련 유틸리티


    - Module Program 작성




  • Device Driver
    : 디바이스와 시스템 사이에 데이터를 주고받기 위한 인터페이스
    : 표준적으로 동일한 서비스 제공을 목적
    : 커널의 일부분으로 내장
    : 서브루틴과 데이터의 집합체
    : 디바이스의 고유한 특성을 내포




    - Device 종류



    - 디바이스 파일



    - File Operations







    ■ Character Device Driver


    ■ Ext4_file_operations



    - 디바이스 드라이버 구현



    : 모드간 데이터 전송

     

     

     : I/O 영역 경쟁 처리

     

     

     

    : 디바이스 종류별 등록/해제

     


    문자 디바이스 드라이버
     - 2.6 이후
      1. 디바이스 번호 등록
      2. cdev 구조체 생성 및 초기화
      3. 디바이스 등록 cdev_add(); -> module_init();
      4. file_operations 구조체에 대입될 함수들을 작성

    - 출처 : http://blog.dasomoli.org/223

    : register_chrdrv_region 함수는 원하는 디바이스의 번호를 미리 알고 있을 때 사용하고, alloc_chrdev_region 함수는 디바이스의 번호를 동적으로 할당받아 파라미터로 받는 dev_t 구조체 포인터를 이용해 dev_t 구조체에 넣는다.
    register_chrdrv 대신 register_chrdrv_region을 사용하는 것으로 혼동할 수 있는데 그게 아닌 cdev_add 함수까지 사용하여야 한다. 실제 커널 소스의 register_chrdrv 함수를 보면 이런 과정이 구현되어 있음을 볼 수 있다.
    cdev_add 함수를 사용하기 위해서는 struct cdev 구조체를 사용하여야 하는데 이 구조체를 초기화 시켜주는 함수가 cdev_init 이다. struct cdev 구조체 등을 사용하려면 <linux/cdev.h> 를 include하여야 한다. 다음은 사용 예이다.

     
    #include <linux/kernel.h>
    #include <linux/cdev.h>
    #include <linux/fs.h>
    
    ...
    struct file_operations dasom_fops;
    
    static struct cdev dasom_cdev = {
        .owner = THIS_MODULE,
        .ops = &dasom_fops,
    };
    
    int _init dasom_init(void)
    {
        dev_t dev;
        int err = 0;
    
        if(major) {
            dev = MKDEV(major, minor);
            err = register_chrdev_region(dev, 1, "dasomoli");
        } else {
            err = alloc_chrdev_region(&dev, mior, 1, "dasomoli");
            major = MAJOR(dev);  
        }
        if(err < 0) {
            err = -ENODEV;
            return err;
        }
        ...
        
        cdev_init(&dasom_cdev, &dasom_fops);
        dasom_cdev.owner = THIS_MODULE;
        dasom_cdev.ops  = &dasom_fops;
    
        if(cdev_add(&dasom_cdev, dev, 1)) {
            printk(KERN_INFO"dasom: cdev creation failed.\n");
            err = -ENODEV;
            goto error_label;
        }
        
        ...
        
        return 0;
        
    error_label:
        return err;
    }
    


     - 2.6 이전
     
     

     블록 디바이스 드라이버
     
     

    ■ 

    네트워크 디바이스 드라이버

     
     

    - Character Device Driver with VFS


    - File System and Device Driver




  • 전체 요약







'Major > Linux' 카테고리의 다른 글

리눅스 - Ext4 File System  (0) 2016.02.01
리눅스 - 가상 파일 시스템  (0) 2016.01.28
리눅스 - 파일 디스크립터  (7) 2016.01.28
리눅스 - 파일 시스템  (0) 2016.01.28
리눅스 - System Call  (0) 2016.01.28
  • Ext4 File System
    : Extended File System
    : Ext2, Ex3 에 이은 리눅스의 최신 파일 시스템

    -> Ex2, Ex3에 비해 최대 지원 파일 크기, 디스크 할당 정책, 저널링, 단편화 해소 등 많은 부분 성능이 향상된 파일 시스템

    - 특징
     ■ 더 큰 파일 시스템과 파일 사이즈
      : 현재 Ext3는 최대 16Tib의 파일시스템 사이즈와 최대 2Tib의 파일 사이즈를 지원
      : Ext4는 48 비트 블록 주소 (48-bit block addressing)를 추가했고, 1EB(2^60)의 최대 파일 시스템 사이즈와 16Tib의 최대 파일 사이즈를 가짐
      -> 4KB x 2^48 = 1EB
      ∴ 왜 64비트가 아니고 48비트?
      : Ext4에 어드레스될 완벽하게 안정적인 48비트가 있기 전에 Ext4가 만들어졌기 때문에 어드레스 비트를 48로 제한할 필요가 있었음.

     ■ 서브 디렉토리 확장성
      : 서브 디렉토리들의 가능한 최대 수는 Ex3에 있는 싱글 디렉토리를 포함하여 32000인데 Ext4는 두배의 값까지 가질 수 있게 되어서 64000의 서브 디렉토리를 허용

     ■ 입출력 성능 향상

      : 익스텐트 (Extent)
      : 다중 블록 할당 (Multiblock allocation)
      : 지연 할당 (Delayed allocation)
     
     ■ 그 외 특징들
      : 저널링 체크섬 - Ext4 체크섬은 블록에 결함이나 붕괴가 있다면 그것을 알 수 있도록 해줌
      : Lager inodes - Ext3은 아이노드 크기를 변경하는 것을 지원 그러나 기본적인 아이노드 크기는 128바이트 -> Ext4는 기본적인 아이노드 크기를 256 바이트로 함
      : Inode reservation - 디렉토리가 만들어질 때 미래에 사용될 몇몇의 아이노드들을 예약하는 것으로 구성



  • Ext4 Partition
    - 파일 시스템 블록 구성
     : 블록 크기 - 1KB ~ 4KB (default 4KB)
     : 파일시스템을 구성하는 정보들은 각 block group에 분산
     : 파일을 저장할 때 특정 부분에 집중적으로 기록하여 하드디스크의 효율을 높이고자 함 (파일 단편화를 줄임)
     : Block group 당 최대 블록의 개수 (블록이 4KB로 가정)
      -> 4KB = 8(bits) x 4096 = 32,768 bits
      -> 32KB X 4KB = 128MB (block group의 최대 크기)
      -> 1024MB / 128MB = 80개 (1GB 일 때 80개의 그룹 생성)

     


      ■ Super Block

       : Super Block이 손상되면 전체 파일 시스템에 대한 정보를 잃기 때문에 Super block과 group descriptor table의 사본은 모든 block group에 저장
       : 각 block group의 첫 번째 block에 위치
       : 해당 파일 시스템 관련 정보 저장 [블록의 크기(1KB, 2KB, 4KB), 총 블록의 개수, 블록 그룹의 개수, Inode의 개수, 그룹 내의 블록/Inode의 개수]
       : $dumpe2fs /dev/sdb1

      ■ Group Descriptor Table
       : Super block 바로 다음 block에 위치
       : 각 Block Group을 관리하는 정보 저장[Block Bitmap의 블록 번호, inode Bitmap의 블록 번호, 첫 번째 inode Table Block의 블록 번호, 그룹 안에 있는 빈 블록 수, 그룹 안에 있는 inode 수, 그룹 안에 있는 빈 디렉토리 수)

      ■ Block bitmap
       : 이 블록에 속한 각 비트는 그룹 내에 있는 각 블록의 사용 상태를 나타냄
       : Block 크기가 4KByte면 4K*8개 블록의 사용 여부 표시
       

      ■ inode
       : inode (index node)
        - 파일에 대한 제어 정보 및 데이터 블록 포인터 저장
        - 모든 파일들과 디렉터리들은 각각 1개의 inode를 할당
       : inode bitmap
        이 블록에 속한 각 비트는 그룹 내에 있는 각 inode의 사용 상태를 나타냄
       : inode table
        - 각각의 i
    node에 대한 정보를 나타내는 inode descriptor로 구성
        - inode table은 Group Descriptor Table에 저장
        - inode 번호를 알면 inode가 속한 Group을 알 수 있음
         -> Block group = (inode - 1) / INODES_PER_GROUP
        
       : inode descriptor
        - Mode, Owner Info, Size, Timestamp
        - Blocks pointers array (파일의 실제 데이터가 저장된 위치)
        

    - 디렉토리 구조


    Blocks vs Pages
     : block는 OS에서 파일을 읽거나 쓰는 가장 작은 데이터의 단위
     : page는 몇몇 OS에서 block 대신 사용된다. page는 기본적으로 virtual block 이며, 4K나 2K로 고정된 크기를 가진다.
     => page는 고정된 크기를 가진 virtual block 
     : 왜 blocks 대신에 pages를 사용할까? - page는 다양한 storage 장치에서 프로세싱을 더 쉽게 만든다. 각각의 디바이스들은 서로 다른 크기의 블록크기를 지원한다. page는 이것을 OS 시스템에서 같은 크기의 page 사이즈로 다룰 수있도록 한다. 이것은 크기가 다른 모든 블록 사이즈들을 계산하고 다루는것 보다 효율적이다. 그래서 페이지는 하드웨어 드라이버와 OS 시스템의 sort of a middleman 역할을 한다.
     =
    > 페이지들이 적절한 블록으로 해석된다.
     : page, blocks 모두 data storage의 기본단위로 사용된다.

  • Ext4 Performance vs Ext3
     

  • Ext2,3 vs Ext4 메커니즘
    - 블록 매핑
     ■ Extent
      - 이전 파일 시스템의 단점
       : Ext3과 같은 유닉스 드라이버의 파일 시스템은 각각의 블록(파일의 데이터에 해당하여 사용되는)들의 트랙을 유지하기 위해 간접 블록 매핑 스키마를 사용
       -> 이것은 큰 파일에 대해서 비효율적이며 특히 큰 파일을 삭제하거나 절단 명령 동안에 비효율적. 왜냐하면 모든 싱글 블록 엔트리에 대한 매핑은 그것이 큰 파일(많은 블록들을 가지고 있는)에 해당할때 거대한 매핑을 필요로 하며 핸들이 느려짐

      - 연속 할당의 변형된 기법
      : 어느 정도의 연속된 공간만 초기에 할당
      : 그 양이 충분히 크지 않을 때. 추후 또 다른 연속된 공간을 익스텐트(extent)라고 부르는 단위로 할당
      : 파일 블록들의 위치는 위치와 블록 수, 다음 익스텐트의 첫 블록을 가리키는 포인터로 기록
      : Ext2,3 Indirect Block vs Ext4 Extent
           

         

     ■ 다중 블록 할당 (Multiblock Allocation)
       - 이전 파일 시스템의 단점
       : 새로운 데이터를 디스크로 쓸 필요가 생길 때, 블록 할당자(Block Allocator)는 데이터가 쓰여질 가용 공간을 결정
       : Ext2,3 블록 할당자는 오직 한번에 한 개의 블록(4KB)만 할당 할 수 있음
       - 만약 시스템이 100MB의 가용공간이 필요하다면 Ext2/3 블록 할당자는 25600번 (100MB = 25600 블록)의 블록 할당 호출을 하게 됨 -> 단일 블록만을 다루기 때문

       - Ext4의 다중 블록 할당
       : Ext4는 매 호출마다 싱글 블럭을 할당하는 대신에 많은 오버헤드를 피하기 위해서 한번의 호출로 많은 블록을 할당할 수 있는 다중 블록 할당자(multi block allocator)를 사용 

     ■ 지연 할당 (Delayed Allocation)
      - 이전 파일 시스템의 할당 정책
      : write() 연산의 경우, 파일 시스템 코드는 심지어 데이터가 디스크에 즉시 쓰여지고 있지 않더라도 이것을 일정한 시간동안 캐시에 유지한 채 즉시 데이터가 위치할 블록으로 할당
      : 이러한 접근은 단점 가짐. 가령 커지는 파일에 지속적으로 write() 연산을 할 때 데이터의 블록으로의 성공적인 write() 할당이 이루어지더라도 그 파일이 계속 커질 것이라는 것은 알지 못하기 때문

      - Ex4의 지연 할당
      : 파일이 캐시에 유지되어 있는 동안에는 그 파일이 실제로 디스크에 쓰여질 때 까지 블록으로의 할당을 지연시킴

     => 시너지 (Synergy)
      : Ext4의 Extent, Multi Allocation, Delayed Allocation은 서로의 특성과 함께 효율적으로 디스크 할당을 수행함
      1. 파일이 디스크에 쓰기를 마쳤을 때 생기는 많은 작업량은 extents로 인접한 블록으로의 할당이 준비
      2. multi block allocation에 의해 많은 작업량에 대한 큰 블록이 할당될 수 있음
      3. 지연된 할당은 위의 두 특성이 효율적으로 작동할 수 있도록 해줌

    - 저널링 [Journaling (Ext3.4)]
     : 시스템이 멈추거나 정전 등으로 꺼지는 경우
      -> 일관성 문제 발생
     : 부팅 시
      - e2fsck (fsck.ext2)를 이용한 일관성 검사
      - 파일 시스템을 마운트하는 시점에 검사 수행
      - 저장장치 내의 모든 inode와 비트맵 검사

     : 모든 저널 로그들은 트랜잭션 단위로 그룹화
     : 각각의 트랜잭션들은 시퀀스 번호를 부여받음
     : 저널 데이터
      - 각 트랜잭션을 관리할 수 있는 정보와 파일 시스템의 변경 내역에 대한 데이터를 가지고 있음
     : e2fsck
      - Commit 된 저널 데이터의 경우 정상 분류하여 해당 데이터들을 파일시스템에 기록
      - Commit 되지 않은 저널의 경우 완료되지 않은 데이터이므로 무시하고 넘어감

     ■ Journaling File System

       : 데이터베이스의 기본적인 특징들을 구현
        - Logging을 이용한 데이터 백업 체계
       : Journal 영역
        - 로그를 기록
        - Commit: 작업이 정상적으로 종료될 경우
        - Rollback: 비정상 종료일 경우 원래의 상태로 복구



  • read(), write() 시스템 콜 루틴


    - read() system call in file system
     

    - read() system call trace (출처 : 수원 삼성소프트웨어멤버십 송인주)
    ∴ 가정
     : 파라미터로 파일 디스크립터, 버퍼, size를 가지는 가장 일반적인 read() 시스템 콜
     : 장치 파일을 통한 읽기가 아닌 파일 시스템을 사용하는 데이터 읽기
     : 읽고자 하는 파일은 이미 open() 되었음
     : O_DIRECT 플래그를 주지 않았기 때문에 페이지 캐시에 접근
     : 페이지 캐시에 read 하고자 하는 페이지가 없음 -> 블록 I/O 요청을 발생시킴
     : 디스크 파일 시스템은 Ext4
     : 디스크는 SCSI Disk Device를 사용
     -> 그러므로 블록 I/O 처리 시 로드되는 디바이스 드라이버는 SCSI 디바이스 드라이버







    - write() system call trace


'Major > Linux' 카테고리의 다른 글

리눅스 - 디바이스 드라이버  (2) 2016.02.15
리눅스 - 가상 파일 시스템  (0) 2016.01.28
리눅스 - 파일 디스크립터  (7) 2016.01.28
리눅스 - 파일 시스템  (0) 2016.01.28
리눅스 - System Call  (0) 2016.01.28
  • VFS(Virtual File System)



  • VFS의 역할 및 구조


    - VFS가 지원하는 파일 시스템
      디스크 기반 파일 시스템
      : 로컬 디스크 파티션의 기억 장소 또는 디스크를 흉내내는 몇 가지 다른 장치(USB 플래시 드라이브 같은)를 관리
      : Ext2, Ext3, Ext4, MS-DOS, FAT, NTFS, SYSV 등
      네트워크 파일 시스템
      : 네트워크로 연결된 다른 컴퓨터의 파일 시스템을 쉽게 접근가능하게 함
      : NFS(Network File System), AFS(앤드류 파일 시슽엠) 등
       
     ■ 특수 파일 시스템
      : /proc 파일 시스템, /tmp 파일 시스템 등
      

  • VFS 메커니즘
    - 공통 파일 모델 (Common File Model)

     : VFS의 핵심 개념은 지원하는 모든 파일 시스템을 표현할 수 있는 공통 파일 모델을 도입 하는 것
     : 각각의 특정 파일 시스템을 구현 하려면 반드시 자신의 물리적인 구성을 VFS의 공통 모델로 변환해야 함
     -> 예를 들어 몇몇 비 유닉스 계열의 디스크 기반 파일 시스템은 각 파일의 위치를 저장한 파일 할당 테이블(FAT)를 사용 -> 이런 파일 시스템에서 디렉토리는 파일이 아님
     -> VFS의 공통 파일 모델을 따르도록 하기 위해 실행 중에 디렉토리에 대응 하는 파일을 생성, 이렇게 생성한 파일을 커널 메모리 객체에 만듦
     => 공통 파일 모델을 구현하기 위한 자료구조(object)
       : 슈퍼 블록 객체, 아이노드 객체, 파일 객체, 디엔트리 객체

    - 프로세스와 VFS 객체의 상호작용
     

    - 공통 파일 모델 객체
     ■ 슈퍼 블록 객체
      : 마운트 된 파일 시스템에 대한 정보를 저장, 전체 파일 시스템을 나타냄
     ■ 아이노드 객체
      : 특정 파일에 대한 일반 정보를 저장. 디스크에 저장한 파일 제어 블록 (FCB, 리눅스에서는 아이노드)에 대응, 각 아이노드 객체에는 아이노드 번호가 할당)
     ■ 파일 객체
      : 열린 파일과 프로세스 사이의 상호 작용과 관련한 정보를 저장. 각 프로세스가 열린 파일을 가지는 동안 커널 메모리에만 존재
     ■ 디엔트리 객체
      : 디렉토리 항목(특정 파일 이름과 아이노드)과 이에 대응하는 파일의 연결에 대한 정보를 저장.

    - VFS 디엔트리 캐시

     : 가장 최근에 사용한 디엔트리 객체를 저장하는 디스크 캐시

     ∴ 디스크 캐시?
      : 보통 디스크에 저장하는 정보 중 일부를 램에 저장하여 이후 정보에 접근할때 더 느린 디스크에 접근하지 않고 빨리 처리하도록 하는 소프트웨어 메커니즘
      : 이렇게 함으로써 파일 경로명을 공려명의 마지막 구성요소인 파일 아이노드로 변환하는 속도를 높임

     ∴ 리눅스의 디스크캐시

      : 디엔트리 캐시, 아이노드 캐시, 페이지 캐시

    - VFS의 시스템 콜 처리 루틴
     

     : 애플리케이션이 read() 시스템 콜 호출 -> 커널은 해당하는 sys read() 호출
     : 커널 메모리 안에 있는 파일 객체의 f op 필드에 타겟 파일 시스템의 해당 함수 처리 루틴이 포인터로 존재
     : read() -> sys_read() -> vfs_read() -> (file->f_op) -> MS-DOS read()

      ■ open() 시스템 콜 메커니즘

       : open() 시스템 콜 -> sys_open() 시스템 콜이 성공하면 파일 객체에 대한 포인터 배열인 current->files->fd 의 인덱스를 반환
       : sys_open() 함수의 동작
       ① fd 반환

       ② 파일 객체의 주소 반환

       ③ f_op 필드를 대응하는 아이노드 객체의 i_fop 필드 내용으로 설정 -> 파일 연산을 위한 메소드 설정 종료

       ④ 탐색된 디엔트리 객체와 마운트된 파일 시스템 객체 등으로 파일 객체 할당

       ⑤ 접근 모드 플래그를 매개변수로 경로명 탐색 시작

       ⑥ 빈 파일 디스크립터 번호를 지역 변수에 저장

       ⑦ 프로세스 주소 공간에서 파일 경로명을 읽음

     

    - VFS가 처리하는 시스템 콜
     



'Major > Linux' 카테고리의 다른 글

리눅스 - 디바이스 드라이버  (2) 2016.02.15
리눅스 - Ext4 File System  (0) 2016.02.01
리눅스 - 파일 디스크립터  (7) 2016.01.28
리눅스 - 파일 시스템  (0) 2016.01.28
리눅스 - System Call  (0) 2016.01.28

+ Recent posts