Node-6 (22/12/13)

nazzzo·2022년 12월 13일
0

express 모듈 없이 구축한 HTTP 서버 코드 및 해설

*기록보관용 포스팅입니다


[server.js]

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


// 인자인 client는 임의로 생성한 '소켓'입니다
// 데이터를 4계층(TCP)까지 보내려면 소켓을 통해서 데이터를 주고받아야 합니다
const server = net.createServer((client) => {
    client.setEncoding('utf8')
    // 소켓을 통해 넘어온 데이터를 인코딩합니다

    client.on('data', (chunk)=>{
        // 데이터를 넘겨받으면 이벤트가 발동됩니다
        // req, res 과정이 잘 이루어지는지 확인 필요
 
        const req = reqFn(chunk) // string > object
        const res = resFn(client, req)

        // 정적파일(css,js) 연결을 자동화 하기 위한 코드
        // "미들웨어" ~ 라우터를 실행할 때마다 매번 반복실행될 코드입니다
        for (const path in static) {
            if(req.method ==='GET' && req.path === path) {
                res.sendStatic(path)
            }
        }

        // "라우터" ~ 분기처리(요청에 따라 그에 해당하는 파일과 연결합니다)
        if (req.method === 'GET' && req.path === '/') {
            // console.log(req) // {request message}
            console.log(req.query) // {name: 'ingoo'}
            // const name = req.query.name
            const name = req.query?.name
            // 키값이 undefined라면 에러가 발생하면서 서버가 강제종료됩니다. 예외처리가 필요
            // ES7 ~ ?.key : 키값이 없을 경우 undefined를 에러없이 출력하는 신문법

            res.sendFile('index.html', { name:name, title:'메인 페이지입니다'})
        } else if (req.method === 'GET' && req.path === '/list' || req.path === '/list.html') {
            res.sendFile('list.html')
        } else if (req.method === 'GET' && req.path === '/view' || req.path === '/view.html') {
            res.sendFile('view.html')
        } else if (req.method === 'GET' && req.path === '/write' || req.path === '/write.html') {
            res.sendFile('write.html')
        } else if (req.method === 'GET' && req.path === '/modify' || req.path === '/modify.html') {
            res.sendFile('modify.html')
        } else if (req.method === 'POST' && req.path === '/write' || req.path === '/write.html') {
            res.sendFile('view.html')
        } 
        //  else if (req.method === 'GET' && req.path === '/css/index.css') {
        //     // req.path는 실제 경로가 아닙니다. (임의대로 써도 무관)
        //     res.sendStatic('/css/index.css')
        // } else if (req.method === 'GET' && req.path === '/js/index.js') {
        //     res.sendStatic('/js/index.js')
        // }
    })
});



server.on("connection", () => {
  console.log('connected to client');
});
// 클라이언트 연결 이벤트

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

[req.js]

const getQuery = (queryString) => {
    if(queryString === undefined) return null

    // ?name=XXX&age=YY
    // map을 써서 이중배열로 만든 뒤 reduce 메서드로 객체화합니다
    return queryString.split("&").map(v=>v.split('=')).reduce((acc,value)=>{
        const [key, val] = value
        acc[key]=val
        return acc
    },{})
}

// 바디영역의 데이터를 변환(스트링 > 객체)하기 위한 함수입니다
// body: '{"name":"XXX","age":'YY'}' > body: { name: 'XXX', age: 'YY' }
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-urlencodded')!== -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("") 
        // 빈줄을 기준으로 헤더와 바디를 구분해서 배열화합니다. key는 배열의 인덱스값. trim()은 문자열의 공백을 제거합니다

        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')
    const [method, url, version] = header.shift().split(' ')
    // shift() 메서드는 배열에서 첫 번째 요소를 제거하고 제거된 요소를 반환합니다

    const[path, queryString] = url.split("?")
    const query = getQuery(queryString)

    const [headers, body] = getMessage(header)
    
    return {method, url, version, path, queryString, query, headers, body}
}


module.exports = parser

[res.js]

const readFile = require("./template");

const message = (content, req) => {
  const body = Buffer.from(content);
  let contentType = ''
    if(req.headers.Accept.indexOf('text/html')!== -1) {
        contentType = 'text/html'
    } else {
        contentType = req.headers.Accept
    }
  // 응답 헤더는 규격에 맞게, 띄어쓰기 & 오타에 주의합니다
  // 콘텐츠 타입은 html, css, js 규격에 맞게 응답합니다
  return `HTTP/1.1 200 OK
Connection:Close
Content-Type:${contentType}; charset=UTF-8
Content-Length:${body.length}

${body.toString()}
`;
};

module.exports = (socket, req) => {
  return {
    send: (body) => {
      // send : 메세지를 ${body.toString()}으로 보내기 위한 메서드
      const response = message(body, req);
      socket.write(response);
    },
    sendFile: (filename, obj = {}) => {
      //template.js에서 인자값 filename을 받아옵니다

      const body = readFile(filename, obj);
      const response = message(body, req);
      socket.write(response);
      // client에 응답 메시지를 넘깁니다
    },

    // 정적 페이지(css,js) 구현을 위한 함수입니다
    sendStatic: (filename) => {
      const defaultDir = "../public";
      const body = readFile(filename, {}, defaultDir);
      // template.js target 변수
      const response = message(body, req);
      socket.write(response);
    },
  };
};

[template.js]

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


// obj={} : 인자의 초기값을 빈객체로
// 쿼리스트링으로 html을 변조하기 위한 세팅입니다 (동적 페이지)
// 기본 디렉토리 경로를 views로 설정합니다
module.exports = (filename, obj={}, defaultDir='../views') => {
    console.log('template req :', obj)
    // obj : {name: "XXX", title" '메인페이지입니다'}

    const target = path.join(__dirname, defaultDir, filename)
    // filename은 senfFile 메서드와 연동합니다

    let readLine = fs.readFileSync(target,'utf8') // {{name}}
    // 동기 코드로 불러온 파일을 읽습니다

    for(const key in obj) {
                                        // {{name}}
                                        // {{title}}
        readLine = readLine.replaceAll(`{{${key}}}`, obj[key])
       // String.prototype.repclassAll(찾을 단어, 바꿀 단어)
    }

    
    return readLine
}

[static.js] ~ 정적 페이지 연결을 위한 모듈

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

// 루트 디렉토리 설정 ~ public
const root = 'public'
const rootDir = path.join(__dirname, '../', root)
// console.log(rootDir)
// > Users/XXX/Documents/workspace/Node/221213/public


let result = {}
// 리턴값을 빈 객체에 담기 위한 변수 선언

// 디렉토리와 파일을 구분하기 위한 함수입니다
const find = (currentPath) => {
    // css js imges 1.js
    // readdir은 배열을 리턴 > 반복문으로 출력해야 합니다

    const directory = fs.readdirSync(currentPath)
    for (const index in directory) {
        // console.log(directory[index])
        const findPath = path.join(currentPath, directory[index]) // 디렉토리
        const isFile = fs.statSync(findPath).isFile() // file: true, directory: false

        if(!isFile) {
            // 디렉토리일 경우 함수가 재실행됩니다(재귀함수, 경로안에 파일만 출력될 때까지)
            find(findPath)
        } else {
            // 파일일 경우 findPath : currentPath + directory[index]
            const key = (currentPath === rootDir)
            ? '/' 
            : currentPath.replaceAll(rootDir,"")
            // currentPath = rootDir(~/public/) + css/index.css
            // > css/index.css

            const httpPath = path.join(key, directory[index]) // /css/index.css
            result[httpPath] = directory[index]
        }
    }
    return result
}

// const a = find(rootDir)
// console.log(a)


module.exports = find(rootDir) // {}

0개의 댓글