关于golang:GO的网络编程分享

2次阅读

共计 9009 个字符,预计需要花费 23 分钟才能阅读完成。

[TOC]

GO 的网络编程分享

回顾一下咱们上次分享的网络协议 5 层模型

  • 物理层
  • 数据链路层
  • 网络层
  • 传输层
  • 应用层

每一层有每一层的独立性能,大多数网络都采纳分层的体系结构,每一层都建设在它的上层之上,向它的上一层提供肯定的服务,而把 如何实现这一服务的细节对上一层加以屏蔽

每一层背地的协定有哪些,具体有啥为什么呈现的,感兴趣的能够看看互联网协议知多少

理解了网络协议的分层,数据包是如何封包,如何拆包,如何失去源数据的,往下看心里就有点谱了

GO 网络编程指的是什么?

GO 网络编程,这里是指的是SOCKET 编程

置信写过 c/c++ 网络编程的敌人看到这里并不生疏吧,咱们再来回顾一下

网络编程这一块,分为客户端局部的开发,和服务端局部的开发,会波及到相应的要害流程

服务端波及的流程

  • socket 建设套接字
  • bind 绑定地址和端口
  • listen 设置最大监听数
  • accept 开始阻塞期待客户端的连贯
  • read 读取数据
  • write 回写数据
  • close 敞开

客户端波及的流程

  • socket 建设套接字
  • connect 连贯服务端
  • write 写数据
  • read 读取数据

咱们来看看 SOCKET 编程是啥?

SOCKET就是套接字,是 BSD UNIX 的过程通信机制,他是一个 句柄 ,用于形容IP 地址端口 的。

当然 SOCKET 也是能够了解为 TCP/IP 网络API(利用程序接口)SOCKET定义了许多函数,咱们能够用它们来开发 T CP/IP 网络 上的应用程序。

电脑上运行的应用程序通常通过 SOCKET 向网络发出请求或者应答网络申请。

哈,忽然想到 面向接口编程

顾名思义,就是在一个面向对象的零碎中,零碎的各种性能是由许许多多的不同对象合作实现的。

在这种状况下,各个对象外部是如何实现本人的,对系统设计人员来讲就不那么重要了

各个对象之间的协作关系则成为零碎设计的要害,面向接口编程的晓得思维就是,无论模块大小,对应模块之间的交互都必须要在零碎设计的时候着重思考。

哈,一味的依赖他人提供的接口,对于接口外部会不会有坑,为什么会失败,咱们就不得而知了

开始 socket 编程

先上一张图,咱们一起瞅瞅

Socket是应用层与 T CP/IP 协定族 通信的两头 软件形象层

在设计模式中,Socket 其实就是一个门面模式,它把简单的 TCP/IP 协定族 暗藏在 Socket 前面

对用户来说只须要调用 Socket 规定的相干函数就能够了,让 Socket 去做剩下的事件

Socket,应用程序通常通过 Socket 向网络收回 申请 / 应答 网络申请

罕用的 Socket 类型有 2 种

  • 流式 Socket(stream)

流式是一种 面向连贯 Socket,针对于面向连贯的 TCP 服务利用

  • 数据报式 Socket

数据报式 Socket 是一种 无连贯的 Socket,针对于无连贯的 UDP 服务利用

简略比照一下:

  • TCP:比拟靠谱,面向连贯,平安,牢靠的传输方式,然而 比较慢
  • UDP:不是太靠谱,不牢靠的,丢包不会重传,然而 比拟快

举一个当初生存中最常见的例子:

案例一

他人买一个小礼物给你,还要货到付款,这个时候快递员将货送到你家的时候,必须看到你人,而且你要付钱,这才是实现了一个流程,这是 TCP

案例二

还是快递的例子,比方你在网上轻易抢了一些不太重要的小东西,小玩意,快递员送货的时候,间接就把你的包含扔到某个快递点,头都不回一下的那种,这是 UDP

网络编程无非简略来看就是 TCP 编程UDP 编程

咱们一起来看看 GOLANG 如何实现基于 TCP 通信 和 基于 UDP 通信的

GO 基于 TCP 编程

那咱们先来看看 TCP 协定是个啥?

TCP/IP(Transmission Control Protocol/Internet Protocol)

传输控制协议 / 网间协定,是一种 面向连贯 (连贯导向)的、 牢靠的 基于字节流 传输层(Transport layer)通信协议

因为是面向连贯的协定,数据像水流一样传输,这样会产生黏包问题。

上述提了个别 socket 编程的服务端流程和客户端流程,实际上 go 的底层实现也离不开这几步,然而咱们从利用的角度来看看go 的 TCP 编程,服务端有哪些流程

TCP 服务端

TCP 服务端能够同时连贯很多个客户端,这个毋庸置疑,要是一个服务端只能承受一个客户端的连贯,那么你完了,你能够拾掇货色回家了

举个栗子

最近也要开始的各种疯狂购物流动,他们的服务端,寰球各地的客户端都会去连贯,那么 TCP 服务端又是如何解决的嘞,在 C/C++ 中咱们会基于 epoll 模型来进行解决,来一个客户端的连贯 / 申请事件,咱们就专门开一个线程去进行解决

那么 golang 中是如何解决的呢?

golang 中,每建设一个连贯,就会开拓一个协程 goroutine 来解决这个申请

服务端解决流程大抵分为如下几步

  • 监听端口
  • 接管客户端申请建设链接
  • 创立 goroutine 解决链接
  • 敞开

能做大这么简洁和敌对的解决形式,得益于 Go 中的net 包

TCP 服务端的具体实现:

func process(conn net.Conn) {
    // 敞开连贯
    defer conn.Close() 
    for {reader := bufio.NewReader(conn)
        var buf [256]byte
        // 读取数据
        n, err := reader.Read(buf[:]) 
        if err != nil {fmt.Println("reader.Read  error :", err)
            break
        }
        recvData := string(buf[:n])
        fmt.Println("receive data:", recvData)
        // 将数据再发给客户端
        conn.Write([]byte(recvData)) 
    }
}

func main() {
    // 监听 tcp
    listen, err := net.Listen("tcp", "127.0.0.1:8888")
    if err != nil {fmt.Println("net.Listen  error :", err)
        return
    }
    for {
        // 建设连贯,看到这里的敌人,有没有感觉这里和 C /C++ 的做法一毛一样
        conn, err := listen.Accept() 
        if err != nil {fmt.Println("listen.Accept error :", err)
            continue
        }
        // 专门开一个 goroutine 去解决连贯
        go process(conn) 
    }
}

TCP 的服务端写起来是不是很简略呢

咱们 看看 TCP 的客户端

TCP 客户端

客户端流程如下:

  • 与服务端建设连贯
  • 读写数据
  • 敞开
func main() {conn, err := net.Dial("tcp", "127.0.0.1:8888")
    if err != nil {fmt.Println("net.Dial error :", err)
        return
    }
    // 敞开连贯
    defer conn.Close() 
    // 键入数据
    inputReader := bufio.NewReader(os.Stdin)
    for {
        // 读取用户输出
        input, _ := inputReader.ReadString('\n') 
        // 截断
        inputInfo := strings.Trim(input, "\r\n")
        // 读取到用户输出 q 或者 Q 就退出
        if strings.ToUpper(inputInfo) == "Q" {return}
        // 将输出的数据发送给服务端
        _, err = conn.Write([]byte(inputInfo)) 
        if err != nil {return}
        buf := [512]byte{}
        n, err := conn.Read(buf[:])
        if err != nil {fmt.Println("conn.Read error :", err)
            return
        }
        fmt.Println(string(buf[:n]))
    }
}

注意事项

  • 服务端与客户端联调,须要先启动服务端,期待客户端的连贯,
  • 若程序弄反,客户端会因为找不到服务端而报错

下面有说到 TCP 是流式协定,会存在黏包的问题,咱们就来模仿一下,看看实际效果

TCP 黏包如何解决?

来模仿写一个服务端

server.go

package main

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

// 专门解决客户端连贯
func process(conn net.Conn) {defer conn.Close()
   reader := bufio.NewReader(conn)
   var buf [2048]byte
   for {n, err := reader.Read(buf[:])
      // 如果客户端敞开,则退出本协程
      if err == io.EOF {break}
      if err != nil {fmt.Println("reader.Read error :", err)
         break
      }
      recvStr := string(buf[:n])
      // 打印收到的数据,稍后咱们次要是看这里输入的数据是否是咱们冀望的
      fmt.Printf("received data:%s\n\n", recvStr)
   }
}

func main() {listen, err := net.Listen("tcp", "127.0.0.1:8888")
   if err != nil {fmt.Println("net.Listen error :", err)
      return
   }
   defer listen.Close()
   fmt.Println("server start ...")

   for {conn, err := listen.Accept()
      if err != nil {fmt.Println("listen.Accept error :", err)
         continue
      }
      go process(conn)
   }
}

写一个客户端进行配合

client.go

package main

import (
   "fmt"
   "net"
)

func main() {conn, err := net.Dial("tcp", "127.0.0.1:8888")
   if err != nil {fmt.Println("net.Dial error :", err)
      return
   }
   defer conn.Close()
   fmt.Println("client start ...")

   for i := 0; i < 30; i++ {

      msg := `Hello world, hello xiaomotong!`

      conn.Write([]byte(msg))
   }

   fmt.Println("send data over...")

}

实际效果

server start ...
received data:Hello world, hello xiaomotong!Hello world, hello xiaomotong!

received data:Hello world, hello xiaomotong!Hello world, hello xiaomotong!Hello world, hello xiaomotong!Hello world, hello xiaomotong!Hello world, hello xiaomotong!Hello worl
d, hello xiaomotong!Hello world, hello xiaomotong!Hello world, hello xiaomotong!Hello world, hello xiaomotong!Hello world, hello xiaomotong!Hello world, hello xiaomotong!Helloworld, hello xiaomotong!

received data:Hello world, hello xiaomotong!Hello world, hello xiaomotong!Hello world, hello xiaomotong!Hello world, hello xiaomotong!Hello world, hello xiaomotong!Hello world, hello xiaomotong!Hello world, hello xiaomotong!Hello world, hello xiaomotong!Hello world, hello xiaomotong!

received data:Hello world, hello xiaomotong!Hello world, hello xiaomotong!Hello world, hello xiaomotong!Hello world, hello xiaomotong!Hello world, hello xiaomotong!Hello worl
d, hello xiaomotong!Hello world, hello xiaomotong!

由上述成果咱们能够看进去,客户端发送了 30 次数据给到服务端,可是服务端只输入了 4 次,而是多条数据黏在了一起输入了,这个景象就是黏包,那么咱们如何解决呢?

如何解决 TCP 黏包问题

黏包起因:

  • tcp数据传递模式是流式的,在放弃长连贯的时候能够进行屡次的收和发

理论状况有如下 2 种

  • 由 Nagle 算法造成的发送端的粘包

Nagle 算法 是一种改善网络传输效率的算法

当咱们提交一段数据给 TCP 发送时,TCP 并不会立即发送此段数据

而是期待一小段时间看看,在这段等待时间里,是否还有要发送的数据,若有则会一次把这两段数据发送进来

  • 接收端接管不及时造成的接收端粘包

TCP 会把接管到的数据存在本人的 缓冲区中,告诉应用层取数据

当应用层因为某些起因不能及时的把 TCP 的数据取出来,就会造成 TCP 缓冲区中寄存了几段数据。

晓得起因之后,咱们来看看如何解决吧

开始解决 TCP 黏包问题

晓得了黏包的起因,咱们就针对起因下手就好了,剖析一下,为什么 tcp 会等一段时间,是不是因为 tcp 他不晓得咱们要发送给他的数据包到底是多大,所以他就想尽可能的多吃点?

那么,咱们的解决形式就是 对数据包进行封包和拆包的操作。

  • 封包:

封包就是给一段数据加上包头,这样一来数据包就分为 包头和包体 两局部内容了,有时候为了过滤非法包,咱们还会加上包尾。

包头局部的长度是固定的,他会明确的指出包体的大小是多少,这样子咱们就能够正确的拆除一个残缺的包了

  • 依据 包头长度固定
  • 依据 包头中含有包体长度的变量

咱们能够本人定义一个协定,比方数据包的前 2 个字节为包头,外面存储的是发送的数据的长度。

这一个自定义协定,客户端和服务端都要晓得,否则就没得玩了

开始解决问题

server2.go

package main

import (
   "bufio"
   "bytes"
   "encoding/binary"
   "fmt"
   "io"
   "net"
)

// Decode 解码音讯
func Decode(reader *bufio.Reader) (string, error) {
   // 读取音讯的长度
   lengthByte, _ := reader.Peek(2) // 读取前 2 个字节,看看包头
   lengthBuff := bytes.NewBuffer(lengthByte)
   var length int16
   // 读取理论的包体长度
   err := binary.Read(lengthBuff, binary.LittleEndian, &length)
   if err != nil {return "", err}
   // Buffered 返回缓冲中现有的可读取的字节数。if int16(reader.Buffered()) < length+2 {return "", err}

   // 读取真正的音讯数据
   realData := make([]byte, int(2+length))
   _, err = reader.Read(realData)
   if err != nil {return "", err}
   return string(realData[2:]), nil
}

func process(conn net.Conn) {defer conn.Close()
   reader := bufio.NewReader(conn)

   for {msg, err := Decode(reader)
      if err == io.EOF {return}
      if err != nil {fmt.Println("Decode error :", err)
         return
      }
      fmt.Println("received data:", msg)
   }
}

func main() {listen, err := net.Listen("tcp", "127.0.0.1:8888")
   if err != nil {fmt.Println("net.Listen error :", err)
      return
   }
   defer listen.Close()
   for {conn, err := listen.Accept()
      if err != nil {fmt.Println("listen.Accept error :", err)
         continue
      }
      go process(conn)
   }
}

client2.go

package main

import (
   "bytes"
   "encoding/binary"
   "fmt"
   "net"
)

// Encode 编码音讯
func Encode(message string) ([]byte, error) {
   // 读取音讯的长度,并且要 转换成 int16 类型(占 2 个字节),咱们约定好的 包头 2 字节
   var length = int16(len(message))
   var nb = new(bytes.Buffer)

   // 写入音讯头
   err := binary.Write(nb, binary.LittleEndian, length)
   if err != nil {return nil, err}

   // 写入音讯体
   err = binary.Write(nb, binary.LittleEndian, []byte(message))
   if err != nil {return nil, err}
   return nb.Bytes(), nil}

func main() {conn, err := net.Dial("tcp", "127.0.0.1:8888")
   if err != nil {fmt.Println("net.Dial error :", err)
      return
   }
   defer conn.Close()
   for i := 0; i < 30; i++ {
      msg := `Hello world,hello xiaomotong!`

      data, err := Encode(msg)
      if err != nil {fmt.Println("Encode msg error :", err)
         return
      }
      conn.Write(data)
   }
}

此处为了演示不便简略,咱们将封包放到了 客户端代码中,拆包,放到了服务端代码中

成果演示

这下子,就不会存在黏包的问题了,因为 tcp 他晓得本人每一次要读多少长度的包,要是缓冲区数据不够冀望的长,那么就等到数据够了再一起读出来,而后打印进去

看到这里的敌人,对于 golang 的 TCP 编程还有点趣味了吧,那么咱们能够看看 UDP 编程了,绝对 TCP 来说就简略多了,不会有黏包的问题

GO 基于 UDP 编程

同样的,咱们先来说说 UDP 协定

UDP 协定(User Datagram Protocol)

是用户数据报协定,一种无连贯的传输层协定

不须要建设连贯就能间接进行数据发送和接管

属于不牢靠的、没有时序的通信,正是因为这样的特点,所以 UDP 协定 的实时性比拟好,通常用于视频直播相干畛域,因为对于视频传输,传输过程中丢点一些帧,对整体影响很小

UDP 服务端

咱们来撸一个 UDP 客户端和服务端

server3.go

func main() {
    listen, err := net.ListenUDP("udp", &net.UDPAddr{IP:   net.IPv4(0, 0, 0, 0),
        Port: 8888,
    })
    if err != nil {fmt.Println("net.ListenUDP error :", err)
        return
    }
    defer listen.Close()
    for {var data [1024]byte
        // 接收数据报文
        n, addr, err := listen.ReadFromUDP(data[:]) 
        if err != nil {fmt.Println("listen.ReadFromUDP error :", err)
            continue
        }
        fmt.Printf("data == %v  , addr == %v , count == %v\n", string(data[:n]), addr, n)
        // 将数据又发给客户端
        _, err = listen.WriteToUDP(data[:n], addr) 
        if err != nil {fmt.Println("listen.WriteToUDP error:", err)
            continue
        }
    }
}

UDP 客户端

client3.go

func main() {
   socket, err := net.DialUDP("udp", nil, &net.UDPAddr{IP:   net.IPv4(0, 0, 0, 0),
      Port: 8888,
   })
   if err != nil {fmt.Println("net.DialUDP error :", err)
      return
   }
   defer socket.Close()
   sendData := []byte("hello xiaomotong!!")
   // 发送数据
   _, err = socket.Write(sendData)
   if err != nil {fmt.Println("socket.Write error :", err)
      return
   }
   data := make([]byte, 2048)
   // 接收数据
   n, remoteAddr, err := socket.ReadFromUDP(data)
   if err != nil {fmt.Println("socket.ReadFromUDP error :", err)
      return
   }
   fmt.Printf("data == %v  , addr == %v , count == %v\n", string(data[:n]), remoteAddr, n)
}

成果展现

服务端打印:data == hello xiaomotong!!  , addr == 127.0.0.1:50487 , count == 18

客户端打印:data == hello xiaomotong!!  , addr == 127.0.0.1:8888 , count == 18

总结

  • 回顾网络的 5 层模型,SOCKET 编程的服务端和客户端的流程
  • GO 基于 TCP 如何编程,如何解决 TCP 黏包问题
  • GO 基于 UDP 如何编程

欢送点赞,关注,珍藏

敌人们,你的反对和激励,是我保持分享,提高质量的能源

好了,本次就到这里,下一次 分享 GO 中如何设置 HTTPS

技术是凋谢的,咱们的心态,更应是凋谢的。拥抱变动,背阴而生,致力向前行。

我是 小魔童哪吒,欢送点赞关注珍藏,下次见~

正文完
 0