• Server
    - 1_server ~ 3_server
     
    #define WIN32_LEAN_AND_MEAN
    #include <stdio.h>
    
    #include <Windows.h>
    
    #include <WinSock2.h>
    
    #pragma comment (lib, "ws2_32.lib")
    
    int main()
    {
    	WSADATA w;
    	WSAStartup(MAKEWORD(2, 2), &w);
    	//-----------------------------------
    
    	// 1. 소켓 생성
    	SOCKET sock = socket(PF_INET, SOCK_STREAM, 0);
    
    	// 2. 소켓에 주소 지정(bind)
    	SOCKADDR_IN addr = { 0, };
    	addr.sin_family = AF_INET;
    	addr.sin_port = htons(4000);
    	addr.sin_addr.s_addr = INADDR_ANY;
    
    	bind(sock, (SOCKADDR*)&addr, sizeof addr);
    
    	// 3. 소켓을 대기 상태로, 5라는 arg는 무시 됨
    	listen(sock, 5);
    
    	// 4. 클라이언트 요청을 수락
    	SOCKADDR_IN caddr = { 0, };
    	int sz = sizeof(caddr);
    
    	SOCKET csock = accept(sock, (SOCKADDR*)&caddr, &sz);
    
    	printf("클라이언트가 접속되었습니다.\n");
    
    
    	char buf[1024];
    	// 핵심 : read() 를 통해 수신하지 않습니다. 리턴값이 가장 중요하다.
    	int ret;
    	while (1) {
    		ret = recv(csock, buf, 1024, 0);
    		if (ret == 0) {
    			printf("연결이 정상적으로 종료되었습니다.\n");
    			break;
    		}
    		else if (ret == -1) {
    			printf("연결이 비정상적으로 종료되었습니다.\n");
    			break;
    		}
    
    		// ...
    		ret = send(csock, buf, ret, 0);
    		if (ret < 0)
    			break;
    	}
    
    	printf("클라이언트가 접속이 해지되었습니다.\n");
    
    	closesocket(sock);
    	closesocket(csock);
    
    	//-----------------------------------
    	WSACleanup();
    }
    

  • Client
    - 1_client
     : 초기 셋팅, 주의 사항

    // 1_client.cpp
    
    // 1. 윈도우즈의 모든 네트워크 관련 함수는
    //		winsock2.h에 있습니다. (헤더만)
    //		기존의 소켓 API를 확장한 형태
    // 2.모든 구현은 sw2_32.dll에 있습니다. 링킹이 필요
    // 3. Windows.h와 같이 사용할 경우 반드시 WinSock2.h를 먼저
    //	  포함해야 합니다.
    // 4. #define WIN32_LEAN_AND_MEAN
    // 5. 환경설정에서 링커 설정
    // 3~5 중 선택적으로 사용
    
    // Windows.h의 헤더에서 자주 사용하지 않는 것을 제외해달라.
    #define WIN32_LEAN_AND_MEAN
    
    #include <WinSock2.h>
    #pragma comment(lib, "ws2_32.lib") // 링킹
    
    #include <Windows.h> // 모든 시스템 함수가 헤더파일에 선언되어 있습니다.
    
    int main()
    {
    	return 0;
    }
    

    - 2_client

    #include <stdio.h>
    #include <WinSock2.h>
    #include <Windows.h> 
    
    #pragma comment(lib, "ws2_32.lib")
    
    // 1. Network 함수를 사용하기 전에 WSAStartup을 통해
    //	라이브러리 초기화를 수행해야 한다.
    
    // 2. WSA : Windows Socket API
    // 3. WSACleanup() 을 통해 자원을 정리해야 한다.
    
    int main()
    {
    	// 찰스 시모니의 헝가리안 표기법 이해 필요, 변수명에 타입에 대한 정보가 나타남
    	// JAVA의 m~은? 안드로이드 표준
    	WSADATA w;
    	int ret = WSAStartup(MAKEWORD(2, 2), &w);
    	// 0x0202 word가 2byte
    
    	if(ret != 0)
    		printf("소켓 라이브러리를 사용할 수 없다.\n");
    
    	// w에 있는 정보는 현재까지 정확하지 않습니다. 사용되지 않습니다.
    	printf("최대 소켓 갯수: %d\n", w.iMaxSockets);
    	printf("소켓 버전: %d.%d\n", w.wHighVersion, w.wVersion);
    
    	// 소켓 라이브러리가 사용하는 자원을 정리
    	WSACleanup();
    }
    

    - 3_client
    #include <stdio.h>
    #include <stdlib.h>
    
    #include <WinSock2.h>
    #include <Windows.h>
    
    #pragma comment(lib, "ws2_32.lib")
    
    #include <locale.h>
    
    int main()
    {
    	setlocale(LC_ALL, "korean"); // 유니코드 셋팅
    	
    	WSADATA w;
    	WSAStartup(MAKEWORD(2, 2), &w);
    	//--------------
    	// 1. 소켓 생성, 리눅스는 fd가 retrun, 윈도우는 SOCKET return
    	SOCKET sock = socket(PF_INET, SOCK_STREAM, 0); // int도 되지만 정확한 타입을 사용!
    
    	// 2. 서버 주소 지정
    	// struct sockaddr_in addr = { 0, };
    	SOCKADDR_IN addr = { 0, };
    	addr.sin_family = AF_INET;
    	addr.sin_port = htons(4000);
    	//addr.sin_port = htons(80);
    	addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    	//addr.sin_addr.s_addr = inet_addr("203.249.22.240");
    
    	// 3. 서버 접속
    	int ret = connect(sock, (SOCKADDR*)&addr, sizeof addr);
    	if (ret == 0)
    		printf("접속 성공\n");
    	else {
    		int err = WSAGetLastError(); // 함수 호출하면 
    		// 1. WSAGetLastError() / 도구 -> 오류 조희 
    		printf("접속 실패 : %d\n", err);
    
    		// 2. WSAGetLastError() 오류 번호를 저장하지 않고 다른 함수를 호출하면 오류 번호가 초기화 되버린다. 
    		WCHAR buf[512];
    		FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM,
    					  0, 
    					  err, 
    					  LANG_SYSTEM_DEFAULT,
    					  buf, 
    					  512, 0);
    		// L은 유니코드라는 표기
    		wprintf(L"에러 발생 : %s", buf);
    
    		// 3. Debug Mode - 조사식
    		// @err
    		// @err, hr
    
    		// 비정상종료 테스트, 자원해지(소멸자 등) 테스트
    		//Sleep(1000 * 10); 
    		//exit(0);
    	}
    
    	//--------------
    
    	closesocket(sock);	// close(sock);
    	
    	WSACleanup();
    }
    


  •  Event-Driven Server Architectures
    : alternative to synchronous blocking I/O
    the mapping of a single thread to multiple connections
    new events are queued and the thread executes a so-called event loop--dequeuing events from the queue, processing the event, then taking the next event or waiting for new events to be pushed.

    - thread-based vs. event-driven

    thread-basedevent-driven
    connection/request statethread contextstate machine/continuation
    main I/O modelsynchronous/blockingasynchronous/non-blocking
    activity flowthread-per-connectionevents and associated handlers
    primary scheduling strategypreemptive (OS)cooperative
    scheduling componentscheduler (OS)event loop
    calling semanticsblockingdispatching/awaiting events
    Table 4.2: Main differences between thread-based and event-driven server architectures.


  • Non-blocking I/O Multiplexing Patterns
    event-based I/O multiplexing

    - Reactor Pattern
     : 주로 리눅스의 구현
     : select, epoll, kqueue, Node.js, Ruby EventMachine, Scala Akka’s IO 모듈 등
     : synchronous, non-blocking I/O handling and relies on an event notification interface.
     : event나 socket 같은 리소스들의 집합을 등록해놓고, 각 리소스들을 콜백 또 후킹하는 적절한 event handler가 필요하다.
     : 핵심 구성요소인 synchronous event demultiplexer blocking event notification interface를 사용하여 리소스의 이벤트를 기다린다.
     : synchronous event demultiplexer가 받는 이벤트는 언제든지 dispatcher로 알리고, 다음 event를 기다린다.
     : dispatcher는 관련 event handler 선택하고, callback/hook execution을 triggering함으로써 event를 처리한다.
     : 리엑터 패턴은 event handling 과 multiplexing 분리한다. 이 패턴은 싱글 스레드를 사용하므로 blocking 동작은 전체적은 application의 동작을 지연시킬 수 있다. 그래서 다양한 Reactor Pattern들이 event handler에 대한 thread poll(thread 개수는 CPU 개수*2 + 1)을 사용한다.

    *장점
    : 리엑터 패턴은 리엑터 구현에서 애플리케이션에 관련된 코드가 완벽하게 분리됨. 그래서 애플리케이션 컴포넌트들이 모듈화되고 재사용 가능한 부분들로 분리될 수 있음. 시스템을 멀티스레드로 인한 복잡성에서 보호해 줌.

    *단점
    : 디버깅하기 어려움. 요청 핸들러를 동기적으로 호출하기 때문에 최대 동시성에 제한이 있음. 요청 핸들러 뿐만 아니라 디멀티플렉서에 의해서도 확장성이 제한됨. (OS 스케줄링 이용 불가, event queue에 대한 성능 이슈)




      ■ 1_epoll
       : select, poll의 문제점 해결

     
    #include <stdio.h>
    
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <sys/epoll.h>   // epoll api
    
    #include <arpa/inet.h>
    #include <netinet/in.h>
    
    // select, poll 문제점
    // 1) 관찰 대상 디스크립터의 정보를 매번 운영체제에 전달해야 한다.
    // 2) 1024 개 이상의 디스크립터를 등록하는 것이 불가능하다.
    
    // epoll
    // - 2.5.44 부터 사용 가능하다.
    // - $ cat /proc/sys/kernel/osrelease
    
    // epoll api 3가지
    // 1) epoll_create : 파일 디스크립터 저장소 생성
    // 2) epoll_ctl    : 디스크립터 등록 & 삭제
    // 3) epoll_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
    
    	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);
    
    
    	// 1. 초기화
    	const int EPOLL_SIZE = 1024;
    	int efd = epoll_create(EPOLL_SIZE);
    	// 최신 커널에서는 사이즈는 무시됩니다.
    
    	// 2. 이벤트 등록
    	struct epoll_event events[EPOLL_SIZE];
    
    	struct epoll_event event;
    	event.events = EPOLLIN;
    	event.data.fd = sock;
    	epoll_ctl(efd, EPOLL_CTL_ADD, sock, &event); 
    
    	while (1) 
    	{
    		int count = epoll_wait(efd, events, EPOLL_SIZE, -1);
    
    		for (int i = 0 ; i < count ; ++i)
    		{
    			if (events[i].data.fd == sock)
    			{
    				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);
    
    				// 새로운 연결을 등록해주어야 한다.
    				event.events = EPOLLIN;
    				event.data.fd = csock;
    				epoll_ctl(efd, EPOLL_CTL_ADD, csock, &event); 
    			} 
    			else
    			{
    				int csock = events[i].data.fd;
    				char buf[1024];
    				int n = read(csock, buf, sizeof buf);
    
    				if (n <= 0)
    				{
    					printf("연결 종료!!\n");
    					close(csock);
    
    					// 종료된 디스크립터를 등록 해지해야 한다.
    					epoll_ctl(efd, EPOLL_CTL_DEL, csock, 0);
    				} 
    				else
    					write(csock, buf, n);
    			}
    		}
    	}
    
    	close(sock);
    }
    


      ■ 2_epoll
       : Level Triggering vs. Edge Triggering

     
    #include <stdio.h>
    
    #include <unistd.h>
    #include <errno.h>
    #include <fcntl.h>       // fcntl()
    
    #include <arpa/inet.h>
    #include <netinet/in.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <sys/epoll.h>   // epoll api
    
    // epoll 이벤트 처리 방식
    // Level Triggering - Default
    // : 수신 버퍼에 데이터가 있다면, 이벤트가 발생
    // Edge Triggering
    // : 수신 버퍼에 데이터가 없다가, 존재하는 시점에 이벤트가 발생
    // 핵심 : 데이터를 처리하는 디스크립터를 Non-blokcing 모드로 설정해야 한다.
    
    
    // ET 장점
    // 1. 데이터를 처리하기 위해 커널에 진입하는 횟수가 적다.
    // 2. 데이터를 수신하는 로직과 데이터를 처리하는 로직을 분리할 수 있다.
    //   - 서버의 구조화에 유리하다.
    
    
    void setNonBlokcing(int fd)
    {
    	// 아래 코드는 문제점이 있습니다.
    	// fcntl(fd, F_SETFL, O_NONBLOCK);
    
    	// Edge Triggering은 연결에 대한 데이터가 들어오는 시점에서 발생하므로 다시 데이터가 올라올때까지 발생하지 않는다. 
    	// ex) aaaa -> aaaa, aaaab -> aaaa
    	// -> blocking이 생길 수 있다. -> read를 non-blocking 되도록 해야한다.
    	// -> Edge trigger + non-blocking -> fcntl
    
    	int flags = fcntl(fd, F_GETFL, 0);
    	flags |= O_NONBLOCK;
    
    	fcntl(fd, F_SETFL, flags);
    }
    
    
    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);
    
    
    	// 1. 초기화
    	const int EPOLL_SIZE = 1024;
    	int efd = epoll_create(EPOLL_SIZE);
    	// 최신 커널에서는 사이즈는 무시됩니다.
    
    	// 2. 이벤트 등록
    	struct epoll_event events[EPOLL_SIZE];
    
    	struct epoll_event event;
    	event.events = EPOLLIN;
    	event.data.fd = sock;
    	epoll_ctl(efd, EPOLL_CTL_ADD, sock, &event); 
    
    	while (1) 
    	{
    		int count = epoll_wait(efd, events, EPOLL_SIZE, -1);
    		printf("epoll_wait()\n");
    
    		for (int i = 0 ; i < count ; ++i)
    		{
    			if (events[i].data.fd == sock)
    			{
    				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);
    
    				// Non-Blocking 으로 설정  
    				setNonBlokcing(csock);
    
    				// 새로운 연결을 등록해주어야 한다.
    				event.events = EPOLLIN | EPOLLET;
    				event.data.fd = csock;
    				epoll_ctl(efd, EPOLL_CTL_ADD, csock, &event); 
    			} 
    			else
    			{
    				int csock = events[i].data.fd;
    				// char buf[1024];
    				char buf[4];
    				while (1)
    				{
    					int n = read(csock, buf, sizeof buf);
    					if (n == 0)
    					{
    						printf("연결 종료!!\n");
    						close(csock);
    
    						// 종료된 디스크립터를 등록 해지해야 한다.
    						epoll_ctl(efd, EPOLL_CTL_DEL, csock, 0);
    					} 
    					else if (n == -1)
    					{
    						int err = errno;
    						if (err == EAGAIN)   // 데이터가 존재하지 않는다.
    							break;
    						else {
    							close(csock);
    
    							epoll_ctl(efd, EPOLL_CTL_DEL, csock, 0);
    						}
    
    					}
    					else {
    						printf("read() : %d\n", n);
    						write(csock, buf, n);
    					}
    				}
    			}
    		}
    	}
    
    	close(sock);
    }


    - Proactor Pattern
     : 주로 윈도우의 구현
     : POSIX AIO, Boost.Asio (C++), 
    Adaptive Communication Environment (C++), libbitcoin (C++), RJR (Ruby), win32 IOCP
     : asynchronous, non-blocking I/O operations
     : Reactor pattern의 asynchronous 방식으로 보기도 함
     : blocking event notification interfaces 대신에 completion events
     : proactive initiator는 main application thread로 
    asynchronous I/O 동작들을 초기화하는 역할
     : 동작이 이슈될때 항상 completion handler와 completion dispatcher에 등록된다.
     : 비동기적인 동작의 수행은 asynchronous operation processor의해 관장된다. 이 프로세스는 OS에 의해 구현된다. 
     : I/O 작업이 완료되었을때, completion dispatcher로 notifiy 되고, 다음으로 completion handler는 그 결과를 처리한다.

     




    - 1_blocking_queue
     : mutex는 커널단 컨텍스트 스위칭이므로 오버헤드가 큼 -> atomic operation

     
    #include <unistd.h>
    
    #include <mutex>
    #include <condition_variable> // Event
    #include <deque>
    
    #include <thread>
    #include <iostream>
    using namespace std;
    
    template <typename T>
    class blocking_queue
    {
    	deque<T> d_queue;
    	condition_variable d_condition;
    	mutex d_mutex;
    
    public:
    	void push(const T& value)
    	{
    		{
    			unique_lock<mutex> lock(d_mutex);
    			d_queue.push_front(value);		
    		}
    		d_condition.notify_one();
    	}
    	
    	T pop()
    	{
    		unique_lock<mutex> lock(d_mutex);
    		d_condition.wait(lock, [=] { return !d_queue.empty(); } );
    
    		T rc(std::move(d_queue.back())); // move fast
    		d_queue.pop_back();
    
    		return rc;
    	}
    };
    

    - 2_lockfree_queue
     : boost를 이용한 생산자 소비자 패턴
     
    #include <iostream>
    
    #include <boost/lockfree/queue.hpp>
    // h + cpp = hpp
    
    #include <boost/thread/thread.hpp>
    #include <boost/atomic.hpp>
    
    using namespace std;
    using namespace boost;
    
    atomic_int producer_count(0);
    atomic_int consumer_count(0);
    
    lockfree::queue<int> queue(128);
    
    const int iterations = 100000000;
    const int producer_thread_count = 4;
    const int consumer_thread_count = 4;
    
    void producer(void)
    {
    	for (int i = 0 ; i < iterations ; ++i) {
    		int value = ++producer_count;
    		while (!queue.push(value))
    			;
    	}
    }
    
    atomic<bool> done(false);
    void consumer(void)
    {
    	int value;
    	while (!done)
    	{
    		while (queue.pop(value))
    			++consumer_count;
    	}
    
    	while (queue.pop(value))
    		++consumer_count;
    }
    
    int main()
    {
    	cout << "queue is ";
    	if (!queue.is_lock_free())
    		cout << "not ";
    	cout << "lockfree" << endl;
    
    	thread_group producer_threads, consumer_threads;
    	for (int i = 0 ; i < producer_thread_count ; ++i)
    		producer_threads.create_thread(producer);
    
    	for (int i = 0 ; i < consumer_thread_count ; ++i)
    		consumer_threads.create_thread(consumer);
    
    	producer_threads.join_all();
    	done = true;
    
    	consumer_threads.join_all();
    
    	cout << "produced " << producer_count << endl;
    	cout << "consumed " << consumer_count << endl;
    


    - 4_asio_echo_server
     : block에 대한 개념이 존재하지 않음
     : boost asio에서 핵심은 io_service
     : 각각의 연산 자체가 비동기로 이루어짐

    #include <boost/asio.hpp>
    using namespace boost;
    
    #include <iostream>
    #include <memory>    // shared_ptr<>
    #include <utility>
    using namespace std;
    
    using boost::asio::ip::tcp;
    class Session : public std::enable_shared_from_this<Session>
    {
    public:
    	Session(tcp::socket s) : socket(std::move(s)) {}
    
    	void start() { doRead(); }
    
    	void doRead() {
    		auto self(shared_from_this());  // 참조 계수 증가, self를 참조안하면 객체가 바로 파괴되버림
    		// 1. async_read      : readn
    		// 2. async_read_some : read
    
    		socket.async_read_some(
    				asio::buffer(data, 1024),
    				[this, self](system::error_code ec, size_t length) {
    				
    				if (!ec) 
    					doWrite(length);
    		});
    	}
    
    	void doWrite(size_t length)
    	{
    		auto self(shared_from_this());
    
    		asio::async_write(
    				socket, asio::buffer(data, length),
    				[this, self](system::error_code ec, size_t) {
    					if (!ec)
    						doRead();
    				});
    	}
    
    
    private:
    	tcp::socket socket;
    	char data[1024];
    };
    
    
    class Server
    {
    public:
    	Server(asio::io_service& io, short port)
    		: acceptor(io, tcp::endpoint(tcp::v4(), port)),
    			socket(io) {
    		
    		doAccept();
    	}
    
    	void doAccept()
    	{
    		acceptor.async_accept(socket, [this](system::error_code ec) {
    			// 클라이언트의 접속 요청이 완료된 시점에 호출되는 핸들러	
    			if (!ec) {
    				// socket...
    				make_shared<Session>(std::move(socket))->start();
    			}
    		});
    
    	}
    
    private:
    	tcp::acceptor acceptor;
    	tcp::socket socket;
    	
    };
    
    
    int main()
    {
    	try {
    		asio::io_service io_service;
    		Server s(io_service, 4000);
    		io_service.run();
    
    	} catch (std::exception& e) {
    		cerr << e.what() << endl;
    	}
    }

  • Reactor, Proactor 간단한 도식화
    - Reactor


    - Proactor




참고: http://berb.github.io/diploma-thesis/original/042_serverarch.html, http://ozt88.tistory.com/25

  • I/O multiplexing
    : synchronous, Blocking I/O 형태의 멀티 프로세스 서버, 멀티 스레드 서버 모두 고성능 서버로는 부적합하다.
    : 클라이언트마다 프로세스나 스레드를 할당하면 Context Switching 문제로 메모리적인 문제가 발생한다.
    -> 멀티플렉싱 개념 필요

    - poll
      ■ 1_poll
       : mkfifo fifo 파이프 만들기
       : IPC 전용 메커니즘
       : cat > myfifo로 리다이렉션

    #include <unistd.h>
    
    #include <stdio.h>
    #include <fcntl.h>
    
    int main()
    {
    	char buff[100];
    	int ret;
    
    	int fd = open("myfifo", O_RDWR);
    
    	while (1)
    	{
    		ret = read(0, buff, sizeof buff);
    		buff[ret] = '\0';
    		printf("Keyboard : %s\n", buff);
    
    		ret = read(fd, buff, sizeof buff);
    		buff[ret] = '\0';
    		printf("myfifo : %s\n", buff);
    	}
    }
    

      ■ 2_poll
    #include <unistd.h>
    
    #include <stdio.h>
    #include <fcntl.h>
    
    #include <poll.h>
    
    int main()
    {
    	char buff[100];
    	int ret;
    
    	int fd = open("myfifo", O_RDWR);
    
    
    	// 비동기적으로 이벤트를 처리할 디스크립터 배열
    	struct pollfd fds[2];
    
    	while (1)
    	{
    		fds[0].fd = 0;
    		fds[0].events = POLLIN;  // 관심 있는 이벤트의 종류
    														 // read  : POLLIN
    														 // write : POLLOUT
    
    		fds[1].fd = fd;
    		fds[1].events = POLLIN;
    
    
    		poll(fds, 2, -1);
    
    
    		// 주의할 점 : 이벤트가 동시에 발생할 수 있으므로
    		//             절대 else 로 묶으면 안된다.
    		if (fds[0].revents & POLLIN) 
    		{
    			ret = read(0, buff, sizeof buff);
    			buff[ret] = '\0';
    			printf("Keyboard : %s\n", buff);
    		}
    
    		if (fds[1].revents & POLLIN)
    		{
    			ret = read(fd, buff, sizeof buff);
    			buff[ret] = '\0';
    			printf("myfifo : %s\n", buff);
    		}
    	}
    }
    


    - select (참고: http://ozt88.tistory.com/21)
     : select는 싱글스레드로 다중 I/O를 처리하는 멀티플렉싱 통지모델의 가장 대표적인 방법이다.
     : 해당 파일 디스크립터가 I/O를 할 준비가 되었는지 알 수 있다면, 그 파일 디스크립터가 할당받은 커널 Buffer에 데이터를 복사해주기만 하면 된다. 이런 목적하에 통지모델은 파일디스크립터의 상황을 파악할 수 있게 하는 기능을 할 수 있어야한다. select는 많은 파일 디스크립터들을 한꺼번에 관찰하는 FD_SET 구조체를 사용하여 빠르고 간편하게 유저에게 파일 디스크립터의 상황을 알려준다.
     : select를 사용해서 I/O의 상황을 알기 위해서는 프로세스가 커널에게 직접 상황 체크를 요청해야한다. 프로세스가 커널의 상황을 지속적으로 확인하고 그에 맞는 대응을 하는 형태로 구성되기 때문에 프로세스와 커널이 서로 동기화된 상태에서 정보를 주고 받는 형태로 볼 수 있다. 따라서 select의 통지형태를 동기형 통지방식이라 부를 수 있다. 그리고 select 그 자체는 I/O를 담당하지 않지만, 통지하는 함수의 호출방식이 timeout에 따라 non-blocking 또는 blocking 형태가 된다. timeout을 설정하지 않으면, 관찰 대상이 변경되지 않는 이상 반환되지 않으므로 blocking 함수가 되고, timeout이 설정되면 주어진 시간이 지나면 시간이 다되었다는 정보를 반환하므로 non-blocking 함수가 된다. 

    #include  <stdio.h>
    
    #include  <unistd.h>
    #include  <sys/types.h>
    #include  <sys/select.h>
    #include  <sys/socket.h>
    
    #include  <arpa/inet.h>
    #include  <netinet/in.h>
    
    inline int max(int a, int b)
    {
    	return a > b ? a : b;
    }
    
    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);
    
    	// fd_set : bit array
    	fd_set socks;					// source data
    	fd_set readsocks;
    
    	FD_ZERO(&socks);
    	FD_SET(sock, &socks);
    
    	int maxfds = sock;
    
    	while (1) 
    	{
    		readsocks = socks;
    
    		int ret = select(maxfds + 1, &readsocks, 0, 0, 0);
    		printf("ret : %d\n", ret);
    
    		for (int i = 0 ; i < maxfds + 1 ; ++i)
    		{
    			if (FD_ISSET(i, &readsocks))
    			{
    				if (i == sock)
    				{
    					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);
    
    					// 새로운 연결을 등록해주어야 한다.
    					FD_SET(csock, &socks); 
    					maxfds = max(maxfds, csock);
    				}
    				else
    				{
    					int csock = i;
    					char buf[1024];
    					int n = read(csock, buf, sizeof buf);
    					
    					if (n <= 0)
    					{
    						printf("연결 종료!!\n");
    						close(csock);
    
    						// 종료된 디스크립터를 등록 해지해야 한다.
    						FD_CLR(csock, &socks);
    					} 
    					else
    						write(csock, buf, n);
    
    				}
    			} 
    		}
    
    #if 0
    		while (1)
    		{
    			char buf[1024];
    			int n = read(csock, buf, sizeof buf);
    			if (n == 0)
    				close(csock);
    			else if (n == -1)
    				close(csock);
    
    			write(csock, buf, n);
    		}
    #endif
    	}
    
    	close(sock);
    }
    


  • 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


+ Recent posts