废话不多说(netty权威指南还没看完不敢多说)。直接开整!
先介绍基本的几个概念
一LINUX网络I/O模型(所有都可以看成对一个文件的操作,共有5种)
1阻塞I/O模型:直到数据包发送到达并被进程缓冲区复制或错误才返回,一直处于阻塞等待状态
2非阻塞I/O模型:recvfrom从应用层到内核的时候,如果缓冲区没有数据,直接返回EWOULDBLOCK错误,模型回轮询此状态来判断是不是有数据到来
3I/O复用模型:Linux提供select/poll,进程通过将一个 或者多个fd传递给select或者poll系统调用,扫描fd是否就绪
4信号驱动I/O模型,开启套接口信号驱动I/O功能,并通过系统调用sigaction执行一个信号处理函数(调用后返回,进程继续工作,她是非阻塞的)。当数据准备就绪时,就为该进程生成一个SIGIO信号,通过信号回调通知应用程序调用recvfrom来读取数据,并通知主循环函数处理数据
5异步I/O:告诉内核某个操作,并让内核在整个操作完成后(将数据从内核复制到缓冲区)通知我们。这个模型与信号模型的主要区别是:一个是通知我们何时开始一个操作,一个是通知我们何时已经完成
JAVA的NIO的核心类库多路复用器Selector就是基于epoll的多路复用技术实现
JDK1.4时,新增java.nio包,提供了很多异步开发的api和类库
JDK1.7的NIO2.0发布(2011年7月),更完善的API,实现AIO(基于文件的异步I/O操作)功能,完成JSR-51定义的通道功能
这些都是基础皮毛,这里讲下可能出现的业务场景:
1.做通信的实时服务
2.做软硬件结合的自动化控制服务
3.所有需要长连接的服务场景
建议开始撸码之前先了解下Netty基本东西(作为开发的话),这里不多说好吧!
二.先建立NettyServer(服务端,C/S架构的思路),
1.找到自己写的一个废弃的Springoot项目(也可以自建花不了几分钟,这里废物利用),先maven构建下
<dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>4.1.32.Final</version> </dependency>
2.在yml文件添加netty配置(本地测试)
netty:
port: 7000
url: 127.0.0.1
3.在启动主类继承CommandLineRunner(命令模式启动,在启动boot后执行@override的run方法)
主类添加NettyServer 类和Socket(套结字通信)服务地址类,添加代码如下:
@Value("${netty.port}")
private int port;
@Value("${netty.url}")
private String url;
@Autowired
private NettyServer server;
/**
* 用户运行bean的回调
* @param args
* @throws Exception on error
*/
@Override
public void run(String... args) throws Exception {
InetSocketAddress address =new InetSocketAddress(url,port);
log.info("服务启动,地址是"+url);
server.start(address);
}
4.对Netty的初始化配置(这里注解解释的比较简单 )
**
* @author zhouxl
* netty服务器
*/
@Component
@Slf4j
public class NettyServer {
public void start(InetSocketAddress address){
//两个独立的Reactor线程池。一个用于接收客户端的TCP连接,另一个用于处理I/O相关的读写操作,或者执行系统Task、定时任务Task等。
EventLoopGroup bossGroup =new NioEventLoopGroup(1);
EventLoopGroup worksGroup =new NioEventLoopGroup();
try {
//一个对服务端做配置和启动的类
ServerBootstrap bootstrap =new ServerBootstrap()//类似构造器
.group(bossGroup,worksGroup)//组的概念
.channel(NioServerSocketChannel.class)//NIO类的SocketServer通道
.localAddress(address)//服务地址
.childHandler(new ServerChannelInitializer())//拦截器实例化
.option(ChannelOption.SO_BACKLOG,128) //父渠道日志配置
.childOption(ChannelOption.SO_KEEPALIVE,true);//子渠道配置
//绑定端口,开始接收进来的连接
ChannelFuture future =bootstrap.bind(address).sync();
log.info("服务已启动,端口为"+address.getPort());
future.channel().closeFuture().sync();
}catch (Exception e){
e.printStackTrace();
bossGroup.shutdownGracefully();
worksGroup.shutdownGracefully();
}
}
}
5.配置渠道实例化
/**
* @author zhouxl
* 出站和入站的编码器和解码器
* 继承ChannelInitializer<SocketChannel>
*/
public class ServerChannelInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel channel) throws Exception {
channel.pipeline().addLast("decoder",new StringDecoder(CharsetUtil.UTF_8));
channel.pipeline().addLast("encoder",new StringEncoder(CharsetUtil.UTF_8));
channel.pipeline().addLast(new ServerHandlers());
}
}
6.配置拦截器适配器
/**
* @author zhouxl
* 作为服务端拦截器,处理群聊天应用(多客户端通讯)
*/
@Slf4j
public class ServerHandlers extends ChannelInboundHandlerAdapter {
/**
* 创建一个静态组
*/
private static ChannelGroup channels =new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
@Override
public void handlerAdded(ChannelHandlerContext ctx){
//加入ChannelGroup
channels.add(ctx.channel());
log.info(ctx.channel().id()+"的设备加入群聊"+"Online:"+channels.size());
}
@Override
public void handlerRemoved(ChannelHandlerContext context){
log.info(context.channel().id()+"的设备退出群聊"+"Online:"+channels.size());
}
@Override
public void channelActive(ChannelHandlerContext ctx) {
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//打印消息然后群发
log.info(msg.toString());
for (Channel channel:channels){
channel.writeAndFlush(msg.toString());
}
}
/**
* 设备异常关闭
* @param ctx
* @param cause
* @throws Exception
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause)throws Exception{
log.info(ctx.channel().id()+"的设备出错"+"Online:"+channels.size());
ctx.close();
}
}
配置完成,启动!(8019是boot端口地址,7000是Netty服务器端口)
三.建立客户端(这里面客户端可能不是springboot,有可能还不是软件项目,可以是支持通讯的多数客户端)
1.新建Client类,定义一些基本参数
static final String Host = System.getProperty("host","127.0.0.1");//需要连接的地址
static final int PORT = Integer.valueOf(System.getProperty("port","7000"));//端口号
static final int SIZE = Integer.valueOf(System.getProperty("size","256"));//文件大小
static final String name = System.getProperty("name","9527"); //客户端名称
//mian函数启动类
public static void main(String[] args) throws Exception{
sendMessage("去吧,皮卡丘!");
}
/**
* 群里发送信息
* @param content
* @throws InterruptedException
*/
public static void sendMessage(String content) throws InterruptedException{
//配置客户端
EventLoopGroup group = new NioEventLoopGroup();//NIO组概念
try {
Bootstrap b =new Bootstrap();//架构器
b.group(group)
.channel(NioSocketChannel.class)//渠道类型
.option(ChannelOption.TCP_NODELAY,true)//模式
.handler(new ChannelInitializer<SocketChannel>(){
@Override
public void initChannel(SocketChannel ch) throws Exception{
ChannelPipeline p =ch.pipeline();
p.addLast("decoder",new StringDecoder());
p.addLast("encoder",new StringEncoder());
p.addLast(new ClientHandler());
}//拦截器设置
});
ChannelFuture future =b.connect(Host,PORT).sync();//渠道连接
Scanner sca =new Scanner(System.in); //扫描黑窗口
while(true){
//输入内容
String str =sca.nextLine();
if(str.equals("exit")){
break;
}
//将名字和信息内容一起发过去
future.channel().writeAndFlush(name+"-:"+str);
}
future.channel().writeAndFlush(content);
future.channel().closeFuture().sync();
}finally {
group.shutdownGracefully();
}
}
2.ClientHander(客户端拦截器)
**
* @author zhouxl
* 作为服务端拦截器,处理一些简单的逻辑
*/
@Slf4j
public class ClientHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext cx){
log.info("客户端拦截器生效");
}
/**
* 读取信息
* @param ctx 渠道连接对象
* @param msg 信息
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx,Object msg)throws Exception{
log.info("客户端读写拦截开始生效-----------");
log.info("客户端端读写远程地址是-----------"+ctx.channel().remoteAddress()+"信息是:"+msg.toString());
}
/**
* 发生异常关闭连接
* @param ctx 渠道连接对象
* @param cause 异常
* @throws Exception
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause) throws Exception{
cause.printStackTrace();
ctx.close();
}
}
3.配置渠道实例化
/**
* @author zhouxl
* 出站和入站的编码器和解码器
* 继承ChannelInitializer<SocketChannel>
*/
public class ServerChannelInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel channel) throws Exception {
ChannelPipeline p =channel.pipeline();
p.addLast("decoder",new StringDecoder(CharsetUtil.UTF_8));
p.addLast("encoder",new StringEncoder(CharsetUtil.UTF_8));
p.addLast(new ClientHandler());
}
}
OK了,启动!
服务端在客户端启动时日志如下:
发送个消息试试,在客户端的黑窗口输入点字符:
服务端如下:
再加一个客户端2,发送信息"去吧,皮卡丘!"
客户端2如下:
服务端如下:
服务端1如下:
至此基本功能实现,也可以采用jar包模式直接启动(java -jar jar包名),黑窗口聊天,BEST聊天!