Web 服务 - Go Web 开发实战笔记

1,154 阅读12分钟

概述

Web 服务可以在 HTTP 协议的基础上通过 XML 或者 JSON 来交换信息。REST 和 SOAP 为目前主流的 Web 服务。

校验 IP 格式

在 Go 的 net 包中定义了很多类型、函数和方法用来网络编程,其中 IP 的定义如下:

type IP []byte

在 net 包中有很多函数来操作 IP,其中 ParseIP(s string) IP 函数会把一个 IPv4 或者 IPv6 的地址转化成 IP 类型,请看下面的例子:

package main

import (
	"fmt"
	"net"
	"reflect"
)

func main()  {
	addr := net.ParseIP("192.168.1.1")
	fmt.Println("类型:", reflect.TypeOf(addr))
	if addr == nil {
		fmt.Println("不正确的IP地址")
	} else {
		fmt.Println("IP地址:", addr.String())
	}
}

执行以上程序,控制台输出:

类型: net.IP
IP地址: 192.168.1.1

TCP Socket

在 Go 语言的 net 包中有一个类型 TCPConn,这个类型可以用来作为客户端和服务器端交互的通道,他有两个主要的函数:

func (c *TCPConn) Write(b []byte) (n int, err os.Error)
func (c *TCPConn) Read(b []byte) (n int, err os.Error)

TCPConn 可以用在客户端和服务器端来读写数据。

TCPAddr 类型,他表示一个 TCP 的地址信息,他的定义如下:

type TCPAddr struct {
    IP IP
    Port int
}

在 Go 语言中通过 ResolveTCPAddr 获取一个 TCPAddr

func ResolveTCPAddr(net, addr string) (*TCPAddr, os.Error)
  • net 参数是 "tcp4"、"tcp6"、"tcp" 中的任意一个,分别表示 TCP(IPv4-only),TCP(IPv6-only) 或者 TCP(IPv4, IPv6 的任意一个)。
  • addr 表示域名或者 IP 地址,例如 "www.hao123.com:80" 或者 "127.0.0.1:22"。

Go 语言中通过 net 包中的 DialTCP 函数来建立一个 TCP 连接,并返回一个 TCPConn 类型的对象,当连接建立时服务器端也创建一个同类型的对象,此时客户端和服务器通过各自拥有的 TCPConn 对象来进行数据交换。一般而言,客户端通过 TCPConn 对象将请求信息发送到服务器端,读取服务器端响应的信息。服务器端读取并解析来自客户端的请求,并返回应答信息,这个连接只有当任一端关闭了连接之后才失效,不然这连接可以一直在使用。建立连接的函数定义如下:

func DialTCP(net string, laddr, raddr *TCPAddr) (c *TCPConn, err os.Error)
  • net 参数是 "tcp4"、"tcp6"、"tcp" 中的任意一个,分别表示 TCP(IPv4-only)、TCP(IPv6-only) 或者 TCP(IPv4,IPv6的任意一个)
  • laddr 表示本机地址,一般设置为 nil
  • raddr 表示远程的服务地址

通过 net 包来创建一个服务器端程序,在服务器端我们需要绑定服务到指定的非激活端口,并监听此端口,当有客户端请求到达的时候可以接收到来自客户端连接的请求。net 包中有相应功能的函数,函数定义如下:

func ListenTCP(net string, laddr *TCPAddr) (l *TCPListener, err os.Error)
func (l *TCPListener) Accept() (c Conn, err os.Error)

实现基于 Socket 的时间服务器示例:

TCP server:

/goweb/src/net/TimeServer.go

package main

import (
	"fmt"
	"net"
	"os"
	"time"
)

func main() {
	// 获取 TCP 的地址信息
	tcpAddr, err := net.ResolveTCPAddr("tcp4", ":9999")
	CheckError(err)
	listener, err := net.ListenTCP("tcp", tcpAddr)
	CheckError(err)
	fmt.Println("时间服务器已经启动...")
	for {
		// 将会被阻塞,直到收到客户端请求为止
		conn, err := listener.Accept()
		if err != nil {
			continue
		}
		daytime := time.Now().String()
		// 服务器端来写数据
		conn.Write([]byte(daytime))
		conn.Close()
	}
}

func CheckError(err error) {
	if err != nil {
		fmt.Fprintf(os.Stderr, "Fatal error: %s",err.Error())
		os.Exit(1)
	}
}

TCP client:

/goweb/src/net/TestTimeServer.go

package main

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

func main() {
	// 获取 TCP 的地址信息
	tcpAddr, _ := net.ResolveTCPAddr("tcp4", "127.0.0.1:9999")
	// 链接服务器(一个TCP连接)
	conn,_ := net.DialTCP("tcp", nil, tcpAddr)
	// 从 conn 中读取全部的服务端响应反馈的信息
	result,_ := ioutil.ReadAll(conn)
	fmt.Println(string(result))
}

执行以上程序,客户端输出:

2019-07-23 11:24:01.104430254 +0800 CST m=+4.751025629

支持同时处理多个客户端请求的时间服务器

上面的代码有个缺点,执行的时候是单任务的,不能同时接收多个请求,那么该如何改造以使它支持多并发呢?Go 里面有一个 goroutine 机制,以下是改造后的代码:

/goweb/src/net/TimeServer2.go

package main

import (
	"fmt"
	"net"
	"os"
	"time"
)

func main() {
	// 获取 TCP 的地址信息
	tcpAddr, err := net.ResolveTCPAddr("tcp4", ":9999")
	CheckError2(err)
	listener, err := net.ListenTCP("tcp", tcpAddr)
	CheckError2(err)
	fmt.Println("时间服务器已经启动...")
	for {
		// 将会被阻塞,直到收到客户端请求为止
		conn, err := listener.Accept()
		if err != nil {
			continue
		}
		// 异步执行
		go handleClient(conn)
	}
}

func CheckError2(err error) {
	if err != nil {
		fmt.Fprintf(os.Stderr, "Fatal error: %s",err.Error())
		os.Exit(1)
	}
}

func handleClient(conn net.Conn)  {
	daytime := time.Now().String()
	// 服务器端来写数据
	conn.Write([]byte(daytime))
	conn.Close()
}

通过把业务处理分离到函数 handleClient,我们就可以进一步地实现多并发执行了。

可以接收客户端数据的时间服务器

如需实现服务端根据客户端实际的请求来处理相应的内容,需要通过从客户端发送不同的请求来获取不同的时间格式,而且需要一个长连接。改造后的代码:

服务端:
/goweb/src/net/TimeServer3.go

package main

import (
	"fmt"
	"net"
	"os"
	"strconv"
	"strings"
	"time"
)

func main() {
	// 获取 TCP 的地址信息
	tcpAddr, err := net.ResolveTCPAddr("tcp4", ":9999")
	CheckError3(err)
	listener, err := net.ListenTCP("tcp", tcpAddr)
	CheckError3(err)
	fmt.Println("时间服务器已经启动...")
	for {
		// 将会被阻塞,直到收到客户端请求为止
		conn, err := listener.Accept()
		if err != nil {
			continue
		}
		// 异步执行
		go handleClient3(conn)
	}
}

func CheckError3(err error) {
	if err != nil {
		fmt.Fprintf(os.Stderr, "Fatal error: %s",err.Error())
		os.Exit(1)
	}
}

func handleClient3(conn net.Conn)  {
	// 设置 2 分钟后超时
	conn.SetReadDeadline(time.Now().Add(2 * time.Minute))

	// 将最大请求长度设置为1024KB
	request := make([]byte, 1024)

	for {
		readLen, err := conn.Read(request)
		if err != nil {
			fmt.Println(err)
			break
		}
		if readLen == 0 {
			break
		} else if strings.TrimSpace(string(request[:readLen])) == "timestamp" {
			daytime := strconv.FormatInt(time.Now().Unix(), 10)
			conn.Write([]byte(daytime))
			conn.Close()
		} else {
			daytime := time.Now().String()
			conn.Write([]byte(daytime))
			conn.Close()
		}
		request = make([]byte, 128)
	}
}

客户端:
/goweb/src/net/TestTimeServer3.go

package main

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

func main() {
	// 获取 TCP 的地址信息
	tcpAddr, _ := net.ResolveTCPAddr("tcp4", "127.0.0.1:9999")
	// 链接服务器(一个TCP连接)
	conn,_ := net.DialTCP("tcp", nil, tcpAddr)

	var data string
	data = "timestamp"

	conn.Write([]byte(data))

	// 从 conn 中读取全部的服务端响应反馈的信息
	result,_ := ioutil.ReadAll(conn)
	fmt.Println(string(result))
}

执行以上程序,客户端输出当前时间戳。

UDP Socket

Go 语言包中处理 UDP Socket 和 TCP Socket 不同的地方就是在服务器端处理多个客户端请求数据包的方式不同,UDP 缺少了对客户端连接请求的 Accept 函数。其他基本几乎一模一样,只有 TCP 换成了 UDP 而已。UDP 的几个主要函数如下所示:

func ResolveUDPAddr(net, addr string) (*UDPAddr, os.Error)
func DialUDP(net string, laddr, raddr *UDPAddr) (c *UDPConn, err os.Error)
func ListenUDP(net string, laddr *UDPAddr) (c *UDPConn, err os.Error)
func (c *UDPConn) ReadFromUDP(b []byte) (n int, addr *UDPAddr, err os.Error
func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (n int, err os.Error)

使用示例:

UDP 服务器端代码
/goweb/src/net/MyUDPServer.go

package main

import (
	"fmt"
	"net"
	"time"
)

func main() {
	service := ":8888"
	updAddr,_ := net.ResolveUDPAddr("udp4",service)

	conn,_ := net.ListenUDP("udp", updAddr)
	fmt.Println("UDP 时间服务器已经启动 ")

	var buf[128]byte
	dataLen, addr, err := conn.ReadFromUDP(buf[0:])
	if err != nil {
		return
	}
	fmt.Println(string(buf[:dataLen]))
	daytime := time.Now().String()
	conn.WriteToUDP([]byte(daytime),addr)
}

UDP 客户端代码
/goweb/src/net/MyUDPClient.go

package main

import (
	"fmt"
	"net"
)

// UDP 客户端

func main() {
	udpAddr,_ := net.ResolveUDPAddr("udp4", "127.0.0.1:8888")
	conn,_ := net.DialUDP("udp", nil, udpAddr)

	conn.Write([]byte("hello world"))

	var buf [512]byte
	n,_ := conn.Read(buf[0:])
	fmt.Println(string(buf[0:n]))
}

WebSocket

WebSocket 是 HTML5 的重要特性,它实现了基于浏览器的远程 socket ,它使浏览器和服务器可以进行全双工通信。

Go 实现 WebSocket

Go 语言标准包里面没有提供对 WebSocket 的支持,但是在由官方维护的 go.net 子包中有对这个的支持,可以通过如下的命令获取该包:

go get golang.org/x/net/websocket  // 需要 VPN
https://codeload.github.com/golang/net/zip/master  // 无需 VPN

WebSocket 分为客户端和服务端,接下来将实现一个简单的例子:用户输入信息,客户端通过 WebSocket 将信息发送给服务器端,服务器端收到信息之后主动 Push 信息到客户端,然后客户端将输出其收到的信息,客户端的代码如下:

服务端:
/goweb/src/net/MyWebSocket.go

package main

import (
	"fmt"
	"golang.org/x/net/websocket"
	"net/http"
)

func Echo(ws *websocket.Conn)  {
	var err error
	for {
		var reply string
		if err = websocket.Message.Receive(ws,&reply); err != nil {
			fmt.Println("不能接收数据")
			break
		}

		fmt.Println("来自客户端端数据:" + reply)

		msg := "返回给客户端的数据:" + reply

		fmt.Println("正在发送数据给客户端:" + msg)

		if err = websocket.Message.Send(ws,msg);err != nil {
			fmt.Println("发送数据失败")
			break
		}
	}
}

func main() {
	http.Handle("/", websocket.Handler(Echo))
	fmt.Println("WebSocket 服务器已经启动")
	if err := http.ListenAndServe(":1234", nil); err != nil {
		fmt.Println("监听错误")
	}
}

客户端:
/goweb/src/net/MyWebSocketClient.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>WebSocket 客户端</title>
</head>
<body>
<script type="text/javascript">
    var sock = null;
    var wsurl = "ws://127.0.0.1:1234";
    window.onload = function () {
        sock = new WebSocket(wsurl)
        sock.onopen = function () {
            console.log("服务器已经连接:" + wsurl)
        }
        sock.onclose = function () {
            console.log("连接已经关闭:" + wsurl)
        }
        sock.onmessage = function (e) {
            console.log("接收到消息:" + e.data)
        }
    }

    function send() {
        var msg = document.getElementById("message").value;
        sock.send(msg)
    }
</script>
<p>
    消息:<input id="message" type="text" value="hello server">
</p>
<button onclick="send()">发送消息</button>

</body>
</html>

运行服务后,浏览器访问客户端网页,前后输入 “hello server”、“play” 然后关闭服务,服务端控制台输出:

WebSocket 服务器已经启动
来自客户端端数据:hello server
正在发送数据给客户端:返回给客户端的数据:hello server
来自客户端端数据:play
正在发送数据给客户端:返回给客户端的数据:play

浏览器控制台输出:

服务器已经连接:ws://127.0.0.1:1234
MyWebSocketClient.html:24 接收到消息:返回给客户端的数据:hello server
MyWebSocketClient.html:24 接收到消息:返回给客户端的数据:play
MyWebSocketClient.html:20 连接已经关闭:ws://127.0.0.1:1234

RPC

RPC(Remote Procedure Call Protocol)—— 远程过程调用协议,是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。

RPC工作原理:

RPC工作流程图

运行时,一次客户机对服务器的 RPC 调用,其内部操作大致有如下十步:

  1. 调用客户端句柄;执行传送参数

  2. 调用本地系统内核发送网络消息

  3. 消息传送到远程主机

  4. 服务器句柄得到消息并取得参数

  5. 执行远程过程

  6. 执行的过程将结果返回服务器句柄

  7. 服务器句柄返回结果,调用远程系统内核

  8. 消息传回本地主机

  9. 客户句柄由内核接收消息

  10. 客户接收句柄返回的数据

Go RPC

Go 标准包中已经提供了对 RPC 的支持,而且支持三个级别的 RPC:TCP、HTTP、JSONRPC。但 Go 的 RPC 包是独一无二的 RPC,它和传统的 RPC 系统不同,它只支持 Go 开发的服务器与客户端之间的交互,因为在内部,它们采用了 Gob 来编码。

Go RPC 的函数只有符合下面的条件才能被远程访问,不然会被忽略,详细的要求如下:

  • 函数必须是导出的(首字母大写)
  • 必须有两个导出类型的参数,
  • 第一个参数是接收的参数,第二个参数是返回给客户端的参数,第二个参数必须是指针类型的
  • 函数还要有一个返回值 error

举个例子,正确的RPC函数格式如下:

func (t *T) MethodName(argType T1, replyType *T2) error

T、T1 和 T2 类型必须能被 encoding/gob 包编解码。

任何的 RPC 都需要通过网络来传递数据,Go RPC 可以利用 HTTP 和 TCP 来传递数据,利用 HTTP 的好处是可以直接复用 net/http 里面的一些函数。

基于 HTTP 的 RPC 服务器的实现

服务端代码实现如下:
/goweb/src/net/HTTPRPCServer.go

package main

import (
	"errors"
	"fmt"
	"net/http"
	"net/rpc"
)

// 参数结构体
type FactorialArgs struct {
	N int	// 函数的参数
}

// 用于计算阶乘的内部函数
func factorial(n int) int {
	if n <= 1 {
		return 1
	} else {
		return factorial(n - 1) * n
	}
}

type Factorial struct {

}

func (this *Factorial) GetFactorial(args *FactorialArgs, reply *int) error {
	n := args.N
	if n < 0 || n > 12 {
		return  errors.New("n 必须是 0 到 12 之间到一个整数!")
	} else {
		*reply = factorial(n)
	}
	return nil
}

func main() {
	f := new(Factorial)
	rpc.Register(f)
	rpc.HandleHTTP()
	fmt.Println("RPC 阶乘服务器已经启动!")
	err := http.ListenAndServe(":5432", nil)
	if err != nil {
		fmt.Println(err.Error())
	}

}

运行服务端代码,控制台输出:

RPC 阶乘服务器已经启动!

客户端代码实现如下:
/goweb/src/net/HTTPRPCClient.go

package main

import (
	"fmt"
	"log"
	"net/rpc"
)

type ClientFactorialArgs struct {
	N int
}

func main() {
	serverAddress := "127.0.0.1:5432"
	client, err := rpc.DialHTTP("tcp", serverAddress)

	if err != nil {
		log.Fatal("连接服务端错误", err)
	}

	args := ClientFactorialArgs{10}

	var reply int

	err = client.Call("Factorial.GetFactorial", args, &reply)

	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("阶乘:%d! = %d\n",args.N, reply)
}

运行客户端代码,控制台输出:

阶乘:10! = 3628800

基于 TCP 的 RPC 服务器的实现

服务端代码实现如下:
/goweb/src/net/TCPRPCServer.go

package main

import (
	"errors"
	"fmt"
	"net"
	"net/rpc"
)

// 参数结构体
type FactorialArgs1 struct {
	N int	// 函数的参数
}

// 用于计算阶乘的内部函数
func factorial1(n int) int {
	if n <= 1 {
		return 1
	} else {
		return factorial1(n - 1) * n
	}
}

type Factorial1 struct {

}

func (this *Factorial1) GetFactorial1(args *FactorialArgs1, reply *int) error {
	n := args.N
	if n < 0 || n > 12 {
		return  errors.New("n 必须是 0 到 12 之间到一个整数!")
	} else {
		*reply = factorial1(n)
	}
	return nil
}

func main() {
	f := new(Factorial1)
	rpc.Register(f)
	tcpAddr, _ := net.ResolveTCPAddr("tcp", ":6543")
	listener, _ := net.ListenTCP("tcp",tcpAddr)
	fmt.Println("RPC [TCP] 阶乘服务器已经启动!")
	for {
		conn,err := listener.Accept()
		if err != nil {
			continue
		}
		rpc.ServeConn(conn)
	}
}

运行服务端代码,控制台输出:

RPC [TCP] 阶乘服务器已经启动!

客户端代码实现如下:
/goweb/src/net/TCPRPCClient.go

package main

import (
	"fmt"
	"log"
	"net/rpc"
)

type ClientFactorialArgs1 struct {
	N int
}

func main() {
	serverAddress := "127.0.0.1:6543"
	client, err := rpc.Dial("tcp", serverAddress)

	if err != nil {
		log.Fatal("连接服务端错误", err)
	}

	args := ClientFactorialArgs1{12}

	var reply int

	err = client.Call("Factorial1.GetFactorial1", args, &reply)

	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("阶乘:%d! = %d\n",args.N, reply)
}

运行客户端代码,控制台输出:

阶乘:12! = 479001600

基于 JSON 的 RPC 服务器的实现

服务端代码实现如下:
/goweb/src/net/JSONRPCServer.go

package main

import (
	"errors"
	"fmt"
	"net"
	"net/rpc"
	"net/rpc/jsonrpc"
)

// 参数结构体
type FactorialArgs2 struct {
	N int	// 函数的参数
}

// 用于计算阶乘的内部函数
func factorial2(n int) int {
	if n <= 1 {
		return 1
	} else {
		return factorial2(n - 1) * n
	}
}

type Factorial2 struct {

}

func (this *Factorial2) GetFactorial2(args *FactorialArgs2, reply *int) error {
	n := args.N
	if n < 0 || n > 12 {
		return  errors.New("n 必须是 0 到 12 之间到一个整数!")
	} else {
		*reply = factorial2(n)
	}
	return nil
}

func main() {
	f := new(Factorial2)
	rpc.Register(f)
	tcpAddr, _ := net.ResolveTCPAddr("tcp", ":6543")
	listener, _ := net.ListenTCP("tcp",tcpAddr)
	fmt.Println("RPC [JSON] 阶乘服务器已经启动!")
	for {
		conn,err := listener.Accept()
		if err != nil {
			continue
		}
		jsonrpc.ServeConn(conn)
	}
}

运行服务端代码,控制台输出:

RPC [JSON] 阶乘服务器已经启动!

客户端代码实现如下:
/goweb/src/net/JSONRPCClient.go

package main

import (
	"fmt"
	"log"
	"net/rpc/jsonrpc"
)

type ClientFactorialArgs2 struct {
	N int
}

func main() {
	serverAddress := "127.0.0.1:6543"
	client, err := jsonrpc.Dial("tcp", serverAddress)

	if err != nil {
		log.Fatal("连接服务端错误", err)
	}

	args := ClientFactorialArgs2{11}

	var reply int

	err = client.Call("Factorial2.GetFactorial2", args, &reply)

	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("阶乘:%d! = %d\n",args.N, reply)
}

运行客户端代码,控制台输出:

阶乘:11! = 39916800