SHIHUC

好记性不如烂笔头,还可以分享给别人看看! 专注基础算法,互联网架构,人工智能领域的技术实现和应用。
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

一个基于netty的websocket聊天demo

Posted on 2017-03-15 16:33  shihuc  阅读(3483)  评论(2编辑  收藏  举报

这里,仅仅是一个demo,模拟客户基于浏览器咨询卖家问题的场景,但是,这里的demo中,卖家不是人,是基于netty的程序(我就叫你uglyRobot吧),自动回复了客户问的问题。

项目特点如下:

1. 前端模拟在第三方应用中嵌入客户咨询页面,这里采用的是基于tornado的web应用,打开页面即进入咨询窗口

2. 客户咨询的内容,将会原封不动的被uglyRobot作为答案返回。(真是情况下,客户是不是会疯掉,哈哈)

3. 客户长时间不说话,uglyRobot会自动给予一段告知信息,并将当前的channel释放掉

4. 客户再次回来问问题时,将不再显示欢迎词,重新分配channel进行"交流"

 

话不多说,直接上关键部分的代码。

首先,看前端的代码. python的web后台部分:

#!/usr/bin/env python
#-*- coding:utf-8 -*-
#__author__ "shihuc"

import tornado.ioloop
import tornado.httpserver
import tornado.web
import tornado.options
import os
import json
import multiprocessing

from tornado.options import define, options
define("port", default=9909, help="Please run on the given port", type=int)

procPool = multiprocessing.Pool()

class ChatHandler(tornado.web.RequestHandler):
    def get(self):
        self.render("chat.html")


settings = {
    'template_path': 'page',          # html文件
    'static_path': 'resource',        # 静态文件(css,js,img)
    'static_url_prefix': '/resource/',# 静态文件前缀
    'cookie_secret': 'shihuc',        # cookie自定义字符串加盐
    'xsrf_cookies': True              # 防止跨站伪造
}

def make_app():
    return tornado.web.Application([
        (r"/", ChatHandler)
    ], default_host='',transforms=None, **settings)

if __name__ == "__main__":
    tornado.options.parse_command_line()
    app = make_app()
    http_server = tornado.httpserver.HTTPServer(app)
    http_server.listen(options.port)
    tornado.ioloop.IOLoop.current().start()

聊天的前端html页面的内容:

<!DOCTYPE html>
<html>
<head lang="en">
    <link rel="shortcut icon" href="{{static_url('image/favicon.ico')}}" type="image/x-icon" />
    <!--<link rel="shortcut icon" href="./favicon.ico" type="image/x-icon" />-->
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"/>
    <title>疯子聊天DEMO</title>
    <link rel="stylesheet" href="{{static_url('css/base.css')}}"/>
    <link rel="stylesheet" href="{{static_url('css/consult.css')}}"/>
</head>
<body>
    <div class="consultBox">
        <div class="consult">
            <div class="consult-Hd">
                <p class="checkmove"></p>
                <img src="{{static_url('image/backProperty.png')}}" alt=""/>
                <span>疯子机器人有限公司</span>
                <a href="javascript:;" class="bell"></a>
                <a href="javascript:;" title="关闭" class="close"></a>
            </div>
            <div class="consult-Bd">
                <div class="consult-cont">
                    <div class="consult-cont-date"></div>
                </div>
            </div>
            <div class="consult-Fd">
                <div class="consult-Fd-hd">
                    <a href="javascript:;" class="brow"></a>
                    <a href="javascript:;" class="picture"></a>
                </div>
                <div>
                    <textarea class="consult-Fd-textarea" id="Ctextarea" autofocus spellcheck="false"></textarea>
                </div>
                <div class="buttonBox">
                    <span class="evaluate">请对服务做出评价</span>
                    <span class="button disable" id="Cbtn">发送</span>
                </div>
            </div>
        </div>
    </div>
    <script src="{{static_url('js/jquery-1.11.1.min.js')}}"></script>
    <script src="{{static_url('js/bootstrap.min.js')}}"></script>
    <script src="{{static_url('js/bootbox.js')}}"></script>
    <script src="{{static_url('js/consult.js')}}"></script>
</body>
</html>

重点前端逻辑consult.js的内容:

  1 /**
  2  * Created by shihuc on 2017/2/21.
  3  */
  4 
  5 var ws = null;
  6 var wsurl = "ws://10.90.9.20:9080/websocket" 
  7 var wshandler = {};
  8 
  9     var consult={};
 10     consult.init=function(){
 11         consult.setDateInfo();
 12         consult.touch();
 13         consult.send();
 14     };
 15     consult.touch=function(){
 16         $('#Ctextarea').on('keyup',function(e){
 17             if(e.keyCode != 13){
 18                 if($('#Ctextarea').val()!=""){
 19                     $('.button').removeClass('disable');
 20                 }else{
 21                     $('.button').addClass('disable');
 22                 }
 23             }
 24         });
 25         $('.close').click(function(){
 26             $('.consultBox').addClass('hide');
 27         });
 28         $('.bell').click(function(){
 29             $(this).toggleClass('bell2');
 30         })
 31     };
 32     consult.send=function(){
 33         $('.button').click(function(){
 34             if(!$(this).hasClass('disable')){
 35                 var cont=$('#Ctextarea').val();
 36                 if(ws == null){
 37                    wshandler.reconnect(wshandler.interval, cont);
 38                 }else{
 39                    consult.fullSend(cont);
 40                 }
 41             }else{
 42                 return false;
 43             }
 44         });
 45         $('#Ctextarea').keydown(function(e){
 46             if(e.keyCode == 13){
 47                 if(!$('.button').hasClass('disable')){
 48                     var cont=$('#Ctextarea').val();
 49                     if(ws == null){
 50                         wshandler.reconnect(wshandler.interval, cont);
 51                     }else{
 52                         consult.fullSend(cont);
 53                     }
 54                 }else{
 55                     return false;
 56                 }
 57             }
 58         });
 59     };
 60     consult.fullSend = function(cont) {
 61         ws.send(cont);
 62         $('.consult-cont').append(consult.clientText(cont));
 63         $('#Ctextarea').val("");
 64         $('.button').addClass('disable');
 65         consult.position();
 66     };
 67     consult.clientText=function(cont){
 68         var newMsg= '<div class="consult-cont-right">';
 69         newMsg +='<div class="consult-cont-msg-wrapper">';
 70         newMsg +='<i class="consult-cont-corner"></i>';
 71         newMsg +='<div class="consult-cont-msg-container">';
 72         newMsg +="<p>Client: "+ cont +"</p>";
 73         newMsg +='</div>';
 74         newMsg +='</div>';
 75         newMsg +='</div>';
 76         return newMsg;
 77     };
 78     consult.serverText=function(cont){
 79         var newMsg= '<div class="consult-cont-left">';
 80         newMsg +='<div class="consult-cont-msg-wrapper">';
 81         newMsg +='<i class="consult-cont-corner"></i>';
 82         newMsg +='<div class="consult-cont-msg-container">';
 83         newMsg +="<p>"+ cont +"</p>";
 84         newMsg +='</div>';
 85         newMsg +='</div>';
 86         newMsg +='</div>';
 87         return newMsg;
 88     };
 89     consult.service = function(cont) {
 90         $('.consult-cont').append(consult.serverText(cont));
 91         consult.position();
 92     };
 93     consult.position=function(){
 94         var offset = $(".consult-Bd")[0].scrollHeight;
 95         $('.consult-Bd').scrollTop(offset);
 96     };
 97    consult.setDateInfo = function() {
 98        var dateInfo = new Date();
 99        console.log(dateInfo.toLocaleTimeString());
100        $('.consult-cont-date').text(dateInfo.toLocaleTimeString());
101    };
102 
103 /*
104  *下面是websocket操作相关的逻辑 by shihuc, 2017/3/9
105  */
106 wshandler.interval = 50;//unit is ms
107 wshandler.cycId = null;
108 wshandler.isFirst = true; //不是第一次的情况下,不显示欢迎语
109 wshandler.connect = function() {  
110    if (ws != null) {  
111        console.log("现已连接");  
112        return ;  
113    }  
114    url = wsurl;  
115    if ('WebSocket' in window) {  
116        ws = new WebSocket(url);  
117    } else if ('MozWebSocket' in window) {  
118        ws = new MozWebSocket(url);  
119    } else {  
120        console.log("您的浏览器不支持WebSocket。");  
121        return ;  
122    }  
123    ws.onopen = function() {  
124        //设置发信息送类型为:ArrayBuffer  
125        ws.binaryType = "arraybuffer";  
126        //发送一个字符串和一个二进制信息  
127        if (wshandler.isFirst) {
128           ws.send("OPEN");  
129        }
130    }  
131    ws.onmessage = function(e) {  
132        consult.service(e.data.toString());
133    }  
134    ws.onclose = function(e) {  
135        console.log("onclose: closed");
136        wshandler.disconnect(); 
137        wshandler.isFirst = false;
138    }  
139    ws.onerror = function(e) {  
140        console.log("onerror: error");  
141        wshandler.disconnect();
142        wshandler.isFirst = false;
143    }  
144 }  
145 
146 function checkOpenState(interval,cont) {
147    if (ws.readyState == ws.OPEN){
148        consult.fullSend(cont);
149        clearInterval(wshandler.cycId);
150    }else{
151        console.log("Wait for ws to be open again");
152        wshandler.cycId = setInterval("checkOpenState(" + interval + "," + cont + ")", interval);
153    }
154 }
155 
156 wshandler.reconnect = function(interval, cont) {
157    wshandler.connect();
158    var newCont = "\'" + cont + "\'";
159    checkOpenState(interval, newCont);
160 }
161 
162 //断开连接  
163 wshandler.disconnect = function() {  
164    if (ws != null) {  
165        ws.close();  
166        ws = null;          
167    }  
168 }
169 
170 //websocket逻辑区域
171 $(document).ready(function(){
172    wshandler.connect();   
173    consult.init();
174 });

 

接下来,看看基于netty的关键代码部分:

/**
 * @author "shihuc"
 * @date   2017年2月20日
 */
package com.tk.ics.gateway.server;

import org.apache.log4j.Logger;

import com.tk.ics.gateway.protocol.ws.WebSocketServerInitializer;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.util.SelfSignedCertificate;

/**
 * @author chengsh05
 *
 */
public final class WebSocketServer {
    
    private static Logger logger = Logger.getLogger(WebSocketServer.class);

    static final boolean SSL = System.getProperty("ssl") != null;
    static final int PORT = Integer.parseInt(System.getProperty("port", SSL? "9443" : "9080"));    

    public static void main(String[] args) throws Exception {
        // Configure SSL.
        final SslContext sslCtx;
        if (SSL) {
            SelfSignedCertificate ssc = new SelfSignedCertificate();
            sslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()).build();
        } else {
            sslCtx = null;
        }

        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
             .channel(NioServerSocketChannel.class)
             .handler(new LoggingHandler(LogLevel.INFO))
             .childHandler(new WebSocketServerInitializer(sslCtx));

            Channel ch = b.bind(PORT).sync().channel();
            logger.info("打开您的浏览器,并在地址栏输入 " + (SSL? "https" : "http") + "://127.0.0.1:" + PORT + '/');

            ch.closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

netty的childHandler相关的配置代码(WebSocketServerInitializer):

 1 /**
 2  * @author "shihuc"
 3  * @date   2017年3月13日
 4  */
 5 package com.tk.ics.gateway.protocol.ws;
 6 
 7 import com.tk.ics.gateway.handler.ws.WebSocketFrameHandler;
 8 
 9 import io.netty.channel.ChannelInitializer;
10 import io.netty.channel.ChannelPipeline;
11 import io.netty.channel.socket.SocketChannel;
12 import io.netty.handler.codec.http.HttpObjectAggregator;
13 import io.netty.handler.codec.http.HttpServerCodec;
14 import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
15 import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketServerCompressionHandler;
16 import io.netty.handler.ssl.SslContext;
17 import io.netty.handler.timeout.IdleStateHandler;
18 
19 /**
20  * @author chengsh05
21  *
22  */
23 public class WebSocketServerInitializer extends ChannelInitializer<SocketChannel> {
24 
25     private static final String WEBSOCKET_PATH = "/websocket";
26 
27     private final SslContext sslCtx;
28 
29     public WebSocketServerInitializer(SslContext sslCtx) {
30         this.sslCtx = sslCtx;
31     }
32 
33     @Override
34     public void initChannel(SocketChannel ch) throws Exception {
35         ChannelPipeline pipeline = ch.pipeline();
36         if (sslCtx != null) {
37             pipeline.addLast(sslCtx.newHandler(ch.alloc()));
38         }
39         //添加超时处理
40         pipeline.addLast(new IdleStateHandler(30, 0, 0));
41         pipeline.addLast(new HttpServerCodec());
42         pipeline.addLast(new HttpObjectAggregator(65536));
43         pipeline.addLast(new WebSocketServerCompressionHandler());
44         pipeline.addLast(new WebSocketServerProtocolHandler(WEBSOCKET_PATH, null, true));
45         pipeline.addLast(new WebSocketFrameHandler());
46     }
47 }

再接下来,重点看看WebSocketFrameHandler的源码:

 1 /**
 2  * @author "shihuc"
 3  * @date   2017年3月13日
 4  */
 5 package com.tk.ics.gateway.handler.ws;
 6 
 7 import java.util.Locale;
 8 
 9 import org.slf4j.Logger;
10 import org.slf4j.LoggerFactory;
11 
12 import com.tk.ics.gateway.channel.ServerChannelMgmt;
13 
14 import io.netty.channel.ChannelFuture;
15 import io.netty.channel.ChannelFutureListener;
16 import io.netty.channel.ChannelHandlerContext;
17 import io.netty.channel.SimpleChannelInboundHandler;
18 import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
19 import io.netty.handler.codec.http.websocketx.WebSocketFrame;
20 import io.netty.handler.timeout.IdleState;
21 import io.netty.handler.timeout.IdleStateEvent;
22 
23 
24 /**
25  * @author chengsh05
26  *
27  */
28 public class WebSocketFrameHandler extends SimpleChannelInboundHandler<WebSocketFrame> {    
29         
30     private static final Logger logger = LoggerFactory.getLogger(WebSocketFrameHandler.class);
31 
32     @Override
33     protected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame frame) throws Exception {
34         // ping and pong frames already handled
35 
36         if (frame instanceof TextWebSocketFrame) {
37             // Send the uppercase string back.
38             String request = ((TextWebSocketFrame) frame).text();
39             logger.info("{} received: {}", ctx.channel(), request);            
40             if(request.equalsIgnoreCase("close")){
41                 ctx.channel().writeAndFlush(new TextWebSocketFrame("you have closed this session".toUpperCase(Locale.US)));
42                 ctx.close();
43             }else{
44                 /*
45                  * 在这个地方添加客服所需的响应逻辑。当前的demo中,将接收到的消息,又原封不动的发送给了客户端
46                  */
47                 //ctx.channel().writeAndFlush(new TextWebSocketFrame(request.toUpperCase(Locale.US)));
48                 if(request.toString().equalsIgnoreCase("OPEN")){
49                     ctx.channel().writeAndFlush(new TextWebSocketFrame("iTker: 欢迎光临疯子机器人有限公司。有什么需要咨询的尽管说!小疯第一时间来给您解答~"));
50                 } else {
51                     ctx.channel().writeAndFlush(new TextWebSocketFrame("iTker: \r\n" + request.toString()));
52                 }
53             }
54         } else {
55             String message = "unsupported frame type: " + frame.getClass().getName();
56             throw new UnsupportedOperationException(message);
57         }
58     }
59     
60     @Override
61     public void channelActive(ChannelHandlerContext ctx) throws Exception {
62         ctx.fireChannelActive();
63         String channelId = ctx.channel().id().asLongText();
64         logger.info("websocket channel active: " + channelId);
65         if(ServerChannelMgmt.getUserChannelMap().get(channelId) == null){
66             ServerChannelMgmt.getUserChannelMap().put(channelId, ctx.channel());
67         }
68     }
69     
70     @Override
71     public void channelInactive(ChannelHandlerContext ctx) throws Exception {
72         String channelId = ctx.channel().id().asLongText();
73         logger.info("websocket channel inactive: " + channelId);
74         if(ServerChannelMgmt.getUserChannelMap().get(channelId) != null){
75             ServerChannelMgmt.getUserChannelMap().remove(channelId);
76         }
77         
78         ctx.fireChannelInactive();
79     }
80     
81     @Override  
82     public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {  
83         if (IdleStateEvent.class.isAssignableFrom(evt.getClass())) {  
84             IdleStateEvent event = (IdleStateEvent) evt;  
85             if (event.state() == IdleState.READER_IDLE) {  
86                 ChannelFuture f = ctx.channel().writeAndFlush(new TextWebSocketFrame("iTker: 您长时间没有咨询了,再见! 若有需求,欢迎您随时与我们联系!"));
87                 f.addListener(ChannelFutureListener.CLOSE);
88             }    
89             else if (event.state() == IdleState.WRITER_IDLE)  
90                 System.out.println("write idle");  
91             else if (event.state() == IdleState.ALL_IDLE)  
92                 System.out.println("all idle");  
93         }  
94     }  
95 }

 

这个简单的demo,核心部分的代码,就都在这里了。最后,上一个运行过程中的前端效果截图分享给读者。

 

这个demo做的事情非常的简单,但是其价值不菲,逻辑实现过程,有需要的,或者有好的建议的,可以留言探讨。这个是本人负责的项目的一个minimini版本的一个小角落一览。

后续,本人将针对这个代码,分析介绍netty的源码重点脉络(基于4.1.7.final版本),敬请关注本人的博客!