ในเนื้อหาต่อไปนี้ เราจะมาทำความเข้าใจ และรู้จักการใช้งาน
การรับส่งข้อมูลผ่าน HTTP ใน NodeJs ว่ามีรูปแบบและคำสั่งเบื้องต้น
เป็นอย่างไร
การสร้าง Web Server
Application ที่พัฒนาด้วย Node จะมีการสร้าง Web server object อยู่ในเกือบทุกๆ ครั้ง
หรืออาจจะเรียกได้ว่า โดยส่วนใหญ่แล้วจะต้องมีการใช้งาน web server ซึ่งใช้คำสั่ง createServer
ตัวอย่าง
const http = require('http'); // เรียกใช้ http module const hostname = '127.0.0.1'; // กำหนด hostname หรือ ip const port = 3000; // กำหนด port // สร้าง web server object const server = http.createServer((request, response) => { // do something }); // server ตรวจจับ request event server.listen(port, hostname, () => { console.log(`Server running at http://${hostname}:${port}/`); });
ทุกๆ ครั้งที่เกิด HTTP Request หรือมีการเรียกใช้งาน web server ผ่านบราวเซอร์ ฟังก์ชันที่ส่งเข้าไป
ภายในคำสั่ง http.createServer() จะถูกเรียกใช้งาน เราเรียกฟังก์ชั่นภายในว่า ตัวจัดการ request
ตัวแปร Server object นั้นก็คือ การทำให้เกิดเหตุการ์หรือ event ขึ้นด้วยคำสั่ง createServer
โดยเมื่อมีตัวสร้างหรือปล่อย event แล้ว เราก็ต้องมีตรวจจับ หรือเฝ้าสังเกต event ซึ่งก็คือคำสัง listen()
เดียวเราจะได้ศึกษาไปตามลำดับ ตัวอย่างโค้ดด้านล่าง เป็นรูปแบบการเขียนแบบย่อ ของการสร้าง
web server object
const server = http.createServer(); // สร้าง server object ที่เป็น EventEmitter หรือตัวปล่อย event server.on('request', (request, response) => { // เมื่อเกิดปล่อย event "request" ขึ้น // do something });
รูปแบบการเกิด event คือ
emitter.on(eventName, listener)
server คือ emitter หรือให้เข้าใจว่าตัวปล่อย event
eventName คือ ชื่อ event ในโค้ดตัวอย่างคือ "request"
listener คือ callback function หรือฟังก์ชั่นที่ทำงานเมื่อเมื่อเกิด event ที่กำหนดขึ้น
(request, response) => { // do something }
เป็นรูปแบบ Arrow function มาจาก
function(request,response){ // do something }
ส่วนของ Method, URL และ Headers
ในการจัดการกับ request ที่เกิดขึ้น ส่วนใหญ่เราจะดูว่ามี method อะไรถูกส่งเข้ามา และส่งมาที่
url ใด
Method ก็เช่น GET,POST,PUT, DELETE เหล่านี้เป็นต้น
URL ก็เช่น / หรือ /api หรือ /home หรือ url ก็คือ ค่าที่ตั้งแต่เครื่องหมาย / ตัวที่สาม
ยกตัวอย่าง htttps://niik.in/forum จะได้ url คือ /forum
ค่าต่างๆ เหล่านี้ เราสามารถใช้จาก request object ที่ส่งเข้ามาใน callback function ตามตัวอย่างด้านบน
สมมติเราดึงค่า method และ url จาก request object มาใว้ในตัวแปร constant ตามโค้ดด้านล่าง
const { method, url } = request; // ดึง property "method" และ "url" ของ request object
หรือดึงค่า headers จาก request object
const { headers } = request; // ดึง headers property const userAgent = headers['user-agent']; // ใน headers property ยังมี property ย่อยด้านในอีก
Request Body
ในกรณีที่มีการใช้งาน POST หรือ PUT request method จะมีการส่งข้อมูลเข้ามาด้วยเสมอ ส่วนนี้
เราสามารถใช้งาน request body เพื่อดึงข้อมูลที่ถูกส่งเข้ามา สำหรับนำไปใช้งาน ดังนั้นส่วนของ
request body จึงมีส่วนสำคัญในกรณีการส่งข้อมูล ตัว request object ในกรณีนี้จะมีการใช้งาน
ReadableStream ดูตัวอย่างโค้ดด้านล่าง
// สร้าง server object ที่สามารถปล่อย event ต่างๆ ขึ้นกับ ข้อมูลและรูปแบบที่ request const server = http.createServer((request, response) => { // `request`คือ http.IncomingMessage, ที่เป็นแบบ Readable Stream // `response` คือ http.ServerResponse, ที่เป็นแบบ Writable Stream const { headers, method, url } = request; // ดึง property บางตัวมาไว้ใช้งาน let body = []; // เมื่อเป็น request ที่ใช้งาน ReadableStream request.on('error', (err) => { // กรณี error console.error(err); }).on('data', (chunk) => { // มีข้อมูลส่งเข้ามา ส่งข้อมูลเข้าไปใน callback function body.push(chunk); // เก็บข้อมูลเพิ่มเข้าไปใน array ที่ชื่อ body }).on('end', () => { // เมื่อส่งข้อมูลครบแล้ว ไม่มีข้อมูลเพิ่มเติม body = Buffer.concat(body).toString(); // แปลงข้อมูลรวมเป็น string }); });
การกำหนด HTTP Status Code
เราได้รู้จักในส่วนของ request object ไปแล้ว ต่อไปเรามาดูในส่วนของ reponse object กันต่อ
โดยปกติทั่วไปแล้ว HTTP status code จะเป็น 200 แต่บางกรณีเราอาจจำเป็น
ต้องกำหนด status เป็นค่าอื่่นๆ ก็สามารถกำหนดได้ผ่าน statusCode property ดังนี้
response.statusCode = 404; // แจ้งไปยัง client ว่าไม่พบเนื้อหาหรือข้อมูลที่เรียกดู
การตั้งค่า Response Headers
เราสามารถกำหนด headers ผ่านคำสั่ง setHeader ตามรูปแบบดังนี้
response.setHeader('Content-Type', 'application/json'); response.setHeader('X-Powered-By', 'bacon');
การกำหนด headers ให้กับ response object ชื่อ property จะเป็นต้วเล็กหรือใหญ่ก็ได้ เป็นแบบ
case insensitive และหากมีการกำหนดซ้ำกัน จะใช้ค่าจากตัวที่ซ้ำตัวสุดท้ายแทน
การตั้งค่า headers และสถานะข้างต้น เป็นลักษณะการกำหนดแบบ "implicit headers" ซึ่งเราไว้ใจว่า
node จะทำการส่งค่าเหล่านั้นในเวลาที่เหมาะสมก่อนที่จะส่งส่วนของข้อมูล body data แบบ "implicit headers"
เข้าใจง่ายๆ ก็คือบอกกับ node ว่าเรากำหนด headers ด้วยค่าต่างๆ ไว้แล้ว node ต้องส่งค่าเหล่านั้นไปก่อน
ที่จะส่งส่วนของ body ไป เป็นต้น
นอกจากการกำหนดแบบ "implicit headers" แล้ว เรายังสามารถกำหนดแบบ "explicit headers" หรือก็คือ
เราต้องการะบุและส่ง "headers" ค่าเหล่านี้แบบชัดเจน หมายความว่า เมื่อกำหนดแบบ "explicit headers" ส่วน
ของ headers ก็จะถูกส่งไปอย่างแน่นอนก่อนที่ส่วนของ body จะถูกส่งไป ในการกำหนดเราจะใช้คำสั่ง
writeHead() โดยจะมีกำหนด status code กับ headers เข้าไป ดังตัวอย่างด้านล่าง
response.writeHead(200, { 'Content-Type': 'application/json', 'X-Powered-By': 'bacon' });
เมื่อมีการส่งส่วนของ response headers ไปแล้ว ไม่ว่าจะใช้วิธี "implicit" หรือ "explicit" ก็ตาม ส่วน
ต่อไปที่เราจะส่งก็คือ reponse data ซึ่งอยู่ในส่วนของ body
การส่ง Response Body
อย่างที่เราทราบแล้วว่า reponse object จะเป็นแบบ WriteableStream ดังนั้นการเขียนในส่วนของ
reponse body ส่งออกไปยัง client จึงเป็นวิธีการโดยทั่วไปสำหรับการใช้งาน stream ตัวอย่างโค้ดด้านล่าง
เป็นรูปแบบการเขียนส่วนของ reponse data ด้วยคำสั่ง write() ส่ง html กลับไปยังผู้ใช้ (client)
response.write('<html>'); response.write('<body>'); response.write('<h1>Hello, World!</h1>'); response.write('</body>'); response.write('</html>'); response.end();
หรือเขียนแบบย่อ โดยใช้คำสั่ง end() ได้เป็นดังนี้
response.end('<html><body><h1>Hello, World!</h1></body></html>');
ใน response stream object เป็นไปได้ที่ในบางครั้งจะเกิด error event ขึ้น ดังนั้น เราควรมีการ
จัดการกับกรณีเงื่อนไขเมื่อมี error ขึ้นด้วย เช่นเดียวกับในกรณี request
ต่อไป เราเอาส่วนต่างๆ ที่ได้กล่าวไปแล้วข้างต้น มารวมกันเป็นส่วนของโค้ดแบบเต็ม จะได้เป็นดังนี้
const http = require('http'); // เรียกใช้ http module const hostname = '127.0.0.1'; // กำหนด hostname หรือ ip const port = 3000; // กำหนด port // สร้าง server object ที่สามารถปล่อย event ต่างๆ ขึ้นกับ ข้อมูลและรูปแบบที่ request const server = http.createServer((request, response) => { // `request`คือ http.IncomingMessage, ที่เป็นแบบ Readable Stream // `response` คือ http.ServerResponse, ที่เป็นแบบ Writable Stream const { headers, method, url } = request; // ดึง property บางตัวมาไว้ใช้งาน let body = []; // เมื่อเป็น request ที่ใช้งาน ReadableStream request.on('error', (err) => { // กรณี error console.error(err); }).on('data', (chunk) => { // มีข้อมูลส่งเข้ามา ส่งข้อมูลเข้าไปใน callback function body.push(chunk); // เก็บข้อมูลเพิ่มเข้าไปใน array ที่ชื่อ body }).on('end', () => { // เมื่อส่งข้อมูลครบแล้ว ไม่มีข้อมูลเพิ่มเติม body = Buffer.concat(body).toString(); // แปลงข้อมูลรวมเป็น string // ตรวจสอบว่ามี error เกิดขึ้นใน response object หรือไม่ response.on('error', (err) => { console.error(err); }); // กำหนด heasers ให้กับ response headers แบบ "implicit" response.statusCode = 200; response.setHeader('Content-Type', 'application/json'); // หรือจะกำหนดแบบ "explicit" ด้วยรูปแบบโค้ดด้านล่างนี้ก็ได้ // response.writeHead(200, {'Content-Type': 'application/json'}) // จำไว้เสมอว่า ส่วนของการกำหนด headers ต้องมาก่อนส่วสนของการกำหนด response body // สร้างตัวแปร เก็บข้อมูล ที่จะส่งออกไปใน response body const responseBody = { headers, method, url, body }; // เขียนข้อมูลเพื่อส่งออกไปยัง client // ฟังก์ชั่น JSON.stringify() เป็นคำส่ังสำหรับแปลง javascript object ให้อยู่ในรูปแบบ json string response.write(JSON.stringify(responseBody)); response.end(); // หรือจะเขียนแบบย่อ ส่งข้อมูลไปใน end() ก็ได้ ตามรูปแบบด้านล่าง // response.end(JSON.stringify(responseBody)) }); }); // server ตรวจจับ request event server.listen(port, hostname, () => { console.log(`Server running at http://${hostname}:${port}/`); });
จากแนวทางข้างต้น ตอนนี้เรารู้จักองค์ประกอบ ส่วนต่างๆ ของการใช้งาน การรับส่งข้อมูลผ่าน
HTTP ไปแล้วพอสมควร ต่อไป เราจะลองประยุกต์สร้าง การทำงานจริงกัน
ทดลองสร้าง Echo Server
เราจะมาสร้าง echo server หรือ web server อย่างง่าย ที่แสดงข้อมูลที่กำหนด กลับไปแสดงยัง
ฝั่งผู้ใช้ (client) ลักษณะการทำงาน คล้ายกับตัวอย่างด้านบน คือรับข้อมูลจาก request object และ
ส่งกลับข้อมูลด้วย response object ด้านล่าง คือรูปแบบโค้ด แบบสั้น ที่เราตัดบางส่วนออก
// สร้าง web server object const server = http.createServer((request, response) => { let body = []; request.on('data', (chunk) => { // นำข้อมูลที่ถูกส่งเข้ามา มาจัดการ body.push(chunk); // โดยเพิ่มไปใน ตัวแปร array ที่ชื่อ body }).on('end', () => { // เมื่อไม่มีข้อมูลที่ส่งมาแล้ว body = Buffer.concat(body).toString(); // แปลงข้อมูลเป็น string response.end(body); // แล้วส่งกลับมาแสดงที่ฝั่ง client }); });
ต่อไปเราใส่เงื่อนไขว่า ต้องเป็นการ request ข้อมูลแบบ POST และ เข้ามาผ่าน url "/echo" เท่านั้น
เข้าใจอย่างง่ายก็คือ เมื่อมีคนส่งข้อมูลแบบ POST ไปยังหน้า /echo ดังนั้น สิ่งที่เราจะได้นำมา
ใช้ในการกำหนดเงื่อนนี้คือ request.method สำหรับดูค่า method ที่ส่งมา และ request.url เพื่อดูว่า
ส่งมายัง url "/echo" หรือไม่
นอกจากนั้ง เรายังเพิ่มเงื่อนไขสำหรับการส่งกลับข้อมูล ว่าถ้าไม่ได้ส่งข้อมูลมาแบบ POST และไม่ได้
ส่งเข้ามาใน "/echo" ให้ response status เป็น 400 หรือ Bad Request ก็จะได้เป็นดังนี้
// สร้าง web server object const server = http.createServer((request, response) => { if (request.method === 'POST' && request.url === '/echo') { let body = []; request.on('data', (chunk) => { // นำข้อมูลที่ถูกส่งเข้ามา มาจัดการ body.push(chunk); // โดยเพิ่มไปใน ตัวแปร array ที่ชื่อ body }).on('end', () => { // เมื่อไม่มีข้อมูลที่ส่งมาแล้ว body = Buffer.concat(body).toString(); // แปลงข้อมูลเป็น string response.end(body); // แล้วส่งกลับมาแสดงที่ฝั่ง client }); } else { response.statusCode = 404; response.end(); } });
ลักษณะการทำงานแบบรับข้อมูล และส่งข้อมูลออกมา ในบรรทัดที่ 4 - 10 นั้น เราสามารถใช้คำสั้ง pipe()
จัดการได้ รูปแบบคำสั่งคือ
readable.pipe(stream.writable)
readable ก็เท่ากับ request และ stream.writable ก็เท่ากับ reponse ก็จะได้เป็น
request.pipe(response);
จะเห็นว่าโค้ดเราเริ่มสั้นและกระชับลงเรื่อยๆ อย่างไรก็ตาม สิ่งสำคัญ ที่เราควรพิจารณาด้วยเสมอ ก็คือ เมื่อ
เกิด error event ขึ้น ทั้งในส่วนของ request และ response เราควรมีการจัดการในส่วนนี้ด้วย ดังนั้น
เราจะได้รูปแบบโค้ดสุดท้าย สำหรับตัวอย่าง echo server ของเราเป็นดังนี้
const http = require('http'); // เรียกใช้ http module const hostname = '127.0.0.1'; // กำหนด hostname หรือ ip const port = 3000; // กำหนด port // สร้าง web server object const server = http.createServer((request, response) => { request.on('error', (err) => { // กรณี error ใน request console.error(err); response.statusCode = 400; // แจ้ง error Bad Request response.end(); }); response.on('error', (err) => { // กรณี error ใน response console.error(err); }); // ตรวจสอบว่าเป็นการ POST ข้อมูล มายัง URL "/echo" หรือไม่ if (request.method === 'POST' && request.url === '/echo') { request.pipe(response); // ส่งข้อมูลที่ได้รับกลับออกไปด้วย คำสั่ง pipe() } else { // กรณีอื่นๆ response.statusCode = 404; // แจ้ง erorr File not found response.end(); } }); // server ตรวจจับ request event server.listen(port, hostname, () => { console.log(`Server running at http://${hostname}:${port}/`); });
นำไฟล์ข้างต้นไปทดสอบ ใช้ชื่อไฟล์ app.js
ทดเรียกคำสั่ง nodemon app จากนั้นเปิดบราวเซอร์ไปยัง web server ของเรา จะได้ผลลัพธ์เบื้องต้นตามรูป
เนื่องจากว่า ไม่ใช่ url ที่เข้าเงื่อนไขตามที่เรากำหนด จึงขึ้นเป็น HTTP ERROR 404 ตามที่เรากำหนด
แต่เมื่อเราเปลี่ยน url เป็น "/echo" ก็ยังขึ้นเป็น 404 เหมือนเดิม เพราะ method หรือวิธีที่เราเรียกเข้าไป
เป็นแบบ GET ยังไม่เข้าเงื่อนไข เราต้องทดสอบส่งข้อมูลแบบ POST เข้าไป
ให้เราสร้างเงื่อนไขกำหนดหน้าฟอร์มไว้หน้าแรก แล้วให้มีปุ่ม submit และ input text สำหรับส่งข้อมูล
โดยเพิ่มโค้ดเข้าไปเป็นดังนี้
const http = require('http'); // เรียกใช้ http module const hostname = '127.0.0.1'; // กำหนด hostname หรือ ip const port = 3000; // กำหนด port // สร้าง web server object const server = http.createServer((request, response) => { request.on('error', (err) => { // กรณี error ใน request console.error(err); response.statusCode = 400; // แจ้ง error Bad Request response.end(); }); response.on('error', (err) => { // กรณี error ใน response console.error(err); }); // ตรวจสอบว่าเป็นการ GET ข้อมูล มายัง URL "/" หรือไม่ if (request.method === 'GET' && request.url === '/') { response.write('<html>'); response.write('<body>'); response.write('<form action="/echo" method="POST">'); response.write('<input name="mytext" type="text">'); response.write('<button type="submit">Submit</button>'); response.write('</form>'); response.write('</body>'); response.write('</html>'); response.end(); // ตรวจสอบว่าเป็นการ POST ข้อมูล มายัง URL "/echo" หรือไม่ } else if (request.method === 'POST' && request.url === '/echo') { request.pipe(response); // ส่งข้อมูลที่ได้รับกลับออกไปด้วย คำสั่ง pipe() } else { // กรณีอื่นๆ response.statusCode = 404; // แจ้ง erorr File not found response.end(); } }); // server ตรวจจับ request event server.listen(port, hostname, () => { console.log(`Server running at http://${hostname}:${port}/`); });
ทดสอบรันโค้ดอีกครั้ง เมื่อเราเข้าไปยัง http://localhost:3000 ตอนนี้จะขึ้นหน้าฟอร็มสำหรับส่งข้อมูล
เพราะเข้าเงื่อนไขเป็นการ request แบบ GET ไปยัง URL "/" หรือ root ของ web server
เมื่อเรากรอกข้อมูล แล้วกดปุ่ม submit โดยฟอร์มจะส่งข้อมูลแบบ POST ไปที่ URL "/echo" จะได้ผลลัพธ์
ส่งข้อมูลที่เราส่งไปในรูปแบบ string
ตอนนี้เราได้รู้จักการใช้งาน การรับส่งข้อมูลผ่าน HTTP request , HTTP response ในเบื้องต้นไปแล้ว
ซึ่งจะเป็นแนวทางทำความเข้าใจ ส่วนอื่นๆ ต่อไป