본문 바로가기

BackEnd/Linux

Linux / network - 리눅스기초를 활용한 데이터 통신 5(서버, 클라이언트, 소켓통신, 데이터 합치기)

안녕하세요 인포돈입니다.

이번 포스팅부터 본격적인 리눅스 소켓 통신 코딩을 적어보겠습니다. 본 포스터에서는 기본적으로 널리 알려져 있는 기본 코드를 활용하여 클라이언트의 데이터를 서버에서 통합하여 각 클라이언트로 다시 보내주는 코드를 작성해보겠습니다.

본 내용은 우분투를 기본으로 작성되었습니다.
Cloud Computing을 활용하여 서버를 구축하였습니다.

이번 코딩의 목적은 클라이언트 3개와 서버 1개로 구성하려 합니다. 여기서 각 클라이언트에서 어떠한 문장을 넘겨주면 서버 측에서는 3개의 문장을 공백으로 구분하여 3개의 클라이언트에 다시 뿌려주는 역할을 담당하게 됩니다. 클라이언트 측 코드는 사실 크게 손본 게 없으므로 마지막에 간단한 코드 리뷰만 남겨놓도록 하겠습니다.

 

서버 코드 및 설명

#include <sys/socket.h>
#include <sys/stat.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#define MAXBUF 1024
int main(int argc, char **argv){
     int server_sockfd, client_sockfd;
     int client_temp[3];
     int client_len, n;
     char buf[MAXBUF];
     struct sockaddr_in clientaddr, serveraddr;
     client_len = sizeof(clientaddr);
     char msg[MAXBUF];
     
     if ((server_sockfd = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP )) == -1){
     perror("socket error : ");
     exit(0);
     }
     
     memset(&serveraddr, 0x00, sizeof(serveraddr));
     serveraddr.sin_family = AF_INET;
     serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
     serveraddr.sin_port = htons(atoi(argv[1]));
     bind (server_sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
     listen(server_sockfd, 5);
     while(1)
     {
         for(int i = 0 ; i < 3 ; i++){
             client_temp[i] = accept(server_sockfd, (struct sockaddr *)&clientaddr,&client_len);
             printf("New Client[%d] Connect: %s\n",i, inet_ntoa(clientaddr.sin_addr));
         }
         memset(buf, 0x00, MAXBUF);
         memset(msg, 0x00, MAXBUF);
         for(int i = 0 ; i < 3 ; i ++){
             if ((n = read(client_temp[i], buf, MAXBUF)) <= 0){
                 close(client_sockfd);
                 continue;
             }
         printf("from client[%d] : %s",i,buf);
         for(int i = 0 ; buf[i]!=0 ; i++){
             if(buf[i]=='\n'){
             buf[i]=0;
             break;
             }
         }
         strcat(msg," ");
         strcat(msg,buf);
         }
         
         strcat(msg,"\n");
         printf("\n %s \n", msg);
         for(int i = 0 ; i < 3 ; i++){
             if (write(client_temp[i], msg, MAXBUF) <=0){
                 perror("write error : ");
                 close(client_sockfd);
             }
             close(client_temp[i]);
         }
     }
     close(server_sockfd);
     return 0;
}

서버 측 코드를 간략히 리뷰해 보겠습니다.


소켓 사용 전처리 코드 리뷰

if ((server_sockfd = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP )) == -1){
     perror("socket error : ");
     exit(0);
     }
     
 memset(&serveraddr, 0x00, sizeof(serveraddr));
 
 serveraddr.sin_family = AF_INET;
 serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
 serveraddr.sin_port = htons(atoi(argv[1]));
 
 bind (server_sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
 
 listen(server_sockfd, 5);

 * socket() 함수

소켓 함수는 어떤 프로토콜을 활용하여 소켓으로 통신할 것인지를 결정하는 함수 있니다. 더욱 쉽게 이해한다면, 그냥 소켓을 생성하는 함수라고 생각해도 무방합니다. 이제 각 인자가 무엇을 의미하는지를 알아야 합니다.

1) AF_INET

첫 번째 인자는 어떤 영역에서 통신할 것인지를 알려주는 인자입니다. 그중 AF_INET은 IPv4 주소 체계를 사용하는 인터넷 망에서 통신할 것임을 알려주는 역할을 합니다.

2) SOCK_STREAM

어떤 타입의 프로토콜을 사용할 것인지에 대해 설정하는 것으로 SOCK_STREAM은 TCP를 활용한다는 의미입니다. UDP를 사용하기 위해서는 SOCK_DGRAM을 사용합니다.

3) IPPORTO_TCP

어떤 프로토콜의 값을 결정하는 것입니다. 일반적으로 TCP일 때는 해당 상수를 사용합니다.

 

 * serveraddr 변수

해당 변수는 이미 c언어에 내장되어 있는 구조체입니다. 그 구조체는 sockaddr_in 해당 구조체는 아래와 같이 구성되어있습니다.

struct sockaddr_in { 
short sin_family; // 주소 체계: AF_INET 
u_short sin_port; // 16 비트 포트 번호
struct in_addr sin_addr; // 32 비트 IP 주소 
char sin_zero[8];
}; 

struct in_addr { 
u_long s_addr; // 32비트 IP 주소를 저장 할 구조체
};

여기서 우리는 구조체를 활용하여 서버와 클라이언트의 정보를 담는 역할을 합니다.

 

 * bind함수

bind함수는 주소 정보를 앞서 생성한 소켓에 할당하는 함수이다. 깊게 생각하면 다소 어려운 감이 있지만, 현 단계에서는 소켓이 어디로 연결을 할지를 결정하는 함수이다.

 

 * listen함수

주소가 할당된 소켓이 연결 요청 대기상태로 들어간다.  사실상 개인적인 견해긴 하지만 listen과 accept은 큰 차이가 없는 함수라고 본다. 그러나 현재 이 두 개의 함수를 따로 지정해야지만 소켓이 사용 가능해진다. 예를 들어 여러 클라이언트가 서버에 접속을 요청하면 대게 순서대로 입장을 시켜준다. 이 순선대로 입장하기 전에 대기하는 일종의 대기 장소이다.

 

 * accept함수

앞선 listen과 굉장히 유사하지만 앞선 대기 순서와 비유하자면 순서가 되어 입장을 하는 것을 의미한다.


서버 / 클라이언트 read, write

이제는 크게 크게 어려울 것이 없다. 우리의 목적은 3개의 클라이언트의 값을 1개의 서버에서 통합하여 보내 주면 된다. 기본 소켓을 만들고 클라이언트를 받아들일 준비가 되었다면, 클라이언트의 값이 들어오면 값을 읽어온다.

for(int i = 0 ; i < 3 ; i++){
     client_temp[i] = accept(server_sockfd, (struct sockaddr *)&clientaddr,&client_len);
     printf("New Client[%d] Connect: %s\n",i, inet_ntoa(clientaddr.sin_addr));
 }
 
 memset(buf, 0x00, MAXBUF);		//값을 받기위해 0으로 초기화
 memset(msg, 0x00, MAXBUF);		//값을 받기위해 0으로 초기화
 
 for(int i = 0 ; i < 3 ; i ++){
     if ((n = read(client_temp[i], buf, MAXBUF)) <= 0){
         close(client_sockfd);
         continue;
     }
     printf("from client[%d] : %s",i,buf);
     for(int i = 0 ; buf[i]!=0 ; i++){		
         if(buf[i]=='\n'){
             buf[i]=0;
             break;
         }
     }
 strcat(msg," ");			//각클라이언트 값을 나눌 '공백' 저장
 strcat(msg,buf);			//각클라이언트 값 저장
 }	
 strcat(msg,"\n");			//개행문자, 즉, 문자열의 끝을 알려줌
 printf("\n %s \n", msg);	//msg값 확인

 - accept을 통해서 서버에 들어오려는 클라이언트를 하나의 배열에 하나씩 저장합니다. 여기서 배열은 3개로 지정합니다.

 

 - 저장된 클라이언트를 순 외하면서 하나씩 값을 읽어옵니다. 

 

 - 여기서 우리는 어떤 문장이 들어올지 모르는 충분한 크기의 buf배열을 만듭니다. 이때 클라이언트에서 받은 값이 외는 알 수 없는 값으로 초기화되어 옳지 않은 출력이 나온다. 이를 해결하기 위해서 buf를 순회하며 문장의 끝을 알리는 0을 넣어준다.

 

 - 해당 메시지를 msg라는 변수에 담는다

 

 - 이를 3개의 클라이언트를 돌며 반복하여 msg에 저장하여 순회 완료 후 서버에 출력하여 확인한다.

 

for(int i = 0 ; i < 3 ; i++){
     if (write(client_temp[i], msg, MAXBUF) <=0){
         perror("write error : ");
         close(client_sockfd);
         }
 close(client_temp[i]);

마지막은 간단하다. 각 클라이언트에서 온 값을 buf에서 -> msg의 변수에 공백을 두어 저장해 두었다. 이제 해당 메시지를 모든 클라이언트에 보내준 후 클라이언트와의 접속을 끊어준다.


실행화면

서버 console
클라이언트 콘솔


 사실 생각해보면 이러한 코딩은 굉장히 잘못된 것을 알 수 있다. 간단히 서버와 클라이언트를 이해하려다 보니 어감이 맞지 않는 코드들이 있다고 생각한다. 그러나 현시점에서는 이상하다는 것은 알지만, 정확히 어떤 식으로 고쳐야 할지 좀 더 고심해볼 필요가 있어 보인다. 우선은 이후 포스팅도 이어가 보면서 좀 많은 이해를 하고 다시 고심해볼 필요가 있다.