Golang实现向客户端推送QR码的技术探讨

Golang实现向客户端推送QR码的技术探讨 尝试制作类似 WhatsApp 的登录界面,将二维码推送给客户端进行扫描,因此我编写了以下服务器端代码:

package main

import (
	"context"
	"fmt"
	"net/http"
	"os"
	"os/signal"
	"syscall"

	_ "github.com/mattn/go-sqlite3"
	"github.com/skip2/go-qrcode"
	"google.golang.org/protobuf/proto"

	"go.mau.fi/whatsmeow"
	"go.mau.fi/whatsmeow/store"
	"go.mau.fi/whatsmeow/store/sqlstore"
	"go.mau.fi/whatsmeow/types/events"
	waLog "go.mau.fi/whatsmeow/util/log"
)

func eventHandler(evt interface{}) {
	switch v := evt.(type) {
	case *events.Message:
		fmt.Println("Received a message!", v.Message.GetConversation())
	}
}

func main() {
	passer := &DataPasser{logs: make(chan string)}

	http.HandleFunc("/", passer.handleHello)
	go http.ListenAndServe(":9999", nil)

	store.DeviceProps.Os = proto.String("WhatsApp GO")
	dbLog := waLog.Stdout("Database", "DEBUG", true)
	// 确保添加了适当的数据库连接器导入,例如用于 SQLite 的 github.com/mattn/go-sqlite3
	container, err := sqlstore.New("sqlite3", "file:datastore.db?_foreign_keys=on", dbLog)
	if err != nil {
		panic(err)
	}
	// 如果需要多个会话,请记住它们的 JID 并使用 .GetDevice(jid) 或 .GetAllDevices()。
	deviceStore, err := container.GetFirstDevice()
	if err != nil {
		panic(err)
	}
	clientLog := waLog.Stdout("Client", "DEBUG", true)
	client := whatsmeow.NewClient(deviceStore, clientLog)
	client.AddEventHandler(eventHandler)

	if client.Store.ID == nil {
		// 未存储 ID,新登录
		qrChan, _ := client.GetQRChannel(context.Background())
		err = client.Connect()
		if err != nil {
			panic(err)
		}

		for evt := range qrChan {
			switch evt.Event {
			case "success":
				{
					passer.logs <- "success"
					fmt.Println("Login event: success")
				}
			case "timeout":
				{
					passer.logs <- "timeout"
					fmt.Println("Login event: timeout")
				}
			case "code":
				{
					passer.logs <- "new code"
					fmt.Println("new code recieved")
					img, err := qrcode.Encode(evt.Code, qrcode.Medium, 200) // evt.Code
					if err != nil {
						fmt.Println("error when write qrImage", err.Error())
					}
					passer.logs <- string(img)
				}
			}
		}
	} else {
		// 已登录,直接连接
		passer.logs <- "Already logged"
		fmt.Println("Already logged")
		err = client.Connect()
		if err != nil {
			panic(err)
		}
	}

	// 监听 Ctrl+C(你也可以执行其他防止程序退出的操作)
	c := make(chan os.Signal, 1)
	signal.Notify(c, os.Interrupt, syscall.SIGTERM)
	<-c

	client.Disconnect()
}

以及以下 API:

package main

import (
	"bytes"
	"fmt"
	"log"
	"net/http"
	"strconv"
	"sync"
)

var (
	mux sync.Mutex
)

type Result struct {
	ResultType, Result string
}

type DataPasser struct {
	logs chan string
}

func (p *DataPasser) handleHello(w http.ResponseWriter, r *http.Request) {
	setupCORS(&w, r)
	w.Header().Set("Content-Type", "text/event-stream")
	flusher, ok := w.(http.Flusher)
	if !ok {
		http.Error(w, "Internal error", 500)
		return
	}
	flusher.Flush()
	done := r.Context().Done()
	defer fmt.Println("EXIT")
	for {
		select {
		case <-done:
			// 客户端断开连接
			return
		case m := <-p.logs:
			fmt.Println(m)
			if m == "new code" || m == "We are logging in" || m == "Already logged" || m == "timeout" || m == "success" {
				if _, err := fmt.Fprintf(w, "data: %s\n\n", m); err != nil {
					// 写入连接失败。后续写入可能也会失败。
					return
				}
				flusher.Flush()
			} else {
				mux.Lock()

				buffer := bytes.NewBuffer([]byte(m))

				/*	img, _, err := image.Decode(buffer)
					if err != nil {
						fmt.Println("err: ", err)
					}

					if err := jpeg.Encode(buffer, img, nil); err != nil {
						log.Println("unable to encode image.")
					} */

				w.Header().Set("Content-Type", "image/jpeg")
				w.Header().Set("Content-Length", strconv.Itoa(len(buffer.Bytes())))
				if _, err := w.Write(buffer.Bytes()); err != nil {
					log.Println("unable to write image.")
				}
				mux.Unlock()
				flusher.Flush()
			}
		}
	}
}

func setupCORS(w *http.ResponseWriter, req *http.Request) {
	(*w).Header().Set("Cache-Control", "no-cache")
	(*w).Header().Set("Connection", "keep-alive")
	(*w).Header().Set("Access-Control-Allow-Origin", "*")
	(*w).Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
	(*w).Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
}

但在客户端读取时遇到了困难,我尝试了类似下面的方法,但迷失了方向:

<html>
<head></head>
<body>
    note: <span id="content"></span><br>
    <img id="photo" style="display: block;-webkit-user-select: none;">
</body>
    <script>
      /*  (function(){
                document.getElementById("content").innerHTML='<object type="text/html" data="http://localhost:9999/" ></object>';
        })(); 
*/
        const myRequest = new Request('http://127.0.0.1:9999/', {
        method: 'GET',
        headers: new Headers(),
        type: "arraybuffer",
        mode: 'cors',
        cache: 'default',
        });  

        var source = new EventSource("http://127.0.0.1:9999/");
        source.onmessage = function (event) {
            console.log(event)
            var counter = event.data; // JSON.parse(event.data);
            document.getElementById("content").innerHTML = counter;
        }

    fetch(myRequest).then(response => {
        console.log(response)
        console.log(response.headers)
    const contentType = response.headers.get("content-type");
        if (contentType && contentType.indexOf("application/json") !== -1) {
            return response.json().then(data => {
            var obj = JSON.parse(str);
            console.log(obj)
            // 将数据作为 JavaScript 对象处理
            });
        }  if (contentType && contentType.indexOf("image/jpeg") !== -1) {
            console.log("Image received")
            return response.blob().then(data => {
                var reader = new FileReader();
                reader.readAsDataURL(blob); 
                reader.onloadend = function() {
                    var imageUrl = reader.result;                
                    var img = document.querySelector("#photo");
                    img.src = imageUrl;
                }
            });
        } else if (contentType && contentType.indexOf("text/event-stream") !== -1) {
            return response.text().then(text => {
            console.log(text)
            var source = new EventSource("http://localhost:9999/");
            source.onmessage = function (event) {
                var response = event.data // JSON.parse(event.data);
                document.getElementById("content").innerHTML = counter;
            } 
            // 将文本作为字符串处理
            });
        } else if (contentType && contentType.indexOf("text/html") !== -1) {
            return response.text().then(text => {
            console.log(text)
            var source = new EventSource("http://localhost:9999/");
            source.onmessage = function (event) {
                var response = event.data // JSON.parse(event.data);
                document.getElementById("content").innerHTML = counter;
            } 
            // 将文本作为字符串处理
            });
        } 
    });

    </script>
</html>

如果我运行并查看显示的页面,会看到:

enter image description here

有什么帮助吗?


更多关于Golang实现向客户端推送QR码的技术探讨的实战教程也可以访问 https://www.itying.com/category-94-b0.html

2 回复

我通过将二维码字符串推送到服务器,并使用 qrcodejs 将其转换为二维码来解决这个问题。如果有人感兴趣,我的完整代码如下:

// main.go
package main

import (
	"context"
	"fmt"
	"net/http"
	"os"
	"os/signal"
	"syscall"

	_ "github.com/mattn/go-sqlite3"
	"google.golang.org/protobuf/proto"

	"go.mau.fi/whatsmeow"
	"go.mau.fi/whatsmeow/store"
	"go.mau.fi/whatsmeow/store/sqlstore"
	"go.mau.fi/whatsmeow/types/events"
	waLog "go.mau.fi/whatsmeow/util/log"
)

func eventHandler(evt interface{}) {
	switch v := evt.(type) {
	case *events.Message:
		fmt.Println("Received a message!", v.Message.GetConversation())
	}
}

func main() {
	passer := &DataPasser{logs: make(chan string)}

	http.HandleFunc("/sse/dashboard", passer.handleHello)
	go http.ListenAndServe(":1234", nil)
	/*
		ticker := time.NewTicker(500 * time.Millisecond)
		defer ticker.Stop()
		done := make(chan bool)
		go func() {
			for {
				select {
				case <-done:
					return
				case <-ticker.C:
					//	fmt.Println("Tick at", t)
					// passer.logs <- buffer.String()
				}
			}
		}()
	*/
	store.DeviceProps.Os = proto.String("WhatsApp GO")
	dbLog := waLog.Stdout("Database", "DEBUG", true)
	// Make sure you add appropriate DB connector imports, e.g. github.com/mattn/go-sqlite3 for SQLite
	container, err := sqlstore.New("sqlite3", "file:datastore.db?_foreign_keys=on", dbLog)
	if err != nil {
		panic(err)
	}
	// If you want multiple sessions, remember their JIDs and use .GetDevice(jid) or .GetAllDevices() instead.
	deviceStore, err := container.GetFirstDevice()
	if err != nil {
		panic(err)
	}
	clientLog := waLog.Stdout("Client", "DEBUG", true)
	client := whatsmeow.NewClient(deviceStore, clientLog)
	client.AddEventHandler(eventHandler)

	if client.Store.ID == nil {
		// No ID stored, new login
		qrChan, _ := client.GetQRChannel(context.Background())
		err = client.Connect()
		if err != nil {
			panic(err)
		}

		for evt := range qrChan {
			switch evt.Event {
			case "success":
				{
					passer.logs <- "success"
					fmt.Println("Login event: success")
				}
			case "timeout":
				{
					passer.logs <- "timeout"
					fmt.Println("Login event: timeout")
				}
			case "code":
				{
					fmt.Println("new code recieved")
					fmt.Println(evt.Code)
					passer.logs <- evt.Code
				}
			}
		}
	} else {
		// Already logged in, just connect
		passer.logs <- "Already logged"
		fmt.Println("Already logged")
		err = client.Connect()
		if err != nil {
			panic(err)
		}
	}

	// Listen to Ctrl+C (you can also do something else that prevents the program from exiting)
	c := make(chan os.Signal, 1)
	signal.Notify(c, os.Interrupt, syscall.SIGTERM)
	<-c

	client.Disconnect()
}

以及

// api.go
package main

import (
	"fmt"
	"net/http"
	"sync"
)

var (
	mux sync.Mutex
)

type DataPasser struct {
	logs chan string
}

func (p *DataPasser) handleHello(w http.ResponseWriter, r *http.Request) {
	fmt.Println("from here")
	flusher, ok := w.(http.Flusher)
	if !ok {
		http.Error(w, "Internal error", 500)
		return
	}
	w.Header().Set("Content-Type", "text/event-stream")
	w.Header().Set("Cache-Control", "no-cache")
	w.Header().Set("Connection", "keep-alive")
	setupCORS(&w, r)

	for {
		select {
		case c := <-p.logs:
			fmt.Println("recieved")
			mux.Lock()
			//counter++
			//c := counter
			mux.Unlock()
			fmt.Fprintf(w, "data: %v\n\n", c)
			flusher.Flush()
		case <-r.Context().Done():
			fmt.Println("Connection closed")
			return
		}
	}
}

func setupCORS(w *http.ResponseWriter, req *http.Request) {
	(*w).Header().Set("Cache-Control", "no-cache")
	(*w).Header().Set("Access-Control-Allow-Origin", "*")
	(*w).Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
	(*w).Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
}

客户端代码如下:

<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">

  <script src="https://cdn.rawgit.com/davidshimjs/qrcodejs/gh-pages/qrcode.min.js"></script>
</head>

<body>
  <h5>Server message/Code: <span id="message"></span></h5>
  <div id="qr"></div>
  <script>
    var source = new EventSource("http://localhost:1234/sse/dashboard");
    source.onmessage = function (event) {
        var message = event.data
        document.querySelector('#message').innerHTML = message;
        if (new String(message).valueOf() == "success" || new String(message).valueOf() == "timeout"
            || new String(message).valueOf() == "Already logged") {
            document.querySelector('#qr').innerHTML = "";
        } else {
            var qrcode = new QRCode("qr", {
                text: message,
                width: 128,
                height: 128,
                colorDark : "#000000",
                colorLight : "#ffffff",
                correctLevel : QRCode.CorrectLevel.M
            });
        }
    }

/*
    var qrcode = new QRCode(
    "qr",
        [
            "BEGIN:VCARD",
            "VERSION:2.1",
            "N:Doe;John;;Dr;",
            "FN:Dr. John Doe",
            "EMAIL:johndoe@hotmail.com",
            "TEL;TYPE=cell:(123) 555-5832",
            "END:VCARD"
        ].join("\r\n")
    ); */
    

  </script>
</body>
</html>

更多关于Golang实现向客户端推送QR码的技术探讨的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


你的服务器端代码混合了SSE和直接图片响应,这导致了客户端处理困难。主要问题是SSE连接和图片响应使用了相同的HTTP端点,但返回了不同类型的内容。以下是修正后的方案:

服务器端修正:

package main

import (
	"bytes"
	"context"
	"encoding/base64"
	"fmt"
	"net/http"
	"os"
	"os/signal"
	"sync"
	"syscall"
	"time"

	_ "github.com/mattn/go-sqlite3"
	"github.com/skip2/go-qrcode"
	"google.golang.org/protobuf/proto"

	"go.mau.fi/whatsmeow"
	"go.mau.fi/whatsmeow/store"
	"go.mau.fi/whatsmeow/store/sqlstore"
	"go.mau.fi/whatsmeow/types/events"
	waLog "go.mau.fi/whatsmeow/util/log"
)

type QRCodeData struct {
	Event string `json:"event"`
	Data  string `json:"data,omitempty"`
}

type DataPasser struct {
	clients map[chan QRCodeData]bool
	mu      sync.RWMutex
}

func NewDataPasser() *DataPasser {
	return &DataPasser{
		clients: make(map[chan QRCodeData]bool),
	}
}

func (p *DataPasser) Broadcast(data QRCodeData) {
	p.mu.RLock()
	defer p.mu.RUnlock()
	
	for client := range p.clients {
		select {
		case client <- data:
		default:
		}
	}
}

func (p *DataPasser) AddClient(client chan QRCodeData) {
	p.mu.Lock()
	defer p.mu.Unlock()
	p.clients[client] = true
}

func (p *DataPasser) RemoveClient(client chan QRCodeData) {
	p.mu.Lock()
	defer p.mu.Unlock()
	delete(p.clients, client)
}

func (p *DataPasser) handleSSE(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "text/event-stream")
	w.Header().Set("Cache-Control", "no-cache")
	w.Header().Set("Connection", "keep-alive")
	w.Header().Set("Access-Control-Allow-Origin", "*")
	
	flusher, ok := w.(http.Flusher)
	if !ok {
		http.Error(w, "SSE not supported", http.StatusInternalServerError)
		return
	}
	
	clientChan := make(chan QRCodeData, 10)
	p.AddClient(clientChan)
	defer p.RemoveClient(clientChan)
	
	flusher.Flush()
	
	for {
		select {
		case <-r.Context().Done():
			return
		case data := <-clientChan:
			fmt.Fprintf(w, "event: %s\n", data.Event)
			fmt.Fprintf(w, "data: %s\n\n", data.Data)
			flusher.Flush()
		}
	}
}

func main() {
	passer := NewDataPasser()
	
	http.HandleFunc("/sse", passer.handleSSE)
	go http.ListenAndServe(":9999", nil)

	store.DeviceProps.Os = proto.String("WhatsApp GO")
	dbLog := waLog.Stdout("Database", "DEBUG", true)
	
	container, err := sqlstore.New("sqlite3", "file:datastore.db?_foreign_keys=on", dbLog)
	if err != nil {
		panic(err)
	}
	
	deviceStore, err := container.GetFirstDevice()
	if err != nil {
		panic(err)
	}
	
	clientLog := waLog.Stdout("Client", "DEBUG", true)
	client := whatsmeow.NewClient(deviceStore, clientLog)
	
	if client.Store.ID == nil {
		qrChan, _ := client.GetQRChannel(context.Background())
		err = client.Connect()
		if err != nil {
			panic(err)
		}

		for evt := range qrChan {
			switch evt.Event {
			case "success":
				passer.Broadcast(QRCodeData{Event: "status", Data: "success"})
				fmt.Println("Login event: success")
			case "timeout":
				passer.Broadcast(QRCodeData{Event: "status", Data: "timeout"})
				fmt.Println("Login event: timeout")
			case "code":
				fmt.Println("new code received")
				
				// 生成二维码图片
				qrImage, err := qrcode.Encode(evt.Code, qrcode.Medium, 256)
				if err != nil {
					fmt.Println("error generating QR code:", err)
					continue
				}
				
				// 转换为base64
				base64Str := base64.StdEncoding.EncodeToString(qrImage)
				
				// 广播二维码数据
				passer.Broadcast(QRCodeData{
					Event: "qrcode",
					Data:  base64Str,
				})
			}
		}
	} else {
		passer.Broadcast(QRCodeData{Event: "status", Data: "already_logged"})
		fmt.Println("Already logged")
		err = client.Connect()
		if err != nil {
			panic(err)
		}
	}

	c := make(chan os.Signal, 1)
	signal.Notify(c, os.Interrupt, syscall.SIGTERM)
	<-c
	client.Disconnect()
}

客户端HTML修正:

<!DOCTYPE html>
<html>
<head>
    <title>WhatsApp QR Login</title>
    <style>
        #status {
            padding: 10px;
            margin: 10px 0;
            border-radius: 5px;
        }
        .success { background-color: #d4edda; color: #155724; }
        .error { background-color: #f8d7da; color: #721c24; }
        .info { background-color: #d1ecf1; color: #0c5460; }
    </style>
</head>
<body>
    <div id="status"></div>
    <div id="qrcode-container">
        <img id="qrcode" style="display: none; max-width: 300px;">
    </div>

    <script>
        let eventSource = null;
        
        function updateStatus(message, type = 'info') {
            const statusDiv = document.getElementById('status');
            statusDiv.textContent = message;
            statusDiv.className = type;
        }
        
        function connectSSE() {
            if (eventSource) {
                eventSource.close();
            }
            
            eventSource = new EventSource('http://localhost:9999/sse');
            
            eventSource.addEventListener('status', function(event) {
                const data = event.data;
                console.log('Status:', data);
                
                switch(data) {
                    case 'success':
                        updateStatus('登录成功!', 'success');
                        document.getElementById('qrcode').style.display = 'none';
                        break;
                    case 'timeout':
                        updateStatus('二维码已过期,正在重新生成...', 'error');
                        document.getElementById('qrcode').style.display = 'none';
                        setTimeout(() => connectSSE(), 2000);
                        break;
                    case 'already_logged':
                        updateStatus('已登录', 'success');
                        break;
                    default:
                        updateStatus(data);
                }
            });
            
            eventSource.addEventListener('qrcode', function(event) {
                const base64Image = event.data;
                console.log('Received QR code');
                
                const qrImg = document.getElementById('qrcode');
                qrImg.src = 'data:image/png;base64,' + base64Image;
                qrImg.style.display = 'block';
                qrImg.alt = 'WhatsApp QR Code';
                
                updateStatus('请使用WhatsApp扫描二维码登录', 'info');
            });
            
            eventSource.onerror = function(error) {
                console.error('SSE Error:', error);
                updateStatus('连接断开,正在重连...', 'error');
                setTimeout(() => connectSSE(), 3000);
            };
        }
        
        // 页面加载时连接
        document.addEventListener('DOMContentLoaded', function() {
            updateStatus('正在连接服务器...', 'info');
            connectSSE();
        });
        
        // 页面卸载时关闭连接
        window.addEventListener('beforeunload', function() {
            if (eventSource) {
                eventSource.close();
            }
        });
    </script>
</body>
</html>

关键修正点:

  1. 分离SSE端点:创建专门的/sse端点处理SSE连接
  2. 结构化数据格式:使用JSON格式传输状态和二维码数据
  3. Base64编码图片:将二维码图片转换为base64字符串通过SSE传输
  4. 事件类型区分:使用不同的事件类型(status/qrcode)区分消息类型
  5. 客户端重连机制:添加SSE连接错误时的自动重连
  6. 状态显示:添加用户友好的状态提示

这个方案确保了:

  • 纯SSE连接,不混合响应类型
  • 二维码以base64格式通过SSE传输
  • 客户端可以正确解析和显示二维码
  • 提供完整的登录状态反馈
  • 支持自动重连和错误处理
回到顶部