使用Kotlin编写一个Http服务器

news/2024/7/8 3:17:19

首发于Enaium的个人博客


引言

在本文中,我们将使用 Kotlin 编写一个简单的 HTTP 服务器。我们将使用 Java 的 ServerSocket 类来实现这个服务器。我们将创建一个简单的服务器,它将监听端口 8000,并在接收到请求时返回一个简单的响应。

Http 的格式

HTTP 请求和响应都是文本格式的。HTTP 请求由请求行、请求头和请求体组成。HTTP 响应由状态行、响应头和响应体组成。

具体可以到 MDN 查看。

代码实现

首先我们需要创建一个Method枚举和一个Version枚举,用于表示请求的方法和版本。

enum class Method {
    GET,
    POST,
    UNKNOWN;

    companion object {
        fun parse(method: String): Method =
            when (method) {
                "GET" -> GET
                "POST" -> POST
                else -> UNKNOWN
            }
    }
}
enum class Version {
    HTTP_1_1,
    UNKNOWN;

    companion object {
        fun parse(version: String): Version = when (version) {
            "HTTP/1.1" -> HTTP_1_1
            else -> UNKNOWN
        }
    }

    override fun toString(): String {
        return when (this) {
            HTTP_1_1 -> "HTTP/1.1"
            UNKNOWN -> "UNKNOWN"
        }
    }
}

然后我们创建一个HttpRequest类,用于表示 HTTP 请求。

data class HttpRequest(
    val method: Method,
    val path: String,
    val version: Version,
    val headers: Map<String, String>,
    val body: String
)

接着我们为HttpRequest类添加一个静态方法parse,用于解析 HTTP 请求。

companion object {
    fun parse(reader: BufferedReader): HttpRequest {
    }
}

parse方法中,我们首先读取请求行,然后读取请求头,最后读取请求体。

首先我们先来定义一些默认值。

var method = Method.UNKNOWN
var path = ""
var version = Version.UNKNOWN
val headers = mutableMapOf<String, String>()
var body = ""

然后我们读取请求的所有数据,不过需要注意这里不不能使用readTextreadLines,这些方法会导致Socket被关闭。

val request = StringBuilder()
var readed: String?
while (reader.readLine().also { readed = it } != null && readed!!.isNotEmpty()) {
    request.append(readed).append("\n")
}

接着我们解析请求的数据。

request.lines().forEachIndexed { index, line ->
    if (index == 0) {
        val splitWithSpace = line.split(" ")
        method = Method.parse(splitWithSpace[0])
        path = splitWithSpace[1]
        version = Version.parse(splitWithSpace[2])
    } else if (line.contains(": ")) {
        val split = line.split(": ")
        headers[split[0]] = split[1]
    } else if (line.isEmpty()) {
    } else {
        body = line
    }
}

首先是第一行,我们使用空格分割,然后解析请求方法、路径和版本。然后是请求头,我们使用冒号空格分割,然后解析请求头。最后是请求体,如果不为空,我们就保存请求体。

最后我们将解析的数据返回。

return HttpRequest(
    method,
    path,
    version,
    headers,
    body
)

接着我们创建一个HttpResponse类,用于表示 HTTP 响应。

data class HttpResponse(
    val version: Version = Version.HTTP_1_1,
    val statusCode: Int,
    val statusText: String,
    val headers: Map<String, String> = emptyMap(),
    val body: String = ""
)

然后我们为HttpResponse类重写toString方法,用于将响应转换为字符串。

override fun toString(): String {
    return """
        $version $statusCode $statusText
        ${headers.map { "${it.key}: ${it.value}" }.joinToString("\n")}
        
        $body
    """.trimIndent()
}

接着我们创建一个Handler类型,用于处理请求。

typealias Handler = (HttpRequest) -> HttpResponse

然后我们创建一个Route类,用于表示路由。

data class Route(val method: Method, val path: String, val handler: Handler)

接着创建一个Router类,用于管理路由。

class Router {
    private val routes = mutableListOf<Route>()

    fun get(path: String, handler: Handler) {
        routes.add(Route(Method.GET, path, handler))
    }

    fun handle(socket: ServerSocket) {

    }
}

Router类中,我们定义了一个routes属性,用于保存所有的路由。然后我们定义了一个get方法,用于添加一个 GET 请求的路由。最后我们定义了一个handle方法,用于处理请求。

接着我们需要实现handle方法。

fun handle(socket: ServerSocket) {
    while (true) {
        val client = socket.accept()
        val reader = client.getInputStream().bufferedReader()
        val writer = client.getOutputStream().bufferedWriter()
        val httpRequest = HttpRequest.parse(reader)
        routes.findLast { it.method == httpRequest.method && it.path == httpRequest.path }?.let {
            val toString = it.handler.invoke(httpRequest).toString()
            writer.write(toString)
            writer.flush()
        } ?: let {
            writer.write(
                HttpResponse(
                    Version.HTTP_1_1,
                    404,
                    "NotFound",
                    headers = mapOf("Content-Type" to "text/html"),
                    body = "<h1>404 Not Found</h1>"
                ).toString()
            )
            writer.flush()
        }
        client.close()
    }
}

handle方法中,我们首先创建一个ServerSocket,然后进入一个无限循环。在循环中,我们首先接受一个客户端连接,然后创建一个BufferedReader和一个BufferedWriter,用于读取请求和写入响应。然后我们解析请求,然后查找路由,如果找到了路由,我们就调用路由的处理函数,然后将响应写入到客户端。如果没有找到路由,我们就返回一个 404 响应。最后我们关闭客户端连接。

最后我们创建一个Server类,用于创建服务器。

class HttpServer(port: Int) {
    private val serverSocket: ServerSocket = ServerSocket(port)

    fun start() {

    }
}

start方法中,我们创建一个Router,然后添加一个路由,最后调用Routerhandle方法。

val router = Router()
router.get("/") { _ ->
    HttpResponse(Version.HTTP_1_1, 200, "OK")
}
router.get("/hello") { _ ->
    HttpResponse(
        Version.HTTP_1_1, 200, "OK",
        headers = mapOf("Content-Type" to "text/html"),
        body = "<h1>Hello World!</h1>"
    )
}
router.handle(serverSocket)

这样我们就完成了一个简单的 HTTP 服务器。

现在来测试一下我们的服务器。

fun main() {
    HttpServer(8080).start()
}

在终端中运行main函数,然后在浏览器中打开http://localhost:8080http://localhost:8080/hello,你应该能看到一个简单的页面。

总结

在本文中,我们使用 Kotlin 编写了一个简单的 HTTP 服务器。我们使用 Java 的 ServerSocket 类来实现这个服务器。我们创建了一个简单的服务器,它监听端口 8080,并在接收到请求时返回一个简单的响应。我们还创建了一个简单的路由系统,用于处理不同的请求。

完整源码


http://lihuaxi.xjx100.cn/news/2203359.html

相关文章

mysql json_quote和json_unquote的用法

在 MySQL 中&#xff0c;JSON_QUOTE() 和 JSON_UNQUOTE() 函数与 JSON 数据类型的处理有关。这两个函数在处理 JSON 字符串时特别有用。 JSON_QUOTE() JSON_QUOTE() 函数用于将字符串值转换为有效的 JSON 字符串。它会将特殊字符&#xff08;如引号、反斜杠等&#xff09;进行…

两句话让LLM逻辑推理瞬间崩溃!!

一道简单的逻辑问题&#xff0c;竟让几乎所有的LLM全军覆没&#xff1f; 对于人类来说&#xff0c;这个名为「爱丽丝梦游仙境」&#xff08;AIW&#xff09;的测试并不算很难—— 「爱丽丝有N个兄弟&#xff0c;她还有M个姐妹。爱丽丝的兄弟有多少个姐妹&#xff1f;」 稍加思考…

TypeScript基础教程学习

菜鸟教程 TypeScript基础类型 数字类型 number 双精度 64 位浮点值。它可以用来表示整数和分数。 let binaryLiteral: number 0b1010; // 二进制 let octalLiteral: number 0o744; // 八进制 let decLiteral: number 6; // 十进制 let hexLiteral: number 0xf00d…

【C++】<知识点> C++11新特性

文章目录 一、auto关键字 二、decltype关键字 三、nullptr关键字 四、智能指针 五、 无序容器&#xff08;哈希表&#xff09; 六、统一的初始化方法 七、成员变量默认初始值 八、范围for循环 九、右值引用与移动语义 十、lambda表达式 一、auto关键字 1. 作用&#…

ABSD-系统架构师(七)

1、以太网交换机转发表叙述中&#xff0c;正确的是&#xff08;&#xff09;。 A交换机的初始MAC地址表为空 B交换机接收到数据帧后&#xff0c;如果没有相应的表项&#xff0c;则不转发该帧 C交换机通过读取输入帧中的目的地址来添加相应的MAC地址表项 D交换机的MAC地址表…

C#——枚举类型详情

枚举类型 枚举类型&#xff08;也可以称为“枚举器”&#xff09;由一组具有独立标识符&#xff08;名称&#xff09;的整数类型常量构成&#xff0c;在 C# 中枚举类型不仅可以在类或结构体的内部声明&#xff0c;也可以在类或结构体的外部声明&#xff0c;默认情况下枚举类型…

[word] word大括号怎么打两行 #其他#其他#微信

word大括号怎么打两行 Word给用户提供了用于创建专业而优雅的文档工具&#xff0c;帮助用户节省时间&#xff0c;并得到优雅美观的结果。 一直以来&#xff0c;Microsoft Office Word 都是最流行的文字处理程序。 作为 Office 套件的核心程序&#xff0c; Word 提供了许多易…

AI 大模型重点行业应用情况

1、AI 大模型重点行业应用情况总览 AI大模型将率先在互联网办公、金融等数字化程度较高的行业快速渗透&#xff0c;医疗、交通、 制造等行业的潜在渗透空间大。 2、AI 大模型在金融行业应用情况 金融行业的应用场景丰富&#xff0c;是最早进行数字化转型的机构&#xff0c;因此…