30分钟写一个聊天板
30分钟写一个聊天板
最近放假在家,无事学习了netty,写一个demo练手,快速编写一个简陋的聊天网页。
思路
基本的结构是后台采用netty,前端采用websocket和后台进行连接。
登陆:
- 前端用户发请求到netty服务器,服务器进行校验,返回响应
聊天:
- 前端用户将消息内容和聊天对象的ID以JSON报文的格式发给后台
- 后台经过Hadnler链拿到包,对里面的用户数据进行解析,并返回响应给用户前端
- 同时通过会话存储拿到聊天对象的channel并将消息发送给目标
本文阅读需要有对netty基础的了解,以及一点点前端websocket的知识
后台部分
创建服务器启动类:
package com.gdou.im.server;import com.gdou.im.server.handler.WebSocketHandler;
import com.gdou.im.server.handler.LoginRequestHandler;
import com.gdou.im.server.handler.MessageRequestHandler;
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;
import io.netty.channel.socket.nio.NioSocketChannel;
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;/*** @ProjectName: demo* @Package: com.gdou.im.server* @ClassName: NettyServer* @Author: carrymaniac* @Description: netty的服务器端* @Date: 2020/1/4 1:08 下午* @Version:*/
public class NettyServer {public static void main(String[] args) throws InterruptedException {//定义线程组NioEventLoopGroup bossGroup = new NioEventLoopGroup();NioEventLoopGroup workerGroup = new NioEventLoopGroup();final ServerBootstrap serverBootstrap = new ServerBootstrap();serverBootstrap.group(bossGroup,workerGroup).channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<NioSocketChannel>(){@Overrideprotected void initChannel(NioSocketChannel ch) throws Exception {//Http编解码器,HttpServerCodec()ch.pipeline().addLast(new HttpServerCodec());//大数据流处理ch.pipeline().addLast(new ChunkedWriteHandler());//HttpObjectAggregator:聚合器,聚合了FullHTTPRequest、FullHTTPResponse。。。,当你不想去管一些HttpMessage的时候,直接把这个handler丢到管道中,让Netty自行处理即可ch.pipeline().addLast(new HttpObjectAggregator(2048*64));//WebSocketServerProtocolHandler:给客户端指定访问的路由(/ws),是服务器端处理的协议,当前的处理器处理一些繁重的复杂的东西,运行在一个WebSocket服务端ch.pipeline().addLast(new WebSocketServerProtocolHandler("/ws"));ch.pipeline().addLast(new WebSocketHandler());ch.pipeline().addLast(new MessageRequestHandler());ch.pipeline().addLast(new LoginRequestHandler());}});ChannelFuture future = serverBootstrap.bind(8080).sync();}
}
其中,里面的WebSocketHandler、MessageRequestHandler、LoginRequestHandler是自定义的handler,下面分别展示:
WebSocketHandler
package com.gdou.im.server.handler;import com.alibaba.fastjson.JSONObject;
import com.gdou.im.protocol.PacketCodeC;
import com.gdou.im.protocol.data.Data;
import com.gdou.im.protocol.data.request.LoginRequest;
import com.gdou.im.protocol.data.request.MessageRequest;
import com.gdou.im.protocol.Packet;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.util.concurrent.GlobalEventExecutor;
import lombok.extern.slf4j.Slf4j;/*** @ProjectName: demo* @Package: com.gdou.im.server.handler* @ClassName: ChatHandler* @Author: carrymaniac* @Description: ChatHandler* @Date: 2020/1/28 12:01 上午* @Version:*/
@Slf4j
public class WebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame>{@Overrideprotected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {String text = msg.text();log.info("接收到了websocket包,内容为:{}",text);Packet packet = JSONObject.parseObject(text, Packet.class);if(packet!=null){Data decode = PacketCodeC.INSTANCE.decode(packet);//分发到下一个Handlerif(decode instanceof MessageRequest){log.info("向下转型完成,内容为:{}",decode);ctx.fireChannelRead((MessageRequest)decode);}else if(decode instanceof LoginRequest){log.info("向下转型完成,内容为:{}",decode);ctx.fireChannelRead((LoginRequest)decode);}}}
}
WebSocketHandler主要职责是用于接收Handler链WebSocketServerProtocolHandler发下来的TextWebSocketFrame,解析其中的JSON正文为Packet (java对象),然后转化为对应的每一个Request对象发送到Handler链的下一个Handler进行处理。
LoginRequestHandler
LoginRequestHandler主要用于处理WebSocketHandler发下来的MessageRequest数据,并生成LoginResponse响应将登陆情况发回给用户。
package com.gdou.im.server.handler;import com.alibaba.fastjson.JSONObject;
import com.gdou.im.protocol.Packet;
import com.gdou.im.protocol.data.request.LoginRequest;
import com.gdou.im.protocol.data.response.LoginResponse;
import com.gdou.im.session.Session;
import com.gdou.im.util.SessionUtil;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import lombok.extern.slf4j.Slf4j;import java.util.UUID;import static com.gdou.im.protocol.command.Command.LOGIN_RESPONSE;/*** @ProjectName: demo* @Package: com.gdou.im.server.handler* @ClassName: LoginRequestHandler* @Author: carrymaniac* @Description:* @Date: 2020/1/28 1:34 下午* @Version:*/
@Slf4j
public class LoginRequestHandler extends SimpleChannelInboundHandler<LoginRequest> {@Overrideprotected void channelRead0(ChannelHandlerContext ctx, LoginRequest msg) throws Exception {LoginResponse response = new LoginResponse();Packet packet = new Packet();//校验用户名和密码合法,这里没有实现,可以自行加入数据库校验等实现if(valid(msg)){//随机生成IDString userId = randomUserId();response.setSuccess(true);response.setUserId(userId);response.setUserName(msg.getUserName());//绑定session和channel,将用户信息和对应的channel进行绑定,以供之后使用SessionUtil.bindSession(new Session(userId,msg.getUserName()),ctx.channel());log.info("用户:{}登陆成功",msg.getUserName());//进行广播,对所有在线的成员channel发送一条消息SessionUtil.broadcast("用户: "+msg.getUserName()+"已上线,他的ID为: "+userId);}else {response.setReason("账号密码校验失败");response.setSuccess(false);log.info("用户:{}登陆失败",msg.getUserName());}packet.setData(JSONObject.toJSONString(response));packet.setCommand(LOGIN_RESPONSE);//将登陆成功的消息发给用户ctx.channel().writeAndFlush(new TextWebSocketFrame(JSONObject.toJSONString(packet)));}/*** 进行登陆校验,todo 之后可以在这个方法中加入数据库进行校验* @param loginRequest* @return*/private boolean valid(LoginRequest loginRequest) {return true;}//生成一个用户IDprivate static String randomUserId() {return UUID.randomUUID().toString().split("-")[0];}/*** channel没有链接到远程节点的时候* @param ctx* @throws Exception*/@Overridepublic void channelInactive(ChannelHandlerContext ctx) throws Exception {//当用户断开连接时,需要将其session和channel移除Session session = SessionUtil.getSession(ctx.channel());log.info("用户{}下线了,移除其session",session.getUserName());SessionUtil.unBindSession(ctx.channel());SessionUtil.broadcast("用户: "+session.getUserName()+"已下线");}
MessageRequestHandler
package com.gdou.im.server.handler;import com.alibaba.fastjson.JSONObject;
import com.gdou.im.protocol.Packet;
import com.gdou.im.protocol.command.Command;
import com.gdou.im.protocol.data.Data;
import com.gdou.im.protocol.data.request.MessageRequest;
import com.gdou.im.protocol.data.response.MessageResponse;
import com.gdou.im.session.Session;
import com.gdou.im.util.SessionUtil;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import lombok.extern.slf4j.Slf4j;/*** @ProjectName: demo* @Package: com.gdou.im.server.handler* @ClassName: DataHandler* @Author: carrymaniac* @Description:* @Date: 2020/1/28 1:15 下午* @Version:*/
@Slf4j
public class MessageRequestHandler extends SimpleChannelInboundHandler<MessageRequest> {@Overrideprotected void channelRead0(ChannelHandlerContext ctx, MessageRequest msg) throws Exception {log.info("拿到了数据,到达此处了:{}",msg);//通过channel获取到用户的信息Session session = SessionUtil.getSession(ctx.channel());//开始写回去Packet packetForConfirm = new Packet();packetForConfirm.setCommand(Command.MESSAGE_RESPONSE);MessageResponse responseForConfirm = new MessageResponse();responseForConfirm.setMessage(msg.getMessage());responseForConfirm.setFromUserName("你");packetForConfirm.setData(JSONObject.toJSONString(responseForConfirm));ctx.channel().writeAndFlush(new TextWebSocketFrame(JSONObject.toJSONString(packetForConfirm)));//构建response,将消息发送给用户要发送的id用户//通过toId获取channelChannel channel = SessionUtil.getChannel(msg.getToId());if(channel!=null&& SessionUtil.hasLogin(channel)){//toID的用户在线,构建包发回给用户MessageResponse response = new MessageResponse();response.setFromUserId(session.getUserId());response.setFromUserName(session.getUserName());response.setMessage(msg.getMessage());Packet packetForToId = new Packet();packetForToId.setData(JSONObject.toJSONString(response));packetForToId.setCommand(Command.MESSAGE_RESPONSE);channel.writeAndFlush(new TextWebSocketFrame(JSONObject.toJSONString(packetForToId)));}else {log.info("用户并不在线");}}
}
到这,三个最重要的handler就介绍完成,还有一个比较重要的部分就是SessionUtil部分:
SessionUtil
//Session.java
@Data
@NoArgsConstructor
public class Session {// 用户唯一性标识private String userId;private String userName;public Session(String userId, String userName) {this.userId = userId;this.userName = userName;}@Overridepublic String toString() {return userId + ":" + userName;}}
//SessionUtil.java
package com.gdou.im.util;import com.alibaba.fastjson.JSONObject;
import com.gdou.im.attribute.Attributes;
import com.gdou.im.protocol.Packet;
import com.gdou.im.protocol.data.response.MessageResponse;
import com.gdou.im.session.Session;
import io.netty.channel.Channel;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;import static com.gdou.im.protocol.command.Command.SYSTEM_MESSAGE_RESPONSE;public class SessionUtil {//用于存储用户ID--->channel的对应关系private static final Map<String, Channel> userIdChannelMap = new ConcurrentHashMap<>();public static void bindSession(Session session, Channel channel) {//在map中存放绑定关系userIdChannelMap.put(session.getUserId(), channel);//在channel中存储用户信息channel.attr(Attributes.SESSION).set(session);}public static void unBindSession(Channel channel) {if (hasLogin(channel)) {//解除绑定userIdChannelMap.remove(getSession(channel).getUserId());channel.attr(Attributes.SESSION).set(null);}}public static boolean hasLogin(Channel channel) {return channel.hasAttr(Attributes.SESSION);}public static Session getSession(Channel channel) {return channel.attr(Attributes.SESSION).get();}public static Channel getChannel(String userId) {return userIdChannelMap.get(userId);}//广播public static void broadcast(String message){Packet packet = new Packet();packet.setCommand(SYSTEM_MESSAGE_RESPONSE);MessageResponse response = new MessageResponse();response.setMessage(message);response.setFromUserName("系统提醒");packet.setData(JSONObject.toJSONString(response));Set<Map.Entry<String, Channel>> entries = userIdChannelMap.entrySet();for(Map.Entry<String, Channel> entry :entries){Channel channel = entry.getValue();channel.writeAndFlush(new TextWebSocketFrame(JSONObject.toJSONString(packet)));}}
}
其余的后台代码比较简单,请参考我的github仓库的代码。
前端部分
前端部分因为本人没学过前端,因此写的比较稀烂,仅供参考:
<!DOCTYPE html>
<html><head><meta charset="utf-8" /><title>小聊天室DEMO</title></head><body>用户名:<input type="text" name="userName" id="userName"/><br>用户密码:<input type="password" name="userPassword" id="userPassword"/><br><input type="button" onclick="CHAT.login()" name="loginButton" id="loginButton" value="登陆"/><br><div id="">发送消息:</div>发送ID:<input type="text" name="toId" id="toId"/><br>发送内容:<input type="text" name="messageContent" id="messageContent"/><br><input type="button" onclick="CHAT.chat()" name="sendButton" id="sendButton" value="发送消息" /><hr><div id="">接收消息列表:</div><br><div id="receiveNsg" style="background-color: gainsboro;"></div><script type="text/javascript">var user = {name:null,id:null}var COMMAND_CODE = {//登陆请求LOGIN_REQUEST:1,// 登陆消息响应LOGIN_RESPONSE:2,// 普通消息请求MESSAGE_REQUEST:3,// 普通消息响应MESSAGE_RESPONSE:4,// 系统消息响应SYSTEM_RESPONSE:-1},_this = this;window.CHAT = {socket: null,//初始化init: function(){//首先判断浏览器是否支持WebSocketif (window.WebSocket){that = this;CHAT.socket = new WebSocket("ws://localhost:8080/ws");CHAT.socket.onopen = function(){console.log("客户端与服务端建立连接成功");},CHAT.socket.onmessage = function(e){var receiveNsg = window.document.getElementById("receiveNsg");var html = receiveNsg.innerHTML;console.log("接收到消息:"+e.data);var response = JSON.parse(e.data);// 说明是登陆的返回消息if(response.command==_this.COMMAND_CODE.LOGIN_RESPONSE){var result = JSON.parse(response.data);console.log(result);if(result.success==true){_this.user.name = result.userName;_this.user.id = result.userId;receiveNsg.innerHTML = html + "<br>" + "用户登陆成功,您的ID为:"+result.userId+",快去告诉你的朋友吧";return;}else{receiveNsg.innerHTML = html + "<br>" + "用户登陆失败,原因是:"+result.reason;}}else if(response.command==_this.COMMAND_CODE.MESSAGE_RESPONSE){var result = JSON.parse(response.data);receiveNsg.innerHTML = html + "<br>" + "["+result.fromUserName+"]"+"说:"+result.message;// 将ID设置到发送id框上去var toId = window.document.getElementById("toId");if(result.fromUserId!=_this.user.id){toId.value = result.fromUserId;}return;}else if(response.command==_this.COMMAND_CODE.SYSTEM_RESPONSE){var result = JSON.parse(response.data);receiveNsg.innerHTML = html + "<br>" + "[系统提示] "+result.message;// 将ID设置到发送id框上去var toId = window.document.getElementById("toId");toId.value = result.fromUserId;return;}},CHAT.socket.onerror = function(){console.log("发生错误");},CHAT.socket.onclose = function(){console.log("客户端与服务端关闭连接成功");} }else{alert("8102年都过了,升级下浏览器吧");}},chat: function(){var msg = window.document.getElementById("messageContent");var toId = window.document.getElementById("toId");var packet = {version:1,command:_this.COMMAND_CODE.MESSAGE_REQUEST,data:{fromid:_this.user.id,toid:toId.value,message:msg.value}}CHAT.socket.send(JSON.stringify(packet));},login:function(){var userName = window.document.getElementById("userName");var userPassword = window.document.getElementById("userPassword");var packet = {version:1,command:_this.COMMAND_CODE.LOGIN_REQUEST,data:{userName:userName.value,password:userPassword.value}}CHAT.socket.send(JSON.stringify(packet));}}CHAT.init();</script></body></html>
大致效果如下图:
打开第二个标签页,再次登陆:
此时会发现,第一个标签页会出现提示:
在第一个标签页输入小红ID,以及内容,在第二个标签页显示如下:
小红用户接收到消息,并在发送ID框上填充上小明的ID。此时可以进行回复,小明用户效果图如下:
到此演示完毕,这个demo主要是为了自己记忆练习netty的主要用法。问题很多,大佬轻喷。
github地址
30分钟写一个聊天板相关推荐
- 【直播】手把手带你 5 分钟写一个小爬虫,从入门到超神!
在程序员界流传着这么一个顺口溜:爬虫玩得好,监狱进得早.数据玩得溜,牢饭吃个够--时不时还有 "XX 公司做违法爬虫,程序员坐牢" 的新闻爆出. 在看热闹的同时,很多人都会提出疑问 ...
- Hexo+gitee:30分钟搭建一个自己的个人博客网站 欢迎友链呀<(▰˘◡˘▰)
Hexo + Gitee 部署自己的个人博客 目前市场上比较火的一些博客框架: Hexo.jekyll.Solo.Halo .gohugo.VuePress.wordpress 等等 ,这些都是开 ...
- python socket能做什么_用python写一个聊天小程序!和女朋友的专属聊天工具!
原标题:用python写一个聊天小程序!和女朋友的专属聊天工具! 1.UDP简介 Internet协议集支持一个无连接的传输协议,该协议称为用户数据报协议(UDP).UDP为应用程序提供了无需建立就可 ...
- c语言编写对答机器人_来,你也可以用 C 语言写一个聊天机器人
来,你也可以用 C 语言写一个聊天机器人 你是不是一直在面对着枯燥的 C 语言特性.摸索着前人写过的各种算法,不是因为自己的兴趣,而是依靠自身的毅力,学得很苦吧. 好吧,我们找一个好玩一点的东西,一起 ...
- 利用itchat写一个聊天机器人
利用itchat写一个聊天机器人 聊天机器人 图灵机器人 需要的库 **自动回复私聊消息** **自动回复群聊消息** 结语: 聊天机器人 偶然在CSDN上看到大佬用20行教你写一个聊天机器人,觉得甚 ...
- 【GitHub探索】v语言上手,用vlang写一个聊天应用
前言 vlang(v语言)自从6月份突然炒热起来,不知不觉到了11月,正式版就要出来了,在11月的GitHub Trending榜中依然排在前10.这着实令人好奇,因此笔者决定试用一下vlang,写一 ...
- 30分钟搭一个wordpress网站
这里是Z哥的个人公众号 每周五11:45 按时送达 当然了,也会时不时加个餐- 我的第「88」篇原创敬上 因为最近工作比较忙,没太多时间思考和写东西.所以今天偷个懒,发一篇实操类文章. 这篇文章非常& ...
- 如何用Java写一个聊天机器人
文章目录 建议结合新版教程看 写在前面的的话 免责声明 你需要提前会的东西 我们要使用的框架 首先我们先下载一个Demo 文件配置 Demo里面的的目录结构 在配置文件中加上你小号的QQ名字和密码 我 ...
- 手把手教你用 30 分钟搭建一个网盘
code小生 一个专注大前端领域的技术平台 公众号回复Android加入安卓技术群 本文出处:码匠笔记公众号 Pandownload 下线大家心里都很苦,不过我们还是的重新站起来,于是我研究了一下花了 ...
- [分享] 30分钟做一个二维码名片应用,有源码!
2019独角兽企业重金招聘Python工程师标准>>> 前言 30分钟带你用Wex5做一个微信公众号上使用的二维码名片,相应技术点有详细讲解,高清有码!(点击下载全部源码) 二维码现 ...
最新文章
- 计算机二级vfp知识点,全国计算机二级等级考试VFP知识点提纲
- redhat linux新建用户,linux redhat 添加用户
- Windows 之 win10快捷键
- C指针原理(17)-C指针基础
- android graphics pipeline
- win10+tensorflow faster-RCNN 训练自己的数据集
- Python 分析国庆热门旅游景点,告诉你哪些地方好玩、便宜、人又少!
- 在Firefox中通过AJAX跨域访问Web资源
- 【网络学习笔记】Excel数据分析实战项目—淘宝用户画像
- 视频教程-初级学习ArcGIS Engine视频课程-C#
- js 设置 Cookie,cookie的作用域设置
- Unity移动端自动翻转及横竖屏的设置与检测
- SQL UCASE() 函数、 LCASE() 函数
- 计算机交互媒体应用范围,浅析交互媒体设计中的科技与艺术的关系
- enabled的使用
- java中break用法
- 诺基亚 android,诺基亚当年为什么走向没落也没用安卓系统?
- 分配工作时需要考虑的问题
- android widget 点击事件,Android Widget点击事件
- Python Scapy使用方法