본문 바로가기
개발/리눅스

[Linux]Iterative 기반의 서버, 클라이언트 구현

by p_human 2020. 3. 31.

오늘은 예정대로 에코 서버, 클라이언트를 구현해보도록 하겠습니다.

이전 글에서 작성했던 TCP 서버, 클라이언트의 코드에서 조금만 변경을 하면 되니 금방 끝날 것입니다.

 

Iterative 서버의 구현 방식

이번에 만들 에코 서버는 클라이언트가 전송한 문자열을 그대로 클라이언트에게 재전송을 해주는데요.

이전에 만든 TCP 서버는 클라이언트가 연결된 것을 확인한 후에 문자열을 전송해주고 바로 종료되는 방식이었습니다. 그래서 listen함수 호출을 통해서 만들어진 연결 요청 대기 큐가 별 소용이 없었습니다.

여러 클라이언트와 데이터를 주고받으려면 어떻게 해야 할까요? 바로 반복문을 돌려서 accept함수를 여러 번 호출하면 됩니다.

왼쪽에는 Iterative 서버의 함수 호출 순서를 나타낸 그림입니다.

 

이렇게 왼쪽과 같은 구조로 서버가 동작하게 되면, 비록 한 번에 한 클라이언트이지만 연결 요청한 모든 클라이언트에게 약속한 서비스가 가능하게 됩니다. 나중에 프로세스와 스레드에 대해서 공부를 하고 나면, 동시에 여러 클라이언트에 대한 처리를 할 수 있게 됩니다.

(정리해서 올리고 있는 저도 스레드까지 공부해서 제가 원하는 서버를 만드는 게 목표입니다. 아마 1 달이면 충분..? 할 것 같네요.)

 


Iterative 에코 서버, 에코 클라이언트

서버는 반복문을 추가해서 여러 클라이언트의 요청을 받고, 클라이언트에게 받은 데이터를 다시 보내는 코드를 추가합니다. 클라이언트는 데이터를 입력받고 다시 서버로부터 입력한 데이터를 받는 코드를 추가하면 됩니다.

 

설명으로 하니까 좀 이해하기가 애매한데, 그림으로 보시면 이해가 빠를 것 같아서 첨부합니다.


에코 서버와 에코 클라이언트의 데이터 송수신 흐름


눈에 띄는 색깔로 나뉘어서 기억에 잘 남게 해 봤어요. ㅎㅎ

이제 코드를 공개할 차례!! 아 그리고 이 코드들은 이 책을 쓰신 윤성우 님의 블로그 자료실에서 다운로드할 수 있어요.
순서대로 서버와 클라이언트의 코드입니다.

이 코드는 이전 글에 작성한 TCP 서버/클라이언트의 코드에서 살짝만 바뀐 코드입니다.


에코 서버 프로그램

// 에코 서버 프로그램
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 1024
void error_handling(char *message);

int main(int argc, char *argv[])
{
    int serv_sock, clnt_sock;
    char message[BUF_SIZE];
    int str_len, i;
	
    struct sockaddr_in serv_adr;
    struct sockaddr_in clnt_adr;
    socklen_t clnt_adr_sz;
	
    if(argc!=2) {
        printf("Usage : %s <port>\n", argv[0]);
        exit(1);
    }
	
    serv_sock=socket(PF_INET, SOCK_STREAM, 0);   
    if(serv_sock==-1)
        error_handling("socket() error");
	
    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family=AF_INET;
    serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
    serv_adr.sin_port=htons(atoi(argv[1]));

    if(bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)
        error_handling("bind() error");
	
    if(listen(serv_sock, 5)==-1)
        error_handling("listen() error");
	
    clnt_adr_sz=sizeof(clnt_adr);
	
    // 여러 클라이언트와의 통신을 위해서 반복문 추가
    for(i=0; i<5; i++)
    {
        // 서버와 각 클라이언트와의 통신을 위해서 생성된 소켓
        clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr, &clnt_adr_sz);
        if(clnt_sock==-1)
            error_handling("accept() error");
        else
            printf("Connected client %d \n", i+1);
		
        // 클라이언트로부터 전송된 데이터를 수신한다.
        // 그리고 다시 클라이언트에게 받았던 데이터를 송신한다.
        while((str_len=read(clnt_sock, message, BUF_SIZE))!=0)
            write(clnt_sock, message, str_len);

        close(clnt_sock);
    }

    close(serv_sock);
    return 0;
}

void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

에코 클라이언트 프로그램

// 에코 클라이언트 프로그램
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 1024
void error_handling(char *message);

int main(int argc, char *argv[])
{
    int sock;
    char message[BUF_SIZE];
    int str_len;
    struct sockaddr_in serv_adr;

    if(argc!=3) {
        printf("Usage : %s <IP> <port>\n", argv[0]);
        exit(1);
    }
	
    sock=socket(PF_INET, SOCK_STREAM, 0);   
    if(sock==-1)
        error_handling("socket() error");
	
    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family=AF_INET;
    serv_adr.sin_addr.s_addr=inet_addr(argv[1]);
    serv_adr.sin_port=htons(atoi(argv[2]));
	
    // 클라이언트가 서버와의 연결을 위해 연결요청을 한다.
    if(connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)
        error_handling("connect() error!");
    else
        puts("Connected...........");
	
    // 사용자가 q를 입력할 때까지 반복
    while(1) 
    {
        // 콘솔창에 출력하는 함수
        fputs("Input message(Q to quit): ", stdout);
        // 사용자의 입력을 받는다.
        fgets(message, BUF_SIZE, stdin);
		
        // strcmp는 문자열의 크기를 비교하는 함수인데,
        // 쉽게 생각해서 앞에 것이 크면 1, 뒤에 것이 크면 -1이다.
        if(!strcmp(message,"q\n") || !strcmp(message,"Q\n"))
            break;
		
        // 서버에게 입력한 데이터를 보낸다.
        write(sock, message, strlen(message));
        // 서버로부터 데이터를 받는다. 반환되는 값은 받아온 바이트 수
        // 실패 시에 -1을 반환
        str_len=read(sock, message, BUF_SIZE-1);
        // 문자열의 끝을 알리기 위해서 추가
        message[str_len]=0;
        printf("Message from server: %s", message);
    }
	
    close(sock);
    return 0;
}

void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

설명이 필요한 부분은 코드 위에 주석을 달아놨으니 편하게 보시면 됩니다.


클라이언트 코드의 문제점

클라이언트의 코드를 보면서 문제점을 느낀 사람은 TCP의 특성을 잘 알고 있으신 분들일 것입니다. 그 이유는 TCP의 전송 특성에서 찾을 수 있습니다. 일단 문제가 되는 클라이언트의 코드를 살펴봅시다.

write(sock, message, strlen(message));
str_len=read(sock, message, BUF_SIZE-1);
message[str_len]=0;
printf("Message from server: %s", message);

위 코드에서 서버에게 보내는 write함수는 문제가 없지만, 서버로부터 수신하는 read함수 부분이 문제가 있습니다. 왜 문제가 되는 것일까요? 저도 문제가 된다는 말을 보고 "이게 왜 문제가 되지..."라고 생각했었습니다. 그런데 책의 앞으로 가서 TCP에 대해서 설명하는 부분을 다시 보니 책에는 이렇게 설명되어 있었습니다.

"TCP는 전송되는 데이터의 경계가 존재하지 않는다."

이 말의 의미는 무엇일까요?

이에 대한 해답은 내일 공개됩니다.

힘들어서...

 

오늘은 여러 클라이언트의 요청에 대한 처리가 가능한 Iterative서버를 만들어보았습니다. 그리고 클라이언트에서 입력해서 전송한 데이터를 서버에서 다시 재전송해주는 에코 서버와 에코 클라이언트를 만들어보았습니다.

 

내일은 TCP 전송 특성에 대해서 소개하고, 오늘 해결하지 못한 에코 클라이언트의 문제점을 해결해보는 시간을 가져보도록 하겠습니다.

긴 글 읽어주셔서 감사합니다. ㅎㅎ