鸿蒙网络编程系列23-实现一个基于鸿蒙API的HTTP服务器

news2024/11/28 15:58:41

1. 鸿蒙HTTP服务器可行性分析

看到这个题目,可能有的小伙伴会有一些疑问:

“鸿蒙API从9到12我都翻烂了,也没见提供HTTP服务器的接口啊”

“我BUG写得少,你可不要骗我”

的确,目前的鸿蒙API没有提供HTTP服务端的实现接口,但是我们知道,HTTP协议是基于TCP的,而鸿蒙API10提供了TCPSocketServer类,包含了如下几个关键接口:

listen(address: NetAddress): Promise<void>

监听方法,绑定IP地址和端口,端口可以指定或由系统随机分配。成功调用该方法后,TCPSocketServer对象监听并接受与此套接字建立的TCPSocket连接。

on(type: 'connect', callback: Callback<TCPSocketConnection>): void

订阅TCPSocketServer的连接事件,在新的客户端套接字连接上以后,会触发callback回调,在回调中包含TCPSocketConnection对象,该对象就表示TCPSocket客户端与服务端的连接。

TCPSocketConnection是服务端和客户端通讯的基础,提供了发送数据到客户端的方法:

send(options: TCPSendOptions): Promise<void>

还提供了订阅客户端消息接收的事件:

on(type: 'message', callback: Callback<SocketMessageInfo>): void

在这个事件的callback回调里,包含了SocketMessageInfo参数,该参数的属性message就是客户端发送过来的消息。

通过上述几个接口,我们就可以在服务端开启TCP监听,并且处理客户端的连接和消息收发。

再来说一下HTTP协议,众所周知,HTTP协议是一个简单的请求响应协议,根据RFC 9112,HTTP协议1.1版本的消息格式如下所示:

  HTTP-message = start-line CRLF
                   *( field-line CRLF )
                   CRLF
                   [ message-body ]

其中,start-line表示起始行,CRLF表示回车换行符号,field-line表示首部字段行,*( field-line CRLF )说明首部字段可以是零个或者多个,最后的[ message-body ]表示可选的消息正文;因为消息分为请求消息和应答消息,所以起始行又可以分为请求行和状态行,如下所示:

start-line     = request-line / status-line

当然,HTTP的协议还是有一点复杂的,这里就不展开了,不过我们明白,只要我们按照协议格式构造出了请求应答的文本,然后使用TCP协议作为传输层进行收发即可。

有了上面的API接口,加上HTTP协议的格式,就可以打造一个最简单的HTTP服务端了。

2. 实现HTTP服务器示例

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

cke_260763.jpg

输入要监听的端口,然后单击“启动”按钮,即可在127.0.0.1上启动对端口的TCP协议监听了。然后在请求地址栏输入服务端地址,再单击“请求”按钮,既可请求刚创建的HTTP服务器,效果如图所示:

cke_410364.jpg

这里实现的HTTP服务器是这样的,接收到客户端的请求后,会把客户端发送的信息作为网页的内容反馈给客户端,也就是上图中看到的这些内容:

cke_493678.png

下面详细介绍创建该应用的步骤。

步骤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 { util } from '@kit.ArkTS';
import { webview } from '@kit.ArkWeb';
​
let tcpServer: socket.TCPSocketServer = socket.constructTCPSocketServerInstance();
​
@Entry
@Component
struct Index {
  @State message: string = '最简单的HTTP服务器示例';
  @State port: number = 8080
  @State running: boolean = false
  @State msgHistory: string = ''
  @State webUrl: string = "https://developer.huawei.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 ? "停止" : "启动")
        .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')
  }
}
​
//构造给客户端的应答内容
function buildRespString(content: string) {
  let result: string = ""
  let bodyContent = "<html>"
  bodyContent += "<head>"
  bodyContent += "<title>"
  bodyContent += "HTTP服务器模拟"
  bodyContent += "</title>"
  bodyContent += "</head>"
  bodyContent += "<body>"
  bodyContent += "<h1>"
  bodyContent += "浏览器发送的请求信息"
  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转utf8字符串
function buf2String(buf: ArrayBuffer) {
  let msgArray = new Uint8Array(buf);
  let textDecoder = util.TextDecoder.create("utf-8");
  return textDecoder.decodeWithStream(msgArray)
}

步骤4:编译运行,可以使用模拟器或者真机,因为当前还处于内测期间,只能使用模拟器。

步骤5:具体的操作过程上面讲过了,就不再赘述了。

3. 关键功能分析

比较关键的代码如下:

   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对象,然后继续订阅clientSocket对象的收到客户端消息事件,把消息转换为字符串,然后写入到历史日志msgHistory里。这里buildRespString函数是创建返回给客户端的信息的,最后通过clientSocket的send方法发送给客户端。

虽然本示例比较简单,但是具备了HTTP服务端的基本功能,可以接收客户端的输入,并且可以对客户端的输入进行处理,最后再发送给客户端。

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

本文源码地址:

https://gitee.com/zl3624/harmonyos_network_samples/tree/master/code/tcp/SimpleWebserver

本系列源码地址:

https://gitee.com/zl3624/harmonyos_network_samples

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2218937.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【每日一题】【算法双周赛】【第 20 场 小白入门赛评价/分享】赛后另类AI写题分析分享

第 20 场 小白入门赛 1. 四个亲戚【算法赛】2. 黛玉泡茶【算法赛】AI分析具体实现代码解析复杂度分析示例运行 结果二 3. 宝玉请安【算法赛】AI分析问题分析路径计算代码实现代码解析示例运行复杂度分析 结果&#xff1a; 交上去 4. 贾母祝寿【算法赛】AI分析问题分析实现步骤代…

【日志】关于多益网申

2024.10.19 早先听闻多益的测试题非常抽象&#xff0c;凡是测过的人都说太抽象了&#xff0c;我还以为他考我各种算法或者编程语言呢。我今天也去做了一下&#xff0c;测试题里面大多都考些计算题&#xff0c;找规律题&#xff0c;判断推理题&#xff0c;还有一些图形转换&…

计算机毕业设计 基于java旅游攻略平台的设计与实现 Java+SpringBoot+Vue 前后端分离 文档报告 代码讲解 安装调试

&#x1f34a;作者&#xff1a;计算机编程-吉哥 &#x1f34a;简介&#xff1a;专业从事JavaWeb程序开发&#xff0c;微信小程序开发&#xff0c;定制化项目、 源码、代码讲解、文档撰写、ppt制作。做自己喜欢的事&#xff0c;生活就是快乐的。 &#x1f34a;心愿&#xff1a;点…

【命令操作】Linux中多种关机和重启的命令介绍 _ 统信 _ 麒麟 _ 方德

往期好文&#xff1a;【系统配置】信创系统配置文件保护与防篡改 | 统信 | 麒麟 | 方德 Hello&#xff0c;大家好啊&#xff01;今天给大家带来一篇关于在Linux系统中多种关机和重启命令介绍的文章。Linux作为一个广泛应用的开源操作系统&#xff0c;提供了多种关机和重启的方式…

jupyter notebook 笔记

nbclassic 经典版 新版的 jupyter notebook 太丑了。 最难受的是字体太小了。 我还是喜欢老版本的 jupyter notebook. 安装经典版: pip install nbclassic 启动经典版: jupyter server 或是 jupyter nbclassic 参考来源: https://github.com/jupyter/nbclassic jupyter note…

计算机网络——传输层服务

传输层会给段加上目标ip和目标端口号 应用层去识别报文的开始和结束

C++ —— 关于多态

目录 1. 多态的概念 2.多态的定义及实现 3. 虚函数 3.1 虚函数的重写/覆盖 3.2 关于多态的面试难题 3.3 虚函数重写的⼀些问题 3.4 override 和 final关键字 3.5 重载/重写/隐藏的对比 3.6 纯虚函数和抽象类 4.多态的原理 4.1虚函数表指针 4.2 多态是如何实现的 4…

go压缩的使用

基础&#xff1a;使用go创建一个zip func base(path string) {// 创建 zip 文件zipFile, err : os.Create("test.zip")if err ! nil {panic(err)}defer zipFile.Close()// 创建一个新的 *Writer 对象zipWriter : zip.NewWriter(zipFile)defer zipWriter.Close()// 创…

如何使用DockerSpy检测你的Docker镜像是否安全

关于DockerSpy DockerSpy是一款针对Docker镜像的敏感信息检测与安全审计工具&#xff0c;该工具可以帮助广大研究人员在Docker Hub上检测和搜索自己镜像的安全问题&#xff0c;并识别潜在的泄漏内容&#xff0c;例如身份验证密钥等敏感信息。 功能介绍 1、安全审计&#xff1a…

linux一二三章那些是重点呢

第一章 静态库动态库的区别 什么是库 库文件是计算机上的一类文件&#xff0c;可以简单的把库文件看成一种代码仓库&#xff0c;它提供给使用者一些可以直接 拿来用的变量、函数或类。 如何制作 静态动态库 静态库&#xff1a; GCC 进行链接时&#xff0c;会把静态库中代码打…

2013 lost connection to MySQL server during query

1.问题 使用navicat连接doris&#xff0c;会有这个错误。 2.解决 换低版本的navicat比如navicat11。

linux运行openfoam并行会报错:attempt to run parallel on 1 processor

linux运行openfoam并行会报错&#xff1a;attempt to run parallel on 1 processor 步骤&#xff1a; 1.先在终端输入which mpirun,查看当前并行路径&#xff1b; 2.输入gedit ~/.bashrc&#xff0c;文本方式打开bashrc文件&#xff1b; 3.修改为export PATH/usr/bin:$PATH&am…

支持阅后即焚的笔记Enclosed

什么是 Enclosed &#xff1f; Enclosed 是一个简约的网络应用程序&#xff0c;旨在发送私人和安全的笔记。所有笔记均经过端到端加密&#xff0c;确保服务器和存储对内容一无所知。用户可以设置密码、定义有效期 (TTL)&#xff0c;并选择在阅读后让笔记自毁。 软件特点&#x…

第一年改考408的学校有炸过的吗?怎么应对突然改考408?

C哥专业提供——计软考研院校选择分析专业课备考指南规划 专业课改考 408 后&#xff0c;分数线不一定会暴涨&#xff0c;其变化受到多种因素影响&#xff1a; 可能导致分数线不暴涨甚至下降的因素&#xff1a; 考试难度增加&#xff1a;408 统考涵盖数据结构、计算机组成原理…

P2-1与P2-2.【C语言基本数据类型、运算符和表达式】第一节与第二节

讲解视频&#xff1a; P2-1.【基本数据类型、运算符和表达式】第一节 P2-2.【基本数据类型、运算符和表达式】第二节 必备知识与理论 1&#xff0e;数据类型概述 所谓数据类型&#xff0c;是按被定义变量的性质&#xff0c;表示形式&#xff0c;占据存储空间的多少&#xff0…

【分布式事务-04】分布式事务seata的XA模式

redis系列整体栏目 内容链接地址【一】分布式事务之2pc两阶段提交https://zhenghuisheng.blog.csdn.net/article/details/142406325【二】分布式事务seata的安装下载与环境搭建https://zhenghuisheng.blog.csdn.net/article/details/142893117【三】分布式事务seata的AT模式htt…

k8s ETCD数据备份与恢复

在 Kubernetes 集群中&#xff0c;etcd 是一个分布式键值存储&#xff0c;它保存着整个集群的状态&#xff0c;包括节点、Pod、ConfigMap、Secrets 等关键信息。因此&#xff0c;定期对 etcd 进行备份是非常重要的&#xff0c;特别是在集群发生故障或需要恢复数据的情况下。本文…

Axure科技感元件:打造可视化大屏设计的得力助手

Axure&#xff0c;作为一款专业的原型设计工具&#xff0c;凭借其强大的设计功能、丰富的组件库和灵活的交互能力&#xff0c;成为了许多设计师打造科技感设计的首选工具。其中&#xff0c;Axure科技感元件更是以其独特的魅力和实用性&#xff0c;在数据可视化大屏、登录界面、…

HarmonyOS开发(State模型)

一、State模型概述 FA&#xff08;Feature Ability&#xff09;模型&#xff1a;从API 7开始支持的模型&#xff0c;已经不再主推。 Stage模型&#xff1a;从API 9开始新增的模型&#xff0c;是目前主推且会长期演进的模型。在该模型中&#xff0c;由于提供了AbilityStage、Wi…

Leetcode—1114. 按序打印【简单】(多线程)

2024每日刷题&#xff08;179&#xff09; Leetcode—1114. 按序打印 C实现代码 class Foo { public:Foo() {firstMutex.lock();secondMutex.lock();}void first(function<void()> printFirst) {// printFirst() outputs "first". Do not change or remove t…