上一节,我们介绍了基于NTP服务器获取网络时间的例子,但在有些情况下,比如我最近在使用RNDIS协议通过4G模块上网,这个协议不支持UDP协议,所以就用不了NTP服务器。或者有时候我们需要有更多的网络时间获取方式,以保证系统100%能获取到网络时间。本节就来介绍一下更通用的获取网络时间的方式:HTTP GET。
1 HTTP GET原理
本节的原理实际上就是类似浏览器访问网站一样获取网站的数据,只需要找到一个能显示时间的网站就行了。下面就来了解一下使用socket请求HTTP GET网络数据的原理。
1.1 网络中的工作流程
- 用户发起请求: 用户通过浏览器或应用向服务器发送一个HTTP GET请求。
- 服务器处理请求: 服务器接收到请求后,解析URL和头部信息,根据请求的资源进行处理。
- 发送响应: 服务器将请求的数据(如HTML页面、图片等)打包在HTTP响应中返回给客户端。
- 客户端处理响应: 客户端(通常是浏览器)接收响应并根据需要渲染或处理数据。
1.2 HTTP GET请求组成部分
- 请求行: 包括方法(GET)、请求的URI和HTTP版本。
- 请求头: 包含请求的元数据,如用户代理信息、接受的内容类型等。
- 空行: 请求头后面跟一个空行,表示请求头的结束。
- 请求体: GET请求通常没有请求体,因为请求的数据包含在URI中。
下面是一些常见的HTTP请求头字段,这些字段在HTTP GET请求中经常使用:
1. Host
- 描述:
Host
请求头指定了被请求资源的互联网主机和端口号,它通常由URI提供。由于一个服务器可能寄存多个域名,Host
请求头用来指定请求的是哪个域名。 - 示例:如果请求
http://www.example.com/index.html
,那么Host
请求头将是:
Host: www.example.com
2. User-Agent
- 描述:
User-Agent
请求头包含了一个特征字符串,用于让服务器识别客户端使用的操作系统,浏览器和浏览器版本等信息。这可以帮助服务器提供与设备兼容的响应。 - 示例:一个典型的
User-Agent
请求头可能看起来像这样:
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36
3. Accept
- 描述:
Accept
请求头用于告诉服务器,客户端能够处理哪些媒体类型。服务器可以根据这个头部信息决定返回什么类型的内容。 - 示例:表明客户端可以处理HTML和XML,以及它们的特定版本:
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
4. Accept-Language
- 描述:
Accept-Language
请求头用于告诉服务器,客户端希望优先接收哪种自然语言(如英语、中文等)。服务器可以据此返回相应语言的内容,实现本地化。 - 示例:
Accept-Language: en-US,en;q=0.5
5. Accept-Encoding
- 描述:
Accept-Encoding
请求头用于告诉服务器,客户端支持哪些压缩格式。服务器可以选择一个适合的压缩方法,以减小响应数据的体积,提高传输效率。 - 示例:
Accept-Encoding: gzip, deflate, br
6. Connection
- 描述:
Connection
请求头用于控制客户端和服务器之间的连接管理,常用的值有keep-alive
和close
。keep-alive
告诉服务器保持连接打开,以便未来的请求可以使用同一连接,close
则相反。 - 示例:
Connection: keep-alive
示例: 一个HTTP GET请求的结构
GET /example?page=1 HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
当然,并非所有请求头都是必须的。某些头部信息如 User-Agent、Accept、Accept-Language 等是可选的,这些头部提供了关于客户端偏好和能力的信息,可以帮助服务器更优化地响应请求,但不包含这些头也不会阻止请求的基本功能。只有在特定场景下,例如多域托管、特定内容协商或需管理连接时,才需要特定的请求头。
1.3 测试
我们直接在电脑上用TCP客户端测试一下这样是否可行,首先我们获取服务器的IP:
然后我们连接这个IP,HTTP端口号一般为80,并发送请求报文:
可以看到,我们请求访问网页后,对方返回了HTTP头和网页内容,即我们成功获取了网页中的时间。
2 代码实现
这里在Linux环境下为例对HTTP网络时间进行获取,使用标准的POSIX/BSD套接字编程,这样如果想在单片机中LwIP实现的话,也可以直接使用。
2.1 实现步骤
1、HTTP时间服务端地址
这里以苏宁的时间服务器为例:https://quan.suning.com/getSysTime.do
HTTP的端口一般都是80:
#define SERVER_PORT 80
#define SERVER_HOST "quan.suning.com"
2、创建套接字、解析域名
实际上HTTP也是基于TCP协议实现的,整个过程无非就是建立一个TCP连接,所以首先我们就是创建一个套接字。接着像苏宁这种大网站,一般会根据不同的地区分配不同的IP服务器以分担服务器负担,所以IP在不同地区解析出来都不一样,这里我们做一下域名DNS解析,用gethostbyname
函数将域名解析为IP。
int sockfd;
struct hostent *server;
// 创建socket
sockfd = socket(AF_INET, SOCK_STREAM, 0);
// 获取服务器的地址
server = gethostbyname(SERVER_HOST);
3、建立连接
填充一下服务端结构体,建立连接。
// 填充服务器地址结构体
memset(&serveraddr, 0, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
memcpy(&serveraddr.sin_addr.s_addr, server->h_addr, server->h_length);
serveraddr.sin_port = htons(SERVER_PORT);
// 连接到服务器
connect(sockfd, (struct sockaddr *) &serveraddr, sizeof(serveraddr));
4、请求网页数据并读取
#define REQUEST "GET /getSysTime.do HTTP/1.1\r\nHost: quan.suning.com\r\n" \
"Connection: close\r\n\r\n"
char response[4096];
// 发送GET请求
write(sockfd, REQUEST, strlen(REQUEST);
read(sockfd, response, sizeof(response) - 1) ;
简单分析一下这个请求头:
- Host: quan.suning.com
- 这个头是必需的,它指定了请求发送到的服务器的域名。在HTTP/1.1版本中,每个请求都必须包含
Host
头,因为一个服务器上可能托管多个域,服务器通过这个头部信息来确定要访问的具体域。
- 这个头是必需的,它指定了请求发送到的服务器的域名。在HTTP/1.1版本中,每个请求都必须包含
- Connection: close
- 这个头部控制着连接的管理,
close
的值意味着一旦请求完成后,客户端和服务器之间的连接将关闭,不会用于后续的请求。
- 这个头部控制着连接的管理,
2.2 完整代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netdb.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define SERVER_PORT 80
#define SERVER_HOST "quan.suning.com"
#define REQUEST "GET /getSysTime.do HTTP/1.1\r\nHost: quan.suning.com\r\nConnection: close\r\n\r\n"
int main() {
int sockfd;
struct sockaddr_in serveraddr;
struct hostent *server;
char response[4096];
// 创建socket
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("ERROR opening socket");
exit(1);
}
// 获取服务器的地址
server = gethostbyname(SERVER_HOST);
if (server == NULL) {
fprintf(stderr, "ERROR, no such host\n");
exit(0);
}
// 填充服务器地址结构体
memset(&serveraddr, 0, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
memcpy(&serveraddr.sin_addr.s_addr, server->h_addr, server->h_length);
serveraddr.sin_port = htons(SERVER_PORT);
// 连接到服务器
if (connect(sockfd, (struct sockaddr *) &serveraddr, sizeof(serveraddr)) < 0) {
perror("ERROR connecting");
exit(1);
}
// 发送GET请求
if (write(sockfd, REQUEST, strlen(REQUEST)) < 0) {
perror("ERROR writing to socket");
exit(1);
}
// 读取响应
memset(response, 0, sizeof(response));
if (read(sockfd, response, sizeof(response) - 1) < 0) {
perror("ERROR reading from socket");
exit(1);
}
// 打印响应
printf("%s\n", response);
// 关闭socket
close(sockfd);
return 0;
}
注意,苏宁的网站有流量控制,有时候访问会出现系统忙,一般再请求一次即可,这个自己在代码中判断。
3 结果
编译后运行:
可以看到,结果和用TCP客户端上位机获取的一样,我们只需要做一些文本的操作就能获取到当前的时间了。
4 总结
本篇博客介绍了如何在Linux下使用C语言和Socket API发起HTTP GET请求。这个示例程序可以扩展到其他类型的HTTP请求和不同的API服务。如果不想用苏宁的服务器,可以随便请求一个网站和不存在的网页,如果网站用的是nginx的话,访问不存在的网页也会返回一个nginx时间。