因为毕设是基于netty的即时通讯,需要用到WebSocket服务器,所以先按照网上视频教程写一个简单的练练手。尽可能多的加注释
源码下载:https://download.csdn.net/download/qq_37437983/10929191
服务端:
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();
}
}
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 {
@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());
}
}
注意:记得换主页地址,我这里是绝对路径
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{
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();
}
}
}
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 {
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"));
}
}
}
}
多人聊天室
注意:最好使用火狐,谷歌进行测试,别的浏览器可能因为版本过低的原因不支持WebSocket协议