客户端与服务器端交互原理(HTTP数据请求与HTTP响应)

前言

回顾有关服务器和客户端的一些概念。了解 HTTP 协议的一些浅层概念(《图解 HTTP》 这本书刚买回来,还没看,看完再写更加详细的笔记),并知道 nodejs 构建服务器的基础知识。

1. 客户端和服务器的基础概念

以下是我对客户端、服务器、软件等相关概念的理解。

1.1 Client 和 Server

客户端(Client) 或称用户端,是指与服务器相对应,为客户提供本地服务的程序。除了一些只在本地运行的应用程序之外,一般安装在客户机上,需要与服务端互相配合运行。例如:下载 QQ 或者微信时,实际上我们下载的是腾讯开发的客户端程序(在我们电脑或者手机执行的程序)。

服务器(Server)也是计算机,不过它比普通计算机运行更快、负载更高。通俗点说它也是一台电脑,它的某些性能可能比我们的普通电脑高。服务器上运行着服务器端程序(客户机不用下载的程序),主要负责根据客户端发送过来的请求进行业务逻辑和数据的处理

对于服务器要求不高的话我们的电脑也可以充当一台服务器(将服务器端程序在本机运行),平时我们线下开发就是这么做的。不过服务器都是一直工作(开机)着的,因为一旦软件上线,一般都要一直为其使用者提供服务的。

生产环境下的服务器一般都是放置在机房中,由专业人员进行远程操控和维护(没有屏幕和键盘等其它部件,跟一台高性能主机差不多)。

1.2 软件

我们常见的软件按结构可以分为两类,B/S 软件和 C/S 软件。两者的服务器端的原理是一样的

C/S 结构,即 客户端 / 服务器 结构的软件。这类软件有专门的客户端程序和对应的服务端程序。当客户端程序更新时,需要重新下载安装。例如 QQ 的更新,实际就是更新了客户端应用程序。

在这里插入图片描述

B/S 结构,即 浏览器 / 服务器 结构的软件,这是前端工程师开发的主要类型软件。这类软件的客户端其实就是浏览器(有浏览器就可以使用),在浏览器可以运行不同软件的客户端程序。在需要使用某个软件时,由浏览器发送请求,该软件服务器响应请求。服务器响应时将该软件的用户端程序(html、css、js)和相关数据响应给浏览器并在浏览器上执行。例如网页版微信、淘宝网购物等。

在这里插入图片描述

上图中能够提供网站服务的机器就是网站服务器(Web 服务器),它能够接收客户端(浏览器)的请求并对请求做出回应。

1.3 服务器基础概念

1、IP 地址
IP(Internet Protocol Address,互联网协议地址) 是互联网设备的唯一标识。即互联网上(已入网)的每一台计算机都有唯一标识,我们可以通过该 IP 找到该计算机

2、域名
由于 IP 地址是一长串数字,比较难记。所以产生了域名的概念,所谓的域名就是 IP 地址的别名。可以使用域名代替 IP 地址找到与之对应的那台计算机。
例如 http://www.yangchen.world => http://124.165.219.098/ ,虽然在地址栏中输入的是网址,但最终还是会将其域名解析为 IP 地址(由 DNS 服务器解析)。并根据该地址找到互联网上与之对应的网站服务器(计算机)。

3、端口
端口是计算机与外界通讯的 IO 接口,用来区分服务器电脑中提供的不同服务。例如下图中提供邮件传输和网页浏览的服务监听的是不同的端口。

在这里插入图片描述

4、URL
统一资源定位符(Uniform Resource Locator , URL),用于标识互联网上的资源的位置。下列即是简单的 URL 组成。

protocol://host[:port]/path/[?query]#fragment
传输协议://服务器IP或者域名:端口/资源所在的位置
http://www.baidu.com/index.html?uname=TKOP&age=18#link
http : 超文本传输协议,提供了一种发布和接收 HTML 页面的方法。

5、开发过程中客户端和服务器端说明
在开发阶段,客户端和服务器端使用同一台电脑,即开发人员电脑。本机访问本机时 IP 使用本地 IP(127.0.0.1)或者本机域名 localhost

在这里插入图片描述

2. HTTP 协议入门

超文本传输协议(Hypertext Transfer Protocol,HTTP) 是一个简单的请求-响应协议,它通常运行在 TCP 之上。它指定了客户端可能发送给服务器什么样的消息以及得到什么样的响应。请求和响应消息的头以 ASCII 形式给出;而消息内容则具有一个类似 MIME 的格式。

2.1 HTTP 基础概念

HTTP 协议通俗讲就是客户端与服务器进行通信的规范,它规定了如何从网站服务器传输超文本到本地浏览器。网站应用在发送请求时,服务器需要知道请求的相关信息和内容;网站在接受来自服务器的响应式,它也需要知道对方响应的相关信息和内容。而 HTTP 正是客户端(用户)和服务器(网站)请求和应答的标准。

2.1.1 报文

在 HTTP 请求和响应的过程中传递的数据块就叫报文,其中请求时浏览器发送的是请求报文,响应时服务器发送的是响应报文。报文中是特定格式的要传递的数据(内容)和一些附加信息(报文首部)

1、创建 web 服务并开启(执行 js 文件)

// node.js 程序中创建 web 服务
// 引入系统模块
const http = require('http');
// 创建服务
const app = http.createServer();
// 响应请求
app.on('request', (req, res) => {
    
    
    res.end('<h1>hello, user</h2>');
});
// 端口监听
app.listen(3000);
console.log('服务已开启,监听3000端口,本机访问localhost:3000');

2、如图所示是我运行代码后开启一个服务后,在浏览器中可以看到每个请求的信息。

在这里插入图片描述

可以看到浏览器一共发送了两个请求(url 的发送和请求网站图标),服务器也响应了两个请求。从中我们可以看到 http 状态码、响应文件的类型、请求发起对象、文件大小、耗时和执行流。

3、点开某个请求处理结果如图,我们可以查看的信息有五栏(Headers、Preview、Response、Initiator、Timing)我们首先看第一栏信息。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HbfC9e4E-1615391534197)(https://s3.ax1x.com/2021/03/10/6Y8I1K.png)]
可以在这个信息栏查看到请求的标准(General)信息,例如:请求 URL 、请求方式、状态码、远程地址和提交策略等信息。也可以查看到响应头信息(Resonse Headers),例如:连接状态、响应时间和超时时长等。当然还可以查看请求首部信息(Request Headers),如下图:里面包含了大量信息。

在这里插入图片描述

可以看到里面包含了请求可以接收的响应文件类型、压缩格式、语言类型、该请求发出的浏览器简单信息和代理商信息(User-Agent)等等

4、Response 那列主要是显式响应文件的内容,如本例中内容如下。

<h1>hello, user</h2>

我们已经大概了解了报文的头信息和报文内容。但是了解之后有什么实质的开发需要吗?以下就是这些信息的使用。

2.1.2 请求报文

各部分的获取

app.on('request', (req, res) => {
    
    
	req.headers // 获取请求报文
	req.url     // 获取请求地址
	req.method  // 获取请求方式
})

请求报文我们主要需要了解以下几个内容:

1、请求的方式(Request Method)

  • GET 请求数据
    1. 浏览器地址栏
    2. link 标签的 href 属性
    3. script 标签的 src 属性
    4. img 标签的 scr 属性
    5. form 表单提交
  • POST 发送数据
    1. form 表单提交

在浏览器输入 url 进行访问时发出的是 get 请求。可以通过 req.method 获得请求方式进行输出判断其值。

const http = require('http');
const app = http.createServer();
app.on('request', (req, res) => {
    
    
	console.log(req.method);
    res.end(''<h1>亲爱的用户您好</h2>'');
});
app.listen(3000);
console.log('服务已开启,监听3000端口,本机访问localhost:3000');

在访问页面时会发出两次 get 请求。如果我使用表单提交数据时使用 post 请求,如下代码:

<body>
    <form action="http://localhost:3000" method="POST">
        <input type="text" value="nihao">
        <button type="submit">提交</button>
    </form>
</body>

当数据提交后控制台输出如下(会发出一个 post 请求和一个 get 请求):

在这里插入图片描述

2、请求地址(Request URL)的使用

app.on('request', (req, res) => {
    
    
    let url = req.url;
    res.writeHead(200, {
    
    
        'Content-Type': 'text/html;charset=utf8'
    })
    if (url == '/index' || url == '/') {
    
    
        res.end('<h1>首页</h2>');
    } else if (url == '/login') {
    
    
        res.end('<h1>登录页</h2>');
    } else if (url == '/register') {
    
    
        res.end('<h1>注册页</h1>')
    } else {
    
    
        res.writeHead(404);
        res.end('<h1>404</h1>');
    }
});

如上示例代码,使用请求报文中的 url 属性可以获得请求地址,并根据地址响应不同的信息。

2.1.3 响应报文

响应报文我们主要需要了解以下几个内容:

1、HTTP 状态码

  • 200 请求成功
  • 404 请求资源没有被找到
  • 500 服务器端程序出错
  • 400 客户端请求语法有误

2、内容类型(高版本的浏览器不用指定)

  • text/html(html 文件)
  • text/css(css 文件)
  • application/javascript( js 脚本)
  • image/jpeg(jpeg 格式的图片文件)
  • application/json(json 文件)

使用上述案例演示时你会发现一个问题,就是浏览器上输出乱码。其主要原因是没有指定文件的编码方式和文件类型。

app.on('request', (req, res) => {
    
    
    res.writeHead(404, {
    
    
        'Content-Type': 'text/text;charset=utf8'
    })
    res.end('<h1>亲爱的用户您好</h2>');
});

上述代码中虽然找到了文件但还是将状态码手动改为了 404 ,且设置了文件类型为 text ,编码为 utf8 。虽然不会出现乱码但是该字符串内容会以纯文本内容呈现(即标签不会解析为标题,而是显式在网页)。

2.2 HTTP 请求与响应

2.2.1 请求参数处理

在客户端向服务器发送请求时,有时需要携带一些客户信息,这些客户信息是通过请求参数的形式传到后端程序的。例如登录时登录信息的传递,我发布这篇博客笔记时发布时间的传递。这中参数分别可以使用 post 和 get 请求参数的方式传递给服务器。

1、get 请求参数

  • 参数被放置在浏览器地址栏中,例如:http://localhost:3000/?publishTime = ‘2021-3-11’
  • 参数的获取需要借助原生模块 url ,url 模块是专门用来处理 url 地址的模块
// 引入系统模块
const http = require('http');
const url = require('url');
// 创建服务
const app = http.createServer();
// 响应请求
app.on('request', (req, res) => {
    
    
    res.writeHead(200, {
    
    
        'Content-Type': 'text/html;charset=utf8'
    })
    console.log(url.parse(req.url));
    console.log(url.parse(req.url, true));
    let {
    
     pathname, query } = url.parse(req.url, true);
    console.log(query.name);
    console.log(query.age);
    if (pathname == '/index' || url == '/') {
    
    
        res.end('<h1>首页</h2>');
    } else if (pathname == '/login') {
    
    
        res.end('<h1>登录页</h2>');
    } else if (pathname == '/register') {
    
    
        res.end('<h1>注册页</h1>')
    } else {
    
    
        res.writeHead(404, {
    
    
            'Content-Type': 'text/html;charset=utf8'
        });
        res.end('<h1>404</h1>');
    }
});
// 端口监听
app.listen(3000);
console.log('服务已开启,监听3000端口,本机访问localhost:3000');

服务启动后,我访问 http://localhost:3000/login?name=“TKOP_”&age=18。终端输出如下:

Url {
    
    
  protocol: null,
  slashes: null,
  auth: null,
  host: null,
  port: null,
  hostname: null,
  hash: null,
  search: '?name=%E2%80%9CTKOP_%E2%80%9D',
  query: 'name=%E2%80%9CTKOP_%E2%80%9D&age=18',
  pathname: '/login',
  path: '/login?name=%E2%80%9CTKOP_%E2%80%9D',        
  href: '/login?name=%E2%80%9CTKOP_%E2%80%9D'
}
Url {
    
    
  protocol: null,
  slashes: null,
  auth: null,
  host: null,
  port: null,
  hostname: null,
  hash: null,
  search: '?name=%E2%80%9CTKOP_%E2%80%9D',
  query: [Object: null prototype] {
    
     name: '“TKOP_”', age: 18 },
  pathname: '/login',
  path: '/login?name=%E2%80%9CTKOP_%E2%80%9D',
  href: '/login?name=%E2%80%9CTKOP_%E2%80%9D'
}TKOP_18

2、post 请求参数

  • post 参数被放置在请求体中进行传输
  • 获取 POST 参数需要使用 data 事件和 end 事件
  • 使用 querystring 系统模块可以将参数转换为对象格式。

既然 post 请求参数是放在请求报文中传输,那我们可以怎么去看到呢?如图是请求报文的 Header 信息的一个表示 post 参数的一个 Form Data 信息栏。
在这里插入图片描述

由图可知其传递的格式也是使用 ‘&’ 符连接的字符串。

<!-- 表单文件 -->
<body>
    <form action="http://localhost:3000/" method="POST">
        <input type="text" name="uname">
        <input type="password" name="psw" id="">
        <button type="submit">提交</button>
    </form>
</body>
// 创建服务的文件
const http = require('http');
const querystring = require('querystring');

const app = http.createServer();
// 声明用来保存参数字符串的变量
let postData = '';
app.on('request', (req, res) => {
    
    
    req.on('data', data => {
    
     // 并不是一次性读完所有参数
        postData += data;
    });
    req.on('end', () => {
    
    
    	// 转为对象格式并输出 
    	console.log(postData);
        console.log(querystring.parse(postData)); 
    });
    res.end('ok');
});
// 端口监听
app.listen(3000);
console.log('服务已开启,监听3000端口,本机访问localhost:3000');
// 控制台输出了包含我输入的两个参数的对象
// uname=TKOP&psw=123456
// [Object: null prototype] { uname: 'TKOP', psw: '123456' }

2.2.2 路由

路由是处理客户端请求信息(例如地址、参数传递方式、参数的值和发送请求的对象等)与服务器端程序代码的对应关系的功能模块。根据请求信息的不同去响应不通的服务端程序代码。通俗的说就是请求什么响应什么。

1、路由的实现原理:使用 url 模块解析请求地址并使用 req.method 获取参数传递方式。根据这两者的值使用 if 判断来执行对应的响应操作。

// 简单实现路由功能
// 	1.获取客户端的请求方式
// 	2.获取客户端的请求地址
const http = require('http');
const url = require('url');
const app = http.createServer();
app.on('request', (req, res) => {
    
    
	// 获取请求方式
	let method = req.method.toLowerCase();
	// 获取请求地址
	let pathname = url.parse(req.url).pathname;

	res.writeHead(200, {
    
    
		'content-type': 'text/html;charset=utf8'
	});
	if (method == 'get') {
    
    
		if (pathname == '/' || pathname == '/index') {
    
    
			res.end('欢迎来到首页')
		}else if (pathname == '/list') {
    
    
			res.end('欢迎来到列表页')
		}else {
    
    
			res.end('您访问的页面不存在')
		}
	}else if (method == 'post') {
    
    
		// ....
	}
});

2、路由可以实现静态资源的获取,例如 html css js (某些不需要处理直接响应给客户端)文件的获取等。下示例代码是将全保存在 public 文件夹下的 html 文件实现了路由获取。但是在获取到相应的 html 文件后执行时,里面外链的其他类型文件(css、js)文件同样需要获取。此时文件的类型是不确定的,所以需要使用第三方模块得到该文件类型(虽然高版本浏览器不需要指定文件类型)。

const http = require('http');
const url = require('url');
const path = require('path');
const fs = require('fs');
const mime = require('mime'); // 判断文件的类型的第三方模块

const app = http.createServer();
app.on('request', (req, res) => {
    
    
	let pathname = url.parse(req.url).pathname;
	pathname = pathname == '/' ? '/default.html' : pathname;
	// 将用户的请求路径转换为实际的服务器硬盘路径
	let realPath = path.join(__dirname, 'public' + pathname);

	let type = mime.getType(realPath)

	// 读取文件
	fs.readFile(realPath, (error, result) => {
    
    
		// 如果文件读取失败
		if (error != null) {
    
    
			res.writeHead(404, {
    
    
				'content-type': 'text/html;charset=utf8'
			})
			res.end('文件读取失败');
			return;
		}
		res.writeHead(200, {
    
    
			'content-type': type
		})

		res.end(result);
	});
});

app.listen(3000);
console.log('服务器启动成功')

3、动态资源(相同的请求地址响应不同的资源)的获取的话需要使用到传递的参数或者配合数据库使用。需要根据上述参数使用 node.js 程序对静态资源的内容进行更改后响应,暂时不去记录。

猜你喜欢

转载自blog.csdn.net/TKOP_/article/details/114640288
今日推荐