当前位置: 首页 > news >正文

鸿蒙网络编程系列61-仓颉版基于TCP实现最简单的HTTP服务器

1. 为什么要实现HTTP服务器

HTTP协议自1991年正式提出后,经历了从0.9版本到HTTP/1.1、HTTP/2、HTTP/3的演进,虽然具体的协议规则变化了不少,但是基本都维持着对以前协议的兼容,在当前互联网几乎覆盖一切的环境下,如果能手动打造一个简单的HTTP服务器,有助于更深入的了解HTTP协议。

众所周知,HTTP/2及之前的版本都是基于TCP做为传输层协议的,而HTTP/3则是基于QUIC(Quick UDP Internet Connections),为简单起见,本文使用TCP协议做为传输层协议,以TCPServer做为HTTP服务器的服务端实现。

再来说一下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协议作为传输层进行收发即可。

接下里,我们就演示如何打造一个HTTP服务器,并通过浏览器进行访问。

2. HTTP服务器示例演示

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

输入要绑定的服务器端口,然后单击“启动”按钮,即可启动HTTP服务器,如图所示:

接着,打开浏览器,输入HTTP服务器地址(本示例中,HTTP所在的手机和浏览器所在的电脑处于同一局域网),发起请求,可以看到服务器返回的信息:

此时查看HTTP服务端,可以看到浏览器给服务器发送的请求信息,如图所示:

3. HTTP服务器示例编写

下面详细介绍创建该示例的步骤(确保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:在index.cj文件里添加如下的代码:

package ohos_app_cangjie_entryimport ohos.base.*
import ohos.component.*
import ohos.state_manage.*
import ohos.state_macro_manage.*
import ohos.net.http.*
import ohos.ability.getStageContext
import ohos.ability.*
import std.convert.*
import std.net.*
import std.socket.*@Entry
@Component
class EntryView {@Statevar title: String = '最简单的HTTP服务器示例';//连接、通讯历史记录@Statevar msgHistory: String = ''//服务器端口@Statevar port: UInt16 = 8080var tcpServer: ?TcpServerSocket = None//运行状态@Statevar running = falselet 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)TextInput(text: port.toString()).onChange({value => port = UInt16.parse(value)}).setType(InputType.Number).width(100).fontSize(11).flexGrow(1)Button(if (running) {"停止"} else {"启动"}).onClick {evt => if (!running) {startServer()} else {stopServer()}}.width(70).fontSize(14)}.width(100.percent).padding(10)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)}//启动web服务器func startServer() {//TCP服务端tcpServer = TcpServerSocket(bindAt: port)tcpServer?.bind()msgHistory += "绑定到端口${port}\r\n"running = true//启动一个线程监听客户端的连接并读取客户端发送过来的消息spawn {msgHistory += "开始监听客户端连接\r\n"while (true) {let echoClientObj = tcpServer?.accept()if (let Some(echoClient) <- echoClientObj) {msgHistory += "接受客户端连接, 客户端地址:${echoClient.remoteAddress}\r\n"//启动一个线程处理新的socketspawn {try {dealWithHttpRequest(echoClient)} catch (exp: Exception) {msgHistory += "从套接字读取数据出错:${exp}\r\n"}}}}}}//根据客户端的请求构造应答内容func createResponse(content: String) {let responseBuilder = StringBuilder()if (content.contains("/favicon.ico")) {responseBuilder.append("HTTP/1.1 307 Internal Redirect \r\n")responseBuilder.append("Location: https://www.baidu.com/favicon.ico \r\n")responseBuilder.append("\r\n")} else {let bodyBuilder = StringBuilder()bodyBuilder.append("<html>")bodyBuilder.append("<head>")bodyBuilder.append("<title>")bodyBuilder.append("HTTP服务器模拟")bodyBuilder.append("</title>")bodyBuilder.append("</head>")bodyBuilder.append("<body>")bodyBuilder.append("<h1>")bodyBuilder.append("浏览器发送的请求信息")bodyBuilder.append("</h1>")bodyBuilder.append("<pre>")bodyBuilder.append(content)bodyBuilder.append("</pre>")bodyBuilder.append("</body>")bodyBuilder.append("</html>")responseBuilder.append("HTTP/1.1 200 OK \r\n")responseBuilder.append("Content-Type: text/html; charset=utf-8 \r\n")responseBuilder.append("Content-Length: ${bodyBuilder.toString().size} \r\n")responseBuilder.append("\r\n")responseBuilder.append(bodyBuilder.toString())}return responseBuilder.toString()}//停止服务器func stopServer() {tcpServer?.close()running = falsemsgHistory += "服务已停止\r\n"}//处理http请求func dealWithHttpRequest(clientSocket: TcpSocket) {//存放从socket读取数据的缓冲区let buffer = Array<UInt8>(1024, item: 0)var readCount = clientSocket.read(buffer)let content = String.fromUtf8(buffer[0..readCount])//输出接收到的信息到日志msgHistory += "${clientSocket.remoteAddress}:${content}\r\n"clientSocket.write(createResponse(content).toArray())clientSocket.close()}
}

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

步骤6:按照本文第2部分“HTTP服务器示例演示”操作即可。

4. 代码分析

要实现一个HTTP服务器,关键的部分还是要了解HTTP协议的格式,然后在此基础上构造对应客户端的响应,本文关于构造响应的代码在函数createResponse中,代码比较简单,就不深入分析了。需要注意的是,在本文的应答中,把请求分成了两类,一类是对于网站图标的请求,也就是请求favicon.ico,本文只是简单的转发到了百度网站;另外一种才是本文的重点,就是构造一个网页给客户端,就是本函数的主题部分。

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

本文源码地址:
https://gitee.com/zl3624/harmonyos_network_samples/tree/master/code/tcp/SimpleWebserver4Cj

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

http://www.xdnf.cn/news/1255051.html

相关文章:

  • 计算机网络:固定网络位长度子网划分flsm和可变长子网掩码划分vlsm的区别
  • 【C++】哈希表原理与实现详解
  • 代码随想录day58图论8
  • Mysql数据仓库备份脚本
  • Android视图状态以及重绘
  • 快速开发实践
  • 内网穿透原理和部署教程
  • 【Kubernetes】部署 kube-bench 实现 K8s 最佳实践
  • tcpdump问题记录
  • Linux下动态库链接的详细过程
  • 【数据结构初阶】--排序(五)--计数排序,排序算法复杂度对比和稳定性分析
  • Python Socket 脚本深度解析与开发指南
  • MySQL梳理四:事务日志机制和多版本并发控制(MVCC)
  • SpringMvc的原理深度剖析及源码解读
  • 前端页面直接生成PDF下载文件
  • “物联网+职业本科”:VR虚拟仿真实训室的发展前景
  • 物联网架构全解析:华为“1+2+1”与格行随身WiFi,技术如何定义未来生活?
  • 基于开源AI智能名片链动2+1模式S2B2C商城小程序的微商产品经营策略研究
  • 技术优势铸就行业标杆:物联网边缘计算网关凭何引领智能变革?
  • 008 前端vue
  • Spring AOP动态代理核心原理深度解析 - 图解+实战揭秘Java代理设计模式
  • RabbitMQ-日常运维命令
  • 嵌入式硬件中MOSFET基本原理与实现
  • python函数--python010
  • Redis中间件(三):Redis存储原理与数据模型
  • 小红书开源多模态视觉语言模型DOTS-VLM1
  • ubuntu 2024 安装拼音输入法
  • VC6800智能相机:赋能智能制造,开启AI视觉新纪元
  • 【关于Java 8 的新特性】
  • 语言模型(LM):n-gram模型原理与困惑度(Perplexity)计算详解