Netty网络编程聊天项目

                                 Netty网络编程聊天项目

 

                                             

后端编写

 

导入依赖


  

 <dependencies>

        <dependency>

            <groupId>io.netty</groupId>

            <artifactId>netty-all</artifactId>

            <version>4.1.15.Final</version>

        </dependency>

    </dependencies>  
<build>

        <plugins>

            <plugin>

                <groupId>org.apache.maven.plugins</groupId>

                <artifactId>maven-compiler-plugin</artifactId>

                <version>3.1</version>

                <configuration>

                    <target>1.8</target>

                    <source>1.8</source>

                </configuration>

            </plugin>

        </plugins>

    </build>



   

 

 

编写Netty Server


 

public class WebsocketServer {
    public static void main(String[] args) throws InterruptedException {
        // 初始化主线程池(boss线程池)
        NioEventLoopGroup mainGroup = new NioEventLoopGroup();
        // 初始化从线程池(worker线程池)
        NioEventLoopGroup subGroup = new NioEventLoopGroup();

        try {
            // 创建服务器启动器
            ServerBootstrap b = new ServerBootstrap();

            // 指定使用主线程池和从线程池
            b.group(mainGroup, subGroup)
                    // 指定使用Nio通道类型
                    .channel(NioServerSocketChannel.class)
                    // 指定通道初始化器加载通道处理器
                    .childHandler(new WsServerInitializer());

            // 绑定端口号启动服务器,并等待服务器启动
            // ChannelFuture是Netty的回调消息
            ChannelFuture future = b.bind(9090).sync();
            // 等待服务器socket关闭
            future.channel().closeFuture().sync();
        } finally {
            // 优雅关闭boos线程池和worker线程池
            mainGroup.shutdownGracefully();
            subGroup.shutdownGracefully();
        }
    }
}

编写通道初始化器

 

public class WsServerInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();

        // ------------------
        // 用于支持Http协议

        // websocket基于http协议,需要有http的编解码器
        pipeline.addLast(new HttpServerCodec());
        // 对写大数据流的支持
        pipeline.addLast(new ChunkedWriteHandler());
        // 添加对HTTP请求和响应的聚合器:只要使用Netty进行Http编程都需要使用
        // 对HttpMessage进行聚合,聚合成FullHttpRequest或者FullHttpResponse
        // 在netty编程中都会使用到Handler
        pipeline.addLast(new HttpObjectAggregator(1024 * 64));


        // ---------支持Web Socket -----------------

        // websocket服务器处理的协议,用于指定给客户端连接访问的路由: /ws
        // 本handler会帮你处理一些握手动作: handshaking(close, ping, pong) ping + pong = 心跳
        // 对于websocket来讲,都是以frames进行传输的,不同的数据类型对应的frames也不同
        pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));

        // 添加自定义的handler
        pipeline.addLast(new ChatHandler());
    }
}

 

编写处理消息的ChannelHandler

 

/**
 * 处理消息的handler
 * TextWebSocketFrame: 在netty中,是用于为websocket专门处理文本的对象,frame是消息的载体
 */
public class ChatHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {

    // 用于记录和管理所有客户端的Channel
    private static ChannelGroup clients = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
        // 获取从客户端传输过来的消息
        String text = msg.text();
        System.out.println("接收到的数据:" + text);

        // 将接收到消息发送到所有客户端
        for(Channel channel : clients) {
            // 注意所有的websocket数据都应该以TextWebSocketFrame进行封装
            channel.writeAndFlush(new TextWebSocketFrame("[服务器接收到消息:]"
                    + LocalDateTime.now() + ",消息为:" + text));
        }
    }

    /**
     * 当客户端连接服务端之后(打开连接)
     * 获取客户端的channel,并且放入到ChannelGroup中去进行管理
     * @param ctx
     * @throws Exception
     */
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        // 将channel添加到客户端
        clients.add(ctx.channel());
    }

    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        // 当触发handlerRemoved,ChannelGroup会自动移除对应客户端的channel
        //clients.remove(ctx.channel());

        // asLongText()——唯一的ID
        // asShortText()——短ID(有可能会重复)
        System.out.println("客户端断开, channel对应的长id为:" + ctx.channel().id().asLongText());
        System.out.println("客户端断开, channel对应的短id为:" + ctx.channel().id().asShortText());
    }
}

 

      1. websocket以及前端代码编写

WebSocket protocol 是HTML5一种新的协议。它实现了浏览器与服务器全双工通信(full-duplex)。一开始的握手需要借助HTTP请求完成。Websocket是应用层第七层上的一个应用层协议,它必须依赖 HTTP 协议进行一次握手,握手成功后,数据就直接从 TCP 通道传输,与 HTTP 无关了。

 

前端编写


 

<!DOCTYPE html>
<html>
   <head>
      <meta charset="UTF-8">
      <title></title>
   </head>
   <body>
      <div>发送消息</div>
      <input type="text" id="msgContent" />
      <input type="button" value="点击发送" onclick="CHAT.chat()"/>
      
      <div>接收消息:</div>
      <div id="recMsg" style="background-color: gainsboro;"></div>
      
      <script type="application/javascript">
         window.CHAT = {
            socket: null,
            init: function() {
               // 判断浏览器是否支持websocket
               if(window.WebSocket) {
                  // 支持WebScoekt
                  // 连接创建socket,注意要添加ws后缀
                  CHAT.socket = new WebSocket("ws://127.0.0.1:9001/ws");
                  CHAT.socket.onopen = function() {
                     console.log("连接建立成功");
                  };
                  
                  CHAT.socket.onclose = function() {
                     console.log("连接关闭")
                  };
                  
                  CHAT.socket.onerror = function() {
                     console.log("发生错误");
                  };
                  
                  CHAT.socket.onmessage = function(e) {
                     console.log("接收到消息:" + e.data);
                     var recMsg = document.getElementById("recMsg");
                     var html = recMsg.innerHTML;
                     recMsg.innerHTML = html + "<br/>" + e.data;
                  };
                  
               }
               else {
                  alert("浏览器不支持websocket协议");
               }
            },
            chat: function() {
               var msg = document.getElementById("msgContent");
               CHAT.socket.send(msg.value);
            }
         }
         
         CHAT.init();
      </script>
   </body>
</html>

      1. MUI、HTML5+、HBuilder介绍

MUI介绍

 

http://dev.dcloud.net.cn/mui/

MUI是一个轻量级的前端框架。MUI以iOS平台UI为基础,补充部分Android平台特有的UI控件。MUI不依赖任何第三方JS库,压缩后的JS和CSS文件仅有100+K和60+K,可以根据自己的需要,自定义去下载对应的模块。并且MUI编写的前端可以打包成APK和IPA安装文件在手机端运行。也就是,编写一套代码,就可以在Android、IOS下运行

 

API地址http://dev.dcloud.net.cn/mui/ui/

 

H5+

 

H5+提供了对HTML5的增强提供了40WAPI给程序员使用。使用H5+ API可以轻松开发二维码扫描、摄像头、地图位置、消息推送等功能

 

 

API地址http://www.html5plus.org/doc/zh_cn/accelerometer.html#

 

HBuilder

 

前端开发工具。本次项目所有的前端使用HBuilder开发。在项目开发完后,也会使用HBuilder来进行打包Android/IOS的安装包

http://www.dcloud.io/

      1. MUI前端开发
        1. 创建项目/页面/添加MUI元素

创建MUI移动App项目

页面创建,添加组件

<header class="mui-bar mui-bar-nav">

<h1 class="mui-title">登录页面</h1>

</header>

<div class="mui-content">

<form class="mui-input-group">

<div class="mui-input-row">

<label>用户名</label>

<input type="text" class="mui-input-clear" placeholder="请输入用户名">

</div>

<div class="mui-input-row">

<label>密码</label>

<input type="password" class="mui-input-password" placeholder="请输入密码">

</div>

<div class="mui-button-row">

<button type="button" class="mui-btn mui-btn-primary">确认</button>

<button type="button" class="mui-btn mui-btn-danger">取消</button>

</div>

</form>

</div>

 

http://dev.dcloud.net.cn/mui/ui/#accordion

        1. 获取页面元素/添加点击事件

获取页面元素

mui.plusReady(function() {

// 使用document.getElementById来获取Input组件数据

var username = document.getElementById("username");

var password = document.getElementById("password");

var confirm = document.getElementById("confirm");



// 绑定事件

confirm.addEventListener("tap", function() {

alert("按下按钮");

});

});





批量绑定页面元素的点击事件

mui(".mui-table-view").on('tap','.mui-table-view-cell',function(){



});



使用原生JS的事件绑定方式

// 绑定事件

confirm.addEventListener("tap", function() {

alert("按下按钮");

});

 

        1. 发起ajax请求

前端

当我们点击确认按钮的时候,将用户名和密码发送给后端服务器

// 发送ajax请求

mui.ajax('http://192.168.1.106:9000/login', {

data: {

username: username.value,

password: password.value

},

dataType: 'json', //服务器返回json格式数据

type: 'post', //HTTP请求类型

timeout: 10000, //超时时间设置为10秒;

headers: {

'Content-Type': 'application/json'

},

success: function(data) {

// 可以使用console.log打印数据,一般用于调试

console.log(data);

},

error: function(xhr, type, errorThrown) {

//异常处理;

console.log(type);

}

});

 

 

后端

基于SpringBoot编写一个web应用,主要是用于接收ajax请求,响应一些数据到前端

@RestController
public class LoginController {

    @RequestMapping("/login")
    public Map login(@RequestBody User user) {
        System.out.println(user);

        Map map = new HashMap<String, Object>();

        if("tom".equals(user.getUsername()) && "123".equals(user.getPassword())) {
            map.put("success", true);
            map.put("message", "登录成功");
        }
        else {
            map.put("success", false);
            map.put("message", "登录失败,请检查用户名和密码是否输入正确");
        }

        return map;
    }
}

字符串转JSON对象以及JSON对象转字符串
将JSON对象转换为字符串



// 使用JSON.stringify可以将JSON对象转换为String字符串

console.log(JSON.stringify(data));



将字符串转换为JSON对象



var jsonObj = JSON.parse(jsonStr);

页面跳转
mui.openWindow({

url: 'login_succss.html',

id:'login_succss.html'

});

App客户端缓存操作
大量的App很多时候都需要将服务器端响应的数据缓存到手机App本地。

http://www.html5plus.org/doc/zh_cn/storage.html



在App中缓存的数据,就是以key-value键值对来存放的。







将数据放入到本地缓存中

var user =  {

username: username.value,

password: password.value

}

// 将对象数据放入到缓存中,需要转换为字符串

plus.storage.setItem("user", JSON.stringify(user));



从本地缓存中读取数据



// 从storage本地缓存中获取对应的数据

var userStr = plus.storage.getItem("user");

 

    1. 构建项目
      1. 项目功能需求技术架构介绍

功能需求

登录/注册

个人信息

搜索添加好友

好友聊天

 

技术架构

前端

开发工具:HBuilder 

框架:MUI、H5+

 

后端

开发工具IDEA

框架:Spring BootMyBatis、Spring MVC、FastDFS、Netty

数据库:mysql

 

      1. 使用模拟器进行测试

安装附件中的夜神Android模拟器nox_setup_v6.2.3.8_full.exe

 

双击桌面图标启动模拟器

 

安装后找到模拟器的安装目录

 

到命令行中执行以下命令

 

nox_adb connect 127.0.0.1:62001

nox_adb devices

 

 

进入到Hbuilder安装目录下的tools/adbs目录

 

切换到命令行中执行以下命令

adb connect 127.0.0.1:62001
adb devices

 

打开HBuilder开始调试

 

 

      1. 前端 - HBuilder前端项目导入

将资料中的heima-chat.zip解压并导入到HBuilder中

      1. 后端 - 导入数据库/SpringBoot项目/MyBatis逆向工程

导入数据库

将资料中的hchat.sql脚本在开发工具中执行

 

数据库表结构介绍

tb_user用户表

 

tb_friend朋友表

 

tb_friend_req申请好友表

 

tb_chat_record聊天记录表

 

 

 

使用MyBatis逆向工程生成代码

将资料中的generatorSqlmapCustom项目导入到IDEA中,并配置项目所使用的JDK

 

创建Spring Boot项目

拷贝资料pom.xml依赖

拷贝资料中的application.properties配置文件

 

      1. 后端 - Spring Boot整合Netty搭建后台

spring boot整合Netty

导入资料中配置文件中的spring-netty文件夹中的java文件

 

启动Spring Boot,导入HTML页面,使用浏览器打开测试Netty是否整合成功

 

    1. 业务开发 - 用户注册/登录/个人信息

 

      1. 用户登录功能 -后端开发

导入IdWorker.java雪花算法ID生成

 

初始化IdWorker


 

@SpringBootApplication
@MapperScan(basePackages = "com.itheima.hchat.mapper")
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class);
    }

    @Bean
    public IdWorker idWorker() {
        return new IdWorker(0, 0);
    }
}

创建Result实体类

 

/**
 * 将返回给客户端的数据封装到实体类中
 */
public class Result {
    private boolean success; // 是否操作成功
    private String message; // 返回消息
    private Object result; // 返回附件的对象

    public Result(boolean success, String message) {
        this.success = success;
        this.message = message;
    }

    public Result(boolean success, String message, Object result) {
        this.success = success;
        this.message = message;
        this.result = result;
    }

    public boolean isSuccess() {
        return success;
    }

    public void setSuccess(boolean success) {
        this.success = success;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public Object getResult() {
        return result;
    }

    public void setResult(Object result) {
        this.result = result;
    }
}

 

创建返回给客户端的User实体类


 

/**
 * 用来返回给客户端
 */
public class User {
    private String id;
    private String username;
    private String picSmall;
    private String picNormal;
    private String nickname;
    private String qrcode;
    private String clientId;
    private String sign;
    private Date createtime;
    private String phone;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPicSmall() {
        return picSmall;
    }

    public void setPicSmall(String picSmall) {
        this.picSmall = picSmall;
    }

    public String getPicNormal() {
        return picNormal;
    }

    public void setPicNormal(String picNormal) {
        this.picNormal = picNormal;
    }

    public String getNickname() {
        return nickname;
    }

    public void setNickname(String nickname) {
        this.nickname = nickname;
    }

    public String getQrcode() {
        return qrcode;
    }

    public void setQrcode(String qrcode) {
        this.qrcode = qrcode;
    }

    public String getClientId() {
        return clientId;
    }

    public void setClientId(String clientId) {
        this.clientId = clientId;
    }

    public String getSign() {
        return sign;
    }

    public void setSign(String sign) {
        this.sign = sign;
    }

    public Date getCreatetime() {
        return createtime;
    }

    public void setCreatetime(Date createtime) {
        this.createtime = createtime;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    @Override
    public String toString() {
        return "User{" +
                "username='" + username + '\'' +
                ", picSmall='" + picSmall + '\'' +
                ", picNormal='" + picNormal + '\'' +
                '}';
    }
}

UserController实现 


@RequestMapping("/login")
public Result login(@RequestBody TbUser user) {
    try {
        User _user = userService.login(user.getUsername(), user.getPassword());

        if(_user == null) {
            return new Result(false, "登录失败,将检查用户名或者密码是否正确");
        }
        else {
            return new Result(true, "登录成功", _user);
        }
    } catch (Exception e) {
        e.printStackTrace();
        return new Result(false, "登录错误");
    }
}

 

UserService接口定义


/**
 * 登录
 * @param user
 * @return
 */
User login(TbUser user);

 

编写UserServiceImpl实现


 

@Override
public User login(TbUser user) {
    TbUserExample example = new TbUserExample();
    TbUserExample.Criteria criteria = example.createCriteria();
    criteria.andUsernameEqualTo(user.getUsername());

    List<TbUser> userList = userMapper.selectByExample(example);
    if(userList != null && userList.size() == 1) {
        TbUser userInDB = userList.get(0);
        // MD5加密认证
        if(userInDB.getPassword().equals(DigestUtils.md5DigestAsHex(user.getPassword().getBytes()))) {
            return loadUserById(userInDB.getId());
        }
        else {
            throw new RuntimeException("用户名或密码错误");
        }
    }
    else {
        throw new RuntimeException("用户不存在");
    }
}

      1. 用户登录功能 - 前端&测试
      2. 注册功能 - 后端

UserController


@RequestMapping("/register")
public Result register(@RequestBody TbUser user) {
    try {
        userService.register(user);
        return new Result(true, "注册成功");
    } catch (RuntimeException e) {
        return new Result(false, e.getMessage());
    }
}

UserService接口


 

void register(TbUser user);

UserServiceImpl实现


@Override
public void register(TbUser user) {
    // 1. 查询用户是否存在
    TbUserExample example = new TbUserExample();
    TbUserExample.Criteria criteria = example.createCriteria();
    criteria.andUsernameEqualTo(user.getUsername());

    List<TbUser> userList = userMapper.selectByExample(example);

    // 1.1 如果存在抛出异常
    if(userList != null && userList.size() > 0 ) {
        throw new RuntimeException("用户名已经存在!");
    }
    else {
        user.setId(idWorker.nextId());
        // MD5加密保存
        user.setPassword(DigestUtils.md5DigestAsHex(user.getPassword().getBytes()));
        user.setPicSmall("");
        user.setPicNormal("");
        user.setNickname(user.getUsername());
        user.setQrcode(""); 
        user.setCreatetime(new Date());
        userMapper.insert(user);
    }
}
      1. 注册功能 - 前端&测试
      2. FASTDFS - 文件服务器介绍与搭建

什么是FastDFS

 

FastDFS 是用 c 语言编写的一款开源的分布式文件系统。FastDFS 为互联网量身定制,充分考虑了冗余备份、负载均衡、线性扩容等机制,并注重高可用、高性能等指标,使用 FastDFS很容易搭建一套高性能的文件服务器集群提供文件上传、下载等服务。

FastDFS 架构包括 Tracker server 和 Storage server。客户端请求 Tracker server 进行文件上传、下载,通过 Tracker server 调度最终由 Storage server 完成文件上传和下载。

Tracker server 作用是负载均衡和调度,通过 Tracker server 在文件上传时可以根据一些策略找到 Storage server 提供文件上传服务。可以将 tracker 称为追踪服务器或调度服务器。

Storage server 作用是文件存储,客户端上传的文件最终存储在 Storage 服务器上,Storageserver 没有实现自己的文件系统而是利用操作系统 的文件系统来管理文件。可以将storage称为存储服务器。

服务端两个角色:

Tracker:管理集群,tracker 也可以实现集群。每个 tracker 节点地位平等。收集 Storage 集群的状态。

Storage:实际保存文件   Storage 分为多个组,每个组之间保存的文件是不同的。每个组内部可以有多个成员,组成员内部保存的内容是一样的,组成员的地位是一致的,没有主从的概念。

 

在Linux中搭建FastDFS

 

解压缩fastdfs-image-server.zip

双击vmx文件,然后启动。

注意:遇到下列提示选择“我已移动该虚拟机”!

IP地址已经固定为192.168.25.133  ,请设置你的仅主机网段为25。

登录名为root  密码为itcast

 

      1. FASTDFS - 整合Spring Boot

 

导入ComponetImport.java工具类

 

导入FastDFSClient.javaFileUtils.java工具类

 

      1. 个人信息 - 后端照片上传功能开发

注入FastDFS相关Bean


@Autowired
private Environment env;
@Autowired
private FastDFSClient fastDFSClient;



编写UserController update Handler上传照片



@RequestMapping("/upload")
public Result upload(MultipartFile file, String userid) {
    try {
        // 上传
        String url = fastDFSClient.uploadFace(file);
        String suffix = "_150x150.";
        String[] pathList = url.split("\\.");
        String thumpImgUrl = pathList[0] + suffix + pathList[1];

        // 更新用户头像
        User user = userService.updatePic(userid, url, thumpImgUrl);
        user.setPicNormal(env.getProperty("fdfs.httpurl") + user.getPicNormal());
        user.setPicSmall(env.getProperty("fdfs.httpurl") + user.getPicSmall());

        return new Result(true, "上传成功", user);
    } catch (IOException e) {
        e.printStackTrace();
        return new Result(false, "上传失败");
    }
}

 

编写UserService

将新上传的图片保存到用户信息数据库中


 

/**
 * 更新用户头像
 * @param userid
 * @param url
 * @param thumpImgUrl
 */
User updatePic(String userid, String url, String thumpImgUrl);

 

编写UserServiceImpl

 

@Override
public User updatePic(String userid, String url, String thumpImgUrl) {
    TbUser user = userMapper.selectByPrimaryKey(userid);
    user.setPicNormal(url);
    user.setPicSmall(thumpImgUrl);;
    userMapper.updateByPrimaryKey(user);

    User userVo = new User();
    BeanUtils.copyProperties(user, userVo);
    return userVo;
}

 

      1. 个人信息 - 前端&测试头像上传

  

      1. 个人信息 - 修改昵称后端实现

 

编写UserController


@RequestMapping("/updateNickname")
public Result updateNickname(@RequestBody TbUser user) {
    try {
        userService.updateNickname(user.getId(), user.getNickname());
        return new Result(true, "修改成功");
    } catch (Exception e) {
        e.printStackTrace();
        return new Result(false, "修改失败");
    }
}

UserSevice接口


 

/**
 * 根据用户id更新用户昵称
 * @param userid
 * @param nickname
 */
void updateNickname(String userid, String nickname);

UserServiceImpl实现

 

@Override
public void updateNickname(String userid, String nickname) {
    System.out.println(userid);
    TbUser user = userMapper.selectByPrimaryKey(userid);
    user.setNickname(nickname);

    userMapper.updateByPrimaryKey(user);
}
      1. 个人信息 -重新加载用户信息后端实现

Controller

 

@RequestMapping("/findById")
public User findById(String userid) {
    return userService.findById(userid);
}

 

 

UserService


 

/**
 * 根据用户id查找用户信息
 * @param userid 用户id
 * @return 用户对象
 */
User findById(String userid);

UserServiceImpl


 

@Override
public User findById(String userid) {
    TbUser tbUser = userMapper.selectByPrimaryKey(userid);
    User user = new User();
    BeanUtils.copyProperties(tbUser, user);

    return user;
}

      1. 个人信息 - 修改昵称前端测试

 

      1. 个人信息 - 二维码生成后端编写

二维码是在用户注册的时候,就根据用户的用户名来自动生成一个二维码图片,并且保存到FastDFS中。

需要对注册的方法进行改造,在注册用户时,编写逻辑保存二维码。并将二维码图片的链接保存到数据库中。

 

二维码前端页面展示

 

 

导入二维码生成工具类

导入QRCodeUtils.java文件

 

UserServiceImpl

 

修改注册方法,在注册时,将使用二维码生成工具将二维码保存到FastDFS,并保存链接更新数据库

 

@Override
public void register(TbUser user) {
    // 1. 查询用户是否存在
    TbUserExample example = new TbUserExample();
    TbUserExample.Criteria criteria = example.createCriteria();
    criteria.andUsernameEqualTo(user.getUsername());

    List<TbUser> userList = userMapper.selectByExample(example);

    // 1.1 如果存在抛出异常
    if(userList != null && userList.size() > 0 ) {
        throw new RuntimeException("用户名已经存在!");
    }
    else {
        user.setId(idWorker.nextId());
        // MD5加密保存
        user.setPassword(DigestUtils.md5DigestAsHex(user.getPassword().getBytes()));
        user.setPicSmall("");
        user.setPicNormal("");
        user.setNickname(user.getUsername());

        // 获取临时目录
        String tmpFolder = env.getProperty("hcat.tmpdir");
        String qrCodeFile = tmpFolder + "/" + user.getUsername() + ".png";
        qrCodeUtils.createQRCode(qrCodeFile, "user_code:" + user.getUsername());
        try {
            String url = fastDFSClient.uploadFile(new File(qrCodeFile));
            user.setQrcode(url);
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException("上传文件失败");
        }
        user.setCreatetime(new Date());
        userMapper.insert(user);
    }
}
      1. 个人信息 - 二维码生成前端测试
    1. 业务开发 - 发现页面与通信录
      1. 搜索朋友 - 后端开发

在搜索朋友的时候需要进行以下判断:

  1. 不能添加自己为好友
  2. 如果搜索的用户已经是好友了就不能再添加了
  3. 如果已经申请过好友并且好友并没有处理这个请求了也不能再申请

 

前端页面展示

 

 

 

搜索朋友其实就是用户搜索,所以我们只需要根据用户名将对应的用户搜索出来即可。

 

编写UserController


@RequestMapping("/findUserById")
public User findUserById(String userid) {
    System.out.println(userid);
    return userService.loadUserById(userid);
}

编写UserService接口

 

/**
 * 根据用户id加载用户信息
 * @param userid
 * @return
 */
User findUserById(String userid);

 

 

编写UserServiceImpl实现


@Override
public User findUserById(String userid) {
    TbUser tbUser = userMapper.selectByPrimaryKey(userid);
    User user = new User();
    BeanUtils.copyProperties(tbUser, user);

    if(StringUtils.isNotBlank(user.getPicNormal())) {
        user.setPicNormal(env.getProperty("fdfs.httpurl") + user.getPicNormal());
    }
    if(StringUtils.isNotBlank(user.getPicSmall())) {
        user.setPicSmall(env.getProperty("fdfs.httpurl") + user.getPicSmall());
    }
    user.setQrcode(env.getProperty("fdfs.httpurl") + user.getQrcode());

    return user;
}

 

      1. 搜索朋友 - 前端测试联调
      2. 添加好友 - 发送好友请求后端开发

添加好友需要发送一个好友请求。

 

编写FriendController


@RequestMapping("/sendRequest")
public Result sendRequest(@RequestBody TbFriendReq tbFriendReq) {
    try {
        friendService.sendRequest(tbFriendReq);
        return new Result(true, "发送请求成功");
    }
    catch (RuntimeException e) {
        return new Result(false, e.getMessage());
    }
    catch (Exception e) {
        e.printStackTrace();
        return new Result(false, "发送请求失败");
    }
}

编写FriendService

 

/**
 * 发送好友请求
 */
void sendRequest(TbFriendReq friendReq);

 

编写FriendServiceImpl实现

 

@Override
public void sendRequest(TbFriendReq friendReq) {

    // 判断用户是否已经发起过好友申请
    TbFriendReqExample example = new TbFriendReqExample();
    TbFriendReqExample.Criteria criteria = example.createCriteria();
    criteria.andFromUseridEqualTo(friendReq.getFromUserid());
    criteria.andToUseridEqualTo(friendReq.getToUserid());

    List<TbFriendReq> friendReqList = friendReqMapper.selectByExample(example);

    if(friendReqList == null || friendReqList.size() == 0) {
        friendReq.setId(idWorker.nextId());
        friendReq.setCreatetime(new Date());
        // 设置请求未处理
        friendReq.setStatus(0);

        friendReqMapper.insert(friendReq);
    }
    else {
        throw new RuntimeException("您已经请求过了");
    }
}

 

      1. 添加好友 -前端测试
      2. 展示好友请求 -后端开发

前端页面展示

 

编写Controller


 

@RequestMapping("/findFriendReqByUserid")
public List<FriendReq> findMyFriendReq(String userid) {
    return friendService.findMyFriendReq(userid);
}

编写FriendService

 

/**
 * 根据用户id查找好友请求
 * @param userid
 * @return
 */
List<FriendReq> findMyFriendReq(String userid);

 

编写FriendServiceImpl实现

 

@Override
public List<FriendReq> findMyFriendReq(String userid) {
    // 查询好友请求
    TbFriendReqExample example = new TbFriendReqExample();
    TbFriendReqExample.Criteria criteria = example.createCriteria();
    criteria.andToUseridEqualTo(userid);
    // 查询没有处理的好友请求
    criteria.andStatusEqualTo(0);

    List<TbFriendReq> tbFriendReqList = friendReqMapper.selectByExample(example);
    List<FriendReq> friendReqList = new ArrayList<FriendReq>();

    // 加载好友信息
    for (TbFriendReq tbFriendReq : tbFriendReqList) {
        TbUser tbUser = userMapper.selectByPrimaryKey(tbFriendReq.getFromUserid());
        FriendReq friendReq = new FriendReq();
        BeanUtils.copyProperties(tbUser, friendReq);
        friendReq.setId(tbFriendReq.getId());
        // 添加HTTP前缀
        friendReq.setPicSmall(env.getProperty("fdfs.httpurl") + friendReq.getPicSmall());
        friendReq.setPicNormal(env.getProperty("fdfs.httpurl") + friendReq.getPicNormal());

        friendReqList.add(friendReq);
    }

    return friendReqList;
}
      1. 展示好友请求 - 前端测试
      2. 添加好友 - 接受好友请求后端开发

添加好友需要双方互相添加。

例如:A接受B的好友申请,则将A成为B的好友,同时B也成为A的好友。

 

编写FriendController

 

@RequestMapping("/acceptFriendReq")
public Result acceptFriendReq(String reqid) {
    try {
        friendService.acceptFriendReq(reqid);
        return new Result(true, "添加好友成功");
    } catch (Exception e) {
        e.printStackTrace();
        return new Result(false, "添加好友失败");
    }
}

 

编写FriendService

 

/**
 * 接受好友请求
 * @param reqid 好友请求ID
 */
void acceptFriendReq(String reqid);

 

编写FriendServiceImpl


@Override
public void acceptFriendReq(String reqid) {
    // 设置请求状态为1
    TbFriendReq tbFriendReq = friendReqMapper.selectByPrimaryKey(reqid);
    tbFriendReq.setStatus(1);
    friendReqMapper.updateByPrimaryKey(tbFriendReq);

    // 互相添加为好友
    // 添加申请方好友
    TbFriend friend1 = new TbFriend();
    friend1.setId(idWorker.nextId());
    friend1.setUserid(tbFriendReq.getFromUserid());
    friend1.setFriendsId(tbFriendReq.getToUserid());
    friend1.setCreatetime(new Date());

    // 添加接受方好友
    TbFriend friend2 = new TbFriend();
    friend2.setId(idWorker.nextId());
    friend2.setFriendsId(tbFriendReq.getFromUserid());
    friend2.setUserid(tbFriendReq.getToUserid());
    friend2.setCreatetime(new Date());

    friendMapper.insert(friend1);
    friendMapper.insert(friend2);

    // 发送消息更新通信录
    // 获取发送好友请求方Channel
    Channel channel = UserChannelMap.get(tbFriendReq.getFromUserid());
    if(channel != null){
        Message message = new Message();
        message.setType(4);

        channel.writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(message)));
    }
}

 

      1. 添加好友 -拒绝添加好友后端开发

在用户选择忽略好友请求时,我们只需要将之前的好友请求状态(status)设置为1。无需添加好友。

 

编写FriendController


 

@RequestMapping("/ignoreFriendReq")
public Result ignoreFriendReq(String reqid) {
    try {
        friendService.ignoreFriendReq(reqid);
        return new Result(true, "忽略成功");
    } catch (Exception e) {
        e.printStackTrace();
        return new Result(false, "忽略失败");
    }
}

编写FriendService接口


 

/**
 * 忽略好友请求
 * @param reqid 好友请求id
 */
void ignoreFriendReq(String reqid);

 

编写FriendServiceImpl实现

 

@Override
public void ignoreFriendReq(String reqId) {
    // 设置请求状态为1
    TbFriendReq tbFriendReq = friendReqMapper.selectByPrimaryKey(reqId);
    tbFriendReq.setStatus(1);
    friendReqMapper.updateByPrimaryKey(tbFriendReq);
}

 

      1. 通信录功能 - 后端

通信录功能就是要根据当前登录用户的id,获取到用户的好友列表。

 

前端页面效果

 

编写FriendController


 

/**
 * 根据用户id查询好友
 * @param userid
 * @return
 */
@RequestMapping("/findFriendsByUserid")
public List<User> findFriendsByUserid(String userid) {
    return friendService.findFriendsByUserid(userid);
}

编写FriendService

 

/**
 * 根据用户id查找好友
 * @param userid
 * @return
 */
List<User> findFriendsByUserid(String userid);

 

编写FriendServiceImpl

 

@Override
public List<User> findFriendsByUserid(String userid) {
    TbFriendExample example = new TbFriendExample();
    TbFriendExample.Criteria criteria = example.createCriteria();
    criteria.andUseridEqualTo(userid);

    List<TbFriend> tbFriendList = friendMapper.selectByExample(example);
    List<User> userList = new ArrayList<User>();

    for (TbFriend tbFriend : tbFriendList) {
        TbUser tbUser = userMapper.selectByPrimaryKey(tbFriend.getFriendsId());
        User user = new User();
        BeanUtils.copyProperties(tbUser, user);
        // 添加HTTP前缀
        user.setPicSmall(env.getProperty("fdfs.httpurl") + user.getPicSmall());
        user.setPicNormal(env.getProperty("fdfs.httpurl") + user.getPicNormal());

        userList.add(user);
    }

    return userList;
}

 

    1. 业务开发 - 聊天业务
      1. 聊天业务 - 用户id关联Netty通道后端开发

 

要使用netty来进行两个客户端之间的通信,需要提前建立好用户id与Netty通道的关联。

服务器端需要对消息进行保存。

 

每一个App客户端登录的时候,就需要建立用户id与通道的关联。

 

 

导入SpringUtil工具类

此工具类主要用来在普通Java类中获取Spring容器中的bean

 

定义消息实体类


 

public class Message implements Serializable{

    private Integer type;   // 消息类型
    private TbChatRecord chatRecord; // 消息体
    private String ext;             // 扩展字段

    // getter/setter
}

定义UserChannelMap用来保存用户id与Channel通道关联


 

public class UserChannelMap {
    public static HashMap<String, Channel> userChannelMap = new HashMap<>();

    public static void put(String userid, Channel channel) {
        userChannelMap.put(userid, channel);
    }

    public static Channel get(String userid) {
        return userChannelMap.get(userid);
    }
}

编写ChatHandller

 

用户在第一次登陆到手机App时,会自动发送一个type为0的消息,此时,需要建立用户与Channel通道的关联。后续,将会根据userid获取到Channel,给用户推送消息。

 

/**
 * 处理消息的handler
 * TextWebSocketFrame: 在netty中,是用于为websocket专门处理文本的对象,frame是消息的载体
 */
public class ChatHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {

    // 用于记录和管理所有客户端的Channel
    private static ChannelGroup clients = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
        // 1. 获取从客户端传输过来的消息
        String text = msg.text();

        // 2. 判断消息的类型,根据不同的消息类型执行不同的处理
        System.out.println(text);
        Message message = JSON.parseObject(text, Message.class);
        Integer type = message.getType();

        switch (type) {
            case 0:
                // 2.1 当websocket第一次Open的时候,初始化channel,channel关联到userid
                String userid = message.getChatRecord().getUserid();
                // 保存userid对应的channel
                UserChannelMap.put(userid, channel);

                for (Channel client : clients) {
                    System.out.println("客户端连接id:" + client.id());
                }

                // 打印当前在线用户
                for(String uid : UserChannelMap.userChannelMap.keySet()) {
                    System.out.print("用户id:" + uid + "\n\n");
                    System.out.println("Channelid:" + UserChannelMap.get(uid));
                }

                break;
            case 1:
                // 2.2 聊天记录保存到数据库,标记消息的签收状态[未签收]
                break;
            case 2:
                // 2.3 签收消息,修改数据库中的消息签收状态[已签收]
                // 表示消息id的列表
                break;
            case 3:
                // 2.4 心跳类型的消息
                break;
        }
    }


  

  /**
     * 当客户端连接服务端之后(打开连接)
     * 获取客户端的channel,并且放入到ChannelGroup中去进行管理
     * @param ctx
     * @throws Exception
     */
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        // 将channel添加到客户端
        clients.add(ctx.channel());
    }

    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        // 当触发handlerRemoved,ChannelGroup会自动移除对应客户端的channel
        clients.remove(ctx.channel());
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        // 抛出异常时移除通道
        cause.printStackTrace();
        ctx.channel().close();
        clients.remove(ctx.channel());
    }
}
      1. 聊天业务 - 用户断开连接、连接异常取消关联通道

服务器端应该根据通道的ID,来取消用户id与通道的关联关系。

 

UserChannelMap类


/**
 * 根据通道id移除用户与channel的关联
 * @param channelId 通道的id
 */
public static void removeByChannelId(String channelId) {
    if(!StringUtils.isNotBlank(channelId)) {
        return;
    }

    for (String s : userChannelMap.keySet()) {
        Channel channel = userChannelMap.get(s);
        if(channelId.equals(channel.id().asLongText())) {
            System.out.println("客户端连接断开,取消用户" + s + "与通道" + channelId + "的关联");
            userChannelMap.remove(s);
            break;
        }
    }
}

 

ChatHandler类


 

@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
    UserChannelMap.removeByChannelId(ctx.channel().id().asLongText());
    ctx.channel().close();
}

@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
    System.out.println("关闭通道");
    UserChannelMap.removeByChannelId(ctx.channel().id().asLongText());
    UserChannelMap.print();
}

      1. 聊天业务 - 发送聊天消息后端开发

将消息发送到好友对应的Channel通道,并将消息记录保存到数据库中

 

编写ChatHandler

 

获取ChatRecordService服务


 


Channel channel = ctx.channel();
ChatRecordService chatRecordService = (ChatRecordService) SpringUtil.getBean("chatRecordServiceImpl");



case 1:
    // 2.2 聊天记录保存到数据库,标记消息的签收状态[未签收]
    TbChatRecord chatRecord = message.getChatRecord();
    String msgText = chatRecord.getMessage();
    String friendid = chatRecord.getFriendid();
    String userid1 = chatRecord.getUserid();

    // 保存到数据库,并标记为未签收

    String messageId = chatRecordService.insert(chatRecord);
    chatRecord.setId(messageId);

    // 发送消息
    Channel channel1 = UserChannelMap.get(friendid);
    if(channel1 != null) {
        // 从ChannelGroup查找对应的额Channel是否存在
        Channel channel2 = clients.find(channel1.id());
        if(channel2 != null) {
            // 用户在线,发送消息到对应的通道
            System.out.println("发送消息到" + JSON.toJSONString(message));
            channel2.writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(message)));
        }
    }

    break;

编写ChatRecordService接口


 

/**
 * 保存聊天记录到服务器
 * @param chatRecord
 */
String insert(TbChatRecord chatRecord);

编写ChatRecordServiceImpl实现


 

@Override
public String insert(TbChatRecord chatRecord) {
    chatRecord.setId(idWorker.nextId());
    chatRecord.setHasRead(0);
    chatRecord.setCreatetime(new Date());
    chatRecord.setHasDelete(0);

    chatRecordMapper.insert(chatRecord);

    return chatRecord.getId();
}

      1. 聊天业务 - 加载聊天记录功能

根据userid和friendid加载未读的聊天记录

 

编写ChatRecordController


 

@RequestMapping("/findUnreadByUserIdAndFriendId")
public List<TbChatRecord> findUnreadByUserIdAndFriendId(String userid, String friendid) {
    return chatRecordService.findUnreadByUserIdAndFriendId(userid, friendid);
}

编写ChatRecordService

 

/**
 * 根据用户ID和朋友ID获取未读的消息
 * @param userid
 * @param friendId
 * @return
 */
List<TbChatRecord> findUnreadByUserIdAndFriendId(String userid, String friendId);

 

编写ChatRecordServiceImpl实现


 

@Override
public List<TbChatRecord> findUnreadByUserIdAndFriendId(String userid, String friendid) {
    TbChatRecordExample example = new TbChatRecordExample();
    TbChatRecordExample.Criteria criteria1 = example.createCriteria();

    criteria1.andUseridEqualTo(friendid);
    criteria1.andFriendidEqualTo(userid);
    criteria1.andHasReadEqualTo(0);
    criteria1.andHasDeleteEqualTo(0);

    TbChatRecordExample.Criteria criteria2 = example.createCriteria();
    criteria2.andUseridEqualTo(userid);
    criteria2.andFriendidEqualTo(friendid);
    criteria2.andHasReadEqualTo(0);
    criteria2.andHasDeleteEqualTo(0);

    example.or(criteria1);
    example.or(criteria2);

    // 加载未读消息
    List<TbChatRecord> chatRecordList = chatRecordMapper.selectByExample(example);

    // 将消息标记为已读
    for (TbChatRecord tbChatRecord : chatRecordList) {
        tbChatRecord.setHasRead(1);
        chatRecordMapper.updateByPrimaryKey(tbChatRecord);
    }

    return chatRecordList;
}

      1. 聊天业务 - 已读/未读消息状态标记

已读消息

当用户接收到聊天消息,且聊天窗口被打开,就会发送一条用来签收的消息到Netty服务器

用户打开聊天窗口,加载所有聊天记录,此时会把发给他的所有消息设置为已读

 

未读消息

如果用户没有打开聊天窗口,就认为消息是未读的

 

ChatRecordController


@RequestMapping("/findUnreadByUserid")
public List<TbChatRecord> findUnreadByUserid(String userid) {
    try {
        return chatRecordService.findUnreadByUserid(userid);
    } catch (Exception e) {
        e.printStackTrace();
        return new ArrayList<TbChatRecord>();
    }
}

 

ChatRecordService

 

/**
 * 设置消息为已读
 * @param id 聊天记录的id
 */
void updateStatusHasRead(String id);

 

ChatRecordServiceImpl


 

@Override
public void updateStatusHasRead(String id) {
    TbChatRecord tbChatRecord = chatRecordMapper.selectByPrimaryKey(id);
    tbChatRecord.setHasRead(1);

    chatRecordMapper.updateByPrimaryKey(tbChatRecord);
}

ChatHandler


case 2:
    // 将消息记录设置为已读
    chatRecordService.updateStatusHasRead(message.getChatRecord().getId());
    break;

 

      1. 聊天业务 - 未读消息读取

在用户第一次打开App的时候,需要将所有的未读消息加载到App

 

ChatRecordController


@RequestMapping("/findUnreadByUserid")
public List<TbChatRecord> findUnreadByUserid(String userid) {
    try {
        return chatRecordService.findUnreadByUserid(userid);
    } catch (Exception e) {
        e.printStackTrace();
        return new ArrayList<TbChatRecord>();
    }
}

ChatRecordService

 

/**
 * 根据用户id,查询发给他的未读消息记录
 * @param userid 用户id
 * @return 未读消息列表
 */
List<TbChatRecord> findUnreadByUserid(String userid);

 

ChatRecordServiceImpl


@Override
public List<TbChatRecord> findUnreadByUserid(String userid) {
    TbChatRecordExample example = new TbChatRecordExample();
    TbChatRecordExample.Criteria criteria = example.createCriteria();

    // 设置查询发给userid的消息
    criteria.andFriendidEqualTo(userid);
    criteria.andHasReadEqualTo(0);

    return chatRecordMapper.selectByExample(example);
}

    1. 业务开发 - 心跳机制
      1. Netty心跳处理以及读写超时设置

Netty并不能监听到客户端设置为飞行模式时,自动关闭对应的通道资源。我们需要让Netty能够定期检测某个通道是否空闲,如果空闲超过一定的时间,就可以将对应客户端的通道资源关闭。

 

编写后端Netty心跳检查的Handler

 

/**
 * 检测Channel的心跳Handler
 */
public class HeartBeatHandler extends ChannelInboundHandlerAdapter {
    // 客户端在一定的时间没有动作就会触发这个事件
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        // 用于触发用户事件,包含读空闲/写空闲
        if(evt instanceof IdleStateEvent) {
            IdleStateEvent event = (IdleStateEvent)evt;

            if(event.state() == IdleState.READER_IDLE) {
                System.out.println("读空闲...");
            }
            else if(event.state() == IdleState.WRITER_IDLE) {
                System.out.println("写空闲...");
            }
            else if(event.state() == IdleState.ALL_IDLE) {
                System.out.println("关闭客户端通道");
                // 关闭通道,避免资源浪费
                ctx.channel().close();
            }
        }
    }
}

 

在通道初始化器中(WebSocketInitailizer)添加心跳检查

 

// 增加心跳事件支持
// 第一个参数:  读空闲4秒
// 第二个参数: 写空闲8秒
// 第三个参数: 读写空闲12秒
pipeline.addLast(new IdleStateHandler(4, 8, 12));

pipeline.addLast(new HeartBeatHandler());

 代码示例下载:

链接:https://pan.baidu.com/s/1Y72ItY7XrcimUHDKuWJgiA 
提取码:4sw6 
 

 

 

 

  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_无往而不胜_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值