WebSocket定义

维基百科百度百科,或者 谷歌百度

WebSocket原理

原理的文字描述还请自行百度或者谷歌,我以自己的看法总结一个简单的原理图,有不足或错误请指正。
在这里插入图片描述

WebSocket的用途

web一般的模式是客户端发送请求给服务端,服务端给出响应,在一些需要服务端主动发送消息给客户端时,一般采用客户端轮循访问服务端获取信息,这种方式非常占用资源,使用WebSocket可以让客户端(网页)和服务端(网站后端)实现TCP链接,两者都能主动发送信息给对方,方便实现服务端主动发送信息给客户端,一般你看到这篇文章时,无非是想是想实现两个功能:

  • 网页实时消息推送
  • 网页聊天室

下面就针对这两个简单功能实现个小Demo。

WebSocket的Django实现

准备

首先在Django中实现websocket有多种途径,一般有:channels和dwebsocket,针对这两种方式,推荐新手使用后者,流程相对简单。本文先讨论使用dwebsocket来实现,在此之前,首先需要安装dwebsocket。

pip install dwebsocket

dwebsocket的官方介绍和相关文档我这里不在赘述了,详见dwebsocket的Github官网,下文中只会对几个常用的对象和方法进行介绍。dwebsocket的Github站。安装之后呢,希望能新建一个Django项目和App,先实验成功再迁移到自己的项目中

djangp-admin startproject wstest

进入到新建的项目文件夹wstest中

djangp-admin startapp app

wstest中新建templates文件夹存放html页面。在setting中配置模板目录,找到TEMPLATES,修改DIRS属性。

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates')],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

实现

实时消息推送
  1. 服务器

在app.view.py中,首先编写方法进行页面跳转:

from django.shortcuts import render,HttpResponse #引入HttpResponse
from dwebsocket.decorators import accept_websocket #引入dwbsocket的accept_websocket装饰器

def to_sendmsg(request):
    return render(request,'sendmsg.html')

def to_recmsg(request):
    return render(request,'recmsg.html')

服务器方法,监听客户端的websocket链接。

clients={} #创建客户端列表,存储所有在线客户端

# 允许接受ws请求
@accept_websocket
def link(request):
    # 判断是不是ws请求
    if request.is_websocket():
        userid=str(uuid.uuid1())
        # 判断是否有客户端发来消息,若有则进行处理,若发来“test”表示客户端与服务器建立链接成功
        while True:
            message=request.websocket.wait()
            if not message:
                break
            else:
                print("客户端链接成功:"+str(message, encoding = "utf-8"))
                #保存客户端的ws对象,以便给客户端发送消息,每个客户端分配一个唯一标识
                clients[userid]=request.websocket


发送消息方法,通过post请求发送消息。

def send(request):
    # 获取消息
    msg=request.POST.get("msg")
    # 获取到当前所有在线客户端,即clients
    # 遍历给所有客户端推送消息
    for client in clients:
        clients[client].send(msg.encode('utf-8'))
    return HttpResponse({"msg":"success"})

配置url。

from django.contrib import admin
from django.urls import path
import app.views as views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('link/', views.link),
    path('send/', views.send),
    path('to_sendmsg/', views.to_recmsg),
    path('to_recmsg/', views.to_sendmsg),
]
  1. 客户端

客户端部分由JS来实现,JS的window对象中包含一个WebSocket类型(如果不包含则说明浏览器不支持),可以根据是否有这个类型来判断浏览器是否支持WebSocket,判断代码如下:

if ("WebSocket" in window) {
//如果返回True则表示支持,否则不支持
}

JS的WebSocket使用也很简单,主要就是创建链接和监听消息,如下代码展示一个最基础的WebSocket的例子:

var ws = new WebSocket("ws://javafeng.com");
//链接打开时执行
ws.onopen = function(evt) { 
  alert("链接成功");
  ws.send("text");
};
//接收到消息时执行
ws.onmessage = function(evt) {
  var received_msg = evt.data;
  alert("收到消息"+received_msg);
};
//链接关闭时执行
ws.onclose = function(evt) {
 alert("链接关闭");
};  

就下来就来根据以上展示的简单案例,来编写页面的脚本。
templates中新建html页面recmsg.html模拟用户接收实时消息,新建sendmsg.html模拟管理员发送实时消息推送。

  • recmsg.html

既然是要接受消息,因此在页面加载时就建立与服务端的链接,在recmsg.html中编写js脚本,使用alert来显示服务端的消息推送,使用js发送ws请求代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>实时消息接收</title>
</head>
<body>
<script>
    window.onload = function () {
        //先判断浏览器是否支持websocket
        if ("WebSocket" in window) {
            alert("您的浏览器支持 WebSocket!");
            // 打开一个 web socket
            ws = new WebSocket("ws://127.0.0.1:8000/link/");
            ws.onopen = function () {
                // Web Socket 已连接上,使用 send() 方法尝试发送数据
                ws.send("test");
            };
            //监听服务端是否有消息发送过来,当有消息时执行方法
            ws.onmessage = function (evt) {
                //获取服务器发来的消息
                var received_msg = evt.data;
                //显示消息
                alert("收到消息:"+received_msg)
            };
            //关闭页面或其他行为导致与服务器断开链接是执行
            ws.onclose = function () {
                // 关闭 websocket
                alert("连接已关闭...");
            };
        } else {
            // 浏览器不支持 WebSocket
            alert("您的浏览器不支持 WebSocket!");
        }
    }
</script>
</body>
</html>

其中在判断是否链接成功时,还可以加代码判断ws.readyState的值,readyState属性有四种取值:
CONNECTING:值为0,表示正在连接。
OPEN:值为1,表示连接成功,可以通信了。
CLOSING:值为2,表示连接正在关闭。
CLOSED:值为3,表示连接已经关闭,或者打开连接失败。

  • sendmsg.html

记得引用jquery<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>,之后就很方便了。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
</head>
<body>
    <input id="msg" placeholder="输入要推送的消息">
    <button onclick="javascript:sendmsg()">推送</button>
<script>
    function sendmsg() {
        $.post("/send/",{msg:$("#msg").val()},function (result) {
            if(result.msg=="success"){
                alert("消息推送成功")
            }
        })
    }
</script>
</body>
</html>

接下来就是启动项目,使用两个客户端,进行消息接收,启动后,打开两个浏览器窗口,均输入http://127.0.0.1:8000/to_recmsg/,效果均如下:
在这里插入图片描述
这里其实有一个坑,就是Django在2.1.5版本之后,会出现ws链接访问400的错误,这里建议使用Django2.1.4版本,后续研究到底是因为什么造成的400错误。

两个窗口都关闭alert后,可以看到两个客户端都连接上了服务器:
在这里插入图片描述
这时打开第三个窗口,输入http://127.0.0.1:8000/to_sendmsg/
在这里插入图片描述
输入信息,点击推送,回到客户端的窗口可以看到推送的消息
在这里插入图片描述
这样我们就完成了一个简单的消息实时推送,后续的复杂功能大家就能按照自己的需求慢慢开发了。

聊天室

接下来是聊天室,可以做一对一或者群组式的聊天室,首先分析下需求,设计页面原型图:
在这里插入图片描述
实现的逻辑顺序和思路:

  • 加载页面同时链接服务器
  • 服务器监听客户端链接,当有客户端链接时,将其添加到在线列表,将在线列表和为其分配的客户端随机id发送给当前链接的客户端,并将列表发送给所有在线客户端
  • 获取服务端返回的随机客户端id和在线用户列表
  • 加载用户列表和本人id
  • 填写消息,选择群组或者指定用户,发送到后台
  • 后台服务器判断是群组消息还是私聊消息,根据不同情况发送给指定客户端
  • 指定客户端接收到消息,显示在消息列表中
  • 离线时,服务器删除制定用户,更新在线列表,并将列表发送给所有在线客户端

根据代码编写逻辑,整理了一个超级大图,囊括了所有的流程以及对应代码和页面的内容,按照大图可以更好地理解整个实现的流程在这里插入图片描述
照例先编写后台服务器部分,按照上述流程,和改造消息推送的代码,实现起来并不难,比较绕的地方就是几种消息类型的判断不要弄混,其余的代码和消息推送的都很相似了。
聊天界面跳转:

#聊天界面
def to_chat(request):
    return render(request,'chat.html')

服务器主要方法,负责处理客户端在线列表,处理客户端的请求,这里没有将消息发送的代码写在里面,把发送的代码单独构建了一个方法。

# 服务器方法,允许接受ws请求
@accept_websocket
def chat(request):
    # 判断是不是ws请求
    if request.is_websocket():
        # 保存客户端的ws对象,以便给客户端发送消息,每个客户端分配一个唯一标识
        userid=str(uuid.uuid1())[:8]
        clients[userid] = request.websocket
        # 判断是否有客户端发来消息,若有则进行处理,表示客户端与服务器建立链接成功
        while True:
            '''获取消息,线程会阻塞,
            他会等待客户端发来下一条消息,直到关闭后才会返回,当关闭时返回None'''
            message=request.websocket.wait()
            if not message:
                break
            else:
                msg=str(message, encoding = "utf-8")
                print(msg)
                #1、发来test表示链接成功
                if msg == "test":
                    print("客户端链接成功:"+userid)
                    #第一次进入,返回在线列表和他的id
                    request.websocket.send(json.dumps({"type":0,"userlist":list(clients.keys()),"userid":userid}).encode("'utf-8'"))
                    #更新所有人的userlist
                    for client in clients:
                        clients[client].send(json.dumps({"type":0,"userlist":list(clients.keys()),"user":None}).encode("'utf-8'"))
    #客户端关闭后从列表删除
    if userid in clients:
        del clients[userid]
        print(userid + "离线")
        # 更新所有人的userlist
        for client in clients:
            clients[client].send(
                json.dumps({"type": 0, "userlist": list(clients.keys()), "user": None}).encode("'utf-8'"))

接收到消息发送的请求之后,判断是群聊消息还是单聊消息,根据指定的类型发送给不同的目标用户:

#消息发送方法
def msg_send(request):
    msg = request.POST.get("txt")
    useridto = request.POST.get("userto")
    useridfrom = request.POST.get("userfrom")
    type=request.POST.get("type")
    #发来{type:"2",msg:data,user:user},表示发送聊天信息,user为空表示群组消息,不为空表示要发送至的用户
    if type == "1":
        #群发
        for client in clients:
            clients[client].send(json.dumps({"type": 1, "data": {"msg": msg, "user": useridfrom}}).encode('utf-8'))
    else:
        # 私聊,对方显示
        clients[useridto].send(json.dumps({"type": 1, "data": {"msg": msg, "user": useridfrom}}).encode('utf-8'))
        # 私聊,自己显示
        clients[useridfrom].send(json.dumps({"type": 1, "data": {"msg": msg, "user": useridfrom}}).encode('utf-8'))
    return HttpResponse(json.dumps({"msg":"success"}))

后台就这么简单,就是消息推送的改造版。然后在url.py中添加如下几条路由:

    # 聊天
    path('to_chat/', views.to_chat),
    path('chat/', views.chat),
    path('msg_send/', views.msg_send),

页面设计比较简单,我是用了最原始的表格框架,就简单设计一下就好,JS脚本基本上也是通过消息推送部分进行了一些修改,整体理解结合大图也很简单。
编写完后台服务器之后,配置URL路由:

新建chat.html,代码如下:

<!DOCTYPE html>
<html lang="zh">
<head>
    <link href="https://cdn.bootcss.com/twitter-bootstrap/4.3.1/css/bootstrap.css" rel="stylesheet">
    <link href="https://cdn.bootcss.com/twitter-bootstrap/4.3.1/css/bootstrap.css" rel="stylesheet">
    <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        td {
            border: 1px #000 solid;
            margin: 0;
        }

        textarea {
            width: 100%;
            height: 100%;
        }
    </style>
</head>
<body style="padding: 30px">
<span id="userid">我的随机账号:</span>
<table>
    <tr>
        <td style="width: 500px;">
            <div id="historymsg" style="height: 400px;overflow: auto"></div>
        </td>
        <td style="width: 400px">
            <div id="userlist" style="height: 400px;overflow: auto"></div>
        </td>
    </tr>
    <tr>
        <td colspan="2">
            <textarea id="msg"></textarea>
        </td>
    </tr>
    <tr>
        <td colspan="2">
            <select class="form-control" id="isgroup">
            </select>
            <input class="btn btn-info btn-block" type="button" onclick="send()" value="发送">
        </td>
    </tr>
</table>
<!--脚本开始-->

<!--脚本结束-->
</body>
</html>

页面脚本

var ws,myid;
    window.onload = function () {
        //先判断浏览器是否支持websocket
        if ("WebSocket" in window) {
            // 打开一个 web socket,链接服务器
            ws = new WebSocket("ws://127.0.0.1:8000/chat/");
            ws.onopen = function () {
                // Web Socket 已连接上,使用 send() 方法尝试发送数据
                ws.send("test");
            };
            //监听服务端是否有消息发送过来,当有消息时执行方法
            ws.onmessage = function (evt) {
                //获取服务器发来的消息
                var received_msg = evt.data;
                //判断是返回的是消息还是用户列表和id,1是消息,0是用户列表和id
                msg = eval("(" + received_msg + ")")
                //用户列表和id
                if (msg.type == 0) {
                    //userid为空表示更新用户列表,不需要更新自己的id,否则为初次登录
                    if (msg.userid != null) {
                        myid = msg.userid
                        $("#userid").append(myid)
                    }
                    //当收到新的用户列表时,清空原来的用户列表,清空原来的用户选择框,添加群组发送选项
                    $("#userlist").empty()
                    $("#isgroup").empty()
                    $("#isgroup").append("<option value='1'>群发(或选择要私聊的用户)</option>")
                    for (var i = 0; i < msg.userlist.length; i++) {
                        //填充用户列表
                        $("#userlist").append(msg.userlist[i] + "<hr />")
                        //填充用户选择框
                        $("#isgroup").append("<option value='" + msg.userlist[i] + "'>" + msg.userlist[i] + "</option>")
                    }
                }
                //用户发送的消息
                else {
                    var myDate = new Date();
                    nowtime = myDate.toLocaleString(); //获取日期与时间
                    newmsg = ""
                    //判断是自己的消息,绿色显示
                    if (myid == msg.data.user) {
                        newmsg = "<span style='color:blue'>" + msg.data.user + ":" + nowtime + "<br />" + msg.data.msg + "</span>" + "<br />"
                    } else {
                        newmsg = "<span >" + msg.data.user + ":" + nowtime + "<br />" + msg.data.msg + "</span>" + "<br />"
                    }
                    $("#historymsg").append(
                        newmsg
                    )
                }
            };
            //关闭页面或其他行为导致与服务器断开链接是执行
            ws.onclose = function () {
                // 关闭 websocket
                alert("连接已关闭...");
            };
        } else {
            // 浏览器不支持 WebSocket
            alert("您的浏览器不支持 WebSocket!");
        }
    }
	//消息发送
    function send() {
        msgtxt = $("#msg").val()
        msguser = $("#isgroup").val()
        //判断是否是群发0是,不是0 则是私聊
        if ($("#isgroup").val() == "1") {
            msg = {
                type: "1",
                txt: msgtxt,
                userfrom: myid
            }
        } else {
            msg = {
                type: "0",
                txt: msgtxt,
                userto: msguser,
                userfrom: myid
            }
        }
        //发送消息后清空消息框,并定位到消息框内
        $.post("/msg_send/", msg, function () {
            $("#msg").val("")
            $("#msg").focus()
        })
    }

代码编写完成之后,运行项目,打开两个窗口分别链接服务器,测试效果
在这里插入图片描述
这样就实现了简单的WebSocket聊天室,后续更多功能各位看官可以继续开发,要是想深入了解更加深入的知识,可以出门右转百度谷歌走一波,后续随着使用也可能会除深入些的文章。
欢迎关注点赞评论,谢谢。有需要请email:javafeng@javafeng.com。代码下载地址:wstest.zip,没有分的朋友联系我。

Logo

致力于链接即构和开发者,提供实时互动和元宇宙领域的前沿洞察、技术分享和丰富的开发者活动,共建实时互动世界。

更多推荐