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