Golang基础学习笔记-Go语言圣经1.8Web服务 个人小并发程序测试学习记录

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))
}

检查结果

我们现在需要进行结果的检查,那么就在命令行当中编译,过程如下:

  1. 打开并运行服务器
go build .\server2.go
.\server2.exe
  1. 编译fetch
go build .\fetch.go
  1. 编译并运行test_local_client
go build .\test_local_client.go
./test_local_client.exe
  1. 检查结果:
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

运行结果

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值