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大概怎么玩的。