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)
}
}
}
}
}
本章完结,这是一个很好的入门练手项目,如果有任何疑问,欢迎评论区留言或私信探讨。