Posts [CS] Text Browser를 만들어보자
Post
Cancel

[CS] Text Browser를 만들어보자

매일봐서 친하다고 생각했던 HTTP의 새로운 면모

어제 오늘은 HTTP 에 대해서 공부를 했다. 매일보고, 오래봐서 친하다고 생각했던 HTTP 와 더욱 친할 필요가 있고, 앞으로 더더욱 친하게 지낼 수 밖에 없다고 한다.

그런 의미에서 HTTP 가 무엇이고, 그에 따라 browser 가 어떻게 작동하는지 이해하기 위해서 Text Browser 를 만들며 일련의 것들을 알 수 있는데까지 공부하며 이해해 보았다.

언제나 그렇듯 모르는 것에 대해 배울 때, 여러가지 설명을 듣고 보게 되지만 결국에는 내가 나의 표현으로 그것을 설명할 수 있을 때, 정말 안다고 할 수 있는 것 같고, 그 설명이 부족하다면 나의 이해가 그 만큼인 것일 수 있다.

그래서 내가 이해한대로 표현해보자면

screenshot092

1. HTTP(HyperText Transfer Protocol)는 WEB의 여러 나라들을 돌아다닐 때 사용할 여권의 양식이다.


**말그대로 ''엄청난 텍스트'' 녀석들이 '패킷'이라는 짐을 싸들고 여행을 다닐 때 사용하는 약속 같은거다.** 국가들간에 ‘‘너네 나라의 텍스트들이 패킷이라는 짐을 싸들고 우리나라에 오려면 응당 이러이러한 것들은 갖춰서 알려줘야지~’’ 라고, 상호간의 약속을 하고, 패킷들이 여행을 떠날 때, 그 정보를 바탕으로 통신을 하는 것이다. 정확한 설명이 되지 않는걸 알고 있지만 스스로 쉽게 이해하려다보니 이 정도에 머물렀다.

**HTTPS 라는 약속도 있는데 이건 좀 더 깐깐하고 안전한 약속이다.**

얼마전 SSL 에 대해서 공부를 하면서 우리나라 공인인증 방식에 많은 문제가 있음을 깨달았었는데, 그 SSL 방식을 이용한 약속인 것 같다. 이 부분도 분명 차차 공부해야 할 것 같다.

screenshot101

  • httpssecure 가 추가되어 이렇게 자물쇠 모양이 나타나는 것으로 확인할 수 있고,

screenshot102

  • 그렇지 않은 사이트는 Not secure 로 확인할 수 있다.

2. URL 정도는 나도 안다


라는 착각속에 살았다.

image_5621392881479370441006 (1)

URL 분석하기

여기 친절한 그림이 있다. URL 에 대해서 막연히 인터넷 사이트 주소 정도로 생각하고 있었다. 틀린 말은 아니지만 그림처럼 내용 자세히 알지는 못했고, 개념도 막연했다.

너무 그리운 사이판에 사는 handyhttp 규정에 따라 domain 이라는 나라의 path 를 찾아 대한민국 서울시 어쩌구 저쩌구에 살고 있는 raccoon 을 정확히 만나러 올 수 있다. 그리고 parameterfragment 를 이용해서 raccoon 에게 볼 일을 정확히 보고 갈 수 있다. 내가 흔히 알고있던 인터넷 주소는 domain 이었고, 나머지 모든 것을 합해서 url 이라고 부른다.

3. TCP/IP 는 또 뭔가…


개알못을 위한 TCP/IP의 개념

TCP/IP 는 위에서 말한 하이퍼텍스트 녀석들이 여행다닐 때 싸들고 다니는 수하물, 즉 패킷 을 어떻게 정리하고 구분해서 돌려줄건지에 대한 규약이다.

ClinetServerTCP 방식으로 통신할 때, 흥미롭게 발견한 점은 Three way handshake 의 방법을 취한다는 것이다.

또 한번 내방식대로 이해하자면

screenshot103사진출처

  1. 하이퍼텍스트들은 엄청 멋진 레고블럭을 전달할 일이 생겼다.
  2. 그런데 하이퍼텍스트 혼자서 그걸 운반하기에는 너무 크고 위험하며, 그 정도의 블럭은 혼자서 기내에 가지고 탑승할 수도 없다고 한다.
  3. 그래서 이 친구들이 캐리어(패킷)를 하나씩 준비하고, 조심스레 레고블럭을 분리해서 담았다.
  4. 공항에 도착한 하이퍼 텍스트들은 순서대로 줄을서서 수하물을 가지고 기내에 탑승했다.
  5. 내릴 때도 각자의 짐을 가지고 한명씩 한명씩 도착지에 레고 블럭을 재조립한다.
  6. 한 비행기에 못타서 늦게 오는 친구들이 있더라도 레고블럭은 차곡차곡 조립되고 있다.
  7. 그렇게 함으로써 하나의 의미있는 정보가 된다.(레고블럭이 운반됐다!)

EN-tcp

  1. Client : 자 이거먼저! (SYNServer)
  2. Server : 확실함?! 이거줄게 맞나봐봐 (SYN + ACKClient)
  3. Client : 봐봐 맞지? (ACKServer)

이 때, TCP 방식은 이런식으로 신뢰성있게 패킷들을 전달한다. 하지만 이런 방법은 절차가 까다롭기 때문에 속도가 느릴 수 밖에 없다. 그래서 상대적으로 빠르게 패킷을 전달할 수 있는 방식이 있는데 바로 UDP 다.

이 친구들은 일단 패킷들을 전부 위탁수하물로 다 보낸다. 그리고 목적지에 도착해서 알맞게 조립한다. 이렇게 하면 속도가 굉장히 개선되지만 패킷들의 흐름을 제어할 수도 없고, 패킷이 분실될 수도 있다.

4. Text Browser 만들어보기


HTTP_Request_Headers2

이런 일련의 과정들이 어떻게 진행되는지 네트워크 상에서 조금 더 와닿게 이해하기 위해서 Text Browser 를 구현해 보았다. 구현을 위해서는 먼저 HTTP 가 어떻게 작동하는지에 대해 알아야 하는데, 그 일련의 과정들은 여기에 비교적 자세히 나와있다.

  1. 나는 그동안 chrome 이나 safari 같은 브라우저를 이용하고 있었기 때문에 HTTP 메시지를 사용할 이유가 없었다.

  2. Text Browser 란 말 그대로 직접 text 메시지를 이용해서 서버에게 request 하고 response 하는 과정을 거치게 된다.

  3. request 의 방식에도 여러가지가 있지만 이번에는 일단 GET 방식만 사용했다.

  4. Request Header 에는 위의 그림과 같은 요소들이 들어가며, ClientServer 에게 요청하는 일종의 주문서 같은 것이다.

    Client : 알리오 올리오랑 풍기 피자에 콜라 한 잔 주세요~

  5. Server 는 주문서를 확인하고, 그에 맞는 답을 응답코드로 보내준다.

    200(OK) : 주문하신 알리오 올리오, 풍기피자, 콜라 한잔 나왔습니다.

    400(Bad Request) : 저희는 쉐프가 파스타만 하는 집이라서 피자는 주문하실 수 없습니다.

  6. 정상적인 응답이 오면 그 동안 내가 보던 google 이라던가 naver 라는 사이트가 화면에 출력된다. (Text Browser 에서는 그 부분이 HTML 문서로 출력된다.)

5. JS로 Text Browser 구현을 위한 Node Module (dns, net.socket)


항상 비슷한 느낌이지만 node 사용 경험도 적고, 개념도 아직 확실히 없기때문에 무언가를 구현하려고 할 때, 무엇이 필요하고 그것이 어떻게 동작하며, 왜 필요한지를 모를 때가 대부분이다.

  1. dns

    • 도메인 주소를 이용해서 ip 를 얻어올 수 있다.

    • 기본적으로 비동기로 동작한다. (다 구현하고보니 당연한 얘기같다.)

    • 그런데 이 모듈이 return 으로 promise 객체를 뱉는것도 모른채 얼마나 동료들을 묻고 괴롭히고 했는지도 모른다. (비동기에 대한 이해가 부족한데다 여러가지 혼란이 가중되니 완전히 다른곳에서 헤매고 있었던 것 같다. 미안하고 고맙습니다.)

  2. 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 설계


  1. dns 모듈로 domain ip 획득
  2. net.socket 모듈로 socket 생성
  3. 획득한 ipsocket 을 통해 서버와 connect
  4. socket.writerequest msg 보내기
  5. response 받은 dataparsing 하여 출력하기

7. Test Browser 구현


  1. dns 모듈로 domain ip 획득

    1
    
    dns.lookup(this.domain, (err, addresses) => {}
    
  2. net.socket 모듈로 socket 생성

    1
    
    this.socket = new net.Socket()
    
  3. 획득한 ipsocket 을 통해 서버와 connect

    1
    
    this.socket.connect(80, addresses, () => {}
    
    • 기본 포트 80으로 설정

    • 1에서 얻은 addresses = ip 를 그대로 전달

  4. socket.writerequest msg 보내기

    1
    
    this.socket.write(`${this.requestMsg()}`)
    
  5. response 받은 dataparsing 하여 출력하기

    1
    2
    3
    4
    5
    6
    
    this.socket.on('data', (data) => {
      resData += data;
      setTimeout(() => {
        this.socket.end();
      }, 3000);
    });
    
    • data event 가 일어나는 동안 server 가 주는 data 를 계속해서 수신해 오지만 listner 가 없으면 datawill be lost 다.
    • server가 한번에 넘겨줄 수 있는 buffer 가 있는데 그걸 초과하면 data 를 나눠서 여러번 받아온다.
    • 그래서 resDatadata 를 적산해주고, 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;
    }
    
    • 여기에서 정말 오랫동안 막혀있고, 답답했다.

    • 받아온 resDataresponse 나 다음으로 넘겨서 사용하고 싶은데 promiseasync await 을 사용할 줄을 몰랐다. (카페 미션의 여파가 이 정도다… 그것만 성공했더라면 비동기에 한걸음 다가섰을텐데…)

    • 과정은 조만간 비동기에 대해 다시 공부하며 정리를 해보는게 좋을 것 같다.

    • 결국 Promise를 생성해서 부모함수의 return으로 넘겨주고, 사용하는 쪽에서 async를 하고, await 으로 return 한 후, then으로 받아썼다.

      • 그룹과 밤코 멤버들에게 매우 감사하게 생각하고 있다.
  6. Response 출력하기

    • 뭔가 설계가 잘못되었던 것 같다.
    • Response 를 객체를 생성하고 나니 constructor 에서 dataparsing 하고 있었고,
    • Response 객체 method 가 하는 일이라곤 print 해주는 것 뿐이었다.

8. 출력 결과


  1. ip 받아온 후 첫 data 출력 결과

    screenshot093

    • 컴퓨터는 다 숫자로 알아들어요 라는 Neis의 한마디로 정리

    • toString() 으로 문자열로 바꿔줬다.

    screenshot094

  2. Wire Shark로 Request 전송 확인

wireshark

  • GET / HTTP/1,1 로 확인
  1. Response Header 와 Body 분리

screenshot099

  • HeaderBody 의 분리기준을 \r\n\r\n 으로 했다.
  • 사실 마지막 3283 부분이 Header 마지막 라인에 붙어야 된다는걸 알고 있지만
  • 정규식 사용이 어려워서 split\r\n\r\n 으로 했더니 두 줄 떨어져있던 3283 기준으로 나뉘어지고 있다.

screenshot100

  • 사이트마다 조금씩 차이가 있지만 일단 보기좋게 분리는 잘 되고 있다.

disneyRequest

9. 수정 필요한 부분


  • 기본 pathrequest 했을 때, 사이트마다 응답코드가 조금씩 달라서 path를 임의로 수정해야했다.
  • 300번대 응답코드가 오는 사이트들은 redrection 하는 걸 구현해봤으면 좋았을 것 같다.
  • 설계를 조금 더 꼼꼼하게 했어야 할 것 같다. 물론 완전히 개념이 없는 상태에서부터 설계를 하다보니 꼼꼼한 설계가 어렵긴했는데 당장 문제 해결에 급급해서 학습했던 것들을 완전히 내것처럼 사용하지 못하고 있는 것 같다.

참조

This post is licensed under CC BY 4.0 by the author.

[CS] Canvas api를 이용한 Chart 그리기

[thinkAbout] 새벽감성으로 생각정리하기

Comments powered by Disqus.