本项目基于 WebRTC,WebSocket 实现了一个聊天系统
一、引言
1. 背景
实时聊天愈加火爆,但是网页版的实时聊天不多,并且很少有支持视频聊天功能的。
HTTPS 协议使得消息可以在安全信道传输。
WebSocket 使得浏览器也可以建立持久连接,使得频繁发送和接收消息的成本降低。
WebRTC 协议是 P2P 协议,使得在传输视频数据时降低了服务器的压力,视频聊天便于实现。
2. 要求
界面友好,可扩展性强,稳定性强,并且要有一定的安全性
3. 目标
传输协议基于 SSL,HTTP/1.1,WebSocket,WebRTC 实现了一个简易的支持视频聊天、表情、图片的实时消息系统。
4. 相关介绍
- base64
用字符串代表二进制序列,4 个字节可以代表二进制序列三个字节。
用字符
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/
代表 0~63 数字,如果二进制序列的字节数不是 3 的倍数,用 0 填充,base64 序列用=填充。
本项目的消息使用 base64 编码传输。 2. Http/1.1
即超文本传输协议-版本 1.1,允许发送流水线请求,并且相比 Http1.0 开销大大降低。 3. WebSocket
WebSocket 允许浏览器和服务器之间打开交互式通信会话(即持久化连接),而无需通过轮询服务器以获得响应。
本项目聊天部分用到了 WebSocket。 4. WebRTC
WebRTC(Web Real-Time Communications)是一种实时通信技术,它允许网络应用或者站点,不通过任何第三方媒介建立 P2P 连接,用以传输音频,视频等其它任何数据,减少服务器压力。
本项目视频聊天功能用到了 WebRTC。 5. SDP
SDP(Session Description Protocol)是一个描述 peer-to-peer 连接的标准,SDP 包含音视频的编解码,源地址,时间等信息。
SDP 也是 WebRTC 的组件之一,用于描述一个会话。 6. ICE
ICE(Interactive Connectivity Establishment)是一个被 WebRTC 应用的框架,即使两端位于 NAT 后面,这个协议可以让两端互相找到对方并建立连接。
ICE 使用以下算法寻找最低等待时间路径去连接两端,通常为以下顺序
- 直接 UDP 连接
- 直接 TCP 连接(HTTP)
- 直接 TCP 连接(HTTPS)
- 间接通过中继器连接(存在位于防火墙后面的一方,无法进行 NAT 穿越)
- 信令服务器
WebRTC 中交换视频请求和响应,交换 ICE 候选时用的服务器。服务器收到视频相关消息只是简单地转发它们,为两端建立连接提供信息。 8. STUN 服务器
通过 STUN 服务器,客户终端可以发现他们的 IP 地址,以及 NAT 相关信息,为 NAT 穿越提供信息。
二、需求分析
1. 业务调查
目前实时消息很火爆,但是网页的实时消息服务不多,支持视频聊天的又很少。
2. 系统的目标(含新系统的功能需求、性能需求、输入输出需求)
2.1 功能需求
需要实现的功能
- 发送和接收消息
- 视频聊天
- 好友系统,包括添加好友,好友列表
- 登陆,注册系统
用例图如下
2.2 性能需求
实时性需求
- 如果双方在线,发送消息延迟不应高于 10 秒
- 视频延迟不应高于 500ms
安全性需求
- 如果不知道用户的用户名和密码,则不能以他的身份发送消息或视频,或者迫使在线用户下线
- 用户的用户名,密码,消息应该在安全信道传输
2.3 输入输出需求
功能 | 输入 | 动作 | 输出 | 异常 |
---|---|---|---|---|
添加朋友 | 朋友名 | 把朋友关系写入朋友关系表 | 新的朋友列表 |
朋友不存在
随机数验证失败 |
发送消息 |
对方用户名
随机数 自己的用户名 |
如果对方在线就把消息转发给对方
如果对方不在线就存入待收消息表 |
新的消息列表
对方新的消息列表 |
随机数验证失败
对方不是朋友 对方不存在 |
视频邀请 |
对方用户名
随机数 自己的用户名 |
直接转发消息 | 视频聊天 |
随机数验证失败
对方不在线 |
登陆 | 用户名,密码 | 把登陆信息写入登陆表 | 会话凭证(随机数) | 用户名或密码错误 |
注册 | 用户名,密码 | 把注册信息写入注册信息表 | 注册成功/失败 | 用户已存在 |
三、系统分析
1. 子系统划分
子系统划分如图
2. 系统业务流程
3. 系统数据流程
注册时的流程
登陆时的数据流程
添加朋友流程
发送消息流程
视频流程
4. 系统的数据字典
表项 | 类型 | 范围 |
---|---|---|
username | 字符串 | 标识符 |
passwd | 字符串 | 数字,字母,符号组成的序列 |
id | 整数 | 0~2^63^-1 的整数 |
message(存储的文件名) | 字符串 | 当前时间||0~65535 的十六进制整数 |
message(实际消息) | base64 串 | 参见 base64 介绍 |
四、系统设计
1. 系统配置设计
1.1 硬件配置设计
- 服务器
阿里云虚拟服务器 - 客户端
满足基本网络通信要求的客户端
需要带有摄像头,麦克风
1.2 软件配置设计
- 服务器
操作系统:Centos7.3
服务器软件:Tomcat8.5 - 客户端
Firefox 76.0.1
Chrome76.0
其它支持 WebSocket,WebRTC,摄像头的浏览器
1.3 网络配置设计
- 服务端
开放 8081 端口
-
由于阿里云有安全策略服务,所以服务器防火墙直接关掉即可
```shell
关闭Firewall防火墙
systemctl disable firewalld systemctl stop firewalld
关闭iptables防火墙
iptables -F iptables -X iptables -P INPUT ACCEPT iptables -P OUTPUT ACCEPT iptables -P FORWARD ACCEPT ``` - 在阿里云安全策略中开放 8081 端口
给 8081 端口支持 SSL
- 购买域名
- 给域名申请免费证书
-
在 Tomcat 配置中添加如下配置信息
xml <!--conf/server.xml--> <Connector port="8081" protocol="HTTP/1.1" SSLEnabled="true" scheme="https" secure="true" clientAuth="false" keystoreFile="<path to cert>" keystorePass="NVDNlti0" ciphers="TLS_RSA_WITH_AES_128_CBC_SHA,TLS_RSA_WITH_AES_256_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,TLS_RSA_WITH_AES_128_CBC_SHA256,TLS_RSA_WITH_AES_256_CBC_SHA256" />
- 重启 tomcat8shell sh tomcat8/bin/shutdown.sh sh tomcat8/bin/startup.sh
2. 程序数据结构设计
2.1 后端的程序数据结构
- WebSocket
每一个登陆成功的用户,都对应着一个 WebSocket 类对象
- user: 用户名
- Session: 会话对象
- Utility
工具类,保存着数据库存储器对象,和一些常用函数
- mFriendsRepo: 朋友数据库存储器对象
- mMessageToReceiveRepo: 待收消息存储器对象
- mHistoryMessageRepo: 历史消息存储器对象
- mLoginUserRepo: 登陆用户存储器对象
- mRegisterUserRepo: 注册用户存储器对象
- DB_URL: 数据库连接地址
- DB_PASS: 数据库密码
- DB_USER: 数据库用户名
- Friends
对应于 friends 表
- host:朋友中的一方
- friend:朋友中的另一方
- MessageToReceive
对应于 message_to_recieve 表
- id
- sendUser: 发送者
- receiveUser: 接收者
- sendDate: 发送时间
- msg: 消息内容(其实是保存消息内容的文件名)
- HistoryMessage
对应于 history_message 表
- id
- sendUser:发送者
- receiveUser:接收者
- sendDate: 发送时间
- receiveDate: 接收时间
- msg: 消息内容
- LoginUser
对应于 login_user 表
- user: 用户名
- rand: 随机数(用于验证)
- RegisterUser
对应于 RegisterUser 表
- user: 用户名
- registerDate: 注册日期
- passwdHash: 密码
2.2 前后端交互时使用的数据结构
使用 JSON 数据结构,在传输时有较小的体积,并且容易被人类读懂,到前端之后便于解析。
- 前端到后端的总体数据结构
json
{
user:"", // 用户名
rand:"", // 密码,用于验证
to:"", // 目的用户,如果是服务器则忽略此字段
type:"", // 消息类型
date:"", // 发送日期
data:[] // 数据,这个字段随着type字段的不同而不同
}
type 可能取下面中的几个值中的一个
-
request-friend-list
获取朋友列表 2. request-message-list
获取待收消息列表,在登陆之后要做 3. add-friend
添加朋友,data 为要添加的朋友 4. message
发送消息,data 为要发送的消息内容 5. video-offer
提出视频请求,data 为对应的 sdp 描述符 6. video-answer
提出视频回应,data 为对应的 sdp 描述符 7. new-ice-candidate
交换 ICE 候选,data 为 ICE 候选 8. video-hang-up
挂断视频 - 后端到前端的总体数据结构
json
{
from:"", // 在转发消息时,发送者的用户名
type:"", // 消息类型
data:[] // 数据,随着type字段不同而不同
}
type 可能取下面中的几个值中的一个
-
friend-list
好友列表,data 为所有好友用户名组成的数组 2. message-list
消息列表组成的数组,数组元素为消息对象
json { from:"", // 消息中的发送者 msg:"", // 消息内容 date:"", // 发送日期 }
3. add-friend添加朋友,data 为朋友用户名 4. message
单条消息,data 为消息对象的数组 5. error
错误,data 为报错信息 6. video-offer
视频请求,data 为 sdp 描述符 7. video-answer
视频回应,data 为 sdp 描述符 8. new-ice-candidate
交换 ICE 候选,data 为 ICE 候选 9. video-hang-up
2.3 前端数据结构
- info
负责保存会话相关信息,做基础通讯
- ws: WebSocket 对象
- user: 用户名
- rand: 会话凭证(随机数)
- videoCur: 当前视频聊天的人
- chatCur: 当前聊天的人
- message
负责发送单条消息
- msg: 发送消息框对象,用于获取消息文本
- messageList
负责管理消息列表,保存历史消息,把消息添加进视图,以及有视频时界面的排版
- list: 消息列表的视图对象
- target: 当前聊天用户
- main: 消息列表容器的视图对象
-
msgs: 保存当前会话所有历史消息,格式如下
json { "username1":[ //和username1的聊天记录 { from:"", // 发送者,是username还是用户 msg:"", date:"" }, //... 可能有很多条消息 ], "username2":[ { from:"", msg:"", date:"" } ], //... }
- friends
保存朋友有关信息,管理朋友视图
- list: 朋友的用户名列表
- listView: 朋友列表视图
- search: 朋友搜索框视图对象
- button
负责按钮的监听器初始化操作 - emotion
负责表情视图的显示和隐藏,以及表情的选择,添加等
- v: 表情视图可见性
- e: 表情视图对应的视图对象
- video
负责视频的初始化,连接,挂断,视频视图的控制
- target: 视频通话的对方用户名
- selfPreview: 本地视频预览框对应的视图对象
- targetPlayer: 对方视频的播放框
- peerConnection: RTCPeerConnection 对象
- windows: 视频预览框容器,控制视频视图显示和隐藏时候用
- localStream: 本地视频流
3. 系统结构设计
采用了 MVC 框架
后端主要负责 M(model),和 C(control),M 对应着数据,C 对应着业务逻辑
前端主要负责 V(View),包括视图的控制,和后端的数据交互,视图的更新
结构图如下
4. 系统功能模块设计
4.1 登陆注册模块
-
注册
-
前端
用户输入用户名和密码,前端会获取用户输入,之后通过 AJAX,以 POST 方式把消息发往服务器。之后获取服务器返回的结果,并且将结果显示给用户。
```javascript register() { let userName = this.form.name; let password = this.form.password; xmlHttp = new XMLHttpRequest();
xmlHttp.onreadystatechange = function () { if (xmlHttp.readyState == 4 && xmlHttp.status == 200) { console.log(xmlHttp.responseText); let obj = JSON.parse(xmlHttp.responseText); console.log(obj); vm.info.title_info=obj.type; vm.info.login_register_info=obj.data[0]; vm.info.visible=true; } } xmlHttp.open("POST", "/chat/register_control", true); xmlHttp.setRequestHeader("Content-type","application/x-www-form-urlencoded"); xmlHttp.send("username=" + userName + "&password="+password); }
} ``` - 后端
后端收到前端发来的请求后,会首先检查用户是否存在,如果不存在则将用户的注册信息存入数据库。并且返回一个结果。
```java List
userNameUsers = Utility.getmRegisterUserRepo().findByUserName(username); if(userNameUsers.size()!=0) { obj.put("type","error"); ja.add("Failed, \""+username+"\"existed!!!"); obj.put("data", ja); }else { RegisterUser ru = new RegisterUser(); ru.setUserName(username); ru.setPasswdHash(password); Date rd = new Date(); ru.setRegisterTime(rd); Utility.getmRegisterUserRepo().save(ru); log.info(username+password+" Saved at "+rd.toString()); obj.put("type","success"); ja.add("Register Successful"); obj.put("data",ja); }
``` 2. 登陆
-
前端
获取用户名和密码,然后发送给后端。和注册的原理基本相同。
但是收到返回的结果后,如果是成功的则需要把用户名和随机数存入会话存储,然后跳转到聊天页面。
javascript if(obj.type=="success"){ sessionStorage.setItem("user",userName); let data = obj.data; let rand = data[0]; sessionStorage.setItem("rand",rand); window.location.href="../index.html"; }
- 后端后端需要先检查用户名和密码是否合法,如果合法才进一步操作。
然后检查用户是否已经登陆,如果已经登陆则应该删除用户的登陆信息,然后写入新的用户登陆信息。并且需要给前端返回新的随机数。
```java List
rus = Utility.getmRegisterUserRepo().findByUserName(username); if(rus.size()==0||!rus.get(0).getPasswdHash().equals(password)) { obj.put("type","error"); ja.add("username or password wrong"); }else { int rand = (int)(Math.random()*Integer.MAX_VALUE); ru = rus.get(0); LoginUser lu = new LoginUser(); lu.setId(ru.getId()); lu.setUserName(username); lu.setRand(rand); Utility.getmLoginUserRepo().save(lu); log.info("login from "+ru.getUserName()); obj.put("type","success"); ja.add(""+rand); }
```
4.2 刷新朋友列表和消息列表
登陆成功后,会首先获取存储在会话存储中的用户名和随机数,然后开启 WebSocket 连接
javascript
info.user = sessionStorage.getItem("user");
info.rand = sessionStorage.getItem("rand");
info.ws = new WebSocket("wss://<Server domain name:port>/chat/ws/" + info.user);
之后会发送获取朋友列表和消息列表的请求的请求
javascript
info.ws.onopen = function () {
console.log("Websocket connect successfuly");
info.to = info.user;
friends.getFriendList();
messageList.getMessageList();
}
后端收到该请求后,会查询数据库,然后把结果返回
```java
public void sendFriendList(JSONObject uObj) {
if(!vertifyRand(uObj.getString("user"),uObj.getString("rand"))) {
sendError("Rand Vertify failed");
return;
}
JSONObject obj = new JSONObject();
List
for (Friends f : fl) {
ja.add(f.getFriend());
}
obj.put("type","friend-list");
obj.put("data",ja);
send(obj.toString());
}
public void sendToReceiveMessageList(String user) {
JSONObject obj = new JSONObject();
List
HistoryMessage hm = new HistoryMessage();
hm.setId(mHistoryMessageRepo.selectMaxId()+1);
hm.setMessage(mtr.getMessage());
hm.setReadDate(new Date());
hm.setSendDate(mtr.getDate());
hm.setReceiveUser(mtr.getReceiveUser());
hm.setSendUser(mtr.getSendUser());
mHistoryMessageRepo.save(hm);
mMessageToReceiveRepo.deleteById(mtr.getId());
}
obj.put("type", "message-list");
obj.put("data",ja);
send(obj.toString());
}
```
前端收到后端发来的消息后,会存储并解析数据,然后显示在视图中
javascript
case "friend-list":
friends.list = obj.data;
friends.refresh();
break;
case "message-list":
messageList.receiveMessageList(obj.data);
break;
4.3 发送和接收消息模块
由于 WebSocket 是持久化连接,所以后端收到消息后,如果对方在线,则直接发送;如果对方不在线,应该把消息存入待收消息列表,等用户登陆后在把消息发给对方。
其中还有验证随机数,验证双方是否是朋友,验证对方是否存在的操作。
```java if(mWebSocketMap.containsKey(to)) { JSONObject msgObj = new JSONObject(); msgObj.put("from",user); msgObj.put("msg",message); msgObj.put("date",new Date().toString()); // System.out.println(msgObj);
JSONObject toObj = new JSONObject();
toObj.put("from",user);
toObj.put("type","message");
ja = new JSONArray();
ja.add(msgObj);
toObj.put("data",ja);
mWebSocketMap.get(to).send(toObj.toString());
HistoryMessage hm = new HistoryMessage();
hm.setId(mHistoryMessageRepo.selectMaxId()+1);
hm.setMessage(message);
hm.setSendDate(new Date());
hm.setReadDate(new Date());
hm.setReceiveUser(to);
hm.setSendUser(user);
mHistoryMessageRepo.save(hm);
}else {
MessageToReceive mtr = new MessageToReceive();
mtr.setDate(new Date());
mtr.setId(mMessageToReceiveRepo.selectMaxId()+1);
mtr.setReceiveUser(to);
mtr.setSendUser(user);
mtr.setMessage(message);
mMessageToReceiveRepo.save(mtr);
}
```
4.4 朋友模块
和发送消息模块大致相同,如果对方在线就把朋友请求发送给对方,对方的前端视图更新朋友列表;如果不在线就存入朋友列表,下次对方登陆获取朋友列表时,就能获取到新朋友。
4.5 视频模块
尝试过两种实现方案
- 第一种是通过 WebSocket 的方式,前端每隔 0.5 秒获取一次用户视频的一帧,然后发送给服务器。服务器直接转发流量。
获取用户摄像头
javascript
navigator.mediaDevices.getUserMedia(video.mediaConstraints)
.then(function (localStream) {
video.selfPreview.srcObject = localStream;
video.localStream = localStream;
video.startSending();
})
使用 canvas 每个 0.5s 获取摄像头的照片,并且转化为 base64 编码
javascript
let can = document.createElement("canvas");
let context = can.getContext("2d");
await context.drawImage(video.localStream,0,0,320,320);
let imgBase64 = context.toDateURL("image/png");
发送给服务器
javascript
info.send({
user:info.user,
type:"video",
rand:info.rand,
to:video.target,
data:[imgBase64]
});
对方收到之后,把数据绘制在 canvas 中
javascript
let ctx = video.targetCanvas.getContext();
ctx.drawImage(imgBase64,320,320);
其中还有设置定时器等细节。
但是这种方式会使服务器由于频繁转发流量而 CPU 跑满,并且视频卡顿严重,所以是用第二种方式 - 第二种是通过 WebRTC 的方式,通过 STUN 服务器获取双方的 IP 地址,然后直接以 P2P 的方式,不通过服务器进行视频聊天。期间双方会协商各种参数。
假设 A 端是视频发起者,B 端是视频应答者
在建立视频连接的过程中,服务器只是简单地转发消息
-
A 获取用户摄像头,然后把流添加到本地和 RTCPeerConnection
```javascript video.createRtcPeerConnection();
navigator.mediaDevices.getUserMedia(video.mediaConstraints) .then(function (localStream) { video.selfPreview.srcObject = localStream; video.peerConnection.addStream(localStream); }) .catch(video.handleGetUserMediaError);
``` 2. 创建好 RTCPeerConnection 后,先设置本地描述符
然后 A 给 B 发送 video-offer 消息,携带 SDP 到 B 端
javascript video.peerConnection.createOffer().then(function (offer) { video.peerConnection.setLocalDescription(offer) }).then(function () { info.send({ user: info.user, type: "video-offer", rand: info.rand, to: video.target, data: [{ sdp: video.peerConnection.localDescription }] }) })
3. B 收到 video-offer 之后,需要设置远端 SDP(即 A 的 SDP)获取用户摄像头,并且添加视频流,设置本地预览流
创建一个本地 SDP,回应一个 video-answer 消息,并携带本地 SDP 到 A
```javascript video.target = msg.from; video.showWindow(); targetUsername = msg.from; video.createRtcPeerConnection();
let desc = new RTCSessionDescription(msg.data[0].sdp); video.peerConnection.setRemoteDescription(desc).then(function () { return navigator.mediaDevices.getUserMedia(video.mediaConstraints); }) .then(function (stream) { video.localStream = stream; video.selfPreview.srcObject = video.localStream; video.localStream.getTracks().forEach(track => video.peerConnection.addTrack(track, video.localStream)); }) .then(function () { return video.peerConnection.createAnswer(); }) .then(function (answer) { return video.peerConnection.setLocalDescription(answer); }) .then(function () { let msg = { user: info.user, to: video.target, type: "video-answer", rand: info.rand, data: [{ sdp: video.peerConnection.localDescription }] }; info.send(msg); }) .catch(video.handleGetUserMediaError);
``` 4. A 收到 video-answer 之后,需要设置远端 SDP(即 B 端 SDP)
javascript let desc = new RTCSessionDescription(msg.data[0].sdp); console.log(desc); await video.peerConnection.setRemoteDescription(desc).catch(video.reportError);
5. 之后 A 和 B 就会开始不断地交换 ICE 候选,直到选出最适合的 ICE 候选发送 ICE 候选
```javascript info.send({ user: info.user, type: "new-ice-candidate", rand: info.rand, to: video.target, data: [{ candidate: event.candidate }]
});
```
接收 ICE 候选
```javascript if (msg.data[0].candidate != null) {
let candidate = new RTCIceCandidate(msg.data[0].candidate); console.log(candidate); video.peerConnection.addIceCandidate(candidate) .catch(video.reportError); }
``` 6. 一旦选出了最适合的 ICE 候选,双方就把视频流添加到视频显示框,然后开始视频通话
javascript video.targetPlayer.srcObject = event.streams[0];
但是 ICE 候选的交互并没有就此结束,如果后续找到更好的 ICE 候选,则会使用新的 ICE 候选
如果通讯情况恶化,也会触发新的 ICE 候选交换 7. 挂断
设 A 主动挂断视频
A 要给 B 发送一个挂断信号(通过服务器)
javascript info.send({ user: info.user, to: video.target, rand: info.rand, type: "hang-up-video" });
然后进行 WebRTC 中的挂断操作
```javascript video.hideWindow(); let remoteVideo = video.targetPlayer; let localVideo = video.selfPreview; // 清除RTCPeerConnection对象 if (video.peerConnection) { video.peerConnection.ontrack = null; video.peerConnection.onremovetrack = null; video.peerConnection.onremovestream = null; video.peerConnection.onicecandidate = null; video.peerConnection.oniceconnectionstatechange = null; video.peerConnection.onsignalingstatechange = null; video.peerConnection.onicegatheringstatechange = null; video.peerConnection.onnegotiationneeded = null; // 停止各个轨道 if (remoteVideo.srcObject) { remoteVideo.srcObject.getTracks().forEach(track => track.stop()); }
if (localVideo.srcObject) { localVideo.srcObject.getTracks().forEach(track => track.stop()); } video.peerConnection.close(); video.peerConnection = null; } // 清除视频框中的视频流 remoteVideo.removeAttribute("src"); remoteVideo.removeAttribute("srcObject"); localVideo.removeAttribute("src"); remoteVideo.removeAttribute("srcObject");
```
至此,一个视频通话就完成了。
5. 编码设计
发送消息时,为了解决中文编码,图片,表情的问题,采用了 base64 编码,保证发送端和接收端收到的二进制序列都是一样的。
6. 数据库设计
ER 图如图
由 ER 图可以设计出以下 5 张数据表
6.1 注册用户表
- id :在数据库中标识一个用户的唯一整数
- 用户名:用户注册时的唯一标识
- 密码
- 注册时间
6.2 登录用户表
- id:对应于注册用户表中的 id
- 用户名
- 用户的会话凭证(随机数)
6.3 待收消息表
- id:用于标识每一条消息的唯一 id
- 发送者
- 接受者
- 消息内容
- 发送时间
6.4 历史消息表
- id:用于标识历史消息中的唯一一条消息
- 发送者
- 接受者
- 消息内容
- 发送时间
- 接收时间
6.5 朋友关系表
- host:朋友中的一方
- friend:朋友中的另一方
五、系统测试
1. 功能性测试
注册
登陆
发送消息
表情和图片
视频(本实例中都是自己的摄像头)
2. 安全性测试
图中更改了发送消息中随机数的值导致验证失败,说明如果不知道一个用户的密码或随机数,则无法以他的身份发送消息。
六、课程设计总结
本项目基于 WebRTC,WebSocket 实现了一个聊天系统。
参考资料
- Vue 文档
- Element-ui 文档
- Servlet--菜鸟教程
- WebSocket 客户端--MDN
- WebSocket 服务器端--MDN
- WebRTC--MDN
- JavaScript--MDN
- Ajax--MDN
附件说明
chat-src/---------源代码文件夹
chat-exe/--------可执行程序文件夹
设计报告.pdf---本文件
录屏.mp4---------使用示例
参考文献
- 基于Struts+JSP的SNS网站系统的设计与实现(吉林大学·王雷)
- 基于SSH架构的Web OA系统的设计与实现(吉林大学·李乐)
- 基于.NET下Web服务的信息查询系统的研究与设计(合肥工业大学·张静)
- 基于JSP的雄霸天下游戏网的后台操作系统的开发设计(电子科技大学·张璇)
- 企业项目管理系统设计与实现(吉林大学·刘舒杨)
- 支持多客户端网站系统的研究与实现(大连海事大学·宋晓慧)
- 基于JMF的视频聊天系统的开发与实现(华南理工大学·李世勇)
- 基于WebSocket协议的实时网页通信的研究与实现(江苏科技大学·包文祥)
- 基于.NET平台的游戏门户系统设计与实现(电子科技大学·余胜鹏)
- 基于Node.js和WebSocket的即时通信系统的设计与实现(南京邮电大学·茆玉庭)
- 基于.NET平台的游戏门户系统设计与实现(电子科技大学·余胜鹏)
- 基于.NET平台的游戏门户系统设计与实现(电子科技大学·余胜鹏)
- 基于WebRTC的视频会议系统的设计与实现(西北大学·孙凯龙)
- 基于Struts+Hibernate的人力资源管理系统(吉林大学·金勇杰)
- WebRTC系统中WEB前端子系统的设计与实现(北京邮电大学·倪礼)
本文内容包括但不限于文字、数据、图表及超链接等)均来源于该信息及资料的相关主题。发布者:毕设海岸 ,原文地址:https://m.bishedaima.com/yuanma/36143.html