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, ")
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, ")
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 应用

浙公网安备 33010602011771号