크래프톤 정글 TIL : 0815

lazyArtisan·2024년 8월 15일
0

정글 TIL

목록 보기
46/147

🎯 To-do List


✴️ Today

  1. 웹서버 공부 | Socket Programming Tutorials In C For Beginners ✅, CSAPP 11장 읽기 🥥
  2. 기술 면접 준비 | 경쟁 조건, 데드락, 뮤텍스, 세마포어, 조건 변수 (Canceled)
  3. 알고리즘 스터디 | 오큰수 풀기 ✅

⌚ Time Tracking

  • 09:30 - 10:30 : 기상, 빨래, 아침
  • 10:30 - 13:00 : 13334 철로 풀기
  • 13:00 - 14:15 : 교육장 이동, 기록 정리, etc.
  • 14:15 - 15:10 : 알고리즘 스터디 | 각자 코드 발표
  • 15:10 - 15:40 : 점심
  • 15:40 - 17:00 : Socket Programming Tutorials In C For Beginners
  • 17:00 - 17:30 : 저녁
  • 17:30 - 21:05 : Socket Programming Tutorials In C For Beginners
  • 21:05 - 21:45 : 산책, 글 포맷 다듬기, 블로그 카테고리 수정, etc.
  • 21:45 - 23:50 : CSAPP 11장 읽기
  • 23:55 - 00:10 : 기술 면접 준비
  • 00:10 - 00:30 : 방향 재설정 및 기록, etc.
  • 00:30 - 01:00 : 17298 오큰수 풀기

⏳ Routine

🍪 Someday

  • 퀴즈 봤던거 정리 (기술 면접 준비 +알파 느낌으로)
  • c++ 공부하기
  • CS:APP 읽기 | 6장

✅🥥❌



📚 Journal


기록 문제 해결

  • 불친절한 설명
    : 이건 나중에 블로그에 한꺼번에 정리하면 될듯. 이건 문제가 아니다.

  • 과도한 시간 소모
    : 아무거나 막 쓰는게 문제인 것 같다. 내가 나중에 보면 좋겠다 싶을만한
    1. 개념에 대한 정리
    2. trouble shooting
    3. 내가 한 것에 대한 설명

    만 적자. 1.이랑 3.의 비중이 낮았던 것도 문제지만,
    trouble shooting을 두서 없이 적었던게 진짜 문제.
    생각하면서 정리하려면 오히려 시간이 더 걸릴수도?
    그래도 의미 있는 기록을 남기는 것이니 괜찮을듯.

  • 주간 회고의 부재
    매주 목요일 공부 시작 전에
    1. 이번 주차에 무엇을 했는지
    2. 느낀 점이 무엇이었는지
    3. 이번 주차에 대한 피드백
    -> 노력에 비해 리턴이 별로일 것 같음. 하지 말자.

Time tracking?

시간 기록하느라 비효율이 있긴 한데 기상이랑 수면 시간 조절하고
비는 시간 줄이도록 노력할 수 있을듯. 일단 3일 정도만 해보자

-> 막상 해보니까 마음이 살짝 조급해지기도 하고 시간 관리도 잘 할 수 있게 되는듯. 계속 해도 될 것 같다.



🎤 기술 면접 준비


경쟁 조건

경쟁 조건은 여러 프로세스나 스레드가 공유 자원에 동시에 접근하려고 할 때 발생하는 문제입니다. 경쟁 조건이 발생하면 프로세스들의 접근 순서에 따라 의도치 않게 실행 결과가 달라질 수 있습니다.

데드락 (교착 상태, Deadlock)

데드락은 두 개 이상의 프로세스나 스레드가 서로가 소유하고 있는 자원을 기다리면서 영원히 대기 상태에 빠지는 상황입니다. 데드락이 발생하려면 네 가지 조건이 필요합니다.

자원을 여러 프로세스가 동시에 사용할 수 없는 상호 배제(Mutual exclusion), 자원을 점유한 상태에서 다른 자원을 기다리는 점유 상태로 대기(Hold and wait), 할당된 자원을 뺏어올 수 없는 선점 불가(No preemption), 모든 프로세스가 원형으로 물고 물리면서 서로의 자원을 기다리는 상황을 순환성 대기(Circular wait)가 모두 충족되면 교착 상태, 즉 데드락이 일어납니다.

-> 방금 나무위키를 보다가 깨달은 건데, 아무리 내가 정리한 걸 잘 외웠다고 하더라도 이 정도의 얕은 지식을 읊어봤자 꼬리 질문 몇 개로 털릴 것 같다. 한 주제에 대해서 deep하게 정리하는게 나을듯? 알고리즘 스터디에 책 읽고 구현까지 해야 할텐데 시간이 너무 모자람. 그렇다고 하더라도 얕게 넓은 지식은 그닥 쓸모가 없을 것 같음. 일단 게임 프로그래머 면접 질문 주제들을 자세하게 정리한 글을 쓰는 식으로 선회해야 함. 조금씩이라도 써보자. 일단 매일 최소 30분은 투자하자.

아 근데 또 너무 정글 과정 이외에 것에 과투자해버리는게 아닐지 생각이 들긴 한다. 어차피 나만무 끝나면 시간 주어질거고, 지금 열심히 해놓고 나중에 지금 쌓아놓은 지식 기반으로 금방 학습하면 되는데 굳이 여기에 시간을 할애해야 하나? 라는 생각이 들어서 안하기로 함. 심심하거나 밥 먹을 때 찾아보는 선에서 끝낼 것. 퀴즈까지는 정글에 있던거 놓친거니까 공부해보면 좋을지도?



📝 배운 것들


🏷️ 파이썬: 2차원 리스트 2번째 요소에 대해 정렬

arr.sort(key=lambda x: x[1])

lambda 함수 사용 이유

arr.sort(key=lambda x: x[1])
이거 람다 꼭 넣어야 정렬됨? key=x[1] 이런 느낌으로 하면 안됨?

파이썬의 sort() 메서드에서 key 매개변수는 함수를 기대합니다. 이 함수는 각 요소에 대해 호출되어 반환된 값을 기준으로 정렬을 수행합니다. lambda 함수를 사용하면 간단하게 인라인으로 함수를 정의할 수 있습니다.



🌐 Web Server


개념 정리

📌 서버 accept() 스레드 분리

컴퍼스 내용 중에

accept() : read/write 또는 receive/send 메소드를 짝으로 사용하여 요청/응답 처리. 단, listen()에서의 스레드와 다른 스레드를 만들어 진행한다.
→ 스레드 분리 이유: 서버는 여러 요청을 받아야 하기에, listen() 스레드는(母) 놔두고 계속 기다리게 하고, accept() 스레드는 (子) 요청을 처리하도록 만들어 둠

라는게 있었음. 살짝 이해가 안돼서 물어봄.

listen()한테 악수 요청(연결 요청) 여러 개 오면 accept()라는 손(연결 수락)을 새로 하나 만들어서 악수(연결)한다는 뜻인듯.


위 설명은 다중 클라이언트 요청을 처리하기 위해 서버가 스레드를 어떻게 사용하는지를 설명하고 있습니다. 이를 이해하기 위해서는 서버 소켓 프로그래밍의 기본 개념과 스레드의 역할을 알아야 합니다.

기본 개념

  1. 서버 소켓:

    • 서버 소켓은 클라이언트의 연결 요청을 기다리고, 연결을 수락하는 역할을 합니다.
    • socket(), bind(), listen(), accept() 등의 함수가 이 과정에 사용됩니다.
  2. 스레드:

    • 스레드는 프로세스 내에서 독립적으로 실행되는 작업 단위입니다.
    • 서버가 여러 클라이언트 요청을 동시에 처리하려면 멀티스레딩을 사용할 수 있습니다.

설명의 요약

  • accept() 함수는 클라이언트의 연결 요청을 수락하고, 클라이언트와의 통신을 위한 새 소켓을 반환합니다.
  • read/write 또는 recv/send 함수는 이 새 소켓을 통해 클라이언트와 데이터를 주고받습니다.
  • 서버는 여러 클라이언트 요청을 동시에 처리해야 하기 때문에, 하나의 스레드가 여러 요청을 처리하는 대신 각 요청을 별도의 스레드에서 처리합니다.

스레드 분리 이유

  1. 주 스레드 (listen() 스레드):

    • 서버는 주 스레드에서 listen() 함수를 호출하여 클라이언트의 연결 요청을 기다립니다.
    • 이 주 스레드는 클라이언트의 연결 요청을 기다리면서 계속 실행됩니다.
  2. 자식 스레드 (accept() 스레드):

    • 주 스레드는 accept() 함수를 호출하여 클라이언트의 연결 요청을 수락합니다.
    • 연결 요청이 수락되면, 자식 스레드가 생성되어 해당 클라이언트의 요청을 처리합니다.
    • 자식 스레드는 read/write 또는 recv/send 함수로 클라이언트와 통신합니다.

Socket Programming Tutorial In C For Beginners | Part 1

https://www.youtube.com/watch?v=LtXEMwSG5-8

📌 기본적인 클라이언트 소켓 구현

소켓 : 네트워크로 정보 전송하기 위한 low-level 도구.
HTTP나 FTP같은 네트워킹 프로토콜들이 연결을 위해 소켓을 사용한다.

원래 "클라이언트" 소켓 같은 건 없다. 소켓은 데이터 주고 받기 둘 다 가능.
정보 받기만 하는 클라이언트 소켓은

  • socket()으로 만들어지고
  • connect()로 원격 주소에 접속할 수 있고
  • recv()로 데이터를 받아올 수 있다

기본 헤더 파일

  • sys/types.h: 다양한 시스템 데이터 타입을 정의
  • sys/socket.h: 소켓 관련 함수와 데이터 타입을 정의
  • netinet/in.h: 인터넷 주소 패밀리와 관련된 구조체 및 상수를 정의
  1. #include <sys/types.h>: size_t, ssize_t, pid_t 등 데이터 타입이 정의돼있음

  2. #include <sys/socket.h>: 소켓 관련 함수들과 데이터 타입들.
    ex. socket(), bind(), listen(), accept(), connect()

  3. #include <netinet/in.h>: 인터넷 주소 패밀리(AF_INET)와 관련된 구조체와 상수들.
    ex. sockaddr_in 구조체와 htons(), htonl(), ntohs(), ntohl() 함수들

소켓 생성

// 소켓 만들기
int network_socket;
network_socket = socket(AF_INET, SOCK_STREAM, 0);
  • int network_socket;: 소켓을 나타내는 정수형 변수 선언
  • network_socket = socket(AF_INET, SOCK_STREAM, 0);
    - socket() 함수는 소켓을 생성하고 그 소켓에 대한 파일 디스크립터를 반환함.
    - AF_INET: IPv4 인터넷 프로토콜을 사용
    - SOCK_STREAM: TCP 소켓을 사용
    - 0: 0은 기본 프로토콜(TCP의 경우 IPPROTO_TCP)

소켓 주소 입력

// 소켓의 주소를 입력
struct sockaddr_in server_address;
server_address.sin_family = AF_INET;
server_address.sin_port = htons(9002);
server_address.sin_addr.s_addr = INADDR_ANY;
  • struct sockaddr_in server_address;: sockaddr_in 구조체를 사용하여 서버 주소 정보를 저장할 server_address 변수를 선언함.
  • server_address.sin_family = AF_INET;: 주소 패밀리를 IPv4로 설정
  • server_address.sin_port = htons(9002);: 포트 번호를 9002로 설정. htons() 함수는 호스트 바이트 순서에서 네트워크 바이트 순서(빅 엔디안)로 변환함. 네트워크에서 데이터가 올바르게 해석될 수 있도록 하기 위함임.
  • server_address.sin_addr.s_addr = INADDR_ANY;: 모든 IP 주소와 연결될 수 있도록 주소를 설정. INADDR_ANY는 로컬 머신의 모든 네트워크 인터페이스를 의미.

"htons() 함수는 호스트 바이트 순서에서 네트워크 바이트 순서(빅 엔디안)로 변환함." 이 뭔소린데?

호스트와 네트워크 바이트 순서

  • 호스트 바이트 순서: 데이터가 현재 실행 중인 머신의 내부에서 저장되는 방식입니다. 이는 머신의 아키텍처에 따라 빅 엔디안 또는 리틀 엔디안일 수 있습니다.
  • 네트워크 바이트 순서: 네트워크를 통해 전송되는 데이터의 순서로, 항상 빅 엔디안 방식입니다. 즉, 네트워크 상에서는 데이터가 빅 엔디안 형식으로 전송됩니다.

htons() 함수

  • htons는 "Host TO Network Short"의 약자로, 호스트 바이트 순서에서 네트워크 바이트 순서(빅 엔디안)로 16비트(2바이트) 숫자를 변환합니다.
  • 이는 네트워크 통신을 할 때, 시스템이 사용하는 내부 바이트 순서(호스트 바이트 순서)와 상관없이 네트워크가 데이터를 일관되게 해석할 수 있도록 보장합니다.

연결 상태 확인

// 0이면 잘 작동하고 있다는 뜻, 1이면 뭔가 오류가 일어났다는 뜻
int connection_status = connect(network_socket, (struct sockaddr *)&server_address, sizeof(server_address));

// 연결 잘 됐는지 확인
if (connection_status == -1)
{
    printf("There was an error making a connection to the remote socket \n\n");
}

connect()

  • connect() 함수는 클라이언트 소켓을 서버 주소에 연결함. 이 함수가 성공하면 클라이언트 소켓은 서버 소켓과 연결되어 데이터를 주고받을 수 있음.
  • 함수의 반환값은 성공 시 0, 실패 시 -1이며, 실패 시에는 errno를 통해 에러의 원인을 알 수 있음.

매개변수

  • network_socket: 클라이언트 소켓의 파일 디스크립터. socket() 함수 호출 시 반환된 값임.
  • (struct sockaddr *)&server_address: 서버 주소를 나타내는 sockaddr_in 구조체의 주소. connect 함수는 sockaddr 구조체 포인터를 인수로 받기 때문에, 타입 캐스팅을 통해 sockaddr_in 구조체 포인터를 sockaddr 포인터로 변환해야 함.
  • sizeof(server_address): sockaddr_in 구조체의 크기. connect 함수는 주소 구조체의 크기를 필요로 함.

서버에서 데이터 받아오기

// 서버에서 데이터 받아오기
char server_response[256];
recv(network_socket, &server_response, sizeof(server_response), 0);

서버에서 데이터 받아오기:
- char server_response[256];: 서버로부터 받을 데이터를 저장할 배열 server_response 선언.
- recv(network_socket, &server_response, sizeof(server_response), 0);: 서버로부터 데이터를 수신함.

응답 출력 후 소켓 닫기

// 서버의 응답을 출력함
printf("The server sent the data: %s\n", server_response);

// 그리고 소켓 닫음
close(network_socket);

전체 코드

#include <stdio.h>
#include <stdlib.h>

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

#include <netinet/in.h>

int main()
{
	// 소켓 만들기
	int network_socket;
	network_socket = socket(AF_INET, SOCK_STREAM, 0);

	// 소켓의 주소를 입력
	struct sockaddr_in server_address;
	server_address.sin_family = AF_INET;
	server_address.sin_port = htons(9002);
	server_address.sin_addr.s_addr = INADDR_ANY;

	// 0이면 잘 작동하고 있다는 뜻, 1이면 뭔가 오류가 일어났다는 뜻
	int connection_status = connect(network_socket, (struct sockaddr *)&server_address, sizeof(server_address));

	// 연결 잘 됐는지 확인
	if (connection_status == -1)
	{
		printf("There was an error making a connection to the remote socket \n\n");
	}

	// 서버에서 데이터 받아오기
	char server_response[256];
	recv(network_socket, &server_response, sizeof(server_response), 0);

	// 서버의 응답을 출력함
	printf("The server sent the data: %s\n", server_response);

	// 그리고 소켓 닫음
	close(network_socket);

	return 0;
}

이게 기본적인 클라이언트 소켓 프로그램을 구현한 것임

  1. 소켓을 생성
  2. 서버 주소를 지정
  3. 서버에 연결을 시도
  4. 서버로부터 데이터를 수신
  5. 수신한 데이터를 출력
  6. 소켓을 닫고 자원을 해제

📌 기본적인 서버 소켓 구현

  1. socket(): 소켓 생성

  2. bind(): 소켓을 IP 주소와 포트에 바인딩

  3. listen(): 소켓을 수신 대기 상태로 만듦

  4. accept(): 들어오는 연결을 수락

  5. send() / recv(): 데이터를 전송하거나 수신

위 코드는 서버 소켓을 설정하고 클라이언트의 연결을 받아들인 후, 클라이언트에게 메시지를 보내는 과정을 보여줍니다. 각 줄의 역할을 설명하겠습니다.

소켓 바인딩

// IP랑 포트를 바인드
bind(server_socket, (struct sockaddr *)&server_address, sizeof(server_address));
  • bind() 함수는 소켓을 특정 IP 주소와 포트에 바인딩함
  • server_socket: 이전에 생성한 서버 소켓의 파일 디스크립터
  • (struct sockaddr *)&server_address: 서버의 IP 주소와 포트 정보가 들어 있는 sockaddr_in 구조체를 sockaddr 구조체로 캐스팅한 것입니다.
  • 성공 시 0을 반환하고, 실패 시 -1을 반환

연결 대기

listen(server_socket, 5);
  • listen() 함수는 소켓을 수신 대기 상태로 설정함
  • 5: 대기열의 최대 길이입니다. 즉, 동시에 대기할 수 있는 연결 요청의 최대 개수입니다.
  • 이 함수는 성공 시 0을 반환하고, 실패 시 -1을 반환합니다. 실패하면 perror() 등을 사용하여 오류를 출력합니다.
  • 성공 시 0을 반환하고, 실패 시 -1을 반환

클라이언트 연결 수락

int client_socket;
client_socket = accept(server_socket, NULL, NULL);
  • accept() 함수는 대기열에서 클라이언트의 연결 요청을 수락하고, 새로 생성된 소켓의 파일 디스크립터를 반환.
  • NULL, NULL: 원래 이 칸에는 클라이언트의 주소 정보를 저장할 변수를 지정할 수 있음. 지금은 주소 정보를 안 써서 NULL로 설정.
  • 성공 시 새로 생성된 소켓의 파일 디스크립터를 반환하고, 실패 시 -1을 반환

클라이언트로 메시지 보내기

// 메세지 보내기
send(client_socket, server_message, sizeof(server_message), 0);
  • send() 함수는 데이터(메시지)를 소켓을 통해 클라이언트로 보냄
  • server_message: 전송할 데이터(메시지)가 저장된 버퍼
  • 0: 기본 플래그 값입니다.
  • 이 함수는 성공 시 전송된 바이트 수를 반환하고, 실패 시 -1을 반환

전체 코드

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main() {
    // 서버 소켓 생성
    int server_socket;
    server_socket = socket(AF_INET, SOCK_STREAM, 0);
    if (server_socket < 0) {
        perror("socket failed");
        exit(1);
    }

    // 서버 주소 설정
    struct sockaddr_in server_address;
    server_address.sin_family = AF_INET;
    server_address.sin_port = htons(9002);
    server_address.sin_addr.s_addr = INADDR_ANY;

    // IP와 포트 바인딩
    if (bind(server_socket, (struct sockaddr *)&server_address, sizeof(server_address)) < 0) {
        perror("bind failed");
        close(server_socket);
        exit(1);
    }

    // 연결 대기
    if (listen(server_socket, 5) < 0) {
        perror("listen failed");
        close(server_socket);
        exit(1);
    }

    // 클라이언트 연결 수락
    int client_socket;
    client_socket = accept(server_socket, NULL, NULL);
    if (client_socket < 0) {
        perror("accept failed");
        close(server_socket);
        exit(1);
    }

    // 클라이언트에게 메시지 보내기
    char server_message[] = "Hello from the server!";
    if (send(client_socket, server_message, sizeof(server_message), 0) < 0) {
        perror("send failed");
        close(client_socket);
        close(server_socket);
        exit(1);
    }

    // 소켓 닫기
    close(client_socket);
    close(server_socket);

    return 0;
}

기본적인 서버 소켓 프로그램

  1. 서버 소켓 생성
  2. 서버 주소 설정
  3. 서버 소켓을 IP 주소와 포트에 바인딩
  4. 서버 소켓을 연결 대기 상태로 설정
  5. 클라이언트의 연결 요청 수락
  6. 클라이언트에게 메시지를 전송
  7. 클라이언트 소켓과 서버 소켓 닫음

의문

➜  TCPclient make tcp_server
cc     tcp_server.c   -o tcp_server
tcp_server.c: In function ‘main’:
tcp_server.c:35:5: warning: implicit declaration of function ‘close’; did you mean ‘pclose’? [-Wimplicit-function-declaration]
   35 |     close(server_socket);
      |     ^~~~~
      |     pclose

코드 똑같이 작성했는데 make 했더니 오류 남. 내 잘못인가? 했는데 아님.
다른 사람도 그런가? 했는데 댓글 창 검색해보니 아님.

일단 경고 무시하고 테스트 진행해봤는데 정상 작동 함. 메시지 잘 받아짐.

close는 파일 디스크립터를 닫는 거고,
pclose는 popen으로 연 파이프 스트림을 닫는 거임.

아까 client_socket이 성공 시 새로 생성된 소켓의 파일 디스크립터를 반환 한다고 했으니 close가 맞는듯.

Socket Programming Tutorials In C For Beginners | Part 2

https://www.youtube.com/watch?v=mStnzIEprH8

📌 기본적 HTTP 서버

HTTP Client : 요청할 resource과 사용한 method를 담아서 request를 보낸다
HTTP Server : response body와 status code와 함께 응답을 보낸다

보낼 파일 열기

// 보낼 파일을 연다
FILE *html_data;
html_data = fopen("index.html", "r");
  • FILE *html_data;: 파일 포인터 선언
  • html_data = fopen("index.html", "r");: 현재 디렉토리에서 index.html 파일을 읽기 모드("r")로 연다.
  • fopen 함수는 파일을 열고 파일 포인터를 반환한다. 파일을 열 수 없는 경우, NULL을 반환한다.

파일 내용 읽기

char response_data[1024];
fgets(response_data, 1024, html_data);
  • char response_data[1024];: 파일 내용을 저장할 배열 선언
  • fgets(response_data, 1024, html_data);: html_data 파일에서 최대 1024바이트를 읽어서 response_data 배열에 저장

fopen, fgets 주의 사항

  1. 파일 열기 오류 처리
    : 실제로는 fopen이 실패했을 경우를 처리해야 함. (ex. 파일이 존재하지 않거나 읽기 권한이 없을 때)
if (html_data == NULL) {
    perror("Failed to open file");
    return 1;
}
  1. 읽기 오류 처리
    : fgets 함수에서 파일 끝에 도달하거나 오류가 발생하면 종료해야 함.
if (fgets(response_data, sizeof(response_data), html_data) == NULL) {
    perror("Failed to read file");
    return 1;
}

HTTP 헤더 생성, 데이터 결합

char http_header[2048] = "HTTP/1.1 200 OK\r\n\n";
strcat(http_header, response_data);
  • char http_header[2048] = "HTTP/1.1 200 OK\r\n\n";: 응답 헤더를 저장할 배열을 선언하고 초기화
  • HTTP/1.1 200 OK: HTTP 버전 1.1을 사용하고, 상태 코드가 200(OK)임을 나타냄.
  • \r\n\n: 헤더와 본문 사이를 구분하기 위한 빈 줄.
  • strcat(http_header, response_data);: response_data의 내용을 http_header 배열 뒤에 이어 붙임. 이렇게 하면 HTTP 응답 헤더와 HTML 파일의 내용이 결합됨.

여러 클라이언트 요청 수락

int client_socket;
while (1)
{
    client_socket = accept(server_socket, NULL, NULL);
    send(client_socket, http_header, sizeof(http_header), 0);
    close(client_socket);
}

무한 루프를 통해 클라이언트의 연결 요청을 지속적으로 수락하고, HTTP 응답을 보냄

전체 코드

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

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

#include <netinet/in.h>

int main()
{
    // 보낼 파일을 연다
    FILE *html_data;
    html_data = fopen("index.html", "r");

	// 응답할 때 사용할 데이터를 준비한다
    char response_data[1024];
    fgets(response_data, 1024, html_data);

	// HTTP 헤더를 준비한다
    char http_header[2048] = "HTTP/1.1 200 OK\r\n\n";
    strcat(http_header, response_data);

    // 소켓 만들기
    int server_socket;
    server_socket = socket(AF_INET, SOCK_STREAM, 0);

    // 주소를 정의한다
    struct sockaddr_in server_address;
    server_address.sin_family = AF_INET;
    server_address.sin_port = htons(8001);
    server_address.sin_addr.s_addr = INADDR_ANY;

    bind(server_socket, (struct sockaddr *)&server_address, sizeof(server_address));

    listen(server_socket, 5);

	// 클라이언트의 연결 요청을 지속적으로 수락한다
    int client_socket;
    while (1)
    {
        client_socket = accept(server_socket, NULL, NULL);
        send(client_socket, http_header, sizeof(http_header), 0);
        close(client_socket);
    }

    return 0;
}
  1. 응답 데이터 준비
  2. HTTP 헤더 준비
  3. 서버 소켓 생성
  4. 주소 정의
  5. 서버 소켓을 연결 대기 상태로 설정
  6. 클라이언트의 연결 요청 수락

HTML 파일 안 불러와짐

코드 잘못 썼나 숨은 그림 찾기 해봤는데 틀린 부분 없었음.
댓글창을 보니 바로 정답을 알려줌.

내 vscode에선 의문의 코드 포매터가 저장할 때마다 강제로 형식을 바꿔버리고 있음.
c에서도 자꾸 그러길래 찾으려고 해봤는데 아무리 봐도 없음.
Prettier나 이런거 싹 다 Disable 해놨는데도 없음.
이유를 모르겠음.

아무튼 댓글에 따르면 이렇게 한 줄로 해야 한다고 함.
영상에도 한 줄로 나와있음.
vim은 신인가? (신 아님)

📌 기본적 HTTP 클라이언트

IP 주소 가져오기

    char *address;
    address = argv[1];
  • 명령줄 인수로 IP 주소를 받아 address 변수에 저장
  • argv[1] : 프로그램 실행 시 제공된 첫 번째 인수

원격 주소 설정

    // 주소로 연결
    struct sockaddr_in remote_address;
    remote_address.sin_family = AF_INET;
    remote_address.sin_port = htons(80);
    inet_aton(address, &remote_address.sin_addr.s_addr);
  • line 1-3 : 서버 주소 저장할 구조체 선언하고 주소 체계는 IPv4, 포트 번호는 80로 설정.

  • inet_aton(address, &remote_address.sin_addr.s_addr);
    : address 문자열(IP 주소)을 네트워크 주소 구조체로 변환하여
    remote_address.sin_addr.s_addr에 저장함.

  • inet_aton() 함수 : 문자열 형식의 IPv4 주소를 네트워크 바이트 순서의 32비트 이진 값으로 변환함. 네트워크 프로그래밍에서 IP 주소를 처리할 때 사용됨.

요청과 응답 버퍼 설정

    char request[] = "GET / HTTP/1.1\r\n\r\n";
    char response[4096];
  • char request[] = "GET / HTTP/1.1\r\n\r\n";:
    : HTTP GET 요청 문자열을 정의함. 이 요청은 루트 경로(/)에 대한 HTTP/1.1 GET 요청임.

  • char response[4096];: 서버로부터의 응답을 저장할 버퍼를 선언

요청 전송 및 응답 수신

    send(client_socket, request, sizeof(request), 0);
    recv(client_socket, &response, sizeof(response), 0);
  • send(client_socket, request, sizeof(request), 0);: 서버에 HTTP GET 요청
  • recv(client_socket, &response, sizeof(response), 0);: 서버로부터의 응답 수신

전체 코드

#include <stdio.h>
#include <stdlib.h>

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

#include <netinet/in.h>
#include <arpa/inet.h>

int main(int argc, char *argv[])
{
    char *address;
    address = argv[1];

    int client_socket;
    client_socket = socket(AF_INET, SOCK_STREAM, 0);

    // 주소로 연결
    struct sockaddr_in remote_address;
    remote_address.sin_family = AF_INET;
    remote_address.sin_port = htons(80);
    inet_aton(address, &remote_address.sin_addr.s_addr);

    connect(client_socket, (struct sockaddr *)&remote_address, sizeof(remote_address));

    char request[] = "GET / HTTP/1.1\r\n\r\n";
    char response[4096];

    send(client_socket, request, sizeof(request), 0);
    recv(client_socket, &response, sizeof(response), 0);

    printf("response from the server: %s\n", response);
    close(client_socket);
}

기본적인 HTTP 클라이언트 구현

명령줄 인수로 제공된 IP 주소의 서버에 HTTP GET 요청을 보내고,
서버로부터의 응답을 받아 출력함.

  1. 프로그램이 시작되면 명령줄 인수로 제공된 IP 주소를 처리
  2. 클라이언트 소켓 생성
  3. 서버 주소 정보 설정
  4. 설정된 서버 주소로 연결을 시도
  5. HTTP GET 요청을 서버로 전송
  6. 서버로부터의 응답을 수신
  7. 수신한 응답을 출력
  8. 클라이언트 소켓을 닫고 프로그램을 종료


📖 CS:APP


📝 Chapter 11 네트워크 프로그래밍

모든 네트워크 응용 프로그램들은 동일한 프로그래밍 모델에 기초하고 있으며, 논리 구조도 비슷하며, 프로그래밍 인터페이스도 동일하다.

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

모든 네트워크 응용 프로그램은 클라이언트-서버 모델에 기초함.

클라이언트-서버 모델

  • 서버 프로세스 1개, 클라이언트 프로세스 1개로 이루어짐
  • 서버 : 일부 리소스 관리, 리소스 조작해서 클라이언트 위한 서비스 제공
  • 서버가 리소스를 중앙 집중적으로 관리하고, 클라이언트의 요청에 따라 리소스를 효율적으로 분배할 수 있어서 시스템의 확장성과 유지보수성을 향상시키는데 도움이 됨
  • 기본적으로 트랜잭션을 통해 모든 것을 처리함

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

  1. 클라이언트가 요청 전송 : 클라이언트가 서비스가 필요할 때 서버에 요청 전송
  2. 서버에서 요청 처리 : 요청을 받고, 해석하고, 자신의 리소스를 조작함
  3. 서버가 응답 전송 : 클라이언트에 응답 보내고 다음 요청 기다림
  4. 클라이언트가 응답 처리 : 클라이언트도 응답 수신하고 이를 처리

클라이언트와 서버는 프로세스임. 호스트 또는 머신이 아님.
한 개의 호스트는 동시에 여러 개의 클라이언트와 서버를 실행할 수 있음.

11.2 네트워크

호스트 입장에서 네트워크는 데이터를 보내고 받는 또 다른 입출력 장치일 뿐임.
네트워크 어댑터가 I/O 버스의 확장 슬록에 꽂혀 네트워크와 물리적 인터페이스를 제공함.
네트워크에서 수신된 데이터는 I/O와 메모리 버스를 거쳐 어댑터에서 메모리로 복사된다.
반대로 메모리에서 네트워크로 복사될 수도 있음.

네트워크의 물리적 구성

네트워크는 지리적 근접성에 따라 계층적으로 구성됨.

가장 Low-level은 로컬 영역 네트워크(LAN, Local Area Network)로, 빌딩이나 캠퍼스에 설치됨.
가장 인기 있는 LAN 기술은 이더넷(Ethernet)으로, 1970년대 개발됨.

이더넷 세그먼트는 허브들로 구성됨. 방이나 빌딩의 층 같이 작은 지역에 설치함.
호스트들은 허브에 연결되고, 허브들은 브리지로 연결됨. 브리지는 다른 브리지랑 연결됨.

각 이더넷 어댑터는 자신의 비휘발성 메모리에 저장된, 전 세계적으로 고유한 48비트 주소를 가지고 있음.

호스트는 프레임이라고 불리는 비트 덩어리들을 다른 호스트한테 보낼 수 있음.
각 프레임은 출발지와 목적지의 주소, 프레임 길이를 식별하는 헤더 비트와 데이터 비트로 구성됨.
모든 호스트 어댑터는 프레임을 수신하지만, 목적지 호스트만이 실제로 프레임을 읽음.


허브가 호스트에 데이터를 전달하는 법

이거 설마 허브가 자기한테 연결된 모든 호스트들한테 프레임을 쫙 뿌린다는 거임?

네, 정확히 이해하셨습니다. 허브는 연결된 모든 호스트들에게 프레임을 브로드캐스트합니다. 허브는 네트워크 계층에서 레이어 1 장치로 동작하여, 들어오는 데이터 패킷을 모든 포트로 전송합니다. 이 과정에서 특정 목적지 호스트를 지정하지 않고, 네트워크에 연결된 모든 장치에게 데이터를 전달합니다.

따라서, 네트워크에 있는 모든 호스트가 프레임을 수신하게 되지만, 프레임의 목적지 주소를 확인하여 실제로 해당 프레임을 읽고 처리하는 것은 목적지 호스트만입니다. 다른 호스트들은 자신과 무관한 프레임이므로 이를 무시하게 됩니다.

이와 달리 스위치는 네트워크 계층에서 레이어 2 장치로 동작하여, MAC 주소 테이블을 이용해 프레임을 목적지 호스트에게만 전달합니다. 이로 인해 네트워크 트래픽이 줄어들고, 보안성도 향상됩니다.


브리지드 이더넷

여러 이더넷 세그먼트는 브리지(bridge)라는 작은 박스를 사용해서 브리지드 이더넷으로 연결될 수 있음.
브리지드 이더넷은 전체 건물이나 캠퍼스 규모로 설치될 수 있음.
브릿지의 일부 선은 브릿지를 브릿지로 연결하고, 다른 선들은 브릿지를 허브로 연결함.
당연하지만 브릿지-브릿지 선(1Gb/s)이 허브-브릿지 선(100Mb/s)보다 대역폭이 높음.

브리지는 우수한 분산 알고리즘을 사용해서 어떤 호스트가 어떤 포트에서 접근 가능한지를 자동으로 학습하고 필요한 경우에만 프레임을 다른 포트로 복사함.

인터넷

여러 호환되지 않는 LAN은 라우터(router)라는 특수 컴퓨터를 사용해서 인터넷(internet)으로 연결될 수 있다.
라우터는 각 네트워크에 대한 어댑터를 가지고 있고, WAN(Wide-Area Network)도 연결할 수 있음.
WAN은 LAN보다 지리적으로 더 넓은 지역에서 운용되므로 붙은 이름임.
라우터는 임의의 LAN과 WAN들로 internet을 구축할 수도 있음.

인터넷은 매우 다양하고 비호환적인 기술을 갖는 여러 LAN과 WAN들로 이루어져 있음.
이러한 어려움을 극복하고 데이터를 전송하기 위해, 네트워크 프로토콜 소프트웨어에서 두 가지 기본 기능을 제공함.

네트워크 프로토콜 소프트웨어 : 호스트와 라우터에서 실행됨

  1. 명명법 (Naming Scheme) : 서로 다른 LAN 기술은 호스트에 주소를 할당하는 방법도 다름. 인터넷 프로토콜은 호스트 주소를 위한 통일된 포맷을 정의해서 차이점을 완화한다. 각 호스트는 자신을 유일하게 식별하는 internet 주소를 최소한 1개 할당받는다.

  2. 전송 매커니즘 (delivery mechanism) : 서로 다른 네트워킹 기술은 비트를 인코딩하고 프레임으로 패키징하는 방법이 다르다. 인터넷 프로토콜은 데이터 비트를 패킷으로 묶어서 이러한 차이를 완화한다. 패킷은 패킷 크기와 출발지 및 목적지 호스트의 주소를 포함하는 헤더와 데이터 비트를 포함하는 페이로드로 구성된다.

데이터 전송 과정

호스트 A에 있는 클라이언트가 LAN1에 있는 호스트 B의 서버로 데이터를 보내는 과정

  1. 호스트 A의 클라이언트가 시스템 콜을 호출해서 데이터를 커널 버퍼로 복사
  2. 프로토콜 소프트웨어가 인터넷 헤더LAN1 프레임 헤더를 데이터에 추가해서 LAN1 프레임을 생성
    • 인터넷 헤더 : 인터넷 호스트 B로 주소가 지정됨
    • LAN1 프레임 헤더 : 라우터로 주소가 지정됨
    • 어댑터로 전달되는 건 LAN1 프레임임.
    • LAN1 프레임의 데이터는 인터넷 패킷이고, 패킷이 실제 사용자 데이터임.
  3. LAN1 어댑터가 프레임을 네트워크로 복사
  4. 프레임이 라우터에 도달하면, 라우터의 LAN1 어댑터가 프레임을 읽고 프로토콜 소프트웨어로 전달
  5. 라우터가 인터넷 패킷 헤더에서 목적지 인터넷 주소를 가져와 어댑터에게 LAN2로 패킷을 전달함.
    이때 이전 LAN1 프레임 헤더를 벗겨내고 호스트 B의 주소를 갖는 새로운 LAN2 프레임 헤더를 앞에 붙임.
  6. 라우터의 LAN2 어댑터가 프레임을 네트워크로 복사
  7. 프레임이 호스트 B에 도달하면, 어댑터가 프레임을 읽고 프로토콜 소프트웨어로 전달
  8. 호스트 B의 프로토콜 소프트웨어가 패킷 헤더와 프레임 헤더를 제거하고, 데이터가 서버의 가상 주소 공간으로 복사됨

근데 이 과정에서 여러 문제가 발생하곤 함.

  • 네트워크들이 서로 다른 최대 프레임 크기를 가지면 어떻게 됨?
  • 라우터들은 어디로 프레임을 전달해야 할지 어떻게 앎?
  • 라우터들은 네트워크 구조가 언제 변경되는지 어떻게 앎?
  • 패킷이 손실되면 어떡함?

11.3 글로벌 IP 인터넷

각 인터넷 호스트는 TCP/IP 프로토콜(Transmission Control Protocol/Internet Protocol)을 구현한 소프트웨어를 실행한다.
인터넷 클라이언트와 서버는 소켓 인터페이스 함수와 Unix I/O 함수의 혼합을 사용하여 통신한다.
소켓 함수는 일반적으로 커널 모드의 TCP/IP 함수들을 호출하는 시스템 콜로 구현됨.
이 시스템 콜들은 커널에서 트랩을 발생시킴.

TCP/IP 프로토콜

TCP/IP는 사실 프로토콜의 집합으로, 각각 서로 다른 기능을 제공함.

IP 매커니즘 : 기본적인 Naming Scheme과 데이터그램이라고 하는 패킷을 한 인터넷 호스트에서 다른 호스트로 보낼 수 있는 전달 매커니즘 제공. 네트워크에서 데이터그램이 손실되거나 중복되는 경우 복구하려고 노력하지 않으므로 안정적이지 않음
UDP (Unreliable Datagram Protocol) : IP를 살짝 확장해서 호스트 간 전송 뿐만 아니라 프로세스 간의 데이터그램 전송을 가능하게 함
TCP : IP를 기반으로 프로세스 간의 안전한 완전 양방향 연결을 제공하는 복잡한 프로토콜


IPv4와 IPv6

32비트 주소를 이용하는 오리지널 인터넷 프로토콜은 Internet Protocol Version4 (IPv4)임. 1996년에 Internet Engineering Task Force (IETF)는 128비트 주소를 갖는 Internet Protocol Version 6 (IPv6)를 만들어 IPv4를 대체하고자 했지만, 거의 20년이 지난 2015년에도 거의 대부분의 인터넷 트래픽이 IPv4 네트워크로 전송되고 있음. 그래서 책에서도 별로 얘기 안 할거임. 라고 적혀있음.


11.3.1 IP 주소

IP 주소는 부호 없는 32비트 정수임.

(내일 이어서)



⚔️ 백준


📌 13334 철로

import sys
input = sys.stdin.readline
import heapq
N=int(input())
ho=[]

# 좌표 정보 힙에 넣기
for _ in range(N):
    a, b = map(int,input().split())
    heapq.heappush(ho,(min(a,b),max(a,b)))
L = int(input())
start=[]
max=0
while(ho):
    se = heapq.heappop(ho)
    s,e=se[0],se[1]
    # 처음이거나 start 좌표가 달라졌고, L보다 길이가 작은 선분이라면 추가
    if not start or start[-1][0] != s:
        if e-s <= L:
            start.append([s,0])
    # 자신의 종료점부터 L만큼보다 앞에 있는 점들 전까지 cnt+1
    for i in range(len(start)-1,-1,-1):
        if e-L <= start[i][0]:
            start[i][1]+=1
        else:
            break
m=0
for i in range(len(start)):
    if start[i][1] > m:
        m=start[i][1]
print(m)

여기까지 풀었는데 시간 초과 뜸

import sys
input = sys.stdin.readline
import heapq
N=int(input())
ho=[]
# 좌표 정보 힙에 넣기
for _ in range(N):
    a, b = map(int,input().split())
    heapq.heappush(ho,(min(a,b),max(a,b)))
L = int(input())
m=0
cnt=[]
while(ho):
    se = heapq.heappop(ho)
    s,e=se[0],se[1]
    heapq.heappush(cnt,s)
    # 자신의 종료점부터 L만큼보다 앞에 있는 점들 빼내기
    while(cnt and cnt[0] < e-L):
        heapq.heappop(cnt)
    m=max(m,len(cnt))
print(m)

답지 보고 수정했는데도 계속 틀려서 봤더니

import sys
import heapq

# 입력값 받기
n = int(sys.stdin.readline().strip())
locations = [] # ([(시작 좌표, 끝 좌표) ...]
for _ in range(n):
    h, o = map(int, sys.stdin.readline().strip().split())
    # 집과 사무실 중 좌표값이 낮은 것을 앞에, 좌표값이 높은 것을 뒤에 넣어줌
    locations.append((min(h, o), max(h, o)))
d = int(sys.stdin.readline().strip())

# 선분의 끝점을 기준으로 오름차순 정렬한 다음, 앞점을 기준으로 오름차순 정렬
locations.sort(key=lambda x: (x[1], x[0]))

heap = []
max_cnt = 0

for location in locations: # 각 사람의 좌표를 기준으로 반복문 돌기
    start, end = location
    heapq.heappush(heap, start) # 최소 힙. pop했을 때 start가 작은 것부터 나올 수 있도록
    line_start = end - d # 현재 사람의 끝 좌표를 기준으로 철로의 시작 지점 계산
    while heap and heap[0] < line_start: # heap[0] = 힙에 저장된 시작 지점 중 최소값
        heapq.heappop(heap) # 철로의 시작 지점보다 시작 좌표가 작은 경우 철로를 벗어나므로 pop
    max_cnt = max(max_cnt, len(heap)) # 현재 철로에 포함되는 사람 수가 최대값인 경우 갱신

print(max_cnt)

https://velog.io/@youngeui_hong/%ED%8C%8C%EC%9D%B4%EC%8D%AC-%EB%B0%B1%EC%A4%80-13334%EB%B2%88-%EC%B2%A0%EB%A1%9C

답지는 좌표 정보를 힙에 넣는게 아니라 정렬을 하고 있었음. 어째서?
-> 힙으로 할 수야 있을 것 같긴 함. 근데 이 경우엔 끝 점에 대해 최소 힙을 만들어야 하는데 심지어 시작 점의 정보도 보존해야 하므로 힙으로 구현하기 힘들듯.

import sys
input = sys.stdin.readline
import heapq
N=int(input())
ho=[]
# 좌표 정보 넣기
for _ in range(N):
    a, b = map(int,input().split())
    ho.append((min(a,b),max(a,b)))
# 좌표 정보 정렬하기
ho.sort(key=lambda x: x[1])
L = int(input())
m=0
cnt=[]
for se in ho:
    s,e=se
    heapq.heappush(cnt,s)
    # 자신의 종료점부터 L만큼보다 앞에 있는 점들 빼내기
    while(cnt and cnt[0] < e-L):
        heapq.heappop(cnt)
    m=max(m,len(cnt))
print(m)

답지에서 불필요하게 시작점에 대해 정렬하길래 이상해보여서 뺐는데 잘 작동함.

힙에 넣어서 이전 선분들에서 계산했던 정보들을 재활용할 수도 있을거라는 생각까지는 했는데 구체적으로 구현을 어떻게 해야할지 생각이 닿지 못했다. 조금만 더 잘했으면 혼자 풀 수 있었을 것 같은데 ㄲㅂ. (근데 좌표 정보를 정렬해야 한다는 걸 전혀 모르고 있었어서 힘들긴 했을듯)

📌 17298 오큰수

import sys
input = sys.stdin.readline
N = int(input())
A = list(map(int,input().split()))
m=0
for i in range(N-1,-1,-1):
    n = A[i]
    if m < n:
        A[i] = -1
        m = n
    else:
        A[i] = m
for i in range(N):
    print(A[i],end=' ')

문제 쓱 보고 뭐야? 개쉬운 문제네? 하고 코드 쓱 적었다가
오른쪽에서 자기보다 큰 수 중에서 '가장 왼쪽에 있는' 수를 골라야 한다는 걸 알아버림

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=' ')

이거 탑이랑 비슷한 문제네? 하고 로직 똑같이 접근함.
맞긴 했음. 근데 이게 최적화된 로직인진 모르겠음. 백준 채점도 느리게 됨.

그리고 다른 사람들 답을 봤는데...

import sys
N = int(sys.stdin.readline())
A = list(map(int, sys.stdin.readline().split()))
NGE= [-1]*N
stack = [0] # 0번 인덱스

for i in range(1, N):
    # 오큰수 : A[i]의 오른쪽에 있으면서 A[i]보다 큰 수 중 가장 왼쪽 값 
    while stack and A[stack[-1]] < A[i]:
        NGE[stack.pop()] = A[i] # 해당 인덱스 칸은 A[i]
    stack.append(i)
print(*NGE)

말도 안되게 간결했음.
애초에 스택에서 뺄 이유도 없었던 거임.

다른 사람들보다 메모리 조금 덜 쓰고 연산 살짝 빠른가? 싶었는데
메모리 덜 쓰는 건 맞는데 연산은 비슷한듯.
시간 다른 코드들도 다 똑같다. 그날 백준 서버 상태에 따라 달라지는듯.

어떤 알고리즘으로 풀어야 하는지, 어느 정도 난이도인지 모르고 푸니까,
거기에 아무 생각 없이 빨리 풀어야겠다는 생각까지 드니 이런 일이 생기는 것 같다.
생각 좀만 더 해서 의 로직을 쓰지 않아도 된다는 사실을 깨달았으면
30분 걸리는게 아니라 10분, 아니 5분만에 풀었을듯.

-> 다시 보니 아니었다. 아래 참고.

좋은 경험이 되었다.

코드에서 noks 부분이 좀 더러워서 다른 코드는 어떻게 했나 봤더니
NGE= [-1]*N을 선언해놔서 코드가 깔끔해졌다.

초기화 값을 미리 깔아놓으면 한 쪽 조건만 판별해도 되도록 코드를 간결하게 만들 수 있다.

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
    # 스택에 냅다 넣는다
    stack.append(n)
for a in A:
    print(a,end=' ')

스택에서 자기보다 낮은 수 지워버리는 과정 없애고 돌렸더니 시간 초과가 뜬다.
뭔가 이상해서 다시 봤더니 단순히 스택에서 값을 안 빼는게 아니라
스택에 인덱스를 넣어놓는다.

지금은 졸려서 이해가 바로 안되니까 내일 다시 와서 봐야겠다.

0개의 댓글