1. 鴻蒙HTTP服務(wù)器可行性分析
看到這個(gè)題目当辐,可能有的小伙伴會(huì)有一些疑問:
“鴻蒙API從9到12我都翻爛了,也沒見提供HTTP服務(wù)器的接口啊”
“我BUG寫得少歼跟,你可不要騙我”
的確阁吝,目前的鴻蒙API沒有提供HTTP服務(wù)端的實(shí)現(xiàn)接口,但是我們知道意蛀,HTTP協(xié)議是基于TCP的,而鴻蒙API10提供了TCPSocketServer類健芭,包含了如下幾個(gè)關(guān)鍵接口:
listen(address: NetAddress): Promise<void>
監(jiān)聽方法县钥,綁定IP地址和端口,端口可以指定或由系統(tǒng)隨機(jī)分配慈迈。成功調(diào)用該方法后若贮,TCPSocketServer對象監(jiān)聽并接受與此套接字建立的TCPSocket連接。
on(type: 'connect', callback: Callback<TCPSocketConnection>): void
訂閱TCPSocketServer的連接事件吩翻,在新的客戶端套接字連接上以后兜看,會(huì)觸發(fā)callback回調(diào),在回調(diào)中包含TCPSocketConnection對象狭瞎,該對象就表示TCPSocket客戶端與服務(wù)端的連接细移。
TCPSocketConnection是服務(wù)端和客戶端通訊的基礎(chǔ),提供了發(fā)送數(shù)據(jù)到客戶端的方法:
send(options: TCPSendOptions): Promise<void>
還提供了訂閱客戶端消息接收的事件:
on(type: 'message', callback: Callback<SocketMessageInfo>): void
在這個(gè)事件的callback回調(diào)里熊锭,包含了SocketMessageInfo參數(shù)弧轧,該參數(shù)的屬性message就是客戶端發(fā)送過來的消息。
通過上述幾個(gè)接口碗殷,我們就可以在服務(wù)端開啟TCP監(jiān)聽精绎,并且處理客戶端的連接和消息收發(fā)。
再來說一下HTTP協(xié)議锌妻,眾所周知代乃,HTTP協(xié)議是一個(gè)簡單的請求響應(yīng)協(xié)議,根據(jù)RFC 9112仿粹,HTTP協(xié)議1.1版本的消息格式如下所示:
HTTP-message = start-line CRLF
*( field-line CRLF )
CRLF
[ message-body ]
其中搁吓,start-line表示起始行,CRLF表示回車換行符號吭历,field-line表示首部字段行堕仔,*( field-line CRLF )說明首部字段可以是零個(gè)或者多個(gè),最后的[ message-body ]表示可選的消息正文晌区;因?yàn)橄⒎譃檎埱笙⒑蛻?yīng)答消息摩骨,所以起始行又可以分為請求行和狀態(tài)行,如下所示:
start-line = request-line / status-line
當(dāng)然朗若,HTTP的協(xié)議還是有一點(diǎn)復(fù)雜的恼五,這里就不展開了,不過我們明白哭懈,只要我們按照協(xié)議格式構(gòu)造出了請求應(yīng)答的文本唤冈,然后使用TCP協(xié)議作為傳輸層進(jìn)行收發(fā)即可。
有了上面的API接口银伟,加上HTTP協(xié)議的格式你虹,就可以打造一個(gè)最簡單的HTTP服務(wù)端了绘搞。
2. 實(shí)現(xiàn)HTTP服務(wù)器示例
本示例運(yùn)行后的界面如下所示:
輸入要監(jiān)聽的端口,然后單擊“啟動(dòng)”按鈕傅物,即可在127.0.0.1上啟動(dòng)對端口的TCP協(xié)議監(jiān)聽了夯辖。然后在請求地址欄輸入服務(wù)端地址,再單擊“請求”按鈕董饰,既可請求剛創(chuàng)建的HTTP服務(wù)器蒿褂,效果如圖所示:
這里實(shí)現(xiàn)的HTTP服務(wù)器是這樣的,接收到客戶端的請求后卒暂,會(huì)把客戶端發(fā)送的信息作為網(wǎng)頁的內(nèi)容反饋給客戶端啄栓,也就是上圖中看到的這些內(nèi)容:
下面詳細(xì)介紹創(chuàng)建該應(yīng)用的步驟。
步驟1:創(chuàng)建Empty Ability項(xiàng)目也祠。
步驟2:在module.json5配置文件加上對權(quán)限的聲明:
"requestPermissions": [
{
"name": "ohos.permission.INTERNET"
}
]
這里添加了獲取互聯(lián)網(wǎng)信息的權(quán)限昙楚。
步驟3:在Index.ets文件里添加如下的代碼:
import { socket } from '@kit.NetworkKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { util } from '@kit.ArkTS';
import { webview } from '@kit.ArkWeb';
let tcpServer: socket.TCPSocketServer = socket.constructTCPSocketServerInstance();
@Entry
@Component
struct Index {
@State message: string = '最簡單的HTTP服務(wù)器示例';
@State port: number = 8080
@State running: boolean = false
@State msgHistory: string = ''
@State webUrl: string = "https://*****.com/"
scroller: Scroller = new Scroller()
webScroller: Scroller = new Scroller()
controller: webview.WebviewController = new webview.WebviewController()
build() {
RelativeContainer() {
Text(this.message)
.id('txtTitle')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.alignRules({
middle: { anchor: '__container__', align: HorizontalAlign.Center },
top: { anchor: '__container__', align: VerticalAlign.Top }
})
.padding(10)
Text("端口")
.id('txtPort')
.fontSize(15)
.height(40)
.alignRules({
left: { anchor: '__container__', align: HorizontalAlign.Start },
top: { anchor: 'txtTitle', align: VerticalAlign.Bottom }
})
.padding(10)
TextInput({ text: this.port.toString() })
.onChange((value) => {
this.port = parseInt(value)
})
.type(InputType.Number)
.width(80)
.height(40)
.id('txtInputPort')
.fontSize(15)
.alignRules({
left: { anchor: 'txtPort', align: HorizontalAlign.End },
top: { anchor: 'txtPort', align: VerticalAlign.Top }
})
.padding(10)
Button(this.running ? "停止" : "啟動(dòng)")
.onClick(() => {
this.running = !this.running
if (this.running) {
this.start()
} else {
this.stop()
}
})
.height(40)
.width(80)
.id('butRun')
.fontSize(15)
.alignRules({
right: { anchor: '__container__', align: HorizontalAlign.End },
top: { anchor: 'txtPort', align: VerticalAlign.Top }
})
.padding(10)
Scroll(this.scroller) {
Text(this.msgHistory)
.textAlign(TextAlign.Start)
.padding(10)
.width('100%')
.fontSize(12)
.backgroundColor(0xeeeeee)
}
.alignRules({
left: { anchor: '__container__', align: HorizontalAlign.Start },
top: { anchor: 'butRun', align: VerticalAlign.Bottom }
})
.align(Alignment.Top)
.backgroundColor(0xeeeeee)
.height(200)
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.On)
.scrollBarWidth(20)
.padding(10)
.id('scrollHis')
Text("請求地址")
.id('txtUrl')
.fontSize(15)
.height(40)
.alignRules({
left: { anchor: '__container__', align: HorizontalAlign.Start },
top: { anchor: 'scrollHis', align: VerticalAlign.Bottom }
})
.padding(10)
TextInput({ text: this.webUrl.toString() })
.onChange((value) => {
this.webUrl = value
})
.height(40)
.id('txtInputWebUrl')
.fontSize(15)
.alignRules({
left: { anchor: 'txtUrl', align: HorizontalAlign.End },
top: { anchor: 'txtUrl', align: VerticalAlign.Top },
right: { anchor: 'butWeb', align: HorizontalAlign.Start }
})
.padding(10)
Button("請求")
.onClick(() => {
this.controller.loadUrl(this.webUrl)
})
.height(40)
.width(80)
.id('butWeb')
.fontSize(15)
.alignRules({
right: { anchor: '__container__', align: HorizontalAlign.End },
top: { anchor: 'txtUrl', align: VerticalAlign.Top }
})
.padding(10)
Scroll(this.webScroller) {
Web({ src: this.webUrl, controller: this.controller })
.padding(10)
.width('100%')
.backgroundColor(0xeeeeee)
.textZoomRatio(200)
}
.alignRules({
left: { anchor: '__container__', align: HorizontalAlign.Start },
top: { anchor: 'txtUrl', align: VerticalAlign.Bottom },
bottom: { anchor: '__container__', align: VerticalAlign.Bottom }
})
.backgroundColor(0xeeeeee)
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.On)
.scrollBarWidth(20)
}
.height('100%')
.width('100%')
}
start() {
this.webUrl = "http://127.0.0.1:" + this.port.toString()
let listenAddr: socket.NetAddress = {
address: '0.0.0.0',
port: this.port,
family: 1
}
tcpServer.listen(listenAddr, (err: BusinessError) => {
if (err) {
this.msgHistory += "listen fail \r\n";
return;
}
this.msgHistory += "listen success \r\n";
})
tcpServer.on('connect', (clientSocket: socket.TCPSocketConnection) => {
clientSocket.on('message', (msgInfo: socket.SocketMessageInfo) => {
let requestMsg = buf2String(msgInfo.message)
this.msgHistory += requestMsg + "\r\n"
let resp = buildRespString(requestMsg)
clientSocket.send({ data: resp })
})
});
}
stop() {
tcpServer.off('connect')
}
}
//構(gòu)造給客戶端的應(yīng)答內(nèi)容
function buildRespString(content: string) {
let result: string = ""
let bodyContent = "<html>"
bodyContent += "<head>"
bodyContent += "<title>"
bodyContent += "HTTP服務(wù)器模擬"
bodyContent += "</title>"
bodyContent += "</head>"
bodyContent += "<body>"
bodyContent += "<h1>"
bodyContent += "瀏覽器發(fā)送的請求信息"
bodyContent += "</h1>"
bodyContent += "<pre><h2>"
bodyContent += content
bodyContent += "</h2></pre>"
bodyContent += "</body>"
bodyContent += "</html>"
let textEncoder = new util.TextEncoder();
let contentBuf = textEncoder.encodeInto(bodyContent)
result += "HTTP/1.1 200 OK \r\n"
result += "Content-Type: text/html; charset=utf-8 \r\n"
result += `Content-Length: ${contentBuf.length} \r\n`
result += "\r\n"
result += bodyContent
return result
}
//ArrayBuffer轉(zhuǎn)utf8字符串
function buf2String(buf: ArrayBuffer) {
let msgArray = new Uint8Array(buf);
let textDecoder = util.TextDecoder.create("utf-8");
return textDecoder.decodeWithStream(msgArray)
}
步驟4:編譯運(yùn)行,可以使用模擬器或者真機(jī)诈嘿,因?yàn)楫?dāng)前還處于內(nèi)測期間堪旧,只能使用模擬器。
步驟5:具體的操作過程上面講過了奖亚,就不再贅述了淳梦。
3. 關(guān)鍵功能分析
比較關(guān)鍵的代碼如下:
tcpServer.on('connect', (clientSocket: socket.TCPSocketConnection) => {
clientSocket.on('message', (msgInfo: socket.SocketMessageInfo) => {
let requestMsg = buf2String(msgInfo.message)
this.msgHistory += requestMsg + "\r\n"
let resp = buildRespString(requestMsg)
clientSocket.send({ data: resp })
})
});
這里連接后得到了clientSocket對象,然后繼續(xù)訂閱clientSocket對象的收到客戶端消息事件昔字,把消息轉(zhuǎn)換為字符串爆袍,然后寫入到歷史日志msgHistory里。這里buildRespString函數(shù)是創(chuàng)建返回給客戶端的信息的作郭,最后通過clientSocket的send方法發(fā)送給客戶端陨囊。
雖然本示例比較簡單,但是具備了HTTP服務(wù)端的基本功能所坯,可以接收客戶端的輸入,并且可以對客戶端的輸入進(jìn)行處理挂捅,最后再發(fā)送給客戶端芹助。
(本文作者原創(chuàng),除非明確授權(quán)禁止轉(zhuǎn)載)
本文源碼地址:
https://gitee.com/zl3624/harmonyos_network_samples/tree/master/code/tcp/SimpleWebserver
本系列源碼地址: