Netty+Websocket 实现一个简易聊天室

后台代码

/**
 * 服务端
 */
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">动脑学院&copy;版权所有</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();
		}
	};
});

猜你喜欢

转载自my.oschina.net/u/3728166/blog/2988192