보안 그룹 생성하기

 

위 사진처럼 인바운드를 설정해주고, 소스의 경우 IPv4로 설정

VPC는 전 포스팅에서 만들었던 VPC로 선택

 

EC2 생성하기

서울 리전인 것을 확인하고 실습하기

 

인스턴스 시작을 눌러줍니다.

이름을 정한 후, EC2 운영체제를 선택

운영체제는 Ubuntu를 선택했고, 인스턴스 유형은 micro 유형을 선택

키 페어를 선택 후( 없으면 생성 / 키 페어 생성을 누르고 이름을 설정한 후 나머지는 기본 유형으로 ) 생성해주면 끝 !

 

탄력적 IP

 

탄력적 IP : 인스턴스의 인터넷 게이트웨이를 거쳐 통신 시 부여받은 IP 주소를 고정시켜 인스턴스가 종료된 후 다시 실행될 때 IP 주소가 바뀌지 않게 해준다.

실제 서비스를 EC2를 통해 제공하고 있었을 때, 아이피 주소가 바뀌게 된다면 많은 혼란이 생기므로 탄력적 아이피를 연결해줘야 한다.

 

 

기본 설정을 아무것도 건드리지 않고 생성해주면 된다.

만들어진 탄력적 아이피를 연결하고 싶은 인스턴스를 선택 후 연결해준다.

VPC 생성

 

VPC 생성에 들어와서 VPC만을 선택하고 위의 사진처럼 설정한다.

IP 대역은 10.0.0.0/16으로 설정하고 나머지는 그대로 두기

생성하면 아래 완료 화면이 뜹니당

 

 

서브넷 만들기

방금 만든 VPC를 선택한다.

서브넷 CIDR은 10.0.1.0/24로 설정

앞으로 서브넷을 이어서 만들게 된다면 10.0.2.0/24 , 10.0.3.0/24 ...로 설정

 

인터넷 게이트웨이 생성

 

인터넷 게이트웨이를 VPC에 연결

이때 리전은 버지니아 북부 말고 서울로 설정하기 !

위에서 만들었던 VPC에 연결해줍니다.

 

라우팅 테이블 수정

[VPC > 라우팅 테이블]

라우팅 테이블은 VPC를 만들면 자동으로 만들어지기 때문에 수정만 하면 된다.

라우팅 편집을 통해 외부로 나가는 설정을 추가해준다.

0.0.0.0/0 ( 외부 아이피 대역)이 목적지일 때, 위에서 만들었던 인터넷 게이트웨이로 나가도록 설정한다.

외부와 통신이 가능하도록 설정이 된 라우팅을 서브넷에 연결해준다.

해당 라우팅 테이블을 원하는 서브넷에 연결해준다.

본 글은 UMC EWHA에서 활동하며 공부하는 워크북을 토대로 정리된 글입니다.

AWS에서 리전과 가용영역

리전

AWS에서 수많은 컴퓨팅 서비스를 하려면 당연히 대규모의 서버 컴퓨터를 모아둔 곳이 필요하다.

한 곳에 전부 다 몰아둔다면

1. 자연 재해가 발생 할 경우 모든 서비스가 마비된다.

2. 모든 자원이 북미에 있다면, 지구 반대편의 아시아 지역은 멀어서 서비스가 느리다.

2가지 불편한 점이 발생한다.

따라서 서비스를 하기 위한 자원들을 여러 곳에 분산해서 배치를 해둔다.

가용영역

리전을 한 번 더 분산해서 배치한 것

 

+ 서브넷, 서브넷마스크, 서브넷팅

IPv4 주소 체계는 클래스를 나누어 IP를 할당한다. -> 매우 비효율적 -> 네트워크 장치 수에 따라 효율적으로 사용할 수 있는 서브넷

서브넷 : IP 주소에서 네트워크 영역을 부분적으로 나눈 부분 네트워크

서브넷마스크 : IP 주소 체계의 Network ID와 Host ID를 분리하는 역할

서브넷팅 : IP 주소 낭비를 방지하기 위해 서브넷 마스크를 이용하여 원본 네트워크를 여러 개의 네트워크로 분리하는 것

-> 서브넷마스크의 bit수를 증가시키는 것

IP주소에 서브넷마스크를 AND 연산하면 Network ID가 된다.

 

 

+ 203.230.7.0/25, 203.230.7.128/25

IP 대역은 위와 같이 두 영역으로 나눌 수 있다.

네트워크 아이디(203.230.7.0, 203.230.7.128)가 다르기에 기본적으로 다른 LAN이다.

따라서 라우터 혹은 L3 스위치의 라우팅이 있어야 통신이 가능하다.

 

VPC

VPC는 기본적으로 가상의 네트워크 영역이기에 사설 아이피 주소를 가진다.

사설 아이피 대역은

10.0.0.0/8.    172.16.0.0/12.     192.168.0.0/24     -> 3개의 대역을 가진다.

하나의 VPC에는 위의 네트워크 대역 혹은 서브넷 대역이 할당 가능하다.

10.0.0.0/8의 서브넷인 10.0.0.0/16도 VPC에 할당 가능

 

VPC의 실제 사용

실제 사용 시 VPC 자체에서도 서브넷을 나눠서 사용하게 된다.

예를 들어 10.0.0.0/16의 IP주소를 VPC에 할당한 상황에서

VPC를 원하면 다시 서브넷으로 나눠서 각각 서브넷을 원하는 가용영역에 배치하여 사용하게 된다.

나눈 서브넷을 다시 나누지는 못한다.

 

유의할 점 !

VPC의 서브넷 IP 대역에서는 실제 네트워크와 달리 총 5개의 아이피주소를 호스트에 할당할 수 없다.

1. 서브넷의 네트워크 대역

2. VPC 라우터에 할당

3. Amazon이 제공하는 DNS에 할당

4. 미래를 위해 예약

5. 브로드 캐스트 주소

 

중요

VPC 내부적으로 라우터가 존재하고, VPC 내부 서브넷끼리 통신이 된다.

 

VPC의 외부통신

기본적으로 사설 아이피 대역은 공용 아이피 대역과 통신이 불가능하다.

하지만 AWS로 인프라를 구축하면 통신이 된다. 왜?

 

Public Subnet

  • VPC 서브넷 중 외부와 통신이 원활하게 되는 서브넷 대역
  • AWS의 Internet Gateway를 통해 해당 서브넷을 퍼블릿 서브넷이 되게 할 수 있다.
  • 서브넷이 외부와 통신할 때 Internet Gateway를 거치게 하면 외부와 통신이 가능 
  • 이때, 서브넷을 Internet Gateway를 통해 밖으로 나가도록 라우팅 테이블 설정을 해줘야 퍼블릭 서브넷이 된다.
  • 아래 그림처럼 Internet Gateway를 만들고 라우팅 테이블에서 내가 원하는 서브넷에 연결하여 퍼블릭 서브넷을 만든다.
    • Destination : 0.0.0.0/0 ( 모든 아이피 주소를 의미 = 외부 모든 아이피 = 밖으로 나갈 때)
    • Target : 만들어둔 Internet Gateway 식별자

+ Destination : 10.0.0.0/16  -> VPC 주소 = VPC로 들어올 때

    Target : Local  -> 내부 VPC 라우터가 알아서 잘 보내줄 것

 

 

Private Subnet

  • 외부와 통신이 되지 않는 서브넷 대역
  • 위의 조치를 취하지 않아 (VPC가 기본적으로 사설 아이피) 외부와 단절된 서브넷
  • 릴리즈 서버의 경우 실제 고객의 데이터가 저장되는 데이터베이스를 보호해야 하므로, 데이터베이스를 Private Subnet에 배치해야 함
  • 데이터베이스를 사용하는 EC2 등과 같은 컴퓨팅 자원을 같은 VPC에 저장하면 외부와 통신이 안되어 데이터베이스를 못 쓰는 문제점 X
  • 데이터베이스에 원격으로 접속하고 싶다면 SSH 터널링이라는 기술이 필요함. DataGrip에서 아주 쉽게 설정 가능하다.

 

사설 아이피 대역의 역할

1. 부족한 IP 주소 문제를 완화

2. 높은 보안성

예를 들어, 집에서 공유기로 와이파이를 사용할 때 공용 아이피 주소는 공유기에만 할당이 되고, 공유기에 연결한 디바이스들은 사설 아이피 대역을 받게 된다.

외부 인터넷은 공유기로 먼저 데이터를 보내고, 공유기는 포트를 통해 각 디바이스들을 구분하여 데이터를 보내준다.

 

공유기의 80번 포트192.168.0.1이 할당 된 노트북

공유기의 8080번 포트192.168.0.2가 할당 된 PC

연결이 된 것

+ 포트포워딩 : 하나의 공용 IP 주소를 가진 공유기가 자신의 포트를 통해 올바른 사설 IP 주소를 가진 디바이스에게 데이터를 주는 것

 

외부 네트워크의 디바이스는 공유기 뒤에 사설 아이피 주소를 가지고 있다.

이 숨어있는 디바이스로 직접 데이터를 절대로 전송할 수 없고, 무조건 공유기를 거친다.

 

키워드 정리

AWS

  • Amazon Web Services 의 약어이며 아마존이 제공하는 클라우드 컴퓨팅 플랫폼
  • 사용자가 웹상에서 IT 인프라를 구축하고 운영할 수 있도록 다양한 서비스를 제공한다.
  • 컴퓨팅 서비스(EC2), 스토리지 및 데이터베이스(S3, RDS, DynamoDB), 네트워킹(VPC, Route 53), 보안 및 ID 관리(IAM), 분석 및 인공지능 등의 서비스 제공
  • 위의 서비스들을 사용자가 필요한 만큼 확장하고 결합하여 다양한 애플리케이션을 구축하고 운영할 수 있는 유연성과 확장성을 제공한다.

리전과 가용영역

  • AWS의 인프라는 지역적으로 구성되어 있고, 각 지역은 하나 이상의 가용 영역으로 구성되어 있다.
  • 리전 : 지리적으로 분리되어 독립적으로 운영되는 영역, 서로 다른 데이터 센터와 인프라를 가지고 있다. 따라서 지역적으로 사용자에게 가까운 서비스를 제공하고, 고가용성과 내구성을 보장한다.
  • 가용영역 : 각 리전 내에서 물리적으로 분리된 데이터 센터 그룹, 가용 영역 간의 네트워크 연결은 매우 빠르고 안정적이며, 장애 발생 . 시다른 가용 영역으로 트래픽을 라우팅하여 고가용성을 유지할 수 있다.

서브네팅

  • 네트워크를 더 작은 부분으로 나누는 프로세스
  • 네트워크를 효율적으로 관리하고 트래픽을 분산시킬 수 있다.
  • IP 주소 공간을 효율적으로 사용하여 네트워크 리소스를 최적화하고 보안을 강화하는 것
  • 서브넷 크기 결정 -> 서브넷 마스크 선택 -> 서브넷 주소 지정 -> 서브넷 구성

라우팅

  • 네트워크에서 데이터 패킷을 한 지점에서 다른 지점으로 전송하는 과정
  • 라우팅 알고리즘을 통해 네트워크에 연결된 각 장치들이 어떻게 데이터를 전달할 지 결정한다.
  • 패킷의 목적지 주소를 기반으로 패킷을 다음 라우터나 최종 목적지로 전송한다.
  • 라우터는 라우팅 테이블을 사용하여 패킷의 목적지 주소를 분석하고, 이를 기반으로 패킷을 전달할 다음 호스트 또는 라우터를 결정한다.
  • 정적 라우팅, 동적 라우팅, 기본 라우팅 

VPC

  • Virtual Private Cloud, AWS에서 제공하는 가상 네트워킹 환경
  • VPC를 사용하여 AWS 클라우드 내에서 독립적인 가상 네트워크를 설정하고 관리할 수 있다.
  • 사용자가 완전히 제어하고 정의할 수 있는 가상 네트워크를 생성할 수 있다.
  • 가상 프라이빗 네트워크를 제공하므로, 인터넷과 외부와의 통신은 보안 그룹 및 네트워크 ACL 등으로 제어된다. 따라서 네트워크 환경을 보호하고 보안 정책을 적용할 수 있다.
  • 인터넷 게이트웨이나 NAT 게이트웨이 등을 사용하여 인터넷과 연결할 수 있다.
  • 클라우드 환경에서 안전하고 확장 가능한 네트워크 인프라를 제공하는 데 중요한 역할

사설 IP주소

  • 인터넷에 연결되지 않은 사설 네트워크 내에서 사용되는 IP주소
  • 인터넷에서 라우팅되지 않으며, 주로 내부 네트워크에서 호스트와 장치 간 통신을 위해 사용된다.
      1. 클래스 A 사설 IP 주소 범위: 10.0.0.0 ~ 10.255.255.255 (10.0.0.0/8)
      2. 클래스 B 사설 IP 주소 범위: 172.16.0.0 ~ 172.31.255.255 (172.16.0.0/12)
      3. 클래스 C 사설 IP 주소 범위: 192.168.0.0 ~ 192.168.255.255 (192.168.0.0/16)
  • 라우터나 네트워크 장비를 통해 인터넷으로 라우팅되지 않으므로, 외부 네트워크와 겹치지 않는 주소 공간을 제공한다.
  • 다수의 장치가 하나의 공용 IP주소를 공유하면서도 내부적으로는 고유한 IP주소를 사용한다.
  • NAT 기술을 사용하여 사설 IP주소를 공용 IP주소로 변환하여 외부와 통신할 수 있다.

포트포워딩

  • 네트워크에서 특정 포트로 들어오는 연결 요청을 네트워크 내부의 다른 장치로 전달하는 기술
  • 라우터나 방화벽과 같은 네트워크 장비에서 사용되며, 외부 네트워크에서 내부 네트워크로 들어오는 트래픽을 관리하는 데 사용된다.
  • 사용자는 외부에서 들어오는 포트와 내부로 전달할 포트를 설정하고, 목적지 IP주소를 지정하여 포트 포워딩을 구성할 수 있다.

NAT 프로토콜

  • Network Address Translation, 네트워크에서 사용되는 주소 변환 기술
  • 여러 장치가 하나의 공용 IP 주소를 공유하는 환경에서 사용된다.
  • 사설 IP주소를 사용하는 내부 네트워크에서 외부 네트워크와 통신할 수 있도록 해준다.
  • IP주소 변환, 포트 변환, 보안 향상
  • 일반적으로 NAT44, NAT64

포트번호

  • 네트워크 통신에서 프로세스나 서비스를 식별하기 위해 사용되는 번호
  • TCP 및 UDP와 같은 네트워크 프로토콜에서 사용된다.
  • 0부터 65535 범위에서 지정될 수 있고, 0부터 1023은 잘 알려진 포트로 예약되어 있음
  • HTTP : 80, HTTPS : 443
  • 잘 알려진 포트 외의 포트 번호는 동적 포트, 클라이언트와 서버 간의 임시 통신에 사용된다.
  • 포트 번호는 IP 주소와 함께 사용되어 특정 서버 또는 프로세스에 접근할 때 사용된다.

본 글은 UMC EWHA에서 활동하며 공부하는 워크북을 토대로 정리된 글입니다.

 

1. 서버의 정의와 역할

2. 서버가 어떻게 구축되는 지

 

서버란 ? 

OS에 의해 동작하는 프로세스이며 클라이언트의 역할을 하는 프로세스와 소켓을 통해 IPC를 수행하는 것

 

OSI 7계층 모델

  • ISO가 발표한 네트워크 통신 표준 모델(개념 모델)
  • 각 계층은 저마다의 맡은 기능이 있으며, 사용하는 프로토콜도 다르다.
  • 각 계층은 직전 계층에서 데이터를 전달받으며, 필요한 데이터를 붙여서 다음 계층으로 보낸다.
  • 즉 우리가 송신자면 7계층(L7) 부터 시작해 1계층(L1)까지 데이터를 만들어 보낸 뒤, 수신자는 다시 1계층부터 7계층까지의 과정을 거쳐 최종적으로 데이터를 받는다.

TCP/IP 4계층 모델

  • TCP/IP는 우리가 보통 많이 쓰는 TCP/IP 통신에 기반한 네트워크 통신 모델이다.
  • OSI 7계층과 유사하지만, 4계층으로 간소화되어 있다.

 

IP주소와 포트번호

컴퓨터가 네트워크 상에서 통신을 하기 위해서는 수많은 정보의 바다에서 자신이 누구인지 유일하게 식별이 가능한 수단이 있어야 한다.

이 때 사용되는 것이 IP주소이다.

따라서 IP주소는 절대로 겹치면 안되며, 고유해야 한다.

컴퓨터가 매우 많아짐에 따라 IP주소의 부족현상이 자연스레 발생하게 되었고, 이를 해결하기 위해

사설 IP주소를 사용하여 이를 공인 IP주소로 바꾸는 NAT, 서브네팅, IPv6 등의 기술들이 등장하게 되었다.

 

컴퓨터가 직접 네트워크에서 통신을 하는 것이 아니고,

컴퓨터에서 동작하는 프로세스가 또 다른 컴퓨터의 프로세스와 통신을 하는 것이다.

 

프로세스 간의 통신을 위해 IPC(Inter-Process Communication)를 하되,

그저 다른 시스템의 프로세스와 IPC를 한다고 생각하는 것이 매우 중요 !!

 

IP주소를 통해 컴퓨터를 식별한 후, 해당 컴퓨터에서 어떤 프로세스에게 데이터를 보내야 하는 지 알아야 하는데,

이 때 사용되는 식별 값이 포트 번호이다.

 

B로 데이터를 보낸다고 할 때, 아래와 같은 형태로 데이터를 보낼 대상을 식별한다.

[서버 프로세스 B가 동작 중인 컴퓨터의 아이피 주소]:[서버 프로세스가 부여받은 포트번호]

[203.230.7.2:80]의 뜻은 [203.230.7.2의 아이피 주소를 가진 컴퓨터의 80번 포트의 프로세스]

 

데이터 송신 과정

1. Application (데이터를 송신하려는 서버 프로세스)

2. Sockets

3. 네트워크 스택

4. NIC

이때 다양한 운영체제의 시스템 콜, 인터럽트를 활용한다.

 

데이터를 송신할 때

서버 프로세스가 운영체제의 write 시스템 콜을 통해 소켓에 데이터를 보내게 되고 이후 TCP/UDP 계층과 IP 계층 그리고 대표적으로 Ethernet을 거쳐 흐름제어, 라우팅 등의 작업을 하게 된다. 마지막으로 NIC(랜 카드)를 통해 외부로 데이터를 보낸다.

 

데이터를 수신할 때

반대로 NIC에서 데이터를 수신하고, 인터럽트를 통해 Driver로 데이터를 옮기고 이후 네트워크 스택에서 데이터가 이동하며 소켓에 데이터가 담기고, 최종적으로 수신 대상이 되는 프로세스에 데이터가 도달한다.

 

소켓

  • TCP 전용 소켓 (=stream 소켓) : UDP와 달리 신뢰성 있는 데이터 송수신
  • UDP 소켓 (=datagram 소켓) : TCP와 달리 비연결지향

TCP 소켓의 핵심은 accept() 시스템 콜

 

1. socket() 시스템 콜

소켓을 만드는 시스템 콜

미리 Ipv4 통신을 위해 사용할지, Ipv6 통신을 위해 사용할지, TCP를 사용할지 아니면 UDP를 사용할지 틀을 만들어두는 것

 

socket(domain, type, protocol);

domain : IPv4, IPv6중 무엇을 사용할지 결정

type : stream, datagram 소켓 중 선택

protocol : 0, 6, 17 중 0을 넣으면 시스템이 프로토콜을 선택하며, 6이면 TCP, 17이면 UDP

 

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

 

socket()의 리턴 값은 파일 디스크립터이다.

리눅스에서는 모든 것을 파일로 취급하며 소켓 역시 파일로 취급한다.

 

web server 프로세스가 데이터를 전송하기 위해 write(), read() 시스템 콜을 사용할 때,

대상 파일의 파일 디스크립터를 파라미터로 전송하여 OS에게 어떤 파일에 데이터를 작성할 지,

혹은 어떤 파일의 데이터를 요청할 지 결정한다.

 

이때 파일 디스크립터가 소켓의 파일 디스크립터일 경우,

소켓에 데이터를 작성(데이터 송신) 혹은 소켓의 데이터를 읽어들이는(데이터 수신) 동작을 하게 된다.

 

2. bind() 시스템 콜

생성한 소켓에 실제 IP주소와 포트번호를 부여하는 시스템 콜

 

bind(sockfd, sockaddr, socklen_t)

sockfd: 바인딩을 할 소켓의 파일 디스크립터

sockaddr: 소켓에 바인딩 할 아이피 주소, 포트번호를 담은 구조체

socklen_t : 위 구조체의 메모리 크기

#include <sys/socket.h>
#include <netinet/in.h>

int main() {
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) {
    	perror("Socket creation failed");
    	return 1;
    }
    
    struct sockaddr_in server_address;
    server_address.sin_family = AF_INET;    //IPv4 주소 체계
    server_address.sin_addr.s_addr = INADDR_ANY; // 모든 가능한 IP 주소
    server_address.sin_port = htons(80);       // 포트 번호 80
    
    if (bind(sockfd, (struct sockaddr *)&server_address, sizeof(server_address)) == -1) {
        perror("Bind failed");
        return 1;
    }
    // 바인딩 성공 처리 및 작업 수행

    return 0;
}

 

OS에게 어떤 소켓에 IP주소와 포트번호를 부여할 지 알려주기 위해 파라미터에 소켓의 파일 디스크립터를 포함한다.

클라이언트는 통신 시 포트번호가 자동으로 부여되기 때문에 bind 시스템 콜은 서버에서만 사용한다.

 

3. listen() 시스템 콜 only for TCP

listen(sockfd, backlog)

sockfd : 소켓의 파일 디스크립터

backlog : 연결요청을 받아줄 크기 = TCP의 백로그 큐의 크기

 

#include <sys/socket.h>

int main() {
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) {
        perror("Socket creation failed");
        return 1;
    }

    // ... 서버 소켓의 주소와 바인딩 설정 ...

    int backlog = 10; // 최대 대기열 크기
    if (listen(sockfd, backlog) == -1) {
        perror("Listen failed");
        return 1;
    }

    // 리스닝 성공 처리 및 연결 요청 처리

    return 0;
}

 

listen() 시스템콜은 연결지향인 TCP에서만 사용

파라미터로 받은 파일 디스크립터에 해당하는 소켓을 클라이언트의 연결요청을 받아들이도록 하며 최대로 받아주는 크기를 backlog로 설정한다.

listen() 시스템 콜에서 설정하는 backlogTCP에서의 backlog queue의 크기

 

어떻게 연결 요청을 받아들이게 하는가 ?

listen() 시스템 콜은 파라미터로 받은 backlog 크기만큼 backlog queue를 만드는 시스템 콜

서버에 셀 수 없이 많은 클라이언트가 요청을 보내게 되고 이 요청들은 모두 backlog queue에 저장

client가 클라이어트 소켓을 통해 처음으로 서버에 요청을 하여 backlog queue에 들어갈 때 syn 요청을 보냄

 

4. accept() 시스템 콜

int accept(sockfd, sockaddr, socklen_t);

sockfd : 백로그 큐의 요청을 받아들이기 위한 소켓의 파일 디스크립터

sockaddr : 선입선출로 빼온 연결 요청에서 알아낸 클라이언트의 주소 정보

socklen_t : 위 구조체의 메모리 크기

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

int main() {
    int server_socket = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in server_address;
    server_address.sin_family = AF_INET;
    server_address.sin_addr.s_addr = INADDR_ANY;
    server_address.sin_port = htons(80);

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

    listen(server_socket, 5);

    printf("Server: Waiting for client's connection...\n");

    struct sockaddr_in client_address;
    socklen_t client_addrlen = sizeof(client_address);

    int client_socket = accept(server_socket, (struct sockaddr *)&client_address, &client_addrlen);

    printf("Server: Accepted connection from %s:%d\n",
           inet_ntoa(client_address.sin_addr), ntohs(client_address.sin_port));

    // 3-way handshake의 나머지 두 단계 수행
    char buffer[1024];
    ssize_t bytes_received = recv(client_socket, buffer, sizeof(buffer), 0); // 클라이언트의 ACK 받기
    if (bytes_received > 0) {
        printf("Server: Received ACK from client.\n");
    }

 

accept 시스템 콜은 backlog queue에서 syn을 보내와

대기 중인 요청을 선입선출 (queue)로 하나씩 연결에 대한 수립을 해준다.

 

파라미터로 클라이언트의 IP주소, 포트번호를 받는데 이 값들은 백로그 큐에서 가장 앞에 있는

연결요청 구조체에서 알아내서 가져온다.

 

[참고] TCP 3-way handshake

TCP의 특징 신뢰성을 구현

클라이언트와 서버간에 서로 신뢰성 있는 통신을 위해 서로 준비가 되었는지를 확인하는 과정

 

 

 

client가 보내는 SYN이 listen 상태인 서버의 소켓에 연결 요청을 보내는 것

그 이후 과정은 accept 시스템 콜 이후 진행하여 최종적으로

established 상태를 수립하고 본격적인 데이터 송/수신이 이루어진다.

 

 

listen 이후 동작

accept 시스템 콜 이후 멀티 프로세스 / 멀티 쓰레드

accept 시스템 콜 이후 곧바로 잔여 3-way handshake 이후 데이터 송/수신이 이루어지는 것은 아니다.

서버의 성능을 위해 멀티 프로세스 혹은 멀티 쓰레드의 테크닉이 들어간다.

하나의 프로세스인 서버가 클라이언트의 수많은 요청을 받는 상황에서, 백로그 큐의 가장 앞에 있던 클라이언트의 요청을 받고 응답까지 . 다주고 다시 다음 요청을 받아준다면 엄청난 병목이 생길 것이다.

따라서 서버는 연결 요청을 받는 부분 따로, 이후 응답까지 주는 부분을 따로 나누게 된다.

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

int main() {
    int server_socket = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in server_address;
    server_address.sin_family = AF_INET;
    server_address.sin_addr.s_addr = INADDR_ANY;
    server_address.sin_port = htons(80); // 웹 서버 포트인 80

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

    listen(server_socket, 5);

    printf("Server: Listening on port 80...\n");

    while (1) {
        struct sockaddr_in client_address;
        socklen_t client_addrlen = sizeof(client_address);

        int client_socket = accept(server_socket, (struct sockaddr *)&client_address, &client_addrlen);

        if (fork() == 0) { // 자식 프로세스 <- 이 부분에 집중!

            printf("Server: Accepted connection from %s:%d\n",
                   inet_ntoa(client_address.sin_addr), ntohs(client_address.sin_port));

            // 3-way handshake의 나머지 두 단계 수행
            // 여기서는 ACK를 보내는 과정만 간단히 보여줍니다.
            sleep(1); // 실제로는 필요한 로직 수행

            // 서버의 응답 전송
            char response[] = "HTTP/1.1 200 OK\r\nContent-Length: 13\r\n\r\nHello, world!";
            send(client_socket, response, strlen(response), 0);
            printf("Server: Sent response to client.\n");

            close(client_socket);
            exit(0);
        }

        close(client_socket);
    }

    close(server_socket);

    return 0;
}

 

accept 시스템 콜의 응답을 받았다

= SYN 요청을 보낸 클라이언트가 적어도 하나 있어서 백로그 큐에 있었고 해당 클라이언트의 요청에 대한 이후 응답을 위해

새로운 소켓을 만들었다.

-> accept() 시스템 콜의 리턴값은 새로운 소켓의 파일 디스크립터인데, 새로운 소캣을 만드는 이유

 

fork() 시스템 콜

fork() -  자식 프로세스 생성

리턴값 = 0이면 자식 프로세스 / 0이 아님 = 원래 본인(부모) 프로세스

 

부모 프로세스 : 연결 요청을 받아주고 자식 프로세스에게 나머지 일을 맡김 , 다시 새로운 연결요청을 받아주는 형태

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

int main() {
    int server_socket = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in server_address;
    server_address.sin_family = AF_INET;
    server_address.sin_addr.s_addr = INADDR_ANY;
    server_address.sin_port = htons(80); // 웹 서버 포트인 80

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

    listen(server_socket, 5);

    printf("Server: Listening on port 80...\n");

    while (1) {
        struct sockaddr_in client_address;
        socklen_t client_addrlen = sizeof(client_address);

        int client_socket = accept(server_socket, (struct sockaddr *)&client_address, &client_addrlen);

        if (fork() == 0 -> false ) { 
           실행안함
        }

      
    }

    close(server_socket);

    return 0;
	}

 

자식 프로세스 : 부모 프로세스가 새로 만들어준 소켓을 이어받아 잔여 3-way handshake 수행 후 데이터 통신 수행

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

int main() {
    int server_socket = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in server_address;
    server_address.sin_family = AF_INET;
    server_address.sin_addr.s_addr = INADDR_ANY;
    server_address.sin_port = htons(80); // 웹 서버 포트인 80

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

    listen(server_socket, 5);

    printf("Server: Listening on port 80...\n");

    while (1) {
        struct sockaddr_in client_address;
        socklen_t client_addrlen = sizeof(client_address);

        int client_socket = accept(server_socket, (struct sockaddr *)&client_address, &client_addrlen);

        if (fork() == 0) { // 자식 프로세스 <- 이 부분에 집중!

            printf("Server: Accepted connection from %s:%d\n",
                   inet_ntoa(client_address.sin_addr), ntohs(client_address.sin_port));

            // 3-way handshake의 나머지 두 단계 수행
            // 여기서는 ACK를 보내는 과정만 간단히 보여줍니다.
            sleep(1); // 실제로는 필요한 로직 수행

            // 서버의 응답 전송
            char response[] = "HTTP/1.1 200 OK\r\nContent-Length: 13\r\n\r\nHello, world!";
            send(client_socket, response, strlen(response), 0);
            printf("Server: Sent response to client.\n");

            close(client_socket);
            exit(0);
        }

        close(client_socket);
    }

    close(server_socket);

    return 0;
}

 

자식 프로세스라면 (fork() == 0) 마지막에 exit(0) 시스템 콜을 호출

-> 새로운 연결요청을 받지 않고 그저 응답을 준 후 종료된다.

 

서버는 연결을 받는 부분과 응답을 주는 부분이 병렬적으로 이루어져 있다.

 

HTTP 웹 서버 코드

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

int main() {
    const char* server_ip = "127.0.0.1";
    int server_port = 8080;

    int server_socket = socket(AF_INET, SOCK_STREAM, 0);
    if (server_socket == -1) {
        perror("Socket creation failed");
        return 1;
    }

    struct sockaddr_in server_addr, client_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(server_port);
    server_addr.sin_addr.s_addr = inet_addr(server_ip);

    if (bind(server_socket, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("Binding failed");
        return 1;
    }

    if (listen(server_socket, 5) == -1) {
        perror("Listening failed");
        return 1;
    }

    printf("Server listening on %s:%d\n", server_ip, server_port);

    while (1) {
        socklen_t client_addr_len = sizeof(client_addr);
        int client_socket = accept(server_socket, (struct sockaddr*)&client_addr, &client_addr_len);
        if (client_socket == -1) {
            perror("Accepting client failed");
            continue;
        }

        printf("Accepted connection from %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));

        char request[1024];
        recv(client_socket, request, sizeof(request), 0);
        printf("Received request:\n%s\n", request);

        char response[] = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\nHello, World!";
        send(client_socket, response, sizeof(response), 0);

        close(client_socket);
    }

    close(server_socket);
    return 0;
}

 

 

키워드 정리

  • TCP
    • 클라이언트와 서버 간에 가상의 연결을 설정하고, 데이터를 전송한 후에는 연결을 종료한다.또한 신뢰성을 위해 세그먼트(segment)라는 단위로 나누어 전송한다.
    • Flow Control, Congestion Control, Packet Sequencing, Retransmission 등의 매커니즘을 통해 신뢰성을 보장한다.
    • 데이터를 안정적으로 전송하기 위한 연결 지향형 프로토콜
  • UDP
    • 연결 설정 과정이 없기에 데이터를 보내거나 받을 준비가 되어 있으면 즉시 전송한다.신뢰성을 보장하지 않기에 데이터가 손실되거나 순서가 바뀔 수 있다.
    • 실시간 데이터 스트리밍 등의 프로토콜에서 널리 사용된다.
    • 연결 설정 과정 및 정보가 포함되지 않기 때문에 오버헤드가 적고 이로 인해 더 빠른 전송 속도를 제공할 수 있다.
    • TCP와 달리 연결지향형이 아니며 신뢰성을 보장하지 않는다.
  • 시스템 콜?
    • 운영체제의 커널(Kernel)에 의해 제공되며, 프로세스나 응용 프로그램이 커널모드로 전환하여 특정한 작업을 수행할 수 있도록 한다.
    • 자원관리, 입출력, 프로세스 제어, 시스템 정보 조회 등의 기능을 수행한다.
    • 운영체제(OS)에서 제공하는 서비스를 사용자 공간의 응용 프로그램이나 사용자 모드의 프로세스가 호출할 수 있도록 하는 인터페이스
  • 하드웨어 인터럽트
    • 입출력 완료 / 타이머 인터럽트 / 하드웨어 오류 같은 상황에서 발생할 수 있다.컴퓨터 시스템의 효율성과 신뢰성을 높이는 데 중요한 역할
    • 하드웨어 인터럽트가 발생하면 CPU는 현재 실행 중인 작업을 중단하고 인터럽트 서비스 루틴(ISR) 코드 블록을 실행한다.
    • 하드웨어 장치의 비동기적인 이벤트를 처리하기 위해 CPU에게 신호를 보내는 메커니즘
  • 리눅스의 파일과 파일 디스크립터
    • 파일은 디렉토리 구조 안에 존재하며 다양한 형식의 데이터를 포함할 수 있따.파일 디스크립터는 프로세스가 파일이나 입출력 장치와 상호작용하기 위한 추상적인 개념이다.
    • 각 파일 디스크립터는 정수로 표현되며, 파일이나 입출력 장치를 참조하는 데 사용된다.
    • 각 파일은 고유한 이름(경로)을 가지고 있다.
    • 리눅스에서 파일은 시스템에서 데이터를 저장하는 데 사용되는 일반적인 개념
  • socket() 시스템 콜
    • 미리 Ipv4 통신을 위해 사용할지, Ipv6 통신을 위해 사용할지, TCP를 사용할지 아니면 UDP를 사용할지 틀을 만들어두는 것
    • 소켓을 만드는 시스템 콜
  • bind() 시스템 콜
    • 생성한 소켓에 실제 IP주소와 포트번호를 부여하는 시스템 콜
  • listen() 시스템 콜
    • 파라미터로 받은 파일 디스크립터에 해당하는 소켓을 클라이언트의 연결요청을 받아들이도록 하며 최대로 받아주는 크기를 backlog로 설정한다.
    • listen() 시스템 콜에서 설정하는 backlog가 TCP에서의 backlog queue의 크기
    • listen() 시스템콜은 연결지향인 TCP에서만 사용
  • accept() 시스템 콜
    • 파라미터로 클라이언트의 IP주소, 포트번호를 받는데 이 값들은 백로그 큐에서 가장 앞에 있는 연결요청 구조체에서 알아내서 가져온다.
    • accept 시스템 콜은 backlog queue에서 syn을 보내와 대기 중인 요청을 선입선출 (queue)로 하나씩 연결에 대한 수립을 해준다.
  • 멀티 프로세스
    • 운영체제가 여러 프로세스를 동시에 실행하고 관리할 수 있도록 해준다.병렬성과 동시성 / 안정성 / 확장성 / 자연 공유와 보호 등의 이유로 멀티 프로세스를 사용한다.
    • 병렬 처리와 성능 최적화를 위해 널리 사용된다.
    • 각 프로세스는 독립적인 메모리 공간을 가지며, 서로 다른 작업을 수행할 수 있다.
    • 컴퓨터 시스템에서 동시에 여러 개의 프로세스가 실행되는 것
  • 병렬 처리
    • 멀티코어 프로세서, 다중 프로세서 시스템 또는 분산 컴퓨팅 시스템에서 활용된다.멀티코어 프로세서 / 멀티스레딩 / 분산 컴퓨팅 / GPU 가속화 등의 방법들이 사용될 수 있다.
    • 병렬 처리를 구현하기 위해서는 하나의 작업을 여러 부분으로 나누어 동시에 처리함으로써 전체 작업의 속도를 향상시키는 방법이다.

+ Recent posts