Services

服务运行在主机上,它们通常长期存在,并且等待请求和响应它们。这里有许多服务的类型,并且以不同的方式向客户端提供服务。在互联网中,许多的服务通信采用TCP和UDP, 虽然也有其它的协议,比如SCTP。其它的服务类型,比如peer-to-peer, remote procedure calls, 通信代理等都是建立在TCP和UDP的基础之上。

Ports

服务是存在于主机上的,IP地址用于查找主机。但每一台电脑可能有很多的服务,所以需要一种简单的方法来区分它们。通常使用TCP, UDP, SCTP,和端口。端口从1到65,535,每一种服务分配一个或者多个端口。

这里有许多标准端口,Telnet服务使用23和TCP协议, DNS使用53,TCP或者UDP。FTP使用21或者20,一个用于命令,一个端口用于数据传输。HTTP通常使用80,但也经常使用8000, 8080, 8088, 协议为TCP.

在Unix系统上,常用的端口在/etc/services中。Go使用以下这个函数来遍历这个文件

func LookupPort(network, service string) (port int, err os.Error)

network 参数的字符串为 "tcp" or "udp", 而service为"telnet" or "domain"(for DNS)

networkType := os.Args[1]
service := os.Args[2]
port, err := net.LookupPort(networkType, service)
if err != nil {
fmt.Println("Error: ", err.Error())
os.Exit(2)
}
fmt.Println("Service port ", port)
os.Exit(0)
//LookupPort tcp telnet
//Service port: 23

The type TCPAddr

TCPAddr是一个structure, 包含IP和一个端口

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

以下这个函数创建一个TCPAddr

func ResolveTCPAddr(net, addr string) (*TCPAddr, error)

ResolveTCPAddr 将addr字符串解析为一个TCP地址,TCP的形式为"host:port"or "[ipv6-host%zone]:port"。net参数为"tcp", "tcp4" or "tcp6", 而addr是一个域名或IP地址,和端口的键值对,比如www.google.com:80 or 127.0.0:22,IPv6的话必须是在中括号内, 比如[::1]:80", "[ipv6-host]:http" or "[ipv6-host%zone]:80".

TCP Sockets

当你知道如何通过网络和port IDs抵达一个服务,然后怎么办呢? 如果你是客户端,你需要一个API, 它允许你连接到这个服务,然后给服务发送message 并从service中读取返回的信息。

如果你是服务器端,你需要绑定端口,并且监听这个端口。当有一个消息进来时,你需要读取并且给客户端写返回信息。

net.TCPConn是一个Go的类型,它允许在客户端和服务器端进行全双工通信。它实现了Conn接口,用于TCP网络的连接. 以下是它两个重要的方法

func (c *TCPConn) Write(b []byte) (n int, err os.Error)
func (c *TCPConn) Read(b []byte) (n int, err os.Error)

TCPConn可以用于客户端,也可以用于服务器端读写message.

TCP Client

客户端需要"dials"(拨通)一个service, 如果成功,dial返回一个TCPConn用于通信。客户端和服务器就可以在TCPConn上进行消息交换。通常客户端发送给服务器的请求,是向TCPConn中写入,而从服务器获得返回,则是读取TCPConn. 这个过程直到双方中的一端关闭,而结束。以下是这个函数的签名

func DialTCP(net string, laddr, raddr *TCPAddr) (c *TCPConn, err os.Error)

laddr是本地的地址,通常设置为nil, raddr是远程服务的地址,net参数是"tcp4" "tcp6" or "tcp", 这依赖于你自己想要的是TCPv4,或者TCPv6,还是无所谓。

一个简单的例子是连接到web服务器(HTTP). 我们将在之后的章节中讨论更多的HTTP客户端和服务器的细节, 但现在我们只是简单的看看

Client发送的第一个消息是一个"HEAD" message. 它用于查询一个服务器的信息和在这个服务器上的文档。服务器向它返回消息,但不会返回文档。以下是一个请求查询HTTP的服务器的消息

"HEAD / HTTP/1.0\r\n\r\n"

它将请求一个root文档和服务器的信息,通常返回如下的消息

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

以下是实际的例子

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 := readFully(conn)
result, err := ioutil.ReadAll(conn)
checkError(err)
fmt.Println(string(result))

第一点需要注意的是在进行一下步之前,进行错误检查。在网络编程中这很常见:相对于独立的程序而且,发生失败的机率会大大的提高。可能客户端,服务器,路由和交换机的硬件发生错误。或者通信被防火墙阻止, 网络超时。或者服务器和客户端通讯时,服务器宕机。所以需要执行以下检测

  1. 指定的地址语法上有错误

  2. 尝试连接远程服务器是发生失败。比如,服务没有运行,或者网络有没有这个主机

  3. 虽然连接建立成功,如果连接突然死亡或者超时,则向服务器写数据会失败

  4. 同样,在读取数据时,发生失败

在从服务器读取时,必须要求一个注释。 在这里,我们读取了单个响应。它将在连接中读取到EOF时终止。可是一个响应由多个TCP packets组成,所以我们需要它一直在读取,直到读取到end of file. io/ioutil包中的ReadAll将会处理这些问题,并返回完整的响应。

这里有一些涉及到语言的问题。首先,许多函数返回双值,通常第二个为error. 如果没有error发生,则为nil. 在C语言中,相同的行为是获得返回的值是否为NULL, -1, zeor. 在Java中,相同错误的检查是通过throwing和catching异常来管理。

A Daytime server

一个最简单的服务是建立一个daytime service. 这是一个标准的互联网服务,定义在RFC867, 默认端口为13,可以是TCP,也可是UDP。

一个服务首先要注册一个端口和监听这个端口。然后它会被阻塞在一个"accept"操作上,等待客户端的连接。当一个客户端连接后,Accept()被调用,并返回一个连接对象。 daytime service非常简单,只是返回当前的时间给客户端, 然后关闭连接,恢复等待下一个客户端。

相关的调用如下

func ListenTCP(net string, laddr *TCPAddr) (l *TCPListener, err os.Error)
func (l *TCPListener) Accept() (c Conn, err os.Error)

net参数可以为"tcp", "tcp4", "tcp6", IP地址可以设置为0,那么它将监听网络中的所有的接口,如果你只想监听单个的网络接口,IP地址则指定为单个网络接口。如果port设置为0,操作系统会为你选择一个端口。需要注意的是,在Unix系统中,你不能监听1024以下的端口,除非你是系统管理程序,128位以下是标准的IETF端口。

service := ":1200"
tcpAddr, err := net.ResolveTCPAddr("ip4", service)
checkError(err)
listener, err := net.ListenTCP("tcp", tcpAddr)
checkError(err)
for {
conn, err := listener.Accept()
if err != nil {
continue
}
daytime := time.Now().String()
conn.Write([]byte(daytime)) // don't care about return value
conn.Close() // we're finished with this client
}
}
//telnet localhost 1200

Multi-thread server

"echo"是另一个简单的IETF服务。它仅读取客户端的类型,并且将它发送回来

func main() {
service := ":1201"
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
}
handleClient(conn)
conn.Close() // we're finished
}
}
func handleClient(conn net.Conn) {
var buf [512]byte
for {
n, err := conn.Read(buf[0:])
if err != nil {
return
}
fmt.Println(string(buf[0:]))
_, err2 := conn.Write(buf[0:n])
if err2 != nil {
return
}
}
}
func checkError(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
os.Exit(1)
}
}

虽然这个程序可以工作,但它是单线程的,会一直处于读取用于输入状态。所以其它的客户端就不能连接。所以我们需要使用go-routine, 同时将连接的关闭移动到handler中。

func main() {
service := ":1201"
tcpAddr, err := net.ResolveTCPAddr("ip4", service)
checkError(err)
listener, err := net.ListenTCP("tcp", tcpAddr)
checkError(err)
for {
conn, err := listener.Accept()
if err != nil {
continue
}
// run as a goroutine
go handleClient(conn)
}
}

func handleClient(conn net.Conn) {
// close connection on exit
defer conn.Close()
var buf [512]byte
for {
// read upto 512 bytes
n, err := conn.Read(buf[0:])
if err != nil {
return
}
// write the n bytes read
_, err2 := conn.Write(buf[0:n])
if err2 != nil {
return
}
}
}

控制TCP连接

Timeout

服务器可能会希望客户端没有足够的响应时,设置超时。比如,没有及时向服务器写一个请求。这可能是一个很长的周期(several minutes)。因为用户可以从容的操作其它事情。相反,客户端也可以等待服务器超时,在服务器多久没有反应后,终止。

func (c *TCPConn) SetTimeout(nsec int64) os.Error

Staying alive

一个客户端希望可以保留它的连接状态,即使它现在没有任何东西可以发送

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

还有很多其它的连接控制的方法,文档在"net" package

UDP Datagrams

在无连接的协议中,每条message信息包含了源(origin)和目标(destination).它们不会使用套接字socket建立会话。UDP clients和servers使用数据报,数据报的每个message都包含源和目的地的信息,这些信息都是没有状态维护的,除非在客户端或者服务器来做状态维护. 这些message不会被保证会到达目的地,或者按顺序到达

客户端最常见的情况是发送一个message,并且等待一个回复。服务器最常的情况是可以接受一个message,然后发送一个或者多个回复给客户端。在一个peer-to-peer的情况下(点到点), 服务器可能仅仅是向其它的节点向前传递message.

在Go语言中,处理TCP和UDP最大的不同在于如何处理来自多个客户端的packet, UDP没有TCP的session缓冲来管理这些事情。以下是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)
//UDPDaytimeClient
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"))

    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)
    }
}
//UDPDaytimeServer.go

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)

    }

}