Golang如何实现简单http服务器

服务器   发布日期:2023年09月23日   浏览次数:486

这篇文章主要介绍“Golang如何实现简单http服务器”,在日常操作中,相信很多人在Golang如何实现简单http服务器问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Golang如何实现简单http服务器”的疑惑有所帮助!接下来,请跟着小编一起来学习吧!

一、基本描述

完成一个http请求的处理和响应,主要有以下几个步骤:

  • 监听端口

  • 建立连接

  • 解析http请求

  • 处理请求

  • 返回http响应

完成上面几个步骤,便能够实现一个简单的http服务器,完成对基本的http请求的处理

二 、具体方法

2.1 连接的建立

go中net包下有提供Listen和Accept两个方法,可以完成连接的建立,可以简单看下示例:

  1. func main() {
  2. // 对8080端口进行监听
  3. l, _ := net.Listen("tcp", ":8080")
  4. // 获取和端口8080完成三次握手的tcp连接
  5. conn, _ := l.Accept()
  6. // 此时便能够使用该连接和客户端进行通信
  7. data := make([]byte, 4096)
  8. // 可以从conn读取数据缓冲区当中
  9. conn.Read(data)
  10. // 将缓冲区的数据打印处理
  11. print(string(data))
  12. }

可以运行这段代码,然后在浏览器对本地8080端口发送请求,该程序能够读取到浏览器发送过来的http请求体数据。

当通过Accept方法获取到连接后,能够使用该连接和客户端进行通信,该连接实现了net.Conn接口,具体接口的定义如下:

  1. type Conn interface {
  2. // Read reads data from the connection.
  3. // Read can be made to time out and return an error after a fixed
  4. // time limit; see SetDeadline and SetReadDeadline.
  5. Read(b []byte) (n int, err error)
  6. // Write writes data to the connection.
  7. // Write can be made to time out and return an error after a fixed
  8. // time limit; see SetDeadline and SetWriteDeadline.
  9. Write(b []byte) (n int, err error)
  10. // Close closes the connection.
  11. // Any blocked Read or Write operations will be unblocked and return errors.
  12. Close() error
  13. }

能够通过调用Read方法从客户端读取数据,使用Write方法往客户端返回数据。

2.2 http请求解析

当和客户端建立连接后,同时也能够读取到客户端发送过来的请求,此时要处理http请求的话,此时是需要解析出http请求体的,然后才能对http请求进行处理。接下来我们看一下一个http请求例子:

GET /ping HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Cache-Control: max-age=0
sec-ch-ua: "Google Chrome";v="107", "Chromium";v="107", "Not=A?Brand";v="24"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "macOS"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
(空行)
hello world

接下来对HTTP请求体来进行分析,第一行是请求行,包含请求的方法,请求URI,以及HTTP版本。下面这个例子中,请求方法是GET,请求URI是/ping,HTTP版本是1.1。

GET /ping HTTP/1.1

请求行到空行之间的内容便是请求头部,每一个头部字段都有其对应的作用,比如Connection首部字段,这里值为keep-alive,这里的作用是告诉服务器,这个连接要处理多个http请求,不要处理完一个http请求就把连接断开了。

而且一个http请求首部字段,是可以有多个对应的值的,多个值之间用逗号隔开。

Cache-Control: public, max-age=31536000

第三部分的内容为请求体,也就是空行之后直到整个http请求的结束。可以看下面例子,请求体的内容是hello world。实际上GET请求是不应该有请求体的内容的,此处是我手动加进去的,只是为了方便展示使用。

GET /ping HTTP/1.1
....(省略http请求体部分首部字段)
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
(空行)
hello world

当我们了解完http请求体的结构,接下来可以编写代码来解析http请求体。

我们定义一个Conn结构体,由Conn完成数据的读取和HTTP请求体的解析,Conn定义如下:

  1. type Conn struct {
  2. rwc net.Conn
  3. // bufio.Reader 提供了缓存的功能,以及一些函数,方便我们解析HTTP请求体
  4. br *bufio.Reader
  5. bw *bufio.Writer
  6. // 是否已经写入响应头
  7. wroteHaeder bool
  8. }
  9. func NewConn(rwc net.Conn) *Conn {
  10. return &Conn{
  11. rwc: rwc,
  12. br: bufio.NewReader(rwc),
  13. bw: bufio.NewWriter(rwc),
  14. }
  15. }

同时解析出来的HTTP请求,也需要有个结构体来存储这部分数据,Request定义如下,这里暂时只支持GET请求,所以并没有保存请求体的内容

  1. type Request struct {
  2. // 存储请求方法,上面例子便是GET
  3. method string
  4. // 用于存储请求的uri
  5. uri string
  6. // 用于存储http版本
  7. proto string
  8. // http首部字段
  9. Header map[string]string
  10. }

接下来由Conn完成HTTP请求体的解析,然后将解析的结果存储到Request对象当中。只需要根据HTTP请求体结构来进行解析即可,具体逻辑如下:

  1. func (c *Conn) readRequest() (*Request, error) {
  2. req := NewRequest()
  3. // 现在只支持Get请求,读取第一行内容
  4. // GET /ping HTTP1.1
  5. line, err := c.br.ReadBytes('
  6. ')
  7. if err != nil {
  8. return req, err
  9. }
  10. var f []string
  11. // 按空格来进行分割,将请求行分割为三部分
  12. if f = strings.Split(string(line), " "); len(f) != 3 {
  13. return req, errors.New("http Header error")
  14. }
  15. // 获取到GET, /ping, HTTP/1.1
  16. req.method, req.url, req.proto = f[0], f[1], f[2]
  17. // 解析请求体首部字段
  18. for {
  19. line, err = c.br.ReadBytes('
  20. ')
  21. if err != nil {
  22. return nil, err
  23. }
  24. // 当读取到空行时,说明已经首部字段已经读取完了
  25. if len(strings.TrimSpace(string(line))) == 0 {
  26. break
  27. }
  28. //举例,读取connection: keep-alive,获取第一个空格的下标
  29. i := bytes.IndexByte(line, ' ')
  30. if i == -1 {
  31. return nil, errors.New("header is error")
  32. }
  33. // 此时获取到请求首部key,为connection
  34. key := string(line[:i-1])
  35. // 读取到对应的值,这里读取到keep-alive
  36. value := strings.TrimSpace(string(line[i:]))
  37. // 简单读取头部字段即可
  38. req.Header[key] = value
  39. }
  40. }

2.3 http请求处理

此时已经获取到HTTP请求了,之后需要对HTTP请求来进行处理,这里可以先简单进行处理,根据不同的请求执行不同的处理逻辑:

  1. func main() {
  2. // 对8080端口进行监听
  3. l, _ := net.Listen("tcp", ":8080")
  4. // 获取和端口8080完成三次握手的tcp连接
  5. conn, _ := l.Accept()
  6. // 获取到conn连接
  7. c := NewConn(conn, handler)
  8. // 读取到请求体
  9. req, _ := c.readRequest()
  10. if request.uri == "/hello" {
  11. ....
  12. }else{
  13. ....
  14. }
  15. }

2.4 http请求响应

当http请求处理完成之后,需要将返回一个处理结果返回给客户端,有时候还需要返回一些数据给客户端,这里返回的数据需要符合HTTP响应体的结构,接下来我们看看HTTP响应体的结构

HTTP/1.1 200 OK
Server: CloudWAF
Date: Sun, 04 Dec 2022 02:29:27 GMT
Content-Type: text/html;charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Vary: Accept-Encoding
Content-Language: zh-CN
Strict-Transport-Security: max-age= 31536000
Content-Encoding: gzip
(空行)
xxxx响应数据

可以看到,HTTP响应体和请求体结构类似,当需要返回数据给客户端时,需要按照HTTP协议定义好的响应体结构来进行返回,这样客户端才能够正确解析。

为了方便使用,构造HTTP响应体结构这部分逻辑应该由Conn对象来承载,由Conn对象提供一个Write方法,当需要返回数据时,只需要调用Write方法写入要返回的数据即可,不需要去操心去构造HTTP响应体的内容,Writer方法具体逻辑如下:

  1. const (
  2. StatusOK = 200
  3. )
  4. var statusText = map[int]string{
  5. StatusOK: "OK",
  6. }
  7. // 返回响应行
  8. // 构造响应行 HTTP/1.1 200 OK
  9. func (c *Conn) writeHeader(status int) error {
  10. if c.wroteHeader {
  11. return errors.New("code has been set")
  12. }
  13. c.wroteHeader = true
  14. var proto string
  15. //GET /hello HTTP/1.1
  16. proto = "HTTP/1.1"
  17. // 获取文本描述,这里为OK
  18. text, ok := statusText[status]
  19. if !ok {
  20. text = "status code " + strconv.Itoa(status)
  21. }
  22. // 写入数据 HTTP1.1 200 OK
  23. c.bw.WriteString(proto + " " + strconv.Itoa(status) + " " + text + "
  24. ")
  25. // 写入空行
  26. c.bw.Write("
  27. ")
  28. return nil
  29. }
  30. // 写入响应数据
  31. func (c *Conn) WriteData(data []byte) error {
  32. // 还没写入请求头
  33. if !c.wroteHeader {
  34. //默认状态码是200 OK
  35. c.writeHeader(StatusOK)
  36. }
  37. c.bw.Write(data)
  38. return nil
  39. }

三、完整示例

  1. func main() {
  2. // 对8080端口进行监听
  3. l, _ := net.Listen("tcp", ":8080")
  4. // 获取和端口8080完成三次握手的tcp连接
  5. conn, _ := l.Accept()
  6. // 获取到conn连接
  7. c := NewConn(conn)
  8. // 读取到请求体
  9. req, _ := c.readRequest()
  10. if request.uri == "hello" {
  11. c.WriteData("hello")
  12. }else{
  13. c.WriteData("hello world")
  14. }
  15. // 在最后,需要将缓冲区的数据进行清空
  16. c.bw.Flush()
  17. // 因为响应没有设置content-length,所以只有连接断开后,
  18. // 浏览器才知道数据读取完成了,此处需要断开连接
  19. c.rwc.Close()
  20. }

以上就是Golang如何实现简单http服务器的详细内容,更多关于Golang如何实现简单http服务器的资料请关注九品源码其它相关文章!