前面文章我们对聊天的协议进行了简单的封装:C#实现WebSocket服务器:(04)实现聊天室-协议和后端部分
这里我们介绍下前端的封装和实现以及演示。
前端是基于Vue来做的,理解起来也不复杂。
0、HTML结构
结构很简单:用户登录框div.login
、消息发送框div.inputs
、消息显示区div.wrapper-contents
。
<div class="wrapper">
<div id="app">
<div class="login" v-if="loginStatus !== 2">
<p><input type="text" v-model="name" placeholder="请输入名称登录" :disabled="loginStatus !== 0" /></p>
<p><input type="button" value="登录" @click="login" :disabled="loginStatus !== 0" /></p>
</div>
<div v-else>
<div class="wrapper-contents">
<div class="contents" ref="contents">
<div v-for="(msg, index) in messages" :key="index" :class="getClass(msg)">
<component :is="contents" :msg="msg"></component>
</div>
</div>
</div>
<div class="inputs ">
<input v-model="message" type="text" @keyup.enter="post" placeholder="输入消息发送" />
<div class="buttons">
<button @click="post">发送</button>
<button @click="quit">离开</button>
</div>
</div>
</div>
</div>
</div>
1、websocket封装
扩展了下事件功能。
1、将服务器发送给客户端的消息(login
、enter
,post
,exit
)映射成事件(@login
,@enter
,@post
,@exit
),方便下游程序处理。
2、将客户端请求协议封装成具体的方法:login
、send
、quit
。
代码也是流水账逻辑,不难理解,就是注意login的逻辑是异步的:先连接服务器,连接事件后发送登录,登录响应收到后才会提交@login
事件。
/**
* 管理websocket连接
* @param {String} wsUrl websocket地址
*/
function connection (wsUrl) {
this.wsUrl = wsUrl;
this.socket = null;
this.status = 0;
this.__events = {};
}
/**
* 简单的事件注册。
* @param {String} ev
* @param {Function} handler
* @param {any} context
*/
connection.prototype.on = function (ev, handler, context) {
this.__events[ev] = { handler, once: false, context: context || this };
}
/**
* 注册一次性事件
* @param {String} ev
* @param {Function} handler
* @param {any} context
*/
connection.prototype.once = function (ev, handler, context) {
this.__events[ev] = { handler, once: true, context: context || this };
}
/**
* 调用事件
* @param {String} ev
* @param {...any} args
* @returns
*/
connection.prototype.emit = function (ev, ...args) {
if (!this.__events[ev]) return;
const handler = this.__events[ev];
handler.handler.apply(handler.context, args);
if (handler.once === true) {
this.__events[ev] = null;
}
};
/**
* 发送消息
* @param {String} action
* @param {Object} payload
* @returns
*/
connection.prototype.send = function (action, payload) {
if (this.status !== 2) return;
this.socket.send(JSON.stringify({ action, payload }));
}
/**
* 退出
* @returns
*/
connection.prototype.quit = function () {
if (this.status !== 2) return;
this.socket.send(JSON.stringify({ action: 'quit', payload: {} }));
}
/**
* 登录
* @param {String} name 用户名
* @returns
*/
connection.prototype.login = function (name) {
if (this.status !== 2) {
this.once('wait-connected', () => this.send('login', { name: name }));
if (this.status === 0) this.connect();
return;
}
this.send('login', { name: name });
};
/**
* 连接服务器
*/
connection.prototype.connect = function () {
this.status = 1;
const that = this
const webSocket = new WebSocket(this.wsUrl);
that.emit('connecting');
webSocket.onopen = function () {
that.socket = webSocket;
that.status = 2
that.connectFailedCount = 0;
that.emit('connected');
that.emit('wait-connected');
}
webSocket.onmessage = function (ev) {
try {
const payload = JSON.parse(ev.data)
that.emit('@' + payload.action, payload.payload);
} catch (ex) {
}
};
webSocket.onclose = function (ev) {
that.status = 0;
that.emit('close', ev);
}
webSocket.onerror = function (ev) {
that.status = 0;
that.emit('error', ev);
}
}
2、业务逻辑封装
封装的内容是Vue实例,以及在Vue实例中订阅、发送消息和对页面进行操作、展示。
对滚动条作了简单的防抖处理。
也是流水账,不复杂。
/**
* 防抖函数
* @param {Function} fn
* @param {Number} timeout
* @returns
*/
function lazyFunction (fn, timeout) {
var timer = 0;
return function () {
if (timer) window.clearTimeout(timer);
var args = arguments, that = this;
timer = window.setTimeout(function () {
fn.apply(that, args)
}, timeout);
};
}
/**
* 实例化Vue
*/
new Vue({
el: '#app',
data () {
return {
url: 'ws://127.0.0.1:4189/',
me: null,
loginHandler: null,
name: '',
connection: null,
loginStatus: 0,
message: '',
messages: []
}
},
watch: {
messages () {
this.updateScroll();
}
},
created () {
/**
* 初始化connection,注册各种事件
* @ 开头的事件为服务器发送的消息
*/
const conn = this.connection = new connection(this.url);
conn.on('connecting', () => this.loginStatus = 1);
conn.on('close', () => this.loginStatus = 0);
conn.on('error', () => this.loginStatus = 0);
conn.on('@login', function (payload) {
this.me = { name: this.name, id: payload.connectionId }
this.loginStatus = 2;
}, this);
conn.on('@enter', (payload) => this.messages.push({ type: 'log', message: `${payload.name} 进入聊天室` }), this);
conn.on('@exit', (payload) => this.messages.push({ type: 'log', message: `${payload.name} 离开聊天室` }), this);
conn.on('@post', (payload) => this.messages.push({ type: 'post', payload }), this)
},
methods: {
/**
* 更新滚动条
*/
updateScroll: lazyFunction(function () {
this.$nextTick(() => {
const contentsRef = this.$refs['contents']
contentsRef.scrollTop = contentsRef.scrollHeight
});
}, 30),
/**
* 设置样式
* @param {Object} msg
* @returns
*/
getClass (msg) {
if (msg.type === 'log') return 'message-type-log';
return [
'message-type-' + msg.type,
'message-owner-' + (msg.payload.connectionId === this.me.id ? 'mine' : 'user')
].join(' ')
},
/**
* 登录
* @returns
*/
login () {
if (!this.name) {
return;
}
this.connection.login(this.name);
},
/**
* 发布消息
* @returns
*/
post () {
if (!this.message) return;
this.connection.send('post', { message: this.message });
this.message = '';
},
/**
* 退出
*/
quit () {
this.connection.quit();
}
},
computed: {
/**
* 渲染消息条目
* @returns
*/
contents () {
const me = this.me;
return {
props: {
msg: { type: Object, required: true }
},
render (h) {
if (this.msg.type === 'log') {
return h('span', [this.msg.message]);
}
return [
h('div',
{
'class': 'message-content'
},
[
h('label', [this.msg.payload.connectionId === me.id ? '我' : this.msg.payload.name]),
h('div', [this.msg.payload.message])
])
];
}
}
}
}
});
3、演示
聊天服务器程序实现很简单,我们把之前OnWebSocket
改成了GetMessager
,方法返回一个Messager
给父类即可。
public class Server : HttpServerBase
{
public Server() : base()
{
//设置根目录
WebRoot = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "web"));
}
protected override Messager GetMessager(HttpRequest request, Stream stream)
{
return new Connection(stream);
}
}
完整项目托管:https://github.com/hooow-does-it-work/websocket-chat
前端内容:https://github.com/hooow-does-it-work/websocket-chat/tree/main/bin/Release/web
运行服务器,浏览器访问:http://127.0.0.1:4189/chat.html
,多开几个页面,相互发送消息。
class Program
{
static void Main(string[] args)
{
StartWebSocketServer(4189);
Console.ReadLine();
}
private static void StartWebSocketServer(int port)
{
HttpServerBase server = new Chat.Server();
try
{
server.Start("0.0.0.0", port);
Console.WriteLine("WebSocket服务器启动成功,监听地址:" + server.LocalEndPoint.ToString());
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
}
}
分别用Alice,Bob,Lily顺序登录,相互发送消息,测试各功能。
4、总结
这两篇文章主要是对我们前面对WebSocket协议的实现,通过自定义payload内容实现一个简单的聊天室。
可以实现多聊天室、聊天室切换功能,后端代码都实现了,只是我们前端没去实现。
到此为止,所有关于WebSocket的介绍和演示都完成了。
完整项目托管地址:https://github.com/hooow-does-it-work/websocket-chat
依赖项目(注意是dev-async分支,不是main分支):https://github.com/hooow-does-it-work/iocp-sharp/tree/dev-async