ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [리눅스 네트워크] - 1 : echo 서버 만들기
    프로그래밍/컴퓨터 네트워크 2024. 4. 5. 00:44

     

     

    네트워크 프로그래밍이란?
    •  네트워크 프로그래밍은 시스템 프로그래밍의 연장선이다!
      • 컴퓨터의 경우, 랜선이라 부르는 이더넷 케이블을 통해 Data가 송/수신 되고 있다.
      • 이때, 케이블은 H/W 장치이기 때문에 케이블을 통해 송/수신 되는 Data에 접근하기 위해서는 kernel의 도움이 있어야 한다.
      • 따라서, 네트워크 프로그래밍 == system call을 이용해 네트워킹을 위한 코드를 작성하고, 실행하는 것이다!

     

     

     

    Socket 프로그래밍 코드 살펴보기 - server

     

    Server의 동작 순서
    1. socket() : 소켓 생성
    2. bind() : 생성한 소켓에 주소 할당
    3. listen() : 클라이언트 연결 요청 대기
    4. accept() : 클라이언트 연결 승인
    5. read() / write() : 소켓 통신 (메시지 수신, 송신)
    6. close() : 소켓 닫기. 종료.

     

     

    Server 동작 코드

     

    [전체코드 보기]

    더보기
    더보기
    #include <stdio.h>
    #include <sys/types.h>      
    #include <sys/socket.h>
    #include <signal.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <arpa/inet.h>
    
    const char *PORT = "12345";
    
    int server_sock;
    int client_sock;
    
    void interrupt(int arg){
    	printf("\nYou typed Ctrl + C\n");
    	printf("Bye\n");
    
    	close(client_sock);
    	close(server_sock);
    	exit(1);
    }
    
    void removeEnterChar(char *buf){
    	int len = strlen(buf);
    	for (int i = len - 1; i >= 0; i--){
    		if (buf[i] == '\n'){
    			buf[i] = '\0';
    			break;
    		}
    	}
    }
    
    int main(){
    	signal(SIGINT, interrupt);
    
    	server_sock = socket(AF_INET, SOCK_STREAM, 0);
    	if (server_sock == -1){
    		printf("ERROR :: 1_Socket Create Error\n");
    		exit(1);
    	}
    	printf("Server On..\n");	
    
    	int optval = 1;
    	setsockopt(server_sock, SOL_SOCKET, SO_REUSEADDR, (void *)&optval, sizeof(optval));
    
    	struct sockaddr_in server_addr = {0};
    	server_addr.sin_family = AF_INET;
    	server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    	server_addr.sin_port = htons(atoi(PORT));
    	socklen_t server_addr_len = sizeof(server_addr);
    
    	if (bind(server_sock, (struct sockaddr *)&server_addr, server_addr_len) == -1){
    		printf("ERROR :: 2_bind Error\n");
    		exit(1);
    	}
    	printf("Bind Success\n");	
    
    	if (listen(server_sock, 5) == -1){
    		printf("ERROR :: 3_listen Error");
    		exit(1);
    	}
    	printf("Wait Client...\n");
    
    	client_sock = 0;
    	struct sockaddr_in client_addr = {0};
    	socklen_t client_addr_len = sizeof(client_addr);
    
    	while (1){
    		memset(&client_addr, 0, client_addr_len);
    
    		client_sock = accept(server_sock, (struct sockaddr *)&client_addr, &client_addr_len);
    		if (client_sock == -1){
    			printf("ERROR :: 4_accept Error\n");
    			break;
    		}
    		printf("Client Connect Success!\n");		
    
    		char buf[100];
    		while (1){
    			memset(buf, 0, 100);
    
    			int len = read(client_sock, buf, 99);
    			removeEnterChar(buf);
    
    			if (len == 0){
    				printf("INFO :: Disconnect with client... BYE\n");
    				break;
    			}
    
    			if (!strcmp("exit", buf)){
    				printf("INFO :: Client want close... BYE\n");
    				break;
    			}
    			write(client_sock, buf, strlen(buf));
    		}
    		close(client_sock);
    		printf("Client Bye!\n");
    	}
    	close(server_sock);
    	printf("Server off..\n");
    	return 0;
    }

     

    1-1. 소켓생성 : server_sock = socket(AF_INET, SOCK_STREAM, 0);

      int socket(int domain, int type, int protocol)

    • 기능 : 입력받은  주소 체계(domain), 소켓 유형(type), 프로토콜(protocol)에 맞는 소켓을 생성함.
    •  입력값
      • domain : 프로토콜 도메인 
        • AF_INET : IPv4
        • AF_INET6 : IPv6
        • AF_UNIX : 로컬통신  등...
      • type : 소켓의 유형
        • ex) SOCK_STREAM : TCP
        • SOCK_DGRAM : UDP  등...
      • protocol : 프로토콜 종류 ex) IPPROTO_TCP / IPPROTO_UDP 등... => 주로 0으로 설정하여 시스템이 적절한 프로토콜을 자동으로 선택 하도록 한다.
    • 반환값 : 생성된 소켓의 파일 디스크립터 (= 소켓을 식별하는 데 사용되는 정수. 식별자), 실패하면 -1을 반환한다.
    • 라이브러리 : #include <sys/socket.h> 

     

    1-2. 소켓 옵션 지정 : setsockopt(server_sock, SOL_SOCKET, SO_REUSEADDR, (void *)&optval, sizeof(optval));

    int setsockopt(int socket, int level, int optname, void *optval, socklen_t *optlen)

    • 기능 : 소켓 옵션을 설정 
    •  입력값
      • socket descriptor : 소켓 파일 디스크립터 (식별자) = > socket()함수의 반환값으로 얻음
      • level : 프로토콜 수준
        • ex) SOL_SOCKET : 소켓의 일반적인 레벨을 다룸
        • IPPROTO_IP : IPv4 소켓에 대해, IP 프로토콜관련 옵션을 설정
        • IPPROTO_TCP : TCP 프로토콜 레벨을 다룸 => 주로 SOL_SOCKET으로 설정!
      • optname : setsockopt() 함수에서 설정하려는 특정 옵션을 식별하는 상수 값 
        • ex) SO_REUSEADDR : 주소 재사용 옵션. 소켓 종료 후에도 같은 주소와 포트를 빠르게 재사용 할 수 있음.
        • SO_KEEPALIVE : 소켓 간 주기적인 keep-alive 패킷이 전송된다.
        • SO_LINGER : close() 호출 시 보류 상태로 머무르는 시간을 제어
        • 등...
      • *optval : setsockopt() 함수에서 사용되는 옵션 값을 저장하는 포인터. 설정하려는 옵션 값의 주소를 가리킨다. 옵션 값은 다양한 데이터 형식일 수 있으므로 void 포인터로 전달된다.
      • optlen : setsockopt() 함수에서 사용되는 옵션 값의 크기(byte)를 나타내는 변수. => 보통 이 값은 sizeof() 연산자를 사용하여 옵션 값의 크기를 계산하여 설정한다.

     

     

    1-3. 소켓 구조체(sockaddr_in) 지정 : struct sockaddr_in { ~~ }

    struct sockaddr_in {
        short int          sin_family;  // 주소 체계(AF_INET)
        unsigned short int sin_port;    // 포트 번호
        struct in_addr     sin_addr;    // IPv4 주소
        unsigned char      sin_zero[8]; // 소켓 구조체의 크기를 맞추기 위한 추가 공간
    };
    
    =================================================================================
    
    struct sockaddr_in server_addr = {0};
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(atoi(PORT));
    socklen_t server_addr_len = sizeof(server_addr);
    • 기능 : socket 의 주소 정보를 담당하는 구조체, IPv4 주소 체계를 사용하는 경우, sockaddr_in 구조체를 사용한다.
    • 입력값
      •  sin_family : 주소체계. AF_INET = IPv4를 의미한다.
      • sin_addr : 소켓의 IP 주소를 나타낸다. 이때, 네트워크 바이트는 빅 엔디안을 사용하고, 호스트 바이트는 리틀 엔디안을 사용하기 때문에 htonl()함수를 이용해 IPv4주소를 빅엔디안으로 변경해준다.
        • 여기서, INADDR_ANY는 소켓을 바인딩할 때 사용되는 특별한 IPv4 주소로 "모든 네트워크 인터페이스"를 나타낸다. => 모든 네트워크 인터페이스에 대한 연결을 수락할 수 있도록 한다.
      • sin_port : 소켓의 PORT 번호를 나타낸다. 마찬가지로, htons() 함수를 통해 PORT를 빅엔디안으로 변경해주어야 한다.
      • sin_zero : 구조체 크기를 맞추기 위한 추가 공간(padding 느낌..?). 실제로 사용되지 않지만, 구조체의 크기를 맞추기 위해 포함됨.

     

    2. 생성한 소켓에 주소 할당 : bind(server_sock, (struct sockaddr *)&server_addr, server_addr_len)

    int bind(int socket, const struct sockaddr*, socklen_t)

    • 기능 : 소켓을 특정 주소에 연결하여 특정 IP, PORT에 바인딩시키고 소켓에 주소 할당.
    • 입력값 
      • socket : 바인딩할 소켓 파일 디스크립터(=식별자) => socket()함수의 반환값으로 얻음
      • sockaddr*  : 바인딩할 주소를 나타내는 sockaddr 구조체에 대한 포인터. (앞서 살펴본 구조체!)
      • : 주소 구조체의 크기를 나타내는 변수. 일반적으로 sizeof(소켓 구조체) 형태로 쓰임 sizeof(struct sockaddr_in)
    • 반환값 : bind() 성공시 0, 실패시 -1
    • 라이브러리 : #include <sys/socket.h> 필요

     

    3. client의 연결 요청 기다림 : listen(server_sock, 5)

    int listen(int socket, int backlog)

    • 기능 : 소켓을 수신 대기 상태로 전환하여 연결을 받아들일 수 있도록 준비.  TCP 서버 프로그램에서 클라이언트의 연결 요청을 수신할 때 사용된다.
    • 입력값
      • socket  : 수신 대기 상태로 전환할 소켓을 나타내는 소켓 디스크립터 = > socket() 함수의 반환값
      • backlog : 대기열의 최대 길이를 지정. 동시에 처리할 수 있는 요청의 최대수
    • 반환값 : listen() 성공시 0, 실패시 -1
    • 라이브러리 : #include <sys/socket.h> 필요

     


    4-1. 클라이언트 소켓 선언 

    	client_sock = 0;
    	struct sockaddr_in client_addr = {0};
    	socklen_t client_addr_len = sizeof(client_addr);
    • Server Socket에서 데이터를 주고 받을 Client 용 Socket을 설정 (소켓 디스크립터, sockaddr_in 구조체, 구조체 len)
    • => 만약, 여러 client와 연결하고자 한다면, 각각을 모두 배열 형식으로 선언해주면 된다!

     

     

    4-2. 클라이언트 연결 수락 및 소켓 생성 :  accept(server_sock, (struct sockaddr *)&client_addr, &client_addr_len);

    int accept(int sock, struct sockaddr*, socklen_t)

    • 기능 : 클라이언트의 연결 요청이 있을 때까지 블로킹 된다! 
      • 클라이언트의 연결 요청을 수락하고, 해당 소켓 생성 (socket + struct 지정 + bind() 기능의 통합)
      • 서버 소켓의 경우, "소켓 생성(socket()) > 서버 소켓 구조체 정의  > 주소 할당(bind())" 을 모두 차례대로 해주어야 했다.
      • 하지만, 서버 소켓 생성 이후 클라이언트를 생성할 때는 이미 서버를 통해 구성된 틀이 있으므로 accept 함수만 호출해주어도 구조체에 알맞게 할당되고, 알맞은 클라이언트 소켓 디스크립터를 반환 받을 수 있다.
    • 입력값
      • sock : 클라이언트의 요청을 수락할 서버 소켓의 디스크립터 (socket, bind, listen이 되어 있어야 한다.)
      • sockaddr* : 클라이언트의 주소 정보를 담을 구조체에 대한 포인터 (클라이언트의 IP 주소와 포트 번호 등 저장)
      • socklen_t  : 클라이언트 주소 구조체의 크기를 나타내는 변수 포인터
    • 반환값 : accept() 성공시 새로운 클라이언트의 파일 디스크립터를 반환. 실패시 -1
    • 라이브러리 :  #include <sys/socket.h> 필요

     

     

    ====================여기까지, 통신을 위한 sever & client의 소켓 생성이 완료 되었다.===========

    이후에는 생성된 소켓을 이용해 파일 입출력 (read, write)처럼 메시지를 주고 받으면 된다!


    5. read() / write() 송수신 기능 (=서버의 역할)

    while (1){
    		memset(&client_addr, 0, client_addr_len);
    
    		client_sock = accept(server_sock, (struct sockaddr *)&client_addr, &client_addr_len);
    		if (client_sock == -1){
    			printf("ERROR :: 4_accept Error\n");
    			break;
    		}
    		printf("Client Connect Success!\n");		
    
    		char buf[100];
    		while (1){
    			memset(buf, 0, 100);
    
    			int len = read(client_sock, buf, 99);
    			removeEnterChar(buf);
    
    			if (len == 0){
    				printf("INFO :: Disconnect with client... BYE\n");
    				break;
    			}
    
    			if (!strcmp("exit", buf)){
    				printf("INFO :: Client want close... BYE\n");
    				break;
    			}
    			write(client_sock, buf, strlen(buf));
    		}
    		close(client_sock);
    		printf("Client Bye!\n");
    	}

     

     

    5-1. 소켓통신을 통한 메시지 수신 : read(client_sock, buf, 99);

    ssize_t read(int fd, void *buf, size_t count);

    • 기능 : 디스크립터로부터 데이터를 읽어 buf에 저장한다. (=메시지 수신)
    • 입력값
      • fd : 데이터를 읽을 디스크립터
      • buf : 읽은 데이터를 저장할 버퍼의 포인터
      • count : 읽을 최대 바이트 수를 나타내는 크기
    • 반환값 : 성공 시: 읽은 바이트 수를 반환 / 실패시 -1을 반환 
      • read()함수는 디스크립터에 데이터가 도착하기 전까지 블로킹된다
      • 이때, 일반적으로 read함수는 파일의 끝에 도달할 때 0을 반환하지만, 소켓통신에서 read()함수가 0을 반환하는 경우는 해당 소켓 디스크립터가 종료된 경우를 의미한다!!

      • 따라서, read()의 반환값이 0이라면, 해당 클라이언트와의 연결이 끊긴 것으로 간주하고 해당 클라이언트 소켓을 close()시켜야 한다. 

     

     

    5-2. 소켓통신을 통한 메시지 송신 : write(client_sock, buf, strlen(buf));

    ssize_t write(int fd, const void *buf, size_t count);

    • 기능 :  파일 디스크립터에 buf에 저장된 데이터를 쓴다. (= 메시지 송신)
    • 입력값
      • fd : 데이터를 쓸 디스크립터
      • buf : 쓸 데이터가 저장된 버퍼의 포인터
      • count : 쓸 바이트 수를 나타내는 크기
    • 반환값 : 성공 시: 쓴 바이트 수를 반환 / 실패 시: -1을 반환

     

     

    [동작 설명]

    • 첫 번째 while(1)문
      • 이때, 특정 client와의 연결이 끊기더라도 서버는 다른 클라이언트와 새롭게 연결 될 수 있으므로, 서버 소켓이 살아있는 한 다른 클라이언트와의 연결을 탐색해야 한다. 
      • 따라서 while(1)안에 accept()함수를 넣어 서버 소켓이 살아있는 한 다른 클라이언트와의 연결을 탐색할 수 있도록 한다.
    • 두번째 whlie(1)문
      • 클라이언트와의 연결이 성공하여 두번째 while(1)문 안으로 들어오면, 해당 클라이언트와의 연결이 끊어지거나 (len == 0) 해당 클라이언트가 exit 문자열을 보내기 전까지 계속 무한하게 클라이언트가 보낸 메시지를 read하고 write해주어야 한다. 
      • 따라서 while(1)문 안에 해당 클라이언트에 대한 서버의 동작을 넣어주어 클라이언트와의 통신이 끝나지 않는 한 무한히 동작하게 해준다.

     


    6-1. 클라이언트 소켓 닫기, 종료 : close(client_sock);

    int close(int sockfd)

    • 기능 : 전달받은 디스크립터를 닫는데 사용된다. 여기서는 소켓 디스크립터를 전달하여 소켓을 종료시킨다.
      • client 소켓에서 읽은 메시지 len == 0
    • 입력값 : 닫을 소켓의 디스트립터
    • 반환값 : close() 성공시 0, 실패시 -1을 반환한다.

     

     

    6-2. 서버 소켓 닫기, 종료 : signal(SIGINT, interrupt);

    signal(int signum, void (*handler)(int))

    • 기능 : 프로그램이 signum에 해당하는 신호를 받았을 때 실행될 handler함수를 등록한다.
    • 입력값
      • signum : 등록할 시그널 번호 => 핸들러 함수의 첫번째 인자로 전달된다!
      • * handler : 시그널을 받았을 때 호출될 핸들러 함수의 포인터
    • 반환값 : 성공 시: 이전의 시그널 핸들러 함수의 포인터를 반환 / 실패 시: SIG_ERR을 반환

     

    [작성한 interrupt() 함수]

    void interrupt(int arg){
    	printf("\nYou typed Ctrl + C\n");
    	printf("Bye\n");
    
    	close(client_sock);
    	close(server_sock);
    	exit(1);
    }
    • arg에는 전달한 signum인 SIGINT가 들어온다.
    • 함수가 실행되면, 먼저 client 소켓을 닫은 후 server 소켓도 닫는다.
    • 모든 소켓을 닫은 후, exit()함수를 통해 프로그램을 종료시킨다.
      • 이때, exit(int status)함수에 인자를 전달하여 프로그램의 종료 상태를 외부로 전달할 수 있다.
      • 여기서 status = 1은 "일반적인 오류" , "비정상적인 종료"를 의미한다.

     

     

    Socket 프로그래밍 코드 살펴보기 - client

     

     

    client 동작 순서
    1.  socket() 소켓 생성
    2. connect() 연결 요청
    3. read() / write() 데이터 송수신
    4. close() 연결 종료

     

    client 코드 분석

     

    [전체 코드 보기]

    더보기
    더보기
    //echo_client
    
    #include <stdio.h>
    #include <unistd.h>
    #include <stdlib.h>
    #include <signal.h>
    #include <sys/types.h>       
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <string.h>
    
    const char *SERVER_IP = "127.0.0.1";
    const char *SERVER_PORT = "12345";
    
    int client_sock;
    void interrupt(int arg){
    	printf("\nYou typped Ctrl + C\n");
    	printf("Bye\n");
    
    	close(client_sock);
    	exit(1);
    }
    
    int main(){
    	signal(SIGINT, interrupt);
    
    	client_sock = socket(AF_INET, SOCK_STREAM, 0);
    	if (client_sock == -1){
    		printf("ERROR :: 1_Socket Create Error\n");
    		exit(1);
    	}
    	//printf("Socket Create!\n");
    
    	struct sockaddr_in server_addr = {0};
    	server_addr.sin_family = AF_INET;
    	server_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
    	server_addr.sin_port = htons(atoi(SERVER_PORT));
    	
    	socklen_t server_addr_len = sizeof(server_addr);
    
    	if (connect(client_sock, (struct sockaddr *)&server_addr, server_addr_len) == -1){
    		printf("ERROR :: 2_Connect Error\n");
    		exit(1);
    	}
    	//printf("Connect Success!\n");
    		
    	char buf[100];
    	while (1){
    		memset(buf, 0, 100);
    		scanf("%s", buf);
    		if (!strcmp(buf, "exit")){
    			write(client_sock, buf, strlen(buf));
    			break;
    		}
    		write(client_sock, buf, strlen(buf));
    
    		memset(buf, 0, 100);
    		int len = read(client_sock, buf, 99);
    		if (len == 0){
    			printf("INFO :: Server Disconnected\n");
    			break;
    		}
    		printf("%s\n", buf);
    	}
    
    	close(client_sock);
    	//printf("Logout\n");
    	return 0;
    }

     

     

    1-1. 소켓 생성 : client_sock = socket(AF_INET, SOCK_STREAM, 0);

    • 앞서 살펴본 server 소켓 생성과 동일하다.
    • 소켓 생성후, 해당 소켓의 디스크립터가 client_sock 변수에 저장된다.

     

     

    1-2. 접속할 서버의 sockaddr_in 구조체 생성 

    	struct sockaddr_in server_addr = { 0 };
    	server_addr.sin_family = AF_INET;
    	server_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
    	server_addr.sin_port = htons(atoi(SERVER_PORT));
    
    	socklen_t server_addr_len = sizeof(server_addr);
    • 서버에서 sockaddr_in 구조체를 생성해준 것 처럼, client 측에서도 접속할 서버의 구조체를 생성해주어야 한다.
    • (코드는 서버측과 동일한다.)

     

    2. socket 연결하기 : connect(client_sock, (struct sockaddr*)&server_addr, server_addr_len)

    int connect(int socket, const struct sockaddr*, socklen_t)

    • 기능 : 클라이언트 소켓을 서버에 연결하는 데 사용된다. 
      • 전달받은 클라이언트의 소켓 디스크립터연결할 서버 소켓의 구조체 정보를 기반으로 서버에 연결 요청을 보내 연결을 수립한다.
      • 해당 클라이언트 소켓은 server의 클라이언트 소켓과 연동되어 있다.
    • 입력값
      • socket : 클라이언트 소켓의 파일 디스크립터
      • sockaddr* : 연결할 서버의 주소 정보를 담고 있는 struct sockaddr_in 구조체의 포인터
      • socklen_t : 서버 소켓 구조체의 크기
    • 반환값 : 성공시 0 / 실패시 -1 반환

     

    3. read() / write() 데이터 송수신 기능

    char buf[100];
    	while (1){
    		memset(buf, 0, 100);
    		scanf("%s", buf);
    		if (!strcmp(buf, "exit")){
    			write(client_sock, buf, strlen(buf));
    			break;
    		}
    		write(client_sock, buf, strlen(buf));
    
    		memset(buf, 0, 100);
    		int len = read(client_sock, buf, 99);
    		if (len == 0){
    			printf("INFO :: Server Disconnected\n");
    			break;
    		}
    		printf("%s\n", buf);
    	}
    
    	close(client_sock);
    • 사용자의 입력을 받아 buf에 저장한다.
    • 만약 입력값이 exit이라면 client소켓을 종료시킨다(close)
    • 아니라면, buf에 저장된 사용자가 입력한 문자열을  wirte()함수를 통해 소켓 디스크립트에 작성한다. (=메시지 송신)
    • read()함수를 통해 clietn_sock에 있는 문자열을 읽어온다 (=메시지 수신)
    • client_sock연결이 끊겼다면(len == 0), client 소켓을 종료시킨다(close)

     


    4. close() 종료시키기 & interrupt 

    signal(SIGINT, interrupt);

    • 마찬가지로,signal()함수를 호출하여 interrupt가 발생하였을 때, interrupt 함수를 실행시켜준다.

     

Designed by Tistory.
-->