一、前言
千言万语化为一个项目,想说的都在这个项目里面了。这是用网络编程结合多线程完成的一个多用户即时通信系统,跟着韩老师敲出来的。最难最关键的部分在开头,看似仅仅是完成一个用户登录验证,实际上最重大的意义是打通了客户端和服务端的数据通道。后续私聊群聊发文件都是在数据通道上进行,只是根据不同的请求采取不同的逻辑处理。
二、项目概述
这是一个多用户即时通信系统,分为服务端和客户端。
实现的功能有:
- 登录验证(用HashMap + 静态代码块模拟数据库)
- 拉取在线用户列表
- 私聊
- 群聊
- 发文件
- 服务器推送新闻
具体步骤我写在注释里,这里说一下实现思路:
1.所有消息,不管类型如何(私聊群聊发文件等),都是封装成对象,用对象流发。
2.每个用户和服务器的连接都是一个线程,该线程持有socket。所以可以和服务器连接,且能实现多用户,并相对独立。
3.消息类型MessageType非常重要,服务端客户端都根据它采取不同的业务处理方式,它是message对象的一个属性。
4.一般消息处理:客户端发–>服务器收–>服务端业务处理–>服务器转发或回复–>客户端收–>客户端业务处理。
5.服务端把所有线程都放入到一个Map集合中,以便功能的实现,可以通过key(某用户的名字)找到该用户的数据通道。或者遍历集合得到所有用户的数据通道群发消息。
三、测试
登录验证
拉取在线用户列表
私聊
群聊
发文件
服务器推送新闻
四、项目结构
客户端:
服务端:
五、源代码
客户端:
view包
QQView (界面,业务逻辑框架)
package com.serein.qqclient.view;
import com.serein.qqclient.server.FileClientService;
import com.serein.qqclient.server.MessageClientService;
import com.serein.qqclient.server.UserClientServer;
import com.serein.qqclient.utils.Utility;
/**
* 这个类是控制面板,也可以说是界面.
* 是客户端是逻辑框架,调用各个类的方法来完成功能
* 说几点:
* 1.所有消息,不管类型如何(私聊群聊发文件等),都是封装成对象,用对象流发.
* 2.每个用户和服务器的连接都是一个线程,该线程持有socket.所以可以和服务器连接.并且相对独立.
* 3.消息类型MessageType非常重要,服务端客户端都根据它采取不同的业务处理方式
* 4.一般消息处理:客户端发-->服务器收-->服务端业务处理-->服务器转发或回复-->客户端收-->客户端业务处理
* 5.服务端把所有线程都放入到一个Map集合中,以便功能的实现,可以通过key(某用户的名字)找到该用户的数据通道。或者遍历集合得到所有用户的数据通道群发消息。
*/
public class QQView {
private boolean loop =true;//循环用的,业务逻辑置为false是退出循环
private String key = "";//用户输入的指令
//以下创建3个对象,以便调用相关功能
private UserClientServer userClientServer = new UserClientServer();
private MessageClientService messageClientService = new MessageClientService();
private FileClientService fileClientService = new FileClientService();
public static void main(String[] args) {
//启动面板
new QQView().mainMenu();
System.out.println("客户端系统退出。。。");
}
private void mainMenu(){
while (loop) {
System.out.println("========欢迎进入多用户通信系统===========");
System.out.println("\t\t 1 登录系统");
System.out.println("\t\t 9 退出系统");
System.out.print("请输入你的选择:");
//调用工具类,读取一位输入的字符.好处是不用定义scanner再使用,直接调用即可.
key = Utility.readString(1);
//根据key的值进入不同的业务逻辑
switch (key){
case "1":
System.out.println("登录系统");
System.out.print("请输入用户号:");
String userId = Utility.readString(50);
System.out.print("请输入密码:");
String pwd = Utility.readString(50);
//用户名密码是否正确.业务逻辑已经在相关方法里,这里是结果(true or false)
if (userClientServer.cheekUser(userId,pwd)){
System.out.println("======欢迎用户" + userId + "登陆成功==========");
//根据业务逻辑给loop赋值,看是否退出while循环
while (loop){
System.out.println("\n=======网络通信系统二级菜单" + userId + "==========");
System.out.println("\t\t1 显示在线用户列表");
System.out.println("\t\t2 群发消息");
System.out.println("\t\t3 私聊消息");
System.out.println("\t\t4 发送文件");
System.out.println("\t\t9 退出系统");
System.out.print("请输入你的选择:");
key = Utility.readString(1);
switch (key){
case "1":
//调用拉取在线用户的方法
userClientServer.olineFriendList();
break;
case "2":
//群聊,方法在相关类中已写好,这里填写参数调用即可
System.out.println("请输入相对大家说的话:");
String s = Utility.readString(100);
messageClientService.sendMessageToAll(s,userId);
break;
case "3":
//私聊,方法在相关类中已写好,这里填写参数调用即可
System.out.print("请输入想私聊的用户号(在线):");
String getterId = Utility.readString(50);
System.out.print("请输入想说的话:");
String content = Utility.readString(100);
messageClientService.sendMessageToOne(content,userId,getterId);
break;
case "4":
//发送文件,方法在相关类中已写好,这里填写参数调用即可
System.out.print("请输入你想把文件发送给的用户(在线的):");
getterId = Utility.readString(50);
System.out.print("请输入发送文件的路径(例如 d:\\xx.jpg)");
String src = Utility.readString(100);
System.out.print("请输入把文件发送到对方的路径(例如 d:\\yy.jpg)");
String dest = Utility.readString(100);
fileClientService.sendFileToOne(src,dest,userId,getterId);
break;
case "9":
//退出系统,注意,专门写了一个退出的方法.因为这个while循环退出后,
//由main方法启动的其他线程还没有退出,是要抛异常的.必须退出整个进程,详情见logout方法
userClientServer.logout();
loop = false;
break;
}
}
}else {
System.out.println("=========登录失败========");
}
break;
case "9":
loop = false;
break;
}
}
}
}
server包
ClientConnectServerThread(主要业务处理逻辑)
package com.serein.qqclient.server;
import com.serein.qqcommom.Message;
import com.serein.qqcommom.MessageType;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.net.Socket;
/**
* 该类是客户端,和服务器端的主要业务处理逻辑
* 对服务端的回复做出处理
*/
public class ClientConnectServerThread extends Thread{
//创建一个管道
private Socket socket;
public ClientConnectServerThread(Socket socket){
this.socket = socket;
}
@Override
public void run() {
//循环,以便客户端可以多次向服务端发送请求
while (true){
try {
System.out.println("客户端线程,等待读取从服务端发送来的消息。。");
//接收服务端的回应,读取服务端回复的消息
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
Message message = (Message) ois.readObject();
//服务端回复了在线用户列表
if (message.getMesType().equals(MessageType.MESSAGE_RET_ONLINE_FRIEND)){
//把用户列表用数组装起来
String[] onlineUsers = message.getContent().split(" ");
System.out.println("\n========当前在线用户列表========");
//遍历这个数组,并打印
for (int i = 0; i < onlineUsers.length; i++) {
System.out.println("用户: " + onlineUsers[i]);
}
} else if (message.getMesType().equals(MessageType.MESSAGE_COMM_MES)) {
//私聊消息,在接受者的屏幕上,打印发送者和他说的话
System.out.println("\n" + message.getSender()
+ " 对 " +message.getGetter() + " 说: " + message.getContent());
} else if (message.getMesType().equals(MessageType.MESSAGE_TO_ALL_MES)) {
//群聊消息,所有的客户端均显示该消息
System.out.println("\n" + message.getSender() + " 对大家说 " + message.getContent());
} else if (message.getMesType().equals(MessageType.MESSAGE_FILE_MES)) {
//接收文件,这里是打印收发双发以及文件的具体信息
System.out.println("\n" + message.getSender() + " 给 " + message.getGetter()
+ " 发文件:" + message.getSrc() + " 到我的电脑目录 " + message.getContent());
//下载文件(消息正确且在线,则自动下载)
//创建字节输出流管道,说明下载路径(发送者定义的)
FileOutputStream fileOutputStream = new FileOutputStream(message.getDest(), true);
//把文件内容装换成字节数组并写入磁盘
fileOutputStream.write(message.getFileBytes());
fileOutputStream.close();
System.out.println("\n 保存文件成功~");
}else {
System.out.println("是其他类型的message,暂不处理...");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
public Socket getSocket() {
return socket;
}
}
FileClientService(文件发送的方法)
package com.serein.qqclient.server;
import com.serein.qqcommom.Message;
import com.serein.qqcommom.MessageType;
import java.io.*;
/**
* 该类专门处理文件发送的方法
*/
public class FileClientService {
public void sendFileToOne(String src,String dest,String senderId,String getterId){
//设置消息的类型,这里是文件发送,定义好相关参数
Message message = new Message();
message.setMesType(MessageType.MESSAGE_FILE_MES);
message.setSender(senderId);
message.setGetter(getterId);
message.setSrc(src);
message.setDest(dest);
//创建输入流管道,即把文件读入内存
FileInputStream fileInputStream = null;
//创建字节数组,存放文件用
byte[] fileBytes = new byte[(int) new File(src).length()];
try {
//与源文件相连通,并读取到字节数组中
fileInputStream = new FileInputStream(src);
fileInputStream.read(fileBytes);
//把该数组写到消息属性里面去
message.setFileBytes(fileBytes);
} catch (Exception e) {
e.printStackTrace();
}finally {
if (fileInputStream != null){
try {
fileInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
//在发送方电脑上显示这次文件发送的参数信息
System.out.println("\n" + senderId + " 给 " + getterId + " 发送文件:" + src
+ " 到对方的电脑目录 " + dest);
try {
//创建一个输出流(对象流)管道,把消息写入管道.
ObjectOutputStream oos = new ObjectOutputStream(ManageClientConnectServerThread.getClientConnectServerThread(senderId).getSocket().getOutputStream());
oos.writeObject(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
ManageClientConnectServerThread (C–S的管道的集合)
package com.serein.qqclient.server;
import java.util.HashMap;
/**
* 该类在客户端这一端管理和服务端的通信管道
* 做成集合,可以满足用户既私聊又发文件.只有一个管道则一次只能实现一个功能
*/
public class ManageClientConnectServerThread {
//key==>用户id,value===>线程
private static HashMap<String,ClientConnectServerThread> hm = new HashMap<>();
//把该线程(某需求)加入集合
public static void addClientConnectServerThread(String usrID,ClientConnectServerThread clientConnectServerThread){
hm.put(usrID,clientConnectServerThread);
}
public static ClientConnectServerThread getClientConnectServerThread(String uerID){
return hm.get(uerID);
}
}
MessageClientService (私聊和群发的办法)
package com.serein.qqclient.server;
import com.serein.qqcommom.Message;
import com.serein.qqcommom.MessageType;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.Date;
/**
* 该类提供私聊消息和群发的处理办法
*/
public class MessageClientService {
//发消息给所有用户
public void sendMessageToAll(String content,String senderId){
//把相关参数封装成消息.参数传入发送者和内容即可.
Message message = new Message();
message.setMesType(MessageType.MESSAGE_TO_ALL_MES);
message.setSender(senderId);
message.setContent(content);
message.setSendTime(new Date().toString());
System.out.println(senderId + " 对大家说:" + content);
try {
//创建发送者和服务器的输出流通道,把消息写到服务器
ObjectOutputStream oos = new ObjectOutputStream(ManageClientConnectServerThread.getClientConnectServerThread(senderId).getSocket().getOutputStream());
oos.writeObject(message);
} catch (IOException e) {
e.printStackTrace();
}
}
//私聊的方法
public void sendMessageToOne(String content,String senderId,String getterId){
//封装该类型的消息,参数需要传入发送者,接受者,内容
Message message = new Message();
message.setMesType(MessageType.MESSAGE_COMM_MES);
message.setSender(senderId);
message.setGetter(getterId);
message.setContent(content);
message.setSendTime(new Date().toString());
System.out.println(senderId + " 对 " + getterId + " 说:" + content);
try {
//创建发送者和服务端的输入流通道,把消息写入服务端.
//服务端再找到接收用户并转发,那就是服务端的事情了.
ObjectOutputStream oos = new ObjectOutputStream(ManageClientConnectServerThread.getClientConnectServerThread(senderId).getSocket().getOutputStream());
oos.writeObject(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
UserClientServer (用户本身操作登录退出等方法)
package com.serein.qqclient.server;
import com.serein.qqcommom.Message;
import com.serein.qqcommom.MessageType;
import com.serein.qqcommom.User;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.InetAddress;
import java.net.Socket;
/**
* 该类主要提供用户在客户端的用户本身操作的一些方法
*/
public class UserClientServer {
//创建一个用户对象,表示该该主体是自己
private User u = new User();
private Socket socket;
//登录的验证,验证当然是服务端的事情.这里是,把用户名密码提交过去.
// 然后根据服务器判断的结果做出相应处理
//有那么点司法权(我判案子对错,怎么执行我不管)和行政权(事情是我来做的,原则上我要根据他的判断来做)分立的意思在里面
public boolean cheekUser(String userId, String pwd) {
//先默认false,等服务器验证通过后,再置为true
boolean b = false;
u.setUserId(userId);
u.setPasswd(pwd);
try {
//建立输出通道,并且把用户类型的消息发给服务器
socket = new Socket(InetAddress.getByName("127.0.0.1"), 9999);
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
oos.writeObject(u);
//读取服务器返回的结果,看用户密码是否合法
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
Message ms = (Message) ois.readObject();
//服务器认为用户名面正确
if (ms.getMesType().equals(MessageType.MESSAGE_LOGIN_SUCCEED)) {
//创建一个线程并启动,即该用户上线了
ClientConnectServerThread clientConnectServerThread = new ClientConnectServerThread(socket);
clientConnectServerThread.start();
//把该线程加入集合中,以便统一管理.当然,在客户端这边更大的意义在于后续扩展
ManageClientConnectServerThread.addClientConnectServerThread(userId, clientConnectServerThread);
//服务端说这是对的,所以我遵从这个结果,把判断置位true
b = true;
} else {
//用户名密码错了,关闭管道
socket.close();
}
} catch (Exception e) {
e.printStackTrace();
}
//返回判断的结果
return b;
}
//拉取在线用户列表
public void olineFriendList(){
//封装消息类型
Message message = new Message();
message.setMesType(MessageType.MESSAGE_GET_ONLINE_FRIEND);
message.setSender(u.getUserId());
try {//创建一个线程,是谁要拉取用户列表就创建一个他和服务器相连的线程
ClientConnectServerThread clientConnectServerThread = ManageClientConnectServerThread.getClientConnectServerThread(u.getUserId());
//该线程持有socket管道
Socket socket = clientConnectServerThread.getSocket();
//把消息写入管道
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
oos.writeObject(message);
} catch (IOException e) {
e.printStackTrace();
}
}
//退出登录
public void logout(){
//封装消息类型
Message message = new Message();
message.setMesType(MessageType.MESSAGE_CLIENT_EXIT);
message.setSender(u.getUserId());
try {
//是谁退出,谁就创建一个和服务器相连的输出管道
ObjectOutputStream oos = new ObjectOutputStream(ManageClientConnectServerThread.getClientConnectServerThread(u.getUserId()).getSocket().getOutputStream());
oos.writeObject(message);
System.out.println(u.getUserId() + " 退出系统 ");
//结束进程.(必须要用这个方法结束掉进程,否则一个线程结束了,另一哥线程还会继续跑,就会出异常)
System.exit(0);
} catch (IOException e) {
e.printStackTrace();
}
}
}
utils包
Utility (工具类,主要是键盘读写)
package com.serein.qqclient.utils;
/**
工具类的作用:
处理各种情况的用户输入,并且能够按照程序员的需求,得到用户的控制台输入。
*/
import java.util.*;
/**
*/
public class Utility {
//静态属性。。。
private static Scanner scanner = new Scanner(System.in);
/**
* 功能:读取键盘输入的一个菜单选项,值:1——5的范围
* @return 1——5
*/
public static char readMenuSelection() {
char c;
for (; ; ) {
String str = readKeyBoard(1, false);//包含一个字符的字符串
c = str.charAt(0);//将字符串转换成字符char类型
if (c != '1' && c != '2' &&
c != '3' && c != '4' && c != '5') {
System.out.print("选择错误,请重新输入:");
} else break;
}
return c;
}
/**
* 功能:读取键盘输入的一个字符
* @return 一个字符
*/
public static char readChar() {
String str = readKeyBoard(1, false);//就是一个字符
return str.charAt(0);
}
/**
* 功能:读取键盘输入的一个字符,如果直接按回车,则返回指定的默认值;否则返回输入的那个字符
* @param defaultValue 指定的默认值
* @return 默认值或输入的字符
*/
public static char readChar(char defaultValue) {
String str = readKeyBoard(1, true);//要么是空字符串,要么是一个字符
return (str.length() == 0) ? defaultValue : str.charAt(0);
}
/**
* 功能:读取键盘输入的整型,长度小于2位
* @return 整数
*/
public static int readInt() {
int n;
for (; ; ) {
String str = readKeyBoard(10, false);//一个整数,长度<=10位
try {
n = Integer.parseInt(str);//将字符串转换成整数
break;
} catch (NumberFormatException e) {
System.out.print("数字输入错误,请重新输入:");
}
}
return n;
}
/**
* 功能:读取键盘输入的 整数或默认值,如果直接回车,则返回默认值,否则返回输入的整数
* @param defaultValue 指定的默认值
* @return 整数或默认值
*/
public static int readInt(int defaultValue) {
int n;
for (; ; ) {
String str = readKeyBoard(10, true);
if (str.equals("")) {
return defaultValue;
}
//异常处理...
try {
n = Integer.parseInt(str);
break;
} catch (NumberFormatException e) {
System.out.print("数字输入错误,请重新输入:");
}
}
return n;
}
/**
* 功能:读取键盘输入的指定长度的字符串
* @param limit 限制的长度
* @return 指定长度的字符串
*/
public static String readString(int limit) {
return readKeyBoard(limit, false);
}
/**
* 功能:读取键盘输入的指定长度的字符串或默认值,如果直接回车,返回默认值,否则返回字符串
* @param limit 限制的长度
* @param defaultValue 指定的默认值
* @return 指定长度的字符串
*/
public static String readString(int limit, String defaultValue) {
String str = readKeyBoard(limit, true);
return str.equals("")? defaultValue : str;
}
/**
* 功能:读取键盘输入的确认选项,Y或N
* 将小的功能,封装到一个方法中.
* @return Y或N
*/
public static char readConfirmSelection() {
System.out.println("请输入你的选择(Y/N): 请小心选择");
char c;
for (; ; ) {//无限循环
//在这里,将接受到字符,转成了大写字母
//y => Y n=>N
String str = readKeyBoard(1, false).toUpperCase();
c = str.charAt(0);
if (c == 'Y' || c == 'N') {
break;
} else {
System.out.print("选择错误,请重新输入:");
}
}
return c;
}
/**
* 功能: 读取一个字符串
* @param limit 读取的长度
* @param blankReturn 如果为true ,表示 可以读空字符串。
* 如果为false表示 不能读空字符串。
*
* 如果输入为空,或者输入大于limit的长度,就会提示重新输入。
* @return
*/
private static String readKeyBoard(int limit, boolean blankReturn) {
//定义了字符串
String line = "";
//scanner.hasNextLine() 判断有没有下一行
while (scanner.hasNextLine()) {
line = scanner.nextLine();//读取这一行
//如果line.length=0, 即用户没有输入任何内容,直接回车
if (line.length() == 0) {
if (blankReturn) return line;//如果blankReturn=true,可以返回空串
else continue; //如果blankReturn=false,不接受空串,必须输入内容
}
//如果用户输入的内容大于了 limit,就提示重写输入
//如果用户如的内容 >0 <= limit ,我就接受
if (line.length() < 1 || line.length() > limit) {
System.out.print("输入长度(不能大于" + limit + ")错误,请重新输入:");
continue;
}
break;
}
return line;
}
}
qqcommom包
Message (定义消息为对象,并给属性)
package com.serein.qqcommom;
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 fileLen = 0;
private String dest;
private String src;
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 getMesType() {
return MesType;
}
public void setMesType(String mesType) {
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;
}
}
MessageType (消息类型接口)
package com.serein.qqcommom;
/**
* 这个接口定义了消息的类型,以便采取不同的方法处理不同的消息
*/
public interface MessageType {
String MESSAGE_LOGIN_SUCCEED = "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_TO_ALL_MES = "7";//群发
String MESSAGE_FILE_MES = "8";//发送文件
}
User (定义了用户的属性)
package com.serein.qqcommom;
import java.io.Serializable;
/**
* 该类定义了用户的属性
*/
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String userId;
private String passwd;
public User() {
}
public User(String userId, String passwd) {
this.userId = userId;
this.passwd = passwd;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getPasswd() {
return passwd;
}
public void setPasswd(String passwd) {
this.passwd = passwd;
}
}
服务端:
qqframe包
QQFrame (启动面板)
package com.serein.qqframe;
import com.serein.qqserver.server.QQServer;
/**
* 该类是服务端启动面板
*/
public class QQFrame {
public static void main(String[] args) {
new QQServer();
}
}
qqserver.server包
ManageClientThreads (管理通信线程)
package com.serein.qqserver.server;
import java.util.HashMap;
import java.util.Iterator;
/**
* 该类是管理多个线程用的
*/
public class ManageClientThreads {
private static HashMap<String, ServerClientClientThread> hm = new HashMap<>();
public static HashMap<String,ServerClientClientThread> getHm(){
return hm;
}
//把某用户的线程添加到服务器集合中
public static void addClientThread(String userId, ServerClientClientThread serverClientClientThread) {
hm.put(userId, serverClientClientThread);
}
public static ServerClientClientThread getServerClientClientThread(String userId) {
return hm.get(userId);
}
//移除某个用户与服务器建立的连接
public static ServerClientClientThread removeServerClientClientThread(String userId){
return hm.remove(userId);
}
//拉取在线用户列表的方法,就是遍历map集合
public static String getOnlineUser() {
Iterator<String> iterator = hm.keySet().iterator();
String onlineUserList = "";
while (iterator.hasNext()){
onlineUserList += iterator.next().toString() + "";
}
return onlineUserList;
}
}
QQServer (登录验证服务,打通数据通道)
package com.serein.qqserver.server;
import com.serein.qqcommom.Message;
import com.serein.qqcommom.MessageType;
import com.serein.qqcommom.User;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ConcurrentHashMap;
/**
* 该类的主要作用是,提供登录验证服务
*/
public class QQServer {
private ServerSocket ss = null;
//ConcurrentHashMap是线程安全的map集合
private static ConcurrentHashMap<String,User> validUsers = new ConcurrentHashMap<>();
//静态代码块,和类预加载用户登录名密码(模拟了数据库)
static {
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("紫霞仙子",new User("紫霞仙子","123456"));
validUsers.put("菩提老祖",new User("菩提老祖","123456"));
}
//判断用户的账户密码是否合法
private boolean checkUser(String userId,String passwd){
User user = validUsers.get(userId);
if (user == null){
return false;
}
if (!user.getPasswd().equals(passwd)){
return false;
}
return true;
}
//启动服务器,打通了服务端和客户端的数据通道
public QQServer() {
try {
System.out.println("服务端在 9999端口监听...");
//启动服务端推送新闻的线程
new Thread(new SendNewsToAllService()).start();
ss = new ServerSocket(9999);
//用while循环,一直保持读取客户端连接的状态
while (true) {
//等待客户端连接
Socket socket = ss.accept();
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
User u = (User) ois.readObject();
Message message = new Message();
//判断当前用户名密码是否合法
if (checkUser(u.getUserId(),u.getPasswd())) {
// if (true) {
message.setMesType(MessageType.MESSAGE_LOGIN_SUCCEED);
oos.writeObject(message);
//创建一个客户端与服务端连接的线程(通道)
ServerClientClientThread serverClientClientThread = new ServerClientClientThread(socket, u.getUserId());
serverClientClientThread.start();
//把这个线程添加到集合中,以便后续统一管理.
ManageClientThreads.addClientThread(u.getUserId(), serverClientClientThread);
} else {
//登录失败
System.out.println("用户 id=" + u.getUserId() + " pwd=" + u.getPasswd() + " 验证失败");
message.setMesType(MessageType.MESSAGE_LOGIN_FAIL);
oos.writeObject(message);
socket.close();
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
//关闭管道
ss.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
SendNewsToAllService (推送新闻)
package com.serein.qqserver.server;
import com.serein.qqcommom.Message;
import com.serein.qqcommom.MessageType;
import com.serein.utils.Utility;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
/**
* 该类的主要作用是,服务端向所有用户推送新闻.
* 做成线程,以便独立启动与关闭
*/
public class SendNewsToAllService implements Runnable{
@Override
public void run() {
while (true) {
System.out.println("请输入服务器要推送的新闻/消息[输入exit表示退出推送服务线程]");
String news = Utility.readString(100);
//输入exit则退出该线程,关闭推送
if ("exit".equals(news)){
break;
}
//设置自己的消息类型
Message message = new Message();
message.setSender("服务器");
message.setMesType(MessageType.MESSAGE_TO_ALL_MES);
message.setContent(news);
message.setSendTime(new Date().toString());
System.out.println("服务器推送消息给所有人 说:" + news);
HashMap<String,ServerClientClientThread> hm = ManageClientThreads.getHm();
//用迭代器遍历map集合,得到所有的用户id
Iterator<String> iterator = hm.keySet().iterator();
while (iterator.hasNext()){
String onLineUserId = iterator.next().toString();
try {
//得到输出流(对象流)
ObjectOutputStream oos = new ObjectOutputStream(hm.get(onLineUserId).getSocket().getOutputStream());
//写数据
oos.writeObject(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
ServerClientClientThread (主要的业务逻辑处理)
package com.serein.qqserver.server;
import com.serein.qqcommom.Message;
import com.serein.qqcommom.MessageType;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;
import java.util.HashMap;
import java.util.Iterator;
/**
* 这个类是服务端,和客户端的通信管道,是主要的业务逻辑处理
*/
public class ServerClientClientThread extends Thread{
private Socket socket;
private String userId;
public ServerClientClientThread(Socket socket, String userId) {
this.socket = socket;
this.userId = userId;
}
public Socket getSocket() {
return socket;
}
@Override
public void run() {
while (true){
try {
System.out.println("服务端和客户端"+ userId + "保持通讯,读取数据...");
//创建读取通道,读取该消息,看客户端发来的是哪类信息
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
Message message = (Message) ois.readObject();
//客户端请求拉取在线用户列表
if (message.getMesType().equals(MessageType.MESSAGE_GET_ONLINE_FRIEND)){
System.out.println(message.getSender() + " 要在线用户列表");
//从管理线程的集合中,拿到所有线程的名字(因为一个线程代表一个客户端)(getOnlineUser方法
//就是遍历map集合的key)
String onlineUser = ManageClientThreads.getOnlineUser();
//把拿到的结果封装成消息
Message message2 = new Message();
message2.setMesType(MessageType.MESSAGE_RET_ONLINE_FRIEND);
message2.setContent(onlineUser);
message2.setGetter(message.getSender());
//把该消息即用户列表,返回提出申请的给客户端.
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
oos.writeObject(message2);
} else if (message.getMesType().equals(MessageType.MESSAGE_COMM_MES)) {
//私聊消息,获取发消息的客户端,做一个和目标用户的线程
ServerClientClientThread serverClientClientThread = ManageClientThreads.getServerClientClientThread(message.getGetter());
//把该消息转发给目标用户
ObjectOutputStream oos = new ObjectOutputStream(serverClientClientThread.getSocket().getOutputStream());
oos.writeObject(message);
} else if (message.getMesType().equals(MessageType.MESSAGE_TO_ALL_MES)) {
//群聊消息,先得到所有线程的集合
HashMap<String,ServerClientClientThread> hm = ManageClientThreads.getHm();
//用迭代器遍历集合,取出所有用户id
Iterator<String> iterator = hm.keySet().iterator();
while (iterator.hasNext()){
String onLineUserId = iterator.next().toString();
//给除自己外的所有用户转发该消息
if (!onLineUserId.equals(message.getSender())){
ObjectOutputStream oos = new ObjectOutputStream(hm.get(onLineUserId).getSocket().getOutputStream());
oos.writeObject(message);
}
}
} else if (message.getMesType().equals(MessageType.MESSAGE_FILE_MES)) {
//发送文件,创建一个与目标用户的写入通道,并写入
ObjectOutputStream oos = new ObjectOutputStream(ManageClientThreads.getServerClientClientThread(message.getGetter()).getSocket().getOutputStream());
oos.writeObject(message);
} else if (message.getMesType().equals(MessageType.MESSAGE_CLIENT_EXIT)) {
//该用户打算退出系统
System.out.println(message.getSender() + " 退出");
//从管理线程的集合中移除该用户和服务器建立的连接(即相关线程)
ManageClientThreads.removeServerClientClientThread(message.getSender());
socket.close();
break;
}else {
//其他业务请求
System.out.println("其他类型的message,暂不处理...");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
qqcommom包
Message (定义消息为对象,并给属性)
package com.serein.qqcommom;
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 fileLen = 0;
private String dest;
private String src;
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 getMesType() {
return MesType;
}
public void setMesType(String mesType) {
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;
}
}
MessageType (消息类型接口)
package com.serein.qqcommom;
/**
* 这个接口定义了消息的类型,以便采取不同的方法处理不同的消息
*/
public interface MessageType {
String MESSAGE_LOGIN_SUCCEED = "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_TO_ALL_MES = "7";//群发
String MESSAGE_FILE_MES = "8";//发送文件
}
User (定义了用户的属性)
package com.serein.qqcommom;
import java.io.Serializable;
/**
* 该类定义了用户的属性
*/
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String userId;
private String passwd;
public User() {
}
public User(String userId, String passwd) {
this.userId = userId;
this.passwd = passwd;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getPasswd() {
return passwd;
}
public void setPasswd(String passwd) {
this.passwd = passwd;
}
}
Utility (工具类,主要是键盘读写)
package com.serein.utils;
/**
工具类的作用:
处理各种情况的用户输入,并且能够按照程序员的需求,得到用户的控制台输入。
*/
import java.util.Scanner;
/**
*/
public class Utility {
//静态属性。。。
private static Scanner scanner = new Scanner(System.in);
/**
* 功能:读取键盘输入的一个菜单选项,值:1——5的范围
* @return 1——5
*/
public static char readMenuSelection() {
char c;
for (; ; ) {
String str = readKeyBoard(1, false);//包含一个字符的字符串
c = str.charAt(0);//将字符串转换成字符char类型
if (c != '1' && c != '2' &&
c != '3' && c != '4' && c != '5') {
System.out.print("选择错误,请重新输入:");
} else break;
}
return c;
}
/**
* 功能:读取键盘输入的一个字符
* @return 一个字符
*/
public static char readChar() {
String str = readKeyBoard(1, false);//就是一个字符
return str.charAt(0);
}
/**
* 功能:读取键盘输入的一个字符,如果直接按回车,则返回指定的默认值;否则返回输入的那个字符
* @param defaultValue 指定的默认值
* @return 默认值或输入的字符
*/
public static char readChar(char defaultValue) {
String str = readKeyBoard(1, true);//要么是空字符串,要么是一个字符
return (str.length() == 0) ? defaultValue : str.charAt(0);
}
/**
* 功能:读取键盘输入的整型,长度小于2位
* @return 整数
*/
public static int readInt() {
int n;
for (; ; ) {
String str = readKeyBoard(10, false);//一个整数,长度<=10位
try {
n = Integer.parseInt(str);//将字符串转换成整数
break;
} catch (NumberFormatException e) {
System.out.print("数字输入错误,请重新输入:");
}
}
return n;
}
/**
* 功能:读取键盘输入的 整数或默认值,如果直接回车,则返回默认值,否则返回输入的整数
* @param defaultValue 指定的默认值
* @return 整数或默认值
*/
public static int readInt(int defaultValue) {
int n;
for (; ; ) {
String str = readKeyBoard(10, true);
if (str.equals("")) {
return defaultValue;
}
//异常处理...
try {
n = Integer.parseInt(str);
break;
} catch (NumberFormatException e) {
System.out.print("数字输入错误,请重新输入:");
}
}
return n;
}
/**
* 功能:读取键盘输入的指定长度的字符串
* @param limit 限制的长度
* @return 指定长度的字符串
*/
public static String readString(int limit) {
return readKeyBoard(limit, false);
}
/**
* 功能:读取键盘输入的指定长度的字符串或默认值,如果直接回车,返回默认值,否则返回字符串
* @param limit 限制的长度
* @param defaultValue 指定的默认值
* @return 指定长度的字符串
*/
public static String readString(int limit, String defaultValue) {
String str = readKeyBoard(limit, true);
return str.equals("")? defaultValue : str;
}
/**
* 功能:读取键盘输入的确认选项,Y或N
* 将小的功能,封装到一个方法中.
* @return Y或N
*/
public static char readConfirmSelection() {
System.out.println("请输入你的选择(Y/N): 请小心选择");
char c;
for (; ; ) {//无限循环
//在这里,将接受到字符,转成了大写字母
//y => Y n=>N
String str = readKeyBoard(1, false).toUpperCase();
c = str.charAt(0);
if (c == 'Y' || c == 'N') {
break;
} else {
System.out.print("选择错误,请重新输入:");
}
}
return c;
}
/**
* 功能: 读取一个字符串
* @param limit 读取的长度
* @param blankReturn 如果为true ,表示 可以读空字符串。
* 如果为false表示 不能读空字符串。
*
* 如果输入为空,或者输入大于limit的长度,就会提示重新输入。
* @return
*/
private static String readKeyBoard(int limit, boolean blankReturn) {
//定义了字符串
String line = "";
//scanner.hasNextLine() 判断有没有下一行
while (scanner.hasNextLine()) {
line = scanner.nextLine();//读取这一行
//如果line.length=0, 即用户没有输入任何内容,直接回车
if (line.length() == 0) {
if (blankReturn) return line;//如果blankReturn=true,可以返回空串
else continue; //如果blankReturn=false,不接受空串,必须输入内容
}
//如果用户输入的内容大于了 limit,就提示重写输入
//如果用户如的内容 >0 <= limit ,我就接受
if (line.length() < 1 || line.length() > limit) {
System.out.print("输入长度(不能大于" + limit + ")错误,请重新输入:");
continue;
}
break;
}
return line;
}
}