네트워크 응용층

suhan cho·2022년 3월 24일
0

클라이언트-서버 개념

  • 로컬 컴퓨터는 원격 컴퓨터에게 서비스를 요청하기 위한 프로그램을 수행한다.
  • 원격 컴퓨터는 요청된 프로그램에게 서비스를 제공

서버

  • 원격에서 동작하는 프로그램으로 클라이언트에게 서비스를 제공
  • 프로그램 시작되면 클라이언트가 보내는 요청을 위해 문을 열어두고 서비스에 대한 요청이 오기 전까지는 서비스 시작 안함
  • 무한프로그램으로 어떤 문제가 발생하기 전까지는 계속 수행하며 클라이언트 요청을 기다린다.
  • 요청 도착 시 순차적으로 혹은 동시에 응답

클라이언트

  • 로컬 컴퓨터에서 동작하며 서버에게 서비스를 요청
  • 클라이언트 프로그램은 유한한데 서비스 요청 시 시작 완료되면 종료
  • 원격 호스트의 IP주소와 그 장치에서 수행되는 특정 서버 프로그램의 잘 알려진 포트를 사용하여 통신 채널 설정

동시성

  • 클라이언트와 서버 모두 동시(concurrent) 모드로 동작 가능

  • 클라이언트 동시성

    • 반복적으로 클라이언트를 수행한다는 것은 하나씩 순차적으로 수행한다는 것을 의미
    • 하나의 클라이언트가 먼저 수행된 뒤 다른 클라이언트를 수행하기 전에 이를 종료해야 함.
  • 서버의 동시성

    • 반복적 서버는 한 순간에 하나의 요청만을 처리가능
    • 동시 서버는 동시에 여러 요청을 처리가능 여로 요청 간에 시간을 나누어 쓸 수 있다.
    • 비연결형 전송층 프로토콜 UDP를 사용하거나 연결형 전송층 프로토콜 TCP/SCTP를 사용
    • 서버의 동작은 두가지 요인에 의존 하나는 전송층 프로토콜이고 다른 하나는 서비스 방식

비연결형 반복 서버

  • UDP를 사용하는 서버는 보통 반복적 서버가 한 번에 하나의 요청만 처리함을 의미
  • 하나의 잘 알려진 포트만을 사용하며 이 포트로 도착하는 모든 데이터그램은 차례대로 서비스 받기를 기다림

연결형 동시 서버

  • TCP를 사용하는 서버들은 보통 동시적, 서버가 동시에 많은 클라이언트를 처리할 수 있다.
  • 하나의 잘 알려진 포트만을 사용하지 않음, 각 연결이 포트를 요청하여 많은 연결이 동시에 개설되어 있을 수 있기 때문
  • 서버는 오직 하나의 잘 알려진 포트만을 사용할 수 있는데, 많은 임시 포트와 하나의 잘 알려진 포트를 사용하는 것이다.
  • 서버는 잘 알려진 포트를 이용하여 연결 요청 수용 클라이언트는 연결을 하기 위해 이 포트를 사용하여 초기 접근 시도 연결이 이루어지면 서버는 임시 포트를 할당한 뒤 잘 알려진 포트를 해제

소켓 인퍼페이스

  • 전송층을 통해 연결 설정하고, 다른 기계로 데이터를 보내고 또한 데이터를 받고, 연결을 종료하는 명령들의 새로운 집합 -> 인터페이스
  • 통신을 위해서는 소켓 인터페이스, 전송 계층 인터페이스, 스트림이 필요

소켓

  • 우리가 일상생활에서 보는 하드웨어 소켓을 소프트웨어로 표현
  • 순서
    1. 운영체제에게 소켓을 생성하기 위한 요청
    2. 응용 프로그램은 데이터를 보내고 받기 위해 소켓에 플러그를 꽂음
    3. 데이터 통신이 시작되기 위해서는 통신하는 종단에 각각 하나씩 한 쌍의 소켓이 필요

데이터 구조

  • 계열(family)
    IPv4(IF_INET), IPv6(IF_INET6) 도메인 프로토콜 등과 같은 프로토콜 그룹 정의

  • 유형(type)
    4가지 소켓유형인 SOCK_STREAM(TCP용), SOCK_DGRAM(UDP용), SOCK_SEQPACKET(SCTP용), SOCK_RAW(IP서버 직접 이용)

  • 프로토콜
    타입에서 정해서 보통 0으로 함

  • 로컬(local) 소켓주소
    소켓주소는 IP주소와 포트 번호의 조합

  • 원격지(remote) 소켓 주소
    원격지 소켓주소 정의

소켓 주소의 구조

IP 주소와 포트 번호의 조합을 이해해야한다.

함수

프로세스 생성하기 위한 함수들

socket()

  • 운영체제는 프로세스가 수행할 때까지는 소켓을 생성하지 않음
  • 프로세스는 소켓을 생성하기 위해 socket함수를 호출해야한다.
int socket(int family, int type, int protocol)
  • 호출 성공하면 함수는 유일한 소켓 기술자 sockfd를 반환 성공 못하면 -1

bind()

  • 소켓을 로컬 컴퓨터의 포트에 결합하기 위해 bind함수 호출
  • 로컬 소켓 주소(로컬 IP 주소와 로컬포트 번호를 채운다)
int bind(int sickfd, const struct sockaddress* localAddress, socklen_t addrLen);
  • sockfd는 socket함수 호출로부터 반환되는 소켓 기술자의 값
  • localAddress는 정의되어야 하는 소켓 주소(시스템, 개발자)에 대한 포인터
  • addLen 소켓 주소의 길이
  • 클라이언트는 먼저 socket함수 호출하여 반환된 값을 소켓 기술자로서 사용

connect()

  • 원격 소켓 주소를 소켓 구조체에 추가하기 위해 사용
int connect(int sockfd, const struct sockaddress* remoteAddress, socklen_t addrLen);
  • 속성은 같지만 두 번째와 세 번째 속성이 로컬주소 대신 원격 주소를 의미

listen()

  • TCP서버에 의해서만 호출
  • TCP가 소켓을 생성한 후에, 소켓이 클라이언트 요청을 수신할 준비가 되어 있다는 것을 운영체제에게 알릴 때 사용
  • backlog는 연결 요청의 최대 개수 실패시 -1
int listen(int sockfd, int backlog);

accept()

  • TCP에게 클라이언트로부터 연결을 받을 준비가 되어 있다는 것을 알리기 위해
int accept(int sockfd, const struct sockaddress* clientAddr, socklen_t* addrLen);
  • 마지막 두 속성은 주소와 길이에 대한 포인터
  • accept() 호출될 때, 클라이언트가 연결을 설정할 때까지 자신을 차단하는 차단함수
  • 클라이언트 소켓 주소와 주소 길이를 얻어 이를 서버 프로세스에게 보내 클라이언트와 접속하는데 사용
  • 주의 사항
    • accept함수에 대한 호출은 프로세스가 클라이언트의 연결 요청이 대기 버퍼에 있는지를 검사하는 것, 큐에 적어도 하나의 요청이 있을 때 가동
    • accept 호출 후, 새로운 소켓이 생성되어 클라이언트 소켓과 서버의 소켓 사이에 통신이 설정
    • 수신된 주소는 새 소켓의 원격 소켓 주소에 채워진다.
    • 클라이언트의 주소는 포인터를 통해 반환, 만일 개발자가 이 주소가 필요하지 않으면 NULL로 채움
    • 반환된 주소 길이 함수로 전달 포인터를 통해 반환, 길이 필요하지 않으면 NULL

fork()

  • 프로세스가 프로세스를 복제하기 위해 사용
  • fork함수를 호출하는 프로세스는 부모 프로세스라 부른다. 복제 되어 생성된 프로세스는 자식 프로세스이다.
pid_t fork(fork);
  • 이 프로세스는 한번 호출되지만 두 번 반환된다.
  • 부모 프로세스 반환 값은 양의 정수, 자식 프로세스 반환 0 오류 있으면 -1

send(), recv()

  • send() 프로세스가 원격 장치에서 수행 중인 다른 프로세스에게 데이터를 보내기 위해 사용됨
  • recv() 프로세스가 원격 장치에서 수행 중인 프로세스로부터 데이터를 받기 위해 사용
  • 이 함수들은 두 기계 사이에 이미 설정된 연결이 있다고 가정한다. 따라서 TCP에 의해서만 사용될 수 있다.
  • 송신하거나 수신된 바이트 수를 반환
int send(int sockfd, const void* sendbuf, int nbytes, int flags);
int recv(int sockfd, void* recvbuf, int nbytes, int flags);
  • 함수 설명
    • sockfd는 소켓 기술자
    • sendbuf는 전송된 데이터가 저장된 버퍼의 포인터
    • recvbuf는 수신된 데이터가 저장된 버퍼의 포인터
    • nbytes는 송신되거나 수신된 데이터의 크기
  • 성공하면 송신되거나 수신된 실제 바이트 수를 반환 오류 -1 반환

sendto(), recvfrom()

  • sendto()는 프로세스가 UDP의 서비스를 사용하는 원격 프로세스에게 데이터를 보내기 위해 사용
  • recvfrom()는 UDP의 서비스를 사용하는 원격 프로세스로부터 데이터를 받기 위해 사용
  • UDP가 비연결형 프로토콜이기 때문에, 속성 중 하나는 원격 소켓 주소를 정의
int sendto(int sockfd, const void* buffer, int nbytes, int flags struct sockaddr* destinationAddress, socklen_t addrLen);

int recvfrom(int sockfd, void* buffer, int nbytes, int flags struct sockaddr* ourceAddress, socklen_t* addrLen);
  • 함수 설명
    • sockfd는 소켓 기술자
    • buffer 전송되거나 수신된 데이터가저장된 버퍼의 포인터
    • buflen은 버퍼의 크기
    • flag의 값은 0이 아닐 수 있지만 간단하게 하기위해 0으로 설정
  • 성공하면 송신되거나 수신된 바이트 수를 반환 오류시 -1 반환

close()

  • 프로세스가 소켓을 종료하기 위해 사용
int close(int sockfd);
  • sockfd는 이 함수를 호출한 후에 유효하지 않다.
  • 성공하면 0 오류면 -1 반환

바이트 순서화 함수

  • 컴퓨터에서 정보는 호스트 바이트 순서로 저장
  • 최하위 바이트(little- end)가 저장되는 little-endian이나 시작 주소에 최상위 바이트(big-end)가 저장되는 big-endian이 될 수 있다.
  • 개발자는 IP주소나 포트 번호 정보가 컴퓨터에 저장된 순서를 알 수 없어서 이들을 네트워크 바이트 순서로 변경해야 한다.
  • 이러한 목적을 위해 두 개의 함수가 정의되어 있다.
    • htons(호스트를 네트워크 short로)는 16비트 정수를 네트워크 바이트 순서로 바꿈
    • htonl(호스트를 네트워크 long)는 32비트 정수에 대해 같은 일을 함
    • 반대 동작인 ntohs, ntohl이 있다.
uint16_t htons (uint16_t shortValue);
uint32_t htonl (uint32_t longValue);
uint16_t ntohs (uint16_t shortValue);
uint32_t ntohl (uint32_t longValue);

메모리 관리 함수

void* memset(void* destination, int chr, size_t len);
void* memcpy(void* destination, const void* source, size_t nbytes);
int* memcmp(const void* ptrl, const void* ptr2, size_t nbytes);
  • memset(메모리 설정)
    지정된 메모리에 지정된 수만큼의 바이트(len의 값)를 설정하기 위해 사용
  • memcpy(메모리 복사)
    메모리의 일부(source)로 부터 또 다른 메모리(destination)의 일부로 지정된 수만큼의 바이트(nbytes의 값)를 복사하기 위해 사용
  • memcmp(메모리 비교)
    ptr1과 ptr2에 시작되는 두 개의 바이트 조합(nbytes)을 비교하기 위해 사용됨
    만일 두 조합이 동일하면 결과는 0이고, 두번째보다 작으면 0보다 작고, 크면 0보다 크다.

주소 변환 함수

  • IP주소는 10진수 형식으로 32비트 주소릴 나타냄
  • 하지만 IP 주소를 소켓에 저장하고자 할 때 숫자로 변환할 필요가 있다.
  • IP주소를 숫자로 변환하고 또 그 역을 수행하기위해 inet_pton(표현에서 숫자로), inet_ntop(숫자에서 표현으로)가 사용됨
  • 이 목적을 위해 family는 항상 AF_INET이다
int inet_pton (int family, const char* stringAddr, void* numericAddr);

char* inet_ntop(int family, const void* numericAddr, char* stringAddr, int len);

헤더 파일들

  • 앞의 함수를 하기위해서는 헤더 파일 필요 headerFiles.h라는 이름을 붙인 별개의 파일로 정의.
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <errno.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
#include <arpa/innet.h>
#include <sys/wait.h>

UDP를 이용한 통신

서버 프로세스

  1. 서버 프로세스가 먼저 시작
  2. 서버는 socket함수를 호출하여 소켓을 생성
  3. bind() 호출 잘 알려진 포트와 서버 프로세스가 동작하는 컴퓨터의 IP주소를 소켓에 결합
  4. recvfrom() 호출 데이터그램이 도착할 때까지 차단(bloc)
  5. 데이터그램이 도착하면, recvfrom() 차단을 풀고(unblock)
  6. 수신된 데이터그램으로부터 클라이언트 소켓 주소와 주소 길이를 추출하여 프로세스에게 반환
  7. 프로세스는 이 두 정보를 저장하고 요청을 처리할 수 있는 절차(함수)를 호출
  8. 결과가 준비되면, 서버 프로세스는 sendto() 호출
  9. 저장된 정보를 사용하여 결과를 요청한 클라이언트에게 결과 전송
  10. 서버는 무한 루프를 사용하여 동일 클라이언트나 다른 클라이언트들로부터 오는 요청에 대해 응답

클라이언트 프로세스

  1. socket()를 호출하여 소켓을 생성
  2. sendto()를 호출하여 서버의 소켓 주소와 UDP가 데이터그램을 만들기 위해 데이터를 얻을 수 있는 버퍼의 위치 전달
  3. 클라이언트는 recvfrom()를 호출하여 응답이 서버로부터 도착할 때까지 차단
  4. 응답 도착하면 UDP는 클라이언트 프로세스에게 데이터를 전달하여 recvfrom()가 차단을 풀고 수신된 데이터를 클라이언트에게 전달할 수 있게 한다.

echo 프로그램(UDP활용)

  1. 클라이언트는 한 줄의 텍스트를 서버로 보낸다.
  2. 서버는 같은 텍스트를 클라이언트로 다시 보낸다.

TCP(client)

//TCP echo client program
#include "headerFiles.h"

int main()
{
	//Declaration and definition
	int sd;			//Socket descriptor
	int n;			//Number of bytes received
	int bytesToRecv;	//Number of bytes to recive
	char sendBuffer[256];	//send buffer
	char recvBuffer[256];	//Recive buffer
	char* movePtr;		//a pointer the received buffer
	struct sockaddr_in serverAddr;	//Server address
	//Create socket
	sd = socket(PF_INET,SOCK_STREAM,0);

	//Create server socket address
	memset(&serverAddr, 0, sizeof(serverAddr));
	serverAddr.sin_family = AF_INET;
	serverAddr.sin_port = htons(80);
	inet_pton(AF_INET, "192.168.200.144", &serverAddr.sin_addr);

	//Connect
	connect(sd,(struct sockaddr*)&serverAddr, sizeof(serverAddr));

	//Send and receive data

	while (1) {
		/* 메시지 입력 전송*/
		fputs("전송할 메시지를 입력하세요(q to quit) : ", stdout);
		fgets(sendBuffer, 256, stdin);
		if (!strcmp(sendBuffer, "q\n"))
			break;
		write(sd, sendBuffer, strlen(sendBuffer));
		/* 메시지 수신 출력 */
		n = read(sd, sendBuffer, 255);
		sendBuffer[n] = 0;
		printf("서버로부터 전송된 메시지 : %s \n", sendBuffer);
	}
	close(sd);
	exit(0);
}

Tcp(server)

//Echo server program
#include "headerFiles.h"
int main(void)
{
	//Declartion and definition
	int listensd;
	int connectsd;
	int n;
	int bytesToRecv;
	int processID;
	char buffer[256];
	char* movePtr;
	struct sockaddr_in serverAddr;
	struct sockaddr_in clientAddr;
	int clAddrLen;
	
	//Create listen socket
	listensd = socket(PF_INET, SOCK_STREAM,0);
	
	//bind listen socket to the local address and port
	//서버의 주소를 담을 구조체 변수를 0으로 초기화
	memset(&serverAddr, 0, sizeof(serverAddr));

	serverAddr.sin_family = AF_INET;

	//htonl 호스트 바이트 순서의 데이터를 long int형 네트워크 바이트 순서로 바꿔주는 함수
	//INADDR_ANY는 서버가 사용하는 IP주소를 알아서 채워준다.
	//만약 안쓰면 주소정보를 찾아서 넣어줘야하고 IP가 바뀌면 코드 자체를 수정해야한다.
	//INADDR_ANY)
	serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
	serverAddr.sin_port = htons(80);

	//지정된 소켓에 대해 지역 주소 및 포트번호를 채움
	bind(listensd,(struct sockaddr*)&serverAddr, sizeof(serverAddr));

	/*Listen to connection request
	클라이언트로부터 들어오는 연결들을 허용
	클라이언트 접속 기다린다. listen()함수가 동작하지 않았는데
	해당 소켓으로 클라가 접속을 해오면 접속해온 클라의 connect()
	함수는 실패 
	즉, 해당 소켓을 이용해 접속을 받을 준비가 되어있다 의미
	*/
	listen(listensd, 5);

	//Handle the connection
	for(;;)
	{

		/* 클라이언트는 해당 소켓을 이용해 메시지를 주고 받는다
		 * 서버는 accept()함수를 호출 이 함수는 듣고 있는
		 * 소켓의 포트번호로 들어오는 연결요구 있을 때까지 블록
		 * 즉,서버 소켓에 결합된 주소와 포트로 들어오는 
		 * 연결들을 기다리면서 블록
		 * 서버 소켓에 listen()도 호출되어야 한다.
		 * 클라이언트 중 연결이 도착하여 TCP핸드세이크 성공하면
		 * 새로운 소켓이 반환되고 클라이언트 소켓 식별자에 반환
		 * 값을 넣어 사용
		 */
		connectsd = accept(listensd,(struct sockaddr*)&clientAddr, &clAddrLen);
		
		/*클라 접속때마다 fork통해 child process 생성해 echo발생
		 * 한클라 연결설정후 해당 클라 종료할때까지 for문 내에 묶여있음, 즉 또다른 클라 동시 통신 불가
		 * fork() 호출한 프로세스와 똑같은 자원이 메모리에 그대로 복사	
		 */
		processID = fork();
		//자식서버일 때
		if(processID == 0)
		{
			//리스닝 소켓 닫아준다.
			close(listensd);
			bytesToRecv = 256;
			movePtr = buffer;

			/*recv()는 전달받은 데이터의 바이트수를 반환
			 * 받은 메시지 크기를 받을 n에 크기를 전달
			 * movePtr라는 버퍼에 데이터 넣는다.
			 */
			while((n=recv(connectsd, movePtr, bytesToRecv,0))>0)
			{
				//상대가 전달할 데이터가 더 있는지
				send(connectsd, movePtr, n,0);
			}
			exit(0);
		}
		close(connectsd);
	}
}

Udp(client)

//UDP echo client program
#include "headerFiles.h"

int main(void)
{
	int sd;		//socket descriptor
	int ns;		//number of bytes send
	int nr; 	//number of bytes received
	char buffer[256];	//data buffer
	struct sockaddr_in serverAddr;	//socket address 
	int addrlen = sizeof(serverAddr);

	sd=socket(PF_INET, SOCK_DGRAM,0);

	memset(&serverAddr, 0 ,sizeof(serverAddr));
	serverAddr.sin_family =AF_INET;
	serverAddr.sin_port = htons(80);
	inet_pton(AF_INET, "192.168.200.144",&serverAddr.sin_addr);

	fgets(buffer,256,stdin);
	ns = sendto (sd,buffer,strlen(buffer),0,
			(struct sockaddr*)&serverAddr, sizeof(serverAddr));
	nr = recvfrom(sd, buffer, strlen(buffer),0,(struct sockaddr*)&serverAddr, &addrlen);
	buffer[nr] = 0;
	printf("Received from server: %s",buffer);

	close(sd);
	exit(0);
}

Udp(server)

//UDP echo server program
#include "headerFiles.h"

int main(void)
{
	int sd;		//Socket descriptor
	int nr;		//Number of bytes receiced
	char buffer[256];	//data buffer
	struct sockaddr_in serverAddr;
	struct sockaddr_in clientAddr;
	int clAddrLen;

	sd = socket(PF_INET, SOCK_DGRAM, 0);

	memset(&serverAddr, 0 ,sizeof(serverAddr));
	serverAddr.sin_family = AF_INET;
	serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
	serverAddr.sin_port = htons(80);

	bind(sd,(struct sockaddr*)&serverAddr, sizeof(serverAddr));

	for(;;)
	{
		nr = recvfrom(sd,buffer,256,0,(struct sockaddr*)&clientAddr, &clAddrLen);
		sendto(sd,buffer,nr,0, (struct sockaddr*)&clientAddr, sizeof(clientAddr));
	}
}
profile
안녕하세요

0개의 댓글