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

发布于:2025-08-09 ⋅ 阅读:(20) ⋅ 点赞:(0)

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_entry

import 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 {
    @State
    var title: String = '最简单的HTTP服务器示例';
    //连接、通讯历史记录
    @State
    var msgHistory: String = ''
    //服务器端口
    @State
    var port: UInt16 = 8080

    var tcpServer: ?TcpServerSocket = None
    //运行状态
    @State
    var running = false

    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)

                    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"
                    //启动一个线程处理新的socket
                    spawn {
                        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 = false
        msgHistory += "服务已停止\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


网站公告

今日签到

点亮在社区的每一天
去签到