因为毕设是基于netty的即时通讯,需要用到WebSocket服务器,所以先按照网上视频教程写一个简单的练练手。尽可能多的加注释
源码下载:https://download.csdn.net/download/qq_37437983/10929191
服务端:
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 blocke.printStackTrace();}finally {boss.shutdownGracefully();worker.shutdownGracefully();}}public static void main(String[] args) {// TODO Auto-generated method stubnew 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> {@Overrideprotected void initChannel(SocketChannel socketChannel) throws Exception {// TODO Auto-generated method stubChannelPipeline 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 blocke.printStackTrace();}}public HttpRequestHandler(String chatUri) {// TODO Auto-generated constructor stubthis.chatUri = chatUri; }@Overrideprotected 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);//当有客户端链接时执行@Overridepublic void handlerAdded(ChannelHandlerContext ctx) throws Exception {// TODO Auto-generated method stubChannel inComing = ctx.channel(); //获得客户端通道//遍历通道队列,发消息for(Channel ch:channels) {if(ch!=inComing) {ch.writeAndFlush(new TextWebSocketFrame("欢迎"+inComing.remoteAddress()+"进入聊天室"+"\n"));}}channels.add(inComing);//将新加入的添加如队列}//当客户端断开链接时执行@Overridepublic void handlerRemoved(ChannelHandlerContext ctx) throws Exception {// TODO Auto-generated method stubChannel outPuting = ctx.channel(); //获得客户端通道//遍历通道队列,发消息for(Channel ch:channels) {if(ch!=outPuting) {ch.writeAndFlush(new TextWebSocketFrame(outPuting.remoteAddress()+"退出聊天室"+"\n"));}}channels.remove(outPuting);//将离开的客户端删除出队列}@Overrideprotected 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协议