@自嗨的江江

爱生活爱叽歪

typescript + node.js 实现的简单聊天服务和客户端(demo)

最近项目没那么急,就花时间多了解一下typescript和node.js,当然学习最好的方式做实例应用。

之前在做叽歪助手站内通知系统时简单了解了一下node.js,但是由于项目要求通知系统的灵活、扩展,关键很多交互不是在Web客户端。叽歪助手的通知系统更多的是系统通知,例如支付通知,订单通知等,都是触发的服务端事件。虽然放在客户端也可以,但是系统通知的第一要求是稳定可靠,放在客户端不可控因素太多,最后还是放在服务端,因此放弃了用node.js来搭建通知系统的方案,最终选择基于SignalR来搭建了一个自宿主的通知服务器,Web应用端作为连接客户端用C#来消费,Web终端只是通知的接收,没有任何通知触发。有关用C#+owin搭建自宿主服务可以参考:http://jiang.jonvie.com/post/2018/06/14/49b32cda4dff7fcb

其实node.js搭建通知服务,应该也可以做到让.net的服务端C#去触发通知事件,但是拐弯较多(可能我设想的方案有问题),但是node.js搭建大量js客户端去消费的服务端还是不错的。这次为了加深对node.js的了解和熟悉,同时学习了解一下typescript,就选择用这两者结合做一个demo来熟悉了解特性,说不定未来哪天会用到。

好了,这里我不去复制网上的文字,只谈我的个人理解:
1、关于node.js,我认为是一个轻量的js运行环境,或者说是javascript的虚拟机。通常我们熟悉跑js的环境就是浏览器,对,这里我就把node当成另外一个独立运行的程序,并提供了js的运行环境。在win下,理论上一个独立运行的exe是可以干很多事情的,node的天下就是提供了很多的开放的包,让js可以通过这些包的功能和接口,用你最熟悉的js语言,让node去干以前在浏览器里面干不了的事情,甚至是让node去监听一个端口,打开http服务,这样你的js一个文件通过node去运行就让node成了一个http服务器(后面马上要用到)。

2、关于typescript,我认为就是换了一个语言习惯和思维去写js,然后在用编译工具tsc把ts源码翻译成js源码。也就是ts并不是直接执行,最终执行的都是js。大概就是js是个很好工具大家都认同,但是写起来真的太灵活(太灵活的东西真的很麻烦)。于是就用ts来强制规定一下按一个套路来写便于管理。另外呢,便于写习惯C#和Java的猿们,可以方便的用自己习惯接近的语法来写js。C#和Java猿们,必须要跟浏览器、nodejs等换进交流时,不懂他们的语言javascript没关系,可以用跟你方言较接近的typescript来整,再通过tsc来翻译成js丢到js环境里去运行。

嗯,我认为的就这样,下面实际看看实现思路和代码,这里我是用vscode作为开发工具,一般项目开发用vs,用习惯了,也便于偷懒。但是一般学习和开发小玩意,还是喜欢用vscode这样的打开快的文本类工具,可以更深入了解编译执行过程。下面来开发(这里略过全部需要工具环境的安装搭建过程,提到要用的包自行百度去安装):

1、首先做及时通讯要建立一个通讯的中间服务器来接收客户响应,并根据客户端的响应来做消息推送,这个其实nodejs很有优势,这里也就如前文所说,用typescript在node下开发一个小的http服务监听,并提供长连接的访问和监听,这里需要用到socket.io(自行去百度安装)。node.js的运行方式是cmd下启动,同时运行一个启动入口文件(这里定义为来启动整个应用,这里将入口文件定义为AppServer.ts,生成后为同名js后缀文件。

import { createServer, Server, IncomingMessage, ServerResponse, request } from 'http';
import { readFile } from 'fs';
import io from 'socket.io';
import {User, UserGroup} from './Com/User';

const hostname: string = "localhost";//提供http服务的主机名或者ip
const port: number = 3000;//http服务的端口
var count: number = 0;//记录并统计当前服务的连接数
var mainUsers:UserGroup=new UserGroup();//定义个链接用户组的实例,用于保存连接用户信息

//创建一个http服务器对象
var server: Server = createServer((req: IncomingMessage, res: ServerResponse) => {
    console.log(req.url);
    readFile('./index.html', function (err: NodeJS.ErrnoException, data: Buffer) {
        res.writeHead(200, { 'Content-Type': 'text/html' });
        res.end(data, 'utf-8');
    });
});
//启动服务器对象监听
server.listen(port, hostname, () => {
    console.log(`服务已经启动: http://${hostname}:${port}/`);
});
 

//通过IO监听当前http连接服务,创建一个socket实例
var ioSer=io.listen(server);
//监听socket的客户端的连接事件,用于处理新的客户端打开连接的事件操作
ioSer.sockets.on('connection', function (socket: io.Socket) {
    count++;
    //为当前的连接客户端创建一个user实例对象,只对当前的连接有效
    let user=new User(socket.client.id);
    user.UserName='anymouse';

    //将当前的连接客户端信息添加到全局的连接用户清单
    mainUsers.addOrUpdateUser(user);
    console.log(`${socket.client.id} 已连接,连接ID:${mainUsers.get(socket.client.id).ConnID} 连接数:${count}`);

    //用户连接时,给所全局有连接客户端推送一个online事件
    //向全局所有的连接客户端广播当前的连接数
    ioSer.emit('online', { number: count});
    //向当前客户端推送updateUser事件
    //上线后给当前客户端发送用户列表,仅向当前打开的连接客户端推送一次用户列表数据
    socket.emit('updateUser', mainUsers.all());
    //socket.emit('online', { number: count});
    //socket.broadcast.emit('online', { number: count });

    //监听客户端发送的updateUser事件,用于处理用户端修改user资料
    socket.on('updateUser',function(data){
        user.UserName=data.userName;
        //将更新的用户资料修改到全局的用户清单
        mainUsers.addOrUpdateUser(user);
        //有用户修改用户信息后,通知诶所有连接客户端
        ioSer.emit('updateUser',  mainUsers.all());
    });

    //监听客户端的toUser事件,并将用户消息转发给指定的目标用户
    socket.on('toUser',function(data){
        let toId:string=data.ToUserMsg.ConnID;
        //如果指定的用户还在线,通过定向广播触发toMe事件,将消息定向转发给用户
        //客户端需要监听toMe事件处理发送给自己的消息
        if (ioSer.sockets.connected[toId]) {
            var msg={ToUserMsg:{ConnID:toId},FromUserMsg:{ConnID:user.ConnID,UserName:user.UserName},MsgContent:data.MsgContent}
            ioSer.sockets.connected[toId].emit('toMe',msg);
        }
    });

    //监听用户连接断开
    socket.on('disconnect', function () {
        count--;
        mainUsers.deleteUser(socket.client.id);
        console.log(`${socket.client.id} 已断开,连接数:${count}`);
        ioSer.emit('online', { number: count});
    });

});

上面用到了自定义对象User,UserGroup,我们这里定义文件User.ts

export class User {
    public ConnID: string = "";
    public UserName: string = "";
    public UserID: string = "";
    public UserSpace: string = "";
    public Room: string = "";
    constructor(_connID: string) {
        this.ConnID = _connID;
    }
}
export class UserGroup{ 
    private _list:{[key: string]: User}={}
    
    //获取当前实例的用户列表
    public all():{[key: string]: User}{
        return this._list;
    }

    public get(connId:string):User{
        return this._list[connId];
    }

    public addOrUpdateUser(u:User):void{
        this._list[u.ConnID]=u;
    }

    public deleteUser(connId:string):void{
        delete this._list[connId];
    }

    public hasUser(u:User): boolean {
        if (this._list[u.ConnID] == undefined)
            return false;
        else
            return true;
    }
 }

 

除了引用的包,这里就是全部服务端代码,接下来就是客户端的处理html界面文件,我们这里也是放在服务端运行。在appserver.ts里

    readFile('./index.html', function (err: NodeJS.ErrnoException, data: Buffer) {
        res.writeHead(200, { 'Content-Type': 'text/html' });
        res.end(data, 'utf-8');
    });

 

这一段就是讲index.html界面文件提供给访问客户端,在html里面引入了3个包,1、jquery,这个太厉害了,必须要用大家懂的,tmpl是用来在客户端格式化html模版的,剩下那个是客户端打开连接必须的。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Socket.IO Example</title>
  </head>
<script src="http://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
<script src="http://ajax.microsoft.com/ajax/jquery.templates/beta1/jquery.tmpl.min.js"></script>
<script src="http://localhost:3000/socket.io/socket.io.js"></script>
<script type="text/x-jquery-tmpl" id="msgTemp">
  <div {{html Style.Box}}>
  <div {{html Style.ID}}>${FromUserMsg.UserName}</div>
  <div {{html Style.Content}}>{{html MsgContent}}</div>
  </div>
 </script>
  <body>
    <h1>TS+nodeJS实现及时通讯</h1>
    <h2>在线用户<span id="count">0</span></h2>
    <div id="msg" style="width:450px;height: 600px;border: 1px solid #000000;overflow:scroll;padding: 5px;"></div> 
    <div>
      <div>用户名:<input type="text" id="userName"value="" /> 
        发消息给:
        <select id="userID">
          <option value="">请选择</option>
        </select>
      </div>
      <div>消息内容:<textarea id="msgText"></textarea></div>
      <button id="send">发送</button>
    </div>
    <script>
      var testData={UserMsg:{ConnID:"userid"},MsgContent:"一个文本消息"}
      var path=window.location.pathname;
      var thisUserName="anymouse";
      console.log(path);
      var count = document.getElementById('count');
      var socket = io.connect('http://localhost:3000'+path);
      socket.on('online',function(data){
        count.innerHTML = data.number
      });
      socket.on('updateUser',function(data){
           //console.log(data);
           $("#userID").find('option').remove();
           Object.keys(data).forEach(function(key){
              //console.log(key,data[key]);
              $("#userID").append(`<option value='${data[key].ConnID}'>${data[key].UserName}[${data[key].ConnID}]</option>`);
           });
      });
      socket.on('toMe',function(data){
        if(data.ToUserMsg.ConnID!=data.FromUserMsg.ConnID)
        {
         addMsg(data,false);
        }
      });

      function addMsg(dataJson,isme)
      {
        dataJson.Style={Box:"",ID:"",Content:""};
        if(isme)
        {
          dataJson.Style.Box='style="text-align: right;"';
          dataJson.Style.ID='style="text-align: right;"';
          dataJson.Style.Content='style="text-align: right;"';
        }
        $("#msgTemp").tmpl(dataJson).appendTo("#msg");
        $("#msg").scrollTop($("#msg")[0].scrollHeight); 
      }
      //addMsg(testData,false);
      //addMsg(testData,true);


      $(function(){
        if($("#userName").val()=="")
        {
          $("#userName").val(thisUserName);
        }

        $("#send").click(function(){
          if($("#userName").val()!="" && thisUserName!=$("#userName").val())
          {
           thisUserName=$("#userName").val();
           socket.emit('updateUser', {userName:$("#userName").val()});
          }

          if($('#msgText').val()!="")
          {
          let msg={
             ToUserMsg:{ConnID:$('#userID').val()},
             FromUserMsg:{ConnID:"",UserName:thisUserName},
             MsgContent:$('#msgText').val()
            };
           socket.emit('toUser',msg);
           addMsg(msg,true);
          }
        });
      });
    </script>
  </body>
</html>

 

好了,以上三个文件就构成了一个完成的聊天程序,这里聊天是通过服务器中转做的模拟点对点,计数通知都是全局的,基本全局发消息,私发消息的关键点都有用到。当然还有指定群组发消息等场景,基于以上demo,在参考官方文档,自己可以再琢磨一下。以上demo源码:
https://github.com/jonvie/nodechar

 
不允许评论