Part1_TCP 기반 서버 클라이언트 1(2)

·2023년 11월 5일
0

[TCP 기반 클라이언트의 구현]

순서 : 소켓 생성(socket()) → 연결 요청(connect()) → 데이터 송/수신(read() & write()) → 연결 종료(close())

서버가 listen()를 호출하게 되면 클라이언트의 연결 요청이 대기 큐에 들어갈 수 있게됨

즉, 서버가 연결 요청을 받아 줄 수 있는 상태가 됨

그렇다면 클라이언트의 연결 요청은 어느 시점에 발생할까?

→ connect()가 호출되는 시점에서 발생하며 이 과정을 연결 요청이라함

#include <sys/types.h>
#include <sys/socket.h>

int connetc(int sockfd, struct sockaddr* serv_addr, int addrlen);

// 연결에 성공하면 0을 실패 시 -1을 리턴

// sockfd : 미리 생성해 놓은 소켓의 파일 디스크립터
// 클라이언트도 연결을 요청하고 데이터를 송/수신하기 위해서는 기본적으로 소켓이 있어야 함

// serv_addr : 연결 요청을 보낼 서버의 주소 정보를 지닌 구조체 변수의 포인터

// addlen : serv_addr 포인터가 가리키는 주소 정보 구조체 변수의 크기가 됨

connect 함수가 리턴되는 시점은 연결 요청이 서버에 의해 수락되거나 오류가 발생해서 연결 요청이 중단되는 경우임

연결 요청이 바로 이루어지지 않고 서버의 대기 큐에서 대기하고 있는 상태라면 connect 함수는 리턴되지 않고 블로킹 상태에 있게 됨


[클라이언트 소켓의 주소 정보는 어디에?]

서버를 구현 할 때 보면 서버 소켓에 주소 정보와 Port 정보를 할당 해 주었음

그런데 클라이언트는 소켓 생성만 하고 바로 연결 요청 작업을 함

그렇다면 클라이언트의 소켓은 주소가 필요 없을까?

→ 당연히 소켓이 필요함

그렇다면 언제 할당 해 준걸까?

→ connect()가 호출될 때 커널이 호스트에 할당되어 있는 IP 주소와 남아 있는 Port 주소 중 하나를 골라 할당해줌

⇒ 그래서 bind 함수를 명시적으로 호출할 필요가 없음


[Hello World 클라이언트 프로그램 다시 보기]

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>

void error_handling(char* message);

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

	if(argc != 3)
	{
		printf("Usage : %s <IP> <port>\n", argv[0]); // 여기 argv[0]에 뭐 있지?
		exit(1);
	}

	sock = socket(PF_INET, SOCK_STREAM, 0); // 서버 접속을 위한 소켓 생성
	if(sock == -1)
		error_handling("socket() error");

	memset(&serv_addr, 0, sizeof(serv_addr));
	serv_addr.sin_family=AF_INET;
	serv_addr.sin_addr.s_addr=inet_addr(argv[1]);
	serv_addr.sin_port=htons(atoi(argv[2]));

	if(connect(sock, (struct socketaddr*)&serv_addr, sizeof(serv_addr))== -1)
		error_handling("connect() error!");

	str_len = read(sock, message, sizeof(message) - 1); // 데이터 수신
	if(str_len == -1)
		error_handling("read() error!");
	message[str_len] = 0;
	printf("Message from server : %s \n", message);
	close(sock);
	return 0;
}

void error_handling(char* message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}
  • 소켓 생성 (socket 함수)
  • 구조체 변수 초기화 후 연결 요청 (connect 함수)
  • 송/수신 (여기서는 read 함수)
  • 연결 종료 (close 함수)

[TCP 서버 / 클라이언트 함수 호출 관계]

클라이언트의 연결 요청은 서버가 ‘연결 요청 대기 상태’에 있을 때 진행되어야 하므로, 서버의 listen() 함수 호출 이후에 클라이언트의 connect 함수가 호출되어야 함

서버가 accpet 함수를 호출 했을 때 연결 요청 대기 큐가 비어 있다면 서버는 accept 함수 호출과 동시에 블로킹 상태가 됨 → 그러다가 클라이언트의 요청이 들어오게 되면, 연결 요청을 바로 수락하면서 블로킹 상태에서 빠져 나옴

[Iteractive 서버의 구현]

이전에 작성한 HelloWorld 예제는 대기 큐의 크기가 5지만 한 클라이언트의 요청에만 응답하고 더 이상 어떤 요청도 무시하고 바로 종료되어 버림

→ 함수 호출 흐름을 수정해야 함

위 그림처럼 송/수신 과정을 거친 후 클라이언트 연결 소켓의 종료를 의미하는 close()를 호출하고,

다른 클라이언트의 연결 요청을 수락하기 위해 다시 accept() 호출 하는 형태로 변경

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>

void error_handling(char* message);

int main(int argc, char** argv)
{
	int serv_sock;
	int clnt_sock;
	struct sockaddr_in serv_addr;
	struct sockaddr_in clnt_addr;
	int clnt_addr_size;
	char message[]="Hello World!\n";
	
	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_addr, 0, sizeof(serv_addr));
	serv_addr.sin_family=AF_INET; // 체계
	serv_addr.sin_addr.s_addr=htonl(INADDR_ANY); // 자동으로 ip주소 가져와서 네트워크 long 주소로 변환
	serv_addr.sin_port=htons(atoi(argv[1])); // argv[1]에 포트 번호가 있으니 가져와서 네트워크 주소로 변환

	if(bind(serv_sock, (struct sockaddr*) &serv_addr,
			sizeof(serv_addr)) == -1) // 소켓에 주소 할당
		error_handling("bind() error");

	if(listen(serv_sock, 5) == -1) // 연결 요청 대기 상태로 진입
		error_handling("listen() error");

	clnt_addr_size = sizeof(clnt_addr);
	// 연결 요청 수락

	for( ; ; )
	{
		clnt_sock = accept(serv_sock, (struct sockaddr*) &clnt_addr, &clnt_addr_size);
	
		if(clnt_sock == -1)
		{
			error_handling("accept() error");
			break;
		}
		
			// 데이터 전송 후 클라이언트 연결 소켓의 연결 종료
			write(clnt_sock, message, sizeof(message));
			close(clnt_sock);
	}

	return 0;
}

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

[실행 결과]

서버는 한 번만 실행시켜 놓고도 여러 번 클라이언트가 접속할 수 있게 됨


[에코(echo) 서버/클라이언트의 구현]

  • 에코 서버 클라이언트가 전송해 주는 데이터를 그대로 되돌려 전송해 주는 기능의 서버를 의미

[에코 서버의 구현]

// echo_server.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>

#define BUFSIZE 1024

void error_handling(char* message);

int main(int argc, char** argv)
{
    int serv_sock;
    int clnt_sock;
    char message[BUFSIZE];
    int str_len;

    struct sockaddr_in serv_addr;
    struct sockaddr_in clnt_addr;
    int clnt_addr_size;

    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_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family=AF_INET;
    serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);
    serv_addr.sin_port=htons(atoi(argv[1]));

    if(bind(serv_sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr)) == -1)
        error_handling("bind() error");
    
    if(listen(serv_sock, 5) == -1)
        error_handling("listen() error");
    
    clnt_addr_size=sizeof(clnt_addr);

    for(;;)
    {
        clnt_sock=accept(serv_sock, (struct sockaddr*) &clnt_addr, &clnt_addr_size);
        if(clnt_sock == -1)
            error_handling("accept() error");
    
        while((str_len=read(clnt_sock, message, BUFSIZE)) != 0)
        {   
            write(clnt_sock, message, str_len);
            write(1, message, str_len);
        }

        close(clnt_sock);
    }

    return 0;
}

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

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>

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

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

    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_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family=AF_INET;
    serv_addr.sin_addr.s_addr=inet_addr(argv[1]);
    serv_addr.sin_port=htons(atoi(argv[2]));

    if(connect(sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr)) == -1)
        error_handling("connect() error!");

    while(1)
    {
        fputs("전송할 메시지를 입력하세요 (q to quit) : ", stdout);
        fgets(message, BUFSIZE, stdin);

        if(!strcmp(message, "q\n")) break;
        write(sock, message, strlen(message));

        str_len=read(sock, message, BUFSIZE-1);
        message[str_len] = 0;
        printf("서버로부터 전송된 메시지 : %s \n", message);
    }

    close(sock);
    return 0;
}

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

[실행 결과]

(server)

(client)

0개의 댓글