매일봐서 친하다고 생각했던 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.EventEmitteremitter도 상속하고 있다.
6. Text Browser 설계
dns모듈로domainip획득net.socket모듈로socket생성- 획득한
ip로socket을 통해 서버와connect socket.write로request msg보내기response받은data를parsing하여 출력하기
7. Test Browser 구현
dns모듈로domainip획득1
dns.lookup(this.domain, (err, addresses) => {}
net.socket모듈로socket생성1
this.socket = new net.Socket()
획득한
ip로socket을 통해 서버와connect1
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); });
dataevent가 일어나는 동안server가 주는data를 계속해서 수신해 오지만listner가 없으면data는will be lost다.server가 한번에 넘겨줄 수 있는buffer가 있는데 그걸 초과하면data를 나눠서 여러번 받아온다.- 그래서
resData에data를 적산해주고, 3초 후에socket통신이 끊기도록endevent를 준비해주었다.
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


