고등학교 기숙사 때 아침마다 체리필터의 'Happy Day'가 기상 음악으로 나왔다.
오늘 아침 눈이 잘 안 떠져서 이 노래 듣고 깸
07:55 입실
오늘은 CSAPP11장 보고 에코서버 구현까지가 목표!
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
// foobar.txt는 아스키 문자 "foobar"가 저장됨
int main()
{
int fd;
char c;
fd = open("foobar.txt", O_RDONLY, 0); // "foobar.txt" 파일을 읽기 전용으로 열고 파일 서술자(fd)에 할당
if (fork() == 0) // 자식 프로세스를 생성
{
read(fd, &c, 1); // 파일에서 1바이트를 읽어서 c에 저장
exit(0); // 자식 프로세스를 종료
}
wait(NULL); // 부모 프로세스는 자식 프로세스의 종료를 기다림
read(fd, &c, 1); // 부모 프로세스가 파일에서 1바이트를 읽어서 c에 저장 (자식이 읽은 다음으로 이동)
printf("c = %c\n", c); // c에 저장된 값인 'o'를 출력
exit(0);
}
exit(0);은 현재 실행 중인 프로세스를 종료시킨다.
즉, 자식 프로세스의 실행 맥락에서 이 코드가 실행되면 자식 프로세스가 종료.
현재 실행 중인 프로세스는 그 시점에서 코드를 실행하는 프로세스를 가리킨다.
이런 해당 코드의 실행 컨텐스트에 따라 달라진다.
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
// foobar.txt는 아스키 문자 "foobar"가 저장됨
int main()
{
int fd1, fd2;
char c;
fd1 = open("foobar.txt", O_RDONLY, 0); // "foobar.txt" 파일을 읽기 전용으로 열고 파일 서술자(fd1)에 할당
fd2 = open("foobar.txt", O_RDONLY, 0); // "foobar.txt" 파일을 읽기 전용으로 열고 파일 서술자(fd2)에 할당
read(fd2, &c, 1); // 파일 서술자 fd2에서 1바이트를 읽고 c에 저장 (이때 f를 읽어 c에 저장)
dup2(fd2, fd1); // fd2를 fd1로 복제 (즉, fd1은 이제 fd2와 동일한 파일을 가리킴)
read(fd1, &c, 1); // fd1을 통해 읽은 결과는 실제로 fd2에서 읽는 것과 동일 (o를 읽어 c에 저장)
printf("c = %c\n", c); // c에 저장된 값인 'o'를 출력
exit(0);
}
한 개의 서버 프로세스와 한 개 이상의 클라이언트 프로세스로 구성
클라이언트-서버 모델의 핵심은 트랜잭션(DB의 트랙잭션과 무관함)
허브: 다수의 클라이언트 연결
브릿지: 다수의 이더넷을 연결
라우터: 네크워트 간의 연결 구성(일종의 컴퓨터), 각 네트워크에 대한 어댑터(포트) 존재
IP 주소도 결국 비트로 표현된다.
128.2.194.224는 0x8002c2f2의 dotted-decimal 표현이다.
이걸 2진법으로 바꾸면 10000000 00000010 11000010 11100000
Dotted-decimal address | Hex address |
---|---|
107.212.122.205 | 0x 6B D4 7A CD |
64.12.149.13 | 0x 40 0C 95 0D |
107.212.96.29 | 6B D4 60 1D |
0.0.0.128 | 0x 00 00 00 80 |
256.256.256.0 | 0x FF FF FF 00 |
10.1.1.64 | 0x 0A 01 01 40 |
클라이언트와 서버가 통신할 때는 IP주소를 쓴다.
하지만 큰 정수는 기억하기 어려워 IP주소를 도메인 집합으로 매핑한다.
도메인 주소는 누가 관리하나?
(Internet Corporation for Assigned Names and Numbers) 비영리단체
도메인 네임 시스템, 도메인 네임 서버
IP와 도메인을 매핑하는 엔트리를 가지고 있는 서버이다.
1988년까지는 이런 매핑이 겨우 HOSTS.TXT 파일 한개로 관리되었다고 한다.
하지만 지금은 전 세계 분산 데이터베이스에 의해 관리된다!
DNS는 계층적인 서버 구조로 이루어져 있는데,
전세계에는 13개의 루트 DNS가 있다.
(총 12개의 기관이 루트 DNS를 관리한다.)
이론적으로 이 루트 DNS가 모두 먹통이 되면 전세계 인터넷은 도메인 주소로 접속이 불가능해진다.
루트 DNS
https://root-servers.org/
클라이언트와 서버는 바이트 스트림을 주고받으며 통신한다.
소켓은 연결의 종단점!!
각 소켓은 인터넷 주소와 16비트 정수 포트로 이루어진 소켓 주소를 가진다.
address:port
형태로 표현함.
포트는 클라이언트가 연결 요청할 때 커널이 자동으로 할당, 단기 포트(ephemeral 포트)라고 함.
서버의 포트는 대개 영구적임.
각 연결은 소켓 주소 쌍으로 식별될 수 있다.
(cliaddr:clioport, servaddr:servport)
예를 들어 클라이언트와 서버의 연결 쌍을 다음과 같이 표현할 수 있다.
인터페이스란 뭔가를 할 수 있는 도구 모음집이라고 생각하면 편하다.
소켓 인터페이스는 소켓 통신을 할 수 있는 뭔가의 도구 모음집 같은 느낌
unix 관점에서 소켓을 해당 식별자를 가진 열린 파일
클라이언트와 서버가 소켓 식별자를 생성하기 위한 함수
소켓 식별자(소켓 디스크럽터)를 반환한다.
int socket(int domain, int type, int protocol);`
domain(프로토콜 도메인) : AF_INET(IPv4), AF_INET6(IPv6) 등이 들어감.
type(소켓 타입) : SOCK_STREAM(연결 지향), SOCK_DGRAM(비연결 지향)
protocol(프토코톨): 0으로 설정하면 domain과 type에 따라 자동 선택
클라이언트가 서버와의 연결을 시도하기 위해 호출하는 함수
이 함수를 호출하면 서버에 연결 요청을 보낸다.
서버가 이를 수락하면 연결이 설정되고 0을 반환한다.
연결에 실패하면 -1를 반환, errno 변수에 오류 코드 설정
int connect(int clientfd, const struct sockaddr *addr, socklen_t addrlen);
clientfd(소켓 식별자)
addr : 연결하려는 서버 주소 정보를 담고 있는 struct sockaddr 구조체 포인터
addrlen: addr 구조체 크기, sizeof(struct sockaddr)
로 설정하면 됨
서버가 클라이언트와 연결하기 위한 함수
서버는 커널에 addr에 있는 서버의 소켓 주소를 소켓 식별자 sockfd와 연결하라고 함.
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd : 서버 소켓의 식별자
addr : 클라이언트가 접속할 수 있는 서버측 로컬 주소 구조체 포인터
addrlen: addr 구조체의 크기, sizeof(struct sockaddr)
로 설정하면 됨
sockfd를 능동 소켓에서 듣기 소켓으로 변환
클라이언트로부터 요청을 받을 수 있는 대기 상태로 돌입
int listen(int sockfd, int backlog);
sockfd : 서버측 소켓 식별자
backlog: 연결 대기 큐의 최대 길이. 서버가 동시에 대기할 수 있는 클라이언트 연경 요청 최대 수
backlog에 지정한 값보다 많은 요청이 들어오면 요청이 거부될 수 있음.
클라이언트 연결 요청을 대기
듣기 식별자를 기다리고 이게 도착하면 연결 식별자(연결된 소켓 식별자)를 리턴한다.
int accept(int listenfd, struct sockaddr *addr, int *addrlen);
listenfd : 클라이언트 연결 요청을 받을 서버 소켓의 식별자, 듣기 소켓으로 변환된 소켓이어야 함.
addr : 클라이언트 주소 정보를 담은 구조체 포인터
addrlen: addr 구조체 크기 포인터(이게 있어야 주소 정보를 제대로 가져올 수 있음)
즉 서버는 최초에 클라이언트의 연결 요청을 듣기 위한 소켓을 생성하고,
accept()로 클라이언트의 요청이 들어오면 이제 진짜 둘 사이의 통신을 위한 새로운 소켓을 생성하고 반환한다.
듣기 식별자와 연결 식별자 차이는?
둘을 구분하면 오로지 요청을 받기 위한 용도로 듣기 식별자를 여러개 운용할 수 있다.
그리고 연결된 애들만 연결 식별자를 생성해서 병렬 처리가 가능해 진다.
듣기 식별자는 최초 연결 후 이제 연결을 연결 식별자로 넘겨 주시 때문에 새로운 요청을 받을 수 있어서 재사용이 가능하다.
연결 식별자는 연결이 끊어지면 소켓이 닫힌다.
통신 시 소켓 구조체와 주소를 상호 변환해야 하는데 이때 리눅스에서 getaddrinfo()
와 getnameinfo()
를 쓸 수 있음.
호스트 이름과 서비스 이름을 소켓 주소 구조체로 변환
IPv4 및 IPv6 주소를 자동으로 처리 가능
소켓 주소 구조체를 호스트 이름과 서비스 이름으로 변환
상위수준 도움 함수인
open_clientfd()
와open_listentfd()
로 감싸면 편리하다.
클라이언트 측에서 서버와 연결을 설정
열린 소켓 식별자를 리턴
int open_clientfd(char *hostname, char *port);
서버 측에서 듣기 식별자 리턴
int open_listenfd(char *port);
도움 함수는 에코 서버 구현하면서 다시 정리!
#include "csapp.h"
int main(int argc, char **argv)
{
int clientfd;
char *host, *port, buf[MAXLINE];
rio_t rio;
if (argc != 3)
{
fprintf(stderr, "usage: %s <host> <port>\n", argv[0]);
exit(0);
}
host = argv[1];
port = argv[2];
clientfd = Open_clientfd(host, port);
Rio_readinitb(&rio, clientfd);
while (Fgets(buf, MAXLINE, stdin) != NULL)
{
Rio_writen(clientfd, buf, strlen(buf));
Rio_readlineb(&rio, buf, MAXLINE);
Fputs(buf, stdout);
}
Close(clientfd);
exit(0);
}
int main(int argc, char **argv)
argc는 argument count. 명령줄 인수의 개수
argv는 argument vector. 명령줄 인수를 저장하는 문자열 배열
char **는 문자열 포인터 배열을 의미하며, 각각의 포인터는 하나의 명령줄 인수를 가리킨다.
argv[0]은 프로그램 이름(실행경로), argv[1]은 호스트, argv[2]는 포트
이건 에코서버만의 매개변수가 아니라 C에서 main 함수 시 기본적으로 넘어가는 파라미터다.
if (argc != 3)
만약 호스트 또는 포트를 입력하지 않으면 프로그램이 종료된다.
clientfd = Open_clientfd(host, port);
클라이언트 소켓을 열어서 리턴받은 소켓 식별자를 할당한다.
Rio_readinitb(&rio, clientfd);
rio 구조체를 초기화한다.
while (Fgets(buf, MAXLINE, stdin) != NULL)
반복적으로 사용자의 입력을 받아 버퍼에 저장한다.
Fgets 함수는 문자열을 한 줄씩 읽는 함수이다.
매개변수로 읽을 위치, 최대치, 읽어올 파일 포인터를 가리킨다.
Rio_writen(clientfd, buf, strlen(buf));
소켓 식별자를 통해 버퍼에 저장된 내용을 소켓 식별자에 쓰기 작업을 수행한다.
Rio_readlineb(&rio, buf, MAXLINE);
rio 소켓에서 데이터를 읽어 온다. 읽어온 데이터는 버퍼에 저장한다.
지정된 소켓으로부터 한 줄씩 데이터를 읽어 온다.
Fputs(buf, stdout);
버퍼에 저장된 문자열을 표준 출력 스트림(stdout)으로 출력한다.
#include "csapp.h"
void echo(int connfd);
int main(int argc, char **argv)
{
int listenfd, connfd;
socklen_t clientlen;
struct sockaddr_storage clientaddr;
char client_hostname[MAXLINE], client_port[MAXLINE];
if (argc != 2)
{
fprintf(stderr, "usage: %s <port>\n", argv[0]);
exit(0);
}
listenfd = Open_listenfd(argv[1]);
while (1)
{
clientlen = sizeof(struct sockaddr_storage);
connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
Getnameinfo((SA *)&clientaddr, clientlen, client_hostname, MAXLINE, client_port, MAXLINE, 0);
printf("Conneted to (%s, %s)\n", client_hostname, client_port);
echo(connfd);
Close(connfd);
}
exit(0);
}
void echo(int connfd)
{
size_t n;
char buf[MAXLINE];
rio_t rio;
Rio_readinitb(&rio, connfd);
while ((n = Rio_readlineb(&rio, buf, MAXLINE)) != 0)
{
printf("server received %d bytes\n", (int)n);
Rio_writen(connfd, buf, n);
}
}
int listenfd, connfd;
듣기 식별자, 연결 식별자
socklen_t clientlen;
클라이언트 소켓 주소 길이
struct sockaddr_storage clientaddr;
클라이언트 주소 정보 저장하는 구조체
char client_hostname[MAXLINE], client_port[MAXLINE];
클라이언트 호스트네임과 포트
listenfd = Open_listenfd(argv[1]);
듣기 소켓을 열어 듣기 식별자를 반환한다.
clientlen = sizeof(struct sockaddr_storage);
클라이언트 소켓 주소 구조체 길이를 반환한다.
connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
듣기 식별자를 통해 연결 요청이 들어오면 해당 클라이언트 주소와 길이를 바탕으로 새로운 연결 소켓을 생성하고 연결 식별자를 반환한다.
SA*는 struct sockaddr 형식의 포인터 식별자이다.
네트워크 프로그래밍에서 일반적으로 사용되는 구조체이다.
Getnameinfo((SA *)&clientaddr, clientlen, client_hostname, MAXLINE, client_port, MAXLINE, 0);
SA *로 캐스팅된 클라이언트 구조체에서 호스트 이름과 포트 번호를 얻는다.
clientlen, client_hostname, client_port는 변수 포인터이다.
이 함수를 실행하면 클라이언트 주소 구조체에서(IP에서) 호스트 이름과 포트 번호를 client_hostname과 client_port 변수에 각각 문자열로 저장한다.
Rio_readinitb(&rio, connfd);
듣기 식별자를 연결시켜서 구조체를 초기화한다.
n = Rio_readlineb(&rio, buf, MAXLINE)
버퍼에 들어온 문자열의 길이를 반환한다.
Rio_writen(connfd, buf, n);
연결 식별자를 통해 클라이언트에 버퍼의 내용을 전송한다.
웹 클라이언트와 서버는 HTTP로 상호 연동한다.
HTTP는 다음과 같이 간단한 프로토콜이다.
1. 클라이언트가 컨텐츠를 요청한다.
2. 서버는 응답하고 연결을 닫는다.
3. 클라이언트는 컨텐츠를 읽고 화면에 그린다.
클라이언트와 서버가 주고 받은 컨텐츠가 MIME 타입을 갖는 바이트 배열이다.
컨텐츠는 정적 컨텐츠, 동적 컨텐츠가 있다.
정적 컨텐츠는 디스크에서 파일을 가져온다.
정적 컨텐츠는 실행파일을 돌려서 런타임에 만든 파일이다.
request와 reponse로 이루어진다.
이 내용은 키워드 학습에서 살펴본 내용!
쿼리 스트링(?으로 시작하고 &로 구분)으로 uri에 인자를 전달할 수 있다.
훈코치님
훈코치님께서 스터디 참관하시고 조언해주심!
레이어링을 무시한 케이스도 있다.
실제 일부 기업에서는 표준 레이어가 아닌 레이어 내에서 커스텀해서 쓰기도 한다.
(하위 계층에서 처리할 일을 상위 계층에서 소프트웨어적으로 처리하는 등)
그렇다고 무조건 나쁜 건 아니다.
테슬라에서 라이다에서 처리할 걸 상위 레이더인 카메라로 처리하는 것처럼 긍정적인 측면도 있다.
라우팅에서 무조건 이상적인 정책대로만 되는 건 아니다.
여러 현실적인 사항들이 많다.
스위치 장비는 L2, L4, L7만 있다.
응용프로그램 수준의 동시성을 사용하는 응용프로그램들을 동시서 프로그램이라고 함.
현대 운영체제는 동시성 프로그램을 위한 기본 접근방법이 있다.
프로세스로 동시성을 구현하거나,
I/O 다중화로 동시성을 구현하거나,
쓰레드로 동시성을 구현할 수 있음.
가장 간단한 방법
fork
, exec
, waitpid
등을 사용
프로세스는 부모와 자식 사이에 상태 정보를 공유하는 깔끔한 모델
프로세스들이 분리된 주소 공간을 갖는다.
원칙적으로 프로세스 간 상태정보 공유는 불가능하나,
IPC(interprocess communications) 메커니즘 공유 가능
비동기 프로그래밍의 일종
예를 들어 자바스크립트의 async, await
I/O 다중화는 프로그래밍을 통해 구현할 수 있으나, 코드가 복잡해지는 단점이 있다.
프로세스 문맥 전환이 이루어지지 않기 때문에 오버헤드가 적다.
프로세스, I/O 다중화의 하이브리드
메인 쓰레드가 피어 쓰레드를 생성한다.
쓰레드 문맥 교환은 프로세스 문맥 교환보다 더 빠르다.
쓰레드는 프로세스처럼 부모-자식의 경직된 관계가 아니라 피어들도 풀을 구성한다.
메인쓰레드만 항상 프로세스에서 돌아가는 첫 번째 프로세스라는 의미
피어 쓰레드들은 모두를 죽이거나, 죽기를 기다릴 수 있다.
쓰레들끼리는 가상메모리를 모두 공유하지만, 레지스터를 공유하지는 않는다.
C에서 쓰레드를 조작하는 표준 인터페이스
mutual exclusion(상호 배제)
동시에 여러 쓰레드나 프로세스가 공유 자원에 접근하는 것을 제어하기 위한 동기화 기법
공유 자원에 한 개의 스레드만 접근 가능
락을 가질 수 있을 때까지 휴식하는 것! 공유 자원이 한 개일 때
세마포어(Semaphore)는 동기화 기법 중 하나로, 공유 자원에 여러 쓰레드나 프로세스가 접근하는 것을 제어하기 위해 사용
뮤텍스와 다르게 공유 자원에 여러 스레드가 접근 가능
다익스트라가 만듦.
특별한 타입의 변수
세마포어 s는 비음수 정수 값을 갖는 전역 변수이다.
이 비음수 정수 값은 특별한 연산인 P, V를 통해서만 조작할 수 있다.
P(s)
V(s)
#include <semaphore.h>
int sem_init(sem_t *sem, 0, unsigned int value);
int sem_wait(sem_t *s); // P(s)
int sem_post(sem_t *s); // V(s)
공유 자원에 여러 스레드가 접근할 수 있을 때 몇 개까지 접근시킬지 제어하는 것
생산자: 데이터를 생성하고 공유 버퍼에 넣는 작업
소비자: 버퍼에서 데이터를 꺼내어 소비하는 작업
공유 버퍼: 생산자와 소비자 간의 데이터를 교환할 수 있는 버퍼 또는 큐
버퍼는 크기가 제한되어 있어어, 가득차면 생산자가 대기, 비어있으면 소비자가 대기
이 상황에서 생산자가 버퍼에 데이터를 넣는 작업과 소비자가 버퍼에서 데이터를 꺼내는 작업은 동시에 일어나는 게 불가능하며 올바른 순서로 수행되어야 한다.
여러 개의 스레드 또는 프로세스가 공유 데이터에 동시에 접근하는 경우 발생
뮤젝스, 세마포어 등을 통해 데이터 접근 조절, 읽기 및 쓰기 작업 간 우선순위 부여
대표적으로 데이터베이스에서 발생할 수 있음.
세마포어는 교착상태(deadlock)라는 런타임 에러를 유발한다.
다수의 쓰레드가 절대 참이 될 수 없는 조건을 기다리면서 정지되어 있는 경우
(address resolution protocol)
뮤텍스와 세마포어 코드로 구현해보기!
https://www.youtube.com/watch?v=gTkvX2Awj6g