【GO语言】实现同步传输系统:局域网内手机和电脑互传文件互发消息。go语言练手项目

项目总览:

1.开发语言:GO语言
2.IDE:Goland
3.开发用时:一周
4.使用到的第三方库:lorca——用于生成UI窗体;gin——提供服务器接口;gorilla——websocket服务;qrcode——链接转二维码。
5.源码已上传到我的GitHub,链接:https://github.com/2394799692/transmit-doc 或点此跳转
6.鸣谢:感谢方应杭老师分享的开源项目,使我有了这次练习的机会,以及在写代码与运行调试中出现一些问题老师在微信及时的解答和远程操控帮助解决,十分感谢。
7.前端代码也打包放在GitHub中,因为本人主要练习后端,故前端方面不介绍具体代码,网页拿来直接使用就行。


以下是本篇文章正文内容,欢迎朋友们进行指正,一起探讨,共同进步。——来自考研路上的lwj。QQ:2394799692

一、项目功能展示

1.用手机传输文件到电脑

在这里插入图片描述在这里插入图片描述

2.用手机传输图片到电脑

在这里插入图片描述

3.用电脑传输文字到手机

在这里插入图片描述在这里插入图片描述

4.服务器显示情况,端口信息:

在这里插入图片描述

二、总体规划

1.需求分析

不开微信,蓝牙,不注册账号,只扫个二维码即可完成数据传输

2.项目构思图

在这里插入图片描述

3.项目结构图

请添加图片描述

三、main函数部分

package main

import (
	"os"
	"os/exec"
	"os/signal"
	"synk/config"
	"synk/server"
)

func main() {
	go server.Run()                 //启动gin协程
	cmd := startBrowser()           //启动ui界面,打开Chrome
	chSignal := listenToInterrupt() //监听关闭信号
	<-chSignal                      //从chan中读出中断信号
	cmd.Process.Kill()              //关闭谷歌浏览器进程
}

func startBrowser() *exec.Cmd {
	// 先写死路径,后面再照着 lorca 改
	chromePath := "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"
	cmd := exec.Command(chromePath, "--app=http://127.0.0.1:"+config.GetPort()+"/static/index.html")
	cmd.Start()
	return cmd
}

func listenToInterrupt() chan os.Signal {
	chSignal := make(chan os.Signal, 1)   //接收系统信号
	signal.Notify(chSignal, os.Interrupt) //用户按下ctrl+c,就发送系统信号
	return chSignal
}

四、server.go文件

package server

import (
	"embed"
	"github.com/gin-gonic/gin"
	"io/fs"
	"log"
	"net/http"
	"strings"
	"synk/config"
	c "synk/server/controller"
	"synk/server/ws"
)

//go:embed frontend/dist/*
var FS embed.FS

//可以在Go语言应用程序中包含任何文件、目录的内容
//也就是说我们可以把文件以及目录中的内容都打包到生成的Go语言应用程序中了,
//部署的时候,直接扔一个二进制文件就可以了,不用再包含一些静态文件了,因为它们已经被打包到生成的应用程序中了。
//静态文件的Web托管,案例:
//var static embed.FS
//func main() {
//	http.ListenAndServe(":8080", http.FileServer(http.FS(static)))
//}

func Run() {
	hub := ws.NewHub()
	go hub.Run()               //启动websocket服务
	gin.SetMode(gin.DebugMode) //gin开发模式
	//使用不同运行模式方便应对不同场景,比如debug模式下,output format不同,logger也不同。
	router := gin.Default() //创建新的引擎,实现服务器监听状态
	//r := gin.Default() 创建带有默认中间件的路由
	//r :=gin.new()      创建带有没有中间件的路由
	//中间间:将具体业务和底层逻辑解耦的组件。
	//需要利用服务的人(前端写业务的),不需要知道底层逻辑(提供服务的)的具体实现,只要拿着中间件结果来用就好了。
	staticFiles, _ := fs.Sub(FS, "frontend/dist")          //把打包好的静态文件变成一个结构化的目录
	router.POST("/api/v1/files", c.FilesController)        //上传文件
	router.GET("/api/v1/qrcodes", c.QrcodesController)     //将局域网ip变为二维码
	router.GET("/uploads/:path", c.UploadsController)      //下载接口
	router.GET("/api/v1/addresses", c.AddressesController) //获取当前局域网ip
	router.POST("/api/v1/texts", c.TextsController)        //上传文本
	router.GET("/ws", func(c *gin.Context) {               //上下文是一个结构体
		ws.HttpController(c, hub) //websocak,实现手机穿文件到电脑,将http请求升级为websocket
	})
	router.StaticFS("/static", http.FS(staticFiles)) //访问本地文件,加载前端页面
	router.NoRoute(func(c *gin.Context) {            //设置默认路由,防止文件路径出错,如果出错返回404如果该目录下没有文件则显示默认页面index
		path := c.Request.URL.Path
		if strings.HasPrefix(path, "/static/") {
			reader, err := staticFiles.Open("index.html")
			if err != nil {
				log.Fatal(err)
			}
			defer reader.Close() //defer表示go会在恰当时间关闭(垃圾回收机制)
			stat, err := reader.Stat()
			if err != nil {
				log.Fatal(err)
			}
			c.DataFromReader(http.StatusOK, stat.Size(), "text/html;charset=utf-8", reader, nil)
		} else {
			c.Status(http.StatusNotFound) //返回状态码404
		}
	})
	router.Run(":" + config.GetPort()) //监听端口
}

五、controller文件夹,对于server中的5个函数,用于对接前端接口实现相应的功能。

1.AddressesController函数:获取局域网中的IP地址然后传给json

思路:

//1.获取电脑在各个局域网的IP地址
//2.转为json写入HTTP响应

代码:

package controller

import (
	"github.com/gin-gonic/gin"
	"net"
	"net/http"
)

//思路:
//1.获取电脑在各个局域网的IP地址
//2.转为json写入HTTP响应

func AddressesController(c *gin.Context) {
	addrs, _ := net.InterfaceAddrs() //获取当前电脑的所有ip地址
	var result []string
	for _, address := range addrs { //遍历所有ip地址
		// check the address type and if it is not a loopback(回环) the display it
		if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
			if ipnet.IP.To4() != nil { //将地址存储到result切片中
				result = append(result, ipnet.IP.String())
			}
		}
	}
	c.JSON(http.StatusOK, gin.H{"addresses": result}) //作为一个json返回给前端
}

2.FilesController函数:实现上传文件功能

思路:

//1.获取go执行文件所在目录
//2.在该目录创建uploads目录
//3.将文件保存为另一个文件
//4.返回后者的下载路径

代码:

package controller

import (
	"log"
	"net/http"
	"os"
	"path"
	"path/filepath"

	"github.com/gin-gonic/gin"
	"github.com/google/uuid"
)

//思路:
//1.获取go执行文件所在目录
//2.在该目录创建uploads目录
//3.将文件保存为另一个文件
//4.返回后者的下载路径

func FilesController(c *gin.Context) {
	file, err := c.FormFile("raw") //读取用户上传的文件
	if err != nil {
		log.Fatal(err)
	}
	exe, err := os.Executable() //输出一个临时文件的路径
	if err != nil {
		log.Fatal(err)
	}
	dir := filepath.Dir(exe) //用于返回指定路径中除最后一个元素以外的所有元素。
	if err != nil {
		log.Fatal(err)
	}
	filename := uuid.New().String() //创建uploads文件
	uploads := filepath.Join(dir, "uploads")
	err = os.MkdirAll(uploads, os.ModePerm)
	if err != nil {
		log.Fatal(err)
	}
	fullpath := path.Join("uploads", filename+filepath.Ext(file.Filename)) //获取本地文件路径
	fileErr := c.SaveUploadedFile(file, filepath.Join(dir, fullpath))      //存储用户上传的文件
	if fileErr != nil {
		log.Fatal(fileErr)
	}
	c.JSON(http.StatusOK, gin.H{"url": "/" + fullpath})
}

3. QrcodesController函数:将链接转为二维码

思路:

//1.获取文本内容
//2.将文本转为图片(用qrcode库)
//3.将图片写入HTTP响应

代码:

package controller

import (
	"log"
	"net/http"

	"github.com/gin-gonic/gin"
	"github.com/skip2/go-qrcode"
)

//思路:
//1.获取文本内容
//2.将文本转为图片(用qrcode库)
//3.将图片写入HTTP响应

func QrcodesController(c *gin.Context) {
	if content := c.Query("content"); content != "" {
		png, err := qrcode.Encode(content, qrcode.Medium, 256) //把文本编程png
		if err != nil {
			log.Fatal(err)
		}
		c.Data(http.StatusOK, "image/png", png) //把图片传给前端,用于展示
	} else {
		c.Status(http.StatusBadRequest) //否则返回一个错误
	}
}

4.TextsController函数:上传文本

思路:

//1.获取go执行文件所在目录
//2.在该目录创建uploads目录、
//3.将文本保存为一个文件
//4.返回该文件的下载路径

代码:

package controller

import (
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"path"
	"path/filepath"

	"github.com/gin-gonic/gin"
	"github.com/google/uuid"
)

//思路:
//1.获取go执行文件所在目录
//2.在该目录创建uploads目录、
//3.将文本保存为一个文件
//4.返回该文件的下载路径

func TextsController(c *gin.Context) { //上传文本函数实现
	var json struct { //声明json,用户上传的json
		Raw string `json:"raw"`
	}
	if err := c.ShouldBindJSON(&json); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
	} else {
		exe, err := os.Executable() // 获取当前执行文件的路径
		if err != nil {
			log.Fatal(err)
		}
		dir := filepath.Dir(exe) // 获取当前执行文件的目录
		if err != nil {
			log.Fatal(err)
		}
		filename := uuid.New().String()          // 生成一个文件名
		uploads := filepath.Join(dir, "uploads") // 拼接 uploads 的绝对路径
		err = os.MkdirAll(uploads, os.ModePerm)  // 创建 uploads 目录
		if err != nil {
			log.Fatal(err)
		}
		fullpath := path.Join("uploads", filename+".txt")                            // 拼接文件的绝对路径(不含 exe 所在目录)
		err = ioutil.WriteFile(filepath.Join(dir, fullpath), []byte(json.Raw), 0644) // 将 json.Raw 写入文件
		if err != nil {
			log.Fatal(err)
		}
		c.JSON(http.StatusOK, gin.H{"url": "/" + fullpath}) // 返回文件的绝对路径(不含 exe 所在目录)
	}

}

5.UploadsController函数:实现下载功能

思路:

//1.将网络路径:path变成本地绝对路径
//2.读取本地文件,写到HTTP响应中

代码:

package controller

import (
	"log"
	"net/http"
	"os"
	"path/filepath"

	"github.com/gin-gonic/gin"
)

//思路:
//1.将网络路径:path变成本地绝对路径
//2.读取本地文件,写到HTTP响应中

func getUploadsDir() (uploads string) {
	exe, err := os.Executable()
	if err != nil {
		log.Fatal(err)
	}
	dir := filepath.Dir(exe)
	uploads = filepath.Join(dir, "uploads")
	return
}

func UploadsController(c *gin.Context) {
	if path := c.Param("path"); path != "" { //获取路径
		target := filepath.Join(getUploadsDir(), path)
		c.Header("Content-Description", "File Transfer")
		c.Header("Content-Transfer-Encoding", "binary")
		c.Header("Content-Disposition", "attachment; filename="+path)
		c.Header("Content-Type", "application/octet-stream")
		c.File(target) //给前端发送一个文件,各种类型
	} else {
		c.Status(http.StatusNotFound)
	}
}

六、ws部分,用于实现websocket功能

1.什么是websocket:

WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
在这里插入图片描述

2.代码块部分(参考github.com/gorilla/websocket所给出的案例):

1.client.go:

package ws

import (
	"bytes"
	"log"
	"time"

	"github.com/gorilla/websocket"
)

const (
	writeWait      = 10 * time.Second
	pongWait       = 60 * time.Second
	pingPeriod     = (pongWait * 9) / 10
	maxMessageSize = 512
)

var (
	newline = []byte{'\n'}
	space   = []byte{' '}
)

var upgrader = websocket.Upgrader{ //升级websocket读和写的缓存大小
	ReadBufferSize:  1024,
	WriteBufferSize: 1024,
}

// Client is a middleman between the websocket connection and the hub.
type Client struct {
	hub  *Hub
	conn *websocket.Conn
	send chan []byte
}

func (c *Client) readPump() {
	defer func() {
		c.hub.unregister <- c
		c.conn.Close()
	}()
	c.conn.SetReadLimit(maxMessageSize)
	c.conn.SetReadDeadline(time.Now().Add(pongWait))
	c.conn.SetPongHandler(func(string) error { c.conn.SetReadDeadline(time.Now().Add(pongWait)); return nil })
	for {
		_, message, err := c.conn.ReadMessage()
		if err != nil {
			if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
				log.Printf("error: %v", err)
			}
			break
		}
		message = bytes.TrimSpace(bytes.Replace(message, newline, space, -1))
		c.hub.broadcast <- message
	}
}

func (c *Client) writePump() {
	ticker := time.NewTicker(pingPeriod)
	defer func() {
		ticker.Stop()
		c.conn.Close()
	}()
	for {
		select {
		case message, ok := <-c.send:
			c.conn.SetWriteDeadline(time.Now().Add(writeWait))
			if !ok {
				c.conn.WriteMessage(websocket.CloseMessage, []byte{})
				return
			}

			w, err := c.conn.NextWriter(websocket.TextMessage)
			if err != nil {
				return
			}
			w.Write(message)

			// Add queued chat messages to the current websocket message.
			n := len(c.send)
			for i := 0; i < n; i++ {
				w.Write(newline)
				w.Write(<-c.send)
			}

			if err := w.Close(); err != nil {
				return
			}
		case <-ticker.C:
			c.conn.SetWriteDeadline(time.Now().Add(writeWait))
			if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
				return
			}
		}
	}
}

2.http_controller.go:

package ws

import (
	"log"
	"net/http"

	"github.com/gin-gonic/gin"
	"github.com/gorilla/websocket"
)

var wsupgrader = websocket.Upgrader{
	ReadBufferSize:  1024,
	WriteBufferSize: 1024,
	CheckOrigin: func(r *http.Request) bool {
		return true
	},
}

func wshandler(hub *Hub, w http.ResponseWriter, r *http.Request) {
	conn, err := wsupgrader.Upgrade(w, r, nil)
	if err != nil {
		log.Println(err)
		return
	}
	client := &Client{hub: hub, conn: conn, send: make(chan []byte, 256)}
	client.hub.register <- client

	go client.writePump()
	go client.readPump()
}
func HttpController(c *gin.Context, hub *Hub) {
	wshandler(hub, c.Writer, c.Request)
}

3.hub.go:

package ws

import (
	"sync"
)

type Hub struct {
	clients    map[*Client]bool
	broadcast  chan []byte  //广播事件
	register   chan *Client //监听事件
	unregister chan *Client //取消监听
}

func NewHub() *Hub {
	return &Hub{
		broadcast:  make(chan []byte),
		register:   make(chan *Client),
		unregister: make(chan *Client),
		clients:    make(map[*Client]bool),
	}
}

var once sync.Once
var singleton *Hub

func (h *Hub) Run() {
	for {
		select {
		case client := <-h.register: //当有人注册后
			h.clients[client] = true
		case client := <-h.unregister:
			if _, ok := h.clients[client]; ok {
				delete(h.clients, client)
				close(client.send)
			}
		case message := <-h.broadcast:
			for client := range h.clients {
				select {
				case client.send <- message:
				default:
					close(client.send)
					delete(h.clients, client)
				}
			}
		}
	}
}


本章完结,这是一个很好的入门练手项目,如果有任何疑问,欢迎评论区留言或私信探讨。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

立志冲海大

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值