写点什么

鸿蒙网络编程系列 33-TLS 回声服务器示例

作者:长弓三石
  • 2024-10-23
    广东
  • 本文字数:4831 字

    阅读完需:约 16 分钟

1. 网络通讯的安全性问题

在本系列的第 1、2、3、25 篇文章,分别讲解了使用 UDP 以及 TCP 进行通讯的方式,并且以回声服务器的形式分别演示了客户端与服务端之间的通讯。这种通讯方式一般来说没什么问题,但是在需要对内容保密的情况下就不可取了,毕竟它们都是通过明文进行通讯的,如果数据包在传输过程中被拦截,攻击者可以直接读取其中的信息,这使得用户的敏感信息(如密码、个人资料等)容易遭受窃听或篡改。要避免这种情况的发生,可以使用 TLS 通讯,它通过加密技术确保数据的保密性和完整性,防止数据在传输过程中被窃听或篡改。当使用 TLS 进行通讯时,客户端和服务器会先进行一个握手过程,在这个过程中双方协商加密算法、交换加密密钥等,之后所有传输的数据都会被加密,即使数据包被第三方截获,由于没有解密密钥,第三方也无法读取数据的真实内容。


在本系列的第 7、8 篇文章,介绍了 TLS 客户端的使用,本篇将介绍 TLS 服务端的使用,TLS 服务端在 HarmonyOS NEXT 的 KIT 开放能力模型中,归属于系统相关 Kit 开放能力中的 Network Kit(网络服务),对应的类名称为 TLSSocketServer,使用如下的代码导入模块:


import { socket } from '@kit.NetworkKit';
复制代码


在使用其方法前需要先通过 socket.constructTLSSocketServerInstance 方法创建实例。


本文将演示 TLS 服务端的用法,创建一个 TLS 回声服务器,并通过 TLS 客户端和其进行通讯。

2. TLS 回声服务器演示

本示例运行后的界面如图所示:



选择服务端数字证书及数字证书对应的私钥,输入要绑定的服务端端口,然后单击“启动”按钮即可启动 TLS 服务,如图所示:



然后启动 TLS 客户端,可以使用本系列前述文章介绍的客户端,也可以使用其他客户端,启动后,再选择服务端 CA 证书,输入服务端地址和端口,最后连接服务端,如图所示:



可以看到,TLS 服务端连接成功了,并且在日志区域输出了服务端的证书信息。


下面测试 TLS 通讯,输入要发送的信息,然后单击“发送”按钮,就会收到服务端自动回复的消息,如图所示:



此时,查看 TLS 服务端界面,可以看到服务端也收到了客户端的消息:


3. TLS 回声服务器示例编写

下面详细介绍创建该示例的步骤。


步骤 1:创建 Empty Ability 项目。


步骤 2:在 module.json5 配置文件加上对权限的声明:


"requestPermissions": [      {        "name": "ohos.permission.INTERNET"      }    ]
复制代码


这里添加了访问互联网的权限。


步骤 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服务端实例let tlsSocketServer: socket.TLSSocketServer = socket.constructTLSSocketServerInstance()
@Entry@Componentstruct Index { @State title: string = 'TLS回声服务器示例'; @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("服务端数字证书") .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("服务端数字证书私钥:") .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("绑定的服务器端口:") .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 ? "停止" : "启动") .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%') }
//停止服务 stopServer() { tlsSocketServer.off('connect') for (let client of this.clientList) { client.off('message') } this.running = false this.msgHistory += "停止服务\r\n" }
//获取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 } }
//启动服务 async startServer() { //获取tls监听配置 let tlsConnOptions = this.getTlsConnOptions() //绑定到指定的端口并启动客户端连接监听 await tlsSocketServer.listen(tlsConnOptions).then(this.onListenSuccessful) .catch((err: BusinessError) => { this.msgHistory += `监听失败: 错误码 ${err.code}, 错误信息 ${JSON.stringify(err)}\r\n`; }) //订阅连接事件消息 tlsSocketServer.on('connect', this.onNewClientConnected); }
//监听成功的回调 onListenSuccessful = async () => { let listenAddr: socket.NetAddress = await tlsSocketServer.getLocalAddress() this.msgHistory += `监听成功[${listenAddr.address}:${listenAddr.port}]\r\n` this.running = true this.msgHistory += "服务启动\r\n" } //接受新的客户端连接的回调 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) => { //收到的信息转化为字符串 let content = buf2String(msgInfo.message) //显示信息日志,最后加上回车换行 this.msgHistory += `[${msgInfo.remoteInfo.address}:${msgInfo.remoteInfo.port}]${content}\r\n` //把收到的信息发回客户端 clientSocket.send(buffer.from(content).buffer) }) }}
//选择一个文件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}
//复制文件到沙箱并读取文件内容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转utf8字符串export function buf2String(buf: ArrayBuffer) { let msgArray = new Uint8Array(buf); let textDecoder = util.TextDecoder.create("utf-8"); return textDecoder.decodeToString(msgArray)}
复制代码


步骤 4:编译运行,可以使用模拟器或者真机。


步骤 5:按照本节第 2 部分“TLS 回声服务器演示”操作即可。

4. 代码分析

本示例关键点在于 TLS 服务器的配置,特别是配置 TLS 服务端的证书,因为文件权限的关系,本示例在用户选择证书和证书私钥文件后,把这些文件首选复制到沙箱,然后再读取文件内容,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 } }
复制代码


复制文件到沙箱并读取文件内容的代码如下所示:


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)}
复制代码


(本文作者原创,除非明确授权禁止转载)


本文源码地址:


https://gitee.com/zl3624/harmonyos_network_samples/tree/master/code/tls/TLSEchoServer


本系列源码地址:


https://gitee.com/zl3624/harmonyos_network_samples


发布于: 刚刚阅读数: 5
用户头像

长弓三石

关注

还未添加个人签名 2024-10-16 加入

二十多年软件开发经验的软件架构师,华为HDE、华为云HCDE、仓颉语言CLD、CCS,著有《仓颉语言网络编程》、《仓颉语言元编程》、《仓颉语言实战》、《鲲鹏架构入门与实战》等书籍,清华大学出版社出版。

评论

发布
暂无评论
鸿蒙网络编程系列33-TLS回声服务器示例_DevEco Studio_长弓三石_InfoQ写作社区