多线程网络实现在线聊天系统(详细源码)

这篇博客整理自韩顺平老师的多线程网络学习,在Java基础中最难的就是多线程以及网络编程了,如果不太熟悉的小伙伴可以跟着课程学习,韩老师讲得很详细,缺点就是太详细有点墨迹。实现后的效果是在一个类似命令行窗口进行聊天,网页版的聊天项目后续我也会更新,不过使用的技术以websocket为主。

TCP网络通信

基本介绍

  1. 基于客户端-服务端的网络通信

  2. 底层使用的是TCP/IP协议

  3. 应用场景举例:客户端发送数据,服务端接受并显示控制台

  4. 基于Socket的TCP编程
    在这里插入图片描述
    在这里插入图片描述

客户端通过Socket(InetAddress address,int port)连接服务端,连接上后生成Socket,通过Socket.getOutputStream()将数据写到数据通道进行数据发送。

服务端通过Socket accept()监听客户端的连接,当没有客户端连接启动端口时,程序会阻塞,等待连接。监听到连接会通过Socket.getInputStream()进行数据通道的数据读取。

客户端和服务端都是通过Socket.getOutputStream()进行数据发送,通过Socket.getInputStream()进行数据读取。

当客户端连接到服务端后,实际上客户端也是通过一个端口和服务端进行通信,这个端口是TCP/IP来分配的,是不确定的,随机的。

练习1

服务端监听本机9999端口,客户端向本机的9999端口发送数据后结束,服务端接受到数据打印然后结束。

【通过字节流的方式】

  • 服务端(注意要先启动服务端,再启动客户端。)
public class SocketTCP01Server {public static void main(String[] args) throws IOException {//1.在本机的9999端口监听,等待连接ServerSocket serverSocket = new ServerSocket(9999);System.out.println("服务端,在9999端口监听,等待连接...");//2.当没有客户端连接9999端口时,程序会阻塞,等待连接//如果有客户端连接,则会返回Socket对象,程序继续Socket socket = serverSocket.accept();System.out.println("服务端 socket="+socket.getClass());//3.通过socket.getInputStream()读取客户端写入到数据通道的数据InputStream inputstream = socket.getInputStream();byte[] buffer = new byte[1024];int readLen = 0;while((readLen = inputstream.read(buffer))!=-1) {System.out.println(new String(buffer,0,readLen));}inputstream.close();socket.close();serverSocket.close();}}
  • 客户端
public class SocketTCP01Client {public static void main(String[] args) throws IOException {//1连接本机的9999端口,连接成功,返回Socket对象Socket socket = new Socket(InetAddress.getLocalHost(),9999);System.out.println("客户端socket返回="+socket.getClass());//2.连接上后,通过输出流将数据写到数据通道OutputStream outputStream = socket.getOutputStream();outputStream.write("hello,server".getBytes());outputStream.close();socket.close();}
}

练习2

服务端监听本机9999端口,客户端向本机的9999端口发送数据,服务端接受到数据向客户端发送相应数据然后结束,客户端接受到后打印然后也结束。【通过字节流的方式】

  • 服务端
public class SocketTCP01Server {public static void main(String[] args) throws IOException {//1.在本机的9999端口监听,等待连接ServerSocket serverSocket = new ServerSocket(9999);System.out.println("服务端,在9999端口监听,等待连接...");//2.当没有客户端连接9999端口时,程序会阻塞,等待连接//如果有客户端连接,则会返回Socket对象,程序继续Socket socket = serverSocket.accept();System.out.println("服务端 socket="+socket.getClass());//3.通过socket.getInputStream()读取客户端写入到数据通道的数据InputStream inputstream = socket.getInputStream();byte[] buffer = new byte[1024];int readLen = 0;while((readLen = inputstream.read(buffer))!=-1) {System.out.println(new String(buffer,0,readLen));}OutputStream outputStream = socket.getOutputStream();outputStream.write("hello,client".getBytes());//4.设置结束标记socket.shutdownOutput();inputstream.close();outputStream.close();socket.close();serverSocket.close();}}
  • 客户端
public class SocketTCP01Client {public static void main(String[] args) throws IOException {//1连接本机的9999端口,连接成功,返回Socket对象Socket socket = new Socket(InetAddress.getLocalHost(),9999);System.out.println("客户端socket返回="+socket.getClass());//2.连接上后,通过输出流将数据写到数据通道OutputStream outputStream = socket.getOutputStream();outputStream.write("hello,server".getBytes());//3.设置结束标记socket.shutdownOutput();InputStream inputstream = socket.getInputStream();byte[] buffer = new byte[1024];int readLen = 0;while((readLen = inputstream.read(buffer))!=-1) {System.out.println(new String(buffer,0,readLen));}outputStream.close();inputstream.close();socket.close();}
}

注意这道题跟第一道的区别:客户端发送数据后还要等待服务端相应数据后输出,不是立即关闭,但是服务端并不知道客户端连接上发送数据后何时结束,因此会一直处于一个等待客户端发送数据的状态,所以客户端需要在数据传输完毕后告诉服务端我已传输结束。所以客户端就需要发送一个socket.shutdownOutput()跟服务端表示传输结束,此时服务端就会接受数据进行处理,处理完毕后向客户端发送数据,同样也要告诉客户端何时结束socket.shutdownOutput(),客户端才能将服务端发送过来的数据进行处理。

练习3

在练习2的基础上改用字符流的方式。

public class SocketTCP01Server {public static void main(String[] args) throws IOException {//1.在本机的9999端口监听,等待连接ServerSocket serverSocket = new ServerSocket(9999);System.out.println("服务端,在9999端口监听,等待连接...");//2.当没有客户端连接9999端口时,程序会阻塞,等待连接//如果有客户端连接,则会返回Socket对象,程序继续Socket socket = serverSocket.accept();System.out.println("服务端 socket="+socket.getClass());//3.读取客户端写入到数据通道的数据InputStream inputstream = socket.getInputStream();BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputstream));String s = bufferedReader.readLine();System.out.println(s);OutputStream outputStream = socket.getOutputStream();BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream));bufferedWriter.write("hello,client 字符流");bufferedWriter.newLine();//表示写入的内容结束,注意:要求对方要使用readLine()bufferedWriter.flush(); //使用字符流需要手动刷新,否则数据不会写入数据通道bufferedWriter.close();outputStream.close();bufferedReader.close();inputstream.close();socket.close();serverSocket.close();}}
public class SocketTCP01Client {public static void main(String[] args) throws IOException {//1连接本机的9999端口,连接成功,返回Socket对象Socket socket = new Socket(InetAddress.getLocalHost(),9999);System.out.println("客户端socket返回="+socket.getClass());//2.连接上后,通过输出流将数据写到数据通道OutputStream outputStream = socket.getOutputStream();BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream));bufferedWriter.write("hello,server 字符流");bufferedWriter.newLine();//表示写入的内容结束,注意:要求对方要使用readLine()bufferedWriter.flush(); //使用字符流需要手动刷新,否则数据不会写入数据通道InputStream inputstream = socket.getInputStream();BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputstream));String s = bufferedReader.readLine();System.out.println(s);bufferedWriter.close();outputStream.close();bufferedReader.close();inputstream.close();socket.close();}
}

聊天室

通用类型

功能:可以进行私聊、群聊、服务器消息推送、文件发送、离线发送消息功能。

离线文件发送是我自己实现的,用了一天,对网络编程方面的内容并不是很熟悉,主要难点就是:因为没有使用数据库存储离线消息,所以需要服务器端和客户端之间来回切换,发送信息时需要先有一条接受通道,不然数据传输不过来会报错。
在这里插入图片描述

用户

用于验证用户的登录权限,这里使用集合方式,不用数据库校验。

User表

public class User implements Serializable {private static final long serialVersionUID = -636779447033767710L;private String userId;private String password;//省略getter和setter方法
}

消息和消息类型

消息

统一消息格式

public class Message implements Serializable {private static final long serialVersionUID = 6684370754287710L;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;  //源文件路径//省略getter、setter、toString方法
}

消息类型

public interface MessageType {String MESSAGE_LOGIN_SUCCESS = "1";  	//登录成功String MESSAGE_LOGIN_FAIL = "2";	 	//登录失败String MESSAGE_LOGIN_MES = "3";         //普通信息包String MESSAGE_GET_ONLINE_FRIEND = "4"; //要求返回在线用户列表String MESSAGE_RET_ONLINE_FRIEND = "5"; //返回在线用户列表String MESSAGE_CLIENT_EXIT = "6"; 		//客户端请求退出String MESSAGE_SEND_ALL = "7"; 			//向所有用户发送信息String MESSAGE_FILE_MES = "8";
}

服务端

QQServer

服务端入口:服务端一直处于一个监听状态,客户端先向服务端发起权限校验,将User对象发给服务端,服务端接受到后进行校验校验成功则发起一个成功标志并开启一个线程用于与这个客户端进行通信。客户端接受到了也会开启一个线程与之进行通信。线程开启先后顺序无关,数据传输通道的开启顺序则有关系,需要先开启接受通道,在开启发送通道。

实现:

  • 建立数据传输通道,接受到表示有客户端向服务端发起连接请求,这个通道是公用的。
  • 用户登录成功服务器开启一个线程跟当前登录成功的用户进行通信【用户登录成功开启一个线程用于与服务器进行通信】,失败则向用户发送登录失败。
//这是服务器在监听9999,等待客户端的连接,并保持通信
public class QQServer {private ServerSocket ss = null;//hashMap没有处理线程安全问题,可以使用concurrentHashMapprivate 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.getPassword().equals(passwd)){return true;}return false;}public QQServer(){System.out.println("服务器在9999端口监听");try {//开启一个新的线程通知所有在线用户有新用户上线new Thread(new SendNewsToAllService()).start();ss = new ServerSocket(9999);while (true){//建立数据传输通道,接受到表示有客户端向服务端发送消息,这个通道是公用的Socket socket = ss.accept();ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());User u = (User) objectInputStream.readObject();  //获取登录用户的信息Message message = new Message();//用户登录成功开启一个线程跟用户通信,失败则向用户发送登录失败if(checkUser(u.getUserId(),u.getPassword())){message.setMesType(MessageType.MESSAGE_LOGIN_SUCCESS);oos.writeObject(message); //客户端也开启相应的线程ServerConnectClientThread serverConnectClientThread =new ServerConnectClientThread(socket,u.getUserId());serverConnectClientThread.start();//线程开启成功后将其交由ManageClientThreads管理,这里不知道会不会发生线程安全问题,serverConnectClientThread跟用户不匹配,我们在添加时校验一下即可ManageClientThreads.addClientThread(u.getUserId(),serverConnectClientThread);}else {message.setMesType(MessageType.MESSAGE_LOGIN_FAIL);oos.writeObject(message);socket.close();}}} catch (Exception e) {e.printStackTrace();}finally {try {//如果服务器推出了while循环,说明服务器不再监听,因此关闭ServerSocketss.close();} catch (IOException e) {e.printStackTrace();}}}
}

ServerConnectClientThread

服务器端连接客户端的线程,就是上面登录成功后开启的线程。用于接受客户端发起的请求并响应。

功能:

  1. 获取在线用户列表
  2. 处理离线消息
  3. 私聊,消息转发
  4. 群聊,消息转发
  5. 退出,这个线程终止

获取在线用户列表

这个功能很简单,因为服务器端针对登录成功的用户开启了对应线程并用集合进行管理,只要获取这个集合即可。

私聊

登录成功后,服务器端和客户端都开启了线程进行通信,客户端就可以进行数据传输,而数据中携带有接收者,这个接受者如果在线,那么也有服务器端也有对应的线程跟其通信,所以只要服务器将对应线程获取,然后进行消息转发即可。如果接收者不在线不进行数据转发,而是保存起来在集合【集合的键为接收者id,值为数组】中。数组是真正用来保存离线消息的。

群聊

跟私聊一样,接受者为在线用户。

处理离线消息

用户上线后可获取离线消息,就是遍历集合中是否有key=userId的键值对存在,如果有证明有人向你发送消息。获取对应的数组【真正保存有离线消息】,然后获取第一条数据,向用户发送,用户接受到后,继续向客户端发起请求获取离线数据,如果数组获取不到则返回另一种消息类型,用户就不会继续获取离线数据。【为什么不采用遍历方式向客户端发送数据?遍历写数据过去,属于并发流写出,会报错误,可能是由于客户端只有一个流在接受的原因。】

//该类的一个对象和某个客户端保持通信
public class ServerConnectClientThread extends Thread{private Socket socket;private String userId;static ArrayList<Message> all_message = new ArrayList<>();static ConcurrentHashMap<String, ArrayList<Message>> offLineDb = new ConcurrentHashMap<>();public ServerConnectClientThread(Socket socket, String userId){this.socket = socket;this.userId = userId;}public Socket getSocket(){return socket;}@Overridepublic void run() {ArrayList<Message> messages = offLineDb.get(userId);offLineDb.remove(userId);if(messages==null){messages = new ArrayList<>();Message m = new Message();m.setMesType(MessageType.MESSAGE_OFFLINE_MESS);m.setContent("您好,暂时没有人在您离线时给您发过消息");messages.add(m);}while (true){try {System.out.println("服务端和客户端保持通信,读取数据...");ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());Message message = (Message) objectInputStream.readObject();message.setSender(userId);ObjectOutputStream oos = null;if(message.getMesType().equals(MessageType.MESSAGE_OFFLINE_MESS)){System.out.println("----------------------------------------");ServerConnectClientThread serverConnectClientThread =ManageClientThreads.getServerConnectClientThread(userId);oos = new ObjectOutputStream(serverConnectClientThread.getSocket().getOutputStream());Message temp = null;try{temp = messages.get(0);messages.remove(0);}catch (IndexOutOfBoundsException e){temp = new Message();temp.setMesType(MessageType.MESSAGE_EMPTY);}oos.writeObject(temp);}else if(message.getMesType().equals(MessageType.MESSAGE_GET_ONLINE_FRIEND)){System.out.println(message.getSender()+"要获取在线用户列表");String onlineUser = ManageClientThreads.getOnlineUser();Message message1 = new Message();message1.setMesType(MessageType.MESSAGE_RET_ONLINE_FRIEND);message1.setContent(onlineUser);message1.setGetter(message.getSender());oos = new ObjectOutputStream(socket.getOutputStream());oos.writeObject(message1);}else if(message.getMesType().equals(MessageType.MESSAGE_LOGIN_MES)){ServerConnectClientThread serverConnectClientThread =ManageClientThreads.getServerConnectClientThread(message.getGetter());if(serverConnectClientThread!=null){ObjectOutputStream objectOutputStream = new ObjectOutputStream(serverConnectClientThread.getSocket().getOutputStream());objectOutputStream.writeObject(message); //转发}else {//为空表示该用户未上线,数据应该暂存起来String getter = message.getGetter();message.setMesType(MessageType.MESSAGE_OFFLINE_MESS);
//                        all_message.add(message); //方法一:所有离线数据都保存到all_message中,每次获取离线数据都要全部遍历//方法二:速度更快,每个用户对应一个ArrayListArrayList<Message> one_messages = offLineDb.get(getter);if(one_messages==null){ArrayList<Message> ms = new ArrayList<>();ms.add(message);offLineDb.put(getter,ms);}else {one_messages.add(message);}}}  else if (message.getMesType().equals(MessageType.MESSAGE_SEND_ALL)){message.setMesType(MessageType.MESSAGE_SEND_ALL);HashSet<Socket> onlineSocket = ManageClientThreads.getOnlineSocket(message.getSender());Iterator<Socket> iterator1 = onlineSocket.iterator();while (iterator1.hasNext()){ObjectOutputStream objectOutputStream = new ObjectOutputStream(iterator1.next().getOutputStream());objectOutputStream.writeObject(message);}}else if(message.getMesType().equals(MessageType.MESSAGE_FILE_MES)) {oos = new ObjectOutputStream(ManageClientThreads.getServerConnectClientThread(message.getGetter()).getSocket().getOutputStream());oos.writeObject(message);} else if (message.getMesType().equals(MessageType.MESSAGE_CLIENT_EXIT)) {System.out.println(message.getSender()+"退出");ManageClientThreads.removeServerConnectClientThread(message.getSender());socket.close();break;}} catch (Exception e) {e.printStackTrace();}}}
}

ManageClientThreads

  • 服务端管理客户端的线程。要通过userId和socket配套起来,形成逻辑上的客户端和服务端的数据传输通道。
    在这里插入图片描述
    注意:socket之间都是能进行数据传输的,那么就存在用户A向用户B发送数据时,用户C获取到了数据的情况。所以我们需要在通信时,将数据正确传输到对应的socket。用户A向用户B发送消息时,由服务器将数据进行转发,所以服务器正确转发数据就显得很重要,所以需要在用户A和用户B都上线的情况下,用户A就与服务器建立起了一条连接通道,用户B也跟服务器建立起了一条通道。一个userId对应一个线程(线程中开启socket),服务器进行数据接受时通过userId获取socket,所以这个socket就一直跟这个用户通信。消息转发时,服务器根据接受者ID获取对应socket,然后将数据传输过去。所以只是逻辑上区分。
public class ManageClientThreads {//把多个线程放入到一个集合中,key就是用户id,value就是线程private static HashMap<String,ServerConnectClientThread> hm = new HashMap<>();public static HashMap<String, ServerConnectClientThread> getHm() {return hm;}public static void addClientThread(String userId,ServerConnectClientThread serverConnectClientThread){if(serverConnectClientThread.getUserId()==userId){hm.put(userId, serverConnectClientThread);}}public static ServerConnectClientThread getServerConnectClientThread(String userId){return hm.get(userId);}public static void removeServerConnectClientThread(String userId){hm.remove(userId);}public static String getOnlineUser(){Iterator<String> iterator = hm.keySet().iterator();String onlineUserList = "";while(iterator.hasNext()){onlineUserList += iterator.next().toString()+" ";}return onlineUserList;}public static HashSet<Socket> getOnlineSocket(String id){HashSet<Socket> sockets = new HashSet<>();Iterator<ServerConnectClientThread> iterator = hm.values().iterator();while (iterator.hasNext()){ServerConnectClientThread next = iterator.next();if(!next.getUserId().equals(id)){sockets.add(next.getSocket());}}return sockets;}
}

SendNewsToAllService

这个线程用于服务器向客户端推送消息,所以另开启线程,这个线程没有终止状态。

public class SendNewsToAllService implements Runnable {Scanner sc = new Scanner(System.in);@Overridepublic void run() {while (true) {System.out.print("请输入服务器要推送的新闻/消息");String news = sc.next();if ("exit".equals(news)) {break;}Message message = new Message();message.setSender("服务器");message.setContent(news);message.setMesType(MessageType.MESSAGE_SEND_ALL);message.setSendTime(new Date().toString());System.out.println("服务器推送消息给所有人说:" + news);HashSet<Socket> onlineSocket = ManageClientThreads.getOnlineSocket(null);Iterator<Socket> iterator = onlineSocket.iterator();while(iterator.hasNext()){try {OutputStream outputStream = iterator.next().getOutputStream();ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);objectOutputStream.writeObject(message);} catch (IOException e) {e.printStackTrace();}}}}
}

客户端

QQView

public class QQView {private static boolean loop = true;private static Scanner sc = new Scanner(System.in);private static UserClientService userClientService = new UserClientService(); //这里应该不能使用static,后续修改static MessageClientServer messageClientServer = new MessageClientServer();static FileClientService fileClientService = new FileClientService();static String userId;static String password;private static void mainMenu(){while (loop){System.out.println("=========欢迎登录网络通信系统");System.out.println("\t\t 1.登录系统");System.out.println("\t\t 9.退出系统");System.out.print("请输入你的选择:");char c = sc.next().charAt(0);switch (c){case '1':System.out.print("请输入您的姓名:");userId = sc.next();System.out.print("请输入您的密码:");password = sc.next();if(userClientService.check(userId,password)){secondMenu();}break;case '9':loop = false;break;}}}public static void secondMenu(){System.out.println("=====欢迎来到二级菜单====");boolean flag = true;while (flag){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 5.获取离线消息");System.out.println("\t\t 9.退出系统");System.out.print("请在输入你的选择:");char c = sc.next().charAt(0);switch (c){case '1':userClientService.onlineFriendList();break;case '2':System.out.print("请输入接收方的ID(在线):");String getterId = sc.next();System.out.println("请输入你要发送的消息");String content = sc.next();messageClientServer.sendMessageToOne(content,userId,getterId);break;case '3':System.out.println("请输入你要发送的消息");String content1 = sc.next();messageClientServer.sendMessageToAll(content1,userId);break;case '4':System.out.print("请输入接收方的ID(在线):");getterId = sc.next();System.out.println("请输入你要发送的文件(格式为:D:\\xx.jpg");String src = sc.next();System.out.println(src);System.out.println("请输入对方接受文件位置(格式为:D:\\xx.jpg");String dest = sc.next();System.out.println(dest);fileClientService.sendFileToOne(src,dest,userId,getterId);break;case '5':messageClientServer.reveiveOfflineMessage(userId);break;case '9':userClientService.logout();break;}}}public static void main(String[] args) {new QQView().mainMenu();}
}

UserClientService

  • 校验用户是否合法,服务端向客户端返回信息,判断是否校验成功,若成功服务器和客户端都会开启一个线程用于通信。【线程开启先后顺序无关,但是数据传输通道的开启就有关系,需要先有一个接受通道,发送通道才能进行发送。】
  • 注意用户退出,要使用System.exit(0);退出进程,因为一个用户会进行登录成功后,有主线程运行,与服务器进行通信的线程运行,如果仅仅退出子线程服务器端会报错,所以应该整个进程退出。

在这里插入图片描述

public class UserClientService {private User user = new User();public boolean check(String userId,String password){user.setUserId(userId);user.setPassword(password);boolean b = false; //检查用户是否合法Socket socket = null;try {//向服务器端写入登录用户信息,服务端先有一个通道在等待接受socket = new Socket(InetAddress.getByName("127.0.0.1"), 9999);ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());oos.writeObject(user);//服务端向客户端返回信息,判断是否校验成功,成功则开启一个线程与其进行通信ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());Message ms = (Message) ois.readObject();if(ms.getMesType().equals(MessageType.MESSAGE_LOGIN_SUCCESS)){ClientConnectServerThread ccst = new ClientConnectServerThread(socket,userId);ccst.start();ManageClientConnectServerThread.addClientConnectServerThread(userId,ccst);b = true;} else {socket.close();}} catch (Exception e) {e.printStackTrace();}return b;}public void onlineFriendList(){//发送一个MessageMessage message = new Message();message.setMesType(MessageType.MESSAGE_GET_ONLINE_FRIEND);//发送给服务器try {ClientConnectServerThread clientConnectServerThread = ManageClientConnectServerThread.getClientConnectServerThread(user.getUserId());Socket socket = clientConnectServerThread.getSocket();ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());objectOutputStream.writeObject(message);} catch (IOException e) {e.printStackTrace();}}//退出客户端,并给服务器发送一个退出系统的message对象public void logout(){Message message = new Message();message.setMesType(MessageType.MESSAGE_CLIENT_EXIT);message.setSender(user.getUserId()); //指出发起退出请求的是哪个客户端idtry{
//            ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());ObjectOutputStream oos = new ObjectOutputStream(ManageClientConnectServerThread.getClientConnectServerThread(user.getUserId()).getSocket().getOutputStream());oos.writeObject(message);System.out.println(user.getUserId()+"退出系统");System.exit(0); //结束进程}catch (IOException e){e.printStackTrace();}}
}

ClientConnectServerThread

这个线程并不是用于用户发送消息给服务器的,而是接受来自服务器的消息。

public class ClientConnectServerThread extends Thread {private Socket socket;private String userId;Message message = new Message();public ClientConnectServerThread(Socket socket,String userId) {this.socket = socket;this.userId = userId;}@Overridepublic void run() {//因为Thread需要在后台和服务器通信,因此我们需要while循环while (true){try{System.out.println("客户端线程等待读取服务端发送的消息");ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());//如果服务器没有发送Message对象,线程会阻塞在这里Message message = (Message) objectInputStream.readObject();if(message.getMesType().equals(MessageType.MESSAGE_RET_ONLINE_FRIEND)){String[] onlineUsers = message.getContent().split(" ");System.out.println("====当前在线用户列表====");for(int i=0;i<onlineUsers.length;i++){System.out.println("用户:"+onlineUsers[i]);}}else if(message.getMesType().equals(MessageType.MESSAGE_LOGIN_MES)){System.out.println("\n"+message.getSender()+"对"+message.getGetter()+"说:"+message.getContent());}else if(message.getMesType().equals(MessageType.MESSAGE_SEND_ALL)){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.getDest());FileOutputStream fileOutputStream = new FileOutputStream(message.getDest());fileOutputStream.write(message.getFileBytes());fileOutputStream.close();}else if(message.getMesType().equals(MessageType.MESSAGE_OFFLINE_MESS)){System.out.println(message.getSender()+"在"+message.getSendTime()+"向你发了:"+message.getContent());ClientConnectServerThread ccst = ManageClientConnectServerThread.getClientConnectServerThread(userId);ObjectOutputStream oos = new ObjectOutputStream(ccst.getSocket().getOutputStream());message.setMesType(MessageType.MESSAGE_OFFLINE_MESS);oos.writeObject(message);}}catch (Exception e){e.printStackTrace();}}}public Socket getSocket(){return socket;}
}

ManageClientConnectServerThread

public class ManageClientConnectServerThread {//把多个线程放入到一个集合中,key就是用户id,value就是线程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);}
}

FileClientServiceh

这只是个方法,并不是开启线程,所以在文件发送时,并不能进行通信。还是有很多问题需要解决的。

public class FileClientService {//将文件内容读取到message中并发送给服务器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();}}
}

MessageClientServer

public class MessageClientServer {public void sendMessageToOne(String content,String senderId,String getterId){Message message = new Message();message.setSender(senderId);message.setGetter(getterId);message.setContent(content);message.setMesType(MessageType.MESSAGE_LOGIN_MES);message.setSendTime(new Date().toString());System.out.println(senderId+"对"+getterId+"说"+content);try {ClientConnectServerThread clientConnectServerThread = ManageClientConnectServerThread.getClientConnectServerThread(senderId);Socket socket = clientConnectServerThread.getSocket();OutputStream outputStream = socket.getOutputStream();ObjectOutputStream oos = new ObjectOutputStream(outputStream);oos.writeObject(message);}catch (IOException e){e.printStackTrace();}}//群发消息public void sendMessageToAll(String content,String senderId){Message message = new Message();message.setSender(senderId);message.setContent(content);message.setMesType(MessageType.MESSAGE_SEND_ALL);message.setSendTime(new Date().toString());System.out.println(senderId+"对所有人说"+content);try {ClientConnectServerThread clientConnectServerThread = ManageClientConnectServerThread.getClientConnectServerThread(senderId);Socket socket = clientConnectServerThread.getSocket();OutputStream outputStream = socket.getOutputStream();ObjectOutputStream oos = new ObjectOutputStream(outputStream);oos.writeObject(message);}catch (IOException e){e.printStackTrace();}}public void reveiveOfflineMessage(String getterId){Message message = new Message();message.setGetter(getterId);message.setMesType(MessageType.MESSAGE_OFFLINE_MESS);message.setSendTime(new Date().toString());System.out.println(getterId+"想要获取离线消息!");try {ClientConnectServerThread clientConnectServerThread = ManageClientConnectServerThread.getClientConnectServerThread(getterId);Socket socket = clientConnectServerThread.getSocket();OutputStream outputStream = socket.getOutputStream();ObjectOutputStream oos = new ObjectOutputStream(outputStream);oos.writeObject(message);}catch (IOException e){e.printStackTrace();}}
}

打包

  • idea打包Java文件为exe
  • exe工具下载
  • 打包成exe文件,需要选择控制台方式输出才可以与用户进行交互,如果采用GUI则不能与用户交互,因为本身就没有通过GUI可视化界面进行编程。
  • 如果电脑没有安装JDK、JRE会出现闪退,可以具体看下:总结就是把JDK和JRE也到打包进去

测试

  • 自己测试:
    先启动服务器,然后启动两个客户端,A客户端就可以发送消息给B客户端了。【注意三个服务都要在同个主机启动】
  • 多人测试:
    如果有云服务器的同学可以将打包后的服务器部署到云服务器上,此时服务器相当于公开的中转站,A客户端发送消息给其他主机上的B客户端,就可以通过这个中转站进行,因为这个中转站是公开的,在A、B客户端启动时就与其进行了通信通道的建立,因此A、B客户端可以进行跨主机通信。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/114051.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

软件测试—测试用例的设计

软件测试—测试用例的设计 测试用例是什么&#xff1f; 首先&#xff0c;测试用例&#xff08;Test Case&#xff09;是为了实施测试而向被测试系统提供的一组集合。这组集合包括&#xff1a;测试环境、操作步骤、测试数据、预期结果等要素。 好的测试用例的特征 一个好的测试…

349. 两个数组的交集

题目来源&#xff1a;力扣 题目描述&#xff1a; 给定两个数组 nums1 和 nums2 &#xff0c;返回 它们的交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序 。 示例 1&#xff1a; 输入&#xff1a;nums1 [1,2,2,1], nums2 [2,2] 输出&#x…

jdbc235

概念&#xff1a;java database connectivity java数据库连接 java语言操作数据库 定义了一套操作所有关系型数据库的规则&#xff08;接口&#xff09; 本质&#xff1a;其实是官方公司定义了一套操作所有关系型数据库的规则&#xff0c;即接口。各个数据库厂商去实现这套接…

SSH远程连接macOS服务器:通过cpolar内网穿透技术实现远程访问的设置方法

文章目录 前言1. macOS打开远程登录2. 局域网内测试ssh远程3. 公网ssh远程连接macOS3.1 macOS安装配置cpolar3.2 获取ssh隧道公网地址3.3 测试公网ssh远程连接macOS 4. 配置公网固定TCP地址4.1 保留一个固定TCP端口地址4.2 配置固定TCP端口地址 5. 使用固定TCP端口地址ssh远程 …

联想电脑装系统无法按F9后无法从系统盘启动的解决方案

开机时按F9发现没有加载系统盘. 打开BIOS设置界面&#xff0c;调整设置如下: BOOT MODE: Legacy Support.允许legacy方式boot. BOOT PRIORITY: Legacy First. Legacy方式作为首选的boot方式. USB BOOT: ENABLED. 允许以usb方式boot. Legacy: 这里设置legacy boot的优先级,…

postgresql-日期函数

postgresql-日期函数 日期时间函数计算时间间隔获取时间中的信息截断日期/时间创建日期/时间获取系统时间CURRENT_DATE当前事务开始时间 时区转换 日期时间函数 PostgreSQL 提供了以下日期和时间运算的算术运算符。 计算时间间隔 age(timestamp, timestamp)函数用于计算两…

栈和队列(优先级队列)

一)删除字符串中所有相邻字符的重复项 1047. 删除字符串中的所有相邻重复项 - 力扣&#xff08;LeetCode&#xff09; 算法原理:栈结构模拟&#xff0c;只是需要遍历所有字符串中的字符&#xff0c;一次存放到栈里面即可&#xff0c;也是可以使用数组来模拟一个栈结构的: class…

如何在Windows本地快速搭建SFTP文件服务器,并通过端口映射实现公网远程访问

文章目录 1. 搭建SFTP服务器1.1 下载 freesshd服务器软件1.3 启动SFTP服务1.4 添加用户1.5 保存所有配置 2 安装SFTP客户端FileZilla测试2.1 配置一个本地SFTP站点2.2 内网连接测试成功 3 使用cpolar内网穿透3.1 创建SFTP隧道3.2 查看在线隧道列表 4. 使用SFTP客户端&#xff0…

2023.8各大浏览器11家对比:Edge/Chrome/Opera/Firefox/Tor/Vivaldi/Brave,安全性,速度,体积,内存占用

测试环境&#xff1a;全默认设置的情况下&#xff0c;均在全新的系统上进行测试&#xff0c;系统并未进行任何改动&#xff0c;没有杀毒软件&#xff0c;浏览器进程全部在后台&#xff0c;且为小窗模式&#xff0c;小窗分辨率均为浏览器厂商默认缩放大小(变量不唯一)&#xff0…

Unity——拖尾特效

拖尾是一种很酷的特效。拖尾的原理来自人类的视觉残留&#xff1a;观察快速移动的明亮物体&#xff0c;会看到物体移动的轨迹。摄像机通过调整快门时间&#xff0c;也可以拍出具有拖尾效果的照片&#xff0c;如在城市的夜景中&#xff0c;汽车的尾灯拖曳出红色的线条。 在较老…

一文看懂DETR(二)

训练流程 1.输入图像经过CNN的backbone获得32倍下采样的深度特征&#xff1b; 2.将图片给拉直形成token&#xff0c;并添加位置编码送入encoder中&#xff1b; 3.将encoder的输出以及Object Query作为decoder的输入得到解码特征&#xff1b; 4.将解码后的特征传入FFN得到预测特…

Ubantu安装mongodb,开启远程访问和认证

最近因为项目原因需要在阿里云服务器上部署MongoDB&#xff0c;操作系统为Ubuntu&#xff0c;网上查阅了一些资料&#xff0c;特此记录一下步骤。 1.运行apt-get install mongodb命令安装MongoDB服务&#xff08;如果提示找不到该package&#xff0c;说明apt-get的资源库版本比…

研磨设计模式day15策略模式

场景 问题描述 经常会有这样的需要&#xff0c;在不同的时候&#xff0c;要使用不同的计算方式。 解决方案 策略模式 定义&#xff1a; 解决思路&#xff1a;

【Mysql问题集锦】:Can‘t create table ‘#sql-58d7_431d‘ (errno: 28)

问题描述&#xff1a; 问题原因&#xff1a; OSError: [Errno 28] No space left on device&#xff0c;即&#xff1a;磁盘空间不足&#xff0c;无法创建文件。因此&#xff0c;导致Mysql无法执行SQL语句。 问题解法&#xff1a; Step 1&#xff0c;查看有哪些目录占用了大量…

servlet初体验之环境搭建!!!

我们需要用到tomcat服务器&#xff0c;咩有下载的小伙伴看过来&#xff1a;如何正确下载tomcat&#xff1f;&#xff1f;&#xff1f;_明天更新的博客-CSDN博客 1. 创建普通的Java项目&#xff0c;并在项目中创建libs目录存放第三方的jar包。 建立普通项目 创建libs目录存放第三…

第三届计算机、物联网与控制工程国际学术会议(CITCE 2023)

第三届计算机、物联网与控制工程国际学术会议&#xff08;CITCE 2023) The 3rd International Conference on Computer, Internet of Things and Control Engineering&#xff08;CITCE 2023) 第三届计算机、物联网与控制工程国际学术会议&#xff08;CITCE 2023&#xff09;…

Git向远程仓库与推送以及拉取远程仓库

理解分布式版本控制系统 1.中央服务器 我们⽬前所说的所有内容&#xff08;⼯作区&#xff0c;暂存区&#xff0c;版本库等等&#xff09;&#xff0c;都是在本地也就是在你的笔记本或者计算机上。⽽我们的 Git 其实是分布式版本控制系统&#xff01;什么意思呢? 那我们多人…

Linux(实操篇二)

Linux实操篇 Linux(实操篇二)1. 常用基本命令1.3 时间日期类1.3.1 date显示当前时间1.3.2 显示非当前时间1.3.3 date设置系统时间1.3.4 cal查看日历 1.4 用户管理命令1.4.1 useradd添加新用户1.4.2 passwd设置用户密码1.4.3 id查看用户是否存在1.4.4 cat /etc/passwd 查看创建了…

C语言练习7(巩固提升)

C语言练习7 编程题 前言 “芳林新叶催陈叶&#xff0c;流水前波让后波。”改革开放40年来&#xff0c;我们以敢闯敢干的勇气和自我革新的担当&#xff0c;闯出了一条新路、好路&#xff0c;实现了从“赶上时代”到“引领时代”的伟大跨越。今天&#xff0c;我们要不忘初心、牢记…

2023有哪些更好用的网页制作工具

过去&#xff0c;专业人员使用HTMLL、CSS、Javascript等代码手动编写和构建网站。现在有越来越多的智能网页制作工具来帮助任何人实现零代码基础&#xff0c;随意建立和设计网站。在本文中&#xff0c;我们将向您介绍2023年流行的网页制作工具。我相信一旦选择了正确的网页制作…