文章目录
- 服务器开发
- 环境准备
- 代码编写
- 服务器后端代码
- 聊天室前端代码
- 聊天室后端代码
- 效果测试
代码全部粘贴下面了,如果想要一个完整的包可以私信我,我也会把打包的链接留在后面
来介绍一下这个小案例:
1.服务器
- 实现一个资源通过http映射类似Nginx
我们的案例都在我们自己实现的服务器上面运行,没有使用Tomcat Nginx Spring等第三方
2.聊天业务
- 实现单聊
- 实现群聊
- 房间人数实时检查
- 实现离线消息
- 实现账号冲突强制下线
- 实现心跳在线检测
案例简介📄
Netty是Java的一个通信框架,是基于NIO来实现的,有许多的java中间件都是Netty来开发的,Netty的高性能是大家有目共睹,大家都认可的
本案例都是通过Netty来编写的,如果有netty基础可能更好理解一些,如果没有不要慌,如有需要私信我,马上更新Netty基础
这个案例分为两个阶段:
- 服务器开发
- 聊天室开发
话不多说开始编写代码
服务器开发
环境准备
先导入pom的配置
Netty的包,复制下来放到pom文件里就行了
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.75.Final</version>
</dependency>
环境准备就这么多,没错就这么多!都手写的还要这么多框架干嘛
代码编写
服务器后端代码
给大家简单介绍一下原理,非常简单
是通过Netty
实现Http
协议,然后根据浏览器发来需要的文件地址,我们通过IO流
把文件字节码读取出来,然后再通过Http
协议发送到浏览器
下面是详细介绍,最后有完整代码
先写一个想要映射的文件夹的路径
我想要映射这个文件夹
创建Netty的启动类,并绑定端口
handler处理器操作http并进行io操作把数据返回给浏览器
这样我们的服务器后端代码就结束了
来测试
在浏览器里输入我们的地址 localhost:8081
回车
OHHHHHHHHHHHHHHHHHH!
OHHHHHHHHHHHHHHHHHH!
我们的页面加载出来了,我写的路径为 / 默认加载index.html
服务器后端的全部代码,简短精悍
public static void main(String[] args) {
String baseUrl="C:\\Users\\Alie\\Desktop\\demo";
//初始化Netty
new ServerBootstrap()
.group(new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
//对http进行解码操作
nioSocketChannel.pipeline().addLast(new HttpServerCodec());
//对管道添加处理器指定类型为http
nioSocketChannel.pipeline().addLast(new SimpleChannelInboundHandler<HttpRequest>() {
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, HttpRequest httpRequest) throws Exception {
System.out.println(httpRequest.uri());
//获取请求的url
String url=httpRequest.uri();
//如果url为/则自动映射index.html文件
url=url.equals("/")?"/index.html":url;
//添加响应状态码200
DefaultFullHttpResponse defaultHttpResponse=new DefaultFullHttpResponse(httpRequest.protocolVersion(), HttpResponseStatus.OK);
//创建输入流从文件夹里去读取文件
FileInputStream fileInputStream=new FileInputStream(new File(baseUrl+url));
//字节数组暂存文件
byte[] bytes=new byte[1024];
int len=0;
//io操作将数据写回浏览器
while (-1!=(len=fileInputStream.read(bytes))) {
defaultHttpResponse.content().writeBytes(bytes,0,len);
}
//关闭资源
fileInputStream.close();
channelHandlerContext.writeAndFlush(defaultHttpResponse);
channelHandlerContext.close();
}
});
}
//绑定端口
}).bind(8081);
}
聊天室前端代码
由于前端代码比较简单就直接给大家,直接用就好了
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>图片提示</title>
<style type="text/css">
body {
margin: 0;
padding: 40px;
}
li {
list-style: none;
float: left;
display: inline;
margin-right: 10px;
border: 1px solid #AAAAAA;
}
.room {
float: left;
width: 100%;
margin-top: 30px;
height: 400px;
border:1px solid black;
}
.room span{
font-size: 14px;
}
/* tooltip */
#tooltip {
position: absolute;
border: 1px solid #ccc;
background: #333;
padding: 2px;
display: none;
color: #fff;
}
</head>
<body>
<h3>图片提示效果</h3>
<div class="room">
<div class="top">
定义用户名:<input type="text" id="userid"><input type="button" id="connt" value="立即连接">
</div>
<br>
接收用户:<input type="text" id="toargetid"> <input type="text" id="msgContent">
<input type="button" id="send" value="发送消息">
<hr>
接收消息:
<div id="receiveMsg"></div>
</div>
<div class="room">
<div class="top">
房间号:<input type="text" id="roomid"><input type="button" id="jionroom" value="加入房间">
</div>
<br>
<input type="text" id="roommsgContent"><input type="button" id="roomsend" value="发送消息">
<hr>
接收消息:
<div id="roomreceiveMsg"></div>
</div>
<div class="video">
</div>
</body>
<script>
//消息模板
messageObj = {
userid: "",
toTarget: "",
msg: "",
msgType: 0,
singe: "",
}
//状态指令代码
var ChatType = {
CHANNEL_INIT: 0,
CHAT_MSG: 1,
CHAT_ROOM_MSG: 2,
SYSTEM_MSG: 3,
JOIN_ROOM: 4,
PING:5
}
//websocket初始化
function initWS() {
mes = messageObj
mes.msgType = ChatType.CHANNEL_INIT
return JSON.stringify(mes)
}
//发送消息函数
function sendMsg(target, msg) {
mes = messageObj
mes.toTarget=target
mes.msgType=ChatType.CHAT_MSG
mes.msg=msg
return JSON.stringify(mes)
}
//聊天室发送消息函数
function sendMsgTORoom(target, msg) {
mes = messageObj
mes.toTarget=target
mes.msgType=ChatType.CHAT_ROOM_MSG
mes.msg=msg
return JSON.stringify(mes)
}
//加入聊天室函数
function addRoom(target) {
mes = messageObj
mes.toTarget=target
mes.msgType=ChatType.JOIN_ROOM
return JSON.stringify(mes)
}
//心跳检测函数
function ping() {
mes = messageObj
mes.msgType=ChatType.PING
return JSON.stringify(mes)
}
//消息处理函数
function dealMsg(data) {
var type=data.msgType
var rece=document.getElementById("receiveMsg")
var roomrece=document.getElementById("roomreceiveMsg")
if(type==ChatType.SYSTEM_MSG){
rece.innerHTML+="<span style='color: rgb(255, 118, 14);' >[系统通知]"+data.msg+"</span><br>"
}else if(type==ChatType.CHANNEL_INIT){
roomrece.innerHTML+="<span>["+data.userid+"]:"+data.msg+"</span><br>"
}else if(type==ChatType.CHAT_ROOM_MSG){
roomrece.innerHTML+="<span>["+data.userid+"]:"+data.msg+"</span><br>"
}else if(type==ChatType.CHAT_MSG){
rece.innerHTML+="<span>["+data.userid+"]:"+data.msg+"</span><br>"
}else if(type==ChatType.PING){
websocket.send(ping())
}
}
document.getElementById("send").onclick = function () {
var str = document.getElementById("msgContent").value
var target=document.getElementById("toargetid").value
var tr=sendMsg(target,str)
console.log(tr);
websocket.send(tr)
}
document.getElementById("connt").onclick = function () {
var userid = document.getElementById("userid").value
messageObj.userid = userid
InitWebSocket()
}
document.getElementById("jionroom").onclick = function () {
var roomid=document.getElementById("roomid").value
websocket.send(addRoom(roomid))
}
document.getElementById("roomsend").onclick = function () {
var str = document.getElementById("roommsgContent").value
var target=document.getElementById("roomid").value
var tr=sendMsgTORoom(target,str)
websocket.send(tr)
}
//websocket
function InitWebSocket() {
websocket = new WebSocket("ws://192.168.43.237:8080/ws")
websocket.onopen = function () {
websocket.send(initWS())
}
websocket.onclose = function () {
console.log("连接断开");
}
websocket.onerror = function () {
console.log("连接出错");
}
websocket.onmessage = function (e) {
var data=JSON.parse(e.data)
dealMsg(data)
console.log(e);
console.log(data);
}
}
</script>
</html>
聊天室后端代码
配置环境文件
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.75.Final</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.72</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>19.0</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.10</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.78</version>
</dependency>
我先把代码和结构发出来按照这个结构还原后就可直接运行
server类
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
public class Server {
public static void main(String[] args) throws InterruptedException {
//两个任务组
NioEventLoopGroup group1 = new NioEventLoopGroup();
NioEventLoopGroup group2 = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap()
.group(group1, group2)
.channel(NioServerSocketChannel.class)
//初始传入我们自己的定义的ConnInit类
.childHandler(new ConnInit());
//绑定端口为8080
ChannelFuture channel = serverBootstrap.bind(8080).sync();
channel.channel().closeFuture().sync();
}finally {
//优雅关闭server
group1.shutdownGracefully();
group2.shutdownGracefully();
}
}
}
Conninit类
连接初始化
package myChatRoom;
import io.netty.channel.*;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.handler.timeout.IdleStateHandler;
import myChatRoom.pojo.MyMessage;
public class ConnInit extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
//心跳机制检测假死 10s未读到数据触发 5s未发送触发
pipeline.addLast(new IdleStateHandler(10,5,0));
//为心跳机制 到达10s 和 5s 时编写触发函数
pipeline.addLast(new ChannelDuplexHandler(){
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
IdleStateEvent event=(IdleStateEvent) evt;
//当5s未写写数据会触发,这时我们可以主动发送数据给前端然后让前端返回数据给我们
if(event.state()==IdleState.WRITER_IDLE){
MyMessage myMessage = new MyMessage();
myMessage.setMsgType(ChatType.PING);
ctx.channel().writeAndFlush(new TextWebSocketFrame(myMessage.toString()));
System.out.println("ping");
}
//当10s未读到数据时会触发,当5s后发送给前端,到10s检测没读到数据说明前端没有给我们返回数据,前端可能已经断开,这时可以断开连接
if(event.state()==IdleState.READER_IDLE){
System.out.println("10s未读到数据");
}
super.userEventTriggered(ctx, evt);
}
});
//http编解码
pipeline.addLast(new HttpServerCodec());
//数据流写支持
pipeline.addLast(new ChunkedWriteHandler());
//聚合操作 解决粘包拆包
pipeline.addLast(new HttpObjectAggregator(1024*64));
//实现websocket
pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
//处理消息加入我们自己的处理器
pipeline.addLast(new MyChatHandler());
}
}
ChatType类
指定消息类型
public class ChatType{
public static final int CHANNEL_INIT=0;
public static final int CHAT_MSG=1;
public static final int CHAT_ROOM_MSG=2;
public static final int SYSTEM_MSG=3;
public static final int JOIN_ROOM=4;
public static final int PING=5;
}
MyMessage
消息实体类
import com.alibaba.fastjson.JSON;
import lombok.Data;
@Data
public class MyMessage {
//发送者id
String userid;
//接收者
String toTarget;
//内容
String msg;
//类型
int msgType;
int singe;
@Override
public String toString() {
return JSON.toJSONString(this);
}
}
MyChatHandler类
我们自己的聊天消息处理类
package myChatRoom;
import com.alibaba.fastjson.JSON;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import myChatRoom.server.Impl.ChatRoomHandlerImpl;
import myChatRoom.server.Impl.ConnectHandlerImpl;
import myChatRoom.pojo.MyMessage;
import myChatRoom.server.Impl.MsgHandlerImpl;
public class MyChatHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
ConnectHandlerImpl connectHandler = new ConnectHandlerImpl();
ChatRoomHandlerImpl chatRoomHandler=new ChatRoomHandlerImpl();
MsgHandlerImpl msgHandler=new MsgHandlerImpl();
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame frame) throws Exception {
//获取消息文本
String text = frame.text();
System.out.println(text);
//把文本消息转换为我们的消息类
MyMessage myMessage = JSON.parseObject(text, MyMessage.class);
int msgType = myMessage.getMsgType();
//对不同的消息类型来分别进行处理
if(msgType==ChatType.CHANNEL_INIT){
//连接建立初始化
connectHandler.addUserConnect(myMessage.getUserid(), ctx.channel());
msgHandler.checkMsg(myMessage.getUserid());
}else if(msgType==ChatType.CHAT_MSG){
//个人消息发送
msgHandler.send(myMessage);
}else if (msgType==ChatType.CHAT_ROOM_MSG){
//聊天室消息发送
chatRoomHandler.sendRoomMsg(myMessage.getUserid(), myMessage);
}else if (msgType==ChatType.JOIN_ROOM){
//加入聊天室消息
chatRoomHandler.addUserInRoom(myMessage.getUserid(), myMessage.getToTarget());
}else if(msgType==ChatType.SYSTEM_MSG){
//系统通知
System.out.println("系统通知");
}
}
//连接断开处理
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
//移除房间的人数
chatRoomHandler.leafRoom(connectHandler.getIdbyChannel().get(ctx.channel()));
//移除通道管理的通道
connectHandler.removeConnect(ctx.channel());
System.out.println("断开连接");
}
}
server接口和实现类
1.ChatRoomHandler和ChatRoomHandlerImpl
聊天室消息处理类
package myChatRoom.server;
import myChatRoom.pojo.MyMessage;
public interface ChatRoomHandler {
void addUserInRoom(String userid,String roomid);
void sendRoomMsg(String userid, MyMessage message);
void leafRoom(String userid);
}
ChatRoomHandlerImpl 实现
package myChatRoom.server.Impl;
import io.netty.channel.Channel;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import myChatRoom.server.ChatRoomHandler;
import myChatRoom.pojo.MyMessage;
import java.util.*;
public class ChatRoomHandlerImpl implements ChatRoomHandler {
//map里存储全部的聊天室 set为聊天室成员
static Map<String, Set<String>> roomMap=new HashMap<>();
ConnectHandlerImpl connectHandler=new ConnectHandlerImpl();
//加入聊天室
@Override
public void addUserInRoom(String userid, String roomid) {
if (roomMap.containsKey(roomid)) {
roomMap.get(roomid).add(userid);
}else {
Set<String> list=new HashSet<>();
list.add(userid);
roomMap.put(roomid,list);
}
MyMessage myMessage = new MyMessage();
myMessage.setUserid(userid);
myMessage.setToTarget(roomid);
myMessage.setMsg("[加入房间] 当前房间在线人数:"+roomMap.get(roomid).size());
sendRoomMsg(userid,myMessage);
}
//发送聊天室消息
@Override
public void sendRoomMsg(String userid, MyMessage message) {
String toTarget = message.getToTarget();
Set<String> list = roomMap.get(toTarget);
Map<String, Channel> connects = connectHandler.getConnects();
for (String s : list) {
Channel channel = connects.get(s);
channel.writeAndFlush(new TextWebSocketFrame(message.toString()));
}
}
//离开房间处理,通知聊天室其他人有人离开
@Override
public void leafRoom(String userid) {
roomMap.forEach((k,v)->{
if (v.contains(userid)) {
v.remove(userid);
MyMessage myMessage = new MyMessage();
myMessage.setUserid(userid);
myMessage.setToTarget(k);
myMessage.setMsg(userid+"[离开房间]当前房间人数"+v.size());
sendRoomMsg(userid,myMessage);
}
});
}
}
2.ConnectionHandler
用户连接处理
ConnectionHandler
package myChatRoom.server;
import io.netty.channel.Channel;
import java.util.Map;
public interface ConnectHandler {
/**
* #Description
* @param userid 用户id
* @param channel 连接通道
* @return void
* @author shuyu
* #Date 2022/4/14
*/
void addUserConnect(String userid, Channel channel);
/**
* #Description
* @param channel 连接通道
* @return void
* @author shuyu
* #Date 2022/4/14
*/
void removeConnect(Channel channel);
Map<String,Channel> getConnects();
Map<Channel,String> getIdbyChannel();
}
package myChatRoom.server.Impl;
import io.netty.channel.Channel;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import myChatRoom.pojo.MyMessage;
import myChatRoom.server.ConnectHandler;
import java.util.HashMap;
import java.util.Map;
public class ConnectHandlerImpl implements ConnectHandler {
static Map<String,Channel> userMap=new HashMap<>();
//所有的连接都在此管理
static Map<Channel,String> channelMap=new HashMap<>();
//用户连接添加新连接
@Override
public void addUserConnect(String userid, Channel channel) {
MyMessage myMessage = new MyMessage();
myMessage.setMsgType(3);
//如果此用户已经登录了就把此用于的通道关闭强制下线,新用户接入连接
if(userMap.containsKey(userid)){
Channel channel1 = userMap.get(userid);
myMessage.setMsg("账号异地登录强制下线!");
channel1.writeAndFlush(new TextWebSocketFrame(myMessage.toString()));
channel1.close();
channelMap.remove(channel1);
}
userMap.put(userid,channel);
channelMap.put(channel,userid);
myMessage.setMsg("欢迎用户:"+userid);
channel.writeAndFlush(new TextWebSocketFrame(myMessage.toString()));
}
//移除连接
@Override
public void removeConnect(Channel channel) {
userMap.remove(channelMap.get(channel));
channelMap.remove(channel);
}
@Override
public Map<String,Channel> getConnects() {
System.out.println(userMap);
return userMap;
}
@Override
public Map<Channel,String> getIdbyChannel() {
return channelMap;
}
}
3.MsgHandler
好友聊天消息处理
MsgHandler
package myChatRoom.server;
import myChatRoom.pojo.MyMessage;
public interface MsgHandler {
/**
* #Description 消息发送
* @param message 消息体
* @return void
* @author shuyu
* #Date 2022/4/14
*/
void send(MyMessage message);
}
MsgHandlerImpl实现类
package myChatRoom.server.Impl;
import io.netty.channel.Channel;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import myChatRoom.server.MsgHandler;
import myChatRoom.pojo.MyMessage;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class MsgHandlerImpl implements MsgHandler {
ConnectHandlerImpl connectHandler=new ConnectHandlerImpl();
Map<String, Channel> connects = connectHandler.getConnects();
//如果有消息但是好友没有在线会在此暂存
public static Map<String, List<MyMessage>> messageQeueu=new HashMap<>();
@Override
public void send(MyMessage message) {
String toTarget = message.getToTarget();
Channel channel = connects.get(toTarget);
if (channel!=null){
channel.writeAndFlush(new TextWebSocketFrame(message.toString()));
}else {
if (messageQeueu.containsKey(message.getToTarget())) {
messageQeueu.get(message.getToTarget()).add(message);
}else {
List<MyMessage> list = new ArrayList<>();
list.add(message);
messageQeueu.put(message.getToTarget(),list);
}
}
System.out.println(message.getToTarget()+":"+channel);
}
//检查函数如果发现map有消息并且好友在线发送消息
public void checkMsg(String userid){
List<MyMessage> myMessages = messageQeueu.get(userid);
if(myMessages!=null){
Channel channel = connects.get(userid);
System.out.println(myMessages);
System.out.println(channel);
for (MyMessage myMessage : myMessages) {
channel.writeAndFlush(new TextWebSocketFrame(myMessage.toString()));
}
messageQeueu.remove(userid);
}
}
}
至此我们聊天的代码也结束了
效果测试
开始测试
输入我们的地址 localhost:8081
1.连接两个用户
成功!
2. 1,2用户互发消息
成功!
3.账号顶替下线
成功!
4.离线消息
此时右边为离线状态,让左边发消息给1,然后再登录1
登录1后立即受到消息
成功!
5,聊天室聊天
成功!6.心跳检测