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>(){
                    @Override
                    protected 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>{

    @Override
    protected 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);
            //分发到下一个Handler
            if(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> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, LoginRequest msg) throws Exception {
        LoginResponse response = new LoginResponse();
        Packet packet = new Packet();
      //校验用户名和密码合法,这里没有实现,可以自行加入数据库校验等实现
        if(valid(msg)){
            //随机生成ID
            String 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;
    }

 		//生成一个用户ID
    private static String randomUserId() {
        return UUID.randomUUID().toString().split("-")[0];
    }

    /**
     * channel没有链接到远程节点的时候
     * @param ctx
     * @throws Exception
     */
    @Override
    public 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> {
    @Override
    protected 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获取channel
        Channel 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;
    }

    @Override
    public 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(){
					//首先判断浏览器是否支持WebSocket
					if (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>

大致效果如下图:

用户1登陆

打开第二个标签页,再次登陆:
用户2小红登陆

此时会发现,第一个标签页会出现提示:

在这里插入图片描述
在第一个标签页输入小红ID,以及内容,在第二个标签页显示如下:

在这里插入图片描述

小红用户接收到消息,并在发送ID框上填充上小明的ID。此时可以进行回复,小明用户效果图如下:

在这里插入图片描述

到此演示完毕,这个demo主要是为了自己记忆练习netty的主要用法。问题很多,大佬轻喷。

github地址

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值