Dawn's Blogs

分享技术 记录成长

0%

Go Web编程学习笔记 (5) Web服务

Socket 编程

常用的 Socket 有两种类型:

  • 流式 Socket(SOCK_STREAM):一种面向连接的 Socket,针对于面向连接的 TCP 服务应用。

  • 数据包式 Socket (SOCK_DGRAM)无连接的 Socket,对应于无连接的 UDP 服务应用。

对于在网络上的应用程序来说,(协议类型,IP 地址,端口号)这个三元组可以唯一确定一个进程。

在 Go 的 net 包中,定义了 IP 地址类型,net 包的函数都可以接收 IPv4 和 IPv6 的 IP 地址作为输入。其中 ParseIP(s string) IP 函数会把一个 IPv4 或者 IPv6 的地址转化成 IP 类型。

1
2
3
type IP []byte

func ParseIP(s string) IP

TCP Socket

在 net 包中,有一个类型为 TCPConn,它用来作为客户端和服务器端交互的通道,主要有两个函数,分别可以读写数据:

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

另外,还有一个 TCPAddr 类型,用于表示 TCP 的地址信息,通过 ResolveTCPAddr 函数可以获取一个 TCPAddr:

1
2
3
4
5
6
7
type TCPAddr struct {
IP IP
Port int
Zone string // IPv6 scoped addressing zone
}

func ResolveTCPAddr(net, addr string) (*TCPAddr, os.Error)
  • net 参数是 “tcp4”、”tcp6”或”tcp” 中的任意一个,分别表示 TCP (IPv4-only), TCP (IPv6-only) 或者 TCP (IPv4, IPv6 的任意一个)。
  • addr 表示域名或者 IP 地址

TCP client

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

1
func DialTCP(network string, laddr, raddr *TCPAddr) (*TCPConn, error)
  • net 必须是**”tcp”、”tcp4”、”tcp6”**。
  • laddr 为本地地址,如果 laddr 不是 nil,将使用它作为本地地址,否则自动选择一个本地地址。通常为 nil
  • raddr 为远程的服务器地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 服务器远程地址
serviceAddr = "127.0.0.1:9617"
raddr, err := net.ResolveTCPAddr("tcp", serviceAddr)
if err != nil {
// ...
}
// 与服务器建立连接
conn, err := net.DialTCP("tcp", nil, raddr)
if err != nil {
// ...
}
// 向服务器端发送数据
_, err = conn.Write([]byte("hello world"))
if err != nil {
// ...
}
// 读取数据
result, err := ioutil.ReadAll(conn)
if err != nil {
// ...
}

TCP server

服务器端需要:

  • 绑定服务到指定的非激活端口,并监听此端口。
1
func ListenTCP(network string, laddr *TCPAddr) (*TCPListener, error)
  • 当有客户端请求到达的时候可以接收到来自客户端连接的请求。
1
func (l *TCPListener) Accept() (Conn, error)

当 Accept 之后,为每一个 conn 开启一个 goroutine 来处理与客户端的通信,这样可以支持多并发:

1
2
3
4
5
6
7
8
for {
conn, err := listener.Accept()
if err != nil {
continue
}
// 开启协程,支持多并发
go handleClient(conn)
}d

控制 TCP 连接

TCP 有很多连接控制函数,常用如下:

  • 设置建立连接的超时时间,当超过设置时间时,连接自动关闭。
1
func DialTimeout(net, addr string, timeout time.Duration) (Conn, error)
  • 设置 写入 / 读取 一个连接的超时时间
1
2
func (c *TCPConn) SetReadDeadline(t time.Time) error
func (c *TCPConn) SetWriteDeadline(t time.Time) error
  • 设置 keepAlive 属性,操作系统在 TCP 上没有数据和 ACK 时,会间隔性的发送 keepalive 包,以此判断这个 TCP 连接是否已经断开。
1
func (c *TCPConn) SetKeepAlive(keepalive bool) os.Error

UDP Socket

UDP Socket 在服务器端没有 Accept 函数,其他几乎一样。

Web Socket

在 WebSocket 出现之前,为了实现即时通信,采用的技术都是 “”轮询”,即在特定的时间间隔内,由浏览器对服务器发出 HTTP Request,服务器在收到请求后,返回最新的数据给浏览器刷新,“轮询” 使得浏览器需要对服务器不断发出请求,这样会占用大量带宽。

Web Socket 采用了特殊的报头,使得浏览器和服务器只需要做一个握手的动作,就可以在浏览器和服务器之间建立一条连接通道。URI 以 ws 或者 wss(SSL) 开头。

原理

Web Socket 协议本质上是一个基于 TCP 的协议。Web Socket 的大致流程如下:

  • 握手过程:为了建立一个 Web Socket 连接,客户端浏览器首先要向服务器发起一个 HTTP 请求,这个请求和通常的 HTTP 请求不同,包含了一些附加头信息,其中附加头信息 Upgrade: WebSocket 表明这是一个申请协议升级的 HTTP 请求,服务器端解析这些附加的头信息然后产生应答信息返回给客户端,客户端和服务器端的 WebSocket 连接就建立起来了。

  • 数据传输:双方就可以通过这个连接通道自由的传递信息,并且这个连接会持续存在直到客户端或者服务器端的某一方主动的关闭连接。

Web Socket在第一次握手之后,连接便建立成功,其后的通讯数据都是以 \x00 开头,以 \xFF 结尾。在上层应用中,这是透明的,Web Socket 组件会自动的去掉头部和尾部。

实现

Go 语言官方标准库中没有对于 Web Socket 的支持,但是可以通过以下命令获取:

1
go get golang.org/x/net/websocket

客户端

客户端示例如下,并且客户端绑定了4个事件:

  • onopen 建立连接后触发。
  • onmessage 收到消息后触发。
  • onerror 发生错误时触发。
  • onclose 关闭连接时触发。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<html>
<head></head>
<body>
<script type="text/javascript">
var sock = null;
var wsuri = "ws://127.0.0.1:9617";

window.onload = function() {

console.log("onload");

sock = new WebSocket(wsuri);

sock.onopen = function() {
console.log("connected to " + wsuri);
}

sock.onclose = function(e) {
console.log("connection closed (" + e.code + ")");
}

sock.onmessage = function(e) {
console.log("message received: " + e.data);
}
};

function send() {
var msg = document.getElementById('message').value;
sock.send(msg);
};
</script>
<h1>WebSocket Echo Test</h1>
<form>
<p>
Message: <input id="message" type="text" value="Hello, world!">
</p>
</form>
<button onclick="send();">Send Message</button>
</body>
</html>

服务器端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package main

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

func Index(w http.ResponseWriter, r *http.Request) {
t, _ := template.ParseFiles("demo.html")
t.Execute(w, nil)
}

func Echo(ws *websocket.Conn) {
var err error

for {
var reply string

if err = websocket.Message.Receive(ws, &reply); err != nil {
fmt.Println("Can't receive")
break
}

fmt.Println("Received back from client: " + reply)

msg := "Received: " + reply
fmt.Println("Sending to client: " + msg)

if err = websocket.Message.Send(ws, msg); err != nil {
fmt.Println("Can't send")
break
}
}
}

func main() {
http.HandleFunc("/", Index)
http.Handle("/ws", websocket.Handler(Echo))
if err := http.ListenAndServe(":9617", nil); err != nil {
log.Fatal("ListenAndServe:", err)
}
}

REST

REST ( REpresentatianal State Transfer ) 表现层状态转化。

基本概念

要理解什么是 REST,需要理解以下几个概念:

  • 资源(Resource):REST 是 表现层状态转化,实际上 表现层指的是 资源的表现层。平常上网访问的一张图片、一个文档、一个视频等就是资源。这些资源我们通过 URI 来定位,也就是一个 URI 表示一个资源。
  • 表现层(Representation):把资源实体展现出来的方式,就是表现层。比如一段文本信息,可以输出为 HTML,JSON,XML 等。URI 确定一个资源,但是如何确定它的具体表现形式呢?应该在 HTTP 请求的头信息中用 AcceptContent-Type 字段指定,这两个字段才是对 “表现层” 的描述。
  • 状态转换(State Transfer):在访问服务器的过程中,服务器与客户端进行交互,这就涉及到了数据和状态的变化。HTTP 中四个方法 GET、POST、PUT、DELETE,分别对应于四种基本操作:GET 用来获取资源,POST 用来新建资源(也可以用于更新资源),PUT 用来更新资源,DELETE 用来删除资源。

RESTful 架构

RESTful 架构就是:

  • 每一个 URI 代表一种资源
  • 客户端和服务器之间,传递这种资源的某种表现层
  • 客户端通过四个 HTTP 动词,对服务器端资源进行操作,实现 表现层状态转化

Web 应用要满足 REST 最重要的原则是:客户端和服务器之间的交互在请求之间是无状态的,即从客户端到服务器的每个请求都必须包含理解请求所必需的信息。如果服务器在请求之间的任何时间点重启,客户端不会得到通知。此外此请求可以由任何可用服务器回答,这十分适合云计算之类的环境。因为是无状态的,所以客户端可以缓存数据以改进性能。

另一个重要的 REST 原则是:系统分层,这表示组件无法了解除了与它直接交互的层次以外的组件。

REST 架构图:

img

RPC

见文章 GO语言杂谈 2 RPC