本文为转载后,加上了部分修改而成。具体入下:
Client-Server模式
使用netty写客户端与服务器,完成聊天。为了优化用户体验,这里加了一个bean类,ClientBean,用于描述客户端。
Server端
netty本身就有很好的分层设计,框架和业务逻辑分界明显,对于不同的业务,我们只需要实现不同的handler即可。
SimpleChatServerHandler.java
/**
* @Date 2018年5月16日
*
* @author 郭 璞
*
*/
package simplechat;
import java.util.HashMap;
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.util.concurrent.GlobalEventExecutor;
/**
* @author 郭 璞
*
*/
public class SimpleChatServerHandler extends SimpleChannelInboundHandler<String> {
public static ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
public static HashMap<Channel, ClientBean> channelmap = new HashMap<Channel, ClientBean>();
/* (non-Javadoc)
* @see io.netty.channel.ChannelHandlerAdapter#handlerAdded(io.netty.channel.ChannelHandlerContext)
*/
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
Channel incoming = ctx.channel();
for(Channel channel: channels) {
channel.writeAndFlush("[SERVER] - " + incoming.remoteAddress() + " 加入啦!\n");
}
channels.add(ctx.channel());
}
/* (non-Javadoc)
* @see io.netty.channel.ChannelHandlerAdapter#handlerRemoved(io.netty.channel.ChannelHandlerContext)
*/
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
Channel incoming = ctx.channel();
for(Channel channel: channels) {
channel.writeAndFlush("[SERVER] - " + incoming.remoteAddress() + "离开了\n");
}
channels.remove(ctx.channel());
}
/* (non-Javadoc)
* @see io.netty.channel.SimpleChannelInboundHandler#channelRead0(io.netty.channel.ChannelHandlerContext, java.lang.Object)
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
Channel incoming = ctx.channel();
for(Channel channel: channels) {
if(channel == incoming) {
channel.writeAndFlush("[YOU] " + msg + "\n");
}else{
// 这里其实放到外层感觉会更好一点,逻辑上
ClientBean clientBean = this.channelmap.get(ctx.channel());
if(clientBean.isIsfirstconnect() == true) {
// 第一次輸入,將msg作為用戶暱稱即可
clientBean.setIsfirstconnect(false);
clientBean.setNickname(msg);
clientBean.setRemoteaddress(incoming.remoteAddress().toString());
}else{
String nickname = this.channelmap.get(ctx.channel()).getNickname();
String remoteaddress = this.channelmap.get(ctx.channel()).getRemoteaddress();
System.out.println("DEBUG:" + nickname);
channel.writeAndFlush("[" + nickname+"@"+ remoteaddress + "]" + msg + "\n");
}
}
}
}
/* (non-Javadoc)
* @see io.netty.channel.ChannelInboundHandlerAdapter#channelActive(io.netty.channel.ChannelHandlerContext)
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
Channel incoming = ctx.channel();
System.out.println("SimpleChatClient:" + incoming.remoteAddress() + "在线!\n");
}
/* (non-Javadoc)
* @see io.netty.channel.ChannelInboundHandlerAdapter#channelInactive(io.netty.channel.ChannelHandlerContext)
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
Channel incoming = ctx.channel();
System.out.println("SimpleChatClient: " + incoming.remoteAddress() + "离线!\n");
}
/* (non-Javadoc)
* @see io.netty.channel.ChannelInboundHandlerAdapter#exceptionCaught(io.netty.channel.ChannelHandlerContext, java.lang.Throwable)
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
Channel incoming = ctx.channel();
System.out.println("SimpleChatClient: " + incoming.remoteAddress() + "异常连接!\n");
cause.printStackTrace();
ctx.close();
}
}
SimpleChatServer.java
/**
* @Date 2018年5月16日
*
* @author 郭 璞
*
*/
package simplechat;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.Delimiters;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
/**
* @author 郭 璞
*
*/
public class SimpleChatServer {
public void run() {
int port = 8888;
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("framer", new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter()));
pipeline.addLast("decoder", new StringDecoder());
pipeline.addLast("encoder", new StringEncoder());
pipeline.addLast("handler", new SimpleChatServerHandler());
// 客户端第一次连接会触发此操作,然后是active,再是read
SimpleChatServerHandler.channelmap.put(ch, new ClientBean(true, ch.remoteAddress().toString(), ch.remoteAddress().toString()));
System.out.println("SimpleChatClient: " + ch.remoteAddress() + "连接上了!");
}
})
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true);
System.out.println("SimpleChatServer is running...");
ChannelFuture f = bootstrap.bind(port).sync();
f.channel().closeFuture().sync();
} catch (Exception e) {
throw new RuntimeException(" :\n" + e);
}finally{
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
System.out.println("SimpleChatServer shutdown gracefully.\n");
}
}
public static void main(String[] args) {
new SimpleChatServer().run();
}
}
Client端
ClientBean
/**
* @Date 2018年5月16日
*
* @author 郭 璞
*
*/
package simplechat;
/**
* @author 郭 璞
*
*/
public class ClientBean {
private boolean isfirstconnect=true;
private String nickname;
private String remoteaddress;
public boolean isIsfirstconnect() {
return isfirstconnect;
}
public void setIsfirstconnect(boolean isfirstconnect) {
this.isfirstconnect = isfirstconnect;
}
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
public String getRemoteaddress() {
return remoteaddress;
}
public void setRemoteaddress(String remoteaddress) {
this.remoteaddress = remoteaddress;
}
@Override
public String toString() {
return "ClientBean [isfirstconnect=" + isfirstconnect + ", nickname=" + nickname + ", remoteaddress="
+ remoteaddress + "]";
}
public ClientBean(boolean isfirstconnect, String nickname, String remoteaddress) {
super();
this.isfirstconnect = isfirstconnect;
this.nickname = nickname;
this.remoteaddress = remoteaddress;
}
}
SimpleChatClientHandler.java
/**
* @Date 2018年5月16日
*
* @author 郭 璞
*
*/
package simplechat;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
/**
* @author 郭 璞
*
*/
public class SimpleChatClientHandler extends SimpleChannelInboundHandler<String> {
/* (non-Javadoc)
* @see io.netty.channel.SimpleChannelInboundHandler#channelRead0(io.netty.channel.ChannelHandlerContext, java.lang.Object)
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
System.out.println(msg);
}
}
SimpleChatClient.java
/**
* @Date 2018年5月16日
*
* @author 郭 璞
*
*/
package simplechat;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.Delimiters;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
/**
* @author 郭 璞
*
*/
public class SimpleChatClient {
public void run(String host, int port) {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("framer", new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter()));
pipeline.addLast("decoder", new StringDecoder());
pipeline.addLast("encoder", new StringEncoder());
pipeline.addLast("handler", new SimpleChatClientHandler());
}
});
Channel channel = b.connect(host, port).sync().channel();
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
// \r\n很重要,否則可能會出現假死
System.out.println("恭喜您链接服务器成功\n请输入您的昵称:");channel.writeAndFlush(in.readLine()+"\r\n");
while(true){
channel.writeAndFlush(in.readLine() + "\r\n");
}
} catch (Exception e) {
throw new RuntimeException(" :\n" + e);
}finally {
group.shutdownGracefully();
System.out.println("客户端平滑关闭了\n");
}
}
public static void main(String[] args) {
new SimpleChatClient().run("localhost", 8888);
}
}
测试效果
首先要将SimpleChatServer跑起来,然后再开启多个SimpleChatClient实例,首次运行会提示输入昵称,后面输入就可以很方便的实现聊天了。
SimpleChatServer is running...
SimpleChatClient: /127.0.0.1:58424连接上了!
SimpleChatClient:/127.0.0.1:58424在线!
SimpleChatClient: /127.0.0.1:58448连接上了!
SimpleChatClient:/127.0.0.1:58448在线!
DEBUG:李四
DEBUG:张三
SimpleChatClient: /127.0.0.1:58448异常连接!(此处省略cause.printStackTrace()日志)
SimpleChatClient: /127.0.0.1:58448离线!
-----------------------------------------------
恭喜您链接服务器成功
请输入您的昵称:
张三
[YOU] 张三
[李四@/127.0.0.1:58424]哈喽,我是李四
嗨,李四,我是张三
[YOU] 嗨,李四,我是张三
------------------------------------------
恭喜您链接服务器成功
请输入您的昵称:
[SERVER] - /127.0.0.1:58448 加入啦!
李四
[YOU] 李四
哈喽,我是李四
[YOU] 哈喽,我是李四
[张三@/127.0.0.1:58448]嗨,李四,我是张三
[SERVER] - /127.0.0.1:58448离开了
Browser-Server模式
使用浏览器和服务器的模式进行聊天,一般会用websocket协议,netty对此支持的也不赖。
Server端
正如参考文献中写的,只有在服务器收到客户端请求升级protocol的时候,才会升级websocket协议,然后进行通信。其他情况,依旧是普通的http请求,所以server端要完成两件事情,一个是普通的http协议,一个是websocket协议。
HTTP协议,HttpRequestHandler.java
/**
* @Date 2018年5月16日
*
* @author 郭 璞
*
*/
package websocket;
import java.io.File;
import java.io.RandomAccessFile;
import java.net.URL;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.DefaultFileRegion;
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.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.ssl.SslHandler;
import io.netty.handler.stream.ChunkedNioFile;
/**
* @author 郭 璞
*
*/
public class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
private String WS_URI;
private static File INDEX;
static {
URL location = HttpRequestHandler.class.getProtectionDomain().getCodeSource().getLocation();
try {
String path = location.toURI() + "WebsocketChatClient.html";
path = !path.contains("file:")?path:path.substring(5);
INDEX = new File(path);
} catch (Exception e) {
throw new RuntimeException("Unable to locate WebsocketChatClient.html :\n" + e);
}
}
public HttpRequestHandler(String wsUri) {
this.WS_URI = wsUri;
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
if(this.WS_URI.equalsIgnoreCase(request.getUri())) {
ctx.fireChannelRead(request.retain());
}else{
//
if(HttpHeaders.is100ContinueExpected(request)) {
send1000Continue(ctx);
}
RandomAccessFile file = new RandomAccessFile(this.INDEX, "r");
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.writeAndFlush(response);
if(ctx.pipeline().get(SslHandler.class) == null) {
ctx.write(new DefaultFileRegion(file.getChannel(), 0, file.length()));
}else{
ctx.write(new ChunkedNioFile(file.getChannel()));
}
ChannelFuture f = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
if(!keepAlive) {
f.addListener(ChannelFutureListener.CLOSE);
}
file.close();
}
}
/**
* @param ctx
*/
private void send1000Continue(ChannelHandlerContext ctx) {
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE);
}
/* (non-Javadoc)
* @see io.netty.channel.ChannelInboundHandlerAdapter#exceptionCaught(io.netty.channel.ChannelHandlerContext, java.lang.Throwable)
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
Channel incoming = ctx.channel();
System.out.println("Client: " + incoming.remoteAddress() + "異常:" + cause.getMessage());
ctx.close();
}
}
WebSocket协议,TextWebSocketFrameHandler.java
/**
* @Date 2018年5月16日
*
* @author 郭 璞
*
*/
package websocket;
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;
/**
* @author 郭 璞
*
*/
public class TextWebSocketFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
public static ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
Channel incoming = ctx.channel();
TextWebSocketFrame responsemsg = null;
for(Channel channel: channels) {
if(channel == incoming) {
responsemsg = new TextWebSocketFrame("["+incoming.remoteAddress()+"]" + msg.text());
}else{
responsemsg = new TextWebSocketFrame("["+incoming.remoteAddress()+"]" + msg.text());
}
channel.writeAndFlush(responsemsg);
}
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception { // (2)
Channel incoming = ctx.channel();
for (Channel channel : channels) {
channel.writeAndFlush(new TextWebSocketFrame("[SERVER] - " + incoming.remoteAddress() + " 加入"));
}
channels.add(ctx.channel());
System.out.println("Client:"+incoming.remoteAddress() +"加入");
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { // (3)
Channel incoming = ctx.channel();
for (Channel channel : channels) {
channel.writeAndFlush(new TextWebSocketFrame("[SERVER] - " + incoming.remoteAddress() + " 离开"));
}
System.out.println("Client:"+incoming.remoteAddress() +"离开");
channels.remove(ctx.channel());
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception { // (5)
Channel incoming = ctx.channel();
System.out.println("Client:"+incoming.remoteAddress()+"在线");
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception { // (6)
Channel incoming = ctx.channel();
System.out.println("Client:"+incoming.remoteAddress()+"掉线");
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
Channel incoming = ctx.channel();
System.out.println("Client:"+incoming.remoteAddress()+"异常: " + cause.getMessage());
// 当出现异常就关闭连接
cause.printStackTrace();
ctx.close();
}
}
WebSocketServer.java
/**
* @Date 2018年5月16日
*
* @author 郭 璞
*
*/
package websocket;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
/**
* @author 郭 璞
*
*/
public class WebSocketChatServer {
public void run() {
int port = 9999;
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(new HttpObjectAggregator(64*1024));
pipeline.addLast(new ChunkedWriteHandler());
pipeline.addLast(new HttpRequestHandler("/ws"));
pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
pipeline.addLast(new TextWebSocketFrameHandler());
}
})
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true);
System.out.println("WebSocketChatServer is running...");
//
ChannelFuture f = b.bind(port).sync();
f.channel().closeFuture().sync();
} catch (Exception e) {
throw new RuntimeException(" :\n" + e);
}finally{
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
System.out.println("WebSocketChatServer finally closed!");
}
}
public static void main(String[] args) {
new WebSocketChatServer().run();
}
}
Browser端
根据刚才HTTPRequestHandler.java设置的uri以及对应的HTML路径,我们需要把WebsocketChatClient.html文件放到项目的bin目录下,否则不能被正常找到。
WebsocketChatClient.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>WebSocket Chat</title>
</head>
<body>
<script type="text/javascript">
var socket;
//检查浏览器是否支持WebSocket
if(window.WebSocket){
console.log('This browser supports WebSocket');
}else{
console.log('This browser does not supports WebSocket');
}
if (!window.WebSocket) {
window.WebSocket = window.MozWebSocket;
}
if (window.WebSocket) {
socket = new WebSocket("ws://localhost:9999/ws");
socket.onmessage = function(event) {
var ta = document.getElementById('responseText');
console.log("来自客户端的消息:" + event.data);
ta.value = ta.value + '\n' + event.data
};
socket.onopen = function(event) {
var ta = document.getElementById('responseText');
console.log(event);
ta.value = "连接开启!";
};
socket.onclose = function(event) {
var ta = document.getElementById('responseText');
console.log(event);
ta.value = ta.value + "连接被关闭";
};
} else {
alert("你的浏览器不支持 WebSocket!");
}
function send() {
if (!window.WebSocket) {
return;
}
if (socket.readyState == WebSocket.OPEN) {
message = document.getElementById("message").value;
console.log("\n======\n");
console.log(message);
console.log("\n======\n");
socket.send(message);
} else {
alert("连接没有开启.");
}
}
</script>
<form onsubmit="return false;">
<h3>WebSocket 聊天室:</h3>
<textarea id="responseText" style="width: 500px; height: 300px;"></textarea>
<input type="text" name="message" id="message" style="width: 300px" value="ping">
<input type="button" value="发送消息" onclick="send()">
<input type="button" onclick="javascript:document.getElementById('responseText').value=''" value="清空聊天记录">
</form>
</body>
</html>
测试效果
对这个例子而言,同样可以通过一个HashMap来实现用户可自由填写昵称的方式来优化用户体验。具体的实现方式和上文类似,这里就不过多的叙述了。
总结
经过这两个小例子,顶多也就是对netty整体的工作流程了解下,至于实际的开发,相比还远远不够。可以多想想如何基于上面的例子实现多人聊天,多房间聊天。
参考链接: