在线聊天室(B/S、servlet、websocket、JDBC)

一、项目简介

该聊天室项目使用的是B/S架构,实现一个在线聊天室, 实现注册、登录、在线用户群聊、不同频道等功能。

后端技术

后端使用Java语言,主要的技术有servlet、websocket、JDBC、json等;

  • servlet:配置资源映射
  • websocket:websocket是一个应用层的协议,使用websocket来实现“消息推送”
  • JDBC:实现数据库连接管理
  • json:解析和封装数据,使用Gson库

使用的开发工具是idea,项目构建/部署工具:maven,项目服务器:tomcat

项目需求

项目需求分析:

  1. 打开主页,看到登陆页面
  2. 登陆成功后,进入主页面.
  3. 主页面显示当前具有的频道列表(当前用户关注了哪些频道)
  4. 点击某个频道到之后,可以看到该频道中的消息、(有某个用户在该频道中发的消息。所有在线用户就都能看到)
  5. 每个用户都能发送消息
  6. 历史消息功能,用户登陆之后,能够查看到之前自己错过的消息(从用户上次下线开始到这次上线这段时间累积的消息)

数据库设计

用户表(user):

  • 用户id:userId ---->int 主键:primary key(不为空,不能重复),自增:auto_increment(数据库自己自增,不需要开发人员设置)
  • 用户名:name ----->varchar(50) 不唯一:unique
  • 密码:password ---->varchar(50)
  • 昵称:nickName ----->vaichar(50)
  • 上次退出时间:lastlogout ---->datetime

消息表(message):

  • 消息id:messageId ------>int 主键:primary key(不为空,不能重复),自增:auto_increment
  • 用户id:userId ------>int
  • 频道id:channelid ------>int
  • 消息正文:context -------->text
  • 消息发送时间:sendTime----->datetime

频道表(channel):

  • 频道id:channelId----->主键:primary key(不为空,不能重复),自增:auto_increment
  • 频道名字:chanellName---->varchar(50)

项目结构

在这里插入图片描述

二、项目具体实现

1、前后端交互数据

前端

前端请求后端如何获取数据?

我们知道,前端请求数据的格式有很多,常见的请求体数据格式:

  • text/html
  • image/png
  • text/css
  • applocation/javascript
  • application/json

我们采取 application/json;通过读取请求body内容:请求体的内容也就是一个字符串串,从前端接收的是json格式的数据;

后端

作为后端,我们应该如何解析这个请求体的数据呢?主要分为以下三步:
1)首先,需要通过字节流的方式读取body的内容,返回一个字符串

/*我们要拿到请求体的数据(body):
 * body中的数据是一个字符串,对于这个字符串我们按照他的长度进行读取 ,单位是字节
 * 所以通过字节流来读取
* */
public class Util {
    public static String readBody(HttpServletRequest request)
    {
        //使用字节流读取
        //读取body的长度,单位是字节(多少字节)
        int contentLength=request.getContentLength();
        //buffer来保存读到的body的内容
        byte[] buffer=new byte[contentLength];

        //使用 语法就不需要手动调用close方法来关闭流
        try(InputStream inputStream=request.getInputStream())
        {
            inputStream.read(buffer,0,contentLength);
        }catch (IOException e)
        {
            e.printStackTrace();
        }
        return new String(buffer);
    }
}

2)然后借助json的工具将json字符串数据(也就是请求体的数据)转为Java的对象(自己封装的Request对象)

我们这里以注册模块接收数据为例:
定义Java类:

	//请求的数据内容
    static class Request{
        public String name;
        public String password;
        public String nickName;
    }
    //响应的数据内容
    //要把这个对象再转为json字符串,并写回给客户端
    static class Response{
        public int ok;//是否表示响应成功:1表示成功;0表示失败
        public String reason;
    }

创建一个gson对象:
我们借助第三方库GSON(Google的库)来完成(json的第三方库有很多,例如fastjson、jackson等)

Gson gson=new GsonBuilder().create();

3)设置服务器给前端的响应

  //返回一个注册成功的响应结果
response.ok=1;
response.reason="注册成功";
  //设置服务器给前端的响应:application/json:json格式
resp.setContentType("application/json;charset=utf-8");
String jsonString=gson.toJson(response);//将Java对象转化为json的字符串
resp.getWriter().write(jsonString);//通过响应对象写回

详细代码:

@WebServlet
public class RegisterServlet extends HttpServlet {

    //创建一个gson对象
    Gson gson=new GsonBuilder().create();

    //这个类以内部类的形式组织,这个request类只针对HttpServlet类来使用
    //其他servlet对于的request类结构可能不相同,所以使用内部类
    //从body的json中转换过来
    static class Request{
        public String name;
        public String password;
        public String nickName;

    }

    //响应的数据内容
    //要把这个对象再转为json字符串,并写回给客户端
    static class Response{
        public int ok;//是否表示响应成功:1表示成功;0表示失败
        public String reason;
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        Response response=new Response();
        try {
            //1.读取body中的信息(json格式的字符串)------>通过Util类里边封装的方法按字节读取请求体的数据,并以字符串形式返回
            String bodyImfomation=Util.readBody(req);

            //2.把json数据转成Java中的对象
            //  创建一个Request类来表示这次请求的结构,把bodyImfomation----->Requestd对象
            //  此处借助第三方库GSON(Google的库)来完成(json的第三方库有很多,例如fastjson、jackson等)
            Request request=gson.fromJson(bodyImfomation,Request.class);

            //3.在数据库中查一下看看用户名是否已经存在,如果存在就认为注册失败
            UserDao userDao=new UserDao();
            User user= userDao.selectUserByName(request.name);
            if(user!=null)
            {
                throw new ChatroomException("该用户已经存在") ;
            }

            user=new User();

            //4.把新的用户名和密码构造成User对象并插入数据库中
            user.setName(request.name);
            user.setPassword(request.password);
            user.setNickName(request.nickName);
            System.out.println(user.toString());
            userDao.add(user);

            //5.返回一个注册成功的响应结果
            response.ok=1;
            response.reason="注册成功";
        } catch (JsonSyntaxException  | ChatroomException e) {
            e.printStackTrace();
            response.ok=0;//代表注册失败
            response.reason=e.getMessage();//getMessage()获取的就是catch中捕获的异常
        }finally {
            //设置服务器给前端的响应:application/json:json格式
            resp.setContentType("application/json;charset=utf-8");
            String jsonString=gson.toJson(response);//将Java对象转化为json的字符串
            resp.getWriter().write(jsonString);//通过响应对象写回
        }

    }
}

2、数据库连接(JDBC)

数据库我们使用的是MySQL,Java就是用JDBC建立连接,同时使用DataSource建立长连接,提高效率;采用单例模式保证线程安全;有关于数据库连接的具体介绍见博客:JDBC操作步骤.

具体就不再重复赘述,直接给出下面代码:

代码:

public class DBUtil {
    public static final String URL="jdbc:mysql://localhost:3306/java_chatroom?useUnicode=true&serverTimezone=GMT%2B8&characterEncoding=utf-8&useSSL=true";
    public static final String root="root";
    public static final String password="";

    public static volatile DataSource dataSource;

    //获取数据源:单例模式
    public static DataSource getDataSource()
    {
        if(dataSource==null)
        {
            synchronized (DBUtil.class)
            {
                if(dataSource==null)
                {
                    dataSource=new MysqlDataSource();
                    ((MysqlDataSource) dataSource).setURL(URL);
                    ((MysqlDataSource) dataSource).setUser(root);
                    ((MysqlDataSource) dataSource).setPassword(password);
                }
            }
        }
        return dataSource;
    }

    //建立连接
    public static java.sql.Connection getConnection()
    {
        try {
            return getDataSource().getConnection();
        } catch (SQLException e) {
            e.printStackTrace();
            throw new ChatroomException( "获取数据库连接失败");
        }
    }
    public static void close(java.sql.Connection connection, Statement statement)
    {
        close(connection,statement,null);
    }

    //关闭连接
    public static void close(java.sql.Connection connection, Statement statement, ResultSet resultSet)
    {
        try {
            if(resultSet!=null)
            {
                resultSet.close();
            }
            if(statement!=null)
            {
                statement.close();
            }
            if(connection!=null)
            {
                connection.close();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

3、不同频道群聊模块

websocket介绍

在聊天模块主要就是使用websocket协议进行通信,websocket相对于http有什么优势呢?

  • Http:都是典型的“一问一答”这样的模型,客户端先给服务器发送一个请求(request),然后服务器根据请求得到一个响应(Response),服务器再把这个响应写回给浏览器。所以HTTP对于消息推送可以实现,但是效率很低
  • websocket:是一个应用层的协议,websocket就是为了解决上面的问题,避免轮询来实现一个更科学的消息推送机制。
    • 通过websoket这个协议,来完成原生的socketAPI的用法在这里插入图片描述
    • 本来在网页中只能操作THHP协议,而websocket给网页更多的操作网络的方式
    • websocket 本质上给浏览器提供了更加灵活的网络传输机制
    • websocket可以想象成类似于tcp这样的一个基础协议,在这个基础协议上,用户就可以构建更加复杂的网络通信程序。例如实现“消息推送”。

使用websocket,我们首先要引入他的依赖:

<dependency>
    <groupId>javax.websocket</groupId>
    <artifactId>javax.websocket-api</artifactId>
    <version>1.1</version>
    <scope>provided</scope>
</dependency>

然后进行通信的servlet必须加上注解:

//用来实现websocket接口
//此处userId是一个变量,客户端链接的时候,具体提交的userId视情况而定
//服务器需要获取到具体的userId是什么
@ServerEndpoint(value = "/message/{userId}")

先介绍几个基于websocket的几个方法:

  • @OnOpen:客户建立连接时被调用
  • @OnClose:客户端断开连接时被调用
  • @OnMessage:在服务器收到客户端的请求时调用
  • @OnError:连接意外终止时(连接出现问题),就会调用

了解了什么是websocket以及他的用法,那我们来看看不同频道的群聊是如何实现的。

具体实现

首先,我们需要定义一个MessageCenter类和MessageAPI类

MessageCenter类:

  • 它的主要功能是:管理消息、管理用户列表、实现消息转发
  • 这个类作为一个单例来实现(因为保存消息列表和用户列表只需要保存一份就可以)
  • 是一个生产者消费者模型,消息队列在生产消息,线程在消费(每次把生产的消息转发给每一个在线用户)

代码:

public class MessageCenter {

    private volatile static MessageCenter instance=null;
    //获取单例对象
    public static MessageCenter getInstance()
    {
        if(instance==null)
        {
            synchronized (MessageCenter.class)
            {
                if(instance==null)
                {
                    instance=new MessageCenter();
                }
            }
        }
        return instance;
    }

    //下面要构造两个数据结构
    //  1.保存消息的队列,采用阻塞队列:阻塞队列方便多线程的处理,方便实现生产者消费之模型机制,更好的处理工作线程之间的冲突
    private BlockingDeque<Message> messages=new LinkedBlockingDeque<>();
    //  2.保存在线用户列表:多线程下ConcurrentMap相比于Hashtable的效率更高
    private ConcurrentHashMap<Integer,Session> onlineUsers=new ConcurrentHashMap<>();


    //获取所有的在线用户 用户id,Session
    public ConcurrentHashMap<Integer,Session> selectOnlineUser()
    {
        return onlineUsers;
    }

    //实现操作这两个数据结构的方法
    //1.用户上线:把用户添加到ConcurrentMap中
    public void addOnlineUser(int userId,Session session)
    {
        onlineUsers.put(userId,session);
    }
    //2.用户下线:把当前用户从ConcurrentMap中移除
    public void delOnlineUser(int userId)
    {
        onlineUsers.remove(userId);
    }
    //3.新增消息
    public void addMessage(Message message) throws InterruptedException
    {
        messages.put(message);
    }
    //创建一个线程来扫描消息队列,把里边存的消息转发给所有的在线用户
    //在构造MessageCenter实例的时候就创建这个线程
    //服务器一收到消息就会转发n次(n是在线用户数)
    private MessageCenter(){
        Thread thread=new Thread()
        {
            @Override
            public void run() {
                Gson gson=new GsonBuilder().create();
                while (true)
                {

                    try {
                        //1.从队列中尝试取消息
                        //如果队列是空此时take就会阻塞
                        Message message=messages.take();
                        //2.把消息转化成json字符出
                        String jsonString =gson.toJson(message);

                        //3.遍历在线用户表,把消息发给每个用户
                        for (ConcurrentHashMap.Entry<Integer,Session> entry:onlineUsers.entrySet()) {
                            Session session=entry.getValue();
                            //通过websocket的session发给在线的用户
                            session.getBasicRemote().sendText(jsonString);
                        }
                    } catch (InterruptedException | IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        thread.start();
    }
}

MessageAPI类:

这个类用来实现websocket接口,重写@OnOpen、@OnClose等这些方法。

代码:

@ServerEndpoint(value = "/message/{userId}")
public class MessageAPI {

    Gson gson=new GsonBuilder().create();
    //通过用户连接的url中获取到
    private int userId;


    //在建立连接时,要知道用户的id,通过websocket的url传给服务器
    //此处@PathParam注解将url中的userId对应到连接处理函数(onOpen)的参数(将url中userId的值赋给参数userId)
    @OnOpen
    public void onOpen(@PathParam("userId")String userIdStr, Session session)
    {
        //1.获取userId
        this.userId=Integer.parseInt(userIdStr);
        System.out.println("连接已建立,用户id:"+userId);
        //2.把该用户加入到在新用户列表(只要有用户上线就把它加入列表中)
        MessageCenter.getInstance().addOnlineUser(userId,session);
        //3.拉取历史消息并转发给在线用户
        //  获取此用户上次退出时间
        UserDao userDao=new UserDao();
        User user=userDao.selectUserById(userId);
        Timestamp lastLogout=user.getLastLogout();
        System.out.println("上次退出的时间为:"+lastLogout);

        //把所有的在线用户 响应给前端---->在线好友列表
        //需要循环通知所有在线用户
        try {
            //得到当前所有在线用户的id和对应的session
            //然后通过当前用户的session给所有在线的用户发送消息
            ConcurrentHashMap<Integer,Session> onlineUsers =MessageCenter.getInstance().selectOnlineUser();

            //获取所有的用户
            List<User> users=new ArrayList<>();
            for (Map.Entry<Integer,Session>  e:onlineUsers.entrySet()) {
                 users.add(userDao.selectUserById(e.getKey()));
            }
            String jsonuserStr=gson.toJson(users);

            //循环通过session发送给每一个在线用户:当前有多少用户在线
            for (ConcurrentHashMap.Entry<Integer,Session> e:onlineUsers.entrySet()) {
                e.getValue().getBasicRemote().sendText(jsonuserStr);
            }

        } catch (IOException e) {
            e.printStackTrace();
        }

        //  获取上次退出时间之前的所有消息,应该是获取每个频道内的历史消息

        //拿到所有的频道id
        ChannelDao channelDao=new ChannelDao();
        List<Channel> channels=channelDao.selectALL();
        for (Channel channel:channels) {
            MessageDao messageDao=new MessageDao();
            List<Message> messages=messageDao.selectByTimeStamp(channel.getChannelId(),lastLogout,new Timestamp(System.currentTimeMillis()));
            System.out.println("获取到"+channel.getChannelName()+"一共有"+messages.size()+"条消息");
            try {
                for (Message message: messages) {
                    System.out.println("message:"+message.toString());
                    String jsonString=gson.toJson(message+user.getName());
                    session.getBasicRemote().sendText(jsonString);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }

    @OnClose
    public void onClose()
    {
        System.out.println("连接断开!"+userId);
        //把用户从在线列表中删掉
        MessageCenter.getInstance().delOnlineUser(userId);
        //更新用户下线时间
        UserDao userDao=new UserDao();
        userDao.updateLogout(userId);
    }
    @OnError
    public void onError(Session session,Throwable error)
    {
        System.out.println("连接出现错误!"+userId);
        error.printStackTrace();
        //把用户从在线列表中删除
        MessageCenter.getInstance().delOnlineUser(userId);
    }

    @OnMessage
    public void onMessage(String request,Session session)
    {
        System.out.println("收到消息!"+userId+":"+request);
        //1.将受到的消息(request:是一个json形式的字符串)解析为message格式
        Message message=gson.fromJson(request,Message.class);
        //2.把消息的发送时间填写一下
        message.setSendTime(new Timestamp(System.currentTimeMillis()));
        //3.把消息写入消息中心(消息中心的阻塞队列)
        try {
            MessageCenter.getInstance().addMessage(message);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //4.把消息写入数据库
        MessageDao messageDao=new MessageDao();
        messageDao.add(message);
    }
}

此项目的核心模块代码已展出,对于基本的数据库CRUD操作在此就不赘述了~~

  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值