본문 바로가기
시작/TIL(Today I Learned)

221213 - HTTP 통신 / Request, Response Message

by 백씨네 2022. 12. 14.

오늘 내가 배운 것

1.HTTP 통신

 - Request message 구현

 - Response message 구현


HTTP 통신하기

통신을 하기 위해서 서버(server)랑 클라이언트(client)가 있어야 한다.
HTTP통신에서는 브라우저가 클라이언트의 역할을 한다.
오늘은 HTTP통신을 하는 과정이기 때문에 클라이언트 대신 브라우저라고 한다.

 

기본적으로 통신을 위해서는 서버와 브라우저의 연결이 필요하기 때문에 3 way Handshake를 이용해서 연결이 되고, 데이터를 요청 후 응답이 오면 통신을 끊기 위해 4 way Handshake로 연결이 끊는데 이 부분은 이전 글에서 했으니 이전 글을 참고하기 바랍니다.

https://baekspace.tistory.com/82

 

221208 - TCP/UDP, TCP서버

오늘 내가 배운 것 1. TCP/UDP 2. TCP 통신하기 TCP(Transmission Control Protocol) 데이터를 중요하게 생각하여 확실히 주고받고 싶을 때는 `TCP`를 사용한다. TCP는 통신할 컴퓨터끼리 ‘보냈습니다’, ‘도착

baekspace.tistory.com

  1. 3way Handshake로 연결
  2. 브라우저가 화면에 HTML을 열기 위해서 서버에 request message를 보내고
  3. request messaage를 받은 서버가 message를 보고 response message를 브라우저에게 보낸다.
  4. 브라우저는 response message를 이용하여 받은 HTML 파일을 읽고 화면에 출력시켜준다.
  5. 응답을 받은 뒤 4 way Handshake로 연결 해제

클라이언트가 브라우저이기 때문에 우리가 컨트롤할 곳은 서버 영역이다.
서버 영역에서 요청 메시지를 받아서 그 요청 메시지를 이용하여 원하는 값을 주기 위해 요청 메시지에서 원하는 데이터를 추출해서 쓸 수 있어야 한다.
그리고 응답을 할 때에도 (위에 경우는 HTML 파일 불러오기) 요청 메시지에 온 내용을 파악하고 원하는 값을 데이터로 담아 같이 보낸다.

 

server.js

const net = require('net')
const PORT = process.env.SERVER_PORT || 3000 
const HOST = process.env.SERVER_HOST || '127.0.0.1'

const server = net.createServer((client)=>{
    client.setEncoding('utf-8')

    client.on('data', (chunk)=>{
        console.log(chunk)
    })
})

server.on('connection', ()=>{
    console.log('connected to client')
})

server.listen(PORT, HOST, ()=>{
    console.log("server start")
})

서버를 열고 확인하기
지금 상태에서 client가 data를 보내주면 console.log(chunk)로 콘솔에 요청 내용이 찍힌다.

res.js

//template.js를 불러오는 내용
const readFile = require('./template')

const message = (content)=>{
    const body = Buffer.from(content)

    return `HTTP/1.1 200 OK
Connection : Close
Content-Type : text/html; charset=UTF-8
Content-Length : ${body.length}

${body.toString()}`
}

module.exports = (socket) =>{
    return {
        send:(body)=>{
            const response = message(body)
            socket.write(response)
        },
        sendFile:(filename) =>{
            const body = readFile(filename)
            const response = message(body)
            socket.write(response)
        }
    }
}

모듈로 뺀 값을 server에서 다시 불러오기 위해 server.js에 내용을 추가해준다.

//모듈로 뺀 값을 server에서 다시 불러오기
const resFn = require('./lib/res')
//client.on 안에 res 넣어주기
client.on('data', (chunk)=>{
    const res = resFn(client)
})

res.js 파일의 역할은 HTTP..즉 Protocol이기 때문에 정해진 약속이 있는데, 그 약속에 맞춰서 응답 메시지를 보내기 위해 만들어진 파일이다.
그 약속에 맞춰서 응답을 해줘야 클라이언트에서 정확하게 받고 안에 body의 데이터를 사용할 수 있다.

send 함수를 이용해서 HTML내용을 바로 보내는 방법도 있지만, sendFile로 파일내용을 전달하는 방법이 있다.
sendFile로 파일 내용을 보낼 때, filename이라는 변수를 넣은 내용이 있는데 이 변수에는 원하는 데이터가 있는 파일의 경로를 포함한 변수이다.


이 변수는 template.js파일에서 선언하였다.

template.js

컴퓨터 내에 있는 파일을 조작하기 위해 Node의 내장 모듈인 fs를 이용한다.

const fs = require('fs')
const path = require('path')

module.exports = (filename, defaultDir='../views')=>{
    const target = path.join(__dirname, defaultDir, filename)
    const readline = fs.readFileSync(target, 'utf8')
    return readline
}

target을 보면 __dirname에 defaultDir을 붙이면 현재 위치해 있는 경로에서 ../views이기 때문에 template위치에서 폴더를 뒤로 가서 views 폴더로 넘어갈 수 있게 된다. 그래서 views폴더 내에 filename 변수에 해당하는 파일을 불러올 수 있다.
readFileSync를 이용하여 동기 방식으로 파일을 읽는다.

req.js

응답 메시지를 위한 틀을 만들어서 원하는 파일에 대한 내용도 보내주는 방법을 알아봤다.
이제는 응답을 보내기 전에 요청을 받는 과정이 있는데 그 요청 메시지는 브라우저가 보내준다.
그래서 그 요청 메시지를 활용해서 브라우저가 원하는 내용을 보내 줄 수 있게 한다.

//원하는 데이터를 쉽게 분해하여 확인하기 위해 브라우저에서 요청한 데이터를 가져와서 test 를 진행한다. 실제 요청을 보내고 응답하는 코드에는 관련이 없다.
const meg = `GET /user?name=baek&age=29 HTTP/1.1
Host: 127.0.0.1:3000
Connection: keep-alive
Cache-Control: max-age=0
sec-ch-ua: "Google Chrome";v="107", "Chromium";v="107", "Not=A?Brand";v="24"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "macOS"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7

{
    "name":"baek",
    "age":29
}`
//위에 내용은 지워도 작동하는데 이상이 없다. 서버를 껐다 켰다 하면서 작업하기 불편해서 테스트코드를 만들어서 작업을 했기 때문에 그냥 적어뒀다.

//#2.
const getQuery = (querySting)=>{
    console.log(querySting)
    if(querySting === undefined) return null
    return querySting.split("&").map(v=>v.split("=")).reduce((acc, value)=>{
        const [key, val] = value
        acc[key] = val
        return acc
    },{})
}
//#4.
const bodyParser = (body, contentType) => {
    if(contentType === undefined) return null
    if(contentType.indexOf('application/json')!== -1) return JSON.parse(body)
    if(contentType.indexOf('application/x-www-form-urlencoded')!== -1 )return getQuery(body)

    return body
}
//#3.
const getMessage = (message) =>{
    let flag = false
    let body = ""
    for(const key in message){
        if(flag) body = message.splice(key).map(v=>v.trim()).join("")
        if(message[key]==="") flag = true
    }
    message.pop()

    const headers = message.map(v=>v.split(':')).reduce((acc, value)=>{
        const [key, val] = value
        acc[key] = val
        return acc 
    },{}) 
    //#4.로 이동해서 작성
    body = bodyParser(body, headers['Content-Type'])
    return [headers, body]
}
//#1.
const parser = (message) => {
    //1-1 불러온 메세지는 String이고 이를 1줄씩 분리해서 배열로 바꿔준다.
    const header = message.split(`\n`) 
    //1-2 header의 맨 윗줄(startline)을 따로 뽑아서 빈칸을 기준으로 배열로 만든다
    const [method, url, version] = header.shift().split(" ")
    //1-3 url은 path+qeurystring 형태로 되어있는데 이를 분리 할 것이다.
    const [path, querystring] = url.split("?")
    //1-4 querystring을 객체로 분리하기 위해 getQuery라는 함수로 꺼내서 객체로 변환한다. #2로 가서 getQuery 완성 후 1-5 진행
    const query = getQuery(querySting)
    //1-5 message의 header영역와 body 영역 분리 #3 진행
    const [headers, body] = getMessage(header) 

}


// 맨 위에 태스트용 msg 를 작동시켜 터미널에 보기위해서 만든 console.log이다.
// 실제 작동하는데는 전혀 관계없는 코드이기 때문에 위에 msg 와 함께 지워도 괜찮다.
const result = parser(msg)
console.log(result)

module.exports = parser

모듈로 뺀 값을 server에서 다시 불러오기 위해 server.js에 내용을 추가해준다.

//모듈로 뺀 값을 server에서 다시 불러오기
const reqFn = require('./lib/req')

//client.on 안에 req 넣어주기
client.on('data', (chunk)=>{
    const req = reqFn(chunk)
    
	if(req.method == 'GET' && req.path === '/'){
    	res.sendFile('index.html')
    }
})

완성 코드 확인하기

 

더보기

server.js

const net = require('net')
const resFn = require('./lib/res')
const reqFn = require('./lib/req')
const PORT = process.env.SERVER_PORT || 3000 
const HOST = process.env.SERVER_HOST || '127.0.0.1'

const server = net.createServer((client)=>{
    client.setEncoding('utf-8')

    client.on('data', (chunk)=>{
        const res = resFn(client)
        console.log(chunk)
        const req = reqFn(chunk)
        
        if(req.method == 'GET' && req.path === '/'){
            res.sendFile('index.html')
        }
    })
})

server.on('connection', ()=>{
    console.log('connected to client')
})

server.listen(PORT, HOST, ()=>{
    console.log("server start")
})

res.js

const readFile = require('./template')
const message = (content)=>{
    const body = Buffer.from(content)
    
    return `HTTP/1.1 200 OK
Connection : Close
Content-Type : text/html; charset=UTF-8
Content-Length : ${body.length}

${body.toString()}`
}

module.exports = (socket) =>{
    return {
        send:(body)=>{
            const response = message(body)
            socket.write(response)
        },
        sendFile:(filename) =>{
            const body = readFile(filename)
            const response = message(body)
            socket.write(response)
        }
    }
}

template.js

const fs = require('fs')
const path = require('path')

module.exports = (filename, defaultDir='../views')=>{
    const target = path.join(__dirname, defaultDir, filename)
    const readline = fs.readFileSync(target, 'utf8')
    return readline
}

req.js

const msg = `GET /user?name=baek&age=29 HTTP/1.1
Host: localhost:3000
Connection: keep-alive
Cache-Control: max-age=0
sec-ch-ua: "Not?A_Brand";v="8", "Chromium";v="108", "Google Chrome";v="108"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7
Content-Type: application/json

{
    "name":"baek",
    "age":29
}`

const getQuery = (querySting)=>{
    console.log(querySting)
    if(querySting === undefined) return null
    return querySting.split("&").map(v=>v.split("=")).reduce((acc, value)=>{
        const [key, val] = value
        acc[key] = val
        return acc
    },{})
}

const bodyParser = (body, contentType) => {
    if(contentType === undefined) return null
    if(contentType.indexOf('application/json')!== -1) return JSON.parse(body)
    if(contentType.indexOf('application/x-www-form-urlencoded')!== -1 )return getQuery(body)
    
    return body
}
const getMessage = (message) =>{
    let flag = false
    let body = ""
    for(const key in message){
        if(flag) body = message.splice(key).map(v=>v.trim()).join("")
        if(message[key]==="") flag = true
    }
    message.pop()

    const headers = message.map(v=>v.split(':')).reduce((acc, value)=>{
        const [key, val] = value
        acc[key] = val
        return acc 
    },{}) 

    body = bodyParser(body, headers['Content-Type'])

    return [headers, body]
}

const parser = (message)=>{
    const header = message.split(`\n`)
    console.log(header) 
    const [method, url, version] = header.shift().split(" ")
    const [path, querySting] = url.split("?")
    const query = getQuery(querySting)
    const [headers, body] = getMessage(header) 

    return {method, url, version, path, querySting, query, headers, body}
}

const result = parser(msg)
console.log(result)

module.exports = parser

 index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    index
</body>
</html>

 

댓글