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分钟写一个聊天板相关推荐

  1. 【直播】手把手带你 5 分钟写一个小爬虫,从入门到超神!

    在程序员界流传着这么一个顺口溜:爬虫玩得好,监狱进得早.数据玩得溜,牢饭吃个够--时不时还有 "XX 公司做违法爬虫,程序员坐牢" 的新闻爆出. 在看热闹的同时,很多人都会提出疑问 ...

  2. Hexo+gitee:30分钟搭建一个自己的个人博客网站 欢迎友链呀<(▰˘◡˘▰)

    Hexo + Gitee 部署自己的个人博客   目前市场上比较火的一些博客框架: Hexo.jekyll.Solo.Halo .gohugo.VuePress.wordpress 等等 ,这些都是开 ...

  3. python socket能做什么_用python写一个聊天小程序!和女朋友的专属聊天工具!

    原标题:用python写一个聊天小程序!和女朋友的专属聊天工具! 1.UDP简介 Internet协议集支持一个无连接的传输协议,该协议称为用户数据报协议(UDP).UDP为应用程序提供了无需建立就可 ...

  4. c语言编写对答机器人_来,你也可以用 C 语言写一个聊天机器人

    来,你也可以用 C 语言写一个聊天机器人 你是不是一直在面对着枯燥的 C 语言特性.摸索着前人写过的各种算法,不是因为自己的兴趣,而是依靠自身的毅力,学得很苦吧. 好吧,我们找一个好玩一点的东西,一起 ...

  5. 利用itchat写一个聊天机器人

    利用itchat写一个聊天机器人 聊天机器人 图灵机器人 需要的库 **自动回复私聊消息** **自动回复群聊消息** 结语: 聊天机器人 偶然在CSDN上看到大佬用20行教你写一个聊天机器人,觉得甚 ...

  6. 【GitHub探索】v语言上手,用vlang写一个聊天应用

    前言 vlang(v语言)自从6月份突然炒热起来,不知不觉到了11月,正式版就要出来了,在11月的GitHub Trending榜中依然排在前10.这着实令人好奇,因此笔者决定试用一下vlang,写一 ...

  7. 30分钟搭一个wordpress网站

    这里是Z哥的个人公众号 每周五11:45 按时送达 当然了,也会时不时加个餐- 我的第「88」篇原创敬上 因为最近工作比较忙,没太多时间思考和写东西.所以今天偷个懒,发一篇实操类文章. 这篇文章非常& ...

  8. 如何用Java写一个聊天机器人

    文章目录 建议结合新版教程看 写在前面的的话 免责声明 你需要提前会的东西 我们要使用的框架 首先我们先下载一个Demo 文件配置 Demo里面的的目录结构 在配置文件中加上你小号的QQ名字和密码 我 ...

  9. 手把手教你用 30 分钟搭建一个网盘

    code小生 一个专注大前端领域的技术平台 公众号回复Android加入安卓技术群 本文出处:码匠笔记公众号 Pandownload 下线大家心里都很苦,不过我们还是的重新站起来,于是我研究了一下花了 ...

  10. [分享] 30分钟做一个二维码名片应用,有源码!

    2019独角兽企业重金招聘Python工程师标准>>> 前言 30分钟带你用Wex5做一个微信公众号上使用的二维码名片,相应技术点有详细讲解,高清有码!(点击下载全部源码) 二维码现 ...

最新文章

  1. 计算机二级vfp知识点,全国计算机二级等级考试VFP知识点提纲
  2. redhat linux新建用户,linux redhat 添加用户
  3. Windows 之 win10快捷键
  4. C指针原理(17)-C指针基础
  5. android graphics pipeline
  6. win10+tensorflow faster-RCNN 训练自己的数据集
  7. Python 分析国庆热门旅游景点,告诉你哪些地方好玩、便宜、人又少!
  8. 在Firefox中通过AJAX跨域访问Web资源
  9. 【网络学习笔记】Excel数据分析实战项目—淘宝用户画像
  10. 视频教程-初级学习ArcGIS Engine视频课程-C#
  11. js 设置 Cookie,cookie的作用域设置
  12. Unity移动端自动翻转及横竖屏的设置与检测
  13. SQL UCASE() 函数、 LCASE() 函数
  14. 计算机交互媒体应用范围,浅析交互媒体设计中的科技与艺术的关系
  15. enabled的使用
  16. java中break用法
  17. 诺基亚 android,诺基亚当年为什么走向没落也没用安卓系统?
  18. 分配工作时需要考虑的问题
  19. android widget 点击事件,Android Widget点击事件
  20. Python Scapy使用方法

热门文章

  1. 支付宝公布春节抢红包大数据 金华人全国第五壕
  2. 期货与期权的主要区别与联系?
  3. 商汤科技面试——CV算法面经
  4. ICRA 2020轨迹预测竞赛冠军的方法总结
  5. U盘启动盘制作(步骤详细)
  6. iOS 支付宝授权登录,思路
  7. 看得懂的三极管工作原理
  8. 下一代无服务器的发展形态: Serverless2.0
  9. Vue 图片加载错误处理(显示默认图片)
  10. 关于Palantir—第四部分:Palantir应用程序