golang socket断点续传大文件

在日常编程中,我们肯定会遇到用socket传送文件内容,如果是大文件的,总不能传送到一半因某原因断掉了,又从新传送文件内容吧。对,我们需要续传,也就是接着上次传送的位置继续发送文件内容。

续传的话,其实并不难,我理解的思路大概如下:

  1. 客户端发送消息询问服务端,你上次接收到的文件内容位置
  2. 服务端告诉客户端上次接收到的文件内容位置
  3. 客户端就从上次断点的位置继续发送文件内容
  4. 客户端发送文件内容完毕后通知服务端,然后断开连接

下面我们看看代码的实现

服务端
// file name: server.go

package main

import (
    "os"
    "io"
    "net"
    "log"
    "strconv"
    // "time"
)

// 把接收到的内容append到文件
func writeFile(content []byte) {
    if len(content) != 0 {
        fp, err := os.OpenFile("test_1.txt", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0755)
        defer fp.Close()
        if err != nil {
            log.Fatalf("open file faild: %s\n", err)
        }
        _, err = fp.Write(content)
        if err != nil {
            log.Fatalf("append content to file faild: %s\n", err)
        }
        log.Printf("append content: 【%s】 success\n", string(content))
    }
}

// 获取已接收内容的大小
// (断点续传需要把已接收内容大下通知客户端从哪里开始发送文件内容)
func getFileStat() int64 {
    fileinfo, err := os.Stat("test_1.txt")
    if err != nil {
        // 如果首次没有创建test_1.txt文件,则直接返回0
        // 告诉客户端从头开始发送文件内容
        if os.IsNotExist(err) {
            log.Printf("file size: %d\n", 0)
            return int64(0)
        }
        log.Fatalf("get file stat faild: %s\n", err)
    }
    log.Printf("file size: %d\n", fileinfo.Size())
    return fileinfo.Size() 
}

func serverConn(conn net.Conn) {
    defer conn.Close()
    for {
        var buf = make([]byte, 10)
        n, err := conn.Read(buf)
        if err != nil {
            if err == io.EOF {
                log.Println("server io EOF\n")
                return
            }
            log.Fatalf("server read faild: %s\n", err)
        }
        log.Printf("recevice %d bytes, content is 【%s】\n", n, string(buf[:n]))
        // 判断客户端发送过来的消息
        // 如果是’start-->‘则表示需要告诉客户端从哪里开始读取文件数据发送
        switch  string(buf[:n]) {
        case "start-->":
            off := getFileStat()
            // int conver string
            stringoff := strconv.FormatInt(off, 10)
            _, err = conn.Write([]byte(stringoff))
            if err != nil {
                log.Fatalf("server write faild: %s\n", err)
            }
            continue
        case "<--end":
            // 如果接收到客户端通知所有文件内容发送完毕消息则退出
            log.Fatalf("receive over\n")
            return
        // default:
        //     time.Sleep(time.Second * 1)
        }
        // 把客户端发送的内容保存到文件
        writeFile(buf[:n])
    }
}

func main() {
    // 建立监听
    l, err := net.Listen("tcp", ":8888")
    if err != nil {
        log.Fatalf("error listen: %s\n", err)
    }
    defer l.Close()

    log.Println("waiting accept.")
    // 允许客户端连接,在没有客户端连接时,会一直阻塞
    conn, err := l.Accept()
    if err != nil {
        log.Fatalf("accept faild: %s\n", err)
    }
    serverConn(conn)
}
客户端
// file name: client.go

package main

import (
    "os"
    "io"
    "net"
    "log"
    "time"
    "strconv"
)

// 获取服务端发送的消息
func clientRead(conn net.Conn) int {
    buf := make([]byte, 5)
    n, err := conn.Read(buf)
    if err != nil {
        log.Fatalf("receive server info faild: %s\n", err)
    }
    // string conver int
    off, err := strconv.Atoi(string(buf[:n]))
    if err != nil {
        log.Fatalf("string conver int faild: %s\n", err)
    }
    return off
}

// 发送消息到服务端
func clientWrite(conn net.Conn, data []byte) {
    _, err := conn.Write(data)
    if err != nil {
        log.Fatalf("send 【%s】 content faild: %s\n", string(data), err)
    }
    log.Printf("send 【%s】 content success\n", string(data))
}

// client conn
func clientConn(conn net.Conn) {
    defer conn.Close()

    // 发送"start-->"消息通知服务端,我要开始发送文件内容了
    // 你赶紧告诉我你那边已经接收了多少内容,我从你已经接收的内容处开始继续发送
    clientWrite(conn, []byte("start-->"))
    off := clientRead(conn)

    // send file content
    fp, err := os.OpenFile("test.txt", os.O_RDONLY, 0755)
    if err != nil {
        log.Fatalf("open file faild: %s\n", err)
    }
    defer fp.Close()

    // set file seek
    // 设置从哪里开始读取文件内容
    _, err = fp.Seek(int64(off), 0)
    if err != nil {
        log.Fatalf("set file seek faild: %s\n", err)
    }
    log.Printf("read file at seek: %d\n", off)

    for {
        // 每次发送10个字节大小的内容
        data := make([]byte, 10)
        n, err := fp.Read(data)
        if err != nil {
            if err == io.EOF {
                // 如果已经读取完文件内容
                // 就发送'<--end'消息通知服务端,文件内容发送完了
                time.Sleep(time.Second * 1)
                clientWrite(conn, []byte("<--end"))
                log.Println("send all content, now quit")
                break
            }
            log.Fatalf("read file err: %s\n", err)
        }
        // 发送文件内容到服务端
        clientWrite(conn, data[:n])
    }
}

func main() {
    // connect timeout 10s
    conn, err := net.DialTimeout("tcp", ":8888", time.Second * 10)
    if err != nil {
        log.Fatalf("client dial faild: %s\n", err)
    }
    clientConn(conn)
 }

客户端读取文件test.txt内容发送到服务端,服务端把接收到的文件内容保存在test_1.txt文件中。我们模拟断点续传的方式是:

  • 第一次先发送test.txt文件内容到服务端
  • 修改test.txt文件,加一些内容
  • 再次运行server socket以及client socket,观察客户端是不是只发送新增的文件内容到服务端
# 假设我的test.txt文件有以下内容
$ cat test.txt
hello golang.

# 先运行server socket再运行client socket(分别在两个终端窗口运行)
$ go run server.go
$ go run client.go

# 服务端会输出以下内容
2018/04/05 23:37:13 waiting accept.
2018/04/05 23:37:15 recevice 8 bytes, content is 【start-->】
2018/04/05 23:37:15 file size: 0
2018/04/05 23:37:15 recevice 10 bytes, content is 【hello gola】
2018/04/05 23:37:15 append content: 【hello gola】 success
2018/04/05 23:37:15 recevice 2 bytes, content is 【n.】
2018/04/05 23:37:15 append content: 【n.】 success
2018/04/05 23:37:16 recevice 6 bytes, content is 【<--end】
2018/04/05 23:37:16 receive over
exit status 1

# 客户端会输出如下内容
2018/04/05 23:37:15 send 【start-->】 content success
2018/04/05 23:37:15 read file at seek: 0
2018/04/05 23:37:15 send 【hello gola】 content success
2018/04/05 23:37:15 send 【n.】 content success
2018/04/05 23:37:16 send 【<--end】 content success
2018/04/05 23:37:16 send all content, now quit

# 这时候我们看看test_1.txt内容跟test.txt完全一致
$ cat test_1.txt
hello golan.

# ------- 模拟断点续传 ----------
# 现在我们往test.txt追加内容: hello python.
$ cat test.txt
hello golang.
hello python.

# 我们再一次运行server socket 和 client socket(分别在两个终端窗口运行)
$ go run server.go
$ go run client.go

# 服务端会输出以下内容
2018/04/05 23:44:31 waiting accept.
2018/04/05 23:44:34 recevice 8 bytes, content is 【start-->】
2018/04/05 23:44:34 file size: 12
2018/04/05 23:44:34 recevice 10 bytes, content is 【
hello pyt】
2018/04/05 23:44:34 append content: 【
hello pyt】 success
2018/04/05 23:44:34 recevice 4 bytes, content is 【hon.】
2018/04/05 23:44:34 append content: 【hon.】 success
2018/04/05 23:44:35 recevice 6 bytes, content is 【<--end】
2018/04/05 23:44:35 receive over
exit status 1
# 服务端在接收到客户端发送的 start--> 信息后会获取上次接收到文件内容位置,并通知客户端(这里file size 是12)

# 客户端会输出以下内容
2018/04/05 23:44:34 send 【start-->】 content success
2018/04/05 23:44:34 read file at seek: 12
2018/04/05 23:44:34 send 【
hello pyt】 content success
2018/04/05 23:44:34 send 【hon.】 content success
2018/04/05 23:44:35 send 【<--end】 content success
2018/04/05 23:44:35 send all content, now quit
# 我们客户端获取到了服务端返回的文件位置,通过 Seek 来指定从哪里开始读取文件
# 通过日志可以看到我们客户端只发送了后面追加的内容: hello python. 到服务端

# 我们看看此时test_1.txt文件的内容是否跟test.txt一致
$ cat test_1.txt
hello golang.
hello python.

好了,希望大家有个愉快的假期 ~~

  • 2
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
基于go的大文件切片上传、断点续传、秒传.zip 1、如何唯一标示一个文件文件的信息后端会存储在mysql数据库表中。 在上传之前,前端通过 spark-md5.js 计算文件的md5值以此去唯一的标示一个文件。 spark-md5.js 地址:satazor/js-spark-md5 README.md中有spark-md5.js的使用demo,可以去看看。 2、断点续传是如何实现的? 断点续传可以实现这样的功能,比如RD上传200M的文件,当用户上传完199M时,断网了,有了断点续传的功能,我们允许RD再次上传时,能从第199M的位置重新上传。 实现原理: 实现断点续传的前提是,大文件切片上传。然后前端得问后端哪些chunk曾经上传过,让前端跳过这些上传过的chunk就好了。 前端的上传器(uploader.js)在上传时会先发送一个GET请求,这个请求不会携带任何chunk数据,作用就是向后端询问哪些chunk曾经上传过。 后端会将这些数据保存在mysql数据库表中。比如按这种格式:1:2:3:5表示,曾经上传过的分片有1,2,3,5。第四片没有被上传,前端会跳过1,2,3,5。 仅仅会将第四个chunk发送给后端。 3、秒传是如何实现的? 秒传实现的功能是:当RD重复上传一份相同的文件时,除了第一次上传会正常发送上传请求后,其他的上传都会跳过真正的上传,直接显示秒成功。 实现方式: 后端存储着当前文件的相关信息。为了实现秒传,我们需要搞一个字段(isUploaded)表示当前md5对应的文件是否曾经上传过。 后端在处理 前端的上传器(uploader.js)发送的第一个GET请求时,会将这个字段发送给前端,比如 isUploaded = true。前端看到这个信息后,直接跳过上传,显示上传成功。 4、上传暂停是如何实现的? 上传的暂停:并不是去暂停一个已经发送出去的正在进行数据传输的http请求~ 而是暂停发送起发送下一个http请求。 就我们的项目而言,因为我们的文件本来就是先切片,对于我们来说,暂停文件的上传,本质上就是暂停发送下一个chunk。 5、前端上传并发数是多少? 前端的uploader.js中默认会三条线程启动并发上传,前端会在同一时刻并发 发送3个chunk,后端就会相应的为每个请求开启三个协程处理上传的过来的chunk。 在我们的项目中,会将前端并发数调整成了1。原因如下: 因为考虑到了断点续传的实现,后端需要记录下曾经上传过哪些切片。(这个记录在mysql的数据库表中,以 ”1:2:3:4:5“ )这种格式记录。 Mysql5.7默认的存储引擎是innoDB,默认的隔离级别是RR。如果我们将前端的并发数调大,就会出现下面的异常情况: 1. goroutine1 获取开启事物,读取当前上传到记录是 1:2 (未提交事物) 2. goroutine1 在现有的记录上加上自己处理的分片3,并和现有的1:2拼接在一起成1:2:3 (未提交事物) 3. goroutine2 获取开启事物,(因为RR,所以它读不到1:2:3)读取当前上传到记录是 1:2 (未提交事物) 4. goroutine1 提交事物,将1:2:3写回到mysql 5. goroutine2 在现有的记录上加上自己处理的分片4,并和现有的1:2拼接在一起成1:2:4 (提交事物) 可以看到,如果前端并发上传,后端就会出现分片丢失的问题。 故前端将并发数置为1。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值