Netty+WebSocket服务器完成Web聊天室(纯文字)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_37437983/article/details/86557730

因为毕设是基于netty的即时通讯,需要用到WebSocket服务器,所以先按照网上视频教程写一个简单的练练手。尽可能多的加注释

服务端:

 WebChatServer.java

package webChat;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.AsciiHeadersEncoder.NewlineType;

public class WebChatServer {

	private int port;
	public WebChatServer(int port) {
		this.port = port;
	}
	public void start() {
                //NIO selector(轮询)
		//Netty有线程模型有三种
		/*
		 * 1, Reactor单线程模型:客户端的连入和IO通讯都使用一个线程
		 * 		
		 * 只适用于访问量不大的场景
		 * 		//不加参数,那么线程池的默认线程数为系统CPU核心数*2
		 * 		EventLoopGroup boss = new NioEventLoopGroup(1);
		 * 		ServerBootstrap bootstrap = new ServerBootstrap();
		 * 		bootstrap.group(boss);
		 * 		ChannelFuture future = bootstrap.bind(8080).sync();
		 * 
		 * 2, Reactor多线程模型,
		 * boss线程池中一般只有一个线程,这个线程就是Accepter线程,只用于接收客户端的连接请求;
		 * worker线程池会有多个,一般是系统CPU核心数*2,负责已经建立连接的Channel之间的I/O通讯
		 * 
		 * 		EventLoopGroup boss = new NioEventLoopGroup();
		 * 		EventLoopGroup worker = new NioEventLoopGroup();
		 * 		ServerBootstrap bootstrap = new ServerBootstrap();
		 * 		bootstrap.group(boss,worker);
		 * 		ChannelFuture future = bootstrap.bind(8080).sync();
		 * 
		 * 3, 主从多线程模型
		 * boss线程池中有可以有多个Accepter线程,用于接收客户端的登录,握手,安全验证;
		 *  服务端的ServerSocketChannel只会与线程池中的一个线程绑定,
		 *  因此在调用NIO的Selector.select轮询客户端的Channel时实际上是在同一个线程中的,
		 *  其他线程没有用到,所以在普通的网络应用(只有一个网络服务)中,boss线程池设置多个线程是没有作用的。
		 *  只有当应用由多个网络服务构成,那么这多个网络服务可以共享同一个bossGroup
		 * 		EventLoopGroup boss = new NioEventLoopGroup(2);
		 * 		EventLoopGroup workerA = new NioEventLoopGroup();
		 * 		EventLoopGroup workerB = new NioEventLoopGroup();
		 * 		ServerBootstrap bootstrapA = new ServerBootstrap();
		 * 		ServerBootstrap bootstrapB = new ServerBootstrap();
		 * 		bootstrapA.group(boss,workerA); 
		 * 		bootstrapB.group(boss,workerB);
		 * 		ChannelFuture futureA = bootstrapA.bind(8080).sync();
		 * 		ChannelFuture futureB = bootstrapB.bind(8888).sync();
		 * 
		 */
		//一,创建两个线程组
		EventLoopGroup boss = new NioEventLoopGroup(); //负责客户端的链接
		//与已经链接的客户端通讯,进行SocketChannel的网络通讯,数据读写
		EventLoopGroup worker = new NioEventLoopGroup();
		
		try {
			//对服务器通道进行一系列的配置
			ServerBootstrap bootstrap = new ServerBootstrap();
			bootstrap.group(boss,worker)  //关联两个线程
			//channel方法指定用于服务端监听socket通道的类,NioServerSocketChannel中会有一个ServerSocketChannel类
					 .channel(NioServerSocketChannel.class)
					 .childHandler(new WebChatServerInitializer())//设置服务端的业务处理流水线
					 .option(ChannelOption.SO_BACKLOG, 128) //设置TCP缓冲区
					 .childOption(ChannelOption.SO_KEEPALIVE, true);//保持链接
			
			ChannelFuture future = bootstrap.bind(port).sync();
			System.out.println("[通知]:服务器已经启动");
			future.channel().closeFuture().sync();
			System.out.println("[通知]:服务器已经关闭");
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}finally {
			boss.shutdownGracefully();
			worker.shutdownGracefully();
		}
	}
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		new WebChatServer(8080).start();
	}

}

WebChatServerInitializer.java

package webChat;

import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.AsciiHeadersEncoder.NewlineType;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.stream.ChunkedWriteHandler;

public class WebChatServerInitializer extends ChannelInitializer<SocketChannel> {

	@Override
	protected void initChannel(SocketChannel socketChannel) throws Exception {
		// TODO Auto-generated method stub
		ChannelPipeline pipeline = socketChannel.pipeline();
		
		/*
		 * Websocket的通信链接的建立
		 * 1,建立WebSocket连接时,客户端浏览器首先要向服务端发送一个握手的请求,这个请求是Http协议的请求
		 * 	 请求头中有一个头信息是"Upgrade:WebSocket",向服务端表明提升协议升级,使用WebSocket协议
		 * 2,服务端接收到这个请求会生成应答信息,返回给客户端,完成握手,返回的应答信息还是http的response响应
		 * 3,客户端收到响应,则WebSocket的链接就建立完毕,后续的通讯就不再是Http协议的而是Websocket协议的
		 * */
		/*
		 * 客户端往服务端发送数据,依次经过关卡 1,2,5
		 * 服务端往客户端发送数据,依次经过关卡 5,4,3(反过来)
		 * 
		 * pipeline.addLast("1",new InBoundHandlerA())
		 * 	       .addLast("2",new InBoundHandlerB())
		 * 	       .addLast("3",new OutBoundHandlerC())
		 * 	       .addLast("4",new OutBoundHandlerD())
		 * 	       .addLast("5",InBoundOutBoundHandler())
		 * 
		 * */
		//pipeline相当于流水线,addLast依次向流水线上添加处理关卡(队列)
		pipeline.addLast(new HttpServerCodec())//将请求和应答消息编码解码为http协议的消息
				.addLast(new HttpObjectAggregator(64*1024))//
				.addLast(new ChunkedWriteHandler())//向客户端发送Html5的页面文件
				//区分http请求,读取html页面,并写回客户端浏览器
				.addLast(new HttpRequestHandler("/chat"))
				.addLast(new WebSocketServerProtocolHandler("/chat"))
				.addLast(new TextWebSocketFrameHandler());
		
	}

}

HttpRequestHandler.java

  注意:记得换主页地址,我这里是绝对路径

package webChat;


import java.io.File;
import java.io.RandomAccessFile;

import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.DefaultHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.stream.ChunkedFile;
import io.netty.handler.stream.ChunkedNioFile;

//用来区分用户的请求是html聊天页面,还是发送WebSocket请求
public class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest>{

	private final String chatUri; //WebSocket的请求地址(聊天的地址)
	private static File indexfile;
	static {
	//	URL location = HttpRequestHandler.class.getProtectionDomain().getCodeSource().getLocation();
		try {
			String path = "/home/ffy/API/index.html";
			path = !path.contains("file:")?path:path.substring(5);
			indexfile = new File(path);
			System.out.println("主页路径为:"+path);
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	public HttpRequestHandler(String chatUri) {
		// TODO Auto-generated constructor stub
		this.chatUri = chatUri; 
	}
	@Override
	protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
		if(chatUri.equalsIgnoreCase(request.getUri())) {
			//用户处理的是WebSocket的地址,那么就不做处理,将请求转到管道的下一个处理环节
			ctx.fireChannelRead(request.retain());
		}else {
			//如果不相等,那么就读取聊天界面,并将页面的内容写回给浏览器
			if(HttpHeaders.is100ContinueExpected(request)) {
				//100继续,200成功
				FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,
						HttpResponseStatus.CONTINUE);
				ctx.writeAndFlush(response);
				System.out.println("继续"); 
			}
			//读取默认的WebSocketChatClient.html页面
			RandomAccessFile file = new RandomAccessFile(indexfile, "r");
			//写一个HttpResponse,并设置头部 
			HttpResponse response = new DefaultHttpResponse(request.getProtocolVersion(),
					HttpResponseStatus.OK); 
			response.headers().set(HttpHeaders.Names.CONTENT_TYPE,"text/html; 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()));
			ChannelFuture future = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
			if(!keepAlive) {
				future.addListener(ChannelFutureListener.CLOSE);
			}
			file.close(); 
		}
	}
}

TextWebSocketFrameHandler.java

package webChat;

import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.util.concurrent.GlobalEventExecutor;

public class TextWebSocketFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {

	private static ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
	
	//当有客户端链接时执行
		@Override
		public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
			// TODO Auto-generated method stub
			Channel inComing  = ctx.channel(); //获得客户端通道
			//遍历通道队列,发消息
			for(Channel ch:channels) {
				if(ch!=inComing) {
					ch.writeAndFlush(new TextWebSocketFrame("欢迎"+inComing.remoteAddress()+"进入聊天室"+"\n"));
				}
			}
			channels.add(inComing);//将新加入的添加如队列
		}
		//当客户端断开链接时执行
		@Override
		public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
			// TODO Auto-generated method stub
			Channel outPuting  = ctx.channel(); //获得客户端通道
			//遍历通道队列,发消息
			for(Channel ch:channels) {
				if(ch!=outPuting) {
					ch.writeAndFlush(new TextWebSocketFrame(outPuting.remoteAddress()+"退出聊天室"+"\n"));
				}
			}
			channels.remove(outPuting);//将离开的客户端删除出队列
		}

	@Override
	protected void channelRead0(ChannelHandlerContext channelHandlerContext, TextWebSocketFrame msg) throws Exception {
		// TODO Auto-generated method stub
		//通过上下文对象可以知道谁发过来的数据
		Channel client = channelHandlerContext.channel();
		for(Channel ch:channels) {
			if(ch!=client) {
				ch.writeAndFlush(new TextWebSocketFrame("用户 "+client.remoteAddress()+" 发来消息:"+msg.text()+"\n"));
			}else {
				ch.writeAndFlush(new TextWebSocketFrame("我说:"+msg.text()+"\n"));
			}
		}		
	}

}

Web前端

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>多人聊天室</title>
</head>
<script>
	var socket;
	if(!window.WebSocket) {
		alert("不支持");
    }
	if(window.WebSocket){
		socket = new WebSocket("ws://localhost:8080/chat");
		socket.onmessage = function(event){
			var textArea = document.getElementById("msgText");
			textArea.value = textArea.value+"\n"+event.data;
		}
		socket.onopen = function(event){
			var textArea = document.getElementById("msgText");
			textArea.value = "已链接服务器";
		}
		socket.onclose = function(event){
			var textArea = document.getElementById("msgText");
			textArea.value = textArea.value+"\n"+"退出聊天室";
		}
		socket.onerror = function(event){
			alert("出错");
		}
	}
	function send(msg){
		if(!window.WebSocket) {
			alert("send");
			return false;
		}
		if(socket.readyState == WebSocket.OPEN){
			socket.send(msg);
			var textArea = document.getElementById("msgText");
		}else{
			alert("链接没有打开")
		}
	}
</script>
<body>
	<form onsubmit = "return false" action="">
		<h1>WebSocket多人聊天室</h1>
		<textarea id = "msgText" rows="20" cols="50"></textarea><br>
		<input type = "text" name = "msg" style = "width:300px"/>
		<input type = "button" value = "发送" onclick = "send(this.form.msg.value)">
		<input type = "button" value = "清空" onclick = "javascript:document.getElementById('msgText').value=''"/>
	</form>
</body>
</html>

运行结果

注意:最好使用火狐,谷歌进行测试,别的浏览器可能因为版本过低的原因不支持WebSocket协议

 

 

猜你喜欢

转载自blog.csdn.net/qq_37437983/article/details/86557730
今日推荐