August 29, 2021
도전 목표는 간단하다 HTTP/1.1
환경 express.js
서버가 먼저 소켓 연결을 끊어보는 것이다.
그리고 실제 packet
을 확인해보겠다.
먼저 가장 기본이 되는 지식인 TCP 연결종료에 대해 간단히 설명하겠다.
Active Close
가 Passive Close
측에게 ‘연결 종료’를 의미하는 FIN
패킷 전송Passive Close
는 ‘연결 종료’에 대한 ACK
응답Passive Close
는 ‘연결 종료’를 의미하는 FIN
전송Active Close
는 ‘연결 종료’에 대한 ACK
응답참고로 일반적인 웹 클라이언트-서버 환경에서는 클라이언트가 Active Close
이다
일반적인 종료과정이 4번의 데이터 송수신을 거쳐 진행되기 때문에 4-way-handshake
라고 불리며 이것이 Graceful Close
이다
어떻게 동작하는지, 더 자세히 알고싶다면?
사실 TCP 연결 요청은 무조건 클라이언트가 먼저하지만, 연결 종료는 서버/클라이언트 어디서든 먼저 가능하다.
그런데 보통 Socket을 먼저 close()
하는것은 클라이언트이다.
더 자세히 말하면
3 way handshake
과정을 거쳐 소켓 연결.HTTP Request
. 서버는 HTTP Response
.
close()
한다
위 과정은 보통 프레임워크나, 라이브러리가 담당해서 눈치채기 힘들다.
TCP/IP
는 보통 OS에 구현되어 있다그럼 어떻게 Socket
을 유지할 수 있을까? TCP와 HTTP의 keepalive
의 특징 덕분이다
Socket
을 유지해서 데이터를 계속 주고받을 수 있게 하는 특징이다.HTTP/1.1
이후로는 커넥션 유지가 가능하다Q.혹시 클라이언트가 Request 헤더에 Connection: close
를 넣는다면?
Connection
헤더와 상관없이 Socket
을 유지할 것이다.Q. 혹시 서버가 Response 헤더에 Connection: close
를 넣는다면?
Connection 헤더에 대해 더 자세히 알고싶다면?
이제 실제 사용 가능한 방법을 알아보자
res.end(), res.send(), res.json()
소켓을 유지하며 응답을 보내는 경우. 즉 원하는 경우가 아니다
res.socket.end()
res.socket.destroy()
2번과 3번 모두 FIN
패킷을 보내며 연결을 끊는건데 어떤 차이점이 있을까?
Half-close
가 무엇일까?
말그래도 절반만 종료한다는 아래의 경우를 의미한다.
res.socket.end()
경우는
FIN
패킷을 보내며 “연결을 종료할것인데, 혹시 더 보낼게 있다면 보내줘”Passive Close
측은 데이터를 다 처리한 후 FIN
패킷을 Active Close
측에 전송할 것이다.다만 이 글에선 해당 특징에 대해 다루지 않는다.
먼저 Repository를 소개한다.
FROM ubuntu:18.04
# install nodejs
RUN apt-get -qq update
RUN apt-get -qq upgrade --yes
RUN apt-get -qq install curl --yes
RUN curl -sL https://deb.nodesource.com/setup_14.x | bash -
RUN apt-get -qq install nodejs --yes
# istall tools
# RUN apt-get -qq install net-tools --yes
RUN apt-get install tshark --yes
WORKDIR /app
COPY ./package.json ./
RUN npm install
COPY . .
패킷을 확인해야 하기 때문에, ubuntu
이미지를 사용했다.
tshark
를 install한다.version: '3'
services:
server:
build: .
container_name: server_app
volumes:
- .:/usr/app/
- /usr/app/node_modules
command: npm run server
client:
build: .
container_name: client_app
volumes:
- .:/usr/app/
- /usr/app/node_modules
command: npm run client
두 컨테이너간 통신을 위해 docker-compose
를 이용했다.
const httpAgent = new http.Agent({ keepAlive: true })
await axios.get('http://server:80/', { httpAgent })
간단하게 axios를 이용해서 요청하는 구조
// 1번 방법
app.get('/', async (req, res) => {
res.send('bye')
})
// 2번 방법
app.get('/socket-close', async (req, res) => {
res.socket.end(
[
'HTTP/1.1 200 OK',
'Content-Type: text/plain; charset=utf-8',
'Content-Length: 4',
`Date: ${new Date().toGMTString()}`,
'Connection: keep-alive',
'Keep-Alive: timeout=5',
'',
'bye!',
].join('\n'),
)
})
// 3번 방법
let socket = null
app.get('/socket-destroy', async (req, res) => {
res.send('bye')
socket = res.socket
setTimeout(() => {
console.log('destroyed: ', socket.destroyed)
if (!socket.destroyed) socket.destroy()
}, 1500)
})
2번은 socket.end()
시 메세지를 함께 보낼 수 있어서, String
으로 HTTP Response
를 만들어보았다.
3번은 HTTP Response
1초 이후, socket.destroy()
를 진행한다.
keep-alive
을 준수하기에 응답 이후 socket
연결을 양측 모두 끊지 않는다.HTTP와 TCP/IP 차이에 대해 더 자세히 알고싶다면?
> docker-compose up --build
> docker exec -it server_app /bin/bash # 서버 컨테이너 bash 접속
> tshark -i eth0 # 패킷 확인
클라이언트에 노란색을 표시했다
...
Client > Server [FIN, ACK] Seq=126 Ack=230
Server > Client [FIN, ACK] Seq=230 Ack=127
Client > Server [ACK] Seq=127 Ack=231
간략히 요약하면 위와 같다
3-way-handshake
과정 (앞 3줄) 이후 Data Transfer
과정을 거치고
FIN
)를 요청하는것을 확인할 수 있다.
...
Server > Client [FIN, ACK] Seq=161 Ack=126
Client > Server [FIN, ACK] Seq=126 Ack=162
Server > Client [ACK] Seq=162 Ack=127
...
Client > Server [TCP Keep-Alive] [ACK] Seq=139 Ack=230
Server > Client [TCP Keep-Alive ACK] [ACK] Seq=230 Ack=140
Server > Client [FIN, ACK] Seq=230 Ack=140
Client > Server [FIN, ACK] Seq=140 Ack=231
Server > Client [ACK] Seq=231 Ack=141
setTimeout(..., 1500)
동안 소켓을 유지하고 있기 때문에 keep-alive
패킷도 확인 할 수 있다.
위 패킷 내용에 대해 더 자세히 학습하고자 한다면?
해당 내용에 대하여, 좀 더 명확한 정보를 찾아 내용 추가한다
또 좀 더 자료를 찾은 후에 이 파트만을 다루는 글을 작성할 예정이다.
답은 간단하다. TCP/IP가 효율을 추구하기 때문이다.
일단 첫번째 단계인 #1 FIN
플래그 전송부터 다시 살펴보자.
#1 FIN
플래그를 전송하는 경우#1 FIN
플래그 + 이전에 전송한 ACK
을 함께 전송하는 경우2번의 경우, 혹시 이전에 전송한 ACK
이 분실되었을때를 대비할 수 있다
ACK
을 위한 공간은 TCP 헤더부분에 존재하기 때문에 ‘무료’로 함께 보낼 수 있다.ACK (1 bit): 클라이언트가 보낸 최초의 SYN 패킷 이후에 전송되는 모든 패킷은 이 플래그가 설정되어 있어야 한다. - Wikipidia
그럼 이제 #2 ACK
, #3 FIN
을 살펴보자. 왜 두 패킷이 함께 전송될까?
호스트 A가
FIN
을 보내고 호스트 B가FIN & ACK
(두 단계를 하나로 합치기)로 응답하고, 호스트 A가ACK
로 응답할 때 3-way handshake로 연결을 종료하는 것도 가능합니다 - Wikipidia
이 파트에 대해 더 자세히 알고싶다면?
현대 웹 서비스에서는 클라이언트가 먼저 소켓을 끊는다는다길래
단순하게 ‘그럼 서버로 먼저 끊어볼까?‘라는 생각으로 몇일동안 여러가지를 학습하게 되었다.