目录
1.什么是netty
2.netty使用场景
3.netty线程模型
4.搭建简易web聊天室
4.1依赖导入
4.2目录结构
4.3编写Netty服务
4.4编写Netty处理器
4.5配置监听器项目启动开启Netty服务
4.6启动并连接websocket
使用js连接服务
5.搭建登录页面
6.搭建信息发送页面
7.测试
NIO 的类库和 API 繁杂, 使用麻烦: 需要熟练掌握Selector、 ServerSocketChannel、 SocketChannel、 ByteBuffer等。 开发工作量和难度都非常大: 例如客户端面临断线重连、 网络善断、心跳处理、半包读写、 网络拥塞和异常流的处 理等等。 Netty 对 JDK 自带的 NIO 的 API 进行了良好的封装,解决了上述问题。且Netty拥有高性能、 吞吐量更高,延迟更 低,减少资源消耗,最小化不必要的内存复制等优点。 Netty 现在都在用的是4.x,5.x版本已经废弃,Netty 4.x 需要JDK 6以上版本支持
- 互联网行业:在分布式系统中,各个节点之间需要远程服务调用,高性能的 RPC 框架必不可少,Netty 作为异步 高性能的通信框架,往往作为基础通信组件被这些 RPC 框架使用。典型的应用有:阿里分布式服务框架 Dubbo 的 RPC 框架使用 Dubbo 协议进行节点间通信,Dubbo 协议默认使用 Netty 作为基础通信组件,用于实现。各进程节 点之间的内部通信。Rocketmq底层也是用的Netty作为基础通信组件。
- 游戏行业:无论是手游服务端还是大型的网络游戏,Java 语言得到了越来越广泛的应用。Netty 作为高性能的基 础通信组件,它本身提供了 TCP/UDP 和 HTTP 协议栈。
- 大数据领域:经典的 Hadoop 的高性能通信和序列化组件 Avro 的 RPC 框架,默认采用 Netty 进行跨界点通 信,它的 Netty Service 基于 Netty 框架二次封装实现
- Netty 抽象出两组线程池BossGroup和WorkerGroup,BossGroup专门负责接收客户端的连接, WorkerGroup专 门负责网络的读写
- BossGroup和WorkerGroup类型都是NioEventLoopGroup
- NioEventLoopGroup 相当于一个事件循环线程组, 这个组中含有多个事件循环线程 , 每一个事件循环线程是 NioEventLoop
- 每个NioEventLoop都有一个selector , 用于监听注册在其上的socketChannel的网络通讯
- 每个Boss NioEventLoop线程内部循环执行的步骤有 3 步 处理accept事件 , 与client 建立连接 , 生成 NioSocketChannel 将NioSocketChannel注册到某个worker NIOEventLoop上的selector 处理任务队列的任务 , 即runAllTasks
- 每个worker NIOEventLoop线程循环执行的步骤 轮询注册到自己selector上的所有NioSocketChannel 的read, write事件 处理 I/O 事件, 即read , write 事件, 在对应NioSocketChannel 处理业务 runAllTasks处理任务队列TaskQueue的任务 ,一些耗时的业务处理一般可以放入TaskQueue中慢慢处 理,这样不影响数据在 pipeline 中的流动处理
- 每个worker NIOEventLoop处理NioSocketChannel业务时,会使用 pipeline (管道),管道中维护了很多 handler 处理器用来处理 channel 中的数据
org.springframework.boot
spring-boot-starter-thymeleaf
org.springframework.boot
spring-boot-starter-web
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
org.junit.vintage
junit-vintage-engine
io.netty
netty-all
4.1.35.Final
com.alibaba
fastjson
1.2.14
com.baomidou
mybatis-plus-boot-starter
3.4.3
mysql
mysql-connector-java
8.0.17
org.springframework.boot
spring-boot-starter-data-redis
org.apache.commons
commons-lang3
3.12.0
com.alibaba
fastjson
1.2.76
package com.wangjie.qqserver.server;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
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;
/**
* @Author: Hello World
* @Date: 2022/2/17 14:05
* @Description:
*/
public class NettyServer {
private final int port;
public NettyServer(int port) {
this.port = port;
}
public void start(){
//创建两个线程组boosGroup和workerGroup,含有的子线程NioEventLoop的个数默认为cpu核数的两倍
//boosGroup只是处理链接请求,真正的和客户端业务处理,会交给workerGroup完成
EventLoopGroup boosGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
//创建服务器的启动对象
ServerBootstrap bootstrap = new ServerBootstrap();
//使用链式编程来配置参数
//设置两个线程组
bootstrap.group(boosGroup,workerGroup)
//使用NioSctpServerChannel作为服务器的通道实现
.channel(NioServerSocketChannel.class)
//初始化服务器链接队列大小,服务端处理客户端链接请求是顺序处理的,所以同一时间只能处理一个客户端链接
//多个客户端同时来的时候,服务端将不能处理的客户端链接请求放在队列中等待处理
.option(ChannelOption.SO_BACKLOG,1024)
//创建通道初始化对象,设置初始化参数
.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
System.out.println("收到到新的链接");
//websocket协议本身是基于http协议的,所以这边也要使用http解编码器
ch.pipeline().addLast(new HttpServerCodec());
//以块的方式来写的处理器
ch.pipeline().addLast(new ChunkedWriteHandler());
ch.pipeline().addLast(new HttpObjectAggregator(8192));
ch.pipeline().addLast(new MessageHandler());//添加测试的聊天消息处理类
ch.pipeline().addLast(new WebSocketServerProtocolHandler("/ws", null, true, 65536 * 10));
}
});
System.out.println("netty server start..");
//绑定一个端口并且同步,生成一个ChannelFuture异步对象,通过isDone()等方法判断异步事件的执行情况
//启动服务器(并绑定端口),bind是异步操作,sync方法是等待异步操作执行完毕
ChannelFuture cf = bootstrap.bind(this.port).sync();
//给cf注册监听器,监听我们关心的事件
cf.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture channelFuture) throws Exception {
if (cf.isSuccess()){
System.out.println("监听端口9000成功");
}else {
System.out.println("监听端口9000失败");
}
}
});
//对通道关闭进行监听,closeFuture是异步操作,监听通道关闭
//通过sync方法同步等待通道关闭处理完毕,这里会阻塞等待通道关闭
cf.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
boosGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
package com.wangjie.qqserver.server;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.wangjie.qqserver.model.SocketMessage;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelId;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.util.concurrent.GlobalEventExecutor;
import java.util.concurrent.ConcurrentHashMap;
/**
* @Author: Hello World
* @Date: 2022/2/17 14:11
* @Description:
*/
public class MessageHandler extends SimpleChannelInboundHandler {
//GlobalEventExecutor.INSTANCE 是全局的事件执行器,是一个单例
private static ChannelGroup channelGroup= new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
/**
* 存储用户id和用户的channelId绑定
*/
public static ConcurrentHashMap userMap = new ConcurrentHashMap<>();
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("客户端链接完成");
//添加到group
channelGroup.add(ctx.channel());
ctx.channel().id();
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("收到客户端消息");
//首次连接是fullHttprequest,,把用户id和对应的channel对象存储起来
if (msg != null && msg instanceof FullHttpRequest){
FullHttpRequest request =(FullHttpRequest) msg;
//获取用户参数
Integer userId = getUrlParams(request.uri());
//保存到登录信息map
userMap.put(userId,ctx.channel().id());
//如果url包含参数,需要处理
if (request.uri().contains("?")) {
String newUri = request.uri().substring(0, request.uri().indexOf("?"));
request.setUri(newUri);
}
}else if (msg instanceof TextWebSocketFrame){
//正常的text类型
TextWebSocketFrame frame= (TextWebSocketFrame) msg;
System.out.println("消息内容"+frame.text());
//转换实体类
SocketMessage socketMessage = JSON.parseObject(frame.text(), SocketMessage.class);
if ("group".equals(socketMessage.getMessageType())) {
//推送群聊信息
//groupMap.get(socketMessage.getChatId()).writeAndFlush(new TextWebSocketFrame(JSONObject.toJSONString(socketMessage)));
System.out.println("推送群聊消息");
} else {
//处理私聊的任务,如果对方也在线,则推送消息
ChannelId channelId = userMap.get(socketMessage.getChatId());
if (channelId != null) {
Channel ct = channelGroup.find(channelId);
if (ct != null) {
ct.writeAndFlush(new TextWebSocketFrame(JSONObject.toJSONString(socketMessage)));
}
}
}
}
super.channelRead(ctx, msg);
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("与客户端断开");
//移除channelGroup 通道组
channelGroup.remove(ctx.channel());
}
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, TextWebSocketFrame textWebSocketFrame) throws Exception {
}
private static Integer getUrlParams(String url) {
if (!url.contains("=")) {
return null;
}
String userId = url.substring(url.indexOf("=") + 1);
return Integer.parseInt(userId);
}
}
接受消息实体
package com.wangjie.qqserver.model;
import lombok.Data;
/**
* @author Scoot
* @createTime 2020/3/4 19:58
* @description 消息实体
**/
@Data
public class SocketMessage {
/**
* 消息类型
*/
private String messageType;
/**
* 消息发送者id
*/
private Integer userId;
/**
* 消息接受者id或群聊id
*/
private Integer chatId;
/**
* 消息内容
*/
private String message;
}
package com.wangjie.qqserver.listen;
import com.wangjie.qqserver.server.NettyServer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
/**
* @Author: Hello World
* @Date: 2022/2/17 14:15
* @Description:
*/
@Component
public class NettyInitListen implements CommandLineRunner {
//netty服务端口,配置文件中为1254
@Value("${netty.port}")
Integer nettyPort;
//springboot服务端口 8965
@Value("${server.port}")
Integer serverPort;
@Override
public void run(String... args) throws Exception {
try {
System.out.println("nettyServer starting ...");
System.out.println("http://127.0.0.1:" + serverPort + "/login");
new NettyServer(nettyPort).start();
} catch (Exception e) {
System.out.println("NettyServerError:" + e.getMessage());
}
}
}
IndexController
package com.wangjie.qqserver.controller;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.GetMapping;
/**
* @Author: Hello World
* @Date: 2022/2/17 14:24
* @Description:
*/
@Controller
public class IndexController {
/**
* 测试主页
* @param id
* @param modelMap
* @return
*/
@GetMapping("/index")
public String toIndex(Integer id, ModelMap modelMap){
modelMap.addAttribute("id",id);
return "/html/index";
}
/**
* login
* @param
* @param
* @return
*/
@GetMapping("/login")
public String toLogin(){
return "/login/index";
}
/**
* login
* @param
* @param
* @return
*/
@GetMapping("/send")
public String toSend(String token,ModelMap modelMap){
modelMap.addAttribute("token",token);
return "/send/index";
}
}
html/index
Title
发送
使用1号用户访问http://127.0.0.1:8965/index?id=1,可以看到这时候已经连接上websocket,
登录控制器
package com.wangjie.qqserver.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.wangjie.qqserver.common.AjaxResult;
import com.wangjie.qqserver.model.User;
import com.wangjie.qqserver.server.orm.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* @Author: Hello World
* @Date: 2022/2/18 08:52
* @Description:
*/
@RestController
@RequestMapping("/api")
public class LoginController {
@Autowired
UserService userService;
@Autowired
RedisTemplate redisTemplate;
@PostMapping("/login")
public AjaxResult login(User user){
User obj = userService.getOne(new QueryWrapper().eq("username", user.getUsername()).eq("password", user.getPassword()));
if (obj != null){
//加入redis,生成token
UUID uuid = UUID.randomUUID();
redisTemplate.opsForValue().set(uuid.toString(),obj,200, TimeUnit.MINUTES);
return new AjaxResult(200,"登录成功",uuid);
}else {
return new AjaxResult(500,"登录失败",null);
}
}
}
Login.html
某某公司后台登录系统
IM内部聊天系统
现在登录
好友列表,为测试数据。具体思路为当前用户id对应多个好友信息。通过查询获取列表生成展示
90后大叔
赵鹏
注:由于没有做持久化,当页面刷新时会清楚聊天记录,后期完善。