在线聊天室的实现(3)--简易聊天室的实现

前言:
  就如前文所讲述的, 聊天室往往是最基本的网络编程的学习案例. 本文以WebSocket为底层协议, 实现一个简单的聊天室服务.
  服务器采用Netty 4.x来实现, 源于其对websocket的超强支持, 基于卓越的性能和稳定.
  本系列的文章链接如下:
  1). websocket协议和javascript版的api 
  2). 基于Netty 4.x的Echo服务器实现 

初步构想:
  本文对聊天室服务的定位还是比较简单. 只需要有简单的账户体系, 能够实现简单的群聊功能即可.
  流程设计初稿:
  1). 用户登陆
  
  2). 群聊界面
  
  这里没有聊天室的选择, 只有唯一的一个. 这边有没有表情支持, 内容过滤. 一切皆从简.

协议约定:
  在websocket协议的基础之上, 我们引入应用层的聊天协议.
  协议以JSON作为数据交互格式, 并进行扩展和阐述.
  • 请求形态约定

1
request: {cmd:"$cmd", params: {}}

  • 响应形态约定

1
response: {cmd:"$cmd", retcode:$retcode, datas:{}}  // retcode => 0: success, 1: fail

  
  1). 用户登陆请求:

1
2
3
4
request: {cmd:"login", params: {username:"$username"}}
response:
  成功: {cmd:"login", retcode: 0, datas:{username:$username, userid:$userid}}
  失败: {cmd:"login", retcode:1, datas:{}}

  注: 成功后, 返回分配的userid(全局唯一).
  2). 消息发送请求:

1
2
3
4
request: {cmd:"send_message", params:{message:$message, messageid:$messageid}}
response:
  成功: {cmd:"send_message", retcode:0, datas:{messageid:$messageid}}
  失败: {cmd:"send_message", retcode:1, datas:{messageid:$messageid}}  

  注: 这边的messageid是必须的, 可以提示那条消息成功还是失败
  3). 消息接受(OnReceive):

1
2
3
4
刷新用户列表
  {cmd:"userlist", retcode:0, datas: {users: [{userid:$userid, username:$username}, {userid:$userid, username:$username}]}}
接收消息
  {cmd:"receive_message", retcode:0, datas: {message:"$message", username:"$username", userid:"$userid", timestamp:"$timestamp"}}

服务端实现:
  借助Netty 4.x来构建服务器端, 其Channel的pipeline设定如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
serverBootstrap.group(bossGroup, workerGroup)
  .channel(NioServerSocketChannel.class)
  .childHandler(new ChannelInitializer<SocketChannel>() {
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
      ChannelPipeline cp = socketChannel.pipeline();
      // *) 支持http协议的解析
      cp.addLast(new HttpServerCodec());
      cp.addLast(new HttpObjectAggregator(65535));
      // *) 对于大文件支持 chunked方式写
      cp.addLast(new ChunkedWriteHandler());
      // *) 对websocket协议的处理--握手处理, ping/pong心跳, 关闭
      cp.addLast(new WebSocketServerProtocolHandler("/chatserver"));
      // *) 对TextWebSocketFrame的处理
      cp.addLast(new ChatLogicHandler(userGroupManager));
    }
  });

  注: HttpServerCodec / HttpObjectAggregator / ChunkedWriteHandler / WebSocketServerProtocolHandler, 依次引入极大简化了websocket的服务编写.
  ChatLogicHandler的定义, 其对TextWebSocketFrame进行解析处理, 并从中提取聊天协议的请求, 并进行相应的状态处理.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame twsf) throws Exception {
  String text = twsf.text();
  JSONObject json = JSON.parseObject(text);
 
  if ( !json.containsKey("cmd") || !json.containsKey("params") ) {
    ctx.close();
    return;
  }
 
  String cmd = json.getString("cmd");
  if ( "login".equalsIgnoreCase(cmd) ) {
    // *) 处理登陆事件
    handleLoginEvent(ctx, json.getJSONObject("params"));
  } else if ( "send_message".equalsIgnoreCase(cmd) ) {
    // *) 处理群聊消息事件
    handleMessageEvent(ctx, json.getJSONObject("params"));
  }
}

  其实这边还可以再加一层, 用于聊天协议的请求/响应的封装, 这样结构上更清晰. 但为了简单起见, 就省略了. 

客户端的实现:
  web客户端最重要的还是, 对chatclient的封装. 其封装了websocket了, 实现了业务上的login和sendmessage函数.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
(function(window) {
 
  ChatEvent.LOGIN_ACK = 1001;
  ChatEvent.SEND_MESSAGE_ACK = 1002;
  ChatEvent.USERLIST = 2001;
  ChatEvent.RECEIVE_MESSAGE = 2002;
  ChatEvent.UNEXPECT_ERROR = 10001;
 
  function ChatEvent(type, retcode, msg) {
    this.type = type;
    this.retcode = retcode; // retcode : 0 => success, 1 => fail
    this.msg = msg;
  }
 
  WebChatClient.UNCONNECTED = 0;
  WebChatClient.CONNECTED = 1;
  WebChatClient.LOGINED = 1;
 
  function WebChatClient() {
    this.websocket = null;
    this.state = WebChatClient.UNCONNECTED;
    this.chatEventListener = null;
  }
 
  WebChatClient.prototype.init = function(wsUrl, chatEventListener) {
    this.state = WebChatClient.UNCONNECTED;
    this.chatEventListener = chatEventListener;
    this.websocket = new WebSocket(wsUrl); //创建WebSocket对象
    var self = this;
    this.websocket.onopen = function(evt) {
      self.state = WebChatClient.CONNECTED;
    }
    this.websocket.onmessage = function(evt) {
      var res = JSON.parse(evt.data);
      if ( !res.hasOwnProperty("cmd") || !res.hasOwnProperty("retcode") || !res.hasOwnProperty("datas") ) {
        self.chatEventListener(new ChatEvent(ChatEvent.UNEXPECT_ERROR, 0));
      } else {
        var cmd = res["cmd"];
        var retcode = res["retcode"];
        var datas = res["datas"];
        switch(cmd) {
          case "login":
            self.chatEventListener(new ChatEvent(ChatEvent.LOGIN_ACK, retcode, datas));
            break;
          case "send_message":
            self.chatEventListener(new ChatEvent(ChatEvent.SEND_MESSAGE_ACK, retcode, datas));
            break;
          case "userlist":
            self.chatEventListener(new ChatEvent(ChatEvent.USERLIST, retcode, datas));
            break;
          case "receive_message":
            self.chatEventListener(new ChatEvent(ChatEvent.RECEIVE_MESSAGE, retcode, datas));
            break;
        }
      }
    }
    this.websocket.onerror = function(evt) {
    }
  }
 
  WebChatClient.prototype.login = function(username) {
    if ( this.websocket.readyState == WebSocket.OPEN && this.state == WebChatClient.CONNECTED ) {
      var msgdata = JSON.stringify({cmd:"login", params:{username:username}});
      this.websocket.send(msgdata);
    }
  }
 
  WebChatClient.prototype.sendMessage = function(message) {
    if ( this.websocket.readyState == WebSocket.OPEN && this.state == WebChatClient.LOGINED ) {
      var msgdata = JSON.stringify({cmd:"send_message", params:{message:message}});
      this.websocket.send(msgdata);
    }
  }
 
  // export
  window.ChatEvent = ChatEvent;
  window.WebChatClient = WebChatClient;
 
})(window);

代码下载:
  由于采用websocket作为底层网络通讯协议, 因此需要浏览器支持(最好为Chrome).
  服务器和web客户端的源码下载地址为: http://pan.baidu.com/s/1pJDYo3p.
  文件的目录结构如下:
  

后期展望:
  编写完初步版本后, 一群有爱的小伙伴们帮我友情测试了一把. 中间反馈了很多体验上的改进意见. 比如Enter键自动发送, 最新留言与页面底部同步, 自己名称高亮显示.
  也有反馈添加表情等高大上的功能, ^_^. 总之感觉棒棒哒, 觉得再做一件很伟大的事情. oh yeah.

写在最后:
  
如果你觉得这篇文章对你有帮助, 请小小打赏下. 其实我想试试, 看看写博客能否给自己带来一点小小的收益. 无论多少, 都是对楼主一种由衷的肯定.

   

posted on   mumuxinfei  阅读(7118)  评论(0编辑  收藏  举报

编辑推荐:
· 你所不知道的 C/C++ 宏知识
· 聊一聊 操作系统蓝屏 c0000102 的故障分析
· SQL Server 内存占用高分析
· .NET Core GC计划阶段(plan_phase)底层原理浅谈
· .NET开发智能桌面机器人:用.NET IoT库编写驱动控制两个屏幕
阅读排行:
· 我干了两个月的大项目,开源了!
· 推荐一款非常好用的在线 SSH 管理工具
· 千万级的大表,如何做性能调优?
· 盘点!HelloGitHub 年度热门开源项目
· Phi小模型开发教程:用C#开发本地部署AI聊天工具,只需CPU,不需要GPU,3G内存就可以运行,

导航

< 2025年1月 >
29 30 31 1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31 1
2 3 4 5 6 7 8

统计

点击右上角即可分享
微信分享提示