本文爲轉載後,加上了部分修改而成。具體入下:
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整體的工作流程瞭解下,至於實際的開發,相比還遠遠不夠。可以多想想如何基於上面的例子實現多人聊天,多房間聊天。
參考鏈接: