目录
项目开发流程
多用户网络通信系统的架构设计
客户端
界面层
服务层
管理层
服务端
服务层
功能层
管理层
总结
项目开发流程
多用户网络通信系统的架构设计
- 整体
作为一个可供多个用户使用的通信系统,那么每个用户和其他用户之间的连接必定不是直接的单通道方式,这样形成的项目会杂乱无章并且需要实时的进行通道的建立和检验。为了解决这种麻烦,分别建立一个客户端和服务端,用户所在的一方为客户端,客户端与其他客户端之间的联系方式为,客户端先连接到服务端,然后由服务端进行消息的转发到发送消息的客户端指定的另一方客户端。 - 分布
- 客户端
客户端进行用户的登录,服务器返回账号和密码的合法与否并返回客户端;如果登录成功,则在客户端启动一个新线程接收消息,保持和服务端之间的实时通讯,并且在相应的功能模块进行消息的编辑发送到服务器端,如果登录失败,则提示用户重新登陆或者退出系统。 - 服务端
服务端新开一个端口,循环实时监听客户端的连接请求;如果监听到客户端的请求,并且客户端正常登录后,则创建一个新线程,用于实时接收客户端发送的信息,主线程则继续进行循环,保持相应连接端口的监听。客户端在建立消息对象发送到相应的和服务端之间的通道时,服务端进行消息对象的接收,并且对该消息的类型进行分析做出相应的处理(回复或者转发)。
- 客户端
- 对象成员设计
消息对象Message,客户端用户对象User,消息类型接口MessageType//Message对象 package com.shuai.qqcommon; import java.io.Serializable; 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 fileLength; //文件的长度 private String src; private String dest; public byte[] getFileBytes() { return fileBytes; } public void setFileBytes(byte[] fileBytes) { this.fileBytes = fileBytes; } public int getFileLength() { return fileLength; } public void setFileLength(int fileLength) { this.fileLength = fileLength; } public String getSrc() { return src; } public void setSrc(String src) { this.src = src; } public String getDest() { return dest; } public void setDest(String dest) { this.dest = dest; } public String getMesType() { return mesType; } public void setMesType(String mesType) { this.mesType = mesType; } 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; } }
//消息类型接口 package com.shuai.qqcommon; public interface MessageType { String MESSAGE_LOGIN_SUCCESS = "1";//登录成功 String MESSAGE_LOGIN_FAIL = "2";//登录失败 String MESSAGE_COMM_MES = "3";//普通信息包 String MESSAGE_GET_ONLINE_FRIEND = "4";//要求返回在线用户列表 String MESSAGE_RET_ONLINE_FRIEND = "5";//返回在线用户列表 String MESSAGE_CLIENT_EXIT = "6";//客户端请求退出 String MESSAGE_ALL_MES = "7"; //群发类型 String MESSAGE_FILE_MES = "8"; //文件类型 }
//客户端用户对象 package com.shuai.qqcommon; import java.io.Serializable; public class User implements Serializable { //保证兼容性 private static final long serialVersionUID = 1L; private String userId;//用户名 private String password;//用户密码 public User() {} public User(String userId, String password) { this.userId = userId; this.password = password; } public String getUserId() { return userId; } public void setUserId(String userId) { this.userId = userId; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } }
客户端
界面层
- 登录界面
- 交互界面
服务层
服务用户登录推出和请求在线用户列表的UserClientService类:
将用户输入的账号和密码包装成一个登录类型的信息发送到服务器进行核验,服务器返回核验的结果。如果登录成功,则启动一个新线程和服务端建立实时的socket连接,并且将该线程加入到线程管理类的集合中;登录失败则退出。
public class UserClientService {
private User user = new User();
private Socket socket;
/**
* 检验用户登录信息
*/
public boolean checkUser(String userId,String pass) {
boolean b = false;
user.setUserId(userId);
user.setPassword(pass);
try {
socket = new Socket(InetAddress.getLocalHost(),9999);
//发送user
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
oos.writeObject(user);
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
Message mess = (Message)ois.readObject();
if(MessageType.MESSAGE_LOGIN_SUCCESS.equals(mess.getMesType())) { //登录成功
//创建一个线程始终和服务端保持连接
ClientConnectServerThread clientConnectServerThread = new ClientConnectServerThread(socket);
clientConnectServerThread.start();
//为了客户端的扩展,将鲜橙放入到一个集合中进行管理
ManageClientConnectServerThread.addClientConnectServerThread(userId,clientConnectServerThread);
b = true;
} else {
socket.close();
}
} catch(ClassNotFoundException e) {
e.printStackTrace();
}
catch (IOException e) {
throw new RuntimeException(e);
}
return b;
}
/**
* 显示在线用户列表
*/
public void printOnlineUser() {
Message message = new Message();
message.setMesType(MessageType.MESSAGE_GET_ONLINE_FRIEND);
message.setSender(user.getUserId());
//将Message对象发送给服务器
try {
ObjectOutputStream oos = new ObjectOutputStream
(ManageClientConnectServerThread.getClientConnectServerThread(user.getUserId()).getSocket().getOutputStream());
oos.writeObject(message);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 退出系统
*/
public void exitSystem() {
Message mess = new Message();
mess.setSender(user.getUserId());
mess.setMesType(MessageType.MESSAGE_CLIENT_EXIT);
try {
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
oos.writeObject(mess);
System.exit(0);//结束进程,进程内包含的线程也都结束
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
服务用户登录后用户和服务器保持实时连接的线程类ClientConnectServerThread:
用户登录成功后,启动该线程类对象,在run方法中创建该客户端连接服务端的socket对象,并获得相应的输入流,实时接收来自服务器端的消息。
package com.shuai.qqclient.service;
import com.shuai.qqcommon.Message;
import com.shuai.qqcommon.MessageType;
import java.io.*;
import java.net.Socket;
public class ClientConnectServerThread extends Thread {
private Socket socket;
private boolean loop = true;
public ClientConnectServerThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
while (loop) {
try {
ObjectInputStream oos = new ObjectInputStream(socket.getInputStream());
Message mess = (Message) oos.readObject();
if (mess.getMesType().equals(MessageType.MESSAGE_RET_ONLINE_FRIEND)) {
System.out.println("== 在线用户列表 ==");
String[] s = mess.getContent().split(" ");//得到包含在线用户的数组
for (int i = 0; i < s.length; i++) {
System.out.println("在线用户: " + s[i]);
}
System.out.println("\n");
} else if (mess.getMesType().equals(MessageType.MESSAGE_COMM_MES)) {
System.out.println(mess.getSender() + " 对你说: " + mess.getContent()
+ "(" + mess.getSendTime() + ")");
} else if (mess.getMesType().equals(MessageType.MESSAGE_ALL_MES)) {
System.out.println(mess.getSender() + " 群发消息说: " + mess.getContent()
+ " (" + mess.getSendTime() + ")");
} else if (mess.getMesType().equals(MessageType.MESSAGE_FILE_MES)) { //文件类型
FileOutputStream fos = null;
System.out.println(mess.getSender() + " 给你发送了文件保存在" + mess.getDest());
try {
fos = new FileOutputStream(mess.getDest());
fos.write(mess.getFileBytes(),0,mess.getFileLength()); //将sender发送过来的文件保存到硬盘
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
} else {
System.out.println("其他类型的message,正在开发...");
}
} catch (IOException | ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
}
public Socket getSocket() {
return socket;
}
}
服务客户端发送信息的MessageService类:
界面层接收用户的特定输入并且调用服务层MessageService类中的对应方法发送给服务器指定消息类型的Message消息对象。
package com.shuai.qqclient.service;
import com.shuai.qqcommon.Message;
import com.shuai.qqcommon.MessageType;
import java.io.*;
import java.util.Date;
public class MessageService {
/**
* 发送普通消息
* @param sender 消息的发送者
* @param getter 消息的接收者
* @param content 消息的内容
*/
public static void sendMessage(String sender, String getter, String content) {
//根据客户端用户的输入创建爱你相应的消息对象,指定消息类型
Message message = new Message();
message.setSender(sender);
message.setGetter(getter);
message.setContent(content);
message.setMesType(MessageType.MESSAGE_COMM_MES);
message.setSendTime(new Date().toString());
try { //得到socket通道的对象输出流
ObjectOutputStream oos = new ObjectOutputStream(ManageClientConnectServerThread.getClientConnectServerThread(sender).getSocket().getOutputStream());
oos.writeObject(message);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 群发消息
* @param sender 消息的发送者
* @param content 消息的内容
*/
public static void sendMessageToUsers(String sender, String content) {
Message message = new Message();
message.setMesType(MessageType.MESSAGE_ALL_MES);
message.setContent(content);
message.setSender(sender);
message.setSendTime(new Date().toString());
try {
ObjectOutputStream oos = new ObjectOutputStream(ManageClientConnectServerThread.getClientConnectServerThread(sender).getSocket().getOutputStream());
oos.writeObject(message);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 发送文件
* @param sender 消息的发送者
* @param getter 消息的接收者
* @param src 待发送文件的路径
* @param dest 对方接收文件后的存储路径
*/
public static void sendFileToUser(String sender, String getter, String src, String dest) {
Message message = new Message();
message.setSendTime(new Date().toString());
message.setMesType(MessageType.MESSAGE_FILE_MES);
message.setSrc(src);
message.setDest(dest);
message.setGetter(getter);
message.setSender(sender);
//读取文件到程序
FileInputStream fis = null;
byte[] fileBytes = new byte[(int) new File(src).length()];
try {
fis = new FileInputStream(src);
int read = fis.read(fileBytes);
message.setFileBytes(fileBytes);
message.setFileLength(read);
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
System.out.println("你给 " + getter + " 发送了文件: " + src + " 到对方的: " + dest);
try {
ObjectOutputStream oos = new ObjectOutputStream(ManageClientConnectServerThread.getClientConnectServerThread(sender).getSocket().getOutputStream());
oos.writeObject(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
管理层
管理用户用于实时接收服务端信息的ManageClientConnectServerThread类:
将所有登陆成功的客户端的ClientConnectServerThread类放在一个集合中集中管理,并且提供相应的方法供外界进行集合账户的添加和获得。方便了客户端群体账户的统一管理和获得。
package com.shuai.qqclient.service;
import java.util.HashMap;
public class ManageClientConnectServerThread {
private static HashMap<String, ClientConnectServerThread> hm = new HashMap<>();
public static void addClientConnectServerThread(String userId, ClientConnectServerThread clientConnectServerThread) {
hm.put(userId, clientConnectServerThread);
}
public static ClientConnectServerThread getClientConnectServerThread(String userId) {
return hm.get(userId);
}
}
服务端
服务层
服务于客户端登录程序的QQServer类:
在该类的构造方法中循环监听创建的端口,发现有来自客户端的登录请求时,就获得相应端口的输入流,接收登录类型的消息,并且对接收的Message对象进行解体拿到相应的密码进行核验,并获得相应的输出流返回返回包含核验结果的消息对象。如果核验成功,揪心启动一个线程并且获得与该客户端进行通信的socket接口的输入流,实时接收来自客户端的消息,并且将该线程加入到服务端线程管理类ManageServerConnectClientThread中的线程管理集合中。
package qqserver.service;
import com.shuai.qqcommon.Message;
import com.shuai.qqcommon.MessageType;
import com.shuai.qqcommon.User;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.concurrent.ConcurrentHashMap;
public class QQServer {
private ServerSocket serverSocket;
//保存用户信息
//使用concurrentHashMap解决线程安全问题
private static ConcurrentHashMap<String,User> validUsers = new ConcurrentHashMap<>();
static { //初始化validUsers
validUsers.put("100",new User("100","123456"));
validUsers.put("200",new User("200","123456"));
validUsers.put("300",new User("300","123456"));
validUsers.put("白小帅",new User("白小帅","123456"));
validUsers.put("admin",new User("admin","123456"));
}
/**
* 检查用户信息
* @param userId 用户id
* @param pass 用户密码
* @return boolean
*/
private boolean checkUserInfo(String userId,String pass) {
User user = validUsers.get(userId);
if(user == null) {
return false;
}
if(!(user.getPassword().equals(pass))) {
return false;
}
return true;
}
public QQServer() {
try {
System.out.println("服务端监听客户端连接(9999端口)...");
new Thread(new sendNewsService()).start();
serverSocket = new ServerSocket(9999);
while (true) {
Socket socket = serverSocket.accept();
//验证客户端发送的user对象
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
User user = (User) ois.readObject();
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
//构建一个Message对象返回
Message message = new Message();
//验证用户
if (checkUserInfo(user.getUserId(), user.getPassword())) {
System.out.print("用户: " + user.getUserId() + " 登录... === ");
message.setMesType(MessageType.MESSAGE_LOGIN_SUCCESS);
oos.writeObject(message);
//启动一个新线程与客户端保持实时连接
ServerConnectClientThread serverConnectClientThread = new ServerConnectClientThread(socket, user.getUserId());
serverConnectClientThread.start();
//将对应的连接客户端的线程放入到线程管理集合中
ManageServerConnectClientThread.addServerConnectClientThread(user.getUserId(), serverConnectClientThread);
//判断用户是否存在离线消息并进行推送
UnLineMessageService.putSelfUnLineMessage(user.getUserId());
} else {
//验证不成功
message.setMesType(MessageType.MESSAGE_LOGIN_FAIL);
oos.writeObject(message);
socket.close();
}
}
} catch (IOException | ClassNotFoundException e) {
throw new RuntimeException(e);
} finally {
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
实时连接客户端的ServerConnectClientThread类:
循环接收来自客户端的消息,并且根据接收的Message对象的MessageType属性分别进行不同的消息处理。
package qqserver.service;
import com.shuai.qqcommon.Message;
import com.shuai.qqcommon.MessageType;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;
import java.util.Iterator;
import java.util.Set;
public class ServerConnectClientThread extends Thread {
private Socket socket;
private String userId;
public ServerConnectClientThread(Socket socket, String userId) {
this.socket = socket;
this.userId = userId;
}
@Override
public void run() {
System.out.println("服务端保持着和 " + userId + "客户端的实时通讯...");
while (true) {
try {
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
Message mess = (Message) ois.readObject(); //客户端发送的信息
if (mess.getMesType().equals(MessageType.MESSAGE_GET_ONLINE_FRIEND)) { //客户端请求在线用户列表
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
System.out.println(mess.getSender() + " 需要在线用户列表");
Message message = new Message();
message.setMesType(MessageType.MESSAGE_RET_ONLINE_FRIEND);
//遍历线程集合,获取在线用户
message.setGetter(mess.getSender());
message.setContent(ManageServerConnectClientThread.getOnlineUserList());
oos.writeObject(message);
} else if (mess.getMesType().equals(MessageType.MESSAGE_CLIENT_EXIT)) {
System.out.println("客户端 " + mess.getSender() + " 退出");
ManageServerConnectClientThread.dropUserThread(userId);
socket.close();
break; //退出run方法
} else if (mess.getMesType().equals(MessageType.MESSAGE_COMM_MES)) { //普通消息
System.out.println("客户端 " + mess.getSender() + " 给客户端 " + mess.getGetter() + " 发送消息");
if (ManageServerConnectClientThread.getServerConnectClientThread(mess.getGetter()) != null) { //用户在线
//拿到getter客户端的线程对应的输出流
ObjectOutputStream oosToGetter = new ObjectOutputStream(ManageServerConnectClientThread.getServerConnectClientThread(mess.getGetter()).getSocket().getOutputStream());
oosToGetter.writeObject(mess);
} else { //用户不在线
UnLineMessageService.addUnLineMessage(mess.getGetter(), mess);
}
} else if (mess.getMesType().equals(MessageType.MESSAGE_ALL_MES)) { //处理群发消息
System.out.println("客户端 " + mess.getSender() + " 群发消息");
//遍历在线用户,发送消息
Set<String> sets = ManageServerConnectClientThread.getHm().keySet();
Iterator<String> iterator = sets.iterator();
while (iterator.hasNext()) {
String next = iterator.next();
if (next.equals(mess.getSender())) {
continue;
}
ObjectOutputStream oos = new ObjectOutputStream(
ManageServerConnectClientThread.getServerConnectClientThread(next).getSocket().getOutputStream());
oos.writeObject(mess);
}
} else if (mess.getMesType().equals(MessageType.MESSAGE_FILE_MES)) { //客户端发送文件
System.out.println("客户端 " + mess.getSender() + " 发送了文件给客户端 " + mess.getGetter());
if (ManageServerConnectClientThread.getServerConnectClientThread(mess.getGetter()) != null) { //用户在线
ObjectOutputStream oos = new ObjectOutputStream(ManageServerConnectClientThread.getServerConnectClientThread(mess.getGetter()).getSocket().getOutputStream());
oos.writeObject(mess); //将信息发送给getter客户端
} else {
UnLineMessageService.addUnLineMessage(mess.getGetter(),mess);
}
} else {
System.out.println("其他类型的信息暂不处理,服务器正在升级哦~");
}
} catch (IOException | ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
}
public Socket getSocket() {
return socket;
}
}
用户转发离线消息的UnLineMessageService类:
该类的对象持有一个用于保存消息获得者离线状态时的ConcurrentHashMap对象,在用户上线后进行消息的推送。
package qqserver.service;
import com.shuai.qqcommon.Message;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.concurrent.ConcurrentHashMap;
public class UnLineMessageService {
private static ConcurrentHashMap<String, ArrayList<Message>> db = new ConcurrentHashMap<>();//保存离线消息
public static void addUnLineMessage(String userId,Message message) {
if(db.get(userId) == null) {
ArrayList<Message> messages = new ArrayList<>();
messages.add(message);
db.put(userId,messages);
} else {
ArrayList<Message> messagesExists = db.get(userId);
messagesExists.add(message);
db.put(userId,messagesExists);
}
}
/**
* 在用户上线后,对用户可能存在的离线消息进行推送
* @param userId 登录成功的用户
*/
public static void putSelfUnLineMessage(String userId) {
if(db.get(userId) != null) {
ArrayList<Message> messages = db.get(userId);
for(Message mess : messages) {
//推送给对应的用户
try {
ObjectOutputStream oos = new ObjectOutputStream(ManageServerConnectClientThread.getServerConnectClientThread(userId).getSocket().getOutputStream());
oos.writeObject(mess);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
db.remove(userId);
}
}
}
功能层
用于发送给客户端公告的线程类SendNewsService:
在服务端运行时即启动该线程接收服务端的输入流信息并通过遍历线程管理类中的服务端线程集合将该信息循环发送给每一个客户端。
package qqserver.service;
import com.shuai.qqcommon.Message;
import com.shuai.qqcommon.MessageType;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.*;
public class sendNewsService implements Runnable {
private Scanner scanner = new Scanner(System.in);
@Override
public void run() {
while (true) {
System.out.println("服务器端推送新闻:[exit退出news推送服务] ");
String content = scanner.next();
if ("exit".equals(content)) {
break;
}
Message message = new Message();
message.setContent(content);
message.setSendTime(new Date().toString());
message.setMesType(MessageType.MESSAGE_ALL_MES);
message.setSender("服务器");
HashMap<String, ServerConnectClientThread> hm = ManageServerConnectClientThread.getHm();
Set<String> onLineUserId = hm.keySet();
Iterator<String> iterator = onLineUserId.iterator();
while (iterator.hasNext()) {
String userId = iterator.next();
try {
ObjectOutputStream oos = new ObjectOutputStream(ManageServerConnectClientThread.getServerConnectClientThread(userId).getSocket().getOutputStream());
oos.writeObject(message);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
}
管理层
管理每一个客户端对应的服务端所启动的每一个线程类的ManageServerConnectClientThread类:
客户端成功上线后,向该类的对象的线程集合中加入对应的线程对象;客户端离线,从集合中删除对应的线程对象。
package qqserver.service;
import java.util.HashMap;
import java.util.Iterator;
public class ManageServerConnectClientThread {
private static HashMap<String,ServerConnectClientThread> hm = new HashMap<>();
public static void addServerConnectClientThread(String userId,ServerConnectClientThread serverConnectClientThread) {
hm.put(userId,serverConnectClientThread);
}
public static ServerConnectClientThread getServerConnectClientThread(String userId) {
return hm.get(userId);
}
public static String getOnlineUserList() {
StringBuffer stringBuffer = new StringBuffer("");
//遍历集合
Iterator<String> iterator = hm.keySet().iterator();
while(iterator.hasNext()) {
stringBuffer.append(iterator.next()+" ");
}
return stringBuffer.toString();
}
//删除集合中的对象
public static void dropUserThread(String userId) {
hm.remove(userId);
}
public static HashMap<String, ServerConnectClientThread> getHm() {
return hm;
}
}
总结
运用到的主要知识点
- 集合
- 多线程编程
- IO流
- 网络通信编程
过程中的设计难点
- 用户的登录和与服务器之间的实时通信
解决方案:用户信息在验证通过后,启动一个新线程,获得服务器端口的socket对象,获得输入流,实时接收服务器的回复信息。 - 无异常退出
解决方案:在用户输入退出请求时,客户端向服务器发送一个退出类型的Message对象后结束进程。服务端在拿到该消息对象后从线程管理集合中删除与该用户保持通信的线程对象并且退出该线程。 - 离线信息和文件的发送
创建一个Map集合(键:userId,值:ArrayList<Message>)用于临时保存离线用户需要接收的消息,得到用户发送的Message对象后拆解得到消息的获得者,判断消息的获得方是否在线,如果不在线,将消息发送者和发送的消息保存在离线消息集合中。在用户登录时检查该用户是否有离线消息,如果有,则进行推送。 - 服务器推送公告
在服务器启动时创建一个新线程,接收输入流的消息,遍历服务端线程管理集合中的每一个线程对象,得到相应socket的输出流,进行内容的推送。