使用Go语言的库非常容易实现一个Web服务器,用来响应像fetch那样的客户端请求。本节将展示一个迷你服务器,返回访问服务器的URL的路径部分。例如,如果请求的URL是http://localhost:8000/hello,响应将是URL.Path="/hello"。
//server1.go这是一个迷你回声服务器packagemainimport("fmt""log""net/http")funcmain(){http.HandleFunc("/",handler)//回声请求调用处理程序log.Fatal(http.ListenAndServe("localhost:8000",nil))}//处理程序回显请求URLr的路径部分funchandler(whttp.ResponseWriter,r*http.Request){fmt.Fprintf(w,"URL.Path=%qn",r.URL.Path)}
这个程序只有寥寥几行代码,因为库函数做了大部分工作。main函数将一个处理函数和以“/”开头的URL链接在一起,代表所有的URL使用这个函数处理,然后启动服务器监听进入8000端口处的请求。
一个请求由一个http.Request类型的结构体表示,它包含很多关联的域,其中一个是所请求的URL。当一个请求到达时,它被转交给处理函数,并从请求的URL中提取路径部分(/hello),使用fmt.Printf格式化,然后作为响应发送回去。
让我们在后台启动服务器。在MacOSX或者Linux上,在命令行后添加一个&符号;在微软Windows上,不需要&符号,而需要单独开启一个独立的命令行窗口。
$gorunsrc/gopl.io/chl/serverl/main.go&
可以从命令行发起客户请求:
$gobuildgopl.io/ch5/fetch
$./fetchhttp://localhost:8000
URL.Path=T
$./fetchhttp://localhost:8000/help
URL.Path="/help"
另外,还可以通过浏览器进行访问,如下图所示。
图:来自回声服务器的响应
为服务器添加功能很容易。一个有用的扩展是一个特定的URL,它返回某种排序的状态。例如,这个版本的程序完成和回声服务器一样的事情,但同时返回请求的数量;URL/count请求返回到现在为止的个数,去掉/count请求本身:
//server2.go这是一个迷你的回声和计数器服务器packagemainimport("fmt""log""net/http""sync")varmusync.Mutexvarcountintfuncmain(){http.HandleFunc("/",handler)http.HandleFunc("/count",counter)log.Fatal(http.ListenAndServe("localhost:8000",nil))}//处理程序回显请求的URL的路径部分funchandler(whttp.ResponseWriter,r*http.Request){mu.Lock()count++mu.Unlock()fmt.Fprintf(w,"URL.Path=%qn",r.URL.Path)}//counter回显目前为止调用的次数funccounter(whttp.ResponseWriter,r*http.Request){mu.Lock()fmt.Fprintf(w,"Count%dn",count)mu.Unlock()}
这个服务器有两个处理函数,通过请求的URL来决定哪一个被调用:请求/count调用counter,其他的调用handler。以“/”结尾的处理模式匹配所有含有这个前缀的URL。在后台,对于每个传入的请求,服务器在不同的goroutine中运行该处理函数,这样它可以同时处理多个请求。
然而,如果两个并发的请求试图同时更新计数值count,它可能会不一致地增加,程序会产生一个严重的竞态bug。为避免该问题,必须确保最多只有一个goroutine在同一时间访问变量,这正是mu.Lock()和mu.Unlock()语句的作用。
作为一个更完整的例子,处理函数可以报告它接收到的消息头和表单数据,这样可以方便服务器审查和调试请求:
//处理程序回显HTTP请求funchandler(whttp.ResponseWriter,r*http.Request){fmt.Fprintf(w,"%s%s%sn",r.Method,r.URL,r.Proto)fork,v:=ranger.Header{fmt.Fprintf(w,"Header[%q]=%qn",k,v)}fmt.Fprintf(w,"Host=%qn",r.Host)fmt.Fprintf(w,"RemoteAddr=%qn",r.RemoteAddr)iferr:=r.ParseForm();err!=nil{log.Print(err)}fork,v:=ranger.Form{fmt.Fprintf(w,"Form[%q]=%qn",k,v)}}
这里使用http.Request结构体的成员来产生类似下面的输出:
GET/?q=queryHTTP/1.1
Header["Accept-Encoding"]=["gzip,deflate,sdch"]
Header["Accept-Language"]=["en-US,en;q=0.8"]
Header["Connection"]=["keep-alive"]
Header["Accept"]=["text/html,application/xhtml+xml,application/xml;…"]
Header["User-Agent"]=["Mozilla/5.0(Macintosh;IntelMacOSX10_7_5)…"]
Host="localhost:8000"
RemoteAddr="127.0.0.1:59911"
Form["q"]=["query"]
注意这里是如何在if语句中嵌套调用ParseForm的。Go允许一个简单的语句(如一个局部变量声明)跟在if条件的前面,这在错误处理的时候特别有用。也可以这样写:
err:=r.ParseForm()
iferr!=nil{
log.Print(err)
}
这些程序中,我们看到了作为输出流的三种非常不同的类型。fetch程序复制HTTP响应到文件os.Stdout,像lissajous一样;fetchall程序通过将响应复制到ioutil.Discard中进行丢弃(在统计其长度时);Web服务器使fmt.Fprintf通过写入http.Responsewriter来让浏览器显示。
尽管三种类型细节不同,但都满足一个通用的接口(interface),该接口允许它们按需使用任何一种输出流。该接口称为io.Writer。
我们来看一下整合Web服务器和lissajous函数是一件多么容易的事情,这样GIF动画将不再输出到标准输出而是HTTP客户端。简单添加这些行到Web服务器:
handler:=func(whttp.ResponseWriter,r*http.Request){
lissajous(w)
}
http.HandleFunc("/",handler)
或者也可以:
http.HandleFunc("/",func(whttp.ResponseWriter,r*http.Request){
lissajous(w)
})
上面HandleFunc函数中立即调用的第二个参数是函数字面量,这是一个在该场景中使用它时才定义的匿名函数。
一旦完成这个改变,就可以通过浏览器访问http://localhost:8000。每次加载页面,将看到一个类似下图的动画。
图:浏览器中的动态利萨茹图形