服务端构建:
基本类及接口:User用户,Message信息,MessageType信息类型
User类:包括用户id和密码,用于登录信息验证。
public class User implements Serializable {
private String UserId;
private String UserPassword;
}
Message类:记录信息内容,信息属性等,作为数据传输载体。
public class Message implements Serializable{
private String sender;//发送者
private String getter;//接收者
private String content;//发送内容
private String sendtime;//发生时间
private String mesTpye;//消息类型 (可以在接口中定义消息类型)
private byte[] filebytes;
private int filebyteslenth;
private String getterfilePath;
private String senderfilePath;
}
MessageType接口:信息类型表,通过与Massage.mesType比较,使服务端知道Message对应操作,进而调用对应方法。
public interface MessageType {
/*
1.在接口中定义了一些常量
2.不同常量的值表示不同消息的类型
*/
String MESSAGE_LINK_SUCCEED="1";//表示登陆成功
String MESSAGE_LINK_FAIL="2";//表示登录失败
String MESSAGE_COMM_MES="3";//普通信息包
String MESSAGE_GET_ONLINEUSER="4";//客户端要求返回在线用户列表
String MESSAGE_RETRUN_ONLINEUSER="5";//返回在线用户列表
String MESSAGE_CLIENT_EXIT="6";//客户端请求退出
String MESSAGE_SENDTOALL="7";//群发
String MESSAGE_SEND_FILE="8";
}
服务端主体部分:
创建服务端主体 qqserver()并运行
public class MainRUn { public static void main(String[] args) throws IOException, ClassNotFoundException {
new qqserver();
}
}
创建HashMap用于存储初始化用户信息:
后期可外接数据库用于存储用户信息和离线信息。
//创建一个集合存放多个用户
private static ConcurrentHashMap<String,User> validUsers=new ConcurrentHashMap<>();
//HashMap没有处理线程安全,因此在多线程情况下不安全
//ConcurrentHashMap处理的线程安全,即线程同步处理,在多线程情况下是安全的
static {
validUsers.put("100",new User("100","123456"));
validUsers.put("200",new User("200","123456"));
validUsers.put("300",new User("300","123456"));
}
checkUser方法用于验证用户登录信息:
//验证用户是否有效的方法
public boolean checkUser(String UserId,String UserPassword){
User user=validUsers.get(UserId);
if(user==null){//说明UserId不存在于用户集合中
return false;
}
if (!user.getUserPassword().equals(UserPassword)){//用户名正确,密码错误
return false;
}
return true;
}
qqserver主体:
new ServerSocket()并监听9999端口。
循环监听端口获取对象输入流,获取登录用户信息,搭建输出流通道。
ss=new ServerSocket(9999);
while (true) {//当和某个客户端连接后继续监听
Socket socket = ss.accept();
//得到监听对象输入流
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
User u = (User)ois.readObject();
Message message = new Message();
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
判断登录用户信息是否与HashMap存储信息匹配。
若匹配,则登陆成功,建立线程并开启同时继续监听,同时将线程加入线程库统一管理。若失败,则输出Message失败类型,socket通道关闭。
if(checkUser(u.getUserId(), u.getUserPassword())){//登陆通过
message.setMesTpye(MessageType.MESSAGE_LINK_SUCCEED);
//将message回复给客户端
oos.writeObject(message);
//创建一个线程,与客户端保持通讯,该线程持有socket对象
ServerConnectClientThread scct = new ServerConnectClientThread(socket,u.getUserId());
scct.start();
//把该线程对象放入到一个集合中进行管理
ManageServerThread.addServerConnectClientThread(u.getUserId(),scct);
}else {//登陆失败
System.out.println("用户"+u.getUserId()+"登陆失败");
message.setMesTpye(MessageType.MESSAGE_LINK_FAIL);
oos.writeObject(message);
socket.close();
}
线程库ManageServerThread类:用HashMap记录线程信息与对应用户id,并包括返回在线用户方法(即遍历HashMap)。
public class ManageServerThread {
private static HashMap<String,ServerConnectClientThread> hm=new HashMap<>();
public static HashMap<String, ServerConnectClientThread> getHm() {
return hm;
}
public static void addServerConnectClientThread(String UserId, ServerConnectClientThread serverConnectClientThread){
hm.put(UserId,serverConnectClientThread);
}
public static ServerConnectClientThread getserverConnectClientThread(String UserId){
return hm.get(UserId);
}
public static void closeServerConnectClientThread(String UserId){
hm.remove(UserId);//移除此线程
}
//编写方法,返回在线用户列表
public static StringBuffer getonlineUser(){
//遍历集合,遍历hashmap的key
Iterator<String> iterator = hm.keySet().iterator();
StringBuffer onlineUsersList=new StringBuffer();
while (iterator.hasNext()){
onlineUsersList.append(iterator.next().toString()+" ");
}
return onlineUsersList;
}
}
总体详见如下:
public class qqserver {
private ServerSocket ss=null;
//创建一个集合存放多个用户
private static ConcurrentHashMap<String,User> validUsers=new ConcurrentHashMap<>();
//HashMap没有处理线程安全,因此在多线程情况下不安全
//ConcurrentHashMap处理的线程安全,即线程同步处理,在多线程情况下是安全的
static {
validUsers.put("100",new User("100","123456"));
validUsers.put("200",new User("200","123456"));
validUsers.put("300",new User("300","123456"));
}
//验证用户是否有效的方法
public boolean checkUser(String UserId,String UserPassword){
User user=validUsers.get(UserId);
if(user==null){//说明UserId不存在于用户集合中
return false;
}
if (!user.getUserPassword().equals(UserPassword)){//用户名正确,密码错误
return false;
}
return true;
}
public qqserver() throws IOException, ClassNotFoundException {
try {
ss=new ServerSocket(9999);
while (true) {//当和某个客户端连接后继续监听
Socket socket = ss.accept();
//得到监听对象输入流
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
User u = (User)ois.readObject();
Message message = new Message();
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
if(checkUser(u.getUserId(), u.getUserPassword())){//登陆通过
message.setMesTpye(MessageType.MESSAGE_LINK_SUCCEED);
//将message回复给客户端
oos.writeObject(message);
//创建一个线程,与客户端保持通讯,该线程持有socket对象
ServerConnectClientThread scct = new ServerConnectClientThread(socket,u.getUserId());
scct.start();
//把该线程对象放入到一个集合中进行管理
ManageServerThread.addServerConnectClientThread(u.getUserId(),scct);
}else {//登陆失败
System.out.println("用户"+u.getUserId()+"登陆失败");
message.setMesTpye(MessageType.MESSAGE_LINK_FAIL);
oos.writeObject(message);
socket.close();
}
}
} catch (IOException e) {
throw new RuntimeException(e);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
} finally {
//如果服务端跳出while循环,说明服务器不在监听,因此关闭ServerSocket
ss.close();
}
}
}
用户登陆成功后,线程开启:
ServerConnectClientThread类负责线程监听。
循环监听端口并获取输入流,判断输入流信息类型,执行不同命令。
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
Message message = (Message) ois.readObject();
1.返回客户端在线列表操作:
构建一个Message用于存储信息,调用线程库中的getonlineUser方法获取在线用户列表,将Message放入输出流发送给客户端。
if(message.getMesTpye().equals(MessageType.MESSAGE_GET_ONLINEUSER)){
//客户端要在线用户列表
StringBuffer sb = ManageServerThread.getonlineUser();
//构建一个Message对象,用于返回在线用户列表
Message message1 = new Message();
message1.setMesTpye(MessageType.MESSAGE_RETRUN_ONLINEUSER);
message1.setContent(sb.toString());
//
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
oos.writeObject(message1);
2.用户退出系统操作:
关闭socket通道,并删除线程库中对应线程,跳出循环监听。在服务端控制台打印退出用户id。
else if(message.getMesTpye().equals(MessageType.MESSAGE_CLIENT_EXIT)){
System.out.println(message.getSender()+"要退出系统了");
ManageServerThread.closeServerConnectClientThread(message.getSender());
socket.close();
break;
3.用户发起私聊操作
调用线程库,获取信息接收者(getter)的线程,然后调用getsocket方法获取目标用户socket通道,将Message输出给对应用户。
else if(message.getMesTpye().equals(MessageType.MESSAGE_COMM_MES)){
ServerConnectClientThread serverConnectClientThread =
ManageServerThread.getserverConnectClientThread(message.getGetter());
ObjectOutputStream oos = new ObjectOutputStream(serverConnectClientThread.getSocket().getOutputStream());
oos.writeObject(message);//转发,可保存到数据库做离线信息
4.用户发起群聊操作
调用线程库,获取当前所有在线用户(即遍历线程库HashMap),然后调用getsocket方法获取所有在线用户socket通道,将Message输出给所有在线用户。
else if(message.getMesTpye().equals(MessageType.MESSAGE_SENDTOALL)){
HashMap<String, ServerConnectClientThread> hm = ManageServerThread.getHm();
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);
}
}
ServerConnectClientThread类监听线程详细代码如下:
public class ServerConnectClientThread extends Thread{
private Socket socket;
private String UeserId;//连接到服务端的用户id
public ServerConnectClientThread(Socket socket, String ueserId) {
this.socket = socket;
UeserId = ueserId;
}
@Override
public void run() {
while (true){
try {
System.out.println("服务端与客户端"+UeserId+"保持通讯");
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
Message message = (Message) ois.readObject();
//判断Message类型,做出相应处理
if(message.getMesTpye().equals(MessageType.MESSAGE_GET_ONLINEUSER)){
//客户端要在线用户列表
StringBuffer sb = ManageServerThread.getonlineUser();
//构建一个Message对象,用于返回在线用户列表
Message message1 = new Message();
message1.setMesTpye(MessageType.MESSAGE_RETRUN_ONLINEUSER);
message1.setContent(sb.toString());
//
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
oos.writeObject(message1);
}else if(message.getMesTpye().equals(MessageType.MESSAGE_CLIENT_EXIT)){
System.out.println(message.getSender()+"要退出系统了");
ManageServerThread.closeServerConnectClientThread(message.getSender());
socket.close();
break;
}else if(message.getMesTpye().equals(MessageType.MESSAGE_COMM_MES)){
ServerConnectClientThread serverConnectClientThread =
ManageServerThread.getserverConnectClientThread(message.getGetter());
ObjectOutputStream oos = new ObjectOutputStream(serverConnectClientThread.getSocket().getOutputStream());
oos.writeObject(message);//转发,可保存到数据库做离线信息
}else if(message.getMesTpye().equals(MessageType.MESSAGE_SENDTOALL)){
HashMap<String, ServerConnectClientThread> hm = ManageServerThread.getHm();
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.getMesTpye().equals(MessageType.MESSAGE_SEND_FILE)){
ServerConnectClientThread serverConnectClientThread =
ManageServerThread.getserverConnectClientThread(message.getGetter());
ObjectOutputStream oos = new ObjectOutputStream(serverConnectClientThread.getSocket().getOutputStream());
oos.writeObject(message);
}
} catch (Exception e) {
throw new RuntimeException(e);}
}
}
public Socket getSocket() {
return socket;
}
public class ToByteArrayTool {
public static byte[] streamToByteArray(InputStream is) throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] b=new byte[1024];
int len;
while ((len=is.read(b))!=-1){
bos.write(b,0,len);
}
byte[] array=bos.toByteArray();
bos.close();
return array;
}
}
}
客户端Client搭建:
基本类和接口与服务端一致

客户端主体部分 :
创建QQView类并调用mainMenu方法运行菜单。
public class MainRun { public static void main(String[] args) throws IOException, InterruptedException {
QQView qqView = new QQView();
qqView.mainMenu();
}
}
QQView主体:
基本属性:将部分工具类写为属性,便于直接调用工具类中方法。
private boolean loop=true;//实现不停循环
private String key;//接收用户输入
private UserClientService userClientService=new UserClientService();//用于登陆服务器/注册
private SendMessageClientService sendMessageClientService=new SendMessageClientService();//用于用户发送信息
private SentFileClientService sentFileClientService=new SentFileClientService();
运行客户端显示菜单,根据提示进行下一步操作。
===========欢迎登陆网络通讯系统=============
1 登陆
9 退出
请输入你的选择:
1
登陆系统
请输入用户名:
100
请输入密码:
123456
===========欢迎登陆(用户100)=============
其中,选择登录操作后,调用checkUser方法验证用户登录信息是否正确。
while (loop){
System.out.println("===========欢迎登陆网络通讯系统=============");
System.out.println("\t\t 1 登陆");
System.out.println("\t\t 9 退出");
System.out.println("请输入你的选择:");
key=KeyboardTool.readscanner();
switch (key){
case "1":
System.out.println("登陆系统");
System.out.println("请输入用户名:");
String UserId=KeyboardTool.readscanner();
System.out.println("请输入密码:");
String UserPassword=KeyboardTool.readscanner();
//编写一个类 UserClientService(用于用户连接)
if(userClientService.checkUser(UserId,UserPassword)){
System.out.println("===========欢迎登陆(用户"+UserId+")=============");
checkUser方法:
向服务端发送登录信息,由服务端根据存储用户信息的HashMap验证登录用户的信息是否正确,接收服务端返回信息。若登陆时成功,则创建线程持续监听并放入线程库管理。若登陆失败则返回登陆失败。
public boolean checkUser(String UserId,String UserPassword){
//创建user对象
u.setUserId(UserId);
u.setUserPassword(UserPassword);
//连接服务端,发送u对象
try {
socket = new Socket("127.0.0.1",9999);
//得到ObjectOutputStream对象
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
oos.writeObject(u);//发送user对象
//服务器验证是否登陆成功
//读取从服务端回复的message对象
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
Message o = (Message) ois.readObject();
if(o.getMesTpye().equals(MessageType.MESSAGE_LINK_SUCCEED)){
//登陆成功
//创建一个和服务器端保持通讯的线程——>创建一个线程类ClientConnectServerThread
ClientConnectServerThread ccst = new ClientConnectServerThread(socket);
ccst.start();//启动线程
//为了后面客户端的扩展,我们将线程放入集合中处理
ManageConnectThread.addClientConnectServerThread(u.getUserId(), ccst);//静态方法直接调用
b=true;
}else {
//登陆失败,不能启动和服务器的通讯线程,关闭socket
socket.close();
b=false;
}
} catch (Exception e) {
throw new RuntimeException(e);
}
return b;
}
用户登陆成功,创建线程持续监听。
public class ClientConnectServerThread extends Thread{
private Socket socket;
@Override
public void run() {
//线程需要在后台保持与服务端通讯,因此我们while循环
while (true){
try {
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
Message message = (Message) ois.readObject();
//判断Message类型,做出相应处理
if(message.getMesTpye().equals(MessageType.MESSAGE_RETRUN_ONLINEUSER)){//读取到返回在线用户列表类型
//取出在线列表信息
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.getMesTpye().equals(MessageType.MESSAGE_COMM_MES)){
System.out.println(message.getSendtime()+"\t\t"+message.getSender()+"对"+message.getGetter()+"说"+message.getContent());
}else if(message.getMesTpye().equals(MessageType.MESSAGE_SENDTOALL)){
System.out.println(message.getSendtime()+"\t\t"+message.getSender()+"在群里说"+message.getContent());
}else if(message.getMesTpye().equals(MessageType.MESSAGE_SEND_FILE)){
System.out.println(message.getSender()+"给"+message.getGetter()+"发送了文件,到"+message.getGetterfilePath());
BufferedInputStream bis=new BufferedInputStream(socket.getInputStream());
//将socket通道中数据转化为字节数组
byte[] bytes1 = ToByteArrayTool.streamToByteArray(bis);
bis.close();
//将bytes数组写入指定路径
String filepath=message.getGetterfilePath();
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(filepath));
bos.write(bytes1);
System.out.println("保存文件成功");
bos.close();
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
用户登录成功后进入二级菜单,根据菜单选择对应操作。
System.out.println("===========欢迎登陆(用户"+UserId+")=============");
while (loop){
System.out.println("\n===========网络通讯系统二级菜单(用户"+UserId+")=============");
System.out.println("\t\t 1 在线用户列表");
System.out.println("\t\t 2 群聊");
System.out.println("\t\t 3 私聊");
System.out.println("\t\t 4 发送文件");
System.out.println("\t\t 9 退出系统");
System.out.println("请输入你的选择");
key=KeyboardTool.readscanner();
1.返回在线用户列表功能:
case "1":userClientService.getOnlineUser();
向服务端发送massage,并设置MessageType为返回在线用户列表。此时,服务端返回信息将被监听线程接收并打印在控制台上。
public void getOnlineUser() throws IOException {
//发送给服务端,要求返回在线用户列表
Message message = new Message(MessageType.MESSAGE_GET_ONLINEUSER);
//获取当前 线程 对应的socket的ObjectOutputString流,并将message发送给服务器
ClientConnectServerThread clientConnectServerThread = ManageConnectThread.getClientConnectServerThread(u.getUserId());
Socket s = clientConnectServerThread.getSocket();
ObjectOutputStream oos = new ObjectOutputStream(s.getOutputStream());
oos.writeObject(message);
}
2.用户发起群聊功能:
case "2":
System.out.println("请输入群发内容:");
String content1=KeyboardTool.readscanner();
sendMessageClientService.sendMessageToAll(content1,UserId);
break;
调用sendMessageClientServer类中sendMessageToAll方法,将信息发送至服务端。此时,服务端返回信息将被监听线程接收并打印在控制台上。
public class SendMessageClientService {
public void sendMessageToOne(String content,String senderId,String getterId) throws IOException {
//构建message对象
Message message = new Message();
message.setMesTpye(MessageType.MESSAGE_COMM_MES);
message.setContent(content);
message.setSender(senderId);
message.setGetter(getterId);
message.setSendtime(new java.util.Date().toString());//发送时间
System.out.println(message.getSendtime()+"\t\t"+message.getSender()+"对"+message.getGetter()+"说"+message.getContent());
ObjectOutputStream oos = new ObjectOutputStream(ManageConnectThread.getClientConnectServerThread(senderId).getSocket().getOutputStream());
oos.writeObject(message);
}
public void sendMessageToAll(String content,String senderId) throws IOException {
Message message = new Message();
message.setContent(content);
message.setSender(senderId);
message.setSendtime(new java.util.Date().toString());
message.setMesTpye(MessageType.MESSAGE_SENDTOALL);
System.out.println(message.getSendtime()+"\t\t"+message.getSender()+"在群里说"+message.getContent());
ObjectOutputStream oos = new ObjectOutputStream(ManageConnectThread.getClientConnectServerThread(senderId).getSocket().getOutputStream());
oos.writeObject(message);
}
}
3.用户发起私聊功能
调用sendMessageClientServer类中sendMessageToAll方法,将信息发送至服务端。此时,服务端返回信息将被监听线程接收并打印在控制台上。
4.用户退出系统功能
线程退出,将退出信息发送给服务端,并跳出菜单循环。此时,服务端会将此线程关闭,并关闭socket通道,将线程移除线程库。
case "9":
//编写退出方法
userClientService.eixt();
loop=false;
break;
public void eixt() throws IOException {
Message message = new Message(MessageType.MESSAGE_CLIENT_EXIT);
message.setSender(u.getUserId());//指定断开连接客户端
ClientConnectServerThread clientConnectServerThread = ManageConnectThread.getClientConnectServerThread(u.getUserId());
Socket s = clientConnectServerThread.getSocket();
ObjectOutputStream oos = new ObjectOutputStream(s.getOutputStream());
oos.writeObject(message);
System.out.println(u.getUserId()+"退出系统");
System.exit(0);
}
全部详情代码:
package qqclient.view;
import qqclient.service.SendMessageClientService;
import qqclient.service.SentFileClientService;
import qqclient.service.UserClientService;
import java.io.IOException;
import java.util.Scanner;
public class QQView {
private boolean loop=true;//实现不停循环
private String key;//接收用户输入
private UserClientService userClientService=new UserClientService();//用于登陆服务器/注册
private SendMessageClientService sendMessageClientService=new SendMessageClientService();//用于用户发送信息
private SentFileClientService sentFileClientService=new SentFileClientService();
public void mainMenu() throws IOException, InterruptedException {
while (loop){
System.out.println("===========欢迎登陆网络通讯系统=============");
System.out.println("\t\t 1 登陆");
System.out.println("\t\t 9 退出");
System.out.println("请输入你的选择:");
key=KeyboardTool.readscanner();
switch (key){
case "1":
System.out.println("登陆系统");
System.out.println("请输入用户名:");
String UserId=KeyboardTool.readscanner();
System.out.println("请输入密码:");
String UserPassword=KeyboardTool.readscanner();
//编写一个类 UserClientService(用于用户连接)
if(userClientService.checkUser(UserId,UserPassword)){
System.out.println("===========欢迎登陆(用户"+UserId+")=============");
while (loop){
System.out.println("\n===========网络通讯系统二级菜单(用户"+UserId+")=============");
System.out.println("\t\t 1 在线用户列表");
System.out.println("\t\t 2 群聊");
System.out.println("\t\t 3 私聊");
System.out.println("\t\t 4 发送文件");
System.out.println("\t\t 9 退出系统");
System.out.println("请输入你的选择");
key=KeyboardTool.readscanner();
switch (key){
case "1":userClientService.getOnlineUser();
break;
case "2":
System.out.println("请输入群发内容:");
String content1=KeyboardTool.readscanner();
sendMessageClientService.sendMessageToAll(content1,UserId);
break;
case "3":
System.out.print("请输入私聊用户名称:");
String getterId=KeyboardTool.readscanner();
System.out.println("请输入内容:");
String content=KeyboardTool.readscanner();
//编写方法将私聊信息发送给服务端
sendMessageClientService.sendMessageToOne(content,UserId,getterId);
break;
case "4":
System.out.println("请输入要发的文件地址");
String fileSenderPath=KeyboardTool.readscanner();
System.out.println("请输入文件接收用户");
String geterId=KeyboardTool.readscanner();;
System.out.println("请输入目标用户接收路径");
String fileGetterPath=KeyboardTool.readscanner();
sentFileClientService.sendFileToOne(fileSenderPath,fileGetterPath,UserId,geterId);
break;
case "9":
//编写退出方法
userClientService.eixt();
loop=false;
break;
default:
System.out.println("输入错误");break;
}
}
}else System.out.println("登陆失败");
break;
case "9":loop=false;break;
default:
System.out.println("输入错误");break;
}
}
}
}
该博客介绍了使用Java开发QQ服务端与客户端的过程。服务端构建包含基本类及接口,主体部分有qqserver、线程库等,可处理用户登录、返回在线列表、私聊、群聊和退出等操作。客户端搭建基本类与服务端一致,主体部分有QQView,能实现获取在线列表、发起群聊和私聊、退出系统等功能。
&spm=1001.2101.3001.5002&articleId=133749970&d=1&t=3&u=3e1c900ebbd34639b4b09af78201b196)
1291

被折叠的 条评论
为什么被折叠?



