매일봐서 친하다고 생각했던 HTTP의 새로운 면모
어제 오늘은 HTTP
에 대해서 공부를 했다. 매일보고, 오래봐서 친하다고 생각했던 HTTP
와 더욱 친할 필요가 있고, 앞으로 더더욱 친하게 지낼 수 밖에 없다고 한다.
그런 의미에서 HTTP
가 무엇이고, 그에 따라 browser
가 어떻게 작동하는지 이해하기 위해서 Text Browser
를 만들며 일련의 것들을 알 수 있는데까지 공부하며 이해해 보았다.
언제나 그렇듯 모르는 것에 대해 배울 때, 여러가지 설명을 듣고 보게 되지만 결국에는 내가 나의 표현으로 그것을 설명할 수 있을 때, 정말 안다고 할 수 있는 것 같고, 그 설명이 부족하다면 나의 이해가 그 만큼인 것일 수 있다.
그래서 내가 이해한대로 표현해보자면
1. HTTP(HyperText Transfer Protocol)는 WEB의 여러 나라들을 돌아다닐 때 사용할 여권의 양식이다.
**말그대로 ''엄청난 텍스트'' 녀석들이 '패킷'이라는 짐을 싸들고 여행을 다닐 때 사용하는 약속 같은거다.** 국가들간에 ‘‘너네 나라의 텍스트들이 패킷이라는 짐을 싸들고 우리나라에 오려면 응당 이러이러한 것들은 갖춰서 알려줘야지~’’ 라고, 상호간의 약속을 하고, 패킷들이 여행을 떠날 때, 그 정보를 바탕으로 통신을 하는 것이다. 정확한 설명이 되지 않는걸 알고 있지만 스스로 쉽게 이해하려다보니 이 정도에 머물렀다.
**HTTPS 라는 약속도 있는데 이건 좀 더 깐깐하고 안전한 약속이다.**
얼마전 SSL 에 대해서 공부를 하면서 우리나라 공인인증 방식에 많은 문제가 있음을 깨달았었는데, 그 SSL 방식을 이용한 약속인 것 같다. 이 부분도 분명 차차 공부해야 할 것 같다.
https
는secure
가 추가되어 이렇게 자물쇠 모양이 나타나는 것으로 확인할 수 있고,
- 그렇지 않은 사이트는
Not secure
로 확인할 수 있다.
2. URL 정도는 나도 안다
라는 착각속에 살았다.
여기 친절한 그림이 있다. URL
에 대해서 막연히 인터넷 사이트 주소
정도로 생각하고 있었다. 틀린 말은 아니지만 그림처럼 내용 자세히 알지는 못했고, 개념도 막연했다.
너무 그리운 사이판에 사는 handy
는 http
규정에 따라 domain
이라는 나라의 path
를 찾아 대한민국 서울시 어쩌구 저쩌구에 살고 있는 raccoon
을 정확히 만나러 올 수 있다. 그리고 parameter
와 fragment
를 이용해서 raccoon
에게 볼 일을 정확히 보고 갈 수 있다. 내가 흔히 알고있던 인터넷 주소는 domain
이었고, 나머지 모든 것을 합해서 url
이라고 부른다.
3. TCP/IP 는 또 뭔가…
TCP/IP
는 위에서 말한 하이퍼텍스트 녀석들이 여행다닐 때 싸들고 다니는 수하물, 즉 패킷
을 어떻게 정리하고 구분해서 돌려줄건지에 대한 규약이다.
Clinet
와 Server
가 TCP
방식으로 통신할 때, 흥미롭게 발견한 점은 Three way handshake 의 방법을 취한다는 것이다.
또 한번 내방식대로 이해하자면
- 하이퍼텍스트들은 엄청 멋진 레고블럭을 전달할 일이 생겼다.
- 그런데 하이퍼텍스트 혼자서 그걸 운반하기에는 너무 크고 위험하며, 그 정도의 블럭은 혼자서 기내에 가지고 탑승할 수도 없다고 한다.
- 그래서 이 친구들이 캐리어(
패킷
)를 하나씩 준비하고, 조심스레 레고블럭을 분리해서 담았다. - 공항에 도착한 하이퍼 텍스트들은 순서대로 줄을서서 수하물을 가지고 기내에 탑승했다.
- 내릴 때도 각자의 짐을 가지고 한명씩 한명씩 도착지에 레고 블럭을 재조립한다.
- 한 비행기에 못타서 늦게 오는 친구들이 있더라도 레고블럭은 차곡차곡 조립되고 있다.
- 그렇게 함으로써 하나의 의미있는 정보가 된다.(레고블럭이 운반됐다!)
Client
: 자 이거먼저! (SYN
→Server
)Server
: 확실함?! 이거줄게 맞나봐봐 (SYN + ACK
→Client
)Client
: 봐봐 맞지? (ACK
→Server
)
이 때, TCP 방식은 이런식으로 신뢰성있게 패킷들을 전달한다. 하지만 이런 방법은 절차가 까다롭기 때문에 속도가 느릴 수 밖에 없다. 그래서 상대적으로 빠르게 패킷을 전달할 수 있는 방식이 있는데 바로 UDP
다.
이 친구들은 일단 패킷들을 전부 위탁수하물로 다 보낸다. 그리고 목적지에 도착해서 알맞게 조립한다. 이렇게 하면 속도가 굉장히 개선되지만 패킷들의 흐름을 제어할 수도 없고, 패킷이 분실될 수도 있다.
4. Text Browser 만들어보기
이런 일련의 과정들이 어떻게 진행되는지 네트워크 상에서 조금 더 와닿게 이해하기 위해서 Text Browser
를 구현해 보았다. 구현을 위해서는 먼저 HTTP
가 어떻게 작동하는지에 대해 알아야 하는데, 그 일련의 과정들은 여기에 비교적 자세히 나와있다.
나는 그동안
chrome
이나safari
같은 브라우저를 이용하고 있었기 때문에HTTP
메시지를 사용할 이유가 없었다.Text Browser
란 말 그대로 직접text
메시지를 이용해서 서버에게request
하고response
하는 과정을 거치게 된다.request
의 방식에도 여러가지가 있지만 이번에는 일단GET
방식만 사용했다.Request Header
에는 위의 그림과 같은 요소들이 들어가며,Client
가Server
에게 요청하는 일종의 주문서 같은 것이다.Client : 알리오 올리오랑 풍기 피자에 콜라 한 잔 주세요~
Server
는 주문서를 확인하고, 그에 맞는 답을 응답코드로 보내준다.200(OK) : 주문하신 알리오 올리오, 풍기피자, 콜라 한잔 나왔습니다.
400(Bad Request) : 저희는 쉐프가 파스타만 하는 집이라서 피자는 주문하실 수 없습니다.
정상적인 응답이 오면 그 동안 내가 보던
google
이라던가naver
라는 사이트가 화면에 출력된다. (Text Browser 에서는 그 부분이 HTML 문서로 출력된다.)
5. JS로 Text Browser 구현을 위한 Node Module (dns, net.socket)
항상 비슷한 느낌이지만 node
사용 경험도 적고, 개념도 아직 확실히 없기때문에 무언가를 구현하려고 할 때, 무엇이 필요하고 그것이 어떻게 동작하며, 왜 필요한지를 모를 때가 대부분이다.
dns
도메인 주소를 이용해서
ip
를 얻어올 수 있다.기본적으로 비동기로 동작한다. (다 구현하고보니 당연한 얘기같다.)
그런데 이 모듈이
return
으로promise
객체를 뱉는것도 모른채 얼마나 동료들을 묻고 괴롭히고 했는지도 모른다. (비동기에 대한 이해가 부족한데다 여러가지 혼란이 가중되니 완전히 다른곳에서 헤매고 있었던 것 같다. 미안하고 고맙습니다.)
net.socket
browser
구현을 위해서socket
방식을 사용했는데, 네트워크 소켓 이해에 의하면 아래와 같은 것이socket
이다.소켓은 IP 주소와 포트의 조합으로 구성된 소켓 주소를 사용해 동작한다. 소켓 연결은 두 가지 형태(서버, 클라이언트)로 존재한다. 서버는 연결을 수신하고, 클라이언트는 서버에 연결을 진행한다.
Node.js의 net 모듈 소켓은 전송 제어 프로토콜(TCP)을 사용해 원시 데이터를 전송한다. Node.js 소켓은 서버와 클라이언트 간 읽기, 쓰기 스트림 데이터 지원을 위한 Duplex 스트림도 지원한다.
개념이 막연하고 어려워서 세계관을 한단계 넓혀서 우주에서 행성간에 통신을 할 때,
socket
이라는rocket
에 패킷을 담아 떠난다고 생각했다.그래서
net.socket
모듈은HTTP message
를 전송할 수 있는socket
객체를 제공하고,connection
,on
,write
와 같은 기특한 메서드를 제공한다.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
class Socket extends stream.Duplex { constructor(options?: SocketConstructorOpts); // Extended base methods write(buffer: Uint8Array | string, cb?: (err?: Error) => void): boolean; write(str: Uint8Array | string, encoding?: BufferEncoding, cb?: (err?: Error) => void): boolean; connect(options: SocketConnectOpts, connectionListener?: () => void): this; connect(port: number, host: string, connectionListener?: () => void): this; connect(port: number, connectionListener?: () => void): this; connect(path: string, connectionListener?: () => void): this; setEncoding(encoding?: BufferEncoding): this; pause(): this; resume(): this; setTimeout(timeout: number, callback?: () => void): this; setNoDelay(noDelay?: boolean): this; setKeepAlive(enable?: boolean, initialDelay?: number): this; address(): AddressInfo | {}; unref(): this; ref(): this;
위 인용에서 언급된
stream.Duplex
를 상속하고 있는걸 확인할 수 있고,1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
// Extended base methods write(buffer: Uint8Array | string, cb?: (err?: Error) => void): boolean; write(str: Uint8Array | string, encoding?: BufferEncoding, cb?: (err?: Error) => void): boolean; connect(options: SocketConnectOpts, connectionListener?: () => void): this; connect(port: number, host: string, connectionListener?: () => void): this; connect(port: number, connectionListener?: () => void): this; connect(path: string, connectionListener?: () => void): this; setEncoding(encoding?: BufferEncoding): this; pause(): this; resume(): this; setTimeout(timeout: number, callback?: () => void): this; setNoDelay(noDelay?: boolean): this; setKeepAlive(enable?: boolean, initialDelay?: number): this; address(): AddressInfo | {}; unref(): this; ref(): this;
내부 추가 메서드로
write
,connect
,address
등이 구현되어 있다. 앞으로 공식문서와 함께 조금씩 읽을 줄 알면 모듈 사용방법에 금방 적응할 수 있을 것 같아서 틈틈히 보는 중이다.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
* events.EventEmitter * 1. close * 2. connection * 3. error * 4. listening */ addListener(event: string, listener: (...args: any[]) => void): this; addListener(event: "close", listener: () => void): this; addListener(event: "connection", listener: (socket: Socket) => void): this; addListener(event: "error", listener: (err: Error) => void): this; addListener(event: "listening", listener: () => void): this; emit(event: string | symbol, ...args: any[]): boolean; emit(event: "close"): boolean; emit(event: "connection", socket: Socket): boolean; emit(event: "error", err: Error): boolean; emit(event: "listening"): boolean; on(event: string, listener: (...args: any[]) => void): this; on(event: "close", listener: () => void): this; on(event: "connection", listener: (socket: Socket) => void): this; on(event: "error", listener: (err: Error) => void): this; on(event: "listening", listener: () => void): this;
이렇게
class Server extends events.EventEmitter
emitter
도 상속하고 있다.
6. Text Browser 설계
dns
모듈로domain
ip
획득net.socket
모듈로socket
생성- 획득한
ip
로socket
을 통해 서버와connect
socket.write
로request msg
보내기response
받은data
를parsing
하여 출력하기
7. Test Browser 구현
dns
모듈로domain
ip
획득1
dns.lookup(this.domain, (err, addresses) => {}
net.socket
모듈로socket
생성1
this.socket = new net.Socket()
획득한
ip
로socket
을 통해 서버와connect
1
this.socket.connect(80, addresses, () => {}
기본 포트 80으로 설정
1에서 얻은
addresses
=ip
를 그대로 전달
socket.write
로request msg
보내기1
this.socket.write(`${this.requestMsg()}`)
response
받은data
를parsing
하여 출력하기1 2 3 4 5 6
this.socket.on('data', (data) => { resData += data; setTimeout(() => { this.socket.end(); }, 3000); });
data
event
가 일어나는 동안server
가 주는data
를 계속해서 수신해 오지만listner
가 없으면data
는will be lost
다.server
가 한번에 넘겨줄 수 있는buffer
가 있는데 그걸 초과하면data
를 나눠서 여러번 받아온다.- 그래서
resData
에data
를 적산해주고, 3초 후에socket
통신이 끊기도록end
event
를 준비해주었다.
1 2 3 4 5 6 7
let dataPromise = new Promise((resolve, reject) => { this.socket.on('end', () => { resolve(resData.toString()); }); }); return dataPromise; }
여기에서 정말 오랫동안 막혀있고, 답답했다.
받아온
resData
를response
나 다음으로 넘겨서 사용하고 싶은데promise
나async await
을 사용할 줄을 몰랐다. (카페 미션의 여파가 이 정도다… 그것만 성공했더라면 비동기에 한걸음 다가섰을텐데…)과정은 조만간 비동기에 대해 다시 공부하며 정리를 해보는게 좋을 것 같다.
결국
Promise
를 생성해서 부모함수의return
으로 넘겨주고, 사용하는 쪽에서async
를 하고,await
으로return
한 후,then
으로 받아썼다.- 그룹과 밤코 멤버들에게 매우 감사하게 생각하고 있다.
Response
출력하기- 뭔가 설계가 잘못되었던 것 같다.
Response
를 객체를 생성하고 나니constructor
에서data
를parsing
하고 있었고,Response
객체method
가 하는 일이라곤print
해주는 것 뿐이었다.
8. 출력 결과
ip
받아온 후 첫data
출력 결과컴퓨터는 다 숫자로 알아들어요 라는
Neis
의 한마디로 정리toString()
으로 문자열로 바꿔줬다.
Wire Shark로 Request 전송 확인
GET / HTTP/1,1
로 확인
- Response Header 와 Body 분리
Header
와Body
의 분리기준을\r\n\r\n
으로 했다.- 사실 마지막 3283 부분이 Header 마지막 라인에 붙어야 된다는걸 알고 있지만
- 정규식 사용이 어려워서
split
을\r\n\r\n
으로 했더니 두 줄 떨어져있던 3283 기준으로 나뉘어지고 있다.
- 사이트마다 조금씩 차이가 있지만 일단 보기좋게 분리는 잘 되고 있다.
9. 수정 필요한 부분
- 기본
path
로request
했을 때, 사이트마다 응답코드가 조금씩 달라서path
를 임의로 수정해야했다. - 300번대 응답코드가 오는 사이트들은
redrection
하는 걸 구현해봤으면 좋았을 것 같다. - 설계를 조금 더 꼼꼼하게 했어야 할 것 같다. 물론 완전히 개념이 없는 상태에서부터 설계를 하다보니 꼼꼼한 설계가 어렵긴했는데 당장 문제 해결에 급급해서 학습했던 것들을 완전히 내것처럼 사용하지 못하고 있는 것 같다.
참조
- 개알못을 위한 TCP/IP의 개념
- URL 분석하기
- 요청, 응답, 서버 객체 이해
- TCP 소켓 서버와 클라이언트 구현
- Class: net.Socket
- 도메인 ip 여러개 (feat. Eamon)
- 자바스크립트 Promise 쉽게 이해하기
- javascript, async/await 왜 pending 상태인지 이해가 안갑니다..!
- HTTP의 Content-Length 필드가 없는경우
[HTTP headers Content-Length](https://www.geeksforgeeks.org/http-headers-content-length/) - HTTP message body
- Hypertext Transfer Protocol
- The OPTIONS method on the request/response chain identifed by the Request-URI