HTTP Sweet HTTP (与 Gastona)






4.87/5 (22投票s)
人人皆宜的 HTTP。
引言
众所周知,HTTP是Web的协议,但很多人可能不知道的是,HTTP的基本思想非常简单,可以用于许多其他与互联网无关的应用程序。
如今,此类应用程序的一个例子,并且非常有用,就是通信连接到家庭WLAN的设备。例如,PC与智能手机、平板电脑等。
在本文中,我们将展示如何做到这一点,为此,我们将实现一些基本但非常有说服力的功能,例如获取PC的屏幕截图、交换文件(下载和上传)以及将从设备写入PC的笔记保存下来。
演示使用了一个方便且小巧的HTTP服务器,但它们在几个方面与典型的Web应用程序不同。考虑到要在诸如家庭WLAN之类的安全网络中运行,因此无需处理安全问题。同样,也正因为如此,我们可以让服务器扮演我们想要的任何角色,从而完全控制它,而这在普通的Web应用程序中是不可能实现的。
HTTP 简述
尽管HTTP规范相当长,包含许多规则和功能,但我们只需要了解其中一小部分但基本的部分。部分原因是像文件传输这样的许多事情已经在我们将要使用的服务器和客户端中实现了,部分原因是像这些“家庭”应用程序中的“家庭”应用程序,其设计比为许多用户设计的通用产品具有更大的自由度和灵活性。最后,这可能是该协议的基础,其简单性和固有的灵活性是HTTP最强大的部分。
TCP/IP 基础通信
HTTP的基础是TCP/IP,它是一种点对点和客户端-服务器通信协议。网络中的每个点(例如PC、智能手机等)都由一个唯一的IP地址标识,并且每个设备都有一个端口号,每个设备都可以使用超过65000个可用端口号。
例如,家庭WLAN中的两个TCP/IP点可能是
Device IP TCP port
----------- --------------- ----------
PC 192.168.178.22 8080
Smart phone 192.168.178.36 1727
在TCP/IP中,客户端-服务器意味着服务器在其端口上监听来自客户端的传入请求,当收到请求时,请求会附带客户端的IP和端口号,因此服务器可以响应请求。目前,将REQUEST
和RESPONSE
视为任意数量字节的集合即可。
Server Client
(PC) (Smart phone)
192.168.178.22:8080 192.168.178.36:1727
| |
[]<------------- REQUEST ------------|
[] |
[] |
[]------------- RESPONSE ----------->|
| |
这种请求-响应原理简单而灵活,但也存在一些限制,在设计使用它的应用程序时必须考虑到这些限制。除其他外,关于这种通信,我们可以说
- 服务器必须先启动并始终保持唤醒(监听)状态
- 客户端需要知道服务器的IP和端口才能与之通信
- 通信由客户端发起请求,服务器只能响应该请求。
- 服务器在收到客户端的请求之前,对客户端一无所知。
TCP/IP地址(IP和端口)不可共享,这意味着我们不能让同一台机器(同一IP地址)上的两个应用程序为HTTP通信打开同一个端口。然而,多个客户端,每个客户端都有自己唯一的TCP/IP地址,可能同时向同一个服务器发送请求。
HTTP消息
在TCP/IP协议之上,HTTP定义了REQUEST
和RESPONSE
的格式。为此,它使用一种称为HTTP消息的简单结构,该结构最多可以包含三个部分,用CRLF(回车符和换行符)分隔:START
行、HEADER
部分和BODY
部分。
General Syntax Request example Response Example
----------------- ----------------------------- ------------------------------
START LINE<CRLF> GET /myIndex.html HTTP/1.1 HTTP/1.1 200 OK
MESSAGE HEADER<CRLF> Content-Type: text/html
... Content-Length: 170
MESSAGE HEADER<CRLF> ...
<CRLF>
MESSAGE BODY <html><body> ...
... ....
所有这些部分都可以用于在客户端和服务器之间以及服务器和客户端之间发送信息。让我们仔细看看它们。
起始行
起始行对于REQUEST
和RESPONSE
消息有所不同。在REQUEST
中,起始行的作用是告诉服务器我们要做什么:获取资源、存储某物等,为此需要提供一些细节,例如文件路径、ID或其他内容。通用语法是
METHOD URI HTTP_VERSION
where
METHOD : GET, POST, ...
URI : path or path?query
HTTP_VERSION : HTTP/1.1
对于METHOD
,规范中有更多定义的取值,但在许多情况下,只使用GET
和POST
,就像我们在这里一样。
"path
"可能对应服务器上的物理文件,也可能不对应。在许多GET
请求的情况下,它确实对应,但最终由服务器决定“path
”的含义,以及总体的请求。例如,将METHOD
和“path
”结合起来,可以使用不同数量的方法来描述完全相同的操作。举个例子,我们可以将检索、存储和删除事件的操作表示为
using GET,PUT,DELETE using only POST
----------------------- ----------------------
GET /event POST /get/event
PUT /event POST /store/event
DELETE /event POST /delete/event
一般来说,HTTP消息中的URI不需要主机和端口信息,就像浏览器中的URL一样,因为这些值已经由TCP/IP连接本身知道了。一些示例显示了这一点
Typical URL in browser Request received by the server
---------------------------------- ---------------------------------
http://www.wakeupthecat.com/info GET /info HTTP/1.1
http://192.168.178.22:1811/info GET /info HTTP/1.1
因为浏览器在这些情况下总是执行GET
。
最后,“query
”是一个可选的string
,其中包含用“&
”字符分隔的“variable=value
”单元。这是放置一些参数以完成请求信息的不错地方。查询部分的示例。
?id=7172&name=Sonia
在RESPONSE
中,起始行是
HTTP_VERSION STATUS_ID STATUS_TEXT
它包含的信息少得多,因为只能返回一组预定义的响应代码,例如“200 OK
”或众所周知的“404 Not Found
”。我们将只使用
HTTP/1.1 200 OK
任何其他反馈仍可以通过响应头或正文发送给客户端。
头部部分
头部部分由任意数量的行组成,每行由两个由冒号分隔的字符串组成,第一个字符串是头部名称,第二个字符串是其值。
在HTTP应用程序中,可以从客户端和服务器端设置自定义头部以传递信息。规范中也定义了标准头部,特别是有一个对于协议本身而言不仅重要而且至关重要:Content-Length
。其值必须正好是正文(如果有)的字节数,以便通信能够正常工作。幸运的是,服务器和客户端(浏览器)都会自动填充此头部,因此我们无需担心它。
“Content-Type
”头部也会在请求和响应中自动设置,例如,浏览器需要它来正确处理CSS、HTML等内容。
即使是来自浏览器的标准头部,其他头部也可以被忽略,因为它们与应用程序无关,尽管如果需要,也可以处理它们。
正文
正文是最自由的部分,它可以传输一切,从文本到二进制数据。当浏览器使用GET
方法请求文件时,如果它是一个实际文件,则正文只包含该文件的所有字节。正文的内容可以是以可读文本形式的HTML、CSS、JavaScript或任何其他结构化数据,如XML、JSON或我们在这里使用的EVA(Estructura Variable de Archivo)。
HTTP客户端和服务器
使用HTTP的通信应用程序几乎可以用任何语言实现。如今,在客户端使用现代浏览器非常有意义。它们具有大量功能,并且已经安装在大多数设备上。当然,通过使用浏览器,我们必须接受出于安全原因对主机设备本身的访问和控制施加的限制,但对于许多应用程序而言,这应该不是问题。
浏览器必须使用HTML、CSS和JavaScript进行编程。在我们的示例中,我们将使用一个名为jGastona
的JavaScript库,它将使这些工作变得更容易一些。
对于服务器端,我们将使用一个我编写的开源项目gastona(gastona.jar),它是一个通用的应用程序构建器,可以非常直接地实现http服务器。
使用浏览器进行HTTP通信
基本上有两种请求,实际上是最常用的,也是我们将在本次演示中使用的,即GET
和POST
。
GET
方法用于检索通常将在浏览器中呈现或用于呈现的资源。例如,当我们单击HTML页面上的链接时,内容(“a
”标签的href属性)是使用GET
请求检索的。检索的URI可以直接是文件名或其他文本,服务器通过BODY
发送结果。浏览器对服务器的第一个请求是使用URI“/
”,服务器通常将其解释为“/index.html”作为默认页面。
Server Client
(PC) (Smart phone)
192.168.178.22:8080 192.168.178.36:1727
| |
[]<------------- "GET /" ---------------------|
[] |
[] |
[]------------- RESPONSE BODY ---------------->|
| <html> |
| <a href="nextPage.html">Next</a> |
| </html> |
| |
| |
[]<------------- "GET /nextPage.html" --------| User click on "Next"
[] |
[] |
[]------------- RESPONSE BODY ---------------->|
| <html>
| .... etc
| </html>
除了浏览器自动执行的请求外,还可以使用JavaScript出于任何目的执行请求:设置或检索数据或与服务器进行任何通信。这种机制称为AJAX,通常使用POST
方法。文件上传也使用POST
请求完成。
0. 第一个HTTP服务器
本文档中解释的示例构建了应用程序的服务器端和客户端。在这两种情况下,GUI(图形用户界面)和逻辑都分为两个大组或单元,称为javaj
和listix
。添加一个公共于GUI和逻辑的第三个单元“data
”,我们最终得到通用结构
#javaj#
variables to define GUI elements and its layout
#data#
variables to hold data of GUI elements and being accessible by listix
#listix#
variables to define the logic containing text and commands
在这个非常简单的HTTP服务器中,我们为客户端使用纯HTML,所以重点将放在服务器端。
目的是仅服务一个包含指向第二个页面的链接的初始页面,第二个页面也包含指向第一个页面的链接。尽管简单,但它实际上是一个多页面应用程序(MPA),因为浏览器每次都在加载一个新页面。接下来的示例将是单页应用程序(SPA),这需要在服务器端和客户端进行更多的编程。
启动服务器所需的命令称为MICOHTTP(或简称为MICO),其语法为
MICO, START, serverNamne, portNumber
如果我们不指定portNumber
,系统将选择一个可用的端口,通常在~50000到65000之间。在这种情况下,MICO还将自动打开一个指向该端口的浏览器,因此浏览器会立即发送第一个请求。这对于开发和测试阶段非常方便,但当然,最终目标是从设备上的浏览器进行通信。
所以,如果我们用命令启动服务器
MICO, START, myFirstServer
我们将有以下通信场景
Server Client
(PC) (Web Browser in PC)
localhost:50130 localhost:56380
| |
[]<------------- "GET /" ---------------------| Web browser opened by the MICO command
[] | pointing to localhost:50130 (the server)
[] |
Notes: localhost is the name equivalent to the IP 127.0.0.1
ports numbers 50130 and 56380 are in this case chosen by the system and browser
On giving an address to the browser this sends immediately a first request to the server
To connect to the server from another device in the network (e.g. via WiFi)
we need to know the IP address of the server, localhost or 127.0.0.1 will not work!
编程服务器响应请求的方式是将响应正文放入一个名为特定请求(METHOD
和path
)的变量中。在这种情况下,两个页面都将是静态文本,可以这样写
<GET />
//<html>
// First page! <a href="/nextPage.html">Go to a second page</a>
//</html>
<GET /nextPage.html>
//<html>
// 2nd page <a href="/">Go to the first one</a>
//</html>
最后,让我们为服务器放一个控制台作为GUI。尽管实际的服务器根本没有GUI,但这将有助于我们进行测试,并且也是关闭服务器窗口时停止服务器的一种方式。
将所有内容放在一个gastona脚本(firstServer.gast)中
#javaj#
<frames> oConsola
#listix#
<main>
MICO, START, myFirstServer
<GET />
//<html>
// First page! <a href="/nextPage.html">Go to a second page</a>
//</html>
<GET /nextPage.html>
//<html>
// 2nd page <a href="/">Go to the first one</a>
//</html>
现在在命令行中输入
java -jar gastona.jar firstServer.gast
我们的第一个服务器正在运行。
在此示例中,我们直接从变量提供页面,但页面也可以放在物理文件中。假设我们将第一个页面命名为index.html,第二个页面命名为nextPage.html,脚本本身将缩减为
#javaj#
<frames> oConsola
#listix#
<main>
MICO, START, myFirstServer
<GET />
@<:infile index.html>
现在,在响应第一个请求时,我们加载index.html的内容,但对于服务器可能找到的任何后续对物理文件的请求,都不需要其他操作,因为它们将由MICO直接从文件提供服务。这意味着如果index.html包含指向文件系统中其他页面、图像等的链接,则上述脚本将足以充当服务器。
仅基于文件(HTML、CSS、JS、GIF等)的站点,这些文件不发生变化,被称为静态网页,现在我们知道如何使用MICO HTTTP服务器来实现它。
接下来的三个示例将是SPA或单页应用程序。
加载JAST脚本用于客户端部分
对于客户端 - 在本例中是浏览器 - 我们当然会使用HTML、CSS和JavaScript,但jGastona JavaScript库将使这些工作变得更容易一些。
首先,必须在浏览器中加载库,然后我们使用一个特殊的脚本JAST初始化jGastona,JAST将定义每个应用程序的客户端应用程序。
正如我们将看到的,用于客户端的JAST脚本和用于服务器的gast脚本在形式上非常相似,但实际上它们是完全不同的实现,一个作为桌面应用程序运行,另一个仅在浏览器中运行。
服务器的以下代码在第一次响应时一次性完成所有必需的加载。
<GET />
//<html><style> * { font-family: Tahoma } </style> <body><script>
//
//@<:infile META-GASTONA/js/jGastonaEva-min.js>
//
// var jgas = jGastona (evaFileUTF82obj ("@<:solve-encode-utf8 MAIN_JGAST>"));
//
//</script></body></html>
其中META-GASTONA/js/jGastonaEva-min.js包含在gastona.jar中,因此将从那里加载,而MAIN_JAST
是我们为特定应用程序放置JAST脚本的变量名。
请注意,我们提供的HTML正文除了script
标签外,没有任何HTML元素。魔术发生在jGastona中,它将根据我们提供的JAST在浏览器中即时生成所有元素。
现在,我们必须在每种情况下对JAST脚本和服务器逻辑进行编程,方法是定义对可以发送的其余http请求的响应。
1. 服务器截图
此实用程序用于获取运行服务器的PC的屏幕截图并将其发送回客户端。因此,例如,我们可以使用智能手机从厨房检查PC上运行的某个任务的进度。
客户端(JAST)
对于客户端的GUI,我们只需要一个按钮和一个图像元素。我们为它们命名以便后续引用,第一个字符或字符表示元素类型,在本例中“b
”表示按钮,“m
”表示图像。我们按照以下方式将它们布置在一个特殊的网格布局EVALAYOUT
中
#javaj#
<layout of main>
EVA, 12, 12, 8, 8
---, X
, bScreenshot
X , mImagen
当用户按下按钮时,服务器必须进行新的屏幕截图,并将新图像发送以更新mImagen
。从客户端的角度来看,这可以简单地描述为:按下按钮时,更新mImagen
的内容。
考虑到按钮事件被捕获在变量“-- bScreenshot
”中,以下代码将完成任务。
#listix#
<-- bScreenshot>
//AJAXgetIdContent ("mImagen");
因此,当按下“bScreenshot
”按钮时,我们调用jGastona函数AJAXgetIdContent
,该函数请求服务器更新图像“mImagen
”,或者更确切地说,是图像的源。
将所有内容合并到变量MAIN_JAST
中
<MAIN_JGAST>
//#javaj#
//
// <layout of main>
// EVA, 12, 12, 8, 8
//
// ---, X
// , bScreenshot
// X , mImagen
//
//#listix#
//
// <-- bScreenshot>
// //AJAXgetIdContent ("mImagen");
服务器端
服务器必须响应客户端通过函数AJAXgetIdContent
发出的调用,该函数无非就是请求
POST /getIdContent?id=mImagen HTTP/1.1
尝试这样做的一个(但很幼稚的)方法是
<POST /getIdContent>
SCREEN, myScreenshot.png, png
myScreenshot.png
其中第一行是SCREEN命令,它在png类型的文件myScreenshot.png上进行屏幕截图。请注意,它已被执行,但实际上没有生成要添加到正文的任何文本,而第二行是文本(每行只有一个元素),它将构成getIdContent
请求的响应正文。
这只能在第一次工作!为什么?
每次截屏后,浏览器应执行请求“GET /myScreenshot.png
”,以便服务器可以交付文件的新内容。但这种情况只发生第一次,后续请求是从浏览器缓存中获取的,服务器不再接收GET
。
为了避免这种缓存机制,我们将旋转文件名并删除旧的屏幕截图以保持目录清洁。这不是唯一的解决方案,但它适用于任何浏览器,并且还说明了如何调用系统函数。
#data#
<NN> 0
<SHOT_NN_PNG> //cache/screenshot_@<NN>.png
#listix#
<POST /getIdContent>
CALL, //CMD /C del @<:path SHOT_NN_PNG>
NUM=, NN, NN + 1
SCREEN, @<SHOT_NN_PNG>, png
@<SHOT_NN_PNG>
第一个命令是CALL
for Windows,用于删除旧的屏幕截图文件,对于Linux或MacOS,我们应该写
CALL, //rm @<SHOT_NN_PNG>
请注意,对于Windows,我们使用“@<:path ...
”将“/
”更改为本机分隔符(在Windows中是“\
”),以便“del
”命令能够工作。我们没有直接在变量SHOT_NN_PNG
中写“\
”,因为对于浏览器,我们需要分隔符“/
”,即使服务器在Windows上运行。
Listix区分命令和文本,因为命令及其参数和选项始终以多列形式表示,用逗号分隔,因此只包含一个值或一列的行是文本。
还请注意“//
”的使用,这次不是在行的开头,而是在命令内部。序列“//
”具有特殊含义,即“从此处到行尾的值”。当值包含逗号或其他特殊字符(如<或引号)时,通常需要它,但总的来说,它始终可以用于行中最后一列的值。
完整的sweet_Screenshot.gast
#javaj#
<frames> oConsola
#data#
<NN> 0
<SHOT_NN_PNG> //cache/screenshot_@<NN>.png
#listix#
<main>
micohttp, start, monoMico
<GET />
//<html><style> * { font-family: Tahoma } </style> <body><script>
//
//@<:infile META-GASTONA/js/jGastonaEva-min.js>
//
// var jgas = jGastona (evaFileUTF82obj ("@<:encode-utf8 MAIN_JGAST>"));
//
//</script></body></html>
<MAIN_JGAST>
//#javaj#
//
// <layout of main>
// EVA, 12, 12, 8, 8
//
// ---, X
// , bScreenshot
// X , mImagen
//
//#listix#
//
// <-- bScreenshot>
// //AJAXgetIdContent ("mImagen");
<POST /getIdContent>
CALL, //CMD /C del @<:path SHOT_NN_PNG>
NUM=, NN, NN + 1
SCREEN, @<SHOT_NN_PNG>, png
@<SHOT_NN_PNG>
2. 具有上传功能的服务器
在此示例中,我们允许下载位于服务器特定目录中的文件,并一次从设备上传一个文件。要下载的文件所在的目录可以在脚本中配置,而上传始终在一个名为“filesUpload”的固定目录中完成,该目录必须在脚本启动时创建或确保存在。
对于要下载的文件,我们只需生成一个包含指向最终文件的链接的HTML表,然后在浏览器中单击这些链接,它们将自动被请求和下载。
对于上传功能,我们需要两个按钮 - 一个用于选择要上传的文件,另一个用于开始上传。
上传将自动在服务器端进行,我们仍然会在客户端编写一些代码以提供基本的上传进度反馈。
客户端(JAST)
在此GUI中,我们也使用单个组件列表,因此我们的网格将是
, X
, lAvailable files
X , dFileList
, lUpload a file
, uSelectUpFile
, bUpload
其中使用的控件类型是
l Label
d div (to place the html table later)
u special button for selecting file to upload
b normal button
对于逻辑,我们最初使用以下命令请求要下载的文件列表
<main>
//AJAXgetIdContent ("dFileList");
对于启动文件上传的按钮,我们使用jGastona
函数进行一些检查、启动上传,一旦开始,我们将按钮文本更改为“uploading ...
”以提供一些反馈,并在收到服务器响应后,恢复初始按钮标签。
<-- bUpload>
//if (canUploadFile ("uSelectUpFile", 500)) {
// if (AJAXUploadFile ("uSelectUpFile", "uploadFile")) {
// setData ("bUpload", "uploading ...");
// }
//}
<-- ajaxResponse uploadFile>
//setData ("bUpload", "Upload file");
服务器端
首先,服务器将接收post请求
POST /getIdContent?id=dFileList HTTP/1.1
为了提供带有链接的表,我们使用LOOP
命令(LOOP
,FILES
),在所有循环命令中,默认选项是BODY
(循环的正文),我们可以省略它,正如我们在后续示例中所做的那样
<FIDIR> filesForDownload
<POST /getIdContent>
// <table>
//
LOOP, FILES, @<FIDIR>
, RECURSIVE, 0
, BODY , // <tr>
, BODY , // <td> <a href="@<FIDIR>/@<fileName>"> @<FIDIR>/@<fileName> </a>
, BODY , // </tr>
//
// </table>
注意在循环中,使用了变量“FIDIR
”,该变量在此之前已声明,但更有趣的是,使用了变量“fileName
”,该变量对于此循环是特定的,并且在每次迭代时都会更改其值。
关于上传,如果文件已成功上传,服务器会将变量_uploadedFile0
设置为服务器端的物理文件名。因此,我们可以检查它并返回适当的响应(ok
或nok
)
<POST /uploadFile>
CHECK, VAR, _uploadedFile0, nok
CHECK, FILE, @<_uploadedFile0>, nok
ok
尽管为了简洁起见,我们实际上并未在客户端使用此信息。
3. 存储在服务器上的简单公告
此实用程序的目的是拥有一个公告栏,其中包含存储在服务器上的事件,包括日期、标题和描述,客户端可以远程处理。
对于此脚本,我们将了解gastona和jGastona的更高级功能,即
- 动态GUI布局切换
- 传递结构化数据(表单)
- 使用SQL数据库
客户端
在此示例中,我们将JAST分离到其自己的文件Boletin.JAST中。
让我们看一下JAST代码以及在浏览器中的结果
在javaj单元中,使用很少的行,创建了由两种不同布局组成的GUI,HTML元素及其位置,除了widget dBoletin
之外,其表格内容由服务器填充。
jGastona最初将加载布局main。按下“新事件”按钮时,jGastona
方法mask(“main
”,“layInput
”)将被调用,它将“layInput
”显示为代替“main
”。当按下“bCancel
”按钮时,会发生相反的情况(恢复“main
”布局)。所有这些都仅发生在浏览器中,无需服务器的请求或响应即可切换布局。
同样重要的是要注意,在创建HTML组件时,会创建带有名称的变量在数据单元中。因此,例如,当输入新事件的日期、标题和描述时,变量eEventDate
、eEventTitle
和xEventDesc
将填充输入值,并在调用AJAXSend ("insertEvent")
时发送到服务器。
服务器端
服务器也非常简洁,如功能分组所示
请注意,现在循环遍历的是SQL select
,获取前50条记录。变量“date
”、“title
”、“evendesc
”和“id
”,最后在DEL_COMMAND
中使用,由LOOP
命令在每次迭代中自动填充。
最后,对于那些没有数据库和SQL经验的人
我们使用以下命令创建表
CREATE TABLE IF NOT EXISTS boletin (
id INTEGER PRIMARY KEY AUTOINCREMENT,
date,
title,
eventdesc);
在这里,我们指定“id
”列为long类型,以便数据库引擎在每次插入时自动填充它。其余列在sqlite中不需要数据类型。通过在可能的情况下不为列提供数据类型,我们可以保持模式的灵活性和效率。
对于insert
一个新事件,SQL是
INSERT INTO boletin (date, title, eventdesc)
VALUES ('@<:encode eEventDate>',
'@<:encode eEventTitle>',
'@<:encode xEventDesc>');
这里,“:encode
”预处理对每个字段都非常重要,它会转义几个字符,包括引号和双引号('
和"
)。您可以自己尝试一下,例如,如果:encode
未使用在eEventTitle
中,并且我们尝试保存一个标题为'
(一个引号)的条目,会发生什么。然后SQL将格式错误,插入将失败。事实上,著名的SQL注入攻击就是基于这种错误编程的SQL。
结论
通过三个小型脚本,我们使用http协议开发了一些非平凡且实际有用的功能。
希望在阅读本文后,即使没有HTTP、HTML、JavaScript和gastona的任何先验知识,任何人都可以对脚本进行一些修改以进行定制,甚至从中创建新应用程序。或者至少能够弄清楚如何做到这一点,这实际上是开发中最重要的一步。
本文的示例将在github存储库 HttpSweetHttp 中维护,作为使用gastona MICO http命令(服务器)和jGastona js库(客户端)一起或单独使用的http应用程序的参考。
历史
- 2017年5月21日:初始版本