基于Socket实现的Java聊天小项目


项目说明

编写该项目的目的在于对之前所学的JavaSE部分的知识进行一个基本的综合运用和回顾。本次项目基于Socket实现,IO模型为阻塞IO-BIO(使用非阻塞IO-NIO或者Netty做聊天案例的文章可能后续会写一下),涵盖的知识点有面向对象、集合、泛型、反射、JDBC、线程基础等部分,项目基于Maven构建(导入依赖比较方便,懒得找依赖jar包),UI界面只使用控制台打印的方式(GUI部分的知识没了解过,学习这部分知识意义不大)。JDK版本为1.8,数据库为MySQL,版本为5.7,开发工具为IDEA。


项目功能说明

本次聊天项目要实现的主要功能有:注册、登录、在线用户列表、私聊、群聊、文件发送、服务端推送新闻。

一、项目准备

1.1 创建maven多模块工程

使用IDEA创建一个maven工程项目(关于Maven的知识不再阐述):

mychat模块为父模块,所以可以删掉父模块的src目录:
在这里插入图片描述本次项目子模块主要分三部分:服务端[chatserver模块]、客户端[chatclient模块]、公共依赖模块[common模块],创建完毕后目录结构如下:
在这里插入图片描述

1.2 项目依赖导入

在父模块的pom.xml文件中设置编码、导入依赖等。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.zkt.study</groupId>
    <artifactId>mychat</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>chatserver</module>
        <module>chatclient</module>
        <module>common</module>
    </modules>

    <properties>
        <java.version>1.8</java.version>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

    <dependencies>
        <!--MYSQL JDBC驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>

        <!--Druid连接池-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.10</version>
        </dependency>


        <!--测试-->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        
    </dependencies>

</project>

在这里插入图片描述
因为子模块中的chatserver模块、chatclient模块都要依赖公共模块,所在分别在这两个子模块中引入公共模块。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>mychat</artifactId>
        <groupId>com.zkt.study</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>chatserver</artifactId>

    <dependencies>
        <dependency>
            <groupId>com.zkt.study</groupId>
            <artifactId>common</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

</project>

到此,项目基本结构就搭建完成了。

1.3 公共模块设置

该模块主要是设置客户端、服务端共用的一些数据、Java类等,如常量类、工具类、数据库连接等(后续有其他需要继续补充)。
在这里插入图片描述

1.4 测试数据库连接

本次数据库连接池使用了Druid连接池(实际上在这个基础的小项目作用不大,只是作为一个知识点复习而已),配置文件如下:

driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://数据库ip地址/数据库?useSSL=false&rewriteBatchedStatements=true
username=用户名
password=密码
# 初始连接数
initialSize=10
# 最多保持5个空闲连接
minIdle=5
# 最大连接数
maxActive=20
# 最大超时等待时间
maxWait=5000

编写数据库连接相关工具类如下:

public class DruidJdbcUtil {
    private DruidJdbcUtil() {}

    private static DataSource ds;

    /**
     * 静态代码块:完成一些初始化工作
     */
    static {
        try {
            //加载配置文件: 从类路径加载
            InputStream is = DruidJdbcUtil.class.getClassLoader().getResourceAsStream(ChatConstant.DB_PROP_NAME);
            Properties prop = new Properties();
            prop.load(is);
            //创建数据库连接池 => 数据源
            ds = DruidDataSourceFactory.createDataSource(prop);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 获取数据库连接
     *
     * @return
     */
    public static Connection getConnection() {
        try {
            return ds.getConnection();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 重载
     *
     * @param conn
     */
    public static void close(Connection conn) {
        close(null, null, conn);
    }

    /**
     * 资源释放
     *
     * @param rs
     * @param stat
     * @param conn
     */
    public static void close(ResultSet rs, Statement stat, Connection conn) {
        try {
            if (rs != null) {
                rs.close();
            }
            if (stat != null) {
                stat.close();
            }
            if (conn != null) {
                conn.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

测试数据库连接是否正常:

public class DbTest {

    @Test
    public void testGetConnection() {
        Connection conn = DruidJdbcUtil.getConnection();
        System.out.println(conn);
    }
}

出现如下结果即表示获取数据库连接正常:
在这里插入图片描述

1.5 Server模块包说明

ChatServer模块的包层级设置如下。
在这里插入图片描述
app:编写服务端相关代码。dao:数据库操作层(持久层)。entity:存放实体类(实际是放在Common模块…)。service:业务层。

1.6 服务端基本代码编写

服务端基于ServerSocket实现,监听的端口为9999。

public class ChatServer {
    private ServerSocket serverSocket;

    /**
     * 服务端初始化
     */
    public ChatServer() {
        try {
            serverSocket = new ServerSocket(ChatConstant.SERVER_PORT);
            System.out.println("服务器在9999端口监听~");
            while (true) {
                //等待客户端连接 => 连接成功后获得交互的socket
                Socket socket = serverSocket.accept();
                //后续相关业务处理
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (null != serverSocket) {
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

服务端启动如下(控制台界面有点单调,那就打印一只小企鹅吧):

public class ChatServerApplication {
    public static void main(String[] args) {
        System.out.println("服务端,启动!");
        bannerPrint();
        new ChatServer();
    }

    /**
     * 打印启动图标
     */
    private static void bannerPrint() {
        String[] linuxPenguin = {
                "    .--.",
                "   |o_o |",
                "   |:_/ |",
                "  //   \\ \\",
                " (|     | )",
                "/'\\_   _/`\\",
                "\\___)=(___/"
        };

        for (String line : linuxPenguin) {
            System.out.println(line);
        }
    }
}

在这里插入图片描述

1.7 客户端基本代码编写

客户端控制台界面分为一级菜单、二级菜单,一级菜单主要是用户注册、登录、退出等功能界面,二级菜单则是对应的聊天、群聊等功能界面。

public class ClientView {
    /**
     * 循环控制
     */
    private boolean loop = true;

    /**
     * 显示客户端主界面
     */
    public void mainMenu() throws Exception {
        while (loop) {
            System.out.println("************** 欢迎来到聊天通信系统 ******************");
            System.out.println("*                                                 *");
            System.out.println("*                                                 *");
            System.out.println("*               1 用户登录                         *");
            System.out.println("*               2 用户注册                         *");
            System.out.println("*               9 退出                            *");
            System.out.println("*                                                *");
            System.out.println("*                                                *");
            System.out.println("***************************************************");
            System.out.print("请输入您的选择:");
            String key = ScannerUtil.readString(1);
            switch (key) {
                case "1":
                    System.out.println("用户登录处理");
                    //登录业务逻辑
                    break;
                case "2":
                    System.out.println("用户注册处理");
                    //注册业务逻辑
                    break;
                case "9":
                    loop = false;
                    break;
                default:
                    break;
            }
        }
    }
}

客户端启动如下:

public class ChatClientApplication {
    public static void main(String[] args) throws Exception {
        new ClientView().mainMenu();
        System.out.println("客户端退出~");
    }
}

在这里插入图片描述

二、功能开发

2.1 注册功能

2.1.1 分析

用户在界面输入用户名、密码后向服务端发送请求,服务端接收该请求并实现用户注册,注册成功后在数据库的用户表插入对应用户数据,再向客户端回复注册成功消息。

2.1.1.1 用户表设计

用户表user创建如下(只设置一些基础字段,实际开发中表结构当然不会这么简单):

drop table if exists user;
create table user (
	id int primary key auto_increment comment ' -- 自增主键',
	userId VARCHAR(50) comment ' -- 用户编号',
	username VARCHAR(20) comment ' -- 用户名',
	password VARCHAR(50) comment ' -- 用户密码',
	registerTime VARCHAR(36) comment ' -- 注册时间'
) comment ' 用户表';
2.1.1.2 实体类创建

为了方便操作数据库数据,一般来说一张数据库表会映射到一个Java实体类,故在entity包下创建User类:

public class User implements Serializable {
    private static final long serialVersionUID = 3524151099125702500L;

    private Integer id;
    private String userId;
    private String username;
    private String password;
    private String registerTime;

    public User() {
    }
    
	public User(String username, String password) {
        this.username = username;
        this.password = password;
    }

    public User(Integer id, String userId, String username, String password, String registerTime) {
        this.id = id;
        this.userId = userId;
        this.username = username;
        this.password = password;
        this.registerTime = registerTime;
    }

    public Integer getId() {
        return id;
    }

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

    public String getUserId() {
        return userId;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }

    public String getUsername() {
        return username;
    }

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

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getRegisterTime() {
        return registerTime;
    }

    public void setRegisterTime(String registerTime) {
        this.registerTime = registerTime;
    }
}

Java对象在网络中传输时,需要实现序列化Serializable 接口。

2.1.1.3 Dao层封装

为了方便对数据库进行操作,可以封装一个BasicDao(只涉及一些简单的逻辑):

public class BasicDao<T> {
    private Connection conn;
    private PreparedStatement ps;
    /**
     * 默认开启事务 -> 手动提交
     */
    protected boolean isAutoCommit = false;

    /**
     * 是否开启事务
     *
     * @param isAutoCommit
     */
    protected void setAutoCommit(boolean isAutoCommit) {
        this.isAutoCommit = isAutoCommit;
    }

    /**
     * 封装prepareStatement参数
     */
    private void setParamsForPs(PreparedStatement ps, Object... params) throws SQLException {
        int len = params.length;
        //ps占位下标从1开始
        for (int i = 0, psIndex = 1; i < len; i++, psIndex++) {
            //判断参数类型
            Object param = params[i];
            //字符串
            if (param instanceof String) {
                ps.setString(psIndex, (String) param);
            }
            //整数
            if (param instanceof Integer) {
                ps.setInt(psIndex, (Integer) param);
            }
            //浮点数
            if (param instanceof Double) {
                ps.setDouble(psIndex, (Double) param);
            }
            //----其他类型
        }
    }

    /**
     * DML操作 -> 增删改
     *
     * @param sql
     * @param params
     * @return
     */
    public int doUpdate(String sql, Object... params) {
        int res = 0;
        try {
            //获取数据库连接
            conn = DruidJdbcUtil.getConnection();
            //开启事务
            conn.setAutoCommit(false);
            ps = conn.prepareStatement(sql);
            //ps参数设置
            setParamsForPs(ps, params);
            //执行sql
            res = ps.executeUpdate();
            //提交事务
            conn.commit();
        } catch (Exception e) {
            //回滚
            try {
                conn.rollback();
            } catch (SQLException throwables) {
                throwables.printStackTrace();
            }
            e.printStackTrace();
        } finally {
            //资源释放
            DruidJdbcUtil.close(null, ps, conn);
        }

        return res;
    }

    /**
     * 查询单个
     *
     * @param sql
     * @param clazz
     * @param params
     * @return
     * @throws Exception
     */
    public T doQueryOne(String sql, Class<T> clazz, Object... params) throws Exception {
        T obj = null;
        int count = 0;
        try {
            conn = DruidJdbcUtil.getConnection();
            ps = conn.prepareStatement(sql);
            //参数封装
            setParamsForPs(ps, params);
            //执行查询
            ResultSet rs = ps.executeQuery();
            while (rs.next()) {
                ++count;
                if (count > 1) {
                    throw new RuntimeException("单个查询返回结果异常!");
                }

                //反射创建对象
                obj = clazz.newInstance();
                //获取类属性
                Field[] declaredFields = clazz.getDeclaredFields();
                for (Field declaredField : declaredFields) {
                    //取消访问检查
                    declaredField.setAccessible(true);
                    //属性名
                    String fieldName = declaredField.getName();
                    //属性类型
                    Class<?> fieldType = declaredField.getType();
                    //String
                    if (fieldType == String.class) {
                        //rs字符串取值
                        String fieldVal = rs.getString(fieldName);
                        //属性赋值
                        declaredField.set(obj, fieldVal);
                    }
                    //Integer
                    if (fieldType == Integer.class) {
                        //rs字符串取值
                        Integer fieldVal = rs.getInt(fieldName);
                        //属性赋值
                        declaredField.set(obj, fieldVal);
                    }
                    //Double
                    if (fieldType == Double.class) {
                        //rs字符串取值
                        Double fieldVal = rs.getDouble(fieldName);
                        //属性赋值
                        declaredField.set(obj, fieldVal);
                    }
                    //其他类型
                }
            }
        } finally {
            //资源释放
            DruidJdbcUtil.close(null, ps, conn);
        }

        return obj;
    }

    /**
     * 查询多个
     *
     * @param sql
     * @param clazz
     * @param params
     * @return
     * @throws Exception
     */
    public List<T> doQueryMany(String sql, Class<T> clazz, Object... params) throws Exception {
        List<T> resList = new ArrayList<>();
        try {
            conn = DruidJdbcUtil.getConnection();
            ps = conn.prepareStatement(sql);
            //参数封装
            setParamsForPs(ps, params);
            //执行查询
            ResultSet rs = ps.executeQuery();
            while (rs.next()) {
                //反射创建对象
                T one = clazz.newInstance();
                //获取类属性
                Field[] declaredFields = clazz.getDeclaredFields();
                for (Field declaredField : declaredFields) {
                    //取消访问检查
                    declaredField.setAccessible(true);
                    //属性名
                    String fieldName = declaredField.getName();
                    //属性类型
                    Class<?> fieldType = declaredField.getType();
                    //String
                    if (fieldType == String.class) {
                        //rs字符串取值
                        String fieldVal = rs.getString(fieldName);
                        //属性赋值
                        declaredField.set(one, fieldVal);
                    }
                    //Integer
                    if (fieldType == Integer.class) {
                        //rs字符串取值
                        Integer fieldVal = rs.getInt(fieldName);
                        //属性赋值
                        declaredField.set(one, fieldVal);
                    }
                    //Double
                    if (fieldType == Double.class) {
                        //rs字符串取值
                        Double fieldVal = rs.getDouble(fieldName);
                        //属性赋值
                        declaredField.set(one, fieldVal);
                    }
                }
                //加入集合
                resList.add(one);
            }
        } finally {
            //资源释放
            DruidJdbcUtil.close(null, ps, conn);
        }

        return resList;
    }

}

当需要操作对应数据库用户数据时,创建一个UserDao并继承BasicDao即可:

public class UserDao extends BasicDao<User> {
}
2.1.1.4 消息类型设置

为了让服务端知道本次客户端请求的类型,需要维护一个消息类型MessageType枚举类,因为客户端、服务端都会使用到该枚举类,所以该类放在公共模块里面。

public enum MessageType {
    /**
     * 用户注册
     */
    CHAT_USER_REGISTER("1");

    /**
     * 消息类型
     */
    private String type;

    /**
     * 构造器
     * @param type
     */
    MessageType(String type) {
        this.type = type;
    }
}
2.1.1.5 通讯消息类Message

为了方便服务端和客户端的消息通讯,可以封装一个Message消息类,该类的成员变量包括发送者、接收者、消息类型、消息内容等。

public class Message<T> implements Serializable {
    private static final long serialVersionUID = -8621512324130711419L;
    /**
     * 发送者
     */
    private String sender;
    /**
     * 接收者
     */
    private String getter;
    /**
     * 消息内容
     */
    private T content;
    /**
     * 发送时间
     */
    private String sendTime;
    /**
     * 消息类型
     */
    private String msgType;

    public Message() {
    }

    public Message(String sender, String getter, T content, String sendTime, String msgType) {
        this.sender = sender;
        this.getter = getter;
        this.content = content;
        this.sendTime = sendTime;
        this.msgType = msgType;
    }

    public String getSender() {
        return sender;
    }

    public void setSender(String sender) {
        this.sender = sender;
    }

    public String getGetter() {
        return getter;
    }

    public void setGetter(String getter) {
        this.getter = getter;
    }

    public T getContent() {
        return content;
    }

    public void setContent(T content) {
        this.content = content;
    }

    public String getSendTime() {
        return sendTime;
    }

    public void setSendTime(String sendTime) {
        this.sendTime = sendTime;
    }

    public String getMsgType() {
        return msgType;
    }

    public void setMsgType(String msgType) {
        this.msgType = msgType;
    }
}

2.1.2 服务端代码实现

用户业务处理接口UserService:

public interface UserService {
    /**
     * 根据用户名查询用户信息
     *
     * @param username
     * @return
     */
    User queryByUsername(String username) throws Exception;

    /**
     * 用户注册处理
     * @param user
     * @return
     * @throws Exception
     */
    boolean userRegister(User user) throws Exception;
}

用户业务处理实现类UserServiceImpl:

public class UserServiceImpl implements UserService {
    private final UserDao userDao = new UserDao();

    /**
     * 根据用户名查询用户信息
     *
     * @param username
     * @return
     */
    @Override
    public User queryByUsername(String username) throws Exception {
        String sql = "select * from user where username = ?";
        return userDao.doQueryOne(sql, User.class, username);
    }

    /**
     * 用户注册处理
     * @param user
     * @return
     * @throws Exception
     */
    @Override
    public boolean userRegister(User user) throws Exception {
        String sql = "insert into user(userId,username,password,registerTime) values(?,?,?,?)";
        //为每位注册的用户生成一个唯一标识 => UUID
        String userId = CommonUtil.createUuid();
        String registerDate = CommonUtil.getNowDate();
        int i = userDao.doUpdate(sql, userId, user.getUsername(), user.getPassword(), registerDate);
        return i > 0;
    }
}

服务端代码:

public class ChatServer {
    private ServerSocket serverSocket;
    private final UserService userService = new UserServiceImpl();

    /**
     * 服务端初始化
     */
    public ChatServer() {
        try {
            serverSocket = new ServerSocket(ChatConstant.SERVER_PORT);
            System.out.println("服务器在9999端口监听~");
            while (true) {
                //等待客户端连接 => 连接成功后获得交互的socket
                Socket socket = serverSocket.accept();
                //读取客户端发送的消息
                ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
                Message<User> msg = (Message<User>) ois.readObject();
                //取出User对象
                User user = msg.getContent();
                //消息类型
                String msgType = msg.getMsgType();
                //服务端回复客户端的消息
                Message<String> replyMsg = new Message<>();

                //用户注册处理
                if (MessageType.CHAT_USER_REGISTER.getType().equals(msgType)) {
                    //用户名是否重复
                    User u = userService.queryByUsername(user.getUsername());
                    if (Objects.isNull(u)) {
                        //可以注册
                        if (Objects.isNull(u)) {
                        //可以注册
                        if (userService.userRegister(user)) {
                            replyMsg.setMsgType(MessageType.USER_REGISTER_SUCCESS.getType());
                            replyMsg.setContent("注册成功!");
                        } else {
                            replyMsg.setMsgType(MessageType.USER_REGISTER_FAIL.getType());
                            replyMsg.setContent("注册异常!");
                        }
                    } else {
                        //当前用户名已存在,不可注册
                        replyMsg.setMsgType(MessageType.USER_REGISTER_FAIL.getType());
                        replyMsg.setContent("当前用户名已被注册!");
                    }
                }

                //回复客户端消息
                ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
                oos.writeObject(replyMsg);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (null != serverSocket) {
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

2.1.3 客户端代码实现

ClientUserService专门用于处理客户端用户相关的业务,如用户注册、用户登录等。

public class ClientUserService {
    /**
     * 和服务端保持通讯的socket
     */
    private Socket socket;
    /**
     * 用户对象
     */
    private User user;

    /**
     * 发送用户注册请求消息
     * @param username
     * @param password
     */
    public void sendUserRegisterMsg(String username, String password) {
        user = new User(username, password);
        Message<User> registerMsg = new Message<>();
        registerMsg.setMsgType(MessageType.CHAT_USER_REGISTER.getType());
        registerMsg.setContent(user);
        try {
            socket = new Socket(ChatConstant.SERVER_IP, ChatConstant.SERVER_PORT);
            //发送
            ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
            oos.writeObject(registerMsg);
            //等待服务端回复[阻塞]
            ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
            Message<String> replyMsg = (Message<String>) ois.readObject();
            System.out.println("********* " + replyMsg.getContent() + " *********");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

客户端注册输入页面:

public class ClientView {
    /**
     * 循环控制
     */
    private boolean loop = true;

    /**
     * 客户端用户相关业务处理
     */
    private final ClientUserService clientUserService = new ClientUserService();

    /**
     * 显示客户端主界面
     */
    public void mainMenu() throws Exception {
        while (loop) {
            System.out.println("************** 欢迎来到聊天通信系统 ******************");
            System.out.println("*                                                 *");
            System.out.println("*                                                 *");
            System.out.println("*               1 用户登录                         *");
            System.out.println("*               2 用户注册                         *");
            System.out.println("*               9 退出                            *");
            System.out.println("*                                                *");
            System.out.println("*                                                *");
            System.out.println("***************************************************");
            //用户输入
            System.out.print("请输入您的选择:");
            String key = ScannerUtil.readString(1);
            switch (key) {
                case "1":
                    System.out.println("用户登录处理");
                    //登录业务逻辑
                    break;
                case "2":
                    System.out.print("请输入用户名: ");
                    String regUsername = ScannerUtil.readString(20);
                    System.out.print("请输入密码: ");
                    String regPassword = ScannerUtil.readString(20);
                    clientUserService.sendUserRegisterMsg(regUsername, regPassword);
                    break;
                case "9":
                    loop = false;
                    break;
                default:
                    break;
            }
        }
    }
}

2.1.4 注册功能测试

分别启动服务端、客户端。在控制台输入用户名、密码进行注册:
在这里插入图片描述
查看数据库的user表:
在这里插入图片描述
密码这种敏感信息一般需要加密处理,不能以明文形式直接显示。本项目中采用MD5进行加密处理,加密方法放在通用工具类中:

public class CommonUtil {
    private CommonUtil() {}

    /**
     * 生成UUID序列号
     * @return
     */
    public static String createUuid() {
        return UUID.randomUUID().toString().replace("-", "");
    }

    /**
     * MD5加密工具
     * @param pwdStr
     * @return
     */
    public static String encrypByMD5(String pwdStr) {
        StringBuilder sb = new StringBuilder();
        try {
            MessageDigest digest = MessageDigest.getInstance("MD5");
            byte[] bytes = digest.digest(pwdStr.getBytes());
            for (byte b :bytes){
                sb.append(Integer.toString((b & 0xff) + 0x100, 16).substring(1));
            }
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }

        return sb.toString();
    }

    /**
     * 获取当前日期 -> 格式yyyy-MM-dd HH:mm:ss
     * @return
     */
    public static String getNowDate() {
        return getNowDate("yyyy-MM-dd HH:mm:ss");
    }

    /**
     * 获得当前日期
     * @param datePattern 日期格式
     * @return
     */
    public static String getNowDate(String datePattern) {
        LocalDateTime now = LocalDateTime.now(ZoneId.of("GMT+8"));
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern(datePattern);
        return dtf.format(now);
    }
}

在插入数据库前需要进行加密处理:

/**
     * 用户注册处理
     * @param user
     * @return
     * @throws Exception
     */
    @Override
    public boolean userRegister(User user) throws Exception {
        String sql = "insert into user(userId,username,password,registerTime) values(?,?,?,?)";
        //为每位注册的用户生成一个唯一标识 => UUID
        String userId = CommonUtil.createUuid();
        String registerDate = CommonUtil.getNowDate();
        //密码加密处理
        String md5Password = CommonUtil.encrypByMD5(user.getPassword());
        int i = userDao.doUpdate(sql, userId, user.getUsername(), md5Password, registerDate);
        return i > 0;
    }

在这里插入图片描述

再次测试注册,可以看到密码已经是按加密后的形式存储。
如果再次按用户名ikun进行注册,则注册失败:
在这里插入图片描述
到此注册功能实现完毕。

2.2 用户登录

2.2.1 功能分析

用户向服务端发送用户名和密码信息,服务端进行相应的登录逻辑处理。因为后续需要实现用户之前的私聊以及群聊等功能,所以在用户登录成功以后,应该要和服务端保持通讯,此时线程就登场了。此外,登录的用户有多个,所以服务端和客户端都需要维护一个管理各个用户和服务端保持通讯的线程的集合。如下图所示:
在这里插入图片描述

2.2.2 服务端代码实现

服务端和客户端保持通讯的线程ServerConnClientThread:

public class ServerConnClientThread extends Thread {
    /**
     * 数据交互的socket
     */
    private Socket socket;
    /**
     * 通讯线程唯一标识[username不可重复]
     */
    private String username;

    public ServerConnClientThread(Socket socket, String username) {
        this.socket = socket;
        this.username = username;
    }

    public Socket getSocket() {
        return socket;
    }

    public String getUsername() {
        return username;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " - 服务端保持和客户端[" + this.username + "]通讯的线程启动...");
        while (true) {
            try {
                ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
                //等待客户端发送消息[阻塞]
                Message clientMsg = (Message) ois.readObject();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
    }
}

服务端管理和客户端保持通讯的线程集合:

public class ServerConnClientThreadManage {
    /**
     * 用户通讯线程集合:key[用户名] -> value[对应通讯线程]
     */
    private static Map<String, ServerConnClientThread> scctMap = new HashMap<>();

    /**
     * 添加
     * @param username
     * @param scct
     */
    public static void addServerConnClientThread(String username, ServerConnClientThread scct) {
        scctMap.put(username, scct);
    }

    /**
     * 移除
     * @param username
     */
    public static void removeServerConnClientThread(String username) {
        scctMap.remove(username);
    }

    /**
     * 获得对应用户的通讯线程
     * @param username
     * @return
     */
    public ServerConnClientThread getServerConnClientThread(String username) {
        return scctMap.get(username);
    }

}

登录逻辑处理(业务层的代码不再展示):

public class ChatServer {
    private ServerSocket serverSocket;
    private final UserService userService = new UserServiceImpl();

    /**
     * 服务端初始化
     */
    public ChatServer() {
        try {
            serverSocket = new ServerSocket(ChatConstant.SERVER_PORT);
            System.out.println("服务器在9999端口监听~");
            while (true) {
                //等待客户端连接 => 连接成功后获得交互的socket
                Socket socket = serverSocket.accept();
                //读取客户端发送的消息
                ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
                Message<User> msg = (Message<User>) ois.readObject();
                //取出User对象
                User user = msg.getContent();
                //消息类型
                String msgType = msg.getMsgType();
                //服务端回复客户端的消息
                Message<String> replyMsg = new Message<>();

                //用户注册处理
                if (MessageType.CHAT_USER_REGISTER.getType().equals(msgType)) {
                    //用户名是否重复
                    User u = userService.queryByUsername(user.getUsername());
                    if (Objects.isNull(u)) {
                        //可以注册
                        if (userService.userRegister(user)) {
                            replyMsg.setMsgType(MessageType.USER_REGISTER_SUCCESS.getType());
                            replyMsg.setContent("注册成功!");
                        } else {
                            replyMsg.setMsgType(MessageType.USER_REGISTER_FAIL.getType());
                            replyMsg.setContent("注册异常!");
                        }
                    } else {
                        //当前用户名已存在,不可注册
                        replyMsg.setMsgType(MessageType.USER_REGISTER_FAIL.getType());
                        replyMsg.setContent("当前用户名已被注册!");
                    }
                }

                //用户登录处理
                if (MessageType.CHAT_USER_LOGIN.getType().equals(msgType)) {
                    //查询用户是否存在
                    User u = userService.queryByUsernameAndPassword(user.getUsername(), user.getPassword());
                    if (Objects.isNull(u)) {
                        //未查到
                        replyMsg.setMsgType(MessageType.USER_LOGIN_FAIL.getType());
                        replyMsg.setContent("用户名或者密码错误!");
                    } else {
                        //登录成功
                        replyMsg.setMsgType(MessageType.USER_LOGIN_SUCCESS.getType());
                        String username = user.getUsername();
                        //创建服务端和客户端保持通讯的线程
                        ServerConnClientThread scct = new ServerConnClientThread(socket, username);
                        //线程启动
                        scct.start();
                        //加入集合管理
                        ServerConnClientThreadManage.addServerConnClientThread(username, scct);
                    }
                }

                //回复客户端消息
                ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
                oos.writeObject(replyMsg);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (null != serverSocket) {
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

2.2.3 客户端代码实现

客户端保持和服务端通讯的线程ClientConnServerThread:

public class ClientConnServerThread extends Thread {
    private Socket socket;
    private String username;

    public ClientConnServerThread(Socket socket, String username) {
        this.socket = socket;
        this.username = username;
    }

    public Socket getSocket() {
        return socket;
    }

    public String getUsername() {
        return username;
    }

    @Override
    public void run() {
    System.out.println(Thread.currentThread().getName() + " - 客户端[" + this.username + "]和服务端保持通讯的线程启动...");
        while (true) {
            try {
                ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
                //等待服务端回复消息[阻塞]
                Message serverMsg = (Message) ois.readObject();
                //根据消息类型做对应处理
                String msgType = serverMsg.getMsgType();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

客户端通讯线程集合管理:

public class ClientConnServerThreadManage {
    /**
     * 不同客户端的通讯线程集合
     */
    private static Map<String, ClientConnServerThread> ccstMap = new HashMap<>();

    /**
     * 通讯线程加入集合
     * @param username
     * @param ccst
     */
    public static void add(String username, ClientConnServerThread ccst) {
        ccstMap.put(username, ccst);
    }

    /**
     * 获得对应客户端通讯线程
     * @param username
     * @return
     */
    public static ClientConnServerThread getClientConnServerThread(String username) {
        return ccstMap.get(username);
    }

    /**
     * 移除对应客户端和服务端通讯的线程
     * @param username
     */
    public static void remove(String username) {
        ccstMap.remove(username);
    }

}

ClientUserService新增发送登录请求方法:

/**
     * 发送用户登录请求消息
     * @param username
     * @param password
     */
    public boolean sendUserLoginMsg(String username, String password) {
        boolean isLoginSuccess = false;
        user = new User(username, password);
        Message<User> loginMsg = new Message<>();
        loginMsg.setMsgType(MessageType.CHAT_USER_LOGIN.getType());
        loginMsg.setContent(user);

        try {
            socket = new Socket(ChatConstant.SERVER_IP, ChatConstant.SERVER_PORT);
            //发送
            ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
            oos.writeObject(loginMsg);
            //等待服务端回复[阻塞]
            ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
            Message<String> replyMsg = (Message<String>) ois.readObject();
            if (MessageType.USER_LOGIN_SUCCESS.getType().equals(replyMsg.getMsgType())) {
                //登录成功
                isLoginSuccess = true;
                //创建和服务器端保持通讯的线程
                ClientConnServerThread ccst = new ClientConnServerThread(socket, username);
                //启动
                ccst.start();
                //加入集合管理
                ClientConnServerThreadManage.add(username, ccst);
            } else {
                System.out.println("***** " + replyMsg.getContent() + " *****");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        return isLoginSuccess;
    }

这时需要返回一个布尔值判断用户登录是否成功,若登录成功则进入二级功能菜单(此阶段还未完成)。
客户端页面新增登录输入(不再粘贴所有代码):

//省略
 case "1":
                    System.out.print("请输入您的用户名: ");
                    String username = ScannerUtil.readString(20);
                    System.out.print("请输入您的密码: ");
                    String password = ScannerUtil.readString(20);
                    boolean isLoginSuccess = clientUserService.sendUserLoginMsg(username, password);
                    if (isLoginSuccess) {
                        System.out.println("登录成功 -> 进入二级菜单");
                    }
                    break;
                    //省略

2.2.4 登录功能测试

启动服务端、客户端,输入用户名、密码查看登录情况:
在这里插入图片描述
在这里插入图片描述
如果输入的是错误的用户名或者密码,则不会登录成功:
在这里插入图片描述

2.3 二级功能菜单初始化

用户登录成功以后即可进入二级系统菜单界面,该界面包括在线用户列表、私聊、群聊、文件发送等功能。二级系统菜单ClientSecondView初始如下:

public class ClientSecondView {
    /**
     * 循环控制
     */
    private boolean loop = true;

    /**
     * 显示客户端二级菜单
     */
    public void mainMenu(String username) throws Exception {
        while (loop) {
            System.out.println("************** 通信系统二级菜单[用户: "+ username +"] ******************");
            System.out.println("*                                                 *");
            System.out.println("*                                                 *");
            System.out.println("*               1 显示在线用户                      *");
            System.out.println("*               2 私聊消息                          *");
            System.out.println("*               3 群发消息                          *");
            System.out.println("*               4 发送文件                          *");
            System.out.println("*               9 退出登录                          *");
            System.out.println("*                                                 *");
            System.out.println("***************************************************");
            //用户输入
            System.out.print("请输入您的选择:");
            String key = ScannerUtil.readString(1);
            switch (key) {
                case "1":
                    System.out.println("显示在线用户");
                    break;
                case "2":
                    System.out.println("私聊消息");
                    break;
                case "3":
                    System.out.println("群发消息");
                    break;
                case "4":
                    System.out.println("发送文件");
                    break;
                case "9":
                    System.out.println("退出登录");
                    loop = false;
                    break;
                default:
                    break;
            }
        }
    }
}

现在即可把一级菜单中登录成功后的逻辑进行替换:

//省略
case "1":
                    System.out.print("请输入您的用户名: ");
                    String username = ScannerUtil.readString(20);
                    System.out.print("请输入您的密码: ");
                    String password = ScannerUtil.readString(20);
                    boolean isLoginSuccess = clientUserService.sendUserLoginMsg(username, password);
                    if (isLoginSuccess) {
                        //进入二级系统菜单
                        new ClientSecondView().mainMenu(username);
                    }
                    break;
                    //省略

2.4 二级功能菜单开发

在本节中主要是对二级功能菜单涉及的功能逐个进行实现。用户登录成功以后进入二级菜单,此时已经启动了和服务端保持通讯的线程,而每个通讯线程中维护了两个成员变量:username(当前登录用户),socket(数据交互),所以此时就可以从通讯线程管理类ClientConnServerManage维护的集合中根据username取出对应通讯线程,再从通讯线程中获取socket即可和服务端进行数据交互。

2.4.1 用户退出登录

2.4.1.1 退出分析

为什么要先处理用户退出功能,因为用户登录成功以后客户端和服务端之间会创建一个保持通讯的线程,如果客户端退出时不做任何处理,那么相当于客户端断开了socket数据通道,但是服务端依旧在通讯线程的循环中保持连接socket数据通道,那么这时就会一直抛出IO异常(忽略此处设计,只要服务端或者客户端断开一边即可看到控制台一直抛出IO异常,不再演示)。退出分析如下图所示:
在这里插入图片描述
客户端退出时,向服务端发送一个退出请求,移除对应通讯线程并关闭socket通道,退出循环。服务端接收到客户端退出请求后,同样需要移除对应通讯线程和关闭socket通道,并退出循环。

2.4.1.2 服务端代码实现

此时客户端、服务端保持通讯的线程已经启动,代码应该写在run()方法中(后续功能代码只粘贴对应if分支处理部分,不再粘贴所有)。

 @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " - 服务端保持和客户端[" + this.username + "]通讯的线程启动...");
        while (true) {
            try {
                ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
                //等待客户端发送消息[阻塞]
                Message clientMsg = (Message) ois.readObject();
                //根据消息类型做对应处理
                String msgType = clientMsg.getMsgType();
                //用户退出登录处理
                if (MessageType.CHAT_USER_EXIT.getType().equals(msgType)) {
                    //从集合中移除该通讯线程
                    ServerConnClientThreadManage.removeServerConnClientThread(username);
                    //socket通道关闭
                    socket.close();
                    System.out.println("用户[" + username + "]已退出~");
                    //退出循环
                    break;
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
2.4.1.3 客户端代码实现

ClientUserService新增发送用户退出登录方法:

/**
     * 发送用户退出登录请求消息
     * @param username
     */
    public void sendUserExitMsg(String username) {
        Message<String> exitMsg = new Message<>();
        exitMsg.setMsgType(MessageType.CHAT_USER_EXIT.getType());
        exitMsg.setContent(username);
        //从通讯线程获取socket
        ClientConnServerThread ccst = ClientConnServerThreadManage.getClientConnServerThread(username);
        if (null != ccst) {
            try {
                Socket socket = ccst.getSocket();
                //发送消息
                ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
                oos.writeObject(exitMsg);
                //服务端回复后的逻辑在对应通讯线程中处理即可
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

ClientConnServerThread新增用户退出登录处理:

@Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " - 客户端[" + this.username + "]和服务端保持通讯的线程启动...");
        while (true) {
            ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
                //等待服务端回复消息[阻塞]
                Message serverMsg = (Message) ois.readObject();
                //根据消息类型做对应处理
                String msgType = serverMsg.getMsgType();
                //用户退出登录
                if (MessageType.USER_EXIT_SUCCESS.getType().equals(msgType)) {
                    //移除通讯线程
                    ClientConnServerThreadManage.remove(username);
                    //socket通道关闭
                    socket.close();
                    System.out.println("***** 您已退出登录 *****");
                    //退出循环
                    break;
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

二级菜单swith处理块添加发送退出登录消息处理:

public void mainMenu(String username) throws Exception {
        while (loop) {
            System.out.println("************** 通信系统二级菜单[用户: "+ username +"] ******************");
            System.out.println("*                                                 *");
            System.out.println("*                                                 *");
            System.out.println("*               1 显示在线用户                      *");
            System.out.println("*               2 私聊消息                          *");
            System.out.println("*               3 群发消息                          *");
            System.out.println("*               4 发送文件                          *");
            System.out.println("*               9 退出登录                          *");
            System.out.println("*                                                 *");
            System.out.println("***************************************************");
            //用户输入
            System.out.print("请输入您的选择:");
            String key = ScannerUtil.readString(1);
            switch (key) {
                case "1":
                    System.out.println("显示在线用户");
                    break;
                case "2":
                    System.out.println("私聊消息");
                    break;
                case "3":
                    System.out.println("群发消息");
                    break;
                case "4":
                    System.out.println("发送文件");
                    break;
                case "9":
                    clientUserService.sendUserExitMsg(username);
                    loop = false;
                    break;
                default:
                    break;
            }
        }
    }
2.4.1.4 退出登录功能测试

在这里插入图片描述
可以看到界面打印有点错乱,为了让打印信息看起来合理一点,可以让打印菜单界面的main线程小休眠一下。

 case "9":
                    clientUserService.sendUserExitMsg(username);
                    //休眠500ms,防止打印信息错乱
                    TimeUnit.MILLISECONDS.sleep(500);
                    loop = false;
                    break;

再次测试:
在这里插入图片描述
这样打印信息看起来就合理一点啦。

2.4.2 显示在线用户

2.4.2.1 功能分析

显示在线用户列表即显示当前所有已登录的用户,根据上述分析可知,服务端会维护和每个登录成功的用户保持通讯的线程集合,遍历集合即可获得所有在线用户列表(实现方式很多,仁者见仁智者见智吧)。

2.4.2.2 服务端代码实现

后续的群聊功能中也会涉及在线用户列表的获取,故在ServerConnClientThreadManage类中维护一个获取在线用户列表的方法:

/**
     * 获取在线用户列表
     * @return
     */
    public static List<String> getOnlineUsers() {
        if (scctMap.isEmpty()) {
            return Collections.emptyList();
        }

        List<String> resList = new ArrayList<>(scctMap.size());
        //遍历key集合[username]即可
        Set<String> keySet = scctMap.keySet();
        resList.addAll(keySet);
        return resList;
    }

ServerConnClientThread:

//请求在线用户列表处理
                if (MessageType.ONLINE_USER_LIST.getType().equals(msgType)) {
                    List<String> onlineUsers = ServerConnClientThreadManage.getOnlineUsers();
                    Message<List<String>> replyMsg = new Message<>();
                    replyMsg.setMsgType(MessageType.ONLINE_USER_LIST.getType());
                    replyMsg.setContent(onlineUsers);
                    //回复
                    oos = new ObjectOutputStream(socket.getOutputStream());
                    oos.writeObject(replyMsg);
                }
2.4.2.3 客户端代码实现

ClientUserService:

public void sendGetOnlineUsersMsg(String username) {
        Message onlineMsg = new Message();
        onlineMsg.setMsgType(MessageType.ONLINE_USER_LIST.getType());
        //从通讯线程获取socket
        ClientConnServerThread ccst = ClientConnServerThreadManage.getClientConnServerThread(username);
        if (null != ccst) {
            try {
                socket = ccst.getSocket();
                //发送消息
                ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
                oos.writeObject(onlineMsg);
                //服务端回复后的逻辑在对应通讯线程中处理即可
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

ClientConnServerThread:

//在线用户列表
                if (MessageType.ONLINE_USER_LIST.getType().equals(msgType)) {
                    List<String> onlineUsers = (List<String>) serverMsg.getContent();
                    //打印在线用户列表
                    StringBuilder sb = new StringBuilder("当前在线用户列表: \n");
                    for (String onlineUser : onlineUsers) {
                        sb.append("\t - ").append(onlineUser).append("\n");
                    }
                    System.out.println(sb.toString());
                }

二级菜单:

 case "1":
                    clientUserService.sendGetOnlineUsersMsg(username);
                    TimeUnit.MILLISECONDS.sleep(500);
                    break;
2.4.2.4 在线用户功能测试

为了方便测试,再增加两个用户数据:
在这里插入图片描述
分别登录用户ikun、xiaoheizi-1、xiaoheizi-2,测试在线用户功能:
在这里插入图片描述
ikun请求在线用户列表:
在这里插入图片描述
xiaoheizi-1退出登录:
在这里插入图片描述
在这里插入图片描述
用户xiaoheizi-2退出登录:
在这里插入图片描述
在这里插入图片描述

2.4.3 用户私聊

2.4.3.1 功能分析

用户A要和用户B私聊,首先向服务端发送私聊消息请求,服务端接收到请求后,根据请求中的接收者信息查询对应用户的通讯线程,若接收消息的用户在线,则转发此条私聊消息(转发消息时使用的socket通道是接收者的socket,不是发送者的socket);若接收消息的用户离线,则可将此条消息存入到数据库表,方便后续进行相关离线消息的处理。

2.4.3.2 服务端代码实现

ServerConnClientThread:

//用户私聊处理
                if (MessageType.CHAT_USER.getType().equals(msgType)) {
                    //接收者
                    String getter = clientMsg.getGetter();
                    //接收者是否在线
                    ServerConnClientThread getterScct = ServerConnClientThreadManage.getServerConnClientThread(getter);
                    if (null == getterScct) {
                        //接收者离线 -> 离线消息处理
                    } else {
                        //接收者在线 -> 消息转发
                        Socket getterSocket = getterScct.getSocket();
                        //聊天内容
                        String chatContent = (String) clientMsg.getContent();
                        //发送时间
                        String sendTime = clientMsg.getSendTime();
                        //消息封装
                        Message<String> chatMsg = new Message<>();
                        chatMsg.setMsgType(MessageType.CHAT_USER.getType());
                        //发送人、聊天消息、发送时间
                        chatMsg.setSender(clientMsg.getSender());
                        chatMsg.setContent(chatContent);
                        chatMsg.setSendTime(sendTime);
                        //转发
                        oos = new ObjectOutputStream(getterSocket.getOutputStream());
                        oos.writeObject(chatMsg);
                    }
2.4.3.3 客户端代码实现

客户端聊天业务的处理封装在ClientChatService中:

public class ClientChatService {
    /**
     * 和服务端保持通讯的socket
     */
    private Socket socket;

    /**
     * 发送用户私聊请求
     * @param sender 发送者
     * @param getter 接收者
     * @param chatContent 聊天消息
     */
    public void sendChatUserMsg(String sender, String getter, String chatContent) {
        Message<String> chatMsg = new Message<>();
        chatMsg.setMsgType(MessageType.CHAT_USER.getType());
        //发送人
        chatMsg.setSender(sender);
        //接收人
        chatMsg.setGetter(getter);
        //聊天内容
        chatMsg.setContent(chatContent);
        //发送时间
        chatMsg.setSendTime(CommonUtil.getNowDate());
        //从通讯线程获取socket
        ClientConnServerThread ccst = ClientConnServerThreadManage.getClientConnServerThread(sender);
        if (null != ccst) {
            try {
                socket = ccst.getSocket();
                //发送消息
                ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
                oos.writeObject(chatMsg);
                //服务端回复后的逻辑在对应通讯线程中处理即可
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

ClientConnServerThread:

//用户私聊消息
                if (MessageType.CHAT_USER.getType().equals(msgType)) {
                    //发送人
                    String sender = serverMsg.getSender();
                    //聊天内容
                    String chatContent = (String) serverMsg.getContent();
                    //发送时间
                    String sendTime = serverMsg.getSendTime();
                    StringBuilder sb = new StringBuilder();
                    sb.append("\n[").append(sendTime).append("] ")
                            .append(sender).append(" 对你说: ").append(chatContent)
                            .append("\n");
                    System.out.println(sb.toString());
                }

在客户端的私聊菜单界面,需要用户输入私聊的用户名和聊天内容。

 case "2":
                    System.out.print("请输入您要私聊的用户: ");
                    String getterUserName = ScannerUtil.readString(20);
                    System.out.print("请输入您要说的话: ");
                    String chatContent = ScannerUtil.readString(100);
                    clientChatService.sendChatUserMsg(username, getterUserName, chatContent);
                    System.out.println("你对 " + getterUserName + "说: " + chatContent);
                    break;
2.4.3.4 私聊功能测试

以用户ikun和用户xiaoheizi-1演示私聊功能。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.4.3.5 离线消息处理说明

本次项目不再编写离线消息处理部分的代码和内容。大致思路就是当用户为离线状态时,将此条聊天消息存放到一张离线消息表,当用户登录后,服务端可以专门启一个线程拉取该用户的离线消息,即从数据库表查询离线消息并转发,转发完成后删除对应的离线消息即可(思路很多,因人而异)。

2.4.4 用户群聊

2.4.4.1 功能分析

用户群聊是指当前登录用户向除自己之外的所有当前在线用户发送聊天消息,即从上述的一对一私聊发消息到一对多发送消息,服务端接收到用户群聊消息请求后,遍历所有在线用户(不包括当前用户)转发聊天消息即可。

2.4.4.2 服务端代码实现

ServerConnClientThread:

//群聊消息处理
                if (MessageType.CHAT_GROUP.getType().equals(msgType)) {
                    //发送人
                    String sender = clientMsg.getSender();
                    String sendTime = clientMsg.getSendTime();
                    //聊天内容
                    String chatContent = (String) clientMsg.getContent();
                    //消息转发 -> 当前在线用户
                    List<String> onlineUsers = ServerConnClientThreadManage.getOnlineUsers();
                    for (String onlineUser : onlineUsers) {
                        //排除自己
                        if (sender.equals(onlineUser)) {
                            continue;
                        }

                        ServerConnClientThread getterScct = ServerConnClientThreadManage.getServerConnClientThread(onlineUser);
                        Socket getterSocket = getterScct.getSocket();
                        //消息封装
                        Message<String> chatMsg = new Message<>();
                        chatMsg.setMsgType(MessageType.CHAT_GROUP.getType());
                        //发送人、聊天消息、发送时间
                        chatMsg.setSender(sender);
                        chatMsg.setContent(chatContent);
                        chatMsg.setSendTime(sendTime);
                        //转发
                        oos = new ObjectOutputStream(getterSocket.getOutputStream());
                        oos.writeObject(chatMsg);
                    }
                }
2.4.4.3 客户端代码实现

ClientChatService:

/**
     * 发送用户群聊请求
     * @param sender 发送者
     * @param chatContent 聊天消息
     */
    public void sendChatGroupMsg(String sender, String chatContent) {
        Message<String> chatMsg = new Message<>();
        chatMsg.setMsgType(MessageType.CHAT_GROUP.getType());
        //发送人
        chatMsg.setSender(sender);
        //聊天内容
        chatMsg.setContent(chatContent);
        //发送时间
        chatMsg.setSendTime(CommonUtil.getNowDate());
        //从通讯线程获取socket
        ClientConnServerThread ccst = ClientConnServerThreadManage.getClientConnServerThread(sender);
        if (null != ccst) {
            try {
                socket = ccst.getSocket();
                //发送消息
                ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
                oos.writeObject(chatMsg);
                //服务端回复后的逻辑在对应通讯线程中处理即可
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

ClientConnServerThread:

//用户群聊消息
                if (MessageType.CHAT_GROUP.getType().equals(msgType)) {
                    //发送人
                    String sender = serverMsg.getSender();
                    //聊天内容
                    String chatContent = (String) serverMsg.getContent();
                    //发送时间
                    String sendTime = serverMsg.getSendTime();
                    StringBuilder sb = new StringBuilder();
                    sb.append("\n[").append(sendTime).append("] ")
                            .append(sender).append(" 对大伙说: ").append(chatContent)
                            .append("\n");
                    System.out.println(sb.toString());
                }

二级功能菜单:

case "3":
                    System.out.print("请输入您想对大伙说的话: ");
                    String groupChatContent = ScannerUtil.readString(100);
                    clientChatService.sendChatGroupMsg(username, groupChatContent);
                    System.out.println("你对大伙说: " + groupChatContent);
                    break;
2.4.4.4 群聊功能测试

登录三个用户:ikun、xiaoheizi-1、xiaoheizi-2测试群聊功能。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.4.5 文件发送

2.4.5.1 功能分析

用户A向用户B发送文件,服务端接收用户A的文件发送请求消息,获得对应的目标文件路径和文件字节数组然后向用户B转发该消息(如果用户B为离线状态,则按离线文件处理,同离线消息处理,文章中不涉及该部分的编码和设计),用户B根据目标文件路径和文件字节数组写入文件。服务端在这个过程中只负责转发,不负责对文件的读写操作!!!(现模拟用户A要将自己"D:\resources\srcDir"目录下的一张图片资源发送到用户B的存储目录"D:\resources\destDir\)。
在这里插入图片描述
在这里插入图片描述

2.4.5.2 服务端代码实现

为了方便处理文件资源的发送、接收等,首先需要对传输消息Message类做一个改造。

public class Message<T> implements Serializable {
    private static final long serialVersionUID = -8621512324130711419L;
    /**
     * 发送者
     */
    private String sender;
    /**
     * 接收者
     */
    private String getter;
    /**
     * 消息内容
     */
    private T content;
    /**
     * 发送时间
     */
    private String sendTime;
    /**
     * 消息类型
     */
    private String msgType;
    /**
     * 存储数据的字节数组
     */
    private byte[] bytes;
    /**
     * 文件源路径
     */
    private String srcPath;
    /**
     * 文件目标路径
     */
    private String destPath;
    //省略

ServerConnClientThread:

//文件转发处理
                if (MessageType.FILE_SEND.getType().equals(msgType)) {
                    //文件源路径
                    String srcPath = clientMsg.getSrcPath();
                    //接收人
                    String getter = clientMsg.getGetter();
                    //接收人是否在线
                    ServerConnClientThread getterScct = ServerConnClientThreadManage.getServerConnClientThread(getter);
                    if (null == getterScct) {
                        //离线
                        System.out.println("离线文件发送处理");
                    } else {
                        //在线 -> 转发
                        Message fileMsg = new Message();
                        fileMsg.setMsgType(MessageType.FILE_SEND.getType());
                        fileMsg.setSender(clientMsg.getSender());
                        fileMsg.setSendTime(clientMsg.getSendTime());
                        fileMsg.setSrcPath(clientMsg.getSrcPath());
                        //文件字节数组
                        fileMsg.setBytes(clientMsg.getBytes());
                        //文件存储路径
                        fileMsg.setDestPath(clientMsg.getDestPath());
                        oos = new ObjectOutputStream(getterScct.socket.getOutputStream());
                        oos.writeObject(fileMsg);
                    }
                }
2.5.4.3 客户端代码实现

文件发送消息请求封装在ClientFileService类:

public class ClientFileService {
    /**
     * 和服务端保持通讯的socket
     */
    private Socket socket;

    /**
     * 发送用户文件消息请求
     *
     * @param sender
     * @param getter
     * @param srcPath
     * @param destPath
     */
    public void sendFileMsg(String sender, String getter, String srcPath, String destPath) {
        Message fileSendMsg = new Message();
        //发送人
        fileSendMsg.setSender(sender);
        //接收人
        fileSendMsg.setGetter(getter);
        //发送时间
        fileSendMsg.setSendTime(CommonUtil.getNowDate());
        //源路径
        fileSendMsg.setSrcPath(srcPath);
        //目标路径
        fileSendMsg.setDestPath(destPath);
        BufferedInputStream bis = null;
        try {
            //10M
            byte[] bytes = new byte[ChatConstant._10M];
            //文件读操作
            bis = new BufferedInputStream(new FileInputStream(new File(srcPath)));
            bis.read(bytes, 0, bytes.length);
            //文件字节数据
            fileSendMsg.setBytes(bytes);
            //从通讯线程获取socket
            ClientConnServerThread ccst = ClientConnServerThreadManage.getClientConnServerThread(sender);
            if (null != ccst) {
                socket = ccst.getSocket();
                //发送消息
                ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
                oos.writeObject(fileSendMsg);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //资源释放
            if (null != bis) {
                try {
                    bis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

ClientConnServerThread:

//用户文件消息
                if (MessageType.FILE_SEND.getType().equals(msgType)) {
                    String sender = serverMsg.getSender();
                    String sendTime = serverMsg.getSendTime();
                    String srcPath = serverMsg.getSrcPath();
                    //目标路径
                    String destPath = serverMsg.getDestPath();
                    //文件字节数组
                    byte[] fileBytes = serverMsg.getBytes();
                    //文件写操作
                    BufferedOutputStream bos = null;
                    bos = new BufferedOutputStream(new FileOutputStream(new File(destPath)));
                    bos.write(fileBytes, 0, fileBytes.length);
                    //--消息打印
                    StringBuilder sb = new StringBuilder();
                    sb.append("\n[").append(sendTime).append("] ")
                            .append("用户: ").append(sender).append(" 发送文件(")
                            .append(srcPath).append(")").append(" 到我的目录: ")
                            .append(destPath);
                    System.out.println(sb.toString());
                    //资源释放
                    bos.close();
                }

二级菜单文件发送时需要用户输入接收人、文件源路径、文件目标存储路径信息:

case "4":
                    System.out.print("请输入文件接收用户: ");
                    String fileGetter = ScannerUtil.readString(20);
                    System.out.print("请输入要发送的文件路径(格式: d:\\xx\\xx.jpg): ");
                    String srcPath = ScannerUtil.readString(50);
                    System.out.print("请输入接收用户的文件存储路径(格式: d:\\xx\\xx.jpg): ");
                    String destPath = ScannerUtil.readString(50);
                    clientFileService.sendFileMsg(username, fileGetter, srcPath, destPath);
                    System.out.println("您发送了文件: " + srcPath + " 到用户[" + fileGetter +"] 的存储目录: " + destPath);
                    break;
2.5.4.4 文件发送功能测试

登录用户ikun、xiaoheizi-1来测试文件发送,用户ikun将文件d:\resources\srcDir\9527.jpg图片文件发送到用户xiaoheizi-1的存储路径d:\resources\destDir\6666.jpg。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
查看目标存储目录:
在这里插入图片描述

2.4.6 服务端推送新闻

2.4.6.1 功能分析

服务端发布公告消息,推送给所有在线用户(类似群聊中的消息群发,只不过是服务端主动推送消息),可以专门设置一个线程来做推送公告处理。

2.4.6.2 服务端代码实现

服务端公告推送线程ServerNewsThread:

public class ServerNewsThread extends Thread {
    @Override
    public void run() {
    	System.out.println("服务端公告推送线程启动~");
        try {
            while (true) {
                System.out.print("请输入您要推送的公告消息[exit: 退出公告推送]: ");
                String serverNews = ScannerUtil.readString(100);
                if ("exit".equals(serverNews)) {
                    System.out.println("服务端停止公告推送~");
                    break;
                }

                //向在线用户转发
                List<String> onlineUsers = ServerConnClientThreadManage.getOnlineUsers();
                for (String onlineUser : onlineUsers) {
                    Message<String> serverNewsMsg = new Message<>();
                    serverNewsMsg.setMsgType(MessageType.SERVER_NEWS.getType());
                    serverNewsMsg.setContent(serverNews);
                    //获得socket通道
                    ServerConnClientThread scct = ServerConnClientThreadManage.getServerConnClientThread(onlineUser);
                    Socket socket = scct.getSocket();
                    ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
                    oos.writeObject(serverNewsMsg);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在服务端启动的同时启动该公告推送线程:

//省略
 /**
     * 服务端初始化
     */
    public ChatServer() {
        try {
            serverSocket = new ServerSocket(ChatConstant.SERVER_PORT);
            System.out.println("服务器在9999端口监听~");
            //启动公告推送线程
            new ServerNewsThread().start();
            while (true) {
                //等待客户端连接 => 连接成功后获得交互的socket
                Socket socket = serverSocket.accept();
                //省略
2.4.6.3 客户端代码实现

ClientConnServerThread:

//服务端公告消息
                if (MessageType.SERVER_NEWS.getType().equals(msgType)) {
                    //公告消息
                    String news = (String) serverMsg.getContent();
                    String sendTime = serverMsg.getSendTime();
                    StringBuilder sb = new StringBuilder();
                    sb.append("\n").append(sendTime).append("<=========== ")
                            .append("系统公告【全体注意!!!】").append(" ===========>\n")
                            .append(news);
                    System.out.println(sb.toString());
                }
2.4.6.4 公告推送测试

登录用户ikun、xiaoheizi-1、xiaoheizi-2测试公告推送功能。
在这里插入图片描述
在这里插入图片描述
查看在线用户接收公告情况:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
服务端输入"exit"即可停止公告推送:
在这里插入图片描述

源码压缩包


总结

这是博主第一次尝试写博客文章,文章结构和语言组织等可能稍显混乱,加上博主Java方面的知识水平依旧尚浅,可能在功能分析、代码逻辑等诸多地方存在不足,还望各位看官多多包涵。本以为写博客也不会太累,但是实际写起来还是有点累的(┭┮﹏┭┮),不管写的好还是不好,创作的过程也是思维锻炼的一个过程,也算是略有收获吧。

java大作业网络聊天室 这个项目是我大一的java期末大作业,学习了一个学期的成果,编程能力不够,做的并不够好,写在这里记录一下自己的成长,也欢迎大家给出宝贵意见! 该篇博客写一个基于java的单/多人网络即时聊天室,聊天室 ... 阅读详情

相关推荐

java实现聊天功能

在发送图片或表情包时,需要将图片或表情包的内容转换为 Base64 编码,并将编码后的字符串作为消息内容发送。在接收到包含图片或表情包的消息时,需要创建一个新的 Image 对象,并将 Base64 编码加上相应的 MIME 类型,将其设置为 Image 对象的 src 属性,并将 Image 对象显示在聊天界面中。其中,"ws://" 是 WebSocket 协议的前缀,后面跟上 WebSocket 服务器的地址和端口号,"/websocket/chat" 是 WebSocket 服务器的路径。

走向CTO的路上... 1252

JAVA中如何使用Socket 实现聊天功能?带具体代码说明

System.out.println("客户端" + socket.getInetAddress() + "发送的消息:" + message);服务器通过监听客户端socket的连接请求,接受客户端连接,然后通过客户端socket进行数据传输。System.out.println("客户端" + socket.getInetAddress() + "已连接");在JAVA中,使用Socket实现聊天功能需要涉及到TCP/IP协议、服务器socket和客户端socket的搭建、数据流传输等知识。

小马总的博客 4067

Java网页版即时通讯聊天系统(附源码)

疫情期间,整天闷在家里又不能聚会,大把的空余时间差点让我发霉,后来有个客户发来新年祝贺,情不自禁想起了一件事情,就是他曾经提起过,要是在后台管理系统里面整合个聊天功能该多好啊,有了这个念头,马上行动起来!!! 一.系统演示 1.1 聊天窗体主界面演示 1.2 模拟两人在线聊天(点击图片查看演示视频) 1.3 在线演示(真实系统展示) 在线演示,点击进入系统 到这里,若是您想要的,接下来听我娓娓道来↘↘ 二.开发工具 开发软件:JDK7.0、MyEclipse 2014 数据库:MySQL5.6 服

踮脚敲代码 1752

Java实现在线聊天

注:可以直接看整合javat-io是基于java开发的一个开源的网络编程架构,大家都知道现在手机上或者电脑上都装了很多APP,这些APP都不是一个个在手机上或电脑上孤立的使用,而是能访问其他的地方数据或者与其他节点进行实时聊天,故每个APP中都要有一个对外进行通信的模块,这块内容从编程的角度来看就能通过网络编程框架来实现,t-io就是完成这块的内容的最好的编程软件。...............

学习学习学习 1万+

实现基于Java的聊天窗口:Socket与Swing实战项目

Socket通信是一种网络编程技术,允许数据在两个或多个网络应用程序之间进行交换。在Java中,Socket通信主要用于客户端和服务器之间的网络交互。理解Socket通信的基本原理是构建稳定和高效的网络应用的基础。Swing提供了从基本的按钮、文本框到复杂的表格、树等组件。这些组件都位于包中,使用时需要导入相关的类。JButton:命令按钮,用户点击时会触发一个动作。JTextField:单行文本输入框,用于输入文本。JTextArea:多行文本输入区域,可读写。

weixin_31688273的博客 942

Java聊天室实现原理与实战:仿QQ的Socket+线程项目

在构建交互协议之前,首先要定义清晰且一致的消息格式。消息格式设计的好坏,直接影响系统的性能、扩展性以及可维护性。一个好的消息格式能确保信息的快速传递和正确解析。事件是事件驱动模型中最基本的单元。它可以是一个信号,通知程序发生了某种特定的情况。而回调机制是处理这些事件的一种方式,它允许我们将一个函数或方法的引用传递给另一个函数,后者将在适当的时候调用这个引用。// 处理事件// 注册监听器// 触发事件// 在内部调用监听器的onEvent方法// 使用// 某个事件发生后。

weixin_30415591的博客 863

java课程设计-基于java的在线聊天系统设计与实现,可适用于swing在线聊天,基于gui在线聊天项目,基于socket聊天项目(附源码和配套资料)

基于java在线聊天系统设计与实现,可适用于swing在线聊天,基于gui在线聊天项目,模仿QQ聊天功能,基于socket聊天项目,基于TCP聊天,基于UDP文件发送,java网络编程。

项目帮 357

CH32V IAP跳转避坑指南:为什么你的APP跳不过去?从mstatus寄存器配置到函数属性详解

本文深入解析CH32V系列MCU的IAP跳转机制,从mstatus寄存器配置到函数属性优化,详细讲解如何避免APP跳转失败。文章涵盖地址准备、模式切换、跳转执行等关键步骤,并提供调试技巧与实战案例,帮助开发者快速解决CH32V IAP跳转中的常见问题。

weixin_30955617的博客 141

Java聊天室项目GUI界面化实现(Java聊天室 IO流 Socket流 GUI界面 )

Java聊天室项目GUI界面化实现(Java聊天室 IO流 Socket流 GUI界面 ) 文章目录Java聊天室项目GUI界面化实现(Java聊天室 IO流 Socket流 GUI界面 )1.Java聊天室 IO流 Socket流 GUI界面 服务端功能及页面介绍2.Java聊天室 IO流 Socket流 GUI界面 客户端功能及页面介绍3.Java聊天室 IO流 Socket流 GUI界面 工具类Utils代码及解释(FileUtils文件发送的工具类Utils PathUtils是设置背景图片的图片

cx9977的博客 7211

大模型技术驱动智慧矿山数字化转型

多模态大模型作为AI领域的前沿技术,通过融合视觉、传感器等多源数据,构建了强大的环境理解与决策能力。其核心价值在于打破数据孤岛,实现实时分析与智能响应,特别适用于工业场景中的复杂系统优化。在智慧矿山建设中,该技术通过数字孪生、设备自诊断和智能调度等模块,显著提升了安全生产效率和资源利用率。典型应用包括基于强化学习的采掘路径规划、多层级灾害预警体系构建,以及可自我更新的矿山知识图谱系统,这些创新正在推动传统矿业向数字化、智能化方向转型。

weixin_29061531的博客 223

shader基于高度的权重混合

效果图: 基于高度的权重混合 , 效果描述: (1)混合:三层混合, 例如 第一层是草,第二层是石头,第三层是沙子,三者混合显示 (2)权重: 三层的权重的和 为1, 三者是此消彼长的关系 (3)高度:高度会限制或者说是 束缚 着 效果 关于混合,再多说几句: (1)基于权重的混合:权重 ,跟顺序没有关系 只跟权重的大小有关系(在一层一层界限不清晰的地方,特别适用!!效果极好!!!!) (2)基于叠加的混合: 叠加 就是 一层一层的往上加。。。。 所以顺序特别重要(在雪地等效果中使用:因为

木马人 1119

java课程设计-基于java的在线聊天系统设计与实现,可适用于swing在线聊天,基于gui在线聊天项目,基于socket聊天项目推荐(附源码和配套资料)

java课程设计-基于java的在线聊天系统设计与实现,可适用于swing在线聊天,基于gui在线聊天项目,基于socket聊天项目推荐(附源码和配套资料)

项目帮 449

葵花(Himawari)8/9卫星数据处理

李国春 葵花8(含葵花9,下同)是静止气象卫星,数据范围是星下点为140°E赤道上空的大圆盘。空间分辨率有500m、1km和2km三种。虽然其空间分辨率不高,但是有极高的时间分辨率,观测数据刷新仅10分钟。这对一些生态、农业和大范围自然灾害的动态检测具有非常高的应用价值。本文介绍如何使用RSD处理葵花8数据。

副组长的博客 1万+

java GUI 实现的socket在线聊天系统项目源码运行教程

大家好,今天给大家演示一下一个简单的图形界面的聊天程序,上次我们演示的是Java实现的群聊程序,还可以发送文件,今天这个小程序只能聊天,不能发文件,功能非常简单,可在局域网内实现互相聊天,源码已经打包成了jar文件,可以在有Java环境的电脑上直接双击运行。今天我们来演示下如何导入项目运行及直接双击jar包运行两种方式。1. 将项目导入到eclipse(myeclipse也可以)。因为这个项目是在我的电脑写的,所以导入后没任何错误,在别人电脑导入后需要做一下简单的配置,否则可能因为Java环境不一样而导致出

所有项目源码实战教程均可在【猿来入此】下载 331

从“储能”到“信号”:拆解Buck电路中那颗不起眼的前馈电容,如何让电源更稳

本文深入解析Buck电路中前馈电容的关键作用,揭示其如何通过频域整形和零点补偿提升电源稳定性。从工作原理到工程实践,详细介绍了容值计算、布局布线及特殊场景调校策略,帮助工程师优化瞬态响应和纹波抑制。

weixin_42530458的博客 404

淘宝滑块验证码破解:Python模拟鼠标轨迹过关详解

本文全面解析了淘宝滑块验证码的破解流程,从机制分析、图像处理、轨迹模拟到自动化实现,提供了完整的Python解决方案。随着2026年风控技术的升级,单纯依靠轨迹模拟已经难以达到高通过率,未来趋势是结合强化学习生成更自然的轨迹,并利用深度神经网络进行端到端的验证码绕过。此外,各大平台正逐步推广"无感验证"和"行为式验证码",将验证融入到用户正常操作中,这对爬虫技术提出了更高的挑战。技术本身是中立的,关键在于使用者的目的。希望本文能帮助开发者更好地理解验证码对抗技术,从而设计出更安全的防御系统。

2201_76125261的博客 97

java socket ssh_SSH 项目中 使用websocket 实现网页聊天功能

1 importjava.io.IOException;2 importjava.util.Set;3 importjava.util.concurrent.CopyOnWriteArraySet;4 importjava.util.concurrent.atomic.AtomicInteger;56 importjavax.servlet.http.HttpSession;7 importjav...

weixin_31834087的博客 494

Unity中语法糖带来的GC问题 ——那些看起来优雅、实际上在背后偷偷生垃圾的代码

*语法糖(Syntactic Sugar)**指的是编程语言中为了让代码更简洁、易读而提供的简便写法。比如C#中的foreachlambda表达式LINQ自动属性等。语法糖本身不是坏事,但在Unity等对GC敏感的环境下,要警惕语法糖带来的隐式内存分配。合理使用语法糖,关键场合用更“丑陋”但高效的写法,才能保证游戏流畅运行。下面我将更深入地分析Unity中语法糖导致GC的原理、具体场景、检测与优化技巧,并补充一些容易忽视的细节。

本博客聚焦游戏开发技巧、创业实战心得及大学热门课程干货。这里既有技术深度,也有思维广度,无论你是开发者、创业者还是高校学子,都能在这里找到适合自己的成长之路! 520

java socket聊天_java_基于Java Socket实现一个简易在线聊天功能(一),最近做了一个项目,其中有一 - phpStudy...

基于Java Socket实现一个简易在线聊天功能(一)最近做了一个项目,其中有一个在线网页交流的需求,好久没写代码了,手都生疏了,于是先写demo练练手,分享到phpstudy平台,以此做个记录,方便自己和大家使用。先给大家说下实现步骤分这样几大步:1、使用awt组件和socket实现简单的单客户端向服务端持续发送消息;2、结合线程,实现多客户端连接服务端发送消息;3、实现服务端转发客户端消息至...

weixin_33147807的博客 351

PowerDesigner 数据库建模使用详解

PowerDesigner 数据库建模使用详解

congge 7082

温度PID控制PLC程序

温度PID稳定控制程序,适用于欧姆龙PLC,对其他PLC也具有很大参考价值

70行代码 ? 就实现两台设备文字聊天【你好,10086♥】,简化版聊天室,理解Socket项目实现【java养成】

Java学习打卡:第三十三天 内容导航实例需求项目代码【70行就可以实现文字通讯】服务器端客户端 Java养成计划(打卡第32天) 简化版聊天【70行代码实现两台设备文字聊天】 ## 简化版---两台电脑端聊天 为便于理解,这里来简化一下Socket通信 昨天的项目中很大一部分就是都是界面设计还有就是事件监听,所以这里就先不使用界面,简单使用控制台输出 实例需求 连接指定ip地址的电脑,在指定端口操作,连接成功就显示连接成功 能够实现两台电脑的用户在控制台相互聊天【就像QQ一样,但是只是文字聊天】

a23452的博客 559

VC中sqlite的使用

在VC中对sqlite的使用,实现数据库大批数据的快速插入,读取数据库中的数据在ListCtrl中显示

java课程设计-基于java的在线聊天系统设计与实现,可适用于swing在线聊天,基于gui在线聊天项目,基于socket聊天项目系统(附源码和配套资料)

java课程设计-基于java的在线聊天系统设计与实现,可适用于swing在线聊天,基于gui在线聊天项目,基于socket聊天项目系统(附源码和配套资料)

项目帮 290

IDEA的使用指南

IDEA基于java的使用手册,可以最快速的上手,在使用中不断的丰富自己的使用技巧.

汉书HE8,好用的汇编代码编写工具

汉书HE8,好用的汇编代码编写工具早期编写汇编代码所用功能强大

Java Socket编程实现多人聊天室

Java Socket编程实现多人聊天室 去发现同类优质开源项目:https://gitcode.com/ 项目简介 本项目是一个基于Java的多人聊天室应用,使用Java的Socket技术实现了服务器与多个客户端之间的通信。它不仅支持基础的群聊功能,让所有在线用户都能看到交流的信息,而且具备私聊能力,允许用户之间进行一对一的秘密交谈。此外,项目还包括了用户账户登录、好友列表展示、以及服务器对用户...

gitblog_09808的博客 982

基于socket---简单聊天室的实现

Socket简介:网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket。建立网络通信连接至少要一对端口号(socket)。socket本质是编程接口(API),对TCP/IP的封装,TCP/IP也要提供可供程序员做网络开发所用的接口,这就是Socket编程接口;HTTP是轿车,提供了封装或者显示数据的具体形式;Socket是发动机,提供了网络通信的能力。Socke

曹世宏的博客 1万+

java实现聊天界面设计_java聊天室设计及实现

这个是我大二刚开始学java时做的东西,是一个聊天室,运行服务器,注册两个客户端,登录后客户端之间可以进行文字聊天,有群发功能。然后我讲讲是怎么实现的:调用ServerSocket来创建绑定在指定端口上的服务器,然后调用accept方法阻塞,等待客户端的连接,连上后返回socket连接对象。然后将这个连接对象作为参数传入,并实例化一个处理客户端的线程对象,将一个客户对应的处理线程对象加到队列中。启...

weixin_42242323的博客 2203
下一篇: JavaSE-位运算
Zzzkt
博客等级 码龄7年 149粉丝 6原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值