后台代码
/**
* 服务端
*/
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)\\]");
}
}
前端部分代码
html
<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();
}
};
});