Netty我就不多說了,是什麼能看到這篇文章的都很清楚
網上很多文章直接黏貼複製的不說,還基本沒辦法拿出來當個例子走一遍。
我這版雖然也是照着能用的修修改改,但最起碼保證能用,而且註釋很詳細。
話不多說,直接搞重點。
我的需求是什麼:
用Netty搭建一個項目,能接到Http、WebSocket請求,處理它,返回它。
請求類型eg:
http://www.anyongliang.cn:8888/Organize/login?user=root&pwd=123456
ws://www.anyongliang.cn:8888/ws
實現需求我需要什麼:
電腦
Jdk1.8(推薦)
IDE(推薦idea,我也用idea做演示)
jar管理(推薦gradle,我也用gradle做演示)
開始實現:
我的註釋很多,就不一一細寫了,沒什麼卵用,demo搭起來,走個幾遍,打幾個斷點,什麼都懂了。
個別不懂的針對類去搜,實在不行去官網譯本:netty 4 官網譯文
1:創建項目,並添加依賴。
項目核心結構如圖:
依賴:
testCompile group: 'junit', name: 'junit', version: '4.12'
//netty-4
compile group: 'io.netty', name: 'netty-all', version: '4.1.19.Final'
//mongo-Bson
compile group: 'org.mongodb', name: 'mongo-java-driver', version: '3.8.2'
//google-Gson
compile group: 'com.google.code.gson', name: 'gson', version: '2.8.5'
//apache-commons工具包
compile group: 'org.apache.commons', name: 'commons-dbcp2', version: '2.1.1'
compile group: 'org.apache.commons', name: 'commons-lang3', version: '3.6'
compile group: 'commons-io', name: 'commons-io', version: '2.5'
compile group: 'commons-codec', name: 'commons-codec', version: '1.10'
//日誌-slf4j
compile group: 'org.slf4j', name: 'slf4j-log4j12', version: '1.7.25'
核心代碼:
1:解碼器,用來解析請求
package cn.ayl.socket.decoder;
import cn.ayl.config.Const;
import cn.ayl.socket.handler.HeartBeatHandler;
import cn.ayl.socket.handler.HttpAndWebSocketHandler;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
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.stream.ChunkedWriteHandler;
import io.netty.handler.timeout.IdleStateHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* created by Rock-Ayl 2019-11-6
* WebSocket請求的解碼器,一個請求需要先從這裏走,實現init
*/
public class HttpAndWebSocketDecoder extends ChannelInitializer<SocketChannel> {
protected static Logger logger = LoggerFactory.getLogger(HttpAndWebSocketDecoder.class);
@Override
protected void initChannel(SocketChannel ch) throws Exception {
logger.info("解析請求.");
ChannelPipeline pipeline = ch.pipeline();
//http解碼
initHttpChannel(pipeline);
//心跳檢測
initHeartBeat(pipeline);
//基於http的WebSocket
initWebSocket(pipeline);
//處理器
pipeline.addLast(new HttpAndWebSocketHandler());
}
//Http部分
private void initHttpChannel(ChannelPipeline pipeline) throws Exception {
//http解碼器(webSocket是http的升級)
pipeline.addLast(new HttpServerCodec());
//以塊的方式來寫的處理器,解決大碼流的問題,ChunkedWriteHandler:可以向客戶端發送HTML5文件
pipeline.addLast(new ChunkedWriteHandler());
//netty是基於分段請求的,HttpObjectAggregator的作用是將HTTP消息的多個部分合成一條完整的HTTP消息,參數是聚合字節的最大長度
pipeline.addLast(new HttpObjectAggregator(Const.MaxContentLength));
}
//心跳部分
private void initHeartBeat(ChannelPipeline pipeline) throws Exception {
// 針對客戶端,如果在1分鐘時沒有向服務端發送讀寫心跳(ALL),則主動斷開,如果是讀空閒或者寫空閒,不處理
pipeline.addLast(new IdleStateHandler(Const.ReaderIdleTimeSeconds, Const.WriterIdleTimeSeconds, Const.AllIdleTimeSeconds));
// 自定義的空閒狀態檢測
pipeline.addLast(new HeartBeatHandler());
}
//WebSocket部分
private void initWebSocket(ChannelPipeline pipeline) throws Exception {
/**
* WebSocketServerProtocolHandler負責websocket握手以及處理控制框架(Close,Ping(心跳檢檢測request),Pong(心跳檢測響應))。
* 參數爲ws請求的訪問路徑 eg:ws://127.0.0.1:8888/WebSocket。
*/
pipeline.addLast(new WebSocketServerProtocolHandler(Const.WebSocketPath));
}
}
2:處理器,一共兩個,一個用來控制心跳,一個用來分發請求並處理
package cn.ayl.socket.handler;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.timeout.IdleStateEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* created by Rock-Ayl 2019-11-17
* WebSocket心跳處理程序
*/
public class HeartBeatHandler extends ChannelInboundHandlerAdapter {
protected static Logger logger = LoggerFactory.getLogger(HeartBeatHandler.class);
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object msg) throws Exception {
// 判斷evt是否是 IdleStateEvent(超時的事件,用於觸發用戶事件,包含讀空閒/寫空閒/讀寫空閒 )
if (msg instanceof IdleStateEvent) {
// 強制類型轉換
IdleStateEvent event = (IdleStateEvent) msg;
switch (event.state()) {
case READER_IDLE:
logger.info("進入讀空閒...");
break;
case WRITER_IDLE:
logger.info("進入寫空閒...");
break;
case ALL_IDLE:
logger.info("開始殺死無用通道,節約資源");
Channel channel = ctx.channel();
channel.close();
break;
}
}
}
}
package cn.ayl.socket.handler;
import cn.ayl.json.JsonObject;
import cn.ayl.json.JsonUtil;
import io.netty.buffer.ByteBuf;
import io.netty.channel.*;
import io.netty.handler.codec.http.*;
import io.netty.handler.codec.http.multipart.DefaultHttpDataFactory;
import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder;
import io.netty.handler.codec.http.multipart.InterfaceHttpData;
import io.netty.handler.codec.http.multipart.MemoryAttribute;
import io.netty.handler.codec.http.websocketx.*;
import io.netty.util.CharsetUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static io.netty.buffer.Unpooled.copiedBuffer;
/**
* created by Rock-Ayl on 2019-11-7
* Http請求和WebSocket請求的處理程序
*/
public class HttpAndWebSocketHandler extends ChannelInboundHandlerAdapter {
protected static Logger logger = LoggerFactory.getLogger(HttpAndWebSocketHandler.class);
private WebSocketServerHandshaker webSocketServerHandshaker;
/**
* 通道,請求過來從這裏分類
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//處理Http請求和WebSocket請求的分別處理
if (msg instanceof FullHttpRequest) {
handleHttpRequest(ctx, (FullHttpRequest) msg);
} else if (msg instanceof HttpContent) {
//todo handleHttpContent
} else if (msg instanceof WebSocketFrame) {
handleWebSocketRequest(ctx, (WebSocketFrame) msg);
}
}
/**
* 每個channel都有一個唯一的id值
* asLongText方法是channel的id的全名
*/
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
//todo 連接打開時
logger.info(ctx.channel().localAddress().toString() + " handlerAdded!, channelId=" + ctx.channel().id().asLongText());
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
//todo 連接關閉時
logger.info(ctx.channel().localAddress().toString() + " handlerRemoved!, channelId=" + ctx.channel().id().asLongText());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
//todo 出現異常
logger.error("Client:" + ctx.channel().remoteAddress() + "error", cause.getMessage());
ctx.close();
}
// 處理Websocket的代碼
private void handleWebSocketRequest(ChannelHandlerContext ctx, WebSocketFrame frame) {
// 判斷是否是關閉鏈路的指令
if (frame instanceof CloseWebSocketFrame) {
webSocketServerHandshaker.close(ctx.channel(), (CloseWebSocketFrame) frame.retain());
return;
}
// 判斷是否是Ping消息
if (frame instanceof PingWebSocketFrame) {
ctx.channel().write(new PongWebSocketFrame(frame.content().retain()));
return;
}
// 文本消息,不支持二進制消息
if (frame instanceof TextWebSocketFrame) {
//請求text
String request = ((TextWebSocketFrame) frame).text();
logger.info("收到信息:" + request);
//返回
ctx.channel().writeAndFlush(new TextWebSocketFrame(JsonObject.Success().append("req", request).toString()));
}
}
/**
* 處理業務
*
* @param path eg: /Organize/login
* @param params eg: user:root pwd:123456
* @return
*/
private JsonObject handleServiceFactory(String path, Map<String, Object> params) {
//todo 根據path和params處理業務並返回
return JsonObject.Success();
}
private void handleHttpRequest(final ChannelHandlerContext ctx, FullHttpRequest req) throws Exception {
//todo http請求內容分類進行細化,目前設定爲全部爲服務請求(可以存在頁面,資源,上傳等等)
handleService(ctx, req);
}
//處理http服務請求
private void handleService(ChannelHandlerContext ctx, FullHttpRequest req) {
FullHttpResponse response;
JsonObject result;
//獲得請求path
String path = getPath(req);
//根據請求類型處理請求 get post ...
if (req.method() == HttpMethod.GET) {
//獲取請求參數
Map<String, Object> params = getGetParamsFromChannel(req);
//業務
result = handleServiceFactory(path, params);
response = responseOKAndJson(HttpResponseStatus.OK, result);
} else if (req.method() == HttpMethod.POST) {
//獲取請求參數
Map<String, Object> params = getPostParamsFromChannel(req);
//處理業務
result = handleServiceFactory(path, params);
response = responseOKAndJson(HttpResponseStatus.OK, result);
} else {
//todo 處理其他類型的請求
response = responseOKAndJson(HttpResponseStatus.INTERNAL_SERVER_ERROR, null);
}
// 發送響應並關閉連接
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
/**
* Http獲取請求Path
*
* @param req
* @return
*/
private String getPath(FullHttpRequest req) {
String path = null;
try {
path = new URI(req.getUri()).getPath();
} catch (Exception e) {
logger.error("接口解析錯誤.");
} finally {
return path;
}
}
/**
* Http獲取GET方式傳遞的參數
*
* @param fullHttpRequest
* @return
*/
private Map<String, Object> getGetParamsFromChannel(FullHttpRequest fullHttpRequest) {
//參數組
Map<String, Object> params = new HashMap<>();
//如果請求爲GET繼續
if (fullHttpRequest.method() == HttpMethod.GET) {
// 處理get請求
QueryStringDecoder decoder = new QueryStringDecoder(fullHttpRequest.uri());
Map<String, List<String>> paramList = decoder.parameters();
for (Map.Entry<String, List<String>> entry : paramList.entrySet()) {
params.put(entry.getKey(), entry.getValue().get(0));
}
return params;
} else {
return null;
}
}
/**
* Http獲取POST方式傳遞的參數
*
* @param fullHttpRequest
* @return
*/
private Map<String, Object> getPostParamsFromChannel(FullHttpRequest fullHttpRequest) {
//參數組
Map<String, Object> params;
//如果請求爲POST
if (fullHttpRequest.method() == HttpMethod.POST) {
// 處理POST請求
String strContentType = fullHttpRequest.headers().get("Content-Type").trim();
if (strContentType.contains("x-www-form-urlencoded")) {
params = getFormParams(fullHttpRequest);
} else if (strContentType.contains("application/json")) {
try {
params = getJSONParams(fullHttpRequest);
} catch (UnsupportedEncodingException e) {
return null;
}
} else {
return null;
}
return params;
} else {
return null;
}
}
/**
* Http解析from表單數據(Content-Type = x-www-form-urlencoded)
*
* @param fullHttpRequest
* @return
*/
private Map<String, Object> getFormParams(FullHttpRequest fullHttpRequest) {
Map<String, Object> params = new HashMap<>();
HttpPostRequestDecoder decoder = new HttpPostRequestDecoder(new DefaultHttpDataFactory(false), fullHttpRequest);
List<InterfaceHttpData> postData = decoder.getBodyHttpDatas();
for (InterfaceHttpData data : postData) {
if (data.getHttpDataType() == InterfaceHttpData.HttpDataType.Attribute) {
MemoryAttribute attribute = (MemoryAttribute) data;
params.put(attribute.getName(), attribute.getValue());
}
}
return params;
}
/**
* Http解析json數據(Content-Type = application/json)
*
* @param fullHttpRequest
* @return
* @throws UnsupportedEncodingException
*/
private Map<String, Object> getJSONParams(FullHttpRequest fullHttpRequest) throws UnsupportedEncodingException {
Map<String, Object> params = new HashMap<>();
ByteBuf content = fullHttpRequest.content();
byte[] reqContent = new byte[content.readableBytes()];
content.readBytes(reqContent);
String strContent = new String(reqContent, "UTF-8");
JsonObject jsonParams = JsonUtil.parse(strContent);
for (Object key : jsonParams.keySet()) {
params.put(key.toString(), jsonParams.get((String) key));
}
return params;
}
/**
* Http響應OK並返回Json
*
* @param status 狀態
* @param result 返回值
* @return
*/
private FullHttpResponse responseOKAndJson(HttpResponseStatus status, JsonObject result) {
ByteBuf content = copiedBuffer(result.toString(), CharsetUtil.UTF_8);
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, content);
if (content != null) {
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json;charset=UTF-8");
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
}
return response;
}
/**
* Http響應OK並返回文本
*
* @param status 狀態
* @param result 返回值
* @return
*/
private FullHttpResponse responseOKAndText(HttpResponseStatus status, String result) {
ByteBuf content = copiedBuffer(result, CharsetUtil.UTF_8);
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, content);
if (content != null) {
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain;charset=UTF-8");
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
}
return response;
}
}
3:服務管控
package cn.ayl.socket.server;
import cn.ayl.config.Const;
import cn.ayl.socket.decoder.HttpAndWebSocketDecoder;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.PooledByteBufAllocator;
import io.netty.channel.Channel;
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* created by Rock-Ayl 2019-11-4
* 通信服務
*/
public class SocketServer {
protected static Logger logger = LoggerFactory.getLogger(SocketServer.class);
private Channel channel;
/**
* NioEventLoopGroup是一個處理I/O操作的事件循環器 (其實是個線程池)。
* netty爲不同類型的傳輸協議提供了多種NioEventLoopGroup的實現。
* 在本例中我們要實現一個服務端應用,並使用了兩個NioEventLoopGroup。
* 第一個通常被稱爲boss,負責接收已到達的 connection。
* 第二個被稱作 worker,當 boss 接收到 connection 並把它註冊到 worker 後,worker 就可以處理 connection 上的數據通信。
* 要創建多少個線程,這些線程如何匹配到Channel上會隨着EventLoopGroup實現的不同而改變,或者你可以通過構造器去配置他們。
*/
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
/**
* 創建一個默認配置的HttpServerBootstrap
*
* @param bossGroup netty-boss
* @param workerGroup netty-work-IO
* @return
*/
public static ServerBootstrap createDefaultServerBootstrap(EventLoopGroup bossGroup, EventLoopGroup workerGroup) {
return new ServerBootstrap()
/**
* 組裝Boss和IO
*/
.group(bossGroup, workerGroup)
/**
* 這裏我們指定NioServerSocketChannel類,用來初始化一個新的Channel去接收到達的connection。
*/
.channel(NioServerSocketChannel.class)
/**
* 你可以給Channel配置特有的參數。
* 這裏我們寫的是 TCP/IP 服務器,所以可以配置一些 socket 選項,例如 tcpNoDeply 和 keepAlive。
* 請參考ChannelOption和ChannelConfig文檔來獲取更多可用的 Channel 配置選項,並對此有個大概的瞭解。
* BACKLOG用於構造服務端套接字ServerSocket對象,標識當服務器請求處理線程全滿時,用於臨時存放已完成三次握手的請求的隊列的最大長度。如果未設置或所設置的值小於1,Java將使用默認值50。
*/
.option(ChannelOption.SO_BACKLOG, Const.ChannelOptionSoBacklogValue)
/**
* 注意到option()和childOption()了嗎?
* option()用來配置NioServerSocketChannel(負責接收到來的connection),
* 而childOption()是用來配置被ServerChannel(這裏是NioServerSocketChannel) 所接收的Channel
*
* ChannelOption.SO_KEEPALIVE表示是否開啓TCP底層心跳機制,true爲開啓
* ChannelOption.SO_REUSEADDR表示端口釋放後立即就可以被再次使用,因爲一般來說,一個端口釋放後會等待兩分鐘之後才能再被使用
* ChannelOption.TCP_NODELAY表示是否開始Nagle算法,true表示關閉,false表示開啓,通俗地說,如果要求高實時性,有數據發送時就馬上發送,就關閉,如果需要減少發送次數減少網絡交互就開啓
*/
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childOption(ChannelOption.SO_REUSEADDR, true)
.childOption(ChannelOption.TCP_NODELAY, true)
.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
}
/**
* 開啓Http與WebSocket
*/
public void startup() {
try {
try {
/**
* ServerBootstrap是用來搭建 server 的協助類。
* 你也可以直接使用Channel搭建 server,然而這樣做步驟冗長,不是一個好的實踐,大多數情況下建議使用ServerBootstrap。
*/
ServerBootstrap bootstrap = SocketServer.createDefaultServerBootstrap(bossGroup, workerGroup);
/**
* 這裏的 handler 會被用來處理新接收的Channel。
* ChannelInitializer是一個特殊的 handler,
* 幫助開發者配置Channel,而多數情況下你會配置Channel下的ChannelPipeline,
* 往 pipeline 添加一些 handler (例如DiscardServerHandler) 從而實現你的應用邏輯。
* 當你的應用變得複雜,你可能會向 pipeline 添加更多的 handler,並把這裏的匿名類抽取出來作爲一個單獨的類。
*/
bootstrap.childHandler(new HttpAndWebSocketDecoder());
/**
* 剩下的事情就是綁定端口並啓動服務器,這裏我們綁定到機器的8080端口。你可以多次調用bind()(基於不同的地址)。
* Bind and start to accept incoming connections.(綁定並開始接受傳入的連接)
*/
ChannelFuture f = bootstrap.bind(Const.SocketPort).sync();
/**
* Wait until the server socket is closed.(等待,直到服務器套接字關閉)
* In this example, this does not happen, but you can do that to gracefully(在本例中,這種情況不會發生,但是您可以優雅地這樣做)
* shut down your server.(關閉你的服務)
*/
channel = f.channel();
channel.closeFuture().sync();
} finally {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
} catch (Exception e) {
logger.error("Run Socket Fail!");
}
}
//關閉Socket
public void destroy() {
if (channel != null) {
channel.close();
}
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
System.out.println("WebsocketChatServer Destroy:" + Const.SocketPort);
}
public static void main(String[] args) {
new SocketServer().startup();
}
}
4:常量池:
package cn.ayl.config;
/**
* created by Rock-Ayl 2019-11-5
* 常量
*/
public class Const {
//SocketPort
public static int SocketPort = 8888;
//http請求聚合字節最大長度
public static int MaxContentLength = 65535;
//WebSocket讀空閒時間閒置/秒
public static int ReaderIdleTimeSeconds = 8;
//WebSocket寫空閒時間閒置/秒
public static int WriterIdleTimeSeconds = 10;
//WebSocket所有空閒時間閒置/秒
public static int AllIdleTimeSeconds = 12;
public static String WebSocketPath = "/WebSocket";
//BACKLOG值用於構造服務端套接字ServerSocket對象,標識當服務器請求處理線程全滿時,用於臨時存放已完成三次握手的請求的隊列的最大長度。如果未設置或所設置的值小於1,Java將使用默認值50。
public static int ChannelOptionSoBacklogValue = 1024;
}
5:WebSocket測試
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>WebSocket客戶端</title>
</head>
<body>
<script type="text/javascript">
var socket;
//如果瀏覽器支持WebSocket
if (window.WebSocket) {
//參數就是與服務器連接的地址
socket = new WebSocket("ws://127.0.0.1:8888/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 = "連接開啓";
}
//連接斷掉的回調函數
socket.onclose = function (event) {
var ta = document.getElementById("responseText");
ta.value = ta.value + "\n" + "連接關閉";
}
} else {
alert("瀏覽器不支持WebSocket!");
}
//發送數據
function send(message) {
if (!window.WebSocket) {
return;
}
//當websocket狀態打開
if (socket.readyState == WebSocket.OPEN) {
socket.send(message);
} else {
alert("連接沒有開啓");
}
}
</script>
<form onsubmit="return false">
<textarea name="message" style="width: 400px;height: 200px"></textarea>
<input type="button" value="發送數據" onclick="send(this.form.message.value);">
<h3>服務器輸出:</h3>
<textarea id="responseText" style="width: 400px;height: 300px;"></textarea>
<input type="button" onclick="javascript:document.getElementById('responseText').value=''" value="清空數據">
</form>
</body>
</html>
-----------------------------------------------End-----------------------------------------------------------------------------------------------------------------
所有的核心代碼都在這裏,替換掉裏面Json就可以拿來去用,啓動後調用接口。
就可以測試http請求
HTML5代碼直接打開可以測試WebSocket接口
如果你照着我這個還是搭不上,好,gitHub會用吧?:Demo-GitHub地址
代碼思想和一些註釋都是網上彙總的
我也不太瞭解Netty比較核心的東西,本博客也只是學習自用,目的是讓我這種人不看官網的能夠快速上手Netty大概怎麼玩的。