美文网首页
Go网络编程之UDP通信和文件传输

Go网络编程之UDP通信和文件传输

作者: 泡泡龙吐泡泡 | 来源:发表于2018-09-05 21:16 被阅读180次

1. UDP通信

  • UDP服务器

由于UDP是“无连接”的,所以,服务器端不需要额外创建监听套接字,只需要指定好IP和port,然后监听该地址,等待客户端与之建立连接,即可通信。

    // 创建监听地址:
    func ResolveUDPAddr(network, address string) (*UDPAddr, error) {
}
    // 创建用户通信的socket:
    func ListenUDP(network string, laddr *UDPAddr) (*UDPConn, error) {
}
    // 接收udp数据:
    func (c *UDPConn) ReadFromUDP(b []byte) (int, *UDPAddr, error) {
}
    // 写出数据到udp:
    func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (int, error)

服务端完整代码实现如下:

package main

import (
   "fmt"
   "net"
)

func main() {
   //创建监听的地址,并且指定udp协议
   udp_addr, err := net.ResolveUDPAddr("udp", "127.0.0.1:8002")
   if err != nil {
      fmt.Println("ResolveUDPAddr err:", err)
      return
   }
   conn, err := net.ListenUDP("udp", udp_addr)    //创建数据通信socket
   if err != nil {
      fmt.Println("ListenUDP err:", err)
      return
   }
   defer conn.Close()

   buf := make([]byte, 1024)
   n, raddr, err := conn.ReadFromUDP(buf)        //接收客户端发送过来的数据,填充到切片buf中。
   if err != nil {
      return
   }
   fmt.Println("客户端发送:", string(buf[:n]))

   _, err = conn.WriteToUDP([]byte("nice to see u in udp"), raddr) // 向客户端发送数据
   if err != nil {
      fmt.Println("WriteToUDP err:", err)
      return
   }
}
  • UDP客户端

udp客户端的编写与TCP客户端的编写,基本上是一样的,只是将协议换成udp。注意只能使用小写。

代码如下:

package main

import (
   "net"
   "fmt"
)

func main() {
   conn, err := net.Dial("udp", "127.0.0.1:8002") 
   if err != nil {
      fmt.Println("net.Dial err:", err)
      return
   }
   defer conn.Close()

   conn.Write([]byte("Hello! I'm client in UDP!"))

   buf := make([]byte, 1024)
   n, err1 := conn.Read(buf)
   if err1 != nil {
      return
   }
   fmt.Println("服务器发来:", string(buf[:n]))
}
  • 并发

其实对于UDP而言,服务器不需要并发,只要循环处理客户端数据即可。客户端也等同于TCP通信并发的客户端。

服务器:

package main

import (
   "net"
   "fmt"
)

func main() {
   // 创建 服务器 UDP 地址结构。指定 IP + port
   laddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:8003")
   if err != nil {
      fmt.Println("ResolveUDPAddr err:", err)
      return
   }
   // 监听 客户端连接
   conn, err := net.ListenUDP("udp", laddr)
   if err != nil {
      fmt.Println("net.ListenUDP err:", err)
      return
   }
   defer conn.Close()

   for {
      buf := make([]byte, 1024)
      n, raddr, err := conn.ReadFromUDP(buf)
      if err != nil {
         fmt.Println("conn.ReadFromUDP err:", err)
         return
      }
      fmt.Printf("接收到客户端[%s]:%s", raddr, string(buf[:n]))

      conn.WriteToUDP([]byte("I-AM-SERVER"), raddr) // 简单回写数据给客户端
   }
}

客户端:

package main

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

func main() {
   conn, err := net.Dial("udp", "127.0.0.1:8003")
   if err != nil {
      fmt.Println("net.Dial err:", err)
      return
   }
   defer conn.Close()
   go func() {
      str := make([]byte, 1024)
      for {
         n, err := os.Stdin.Read(str) //从键盘读取内容, 放在str
         if err != nil {
            fmt.Println("os.Stdin. err1 = ", err)
            return
         }
         conn.Write(str[:n])       // 给服务器发送
      }
   }()
   buf := make([]byte, 1024)
   for {
      n, err := conn.Read(buf)
      if err != nil {
         fmt.Println("conn.Read err:", err)
         return
      }
      fmt.Println("服务器写来:", string(buf[:n]))
   }
}
  • UDP与TCP的差异

TCP UDP
面向连接 面向无连接
要求系统资源较多 要求系统资源较少
TCP程序结构较复杂 UDP程序结构较简单
使用流式 使用数据包式
保证数据准确性 不保证数据准确性
保证数据顺序 不保证数据顺序
通讯速度较慢 通讯速度较快

2. 文件传输

  • 流程简析

借助TCP完成文件的传输,基本思路如下:

  1. 发送方(客户端)向服务端发送文件名,服务端保存该文件名。
  2. 接收方(服务端)向客户端返回一个消息ok,确认文件名保存成功。
  3. 发送方(客户端)收到消息后,开始向服务端发送文件数据。
  4. 接收方(服务端)读取文件内容,写入到之前保存好的文件中。


    流程简析.png

首先获取文件名。借助os包中的stat()函数来获取文件属性信息。在函数返回的文件属性中包含文件名和文件大小。Stat参数name传入的是文件访问的绝对路径。FileInfo中的Name()函数可以将文件名单独提取出来。

func Stat(name string) (FileInfo, error) 

type FileInfo interface {   Name() string       
   Size() int64        
   Mode() FileMode        ModTime() time.Time    IsDir() bool           Sys() interface{}   }

获取文件属性示例:

package main

import (
   "os"
   "fmt"
)

func main()  {
   list := os.Args              // 获取命令行参数,存入list中
   if len(list) != 2 {          // 确保用户输入了一个命令行参数
      fmt.Println("格式为:xxx.go 文件名")
      return
   }
   fileName := list[1]                   // 从命令行保存文件名(含路径)

   fileInfo, err := os.Stat(fileName)    //根据文件名获取文件属性信息 fileInfo
   if err != nil {
      fmt.Println("os.Stat err:", err)
      return
   }
   fmt.Println("文件name为:", fileInfo.Name())   // 得到文件名(不含路径)
   fmt.Println("文件size为:", fileInfo.Size())   // 得到文件大小。单位字节
}
  • 客户端实现

实现流程大致如下:

  1. 提示用户输入文件名。接收文件名path(含访问路径)
  2. 使用os.Stat()获取文件属性,得到纯文件名(去除访问路径)
  3. 主动连接服务器,结束时关闭连接
  4. 给接收端(服务器)发送文件名conn.Write()
  5. 读取接收端回发的确认数据conn.Read()
  6. 判断是否为“ok”。如果是,封装函数SendFile() 发送文件内容。传参path和conn
  7. 只读Open文件, 结束时Close文件
  8. 循环读文件,读到EOF终止文件读取
  9. 将读到的内容原封不动Write给接收端(服务器)

代码实现:

package main

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

func SendFile(path string, conn net.Conn)  {
   // 以只读方式打开文件
   f, err := os.Open(path)
   if err != nil {
      fmt.Println("os.Open err:", err)
      return
   }
   defer f.Close()                   // 发送结束关闭文件。

   // 循环读取文件,原封不动的写给服务器
   buf := make([]byte, 4096)
   for {
      n, err := f.Read(buf)        // 读取文件内容到切片缓冲中
      if err != nil {
         if err == io.EOF {
            fmt.Println("文件发送完毕")
         } else {
            fmt.Println("f.Read err:", err)
         }
         return
      }
      conn.Write(buf[:n])  // 原封不动写给服务器
   }
}

func main()  {
   // 提示输入文件名
   fmt.Println("请输入需要传输的文件:")
   var path string
   fmt.Scan(&path)

   // 获取文件名   fileInfo.Name()
   fileInfo, err := os.Stat(path)
   if err != nil {
      fmt.Println("os.Stat err:", err)
      return
   }

   // 主动连接服务器
   conn, err := net.Dial("tcp", "127.0.0.1:8005")
   if err != nil {
      fmt.Println("net.Dial err:", err)
      return
   }
   defer conn.Close()

   // 给接收端,先发送文件名
   _, err = conn.Write([]byte(fileInfo.Name()))
   if err != nil {
      fmt.Println("conn.Write err:", err)
      return
   }

   // 读取接收端回发确认数据 —— ok
   buf := make([]byte, 1024)
   n, err := conn.Read(buf)
   if err != nil {
      fmt.Println("conn.Read err:", err)
      return
   }

   // 判断如果是ok,则发送文件内容
   if "ok" == string(buf[:n]) {
      SendFile(path, conn)   // 封装函数读文件,发送给服务器,需要path、conn
   }
}
  • 服务端实现

实现流程大致如下:

  1. 创建监听listener,程序结束时关闭。
  2. 阻塞等待客户端连接,程序结束时关闭conn。
  3. 读取客户端发送文件名。保存fileName。
  4. 回发“ok”给客户端做应答
  5. 封装函数 RecvFile接收客户端发送的文件内容。传参fileName 和conn
  6. 按文件名Create文件,结束时Close
  7. 循环Read客户端发送的文件内容,当读到EOF说明文件读取完毕
  8. 将读到的内容原封不动Write到创建的文件中

代码实现:

package main

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

func RecvFile(fileName string, conn net.Conn)  {
   // 创建新文件
   f, err := os.Create(fileName)
   if err != nil {
      fmt.Println("Create err:", err)
      return
   }
   defer f.Close()

   // 接收客户端发送文件内容,原封不动写入文件
   buf := make([]byte, 4096)
   for {
      n, err := conn.Read(buf)
      if err != nil {
         if err == io.EOF {
            fmt.Println("文件接收完毕")
         } else {
            fmt.Println("Read err:", err)
         }
         return
      }
      f.Write(buf[:n])   // 写入文件,读多少写多少
   }
}

func main()  {
   // 创建监听
   listener, err := net.Listen("tcp", "127.0.0.1:8005")
   if err != nil {
      fmt.Println("Listen err:", err)
      return
   }
   defer listener.Close()

   // 阻塞等待客户端连接
   conn, err := listener.Accept()
   if err != nil {
      fmt.Println("Accept err:", err)
      return
   }
   defer conn.Close()

   // 读取客户端发送的文件名
   buf := make([]byte, 1024)
   n, err := conn.Read(buf)
   if err != nil {
      fmt.Println("Read err:", err)
      return
   }
   fileName := string(buf[:n])       // 保存文件名

   // 回复 0k 给发送端
   conn.Write([]byte("ok"))

   // 接收文件内容
   RecvFile(fileName, conn)      // 封装函数接收文件内容, 传fileName 和 conn

相关文章

网友评论

      本文标题:Go网络编程之UDP通信和文件传输

      本文链接:https://www.haomeiwen.com/subject/wmpzwftx.html