Go语言学习笔记(十三)------网络,模板和网页应用

一、tcp服务器

1.go在编写web应用方面非常得力。因为目前它还没有GUI(Graphic User Interface 即图形化用户界面)的框架,通过文本或者模板展现的html界面是目前go编写应用程序的唯一方式。(注:有了一些不太成熟的GUI库例如:go ui)

2.简单的客户端-服务器应用,一个(web)服务器应用需要响应众多客户端的并发请求:go会为每一个客户端产生一个协程用来处理请求。我们需要使用net包中网络通信的功能。它包含了用于TCP/IP以及UDP协议、域名解析等方法。main() 创建了一个 net.Listener 的变量,他是一个服务器的基本函数:监听和接收来自客户端的请求(来自localhost即IP地址为127.0.0.1端口为50000基于TCP协议)。这个 Listen() 函数可以返回一个 error 类型的错误变量。用一个无限for循环的 listener.Accept() 来等待客户端的请求。客户端的请求将产生一个 net.Conn 类型的连接变量。然后一个独立的协程使用这个连接执行 doServerStuff() ,开始使用一个512字节的缓冲 data 来读取客户端发送来的数据并且把它们打印到服务器的终端, len 获取客户端发送的数据字节数;当客户端发送的所有数据都被读取完成时,协程就结束了。这段程序会为每一个客户端连接创建一个独立的协程。必须先运行服务器代码,再运行客户端代码。服务器代码,单独的一个文件:

package main
import (
"fmt"
"net"
)
func main() {
fmt.Println("Starting the server ...")
// 创建 listener
listener, err := net.Listen("tcp", "localhost:50000")
if err != nil {
fmt.Println("Error listening", err.Error())
return //终止程序
}
// 监听并接受来自客户端的连接
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println("Error accepting", err.Error())
return // 终止程序
}
go doServerStuff(conn)
}
}
func doServerStuff(conn net.Conn) {
for {
buf := make([]byte, 512)
len, err := conn.Read(buf)
if err != nil {
fmt.Println("Error reading", err.Error())
return //终止程序
}
fmt.Printf("Received data: %v", string(buf[:len]))
}
}

客户端代码写在另外一个文件client.go中,客户端通过 net.Dial 创建了一个和服务器之间的连接它通过无限循环中的os.Stdin接收来自键盘的输入直到输入了“Q”。注意使用 \r 和 \n 换行符分割字符串(在windows平台下使用 \r\n )。接下来分割后的输入通过 connection 的 Write 方法被发送到服务器:

package main
import (
	"bufio"
	"fmt"
	"net"
	"os"
	"strings"
)
func main() {
	//打开连接:
	conn, err := net.Dial("tcp", "localhost:50000")
	if err != nil {
		//由于目标计算机积极拒绝而无法创建连接
		fmt.Println("Error dialing", err.Error())
		return // 终止程序
	}
	inputReader := bufio.NewReader(os.Stdin)
	fmt.Println("First, what is your name?")
	clientName, _ := inputReader.ReadString('\n')
	// fmt.Printf("CLIENTNAME %s", clientName)
	trimmedClient := strings.Trim(clientName, "\r\n") // Windows 平台下用 "\r\n",Linux平台下使用 "\n"
	// 给服务器发送信息直到程序退出:
	for {
		fmt.Println("What to send to the server? Type Q to quit.")
		input, _ := inputReader.ReadString('\n')
		trimmedInput := strings.Trim(input, "\r\n")
		// fmt.Printf("input:--%s--", input)
		// fmt.Printf("trimmedInput:--%s--", trimmedInput)
		if trimmedInput == "Q" {
			return
		}
		_, err = conn.Write([]byte(trimmedClient + " says: " + trimmedInput))
	}
}

当然,服务器必须先启动好,如果服务器并未开始监听,客户端是无法成功连接的。如果在服务器没有开始监听的情况下运行客户端程序,客户端会停止并打印出以下错误信息: 对tcp 127.0.0.1:50000发起连接时产生错误:由于目标计算机的积极拒绝而无法创建连接 。打开控制台并转到服务器和客户端可执行程序所在的目录,Windows系统下输入server.exe(或者只输入server),Linux系统下输入./server。接下来控制台出现以下信息: Starting the server ...。在Windows系统中,我们可以通过CTRL/C停止程序。然后开启2个或者3个独立的控制台窗口,然后分别输入client回车启动客户端程序,以下是服务器的输出:
Starting the Server ...
Received data: IVO says: Hi Server, what's up ?
Received data: CHRIS says: Are you busy server ?
Received data: MARC says: Don't forget our appointment tomorrow !
当客户端输入 Q 并结束程序时,服务器会输出以下信息:
Error reading WSARecv tcp 127.0.0.1:50000: The specified network name is no longer available.
在网络编程中 net.Dial 函数是非常重要的,一旦你连接到远程系统,就会返回一个Conn类型接口,我们可以用它发送和接收数据。 Dial 函数巧妙的抽象了网络结构及传输。所以IPv4或者IPv6,TCP或者UDP都可以使用这个公用接口。下边这个示例先使用TCP协议连接远程80端口,然后使用UDP协议连接,最后使用TCP协议连接IPv6类型的地址:

package main
import (
	"fmt"
	"io"
	"net"
	"os"
)
func main() {
	conn, err := net.Dial("tcp", "192.0.32.10:80") // tcp ipv4
	checkConnection(conn, err)
	conn, err = net.Dial("udp", "192.0.32.10:80") // udp
	checkConnection(conn, err)
	conn, err = net.Dial("tcp", "[2620:0:2d0:200::10]:80") // tcp ipv6
	checkConnection(conn, err)
}
func checkConnection(conn net.Conn, err error) {
	if err != nil {
		fmt.Printf("error %v connecting!", err)
		os.Exit(1)
	}
	fmt.Printf("Connection is made with %v\n", conn)
}

下边也是一个使用net包从socket中打开,写入,读取数据的例子:

package main
import (
"fmt"
"io"
"net"
)
func main() {
	var (
		host = "www.apache.org"
		port = "80"
		remote = host + ":" + port
		msg string = "GET / \n"
		data = make([]uint8, 4096)
		read = true
		count = 0
	)
	// 创建一个socket
	con, err := net.Dial("tcp", remote)
	// 发送我们的消息,一个http GET请求
	io.WriteString(con, msg)
	// 读取服务器的响应
	for read {
		count, err = con.Read(data)
		read = (err == nil)
		fmt.Printf(string(data[0:count]))
	}
	con.Close()
}

这个版本的 simple_tcp_server.go 从很多方面优化了第一个tcp服务器的示例 server.go 并且拥有更好的结构,它只用了80行代码,服务器地址和端口不再是硬编码,而是通过命令行传入参数并通过 flag 包来读取这些参数。这里使用了 flag.NArg() 检查是否按照期望传入了2个参数:

if flag.NArg() != 2{
panic("usage: host port")
}

传入的参数通过 fmt.Sprintf 函数格式化成字符串

hostAndPort := fmt.Sprintf("%s:%s", flag.Arg(0), flag.Arg(1))

在 initServer 函数中通过 net.ResolveTCPAddr 指定了服务器地址和端口,这个函数最终返回了一个 *net.TCPListener。每一个连接都会以协程的方式运行 connectionHandler 函数。这些开始于当通过 conn.RemoteAddr() 获取到客户端的地址。它使用 conn.Write 发送改进的go-message给客户端,它使用一个25字节的缓冲读取客户端发送的数据并一一打印出来。如果读取的过程中出现错误,代码会进入 switch 语句的 default 分支关闭连接。如果是操作系统的 EAGAIN 错误,它会重试。所有的错误检查都被重构在独立的数'checkError'中,用来分发出现的上下文错误。

// Simple multi-thread/multi-core TCP server.
package main
import (
"flag"
"fmt"
"net"
"os"
)
const maxRead = 25
func main() {
flag.Parse()
if flag.NArg() != 2 {
panic("usage: host port")
}
hostAndPort := fmt.Sprintf("%s:%s", flag.Arg(0), flag.Arg(1))
listener := initServer(hostAndPort)
for {
conn, err := listener.Accept()
checkError(err, "Accept: ")
go connectionHandler(conn)
}
}
func initServer(hostAndPort string) *net.TCPListener {
serverAddr, err := net.ResolveTCPAddr("tcp", hostAndPort)
checkError(err, "Resolving address:port failed: '"+hostAndPort+"'")
listener, err := net.Listen("tcp", serverAddr)
checkError(err, "ListenTCP: ")
println("Listening to: ", listener.Addr().String())
return listener
}
func connectionHandler(conn net.Conn) {
connFrom := conn.RemoteAddr().String()
println("Connection from: ", connFrom)
sayHello(conn)
for {
var ibuf []byte = make([]byte, maxRead+1)
length, err := conn.Read(ibuf[0:maxRead])
ibuf[maxRead] = 0 // to prevent overflow
switch err {
case nil:
handleMsg(length, err, ibuf)
case os.EAGAIN: // try again
continue
default://如果读取的过程中出现错误,代码会进入switch语句的 default 分支关闭连接。
goto DISCONNECT
}
}
DISCONNECT:
err := conn.Close()
println("Closed connection: ", connFrom)
checkError(err, "Close: ")
}
func sayHello(to net.Conn) {
obuf := []byte{'L', 'e', 't', '\'', 's', ' ', 'G', 'O', '!', '\n'}
wrote, err := to.Write(obuf)
checkError(err, "Write: wrote "+string(wrote)+" bytes.")
}
func handleMsg(length int, err error, msg []byte) {
if length > 0 {
print("<", length, ":")
for i := 0; ; i++ {
if msg[i] == 0 {
break
}
fmt.Printf("%c", msg[i])
}
print(">")
}
}
func checkError(error error, info string) {
if error != nil {
panic("ERROR: " + info + " " + error.Error()) // terminate program
}
}

命令行中输入 simple_tcp_server localhost 50000 来启动服务器程序,然后在独立的命令行窗口启动一些client.go的客
户端。当有两个客户端连接的情况下服务器的典型输出如下,这里我们可以看到每个客户端都有自己的地址:

$simple_tcp_server localhost 50000
Listening to: 127.0.0.1:50000
Connection from: 127.0.0.1:49346
<25:Ivo says: Hi server, do y><12:ou hear me ?>
Connection from: 127.0.0.1:49347
<25:Marc says: Do you remembe><25:r our first meeting serve><2:r?>

net.Error: 这个 net 包返回错误的错误类型,下边是约定的写法,不过 net.Error 接口还定义了一些其他的错误实现,有些额外的方法。

package net
type Error interface{
Timeout() bool // 错误是否超时
Temporary() bool // 是否是临时错误
}

通过类型断言,客户端代码可以用来测试 net.Error ,从而区分哪些临时发生的错误或者必然会出现的错误。举例来说,一个网络爬虫程序在遇到临时发生的错误时可能会休眠或者重试,如果是一个必然发生的错误,则他会放弃继续执行。

// in a loop - some function returns an error err
if nerr, ok := err.(net.Error); ok && nerr.Temporary(){
time.Sleep(1e9)
continue // try again
}
if err != nil{
log.Fatal(err)
}

二、一个简单的网页服务器

Http是一个比tcp更高级的协议,它描述了客户端浏览器如何与网页服务器进行通信。Go有自己的 net/http 包,简单的示例开始, 首先编写一个“Hello world!”:查看示例我们引入了 http 包并启动了网页服务器,和 net.Listen("tcp", "localhost:50000") 函数的tcp服务器是类似的,使用 http.ListenAndServe("localhost:8080", nil) 函数,如果成功会返回空,否则会返回一个错误(可以指定localhost为其他地址,8080是指定的端口号)http.URL 描述了web服务器的地址,内含存放了url字符串的 Path 属性; http.Request 描述了客户端请求,内含一
个 URL 属性如果 req 请求是一个POST类型的html表单,“var1”就是html表单中一个输入属性的名称,然后用户输入的值就可以通过
GO代码: req.FormValue("var1") 获取到。还有一种方法就是先执行 request.ParseForm() 然后再获取 request.Form["var1"] 的第一个返回参数,就像这样:var1, found := request.Form["var1"]。第二个参数 found 就是 true ,如果 var1 并未出现在表单中, found 就是 false
表单属性实际上是一个 map[string][]string 类型。网页服务器返回了一个 http.Response ,它是通过 http.ResponseWriter 对象输出的,这个对象整合了HTTP服务器的返回结果;通过对它写入内容,我们就将数据发送给了HTTP客户端。现在我们还需要编写网页服务器必须执行的程序,它是如何处理请求的呢。这是在 http.HandleFunc 函数中完成的,就是在这个例子中当根路径“/”(url地址是http://localhost:8080 )被请求的时候(或者这个服务器上的其他地址), HelloServer 函数就被执行了。这个函数是 http.HandlerFunc 类型的,它们通常用使用Prehandler来命名,在前边加了一个Pref前缀。http.HandleFunc 注册了一个处理函数(这里是 HelloServer )来处理对应 / 的请求。/ 可以被替换为其他特定的url比如 /create , /edit 等等;你可以为每一个特定的url定义一个单独的处理函数。这个函数需要两个参数:第一个是 ReponseWriter 类型的 w ;第二个是请求 req 。程序向 w 写入了 Hello 和 r.URL.Path[1:] 组成的字符串后边的 [1:] 表示“创建一个从第一个字符到结尾的子切片”,用来丢弃掉路径开头的“/”, fmt.Fprintf() 函数完成了本次写入;另外一种写法是 io.WriteString(w, "hello,world!\n")。总结:第一个参数是请求的路径,第二个参数是处理这个路径请求的函数的引用。

package main
import (
"fmt"
"log"
"net/http"
)
func HelloServer(w http.ResponseWriter, req *http.Request) {
fmt.Println("Inside HelloServer handler")
fmt.Fprintf(w, "Hello,"+req.URL.Path[1:])
}
func main() {
http.HandleFunc("/", HelloServer)
err := http.ListenAndServe("localhost:8080", nil)
if err != nil {
log.Fatal("ListenAndServe: ", err.Error())
}
}

然后打开你的浏览器并输入url地址: http://localhost:8080/world ,浏览器就会出现文字: Hello, world ,网页服务器会响应你在 :8080/ 后边输入的内容:使用 fmt.Println 在控制台打印状态,在每个handler被请求的时候,在他们内部打印日志会很有帮助
注: 1)前两行(没有错误处理代码)可以替换成以下写法:
http.ListenAndServe(":8080", http.HandlerFunc(HelloServer))
2) fmt.Fprint 和 fmt.Fprintf 都是用来写入 http.ResponseWriter 的不错的函数(他们实现了 io.Writer )。 比如我们可以使用
fmt.Fprintf(w, "<h1>%s<h1><div>%s</div>", title, body)
来构建一个非常简单的网页并插入 title 和 body 的值,如果你需要更多复杂的替换,使用模板包

 3)如果你需要使用安全的https连接,使用 http.ListenAndServeTLS() 代替 http.ListenAndServe()

4) http.HandleFunc("/", Hfunc) 中的 HFunc 是一个处理函数,如下:
func HFunc(w http.ResponseWriter, req *http.Request) {
...
}
也可以使用这种方式: http.Handle("/", http.HandlerFunc(HFunc)),上边的 HandlerFunc 只是一个类型名称,它定义如下:
type HandlerFunc func(ResponseWriter, *Request),它是一个可以把普通的函数当做HTTP处理器的适配器。如果 f 函数声明的合适, HandlerFunc(f) 就是一个执行了 f 函数的处理器对象。http.Handle 的第二个参数也可以是 T 的一个obj对象: http.Handle("/", obj) 给T提供了 ServeHTTP 方法,实现了http的 Handler 接口:
func (obj *Typ) ServeHTTP(w http.ResponseWriter, req *http.Request) {
...
}

三、访问并读取页面

在下边这个程序中,数组中的url都将被访问:会发送一个简单的 http.Head() 请求查看返回值;它的声明如下: func Head(url string) (r *Response, err error)返回状态码会被打印出来。

package main
import (
	"fmt"
	"net/http"
)
var urls = []string{
	"http://www.baidu.com/",
	"http://studygolang.com/",
	"http://qq.com/",
}
func main() {
	// Execute an HTTP HEAD request for all url's
	// and returns the HTTP status string or an error string.
	for _, url := range urls {
		resp, err := http.Head(url)
		if err != nil {
			fmt.Println("Error:", url, err)
		}
		fmt.Println(url, ": ", resp.Status)
	}
}

在下边的程序中我们使用 http.Get() 获取网页内容; Get 的返回值 res 中的 Body 属性包含了网页内容,然后我们用 ioutil.ReadAll 把它读出来:

package main
import (
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
)
func main() {
	res, err := http.Get("http://www.baidu.com")
	checkError(err)
	data, err := ioutil.ReadAll(res.Body)
	checkError(err)
	fmt.Printf("Got: %q", string(data))
}
func checkError(err error) {
	if err != nil {
		log.Fatalf("Get : %v", err)
	}
}

用到 http 包中的其他重要的函数:
http.Redirect(w ResponseWriter, r *Request, url string, code int) :这个函数会让浏览器重定向到url(是请求的url的相对路径)以及状态码。
http.NotFound(w ResponseWriter, r *Request) :这个函数将返回网页没有找到,HTTP 404错误。
http.Error(w ResponseWriter, error string, code int) :这个函数返回特定的错误信息和HTTP代码。
另 http.Request 对象的一个重要属性 req : req.Method ,这是一个包含 GET 或 POST 字符串,用来描述网页是以何种方式被请求的。
go为所有的HTTP状态码定义了常量,比如:
http.StatusContinue = 100
http.StatusOK = 200
http.StatusFound = 302
http.StatusBadRequest = 400
http.StatusUnauthorized = 401
http.StatusForbidden = 403
http.StatusNotFound = 404
http.StatusInternalServerError = 500
你可以使用 w.header().Set("Content-Type", "../..") 设置头信息
比如在网页应用发送html字符串的时候,在输出之前执行 w.Header().Set("Content-Type", "text/html") 。

四、写一个简单的网页应用

下边的程序在端口8088上启动了一个网页服务器; SimpleServer 会处理 /test1 url使它在浏览器输出 helloworld 。 FormServer 会处理 /test2 url:如果url最初由浏览器请求,那么它就是一个 GET 请求,并且返回一个 form 常量,包含了简单的 input 表单,这个表单里有一个文本框和一个提交按钮。当在文本框输入一些东西并点击提交按钮的时候,会发起一个 POST 请求。 FormServer 中的代码用到了 switch 来区分两种情况。在 POST 情况下,使用 request.FormValue("inp") 通过文本框的 name 属性 inp 来获取内容,并写回浏览器页面。在控制台启动程序并在浏览器中打开url http://localhost:8088/test2 来测试这个程序。

package main
import (
	"io"
	"net/http"
)
const form = `
<html><body>
<form action="#" method="post" name="bar">
<input type="text" name="in" />
<input type="submit" value="submit"/>
</form>
</body></html>
`
/* handle a simple get request */
func SimpleServer(w http.ResponseWriter, request *http.Request) {
	io.WriteString(w, "<h1>hello, world</h1>")
}
func FormServer(w http.ResponseWriter, request *http.Request) {
	w.Header().Set("Content-Type", "text/html")
	switch request.Method {
	case "GET":
		/* display the form to the user */
		io.WriteString(w, form)
	case "POST":
		/* handle the form data, note that ParseForm must
		be called before we can extract form data */
		//request.ParseForm();
		//io.WriteString(w, request.Form["in"][0])
		io.WriteString(w, request.FormValue("in"))
	}
}
func main() {
	http.HandleFunc("/test1", SimpleServer)
	http.HandleFunc("/test2", FormServer)
	if err := http.ListenAndServe(":8088", nil); err != nil {
		panic(err)
	}
}

当使用字符串常量表示html文本的时候,包含 <html><body></body></html>对于让浏览器识别它收到了一个html非常重要。更安全的做法是在处理器中使用 w.Header().Set("Content-Type", "text/html") 在写入返回之前将 header 的 contenttype 设置为 text/html,content-type 会让浏览器认为它可以使用函数 http.DetectContentType([]byte(form)) 来处理收到的数据。

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值