modbus tcp 入门详解
Modbus tcp 格式說(shuō)明 通訊機(jī)制 附C#測(cè)試工具用于學(xué)習(xí),測(cè)試
前言:
?之前的博客介紹了如何用C#來(lái)讀寫(xiě)modbus tcp服務(wù)器的數(shù)據(jù),文章:http://www.cnblogs.com/dathlin/p/7885368.html
當(dāng)然也有如何創(chuàng)建一個(gè)服務(wù)器文章:http://www.cnblogs.com/dathlin/p/7782315.html
但是上面的兩篇文章是已經(jīng)封裝好的API,只要調(diào)用就可以實(shí)現(xiàn)功能了,對(duì)于想了解modbus tcp的原理的人可能就不適合了,最近有不少網(wǎng)友的想要了解這個(gè)協(xié)議,所以在這里再寫(xiě)一篇介紹Modbus tcp的文章,不過(guò)這篇文章是簡(jiǎn)易版本的,未來(lái)我再研究深入的話,再開(kāi)一篇高級(jí)版,在簡(jiǎn)易版中,就略去了成功標(biāo)志位及其他數(shù)據(jù)標(biāo)志,這些到等到后面再說(shuō)。
先分享一下,我自己學(xué)習(xí)的地址來(lái)源:http://blog.csdn.net/thebestleo/article/details/52269999? 聲明:本文并非轉(zhuǎn)載,并非照搬原文章,是在我參照原博客的基礎(chǔ)上,理解了基本的modbus通訊,并結(jié)合自己的理解,重新寫(xiě)一篇更好入門(mén)的文章,此處貼出原作者的帖子以示尊重知識(shí)產(chǎn)權(quán),原文章有些地方有一點(diǎn)錯(cuò)誤,而且早就停止更新了,也沒(méi)有提供方便的測(cè)試工具,官方的modbus 測(cè)試工具是試驗(yàn)版本的,需要購(gòu)買(mǎi)序列號(hào)才可以,所以此處提供我自己的測(cè)試工具,地址如下,下面的介紹的例子都是基于這個(gè)工具來(lái)實(shí)現(xiàn)的。
https://github.com/dathlin/ModBusTcpTools/raw/master/download/download.zip
關(guān)于該測(cè)試工具也是開(kāi)放源代碼的,如果想要查看源代碼:https://github.com/dathlin/ModBusTcpTools
技術(shù)支持QQ群:592132877
?
準(zhǔn)備條件:
在上面的測(cè)試工具下載之前,需要一些額外的知識(shí)補(bǔ)充,此處不管你是學(xué)習(xí)什么語(yǔ)言的,對(duì)于socket通信層來(lái)說(shuō),其實(shí)是一樣的,下面的講解的內(nèi)容是直接基于底層的,無(wú)關(guān)語(yǔ)法的操作。
但是需要你對(duì)字節(jié)概念非常清晰,一般都是byte數(shù)組,一個(gè)byte有8個(gè)位,這個(gè)也要非常的清晰,如果連byte是什么都搞不清楚,那么對(duì)本文下面的內(nèi)容理解會(huì)非常的吃力,那么還是建議你再看看計(jì)算機(jī)原理這些書(shū),對(duì)于socket通信,每種語(yǔ)言都有不同的寫(xiě)法,但是所有的語(yǔ)言都有兩個(gè)共同點(diǎn),都能實(shí)現(xiàn)把數(shù)據(jù)發(fā)送到socket和從socket接收數(shù)據(jù),至于這個(gè)如果去做,就參照你自己需要使用的語(yǔ)言了,此處不做這方面的說(shuō)明了。
關(guān)于十六進(jìn)制文本,在本文的下面的內(nèi)容上,所有的byte字節(jié)數(shù)組都表示成十六進(jìn)制形式,比如 FF 10 代表2個(gè)字節(jié),一個(gè)是255,另一個(gè)是16。
| 1 2 3 | byte[] temp =?new?byte[2]; temp[0] = 0xFF; temp[1] = 0x10; |
如果將上述的temp看作是讀取到的線圈的數(shù)據(jù),那么轉(zhuǎn)換規(guī)則如下:
先將上述數(shù)據(jù)轉(zhuǎn)化成二進(jìn)制 :?1111 1111????(第一個(gè)byte,我們從高位寫(xiě)到地位)??????????? ?0001 0000?? (第二個(gè)byte,我們從高位寫(xiě)到地位)
對(duì)應(yīng)的線圈就是,線圈7-線圈0,,,,第二個(gè)byte對(duì)應(yīng)的線圈是,線圈15-線圈8?????這里一定要好好理解,從byte上來(lái)說(shuō),temp[0]是地位,temp[1]是高位,深入到每個(gè)byte里面的二進(jìn)制,高位在前,低位在后。
在C#里等同于下面的代碼,和C語(yǔ)言,java也是非常的相近,還算比較好理解。
如果我說(shuō),發(fā)送 00 00 00 00 00 06 FF 01 00 00 00 01 到socket上去,那么也就是:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | byte[] temp =?new?temp[12]; temp[0] = 0x00; temp[1] = 0x00; temp[2] = 0x00; temp[3] = 0x00; temp[4] = 0x00; temp[5] = 0x06; temp[6] = 0xFF; temp[7] = 0x01; temp[8] = 0x00; temp[9] = 0x00; temp[10] = 0x00; temp[11] = 0x01; socket.Send(temp); |
先不要管上面的數(shù)據(jù)是什么含義,知道上面的代碼是啥含義就行了。接下來(lái)就是下載上面的測(cè)試工具,開(kāi)始真正的學(xué)習(xí)modbus tcp協(xié)議了!
?
測(cè)試工具初始化
先運(yùn)行Server.exe文件,端口里輸入502,然后點(diǎn)擊啟動(dòng)服務(wù)即可,如下:
然后運(yùn)行Client.exe程序,在Ip地址里輸入127.0.0.1,端口里輸入502,點(diǎn)擊配置即可,我們看到,如果你的服務(wù)器程序運(yùn)行在了別的電腦上,甚至是云端,只要客戶端的ip修改成服務(wù)器的ip,端口號(hào)對(duì)應(yīng)上,就可以訪問(wèn)到服務(wù)器的數(shù)據(jù)了。
特殊測(cè)試不用去管,和我們現(xiàn)在學(xué)習(xí)的東西不一致。
?
功能碼詳細(xì)解釋
對(duì)于modbus來(lái)說(shuō),涉及的功能碼也就是0x01,0x02,0x03,0x05,0x06,0x0F,0x10了,其實(shí)分類(lèi)來(lái)說(shuō),就只有兩種,線圈和寄存器,也就是位讀寫(xiě)和字讀寫(xiě),首先需要清楚的是功能碼不一樣,對(duì)應(yīng)數(shù)據(jù)的解析規(guī)則也不一樣,下面就針對(duì)不同的情況來(lái)說(shuō)。
首先說(shuō)明的是,modbus協(xié)議呢,最終目的還是為了實(shí)現(xiàn)數(shù)據(jù)交互,既然是數(shù)據(jù)交互,那就是包含了數(shù)據(jù)讀和寫(xiě),我們把我們的想法轉(zhuǎn)化成一串?dāng)?shù)據(jù),發(fā)送給設(shè)備(或者叫服務(wù)器),它返回一串?dāng)?shù)據(jù),根據(jù)規(guī)則解析出來(lái),這樣就得到了我們真正想要的數(shù)據(jù)。下面就來(lái)第一個(gè)想法實(shí)現(xiàn)吧。
另外,在modbus服務(wù)器端,數(shù)據(jù)是使用地址的方式來(lái)公開(kāi)的,這很好理解,服務(wù)器端保存了很多數(shù)據(jù),你想要訪問(wèn)某個(gè)數(shù)據(jù)肯定需要指定唯一的身份標(biāo)識(shí),從連續(xù)的地址來(lái)區(qū)分?jǐn)?shù)據(jù)是最常用的做法,不僅好理解,還便于擴(kuò)展,比如你還可以讀取連續(xù)地址的數(shù)據(jù)塊。如果采用字符串名字來(lái)標(biāo)識(shí)數(shù)據(jù),就沒(méi)有這個(gè)特點(diǎn)。
對(duì)于位操作來(lái)說(shuō)(各種線圈和離散量),一個(gè)地址代表了一個(gè)bool變量,即 0 和 1,要么通要么斷,就好比一些普通的開(kāi)關(guān)。
對(duì)于寄存器來(lái)說(shuō),一個(gè)地址代表了2個(gè)byte,共有65536種方式,可以滿足大多數(shù)日常使用了,比如我們讀取地址0的寄存器,返回 00 00 及代表寄存器0數(shù)據(jù)為0,如果返回 01 00 ,那么代表寄存器0數(shù)據(jù)為 256?
?
功能碼0x01:
我不直接上一串?dāng)?shù)據(jù),這樣看著也累,我們從例子出發(fā),現(xiàn)在我們需要讀取線圈(離散量)操作,我想讀取地址0的線圈是否是通還是斷的。我們有了這個(gè)功能需求后,就可以根據(jù)需求來(lái)寫(xiě)出特殊的指令了。根據(jù)協(xié)議指定,需要填寫(xiě)長(zhǎng)度為12的byte數(shù)組
byte[0]??? byte[1]??? byte[2]?? byte[3]?? byte[4]?? byte[5]?? byte[6]?? byte[7]?? byte[8]?? byte[9]?? byte[10]?? byte[11]??
byte[0]??? byte[1]???????????? : 消息號(hào)---------隨便指定,服務(wù)器返回的數(shù)據(jù)的前兩個(gè)字和這個(gè)一樣
byte[2]?? byte[3]????????????? :modbus標(biāo)識(shí),強(qiáng)制為0即可
byte[4]???byte[5]????????????? ?:指示排在byte[5]后面所有字節(jié)的個(gè)數(shù),也就是總長(zhǎng)度-6
byte[6]:?????????????????????? 站號(hào),隨便指定,00? -- FF 都可以
byte[7] :???????????????????? 功能碼,這里就需要填入我們的真正的想法了
byte[8]? byte[9]?????????????? :起始地址,比如我們想讀取地址0的數(shù)據(jù),就填 00 00 ,如果我們想讀取地址1000的數(shù)據(jù),怎么辦,填入 03 E8 ,也就是將1000轉(zhuǎn)化十六進(jìn)制填進(jìn)去。
byte[10]? byte[11]????????????? :指定想讀取的數(shù)據(jù)長(zhǎng)度,比如我們就想讀取地址0的一個(gè)數(shù)據(jù),這里就寫(xiě) 00 01,如果我們想讀取地址0-999共計(jì)一個(gè)數(shù)據(jù)的長(zhǎng)度,就寫(xiě) 03 E8。和起始地址是一樣的。
?
有了上面的格式之后,接下來(lái)我們就按照格式來(lái)填寫(xiě)數(shù)據(jù)吧,我們需要讀取地址0的數(shù)據(jù),那么指定如下
00 00 00 00 00 06 FF 01 00 00 00 01
消息號(hào)設(shè)為0,站號(hào)FF,功能碼01,地址01,長(zhǎng)度01:將上面的指令在客戶端程序里進(jìn)行輸入,點(diǎn)擊發(fā)送,這樣就在下面的響應(yīng)框里接收到服務(wù)器反饋的數(shù)據(jù),我們最終需要的信息就在反饋的數(shù)據(jù)里了。
前面是接收到數(shù)據(jù)的時(shí)間,自動(dòng)忽略,那么返回的數(shù)據(jù)就是 00 00 00 00 00 04 FF 01 01 00 共計(jì)10個(gè)字節(jié)的數(shù)據(jù),ok,這玩意到底是什么意思呢,我們來(lái)分別解析下:
byte[0]? byte[1] :???????????? 消息號(hào),我們之前寫(xiě)發(fā)送指令的時(shí)候,是多少,這里就是多少。
byte[2]? byte[3]:?????????? 必須都為0,代表這是modbus 通信
byte[4]? byte[5]:?????????? 指示byte[5]后面的所有字節(jié)數(shù),你數(shù)數(shù)看是不是4個(gè)?所以這里是00 04,如果后面共有100個(gè),那么這里就是 00 64
byte[6]:??????????????????????? 站號(hào),之前我們寫(xiě)了FF,那么這里也就是FF
byte[7]:??????????????????????? 功能碼,我們之前寫(xiě)了01的功能碼,這里也是01,和我們發(fā)送的指令是一致的
byte[8]:??????????????????????? 指示byte[8]后面跟隨的字節(jié)數(shù)量,因?yàn)楦赽yte[8]后面的就是真實(shí)的數(shù)據(jù),我們最終想要的結(jié)果就在byte[8]后面
byte[9]:??????????????????????? 真實(shí)的數(shù)據(jù),哈哈,這肯定就是我們真正想要的東西了,我們知道一個(gè)byte有8位,但是我們只讀取了一個(gè)位數(shù)據(jù),所有這里的有效值只是byte[9]的最低位,二進(jìn)制為 0000 0000 我們看到最低位為0,所以最終我們讀取的地址0的線圈為斷。
?
假設(shè)我們讀取地址10,開(kāi)始的共10個(gè)線圈呢,那么會(huì)返回什么?所以我們發(fā)送 00 00 00 00 00 06 FF 01 00 0A 00 0A
我們接收到了:00 00 00 00 00 05 FF 01 02 79 01????? 前面的8個(gè)字節(jié)的信息參照上面的分析,是一致的,我們就針對(duì)后面三個(gè)字節(jié)著重分析。我們讀取了10個(gè)位,那么一個(gè)字節(jié)可以表示8個(gè)位,那么我們的結(jié)果至少需要2個(gè)byte才能表示完,所以最終的數(shù)據(jù)肯定是2個(gè)字節(jié),那么02就是后面的字節(jié)數(shù)量,也就是真實(shí)的數(shù)據(jù)長(zhǎng)度。
要想從 79 01 數(shù)據(jù)中分析出我們真實(shí)想要的數(shù)據(jù),還需要經(jīng)過(guò)最后一次數(shù)據(jù)轉(zhuǎn)換。先轉(zhuǎn)為二進(jìn)制:
0111 1001???????? ? 0000 0001
第二步:按每八位進(jìn)行分割,上述其實(shí)已經(jīng)分割好了,中間空格多的是分割,以字為單位,將二進(jìn)制順序顛倒:
1001 1110???????? 1000000
第三步:最終數(shù)據(jù)就是??? 線圈10-線圈19的通斷情況是:通,斷,斷,通,通,通,通,斷,通,斷???? 再后面的0都是無(wú)效的
至此我們獲取到了我們最終的數(shù)據(jù)!因?yàn)榇颂幏?wù)器都是0,所以所有的線圈都是斷,等會(huì)可以結(jié)合05功能碼寫(xiě)線圈進(jìn)行聯(lián)合測(cè)試。
?
功能碼0x02:
這個(gè)功能碼和上面的一致,在本服務(wù)器里不支持這個(gè)功能碼。發(fā)送和解析規(guī)則和上面的一致,不再贅述。
?
功能碼0x05:
我們先講解05功能碼,這個(gè)功能碼是實(shí)現(xiàn)數(shù)據(jù)寫(xiě)入,它能實(shí)現(xiàn)什么功能呢,我們可以利用這個(gè)功能碼來(lái)指定某個(gè)線圈通或斷,具體怎么操作呢,有了之前01功能碼的經(jīng)驗(yàn),下面的代碼看起來(lái)就順利多了。
比如我要指定地址0的寄存器為通:?00 00 00 00 00 06 FF 05 00 00 FF 00????前面的含義都是一致的,我們就分析?05 00 00 FF 00
05 是功能碼, 00 00 是我們指定的地址,如果我們想寫(xiě)地址1000為通,那么就為 03 E8,至于FF 00是規(guī)定的數(shù)據(jù),如果你想地址線圈通,就填這個(gè)值,想指定線圈為斷,就填 00 00 ,其他任何的值都對(duì)結(jié)果無(wú)效。
然后我們看看寫(xiě)入的操作服務(wù)器返回了什么 ???我們看到也是??00 00 00 00 00 06 FF 05 00 00 FF 00???因?yàn)樵谀銓?xiě)入的操作中,是不帶讀取數(shù)據(jù)的,所以服務(wù)器會(huì)直接復(fù)制一遍你的指令并返回。
?
下面再舉例一些方便理解(我們只需要指定地址及是否通斷的情況即可):
寫(xiě)入地址100為通:?00 00 00 00 00 06 FF 05 00?64 FF 00???
寫(xiě)入地址1000為斷:00 00 00 00 00 06 FF 05 03 E8 00 00
?
功能碼0x0F:
我們已經(jīng)實(shí)現(xiàn)了0x05來(lái)單個(gè)的線圈寫(xiě)入,我們可以指定線圈100為通,其實(shí)就兩個(gè)信息需要指定,線圈地址是什么,通還是端,然后我們就可以自然而然的寫(xiě)出指令碼了,但是現(xiàn)在我們需要實(shí)現(xiàn)一個(gè)功能時(shí),將地址0-999共計(jì)1000個(gè)線圈全部為off,這怎么搞?
按照我們之前的經(jīng)驗(yàn),可以發(fā)送一千次的0x05功能碼的指令來(lái)實(shí)現(xiàn),大不了寫(xiě)1000次么。。。。。(寫(xiě)到第100次的時(shí)候估計(jì)已經(jīng)吐血了)
所以我們就繼續(xù)研究有沒(méi)有其他的功能碼來(lái)實(shí)現(xiàn),突然發(fā)現(xiàn)0x0F這一個(gè)神奇的功能碼,這個(gè)功能碼是什么意思呢,就是為了批量寫(xiě)入而存在的,就比如上面的例子0-999都為off,那么指令是什么呢。
00 00 00?00 00?84 FF 0F 00 00 03?E8?7D?...(后面跟125個(gè)byte,都是00)?
上面的指令就實(shí)現(xiàn)了我們的需求,現(xiàn)在來(lái)詳細(xì)解釋下,它怎么就實(shí)現(xiàn)了我們的需求。分析之前,我們發(fā)現(xiàn)不同的功能碼,的前8個(gè)字節(jié)的規(guī)律是一模一樣了,都是標(biāo)識(shí)號(hào)+modbus號(hào)+長(zhǎng)度+站號(hào),后面基本是跟地址和長(zhǎng)度,或是直接是地址和數(shù)據(jù)。
00 00???????? 消息標(biāo)識(shí)號(hào),隨便寫(xiě)什么,反正你寫(xiě)什么數(shù)據(jù),服務(wù)器就復(fù)制一遍而已。
00 00?????????modbus標(biāo)志號(hào),都是00 就對(duì)了。
00 84?????????我們先轉(zhuǎn)化為十進(jìn)制,0x0084轉(zhuǎn)化十進(jìn)制就是132,也就是說(shuō),00 84(不包含00 84)后面跟了132個(gè)字節(jié)
FF????????????? 站號(hào),其實(shí)也是隨便寫(xiě),反正服務(wù)器返回一樣的
00 00???????? 起始地址,此處就是0,如果起始地址為100,那么就寫(xiě)00 64,如果起始地址為1000,那么就寫(xiě)03 E8
03 E8?????????我們需要寫(xiě)的數(shù)據(jù)長(zhǎng)度,因?yàn)槲覀冃枰獙?xiě)1000個(gè)線圈,就是03 E8,如果我們寫(xiě)999個(gè)線圈,那么就是03 E7。
7D???????????? 這個(gè)字節(jié)代表后面跟隨的真實(shí)寫(xiě)入的數(shù)據(jù)的長(zhǎng)度,為125個(gè)字節(jié)。
125個(gè)字節(jié)? 真實(shí)的數(shù)據(jù),我們寫(xiě)1000個(gè)位,那么一個(gè)字節(jié)為8位,那么剛好125個(gè)字節(jié)可以塞完數(shù)據(jù),那么問(wèn)題來(lái)了,如果我們想實(shí)現(xiàn)000-998共計(jì)999個(gè)地址都是off。那怎么搞。?
那么指令為 00 00 00 00 00 84 FF 0F 00 00 03 E7 7D ...(后面125個(gè)byte,都是00)咦,怎么還是125個(gè),原來(lái)無(wú)論寫(xiě)多少個(gè),比如x個(gè),如果是8的倍數(shù),剛好x/8個(gè)byte,如果除不盡怎么辦,就是x/8+1個(gè)字節(jié),這樣才能裝滿我們需要寫(xiě)的數(shù)據(jù)。
既然后面都是125個(gè)字節(jié),那么寫(xiě)1000個(gè)還是999個(gè),那么區(qū)分的關(guān)鍵就在于長(zhǎng)度,03 E8還是03 E7。
大致的數(shù)據(jù)在上面已經(jīng)說(shuō)明了,具體怎么寫(xiě)數(shù)據(jù)看下面,比如我們寫(xiě)入地址10-地址19共計(jì)10個(gè)長(zhǎng)度的線圈,要求的結(jié)果分別是,On,Off,Off,On,On,On,On,Off,On,Off,也就是 通,斷,斷,通,通,通,通,斷,通,斷,接下來(lái)轉(zhuǎn)換0和1,如下:
1001111010
接下來(lái)就是關(guān)鍵了,怎么轉(zhuǎn)化成真實(shí)的byte,這樣我們就可以最終寫(xiě)出來(lái)指令了。
第一步:以8個(gè)8個(gè)為單位進(jìn)行切割,結(jié)果為?10011110??? 10
第二步:第一步的字單位,每個(gè)單位前后順序顛倒,不然不足8位,前面補(bǔ)零,結(jié)果為???0111 1001????????? 0000?0001
第三步:這下可以寫(xiě)成真實(shí)的數(shù)據(jù)了,79 01
?
那么最接下來(lái)我們就可以寫(xiě)最終的指令了,實(shí)現(xiàn)寫(xiě)入地址10-19為:通,斷,斷,通,通,通,通,斷,通,斷??也即?00 00 00 00 00 09 FF 0F 00 0A 00 0A 02 79 01???????? (地址10-19的線圈分別為 通,斷,斷,通,通,通,通,斷,通,斷)
注意上述第二步為什么要順序顛倒,那是因?yàn)樵谟?jì)算機(jī)的單個(gè)byte存儲(chǔ)中,高位在前,地位在后,而對(duì)于多個(gè)連續(xù)的byte來(lái)說(shuō),地位在前,高位在后,所以需要顛倒,如果還是不明白,就先死記,終有一天會(huì)恍然大悟。
現(xiàn)在應(yīng)該能實(shí)現(xiàn)任何的連續(xù)線圈的寫(xiě)入了吧。
寫(xiě)入之后,看看了服務(wù)器返回了什么:
00 00 00 00 00 06 FF 0F 00?0A?00 0A????:現(xiàn)在再來(lái)看這個(gè)數(shù)據(jù)就很簡(jiǎn)單了,就是返回了我們寫(xiě)入數(shù)據(jù)的前12分字節(jié),然后把00 09長(zhǎng)度更改為實(shí)際的長(zhǎng)度 00 06,因?yàn)槭菍?xiě)入操作,所以返回的數(shù)據(jù)沒(méi)什么意義。
?
?
功能碼0x03:
該功能碼實(shí)現(xiàn)寄存器的數(shù)據(jù)讀取,我們需要知道的是,一個(gè)寄存器占2個(gè)byte,而且是高位在前,地位在后,那么如果寄存器0的數(shù)據(jù)為1000,那么我們讀取到的數(shù)據(jù)就是03 E8,這是我們最終想要的東西,03功能碼和01功能碼很接近,就是功能碼替換一下,返回的數(shù)據(jù)解析不一樣而已,比如我們需要讀取地址0的寄存器數(shù)據(jù):
00 00 00 00 00 06 FF 03 00 00 00 01?????????????? 是不是很熟悉?,當(dāng)你看到這個(gè)的時(shí)候,腦力里馬上就是功能碼03,讀取寄存器,地址0,長(zhǎng)度1??????? 返回如下:
00 00 00 00 00 05 FF 03 02 03?E8??????????????????? 主要就是看功能碼后面的數(shù)據(jù)了,我們想要的真實(shí)數(shù)據(jù)肯定藏在后面,也就是?02 03 E8,不是說(shuō)一個(gè)寄存器返回2個(gè)字節(jié)嘛,怎么就變成3個(gè)了?事實(shí)上第一個(gè)字節(jié)不是代表數(shù)據(jù),而是代表后面的字節(jié)長(zhǎng)度是2個(gè)字節(jié),那么03 E8就是真實(shí)的數(shù)據(jù)了,代表了寄存器0存儲(chǔ)了1000這個(gè)數(shù)據(jù)。
?
未完待續(xù)...
轉(zhuǎn)載于:https://www.cnblogs.com/duanweishi/p/9351916.html
總結(jié)
以上是生活随笔為你收集整理的modbus tcp 入门详解的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: JavaScript中的数组和字符串
- 下一篇: 小程序的快速学习