网络编程是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的基本协议的实现是基础。