Golang 开发一个 HTTP Web 服务器/客户端

之前的 zinx 学习,是基于 TCP/UDP 的 socket 协议进行编写

而本次要实现的是基于 HTTP 协议开发一个 Web 服务器端与客户端!

要求

服务器端要求:

  • 1、服务端维护一个内存数据结构,所有数据进程重启丢失,不做数据持久化,不考虑内存容量问题
  • 2、服务端实现一个网络 API 接口,客户端向该 API 发送一个网络请求,请求数据是一个 string 的信息
  • 3、服务端该 API 收到请求后响应一个 []string 的信息,返回之前发送过的所有 string,当次请求发送的 string 一定在最后,不关心是否重复

示例:

第一次请求发送:”a”,响应: []string{“a”}

第二次请求发送:”b”,响应: []string{“a”, “b”}

第三次请求发送:”a”,响应: []string{“a”, “b”, “a”}

注意服务端代码部分,任何情况下均不能退出进程。

客户端要求:

  • 1、客户端必须是 Golang 函数(该函数后面简称 BcjClient ),调用该服务端的 API 接口

  • 2、该客户端函数输入参数为:string,输出参数为: []string,和 error
    客户端函数是个function/函数,不是method/方法

  • 3、注意客户端函数业务代码执行过程中,任何情况下均不能退出进程。

  • 4、客户端函数的类型必须严格匹配。

源码要求:

  • 1、代码运行结果应当正确

  • 2、不应该有 data race ( race condition/竞争条件/数据竞争 )。多线程客户端调用服务端时,不应当出现任何2个客户端得到的同样响应的可能性。

  • 3、需要满足 Database transaction(数据库事务)的 Serializability(可串行性)要求,(注意:Golang 的 data race 工具只能找到部分 data race 情况。)

  • 4、代码代码不允许忽略错误,而且只能调用官方标准库,不能包含其他第三方代码(比如 github.com/xxx,比如 “golang.org/x/sync/xx” )。只有 GOROOT/src 下面的库算官方库,其他都不算官方库。

  • 5、实现应当简洁,代码可读性不能过差

设计思路

1、http 服务,基于 http 协议,并非 socket 进行实现

2、web 请求,restful 标准下,采用 POST 进行客户端的 web 服务请求

3、开发第一个版本时服务器维护的内存数据结构采用 sync.Map,采用 client 不设置用户名称,统一设置 token 值为 Client,返回客户端数据为 string 类型,并通过简单的空格字符串 “ “ 进行分离每一个 []string 类型的元素。

4、(未完成)第二个版本客户端自设名称为 token 值,通过header 包头的 token 值来对客户端进行区分,这个token 值的维护放置于客户端,理论上为保证数据安全,应该放置于服务器。

开发的细节

具体内容就不再此显示,说几个开发中的细节

1、Go 语言很适合使用测试驱动开发,开发过程中,首先需要进行逻辑的梳理,逻辑梳理好之后再开始代码的撰写,上来就写代码都是想到哪里写到哪里,这种习惯不好,最合适的方法是先使用注释写好相应的需求逻辑,再写代码,很实用

2、在做 string 字符串存储成 []string 类型的切片的时候,服务器端程序也需要使用一个 []string 类型进行保存,但是服务器回显给客户端是以 io.writer 实现的 writer 接口返回 []byte 流的数据,而 []string 直接转为 []byte 流数据形式,之后客户端对于格式的处理较为麻烦,所以最好还是 []string 类型,通过拼接一个 string 传输给客户端,我选择 []string 中间分割字符为空格字符 。客户端接受到这个string类型的字符串后,使用 strings.Split() 函数,以空格字符进行分割。

3、为了解决客户端先于服务器端启动时间不同,导致的客户端 Dail 服务器端发生端口拒绝错误,采用 for 循环提交POST请求,直到请求建立成功后,再 break for 循环。

4、bug fix: 防止程序panic 掉,mutex.unlock 并未释放,所以采用 defer m.Unlock 合适

优化

在服务器端,使用 sync.Map 数据结构对传输的字符串进行存储,防止进行并发访问与并发存储,但是由于自己在并发的能力功底不足,导致虽然设计是 sync.Map 控制,但仍存在 data race 的情况,无奈之下还是对一整段进行了加锁处理,其实也能使用全局的 channel 进行处理。但是这都与最初自己想要一步实现拒绝数据的竞争性访问的设计理念冲突,暂时也没想到很好的解决办法!等日后再对其进行优化!

代码附件

客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package gohttp

import (
"bytes"
"fmt"
"io"
"io/ioutil"
"net/http"
"strings"
"time"
)

func BcjClient(writer string) ([]string, error) {
var r *http.Response
for {
client := &http.Client{}
req, err := http.NewRequest(
http.MethodPost,
"http://127.0.0.1:8999/v1",
bytes.NewReader([]byte(writer)),
)
req.Header.Add("Token", "Client")
req.Header.Add("content-Type", "text/plain")

r, err = client.Do(req)
if err == nil {
break
} else {
fmt.Printf("err: %s\n", err)
time.Sleep(3 * time.Second)
continue
}
}
defer func(Body io.ReadCloser) {
_ = Body.Close()
}(r.Body)

content, err := ioutil.ReadAll(r.Body)
if err != nil {
return nil, err
}

//将传入的字符串变成字符串切片,并除去最后的换行格式问题!
ToVisual := strings.Split(string(content), " ")
ToVisual = ToVisual[:len(ToVisual)-1]
/*fmt.Printf("%s\n", ToVisual)*/

return ToVisual, nil
}

/*func main() {
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
BcjClient(scanner.Text())
}
}*/

服务器端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package gohttp

import (
"fmt"
"io/ioutil"
"net/http"
"sync"
)

var cache sync.Map
var m sync.Mutex

func ClientHandler(writer http.ResponseWriter, req *http.Request) {

if req.Method != "POST" {
_, _ = fmt.Fprintf(writer, "Request is not POST! please send POST request!")
return
}

token := req.Header.Get("Token")
body, err := ioutil.ReadAll(req.Body)
if err != nil {
_, _ = fmt.Fprintf(writer, "read body err, %v\n", err)
return
}

m.Lock()
defer m.Unlock()

GetFromCache, ok := cache.Load(token)
if ok {
GetFromCache = append(GetFromCache.([]string), []string{string(body)}...)
cache.Store(token, GetFromCache)
} else if !ok {
cache.Store(token, []string{string(body)})
GetFromCache, _ = cache.Load(token)
}

change := GetFromCache.([]string)
var ToClient string
for _, v := range change {
ToClient += v
ToClient += " "
}
_, _ = fmt.Fprintf(writer, "%s\n", ToClient)
}


Golang 开发一个 HTTP Web 服务器/客户端
https://chaggle.github.io/2022/03/31/go/concurrency/HTTP/
作者
chaggle
发布于
2022年3月31日
许可协议