這里,僅僅是一個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版本),敬請關注本人的博客!