网络编程¶
Web 服务器¶
包 http 通过任何实现了 http.Handler
的值来响应 HTTP 请求:
package http
type Handler interface {
ServeHTTP(w ResponseWriter, r *Request)
}
举个🌰,以下代码中,类型 Hello
实现了 http.Handler
。
package main
import (
"fmt"
"log"
"net/http"
)
type Hello struct{}
func (h Hello) ServeHTTP(
w http.ResponseWriter,
r *http.Request) {
fmt.Fprint(w, "Hello!")
}
func main() {
var h Hello
err := http.ListenAndServe("localhost:4000", h)
if err != nil {
log.Fatal(err)
}
}
IP 类型¶
IP类型的定义如下:
type IP []byte
ParseIP(s string) IP
函数会把一个IPv4或者IPv6的地址转化成IP类型。
举个🌰:
package main
import (
"net"
"os"
"fmt"
)
func main() {
if len(os.Args) != 2 {
fmt.Fprintf(os.Stderr, "Usage: %s ip-addr\n", os.Args[0])
os.Exit(1)
}
name := os.Args[1]
addr := net.ParseIP(name)
if addr == nil {
fmt.Println("Invalid address")
} else {
fmt.Println("The address is ", addr.String())
}
os.Exit(0)
}
执行之后,输入一个IP地址就会给出相应的IP格式
TCP Socket¶
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"中的任意一个,分别表示TCP(IPv4-only), TCP(IPv6-only)或者TCP(IPv4, IPv6的任意一个)。
- addr表示域名或者IP地址及其端口号,例如"www.google.com:80" 或者"127.0.0.1:22"。
TCP client¶
Go语言中通过net包中的DialTCP
函数来建立一个TCP连接,并返回一个TCPConn
类型的对象,当连接建立时服务器端也创建一个同类型的对象,此时客户端和服务器端通过各自拥有的TCPConn
对象来进行数据交换。一般而言,客户端通过TCPConn
对象将请求信息发送到服务器端,读取服务器端响应的信息。服务器端读取并解析来自客户端的请求,并返回应答信息,这个连接只有当任一端关闭了连接之后才失效,不然这连接可以一直在使用。
建立连接的函数定义如下:
func DialTCP(network string, laddr, raddr *TCPAddr) (*TCPConn, error)
- network参数是"tcp4"、"tcp6"、"tcp"中的任意一个,分别表示TCP(IPv4-only)、TCP(IPv6-only)或者TCP(IPv4,IPv6的任意一个)
- laddr表示本机地址,一般设置为nil
- raddr表示远程的服务地址
举个🌰:模拟一个基于HTTP协议的客户端请求去连接一个Web服务端。我们要写一个简单的http请求头,格式类似如下:
"HEAD / HTTP/1.0\r\n\r\n"
从服务端接收到的响应信息格式可能如下:
HTTP/1.0 200 OK
ETag: "-9985996"
Last-Modified: Thu, 25 Mar 2010 17:51:10 GMT
Content-Length: 18074
Connection: close
Date: Sat, 28 Aug 2010 00:43:48 GMT
Server: lighttpd/1.4.23
我们的客户端代码如下所示:
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)
result := make([]byte, 256)
_, err = conn.Read(result)
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¶
在服务器端我们需要绑定服务到指定的非激活端口,并监听此端口,当有客户端请求到达的时候可以接收到来自客户端连接的请求。
net包中有相应功能的函数,函数定义如下:
func ListenTCP(network string, laddr *TCPAddr) (*TCPListener, error)
func (l *TCPListener) Accept() (Conn, error)
参数说明同DialTCP的参数一样。
举个🌰:
我们实现一个简单的时间同步服务,监听7777端口,当有新的客户端请求到达并同意接受Accept
该请求的时候他会反馈当前的时间信息。在代码中for
循环里,当有错误发生时,直接continue而不是退出,是因为在服务器端跑代码的时候,当有错误发生的情况下最好是由服务端记录错误,然后当前连接的客户端直接报错而退出,从而不会影响到当前服务端运行的整个服务。
package main
import (
"fmt"
"net"
"os"
"time"
"strconv"
"strings"
)
func main() {
service := ":1200"
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) // 实现并发
}
}
// 把业务处理分离到函数 handleClient
func handleClient(conn net.Conn) {
conn.SetReadDeadline(time.Now().Add(2 * time.Minute)) // set 2 minutes timeout
request := make([]byte, 128) // set maxium request length to 128B to prevent flood attack
defer conn.Close() // close connection before exit
for {
read_len, err := conn.Read(request)
if err != nil {
fmt.Println(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
}
}
func checkError(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
os.Exit(1)
}
}
在上面这个例子中,我们使用conn.Read()
不断读取客户端发来的请求。由于我们需要保持与客户端的长连接,所以不能在读取完一次请求后就关闭连接。由于conn.SetReadDeadline()
设置了超时,当一定时间内客户端无请求发送,conn
便会自动关闭,下面的for循环即会因为连接已关闭而跳出。需要注意的是,request
在创建时需要指定一个最大长度以防止flood attack;每次读取到请求处理完毕后,需要清理request,因为conn.Read()
会将新读取到的内容append到原内容之后。
控制TCP连接¶
func DialTimeout(net, addr string, timeout time.Duration) (Conn, error) // 设置建立连接的超时时间
func (c *TCPConn) SetReadDeadline(t time.Time) error // 设置读取一个连接的超时时间
func (c *TCPConn) SetWriteDeadline(t time.Time) error // 设置写入一个连接的超时时间
func (c *TCPConn) SetKeepAlive(keepalive bool) os.Error // 设置keepAlive属性
UDP Socket¶
Go语言包中处理UDP Socket和TCP Socket不同的地方就是在服务器端处理多个客户端请求数据包的方式不同,UDP缺少了对客户端连接请求的Accept函数。
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的客户端:(TCP换成了UDP而已)
package main
import (
"fmt"
"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]
udpAddr, err := net.ResolveUDPAddr("udp4", service)
checkError(err)
conn, err := net.DialUDP("udp", nil, udpAddr)
checkError(err)
_, err = conn.Write([]byte("anything"))
checkError(err)
var buf [512]byte
n, err := conn.Read(buf[0:])
checkError(err)
fmt.Println(string(buf[0:n]))
os.Exit(0)
}
func checkError(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal error %s", err.Error())
os.Exit(1)
}
}
相应的UDP服务器端:
package main
import (
"fmt"
"net"
"os"
"time"
)
func main() {
service := ":1200"
udpAddr, err := net.ResolveUDPAddr("udp4", service)
checkError(err)
conn, err := net.ListenUDP("udp", udpAddr)
checkError(err)
for {
handleClient(conn)
}
}
func handleClient(conn *net.UDPConn) {
var buf [512]byte
_, addr, err := conn.ReadFromUDP(buf[0:])
if err != nil {
return
}
daytime := time.Now().String()
conn.WriteToUDP([]byte(daytime), addr)
}
func checkError(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal error %s", err.Error())
os.Exit(1)
}
}
WebSocket¶
WebSocket 分为客户端和服务端
WebSocket 采用了一些特殊的报头,使得浏览器和服务器只需要做一个握手的动作,就可以在浏览器和服务器之间建立一条连接通道。且此连接会保持在活动状态,你可以使用JavaScript来向连接写入或从中接收数据,就像在使用一个常规的TCP Socket一样。它解决了Web实时化的问题,相比传统HTTP有如下好处:
- 一个Web客户端只建立一个TCP连接
- Websocket服务端可以推送(push)数据到web客户端.
- 有更加轻量级的头,减少数据传送量
WebSocket URL的起始输入是ws://或是wss://(在SSL上)。
Go语言标准包里面没有提供对WebSocket的支持,但是在由官方维护的go.net子包中有对这个的支持,可以通过如下的命令获取该包:
go get golang.org/x/net/websocket
举个🌰:
需求:用户输入信息,客户端通过WebSocket将信息发送给服务器端,服务器端收到信息之后主动Push信息到客户端,然后客户端将输出其收到的信息。
客户端的代码如下:
<html>
<head></head>
<body>
<script type="text/javascript">
var sock = null;
var wsuri = "ws://127.0.0.1:1234";
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>
可以看到客户端JS,很容易的就通过WebSocket函数建立了一个与服务器的连接sock,当握手成功后,会触发WebSocket对象的onopen事件,告诉客户端连接已经成功建立。客户端一共绑定了四个事件。
- 1)onopen 建立连接后触发
- 2)onmessage 收到消息后触发
- 3)onerror 发生错误时触发
- 4)onclose 关闭连接时触发
我们服务器端的实现如下:
package main
import (
"golang.org/x/net/websocket"
"fmt"
"log"
"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("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.Handle("/", websocket.Handler(Echo))
if err := http.ListenAndServe(":1234", nil); err != nil {
log.Fatal("ListenAndServe:", err)
}
}
当客户端将用户输入的信息Send之后,服务器端通过Receive接收到了相应信息,然后通过Send发送了应答信息。
REST¶
- HTML标准只能通过链接和表单支持
GET
和POST
。在没有Ajax支持的网页浏览器中不能发出PUT
或DELETE
命令 - 有些防火墙会挡住HTTP
PUT
和DELETE
请求,要绕过这个限制,客户端需要把实际的PUT
和DELETE
请求通过 POST 请求穿透过来。RESTful 服务则要负责在收到的 POST 请求中找到原始的 HTTP 方法并还原。可以通过POST
里面增加隐藏字段_method
这种方式可以来模拟PUT
、DELETE
等方式,但是服务器端需要做转换。
举个🌰:
需求:我们访问的资源是用户,我们通过不同的method来访问不同的函数。
第三方库github.com/julienschmidt/httprouter
,这个库实现了自定义路由和方便的路由规则映射,通过它,我们可以很方便的实现REST的架构。
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))
}
通过上面的代码可知,REST就是根据不同的method访问同一个资源的时候实现不同的逻辑处理。