크래프톤 정글 TIL : 0817

lazyArtisan·2024년 8월 17일
0

정글 TIL

목록 보기
48/147

🎯 To-do List


⌚ Time Tracking

  • 09:30 - 10:45 : 아침, 휴식, 라운지 이동
  • 10:45 - 11:00 : 알고리즘 스터디 준비 | 간단한 복습
  • 11:00 - 12:00 : 에코 서버 구현
  • 12:00 - 14:00 : 점심, 산책
  • 14:00 - 14:25 : 알고리즘 스터디
  • 14:25 - 15:05 : 키워드 코어타임
  • 15:05 - 15:35 : 라운지 이동
  • 15:35 - 17:45 : 에코 서버 구현
  • 17:45 - 19:00 : 저녁
  • 19:00 - 20:10 : 에코 서버 구현
  • 20:10 - 20:30 : 오큰수 복습
  • 20:30 - 20:40 : 산책
  • 20:40 - 23:45 : 에코 서버 구현

🔆 Today

  1. 알고리즘 스터디 준비 | 오큰수 복습 ✅
  2. 에코 서버 만들기 ✅

⏳ Routine

🍪 Plan

  • tiny 웹서버를 만들고 → 숙제문제 11.6c, 7, 9, 10, 11 중 세문제 이상 풀기 → 프록시 과제 도전
  • 퀴즈 봤던거 정리 (기술 면접 준비 +알파 느낌으로)
  • CS:APP 읽기 | 6장, 11.5 구현 전까지

✅☑️❌



📚 Journal


네트워크 공부량

네트워크를 일주일 안에 공부하고 구현까지 다 해야 하는데,
이걸 전부 다 습득한다는 건 말이 안되는 것 같다. 양과 범위가 너무 많음.
적당히 하다가 자르는 자세가 필요한듯.
딱 필요한 것까지만 이해하고 그 이상으로 개념을 타고타고 가면 안될듯.

GPT 질답 정리

내가 해놓은 정리의 가독성이 안 좋았던 이유가
gpt한테 물었던 걸 내 방식대로 해석해서 적은게 아니라
이해할 수 있을만큼만 다듬어서 적어서 그랬던 것 같음.
좀 더 내 머리를 거쳐서 정리해보자.



📝 배운 것들


🏷️ 3-way handshake

3-way handshake는 TCP 연결하는 방법임.

  1. 클라가 서버에 "나 접속하겠다" 요청함 (SYN 보냄)
  2. 서버는 요청 받으면 "그래 접속 요청 받았다, 나도 접속하겠다" 요청 보냄 (ACK, SYN 보냄)
  3. 클라는 서버가 보낸 접속 요청에 "그래 접속 요청 받았다" 대답함 (ACK 보냄)

🏷️ 4-way handshake

4-way handshake는 TCP 연결 끊는 방법임.

그림 출처 : https://www.geeksforgeeks.org/tcp-connection-termination
  1. 접속을 끊으려는 호스트(주로 클라)가 FIN 패킷 보냄.
    (FIN: "이제 너한테 데이터 더 안 보낼 거임")

  2. 상대방 호스트(주로 서버)가 FIN 받으면 ACK 패킷 보냄.
    (ACK: "알았음")

  3. 상대방 호스트가 남은 데이터를 마저 보낸 후 FIN을 보냄.
    (ACK를 받을 때까지 기다리고, ACK를 받으면 연결을 종료. ACK 오랫동안 안 오면 다시 보냄)

  4. 원래 호스트가 FIN을 받으면 ACK를 보냄.
    (클라이언트는 ACK를 보낸 후 2MSL 동안 기다림. 서버가 FIN을 다시 보내버리면 내가 보낸 ACK 안 갔구나 생각하고 다시 ACK 보냄)

🏷️ TIME_WAIT와 2MSL

처음 연결 끊자고 한 쪽(클라)이 마지막으로 ACK 보내고 기다리는 걸 TIME_WAIT라고 함.
이 상태는 2MSL만큼 유지됨. 일반적으로 240초(4분)임.
시스템 환경이나 구현한 방법에 따라 달라짐.

(서버가 FIN 보내고 기다리는 시간은 이거랑 별개의 이야기임. 2MSL보단 짧게 기다린다고 함.)

🏷️ 파일 디스크립터 (file descriptor)

파일 디스크립터는 파일이나 소켓 열 때 운영체제가 할당하는 번호임.
읽기, 쓰기 같은 작업들 할 때 파일 디스크립터 사용함.

어디에 저장돼있는 건 아니고, 열 때마다 다름.
심지어 같은 파일을 각기 다른 프로세스가 열어도 다른 식별자가 나옴.

프로그래밍 언어 따라 불러오는 방법 다름.
C는 open() 함수 쓰면 됨.

소켓 디스크립터도 파일 디스크립터의 일종임.
소켓 디스크립터는 fd라는 값임. 이걸로 통신함.
소켓은 socket(), accept() 함수로 소켓 디스크립터 받아올 수 있음.

🏷️ EOF (End of File)

EOF는 파일이나 데이터 스트림의 끝을 나타내는 신호임.
fgetc()나 fgets() 같은 C 함수는 파일의 끝에 도달하면 EOF를 반환함.
C에서 EOF는 보통 -1로 정의돼있음.

EOF가 발생하는 상황

  1. 파일 읽다가 데이터 끝에 도달하면 EOF 반환
  2. 콘솔에서 ctrl+d(Unix/Linux) 또는 ctlr+z(Window) 입력하면 EOF 처리
  3. 상대방이 연결 닫으면 EOF


🌐 Web Server


🔷 Echo server 구현

책에 있는 코드 따라치면서 이해하고 주석 달기

📌 Annotating : open_clientfd


#define _POSIX_C_SOURCE 200112L // POSIX 기능 활성화

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

// 클라이언트가 서버와 연결 설정하는 함수
int open_clientfd(char *hostname, char *port)
{
    int clientfd;
    struct addrinfo hints, *listp, *p;

    /* 가능한 서버 주소들의 리스트를 받는다 */
    memset(&hints, 0, sizeof(struct addrinfo)); // addrinfo 구조체인 hints에 있는 값들을 0으로 초기화
    // 이름이 왜 hints일까? : getaddrinfo 함수 매개변수 이름이 hints
    hints.ai_socktype = SOCK_STREAM; // TCP 소켓으로 설정
    hints.ai_flags = AI_NUMERICSERV; // 서비스 이름 탐색하지 말고 바로 포트 번호로 해석 
    hints.ai_flags |= AI_ADDRCONFIG; // 시스템의 IPv4, IPv6 사용 여부 가져옴
    getaddrinfo(hostname, port, &hints, &listp);

    /* 소켓 주소 구조체의 연결 리스트를 한 번 순회하면서 연결을 시도하고, 연결되면 순회 중지 */
    // getaddrinfo로 hostname과 port에 대응되는 소켓 주소 구조체의 리스트를 얻었을 거임
    // 단일 addrinfo 구조체가 아니라 리스트를 주는 이유는,
    // 호스트 이름이 여러 개의 IP 주소에 대응될 수 있기 때문임.
    for (p = listp; p; p = p->ai_next)
    {
        // 소켓 식별자 생성
        if ((clientfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) < 0)
            continue; // 소켓을 못 만들어냈음. 다음 거 시도.

        // 서버에 연결
        if (connect(clientfd, p->ai_addr, p->ai_addrlen) != -1)
            break; // 연결 했음. 순회 중지.

        close(clientfd); // 연결 실패했으므로 소켓을 닫음
        // (소켓 파일 디스크립터를 닫고, 시스템 리소스를 해제한다는 뜻)
    }

    /* 메모리 해제 */
    freeaddrinfo(listp);
    if (!p) // 모든 연결이 실패했다면 (포인터가 최종적으로 NULL을 가리킨다면)
        return -1;
    else // 연결이 성공했다면
        return clientfd;
}

📌 Annotating : open_listenfd


보호 구문 작성 요령

if ((clientfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) < 0)

저번부터 느낀거지만, 함수를 if에 넣어버리고 반환하는 값이 오류를 의미하면 바로 조치를 취하는 형식이 많은듯. 따로 하는 것보다 코드가 간결해져서 좋은 것 같다.

1. troubleshooting : NULL 빨간 줄

NULL은 헤더 파일 같은거 include 안 해도 그냥 되는거 아니었음?

네, 맞습니다. NULL은 일반적으로 C와 C++ 프로그램에서 널 포인터 상수를 나타내는 데 사용되며, 기본적으로 stddef.h 또는 stdio.h 등의 표준 헤더 파일에 정의되어 있습니다. 하지만, 대부분의 C와 C++ 컴파일러는 NULL을 인식할 수 있도록 이 헤더 파일들을 자동으로 포함하거나, 이미 정의된 상태로 제공하기 때문에 별도의 #include 지시문 없이도 사용할 수 있는 경우가 많습니다.

#include <stddef.h>
#include <stdio.h>

이거 둘 중 하나 넣어야 한다고 함.
그냥 컴파일해도 정상적으로 작동하겠지만
빨간 줄이 거슬려서 그냥 추가함.

2. Question : Setsockopt

/* bind() 함수 호출 시 발생 가능한 "주소가 이미 사용되고 있음" 오류 방지 */
// "Address already in use" 오류는 주로 서버 소켓이 특정 포트에 바인딩될 때, 
// 이전 연결이 제대로 종료되지 않아 해당 포트가 아직 사용 중인 경우 발생
Setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (const void *)&optval, sizeof(int));

이게 뭐하는 코드인지 모르겠어서 좀 더 찾아봄.

TCP 통신 끊을 때 4-way handshake라는 걸 함. (위에 정리함)
연결 끊었다고 해도 아직 완전히 안 끊긴 상태로 남아있을 수 있음.

자기가 끊으려고 했던 거면 TIME_WAIT,
연결 해제 요청 받은 거면 LAST_ACK 상태에 있을 수 있다는 것임.
이러면 해당 포트를 사용 못함. 오류 남.

그래서 SO_REUSEADDR를 설정해서
TIME_WAIT나 LAST_ACK여도 무시하고 포트 번호 써버리는 거임.

(라고는 이해했는데, 구글링 해보니 SO_REUSEADDR는
TIME_WAIT와 연계해서만 설명되고 있었다.
확실한 것은 포트 번호를 오류 없이 재사용 할 수 있게 해준다는 것.)

그나마 신뢰성 높아보이는 문서 : https://tech.kakao.com/posts/321

3. Question : LISTENQ

LISTENQ에 빨간 줄이 그임. 뭔지 찾아봄.

LISTENQ는 서버 소켓이 동시에 처리할 수 있는 연결 대기 요청의 최대 수를 지정하는 상수임.

#define LISTENQ 1024

이렇게 아무 값이나 정의해주면 됨.

✅ 최종 코드

#define LISTENQ 1024

int open_listenfd(char *port)
{
    struct addrinfo hints, *listp, *p;
    int listenfd, optval = 1;

    /* 가능한 서버 주소들의 리스트를 받는다 */
    memset(&hints, 0, sizeof(struct addrinfo));  // addrinfo 구조체인 hints에 있는 값들을 0으로 초기화
    hints.ai_socktype = SOCK_STREAM;             // TCP 소켓으로 설정
    hints.ai_flags = AI_PASSIVE | AI_ADDRCONFIG; // AI_PASSIVE 플래그 적용하면
    // getaddrinfo 함수는 자신이 반환하는 주소 정보를 서버 측 소켓에 사용할 수 있도록 설정함
    hints.ai_flags |= AI_NUMERICSERV; // serivice를 포트 번호로 해석할 것
    getaddrinfo(NULL, port, &hints, &listp);

    for (p = listp; p; p = p->ai_next)
    {
        /* 소켓 식별자 생성 */
        if ((listenfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) < 0)
            continue; // 소켓을 못 만들어냈음. 다음 거 시도.

        /* bind() 함수 호출 시 발생 가능한 "주소가 이미 사용되고 있음" 오류 방지 */
        // "Address already in use" 오류는 주로 서버 소켓이 특정 포트에 바인딩될 때,
        // 이전 연결이 제대로 종료되지 않아 해당 포트가 아직 사용 중인 경우 발생
        setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (const void *)&optval, sizeof(int));

        /* Bind the descriptor to the address */
        if (bind(listenfd, p->ai_addr, p->ai_addrlen) == 0)
            break;       // 바인드 성공하면 순회 끝
        close(listenfd); // bind 실패. 다음 거 시도.
    }

    /* 메모리 해제 */
    freeaddrinfo(listp);
    if (!p) // 아무 주소랑도 bind 못 함
        return -1;
    /* 서버 소켓을 클라 연결 요청을 받을 준비가 된 상태로 만듦 */
    // listen은 서버 소켓을 수신 대기 상태로 만듦
    // 오류 생기면 닫아버림
    if (listen(listenfd, LISTENQ) < 0)
    {
        close(listenfd);
        return -1;
    }
    return listenfd;
}

📌 Annotating : echoclient

#include "csapp.h"

int main(int argc, char **argv)
{
    int clientfd; // 클라 소켓 디스크립터
    char *host, *port, buf[MAXLINE];
    // host : 서버 호스트 이름 또는 IP 주소 저장하는 변수의 포인터
    // port : 서버 포트 주소 저장하는 변수의 포인터
    // buf[MAXLINE] : 서버로 보낼 데이터와 서버에서 받은 데이터를 저장할 버퍼
    rio_t rio; // 구조체. 버퍼링된 입출력 처리.

    if (argc != 3) // 인자 3개 아니면 종료
    {
        fprintf(stderr, "usage: %s <host> <port>\n", argv[0]);
        exit(0);
    }
    host = argv[1]; // 명령줄에서 입력받은 host
    port = argv[2]; // 명령줄에서 입력받은 port

    clientfd = open_clientfd(host, port); 
    // 주어진 호스트와 포트로 서버와의 연결을 설정하고, 해당 소켓 디스크립터를 반환
    rio_readinitb(&rio, clientfd);
    // rio_t 구조체를 초기화하여 소켓 디스크립터를 이용한 버퍼링된 읽기 작업을 준비

    while (fgets(buf, MAXLINE, stdin) != NULL)
    // 사용자가 표준 입력(stdin)으로 입력한 텍스트를 읽어와 buf에 저장
    // 루프는 fgets가 EOF 표준 입력(Ctrl+D 혹은 파일 텍스트 소진) 만나면 종료
    {
        rio_writen(clientfd, buf, strlen(buf));
        // 버퍼에 저장된 데이터를 서버로 전송
        // 소켓에 데이터를 쓰는 것은 데이터를 서버로 전송하는 것과 같음
        rio_readlineb(&rio, buf, MAXLINE);
        // 서버로부터 응답을 읽어와 buf에 저장
        fputs(buf, stdout);
        // 서버의 응답을 표준 출력(stdout)에 출력
    }
    close(clientfd);
    // 루프가 종료하고 클라이언트가 식별자를 닫으면
    // 서버로 EOF라는 통지가 전송됨

    // 열었었던 식별자들은 프로세스 종료할 때 커널이 자동으로 닫아주지만,
    // 열었던 모든 식별자들을 명시적으로 닫아주는게 올바른 습관이라고 함.
    exit(0);
}

소켓 디스크립터로 데이터를 쓰는 것은
데이터를 서버로 전송하는 것과 같다는 건
중요한 부분인듯

📌 Annotating : echoserveri

#include "csapp.h"

void echo(int connfd);

int main(int argc, char **argv)
{
    int listenfd, connfd;
    // listenfd : 클라 연결 요청 대기하는 소켓 식별자
    // connfd : 클라와 연결되면 생성되는 소켓 식별자. 이걸로 데이터 주고 받음.
    socklen_t clientlen; // 클라 주소 구조체 크기
    struct sockaddr_storage clientaddr; // IPv4, IPv6 주소 모두 담을 수 있는 구조체로 선언
    char client_hostname[MAXLINE], client_port[MAXLINE];
    // 각각 클라 호스트 이름, 클라 포트 번호 저장할 버퍼 

    if (argc != 2) // 인수가 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);
        // 클라의 연결 요청을 수락하면 소켓 식별자 connfd를 얻음
        getnameinfo((SA *)&clientaddr, clientlen, client_hostname, MAXLINE, client_port, MAXLINE, 0);
        // 클라 호스트 이름과 포트 번호 알아내서 client_hostname, client_port에 저장
        printf("Connected to (%s %s)\n", client_hostname, client_port);
        echo(connfd); // 클라가 보낸 데이터 그대로 돌려줌
        close(connfd);
    }
    exit(0); // 무한 루프라 도달 안 하는데 오류를 대비해 넣어놓음
}

이 서버는 한 번에 한 개의 클라이언트만 반복해서 처리함.
이런 서버를 반복 서버(iterative server)라고 함.
다수의 클라이언트를 동시에 처리하는 복잡한 서버는 동시성 서버라고 함.

📌 Annotating : echo

#include "csapp.h"

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

📌 Execution : Echo server

1. troubleshooting : header for memset

전부 다 실행 파일로 만들어야 하는걸로 착각하고 (뒤에 말하겠지만, 아니었다)
open_clientfd와 open_listenfd가 들어있는 csapp.c를 위해 make csapp를 했는데

이렇게 나옴. memset이 정의되어 있지 않으니 <string.h>를 넣으라는 뜻.
넣어서 해결했다.

<string.h>는 memset, strcpy, strlen 등과 같은
문자열 및 메모리 조작 함수를 사용하는 데 필요한 헤더 파일이라고 함.

2. troubleshooting : header for close

close()도 문제를 일으킴.

<unistd.h> 넣었더니 해결됨.

3. troubleshooting : not for execution, need header file!

헤더 오류를 전부 해결했지만 여전히 make가 먹히지 않음.
단순히 라이브러리 함수를 담는 c 파일인데 make를 하려고 해서였음.
(makefile도 없는데 make를 한 것도 바보같았던 부분)

gcc -o echoclient echoclient.c csapp.c

echoclient.c에서 csapp.c의 함수들을 사용한다면,
위와 같이 링크를 해줘야 함.

main 함수도 없는 csapp를 막무가내로 make해버리면 안됨.

그리고 gcc로 컴파일할 땐 csapp.c를 링크시켜준다고 해도,
echoclient.c에 참조돼있는 것은 csapp.h이므로 이것을 작성해줘야함.

두 개 이상의 파일 묶어서 실행 파일 만드는 법

  1. c파일 2개 작성. 하나는 main 함수 있고, 하나는 main 없이 라이브러리 함수들만 있음
  2. 라이브러리 함수들만 있는 파일에서 선언한 함수들을 헤더 파일에 이름만 매개변수로만 선언
  3. 컴파일할 때 -o 로 링크해서 컴파일해준다

csapp.h

#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <stddef.h>
#include <fcntl.h>  // for open
#include <unistd.h> // for close
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAXLINE 8192

// 클라이언트가 서버와 연결 설정하는 함수
int open_clientfd(char *hostname, char *port);

// 서버 소켓을 열고 클라이언트 연결 요청을 수신 대기하는 함수
int open_listenfd(char *port);

헤더 파일에 컴파일러가 넣으라는거 이것저것 다 넣어서 오류를 대부분 잡았지만,

rio 패키지 함수들은 여전히 컴파일 오류를 냄.

그리고 여기서 뭔가 쎄함을 느끼고 정글 컴퍼스를 찾아봄.
설마 10장에 있던 rio 패키지를 구현하고 왔어야 했나? 생각했는데
다시 원본 repo에 있는 파일을 확인했더니

이미 나는 완성된 csapp.h를 가지고 있었다.
어쩐지 open_listenfd 따라친 다음 다른 곳에서 쓸 때
vscode 자동 완성에 똑같은 이름인(첫 글자가 대문자인 것만 달랐음) 함수도 있더라.

csapp.ccsapp.h 그대로 Echo 폴더로 복붙해서 가져옴.

컴파일 정상적으로 됐음. (echo.c 빼먹었더니 오류 났었음. 포함시켜야 됨.)

✅ 실행 완료

작동도 정상적으로 됐다.

server 터미널 앞쪽에 server received -1 bytes가 마구 출력된 이유는,
처음에 아무 생각 없이 ./echoserveri 80 를 입력했었는데,
80은 웹서버의 기본 포트라고 함. 그래서인듯. 더 근본적인 원인은 모르겠음.

gpt가 "권한 문제 아닐까요?" 라길래 sudo로 서버 열어봤는데도 -1 계속 출력됨.
더 알아보지는 말자. 깊게 갈 시간이 없다.

⚔️ 백준


📌 17298 오큰수

import sys
input = sys.stdin.readline
N = int(input())
A = list(map(int,input().split()))
stack=[]
# 오른쪽에서 왼쪽으로 쭉 진행한다
for i in range(N-1,-1,-1):
    n = A[i]
    # 오큰수랑 비교해서 원래 수열에선 오큰수로 기록해버린 다음
    noks = True
    for j in range(len(stack)-1,-1,-1):
        if stack[j] > n:
            noks = False
            A[i] = stack[j]
            break
    if noks:
        A[i] = -1
    # 스택에서 자기보다 약하거나 같은 놈들 다 죽여버린다
    while(len(stack)>1 and stack[-1] <= n):
        stack.pop()
    # 스택에 냅다 넣는다
    stack.append(n)

    # 스택 맨 위 peek하면 그게 오큰수
for a in A:
    print(a,end=' ')

내가 접근했었던 방향

오른쪽에서 왼쪽으로 순회하며 오른쪽에 있는 정보들을 저장함.
이는 곧 '오큰수가 될 수 있는 수'들의 집합임.

오큰수를 찾으려면 그 집합을 탐색해야 함.
최적화를 위해 불필요한 연산을 없앨 수 있음.

불필요한 연산이라 함은, 511116 <- 이런 수열이 있다면
중간에 있는 1111은 탐색하지 않아도 되는데 탐색해버린다는 것.

'오큰수가 될 수 있는 수'에 숫자를 넣을 때마다
자신보다 작거나 같은 수들을 삭제하면 된다.
배열의 맨 끝 값을 삭제하므로 삭제 연산의 비용도 작다.

N = int(input())
seq = list(map(int, input().split()))
stack = []
res = [-1] * N

for i in range(N):
    while stack and seq[stack[-1]] < seq[i]:
        res[stack.pop()] = seq[i]
    stack.append(i)

print(res)

다른 사람들이 접근했었던 방향

왼쪽에서 오른쪽으로 순회하며 왼쪽에 있는 정보들을 저장함.
이는 곧 '오큰수를 찾고 싶은 수'들의 집합임.

오른쪽으로 갈 때마다 만나는 숫자(정보)들은
'오큰수가 될 수 있는 수'임.

'오큰수를 찾고 싶은 수'들의 집합을 탐색하며
'오큰수가 될 수 있는 수'보다 작거나 같은지 확인함.
오큰수가 되었다면 결과를 저장하고 집합에서 삭제하면 됨.

오큰수를 찾기 위해선, 왼쪽에 있는 정보들 중에서 필요한 것이 2가지가 있음.

  1. 오큰수를 찾고 싶은 숫자들이 어디에 있었는지 (결과 저장해야 하니까)
  2. 오큰수를 찾고 싶은 숫자들의 값은 얼마였는지 (비교해야 하니까)

이를 위해선 1. 인덱스만 저장하면 됨.
배열에 arr[i] 이런 식으로 접근하면 2. 도 알 수 있기 때문임.

0개의 댓글