MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

JavaScript构建Node HTTP客户端与服务器的要点

2024-04-264.2k 阅读

一、Node.js 与 HTTP 基础概述

(一)Node.js 的事件驱动与非阻塞 I/O 模型

Node.js 基于 Chrome 的 V8 引擎构建,它采用事件驱动、非阻塞 I/O 模型,这使得它在处理高并发 I/O 操作时表现优异。在传统的服务器开发中,每一个请求到来,服务器可能会为其分配一个新的线程来处理,线程在进行 I/O 操作(如读取文件、网络请求等)时会阻塞,等待 I/O 完成。这在高并发场景下,会消耗大量的系统资源,导致性能瓶颈。

而 Node.js 的事件驱动模型,将 I/O 操作交给底层的操作系统,主线程不会阻塞等待 I/O 完成,而是继续执行后续代码。当 I/O 操作完成后,操作系统通过事件通知 Node.js,Node.js 再将这个事件放入事件队列中,由事件循环来处理。这种方式极大地提高了服务器的并发处理能力。

(二)HTTP 协议简介

HTTP(Hyper - Text Transfer Protocol)是一种应用层协议,用于在客户端和服务器之间传输超文本。它基于请求 - 响应模型,客户端向服务器发送请求,服务器根据请求返回相应的响应。

一个完整的 HTTP 请求包含请求行、请求头和请求体。请求行通常包含请求方法(如 GET、POST、PUT、DELETE 等)、请求 URL 和 HTTP 版本。请求头包含一些关于请求的元信息,如用户代理、内容类型等。请求体则在一些请求方法(如 POST)中用于传输数据。

HTTP 响应同样包含状态行、响应头和响应体。状态行包含 HTTP 版本、状态码(如 200 表示成功,404 表示未找到等)和状态描述。响应头提供关于响应的元信息,响应体则是服务器返回给客户端的数据。

二、构建 Node HTTP 服务器要点

(一)创建基本的 HTTP 服务器

在 Node.js 中,构建 HTTP 服务器非常简单,通过内置的 http 模块即可实现。以下是一个简单的示例:

const http = require('http');

const server = http.createServer((req, res) => {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.end('Hello, World!');
});

const port = 3000;
server.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

在上述代码中,首先引入了 http 模块。然后通过 http.createServer 创建一个服务器实例,该实例接受一个回调函数,回调函数的两个参数 reqres 分别代表请求和响应对象。res.writeHead 方法用于设置响应头,这里设置状态码为 200,并指定内容类型为纯文本。res.end 方法用于结束响应并发送响应体。最后,服务器通过 listen 方法监听指定端口。

(二)处理不同的 HTTP 请求方法

实际应用中,服务器需要处理多种 HTTP 请求方法。可以通过检查 req.method 来区分不同的请求方法。以下是一个处理 GET 和 POST 请求的示例:

const http = require('http');
const querystring = require('querystring');

const server = http.createServer((req, res) => {
    if (req.method === 'GET') {
        res.writeHead(200, { 'Content-Type': 'text/plain' });
        res.end('This is a GET request');
    } else if (req.method === 'POST') {
        let data = '';
        req.on('data', chunk => {
            data += chunk;
        });
        req.on('end', () => {
            const postData = querystring.parse(data);
            res.writeHead(200, { 'Content-Type': 'application/json' });
            res.end(JSON.stringify({ message: 'This is a POST request', data: postData }));
        });
    }
});

const port = 3000;
server.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

在这个示例中,当请求方法为 GET 时,直接返回简单的文本。当请求方法为 POST 时,通过监听 data 事件收集请求体数据,当 end 事件触发时,使用 querystring 模块解析数据,并以 JSON 格式返回响应。

(三)路由设计

随着应用的复杂,需要根据不同的 URL 路径来处理不同的请求,这就涉及到路由。可以通过解析 req.url 来实现简单的路由。以下是一个示例:

const http = require('http');
const url = require('url');

const server = http.createServer((req, res) => {
    const { pathname } = url.parse(req.url);
    if (pathname === '/home') {
        res.writeHead(200, { 'Content-Type': 'text/plain' });
        res.end('Welcome to the home page');
    } else if (pathname === '/about') {
        res.writeHead(200, { 'Content-Type': 'text/plain' });
        res.end('This is the about page');
    } else {
        res.writeHead(404, { 'Content-Type': 'text/plain' });
        res.end('Page not found');
    }
});

const port = 3000;
server.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

在这个代码中,使用 url 模块解析 req.url 获取路径名,根据不同的路径名返回不同的响应。对于更复杂的应用,可以使用第三方路由库,如 express - router 等。

(四)中间件概念与应用

中间件是 Node.js 服务器开发中的一个重要概念。中间件是一个函数,它可以对请求和响应进行处理,并且可以将控制权传递给下一个中间件。中间件可以用于多种目的,如日志记录、错误处理、身份验证等。

以下是一个简单的日志中间件示例:

const http = require('http');

const loggerMiddleware = (req, res, next) => {
    console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
    next();
};

const server = http.createServer((req, res) => {
    loggerMiddleware(req, res, () => {
        res.writeHead(200, { 'Content-Type': 'text/plain' });
        res.end('Hello, World!');
    });
});

const port = 3000;
server.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

在这个示例中,loggerMiddleware 中间件记录每个请求的时间、方法和 URL,然后通过调用 next 函数将控制权传递给后续处理逻辑。

(五)处理静态文件

在 Web 应用中,经常需要处理静态文件,如 HTML、CSS、JavaScript 文件等。可以使用 fs 模块(文件系统模块)来读取文件并将其作为响应返回。以下是一个简单的处理 HTML 文件的示例:

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

const server = http.createServer((req, res) => {
    if (req.url === '/') {
        const filePath = path.join(__dirname, 'index.html');
        fs.readFile(filePath, 'utf8', (err, data) => {
            if (err) {
                res.writeHead(500, { 'Content-Type': 'text/plain' });
                res.end('Error reading file');
            } else {
                res.writeHead(200, { 'Content-Type': 'text/html' });
                res.end(data);
            }
        });
    }
});

const port = 3000;
server.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

在这个示例中,当请求根路径时,读取 index.html 文件并将其内容作为响应返回。对于不同类型的静态文件,需要设置正确的 Content - Type 响应头。

(六)错误处理

在服务器开发中,错误处理至关重要。对于 http 服务器,可以通过监听 error 事件来处理错误。以下是一个示例:

const http = require('http');

const server = http.createServer((req, res) => {
    // 模拟一个可能出错的操作
    throw new Error('Something went wrong');
});

server.on('error', (err) => {
    console.error(`Server error: ${err.message}`);
});

const port = 3000;
server.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

在这个示例中,模拟了一个抛出错误的操作,通过监听 servererror 事件,将错误信息记录到控制台。在实际应用中,还可以向客户端返回合适的错误响应,如 500 状态码及错误描述。

三、构建 Node HTTP 客户端要点

(一)使用 http 模块发送简单请求

在 Node.js 中,http 模块不仅可以用于构建服务器,也可以用于发送 HTTP 请求作为客户端。以下是一个发送 GET 请求的示例:

const http = require('http');

const options = {
    hostname: 'localhost',
    port: 3000,
    path: '/',
    method: 'GET'
};

const req = http.request(options, res => {
    let data = '';
    res.on('data', chunk => {
        data += chunk;
    });
    res.on('end', () => {
        console.log('Response received:', data);
    });
});

req.on('error', err => {
    console.error(`Request error: ${err.message}`);
});

req.end();

在这个示例中,通过 http.request 方法创建一个请求对象,传入请求的选项,包括主机名、端口、路径和请求方法。通过监听 resdataend 事件来收集响应数据。同时,监听 reqerror 事件来处理请求过程中的错误。

(二)发送 POST 请求

发送 POST 请求时,需要在请求体中发送数据。以下是一个发送 POST 请求的示例:

const http = require('http');
const querystring = require('querystring');

const data = {
    username: 'testuser',
    password: 'testpass'
};

const postData = querystring.stringify(data);

const options = {
    hostname: 'localhost',
    port: 3000,
    path: '/login',
    method: 'POST',
    headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
        'Content-Length': Buffer.byteLength(postData)
    }
};

const req = http.request(options, res => {
    let data = '';
    res.on('data', chunk => {
        data += chunk;
    });
    res.on('end', () => {
        console.log('Response received:', data);
    });
});

req.write(postData);
req.end();

req.on('error', err => {
    console.error(`Request error: ${err.message}`);
});

在这个示例中,首先使用 querystring.stringify 方法将数据转换为 URL 编码格式。然后在请求选项中设置 Content - TypeContent - Length 头。通过 req.write 方法将数据写入请求体,最后调用 req.end 结束请求。

(三)处理响应

在客户端发送请求后,需要正确处理服务器返回的响应。除了前面示例中简单地收集响应数据,还需要处理响应头和状态码等信息。以下是一个更详细处理响应的示例:

const http = require('http');

const options = {
    hostname: 'localhost',
    port: 3000,
    path: '/',
    method: 'GET'
};

const req = http.request(options, res => {
    console.log('Status code:', res.statusCode);
    console.log('Headers:', res.headers);

    let data = '';
    res.on('data', chunk => {
        data += chunk;
    });
    res.on('end', () => {
        console.log('Response received:', data);
    });
});

req.on('error', err => {
    console.error(`Request error: ${err.message}`);
});

req.end();

在这个示例中,通过 res.statusCode 获取响应状态码,通过 res.headers 获取响应头信息,然后再收集响应体数据。

(四)使用 https 模块发送安全请求

对于需要安全传输的请求,可以使用 https 模块,它与 http 模块用法类似。以下是一个发送 HTTPS GET 请求的示例:

const https = require('https');

const options = {
    hostname: 'example.com',
    port: 443,
    path: '/',
    method: 'GET'
};

const req = https.request(options, res => {
    let data = '';
    res.on('data', chunk => {
        data += chunk;
    });
    res.on('end', () => {
        console.log('Response received:', data);
    });
});

req.on('error', err => {
    console.error(`Request error: ${err.message}`);
});

req.end();

在实际应用中,对于自签名证书等情况,可能需要设置额外的选项来忽略证书验证等操作,但这在生产环境中需要谨慎处理,以确保安全性。

(五)第三方 HTTP 客户端库

虽然 Node.js 内置的 httphttps 模块可以满足基本的 HTTP 客户端需求,但在实际开发中,第三方库可以提供更丰富的功能和更便捷的使用方式。例如,axios 是一个流行的 HTTP 客户端库,它支持 Promise 接口,使得异步请求处理更加简洁。

以下是使用 axios 发送 GET 请求的示例:

const axios = require('axios');

axios.get('http://localhost:3000')
   .then(response => {
        console.log('Response received:', response.data);
    })
   .catch(error => {
        console.error('Request error:', error.message);
    });

使用 axios 发送 POST 请求也非常简单:

const axios = require('axios');

const data = {
    username: 'testuser',
    password: 'testpass'
};

axios.post('http://localhost:3000/login', data)
   .then(response => {
        console.log('Response received:', response.data);
    })
   .catch(error => {
        console.error('Request error:', error.message);
    });

axios 还支持更多高级功能,如拦截器、自动转换响应数据格式等,这些功能可以提高开发效率并增强代码的可维护性。

(六)处理重定向

在 HTTP 请求中,服务器可能会返回重定向响应(如 301、302 状态码)。Node.js 的 http 模块默认不会自动处理重定向,需要手动处理。而像 axios 这样的第三方库则默认会自动处理重定向。

以下是使用 http 模块手动处理重定向的示例:

const http = require('http');

const options = {
    hostname: 'example.com',
    port: 80,
    path: '/redirect - target',
    method: 'GET'
};

const req = http.request(options, res => {
    if (res.statusCode >= 300 && res.statusCode < 400) {
        const redirectUrl = res.headers.location;
        const newOptions = {
            hostname: new URL(redirectUrl).hostname,
            port: new URL(redirectUrl).port || 80,
            path: new URL(redirectUrl).pathname,
            method: 'GET'
        };
        const newReq = http.request(newOptions, newRes => {
            let data = '';
            newRes.on('data', chunk => {
                data += chunk;
            });
            newRes.on('end', () => {
                console.log('Final response received:', data);
            });
        });
        newReq.on('error', err => {
            console.error(`New request error: ${err.message}`);
        });
        newReq.end();
    } else {
        let data = '';
        res.on('data', chunk => {
            data += chunk;
        });
        res.on('end', () => {
            console.log('Response received:', data);
        });
    }
});

req.on('error', err => {
    console.error(`Request error: ${err.message}`);
});

req.end();

在这个示例中,当接收到重定向响应时,解析重定向的 URL,创建新的请求并发送,从而实现手动处理重定向。

通过以上对构建 Node HTTP 客户端与服务器要点的详细阐述及代码示例,希望能帮助开发者在实际项目中更好地运用 JavaScript 和 Node.js 进行 HTTP 相关的开发工作。无论是构建高性能的服务器,还是实现灵活的客户端请求,掌握这些要点都是非常关键的。