Go Web编程(八——web服务)

Socket编程

  • 什么是socket

socket起源于Unix,网络的Socket数据传输是一种很特殊的I/O。Socket也是一种文件描述符,Socket也具有一个类似于打开文件的函数调用:Socket(),该函数返回一个整型的Socket描述符,随后的连接建立、数据传输等操作都是通过该Socket实现的。

常用的Socket类型有两张:流式Socket(SOCK_STREAM)和数据报式Socket(SOCK_DGRAM)。

流式是面向连接的Socket,针对于面向连接的TCP服务应用。数据报式Socket是一种面向无连接的Socket,对应于无连接的UDP服务应用。

  • Socket如何通信

首先解决如何唯一标识一个进程,在本地可以通过PID,但是在网络中行不通。

通过传输层的协议+端口就可以唯一标识主机中的应用程序(进程)。三元组(ip地址、协议、端口)。

  • GO支持的IP类型

net包中定义了很多类型、函数和方法用来网络编程。

type IP []type

ParseIP(s string)IP函数会把一个IPv4或者IPv6的地址转化成IP类型。

  • TCP Socket

net包中有一个类型TCPConn,可以用来作为客户端和服务端交互的通道。

func (c *TCPConn)Write(b []byte)(int,error)
func (c *TCPConn)Read(b []byte)(int,error)

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

TCPAddr类型,表示一个TCP类型信息:

type TCPAddr struct {
    IP IP
    Port int
    Zone string //IPv6 scoped addressing zone
}

通过ResolveTCPAddr来获取一个TCPAddr

func ResolveTCPAddr(net,addr string)(*TCPAddr,os.Error)

net参数时tcp4,tcp6,tcp中任意一个。addr表示域名或者IP地址。

  • TCP client

GO通过net包中的DialTCP函数来建立一个TCP连接,并返回一个TCPConn类型的对象。

func DialTCP(network string,laddr,raddr *TCPAddr)(*TCPConn,error)

laddr表示本机地址,一般设置为nil,raddr表示远程的服务地址。

客户端代码:

package main

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

func main() {
    if len(os.Args) != 2 {
        fmt.Fprintf(os.Stderr, "Usage: %s host:port ", os.Args[0])
        os.Exit(1)
    }
    service := os.Args[1]
    tcpAddr, err := net.ResolveTCPAddr("tcp4", service)
    checkError(err)
    conn, err := net.DialTCP("tcp", nil, tcpAddr)
    checkError(err)
    _, err = conn.Write([]byte("HEAD / HTTP/1.0\r\n\r\n"))
    checkError(err)
    result, err := ioutil.ReadAll(conn)
    checkError(err)
    fmt.Println(string(result))
    os.Exit(0)
}
func checkError(err error) {
    if err != nil {
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
        os.Exit(1)
    }
}

首先程序将用户的输入作为参数service传入net.ResolveTCPAddr获取一个TCPAddr,然后将TCPAddr传入DialTCP后创建一个TCP连接conn,通过conn来发送请求信息,最后通过ioutil.ReadAll从conn中读取全部的文本,也就是服务端响应反馈的信息。

  • TCP Server

在服务器端我们需要绑定服务到指定的非激活端口,并监听此端口,当有客户端请求到达的时候可以接收到来自客户端连接的请求。

func ListenTCP(network string,laddr *TCPAddr)(*TCPListener,error)
func(l *TCPListener)Accept()(Conn,err)

简单的时间同步服务,监听777端口:

package main
import {
    "fmt"
    "net"
    "os"
    "time"
}
func main(){
    service := ":7777"
    tcpAddr,err := net.ResolveTCPAddr("tcp4",service)
    checkError(err)
    listener,err := net.ListenTCP("tcp",tcpAddr)
    checkError(err)
    for{
        conn,err := listener.Accept()
        if err != nil {
            continue
        }
        go handleClient(conn)
    }
    func handlerClient(conn net.Conn){
          defer conn.Close()
    	  daytime := time.Now().String()
    	  conn.Write([]byte(daytime)) // don't care about return value
          // we're finished with this client
    }
    func checkError(err error){
        if err!= nil {
            fmt.Fprintf(os.Stderr,"Fatal error:%s",err.Error())
            os.Exit(1)
        }
    }
}

值得注意的是,在代码中 for 循环里,当有错误发生时,直接 continue 而不是退出,是因为在服务器端跑代码的时候,当有错误发生的情况下最好是由服务端记录错误,然后当前连接的客户端直接报错而退出,从而不会影响到当前服务端运行的整个服务。

长连接:

func handleClient(conn net.Conn){
    conn.SetReadDeadline(time.Now().Add(2*time.Minute))
    request := make([]byte,128)
    defer conn.Close()
    for {
        read_len,err:=conn.Read(request)
        if err!=nil {
            fmt.Print(err)
            break
        }
		if read_len == 0 {
                break // connection already closed by client
            } else if strings.TrimSpace(string(request[:read_len])) == "timestamp" {
                daytime := strconv.FormatInt(time.Now().Unix(), 10)
                conn.Write([]byte(daytime))
            } else {
                daytime := time.Now().String()
                conn.Write([]byte(daytime))
            }
            request = make([]byte, 128) // clear last read content
    	}
	}
  • 控制TCP连接
func DialTimeout(net,addr string,timeout time.Duration)(Conn,err)

设置建立连接的超时时间,客户端和服务端都适用,当超过设置时间时,连接自动关闭

func (c *TCPConn) SetReadDeadline(t time.Time) error
func (c *TCPConn) SetWriteDeadline(t time.Time) error

用来设置写入/读取一个连接的超时时间。

func (c *TCPConn) SetKeepAlice(keepalive bool) os.Error

设置keepAlice属性,是操作系统层在tcp上没有数据和ACK的时候,会间隔性的发送keepalive包,操作系统通过该包来判断一个tcp连接是够已经断开。

  • UDP Socket

TCP和UDP区别:UDP缺少了对客户端连接请求的ACCEPT函数,其他几乎一模一样,只有TCP换成了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)

Web Socket

基于浏览器的远程socket,它使浏览器和服务器可以进行全双工通信。

原理:

WebSocket URL的起始输入是ws://或是wss:

websocket协议在第一次handshake通过一手,连接便建立成功,其后的数据都是以"\x00"开头,以"\xFF"结尾。在客户端,这个是透明的,WebSocket组件会自动将原始数据"掐头去尾"。

在客户端,这个是透明的,websocket组件会自动将原始数据“掐头去尾”。

Request/Response

  • GO实现WebSocket

go.net子包中有对websocket的支持

go get golang.org/x/net/websocket

REST

资源:通过URL来定位,也就是URL表示一个资源

表现层:通过格式把实体展现出来,txt,html,json,png等,URL确定一个资源,在HTTP请求的头信息中用Accept和Content-Type字段指定。

状态转化:GET:获取资源,POST:新建资源,PUT:更新资源,DELET:删除资源

  • RESTful的实现

Go没有为REST提供直接支持,可以通过Net/http包来实现。

有些防火墙会挡住 HTTP PUT 和 DELETE 请求,要绕过这个限制,客户端需要把实际的 PUT 和 DELETE 请求通过 POST 请求穿透过来。RESTful 服务则要负责在收到的 POST 请求中找到原始的 HTTP 方法并还原。

RESTful的应用设计:

package main

import (
    "fmt"
    "log"
    "net/http"

    "github.com/julienschmidt/httprouter"
)

func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
    fmt.Fprint(w, "Welcome!\n")
}

func Hello(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
    fmt.Fprintf(w, "hello, %s!\n", ps.ByName("name"))
}

func getuser(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
    uid := ps.ByName("uid")
    fmt.Fprintf(w, "you are get user %s", uid)
}

func modifyuser(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
    uid := ps.ByName("uid")
    fmt.Fprintf(w, "you are modify user %s", uid)
}

func deleteuser(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
    uid := ps.ByName("uid")
    fmt.Fprintf(w, "you are delete user %s", uid)
}

func adduser(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
    // uid := r.FormValue("uid")
    uid := ps.ByName("uid")
    fmt.Fprintf(w, "you are add user %s", uid)
}

func main() {
    router := httprouter.New()
    router.GET("/", Index)
    router.GET("/hello/:name", Hello)

    router.GET("/user/:uid", getuser)
    router.POST("/adduser/:uid", adduser)
    router.DELETE("/deluser/:uid", deleteuser)
    router.PUT("/moduser/:uid", modifyuser)

    log.Fatal(http.ListenAndServe(":8080", router))
}

RPC

RPC 就是想实现函数调用模式的网络化。客户端就像调用本地函数一样,然后客户端把这些参数打包之后通过网络传递到服务端,服务端解包到处理过程中执行,然后执行的结果反馈给客户端。

RPC(远程过程调用协议)。

1.调用客户端句柄;执行传送参数。2.调用本地系统内核发送网络消息。3.消息传送到远程主机。4.服务器句柄得到消息并取得参数。5.执行远程过程。6.执行的过程将结果返回。7.服务器句柄返回结果,调用远程系统内核。8.消息传回本地主机。9.客户句柄由内核接收消息。10.客户接收句柄返回的数据

  • GO RPC

GO标准包中已经提供了对RPC的支持:TCP、HTTP、JSONRPC。但是支持GO开发的服务器与客户端之间的交互,因为在内部,它采用Gob来编码。

RPC格式:

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

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

  • HTTP RPC

服务端:

package main

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

type Args struct {
    A, B int
}

type Quotient struct {
    Quo, Rem int
}

type Arith int

func (t *Arith) Multiply(args *Args, reply *int) error {
    *reply = args.A * args.B
    return nil
}

func (t *Arith) Divide(args *Args, quo *Quotient) error {
    if args.B == 0 {
        return errors.New("divide by zero")
    }
    quo.Quo = args.A / args.B
    quo.Rem = args.A % args.B
    return nil
}

func main() {

    arith := new(Arith)
    rpc.Register(arith)
    rpc.HandleHTTP()

    err := http.ListenAndServe(":1234", nil)
    if err != nil {
        fmt.Println(err.Error())
    }
}

注册了一个Arith的RPC服务,然后通过HandleHTTP函数把该服务注册到了HTTP协议上,就可以利用HTTP的方式来传递数据了

客户端代码:

package main

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

type Args struct {
    A, B int
}

type Quotient struct {
    Quo, Rem int
}

func main() {
    if len(os.Args) != 2 {
        fmt.Println("Usage: ", os.Args[0], "server")
        os.Exit(1)
    }
    serverAddress := os.Args[1]

    client, err := rpc.DialHTTP("tcp", serverAddress+":1234")
    if err != nil {
        log.Fatal("dialing:", err)
    }
    // Synchronous call
    args := Args{17, 8}
    var reply int
    err = client.Call("Arith.Multiply", args, &reply)
    if err != nil {
        log.Fatal("arith error:", err)
    }
    fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)

    var quot Quotient
    err = client.Call("Arith.Divide", args, &quot)
    if err != nil {
        log.Fatal("arith error:", err)
    }
    fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)

}

客户端最重要的就是这个 Call 函数,它有 3 个参数,第 1 个要调用的函数的名字,第 2 个是要传递的参数,第 3 个要返回的参数 (注意是指针类型)

  • TCP RPC

服务端:

package main

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

type Args struct {
    A, B int
}

type Quotient struct {
    Quo, Rem int
}

type Arith int

func (t *Arith) Multiply(args *Args, reply *int) error {
    *reply = args.A * args.B
    return nil
}

func (t *Arith) Divide(args *Args, quo *Quotient) error {
    if args.B == 0 {
        return errors.New("divide by zero")
    }
    quo.Quo = args.A / args.B
    quo.Rem = args.A % args.B
    return nil
}

func main() {

    arith := new(Arith)
    rpc.Register(arith)

    tcpAddr, err := net.ResolveTCPAddr("tcp", ":1234")
    checkError(err)

    listener, err := net.ListenTCP("tcp", tcpAddr)
    checkError(err)

    for {
        conn, err := listener.Accept()
        if err != nil {
            continue
        }
        rpc.ServeConn(conn)
    }

}

func checkError(err error) {
    if err != nil {
        fmt.Println("Fatal error ", err.Error())
        os.Exit(1)
    }
}

RPC客户端:

package main

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

type Args struct {
    A, B int
}

type Quotient struct {
    Quo, Rem int
}

func main() {
    if len(os.Args) != 2 {
        fmt.Println("Usage: ", os.Args[0], "server:port")
        os.Exit(1)
    }
    service := os.Args[1]

    client, err := rpc.Dial("tcp", service)
    if err != nil {
        log.Fatal("dialing:", err)
    }
    // Synchronous call
    args := Args{17, 8}
    var reply int
    err = client.Call("Arith.Multiply", args, &reply)
    if err != nil {
        log.Fatal("arith error:", err)
    }
    fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)

    var quot Quotient
    err = client.Call("Arith.Divide", args, &quot)
    if err != nil {
        log.Fatal("arith error:", err)
    }
    fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)

}
  • 总结

Go 已经提供了对 RPC 的良好支持,通过上面 HTTP、TCP、JSON RPC 的实现,我们就可以很方便的开发很多分布式的 Web 应用

posted @ 2021-05-17 21:36  Gumi-21  阅读(189)  评论(0)    收藏  举报