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))
第一点需要注意的是在进行一下步之前,进行错误检查。在网络编程中这很常见:相对于独立的程序而且,发生失败的机率会大大的提高。可能客户端,服务器,路由和交换机的硬件发生错误。或者通信被防火墙阻止, 网络超时。或者服务器和客户端通讯时,服务器宕机。所以需要执行以下检测
指定的地址语法上有错误
尝试连接远程服务器是发生失败。比如,服务没有运行,或者网络有没有这个主机
虽然连接建立成功,如果连接突然死亡或者超时,则向服务器写数据会失败
- 同样,在读取数据时,发生失败
在从服务器读取时,必须要求一个注释。 在这里,我们读取了单个响应。它将在连接中读取到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)
}
}