개발 · 컴퓨터공학 / / 2021. 10. 4. 05:10

네트워크 프로그래밍 - TCP 함수 호출 순서, Iterative 서버

728x90
반응형

인터넷 주소의 초기화

일반적인 인터넷 주소의 초기화 과정은 다음과 같다.

위 코드는 서버에서도 클라이언트에서도 진행하는 과정이다. sockaddr_in 구조체에 서버에서 사용하는 IP주소와 PORT번호를 담는다. 클라이언트에서는 IP와 PORT번호를 시스템에서 알아서 처리해주므로 클라이언트의 IP와 PORT번호를 제공하지 않아도 된다. 서버에서는 구조체에 IP주소와 PORT번호를 담고 binding을 해준다.

 

서버에서 주소를 설정하는 것은 해당 IP와 PORT번호로 들어오는 정보는 모두 서버로 보내라고 시스템에게 알려주는 것이고, 클라이언트에서 주소를 설정하는 것은 해당 IP와 PORT번호로 연결하라는 의미이다.

 

INADDR_ANY

현재 실행중인 컴퓨터의 IP를 소켓에 부여할 때 INADDR_ANY이다. 서버에서는 자신의 IP를 포함한 127로 시작하는 IP주소들, 그 외에 여러개 할당된 IP주소들로 오는 것들을 모두 서버로 보내준다. 

 

./hserver 9190

서버는 위와 같은 방식으로 실행한다. 서버가 받는 소켓 주소는 INADDR_ANY로 지정하므로, 소켓의 PORT번호만 인자를 통해 전달하면 된다. 

./hclient 127.0.0.1 9190

클라이언트는 서버의 IP와 PORT번호를 인자로 전달한다. 127.0.0.1은 루프백 주소라고 하여 클라이언트를 실행하는 컴퓨터의 IP주소를 의미한다. 위의 경우는 같은 컴퓨터에서 실행하였기에 루프백 주소를 전달한다. 

소켓 인터넷 주소 할당

#include <sys/socket.h>

int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen);
//  성공 시 0, 실패 시 -1 반환
// sockfd : 주소 정보를(IP와 PORT를) 할당할 소켓의 파일 디스크립터
// myaddr : 할당하고자 하는 주소정보를 지니는 구조체 변수의 주소 값
// addrlen : 두 번째 인자로 전달된 구조체 변수의 길이정보

bind함수를 통해 IP와 PORT번호가 담긴 구조체 myaddr을 소켓에 할당한다. 

int serv_sock;
struct sockaddr_in serv_addr;
char *serv_port="9190";

/* 서버 소켓(리스닝 소켓) 생성 */
serv_sock=socket(PF_INET, SOCK_STREAM, 0);

/* 주소정보 초기화 */
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(serv_port));

/* 주소정보 할당 */
bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

위와 같은 과정이 일반적인 주소할당 과정 코드이다.

socket함수로 소켓을 생성하고, bind를 통해 생성한 소켓에 주소를 할당한다.

htonl과 htons와 같이 전달하기 위해 구조체에 넣을 때에는 network byte order로 변경해주어야 한다. 

반대로 읽을 때는 host byte order로 변경해준다.

 

TCP 기반 서버, 클라이언트 구현

TCP 서버 함수 호출 순서

소켓을 생성하고 bind 함수까지 호출되면 주소가 할당된 소켓을 얻게 된다. listen함수의 호출로 연결 요청이 가능한 상태가 된다. listen함수 호출까지의 과정을 알아보자

연결요청 대기 상태로의 진입

#include <sys/type.h>

int listen(int sock, int backlog);
// 성공 시 0, 실패 시 -1 반환
// sock : 연결 요청 대기상태에 두고자 하는 소켓의 파일 디스크립터 전달, 이 함수의 인자로 
// 전달된 디스크립터의 소켓이 서버 소켓(리스닝 소켓)이 된다.
// backlog : 연결요청 대기 큐(Queue)의 크기정보 전달, 5가 전달되면 큐의 크기가 5가 되어
// 클라이언트의 연결요청을 5개까지 대기시킬 수 있다.

file descriptor의 0~2는 입출력으로 지정되어있고 소켓을 할당하면 3부터 할당받는다. 소켓에도 일반소켓과 서버소켓이 구분되는데, 일반소켓은 SYN이 왔을 때 SYN + ACK을 보내는 역할을 하지 못하고 서버소켓으로 변환해주어야 가능하다.

 서버가 SYN을 받고 SYN + ACK을 클라이언트에게 보내면 클라이언트로부터 ACK이 올 때 까지 기다리는데, 해당 클라이언트를 대기시켜놓는 대기 큐(Queue)라는 것이 있다. 

클라이언트의 요청 SYN에 대한 응답으로 SYN+ACK을 보내고 그에대한 ACK이 올 때 까지는 해당 클라이언트를 대기 큐에 넣는데 backlog는 그 크기의 값을 인자로 설정하는 것이다. 

이러한 역할을 하는 소켓을 서버소켓 또는 리스닝 소켓이라 한다. listen함수를 호출하는 것이 소켓을 리스닝 소켓으로 만드는 작업이다.

클라이언트 연결요청 수락

#include <sys/socket.h>

int accept(int sock, struct sockaddr * addr, socklen_t * addrlen);
// 성공 시 생성된 소켓의 파일 디스크립터, 실패 시 -1 반환

// sock : 서버 소켓의 파일 디스크립터 전달
// addr : 연결요청 한 클라이언트의 주소정보를 담을 변수의 주소 값 전달, 함수호출이 완료되면
// 인자로 전달된 주소의 변수에는 클라이언트의 주소 정보가 채워진다
// addrlen : 두 번째 매개변수 addr에 전달된 주소의 변수 크기를 바이트 단위로 전달, 단 크기정보를
// 변수에 저장한 다음에 변수의 주소 값을 전달한다. 그리고 함수호출이 완료되면 크기정보로 채워져 
// 있던 변수에는 클라이언트의 주소정보 길이가 바이트 단위로 계산되어 채워진다.

대기 큐에 클라이언트가 저장된 상태로부터 해당 클라이언트로부터 ACK이 오게되면 대기 큐에서 accept함수를 통해 클라이언트를 꺼내 반환하고, 해당 반환값이 바로 새로운 소켓이다. 이렇게 생성된 새로운 소켓을 통해 read, write를 진행한다. 서버 소켓은 SYN을 받아 새로운 소켓에 클라이언트를 할당하는 역할만 진행한다. 

 accept 함수는 클라이언트로부터의 ACK이 오면 연결요청 대기 큐에서 해당 클라이언트를 꺼내고 새로 생성된 소켓을 통해 해당 클라이언트와 송수신을 할 수 있도록 한다. 

 sock이라는 서버에서 accept를 실행하고, addr은 상대방에 대한 정보이다. 

addr은 sockaddr이라는 구조체로 읽을 때는 host byte order로 바꾸어 읽어야 하고 보낼 때는 network byte order로 보내거나 문자열로 보내서 받을 때 정수로 parsing 하거나 둘 중 하나의 방법으로 전송한다.

TCP 클라이언트의 함수호출 순서

#include <sys/socket.h>

int connect(int sock, const struct sockaddr * servaddr, socklen_t addrlen);
// 성공 시 생성된 소켓의 파일 디스크립터, 실패 시 -1 반환

// sock : 클라이언트 소켓의 파일 디스크립터 전달
// servaddr : 연결요청 한 클라이언트의 주소정보를 담을 변수의 주소 값 전달, 함수호출이 완료되면
// 인자로 전달된 주소의 변수에는 클라이언트의 주소정보가 채워진다.
// addrlen : 두 번째 매개변수 servaddr에 전달된 주소의 변수 크기를 바이트 단위로 전달, 단, 
// 크기정보를 변수에 저장한 다음에 변수의 주소 값을 전달한다. 그리고 함수호출이 완료되면 
// 크기정보로 채워져 있던 변수에는 클라이언트의 주소정보 길이가 바이트 단위로 계산되어 채워진다.

socket함수로 소켓을 생성하고, connect함수를 호출하면 SYN 패킷을 전송한다. connect함수를 호출할 때, 자동적으로 IP주소를 만들고, PORT번호는 쓰고있지 않는 PORT번호를 자동적으로 할당한다. 

TCP 기반 서버, 클라이언트 함수호출 관계

서버는 socket와 bind에서 서버가 사용할 IP주소 PORT번호 할당을 하고, listen을 통해 생성한 소켓을 서버소켓으로 만들고 연결요청 대기 큐를 만들어 놓는다. accept를 통해 클라이언트의 연결 요청을 대기한다.

 클라이언트에서는 socket을 생성하고 connect을 호출한다. connect함수를 통해 SYN을 보내고 SYN + ACK이 오기까지 대기한다. SYN을 서버에게 보내면 실제로는 listen이 SYN+ACK을 보내지만 서버는 accept대기를 이미 하고있으므로 클라이언트는 accept이 SYN+ACK을 보내는 것처럼 느낀다. 서버가 SYN+ACK을 보내고 클라이언트가 ACK을 보내면 accept이 return한다. 

이렇게 연결된 후 read / write를 한다. read는 block함수이므로 데이터가 들어와야 동작한다. 

 

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

Iterative 서버 구현

반복적으로 accept함수를 호출하여 계속해서 클라이언트의 연결요청을 수락하는 서버의 형태를 Iterative 서버라고 한다. 하나의 클라이언트에 대한 요청이 끝나야 다음 클라이언트에 대한 요청을 처리하는 형태이다. 즉 동시에 둘 이상의 클라이언트에게 서비스를 제공할 수 있는 모델은 아니다.

 

while(1)
{
	fputs("Input message(Q to quit): ", stdout);
	fgets(message, BUF_SIZE, stdin);
    
	if(!strcmp(message,"q\n") || !strcmp(message, "Q\n"))
		break;
	write(sock, message, strlen(message));
	str_len=read(sock, message, BUF_SIZE-1);
	message[str_len]=0;
	printf("Message from server: %s", message);
}

위 코드는 클라이언트 코드의 일부이다.

 

stdin 즉 키보드에서 읽은 값을 message에 저장한다.

사용자가 q / Q를 입력하면 while문을 탈출하고 close함수로 진행된다. 그렇지 않으면 입력 받은 내용을 write함수를 통해 sock에 전달한다. read를 통해 보냈던 값을 그대로 다시 읽어 message에 저장하고, 받은 byte수를 반환한다. 

문자의 마지막을 0으로 설정하고 출력한다. 

 

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);
}

위 코드는 서버 코드의 일부이다.

 

클라이언트가 연결요청을 하면 연결 요청 대기 큐에 넣고, 클라이언트가 마지막 ACK을 보내면 accept이라는 함수가 해당 클라이언트를 꺼내서 새로운 소켓 clnt_sock을 생성하고 clnt_adr에 상대 클라이언트의 정보를 저장한다. 

read함수에서 clnt_sock 소켓에서 읽은 값을 message에 저장하고, 읽은 byte 수를 반환한다. write에서 받았던 message의 내용을 그대로 clnt_sock 소켓으로 보낸다.

read함수는 receiving buffer가 비어있는 경우에는 block 된 상태로 대기하며, 해당 while문은 상대방이 FIN을 보내야만 read의 return값이 0이되어 탈출한다. 

클라이언트가 FIN을 보내고 FIN + ACK을 서버가 보내고 마지막으로 클라이언트에게 ACK을 받아 close로 소켓을 닫으면 for문이 진행되어 대기 큐에 클라이언트가 있는 경우 해당 클라이언트를, 없는 경우 accept상대로 대기한다.

 

에코 클라이언트의 문제점

위와 같이 write로 전송한 message를 read로 다시 읽는 에코 클라이언트는 한 번의 read 함수 호출로 전송되었던 문자열 전체를 읽는데, 이것이 제대로 될 때도 있지만, 그렇지 않을 수도 있다.

728x90
반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유