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) // {}