Go 简单服务器并发测试
Go 语言天然支持并发的特性让我眼前一亮,今天就开始在《Go语言圣经》-Web服务中学习了如何简单构建一个服务器,并参考了这篇文章来学习如何使用go进行命令行的程序的构建:go调用外部程序。
搭建简单服务器
Go语言的内置库使得写一个类似fetch的web服务器变得异常地简单。在本节中,我们会展示一个微型服务器,这个服务器的功能是返回当前用户正在访问的URL。比如用户访问的是 http://localhost:8000/hello ,那么响应是URL.Path = “hello”。
server1.go
// Server1 is a minimal "echo"
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc("/", handler) // each request calls handler
log.Fatal(http.ListenAndServe("localhost:8000",nil))
}
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "URL.PAth = %q\n", r.URL.Path)
}
我们只用了八九行代码就实现了一个Web服务程序,这都是多亏了标准库里的方法已经帮我们完成了大量工作。main函数将所有发送到/路径下的请求和handler函数关联起来,/开头的请求其实就是所有发送到当前站点上的请求,服务监听8000端口。发送到这个服务的“请求”是一个http.Request类型的对象,这个对象中包含了请求中的一系列相关字段,其中就包括我们需要的URL。当请求到达服务器时,这个请求会被传给handler函数来处理,这个函数会将/hello这个路径从请求的URL中解析出来,然后把其发送到响应中,这里我们用的是标准输出流的fmt.Fprintf。
我们可以简单的运行一下这个程序,使用命令行输入下面的命令:
go build .\server2.go
.\server2.exe
或者直接用run来跑服务器:
go run .\server2.go
现在可以通过命令行来发送客户端请求了:
$ go build gopl.io/ch1/fetch
$ ./fetch http://localhost:8000
URL.Path = "/"
$ ./fetch http://localhost:8000/help
URL.Path = "/help"
经过试验,我们也可以在浏览器当中来观察对应的服务器获取的内容:
在这个服务的基础上叠加特性是很容易的。一种比较实用的修改是为访问的url添加某种状态。比如,下面这个版本输出了同样的内容,但是会对请求的次数进行计算;对URL的请求结果会包含各种URL被访问的总次数,直接对/count这个URL的访问要除外。
server2.go
// Servers2 is a minimal "echo" and counter server
package main
import (
"fmt"
"log"
"net/http"
"sync"
)
var mu sync.Mutex
var count int
func main() {
http.HandleFunc("/", handler)
http.HandleFunc("/count", counter)
log.Fatal(http.ListenAndServe("localhost:8000", nil))
}
// handler echoes the Path compoent of the requested URL.
func handler(w http.ResponseWriter, r *http.Request) {
mu.Lock()
count++
mu.Unlock()
fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
}
// counter echoes the nuber of calls so far.
func counter(w http.ResponseWriter, r *http.Request) {
mu.Lock()
fmt.Fprintf(w, "Count %d\n", count)
mu.Unlock()
}
这个服务器有两个请求处理函数,根据请求的url不同会调用不同的函数:对/count这个url的请求会调用到count这个函数,其它的url都会调用默认的处理函数。如果你的请求pattern是以/结尾,那么所有以该url为前缀的url都会被这条规则匹配。在这些代码的背后,服务器每一次接收请求处理时都会另起一个goroutine,这样服务器就可以同一时间处理多个请求。然而在并发情况下,假如真的有两个请求同一时刻去更新count,那么这个值可能并不会被正确地增加;这个程序可能会引发一个严重的bug:竞态条件。为了避免这个问题,我们必须保证每次修改变量的最多只能有一个goroutine,这也就是代码里的mu.Lock()和mu.Unlock()调用将修改count的所有行为包在中间的目的。
搭建访问某一个地址的简单客户端
这里我的代码完成了圣经里面的1.5,1.6的习题,具体的客户端的代码如下:
fetch.go
package main
import(
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"strings"
)
func main() {
// read_all()
// partice_1_7()
// partice_1_8()
partice_1_9()
}
func partice_1_9() {
// 打印HTTP协议状态码
for _, url := range os.Args[1:] {
deal_url_prefix(&url)
// fmt.Printf("%s", url)
print_body(url)
}
}
// 处理url的前缀问题,利用引用传递直接在内部修改这个值
func deal_url_prefix(url *string) {
http_pre := strings.HasPrefix(*url, "http://")
if !http_pre {
// 判断是否是填写的https的前缀
https_pre := strings.HasPrefix(*url, "https://")
if !https_pre {
// 添加前缀
*url = "https://" + *url
}
}
}
// 打印获得的网页内容
func print_body(url string) {
resp, err := http.Get(url)
if err != nil {
fmt.Fprintf(os.Stderr, "fetch: %v\n", err)
os.Exit(1)
}
fmt.Printf("%d\t, %s\n", resp.StatusCode, resp.Status)
// 打印网页内容到标准输出
i, err := io.Copy(os.Stdout, resp.Body)
if err != nil {
fmt.Fprintf(os.Stderr, "fetch: %v\n", err)
os.Exit(1)
}
fmt.Printf("%d",i)
}
func partice_1_8() {
for _, url := range os.Args[1:] {
// 判断是否有http前缀
deal_url_prefix(&url)
print_body(url)
}
}
func partice_1_7() {
// 习题1.7
for _, url := range os.Args[1:] {
resp, err := http.Get(url)
if err != nil {
fmt.Fprintf(os.Stderr, "fetch: %v\n", err)
os.Exit(1)
}
// 使用io.Copy可以减少一次缓存
i ,err := io.Copy(os.Stdout, resp.Body)
fmt.Printf("%d",i)
if err != nil {
fmt.Fprintf(os.Stderr, "fetch: reading %s: %v\n", url, err)
os.Exit(1)
}
// fmt.Printf("%s", os.Stdout)
}
}
func read_all() {
for _, url := range os.Args[1:] {
resp, err := http.Get(url)
if err != nil {
fmt.Fprintf(os.Stderr, "fetch: %v\n", err)
os.Exit(1)
}
b, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
fmt.Fprintf(os.Stderr, "fetch: reading %s: %v\n", url, err)
os.Exit(1)
}
fmt.Printf("%s", b)
}
}
使用命令行调用程序批量并发运行客户端
编写一个简单的并发调用外部程序的一个程序,来访问对应的本地服务器,具体的代码如下:
test_local_client.go
// 测试用命令行连续运行客户端程序
package main
import (
"os/exec"
"fmt"
"time"
)
// 并发去跑20个客户端,测试看看本地服务器运行时间
func main() {
start := time.Now()
ch := make(chan string)
for i := 0; i <= 20; i++ {
go del_command(i, ch)
}
for i := 0; i <= 20; i++ {
fmt.Println(<-ch) // receive from channel 从通道接收数据
}
cmd := exec.Command("./fetch", "http://localhost:8000/count")
buf, err := cmd.Output()
if err != nil {
fmt.Println(err.Error())
}
fmt.Println(string(buf))
fmt.Printf("%.2fs elapsed\n", time.Since(start).Seconds())
}
func del_command(i int, ch chan<- string) {
cmd := exec.Command("./fetch", "http://localhost:8000/", string(i))
buf, err := cmd.Output()
if err != nil {
ch <- fmt.Sprintf("%v", err)
}
ch <- fmt.Sprintf(string(buf))
}
检查结果
我们现在需要进行结果的检查,那么就在命令行当中编译,过程如下:
- 打开并运行服务器
go build .\server2.go
.\server2.exe
- 编译fetch
go build .\fetch.go
- 编译并运行test_local_client
go build .\test_local_client.go
./test_local_client.exe
- 检查结果:
exit status 1
200 , 200 OK
URL.Path = "/"
15
exit status 1
200 , 200 OK
URL.Path = "/"
15
exit status 1
200 , 200 OK
URL.Path = "/"
15
exit status 1
200 , 200 OK
URL.Path = "/"
15
exit status 1
200 , 200 OK
URL.Path = "/"
15
exit status 1
200 , 200 OK
URL.Path = "/"
15
exit status 1
200 , 200 OK
URL.Path = "/"
15
exit status 1
200 , 200 OK
URL.Path = "/"
15
exit status 1
200 , 200 OK
URL.Path = "/"
15
exit status 1
200 , 200 OK
Count 12436
12
0.74s elapsed