【编写中】html5+go+websocket不到150行代码,实现一个在线实时聊天的功能

news2024/11/24 4:19:09

阮一峰websocket
相关参考

websocket

什么是websocket

在了解什么是websocket之前,我们下说一说http,因为HTTP我们太熟了。我们知道,HTTP是一种基于应用层的网络协议,往往都是一个请求,一个相应。websocket呢,也是一种基于应用层的网络协议,但是它不仅可以实现请求-相应这种模式,还可以实现主动推送,即你不请求,我也可以给你发消息通知。它实现了浏览器与服务器之间的双工通信。浏览器和服务器只需要完成一次握手,两者就可以创建一个持续的链接。

为什么出现websocket

  • http满足了我们大部分的需求,比如浏览网页 图片,视频,音频等等,我想要什么内容,我就给服务器发什么请求。
  • 但是随着互联网的发展,我们有了网络聊天的功能,在最早时候,要看对方有没有给你发送消息,得需要ajax轮询来实现,还有一些其他需要实时推送消息的场景,比如股票价格,体育解说,弹幕,直播这种,都需要服务器来主动推送消息到客户端。http不再能满足我们的需求。所以,在2008年,websocket诞生了。2011年成为了国际标准。

websocket特点

  • websocket最大的特点就是实现了浏览器和服务端之间的双工通信,更好的支持实时通信。

  • websocket头部信息很少,一般只有2bytes左右,节省了网络IO。

  • 因为websocket大部分的使用场景也是在浏览器中使用,HTTP、WebSocket 等应用层协议,都是基于 TCP 协议来传输数据的,因此其连接和断开,都要遵循 TCP 协议中的三次握手和四次挥手 ,所以websocket复用了http的握手机制,所以很好的兼容了http,默认端口也是80和443,复用 HTTP 的 Upgrade 机制,完成升级协议的协商过程,能通过各种http代理服务器。

  • 可以发送二进制数据

  • 没有同源策略,客户端可以和任意服务器通信。

  • 协议标识符是ws或者wss(加密),比如:ws://example.com:80/some/path

  • WebSocket API 是 HTML5 标准的一部分, 但这并不代表 WebSocket 一定要用在 HTML 中,或者只能在基于浏览器的应用程序中使用。 实际上,许多语言、框架和服务器都提供了 WebSocket 支持,例如 Nginx Apache C C++ Node Python等

websocket的工作原理
  • 我们先来启动一个简单的websocket服务,代码非常简单,大家理解完代码之后,再来下一步分析
    服务端代码
package main

import (
	"fmt"
	"github.com/gorilla/websocket"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"time"
)
//显示页面
func rootHandler(w http.ResponseWriter, r *http.Request) {
	file, err := ioutil.ReadFile("othor-project/websocket-demo/index.html")
	if err != nil {
		fmt.Println(os.Getwd())
		fmt.Println(err)
	}
	_, err = fmt.Fprintf(w, "%s", file)
	if err != nil {
		fmt.Println(err)
	}
}

//ws连接
func wsHandler(w http.ResponseWriter, r *http.Request) {
	//声明协议是websocket
	var upgrader = websocket.Upgrader{} //定义一个websocket
	ws, err := upgrader.Upgrade(w, r, nil)//初始化
	fmt.Println("i got a websocket")
	if err != nil {
		log.Print("upgrade:", err)
		return
	}
	ws.PongHandler()
	//主动发送消息给浏览器
	for i:=0;i<3;i++ {
		time.Sleep(time.Second*2)
		str:=fmt.Sprintf("hello i am %d",i)
		fmt.Println(str)
		err := ws.WriteMessage(websocket.TextMessage, []byte(str))
		if err != nil {
			fmt.Println("send message err")
			fmt.Println(err)
		}
	}

	//监听消息
	for  {
		_, i, err := ws.ReadMessage()
		if err != nil {
			fmt.Println("read msg error")
			fmt.Println(err)
		}
		fmt.Println(string(i))

	}
}

func main() {
	fmt.Println("server start")
	//展示页面
	http.HandleFunc("/", rootHandler)
	//ws连接
	http.HandleFunc("/ws", wsHandler)
	//先开启端口监听
	err := http.ListenAndServe(":30000", nil)
	if err != nil {
		fmt.Println(err)
	}
}

我们简单了解一下h5怎么操作websocket
在这里插入图片描述
我们使用var ws = new WebSocket("ws://hostname/path", ["protocol1", "protocol2"])来建立一个连接。第一个参数是服务端websocket地址,如果是https+websocket,那么前缀写成wss。第二个参数并不是必须的,它约定了双方通讯使用的自定义子协议,会被放到这个Header中: Sec-WebSocket-Protocol。子协议在某些场合是很必要的,例如服务端要与多个客户端版本兼容,那么若干个版本之后,服务端设定支持子协议 v1.5, v2.0, 而客户端发送的却是 v1.0,那么他们就可以在握手阶段失败,不会继续通信下去导致奇奇怪怪的错误。

WebSocket构造函数只有两个变量,不能提供通过设置自定义Header的方式来携带其它信息,那么我们如何对ws的连接认证呢?可以通过以下方式实现认证:

  • 通过ws地址填写形如 ws://username:password@hostname/path, 即构造出了 Authorization Header
  • 通过ws地址填写形如 ws://:password@hostname/path ,即构造出了 Bearer Token Header
  • 通过在Cookie中加入值,也能够携带额外的信息
  • 在websocket连接建立后,再通过自定义的认证协议,走websocket进行认证。

客户端代码

<script type="text/javascript">
    var mess = document.getElementById("mess");
    // 1 创建一个ws连接
    var ws = new WebSocket("ws://" + location.host + "/ws")
    //监听消息
    ws.onmessage = function (event) {
        var chat = event.data;
        var res = chat.toString();
        console.log(res);
    }

    ws.onopen = function()
    {
        // Web Socket 已连接上,使用 send() 方法发送数据
        ws.send("i am client")
    };

</script>

启动后的效果,这是服务端往客户端推送的消息
在这里插入图片描述
这是客户端往服务端推送的消息
在这里插入图片描述

  • 建立连接
    具体连接过程如下图(ws表示协议头,101表示协议切换,upgrade表示要升级协议,upgrade:websocket 表示要升级到websocket协议,Sec-WebSocket-Key表示),websocket-key和websocket-accept之间的加密方式是base64(hsa1(sec-websocket-key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11)),如果这个Sec-WebSocket-Accept计算错误浏览器会提示Sec-WebSocket-Accept dismatch,如果返回成功,Websocket就会回调onopen事件。注意,每个ws的的key都是唯一的,所以如果你刷新了页面,ws的key也会刷新,服务端就会认为是新连接。
    在这里插入图片描述

  • 交换数据
    具体的数据格式是怎么样的呢?WebSocket 的每条消息可能会被切分成多个数据帧(最小单位)。发送端会将消息切割成多个帧发送给接收端,接收端接收消息帧,并将关联的帧重新组装成完整的消息。

  • 保持连接
    websocket使用心跳机制(ping和pong)来保持正常通信。定时发送一个数据包,让对方知道自己在线且正常工作。如果对方无法相应,则可以弃用旧连接,开启新连接。

群聊服务

  • 基于上面的知识,我们写一个简单的群聊系统
    服务端代码
package main

import (
	"encoding/json"
	"fmt"
	"github.com/gorilla/websocket"
	"io/ioutil"
	"log"
	"net/http"
	"os"
)

//定义一个聊天的结构体,姓名和信息
type Msg struct {
	Uid string `json:"uid"`
	Msg string `json:"msg"`
}

//定义一个全局的channel用来接收消息
var msgChan = make(chan *Msg, 10)
var upgrader = websocket.Upgrader{} // use default options
//全局变量用来存储用户连接 用于发送消息
var clients = make(map[*websocket.Conn]int)

//用来存放用户连接,读取完毕开启一个协程处理客户端发来的请求,用于读取消息
var clientsMsg = make(map[*websocket.Conn]int)

//将channel中的消息推送给客户端
func sendMsg() {
	for {
		//从channel获取值
		msg := <-msgChan
		if msg != nil {
			fmt.Printf("我接收到了信息,我开始发送信息了\r\n")
			fmt.Println(msg)
			msgStr, _ := json.Marshal(msg)
			//给每一个websocket发送消息
			for client, v := range clients {
				fmt.Printf("我在给第%d个用户发信息\n\r", v)
				err := client.WriteMessage(websocket.TextMessage, msgStr)
				defer client.Close()
				if err != nil {
					fmt.Printf("给第%d个用户发信息失败了\r\n", v)
					fmt.Println(err)
				}
			}

		}

	}
}

//接收客户端发来的msg,并写入chan
func getMsg() {
	for {
		//遍历所有的链接
		for client := range clientsMsg {
			defer client.Close()
			if client == nil {
				delete(clientsMsg, client)
				break
			} else {
				go listenMsg(client)
				delete(clientsMsg, client)
			}

		}
	}
}

//给连接设置一个监听消息
func listenMsg(client *websocket.Conn) {
	var jsonMsg Msg
	for {
		err := client.ReadJSON(&jsonMsg)
		if err != nil {
			fmt.Println("get msg error")
			fmt.Println(err)
		}
		fmt.Println(jsonMsg)
		if err != nil {
			fmt.Println("json error")
		}
		msgChan <- &jsonMsg
	}
}

//显示网页
func rootHandler(w http.ResponseWriter, r *http.Request) {
	file, err := ioutil.ReadFile("othor-project/websocket/index.html")
	if err != nil {
		fmt.Println(os.Getwd())
		fmt.Println(err)
	}
	_, err = fmt.Fprintf(w, "%s", file)
	if err != nil {
		fmt.Println(err)
	}
}

//建立ws连接
func wsHandler(w http.ResponseWriter, r *http.Request) {
	//声明协议是websocket
	ws, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		log.Print("upgrade:", err)
		return
	}
	ws.PongHandler()
	clength := len(clients) + 1
	clients[ws] = clength //用于发送消息 把所有的socket连接都放到一个map,因为我收到一个消息,要发送到每一个连接的的socket
	//这里为了方便 我们就用第几个来代表用户id,项目中需要谨慎定义
	clientsMsg[ws] = clength //用于读取消息
	fmt.Printf("新用户id:%d", clength)
	clientLen := fmt.Sprintf("%d", clength)
	err = ws.WriteMessage(websocket.TextMessage, []byte(clientLen))
	if err != nil {
		fmt.Println("send id error")
	}
}

/**
 * Notes:利用websocket快速实现一个聊天室
 */
func main() {
	fmt.Println("server start")
	//页面
	http.HandleFunc("/", rootHandler)
	http.HandleFunc("/ws", wsHandler)
	// 监听所有ws发送的消息
	go getMsg()
	//发送消息给所有的ws
	go sendMsg()
	//监听端口
	err := http.ListenAndServe(":30000", nil)
	if err != nil {
		fmt.Println(err)
	}
}

客户端代码

<!doctype html>
<html lang="en">
<meta charset="UTF-8">
<body>
<style>
    .chat-group {
        background-color: rgb(73, 73, 73);
    }

    .chat {
        margin-top: 10px;
        background-color: #dbdede;
    }
</style>
<h1>聊天系统:</h1>
<div class="chat-group" id="chat-group">

</div>

<p>uid:<span id="uid"></span></p>
<p>信息:<input type="text" name="msg" id="msg"></p>
<button id="btn">发送</button>
<script src="https://m.acurd.com/template/home/default/js/vendor/jquery-1.12.4.min.js"></script>
<script type="text/javascript">
    // 1.建立连接
    var ws = new WebSocket("ws://" + location.host + "/ws")
    //接收消息
    ws.onmessage = function (event) {
        var data = event.data;
        //如果是消息 返回的是json
        if (!isJSON(data)) {
            $("#uid").text(data)
            return
        }
        addMsg(data)
    }
    ws.onopen = function (event) {
        console.log("连接上了");
    }
    //发送消息
    $("#btn").click(function () {
        var msg = $("#msg").val();
        var uid = $("#uid").text();
        var obj = {msg: msg, uid: uid}
        var json = JSON.stringify(obj)
        //发送json字符串
        ws.send(json)
    })
    function addMsg(data) {
        data = $.parseJSON(data)
        $("#chat-group").append("<div class=\"chat\">\n" +
            "        id: <span>" +
            data.uid +
            "</span>\n" +
            "        信息<span>" +
            data.msg +
            "</span>\n" +
            "    </div>")
        console.log(data.uid);
    }

    function isJSON(str) {
        if (typeof str == 'string') {
            try {
                var obj = JSON.parse(str);
                if (typeof obj == 'object' && obj) {
                    return obj;
                } else {
                    return false;
                }

            } catch (e) {
                console.log('error:' + str + '!!!' + e);
                return false;
            }
        }
        console.log('It is not a string!')
    }
</script>
</body>
</html>
  • 我模拟了3个用户,我们来看一下效果吧
    在这里插入图片描述
    看一下服务端的信息打印
    在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/392777.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

为啥用 时序数据库 TSDB

前言 其实我之前是不太了解时序数据库以及它相关的机制的&#xff0c;只是大概知晓它的用途。但因为公司的业务需求&#xff0c;我意外参与并主导了公司内部开源时序数据库influxdb的引擎改造&#xff0c;所以我也就顺理成章的成为时序数据库“从业者”。 造飞机的人需要时刻…

VMware vCenter Server的安装和使用

准备工作 首先去官网下载好VCenter Server&#xff0c;然后准备安装&#xff0c;我这里下载的是6.0对应的镜像为VMware-VIMSetup-all-6.0.0-2656757.iso 需要注意&#xff1a; 开始安装 和安装其他操作系统一样&#xff0c;把镜像放入光驱或解压缩&#xff0c;我这里是在…

Dropout Reduces Underfitting论文解读

Dropout 在欠拟合的应用Dropout Reduces Underfitting&#xff08;2023.3.2&#xff09;写在前面摘要一、简介二、重新审视过拟合和欠拟合三、Dropout如何减少欠拟合四、方法五、实验早期随机失活分析晚期随机失活&#xff08;Late Dropout&#xff09;六、下游任务七、相关工作…

【零代码工具推荐】Max Creation Graph (MCG) 可视化图形编程工具

从3dMax 2016开始新加入了一个很牛great的功能&#xff0c;也就是“MCG”全称是Max Creation Graph&#xff0c;MCG可以让用户使用全可视化节点工作流程来创建修改器&#xff0c;几何体工具插件&#xff0c;使用MCG&#xff0c;可以创建一个新的插件&#xff0c;没错是插件&…

【大数据实时数据同步】超级详细的生产环境OGG(GoldenGate)12.2实时异构同步Oracle数据部署方案(下)

系列文章目录 【大数据实时数据同步】超级详细的生产环境OGG(GoldenGate)12.2实时异构同步Oracle数据部署方案(上) 【大数据实时数据同步】超级详细的生产环境OGG(GoldenGate)12.2实时异构同步Oracle数据部署方案(中) 【大数据实时数据同步】超级详细的生产环境OGG(GoldenGate…

要点提炼|《数字中国建设整体布局规划》,看这一篇就够了!

《数字中国建设整体布局规划》/// 近日&#xff0c;中共中央、国务院印发了《数字中国建设整体布局规划》&#xff08;以下简称“《规划》”&#xff09;&#xff0c;作为影响中国未来发展的重磅文件&#xff0c;被业界评价为“数字挂帅时代来临”。《数字中国建设整体布局规划…

【持续集成】Jenkins详细教程

文章目录一、jenkins是什么&#xff1f;二、CI/CD是什么&#xff1f;三、使用Jenkins进行PHP代码(单元)测试、打包。1.General2.源码管理3.构建触发器4.构建环境5.构建6.构建后操作7.其他相关配置四、进行jenkins project 构建五、构建结果说明六、jenkins权限管理最后&#xf…

【拼图】拼图游戏-微信小程序开发流程详解

还记得小时候玩过的经典拼图游戏吗&#xff0c;上小学时&#xff0c;在路边摊用买个玩具&#xff0c;是一个正方形盒子形状&#xff0c;里面装的是图片分割成的很多块&#xff0c;还差一块&#xff0c;怎么描述好呢&#xff0c;和魔方玩具差不多&#xff0c;有没有听说叫二维的…

【Leetcode——重排链表】

文章目录一、重排链表思路1.思路2.总结一、重排链表 对于这道题&#xff0c;有两种思路&#xff1a; 思路1. 1.使用一个线性表&#xff0c;存储链表中的每个节点&#xff0c;然后按照题目的条件&#xff0c;来链接线性表的各个节点即可。 使用左下标和右下标来定位线性表中的…

硬件学习 软件 Cadence day09 芯片PCB 封装导出DXF 文件

1.打开自己要导出 DXF 文件的 PCB 封装 (Allegro 软件) 2.导出DXF 文件的按钮 1.点击按钮&#xff0c;打开窗口 2.填写数据 3. 按下 Edit... 按钮 4. 编辑数据 5. 导出数据 &#xff0c;生成DXF 文件 下面的选项自己选择 &#xff1a; Color mapping &#xff1a; …

希腊字母及读音

希腊字母24个希腊字母分别是&#xff1a;Αα、Ββ、Γγ、Δδ、Εε、Ϝϝ、Ζζ、Ηη、Θθ、Ιι、Κκ、Λλ、Μμ、Νν、Ξξ、Οο、Ππ、Ρρ、Σσ、Ττ、Υυ、Φφ、Χχ、Ψψ、Ωω。拼写Α α&#xff1a;阿尔法 AlphaΒ β&#xff1a;贝塔 BetaΓ γ&…

算法套路二:相向双指针

算法套路二:相向双指针 算法套路示例讲解&#xff1a;LeetCode167. 两数之和 II - 输入有序数组 给你一个下标从 1 开始的整数数组 numbers &#xff0c;该数组已按 非递减顺序排列 &#xff0c;请你从数组中找出满足相加之和等于目标数 target 的两个数。如果设这两个数分别是…

如何在MacOS上卸载IPGuard的软件--LAgent/LSDhelper程序

IPGuard类的软件一般企业用于办公设备监控&#xff0c;获取员工在设备上操作的信息&#xff0c;同时对文件等信息加密&#xff0c;用于防止企业信息外泄到网络上。但是设备上安装了此类软件一般不容易卸载掉&#xff0c;针对在macos上卸载过程作下讲解。 1. 一般服务类的程序都…

shell文件通配符:任意一个:?、任意数量:*、任意包含[]、[^]:任意不包含

文章目录一. 有哪些文件通配符二. 匹配任意?&#xff1a;匹配任意一个字符*&#xff1a;匹配任意数量的字符串三. 匹配任意指定一个字符1. []&#xff1a;匹配任意包含匹配任意包含字符范围2. [!]与[^]&#xff1a;匹配任意不包含四. 注意事项1. 通配符组合2. 不能跨越目录层级…

运筹系列67:大规模TSP问题的EAX遗传算法

1. 算法介绍 EAX是edge assembly crossover 算子的缩写。本算法有Y nagata教授公布&#xff0c;目前在VLSI最大的几个案例上获得了best的成绩。另外目前MonoLisa 100K问题的最优解也是由其公布&#xff0c;若能得到更优解&#xff0c;可以获得1000美元奖励。 算法步骤如下&…

【教学典型案例】用户称为设计者的正例

目录一&#xff1a;背景介绍二&#xff1a;设计理念三&#xff1a;设计过程按照设计理念设计的功能&#xff1a;1、用户可以根据自己的情况来选择显示哪些活动参与数据。2、用户可以对请假功能和点读功能进行开启和关闭操作&#xff08;默认为全部开启&#xff09;四&#xff1…

我一个女孩子居然做了十年硬件……

2011年&#xff0c;一个三本大学的电子信息专业的大三女学生跟2个通信专业的大二男生组成了一组代表学校参加2011年“瑞萨杯”全国大学生电子设计大赛&#xff0c;很意外的获得了湖北赛区省三等奖&#xff0c;虽然很意外&#xff0c;但还是挺高兴的&#xff0c;毕竟第一次为喜欢…

数据大爆炸时代,大容量硬盘为何不可或缺?

2月27日&#xff0c;中共中央、国务院正式印发《数字中国建设整体布局规划》&#xff08;以下简称《规划》&#xff09;&#xff0c;明确提出要夯实数字中国建设基础&#xff1a;一是打通数字基础设施大动脉&#xff0c;优化各种级别数据中心的合理梯次布局&#xff1b;二是畅通…

Arduino双色LED实验记录

接线图片&#xff1a;双色LED实物和布线有区别&#xff1a;代码&#xff1a;int RED_LED 11; //设置红色为11 int GREEN_LED 10; //设置绿色为10 int val 0;//全局变量val void setup() {// put your setup code here, to run once:pinMode(RED_LED,OUTPUT);//引脚配置pinMo…

HCIP总结(一)

抽象语言---编码---二进制---电信号----处理电信号 &#xff08;电脑工作流程&#xff09; OSI参考模型 ----OSI/RM (核心思想&#xff1a;分层) 应用层----提供各种应用服务&#xff0c;将抽象语言转换成编码&#xff0c;提供人机交互的接口 表示层----将编码转换成二进制 …