ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 웹서버와 클라이언트의 송수신 동작 - 소켓, TCP
    네트워크 2022. 4. 15. 18:26
    '성공과 실패를 결정하는 1%의 네트워크 원리' 책을 정리한 포스트입니다.

     

     

    1. 📕 서버의 개요


    1-1. 클라이언트와 서버의 차이점

    네트워크에 관한 부분, 즉 LAN 어댑터, 프로토콜 스택, Socket 라이브러리 등의 기능은 클라이언트와 서버는 조금도 다르지 않습니다.

    데이터를 송수신하는 관점에서 보면 클라이언트와 서버는 차이점이 없는게 좋습니다.

    클라이언트와 서버라는 식으로 역할을 정하지 않고 좌우 대충 어느에서나 자유롭게 데이터를 송수신할 수 있도록 두는게 좋습니다.

    데이터 송수신 측면에서의 차이점 :
    [접속동작], 접속 동작은 한쪽은 기다리고 다른 한 쪽이 연결해야합니다.
    접속하는 측이 클라이언트고 접속을 기다리는 측이 서버입니다.
    정확히는 Socket 라이브러리를 호출하는 부분에서 차이가 있습니다.

    1-2. 서버 애플리케이션의 구조

    서버는 동시에 복수의 클라이언트와 통신합니다.
    클라이언트가 접속할 때 마다 새로 서버 프로그램을 작동하여 서버 어플리케이션이 클라이언트와 1:1로 대화합니다.

    일반적인 서버 프로그램 제작 :

    1. 먼저 서버가 클라이언트의 접속을 대기하는 함수/클래스
      접속을 접수하면 새 프로세스/쓰레드를 생성
      그 후 새로 생성한 소켓을 건네줌
    1. 그리고 서버와 클라이언트가 대화하는 함수/클래스
      클라이언트와 대화하는 부분은 새 클라이언트가 접속할 때 마다
      잇달아 가동되므로 한 대의 클라이언트와 1 대 1로 대응함

    미리 클라이언트와 대화하는 몇 개 부분을 작동시켜 두고 클라이언트가
    접속했을 때 클라이언트 상태를 처리하지 않는 비어있는 것을 찾아
    여기에 접속한 소켓을 건네주어 클라이언트와 통신하는 방법도 있음.


    1-3. 서버측의 소켓과 포트 번호

    클라이언트 송수신 동작 :

    1. 소켓을 생성(소켓 작성 단계)
    2. 서버측의 소켓과 파이프로 연결
    3. 데이터를 송수신
    4. 파이프를 분리하고 소켓을 말소(연결 끊기 단계)

    서버 송수신 동작:

    1. 소켓을 생성(소켓 작성 단계)
    2. 소켓을 접속 대기 상태로 만든다.
    3. 접속을 접수한다.
    4. 데이터를 송수신
    5. 파이프를 분리하고 소켓을 말소(연결 끊기 단계)
    /*----------------(a) 접속을 기다리는 부분------------------*/
    동작개시;
    ...
    <디스크립터> = socket(<IPv4를 사용>, <TCP를 사용함>);
    ...
    bind(<디스크립터>, <포트 번호 등>, ...);
    ...
    listen(<디스크립터1>, ...);
    ...
    <디스크립터2> = accept(<디스크립터>, ...);
    ...
    클라이언트와 대화하는 부분을 호출(<디스크립터2>);
    ...
    accept로 돌아감;
    ...
    /*-----------------------------------------------------*/
    
    /*----------(b) 클라이언트와 대화하는 부분---------------*/
    ...
    <수신 데이터 길이> = read(<디스크립터2>, <수신 버퍼>, <수신 버퍼 길이>);
    ...
    <리퀘스트 메시지의 의뢰 내용을 처리한다>;
    ...
    write(...);
    ...
    close();
    ...
    종료;
    /*-----------------------------------------------------*/
    1. socket() 함수 호출하여 소켓 생성
    2. bind() 함수 호출하여 소켓에 포트 번호 저장
    3. listen() 함수 호출하여 소켓에 접속하기를 기다리는 상태라는 제어 정보를 저장
      소켓은 접속 동작 패킷이 도착하는 것을 기다리는 상태가 됩니다.
    4. accept() 함수를 호출하여 접속을 접수
      accept() 함수를 호출한 시점에서 서버측은 보통 패킷의 도착을 기다리는 상태이고 애플리케이션은 쉬는 상태가 됩니다.
      접속 패킷이 도착하면 접속 대기의 소켓을 복사하여 새로운 소켓을 만들고, 접속 상대의 정보를 새 소켓에 저장

     

    접속 대기 소켓:
    접속 패킷이 도착하면 접속 대기의 소켓을 복사하여 새로운 소켓을 만든다.
    그럼 원래 있던 접속 대기 중이던 소켓은 어떻게 되지??

    💡 접속 대기 상태인 채로 계속 존재한다.

     

    accept()함수가 호출되어 접속 패킷이 도착하면 접속 대기 소켓을
    복사하여 새 소켓을 생성한다. 그리고 원래 소켓은 계속 대기 상태인채로 둔다.

    이렇게 잇달아 복사해서 새 소켓을 만드는게 중요함!
    접속 대기 소켓에 그대로 접속하면 접속 대기 중인 소켓이 없어져 버리므로
    다음에 접속하는 클라이언트는 난감해진다.


    새 소켓을 만들 때 포트 번호도 중요하다!

    💡 새로 만든 소켓에도 접속 대기 소켓과 같은 포트 번호를 할당한다.

     

    하지만 이렇게 하면 문제가 발생합니다.
    포트 번호는 소켓을 지정하기 위한 번호인데, 같은 번호가 여러개면
    소켓을 어떻게 구분??

    클라이언트에서 패킷이 도착했을 때 TCP 헤더에 기록되어 있는
    수신처 포트 번호만 조사해서는 패킷이 어느 소켓에서 대화하고 있는지
    판단할 수가 없다.

    그래서 포트 번호만으로 구분하지 않는다.

    • 클라이언트측의 IP 주소
    • 클라이언트측의 포트 번호
    • 서버측의 IP 주소
    • 서버측의 포트 번호

     

    2. 🖥️ 서버의 수신 동작


    2-1. LAN 어댑터에서 디지털 신호로 변환

    💡 LAN 어댑터의 MAC 부분이 패킷을 신호로부터 디지털 데이터로 되돌리고 FCS를 점검한 후 버퍼 메모리에 저장

     

    인터럽트를 사용하여 LAN 어댑터에서 CPU로 패킷의 도착을 알림.
    CPU는 실행을 멈추고 랜 드라이브로 실행을 전환
    랜 드라이버가 동작하여 버퍼 메모리에 있던 패킷을 추출

    💡 랜 드라이버가 MAC 헤더로부터 프로토콜을 판단하여 프로토콜 스택에 패킷을 전달


    2-2. IP 담당 부분의 수신 동작

    프로토콜 스택에 패킷이 전달되면 우선 IP 담당 부분이 동작하여 IP 헤더를 점검.
    수신처 IP가 자신을 대상으로 하는지 조사.
    그 후 분할된 패킷을 일시적으로 메모리에 저장,
    분할된 패킷이 전부 도착하는 시점에 조각을 원래 패킷으로 복원
    IP 헤더의 프로토콜 번호 항목을 조사하여 TCP(0x06)나 UDP(0x11)로 넘김.

    💡 프로토콜 스택의 IP 담당 부분은 IP 헤더를 점검하고 (1) 자신을 대상으로 한 것인지 판단한 후 (2) 조각 나누기에 의한 패킷의 분할이 있는지 조사하고 (3) TCP 담당 부분 또는 UDP 담당 부분에 패킷을 전달


    2-3. TCP 담당 부분의 수신 동작(접속 패킷)

    💡 TCP 헤더에 있는 SYN비트가 1이면 접속 동작의 패킷이다.

    먼저 도착한 패킷의 수신처 포트 번호를 조사하여 이 번호와 같은 번호를 할당한 접속 대기 상태의 소켓이 있는지 확인합니다.
    만약 없으면 오류 통지 패킷을 클라이언트에게 반송합니다.

    접속 대기 소켓이 있으면 복사하여 새 소켓을 만들고 IP, 포트 번호, 시퀀스 번호의 초깃값 등을 기록 그리고 패킷을 받았음을 나타내는 ACK 번호, 시퀀스 초깃값 등을 TCP 헤더에 기록하고 클라이언트에게 전송합니다.

    이 패킷이 클라이언트에 도착하면 클라이언트에서 패킷을 받았음을 나타내는 ACK 번호가 돌아옵니다. 이것이 돌아오면 접속 동작은 완료됩니다.

    이때 서버측의 앱은 accept()를 호출하여 실행을 쉬는 상태, 여기에 새로 만든 소켓의 디스크립터를 전달하여 서버 애플리케이션의 동작을 재개합니다.

    💡 패킷이 접속 동작의 패킷인 경우 TCP 담당 부분은 (1) TCP 헤더의 SYN의 컨트롤 비트를 확인하고 (2) 수신처 포트 번호를 조사한 후 (3) 해당하는 접속 대기 소켓을 복사하여 새 소켓을 작성하고 (4) 송신처의 IP 주소나 포트 번호 등을 기록


    2-4. TCP 담당 부분의 수신 동작(데이터 패킷)

    이번엔 접속 동작이 아니라 이미 접속이 완료되고 데이터 송수신 단계에서 서버측이 어떻게 동작하는지 확인합니다.

    우선 TCP 프로토콜은 도착한 패킷이 어느 소켓에 해당하는지 확인합니다.

    • 클라이언트측의 IP 주소
    • 클라이언트측의 포트 번호
    • 서버측의 IP 주소
    • 서버측의 포트 번호

    위 4가지 정보를 바탕으로 소켓을 판별합니다.

    알맞은 소켓을 판별하면 소실된 데이터가 없는지 조사합니다.

    TCP 헤더에 기록된 시퀀스 번호와 데이터 조각의 길이를 확인하고 그 정보로 부터 다음 시퀀스 번호를 계산합니다. 만약 중간에 건너뛴 시퀀스 번호가 없다면 데이터가 제대로 도착한 것입니다.

    그 후 클라이언트에게 보낼 응답용 TCP 헤더를 만듭니다. 수신 패킷의 시퀀스 번호와 데이터 조각의 길이로부터 계산한 ACK 번호를 기록합니다. IP 프로토콜에 의뢰하여 클라이언트에게 전송합니다.

    💡 데이터의 패킷을 수신한 경우 TCP 담당 부분은 (1) 도착한 패킷의 송신처 IP 주소, 송신처 포트 번호, 수신처 IP 주소, 수신처 포트 번호로부터 해당하는 소켓을 판단하고 (2) 데이터의 조각을 연결해서 수신 버퍼에 보관한 후 (3) 클라이언트에게 ACK를 되돌려준다


    2-5. TCP 담당 부분의 연결 끊기 동작

    데이터 송수신이 끝나면 연결 끊기 동작에 들어갑니다.
    연결 끊기 동작은 어느쪽이 먼저 해도 상관없습니다.

    HTTP/1.0은 서버가 먼저 끊음

    서버측 연결 끊기

    1. 먼저 Socket 라이브러리의 close() 함수를 호출합니다.
    2. 그러면 서버측의 프로토콜 스택이 TCP 헤더를 만들고 FIN 컨트롤 비트를 1로 설정합니다.
    3. IP 프로토콜에 의뢰하여 클라이언트에 송신해달라고 합니다.
    4. 이와 동시에 서버측의 소켓에 연결 끊기 동작에 들어갔다는 정보를 기록합니다.

    클라이언트측 연결 끊기

    1. 서버에서 FIN에 1을 설정한 TCP 헤더가 도착하면 클라이언트측의 프로토콜 스택은 자신의 소켓에 서버측이 연결 끊기 동작에 들어갔다는 것을 기록합니다.
    2. 패킷을 받은 사실을 알리기 위해 서버측에 ACK 번호를 반송
    3. 서버측에서 보낸 데이터를 다 받을 때 까지 기다린 후 close() 호출
    4. 프로토콜 스택이 TCP 헤더를 만들고 FIN 컨트롤 비트를 1로 설정 후 서버에게 보냄
    5. 서버측에서 ACK 번호를 받으면 서버와의 대화가 끝남

     

    3. 🗨️ 웹 서버가 리퀘스트 메시지에 따라 동작


    웹 서버의 경우 read()에서 받은 데이터가 HTTP 리퀘스트 메시지가 됩니다.
    메세지에 기록된 내용을 따라 적절히 조치 후 Response 메시지를 만들고,
    write() 함수를 통해 클라이언트에게 Response 메시지를 전송합니다.

    리퀘스트 메시지는 3가지로 영역으로 구분됩니다.
    1. 리퀘스트 라인
    2. 메시지 헤더
    3. 메시지 본문

    리퀘스트 라인

    리퀘스트 메시지의 첫 번째 행에는 리퀘스트 라인을 쓴다.
    이 행에서 중요한 것은 맨 앞에 있는 메소드입니다.

    리퀘스트 라인 예

    GET  /cgi/sample.cgi?Field1=ABCDEFG&SendButton=SEND  HTTP/1.1

    <메소드><공백><공백><HTTP 버전>

    URL 입력 상자에 URL을 입력하면 해당 페이지를 표시하므로 GET 메소드를 사용, 하이퍼링크를 클릭한 경우에도 GET 메소드를 사용합니다.

    폼의 경우 폼 부분의 HTML 소스 코드에 어느 메소드를 사용하여 리퀘스트를 보낼 것인지를 지정한 후 GET과 POST를 구분하여 사용합니다.

    메소드를 썼으면 한 칸 띄운 다음에 URI를 씁니다. URI 부분에는 다음과 같이 파일이나 프로그램의 경로명을 쓰는 것이 보통입니다.


    메시지 헤더

    첫 번째 행에서 리퀘스트 메시지의 내용을 대략 알 수 있지만, 부가적인 자세한 정보가 필요한 경우도 있는데, 이것을 써 두는 것이 메시지 헤더의 역할입니다.

    날짜, 클라이언트측이 취급하는 데이터의 종류, 언어, 압축 형식, 클라이언트나 서버의 소프트웨어 명칭과 버전, 데이터의 유효 기간이나 최종 변경 일시 등 다수의 항목이 사양으로 정해져 있습니다.

    브라우저 종류나 버전, 설정 등에 따라 메시지 헤더는 달라집니다.

    메시지 헤더 예

    GET /gazou.jpg HTTP/1.1
    Accept: * / *
    Referer : http://www.lab.cyber.co.kr/sample1.htm
    Accept-Language: ja
    Accept-Encoding: gzip, deflate
    User-Agent : Mozilla/4.0 (compatible; [오른쪽 끝 생략]
    Host : www.lab.cyber.co.kr
    Connection: Keep-Alive
    [공백]

    메세지 헤더를 쓰면 그 뒤에 아무 것도 쓰지 않은 하나의 공백 행을 넣고,
    그 뒤에 송신할 데이터를 씁니다.


    메시지 본문

    클라이언트가 서버에게 송신할 데이터를 쓰는 영역

    메소드가 “GET’인 경우에는 메소드와 URI로 웹 서버가 무엇을 할지
    판단이 가능하므로 메시지 본문에 쓰는 송신 데이터는 아무 것도 없습니다.
    메시지가 POST인 경우에는 폼에 입력한 데이터 등을 메시지 본문 부분에 씁니다.

    '네트워크' 카테고리의 다른 글

    쿠키(Cookie)와 세션(Session) 개념  (0) 2022.07.03
    부동 소수점(floating-point)이란?  (0) 2022.04.15
Designed by Tistory.