鸿蒙网络编程系列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