[java]基于socket通信技术实现的简单聊天室
引言
socket,你也许没听说过,但你不可能没用到过(除非你21世纪以来没有接触过电子产品)。常见如QQ,微信,只要存在聊天框和评论的地方,socket都发挥着它的作用。本文章将会分三大模块(socket后端编程,数据库,UI编程),带你DIY一个属于自己的聊天室。
本项目已在GitHub开源:Torchman005/Simple-Chat-Room
socket核心模块:了解客户端与服务端之间信息传递的原理,手刃客户端,服务端
数据库模块:学习如何保存用户信息、群聊信息,实现数据的持久化
UI模块:为用户提供一个易于操作的窗口,美化聊天室,降低使用门槛
本项目需要的工具:jdk17(本人使用),IDEA,maven(IDEA有自带的),Navicat,mysql,javaFX相关依赖,JDBC相关依赖,最低2C2G云服务器,Xterminal(只要能连接服务器就行),一个会思考的大脑
需要会的东西:JavaSE基础,sql语句,数据库操作,maven依赖管理,Linux基本命令,docker操作(不作硬性要求)
socket核心java编程
简介
socket编程属于网络编程的一种经典方式,我们可以通过socket关键字实现网络上的通信。socket(插座),我们可以把它抽象的理解为客户端和服务端都需要一个socket插座,通过网络将这两个插座连接来实现客户端与客户端、客户端与服务端之间信息的传递。
一般的大型项目会使用WebSocket技术以及Netty+SpringBoot来支持大用户量的服务,但本文仅提供了聊天室的入门小项目教程,遂采用传统socket实现聊天功能
环境准备
JDK下载及环境变量配置
鉴于欲编写本项目的人应基本掌握Java基础,这里我会一笔带过
因为本项目是基于java语言编程,所以需要从Java官网下载对应版本的JDK
Java官网:https://www.java.com/zh-CN/
下载安装包后双击安装程序,选择安装路径(可以保持默认),然后一直点下一步,选项都保持默认,直至安装完成
安装完成以后在高级系统设置中添加Path
如果这些东西还需要我详细说的话,建议先从Java基础开始学起→_→
项目编程
开始
首先我们要在IDEA中新建maven项目,然后在main目录下创建包,以下是我文件的目录结构


说明:
cfg存放配置文件
service存放一些聊天功能如私聊、群聊的实现类
chatcommon存放User用户类和Message信息类,客户端与服务端需保持一致
utils为工具包,存放自己写的方便编程的工具类
main和chatframe分别为客户端和服务端的启动类
chatcommon
我们先从这个包入手,因为客户端与服务端的信息传递都靠这个包中的类,只有写完其中的类才能实现双端交互
User类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| package com.jinyu.chatcommon;
import java.io.Serializable;
public class User implements Serializable {
private static final long serialVersionUID = 1L; private String userId; private String pwd; private String userType;
public String getUserType() { return userType; }
public void setUserType(String userType) { this.userType = userType; }
public User(String userId, String pwd) { this.userId = userId; this.pwd = pwd; }
public User() { }
@Override public String toString() { return "User{" + "userId='" + userId + '\'' + ", pwd='" + pwd + '\'' + '}'; }
public String getUserId() { return userId; }
public void setUserId(String userId) { this.userId = userId; }
public String getPwd() { return pwd; }
public void setPwd(String pwd) { this.pwd = pwd; } }
|
声明了用户的userId、pwd,添加了构造方法和getter,setter方法,并重写了toString方法
UserType接口
1 2 3 4 5 6 7
| package com.jinyu.chatcommon;
public interface UserType { String USER_LOGIN = "1"; String USER_REGISTER = "2"; }
|
这个接口用来设定客户端发送给服务端的用户信息类型,判断是要登录还是要注册
Message类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153
| package com.jinyu.chatcommon;
import java.io.Serializable; import java.util.PrimitiveIterator; import java.util.Queue;
public class Message implements Serializable { private static final long serialVersionUID = 1L; private String sender; private String getter; private String content; private String sendTime; private String mesType;
private byte[] fileBytes; private int fileLen = 0; private String dest; private String src; private Queue<String> groupMembers; private Queue<String> onlineUsers; private boolean isUser; private boolean isGroup; private String groupName;
public Message() { }
public Message(String mesType, String content) { this.mesType = mesType; this.content = content; this.sendTime = new java.util.Date().toString(); }
public Message(String mesType) { this.mesType = mesType; this.sendTime = new java.util.Date().toString(); }
public boolean isUser() { return isUser; }
public void setUser(boolean user) { isUser = user; }
public boolean isGroup() { return isGroup; }
public void setGroup(boolean group) { isGroup = group; }
public Queue<String> getOnlineUsers() { return onlineUsers; }
public void setOnlineUsers(Queue<String> onlineUsers) { this.onlineUsers = onlineUsers; }
public Queue<String> getGroupMembers() { return groupMembers; }
public void setGroupMembers(Queue<String> groupMembers) { this.groupMembers = groupMembers; }
public String getGroupName() { return groupName; }
public void setGroupName(String groupName) { this.groupName = groupName; }
public byte[] getFileBytes() { return fileBytes; }
public void setFileBytes(byte[] fileBytes) { this.fileBytes = fileBytes; }
public int getFileLen() { return fileLen; }
public void setFileLen(int fileLen) { this.fileLen = fileLen; }
public String getDest() { return dest; }
public void setDest(String dest) { this.dest = dest; }
public String getSrc() { return src; }
public void setSrc(String src) { this.src = src; }
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 String getContent() { return content; }
public void setContent(String content) { this.content = content; }
public String getSendTime() { return sendTime; }
public void setSendTime(String sendTime) { this.sendTime = sendTime; }
public String getMesType() { return mesType; }
public void setMesType(String mesType) { this.mesType = mesType; } }
|
Message接口(主要设置消息类型):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| package com.jinyu.chatcommon;
public interface MessageType { String MESSAGE_LOGIN_SUCCEED = "1"; String MESSAGE_LOGIN_FAIL = "2"; String MESSAGE_COMM_MES = "3"; String MESSAGE_REQ_ONLINE_USERS = "4"; String MESSAGE_RET_ONLINE_USERS_LIST = "5"; String MESSAGE_CLIENT_EXIT = "6"; String MESSAGE_TO_GROUP_MES = "7"; String MESSAGE_FILE_MES = "8"; String MESSAGE_PULL_GROUP_MES = "9"; String MESSAGE_SEND_TO_ALL = "10"; String MESSAGE_SYSTEM = "11"; String MESSAGE_REGISTER = "12"; String MESSAGE_REGISTER_SUCCEED = "12"; String MESSAGE_REGISTER_FAIL = "13"; }
|
通过设置消息类型(MessageType)来让服务端识别不同的功能请求,从而实现不同的功能
chatmain/chatframe
这两个包下都分别只有一个类,那就是程序的启动入口(主类)
客户端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| package com.jinyu.main;
import com.jinyu.ui.LoginUI;
import javafx.application.Application;
public class ChatMain { public static void main(String[] args) { System.setProperty("file.encoding", "UTF-8"); Application.launch(LoginUI.class, args); } }
|
这个main方法启动的是UI界面,UI的逻辑全部写在ui这个包下
服务端:
1 2 3 4 5 6 7 8 9 10
| package com.jinyu.chatframe;
import com.jinyu.chatserver.service.ChatServer; import java.io.IOException;
public class ChatFrame { public static void main(String[] args) throws Exception { new ChatServer(); } }
|
通过new个ChatServer对象来启动服务端
resources
resources中专门存放项目的各种配置文件,在这个项目中,我们要写的配置文件十分简单
客户端:
1 2 3
| host=127.0.0.1 port=2323
|
这里配置了要连接的服务器的ip和端口
服务端:
service
注册与登录功能
前言
无论是注册还是登录,都需要用户的信息,而用户的信息需要用户自己在UI界面给的输入框中输入回车才会读取,所以在LoginUI和RegisterUI类中,我会读取输入框用户输入的数据,紧接着调用ToUserFunction中的注册和登录方法并传入参数
关于UI方面都会在第三模块中解释,所以在这里不作代码演示,我们直接来看service中的逻辑<( ̄︶ ̄)↗[GO!]

注册
众所周知,不管你用哪个应用或上哪个网站,用户都得先注册账号,将用户信息存储在数据库才能够使用原有账号密码登录,所以我这里先介绍注册的逻辑及实现
客户端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| public boolean registerUser(String userId, String pwd) { boolean b = false; try { ClassLoader classLoader = ToUserFunction.class.getClassLoader(); InputStream input = classLoader.getResourceAsStream("config.properties"); Properties prop = new Properties(); prop.load(input); String host = prop.getProperty("host"); String sport = prop.getProperty("port"); int port = Integer.parseInt(sport);
socket = new Socket(InetAddress.getByName(host), port);
user.setUserId(userId); user.setPwd(pwd); user.setUserType(UserType.USER_REGISTER); ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream()); oos.writeObject(user);
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream()); Message mes = (Message)ois.readObject();
if(mes.getMesType().equals(MessageType.MESSAGE_REGISTER_SUCCEED)) { b = true; } else { displayMessage(new Message(MessageType.MESSAGE_SYSTEM, "注册失败(用户名已存在)")); socket.close(); } } catch (Exception e) { displayMessage(new Message(MessageType.MESSAGE_SYSTEM, "注册失败: " + e.getMessage())); } return b; }
|
这个是用户在客户端的注册实现,我们来剖析一下
首先,我们需要让客户端与服务端建立连接,就需要用到socket
1
| Socket socket = new Socket(InetAddress.getByName(host), port);
|
这里我们首先创建了一个socket对象,其中的参数分别是你要连接的服务器的InetAddress对象和所在端口号(这里解释一下,InetAddress类中的getByName方法是利用主机的IP地址来返回InetAddress对象的),所以要读取resources中的配置文件(config)
此时服务端需要监听来自客户端socket的动作,客户端与服务端之间才能建立连接,于是在服务端写出如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| ServerSocket ss = null; public ChatServer() throws Exception{
try { new Thread(new SendNewsToAllService()).start(); ClassLoader classLoader = ChatServer.class.getClassLoader(); InputStream input = classLoader.getResourceAsStream("config.properties"); Properties prop = new Properties(); prop.load(input); String sport = prop.getProperty("port"); int port = Integer.parseInt(sport); System.out.println("服务端在" + port + "端口监听"); ss = new ServerSocket(port);
while(true){ Socket socket = ss.accept(); ObjectInputStream ois = new ObjectInputStream(socket.getInputStream()); ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream()); User user = (User) ois.readObject(); Message message = new Message(); } catch (Exception e) { e.printStackTrace(); }finally { ss.close(); } }
|
这整个代码都是在ChatServer这个类的构造方法中执行的,保证服务端启动后这些代码会立即执行
可以看出,服务端首先也是读取配置文件,创建ServerSocket对象,把从配置文件读取的端口号(port)作为参数传入
1
| ServerSocket ss = new ServerSocket(port);
|
然后调用ServerSocket的accept方法来监听客户端连接
1
| Socket socket = ss.accept();
|
注意:因为将会有多个客户端登录,所以客户端只监听一次是远远不够的,这时就需要在外面包裹一层while循环来持续监听
至此,客户端与服务端之间已建立连接
服务端就先告一段落,让我们回到客户端
客户端:
1 2 3 4 5 6
|
user.setUserId(userId); user.setPwd(pwd); user.setUserType(UserType.USER_REGISTER);
|
用户的注册,就是要把用户输入的信息传到服务端,让服务端写入数据库
这里的目的,就是封装好User对象,set好User的Id,pwd和你此次传输的用户信息类型(UserType),然后通过IO流传给服务端
下面,是创建ObjectInputStream和ObjectOutputStream来读写User对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream()); oos.writeObject(user);
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream()); Message mes = (Message)ois.readObject();
if(mes.getMesType().equals(MessageType.MESSAGE_REGISTER_SUCCEED)) { b = true; } else { displayMessage(new Message(MessageType.MESSAGE_SYSTEM, "注册失败(用户名已存在)")); socket.close(); } } catch (Exception e) { displayMessage(new Message(MessageType.MESSAGE_SYSTEM, "注册失败: " + e.getMessage())); }
|
ObjectOutputStream写入服务端的socket后,服务端读入
1 2 3 4 5 6
|
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream()); User user = (User) ois.readObject();
Message message = new Message();
|
读入以后就要判断这个User的类型了,判断它是登录还是注册
1 2 3 4 5 6 7 8 9 10
| if(user.getUserType().equals(UserType.USER_REGISTER)){ message.setMesType(MessageType.MESSAGE_REGISTER_SUCCEED); } else { message.setMesType(MessageType.MESSAGE_REGISTER_FAIL); } oos.writeObject(message); }
|
随后将message传给客户端
oos.writeObject(message);
客户端:
1 2 3 4 5 6 7 8 9 10 11 12 13
|
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream()); Message mes = (Message)ois.readObject();
if(mes.getMesType().equals(MessageType.MESSAGE_REGISTER_SUCCEED)) { b = true; } else { displayMessage(new Message(MessageType.MESSAGE_SYSTEM, "注册失败(用户名已存在)")); socket.close(); }
|
客户端接收到来自客户端注册成功与否的信息,并将结果返回
注册逻辑已完成o( ̄︶ ̄)o
登录
其实登录的大体逻辑和注册差不多,只不过多了验证Id、密码和创建用户线程的步骤
客户端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
|
public boolean checkUser(String userId, String pwd){ user.setUserId(userId); user.setPwd(pwd); user.setUserType(UserType.USER_LOGIN);
boolean b = false; try { ClassLoader classLoader = ToUserFunction.class.getClassLoader(); InputStream input = classLoader.getResourceAsStream("config.properties"); Properties prop = new Properties(); prop.load(input); String host = prop.getProperty("host"); String sport = prop.getProperty("port"); int port = Integer.parseInt(sport);
socket = new Socket(InetAddress.getByName(host), port);
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream()); oos.writeObject(user);
return b; }
|
服务端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
if(user.getUserType().equals(UserType.USER_LOGIN)){ if(){ message.setMesType(MessageType.MESSAGE_LOGIN_SUCCEED); ServerConnectClientThread thread = new ServerConnectClientThread(socket, user.getUserId()); thread.start(); ClientThreadsManage.addServerConnectClientThread(user.getUserId(), thread);
OnlineUsers.addOnlineUsers(user.getUserId());
oos.writeObject(message); }else{ System.out.println("用户账号或密码不正确"); message.setMesType(MessageType.MESSAGE_LOGIN_FAIL); oos.writeObject(message); socket.close(); } }
|
服务端验证用户Id和密码是否匹配,若匹配则登陆成功,给message设置登陆成功的类型,并增加对应用户线程
客户端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| ObjectInputStream ois = new ObjectInputStream(socket.getInputStream()); Message mes = (Message)ois.readObject();
if(mes.getMesType().equals(MessageType.MESSAGE_LOGIN_SUCCEED)){ b = true; ClientConnectServerThread thread = new ClientConnectServerThread(socket, chatUI); thread.start(); ClientConnServerThreadsManage.addClientConnectServerThread(userId, thread);
}else{ displayMessage(new Message(MessageType.MESSAGE_SYSTEM, "用户名或密码不正确")); socket.close(); } } catch (Exception e) { displayMessage(new Message(MessageType.MESSAGE_SYSTEM, "登录失败: " + e.getMessage())); }
|
客户端再次接收到服务端传来的信息,确定自己是登录成功还是登录失败,若登陆成功则添加自身对应线程
这里遇到了一个新的内容:线程
认真思考的人已经注意到了,既然这里new了一个新线程,那这个线程类的内容是什么呢?
如你所见,下面分别是客户端和服务端的线程类:
客户端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
|
package com.jinyu.chatclient.service;
import com.jinyu.chatcommon.Message; import com.jinyu.chatcommon.MessageType; import com.jinyu.ui.ChatUI; import com.jinyu.utils.Utility;
import javax.management.ObjectName; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInput; import java.io.ObjectInputStream; import java.net.Socket; import java.util.Queue;
public class ClientConnectServerThread extends Thread{ private Socket socket; private ChatUI chatUI;
public ClientConnectServerThread(Socket socket,ChatUI chatUI){ this.socket = socket; this.chatUI = chatUI; } @Override public void run() { while (true) {
try { ObjectInputStream ois = new ObjectInputStream(socket.getInputStream()); Message mes = (Message) ois.readObject(); if (mes.getMesType().equals(MessageType.MESSAGE_RET_ONLINE_USERS_LIST)) { Queue<String> onlineUsers = mes.getOnlineUsers(); chatUI.updateOnlineUsers(onlineUsers); } else if (mes.getMesType().equals(MessageType.MESSAGE_COMM_MES)) { chatUI.displayMessage(mes); } else if(mes.getMesType().equals(MessageType.MESSAGE_FILE_MES)){ chatUI.displayMessage(mes); } else if(mes.getMesType().equals(MessageType.MESSAGE_SEND_TO_ALL)){ chatUI.displayMessage(mes); } else if(mes.getMesType().equals(MessageType.MESSAGE_TO_GROUP_MES)){ if(mes.isGroup()){ chatUI.displayMessage(mes); } else{ chatUI.displayMessage(new Message(MessageType.MESSAGE_SYSTEM, "无此群聊>﹏<")); } } else { chatUI.displayMessage(new Message(MessageType.MESSAGE_SYSTEM, "其他类型的信息,暂时不做处理")); }
} catch (Exception e) { chatUI.displayMessage(new Message(MessageType.MESSAGE_SYSTEM, "接收消息错误: " + e.getMessage())); break; } } } public Socket getSocket(){ return socket; } }
|
服务端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109
|
package com.jinyu.chatserver.service;
import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.net.Socket; import java.util.Iterator; import java.util.Queue;
import com.jinyu.chatcommon.Message; import com.jinyu.chatcommon.MessageType;
public class ServerConnectClientThread extends Thread{ private Socket socket; private String userId; public ServerConnectClientThread(Socket socket, String userId){ this.socket = socket; this.userId = userId; } public Socket getSocket(){ return this.socket; } @Override public void run(){ while(true){ try { System.out.println(userId + "已连接并保持通信"); ObjectInputStream ois = new ObjectInputStream(socket.getInputStream()); Message mes = (Message) ois.readObject(); if(mes.getMesType().equals(MessageType.MESSAGE_REQ_ONLINE_USERS)){ System.out.println(mes.getSender() + "请求获取在线用户列表"); Queue<String> onlineUsers = ClientThreadsManage.getOnlineUsers(); Message mes2 = new Message(); mes2.setMesType(MessageType.MESSAGE_RET_ONLINE_USERS_LIST); mes2.setOnlineUsers(onlineUsers); mes2.setGetter(mes.getSender());
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream()); oos.writeObject(mes2); }else if(mes.getMesType().equals(MessageType.MESSAGE_CLIENT_EXIT)){ ClientThreadsManage.removeSCCThread(mes.getSender()); System.out.println(userId + "退出登录"); OnlineUsers.deleteUser(userId); socket.close(); break; }else if(mes.getMesType().equals(MessageType.MESSAGE_COMM_MES)){ ObjectOutputStream oos = null; try { oos = new ObjectOutputStream(ClientThreadsManage.getServerConnectClientThread(mes.getGetter()).getSocket().getOutputStream()); oos.writeObject(mes); } catch (Exception e) { System.out.println( mes.getGetter() + "不在线,无法私聊"); } } else if(mes.getMesType().equals(MessageType.MESSAGE_FILE_MES)){ ServerConnectClientThread thread = ClientThreadsManage.getServerConnectClientThread(mes.getGetter()); ObjectOutputStream oos = new ObjectOutputStream(thread.getSocket().getOutputStream()); oos.writeObject(mes); } else if(mes.getMesType().equals(MessageType.MESSAGE_PULL_GROUP_MES)) { Groups.addGroup(mes.getGroupName(), mes.getGroupMembers()); } else if(mes.getMesType().equals(MessageType.MESSAGE_TO_GROUP_MES)){ if(Groups.hasGroup(mes.getGroupName())){ mes.setGroup(true); Queue<String> group = Groups.getGroup(mes.getGroupName()); Iterator<String> iterator = group.iterator(); while(iterator.hasNext()){ String onlineUser = iterator.next(); if(!onlineUser.equals(mes.getSender())){ ObjectOutputStream oos = new ObjectOutputStream(ClientThreadsManage.getServerConnectClientThread(onlineUser).getSocket().getOutputStream()); oos.writeObject(mes); } } } else{ System.out.println("不存在此群聊"); mes.setGroup(false); ObjectOutputStream oos = new ObjectOutputStream(ClientThreadsManage.getServerConnectClientThread(mes.getSender()).getSocket().getOutputStream()); oos.writeObject(mes); } } else{ System.out.println("其他类型的信息,暂时不作处理"); } } catch (IOException e) { System.out.println(userId + "退出登录"); ClientThreadsManage.removeSCCThread(userId); OnlineUsers.deleteUser(userId); try { socket.close(); } catch (IOException ex) { ex.printStackTrace(); } break; } catch (ClassNotFoundException e) { e.printStackTrace(); } } } }
|
客户端和服务端的线程类代码看似很长,其实逻辑很简单,那就是接收来自服务端/客户端的信息,判断信息的类型来实现相应的代码/功能
创建这些线程后,得需要有一个专门的单位去管理它们,于是就出现了线程管理类
客户端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
package com.jinyu.chatclient.service;
import java.util.HashMap;
public class ClientConnServerThreadsManage { private static HashMap<String, ClientConnectServerThread> hm = new HashMap<>();
public static void addClientConnectServerThread(String userId, ClientConnectServerThread thread) { hm.put(userId, thread); }
public static ClientConnectServerThread getClientConnectServerThread(String userId) { return hm.get(userId); }
public static void removeCCSThread(String userId){ hm.remove(userId); } }
|
服务端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
|
package com.jinyu.chatserver.service;
import java.util.HashMap; import java.util.Queue;
public class ClientThreadsManage { private static HashMap<String, ServerConnectClientThread> threads = new HashMap<>();
public static HashMap<String, ServerConnectClientThread> getThreads() { return threads; }
public static void addServerConnectClientThread(String userId, ServerConnectClientThread serverConnectClientThread){
threads.put(userId, serverConnectClientThread);
} public static ServerConnectClientThread getServerConnectClientThread(String userId){ return threads.get(userId); } public static Queue<String> getOnlineUsers(){ return OnlineUsers.onlineUsers; } public static void removeSCCThread(String userId){ threads.remove(userId); } }
|
每次创建线程之后,需将线程纳入线程管理类,将线程都放入一个集合统一管理
了解完线程的大体内容之后,我们再回到登录逻辑
添加好对应用户的线程之后,这名用户便成为了我们平常所说的在线用户
登录已完成,看似复杂,实际上只要捋清楚每个类的职责,并不困难(迫真)
私聊功能
其实本项目的大体框架已经完成,剩下的功能实现逻辑都很相似,我在这就先就着一个典型例子详细介绍
就比如私聊逻辑,一开始客户端选择进行私聊,并将发送者ID(senderId)、接收者ID(getterId)、发送的内容(content)、消息类型(MessageType)封装进message对象中发送给服务端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
public void sendMessageToOne( String content, String senderId, String getterId){ Message mes = new Message(); mes.setMesType(MessageType.MESSAGE_COMM_MES); mes.setSender(senderId); mes.setContent(content); mes.setGetter(getterId); mes.setSendTime(new Date().toString()); System.out.println("\n" + mes.getSendTime() + " " + mes.getSender() + ": " + mes.getContent());
try { ObjectOutputStream oos = new ObjectOutputStream(ClientConnServerThreadsManage.getClientConnectServerThread(senderId).getSocket().getOutputStream()); oos.writeObject(mes); } catch (Exception e) { e.printStackTrace(); } }
|
服务端接收信息的代码就由线程类来执行
1 2 3 4 5 6 7 8 9 10 11 12
|
if(mes.getMesType().equals(MessageType.MESSAGE_COMM_MES)){ ObjectOutputStream oos = null; try { oos = new ObjectOutputStream(ClientThreadsManage.getServerConnectClientThread(mes.getGetter()).getSocket().getOutputStream()); oos.writeObject(mes); } catch (Exception e) { System.out.println( mes.getGetter() + "不在线,无法私聊"); } }
|
OK,服务端接收了来自客户端的message,那么服务端在这里就起到一个转发的作用,有了getterId,我们就可以从线程管理类中找到这个getterId对应的线程,服务端就可以准确地把message发送给这个客户端,接收者接收到message以后,客户端线程就会再次判断信息的类型,如果是私聊类型,便会输出私聊消息的内容到指定区域
其他功能
私聊功能已完成,其他功能大同小异,我不会再详细剖析,我会交代一些细节,读者可以自己尝试实现
获取在线用户列表:实现这个功能主要在于如何获取在线用户Id,而我们知道判断一个用户是否在线就要看线程集合中有没有这个用户Id对应的线程,所以,我们可以遍历线程集合来获取在线用户的Id并传给客户端
创建群聊:拉群需要知道群聊名、在线用户都有谁,我们可以将每个群的群成员放入到一个队列中,再将群聊放入一个HashMap中集中管理,key为群聊名,value为用户队列
群聊:跟私聊的逻辑实现差不多,只不过这个要发给好几个人,就要在要发的群聊的用户队列中遍历用户来传信息
文件发送:跟私聊,群聊很相似,唯一不同就是要使用File的IO流来传文件
服务端新闻推送:主要还是遍历,遍历每个在线客户端,给它们发消息,并且用while循环不断读取需要发送的消息
无异常退出:这个需要在用户想要退出的时候,服务端从管理线程的集合中移除该用户线程
后端模块已基本完成,现在你就可以打开服务端,多开客户端来在控制台体验一下自己写的无UI聊天室