鴻蒙網(wǎng)絡(luò)編程系列23-實(shí)現(xiàn)一個(gè)基于鴻蒙API的HTTP服務(wù)器

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)行后的界面如下所示:


0.png

輸入要監(jiān)聽的端口,然后單擊“啟動(dòng)”按鈕傅物,即可在127.0.0.1上啟動(dòng)對端口的TCP協(xié)議監(jiān)聽了夯辖。然后在請求地址欄輸入服務(wù)端地址,再單擊“請求”按鈕董饰,既可請求剛創(chuàng)建的HTTP服務(wù)器蒿褂,效果如圖所示:

image.png

這里實(shí)現(xiàn)的HTTP服務(wù)器是這樣的,接收到客戶端的請求后卒暂,會(huì)把客戶端發(fā)送的信息作為網(wǎng)頁的內(nèi)容反饋給客戶端啄栓,也就是上圖中看到的這些內(nèi)容:

image.png

下面詳細(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

本系列源碼地址:

https://gitee.com/zl3624/harmonyos_network_samples

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末闲先,一起剝皮案震驚了整個(gè)濱河市状土,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌伺糠,老刑警劉巖蒙谓,帶你破解...
    沈念sama閱讀 219,490評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異训桶,居然都是意外死亡累驮,警方通過查閱死者的電腦和手機(jī)酣倾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來谤专,“玉大人躁锡,你說我怎么就攤上這事≈檬蹋” “怎么了映之?”我有些...
    開封第一講書人閱讀 165,830評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長蜡坊。 經(jīng)常有香客問我杠输,道長,這世上最難降的妖魔是什么秕衙? 我笑而不...
    開封第一講書人閱讀 58,957評論 1 295
  • 正文 為了忘掉前任蠢甲,我火速辦了婚禮,結(jié)果婚禮上灾梦,老公的妹妹穿的比我還像新娘峡钓。我一直安慰自己,他們只是感情好若河,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,974評論 6 393
  • 文/花漫 我一把揭開白布能岩。 她就那樣靜靜地躺著,像睡著了一般萧福。 火紅的嫁衣襯著肌膚如雪拉鹃。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,754評論 1 307
  • 那天鲫忍,我揣著相機(jī)與錄音膏燕,去河邊找鬼。 笑死悟民,一個(gè)胖子當(dāng)著我的面吹牛坝辫,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播射亏,決...
    沈念sama閱讀 40,464評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼近忙,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了智润?” 一聲冷哼從身側(cè)響起及舍,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎窟绷,沒想到半個(gè)月后锯玛,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,847評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡兼蜈,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,995評論 3 338
  • 正文 我和宋清朗相戀三年攘残,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了拙友。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,137評論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡肯腕,死狀恐怖献宫,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情实撒,我是刑警寧澤姊途,帶...
    沈念sama閱讀 35,819評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站知态,受9級特大地震影響捷兰,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜负敏,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,482評論 3 331
  • 文/蒙蒙 一贡茅、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧其做,春花似錦顶考、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至蹈胡,卻和暖如春渊季,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背罚渐。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評論 1 272
  • 我被黑心中介騙來泰國打工却汉, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人荷并。 一個(gè)月前我還...
    沈念sama閱讀 48,409評論 3 373
  • 正文 我出身青樓合砂,卻偏偏與公主長得像,于是被迫代替她去往敵國和親源织。 傳聞我的和親對象是個(gè)殘疾皇子翩伪,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,086評論 2 355

推薦閱讀更多精彩內(nèi)容