网络编程是go语言使用的一个核心模块。golang的网络封装使用对于底层socket或者上层的http,甚至是web服务都很友好。

net

net包提供了可移植的网络I/O接口,包括TCP/IP、UDP、域名解析和Unix域socket等方式的通信。其中每一种通信方式都使用 xxConn 结构体来表示,诸如IPConn、TCPConn等,这些结构体都实现了Conn接口,Conn接口实现了基本的读、写、关闭、获取远程和本地地址、设置timeout等功能。

conn的接口定义

type Conn interface {
    // Read从连接中读取数据
    // Read方法可能会在超过某个固定时间限制后超时返回错误,该错误的Timeout()方法返回真
    Read(b []byte) (n int, err error)
    // Write从连接中写入数据
    // Write方法可能会在超过某个固定时间限制后超时返回错误,该错误的Timeout()方法返回真
    Write(b []byte) (n int, err error)
    // Close方法关闭该连接
    // 并会导致任何阻塞中的Read或Write方法不再阻塞并返回错误
    Close() error
    // 返回本地网络地址
    LocalAddr() Addr
    // 返回远端网络地址
    RemoteAddr() Addr
    // 设定该连接的读写deadline,等价于同时调用SetReadDeadline和SetWriteDeadline
    // deadline是一个绝对时间,超过该时间后I/O操作就会直接因超时失败返回而不会阻塞
    // deadline对之后的所有I/O操作都起效,而不仅仅是下一次的读或写操作
    // 参数t为零值表示不设置期限
    SetDeadline(t time.Time) error
    // 设定该连接的读操作deadline,参数t为零值表示不设置期限
    SetReadDeadline(t time.Time) error
    // 设定该连接的写操作deadline,参数t为零值表示不设置期限
    // 即使写入超时,返回值n也可能>0,说明成功写入了部分数据
    SetWriteDeadline(t time.Time) error
}

然后每种类型都是对应的结构体实现这些接口。

还有一个常用的接口定义PacketConn

type PacketConn interface {
    // ReadFrom方法从连接读取一个数据包,并将有效信息写入b
    // ReadFrom方法可能会在超过某个固定时间限制后超时返回错误,该错误的Timeout()方法返回真
    // 返回写入的字节数和该数据包的来源地址
    ReadFrom(b []byte) (n int, addr Addr, err error)
    // WriteTo方法将有效数据b写入一个数据包发送给addr
    // WriteTo方法可能会在超过某个固定时间限制后超时返回错误,该错误的Timeout()方法返回真
    // 在面向数据包的连接中,写入超时非常罕见
    WriteTo(b []byte, addr Addr) (n int, err error)
    // Close方法关闭该连接
    // 会导致任何阻塞中的ReadFrom或WriteTo方法不再阻塞并返回错误
    Close() error
    // 返回本地网络地址
    LocalAddr() Addr
    // 设定该连接的读写deadline
    SetDeadline(t time.Time) error
    // 设定该连接的读操作deadline,参数t为零值表示不设置期限
    // 如果时间到达deadline,读操作就会直接因超时失败返回而不会阻塞
    SetReadDeadline(t time.Time) error
    // 设定该连接的写操作deadline,参数t为零值表示不设置期限
    // 如果时间到达deadline,写操作就会直接因超时失败返回而不会阻塞
    // 即使写入超时,返回值n也可能>0,说明成功写入了部分数据
    SetWriteDeadline(t time.Time) error
}

ip

使用IPConn结构体来表示,它实现了Conn、PacketConn两种接口。使用如下两个函数进行Dial(连接)和Listen(监听)。

func DialIP(netProto string, laddr, raddr *IPAddr) (*IPConn, error)   

DialIP在网络协议netProto上连接本地地址laddr和远端地址raddr,netProto必须是”ip”、”ip4”或”ip6”后跟冒号和协议名或协议号。

func ListenIP(netProto string, laddr *IPAddr) (*IPConn, error)

ListenIP创建一个接收目的地是本地地址laddr的IP数据包的网络连接,返回的*IPConn的ReadFrom和WriteTo方法可以用来发送和接收IP数据包。(每个包都可获取来源址或者设置目标地址)

类型

1、IPAddr类型

位于iprawsock.go中在net包的许多函数和方法会返回一个指向IPAddr的指针。这不过只是一个包含IP类型的结构体。

type IPAddr struct {
    IP   IP
}

这个类型的另一个主要用途是通过IP主机名执行DNS查找。

ResolveIPAddr
ResolveIPAddr有两个参数第一个参数.必须为"ip","ip4","ip6",第二个参数多为要解析的域名.返回一个IPAddr的指针类型

addr, _ := net.ResolveIPAddr("ip", "www.baidu.com")
fmt.Println(addr)

ip.go 中还定义了三个类型.分别是IP,IPMask,IPNet

2、IP类型

type IP []byte

IP类型被定义为一个字节数组。 ParseIP(String) 可以将字符窜转换为一个IP类型.

name := "127.0.0.1"
addr := net.ParseIP(name)
fmt.Println(addr.IsLoopback())// IsLoopback reports whether ip is a loopback address.

3、IPMask类型

// An IP mask is an IP address.
type IPMask []byte

一个掩码的字符串形式是一个十六进制数,如掩码255.255.0.0为ffff0000。

func IPv4Mask(a, b, c, d byte) IPMask :用一个4字节的IPv4地址来创建一个掩码.
func CIDRMask(ones, bits int) IPMask : 用ones和bits来创建一个掩码

4、IPNet类型

// An IPNet represents an IP network.
type IPNet struct {
    IP   IP     // network number
    Mask IPMask // network mask
}

由IP类型和IPMask组成一个网段,其字符串形式是CIDR地址,如:“192.168.100.1/24”或“2001:DB8::/ 48”

func main() {
    mask := net.IPv4Mask(byte(255), byte(255), byte(255), byte(0))
    ip := net.ParseIP("192.168.1.125").Mask(mask)
    in := &net.IPNet{ip, mask}
    fmt.Println(in)         //  192.168.1.0/24
}

实例

这边插播一个经常使用的实例:获取本地IP

package main
import (
    "fmt"
    "net"
    "os"
)
func main() {
    addrs, err := net.InterfaceAddrs()
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    for _, address := range addrs {
        // 检查ip地址判断是否回环地址
        if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
            if ipnet.IP.To4() != nil {
                fmt.Println(ipnet.IP.String())
            }
        }
    }
}

tcp

使用TCPConn结构体来表示,它实现了Conn接口。

使用DialTCP进行Dial操作:

func DialTCP(net string, laddr, raddr *TCPAddr) (*TCPConn, error)

DialTCP在网络协议net上连接本地地址laddr和远端地址raddr。net必须是”tcp”、”tcp4”、”tcp6”;如果laddr不是nil,将使用它作为本地地址,否则自动选择一个本地地址。

func ListenTCP(net string, laddr *TCPAddr) (*TCPListener, error)

使用 ListenTCP函数进行Listen,产生一个TCPListener结构体,使用TCPListener的AcceptTCP方法建立通信链路,得到TCPConn。

TCPAddr类型

位于tcpsock.go中TCPAddr类型包含一个IP和一个port的结构:

type TCPAddr struct {
    IP   IP
    Port int
}

ResolveTCPAddr

func ResolveTCPAddr(net, addr string) (*TCPAddr, os.Error) 

该函数用来创建一个TCPAddr,第一个参数为,tcp,tcp4或者tcp6,addr是一个字符串,由主机名或IP地址,以及”:“后跟随着端口号组成,例如: “www.google.com:80” 或 ‘127.0.0.1:22”。如果地址是一个IPv6地址,由于已经有冒号,主机部分,必须放在方括号内, 例如:”[::1]:23”. 另一种特殊情况是经常用于服务器, 主机地址为0, 因此,TCP地址实际上就是端口名称, 例如:”:80” 用来表示HTTP服务器。

addr, _ := net.ResolveTCPAddr("tcp", "www.baidu.com:80")
fmt.Println(addr)   //220.181.111.147:80

udp

使用UDPConn接口体来表示,它实现了Conn、PacketConn两种接口。使用如下两个函数进行Dial和Listen。

func DialUDP(net string, laddr, raddr *UDPAddr) (*UDPConn, error)    

DialTCP在网络协议net上连接本地地址laddr和远端地址raddr。net必须是”udp”、”udp4”、”udp6”;如果laddr不是nil,将使用它作为本地地址,否则自动选择一个本地地址。

func ListenUDP(net string, laddr *UDPAddr) (*UDPConn, error)

ListenUDP创建一个接收目的地是本地地址laddr的UDP数据包的网络连接。net必须是”udp”、”udp4”、”udp6”;如果laddr端口为0,函数将选择一个当前可用的端口,可以用Listener的Addr方法获得该端口。返回的*UDPConn的ReadFrom和WriteTo方法可以用来发送和接收UDP数据包(每个包都可获得来源地址或设置目标地址)。

类型

1、UDPAddr类型

type UDPAddr struct {
    IP   IP
    Port int
}

ResolveUDPAddr同样的功能

2、UnixAddr类型

type UnixAddr struct {
    Name string
    Net  string
}

ResolveUnixAddr同样的功能

unix

UnixConn实现了Conn、PacketConn两种接口,其中unix又分为SOCK_DGRAM、SOCK_STREAM。

1.对于unix(SOCK_DGRAM),使用如下两个函数进行Dial和Listen。

func DialUnix(net string, laddr, raddr *UnixAddr) (*UnixConn, error)    

func ListenUnixgram(net string, laddr *UnixAddr) (*UnixConn, error)

2.对于unix(SOCK_STREAM)

客户端使用DialUnix进行Dial操作

func DialUnix(net string, laddr, raddr *UnixAddr) (*UnixConn, error)   

服务端使用ListenUnix函数进行Listen操作,然后使用UnixListener进行AcceptUnix

func ListenUnix(net string, laddr *UnixAddr) (*UnixListener, error)

函数整合

为了使用方便,golang将上面一些重复的操作集中到一个函数中。在参数中制定上面不同协议类型。

func ListenPacket(net, laddr string) (PacketConn, error) 

这个函数用于侦听ip、udp、unix(DGRAM)等协议,返回一个PacketConn接口,同样根据侦听的协议不同,这个接口可以包含IPCon、UDPConn、UnixConn等,它们都实现了PacketConn。可以发现与ip、unix(stream)协议不同,直接返回的是xxConn,不是间接的通过Listener进行Accept操作后,才得到一个Conn。

func Listen(net, laddr string) (Listener, error)

这个函数用于侦听tcp、unix(stream)等协议,返回一个Listener接口、根据侦听的协议不同,这个接口可以包含TCPListener、UnixListener等,它们都实现了Listener接口,然后通过调用其Accept方法可以得到Conn接口,进行通信。

func Dial(network, address string) (Conn, error)

这个函数对于所有的协议都是相同的操作,返回一个Conn接口,根据协议的不同实际上包含IPConn、UDPConn、UnixConn、IPConn,它们都实现了Conn接口

基本c/s功能

在 Unix/Linux 中的 Socket 编程主要通过调用 listen, accept, write read 等函数来实现的. 具体如下图所示:

服务端

服务端listen, accept

func connHandler(c net.Conn) {
    for {
        cnt, err := c.Read(buf)
        c.Write(buf)
    }
}
func main() {
    server, err := net.Listen("tcp", ":1208")
    for {
        conn, err := server.Accept()
        go connHandler(conn)
    }
}

直接使用net的listen返回的就是对应协议已经定义好的结构体,比如tcp

type TCPListener struct {
    fd *netFD
}

这个结构体实现了listener接口的所有接口,所以可以作为返回值返回。其他协议类型也是一样。

accept后返回的conn是一个存储着连接信息的结构体

// Network file descriptor.
type netFD struct {
    pfd poll.FD

    // immutable until Close
    family      int
    sotype      int
    isConnected bool // handshake completed or use of association with peer
    net         string
    laddr       Addr
    raddr       Addr
}

客户端

客户端dial

func connHandler(c net.Conn) {
    for {
        c.Write(...)
        c.Read(...)
    }
}
func main() {
    conn, err := net.Dial("tcp", "localhost:1208")
    connHandler(conn)
}

主要函数

func Dial(net, addr string) (Conn, error)

其中net参数是网络协议的名字, addr参数是IP地址或域名,而端口号以“:”的形式跟随在地址 或域名的后面,端口号可选。如果连接成功,返回连接对象,否则返回error。

Dial() 函数支持如下几种网络协议:

"tcp" 、 "tcp4" (仅限IPv4)、 "tcp6" (仅限IPv6)、 "udp" 、 "udp4"(仅限IPv4)、 "udp6"(仅限IPv6)、 "ip" 、 "ip4"(仅限IPv4)和"ip6"(仅限IPv6)。

可以直接用相关协议的函数

func DialTCP(net string, laddr, raddr *TCPAddr) (c *TCPConn, err error)
func DialUDP(net string, laddr, raddr *UDPAddr) (c *UDPConn, err error)
func DialIP(netProto string, laddr, raddr *IPAddr) (*IPConn, error)
func DialUnix(net string, laddr, raddr *UnixAddr) (c *UnixConn, err error)

特性功能

1、控制TCP连接

TCP连接有很多控制函数,我们平常用到比较多的有如下几个函数:

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

第一个函数用来设置超时时间,客户端和服务器端都适用,当超过设置的时间时那么该链接就失效。

第二个函数用来设置客户端是否和服务器端一直保持着连接,即使没有任何的数据发送

实例

从零开始写Socket Server: Socket-Client框架

在golang中,网络协议已经被封装的非常完好了,想要写一个Socket的Server,我们并不用像其他语言那样需要为socket、bind、listen、receive等一系列操作头疼,只要使用Golang中自带的net包即可很方便的完成连接等操作~

在这里,给出一个最最基础的基于Socket的Server的写法:

package main
import (
    "fmt"
    "net"
    "log"
    "os"
)


func main() {

//建立socket,监听端口
    netListen, err := net.Listen("tcp", "localhost:1024")
    CheckError(err)
    defer netListen.Close()

    Log("Waiting for clients")
    for {
        conn, err := netListen.Accept()
        if err != nil {
            continue
        }

        Log(conn.RemoteAddr().String(), " tcp connect success")
        handleConnection(conn)
    }
}
//处理连接
func handleConnection(conn net.Conn) {

    buffer := make([]byte, 2048)

    for {

        n, err := conn.Read(buffer)

        if err != nil {
            Log(conn.RemoteAddr().String(), " connection error: ", err)
            return
        }


        Log(conn.RemoteAddr().String(), "receive data string:\n", string(buffer[:n]))

    }

}
func Log(v ...interface{}) {
    log.Println(v...)
}

func CheckError(err error) {
    if err != nil {
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
        os.Exit(1)
    }
}

唔,抛除Go语言里面10行代码有5行error的蛋疼之处,你可以看到,Server想要建立并接受一个Socket,其核心流程就是

netListen, err := net.Listen("tcp", "localhost:1024")
conn, err := netListen.Accept()
n, err := conn.Read(buffer)

这三步,通过Listen、Accept 和Read,我们就成功的绑定了一个端口,并能够读取从该端口传来的内容~

这边插播一个内容,关于read是阻塞的,如果读取不到内容,代码会阻塞在这边,直到有内容可以读取,包括connection断掉返回的io.EOF,一般对这个都有特殊处理。一般重conn读取数据也是在for循环中。

package main

import (
    "fmt"
    "io"
    "net"
)

func main(){
    ln, err := net.Listen("tcp","127.0.0.1:10051")

    if err != nil {
        panic(err)
    }

    for {
        conn, _ := ln.Accept() //The loop will be held here
        fmt.Println("get connect")
        go handleread(conn)


    }
}

func handleread(conn net.Conn){
    defer conn.Close()

    var tatalBuffer  []byte
    var all int
    for {
        buffer := make([]byte, 2)
        n,err := conn.Read(buffer)
        if err == io.EOF{
            fmt.Println(err,n)
            break
        }

        tatalBuffer = append(tatalBuffer,buffer...)
        all += n

        fmt.Println(string(buffer),n,string(tatalBuffer[:all]),all)
    }



}

上面这个例子中,会重conn中两个字符循环读取内容,这边slice不会动态扩容,所以需要使用append来获取全部内容。

还有一点,buffer := make([]byte, 2)这个代码,放在for循环中,浪费内存,可以放在gor循环外部,然后使用n来截取buf[:n]可以解决buf最后一部分重复的问题。

插播结束,回到server。

Server写好之后,接下来就是Client方面啦,我手写一个HelloWorld给大家:

package main

import (
    "fmt"
    "net"
    "os"
)

func sender(conn net.Conn) {
        words := "hello world!"
        conn.Write([]byte(words))
    fmt.Println("send over")

}



func main() {
    server := "127.0.0.1:1024"
    tcpAddr, err := net.ResolveTCPAddr("tcp4", server)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
        os.Exit(1)
    }

    conn, err := net.DialTCP("tcp", nil, tcpAddr)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
        os.Exit(1)
    }


    fmt.Println("connect success")
    sender(conn)

}

可以看到,Client这里的关键在于

tcpAddr, err := net.ResolveTCPAddr("tcp4", server)
conn, err := net.DialTCP("tcp", nil, tcpAddr)

这两步,主要是负责解析端口和连接。

这边插播一个tcp协议的三次握手图,加强理解。

扩展

其实我们最常用的还是http协议,也即是应用层的协议,其实http协议是在tcp协议的基础上进行封装,最终还是使用的这边基本的网络IO,所以在网络传输中,网络IO的基本协议的实现是基础。