鴻蒙網(wǎng)絡(luò)編程系列33-TLS回聲服務(wù)器示例

1. 網(wǎng)絡(luò)通訊的安全性問題

在本系列的第1、2、3、25篇文章,分別講解了使用UDP以及TCP進(jìn)行通訊的方式,并且以回聲服務(wù)器的形式分別演示了客戶端與服務(wù)端之間的通訊。這種通訊方式一般來說沒什么問題,但是在需要對內(nèi)容保密的情況下就不可取了,畢竟它們都是通過明文進(jìn)行通訊的,如果數(shù)據(jù)包在傳輸過程中被攔截,攻擊者可以直接讀取其中的信息,這使得用戶的敏感信息(如密碼、個(gè)人資料等)容易遭受竊聽或篡改。要避免這種情況的發(fā)生,可以使用TLS通訊,它通過加密技術(shù)確保數(shù)據(jù)的保密性和完整性,防止數(shù)據(jù)在傳輸過程中被竊聽或篡改。當(dāng)使用TLS進(jìn)行通訊時(shí),客戶端和服務(wù)器會(huì)先進(jìn)行一個(gè)握手過程,在這個(gè)過程中雙方協(xié)商加密算法、交換加密密鑰等,之后所有傳輸?shù)臄?shù)據(jù)都會(huì)被加密,即使數(shù)據(jù)包被第三方截獲,由于沒有解密密鑰,第三方也無法讀取數(shù)據(jù)的真實(shí)內(nèi)容。

在本系列的第7、8篇文章,介紹了TLS客戶端的使用,本篇將介紹TLS服務(wù)端的使用,TLS服務(wù)端在HarmonyOS NEXT的KIT開放能力模型中,歸屬于系統(tǒng)相關(guān)Kit開放能力中的Network Kit(網(wǎng)絡(luò)服務(wù)),對應(yīng)的類名稱為TLSSocketServer,使用如下的代碼導(dǎo)入模塊:

import { socket } from '@kit.NetworkKit';

在使用其方法前需要先通過socket.constructTLSSocketServerInstance方法創(chuàng)建實(shí)例。

本文將演示TLS服務(wù)端的用法,創(chuàng)建一個(gè)TLS回聲服務(wù)器,并通過TLS客戶端和其進(jìn)行通訊。

2. TLS回聲服務(wù)器演示

本示例運(yùn)行后的界面如圖所示:


0070086000024177922.20241022082112.09056532284099492479094490912753.png

選擇服務(wù)端數(shù)字證書及數(shù)字證書對應(yīng)的私鑰,輸入要綁定的服務(wù)端端口,然后單擊“啟動(dòng)”按鈕即可啟動(dòng)TLS服務(wù),如圖所示:


0070086000024177922.20241022082149.61400148519038197683585768422766.png

然后啟動(dòng)TLS客戶端,可以使用本系列前述文章介紹的客戶端,也可以使用其他客戶端,啟動(dòng)后,再選擇服務(wù)端CA證書,輸入服務(wù)端地址和端口,最后連接服務(wù)端,如圖所示:

0070086000024177922.20241022082227.75079893149114246197136275857174.png

可以看到,TLS服務(wù)端連接成功了,并且在日志區(qū)域輸出了服務(wù)端的證書信息。

下面測試TLS通訊,輸入要發(fā)送的信息,然后單擊“發(fā)送”按鈕,就會(huì)收到服務(wù)端自動(dòng)回復(fù)的消息,如圖所示:

0070086000024177922.20241022082305.44186331433616967776911687611378.png

此時(shí),查看TLS服務(wù)端界面,可以看到服務(wù)端也收到了客戶端的消息:

0070086000024177922.20241022082322.61655721921566012678436739167303.png

3. TLS回聲服務(wù)器示例編寫

下面詳細(xì)介紹創(chuà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 { ArrayList, buffer, util } from '@kit.ArkTS';
import fs from '@ohos.file.fs';
import { picker } from '@kit.CoreFileKit';

//TLS服務(wù)端實(shí)例
let tlsSocketServer: socket.TLSSocketServer = socket.constructTLSSocketServerInstance()

@Entry
@Component
struct Index {
  @State title: string = 'TLS回聲服務(wù)器示例';
  @State running: boolean = false
  //連接、通訊歷史記錄
  @State msgHistory: string = ''
  //本地端口
  @State port: number = 9999
  //選擇的證書文件
  @State certFileUri: string = ''
  //選擇的私鑰文件
  @State keyFileUri: string = ''
  scroller: Scroller = new Scroller()
  //已連接的客戶端列表
  clientList = new ArrayList<socket.TLSSocketConnection>()

  build() {
    Row() {
      Column() {
        Text(this.title)
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .width('100%')
          .textAlign(TextAlign.Center)
          .padding(10)

        Flex({ justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) {
          Text("服務(wù)端數(shù)字證書")
            .fontSize(14)
            .flexGrow(1)

          Button("選擇")
            .onClick(async () => {
              this.certFileUri = await selectSingleDocFile(getContext(this))
            })
            .width(70)
            .fontSize(14)
        }
        .width('100%')
        .padding(5)

        Text(this.certFileUri)
          .width('100%')
          .padding(5)

        Flex({ justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) {
          Text("服務(wù)端數(shù)字證書私鑰:")
            .fontSize(14)
            .flexGrow(1)

          Button("選擇")
            .onClick(async () => {
              this.keyFileUri = await selectSingleDocFile(getContext(this))
            })
            .width(70)
            .fontSize(14)
        }
        .width('100%')
        .padding(5)

        Text(this.keyFileUri)
          .width('100%')
          .padding(5)


        Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {
          Text("綁定的服務(wù)器端口:")
            .fontSize(14)
            .width(150)

          TextInput({ text: this.port.toString() })
            .type(InputType.Number)
            .onChange((value) => {
              this.port = parseInt(value)
            })
            .fontSize(12)
            .width(100)
            .flexGrow(1)

          Button(this.running ? "停止" : "啟動(dòng)")
            .onClick(() => {
              if (!this.running) {
                this.startServer()
              } else {
                this.stopServer()
              }
            })
            .width(70)
            .fontSize(14)
        }
        .width('100%')
        .padding(5)

        Scroll(this.scroller) {
          Text(this.msgHistory)
            .textAlign(TextAlign.Start)
            .padding(10)
            .width('100%')
            .backgroundColor(0xeeeeee)
        }
        .align(Alignment.Top)
        .backgroundColor(0xeeeeee)
        .height(300)
        .flexGrow(1)
        .scrollable(ScrollDirection.Vertical)
        .scrollBar(BarState.On)
        .scrollBarWidth(20)
      }
      .width('100%')
      .justifyContent(FlexAlign.Start)
      .height('100%')
    }
    .height('100%')
  }

  //停止服務(wù)
  stopServer() {
    tlsSocketServer.off('connect')
    for (let client of this.clientList) {
      client.off('message')
    }
    this.running = false
    this.msgHistory += "停止服務(wù)\r\n"
  }

  //獲取tls監(jiān)聽配置信息
  getTlsConnOptions(): socket.TLSConnectOptions {
    let listenAddress: socket.NetAddress = { address: '0.0.0.0', port: this.port }
    let context = getContext(this)
    let tlsSecOptions: socket.TLSSecureOptions = {
      cert: copy2SandboxAndReadContent(context, this.certFileUri),
      key: copy2SandboxAndReadContent(context, this.keyFileUri)
    }

    return { address: listenAddress, secureOptions: tlsSecOptions }
  }

  //啟動(dòng)服務(wù)
  async startServer() {
    //獲取tls監(jiān)聽配置
    let tlsConnOptions = this.getTlsConnOptions()
    //綁定到指定的端口并啟動(dòng)客戶端連接監(jiān)聽
    await tlsSocketServer.listen(tlsConnOptions).then(this.onListenSuccessful)
      .catch((err: BusinessError) => {
        this.msgHistory += `監(jiān)聽失敗: 錯(cuò)誤碼 ${err.code}, 錯(cuò)誤信息 ${JSON.stringify(err)}\r\n`;
      })
    //訂閱連接事件消息
    tlsSocketServer.on('connect', this.onNewClientConnected);
  }

  //監(jiān)聽成功的回調(diào)
  onListenSuccessful = async () => {
    let listenAddr: socket.NetAddress = await tlsSocketServer.getLocalAddress()
    this.msgHistory += `監(jiān)聽成功[${listenAddr.address}:${listenAddr.port}]\r\n`
    this.running = true
    this.msgHistory += "服務(wù)啟動(dòng)\r\n"
  }
  //接受新的客戶端連接的回調(diào)
  onNewClientConnected = async (clientSocket: socket.TLSSocketConnection) => {
    this.clientList.add(clientSocket)
    //客戶端地址
    let clientAddr: socket.NetAddress = await clientSocket.getRemoteAddress()
    this.msgHistory += `接受新的客戶端連接[${clientAddr.address}:${clientAddr.port}]\r\n`

    clientSocket.on('message', (msgInfo: socket.SocketMessageInfo) => {
      //收到的信息轉(zhuǎn)化為字符串
      let content = buf2String(msgInfo.message)
      //顯示信息日志,最后加上回車換行
      this.msgHistory += `[${msgInfo.remoteInfo.address}:${msgInfo.remoteInfo.port}]${content}\r\n`
      //把收到的信息發(fā)回客戶端
      clientSocket.send(buffer.from(content).buffer)
    })
  }
}

//選擇一個(gè)文件
async function selectSingleDocFile(context: Context): Promise<string> {
  let selectedFilePath: string = ""
  let documentPicker = new picker.DocumentViewPicker(context);
  await documentPicker.select({ maxSelectNumber: 1 }).then((result) => {
    if (result.length > 0) {
      selectedFilePath = result[0]
    }
  })
  return selectedFilePath
}

//復(fù)制文件到沙箱并讀取文件內(nèi)容
function copy2SandboxAndReadContent(context: Context, filePath: string): string {
  let segments = filePath.split('/')
  let fileName = segments[segments.length-1]
  let realUri = context.cacheDir + "/" + fileName
  let file = fs.openSync(filePath);
  fs.copyFileSync(file.fd, realUri)
  fs.closeSync(file)

  return fs.readTextSync(realUri)
}

//ArrayBuffer轉(zhuǎn)utf8字符串
export function buf2String(buf: ArrayBuffer) {
  let msgArray = new Uint8Array(buf);
  let textDecoder = util.TextDecoder.create("utf-8");
  return textDecoder.decodeToString(msgArray)
}

步驟4:編譯運(yùn)行,可以使用模擬器或者真機(jī)。

步驟5:按照本節(jié)第2部分“TLS回聲服務(wù)器演示”操作即可。

4. 代碼分析

本示例關(guān)鍵點(diǎn)在于TLS服務(wù)器的配置,特別是配置TLS服務(wù)端的證書,因?yàn)槲募?quán)限的關(guān)系,本示例在用戶選擇證書和證書私鑰文件后,把這些文件首選復(fù)制到沙箱,然后再讀取文件內(nèi)容,TLS配置的代碼如下所示:

  getTlsConnOptions(): socket.TLSConnectOptions {
    let listenAddress: socket.NetAddress = { address: '0.0.0.0', port: this.port }
    let context = getContext(this)
    let tlsSecOptions: socket.TLSSecureOptions = {
      cert: copy2SandboxAndReadContent(context, this.certFileUri),
      key: copy2SandboxAndReadContent(context, this.keyFileUri)
    }

    return { address: listenAddress, secureOptions: tlsSecOptions }
  }

復(fù)制文件到沙箱并讀取文件內(nèi)容的代碼如下所示:

function copy2SandboxAndReadContent(context: Context, filePath: string): string {
  let segments = filePath.split('/')
  let fileName = segments[segments.length-1]
  let realUri = context.cacheDir + "/" + fileName
  let file = fs.openSync(filePath);
  fs.copyFileSync(file.fd, realUri)
  fs.closeSync(file)

  return fs.readTextSync(realUri)
}

(本文作者原創(chuàng),除非明確授權(quán)禁止轉(zhuǎn)載)

本文源碼地址:
https://gitee.com/zl3624/harmonyos_network_samples/tree/master/code/tls/TLSEchoServer

本系列源碼地址:
https://gitee.com/zl3624/harmonyos_network_samples

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容