以前主要使用的是Linux C网络编程,现在学习了Golang,就总结一下Go中的socket编程,本文基于Go 1.7.4,和socket相关的代码在go/src/net/net.go
文件中。
网络协议模型回顾
我们先来简单回顾一下网络写协模型。说到网络协议一般就两个模型:概念意义上的OSI(Open Systems Interconnect)的七层协议和实际中使用的TCP/IP四层协议。
OSI七层网络模型:
从第一层到第七层分别是:物理层、数据链路层、网络层、传输层、会话层、表示层、应用层。
TCP/IP四层网络模型:
从第一层到第四层分别是:网络接口层、网间层、传输层、应用层。
最后用一张图来结束回顾:
完整版可下载:http://www.colasoft.com.cn/download/protocols_map.php
Go socket编程
首先需要明确一点,这里介绍socket编程的主要是面向三层和四层的协议,再具体点就是主要是针对IP协议、TCP、UDP的,像HTTP这种基于TCP的七层协议不在讨论范围内。
通用socket编程API
Go中socket编程的API都在net
包中。其实和C中类似,Go提供了Dial
函数来连接服务器,使用Listen
监听,Accept
接受连接,所以如果写过其他语言(尤其是POSIX标准的)的网络编程的话,其实Go的网络编程也主要就是熟悉一下API而已,所以我们就直入主题了。
Go socket编程中最常用的类型Conn
:
type Conn interface {
// Read reads data from the connection.
// Read can be made to time out and return an Error with Timeout() == true
// after a fixed time limit; see SetDeadline and SetReadDeadline.
Read(b []byte) (n int, err error)
// Write writes data to the connection.
// Write can be made to time out and return an Error with Timeout() == true
// after a fixed time limit; see SetDeadline and SetWriteDeadline.
Write(b []byte) (n int, err error)
// Close closes the connection.
// Any blocked Read or Write operations will be unblocked and return errors.
Close() error
// LocalAddr returns the local network address.
LocalAddr() Addr
// RemoteAddr returns the remote network address.
RemoteAddr() Addr
// SetDeadline sets the read and write deadlines associated
// with the connection. It is equivalent to calling both
// SetReadDeadline and SetWriteDeadline.
//
// A deadline is an absolute time after which I/O operations
// fail with a timeout (see type Error) instead of
// blocking. The deadline applies to all future and pending
// I/O, not just the immediately following call to Read or
// Write. After a deadline has been exceeded, the connection
// can be refreshed by setting a deadline in the future.
//
// An idle timeout can be implemented by repeatedly extending
// the deadline after successful Read or Write calls.
//
// A zero value for t means I/O operations will not time out.
SetDeadline(t time.Time) error
// SetReadDeadline sets the deadline for future Read calls
// and any currently-blocked Read call.
// A zero value for t means Read will not time out.
SetReadDeadline(t time.Time) error
// SetWriteDeadline sets the deadline for future Write calls
// and any currently-blocked Write call.
// Even if write times out, it may return n > 0, indicating that
// some of the data was successfully written.
// A zero value for t means Write will not time out.
SetWriteDeadline(t time.Time) error
}
Conn
是一个接口,在Go中代表一个普通的面向流的网络连接。该接口定义了8个方法,基本上都是最基础的网络操作函数,后面我们会看到TCP和UDP分别实现了该接口,并且加入了一些自己的特性。需要注意的是,不同的goroutine是可以并发的调用同一个Conn
的方法的。
再看Dial
函数:
func Dial(network, address string) (Conn, error)
如前面所述,该函数用于连接服务器。
network
:用于指定网络类型,目前支持的值有:tcp
、tcp4
(IPv4-only)、tcp6
(IPv6-only)、udp
、udp4
(IPv4-only)、udp6
(IPv6-only)、ip
、ip4
(IPv4-only)、ip6
(IPv6-only)、unix
、unixgram
、unixpacket
。address
:指定要连接的地址,对于TCP和UDP来说,地址格式为host:port
。对于IPv6因为地址中已经有冒号了,所以需要用中括号将IP地址括起来,比如[::1]:80
。如果省略掉host
的话,比如:80
,就认为是本地系统。
一些例子:
Dial("tcp", "192.0.2.1:80")
Dial("tcp", "golang.org:http")
Dial("tcp", "[2001:db8::1]:http")
Dial("tcp", "[fe80::1%lo0]:80")
Dial("tcp", ":80")
如果是IP网络的话,network
必须是ip
、ip4
或ip6
,且必须在后面加上冒号说明协议号或者名字,比如:
Dial("ip4:1", "192.0.2.1")
Dial("ip6:ipv6-icmp", "2001:db8::1")
如果address
对应多个地址(比如可能是一个域名对用多个IP),那么Dial
会挨个尝试直到成功连上某一个。
除了Dial
,还有一个DialTimeout
多提供了一个超时的功能:
func DialTimeout(network, address string, timeout time.Duration) (Conn, error)
OK,了解了Conn
和Dial
我们就可以写一个小的程序来模拟HTTP中的HEAD方法了。
Example 1
main.go:
package main
import (
"fmt"
"io/ioutil"
"net"
)
func main() {
conn, err := net.Dial("tcp", "time-track.cn:80")
checkErr(err)
_, err = conn.Write([]byte("HEAD / HTTP/1.0\r\n\r\n"))
checkErr(err)
result, err := ioutil.ReadAll(conn)
checkErr(err)
fmt.Println(string(result))
}
func checkErr(err error) {
if err != nil {
panic(err)
}
}
运行go run main.go
:
HTTP/1.1 200 OK
Cache-Control: private
Content-Length: 1452
Content-Type: text/html
Server: Microsoft-IIS/8.5
Set-Cookie: CookieZJWFANGDAOLIAN=113.200.50.42#2017-03-07-14#111.67.196.107; expires=07-03-2017 23:59:59; path=/
Set-Cookie: ASPSESSIONIDQCADSRQC=GAOJKJCDPPGPDFNIABBDGCNI; path=/
X-Powered-By: ASP.NET
接下来我们看Listen
和Accept
:
type Listener interface {
// Accept waits for and returns the next connection to the listener.
Accept() (Conn, error)
// Close closes the listener.
// Any blocked Accept operations will be unblocked and return errors.
Close() error
// Addr returns the listener's network address.
Addr() Addr
}
func Listen(net, laddr string) (Listener, error)
Listener
接口定义了普通的面向流的监听器,不同的goroutine可以并发的调用同一个监听器的方法。
Listen
函数创建一个监听,
net
:网络类型必须是面向流的网络,目前可选值为tcp
、tcp4
、tcp6
、unix
、unixpacket
。laddr
:指定监听本地哪些网络接口,对于TCP和UDP,格式为host:port
。如果省略host
,表示监听所有本地地址。
现在我们来写一个简单的echo服务器程序:
Example 2:
package main
import (
"io"
"log"
"net"
)
func main() {
l, err := net.Listen("tcp", ":2000")
checkErr(err)
defer l.Close()
for {
conn, err := l.Accept()
if err != nil {
log.Fatal(err)
continue
}
go func(c net.Conn) {
io.Copy(c, c)
c.Close()
}(conn)
}
}
func checkErr(err error) {
if err != nil {
panic(err)
}
}
这里我们对于每个客户端的请求,都分配一个goroutine去处理,这样服务端就不会阻塞在某一个客户端上了,这和C中的fork机制是一样的。
所以,掌握了Conn
、Dial
、Listen
、Accept
等API,我们就可以在简单的Go socket编程了。上面我们也看到,这些API一般都有一个参数用来指定具体的网络类型,比如是IP还是TCP,亦或是UDP、UNIX套接字等,也就是说它将多种不同的协议都整合到了同一些API之中,记忆和使用起来也比较方便,。当然,针对每种具体的协议,也有特定的API,其实质也就是实现了诸如Conn
等接口,当然通用的socket API也实现了这些接口(比如conn
类型就是Conn
接口的通用实现),只不过只是实现了一些比较通用的方法等,但是特定协议的socket API会针对本协议多提供一些方法等。Go推荐使用通用的API进行socket编程,而不是具体某种协议的socket API,除非我们需要使用某些特定的方法。当然,即使要使用也可以使用类型转换等。下面我们分别对各种协议特定的socket API进行简单的介绍,主要目的是熟悉一下类型定义和API。
IP协议的API
Go用IP
类型表示一个IP地址(IPv4和IPv6):
type IP []byte
func ParseIP(s string) IP
一般我们通过上面列的ParseIP
函数将一个合法的string类型(比如"192.0.2.1"
、"2001:db8::68"
等)的IP转换为IP
类型的IP。该类型也提供了许多方法用来操作对IP地址进行操作。
Go用IPAddr
类型来表示一个IP端:
type IPAddr struct {
IP IP
Zone string // IPv6 scoped addressing zone
}
func ResolveIPAddr(net, addr string) (*IPAddr, error)
func (a *IPAddr) Network() string
func (a *IPAddr) String() string
一般我们使用ResolveIPAddr
来获取一个IPAddr
类型的指针:
net
:指明IP网络,可选值为"ip", "ip4", "ip6"。addr
:指定网络地址,可以是域名、IP等。
后面我们会看到TCP会有一个ResolveTCPAddr
,UDP会有一个ResolveUDPAddr
,UNIX套接字会有一个ResolveUnixAddr
,其用法和作用其实都是类似的。IPAddr
只有两个方法。
IP协议中IPConn
实现了Conn
接口(同时也实现了PacketConn
接口),用来表示一个IP连接:
type IPConn struct {
// contains filtered or unexported fields
}
func DialIP(netProto string, laddr, raddr *IPAddr) (*IPConn, error)
func ListenIP(netProto string, laddr *IPAddr) (*IPConn, error)
// 方法省略
最后再介绍两个和IP相关的类型IPMask
和IPNet
,分别代表IP掩码(实质也是IP)和一个IP网络,其实就是我们日常说的子网掩码和子网:
type IPMask []byte
type IPNet struct {
IP IP // network number
Mask IPMask // network mask
}
总结,其实如前面所说,每种具体的协议其实都是在通用的协议实现的基础上又增加了一些本协议特有的东西,在API层面很多时候就是增加了一些前后缀,然后新增了一些特有的API而已。所以后面的几种协议的介绍会更加精简。最后以一个简单实现的ping作为例子,ping使用的是ICMP协议,协议格式为:
- 第1个字节是8,表名是echo消息
- 第2个字节是0
- 第3、4个字节是整个消息的校验和
- 第5、6个字节是任意标识符
- 第7、8个字节是任意的序列号
- 剩余部分是用户数据。
package main
import (
"os"
"fmt"
"net"
)
func main() {
if len(os.Args) != 2 {
fmt.Println("Usage: ", os.Args[0], "host")
os.Exit(1)
}
addr, err := net.ResolveIPAddr("ip", os.Args[1])
if err != nil {
fmt.Println("Resolution error", err.Error())
os.Exit(1)
}
conn, err := net.DialIP("ip4:icmp", addr, addr)
checkErr(err)
var msg [512]byte
msg[0] = 8 // echo
msg[1] = 0 // code 0
msg[2] = 0 // checksum
msg[3] = 0 // checksum
msg[4] = 0 // identifier[0]
msg[5] = 13 // identifier[1]
msg[6] = 0 // sequence[0]
msg[7] = 37 // sequence[1]
len := 8
check := checkSum(msg[0:len])
msg[2] = byte(check >> 8)
msg[3] = byte(check & 255)
_, err = conn.Write(msg[0:len])
checkErr(err)
_, err = conn.Read(msg[0:])
checkErr(err)
fmt.Println("Got response")
if msg[5] == 13 {
fmt.Println("identifier matches")
}
if msg[7] == 37 {
fmt.Println("Sequence matches")
}
os.Exit(0)
}
func checkSum(msg []byte) uint16 {
sum := 0
// assume even for now
for n := 1; n < len(msg) - 1; n += 2 {
sum += int(msg[n])*256 + int(msg[n+1])
}
sum = (sum >> 16) + (sum & 0xffff)
sum += (sum >> 16)
var answer uint16 = uint16(^sum)
return answer
}
func checkErr(err error) {
if err != nil {
//fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
//os.Exit(1)
panic(err)
}
}
NB:运行这个程序需要root权限。
TCP协议的API
TCPAddr
类型表示一个TCP连接端:
type TCPAddr struct {
IP IP
Port int
Zone string // IPv6 scoped addressing zone
}
// 使用该函数获取一个*TCPAddr
func ResolveTCPAddr(net, addr string) (*TCPAddr, error)
TCPConn
类型表示一个TCP连接:
type TCPConn struct {
// contains filtered or unexported fields
}
func DialTCP(net string, laddr, raddr *TCPAddr) (*TCPConn, error)
TCPListener
类型表示一个TCP监听:
type TCPListener struct {
// contains filtered or unexported fields
}
func ListenTCP(net string, laddr *TCPAddr) (*TCPListener, error)
最后我们实现一个简单的时间服务器作为示例:
package main
import (
"net"
"time"
)
func main() {
service := ":2000"
tcpAddr, err := net.ResolveTCPAddr("tcp", service)
checkErr(err)
listener, err := net.ListenTCP("tcp", tcpAddr)
checkErr(err)
for {
conn, err := listener.Accept()
if err != nil {
continue
}
daytime := time.Now().String()
conn.Write([]byte(daytime))
conn.Close()
}
}
func checkErr(err error) {
if err != nil {
panic(err)
}
}
UDP协议的API
UDPAddr
类型表示一个UDP端:
type UDPAddr struct {
IP IP
Port int
Zone string // IPv6 scoped addressing zone
}
// 获取一个*UDPAddr
func ResolveUDPAddr(net, addr string) (*UDPAddr, error)
UDPConn
类型表示一个UDP连接:
type UDPConn struct {
// contains filtered or unexported fields
}
func DialUDP(net string, laddr, raddr *UDPAddr) (*UDPConn, error)
func ListenMulticastUDP(network string, ifi *Interface, gaddr *UDPAddr) (*UDPConn, error)
func ListenUDP(net string, laddr *UDPAddr) (*UDPConn, error)
最后我们再用UDP实现一下上一节的时间服务器:
package main
import (
"net"
"time"
)
func main() {
service := ":2000"
udpAddr, err := net.ResolveUDPAddr("udp4", service)
checkErr(err)
conn, err := net.ListenUDP("udp", udpAddr)
checkErr(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 checkErr(err error) {
if err != nil {
panic(err)
}
}
UNIX套接字的API
UnixAddr
类型代表一个UNIX套接字端:
type UnixAddr struct {
Name string
Net string
}
func ResolveUnixAddr(net, addr string) (*UnixAddr, error)
UnixConn
类型代表一个UNIX套接字连接:
type UnixConn struct {
// contains filtered or unexported fields
}
func DialUnix(net string, laddr, raddr *UnixAddr) (*UnixConn, error)
func ListenUnixgram(net string, laddr *UnixAddr) (*UnixConn, error)
UnixListener
类型代表一个UNIX监听:
type UnixListener struct {
// contains filtered or unexported fields
}
func ListenUnix(net string, laddr *UnixAddr) (*UnixListener, error)
最后再用Unix socket实现一个echo servier:
package main
import (
"net"
"io"
)
func main() {
unixAddr, err := net.ResolveUnixAddr("unix", "/tmp/echo.sock")
checkErr(err)
l, err := net.ListenUnix("unix", unixAddr)
checkErr(err)
for {
conn, err := l.Accept()
if err != nil {
continue
}
go func(c net.Conn) {
io.Copy(c, c)
c.Close()
}(conn)
}
}
func checkErr(err error) {
if err != nil {
panic(err)
}
}
感觉用Go写网络程序比C简单一些,不需要记很多结构体等。最重要的是跨平台...感觉已经不想再写C了...
参考:
- Go官方文档及源码。
- Network Programming with Go.
评论已关闭