golang服务端与web前端使用websocket通信

前言

        使用go语言开发web服务器时,常常需要web端发送请求给服务端,如果碰到需要长连接的情况,服务端处理http请求往往占用大量资源,而websocket则能使web端和服务端维持长连接。除此之外,建立长连接亦可以使服务端主动向web端推送消息,从而为项目提供更加丰富的功能。

        本文面向初次使用go开发web服务端的读者,建立并完成一个完整的web端与服务端实时通信项目。并在文章末尾贴出了开发中的踩坑点,供初学者参考。

环境

OS:Windows 10 专业版(21H1)

编辑器:vscode

服务端:Go(1.17)

web端:HTML5+JavaScript

浏览器:Edge

正文

目标:

        web端发起websocket连接,服务端侦听到连接,并处理前端的请求。

背景:

        JavaScript完全支持完整的websocket通信,仅需要使用相应函数即可实现通信需求;

        golang对websocket无原生支持,需要以将通信发起之初的http连接升级为websocket连接,其原理相关介绍很多,由于需求场景很多,golang有维护较好的第三方库可供方便使用,本文仅针对需快速实现连接功能需求的读者参考。

前端

目标:

        一个显示服务器时间的段落,一个触发websocket连接的按钮

代码:

<!DOCTYPE html>
<html>

<head>
    <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
    <title>TestWebsocket</title>

    <script type="text/javascript">
        function LinkServer() {
            // 声明连接
            var Server_Com;
            if ("WebSocket" in window) {
                Server_Com = new WebSocket("ws://127.0.0.1:13000/getTime");
                console.log("新建连接到->127.0.0.1:13000");
            }

            // 建立连接后发送
            Server_Com.onopen = function() {

                Server_Com.send("Hello Server!"); // Web Socket 已连接上,使用 send() 方法发送数据
                console.log("已连接上服务器");
            }

            // 接收服务器消息
            Server_Com.onmessage = function(event) {
                var recv_msg = event.data;

                if (recv_msg == "Hello Client!") {
                    console.log("接收到服务器的问候: " + recv_msg); // 用于提示收到信息
                } else {
                    document.getElementById("Time").textContent = recv_msg; // 实时更新显示服务器发回的时间
                    console.log("接收到服务器数据: " + recv_msg);
                }
            }
        }
    </script>
</head>

<body>
    <p>服务器时间:</p>
    <p id="Time">2022-03-02 20:00:00</p>

    <button onclick="LinkServer()">连接</button>

</body>

</html>

按F12可显示console.log内容。

服务器

目标:

        监听web发来的请求,升级为websocket通讯协议,发送打招呼信息,循环发送当前时间

golang的官方websocket库未得到很好的维护,且功能及稳定性不如第三方开源的gorilla 

golang原生支持http连接,websocket连接从http连接升级而来,每次创建websocket连接前,需要首先升级在http连接上,之后可维持websocket连接。

 golang需要使用http.HandleFunc来开启http监听,它将在收到http请求后执行函数。同时,升级websocket连接也需要在函数开始执行后才能创建。所以在go中会有net/http包来分配及启动监听,gorrila/websocket包来创建websocket连接。

代码:

package main

import (
	"fmt"
	"net/http"
	"time"

	"./websocket-master" // "github.com/gorrila/websocket"连接不上,我直接下载了源码
)

var upgrader = websocket.Upgrader{
	// 解决跨域问题
	CheckOrigin: func(r *http.Request) bool {
		return true
	},
}

func main() {

	// 注册服务器事件
	http.HandleFunc("/getTime", SendTime) // 支持多端同时访问,相当于收到不同请求后未每个请求分别创建线程处理
	fmt.Println("已向 DefaultServeMux 注册 SendTime 事件")

	// 打开要侦听的前端请求连接端口
	err := http.ListenAndServe(":13000", nil) // 这里是接受对本机所有的通过13000的请求,本机有多个IP则可在端口号前加IP地址区分,如"10.11.100.123:13000"
	if err != nil {
		fmt.Println("端口侦听错误:", err)
	}

}

func SendTime(w http.ResponseWriter, r *http.Request) {
	c, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		fmt.Print("升级websocket连接错误:", err)
		return
	}

	defer c.Close()

	// 接受消息
	mt, message, err := c.ReadMessage()
	if err != nil {
		fmt.Println("接收错误:", err)
	} else {

		fmt.Println("接收到前端消息:", string(message))

		// 发送问候
		var sendbuff string = "Hello Client!"
		c.WriteMessage(mt, []byte(sendbuff)) // 为阅读方便,省去错误处理

		// 不停地循环发送时间
		fmt.Println("开始发送时间")
		for {
			sendbuff = time.Now().Format("2006-01-02 15:04:05")
			err = c.WriteMessage(mt, []byte(sendbuff))
			if err != nil {
				fmt.Println("发送错误:", err)
				break
			}

			fmt.Println("发送时间:", sendbuff)
			time.Sleep(1 * time.Second) // 间隔1s,降低资源消耗,如果想提高实时性可减少时间间隔
		}

		fmt.Println("停止发送时间")
	}
}

测试

       先启动服务器,再打开test.html并打开调试,点击网页上的“连接”按钮,观察控制台与服务器运行窗口(这里是vscode的集成终端)。

浏览器显示

服务器运行

 

踩坑


问题表现:

  • 前端发起请求后没有任何响应

问题细节:

  • web端发出了websocket连接请求,浏览器控制台显示failed

         

  • 服务端没有任何提示 

问题原因:

web端的websocket参数与服务端不一致,请求的连接没有触发服务端的事件响应

  • 服务端的问题代码及修改:
http.HandleFunc("/echo", echo)  
// 函数的第一个传入参数为事件名称(从http请求中读取),第二个传入参数为事件
// 事件名称默认为"/"

// 函数格式(附):
func http.HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request))
  • web端的代码:
// 原始代码
Server_Com = new WebSocket("ws://127.0.0.1:12033");

解决:

修改服务端或web端代码均可,以使两端事件统一

  • 服务端修改:
// 修改为
http.HandleFunc("/", echo)  
  • web端修改:
// 修改
Server_Com = new WebSocket("ws://127.0.0.1:12033/echo");
### WebSocket 服务端实现与配置 WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议,广泛用于实时数据传输场景。以下是几种常见的技术栈下 WebSocket 服务端的实现方式。 #### Spring Boot 实现 WebSocket 服务端 Spring Boot 提供了内置支持来简化 WebSocket 的开发过程。通过创建一个类似于 HTTP Controller 的类,可以处理 WebSocket 请求[^1]。 下面是一个简单的例子: ```java import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.config.annotation.EnableWebSocket; import org.springframework.web.socket.config.annotation.WebSocketConfigurer; import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; @Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(new MyWebSocketHandler(), "/my-websocket").setAllowedOrigins("*"); } } ``` 其中 `MyWebSocketHandler` 类继承自 `TextWebSocketHandler` 或其他相关处理器,用来管理消息接收、发送以及连接事件。 非加密访问可以通过 URL 如 `ws://localhost:8080/my-websocket` 完成;如果需要启用 SSL/TLS,则应使用 WSS 协议,例如 `wss://localhost:8080/my-websocket`[^3]。 --- #### 基于 Netty 实现 WebSocket 服务端 Netty 是一款高性能网络框架,它提供了对 WebSocket 和 HTTP 协议的支持。利用其编解码器和 Handler 可以轻松构建 WebSocket 服务器[^2]。 以下是一个基本的服务端代码片段: ```java import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.codec.http.HttpServerCodec; import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; import io.netty.channel.socket.SocketChannel; public class WebSocketServer { private final int port; public WebSocketServer(int port) { this.port = port; } public void start() throws Exception { EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) { ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast(new HttpServerCodec()); pipeline.addLast(new HttpObjectAggregator(65536)); pipeline.addLast(new WebSocketServerProtocolHandler("/websocket")); pipeline.addLast(new TextWebSocketFrameHandler()); } }); ChannelFuture f = b.bind(port).sync(); System.out.println("WebSocket server started at ws://localhost:" + port); f.channel().closeFuture().sync(); } finally { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } static class TextWebSocketFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> { @Override protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) { String request = msg.text(); System.out.println("Received message: " + request); ctx.channel().writeAndFlush(new TextWebSocketFrame("Echo: " + request)); } } public static void main(String[] args) throws Exception { new WebSocketServer(8080).start(); } } ``` 上述代码展示了如何设置管道并响应客户端的消息。 --- #### Golang 中的 WebSocket 服务端实现 Go 语言中的 Gorilla 工具包是常用的 WebSocket 库之一。它可以方便地集成到现有的 Web 框架中,并且能够很好地解决并发问题。然而,在实际应用中需要注意认证等问题[^4]。 下面是 Go 版本的一个简单示例: ```go package main import ( "fmt" "log" "net/http" "github.com/gorilla/websocket" ) var upgrader = websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, CheckOrigin: func(r *http.Request) bool { return true }, } func echo(w http.ResponseWriter, r *http.Request) { conn, err := upgrader.Upgrade(w, r, nil) if err != nil { log.Println(err) return } defer conn.Close() for { messageType, message, err := conn.ReadMessage() if err != nil { log.Println(err) break } fmt.Printf("Received: %s\n", message) err = conn.WriteMessage(messageType, []byte("Echo: "+string(message))) if err != nil { log.Println(err) break } } } func main() { http.HandleFunc("/echo", echo) log.Fatal(http.ListenAndServe(":8080", nil)) } ``` 为了增强安全性,可以在握手阶段附加查询参数或者依赖 Cookie 来传递令牌信息。 --- #### 总结 无论选择哪种编程语言和技术栈,都需要考虑以下几个方面: - **安全机制**:如 HTTPS/WSS 支持、身份验证等。 - **性能优化**:针对高并发环境下的资源管理和线程池设计。 - **错误处理**:合理捕获异常情况并返回友好的提示给前端用户。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值