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=141setTimeout(..., 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
이 파트에 대해 더 자세히 알고싶다면?
현대 웹 서비스에서는 클라이언트가 먼저 소켓을 끊는다는다길래
단순하게 ‘그럼 서버로 먼저 끊어볼까?‘라는 생각으로 몇일동안 여러가지를 학습하게 되었다.