Pyhton 程序获取UTC时间
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import socket
import fcntl
import struct
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
PORT = 12345
def get_iface_ip(ifname):
s = None
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
return socket.inet_ntoa(
fcntl.ioctl(
s.fileno(),
0x8915,
struct.pack("256s", ifname[:15].encode("utf-8"))
)[20:24]
)
except Exception:
return None
finally:
if s:
s.close()
def print_urls():
found = False
seen = set()
try:
for _, ifname in socket.if_nameindex():
if ifname == "lo":
continue
ip = get_iface_ip(ifname)
if not ip or ip in seen:
continue
seen.add(ip)
print(f"{ifname}: http://{ip}:{PORT}")
found = True
except Exception:
pass
if not found:
print(f"localhost: http://127.0.0.1:{PORT}")
def get_time_text():
try:
return os.popen("date").read().strip() or "--"
except Exception:
return "--"
def build_html():
now = get_time_text()
html_tpl = """<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta http-equiv="refresh" content="1">
<title>当前时间</title>
<style>
body {
margin: 0;
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: #0d1117;
color: #e6edf3;
font-family: Arial, Consolas, monospace;
}
.label {
font-size: 32px;
font-weight: 700;
margin-bottom: 12px;
}
.value {
font-size: 48px;
color: #79c0ff;
font-weight: 700;
}
</style>
</head>
<body>
<div class="label">当前时间</div>
<div class="value">__TIME__</div>
</body>
</html>"""
return html_tpl.replace("__TIME__", now)
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path != "/":
self.send_error(404)
return
data = build_html().encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
def log_message(self, format, *args):
pass
if __name__ == "__main__":
print_urls()
ThreadingHTTPServer(("0.0.0.0", PORT), Handler).serve_forever()
h@raspberrypi:~/Music/SSD $ python3 date.py

代码逐行表格解析
| 行号 | 代码 | 解释 |
|---|---|---|
| 1 | #!/usr/bin/env python3 |
Linux 脚本头,表示直接运行这个文件时,用 python3 来解释。 |
| 2 | # -*- coding: utf-8 -*- |
声明源码文件使用 UTF-8 编码,方便写中文。 |
| 3 | 空行 | 只是分隔,没有语法作用。 |
| 4 | import os |
导入 os 模块,这里用来执行 date 命令。 |
| 5 | import socket |
导入 socket 模块,这里用来获取网卡名、IP、启动 HTTP 服务。 |
| 6 | import fcntl |
导入 fcntl 模块,这里用 ioctl 获取指定网卡的 IP 地址。 |
| 7 | import struct |
导入 struct 模块,这里把网卡名打包成二进制数据给 ioctl。 |
| 8 | from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer |
从标准库里导入 HTTP 请求处理类和多线程 HTTP 服务器类。 |
| 9 | 空行 | 分隔。 |
| 10 | PORT = 1234 |
定义端口号常量,后面服务监听 1234 端口。 |
| 11 | 空行 | 分隔。 |
| 12 | 空行 | 分隔。 |
| 13 | def get_iface_ip(ifname): |
定义函数:根据网卡名获取它的 IPv4 地址。 |
| 14 | s = None |
先定义变量 s,后面在 finally 里方便安全关闭。 |
| 15 | try: |
开始异常保护代码块。 |
| 16 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) |
创建一个 IPv4 的 UDP socket,仅用于查询网卡信息,不是用来通信。 |
| 17 | return socket.inet_ntoa( |
把二进制格式 IP 地址转成字符串格式。 |
| 18 | fcntl.ioctl( |
调用底层 ioctl,向系统查询网卡地址。 |
| 19 | s.fileno(), |
获取 socket 的文件描述符。 |
| 20 | 0x8915, |
这是 Linux 的 SIOCGIFADDR 命令号,意思是"获取网卡地址"。 |
| 21 | struct.pack("256s", ifname[:15].encode("utf-8")) |
把网卡名编码成固定长度的二进制结构,传给 ioctl。只取前 15 个字符。 |
| 22 | )[20:24] |
从系统返回的二进制结果里截出 IPv4 地址那 4 个字节。 |
| 23 | ) |
结束 inet_ntoa(...)。 |
| 24 | except Exception: |
如果前面任何一步失败,就进这里。 |
| 25 | return None |
获取失败时返回 None。 |
| 26 | finally: |
无论成功还是失败,最后都执行这里。 |
| 27 | if s: |
如果 socket 已经创建成功。 |
| 28 | s.close() |
关闭 socket,释放资源。 |
| 29 | 空行 | 分隔。 |
| 30 | 空行 | 分隔。 |
| 31 | def print_urls(): |
定义函数:启动时打印访问地址。 |
| 32 | found = False |
标记是否找到有效 IP。 |
| 33 | seen = set() |
建立一个集合,用来给 IP 去重。 |
| 34 | 空行 | 分隔。 |
| 35 | try: |
开始异常保护。 |
| 36 | for _, ifname in socket.if_nameindex(): |
遍历系统所有网卡。返回值是 (编号, 网卡名),这里只取网卡名。 |
| 37 | if ifname == "lo": |
如果当前网卡是回环网卡 lo。 |
| 38 | continue |
跳过本轮循环,继续看下一块网卡。 |
| 39 | 空行 | 分隔。 |
| 40 | ip = get_iface_ip(ifname) |
调用前面的函数,读取当前网卡的 IP。 |
| 41 | if not ip or ip in seen: |
如果没读到 IP,或者这个 IP 已经打印过了。 |
| 42 | continue |
跳过当前网卡。 |
| 43 | 空行 | 分隔。 |
| 44 | seen.add(ip) |
把这个 IP 放入去重集合。 |
| 45 | print(f"{ifname}: http://{ip}:{PORT}") |
在终端打印访问地址,比如 eth0: http://192.168.1.20:1234。 |
| 46 | found = True |
说明已经找到至少一个可用地址。 |
| 47 | except Exception: |
如果遍历网卡时报错。 |
| 48 | pass |
忽略异常,不让程序退出。 |
| 49 | 空行 | 分隔。 |
| 50 | if not found: |
如果没有找到任何有效网卡地址。 |
| 51 | print(f"localhost: http://127.0.0.1:{PORT}") |
打印回环地址作为兜底。 |
| 52 | 空行 | 分隔。 |
| 53 | 空行 | 分隔。 |
| 54 | def get_time_text(): |
定义函数:读取当前系统时间。 |
| 55 | try: |
开始异常保护。 |
| 56 | return os.popen("date").read().strip() or "--" |
执行 date 命令,读结果,去掉首尾空白;如果结果为空,就返回 "--"。 |
| 57 | except Exception: |
如果执行命令失败。 |
| 58 | return "--" |
失败时返回占位符。 |
| 59 | 空行 | 分隔。 |
| 60 | 空行 | 分隔。 |
| 61 | def build_html(): |
定义函数:拼接整个 HTML 页面。 |
| 62 | now = get_time_text() |
先读取当前时间。 |
| 63 | 空行 | 分隔。 |
| 64 | html_tpl = """<!doctype html> |
定义一个普通的多行 HTML 模板字符串,不是 f-string。 |
| 65 | <html lang="zh-CN"> |
HTML 根节点,语言设为中文。 |
| 66 | <head> |
HTML 头部开始。 |
| 67 | <meta charset="utf-8"> |
指定网页字符编码为 UTF-8。 |
| 68 | <meta name="viewport" content="width=device-width,initial-scale=1"> |
移动端适配,让页面宽度等于设备宽度。 |
| 69 | <meta http-equiv="refresh" content="1"> |
页面每 1 秒自动刷新一次。 |
| 70 | <title>当前时间</title> |
浏览器标签页显示的标题。不是页面正文内容。 |
| 71 | <style> |
CSS 样式开始。 |
| 72 | body { |
开始写 body 样式。注意这里是普通字符串,所以可以直接写单个 {}。 |
| 73 | margin: 0; |
页面外边距为 0。 |
| 74 | height: 100vh; |
页面高度等于整个浏览器视口高度。 |
| 75 | display: flex; |
使用 flex 布局。 |
| 76 | flex-direction: column; |
纵向排列内容。 |
| 77 | align-items: center; |
横向居中。 |
| 78 | justify-content: center; |
纵向居中。 |
| 79 | background: #0d1117; |
背景色。 |
| 80 | color: #e6edf3; |
默认文字颜色。 |
| 81 | font-family: Arial, Consolas, monospace; |
页面默认字体。 |
| 82 | } |
body 样式结束。 |
| 83 | .label { |
开始写标题文字样式。 |
| 84 | font-size: 32px; |
标题字号 32 像素。 |
| 85 | font-weight: 700; |
标题加粗。 |
| 86 | margin-bottom: 12px; |
标题和下面时间值之间留 12 像素间距。 |
| 87 | } |
.label 样式结束。 |
| 88 | .value { |
开始写时间数值样式。 |
| 89 | font-size: 48px; |
时间值字号 48 像素。 |
| 90 | color: #79c0ff; |
时间值颜色蓝色。 |
| 91 | font-weight: 700; |
时间值加粗。 |
| 92 | } |
.value 样式结束。 |
| 93 | </style> |
CSS 样式结束。 |
| 94 | </head> |
HTML 头部结束。 |
| 95 | <body> |
HTML 正文开始。 |
| 96 | <div class="label">当前时间</div> |
页面正文里显示标题"当前时间"。 |
| 97 | <div class="value">__TIME__</div> |
页面正文里显示占位符 __TIME__,后面会被替换成真实时间。 |
| 98 | </body> |
HTML 正文结束。 |
| 99 | </html>""" |
HTML 文档结束,同时结束这个多行字符串。 |
| 100 | 空行 | 分隔。 |
| 101 | return html_tpl.replace("__TIME__", now) |
把模板里的 __TIME__ 替换成真实时间后返回。 |
| 102 | 空行 | 分隔。 |
| 103 | 空行 | 分隔。 |
| 104 | class Handler(BaseHTTPRequestHandler): |
定义 HTTP 请求处理类,继承标准库的请求处理基类。 |
| 105 | def do_GET(self): |
浏览器发 GET 请求时,服务器会自动调用这个方法。 |
| 106 | if self.path != "/": |
如果访问路径不是根路径 /。 |
| 107 | self.send_error(404) |
返回 404 错误。 |
| 108 | return |
结束这次请求处理。 |
| 109 | 空行 | 分隔。 |
| 110 | data = build_html().encode("utf-8") |
生成 HTML 字符串,并编码成 UTF-8 字节。HTTP 发送的是字节,不是普通字符串。 |
| 111 | self.send_response(200) |
返回 HTTP 状态码 200,表示成功。 |
| 112 | self.send_header("Content-Type", "text/html; charset=utf-8") |
告诉浏览器返回内容类型是 UTF-8 HTML。 |
| 113 | self.send_header("Content-Length", str(len(data))) |
告诉浏览器正文有多少字节。 |
| 114 | self.end_headers() |
结束 HTTP 头部。 |
| 115 | self.wfile.write(data) |
把 HTML 字节数据写给浏览器。 |
| 116 | 空行 | 分隔。 |
| 117 | def log_message(self, format, *args): |
重写默认日志函数。 |
| 118 | pass |
什么都不做,这样访问网页时终端不会刷日志。 |
| 119 | 空行 | 分隔。 |
| 120 | 空行 | 分隔。 |
| 121 | if __name__ == "__main__": |
只有直接运行这个脚本时,下面两行才会执行。 |
| 122 | print_urls() |
启动服务前先打印访问地址。 |
| 123 | ThreadingHTTPServer(("0.0.0.0", PORT), Handler).serve_forever() |
启动多线程 HTTP 服务器,监听所有网卡地址,永久运行。 |
这版和你之前报错版的核心区别
| 项目 | 之前报错版 | 现在这版 |
|---|---|---|
| HTML 写法 | return f"""...""" |
普通三引号字符串 |
| CSS 大括号 | 必须写成 {``{ }} |
直接写 { } |
| 插值方式 | {变量} |
.replace("__TIME__", now) |
| 出错点 | 容易被单个 } 搞炸 |
基本不会有这个问题 |
你现在最常需要改的地方
| 你要改什么 | 改哪一行 |
|---|---|
| 端口号 | 第 10 行 |
| 自动刷新秒数 | 第 69 行 |
| 浏览器标签标题 | 第 70 行 |
| 页面正文标题"当前时间" | 第 96 行 |
| 时间占位符名字 | 第 97、101 行,要对应一致 |
| 页面背景色 | 第 79 行 |
| 标题字号 | 第 84 行 |
| 时间值字号 | 第 89 行 |
| 终端打印 URL 逻辑 | 第 31 到 51 行 |
程序执行顺序
| 顺序 | 发生什么 |
|---|---|
| 1 | 程序启动,进入第 121 行 |
| 2 | 调用 print_urls() 打印访问地址 |
| 3 | 启动 HTTP 服务,开始监听 1234 端口 |
| 4 | 浏览器访问 / 时,触发 do_GET() |
| 5 | build_html() 被调用 |
| 6 | build_html() 里调用 get_time_text() 取当前时间 |
| 7 | 时间替换进 HTML 模板 |
| 8 | HTML 编码成字节并返回给浏览器 |
| 9 | 浏览器每 1 秒刷新一次,再重复步骤 4 到 8 |
C 语言显示树莓派UTC时间 自带 CPU 温度
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <ifaddrs.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define PORT 1234
#define BUF_SIZE 8192
void get_temp(char *out, size_t size) {
FILE *fp = popen("vcgencmd measure_temp 2>/dev/null", "r");
if (!fp) {
snprintf(out, size, "--");
return;
}
char buf[128] = {0};
if (!fgets(buf, sizeof(buf), fp)) {
pclose(fp);
snprintf(out, size, "--");
return;
}
pclose(fp);
buf[strcspn(buf, "\r\n")] = 0;
if (strncmp(buf, "temp=", 5) == 0) {
snprintf(out, size, "%s", buf + 5);
} else {
snprintf(out, size, "--");
}
}
void get_time_text(char *out, size_t size) {
FILE *fp = popen("date", "r");
if (!fp) {
snprintf(out, size, "--");
return;
}
if (!fgets(out, size, fp)) {
pclose(fp);
snprintf(out, size, "--");
return;
}
pclose(fp);
out[strcspn(out, "\r\n")] = 0;
}
void print_urls() {
struct ifaddrs *ifaddr, *ifa;
char ip[INET_ADDRSTRLEN];
int found = 0;
if (getifaddrs(&ifaddr) == -1) {
perror("getifaddrs");
return;
}
for (ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next) {
if (!ifa->ifa_addr) continue;
if (ifa->ifa_addr->sa_family != AF_INET) continue;
if (strcmp(ifa->ifa_name, "lo") == 0) continue;
struct sockaddr_in *sa = (struct sockaddr_in *)ifa->ifa_addr;
if (!inet_ntop(AF_INET, &sa->sin_addr, ip, sizeof(ip))) continue;
printf("%s: http://%s:%d\n", ifa->ifa_name, ip, PORT);
found = 1;
}
if (!found) {
printf("localhost: http://127.0.0.1:%d\n", PORT);
}
freeifaddrs(ifaddr);
}
void send_response(int client_fd) {
char temp[64];
char now[128];
char body[4096];
char response[BUF_SIZE];
get_temp(temp, sizeof(temp));
get_time_text(now, sizeof(now));
snprintf(body, sizeof(body),
"<!doctype html>"
"<html lang=\"zh-CN\">"
"<head>"
"<meta charset=\"utf-8\">"
"<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">"
"<meta http-equiv=\"refresh\" content=\"1\">"
"<title>温度和时间</title>"
"<style>"
"body{"
"margin:0;"
"height:100vh;"
"display:flex;"
"flex-direction:column;"
"align-items:center;"
"justify-content:center;"
"gap:28px;"
"background:#0d1117;"
"color:#e6edf3;"
"font-family:Arial,Consolas,monospace;"
"}"
".label{"
"font-size:32px;"
"font-weight:700;"
"margin-bottom:8px;"
"}"
".value{"
"font-size:64px;"
"color:#79c0ff;"
"font-weight:700;"
"}"
".block{"
"text-align:center;"
"}"
"</style>"
"</head>"
"<body>"
"<div class=\"block\">"
"<div class=\"label\">当前 CPU 温度</div>"
"<div class=\"value\">%s</div>"
"</div>"
"<div class=\"block\">"
"<div class=\"label\">当前时间</div>"
"<div class=\"value\" style=\"font-size:40px;\">%s</div>"
"</div>"
"</body>"
"</html>",
temp, now
);
snprintf(response, sizeof(response),
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/html; charset=utf-8\r\n"
"Content-Length: %zu\r\n"
"Connection: close\r\n"
"\r\n"
"%s",
strlen(body), body
);
send(client_fd, response, strlen(response), 0);
}
int main() {
int server_fd, client_fd;
struct sockaddr_in addr;
int opt = 1;
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd < 0) {
perror("socket");
return 1;
}
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(PORT);
if (bind(server_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
perror("bind");
close(server_fd);
return 1;
}
if (listen(server_fd, 5) < 0) {
perror("listen");
close(server_fd);
return 1;
}
print_urls();
while (1) {
client_fd = accept(server_fd, NULL, NULL);
if (client_fd < 0) continue;
char req[1024];
recv(client_fd, req, sizeof(req) - 1, 0);
send_response(client_fd);
close(client_fd);
}
close(server_fd);
return 0;
}
h@raspberrypi:~/Music/c-web/tem $ gcc temp_web.c -o temp_web
h@raspberrypi:~/Music/c-web/tem $ ./temp_web

程序解析
一、头文件和宏定义
| 行号 | 代码 | 说明 |
|---|---|---|
| 1 | #include <stdio.h> |
引入标准输入输出库。这里会用到 printf、snprintf、FILE、popen、fgets。 |
| 2 | #include <stdlib.h> |
引入标准库。这里主要是为了让一些通用函数声明完整。 |
| 3 | #include <string.h> |
引入字符串处理库。这里会用到 strncmp、strcmp、strlen、strcspn、memset。 |
| 4 | #include <unistd.h> |
引入 Unix/Linux 常用函数。这里会用到 close。 |
| 5 | #include <ifaddrs.h> |
引入网卡地址相关结构和函数。这里会用到 getifaddrs、freeifaddrs。 |
| 6 | #include <arpa/inet.h> |
引入 IP 地址转换相关函数。这里会用到 inet_ntop。 |
| 7 | #include <sys/socket.h> |
引入 socket 网络编程函数。这里会用到 socket、bind、listen、accept、send、recv。 |
| 8 | #include <netinet/in.h> |
引入 sockaddr_in 这种 IPv4 地址结构体。 |
| 9 | 空行 | 只是为了排版清楚,没有语法作用。 |
| 10 | #define PORT 1234 |
宏定义端口号,后面写 PORT 就等于写 1234。 |
| 11 | #define BUF_SIZE 8192 |
宏定义响应缓冲区大小为 8192 字节。 |
| 12 | 空行 | 只是分隔代码块。 |
二、读取 CPU 温度函数
| 行号 | 代码 | 说明 |
|---|---|---|
| 13 | void get_temp(char *out, size_t size) { |
定义函数 get_temp。void 表示这个函数本身不返回值。char *out 是把结果写到外面传进来的字符数组里。size_t size 是这个数组有多大。 |
| 14 | FILE *fp = popen("vcgencmd measure_temp 2>/dev/null", "r"); |
popen 执行 shell 命令,并把命令输出当成文件读。fp 是文件指针。"r" 表示只读。2>/dev/null 表示把错误输出丢弃。 |
| 15 | if (!fp) { |
如果 fp 是空指针,说明命令执行失败。!fp 就是"没有成功打开"。 |
| 16 | snprintf(out, size, "--"); |
把 "--" 写到输出数组里,表示失败时的占位值。snprintf 比 sprintf 安全,因为它知道数组最大长度。 |
| 17 | return; |
立刻结束这个函数。 |
| 18 | } |
结束 if 代码块。 |
| 19 | 空行 | 只是排版。 |
| 20 | char buf[128] = {0}; |
定义一个 128 字节的字符数组 buf,并全部初始化为 0。用来临时接命令输出。 |
| 21 | if (!fgets(buf, sizeof(buf), fp)) { |
从 fp 里读一行到 buf。如果读不到,进入失败处理。sizeof(buf) 是数组总长度。 |
| 22 | pclose(fp); |
关闭 popen 打开的"命令文件"。和普通 fclose 不一样,popen 配套的是 pclose。 |
| 23 | snprintf(out, size, "--"); |
读失败也写 "--"。 |
| 24 | return; |
结束函数。 |
| 25 | } |
结束这个 if。 |
| 26 | pclose(fp); |
正常读完后也要关闭文件指针。 |
| 27 | 空行 | 排版。 |
| 28 | buf[strcspn(buf, "\r\n")] = 0; |
去掉行末换行符。strcspn(buf, "\r\n") 会找到 \r 或 \n 第一次出现的位置,然后把那里改成字符串结束符 0。 |
| 29 | if (strncmp(buf, "temp=", 5) == 0) { |
比较 buf 的前 5 个字符是不是 "temp="。如果是,就说明 vcgencmd 输出格式正常。 |
| 30 | snprintf(out, size, "%s", buf + 5); |
把 buf 从第 6 个字符开始的内容复制到 out。buf + 5 就是跳过 "temp="。例如 temp=84.0'C 会变成 84.0'C。 |
| 31 | } else { |
如果前缀不是 "temp=",走这里。 |
| 32 | snprintf(out, size, "--"); |
不符合预期格式,也返回 "--"。 |
| 33 | } |
结束 else。 |
| 34 | } |
结束 get_temp 函数。 |
| 35 | 空行 | 分隔函数。 |
三、读取时间函数
| 行号 | 代码 | 说明 |
|---|---|---|
| 36 | void get_time_text(char *out, size_t size) { |
定义读取当前时间的函数,结果也写到外部字符数组里。 |
| 37 | FILE *fp = popen("date", "r"); |
执行 date 命令,读取当前系统时间。 |
| 38 | if (!fp) { |
如果命令执行失败。 |
| 39 | snprintf(out, size, "--"); |
写入失败占位值。 |
| 40 | return; |
结束函数。 |
| 41 | } |
结束 if。 |
| 42 | 空行 | 排版。 |
| 43 | if (!fgets(out, size, fp)) { |
直接把 date 输出读进 out。 |
| 44 | pclose(fp); |
读失败时先关闭。 |
| 45 | snprintf(out, size, "--"); |
失败占位值。 |
| 46 | return; |
结束函数。 |
| 47 | } |
结束这个 if。 |
| 48 | pclose(fp); |
正常读完后关闭命令句柄。 |
| 49 | 空行 | 排版。 |
| 50 | out[strcspn(out, "\r\n")] = 0; |
去掉读进来的换行符。否则网页上可能多一行空白。 |
| 51 | } |
结束 get_time_text 函数。 |
| 52 | 空行 | 分隔函数。 |
四、打印访问地址函数
| 行号 | 代码 | 说明 |
|---|---|---|
| 53 | void print_urls() { |
定义打印访问地址的函数。 |
| 54 | struct ifaddrs *ifaddr, *ifa; |
定义两个网卡链表指针。ifaddr 指向整条链表开头,ifa 用来遍历。 |
| 55 | char ip[INET_ADDRSTRLEN]; |
定义字符串数组,保存转换后的 IPv4 地址。INET_ADDRSTRLEN 是系统给的 IPv4 字符串最大长度。 |
| 56 | int found = 0; |
标记有没有找到可用地址。0 表示没找到,1 表示找到了。 |
| 57 | 空行 | 排版。 |
| 58 | if (getifaddrs(&ifaddr) == -1) { |
获取系统所有网卡地址信息。如果返回 -1 就失败。注意这里传的是 &ifaddr,因为函数要修改这个指针。 |
| 59 | perror("getifaddrs"); |
打印错误信息,前缀是 "getifaddrs"。 |
| 60 | return; |
获取失败就直接返回。 |
| 61 | } |
结束 if。 |
| 62 | 空行 | 排版。 |
| 63 | for (ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next) { |
遍历整个网卡地址链表。ifa = ifa->ifa_next 就是走到下一个节点。 |
| 64 | if (!ifa->ifa_addr) continue; |
如果当前节点没有地址信息,直接跳过这一次循环。continue 表示进入下一轮。 |
| 65 | if (ifa->ifa_addr->sa_family != AF_INET) continue; |
如果不是 IPv4 地址,就跳过。AF_INET 表示 IPv4。 |
| 66 | if (strcmp(ifa->ifa_name, "lo") == 0) continue; |
如果网卡名是 lo,也跳过。lo 是本地回环接口。 |
| 67 | 空行 | 排版。 |
| 68 | struct sockaddr_in *sa = (struct sockaddr_in *)ifa->ifa_addr; |
把通用地址指针强制转换成 IPv4 专用结构体指针,这样后面才能取 sin_addr。 |
| 69 | if (!inet_ntop(AF_INET, &sa->sin_addr, ip, sizeof(ip))) continue; |
把二进制 IP 地址转成字符串,例如 192.168.1.100。如果失败就跳过。 |
| 70 | 空行 | 排版。 |
| 71 | printf("%s: http://%s:%d\n", ifa->ifa_name, ip, PORT); |
打印一行访问地址,比如 eth0: http://192.168.1.20:1234。 |
| 72 | found = 1; |
说明已经找到至少一个可用地址。 |
| 73 | } |
结束 for 循环。 |
| 74 | 空行 | 排版。 |
| 75 | if (!found) { |
如果一个地址都没找到。 |
| 76 | printf("localhost: http://127.0.0.1:%d\n", PORT); |
那就退回打印本机地址。 |
| 77 | } |
结束 if。 |
| 78 | 空行 | 排版。 |
| 79 | freeifaddrs(ifaddr); |
释放 getifaddrs 分配的链表内存。这个不能漏。 |
| 80 | } |
结束 print_urls 函数。 |
| 81 | 空行 | 分隔函数。 |
五、拼网页并发送 HTTP 响应
| 行号 | 代码 | 说明 |
|---|---|---|
| 82 | void send_response(int client_fd) { |
定义发送网页响应的函数。参数 client_fd 是客户端 socket 文件描述符。 |
| 83 | char temp[64]; |
定义字符数组,保存温度文本。 |
| 84 | char now[128]; |
定义字符数组,保存时间文本。 |
| 85 | char body[4096]; |
定义字符数组,保存 HTML 正文。 |
| 86 | char response[BUF_SIZE]; |
定义字符数组,保存完整 HTTP 响应。 |
| 87 | 空行 | 排版。 |
| 88 | get_temp(temp, sizeof(temp)); |
调用前面写的函数,读取温度到 temp。 |
| 89 | get_time_text(now, sizeof(now)); |
读取当前时间到 now。 |
| 90 | 空行 | 排版。 |
| 91 | snprintf(body, sizeof(body), |
开始把 HTML 页面内容格式化写进 body 数组。 |
| 92 | "<!doctype html>" |
HTML 文档类型声明。 |
| 93 | "<html lang=\"zh-CN\">" |
HTML 根标签,语言设为中文。C 字符串里双引号要写成 \"。 |
| 94 | "<head>" |
HTML 头部开始。 |
| 95 | "<meta charset=\"utf-8\">" |
指定字符编码 UTF-8。 |
| 96 | "<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">" |
手机浏览器自适应宽度。 |
| 97 | "<meta http-equiv=\"refresh\" content=\"1\">" |
网页每 1 秒自动刷新一次。 |
| 98 | "<title>温度和时间</title>" |
浏览器标签页标题。不是页面正文。 |
| 99 | "<style>" |
CSS 样式开始。 |
| 100 | "body{" |
开始写 body 的 CSS。 |
| 101 | "margin:0;" |
页面外边距设为 0。 |
| 102 | "height:100vh;" |
页面高度设为整个视口高度。 |
| 103 | "display:flex;" |
使用 flex 布局。 |
| 104 | "flex-direction:column;" |
flex 布局方向为纵向,从上到下。 |
| 105 | "align-items:center;" |
横向居中。 |
| 106 | "justify-content:center;" |
纵向居中。 |
| 107 | "gap:28px;" |
上下两个块之间留 28 像素间距。 |
| 108 | "background:#0d1117;" |
背景颜色。 |
| 109 | "color:#e6edf3;" |
默认文字颜色。 |
| 110 | "font-family:Arial,Consolas,monospace;" |
字体设置。 |
| 111 | "}" |
body 样式结束。 |
| 112 | ".label{" |
.label 类样式开始。 |
| 113 | "font-size:32px;" |
标题字号 32 像素。 |
| 114 | "font-weight:700;" |
字体加粗。 |
| 115 | "margin-bottom:8px;" |
标题下方留 8 像素空隙。 |
| 116 | "}" |
.label 样式结束。 |
| 117 | ".value{" |
.value 类样式开始。 |
| 118 | "font-size:64px;" |
数值字号 64 像素。 |
| 119 | "color:#79c0ff;" |
数值颜色蓝色。 |
| 120 | "font-weight:700;" |
数值加粗。 |
| 121 | "}" |
.value 样式结束。 |
| 122 | ".block{" |
.block 类样式开始。 |
| 123 | "text-align:center;" |
块内文字居中。 |
| 124 | "}" |
.block 样式结束。 |
| 125 | "</style>" |
CSS 样式结束。 |
| 126 | "</head>" |
头部结束。 |
| 127 | "<body>" |
正文开始。 |
| 128 | "<div class=\"block\">" |
第一个信息块开始。 |
| 129 | "<div class=\"label\">当前 CPU 温度</div>" |
第一个块的标题。 |
| 130 | "<div class=\"value\">%s</div>" |
第一个块的值占位符 %s,后面会用 temp 替换。 |
| 131 | "</div>" |
第一个块结束。 |
| 132 | "<div class=\"block\">" |
第二个信息块开始。 |
| 133 | "<div class=\"label\">当前时间</div>" |
第二个块的标题。 |
| 134 | "<div class=\"value\" style=\"font-size:40px;\">%s</div>" |
第二个块的值占位符 %s,后面用 now 替换。这里还额外写了行内样式,把时间字号改成 40 像素。 |
| 135 | "</div>" |
第二个块结束。 |
| 136 | "</body>" |
正文结束。 |
| 137 | "</html>", |
HTML 结束。这里逗号表示 snprintf 后面还要继续接参数。 |
| 138 | temp, now |
用 temp 替换第一个 %s,用 now 替换第二个 %s。 |
| 139 | ); |
结束这次 snprintf。 |
| 140 | 空行 | 排版。 |
| 141 | snprintf(response, sizeof(response), |
开始拼完整 HTTP 响应。 |
| 142 | "HTTP/1.1 200 OK\r\n" |
HTTP 状态行,表示请求成功。 |
| 143 | "Content-Type: text/html; charset=utf-8\r\n" |
告诉浏览器返回的是 UTF-8 编码 HTML。 |
| 144 | "Content-Length: %zu\r\n" |
告诉浏览器正文长度是多少字节。%zu 用来打印 size_t 类型。 |
| 145 | "Connection: close\r\n" |
告诉浏览器这次响应完就关闭连接。 |
| 146 | "\r\n" |
HTTP 头和正文之间必须有一个空行。 |
| 147 | "%s", |
把 HTML 正文拼进去。 |
| 148 | strlen(body), body |
用 strlen(body) 替换 %zu,用 body 替换 %s。 |
| 149 | ); |
结束 snprintf。 |
| 150 | 空行 | 排版。 |
| 151 | send(client_fd, response, strlen(response), 0); |
通过客户端 socket 把整个 HTTP 响应发送出去。 |
| 152 | } |
结束 send_response 函数。 |
| 153 | 空行 | 分隔函数。 |
六、主函数 main
| 行号 | 代码 | 说明 |
|---|---|---|
| 154 | int main() { |
主函数入口。int 表示最后返回一个整数给系统。 |
| 155 | int server_fd, client_fd; |
定义两个文件描述符变量。server_fd 是服务器 socket,client_fd 是接受到的客户端 socket。 |
| 156 | struct sockaddr_in addr; |
定义 IPv4 地址结构体,用来保存绑定地址和端口。 |
| 157 | int opt = 1; |
定义一个选项值 1,后面设置 socket 参数时会用。 |
| 158 | 空行 | 排版。 |
| 159 | server_fd = socket(AF_INET, SOCK_STREAM, 0); |
创建一个 TCP socket。AF_INET 表示 IPv4,SOCK_STREAM 表示 TCP。 |
| 160 | if (server_fd < 0) { |
如果返回值小于 0,说明创建失败。 |
| 161 | perror("socket"); |
打印错误信息。 |
| 162 | return 1; |
程序异常退出,返回 1。 |
| 163 | } |
结束 if。 |
| 164 | 空行 | 排版。 |
| 165 | setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); |
设置 socket 选项,允许地址快速复用,避免程序重启时提示端口被占用。 |
| 166 | 空行 | 排版。 |
| 167 | memset(&addr, 0, sizeof(addr)); |
先把 addr 结构体全部清零,避免里面有脏数据。 |
| 168 | addr.sin_family = AF_INET; |
指定地址族为 IPv4。 |
| 169 | addr.sin_addr.s_addr = INADDR_ANY; |
绑定到所有本机网卡地址,相当于 0.0.0.0。 |
| 170 | addr.sin_port = htons(PORT); |
设置端口号。htons 把主机字节序转成网络字节序。 |
| 171 | 空行 | 排版。 |
| 172 | if (bind(server_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { |
把 socket 绑定到指定地址和端口。这里把 sockaddr_in* 强制转成 sockaddr*,因为 bind 需要通用类型。 |
| 173 | perror("bind"); |
打印绑定失败原因。 |
| 174 | close(server_fd); |
关闭服务器 socket,防止资源泄露。 |
| 175 | return 1; |
异常退出。 |
| 176 | } |
结束 if。 |
| 177 | 空行 | 排版。 |
| 178 | if (listen(server_fd, 5) < 0) { |
开始监听端口。5 是等待队列长度。 |
| 179 | perror("listen"); |
打印监听失败原因。 |
| 180 | close(server_fd); |
关闭 socket。 |
| 181 | return 1; |
异常退出。 |
| 182 | } |
结束 if。 |
| 183 | 空行 | 排版。 |
| 184 | print_urls(); |
打印所有可访问地址。程序启动后你在终端看到的地址就是这里来的。 |
| 185 | 空行 | 排版。 |
| 186 | while (1) { |
无限循环。1 永远为真,所以程序一直运行。 |
| 187 | client_fd = accept(server_fd, NULL, NULL); |
等待浏览器连接。一旦有人访问,就返回一个客户端 socket。 |
| 188 | if (client_fd < 0) continue; |
如果接受连接失败,直接开始下一轮循环。 |
| 189 | 空行 | 排版。 |
| 190 | char req[1024]; |
定义一个 1024 字节数组,简单接收浏览器发来的请求数据。 |
| 191 | recv(client_fd, req, sizeof(req) - 1, 0); |
从客户端读请求内容。这里其实没认真解析 HTTP,只是读一下,避免某些客户端直接断掉。 |
| 192 | send_response(client_fd); |
调用前面的函数,把网页响应发送给浏览器。 |
| 193 | close(client_fd); |
响应完成后关闭这个客户端连接。 |
| 194 | } |
结束 while 循环体。由于是无限循环,正常情况下不会跳出来。 |
| 195 | 空行 | 排版。 |
| 196 | close(server_fd); |
关闭服务器 socket。实际上这里通常执行不到,因为上面是死循环。 |
| 197 | return 0; |
正常结束程序返回 0。 |
| 198 | } |
结束 main 函数。 |
七、你最该先看懂的 10 个语法点
| 语法 | 例子 | 意思 |
|---|---|---|
| 指针 | char *out |
out 不是普通字符,而是"指向字符数组开头"的地址。 |
| 数组 | char buf[128] |
分配 128 个字符空间。 |
| 取地址 | &addr |
取变量 addr 的内存地址。 |
| 结构体成员 | addr.sin_port |
取结构体里的某个字段。 |
| 指针访问结构体成员 | ifa->ifa_name |
ifa 是指针,所以用 -> 访问字段。 |
| 强制类型转换 | (struct sockaddr_in *)ifa->ifa_addr |
告诉编译器"把这个地址按 sockaddr_in* 来解释"。 |
| 条件判断 | if (...) { ... } |
如果条件成立就执行大括号里的代码。 |
| 提前结束函数 | return; |
立刻退出当前函数。 |
| 跳过本次循环 | continue; |
不再执行本轮后面的代码,直接进入下一轮循环。 |
| 无限循环 | while (1) |
一直循环,除非程序被你手动停止。 |
八、如果你现在只想自己改字,不想碰逻辑
你平时最常改的其实就这几块:
| 你要改什么 | 改哪一行 |
|---|---|
| 端口号 | 第 10 行 #define PORT 1234 |
| 网页标题 | 第 98 行 <title>温度和时间</title> |
| 页面显示"当前 CPU 温度"这几个字 | 第 129 行 |
| 页面显示"当前时间"这几个字 | 第 133 行 |
| 自动刷新秒数 | 第 97 行 content="1" |
| 温度颜色 | 第 119 行 color:#79c0ff; |
| 背景色 | 第 108 行 background:#0d1117; |
| 时间字体大小 | 第 134 行 font-size:40px; |