後臺代碼javascript
/** * 服務端 */ public class ChatServer { public static void main(String[] args) throws Exception { int port=8080; //服務端默認端口 new ChatServer().bind(port); } public void bind(int port) throws Exception{ //1用於服務端接受客戶端的鏈接 EventLoopGroup acceptorGroup = new NioEventLoopGroup(); //2用於進行SocketChannel的網絡讀寫 EventLoopGroup workerGroup = new NioEventLoopGroup(); try { //Netty用於啓動NIO服務器的輔助啓動類 ServerBootstrap sb = new ServerBootstrap(); //將兩個NIO線程組傳入輔助啓動類中 sb.group(acceptorGroup, workerGroup) //設置建立的Channel爲NioServerSocketChannel類型 .channel(NioServerSocketChannel.class) //配置NioServerSocketChannel的TCP參數 .option(ChannelOption.SO_BACKLOG, 1024) //設置綁定IO事件的處理類 .childHandler(new ChannelInitializer<SocketChannel>() { //建立NIOSocketChannel成功後,在進行初始化時,將它的ChannelHandler設置到ChannelPipeline中,用於處理網絡IO事件 @Override protected void initChannel(SocketChannel arg0) throws Exception { ChannelPipeline pipeline = arg0.pipeline(); pipeline.addLast(new SFPDecoder()); pipeline.addLast(new SFPEncoder()); pipeline.addLast(new SFPHandler()); //支持Http協議 //Http請求處理的編解碼器 pipeline.addLast(new HttpServerCodec()); //用於將HTTP請求進行封裝爲FullHttpRequest對象 pipeline.addLast(new HttpObjectAggregator(1024*64)); //處理文件流 pipeline.addLast(new ChunkedWriteHandler()); //Http請求的具體處理對象 pipeline.addLast(new HttpHandler()); //支持WebSocket協議 pipeline.addLast(new WebSocketServerProtocolHandler("/im")); pipeline.addLast(new WebSocketHandler()); } }); //綁定端口,同步等待成功(sync():同步阻塞方法,等待bind操做完成才繼續) //ChannelFuture主要用於異步操做的通知回調 ChannelFuture cf = sb.bind(port).sync(); System.out.println("服務端啓動在8080端口。"); //等待服務端監聽端口關閉 cf.channel().closeFuture().sync(); } finally { //優雅退出,釋放線程池資源 acceptorGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } }
/** * HttpHandler */ public class HttpHandler extends SimpleChannelInboundHandler<FullHttpRequest> { @Override protected void messageReceived(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception { //處理客戶端的Http請求 String uri = request.getUri(); String source = uri.equals("/")?"chat.html":uri; //拿到資源文件 RandomAccessFile file; try { file = new RandomAccessFile(getResource(source), "r"); } catch (Exception e) { //繼續下一次請求 ctx.fireChannelRead(request.retain()); return ; } //將資源響應給客戶端 HttpResponse response = new DefaultHttpResponse(request.getProtocolVersion(), HttpResponseStatus.OK); //設置響應頭的ContentType String contentType = "text/html"; if(uri.endsWith(".js")){ contentType = "text/javascript"; }else if(uri.endsWith(".css")){ contentType = "text/css"; }else if(uri.toLowerCase().matches("(jpg|png|gif|ico)$")){ String type = uri.substring(uri.lastIndexOf(".")); contentType = "image/"+type; } response.headers().set(HttpHeaders.Names.CONTENT_TYPE, contentType+"; charset=utf-8"); boolean keepAlive = HttpHeaders.isKeepAlive(request); if(keepAlive){ //若是請求是一個長鏈接 response.headers().set(HttpHeaders.Names.CONTENT_LENGTH, file.length()); response.headers().set(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.KEEP_ALIVE); } //向客戶端響應消息頭 ctx.write(response); //向客戶端響應消息體 ctx.write(new ChunkedNioFile(file.getChannel())); //響應結束添加Http響應結束標記 ChannelFuture writeAndFlush = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT); if(!keepAlive){ writeAndFlush.addListener(ChannelFutureListener.CLOSE); } //收尾 file.close(); } private String getResource(String source) throws URISyntaxException { //class文件的地址 URL location = HttpHandler.class.getProtectionDomain().getCodeSource().getLocation(); String webroot = "templates"; String path = location.toURI()+webroot+"/"+source; path = path.replace("file:", ""); return path; } }
/** * WebSocketHandler */ public class WebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> { private MessageProcessor processor = new MessageProcessor(); @Override protected void messageReceived(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception { //服務端與客戶端的WebSocket交互 processor.messageHandler(ctx.channel(), msg.text()); } //客戶端鏈接斷開事件 @Override public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { processor.logout(ctx.channel()); } }
/** * WebScoket 消息處理類 */ public class MessageProcessor { //用於記錄/管理全部客戶端的Channel private static ChannelGroup users = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE); private MessageCodec codec = new MessageCodec(); //設置一些Channel的屬性 private AttributeKey<String> nickName = AttributeKey.valueOf("nickName"); public void messageHandler(Channel client, String message){ if(message == null || "".equals(message.trim())){ return ; } System.out.println("客戶端發送的消息:"+message); MessageObject msgObj = codec.decoder(message); if(msgObj.getCmd().equals(MessageStatus.LOGIN)){ //爲Channel綁定暱稱屬性 client.attr(nickName).set(msgObj.getNickName()); users.add(client); //將用戶的channel添加到ChannelGroup中 //將用戶登錄的消息發給全部的其餘用戶 for (Channel channel : users) { //封裝一個System的消息對象 if(channel == client){ msgObj = new MessageObject(MessageStatus.SYSTEM, System.currentTimeMillis(), msgObj.getNickName(), "已經與服務器創建鏈接", users.size()); }else{ msgObj = new MessageObject(MessageStatus.SYSTEM, System.currentTimeMillis(), msgObj.getNickName(), msgObj.getNickName()+"加入了聊天室", users.size()); } //將消息發送給全部客戶端 channel.writeAndFlush(new TextWebSocketFrame(codec.encoder(msgObj))); } } else if(msgObj.getCmd().equals(MessageStatus.CHAT)) { for (Channel channel : users) { if(channel == client){ //發送給本身 msgObj.setNickName("SELF"); }else{ msgObj.setNickName(client.attr(nickName).get()); } //從新編碼 String content = codec.encoder(msgObj); channel.writeAndFlush(new TextWebSocketFrame(content)); } } } public void messageHandler(Channel client, MessageObject message){ messageHandler(client, codec.encoder(message)); } public void logout(Channel client){ //封裝一個登出指令發送給客戶端 users.remove(client); //得到客戶的綁定的暱稱 String userName = client.attr(nickName).get(); if(userName!=null && !userName.equals("")){ MessageObject messageObject = new MessageObject(MessageStatus.SYSTEM, System.currentTimeMillis(), null, userName+"退出了聊天室", users.size()); String content = codec.encoder(messageObject); for (Channel channel : users) { channel.writeAndFlush(new TextWebSocketFrame(content)); } } } }
/** * 消息編解碼 */ public class MessageCodec { //將字符串指令解碼爲MessageObject對象 public MessageObject decoder(String message){ if(message ==null || "".equals(message.trim())){return null;} Pattern pattern = Pattern.compile("^\\[(.*)\\](\\s-\\s(.*))?"); Matcher matcher = pattern.matcher(message); String headers = ""; //消息頭 String content = ""; //消息體 if(matcher.find()){ headers = matcher.group(1); content = matcher.group(3); } String[] split = headers.split("\\]\\["); String cmd = split[0]; long time = Long.parseLong(split[1]); String nickName = split[2]; //將客戶發送的消息封裝爲MessageObject對象 if(cmd.equals(MessageStatus.LOGIN) || cmd.equals(MessageStatus.LOGOUT)){ return new MessageObject(cmd, time, nickName); }else if(cmd.equals(MessageStatus.CHAT) || cmd.equals(MessageStatus.SYSTEM)){ return new MessageObject(cmd, time, nickName, content); } return null; } //將MessageObject對象編碼爲字符串指令 public String encoder(MessageObject msg){ if(msg == null){return null;} String message = "["+msg.getCmd()+"]["+msg.getTime()+"]"; if(msg.getCmd().equals(MessageStatus.SYSTEM)){ message += "["+msg.getOnline()+"]"; }else if(msg.getCmd().equals(MessageStatus.CHAT) ||msg.getCmd().equals(MessageStatus.LOGIN) ||msg.getCmd().equals(MessageStatus.LOGOUT)){ message += "["+msg.getNickName()+"]"; } if(msg.getContent() != null && !msg.getContent().equals("")){ message += " - "+msg.getContent(); } return message; } }
/** * 消息實體類 */ @Message public class MessageObject { private String cmd; //指令類型 例如:LOGIN\LOGOUT\CHAT\SYSTEM private long time; //消息發送的時間戳 private String nickName; //消息發送人 private String content; //消息體 private int online;//在線人數 /** * */ public MessageObject() { super(); } /** * @param cmd * @param time * @param nickName */ public MessageObject(String cmd, long time, String nickName) { super(); this.cmd = cmd; this.time = time; this.nickName = nickName; } /** * @param cmd * @param time * @param nickName * @param content */ public MessageObject(String cmd, long time, String nickName, String content) { super(); this.cmd = cmd; this.time = time; this.nickName = nickName; this.content = content; } /** * @param cmd * @param time * @param nickName * @param content * @param online */ public MessageObject(String cmd, long time, String nickName, String content, int online) { super(); this.cmd = cmd; this.time = time; this.nickName = nickName; this.content = content; this.online = online; } public String getCmd() { return cmd; } public void setCmd(String cmd) { this.cmd = cmd; } public long getTime() { return time; } public void setTime(long time) { this.time = time; } public String getNickName() { return nickName; } public void setNickName(String nickName) { this.nickName = nickName; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } public int getOnline() { return online; } public void setOnline(int online) { this.online = online; } }
/** * 常量 */ public class MessageStatus { public static final String LOGIN="LOGIN"; public static final String LOGOUT="LOGOUT"; public static final String CHAT="CHAT"; public static final String SYSTEM="SYSTEM"; public static boolean isSFP(String msg){ return msg.matches("^\\[(SYSTEM|LOGIN|LOGOUT|CHAT)\\]"); } }
前端部分代碼css
htmlhtml
<html> <head> <meta charset="utf-8"> <link rel="stylesheet" type="text/css" href="/css/style.css" /> <script type="text/javascript" src="/js/lib/jquery.min.js"></script> <script type="text/javascript" src="/js/chat.js"></script> </head> <body> <div id="loginbox"> <div style="width:300px;margin:200px auto;"> 歡迎來到動腦學院WebSocket聊天室 <br/> <br/> <input type="text" style="width:180px;" placeholder="進入前,請先輸入暱稱" id="nickname" name="nickname" /> <input type="button" style="width:50px;" value="進入" onclick="CHAT.login();" /> <div id="error-msg" style="color:red;"></div> </div> </div> <div id="chatbox" style="display: none;"> <div style="background:#3d3d3d;height: 28px; width: 100%;font-size:12px;position: fixed;top: 0px;z-index: 999;"> <div style="line-height: 28px;color:#fff;"> <span style="text-align:left;margin-left:10px;">動腦學院聊天室</span> <span style="float:right; margin-right:10px;"> <span>當前在線<span id="onlinecount">0</span>人</span> | <span id="shownikcname">匿名</span> | <a href="javascript:;" onclick="CHAT.logout()" style="color:#fff;">退出</a> </span> </div> </div> <div id="doc"> <div id="chat"> <div id="message" class="message"> <div id="onlinemsg" style="background:#EFEFF4; font-size:12px; margin-top:40px; margin-left:10px; color:#666;"> </div> </div> <form onsubmit="return false;"> <div class="tool-box"> <div class="face-box" id="face-box"></div> <span class="face" onclick="CHAT.openFace()" title="選擇表情"></span> </div> <div class="input-box"> <div class="input" contenteditable="true" id="send-message"></div> <div class="action"> <input class="button" type="button" id="mjr_send" onclick="CHAT.chat()" value="發送"/> </div> </div> <div class="copyright">動腦學院©版權全部</div> </form> </div> </div> </div> </body> </html>
JS前端
var do4ServerMessage = function(msg){ //客戶端解析消息 var _reg = /^\[(.*)\](\s\-\s(.*))?/g; var group = ''; var header = "",content="",cmd="",time=0,sender=""; while(group = _reg.exec(msg)){ header = group[1]; content = group[3]; } var headers = header.split("]["); cmd = headers[0]; time = headers[1]; sender = headers[2];//消息發送人 if(cmd == "SYSTEM"){ var online = headers[2]; $("#onlinecount").html(online); //同時在聊天窗口顯示系統消息 showServerMessage(content); }else if(cmd == "CHAT"){ //聊天窗口添加系統時間 var date = new Date(parseInt(time)); showServerMessage('<span class="time-label">' + date.format("hh:mm:ss") + '</span>'); //將聊天消息添加到聊天面板中 var contentDiv = '<div>' + content + '</div>'; var usernameDiv = '<span>' + sender + '</span>'; var section = document.createElement('section'); //判斷消息發送人是否本身 if(sender == "SELF"){ section.className = 'user'; section.innerHTML = usernameDiv + contentDiv; }else{ section.className = 'service'; section.innerHTML = usernameDiv + contentDiv; } $("#onlinemsg").append(section); } scorllToBottom(); }; var scorllToBottom = function(){ window.scrollTo(0,$("#onlinemsg")[0].scrollHeight); } var showServerMessage = function(c){ var html = ""; html += '<div class="msg-system">'; html += c; html += '</div>'; var section = document.createElement('section'); section.className = 'system J-mjrlinkWrap J-cutMsg'; section.innerHTML = html; $("#onlinemsg").append(section); }; //擴展一個date對象的format方法 Date.prototype.format = function(format){ var o = { "M+" : this.getMonth()+1, //月 "d+" : this.getDate(), //日 "h+" : this.getHours(), //時 "m+" : this.getMinutes(), //分 "s+" : this.getSeconds(), //秒 "q+" : Math.floor((this.getMonth()+3)/3), //刻 "S" : this.getMilliseconds() //毫秒 } if(/(y+)/.test(format)) { format = format.replace(RegExp.$1, (this.getFullYear()+"").substr(4 - RegExp.$1.length)); } for(var k in o) { if(new RegExp("("+ k +")").test(format)) { format = format.replace(RegExp.$1, RegExp.$1.length==1 ? o[k] : ("00"+ o[k]).substr((""+ o[k]).length)); } } return format; }; $(document).ready(function(){ window.CHAT = { nickName:"匿名用戶", socket:null, login:function(){ $("#error-msg").empty(); //用戶註冊的名字 var nickname = $("#nickname").val(); CHAT.nickName = nickname; var _reg = /^\S{1,10}/; if(!_reg.test($.trim(nickname))){ $("#error-msg").html("請輸入1-10位正確的暱稱"); return false; } $("#shownikcname").html(nickname); $("#loginbox").hide(); $("#chatbox").show(); CHAT.init(); }, init:function(){ //判斷瀏覽器是否支持WebSocket協議 if(!window.WebSocket){ window.WebSocket = window.MozWebSocket; } if(window.WebSocket){ CHAT.socket = new WebSocket("ws://localhost:8080/im"); CHAT.socket.onopen = function(e){ console.log("客戶端鏈接成功."); CHAT.socket.send("[LOGIN]["+new Date().getTime()+"]["+CHAT.nickName+"]"); }; CHAT.socket.onclose = function(e){ console.log("客戶端關閉鏈接."); }; CHAT.socket.onmessage = function(e){ console.log("客戶端接收服務端信息:"+e.data); do4ServerMessage(e.data); } }else{ alert("您的瀏覽器不支持WebSocket協議!"); } }, logout:function(){ location.reload();//刷新 }, chat:function(){ var input = $("#send-message"); if($.trim(input.html())==""){ return; } //離線判斷 if(CHAT.socket.readyState == WebSocket.OPEN){ var msg = input.html().replace(/\n/ig,"<br/>"); CHAT.socket.send("[CHAT]["+new Date().getTime()+"]["+CHAT.nickName+"] - "+msg); input.empty(); input.focus(); }else{ showServerMessage("您以處於離線狀態,沒法發送消息。") } }, //選擇表情 openFace:function(){ var box = $("#face-box"); //避免重複打開表情選擇框 if(box.hasClass("open")){ box.hide(); box.removeClass("open"); return; } box.addClass("open"); box.show(); if(box.html() != ""){ return; } var faceIcon = ""; for(var i = 1; i <= 130; i ++){ var path = '/images/face/' + i + ".gif"; faceIcon += '<span class="face-item" onclick="CHAT.selectFace(\'' + path + '\')">' faceIcon += '<img src="' + path + '"/>'; faceIcon += '</span>'; } box.html(faceIcon); }, //選擇一張圖片 selectFace:function(path){ var faceBox = $("#face-box"); faceBox.hide(); faceBox.removeClass("open"); var img = '<img src="' + path + '"/>'; $("#send-message").append(img); $("#send-message").focus(); } }; });