首页 > 其他分享 >(转)Golang使用系列---- Go Net 协议层

(转)Golang使用系列---- Go Net 协议层

时间:2023-03-07 22:47:26浏览次数:66  
标签:Golang err IP ---- func error Go net conn

原文:https://kingjcy.github.io/post/golang/go-net/

Golang使用系列---- Go Net 协议层

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

标签:Golang,err,IP,----,func,error,Go,net,conn
From: https://www.cnblogs.com/liujiacai/p/17189998.html

相关文章

  • 什么是JVM
    定义:JavaVirtualMachine,是Java程序的运行环境(Java二进制字节码的运行环境)好处:一次编译,到处运行自动内存管理,垃圾回收数组下标越界检查多态......
  • 渗透小tis
    知己知彼,百战不殆1、如果提示缺少参数,如{msg:paramserror},可尝使用字典模糊测试构造参数,进一步攻击。2、程序溢出,int最大值为2147483647,可尝试使用该值进行整数溢出,观察......
  • (转)Golang网络开发系列(二)—— net包
    原文:https://zhuanlan.zhihu.com/p/575280551这篇文章我们将开始学习net包。因为我们大多是从net.Listen开始写一个tcpserver的,这篇文章我们就从上到下去分析,直到遇到int......
  • 春测2023 T2题解
    这是一个从暴力到正解的过程。暴力从\(1\)枚举到\(\sqrtn\),枚举每一个\(x\),进行累乘,顺便用一个map数组判重,时间复杂度\(\mathcal{O}(\sqrtn\logn\logn)\),直接T飞......
  • MySQL
    MySQL所有的sql语句都需要用分号结尾基本命令showdatabases;--查看所有的数据库use数据库名;--切换数据库use数据库名showtables;--查看数据库中所有的表......
  • 什么是JSON
    前后端分离时代:后端部署后端,提供接口;串联前后端:JSON前端独立部署,负责渲染后端的数据。什么是JSON:JSON(JavaScriptObjectNotation,JS对象简谱)是一种轻量级的数据交换......
  • 3.7
    E.Arena2100https://codeforces.com/problemset/problem/1606/E题意见洛谷。n,x<=500题解:显然是一道dp,考虑状态:需要能够随转移变化的状态,而转移显然是生命值的变化和......
  • ArrayList扩容机制
    ArrayList扩容机制ArrayList的底层实现是Object数组队列,相当于动态数组。它的容量能动态增长。在添加大量元素前,应用程序可以使用ensureCapacity操作来增加ArrayList实......
  • 数据结构01
    一.总览二.数据结构的基本概念2.1.导图2.2.什么是数据?数据是信息的载体,是描述客观事物属性的数、字符及所有能够被输入到计算机中并被计算机程序识别和处理的符号的......
  • 思科防火墙5506-x基础
    防火墙的基本命令查看防火墙的接口配置showinterfaceipbrief#这里和以往路由器和交换机不同(shipinterfacebrief)查看路由:showroute默认策略高......