目录
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使用场景
- 互联网行业:在分布式系统中,各个节点之间需要远程服务调用,高性能的 RPC 框架必不可少,Netty 作为异步 高性能的通信框架,往往作为基础通信组件被这些 RPC 框架使用。典型的应用有:阿里分布式服务框架 Dubbo 的 RPC 框架使用 Dubbo 协议进行节点间通信,Dubbo 协议默认使用 Netty 作为基础通信组件,用于实现。各进程节 点之间的内部通信。Rocketmq底层也是用的Netty作为基础通信组件。
- 游戏行业:无论是手游服务端还是大型的网络游戏,Java 语言得到了越来越广泛的应用。Netty 作为高性能的基 础通信组件,它本身提供了 TCP/UDP 和 HTTP 协议栈。
- 大数据领域:经典的 Hadoop 的高性能通信和序列化组件 Avro 的 RPC 框架,默认采用 Netty 进行跨界点通 信,它的 Netty Service 基于 Netty 框架二次封装实现
3.netty线程模型
- Netty 抽象出两组线程池BossGroup和WorkerGroup,BossGroup专门负责接收客户端的连接, WorkerGroup专 门负责网络的读写
- BossGroup和WorkerGroup类型都是NioEventLoopGroup
- NioEventLoopGroup 相当于一个事件循环线程组, 这个组中含有多个事件循环线程 , 每一个事件循环线程是 NioEventLoop
- 每个NioEventLoop都有一个selector , 用于监听注册在其上的socketChannel的网络通讯
- 每个Boss NioEventLoop线程内部循环执行的步骤有 3 步 处理accept事件 , 与client 建立连接 , 生成 NioSocketChannel 将NioSocketChannel注册到某个worker NIOEventLoop上的selector 处理任务队列的任务 , 即runAllTasks
- 每个worker NIOEventLoop线程循环执行的步骤 轮询注册到自己selector上的所有NioSocketChannel 的read, write事件 处理 I/O 事件, 即read , write 事件, 在对应NioSocketChannel 处理业务 runAllTasks处理任务队列TaskQueue的任务 ,一些耗时的业务处理一般可以放入TaskQueue中慢慢处 理,这样不影响数据在 pipeline 中的流动处理
- 每个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.测试
注:由于没有做持久化,当页面刷新时会清楚聊天记录,后期完善。