네이버 부스트캠프에서 웹 서버 구현 미션을 진행하면서, HTTP 요청의 순서와 응답의 순서가 반드시 일치하도록 만들어야 하는지 의문이 들었다. 이를 위해 조사한 내용이 본문에 정리되어 있다.
다음 이미지는 설계한 웹 서버 시스템 구조도이다.

HTTP Socket은 Event Emitter로 구현되어 있으며, HTTP 요청 객체를 이벤트로 받는다.
export class Server {
private _server: net.Server
private router: Router
constructor() {
this._server = net.createServer()
this._server.on('connection', (socket) => {
this.serverConnectionHandler(socket)
})
this.router = new Router()
}
private serverConnectionHandler(socket: net.Socket) {
const httpSocket = new HTTPSocket(socket, this.router)
const httpRequestParser = new HTTPRequestParser()
socket.on('data', this.socketDataHandler(httpSocket, httpRequestParser))
}
private socketDataHandler(httpSocket: HTTPSocket, httpRequestParser: HTTPRequestParser) {
return (data: Buffer) => {
const requests = httpRequestParser.getCompleteRequests(data)
for (const req of requests) {
httpSocket.emit('request', req)
}
}
}
// ...
}
동일한 HTTP 소켓으로 요청1과 요청2를 보냈는데, 응답1이 응답2보다 늦게 생성되는 경우를 가정해보자. 예를 들어, 요청1은 처리되기까지 5초가 걸리고 요청2는 1초가 걸리는 상황이다.
이때, 응답2를 응답1보다 먼저 소켓에 write해도 될까?
응답2를 먼저 보낼 수 있다면 전체적인 Response Time을 낮출 수 있겠지만, 브라우저는 동일한 소켓에서 온 응답의 순서가 요청과 달라지는 경우를 처리할 수 있을지 확인이 필요하다.
https://developer.mozilla.org/en-US/docs/Web/HTTP/Connection_management_in_HTTP_1.x#http_pipelining

이전 요청의 응답을 기다리지 않고 다음 요청을 곧바로 보낼 수 있게 하는 기술이다. 다만, 이전 요청의 응답이 늦어지면 다음 요청도 늦어지는 Head Of Line Blocking 문제와 HTTP Pipelining을 제대로 처리하지 못하는 buggy proxy 문제 때문에, modern browser는 pipelining을 사용하지 않는다.
브라우저는 대신 동일한 도메인에 한해 TCP 소켓을 최대 6개(브라우저마다 다름) 열어 요청을 병렬로 보내는 방법으로 response time을 줄인다. 브라우저의 최대 소켓 수 제한을 우회하려면 Domain Sharding을 사용할 수 있다.
Domain Sharding에 대한 간단 설명
example.com도메인에 대해서는 기본적으로 6개까지 병렬 요청이 가능함.aaa.example.com,bbb.example.com을 모두example.com으로 프록시되도록 조치하면, 브라우저에서는aaa.example.com으로 최대 6개,bbb.example.com으로 최대 6개를 보낼 수 있으니, 이론상 최대 12개의 병렬 요청을 보낼 수 있게 됨.
위 조사 결과를 바탕으로, HTTP/1.1의 경우에 HTTP Pipelining 사용 여부와 상관 없이, 서버는 동일한 소켓에 도착한 요청 순서대로 응답을 보내야 한다는 사실을 알 수 있다.

HTTP/2에서는 Frame, Message, Stream이라는 개념이 존재한다.
하나의 HTTP 요청, 응답이 각각 하나의 Message가 된다. 이건 HTTP/1.1과 동일하다.
Frame은 Header frame, Data frame을 구분하는 최소 단위가 된다. Frame이 header인지 body인지는 Frame 내부 헤더로 파악할 수 있다. Frame의 길이도 헤더에 나와있다.
Stream은 HTTP 요청과 요청에 대한 응답을 의미한다. 모든 Frame에는 Stream ID가 포함되어 있기 때문에 Frame의 스트림을 구분할 수 있다.
즉, 응답2를 응답1보다 먼저 보내도 수신 측에서 응답2의 stream ID로 요청2에 대한 응답임을 알 수 있다.
HTTP/1.1을 사용하면 같은 소켓에 도착한 요청의 순서에 따라 응답을 보내야 한다.
HTTP/2를 사용하면 같은 소켓에 도착한 요청의 순서에 상관 없이 응답을 보낼 수 있다.
HTTP/2의 다른 특징은 여기에 정리해두었다.