当前位置:首页 » 《随便一记》 » 正文

C#实现WebSocket服务器:(05)实现聊天室-前端部分_艾恩

9 人参与  2021年11月24日 15:23  分类 : 《随便一记》  评论

点击全文阅读


前面文章我们对聊天的协议进行了简单的封装: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、将服务器发送给客户端的消息(loginenterpostexit)映射成事件(@login@enter@post@exit),方便下游程序处理。
2、将客户端请求协议封装成具体的方法:loginsendquit
代码也是流水账逻辑,不难理解,就是注意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


点击全文阅读


本文链接:http://m.zhangshiyu.com/post/31247.html

封装  服务器  登录  
<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1