文章目录
简介
- Netty
Netty 是一个基于NIO的客户、服务器端的编程框架,使用Netty 可以确保你快速和简单的开发出一个网络应用,例如实现了某种协议的客户、服务端应用。Netty相当于简化和流线化了网络应用的编程开发过程,例如:基于TCP和UDP的socket服务开发。 - WebSocket
WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket通信协议于2011年被IETF定为标准RFC 6455,并由RFC7936补充规范。WebSocket API也被W3C定为标准。
WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
最终功能实现的效果图
在线客服功能包含2个页面,客服回复页面和游客页面,目前都支持pc端和移动端使用。
pc端
移动端
实战应用
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--netty核心组件-->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.36.Final</version>
</dependency>
<!--json转换器-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.14</version>
</dependency>
<!--模版引擎-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
配置文件
server:
port: 8018
netty:
#监听websocket连接的端口
port: 11111
#websocket连接地址 (此处要用电脑的ip,不然手机访问会出现问题)
ws: ws://192.168.3.175:${
netty.port}/ws
测试demo
消息内容实体类
public class SocketMessage {
/**
* 消息类型
*/
private String messageType;
/**
* 消息发送者id
*/
private Integer userId;
/**
* 消息接受者id或群聊id
*/
private Integer chatId;
/**
* 消息内容
*/
private String message;
//....省略get set方法
}
处理请求的Handler类
public class TestWebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
private final Logger logger = LoggerFactory.getLogger(TestWebSocketHandler.class);
/**
* 存储已经登录用户的channel对象
*/
public static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
/**
* 存储用户id和用户的channelId绑定
*/
public static ConcurrentHashMap<Integer, ChannelId> userMap = new ConcurrentHashMap<>();
/**
* 用于存储群聊房间号和群聊成员的channel信息
*/
public static ConcurrentHashMap<Integer, ChannelGroup> groupMap = new ConcurrentHashMap<>();
/**
* 获取用户拥有的群聊id号
*/
UserGroupRepository userGroupRepositor = SpringUtil.getBean(UserGroupRepository.class);
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
logger.info("与客户端建立连接,通道开启!");
//添加到channelGroup通道组
channelGroup.add(ctx.channel());
ctx.channel().id();
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
logger.info("与客户端断开连接,通道关闭!");
//添加到channelGroup 通道组
channelGroup.remove(ctx.channel());
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//首次连接是FullHttpRequest,把用户id和对应的channel对象存储起来
if (null != msg && msg instanceof FullHttpRequest) {
FullHttpRequest request = (FullHttpRequest) msg;
String uri = request.uri();
Integer userId = getUrlParams(uri);
userMap.put(getUrlParams(uri), ctx.channel().id());
logger.info("登录的用户id是:{}", userId);
//第1次登录,需要查询下当前用户是否加入过群,加入过群,则放入对应的群聊里
List<Integer> groupIds = userGroupRepositor.findGroupIdByUserId(userId);
ChannelGroup cGroup = null;
//查询用户拥有的组是否已经创建了
for (Integer groupId : groupIds) {
cGroup = groupMap.get(groupId);
//如果群聊管理对象没有创建
if (cGroup == null) {
//构建一个channelGroup群聊管理对象然后放入groupMap中
cGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
groupMap.put(groupId, cGroup);
}
//把用户放到群聊管理对象里去
cGroup.add(ctx.channel());
}
//如果url包含参数,需要处理
if (uri.contains("?")) {
String newUri = uri.substring(0, uri.indexOf("?"));
request.setUri(newUri);
}
} else if (msg instanceof TextWebSocketFrame) {
//正常的TEXT消息类型
TextWebSocketFrame frame = (TextWebSocketFrame) msg;
logger.info("客户端收到服务器数据:{}", 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)));
} 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
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);
}
}
Netty服务启动类
public class NettyServer {
private final int port;
public NettyServer(int port) {
this.port = port;
}
public void start() throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup group = new NioEventLoopGroup();
try {
ServerBootstrap sb = new ServerBootstrap();
sb.option(ChannelOption.SO_BACKLOG, 1024);
// 绑定线程池
sb.group(group, bossGroup)
// 指定使用的channel
.channel(NioServerSocketChannel.class)
// 绑定监听端口
.localAddress(this.port)
// 绑定客户端连接时候触发操作
.childHandler(new ChannelInitializer<SocketChannel>() {
@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 OnlineWebSocketHandler());//添加在线客服聊天消息处理类
ch.pipeline().addLast(new TestWebSocketHandler());//添加测试的聊天消息处理类
ch.pipeline().addLast(new WebSocketServerProtocolHandler("/ws", null, true, 65536 * 10));
}
});
// 服务器异步创建绑定
ChannelFuture cf = sb.bind().sync();
System.out.println(NettyServer.class + " 启动正在监听: " + cf.channel().localAddress());
// 关闭服务器通道
cf.channel().closeFuture().sync();
} finally {
// 释放线程池资源
group.shutdownGracefully().sync();
bossGroup.shutdownGracefully().sync();
}
}
}
容器启动后加载Netty服务类
@Component
public class NettyInitListen implements CommandLineRunner {
@Value("${netty.port}")
Integer nettyPort;
@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());
}
}
}
初始化用户群聊信息
public interface UserGroupRepository {
List<Integer> findGroupIdByUserId(Integer userId);
}
@Component
public class UserGroupRepositoryImpl implements UserGroupRepository {
/**
* 组装假数据,真实环境应该重数据库获取
*/
HashMap<Integer, List<Integer>> userGroup = new HashMap<>(4);
{
List<Integer> list = Arrays.asList(1, 2);
userGroup.put(1, list);
userGroup.put(2, list);
userGroup.put(3, list);
userGroup.put(4, list);
}
@Override
public List<Integer> findGroupIdByUserId(Integer userId) {
return this.userGroup.get(userId);
}
}
```·
### web控制器层
@Controller
public class TestController {
@Value("${netty.ws}")
private String ws;
@Autowired
UserGroupRepository userGroupRepository;
/**
* 登录页面
*/
@RequestMapping("/login")
public String login() {
return "test/login";
}
/**
* 登录后跳转到测试主页
*/
@PostMapping("/login.do")
public String login(@RequestParam Integer userId, HttpSession session, Model model) {
model.addAttribute("ws", ws);
session.setAttribute("userId", userId);
model.addAttribute("groupList", userGroupRepository.findGroupIdByUserId(userId));
return "test/index";
}
}
页面代码
登录页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/login.do" method="post">
登录(默认的4个用户id:[1,2,3,4])
用户Id:<input type="number" name="userId"/>
<input type="submit" value="登录"/>
</form>
</body>
</html>
测试主页
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<style type="text/css">
.flexBox {
display: flex;width: 100%;}
.flexBox div {
width: 50%;background-color: pink;}
#messageBox ul {
border: solid 1px #ccc;width: 600px;height: 400px}
</style>
<body>
<div class="flexBox">
<div style="text-align: right;" th:text="'当前登录的用户:'+${session.userId}"></div>
</div>
<!-- 聊天息 -->
<div class="flexBox" id="messageBox">
<ul th:id="${groupId}" th:each="groupId,iterObj : ${groupList}">
<li th:text="房间号+${groupId}"></li>
</ul>
<ul id="chat">
<li>好友消息</li>
</ul>
</div>
<div style="width:100%;border: solid 1px #ccc;">
<form style="width: 40%;border: solid 1px red;margin: 0px auto">
<h3>给好友或者群聊发送数据</h3>
<div>
测试数据: (好友 1-4 ,房间号 1-2 )<br/>
请输出好友编号或房间号 <input type="number" id="chatId" value="1"><br/>
<textarea id="message" style="width: 100%">在不?</textarea>
</div>
<div>
消息类型<input name="messageType" type="radio" checked value="group">群聊<input name="messageType" type="radio" value="chat">私聊
<a href="#" id="send">发送</a>
</div>
</form>
</div>
</body>
<!--在js脚本中获取作用域的值-->
<script th:inline="javascript">
//获取session中的user
var userId = [[${
session.userId}]];
//获取ws服务地址
var ws = [[${
ws}]]
</script>
<script type="text/javascript">
var websocket;
if (!window.WebSocket) {
window.WebSocket = window.MozWebSocket;
}
if (window.WebSocket) {
websocket = new WebSocket(ws + "?userId=" + userId);
websocket.onmessage = function (event) {
var json = JSON.parse(event.data);
console.log(json)
chat.onmessage(json);
};
websocket.onopen = function (event) {
console.log("Netty-WebSocket服务器。。。。。。连接");
};
websocket.onclose = function (event) {
console.log("Netty-WebSocket服务器。。。。。。关闭");
};
} else {
alert("您的浏览器不支持WebSocket协议!");
}
//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function () {
if (websocket != null) {
websocket.close();
}
};
</script>
<script>
/**
* sendMessage 发送消息推送给websocket对象
* onmessage 接受来自服务端推送的消息,并显示在页面
* */
var chat = {
sendMessage: function () {
var message = document.getElementById("message").value; //发送的内容
if (message == "") {
alert('不能发送空消息');
return;
}
if (!window.WebSocket) {
return;
}
var chatId = document.getElementById("chatId").value; //好友Id或房间号id
var radio=document.getElementsByName("messageType");
var messageType=null; // 聊天类型
for(var i=0;i<radio.length;i++){
if(radio[i].checked==true) {
messageType=radio[i].value;
break;
}
}
if (messageType == "chat") {
if (chatId == userId) {
alert("不能给自己发私聊信息,请换个好友吧");
}
var li = document.createElement("li");
li.innerHTML = "My:" + message
var ul = document.getElementById("chat");
ul.appendChild(li);
}
if (websocket.readyState == WebSocket.OPEN) {
var data = {
};
data.chatId = chatId;
data.message = message;
data.userId = userId;
data.messageType = messageType;
websocket.send(JSON.stringify(data));
} else {
alert("和服务器连接异常!");
}
},
onmessage: function (jsonData) {
var id;
if (jsonData.messageType == "chat") {
id = "chat";
} else {
id = jsonData.chatId;
}
console.log(id);
var li = document.createElement("li");
li.innerHTML = "用户id=" + jsonData.userId + ":" + jsonData.message;
var ul = document.getElementById(id);
ul.appendChild(li);
}
}
document.onkeydown = keyDownSearch;
function keyDownSearch(e) {
// 兼容FF和IE和Opera
var theEvent = e || window.event;
var code = theEvent.keyCode || theEvent.which || theEvent.charCode;
// 13 代表 回车键
if (code == 13) {
// 要执行的函数 或者点击事件
chat.sendMessage();
return false;
}
return true;
}
document.getElementById("send").onclick = function () {
chat.sendMessage();
}
</script>
</html>
测试功能
访问登录页面: http://localhost:8018/login
分别打开2个浏览器,一个用 id=1登录,另外一个用id=2登录
选择消息类型为群聊,然后输入房间号就可以发送群聊消息了
选择消息类型为私聊,然后输入好友Id就可以发送私聊信息
在线客服功能实现
消息内容实体类
public class OnlineMessage {
/**
* 消息发送者id
*/
private String sendId;
/**
* 消息接受者id
*/
private String acceptId;
/**
* 消息内容
*/
private String message;
/**
* 头像
*/
private String headImg;
//省略get set方法
}
处理请求的Handler类
public class OnlineWebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
private final Logger logger = LoggerFactory.getLogger(TestWebSocketHandler.class);
/**
* 存储已经登录用户的channel对象
*/
public static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
/**
* 存储用户id和用户的channelId绑定
*/
public static ConcurrentHashMap<String, ChannelId> userMap = new ConcurrentHashMap<>();
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
logger.info("与客户端建立连接,通道开启!");
//添加到channelGroup通道组
channelGroup.add(ctx.channel());
//ctx.channel().id();
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
logger.info("与客户端断开连接,通道关闭!");
//添加到channelGroup 通道组
channelGroup.remove(ctx.channel());
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//首次连接是FullHttpRequest,把用户id和对应的channel对象存储起来
if (null != msg && msg instanceof FullHttpRequest) {
FullHttpRequest request = (FullHttpRequest) msg;
String uri = request.uri();
String userId = getUrlParams(uri);
//登录后把用户id和channel关联上
userMap.put(userId, ctx.channel().id());
logger.info("登录的用户id是:{}", userId);
//如果url包含参数,需要处理
if (uri.contains("?")) {
String newUri = uri.substring(0, uri.indexOf("?"));
request.setUri(newUri);
}
} else if (msg instanceof TextWebSocketFrame) {
//正常的TEXT消息类型
TextWebSocketFrame frame = (TextWebSocketFrame) msg;
logger.info("客户端收到服务器数据:{}", frame.text());
OnlineMessage onlineMessage = JSON.parseObject(frame.text(), OnlineMessage.class);
//处理私聊的任务,如果对方也在线,则推送消息
ChannelId channelId = userMap.get(onlineMessage.getAcceptId());
if (channelId != null) {
Channel ct = channelGroup.find(channelId);
if (ct != null) {
ct.writeAndFlush(new TextWebSocketFrame(JSONObject.toJSONString(onlineMessage)));
}
}
}
super.channelRead(ctx, msg);
}
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, TextWebSocketFrame textWebSocketFrame) throws Exception {
}
/**
* 解析url中的参数
* @return 获取用户的id
*/
private String getUrlParams(String url) {
if (!url.contains("=")) {
return null;
}
String userId = url.substring(url.indexOf("=") + 1);
return userId;
}
}
Netty服务启动类
和测试demo用的一致,只是handler处理类不一致,把测试的注释掉,在线聊天的handler放开,加载netty服务类用同一个即可
web控制器
@Controller
public class OnlineController {
@Value("${netty.ws}")
private String ws;
/**
* 客服界面
*/
@GetMapping(value = {
"/index", "/customer","/"})
public String index(Model model) {
model.addAttribute("ws", ws);
return "customer";
}
/**
* 游客页面
*/
@GetMapping("/tourist")
public String tourist(Model model) {
model.addAttribute("ws", ws);
return "tourist";
}
}
测试
在线客户的html代码和js代码请参考本博客配套代码
访问游客页面: http://localhost:8018/tourist
访问客服页面: http://localhost:8018/customer
项目配套代码
要是觉得我写的对你有点帮助的话,麻烦在github上帮我点 Star
【SpringBoot框架篇】其它文章如下,后续会继续更新。
- 1.搭建第一个springboot项目
- 2.Thymeleaf模板引擎实战
- 3.优化代码,让代码更简洁高效
- 4.集成jta-atomikos实现分布式事务
- 5.分布式锁的实现方式
- 6.docker部署,并挂载配置文件到宿主机上面
- 7.项目发布到生产环境
- 8.搭建自己的spring-boot-starter
- 9.dobbo入门实战
- 10.API接口限流实战
- 11.Spring Data Jpa实战
- 12.使用druid的monitor工具查看sql执行性能
- 13.使用springboot admin对springboot应用进行监控
- 14.mybatis-plus实战
- 15.使用shiro对web应用进行权限认证
- 16.security整合jwt实现对前后端分离的项目进行权限认证
- 17.使用swagger2生成RESTful风格的接口文档
- 18.使用Netty加websocket实现在线聊天功能