基于Netty+websocket实现IM即时通讯(简易版)

目录

1.什么是netty

2.netty使用场景

3.netty线程模型

4.搭建简易web聊天室 

   4.1依赖导入

  4.2目录结构

4.3编写Netty服务

 4.4编写Netty处理器

 4.5配置监听器项目启动开启Netty服务

  4.6启动并连接websocket

 使用js连接服务

 5.搭建登录页面

  6.搭建信息发送页面

7.测试


1.什么是netty

NIO 的类库和 API 繁杂, 使用麻烦: 需要熟练掌握Selector、 ServerSocketChannel、 SocketChannel、 ByteBuffer等。 开发工作量和难度都非常大: 例如客户端面临断线重连、 网络善断、心跳处理、半包读写、 网络拥塞和异常流的处 理等等。 Netty 对 JDK 自带的 NIO 的 API 进行了良好的封装,解决了上述问题。且Netty拥有高性能、 吞吐量更高,延迟更 低,减少资源消耗,最小化不必要的内存复制等优点。 Netty 现在都在用的是4.x,5.x版本已经废弃,Netty 4.x 需要JDK 6以上版本支持

2.netty使用场景

  1. 互联网行业:在分布式系统中,各个节点之间需要远程服务调用,高性能的 RPC 框架必不可少,Netty 作为异步 高性能的通信框架,往往作为基础通信组件被这些 RPC 框架使用。典型的应用有:阿里分布式服务框架 Dubbo 的 RPC 框架使用 Dubbo 协议进行节点间通信,Dubbo 协议默认使用 Netty 作为基础通信组件,用于实现。各进程节 点之间的内部通信。Rocketmq底层也是用的Netty作为基础通信组件。
  2. 游戏行业:无论是手游服务端还是大型的网络游戏,Java 语言得到了越来越广泛的应用。Netty 作为高性能的基 础通信组件,它本身提供了 TCP/UDP 和 HTTP 协议栈。
  3. 大数据领域:经典的 Hadoop 的高性能通信和序列化组件 Avro 的 RPC 框架,默认采用 Netty 进行跨界点通 信,它的 Netty Service 基于 Netty 框架二次封装实现

3.netty线程模型

 基于Netty+websocket实现IM即时通讯(简易版)_第1张图片

  1.  Netty 抽象出两组线程池BossGroup和WorkerGroup,BossGroup专门负责接收客户端的连接, WorkerGroup专 门负责网络的读写
  2. BossGroup和WorkerGroup类型都是NioEventLoopGroup
  3. NioEventLoopGroup 相当于一个事件循环线程组, 这个组中含有多个事件循环线程 , 每一个事件循环线程是 NioEventLoop
  4. 每个NioEventLoop都有一个selector , 用于监听注册在其上的socketChannel的网络通讯
  5. 每个Boss NioEventLoop线程内部循环执行的步骤有 3 步 处理accept事件 , 与client 建立连接 , 生成 NioSocketChannel 将NioSocketChannel注册到某个worker NIOEventLoop上的selector 处理任务队列的任务 , 即runAllTasks
  6. 每个worker NIOEventLoop线程循环执行的步骤 轮询注册到自己selector上的所有NioSocketChannel 的read, write事件 处理 I/O 事件, 即read , write 事件, 在对应NioSocketChannel 处理业务 runAllTasks处理任务队列TaskQueue的任务 ,一些耗时的业务处理一般可以放入TaskQueue中慢慢处 理,这样不影响数据在 pipeline 中的流动处理
  7. 每个worker NIOEventLoop处理NioSocketChannel业务时,会使用 pipeline (管道),管道中维护了很多 handler 处理器用来处理 channel 中的数据

4.搭建简易web聊天室 

   4.1依赖导入

      
        
            org.springframework.boot
            spring-boot-starter-thymeleaf
        
        
        
            org.springframework.boot
            spring-boot-starter-web
        
        
        
            org.projectlombok
            lombok
            true
        
        
            org.springframework.boot
            spring-boot-starter-test
            test
            
                
                    org.junit.vintage
                    junit-vintage-engine
                
            
        
        
        
            io.netty
            netty-all
            4.1.35.Final
        

        
        
            com.alibaba
            fastjson
            1.2.14
        

        
        
            com.baomidou
            mybatis-plus-boot-starter
            3.4.3
        
        
        
            mysql
            mysql-connector-java
            8.0.17
        
        
        
            org.springframework.boot
            spring-boot-starter-data-redis
        
        
            org.apache.commons
            commons-lang3
            3.12.0
        
        
            com.alibaba
            fastjson
            1.2.76
        

  4.2目录结构

                        ​​​​​​​        ​​​​​​​        基于Netty+websocket实现IM即时通讯(简易版)_第2张图片

4.3编写Netty服务

package com.wangjie.qqserver.server;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
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;

/**
 * @Author: Hello World
 * @Date: 2022/2/17 14:05
 * @Description:
 */
public class NettyServer {

    private final  int port;

    public NettyServer(int port) {
        this.port = port;
    }


    public void start(){

        //创建两个线程组boosGroup和workerGroup,含有的子线程NioEventLoop的个数默认为cpu核数的两倍
        //boosGroup只是处理链接请求,真正的和客户端业务处理,会交给workerGroup完成
        EventLoopGroup boosGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            //创建服务器的启动对象
            ServerBootstrap bootstrap = new ServerBootstrap();
            //使用链式编程来配置参数
            //设置两个线程组
            bootstrap.group(boosGroup,workerGroup)
                    //使用NioSctpServerChannel作为服务器的通道实现
                    .channel(NioServerSocketChannel.class)
                    //初始化服务器链接队列大小,服务端处理客户端链接请求是顺序处理的,所以同一时间只能处理一个客户端链接
                    //多个客户端同时来的时候,服务端将不能处理的客户端链接请求放在队列中等待处理
                    .option(ChannelOption.SO_BACKLOG,1024)
                    //创建通道初始化对象,设置初始化参数
                    .childHandler(new ChannelInitializer() {
                        @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 MessageHandler());//添加测试的聊天消息处理类
                            ch.pipeline().addLast(new WebSocketServerProtocolHandler("/ws", null, true, 65536 * 10));
                        }
                    });
            System.out.println("netty server start..");
            //绑定一个端口并且同步,生成一个ChannelFuture异步对象,通过isDone()等方法判断异步事件的执行情况
            //启动服务器(并绑定端口),bind是异步操作,sync方法是等待异步操作执行完毕
            ChannelFuture cf = bootstrap.bind(this.port).sync();
            //给cf注册监听器,监听我们关心的事件
            cf.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture channelFuture) throws Exception {
                    if (cf.isSuccess()){
                        System.out.println("监听端口9000成功");
                    }else {
                        System.out.println("监听端口9000失败");
                    }
                }
            });
            //对通道关闭进行监听,closeFuture是异步操作,监听通道关闭
            //通过sync方法同步等待通道关闭处理完毕,这里会阻塞等待通道关闭
            cf.channel().closeFuture().sync();

        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            boosGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }

    }

}

 4.4编写Netty处理器

package com.wangjie.qqserver.server;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.wangjie.qqserver.model.SocketMessage;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelId;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.util.concurrent.GlobalEventExecutor;

import java.util.concurrent.ConcurrentHashMap;

/**
 * @Author: Hello World
 * @Date: 2022/2/17 14:11
 * @Description:
 */
public class MessageHandler extends SimpleChannelInboundHandler {


    //GlobalEventExecutor.INSTANCE 是全局的事件执行器,是一个单例
    private static ChannelGroup channelGroup=  new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

    /**
     * 存储用户id和用户的channelId绑定
     */
    public static ConcurrentHashMap userMap = new ConcurrentHashMap<>();
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("客户端链接完成");
        //添加到group
        channelGroup.add(ctx.channel());
        ctx.channel().id();

    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("收到客户端消息");
        //首次连接是fullHttprequest,,把用户id和对应的channel对象存储起来
        if (msg != null && msg instanceof FullHttpRequest){
            FullHttpRequest request =(FullHttpRequest) msg;
            //获取用户参数
            Integer userId = getUrlParams(request.uri());
            //保存到登录信息map
            userMap.put(userId,ctx.channel().id());

            //如果url包含参数,需要处理
            if (request.uri().contains("?")) {
                String newUri = request.uri().substring(0, request.uri().indexOf("?"));
                request.setUri(newUri);
            }

        }else if (msg instanceof TextWebSocketFrame){
            //正常的text类型
            TextWebSocketFrame frame= (TextWebSocketFrame) msg;
            System.out.println("消息内容"+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)));
                System.out.println("推送群聊消息");
            } 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
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("与客户端断开");
        //移除channelGroup 通道组
        channelGroup.remove(ctx.channel());
    }

    @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);
    }
}

  接受消息实体

package com.wangjie.qqserver.model;

import lombok.Data;

/**
 * @author Scoot
 * @createTime 2020/3/4 19:58
 * @description 消息实体
 **/
@Data
public class SocketMessage {

    /**
     * 消息类型
     */
    private String messageType;
    /**
     * 消息发送者id
     */
    private Integer userId;
    /**
     * 消息接受者id或群聊id
     */
    private Integer chatId;
    /**
     * 消息内容
     */
    private String message;


}

 4.5配置监听器项目启动开启Netty服务

package com.wangjie.qqserver.listen;

import com.wangjie.qqserver.server.NettyServer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

/**
 * @Author: Hello World
 * @Date: 2022/2/17 14:15
 * @Description:
 */
@Component
public class NettyInitListen implements CommandLineRunner {
    
    //netty服务端口,配置文件中为1254 
    @Value("${netty.port}")
    Integer nettyPort;
    //springboot服务端口 8965
    @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());
        }
    }
}

  4.6启动并连接websocket

基于Netty+websocket实现IM即时通讯(简易版)_第3张图片

 使用js连接服务

    IndexController

   

package com.wangjie.qqserver.controller;

import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.GetMapping;

/**
 * @Author: Hello World
 * @Date: 2022/2/17 14:24
 * @Description:
 */
@Controller
public class IndexController {

    /**
     * 测试主页
     * @param id
     * @param modelMap
     * @return
     */
    @GetMapping("/index")
    public String toIndex(Integer id, ModelMap modelMap){
        modelMap.addAttribute("id",id);
        return "/html/index";
    }

    /**
     * login
     * @param
     * @param
     * @return
     */
    @GetMapping("/login")
    public String toLogin(){
        return "/login/index";
    }

    /**
     * login
     * @param
     * @param
     * @return
     */
    @GetMapping("/send")
    public String toSend(String token,ModelMap modelMap){
        modelMap.addAttribute("token",token);
        return "/send/index";
    }
}

 html/index 




    
    Title




 发送






 使用1号用户访问http://127.0.0.1:8965/index?id=1,可以看到这时候已经连接上websocket,

基于Netty+websocket实现IM即时通讯(简易版)_第4张图片

 5.搭建登录页面

基于Netty+websocket实现IM即时通讯(简易版)_第5张图片

登录控制器

package com.wangjie.qqserver.controller;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.wangjie.qqserver.common.AjaxResult;
import com.wangjie.qqserver.model.User;
import com.wangjie.qqserver.server.orm.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * @Author: Hello World
 * @Date: 2022/2/18 08:52
 * @Description:
 */
@RestController
@RequestMapping("/api")
public class LoginController {
    @Autowired
    UserService userService;
    @Autowired
    RedisTemplate redisTemplate;


    @PostMapping("/login")
    public AjaxResult login(User user){
        User obj = userService.getOne(new QueryWrapper().eq("username", user.getUsername()).eq("password", user.getPassword()));
        if (obj != null){
            //加入redis,生成token
            UUID uuid = UUID.randomUUID();
            redisTemplate.opsForValue().set(uuid.toString(),obj,200, TimeUnit.MINUTES);
            return new AjaxResult(200,"登录成功",uuid);
        }else {
            return new AjaxResult(500,"登录失败",null);
        }
    }

}

 Login.html






	某某公司后台登录系统
	

	
	
		
		
		
		
	
		



    

IM内部聊天系统

现在登录

用户:
密码:
记得我   
忘记了密码

  6.搭建信息发送页面

     好友列表,为测试数据。具体思路为当前用户id对应多个好友信息。通过查询获取列表生成展示

基于Netty+websocket实现IM即时通讯(简易版)_第6张图片

 













90后大叔
赵鹏

7.测试

基于Netty+websocket实现IM即时通讯(简易版)_第7张图片

 基于Netty+websocket实现IM即时通讯(简易版)_第8张图片

 基于Netty+websocket实现IM即时通讯(简易版)_第9张图片

 注:由于没有做持久化,当页面刷新时会清楚聊天记录,后期完善。

你可能感兴趣的:(websocket,网络协议,网络,java,netty)