sw사관학교정글

[week07] 네트워크 프로그래밍

D cron 2021. 12. 27. 00:21

클라이언트-서버 프로그래밍 모델

모든 네트워크 응용 프로그램은 클라이언트-서버 모델에 기초하고 있다. 이 모델을 사용해서 응용 프로그램은 한 개의 서버(server) 프로세스와 한 개 이상의 클라이언트(client) 프로세스로 구성된다. 서버는 일부 리소스를 관리하고, 이 리소스를 조작해서 클라이언트를 위한 일부 서비스를 제공한다.

클라이언트-서버 모델에서 근본적인 연산은 트랜잭션(transaction)이다.


클라이언트- 서버 모델 트랜잭션

  1. 클라이언트가 서버에 요청(request)
  2. 서버가 요청(request)을 받고, 해석하고, 자신의 자원을 적절히 조작
  3. 서버는 응답(response)을 클라이언트로 보내고 다음 요청(request)을 기다림
  4. 클라이언트는 응답을 받고 이것을 처리함

글로벌 IP 인터넷

다음은 인터넷 클라이언트-서버 응용 프로그램의 기본적인 하드웨어 및 소프트웨어 구조다.

각 인터넷 호스트(PC)는 TCP/IP 프로토콜을 구현한 소프트웨어를 실행한다. 인터넷 클라이언트와 서버는 소켓 인터페이스와 Unix I/O 함수들의 혼합을 사용해서 통신한다. 소켓 함수들은 일반적으로 시스템 콜(system call)들로 구현되는데, 이 시스템 콜은 커널에서 트랩(trap)을 발생시키며, TCP/IP에서 다양한 커널 모드 함수들을 호출한다.

TCP 와 IP의 차이점

IP는 기본 명명법(name scheme)과 데이터그램(datagrams)이라고 하는 패킷(데이터 한 묶음)을 한 인터넷 호스트에서 다른 호스트로 보낼 수 있는 배달 매커니즘을 제공한다.
IP 매커니즘은 만일 데이터그램을 잃어버리거나 네트워크 내에서 중복되어도 복구하지 않는다. 따라서 안정적이지 못하다.
TCP는 IP위에 구현한 복잡한 프로토콜로 프로세스들 간에 안전한 완전 양방향 연결(reliable full duplex connections)을 제공한다.

IP 주소

IP 주소는 unsigned 32bit 정수이다. 네트워크 프로그램은 IP주소를 IP 주소 구조체에 저장한다.
IP주소는 대개 사람들에게 dotted-decimal 표기라고 하는 형식으로 제시된다.
예를 들어 128.2.194.242는 주소 0x8002c2f2의 dotted-decimal 표현이다. ( 80 / 02 / c2 / f2 이렇게 구분해서 점을 찍음 - 0x로 시작하면 16진수임을 주목)

인터넷 도메인 이름(Internet Domain name)

인터넷 클라이언트와 서버는 서로 통신할 때 IP 주소를 사용한다. 하지만 우리에게는 www.google.com 같은 주소가 더 친숙하다. IP 주소는 사람들이 기억하기 어렵기 때문에 별도의 도메인 이름 집합을 정의하는 것이다.
일반적인 경우, 다수의 도메인 이름들은 다수의 IP 주소로 매핑될 수 있다.

인터넷 연결(Internet Connections)

인터넷 클라이언트와 서버는 연결(connection)을 통해서 바이트 스트림(연속된 바이트)을 주고받는 방식으로 통신한다. 이 연결은 두 개의 프로세스를 연결한다는 점에서 point-to-point 연결이다. 데이터가 동시에 양방향으로 흐를 수 있다는 의미에서 완전양방향(full-duplex)이다.


소켓(socket)은 연결(connection)의 종단점이다. 각 소켓은 IP 주소, 정수 포트(port)로 이뤄진 소켓 주소를 가지며 이것은 address : port로 나타낸다.

클라이언트의 소켓 주소 내의 포트는 클라이언트가 연결 요청을 할 때 커널이 자동으로 할당하며 이것은 단기 포트라고 한다.


반면에 서버의 소켓 주소에 있는 포트는 대개 잘 알려진(well-known) 포트(port)이며 이것은 서비스 이름과 영구적으로 연결되어 있다. (ex: 웹 서버는 대개 포트 80을 사용함)


연결(connection)은 두 개의 종단점의 소켓 주소에 의해 유일하게 식별된다. 이 두개의 소켓 주소는 소켓 쌍이라고 알려져 있으며 tuple로 나타낸다.
(cliaddr : cliport , servaddr : servport)

소켓 인터페이스

다음의 그림은 굉장히 중요한 그림으로 머릿속에 넣어보려고 노력하자.

[소켓 인터페이스 기반 네트워크 응용프로그램의 개요]

Start Client

소켓(socket) 함수

클라이언트와 서버는 소켓 식별자(socket descriptor)를 생성하기 위해서 socket 함수를 사용한다.

식별자??

파일 식별자(file descriptor)라는 개념을 이해하기 위해서는 배경지식이 필요하다. 리눅스에서 파일은 연속된 m개의 byte이다. 네트워크, 디스크, 터미널 같은 모든 I/O 디바이스들은 파일로 모델링되며, 모든 입력과 출력은 해당 파일을 읽거나 쓰는 형식으로 수행된다.


파일을 여는 상황을 가정해보자.
응용프로그램은 I/O 디바이스에 접근하려고 할 때 해당 파일을 열겠다고 커널에 요청한다.

커널은 식별자(descriptor)라고 하는 작은 비음수를 리턴하며, 이 식별자란 놈은 이후의 파일에 관한 모든 연산에서 해당 파일을 나타낸다.


#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
// returns descriptor if OK, -1 on error
// 실제 사용
clientfd = socket(AF_INET, SOCK_STREAM, 0);

AF_INET은 우리가 32bit IP 주소를 사용하고 있다는 것을 나타내고, SOCK_STREAM은 TCP type을 나타낸다.

우리는 소켓을 생성했고, clientfd라는 변수에 우리의 소켓 식별자를 담아 놓았다.
그러나, socket에 의해 리턴된 clientfd 식별자는 부분적으로 열린 것이며, 아직은 읽거나 쓸 수 없다.

connect 함수

클라이언트는 connect함수를 호출해서 서버와의 연결을 수립한다. (connect가 끝나면 드디어 사용 가능!)


#include <sys/socket.h>
int connect(int clientfd, const struct sockaddr *addr, socklen_t addrlen);

여기서 clientfd는 우리가 socket함수로 열어주고 받아온 file descriptor이다(클라이언트 측 소켓).
connect 함수는 clientfd를 가지고 소켓 주소가 addr인 서버(서버측 소켓)와 인터넷 연결을 시도한다.
만약 성공하면 clientfd 식별자는 이제 읽거나 쓸 준비가 되었으며, 이 연결은 다음과 같은 소켓 쌍으로 규정된다.
(client IP : client port , server IP : server port)

클라이언트 소켓은 이렇게 간단하게 끝나게 된다. 이제 서버 소켓을 열어보자.

Start Server

bind 함수

일단 우리는 socket 함수로 socket을 생성했다고 치자.


#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
//returns 0 if OK, -1 on error

bind함수는 커널에게 addr에 있는 서버의 소켓 주소와 소켓 식별자 sockfd의 연결을 요청한다.

listen 함수

클라이언트는 연결 요청을 개시하는 능동적 개체이다. 서버는 클라이언트로부터의 연결 요청을 기다리는 수동적 개체이다. 기본적으로 socket 함수가 만든 식별자는 능동 소켓에 대응된다. 그러니까 이 socket을 서버용으로 바꿔주어야 한다. 이 역할을 수행하는 것이 listen 함수이다.


#include <sys/socket.h>
int listen(int sockfd, int backlog);
returns 0 if OK, -1 on error

listen함수는 sockfd를 능동 소켓에서 듣기 소켓으로 변환하며, 듣기 소켓은 클라이언트로부터의 연결 요청을 승락할 수 있다.

accept 함수

서버는 accept 함수를 호출해서 클라이언트로부터의 연결 요청을 기다린다.


#include <sys/socket.h>
int accept(int listenfd, struct sockaddr * addr, int *addrlen);
//returns connected descriptor if OK, -1 on error

accept 함수는 클라이언트로부터의 연결 요청이 듣기 식별자 listenfd에 도달하기를 기다리고 연결요청이 도착하면 addr 내의 클라이언트의 소켓 주소를 채우고, 연결 식별자(connected descriptor)를 return한다.

듣기 식별자(listenfd) vs 연결 식별자(connfd)

듣기 식별자(listenfd)는 대부분 한 번 생성되며, 서버가 살아있는 동안 계속 존재한다.
연결 식별자는 클라이언트와 서버 사이에 성립된 연결의 끝점이다. 이것(connfd)은 서버가 연결 요청을 수락할 때마다 생성되며, 서버가 클라이언트에 서비스하는 동안에만 존재한다.

호스트와 서비스 변환

getaddrinfo 함수
getaddrinfo함수는 www.google.com을 127.x.x.x, 128.xx.xxx.x 등으로 변환시키는데, 소켓 주소 구조체 형태로 변환시킨다.

getnameinfo함수
getaddrinfo의 역이다. 이것은 소켓 주소 구조체를 대응되는 호스트와 서비스이름 스트링으로 변환한다(127.x.x.x -> www.google.com)

소켓 인터페이스를 위한 도움함수들

지금까지 배운 함수들은 처음 들었을 때 다소 어려울 수 있다. 이들을 묶어서 더 큰 단위인 open_clientfd함수와 open_listenfd함수로 감싸서 사용하면 편리하다.

open_clientfd 함수

클라이언트는 open_clientfd를 호출해서 서버와 연결을 설정한다.


#include "csapp.h"
int open_clientfd(char *hostname, char *port);
//returns descriptor if OK, -1 on error

여기서 hostname과 port는 모두 서버의 것임을 주의하자. 왜냐하면 어디에 요청을 할지 알아야 하기 때문이다.


우선 getaddrinfo를 호출하고, 이것은 addrinfo 구조체의 리스트를 리턴한다. 이 리스트를 방문하여 각 리스트 항목을 socket과 connect로의 호출이 성공할 때까지 시도한다.

connect가 실패하면, 소켓 식별자를 닫는다.
connect가 성공하면, 우리는 리스트 메모리를 반환하고, 소켓 식별자를 클라이언트에 리턴한다. 그리고 즉시 UNIX I/O가 서버와 통신을 시작할 수 있도록 한다.


open_listenfd 함수

서버는 open_listenfd함수를 호출해서 연결요청을 받을 준비가 된 듣기 식별자를 생성한다.


#include "csapp.h"
int open_listenfd(char *port)
//returns descriptor if OK, -1 on error

open_listenfd함수는 port에 연결 요청을 받을 준비가 된 듣기 식별자를 return 한다.

getaddrinfo를 호출해서 결과 리스트를 socket과 bind로의 호출이 성공할 때까지 탐색한다.
우리는 커널에게 이 서버가 모든 client 주소에 대해 요청을 받을 것이라고 말한다