鸿蒙网络编程系列59-仓颉版TLS回声服务器示例
1. 网络通讯的安全性问题及解决方案
在基于TCP或者UDP的通讯中,通讯内容是明文发送和接收的,对于安全性要求不太高的通讯场景,这种方式因为实现简单,传输效率高而得到广泛应用;
但是,如果数据包在传输过程中被拦截,攻击者可以直接读取其中的信息,这使得用户的敏感信息(如密码、个人资料等)容易遭受窃听或篡改。要避免这种情况的发生,
就需要对传输过程进行加密,典型的是基于TLS协议的通讯,它通过加密技术确保数据的保密性和完整性,防止数据在传输过程中被窃听或篡改。当使用TLS进行通讯时,客户端和服务器先进行握手,在这个过程中双方协商加密算法、交换加密密钥等,之后所有传输的数据都会被加密,即使数据包被第三方截获,由于没有解密密钥,第三方也无法读取数据的真实内容。
本系列的第33篇文章,在API 12环境下使用ArkTS语言实现了TLS回声服务器,本篇文章将在API 17环境下,使用仓颉语言实现TLS回声服务器。
在目前的版本里,鸿蒙并没有提供TLS服务端相关的API,所以,本文将使用仓颉语言原生的TLS相关API实现,典型的类有TlsSocket、TlsServerConfig等,它们在net.tls包里。
TLS服务端的实现还需要服务端数字证书及私钥,需要预先准备好相关的文件并上传到鸿蒙设备中。
2. TLS回声服务器演示
本示例运行后的界面如图所示:
选择服务端数字证书及服务端数字证书私钥,然后单击“启动”按钮,可以绑定服务端到本地端口,如图所示:
更进一步的TLS通讯需要TLS客户端的配合,我们将在下一篇文章介绍TLS服务端和客户端的数据收发过程。
3. TLS回声服务器示例编写
下面详细介绍创建该示例的步骤(确保DevEco Studio已安装仓颉插件)。
步骤1:创建[Cangjie]Empty Ability项目。
步骤2:在module.json5配置文件加上对权限的声明:
\"requestPermissions\": [ { \"name\": \"ohos.permission.INTERNET\" } ]
这里添加了访问互联网的权限。
步骤3:在build-profile.json5配置文件加上仓颉编译架构:
\"cangjieOptions\": { \"path\": \"./src/main/cangjie/cjpm.toml\", \"abiFilters\": [\"arm64-v8a\", \"x86_64\"] }
步骤4:在main_ability.cj文件里添加如下的代码:
package ohos_app_cangjie_entryinternal import ohos.base.AppLoginternal import ohos.ability.AbilityStageinternal import ohos.ability.LaunchReasoninternal import cj_res_entry.appimport ohos.ability.*//Ability全局上下文var globalAbilityContext: Option<AbilityContext> = Option<AbilityContext>.Noneclass MainAbility <: Ability { public init() { super() registerSelf() } public override func onCreate(want: Want, launchParam: LaunchParam): Unit { AppLog.info(\"MainAbility OnCreated.${want.abilityName}\") globalAbilityContext = Option<AbilityContext>.Some(this.context) match (launchParam.launchReason) { case LaunchReason.START_ABILITY => AppLog.info(\"START_ABILITY\") case _ => () } } public override func onWindowStageCreate(windowStage: WindowStage): Unit { AppLog.info(\"MainAbility onWindowStageCreate.\") windowStage.loadContent(\"EntryView\") }}
步骤5:在index.cj文件里添加如下的代码:
package ohos_app_cangjie_entryimport ohos.base.*import ohos.component.*import ohos.state_manage.*import ohos.state_macro_manage.*import ohos.file_picker.*import ohos.ability.*import ohos.file_fs.*import crypto.x509.*import ohos.crypto.*import std.convert.*import net.tls.*import std.socket.*@Observed//文件选择状态class FileSelectStatus { @Publish public var fileSelected: Bool = false @Publish public var fileUri: String = \"\"}@Entry@Componentclass EntryView { @State var title: String = \'TLS回声服务器示例\'; //连接、通讯历史记录 @State var msgHistory: String = \'\' //证书文件选择状态 @State var certFileStatus: FileSelectStatus = FileSelectStatus() //私钥文件选择状态 @State var keyFileStatus: FileSelectStatus = FileSelectStatus() //服务端端口 @State var port: UInt16 = 9990 //服务运行状态 @State var running: Bool = false //服务端套接字 var echoServer: ?TcpServerSocket = None let scroller: Scroller = Scroller() func build() { Row { Column { Text(title) .fontSize(14) .fontWeight(FontWeight.Bold) .width(100.percent) .textAlign(TextAlign.Center) .padding(10) Flex(FlexParams(justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center)) { Text(\"服务端数字证书:\").fontSize(14).width(90).flexGrow(1) Button(\"选择\").onClick { evt => selectFile(this.certFileStatus) }.width(60).fontSize(14) }.width(100.percent).padding(5) Text(certFileStatus.fileUri).fontSize(14).width(100.percent).padding(10) Flex(FlexParams(justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center)) { Text(\"服务端数字证书私钥:\").fontSize(14).width(90).flexGrow(1) Button(\"选择\").onClick { evt => selectFile(this.keyFileStatus) }.width(60).fontSize(14) }.width(100.percent).padding(5) Text(keyFileStatus.fileUri).fontSize(14).width(100.percent).padding(10) Flex(FlexParams(justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center)) { Text(\"绑定的服务器端口:\").fontSize(14).width(90).flexGrow(1) TextInput(text: port.toString()) .onChange({ value => if (value == \"\") { port = 0 } else { port = UInt16.parse(value) } }) .setType(InputType.Number) .width(80) .fontSize(11) Button(if (running) { \"停止\" } else { \"启动\" }) .onClick { evt => if (!running) { startServer() } else { stopServer() } } .width(60) .fontSize(14) .enabled(certFileStatus.fileSelected && keyFileStatus.fileSelected && port != 0) }.width(100.percent).padding(5) Scroll(scroller) { Text(msgHistory) .textAlign(TextAlign.Start) .padding(10) .width(100.percent) .backgroundColor(0xeeeeee) } .align(Alignment.Top) .backgroundColor(0xeeeeee) .height(300) .flexGrow(1) .scrollable(ScrollDirection.Vertical) .scrollBar(BarState.On) .scrollBarWidth(20) }.width(100.percent).height(100.percent) }.height(100.percent) } //选择文件 func selectFile(fileSelectStatus: FileSelectStatus) { let picker = DocumentViewPicker(getContext()) let documentSelectCallback = { errorCode: Option<AsyncError>, data: Option<Array<String>> => match (errorCode) { case Some(e) => msgHistory += \"选择失败,错误码:${e.code}\\r\\n\" case _ => match (data) { case Some(value) => fileSelectStatus.fileUri = value[0] fileSelectStatus.fileSelected = true case _ => () } } } picker.select(documentSelectCallback, option: DocumentSelectOptions(selectMode: DocumentSelectMode.MIXED)) } //启动tls服务器 func startServer() { let socketAddress = SocketAddress(\"0.0.0.0\", port) //回显TcpSocket服务端 echoServer = TcpServerSocket(bindAt: socketAddress) let tlsCfg = getTlsServerCfg() //允许恢复tls会话 let sessionContext = TlsSessionContext.fromName(\"echo-server\") //启动一个线程监听客户端连接 spawn { //绑定到本地端口 echoServer?.bind() msgHistory += \"绑定到本地端口,等待连接...\\r\\n\" running = true while (true) { //已接受客户端连接 let acceptEchoSocket = echoServer?.accept() if (let Some(echoSocket) <- acceptEchoSocket) { msgHistory += \"接受新的连接,对端地址为:${echoSocket.remoteAddress.kapString()}\\r\\n\" //启动一个线程处理新的socket spawn { try { //生成服务端TLS套接字 let tlsSocket = TlsSocket.server(echoSocket, sessionContext: sessionContext, serverConfig: tlsCfg) //握手 tlsSocket.handshake() msgHistory += \"已握手\\r\\n\" //处理加密通讯 dealWithEchoSocket(tlsSocket) } catch (err: SocketException) { println(err.message) } } } } } } //从socket读取数据并回写到socket func dealWithEchoSocket(echoSocket: TlsSocket) { //存放从socket读取数据的缓冲区 let buffer = Array<UInt8>(1024, item: 0) while (true) { //从socket读取数据 var readCount = echoSocket.read(buffer) if (readCount > 0) { //把接收到的数据转换为字符串 let content = String.fromUtf8(buffer[0..readCount]) msgHistory += \"[${echoSocket.remoteAddress}]:${content}\\r\\n\" //回写客户端,把content写入echoSocket echoSocket.write(content.toArray()) } } } //停止tls服务器 func stopServer() { echoServer?.close() running = false msgHistory += \"服务已停止\\r\\n\" } //获取服务端TLS配置信息 func getTlsServerCfg() { //得到服务端x509数字证书 let x509 = getCert(certFileStatus.fileUri) //得到服务端私钥 let privateKey = getPrivateKey(keyFileStatus.fileUri) var tlsCfg = TlsServerConfig(x509, privateKey) //设置支持的TLS版本 tlsCfg.maxVersion = TlsVersion.V1_3 tlsCfg.minVersion = TlsVersion.V1_2 return tlsCfg } //获取私钥 func getPrivateKey(keyPath: String) { //获取文件在沙箱cache文件夹的路径 let sandboxFilePath = getSandboxFilePath(keyPath) //从沙箱读取证书文件信息 let certContent = FileFs.readText(sandboxFilePath) return PrivateKey.decodeFromPem(certContent) } //把文件复制到沙箱并返回沙箱中的文件路径 func getSandboxFilePath(oriFilePath: String) { let fileName = getFileNameFromPath(oriFilePath) let file = FileFs.open(oriFilePath) //构造文件在沙箱cache文件夹的路径 let sandboxFilePath = getContext().filesDir.replace(\"files\", \"cache\") + \"/\" + fileName //复制私钥到沙箱给定路径 FileFs.copyFile(file.fd, sandboxFilePath) //关闭文件 FileFs.close(file) return sandboxFilePath } //获取数字证书 func getCert(certPath: String) { //获取文件在沙箱cache文件夹的路径 let sandboxFilePath = getSandboxFilePath(certPath) //从沙箱读取证书文件信息 let certContent = FileFs.readText(sandboxFilePath) return X509Certificate.decodeFromPem(certContent) } //从文件路径获取文件名称 public func getFileNameFromPath(filePath: String) { let segments = filePath.split(\'/\') //文件名称 return segments[segments.size - 1] } //获取Ability上下文 func getContext(): AbilityContext { match (globalAbilityContext) { case Some(context) => context case _ => throw Exception(\"获取全局Ability上下文异常\") } }}
步骤6:编译运行,可以使用模拟器或者真机。
步骤7:按照本文第2部分“TLS回声服务器演示”操作即可。
4. 代码分析
仓颉语言版本的TLS服务器和ArkTS版本的实现差异非常大,基本没有相似性,相对来说,仓颉语言版本更靠底层一些,首先是启动一个TCP服务器,在指定的端口进行监听,在监听到新的客户端连接时,启动一个新的线程处理该连接,该线程调用TlsSocket的server函数生成一个服务端TLS套接字,接着处理该套接字的读写,主线程则继续监听新的连接。
(本文作者原创,除非明确授权禁止转载)
本文源码地址:
https://gitee.com/zl3624/harmonyos_network_samples/tree/master/code/tls/TLSEchoServer4Cj
本系列源码地址:
https://gitee.com/zl3624/harmonyos_network_samples