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>
如果我运行并查看显示的页面,会看到:

有什么帮助吗?
更多关于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>
关键修正点:
- 分离SSE端点:创建专门的
/sse端点处理SSE连接 - 结构化数据格式:使用JSON格式传输状态和二维码数据
- Base64编码图片:将二维码图片转换为base64字符串通过SSE传输
- 事件类型区分:使用不同的事件类型(status/qrcode)区分消息类型
- 客户端重连机制:添加SSE连接错误时的自动重连
- 状态显示:添加用户友好的状态提示
这个方案确保了:
- 纯SSE连接,不混合响应类型
- 二维码以base64格式通过SSE传输
- 客户端可以正确解析和显示二维码
- 提供完整的登录状态反馈
- 支持自动重连和错误处理

