注:源代碼來自享學課堂,略有修改,學習之後所做筆記,方便回顧,也給大家一個參考
服務端
主函數
和普通的服務代碼相同,這裏加上了ssl的支持,如果不需要ssl支持,默認是false
package com.gg.socket.netty.server;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.util.SelfSignedCertificate;
import io.netty.util.concurrent.ImmediateEventExecutor;
/**
* @Description WebSecketServer
* @Author honry.guan
* @Date 2020/5/26 17:22
*/
public class WebSocketServer {
/**
* 創建DefaultChannelGroup,保存已有鏈接socketChannel,羣發和一對一功能可以使用
*/
public static final ChannelGroup CHANNEL_GROUP = new DefaultChannelGroup(ImmediateEventExecutor.INSTANCE);
/**
* 是否啓用ssl,即https
*/
public static final boolean SSL = false;
/**
* 通過ssl訪問8443,否則就訪問8080
*/
public static final int PORT = Integer.parseInt(System.getProperty("port", SSL ? "8443" : "8080"));
public static void main(String[] args) throws Exception {
// SSL配置
final SslContext sslContext;
if (SSL) {
SelfSignedCertificate ssc = new SelfSignedCertificate();
sslContext = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()).build();
} else {
sslContext = null;
}
// 一個線程負責接受新的連接,一個負責處理讀寫
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new WebSocketServerInitializer(sslContext, CHANNEL_GROUP));
Channel ch = b.bind(PORT).sync().channel();
System.out.println("打開瀏覽器訪問: " + (SSL ? "https" : "http") + "://127.0.0.1:" + PORT + '/');
ch.closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
WebSocketServerInitializer
websocket需要藉助http握手支持
package com.gg.socket.netty.server;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.socket.SocketChannel;
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.codec.http.websocketx.extensions.compression.WebSocketServerCompressionHandler;
import io.netty.handler.ssl.SslContext;
/**
* @Description WebSocketServerInitializer
* @Author honry.guan
* @Date 2020/5/26 17:22
*/
public class WebSocketServerInitializer extends ChannelInitializer<SocketChannel> {
private final ChannelGroup group;
/**
* websocket訪問路徑
*/
public static final String WEBSOCKET_PATH = "/websocket";
/**
* ssl支持
*/
private final SslContext sslCtx;
public WebSocketServerInitializer(SslContext sslCtx, ChannelGroup group) {
this.sslCtx = sslCtx;
this.group = group;
}
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
if(sslCtx != null){
pipeline.addLast(sslCtx.newHandler((socketChannel.alloc())));
}
//http握手支持
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(new HttpObjectAggregator(655536));
//netty提供的,支持websocket應答數據壓縮傳輸
pipeline.addLast(new WebSocketServerCompressionHandler());
//netty提供,對整個websocket的通信進行了初始化(發現http保溫中國有升級爲websocket的請你去,包括握手,以及後面的一些通信控制)
pipeline.addLast(new WebSocketServerProtocolHandler(WEBSOCKET_PATH,null,true));
//瀏覽器訪問是,展示index頁面,通過代碼生成的HTML頁面
pipeline.addLast(new ProcessWsIndexPageHandler(WEBSOCKET_PATH));
//對websocket的數據記性處理
pipeline.addLast(new ProcessWsFrameHandler(group));
}
}
ProcessWsIndexPageHandler
通過http請求訪問:8080/的時候,會返回一個HTML頁面
package com.gg.socket.netty.server;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.handler.codec.http.*;
import io.netty.handler.ssl.SslHandler;
import io.netty.util.CharsetUtil;
import static io.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND;
/**
* @Description ProcessWsIndexPageHandler
* @Author honry.guan
* @Date 2020/5/26 17:23
*/
public class ProcessWsIndexPageHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
private final String webSocketPath;
public ProcessWsIndexPageHandler(String websocketPath) {
this.webSocketPath = websocketPath;
}
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, FullHttpRequest fullHttpRequest) throws Exception {
//處理髮送錯誤或者無法解析的http請求
if (!fullHttpRequest.decoderResult().isSuccess()) {
sendHttpResponse(channelHandlerContext, fullHttpRequest, new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST));
return;
}
//只允許get請求
if (fullHttpRequest.method() != HttpMethod.GET) {
sendHttpResponse(channelHandlerContext, fullHttpRequest, new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.FORBIDDEN));
return;
}
//發送index頁面內容
if ("/".equals(fullHttpRequest.getUri()) || "index.html".equals(fullHttpRequest.getUri())) {
//生成websocket的訪問地址,寫入index頁面中
String webSocketLocation = getWebSocketLocation(channelHandlerContext.pipeline(), fullHttpRequest,
webSocketPath);
System.out.println("webSocketLocation:" + webSocketLocation);
//生成index頁面的具體內容,發送瀏覽器
ByteBuf byteBuf = MakeIndexPage.getContent(webSocketLocation);
FullHttpResponse res = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, byteBuf);
res.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html;charset=utf-8");
HttpUtil.setContentLength(res, byteBuf.readableBytes());
sendHttpResponse(channelHandlerContext, fullHttpRequest, res);
} else {
sendHttpResponse(channelHandlerContext, fullHttpRequest, new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, NOT_FOUND));
}
}
private String getWebSocketLocation(ChannelPipeline pipeline, FullHttpRequest fullHttpRequest, String path) {
String protocol = "ws";
if (pipeline.get(SslHandler.class) != null) {
protocol = "wss";
}
return protocol + "://" + fullHttpRequest.headers().get(HttpHeaderNames.HOST) + path;
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
private void sendHttpResponse(ChannelHandlerContext channelHandlerContext, FullHttpRequest fullHttpRequest, FullHttpResponse fullHttpResponse) {
//對錯誤的請求進行處理
if (fullHttpResponse.status().code() != 200) {
ByteBuf buf = Unpooled.copiedBuffer(fullHttpResponse.status().toString(), CharsetUtil.UTF_8);
fullHttpResponse.content().writeBytes(buf);
buf.release();
HttpUtil.setContentLength(fullHttpResponse, fullHttpRequest.content().readableBytes());
}
//發送應答
ChannelFuture f = channelHandlerContext.channel().writeAndFlush(fullHttpResponse);
//對於不是長鏈接或者錯誤的額請求直接關閉連接
if (!HttpUtil.isKeepAlive(fullHttpRequest) || fullHttpResponse.status().code() != 200) {
f.addListener(ChannelFutureListener.CLOSE);
}
}
}
ProcessWsFrameHandler
對瀏覽器或者客戶端發送的消息進行處理,再對當前channel或者所有連接上的客戶端拳法消息
package com.gg.socket.netty.server;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import java.util.Locale;
import java.util.logging.Logger;
/**
* @Description ProcessWsFrameHandler
* @Author honry.guan
* @Date 2020/5/26 17:23
*/
public class ProcessWsFrameHandler extends SimpleChannelInboundHandler<WebSocketFrame> {
private final ChannelGroup group;
public ProcessWsFrameHandler(ChannelGroup group) {
this.group = group;
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame frame) throws Exception {
// 判斷是否是文本幀,目前只處理文本幀
if(frame instanceof TextWebSocketFrame){
String request = ((TextWebSocketFrame)frame).text();
System.out.println("服務端收到消息:channelId="+ctx.channel().id()+" 消息:" + request);
//這個是對當前channel的單發消息
ctx.channel().writeAndFlush(new TextWebSocketFrame(("客戶端收到單發消息:"+request).toUpperCase(Locale.CHINA)));
//下面是羣發,group保存的是添加進來的channel
group.writeAndFlush(new TextWebSocketFrame(("客戶端收到羣發消息:"+request).toUpperCase(Locale.CHINA)));
}else{
String message = "不支持的文本類型:"+ frame.getClass().getName();
throw new UnsupportedOperationException(message);
}
}
/**
* 重寫自定義處理事件
* @param ctx
* @param evt
* @throws Exception
*/
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
//檢測事件,如果握手成功事件,做業務處理
if(evt == WebSocketServerProtocolHandler.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE){
//通知所有已經連接的websocket客戶端那新的客戶端已經連接上了
group.writeAndFlush(new TextWebSocketFrame("客戶端:"+ctx.channel().id()+"連接上了"));
//將新的websocket channel添加到channelgroup中,一遍他客戶已接收所有的消息
group.add(ctx.channel());
}else{
super.userEventTriggered(ctx,evt);
}
}
}
MakeIndexPage
生成html的工具類,當然也可以自己新建一個HTML文本來訪問服務器
package com.gg.socket.netty.server;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.util.CharsetUtil;
public final class MakeIndexPage {
private static final String NEWLINE = "\r\n";
public static ByteBuf getContent(String webSocketLocation) {
return Unpooled.copiedBuffer(
"<html><head><title>Web Socket Test</title></head>"
+ NEWLINE +
"<body>" + NEWLINE +
"<script type=\"text/javascript\">" + NEWLINE +
"var socket;" + NEWLINE +
"if (!window.WebSocket) {" + NEWLINE +
" window.WebSocket = window.MozWebSocket;" + NEWLINE +
'}' + NEWLINE +
"if (window.WebSocket) {" + NEWLINE +
" socket = new WebSocket(\"" + webSocketLocation + "\");"
+ NEWLINE +
" socket.onmessage = function(event) {" + NEWLINE +
" var ta = document.getElementById('responseText');"
+ NEWLINE +
" ta.value = ta.value + '\\n' + event.data" + NEWLINE +
" };" + NEWLINE +
" socket.onopen = function(event) {" + NEWLINE +
" var ta = document.getElementById('responseText');"
+ NEWLINE +
" ta.value = \"Web Socket opened!\";" + NEWLINE +
" };" + NEWLINE +
" socket.onclose = function(event) {" + NEWLINE +
" var ta = document.getElementById('responseText');"
+ NEWLINE +
" ta.value = ta.value + \"Web Socket closed\"; "
+ NEWLINE +
" };" + NEWLINE +
"} else {" + NEWLINE +
" alert(\"Your browser does not support Web Socket.\");"
+ NEWLINE +
'}' + NEWLINE +
NEWLINE +
"function send(message) {" + NEWLINE +
" if (!window.WebSocket) { return; }" + NEWLINE +
" if (socket.readyState == WebSocket.OPEN) {" + NEWLINE +
" socket.send(message);" + NEWLINE +
" } else {" + NEWLINE +
" alert(\"The socket is not open.\");" + NEWLINE +
" }" + NEWLINE +
'}' + NEWLINE +
"</script>" + NEWLINE +
"<form οnsubmit=\"return false;\">" + NEWLINE +
"<input type=\"text\" name=\"message\" " +
"value=\"Hello, World!\"/>" +
"<input type=\"button\" value=\"Send Web Socket Data\""
+ NEWLINE +
" οnclick=\"send(this.form.message.value)\" />"
+ NEWLINE +
"<h3>Output</h3>" + NEWLINE +
"<textarea id=\"responseText\" " +
"style=\"width:500px;height:300px;\"></textarea>"
+ NEWLINE +
"</form>" + NEWLINE +
"</body>" + NEWLINE +
"</html>" + NEWLINE, CharsetUtil.US_ASCII);
}
}
生成之後的HTML代碼,也可以自定義一個html文件,直接打開瀏覽器
<html><head><title>Web Socket Test</title></head>
<body>
<script type="text/javascript">
var socket;
if (!window.WebSocket) {
window.WebSocket = window.MozWebSocket;
}
if (window.WebSocket) {
socket = new WebSocket("ws://127.0.0.1:8080/websocket");
socket.onmessage = function(event) {
var ta = document.getElementById('responseText');
ta.value = ta.value + '\n' + event.data
};
socket.onopen = function(event) {
var ta = document.getElementById('responseText');
ta.value = "Web Socket opened!";
};
socket.onclose = function(event) {
var ta = document.getElementById('responseText');
ta.value = ta.value + "Web Socket closed";
};
} else {
alert("Your browser does not support Web Socket.");
}
function send(message) {
if (!window.WebSocket) { return; }
if (socket.readyState == WebSocket.OPEN) {
socket.send(message);
} else {
alert("The socket is not open.");
}
}
</script>
<form οnsubmit="return false;">
<input type="text" name="message" value="Hello, World!"/><input type="button" value="Send Web Socket Data"
οnclick="send(this.form.message.value)" />
<h3>Output</h3>
<textarea id="responseText" style="width:500px;height:300px;"></textarea>
</form>
</body>
</html>
通過瀏覽器直接發送和接收消息
啓動服務,訪問http://127.0.0.1:8080/,這個時候就能夠和服務器進行通信了,如圖
服務端收到消息:
channelId=c7882173 消息:Hello, World!
通過自定義客戶端發送和接收消息
主函數
package com.gg.socket.client;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.Unpooled;
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.http.DefaultHttpHeaders;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.websocketx.*;
import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketClientCompressionHandler;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URI;
/**
* @Description WebSocketClient
* @Author honry.guan
* @Date 2020/5/25 10:47
*/
public class WebSocketClient {
public static final String URL = System.getProperty("url", "ws://127.0.0.1:8080/websocket");
public static final String SURL = System.getProperty("url", "ws://127.0.0.1:8843/websocket");
public static void main(String[] args) throws Exception {
URI uri = new URI(URL);
String scheme = uri.getScheme() == null ? "ws" : uri.getScheme();
final String host = uri.getHost() == null ? "127.0.0.1" : uri.getHost();
final int port = uri.getPort();
if (!"ws".equalsIgnoreCase(scheme) && !"wss".equalsIgnoreCase(scheme)) {
System.out.println("只支持ws");
return;
}
final boolean ssl = "wss".equalsIgnoreCase(scheme);
final SslContext sslCtx;
if (ssl) {
sslCtx = SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE).build();
} else {
sslCtx = null;
}
EventLoopGroup group = new NioEventLoopGroup();
try {
final WebSocketClientHandler handler = new WebSocketClientHandler(WebSocketClientHandshakerFactory.newHandshaker(uri, WebSocketVersion.V13, null, true, new DefaultHttpHeaders()));
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline p = socketChannel.pipeline();
if (sslCtx != null) {
p.addLast(sslCtx.newHandler(socketChannel.alloc(), host, port));
}
//http協議握手
p.addLast(new HttpClientCodec());
p.addLast(new HttpObjectAggregator(8192));
//支持websocket數據壓縮
p.addLast(WebSocketClientCompressionHandler.INSTANCE);
p.addLast(handler);
}
});
//連接服務器
Channel ch = b.connect(uri.getHost(), port).sync().channel();
//等待握手完成
handler.handshakeFuture().sync();
BufferedReader console = new BufferedReader(new InputStreamReader(System.in));
while (true) {
String msg = console.readLine();
if (msg == null) {
break;
} else if ("bye".equals(msg.toLowerCase())) {
ch.writeAndFlush(new CloseWebSocketFrame());
ch.closeFuture().sync();
break;
} else if ("ping".equals(msg.toLowerCase())) {
WebSocketFrame frame = new PingWebSocketFrame(
Unpooled.wrappedBuffer(new byte[]{8, 1, 8, 1}));
ch.writeAndFlush(frame);
} else {
WebSocketFrame frame = new TextWebSocketFrame(msg);
ch.writeAndFlush(frame);
}
}
} finally {
group.shutdownGracefully();
}
}
}
自定義handler
package com.gg.socket.client;
import io.netty.channel.*;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.websocketx.*;
import io.netty.util.CharsetUtil;
/**
* @Description WebSocketClientHandler
* @Author honry.guan
* @Date 2020/5/25 10:47
*/
public class WebSocketClientHandler extends SimpleChannelInboundHandler<Object> {
/**
* 負責和服務器握手
*/
private final WebSocketClientHandshaker handshaker;
/**
* 握手結果
*/
private ChannelPromise handshakeFuture;
public WebSocketClientHandler(WebSocketClientHandshaker handshaker) {
this.handshaker = handshaker;
}
public ChannelFuture handshakeFuture() {
return handshakeFuture;
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
handshaker.handshake(ctx.channel());
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("連接斷開");
}
/**
* 當前handler被添加到pipeline時,new出握手的結果示例,以備將來使用
*
* @param ctx
* @throws Exception
*/
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
handshakeFuture = ctx.newPromise();
}
/**
* 讀取數據
*
* @param ctx
* @param msg
* @throws Exception
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
Channel ch = ctx.channel();
//握手未完成,完成握手
if (!handshaker.isHandshakeComplete()) {
try {
handshaker.finishHandshake(ch, (FullHttpResponse) msg);
System.out.println("完成連接");
handshakeFuture.setSuccess();
} catch (WebSocketHandshakeException e) {
System.out.println("握手連接失敗");
handshakeFuture.setFailure(e);
}
return;
}
//握手完成,升級爲websocket,不應該再收到http報文
if (msg instanceof FullHttpResponse) {
FullHttpResponse response = (FullHttpResponse) msg;
throw new IllegalStateException(
"Unexpected FullHttpResponse (getStatus=" + response.status() +
", content=" + response.content().toString(CharsetUtil.UTF_8) + ')');
}
//處理websocket報文
WebSocketFrame frame = (WebSocketFrame) msg;
if (frame instanceof TextWebSocketFrame) {
TextWebSocketFrame textWebSocketFrame = (TextWebSocketFrame) frame;
System.out.println("收到消息:" + textWebSocketFrame.text());
} else if (frame instanceof PongWebSocketFrame) {
System.out.println("客戶端收到pong");
} else if (frame instanceof CloseWebSocketFrame) {
System.out.println("客戶端手動關閉");
ch.close();
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
if (!handshakeFuture.isDone()) {
handshakeFuture.setFailure(cause);
}
ctx.close();
}
}
在窗口發送123
完成連接
你好
收到消息:客戶端收到單發消息:你好
收到消息:客戶端收到羣發消息:你好