后台代码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(); } }; });