基于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线程模型

 

  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依赖导入

      <!--thymeleaf-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <!--web启动器-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!--netty-->
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.35.Final</version>
        </dependency>

        <!--json转换器-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.14</version>
        </dependency>

        <!--mybatis-plus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.3</version>
        </dependency>
        <!--Mysql jdbc驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.17</version>
        </dependency>
        <!-- redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.12.0</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.76</version>
        </dependency>

  4.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<SocketChannel>() {
                        @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<TextWebSocketFrame> {


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

    /**
     * 存储用户id和用户的channelId绑定
     */
    public static ConcurrentHashMap<Integer, ChannelId> 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

 使用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 

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<style type="text/css">
    .flexBox {display: flex;width: 100%;}
    .flexBox div {width: 50%;background-color: pink;}
    #messageBox ul {border: solid 1px #ccc;width: 600px;height: 400px}
</style>
<body>

 <a href="#" id="send">发送</a>

</body>
<!--在js脚本中获取作用域的值-->
<script th:inline="javascript">
    //获取session中的user
    var id=[[${id}]];
    var userId = id;//
    //获取ws服务地址
    var ws = "ws://192.168.0.231:1254/ws" //[[${ws}]]


</script>

<script type="text/javascript">
    var websocket;
    if (!window.WebSocket) {
        window.WebSocket = window.MozWebSocket;
    }
    if (window.WebSocket) {
        websocket = new WebSocket(ws + "?userId=" + userId);
        websocket.onmessage = function (event) {
            var json = JSON.parse(event.data);
            console.log(json)
            chat.onmessage(json);
        };
        console.log(websocket)
        websocket.onopen = function (event) {
            console.log("Netty-WebSocket服务器。。。。。。连接");
        };
        websocket.onclose = function (event) {
            console.log("Netty-WebSocket服务器。。。。。。关闭");
        };
        websocket.onerror = function(evt) {
            console.log('发生错误..., evt');
        };
        websocket.CONNECTING = function(evt) {
            console.log('正在链接中');
        };
    } else {
        alert("您的浏览器不支持WebSocket协议!");
    }
    //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
    window.onbeforeunload = function () {
        if (websocket != null) {
            websocket.close();
        }
    };
</script>

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

 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<User>().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

<!--
	Author: W3layouts
	Author URL: http://w3layouts.com
	License: Creative Commons Attribution 3.0 Unported
	License URL: http://creativecommons.org/licenses/by/3.0/
-->

<!DOCTYPE html>
<html>
<head>
	<title>某某公司后台登录系统</title>
	<link rel="stylesheet" href="css/style.css">

	<!--<link href='//fonts.googleapis.com/css?family=Open+Sans' rel='stylesheet' type='text/css'>
-->
	<!-- For-Mobile-Apps-and-Meta-Tags -->
		<meta name="viewport" content="width=device-width, initial-scale=1" />
		<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
		<meta name="keywords" content="Simple Login Form Widget Responsive, Login Form Web Template, Flat Pricing Tables, Flat Drop-Downs, Sign-Up Web Templates, Flat Web Templates, Login Sign-up Responsive Web Template, Smartphone Compatible Web Template, Free Web Designs for Nokia, Samsung, LG, Sony Ericsson, Motorola Web Design" />
		<script type="application/x-javascript"> addEventListener("load", function() { setTimeout(hideURLbar, 0); }, false); function hideURLbar(){ window.scrollTo(0,1); } </script>
	<!-- //For-Mobile-Apps-and-Meta-Tags -->
		<script src="/js/jquery-3.6.0.min.js"></script>
</head>

<body>
    <h1>IM内部聊天系统</h1>
    <div class="container w3">
        <h2>现在登录</h2>
		<form action="#" id="loginForm" method="post">
			<div class="username">
				<span class="username" style="height:19px">用户:</span>
				<input type="text" name="username" class="name" placeholder="" required="">
				<div class="clear"></div>
			</div>
			<div class="password-agileits">
				<span class="username"style="height:19px">密码:</span>
				<input type="password" name="password" class="password" placeholder="" required="">
				<div class="clear"></div>
			</div>
			<div class="rem-for-agile">
				<input type="checkbox" name="remember" class="remember">记得我
  
<br>
				<a href="#">忘记了密码</a><br>
			</div>
			<div class="login-w3">
					<input type="button" onclick="login()" class="login" value="Login">
			</div>
			<div class="clear"></div>
		</form>
	</div>
	<div class="footer-w3l">
		<p> IM内部聊天系统</p>
	</div>
</body>
<script>
	function login(){
		//序列化
		let form = $("#loginForm").serialize();
	    $.post("/api/login",form,function (result){
	    	if (result.code==200){
				location.href="/send?token="+result.data;
			}else {
	    		alert(result.msg);
			}
		})
	}


</script>

</html>

  6.搭建信息发送页面

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

<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title></title>

<link rel="stylesheet" type="text/css" href="/css/qq.css"/>

</head>
<body onload="init()">
<input type="hidden" id="token" th:value="${token}">
<input type="hidden" id="userId">
<div class="qqBox">
	<div class="BoxHead">
		<div class="headImg">
			<img id="head" src="../img/6.jpg"/>
		</div>
		<div class="internetName" id="nickName">90后大叔</div>
	</div>
	<div class="context">
		<div class="conLeft">
			<ul id="friends">

			</ul>
		</div>
		<div class="conRight">
			<div class="Righthead">
				<div class="headName">赵鹏</div>
				<input id="charId" type="hidden" value="">
				<div class="headConfig">
					<ul>
						<li><img src="img/20170926103645_06.jpg"/></li>
						<li><img src="img/20170926103645_08.jpg"/></li>
						<li><img src="img/20170926103645_10.jpg"/></li>
						<li><img src="img/20170926103645_12.jpg"/></li>
					</ul>
				</div>
			</div>
			<div class="RightCont">
				<ul class="newsList">

				</ul>
			</div>
			<div class="RightFoot">
				<div class="emjon">
					<ul>
						<li><img src="img/em_02.jpg"/></li>
						<li><img src="img/em_05.jpg"/></li>
						<li><img src="img/em_07.jpg"/></li>
						<li><img src="img/em_12.jpg"/></li>
						<li><img src="img/em_14.jpg"/></li>
						<li><img src="img/em_16.jpg"/></li>
						<li><img src="img/em_20.jpg"/></li>
						<li><img src="img/em_23.jpg"/></li>
						<li><img src="img/em_25.jpg"/></li>
						<li><img src="img/em_30.jpg"/></li>
						<li><img src="img/em_31.jpg"/></li>
						<li><img src="img/em_33.jpg"/></li>
						<li><img src="img/em_37.jpg"/></li>
						<li><img src="img/em_38.jpg"/></li>
						<li><img src="img/em_40.jpg"/></li>
						<li><img src="img/em_45.jpg"/></li>
						<li><img src="img/em_47.jpg"/></li>
						<li><img src="img/em_48.jpg"/></li>
						<li><img src="img/em_52.jpg"/></li>
						<li><img src="img/em_54.jpg"/></li>
						<li><img src="img/em_55.jpg"/></li>
					</ul>
				</div>
				<div class="footTop">
					<ul>
						<li><img src="img/20170926103645_31.jpg"/></li>
						<li class="ExP"><img src="img/20170926103645_33.jpg"/></li>
						<li><img src="img/20170926103645_35.jpg"/></li>
						<li><img src="img/20170926103645_37.jpg"/></li>
						<li><img src="img/20170926103645_39.jpg"/></li>
						<li><img src="img/20170926103645_41.jpg" alt="" /></li>
						<li><img src="img/20170926103645_43.jpg"/></li>
						<li><img src="img/20170926103645_45.jpg"/></li>
					</ul>
				</div>
				<div class="inputBox">
					<textarea id="message" style="width: 99%;height: 75px; border: none;outline: none;" name="" rows="" cols=""></textarea>
					<button class="sendBtn">发送(s)</button>
				</div>
			</div>
		</div>
	</div>
</div>
<script type="text/javascript" src="js/jquery.min.js"></script>
<script>
	var websocket;
	//初始化方法
  	function init(){
  		//获取token
		let token = $("#token").val();
  		//获取用户信息
		$.get("/user/info?token="+token,function (result){
			if(result.code!=200){
				alert("获取用户信息失败,请重新登录");
				location.href="/login";
				return ;
			}else {
				console.log(result)
				$("#nickName").text(result.data.nickName);
				$("#head").attr('src',result.data.photo);
				$("#userId").val(result.data.id);
				//建立websocket
				var userId = result.data.id;//
				//获取ws服务地址
				var ws = "ws://192.168.0.231:1254/ws" //[[${ws}]]
				if (!window.WebSocket) {
					window.WebSocket = window.MozWebSocket;
				}
				if (window.WebSocket) {
					websocket = new WebSocket(ws + "?userId=" + userId);
					websocket.onmessage = function (event) {
						var json = JSON.parse(event.data);

						// chat.onmessage(json);
						console.log("接受到消息"+json.message)
						console.log("发送者id"+json.userId)
						var str='';
						$.get("/user/get?id="+json.userId,function (result){
							str+='<li>'+
									'<div class="nesHead"><img src="'+result.data.photo+'"/></div>'+
									'<div class="news"><img class="jiao" src="img/20170926103645_03_02.jpg">'+json.message+'</div>'+
									'</li>';
							$('.newsList').append(str);
							$('.RightCont').scrollTop($('.RightCont')[0].scrollHeight );
						})

					};
					console.log(websocket)
					websocket.onopen = function (event) {
						console.log("Netty-WebSocket服务器。。。。。。连接");
					};
					websocket.onclose = function (event) {
						console.log("Netty-WebSocket服务器。。。。。。关闭");
					};
					websocket.onerror = function(evt) {
						console.log('发生错误..., evt');
					};
					websocket.CONNECTING = function(evt) {
						console.log('正在链接中');
					};
				} else {
					alert("您的浏览器不支持WebSocket协议!");
				}

			}
		})

	//获取好友列表
	$.get("/user/frends?token="+token,function (result){
			if(result.code!=200){
				alert("获取用户信息失败,请重新登录");
				location.href="/login";
				return ;
			}else {
				console.log("好友列表:"+result);
				for (let i = 0; i <result.data.length ; i++) {
					 if (i==0){
					 	$("#friends").append("<li class=\"bg\" onclick='next(this,"+result.data[i].id+")'><div class=\"liLeft\"><img  style=\"width: 43px;height: 41px\" src="+result.data[i].photo+"/></div><div class=\"liRight\"><span  class=\"intername\">"+result.data[i].nickName+"</span><span class=\"infor\">[流泪]</span></div></li>")
					 }else {
					 	 $("#friends").append("<li class=\"bg\" onclick='next(this,"+result.data[i].id+")'><div class=\"liLeft\"><img  style=\"width: 43px;height: 41px\" src="+result.data[i].photo+"/></div><div class=\"liRight\"><span  class=\"intername\">"+result.data[i].nickName+"</span><span class=\"infor\">[流泪]</span></div></li>")
					 }

				}
			}
		})
	}

    //切换好友
	function next(obj,userId){
		$(obj).addClass('bg').siblings().removeClass('bg');
		 var intername=$(obj).children('.liRight').children('.intername').text();
		 $('.headName').text(intername);
		 $('.newsList').html('');
		 //绑定用户id
		 $("#charId").val(userId);
	}


	//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
	window.onbeforeunload = function () {
		if (websocket != null) {
			websocket.close();
		}
	};


	//发送消息
	$('.sendBtn').on('click',function(){
		//新消息
		var news=$('#message').val();
		console.log("发送的消息"+news)
		//好友id
		let charId=$("#charId").val();
		if (charId == ''){
			alert("选择要发送的好友");
			return ;
		}
		if(news==''){
			alert('不能为空');
			return ;
		}else{
			let userId=$("#userId").val();
			//清空发送框内容
			$('#message').val('');
			if (websocket.readyState == WebSocket.OPEN) {
				var data = {};
				data.chatId = charId;
				data.message = news;
				data.userId = userId;
				data.messageType = "single";
				console.log("发送消息"+JSON.stringify(data))
				websocket.send(JSON.stringify(data));
				$.get("/user/get?id="+userId,function (result){
					var answer='';
					answer+='<li>'+
							'<div class="answerHead"><img src="'+result.data.photo+'"/></div>'+
							'<div class="answers"><img class="jiao" src="img/jiao.jpg">'+news+'</div>'+
							'</li>';
					$('.newsList').append(answer);
					$('.RightCont').scrollTop($('.RightCont')[0].scrollHeight );
				})

			} else {
				alert("和服务器连接异常!");
			}
		}
	})
</script>
</body>
</html>

7.测试

 

 

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

猜你喜欢

转载自blog.csdn.net/qq_37612049/article/details/123004388