Netty是基于JDK NIO的网络框架

简化了NIO编程, 不用程序自己维护selector, 将网络通信和数据处理的部分做了分离

多用于做底层的数据通信, 心跳检测(keepalived)

1. 数据通信

1.1 Hello World

public class Server {

    public static void main(String[] args) throws Exception {
// 1 创建线两个事件循环组
// 一个是用于处理服务器端接收客户端连接的
// 一个是进行网络通信的(网络读写的)
EventLoopGroup pGroup = new NioEventLoopGroup();
EventLoopGroup cGroup = new NioEventLoopGroup(); // 2 创建辅助工具类ServerBootstrap,用于服务器通道的一系列配置
ServerBootstrap b = new ServerBootstrap();
b.group(pGroup, cGroup) // 绑定俩个线程组
.channel(NioServerSocketChannel.class) // 指定NIO的模式.NioServerSocketChannel对应TCP, NioDatagramChannel对应UDP
.option(ChannelOption.SO_BACKLOG, 1024) // 设置TCP缓冲区
.option(ChannelOption.SO_SNDBUF, 32 * 1024) // 设置发送缓冲大小
.option(ChannelOption.SO_RCVBUF, 32 * 1024) // 这是接收缓冲大小
.option(ChannelOption.SO_KEEPALIVE, true) // 保持连接
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel sc) throws Exception { //SocketChannel建立连接后的管道
// 3 在这里配置 通信数据的处理逻辑, 可以addLast多个...
sc.pipeline().addLast(new ServerHandler());
}
}); // 4 绑定端口, bind返回future(异步), 加上sync阻塞在获取连接处
ChannelFuture cf1 = b.bind(8765).sync();
//ChannelFuture cf2 = b.bind(8764).sync(); //可以绑定多个端口
// 5 等待关闭, 加上sync阻塞在关闭请求处
cf1.channel().closeFuture().sync();
//cf2.channel().closeFuture().sync();
pGroup.shutdownGracefully();
cGroup.shutdownGracefully();
}
}

SO_BACKLOG详解:
服务器的TCP内核维护两个队列A和B
客户端向服务端请求connect时, 发送SYN(第一次握手)
服务端收到SYN后, 向客户端发送SYN ACK(第二次握手),  TCP内核将连接放入队列A
客户端收到后向服务端发送ACK(第三次握手),  TCP内核将连接从A->B, accept返回, 连接完成
A/B队列的长度和即为BACKLOG, 当accept速度跟不上, A/B队列使得BACKLOG满了, 客户端连接就会被TCP内核拒绝
可以调大backlog缓解这一现象, 经验值~100

public class ServerHandler extends ChannelHandlerAdapter {

    @Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("server channel active... ");
} @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
byte[] req = new byte[buf.readableBytes()];
buf.readBytes(req);
String body = new String(req, "utf-8");
System.out.println("Server :" + body );
String response = "返回给客户端的响应:" + body ;
ctx.writeAndFlush(Unpooled.copiedBuffer(response.getBytes()));
// future完成后触发监听器, 此处是写完即关闭(短连接). 因此需要关闭连接时, 要通过server端关闭. 直接关闭用方法ctx[.channel()].close()
//.addListener(ChannelFutureListener.CLOSE);
} @Override
public void channelReadComplete(ChannelHandlerContext ctx)
throws Exception {
System.out.println("读完了");
ctx.flush();
} @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable t)
throws Exception {
ctx.close();
}
}
public class Client {

    public static void main(String[] args) throws Exception {

        EventLoopGroup group = new NioEventLoopGroup();
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel sc) throws Exception {
sc.pipeline().addLast(new ClientHandler());
}
}); ChannelFuture cf1 = b.connect("127.0.0.1", 8765).sync();
//ChannelFuture cf2 = b.connect("127.0.0.1", 8764).sync(); //可以使用多个端口
//发送消息, Buffer类型. write需要flush才发送, 可用writeFlush代替
cf1.channel().writeAndFlush(Unpooled.copiedBuffer("777".getBytes()));
cf1.channel().writeAndFlush(Unpooled.copiedBuffer("666".getBytes()));
Thread.sleep(2000);
cf1.channel().writeAndFlush(Unpooled.copiedBuffer("888".getBytes()));
//cf2.channel().writeAndFlush(Unpooled.copiedBuffer("999".getBytes())); cf1.channel().closeFuture().sync();
//cf2.channel().closeFuture().sync();
group.shutdownGracefully();
}
}
public class ClientHandler extends ChannelHandlerAdapter{

    @Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
} @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
try {
ByteBuf buf = (ByteBuf) msg;
byte[] req = new byte[buf.readableBytes()];
buf.readBytes(req);
String body = new String(req, "utf-8");
System.out.println("Client :" + body );
} finally {
// 记得释放xxxHandler里面的方法的msg参数: 写(write)数据, msg引用将被自动释放不用手动处理; 但只读数据时,!必须手动释放引用数
ReferenceCountUtil.release(msg);
}
} @Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
} @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
ctx.close();
}
}

1.2 拆包粘包问题

TCP/IP确保了包的传送, 包的顺序等, 但编程中还需要解决拆包粘包问题

-> 接收的一连串包中的数据, 处理的分隔在哪里?  基本解决方案:

1)特殊字符作为结束分隔符

2)消息定长. 固定包的长度, 长度不够用空格补全. 接收方需要trim, 效率不高不推荐

3)自定义协议. 在消息头中包含消息总长度的字段. 需要安全性时可以考虑.

特殊字符

public class Server {

    public static void main(String[] args) throws Exception {
EventLoopGroup pGroup = new NioEventLoopGroup();
EventLoopGroup cGroup = new NioEventLoopGroup(); ServerBootstrap b = new ServerBootstrap();
b.group(pGroup, cGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.option(ChannelOption.SO_SNDBUF, 32*1024)
.option(ChannelOption.SO_RCVBUF, 32*1024)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel sc) throws Exception {
// 使用DelimiterBasedFrameDecoder设置结尾分隔符$_
ByteBuf buf = Unpooled.copiedBuffer("$_".getBytes());
sc.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, buf));
// 设置字符串形式的解码. 经过StringDecoder, Handler回调方法中接收的msg的具体类型就是String了(不再是ByteBuffer). 但写时仍需要传入ByteBuffer
sc.pipeline().addLast(new StringDecoder());
// 通信数据的处理逻辑
sc.pipeline().addLast(new ServerHandler());
}
});
//4 绑定连接
ChannelFuture cf = b.bind(8765).sync(); //等待服务器监听端口关闭
cf.channel().closeFuture().sync();
pGroup.shutdownGracefully();
cGroup.shutdownGracefully();
}
}
public class ServerHandler extends ChannelHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println(" server channel active... ");
} @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("Server :" + msg);
String response = "服务器响应: " + msg + "$_";
ctx.writeAndFlush(Unpooled.copiedBuffer(response.getBytes()));
} @Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
} @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable t) throws Exception {
ctx.close();
}
}
public class Client {

    public static void main(String[] args) throws Exception {
EventLoopGroup group = new NioEventLoopGroup(); Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel sc) throws Exception {
ByteBuf buf = Unpooled.copiedBuffer("$_".getBytes());
sc.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, buf));
sc.pipeline().addLast(new StringDecoder());
sc.pipeline().addLast(new ClientHandler());
}
}); ChannelFuture cf = b.connect("127.0.0.1", 8765).sync(); cf.channel().writeAndFlush(Unpooled.wrappedBuffer("bbbb$_".getBytes()));
cf.channel().writeAndFlush(Unpooled.wrappedBuffer("cccc$_".getBytes())); cf.channel().closeFuture().sync();
group.shutdownGracefully(); }
}
public class ClientHandler extends ChannelHandlerAdapter {

    @Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("client channel active... ");
} @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
try {
String response = (String) msg;
System.out.println("Client: " + response);
} finally {
ReferenceCountUtil.release(msg);
}
} @Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
} @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}

定长

public class Server {

    public static void main(String[] args) throws Exception{
EventLoopGroup pGroup = new NioEventLoopGroup();
EventLoopGroup cGroup = new NioEventLoopGroup(); ServerBootstrap b = new ServerBootstrap();
b.group(pGroup, cGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.option(ChannelOption.SO_SNDBUF, 32*1024)
.option(ChannelOption.SO_RCVBUF, 32*1024)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel sc) throws Exception {
//设置定长字符串接收, 定长为5, 积累到5个字节才会把数据发出去
sc.pipeline().addLast(new FixedLengthFrameDecoder(5));
//设置字符串形式的解码
sc.pipeline().addLast(new StringDecoder());
sc.pipeline().addLast(new ServerHandler());
}
}); ChannelFuture cf = b.bind(8765).sync();
cf.channel().closeFuture().sync();
pGroup.shutdownGracefully();
cGroup.shutdownGracefully();
}
}
public class ServerHandler extends ChannelHandlerAdapter {

    @Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println(" server channel active... ");
} @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
String request = (String)msg;
System.out.println("Server :" + msg);
String response = request ;
ctx.writeAndFlush(Unpooled.copiedBuffer(response.getBytes()));
} @Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
} @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable t) throws Exception {
}
}
public class Client {

    public static void main(String[] args) throws Exception {

        EventLoopGroup group = new NioEventLoopGroup();

        Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel sc) throws Exception {
sc.pipeline().addLast(new FixedLengthFrameDecoder(5));
sc.pipeline().addLast(new StringDecoder());
sc.pipeline().addLast(new ClientHandler());
}
}); ChannelFuture cf = b.connect("127.0.0.1", 8765).sync(); cf.channel().writeAndFlush(Unpooled.wrappedBuffer("aaa".getBytes()));
cf.channel().writeAndFlush(Unpooled.copiedBuffer("bbccccc".getBytes())); cf.channel().closeFuture().sync();
group.shutdownGracefully();
}
}
public class ClientHandler extends ChannelHandlerAdapter {

    @Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("client channel active... ");
} @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
String response = (String) msg;
System.out.println("Client: " + response);
} @Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
} @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
}
}

1.3 编解码

即对象序列化技术, 目的是为了实现对象的网络传输和本地持久化
如果使用java的序列化, 码流较大. 因此多用Marshalling, Kyro(基于Protobuf)

下面的例子, 使用编解码传输javabean(Marshalling的javabean需要实现serializable), 并将message进行gzip压缩

自定义编解码器

public final class MarshallingCodeCFactory {
// 解码
public static MarshallingDecoder buildMarshallingDecoder() {
//创建工厂对象, 参数serial指创建的是java对象序列化的工厂对象
final MarshallerFactory marshallerFactory = Marshalling.getProvidedMarshallerFactory("serial");
//创建配置对象,版本号为5
final MarshallingConfiguration configuration = new MarshallingConfiguration();
configuration.setVersion(5);
//根据工厂对象和配置对象创建解码provider
UnmarshallerProvider provider = new DefaultUnmarshallerProvider(marshallerFactory, configuration);
//创建解码器对象. 第一个参数是provider, 第二个参数是单个消息序列化后的最大长度, 超过后拒绝处理
MarshallingDecoder decoder = new MarshallingDecoder(provider, 1024 * 1024 * 1);
return decoder;
} // 编码
public static MarshallingEncoder buildMarshallingEncoder() {
final MarshallerFactory marshallerFactory = Marshalling.getProvidedMarshallerFactory("serial");
final MarshallingConfiguration configuration = new MarshallingConfiguration();
configuration.setVersion(5);
MarshallerProvider provider = new DefaultMarshallerProvider(marshallerFactory, configuration);
//创建编码器对象. 用于将实现Serializable接口的JavaBean序列化为二进制数组
MarshallingEncoder encoder = new MarshallingEncoder(provider);
return encoder;
}
}

javabean

public class Request implements Serializable {  // 标记Serializable接口

    private String id ;
private String name ;
private String requestMessage ;
private byte[] attachment; public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getRequestMessage() {
return requestMessage;
}
public void setRequestMessage(String requestMessage) {
this.requestMessage = requestMessage;
}
public byte[] getAttachment() {
return attachment;
}
public void setAttachment(byte[] attachment) {
this.attachment = attachment;
}
}
public class Response implements Serializable { // 标记Serializable接口

    private String id;
private String name;
private String responseMessage; public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getResponseMessage() {
return responseMessage;
}
public void setResponseMessage(String responseMessage) {
this.responseMessage = responseMessage;
}
}

GZip压缩的Util

public class GzipUtils {

    public static byte[] gzip(byte[] data) throws Exception {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
GZIPOutputStream gzip = new GZIPOutputStream(bos);
gzip.write(data);
gzip.finish();
gzip.close();
byte[] ret = bos.toByteArray();
bos.close();
return ret;
} public static byte[] ungzip(byte[] data) throws Exception{
ByteArrayInputStream bis = new ByteArrayInputStream(data);
GZIPInputStream gzip = new GZIPInputStream(bis);
byte[] buf = new byte[1024];
int num = -1;
ByteArrayOutputStream bos = new ByteArrayOutputStream();
while((num = gzip.read(buf)) != -1 ){
bos.write(buf, 0, num);
}
gzip.close();
bis.close();
byte[] ret = bos.toByteArray();
bos.close();
return ret;
}
}

服务端与客户端

public class Server {
public static void main(String[] args) throws Exception{ EventLoopGroup pGroup = new NioEventLoopGroup();
EventLoopGroup cGroup = new NioEventLoopGroup(); ServerBootstrap b = new ServerBootstrap();
b.group(pGroup, cGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
//设置日志
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel sc) throws Exception {
// 添加编解码. 发送自定义的类型, 而Handler的方法接收的msg参数的实际类型也是相应的自定义类了
sc.pipeline().addLast(MarshallingCodeCFactory.buildMarshallingDecoder());
sc.pipeline().addLast(MarshallingCodeCFactory.buildMarshallingEncoder());
sc.pipeline().addLast(new ServerHandler());
}
}); ChannelFuture cf = b.bind(8765).sync();
cf.channel().closeFuture().sync();
pGroup.shutdownGracefully();
cGroup.shutdownGracefully();
}
public class ServerHandler extends ChannelHandlerAdapter{

    @Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
} @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
Request req = (Request)msg;
System.out.println("Server : " + req.getId() + ", " + req.getName() + ", " + req.getRequestMessage());
byte[] attachment = GzipUtils.ungzip(req.getAttachment()); String path = System.getProperty("user.dir") + File.separatorChar + "receive" + File.separatorChar + "001.jpg";
FileOutputStream fos = new FileOutputStream(path);
fos.write(attachment);
fos.close(); Response resp = new Response();
resp.setId(req.getId());
resp.setName("resp" + req.getId());
resp.setResponseMessage("响应内容" + req.getId());
ctx.writeAndFlush(resp);//.addListener(ChannelFutureListener.CLOSE);
} @Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
} @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
public class Client {
public static void main(String[] args) throws Exception{ EventLoopGroup group = new NioEventLoopGroup();
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel sc) throws Exception {
sc.pipeline().addLast(MarshallingCodeCFactory.buildMarshallingDecoder());
sc.pipeline().addLast(MarshallingCodeCFactory.buildMarshallingEncoder());
sc.pipeline().addLast(new ClientHandler());
}
}); ChannelFuture cf = b.connect("127.0.0.1", 8765).sync(); for(int i = 0; i < 5; i++){
Request req = new Request();
req.setId("" + i);
req.setName("req" + i);
req.setRequestMessage("数据信息" + i);
String path = System.getProperty("user.dir") + File.separatorChar + "sources" + File.separatorChar + "001.jpg";
File file = new File(path);
FileInputStream in = new FileInputStream(file);
byte[] data = new byte[in.available()];
in.read(data);
in.close();
req.setAttachment(GzipUtils.gzip(data)); //压缩
cf.channel().writeAndFlush(req);
} cf.channel().closeFuture().sync();
group.shutdownGracefully();
}
}
public class ClientHandler extends ChannelHandlerAdapter{

    @Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
} @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
try {
Response resp = (Response) msg;
System.out.println("Client : " + resp.getId() + ", " + resp.getName() + ", " + resp.getResponseMessage());
} finally {
ReferenceCountUtil.release(msg);
}
} @Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
} @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}

1.4 长连接/短连接

1.长连接, 一致保持着连接不主动中断, 实时性强
2.短连接. 数据放在缓存, 一次性批量提交所有数据, 服务端接收后即关闭连接
以上两种根据是否给ChannelHandlerContext添加ChannelFutureListener.ClOSE监听器实现

3.长连接, 一定时间不活跃则关闭连接. 给SocketChannel添加ReadTimeoutHandler实现. 实例如下:

public final class MarshallingCodeCFactory {
public static MarshallingDecoder buildMarshallingDecoder() {
final MarshallerFactory marshallerFactory = Marshalling.getProvidedMarshallerFactory("serial");
final MarshallingConfiguration configuration = new MarshallingConfiguration();
configuration.setVersion(5);
UnmarshallerProvider provider = new DefaultUnmarshallerProvider(marshallerFactory, configuration);
MarshallingDecoder decoder = new MarshallingDecoder(provider, 1024);
return decoder;
}
public static MarshallingEncoder buildMarshallingEncoder() {
final MarshallerFactory marshallerFactory = Marshalling.getProvidedMarshallerFactory("serial");
final MarshallingConfiguration configuration = new MarshallingConfiguration();
configuration.setVersion(5);
MarshallerProvider provider = new DefaultMarshallerProvider(marshallerFactory, configuration);
MarshallingEncoder encoder = new MarshallingEncoder(provider);
return encoder;
}
}
public class Request implements Serializable{
private String id ;
private String name ;
private String requestMessage ; public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getRequestMessage() {
return requestMessage;
}
public void setRequestMessage(String requestMessage) {
this.requestMessage = requestMessage;
}
}
public class Response implements Serializable{
private String id;
private String name;
private String responseMessage; public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getResponseMessage() {
return responseMessage;
}
public void setResponseMessage(String responseMessage) {
this.responseMessage = responseMessage;
}
}
public class Server {

    public static void main(String[] args) throws Exception{

        EventLoopGroup pGroup = new NioEventLoopGroup();
EventLoopGroup cGroup = new NioEventLoopGroup(); ServerBootstrap b = new ServerBootstrap();
b.group(pGroup, cGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
//设置日志
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel sc) throws Exception {
sc.pipeline().addLast(MarshallingCodeCFactory.buildMarshallingDecoder());
sc.pipeline().addLast(MarshallingCodeCFactory.buildMarshallingEncoder());
sc.pipeline().addLast(new ReadTimeoutHandler(5)); // 时限, 读客户端超时没数据则断开
sc.pipeline().addLast(new ServerHandler());
}
}); ChannelFuture cf = b.bind(8765).sync();
cf.channel().closeFuture().sync();
pGroup.shutdownGracefully();
cGroup.shutdownGracefully();
}
}
public class ServerHandler extends ChannelHandlerAdapter{

    @Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
} @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
Request request = (Request) msg;
System.out.println("Server : " + request.getId() + ", " + request.getName() + ", " + request.getRequestMessage());
Response response = new Response();
response.setId(request.getId());
response.setName("response" + request.getId());
response.setResponseMessage("响应内容" + request.getId());
ctx.writeAndFlush(response);
} @Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
} @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
public class Client {
private EventLoopGroup group;
private Bootstrap b;
private ChannelFuture cf ; // 单例
private static class SingletonHolder {
static final Client instance = new Client();
}
public static Client getInstance(){
return SingletonHolder.instance;
} private Client(){
group = new NioEventLoopGroup();
b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel sc) throws Exception {
sc.pipeline().addLast(MarshallingCodeCFactory.buildMarshallingDecoder());
sc.pipeline().addLast(MarshallingCodeCFactory.buildMarshallingEncoder());
//超时handler(当服务器端与客户端在指定时间以上没有任何进行通信,则会关闭通道)
sc.pipeline().addLast(new ReadTimeoutHandler(5)); // 时限5s, 读服务端超时没数据则断开
sc.pipeline().addLast(new ClientHandler());
}
});
} public void connect(){
try {
this.cf = b.connect("127.0.0.1", 8765).sync();
System.out.println("远程服务器已经连接, 可以进行数据交换");
} catch (Exception e) {
e.printStackTrace();
}
} public ChannelFuture getChannelFuture(){
if(this.cf == null) { //初次连接
this.connect();
}
if(!this.cf.channel().isActive()){ //重连
this.connect();
}
return this.cf;
} public static void main(String[] args) throws Exception{
final Client c = Client.getInstance(); ChannelFuture cf = c.getChannelFuture();
for(int i = 1; i <= 3; i++ ){
Request request = new Request();
request.setId("" + i);
request.setName("request" + i);
request.setRequestMessage("数据信息" + i);
cf.channel().writeAndFlush(request);
TimeUnit.SECONDS.sleep(4); //间隔4s发送一次数据
} cf.channel().closeFuture().sync(); //阻塞至超时关闭 // 这里用子线程重连并发送数据一次
new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("进入子线程重连一次");
ChannelFuture cf = c.getChannelFuture();
assert true == cf.channel().isActive(); //断言
//再次发送数据
Request request = new Request();
request.setId("" + 4);
request.setName("request" + 4);
request.setRequestMessage("数据信息" + 4);
cf.channel().writeAndFlush(request);
cf.channel().closeFuture().sync();
System.out.println("子线程完成");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start(); System.out.println("断开连接,主线程结束..");
} }
public class ClientHandler extends ChannelHandlerAdapter{

    @Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
} @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
try {
Response resp = (Response) msg;
System.out.println("Client : " + resp.getId() + ", " + resp.getName() + ", " + resp.getResponseMessage());
} finally {
ReferenceCountUtil.release(msg);
}
} @Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
} @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}

1.5 使用UDP (较少使用)

public class Server {
public void run(int port) throws Exception {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group).channel(NioDatagramChannel.class) // UDP: NioDatagramChannel
.option(ChannelOption.SO_BROADCAST, true) // 广播
.handler(new ServerHandler());
b.bind(port).sync().channel().closeFuture().await();
} finally {
group.shutdownGracefully();
}
} public static void main(String[] args) throws Exception {
new Server().run(8765);
}
}
public class ServerHandler extends SimpleChannelInboundHandler<DatagramPacket> {

    // 谚语列表
private static final String[] DICTIONARY = {
"只要功夫深,铁棒磨成针。",
"旧时王谢堂前燕,飞入寻常百姓家。",
"洛阳亲友如相问,一片冰心在玉壶。",
"一寸光阴一寸金,寸金难买寸光阴。",
"老骥伏枥,志在千里。烈士暮年,壮心不已!"
}; private String nextQuote() {
int quoteId = ThreadLocalRandom.current().nextInt(DICTIONARY.length);
return DICTIONARY[quoteId];
} @Override
public void messageReceived(ChannelHandlerContext ctx, DatagramPacket packet) throws Exception {
String req = packet.content().toString(CharsetUtil.UTF_8);
System.out.println(req);
if ("谚语字典查询?".equals(req)) {
ctx.writeAndFlush(new DatagramPacket(Unpooled.copiedBuffer("谚语查询结果: " + nextQuote(), CharsetUtil.UTF_8), packet.sender()));
}
} @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
ctx.close();
cause.printStackTrace();
}
}
public class Client {

    public void run(int port) throws Exception {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group).channel(NioDatagramChannel.class)
.option(ChannelOption.SO_BROADCAST, true)
.handler(new ClientHandler());
Channel ch = b.bind(0).sync().channel();
// 向网段内的所有机器广播UDP消息
ch.writeAndFlush(new DatagramPacket(Unpooled.copiedBuffer("谚语字典查询?", CharsetUtil.UTF_8), new InetSocketAddress("255.255.255.255", port))).sync();
if (!ch.closeFuture().await(15000)) {
System.out.println("查询超时!");
}
} finally {
group.shutdownGracefully();
}
} public static void main(String[] args) throws Exception {
new Client().run(8765);
}
}
public class ClientHandler extends SimpleChannelInboundHandler<DatagramPacket> {

    @Override
public void messageReceived(ChannelHandlerContext ctx, DatagramPacket msg) throws Exception {
String response = msg.content().toString(CharsetUtil.UTF_8);
if (response.startsWith("谚语查询结果: ")) {
System.out.println(response);
ctx.close();
}
} @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}

2. 心跳检测

集群中主服务器需要知道从服务器的状态
因此client每隔5~10秒给server发送心跳包

可通过netty与定时任务来实现

public final class MarshallingCodeCFactory {
public static MarshallingDecoder buildMarshallingDecoder() {
final MarshallerFactory marshallerFactory = Marshalling.getProvidedMarshallerFactory("serial");
final MarshallingConfiguration configuration = new MarshallingConfiguration();
configuration.setVersion(5);
UnmarshallerProvider provider = new DefaultUnmarshallerProvider(marshallerFactory, configuration);
MarshallingDecoder decoder = new MarshallingDecoder(provider, 1024 * 1024 * 1);
return decoder;
}
public static MarshallingEncoder buildMarshallingEncoder() {
final MarshallerFactory marshallerFactory = Marshalling.getProvidedMarshallerFactory("serial");
final MarshallingConfiguration configuration = new MarshallingConfiguration();
configuration.setVersion(5);
MarshallerProvider provider = new DefaultMarshallerProvider(marshallerFactory, configuration);
MarshallingEncoder encoder = new MarshallingEncoder(provider);
return encoder;
}
}
public class RequestInfo implements Serializable {
private String ip ;
private HashMap<String, Object> cpuPercMap ;
private HashMap<String, Object> memoryMap; public String getIp() {
return ip;
}
public void setIp(String ip) {
this.ip = ip;
}
public HashMap<String, Object> getCpuPercMap() {
return cpuPercMap;
}
public void setCpuPercMap(HashMap<String, Object> cpuPercMap) {
this.cpuPercMap = cpuPercMap;
}
public HashMap<String, Object> getMemoryMap() {
return memoryMap;
}
public void setMemoryMap(HashMap<String, Object> memoryMap) {
this.memoryMap = memoryMap;
}
}
public class Server {

    public static void main(String[] args) throws Exception{

        EventLoopGroup pGroup = new NioEventLoopGroup();
EventLoopGroup cGroup = new NioEventLoopGroup(); ServerBootstrap b = new ServerBootstrap();
b.group(pGroup, cGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
//设置日志
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel sc) throws Exception {
sc.pipeline().addLast(MarshallingCodeCFactory.buildMarshallingDecoder());
sc.pipeline().addLast(MarshallingCodeCFactory.buildMarshallingEncoder());
sc.pipeline().addLast(new ServerHeartBeatHandler());
}
}); ChannelFuture cf = b.bind(8765).sync();
cf.channel().closeFuture().sync();
pGroup.shutdownGracefully();
cGroup.shutdownGracefully();
}
}
public class ServerHeartBeatHandler extends ChannelHandlerAdapter {

    private static HashMap<String, String> AUTH_IP_MAP = new HashMap<String, String>();
private static final String SUCCESS_KEY = "auth_success_key"; static {
AUTH_IP_MAP.put("127.0.0.1", "1234");
} private boolean auth(ChannelHandlerContext ctx, Object msg){
String [] ret = ((String) msg).split(",");
String auth = AUTH_IP_MAP.get(ret[0]);
if(auth != null && auth.equals(ret[1])){
// 认证成功, 返回确认信息
ctx.writeAndFlush(SUCCESS_KEY);
return true;
} else {
ctx.writeAndFlush("auth failure !").addListener(ChannelFutureListener.CLOSE);
return false;
}
} @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if(msg instanceof String){
auth(ctx, msg);
} else if (msg instanceof RequestInfo) {
RequestInfo info = (RequestInfo) msg;
System.out.println("--------------------------------------------");
System.out.println("当前主机ip为: " + info.getIp());
System.out.println("当前主机cpu情况: ");
HashMap<String, Object> cpu = info.getCpuPercMap();
System.out.println("总使用率: " + cpu.get("combined"));
System.out.println("用户使用率: " + cpu.get("user"));
System.out.println("系统使用率: " + cpu.get("sys"));
System.out.println("等待率: " + cpu.get("wait"));
System.out.println("空闲率: " + cpu.get("idle")); System.out.println("当前主机memory情况: ");
HashMap<String, Object> memory = info.getMemoryMap();
System.out.println("内存总量: " + memory.get("total"));
System.out.println("当前内存使用量: " + memory.get("used"));
System.out.println("当前内存剩余量: " + memory.get("free"));
System.out.println("--------------------------------------------"); ctx.writeAndFlush("info received!");
} else {
ctx.writeAndFlush("connect failure!").addListener(ChannelFutureListener.CLOSE);
}
}
}
public class Client {
public static void main(String[] args) throws Exception {
EventLoopGroup group = new NioEventLoopGroup();
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel sc) throws Exception {
sc.pipeline().addLast(MarshallingCodeCFactory.buildMarshallingDecoder());
sc.pipeline().addLast(MarshallingCodeCFactory.buildMarshallingEncoder());
sc.pipeline().addLast(new ClienHeartBeatHandler());
}
}); ChannelFuture cf = b.connect("127.0.0.1", 8765).sync();
cf.channel().closeFuture().sync();
group.shutdownGracefully();
}
}
public class ClienHeartBeatHandler extends ChannelHandlerAdapter {

    private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
private ScheduledFuture<?> heartBeat; //定时任务
//主动向服务器发送认证信息
private InetAddress addr ;
private static final String SUCCESS_KEY = "auth_success_key"; @Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
addr = InetAddress.getLocalHost();
//String ip = addr.getHostAddress();
String ip = "127.0.0.1";
String key = "1234";
//证书
String auth = ip + "," + key;
// 发送认证
ctx.writeAndFlush(auth);
} @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
try {
if(msg instanceof String){
String ret = (String) msg;
if(SUCCESS_KEY.equals(ret)){
// 收到认证 确认信息,设置每隔5秒发送心跳消息
this.heartBeat = this.scheduler.scheduleWithFixedDelay(new HeartBeatTask(ctx), 0, 5, TimeUnit.SECONDS);
System.out.println(msg);
} else {
// 收到心跳包 确认信息
System.out.println(msg);
}
}
} finally {
// 只读, 需要手动释放引用计数
ReferenceCountUtil.release(msg);
}
} private class HeartBeatTask implements Runnable {
private final ChannelHandlerContext ctx;
public HeartBeatTask(final ChannelHandlerContext ctx) {
this.ctx = ctx;
}
@Override
public void run() {
try {
RequestInfo info = new RequestInfo();
//ip
info.setIp(addr.getHostAddress());
Sigar sigar = new Sigar();
//cpu prec
CpuPerc cpuPerc = sigar.getCpuPerc();
HashMap<String, Object> cpuPercMap = new HashMap<String, Object>();
cpuPercMap.put("combined", cpuPerc.getCombined());
cpuPercMap.put("user", cpuPerc.getUser());
cpuPercMap.put("sys", cpuPerc.getSys());
cpuPercMap.put("wait", cpuPerc.getWait());
cpuPercMap.put("idle", cpuPerc.getIdle());
// memory
Mem mem = sigar.getMem();
HashMap<String, Object> memoryMap = new HashMap<String, Object>();
memoryMap.put("total", mem.getTotal() / 1024L);
memoryMap.put("used", mem.getUsed() / 1024L);
memoryMap.put("free", mem.getFree() / 1024L);
info.setCpuPercMap(cpuPercMap);
info.setMemoryMap(memoryMap); ctx.writeAndFlush(info);
} catch (Exception e) {
e.printStackTrace();
}
} public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
// 取消定时发送心跳包的任务
if (heartBeat != null) {
heartBeat.cancel(true);
heartBeat = null;
}
ctx.fireExceptionCaught(cause);
}
}
}

3. HTTP

3.1 Hello World

public final class HttpHelloWorldServer {

      static final boolean SSL = System.getProperty("ssl") != null;
static final int PORT = Integer.parseInt(System.getProperty("port", SSL? "8443" : "8080")); public static void main(String[] args) throws Exception {
final SslContext sslCtx;
if (SSL) {
SelfSignedCertificate ssc = new SelfSignedCertificate();
sslCtx = SslContext.newServerContext(ssc.certificate(), ssc.privateKey());
} else {
sslCtx = null;
} EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.option(ChannelOption.SO_BACKLOG, 1024);
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new HttpHelloWorldServerInitializer(sslCtx)); Channel ch = b.bind(PORT).sync().channel(); System.err.println("Open your web browser and navigate to " +
(SSL? "https" : "http") + "://127.0.0.1:" + PORT + '/'); ch.closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
public class HttpHelloWorldServerInitializer extends ChannelInitializer<SocketChannel> {

    private final SslContext sslCtx;

    public HttpHelloWorldServerInitializer(SslContext sslCtx) {
this.sslCtx = sslCtx;
} @Override
public void initChannel(SocketChannel ch) {
ChannelPipeline p = ch.pipeline();
if (sslCtx != null) {
p.addLast(sslCtx.newHandler(ch.alloc()));
}
p.addLast(new HttpServerCodec()); // !使用http通信, HttpRequest和HttpResponse
p.addLast(new HttpHelloWorldServerHandler());
}
}
public class HttpHelloWorldServerHandler extends ChannelHandlerAdapter {
private static final byte[] CONTENT = "HELLO WORLD".getBytes(); @Override
public void channelReadComplete(ChannelHandlerContext ctx) {
ctx.flush();
} @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
if (msg instanceof HttpRequest) {
HttpRequest req = (HttpRequest) msg; if (HttpHeaderUtil.is100ContinueExpected(req)) {
ctx.write(new DefaultFullHttpResponse(HTTP_1_1, CONTINUE));
}
boolean keepAlive = HttpHeaderUtil.isKeepAlive(req);
// 构造响应
FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK, Unpooled.wrappedBuffer(CONTENT));
response.headers().set(CONTENT_TYPE, "text/plain;charset=UTF-8");
response.headers().setInt(CONTENT_LENGTH, response.content().readableBytes()); if (!keepAlive) {
// Request短连接, 写完后直接关闭
ctx.write(response).addListener(ChannelFutureListener.CLOSE);
} else {
// 长连接, response也设置为KEEP_ALIVE
response.headers().set(CONNECTION, HttpHeaderValues.KEEP_ALIVE);
ctx.write(response);
}
}
} @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}

3.2 HTTP下载文件

public class HttpDownloadServer {

    private static final String DEFAULT_URL = "/sources/";

    public void run(final int port, final String url) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// addLast的第一项为key, 自定义的
// request解码器
ch.pipeline().addLast("http-decoder", new HttpRequestDecoder());
// response的编码器
ch.pipeline().addLast("http-encoder", new HttpResponseEncoder());
// chunked, 传输文件时分多个response分解地传输文件
ch.pipeline().addLast("http-chunked", new ChunkedWriteHandler());
// ObjectAggregator, 将多个response合并为一个FullHttpResponse
ch.pipeline().addLast("http-aggregator", new HttpObjectAggregator(65536));
// 自定义业务逻辑handler
ch.pipeline().addLast("fileServerHandler", new HttpDownoadServerHandler(url));
}
});
ChannelFuture future = b.bind("127.0.0.1", port).sync();
System.out.println("HTTP文件目录服务器启动,网址是 : " + "http://localhost:" + port + url);
future.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
} public static void main(String[] args) throws Exception {
int port = 8765;
new HttpDownloadServer().run(port, DEFAULT_URL);
}
}
// 注意这里继承了SimpleChannelInboundHandler<T>, 含泛型, 即指定了传入参数msg的类型
public class HttpDownoadServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> { private final String url; public HttpDownoadServerHandler(String url) {
this.url = url;
} @Override
public void messageReceived(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
//是否能理解(解码)请求
if (!request.decoderResult().isSuccess()) {
// 400
sendError(ctx, BAD_REQUEST);
return;
}
//对请求的方法进行判断:如果不是GET方法则返回异常
if (request.method() != GET) {
// 405
sendError(ctx, METHOD_NOT_ALLOWED);
return;
}
//获取请求uri路径
final String uri = request.uri();
//对url进行分析,返回本地路径
final String path = parseURI(uri);
//如果 路径构造不合法,则path为null
if (path == null) {
//403
sendError(ctx, FORBIDDEN);
return;
} // 创建file对象
File file = new File(path);
// 文件隐藏或不存在
if (file.isHidden() || !file.exists()) {
// 404
sendError(ctx, NOT_FOUND);
return;
}
// 是文件夹
if (file.isDirectory()) {
if (uri.endsWith("/")) {
//如果以正常"/"结束 说明是访问的一个文件目录:则进行展示文件列表
sendListing(ctx, file);
} else {
//如果非"/"结束 则重定向,让客户端补全"/"并再次请求
sendRedirect(ctx, uri + '/');
}
return;
}
// 如果所创建的file对象不是文件类型
if (!file.isFile()) {
// 403
sendError(ctx, FORBIDDEN);
return;
} //随机文件读写对象
RandomAccessFile randomAccessFile = null;
try {
randomAccessFile = new RandomAccessFile(file, "r");// 以只读的方式打开文件
} catch (FileNotFoundException fnfe) {
// 404
sendError(ctx, NOT_FOUND);
return;
} //获取文件长度
long fileLength = randomAccessFile.length();
//建立响应对象
HttpResponse response = new DefaultHttpResponse(HTTP_1_1, OK);
//设置响应信息
HttpHeaderUtil.setContentLength(response, fileLength);
//设置Content-Type
setContentTypeHeader(response, file);
//设置为KeepAlive
if (HttpHeaderUtil.isKeepAlive(request)) {
response.headers().set(CONNECTION, HttpHeaderValues.KEEP_ALIVE);
}
//输出response header, HttpObjectAggregator能将其与下面输出整合合并
ctx.write(response); //写出ChunkedFile. 创建ChunkedFile需要使用RandomAccessFile并设置分段. 这里每次传输8192个字节
ChannelFuture sendFileFuture = ctx.write(new ChunkedFile(randomAccessFile, 0, fileLength, 8192), ctx.newProgressivePromise());
//添加传输监听
sendFileFuture.addListener(new ChannelProgressiveFutureListener() {
@Override
public void operationProgressed(ChannelProgressiveFuture future, long progress, long total) {
if (total < 0) {
System.err.println("Transfer progress: " + progress);
} else {
System.err.println("Transfer progress: " + progress + " / " + total);
}
}
@Override
public void operationComplete(ChannelProgressiveFuture future) throws Exception {
System.out.println("Transfer complete.");
}
}); //使用Chunked, 完成时需要发送标记结束的空消息体!
ChannelFuture lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
//如果当前连接请求非Keep-Alive, 最后一包消息发送完后, 服务器主动关闭连接
if (!HttpHeaderUtil.isKeepAlive(request)) {
lastContentFuture.addListener(ChannelFutureListener.CLOSE);
}
} @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
if (ctx.channel().isActive()) {
// 500
sendError(ctx, INTERNAL_SERVER_ERROR);
ctx.close();
}
} //判断非法URI的正则
private static final Pattern INSECURE_URI = Pattern.compile(".*[<>&\"].*");
private String parseURI(String uri) {
try {
//使用UTF-8字符集
uri = URLDecoder.decode(uri, "UTF-8");
} catch (UnsupportedEncodingException e) {
try {
//尝试ISO-8859-1
uri = URLDecoder.decode(uri, "ISO-8859-1");
} catch (UnsupportedEncodingException e1) {
//抛出预想外异常信息
throw new Error();
}
}
// 对uri进行细粒度判断:4步验证操作
// step 1 基础验证
if (!uri.startsWith(url)) {
return null;
}
// step 2 基础验证
if (!uri.startsWith("/")) {
return null;
}
// step 3 将文件分隔符替换为本地操作系统的文件路径分隔符
uri = uri.replace('/', File.separatorChar);
// step 4 验证路径合法性
if (uri.contains(File.separator + '.') || uri.contains('.' + File.separator) ||
uri.startsWith(".") || uri.endsWith(".") ||
INSECURE_URI.matcher(uri).matches()) {
return null;
}
//利用当前工程所在目录 + URI相对路径 构造绝对路径
return System.getProperty("user.dir") + File.separator + uri;
} //用正则表达式过滤文件名
private static final Pattern ALLOWED_FILE_NAME = Pattern.compile("[A-Za-z0-9][-_A-Za-z0-9\\.]*");
//文件列表, 拼html文件
private static void sendListing(ChannelHandlerContext ctx, File dir) {
// 设置响应对象
FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK);
// 响应头
response.headers().set(CONTENT_TYPE, "text/html; charset=UTF-8");
// 构造文本内容
StringBuilder ret = new StringBuilder();
String dirPath = dir.getPath();
ret.append("<!DOCTYPE html>\r\n");
ret.append("<html><head><title>");
ret.append(dirPath);
ret.append(" 目录:");
ret.append("</title></head><body>\r\n");
ret.append("<h3>");
ret.append(dirPath).append(" 目录:");
ret.append("</h3>\r\n");
ret.append("<ul>");
ret.append("<li>链接:<a href=\"../\">..</a></li>\r\n"); // 遍历文件, 生成超链接
for (File f : dir.listFiles()) {
//step 1: 跳过隐藏文件和不可读文件
if (f.isHidden() || !f.canRead()) {
continue;
}
String name = f.getName();
//step 2: 跳过正则过滤的文件名
if (!ALLOWED_FILE_NAME.matcher(name).matches()) {
continue;
}
ret.append("<li>链接:<a href=\"");
ret.append(name);
ret.append("\">");
ret.append(name);
ret.append("</a></li>\r\n");
}
ret.append("</ul></body></html>\r\n");
//构造ByteBuf,写入缓冲区
ByteBuf buffer = Unpooled.copiedBuffer(ret, CharsetUtil.UTF_8);
//进行写出操作
response.content().writeBytes(buffer);
//重置ByteBuf
buffer.release();
//发送完成并主动关闭连接
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
} //重定向操作
private static void sendRedirect(ChannelHandlerContext ctx, String newUri) {
FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, FOUND);
response.headers().set(LOCATION, newUri);
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
} //错误信息
private static void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) {
FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, status, Unpooled.copiedBuffer("Failure: " + status.toString()+ "\r\n", CharsetUtil.UTF_8));
response.headers().set(CONTENT_TYPE, "text/plain; charset=UTF-8");
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
} private static void setContentTypeHeader(HttpResponse response, File file) {
//使用mime对象获取文件对应的Content-Type
MimetypesFileTypeMap mimeTypesMap = new MimetypesFileTypeMap();
response.headers().set(CONTENT_TYPE, mimeTypesMap.getContentType(file.getPath()));
}
}

3.3 HTTP上传文件 (较少使用)

实际应用中文件上传服务端有成熟的框架fastDFS(小文件)和HDFS(大文件)

如要实现断点续传, 需要记录上传进度. 参考HTTP头的Range和Content-Range

public final class HttpUploadServer {

    static final boolean SSL = System.getProperty("ssl") != null;
static final int PORT = Integer.parseInt(System.getProperty("port", SSL? "8443" : "8080")); public static void main(String[] args) throws Exception {
// Configure SSL.
final SslContext sslCtx;
if (SSL) {
SelfSignedCertificate ssc = new SelfSignedCertificate();
sslCtx = SslContext.newServerContext(ssc.certificate(), ssc.privateKey());
} else {
sslCtx = null;
} EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup);
b.channel(NioServerSocketChannel.class);
b.handler(new LoggingHandler(LogLevel.INFO));
b.childHandler(new HttpUploadServerInitializer(sslCtx)); Channel ch = b.bind(PORT).sync().channel();
System.err.println("Open your web browser and navigate to " + (SSL ? "https" : "http") + "://127.0.0.1:" + PORT + '/'); ch.closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
public class HttpUploadServerInitializer extends ChannelInitializer<SocketChannel> {

    private final SslContext sslCtx;

    public HttpUploadServerInitializer(SslContext sslCtx) {
this.sslCtx = sslCtx;
} @Override
public void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
if (sslCtx != null) {
pipeline.addLast(sslCtx.newHandler(ch.alloc()));
}
pipeline.addLast(new HttpRequestDecoder());
pipeline.addLast(new HttpResponseEncoder());
// 压缩
pipeline.addLast(new HttpContentCompressor());
pipeline.addLast(new HttpUploadServerHandler());
}
}
public class HttpUploadServerHandler extends SimpleChannelInboundHandler<HttpObject> {

    private static final Logger logger = Logger.getLogger(HttpUploadServerHandler.class.getName());

    private HttpRequest request;

    private boolean readingChunks;

    private final StringBuilder responseContent = new StringBuilder();

    private static final HttpDataFactory factory = new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE); // 大小超过minsize放磁盘上

    private HttpPostRequestDecoder decoder;

    static {
DiskFileUpload.deleteOnExitTemporaryFile = true; //退出时是否删除临时文件
DiskFileUpload.baseDirectory = "D:" + File.separatorChar + "aa"; //文件存储路径 DiskAttribute.deleteOnExitTemporaryFile = true; //退出时是否删除临时文件
DiskAttribute.baseDirectory = "D:" + File.separatorChar + "aa"; //文件存储路径
} @Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
if (decoder != null) {
decoder.cleanFiles();
}
} @Override
public void messageReceived(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
if (msg instanceof HttpRequest) { // HttpRequest传输头
HttpRequest request = this.request = (HttpRequest) msg;
URI uri = new URI(request.uri());
if (!uri.getPath().startsWith("/form")) {
// 返回上传菜单
writeMenu(ctx);
return;
}
// 拼接反馈内容
responseContent.setLength(0);
responseContent.append("WELCOME TO THE WILD WILD WEB SERVER\r\n");
responseContent.append("===================================\r\n");
responseContent.append("VERSION: " + request.protocolVersion().text() + "\r\n");
responseContent.append("REQUEST_URI: " + request.uri() + "\r\n\r\n");
responseContent.append("\r\n\r\n"); for (Entry<CharSequence, CharSequence> entry : request.headers()) {
responseContent.append("HEADER: " + entry.getKey() + '=' + entry.getValue() + "\r\n");
}
responseContent.append("\r\n\r\n"); Set<Cookie> cookies = null;
String value = request.headers().getAndConvert(HttpHeaderNames.COOKIE);
if (value == null) {
cookies = Collections.emptySet();
} else {
cookies = ServerCookieDecoder.decode(value);
}
for (Cookie cookie : cookies) {
responseContent.append("COOKIE: " + cookie + "\r\n");
}
responseContent.append("\r\n\r\n"); QueryStringDecoder decoderQuery = new QueryStringDecoder(request.uri());
Map<String, List<String>> uriAttributes = decoderQuery.parameters();
for (Entry<String, List<String>> attr: uriAttributes.entrySet()) {
for (String attrVal: attr.getValue()) {
responseContent.append("URI: " + attr.getKey() + '=' + attrVal + "\r\n");
}
}
responseContent.append("\r\n\r\n"); // GET方法, 就此return
if (request.method().equals(HttpMethod.GET)) {
responseContent.append("\r\n\r\nEND OF GET CONTENT\r\n");
return;
} // POST方法
try {
decoder = new HttpPostRequestDecoder(factory, request);
} catch (ErrorDataDecoderException e1) {
e1.printStackTrace();
responseContent.append(e1.getMessage());
writeResponse(ctx.channel());
ctx.channel().close();
return;
} readingChunks = HttpHeaderUtil.isTransferEncodingChunked(request);
responseContent.append("Is Chunked: " + readingChunks + "\r\n");
responseContent.append("IsMultipart: " + decoder.isMultipart() + "\r\n");
if (readingChunks) {
responseContent.append("Chunks: ");
}
} if (decoder != null) {
if (msg instanceof HttpContent) { //HttpContent具体传输的内容
// 读取到一个chunk
HttpContent chunk = (HttpContent) msg;
try {
decoder.offer(chunk);
} catch (ErrorDataDecoderException e1) {
e1.printStackTrace();
responseContent.append(e1.getMessage());
writeResponse(ctx.channel());
ctx.channel().close();
return;
}
responseContent.append('o'); //每读一个chunk标记一个'o'
readHttpDataChunkByChunk();
// 最后一块chunk
if (chunk instanceof LastHttpContent) {
writeResponse(ctx.channel());
readingChunks = false;
reset();
}
}
} else {
writeResponse(ctx.channel());
}
} private void reset() {
request = null;
decoder.destroy(); //释放资源
decoder = null;
} private void readHttpDataChunkByChunk() throws Exception {
try {
while (decoder.hasNext()) {
InterfaceHttpData data = decoder.next();
if (data != null) {
try {
writeHttpData(data);
} finally {
data.release();
}
}
}
} catch (EndOfDataDecoderException e1) {
responseContent.append("\r\n\r\nEND OF CONTENT CHUNK BY CHUNK\r\n\r\n");
}
} private void writeHttpData(InterfaceHttpData data) throws Exception {
if (data.getHttpDataType() == HttpDataType.Attribute) {
Attribute attribute = (Attribute) data;
String value = null;
try {
value = attribute.getValue();
} catch (IOException e1) {
e1.printStackTrace();
responseContent.append("\r\nBODY Attribute: " + attribute.getHttpDataType().name() + ": " + attribute.getName() + " Error while reading value: " + e1.getMessage() + "\r\n");
return;
}
if (value.length() > 100) {
responseContent.append("\r\nBODY Attribute: " + attribute.getHttpDataType().name() + ": " + attribute.getName() + " data too long\r\n");
} else {
responseContent.append("\r\nBODY Attribute: " + attribute.getHttpDataType().name() + ": " + attribute + "\r\n");
}
} else {
responseContent.append("\r\n -----------start-------------" + "\r\n");
responseContent.append("\r\nBODY FileUpload: " + data.getHttpDataType().name() + ": " + data + "\r\n");
responseContent.append("\r\n ------------end------------" + "\r\n");
if (data.getHttpDataType() == HttpDataType.FileUpload) {
FileUpload fileUpload = (FileUpload) data;
if (fileUpload.isCompleted()) {
System.out.println("file name : " + fileUpload.getFilename());
System.out.println("file length: " + fileUpload.length());
System.out.println("file maxSize : " + fileUpload.getMaxSize());
System.out.println("file path :" + fileUpload.getFile().getPath());
System.out.println("file absolutepath :" + fileUpload.getFile().getAbsolutePath());
System.out.println("parent path :" + fileUpload.getFile().getParentFile()); if (fileUpload.length() < 1024 * 1024 * 10) {
responseContent.append("\tContent of file\r\n");
try {
responseContent.append(fileUpload.getString(fileUpload.getCharset()));
} catch (Exception e1) {
e1.printStackTrace();
}
responseContent.append("\r\n");
} else {
responseContent.append("\tFile too long to be printed out:" + fileUpload.length() + "\r\n");
}
fileUpload.renameTo(new File(fileUpload.getFile().getPath())); // 核心操作, 写文件
decoder.removeHttpDataFromClean(fileUpload);
} else {
responseContent.append("\tFile to be continued but should not!\r\n");
}
}
}
} private void writeResponse(Channel channel) {
ByteBuf buf = copiedBuffer(responseContent.toString(), CharsetUtil.UTF_8);
responseContent.setLength(0); // 是否是短连接
boolean close = request.headers().contains(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE, true)
|| request.protocolVersion().equals(HttpVersion.HTTP_1_0)
&& !request.headers().contains(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE, true); FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, buf);
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8");
// 最后一次连接不需要Content-Length
if (!close) {
response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, buf.readableBytes());
} Set<Cookie> cookies = null;
String value = request.headers().getAndConvert(HttpHeaderNames.COOKIE);
if (value == null) {
cookies = Collections.emptySet();
} else {
cookies = ServerCookieDecoder.decode(value);
}
if (!cookies.isEmpty()) {
for (Cookie cookie : cookies) {
response.headers().add(HttpHeaderNames.SET_COOKIE, ServerCookieEncoder.encode(cookie));
}
}
ChannelFuture future = channel.writeAndFlush(response);
if (close) {
future.addListener(ChannelFutureListener.CLOSE);
}
} //拼接上传页html菜单
private void writeMenu(ChannelHandlerContext ctx) {
responseContent.setLength(0); // create Pseudo Menu
responseContent.append("<html>");
responseContent.append("<head>");
responseContent.append("<title>Netty Test Form</title>\r\n");
responseContent.append("</head>\r\n");
responseContent.append("<body bgcolor=white><style>td{font-size: 12pt;}</style>"); responseContent.append("<table border=\"0\">");
responseContent.append("<tr>");
responseContent.append("<td>");
responseContent.append("<h1>Netty Test Form</h1>");
responseContent.append("Choose one FORM");
responseContent.append("</td>");
responseContent.append("</tr>");
responseContent.append("</table>\r\n"); // GET
responseContent.append("<CENTER>GET FORM<HR WIDTH=\"75%\" NOSHADE color=\"blue\"></CENTER>");
responseContent.append("<FORM ACTION=\"/formget\" METHOD=\"GET\">");
responseContent.append("<input type=hidden name=getform value=\"GET\">");
responseContent.append("<table border=\"0\">");
responseContent.append("<tr><td>Fill with value: <br> <input type=text name=\"info\" size=10></td></tr>");
responseContent.append("<tr><td>Fill with value: <br> <input type=text name=\"secondinfo\" size=20>");
responseContent.append("<tr><td>Fill with value: <br> <textarea name=\"thirdinfo\" cols=40 rows=10></textarea>");
responseContent.append("</td></tr>");
responseContent.append("<tr><td><INPUT TYPE=\"submit\" NAME=\"Send\" VALUE=\"Send\"></INPUT></td>");
responseContent.append("<td><INPUT TYPE=\"reset\" NAME=\"Clear\" VALUE=\"Clear\" ></INPUT></td></tr>");
responseContent.append("</table></FORM>\r\n");
responseContent.append("<CENTER><HR WIDTH=\"75%\" NOSHADE color=\"blue\"></CENTER>"); // POST
responseContent.append("<CENTER>POST FORM<HR WIDTH=\"75%\" NOSHADE color=\"blue\"></CENTER>");
responseContent.append("<FORM ACTION=\"/formpost\" METHOD=\"POST\">");
responseContent.append("<input type=hidden name=getform value=\"POST\">");
responseContent.append("<table border=\"0\">");
responseContent.append("<tr><td>Fill with value: <br> <input type=text name=\"info\" size=10></td></tr>");
responseContent.append("<tr><td>Fill with value: <br> <input type=text name=\"secondinfo\" size=20>");
responseContent.append("<tr><td>Fill with value: <br> <textarea name=\"thirdinfo\" cols=40 rows=10></textarea>");
responseContent.append("<tr><td>Fill with file (only file name will be transmitted): <br> <input type=file name=\"myfile\">");
responseContent.append("</td></tr>");
responseContent.append("<tr><td><INPUT TYPE=\"submit\" NAME=\"Send\" VALUE=\"Send\"></INPUT></td>");
responseContent.append("<td><INPUT TYPE=\"reset\" NAME=\"Clear\" VALUE=\"Clear\" ></INPUT></td></tr>");
responseContent.append("</table></FORM>\r\n");
responseContent.append("<CENTER><HR WIDTH=\"75%\" NOSHADE color=\"blue\"></CENTER>"); // POST with enctype="multipart/form-data"
responseContent.append("<CENTER>POST MULTIPART FORM<HR WIDTH=\"75%\" NOSHADE color=\"blue\"></CENTER>");
responseContent.append("<FORM ACTION=\"/formpostmultipart\" ENCTYPE=\"multipart/form-data\" METHOD=\"POST\">");
responseContent.append("<input type=hidden name=getform value=\"POST\">");
responseContent.append("<table border=\"0\">");
responseContent.append("<tr><td>Fill with value: <br> <input type=text name=\"info\" size=10></td></tr>");
responseContent.append("<tr><td>Fill with value: <br> <input type=text name=\"secondinfo\" size=20>");
responseContent.append("<tr><td>Fill with value: <br> <textarea name=\"thirdinfo\" cols=40 rows=10></textarea>");
responseContent.append("<tr><td>Fill with file: <br> <input type=file name=\"myfile\">");
responseContent.append("</td></tr>");
responseContent.append("<tr><td><INPUT TYPE=\"submit\" NAME=\"Send\" VALUE=\"Send\"></INPUT></td>");
responseContent.append("<td><INPUT TYPE=\"reset\" NAME=\"Clear\" VALUE=\"Clear\" ></INPUT></td></tr>");
responseContent.append("</table></FORM>\r\n");
responseContent.append("<CENTER><HR WIDTH=\"75%\" NOSHADE color=\"blue\"></CENTER>"); responseContent.append("</body>");
responseContent.append("</html>"); ByteBuf buf = copiedBuffer(responseContent.toString(), CharsetUtil.UTF_8);
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, buf);
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8");
response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, buf.readableBytes()); ctx.channel().writeAndFlush(response);
} @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
logger.log(Level.WARNING, responseContent.toString(), cause);
ctx.channel().close();
}
}

3.4 WebSocket(较少使用)

public class WebSocketServer {
public void run(int port) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("http-codec", new HttpServerCodec());
pipeline.addLast("aggregator", new HttpObjectAggregator(65536));
ch.pipeline().addLast("http-chunked", new ChunkedWriteHandler());
pipeline.addLast("handler", new WebSocketServerHandler());
}
}); Channel ch = b.bind(port).sync().channel();
System.out.println("Web socket server started at port " + port + '.');
System.out.println("Open your browser and navigate to http://localhost:" + port + '/'); ch.closeFuture().sync();
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
} public static void main(String[] args) throws Exception {
new WebSocketServer().run(8765);
}
}
public class WebSocketServerHandler extends SimpleChannelInboundHandler<Object> {
private static final Logger logger = Logger.getLogger(WebSocketServerHandler.class.getName()); private WebSocketServerHandshaker handshaker; @Override
public void messageReceived(ChannelHandlerContext ctx, Object msg) throws Exception {
// 传统的HTTP接入
if (msg instanceof FullHttpRequest) {
handleHttpRequest(ctx, (FullHttpRequest) msg);
}
// WebSocket接入
else if (msg instanceof WebSocketFrame) {
handleWebSocketFrame(ctx, (WebSocketFrame) msg);
}
} @Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
} private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest req) throws Exception {
// 如果HTTP解码失败,返回HTTP异常
if (!req.decoderResult().isSuccess() || (!"websocket".equals(req.headers().get("Upgrade")))) {
sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1, BAD_REQUEST));
return;
} // 构造握手响应返回,本机测试
WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory("ws://localhost:8080/websocket", null, false);
handshaker = wsFactory.newHandshaker(req); if (handshaker == null) {
WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
} else {
handshaker.handshake(ctx.channel(), req);
}
} private void handleWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) { // 判断是否是关闭链路的指令
if (frame instanceof CloseWebSocketFrame) {
handshaker.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)) {
throw new UnsupportedOperationException(String.format("%s frame types not supported", frame.getClass().getName()));
} // 返回应答消息
String request = ((TextWebSocketFrame) frame).text();
if (logger.isLoggable(Level.FINE)) {
logger.fine(String.format("%s received %s", ctx.channel(), request));
}
ctx.channel().write(
new TextWebSocketFrame(request + " , 欢迎使用Netty WebSocket服务,现在时刻:" + new java.util.Date().toString()));
} private static void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest req, FullHttpResponse res) {
// 返回应答给客户端
if (res.status().code() != 200) {
ByteBuf buf = Unpooled.copiedBuffer(res.status().toString(), CharsetUtil.UTF_8);
res.content().writeBytes(buf);
buf.release();
HttpHeaderUtil.setContentLength(res, res.content().readableBytes());
} // 如果是非Keep-Alive,关闭连接
ChannelFuture f = ctx.channel().writeAndFlush(res);
if (!HttpHeaderUtil.isKeepAlive(req) || res.status().code() != 200) {
f.addListener(ChannelFutureListener.CLOSE);
}
} @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
cause.printStackTrace();
ctx.close();
}
}

客户端是网页

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
Netty WebSocket 时间服务器
</head>
<br>
<body>
<br>
<script type="text/javascript">
var socket;
if (!window.WebSocket) {
window.WebSocket = window.MozWebSocket;
}
if (window.WebSocket) {
socket = new WebSocket("ws://localhost:8765/websocket");
socket.onmessage = function(event) {
var ta = document.getElementById('responseText');
ta.value = "";
ta.value = event.data
};
socket.onopen = function(event) {
var ta = document.getElementById('responseText');
ta.value = "打开WebSocket服务正常,浏览器支持WebSocket!";
};
socket.onclose = function(event) {
var ta = document.getElementById('responseText');
ta.value = "";
ta.value = "WebSocket 关闭!";
};
} else {
alert("抱歉,您的浏览器不支持WebSocket协议!");
} function send(message) {
if (!window.WebSocket) {
return;
}
if (socket.readyState == WebSocket.OPEN) {
socket.send(message);
} else {
alert("WebSocket连接没有建立成功!");
}
}
</script>
<form onsubmit="return false;">
<input type="text" name="message" value="Netty最佳实践" /> <br>
<br> <input type="button" value="发送WebSocket请求消息"
onclick="send(this.form.message.value)" />
<hr color="blue" />
<h3>服务端返回的应答消息</h3>
<textarea id="responseText" style="width: 500px; height: 300px;"></textarea>
</form>
</body>
</html>

最新文章

  1. EF Code First学习系列
  2. platanus
  3. Linux系统时间与RTC时间【转】
  4. LinGo:投资问题——线性规划
  5. no device found for connection ‘ System eth0′问题
  6. java I/O框架 (一)总览
  7. [Swift]LeetCode155. 最小栈 | Min Stack
  8. [pycharm]远程调试服务器项目
  9. while true 死循环判断端口按顺序启动应用
  10. 远程链接 aws Windows Server 2016 Base Nano
  11. C# .NET 配置404,500等错误信息
  12. uni-app - 支付(app支付、小程序支付、h5(微信端)支付)
  13. WdatePicker设置日期范围
  14. linux内核启动时报错ubi0 error: validate_ec_hdr: bad data offset 256, expected 128
  15. Android-原生对话框
  16. git登陆迁移 SourceTree 不能自动识别
  17. 智课雅思词汇---二十三、名词性后缀mony
  18. TOJ 1721 Partial Sums
  19. 洛谷 P3383 【模板】线性筛素数-线性筛素数(欧拉筛素数)O(n)基础题贴个板子备忘
  20. mysql查询哪张表数据最大

热门文章

  1. 如何在Ubuntu 18.04上安装Go
  2. PSP DAILY软件功能说明书
  3. [转]精选!15个必备的VSCode插件
  4. a6
  5. php伪静态配置
  6. 超强汇总!110 道 Python 面试笔试题
  7. Struts2拦截器配置和使用
  8. 【Quartz.NET】Quartz.NET 入门
  9. EM算法【转】
  10. 51nod-1227-平均最小公倍数