上一节,我们介绍了基于NTP服务器获取网络时间的例子,但在有些情况下,比如我最近在使用RNDIS协议通过4G模块上网,这个协议不支持UDP协议,所以就用不了NTP服务器。或者有时候我们需要有更多的网络时间获取方式,以保证系统100%能获取到网络时间。本节就来介绍一下更通用的获取网络时间的方式:HTTP GET。


文章目录

  • 1 HTTP GET原理
  • 1.1 网络中的工作流程
  • 1.2 HTTP GET请求组成部分
  • 1.3 测试
  • 2 代码实现
  • 2.1 实现步骤
  • 2.2 完整代码
  • 3 结果
  • 4 总结


1 HTTP GET原理

本节的原理实际上就是类似浏览器访问网站一样获取网站的数据,只需要找到一个能显示时间的网站就行了。下面就来了解一下使用socket请求HTTP GET网络数据的原理。

1.1 网络中的工作流程

  1. 用户发起请求: 用户通过浏览器或应用向服务器发送一个HTTP GET请求。
  2. 服务器处理请求: 服务器接收到请求后,解析URL和头部信息,根据请求的资源进行处理。
  3. 发送响应: 服务器将请求的数据(如HTML页面、图片等)打包在HTTP响应中返回给客户端。
  4. 客户端处理响应: 客户端(通常是浏览器)接收响应并根据需要渲染或处理数据。

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-aliveclosekeep-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:

Android 11 自动获取网络时间 获取网络时间api_php


然后我们连接这个IP,HTTP端口号一般为80,并发送请求报文:

Android 11 自动获取网络时间 获取网络时间api_http_02


可以看到,我们请求访问网页后,对方返回了HTTP头和网页内容,即我们成功获取了网页中的时间。

2 代码实现

这里在Linux环境下为例对HTTP网络时间进行获取,使用标准的POSIX/BSD套接字编程,这样如果想在单片机中LwIP实现的话,也可以直接使用。

2.1 实现步骤

1、HTTP时间服务端地址

这里以苏宁的时间服务器为例:https://quan.suning.com/getSysTime.do

Android 11 自动获取网络时间 获取网络时间api_网络_03

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头,因为一个服务器上可能托管多个域,服务器通过这个头部信息来确定要访问的具体域。
  • 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 结果

编译后运行:

Android 11 自动获取网络时间 获取网络时间api_服务器_04


可以看到,结果和用TCP客户端上位机获取的一样,我们只需要做一些文本的操作就能获取到当前的时间了。

4 总结

本篇博客介绍了如何在Linux下使用C语言和Socket API发起HTTP GET请求。这个示例程序可以扩展到其他类型的HTTP请求和不同的API服务。如果不想用苏宁的服务器,可以随便请求一个网站和不存在的网页,如果网站用的是nginx的话,访问不存在的网页也会返回一个nginx时间。