概述
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 调用,其内部操作大致有如下十步:
-
调用客户端句柄;执行传送参数
-
调用本地系统内核发送网络消息
-
消息传送到远程主机
-
服务器句柄得到消息并取得参数
-
执行远程过程
-
执行的过程将结果返回服务器句柄
-
服务器句柄返回结果,调用远程系统内核
-
消息传回本地主机
-
客户句柄由内核接收消息
-
客户接收句柄返回的数据
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