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

news2024/11/20 6:16:32

这篇博客整理自韩顺平老师的多线程网络学习,在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没有处理线程安全问题,可以使用concurrentHashMap
    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.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循环,说明服务器不再监听,因此关闭ServerSocket
                ss.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;
    }

    @Override
    public 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中,每次获取离线数据都要全部遍历

                        //方法二:速度更快,每个用户对应一个ArrayList
                        ArrayList<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);
    @Override
    public 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(){
        //发送一个Message
        Message 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()); //指出发起退出请求的是哪个客户端id
        try{
//            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;
    }

    @Override
    public 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.coloradmin.cn/o/945767.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

数论基础(II)。

数论基础&#xff08;II&#xff09;TOC 数论按照研究的数据、方法、方向不同&#xff0c;通常可以分为玄数论、素数论、和数论。无限个数&#xff0c;真正用得到的只有数头&#xff1b;数头比较重要的关限是100&#xff0c;120&#xff0c;十万&#xff0c;百亿&#xff0c;&…

【USRP】调制解调系列5:16QAM、32QAM、64QAM、256QAM、1024QAM、基于labview的实现

QAM 正交振幅键控是一种将两种调幅信号&#xff08;2ASK和2PSK&#xff09;汇合到一个信道的方法&#xff0c;因此会双倍扩展有效带宽&#xff0c;正交调幅被用于脉冲调幅。正交调幅信号有两个相同频率的载波&#xff0c;但是相位相差90度&#xff08;四分之一周期&#xff0c…

打造互动体验:品牌 DTC 如何转变其私域战略

越来越多的品牌公司选择采用DTC 模式与消费者进行互动&#xff0c;而非仅仅销售产品。通过与消费者建立紧密联系&#xff0c;DTC模式不仅可以提供更具成本效益的规模扩张方式&#xff0c;还能够控制品牌体验、获取宝贵的第一方数据并提升盈利能力。然而DTC模式的经济模型比许多…

嵌入式通用硬件模块设计——串口音频播放模块

模块功能展示&#xff1a; 串口音频控制模块 一、简介 方案为串口音频播放芯片功放芯片&#xff0c;口音频播放芯片IC为my1690-16s&#xff0c;功放为PAM8406。 1、my1690-16s 迈优科技的一款由串口控制的插卡MP3播放控制芯片&#xff0c;支持串口控制播放指定音频、音量调节…

【Unity小技巧】手戳一个简单易用的游戏UI框架(附源码)

文章目录 前言整套框架分为三大部分框架代码调用源码参考完结 前言 开发一款游戏美术成本是极其高昂的&#xff0c;以我们常见的宣传片CG为例&#xff0c;动辄就要成百上千万的价格&#xff0c;因此这种美术物料一般只会放在核心剧情节点&#xff0c;引爆舆论&#xff0c;做高…

对于Android开发,我们为何要学Jetpack Compose?

概述 Jetpack Compose 是用于构建原生 Android 界面的新工具包。它可简化并加快 Android 上的界面开发&#xff0c;使用更少的代码、强大的工具和直观的 Kotlin API&#xff0c;快速让应用生动而精彩。Compose 使用全新的组件——可组合项 (Composable) 来布局界面&#xff0c…

万户协同办公平台 ezoffice存在未授权访问漏洞 附POC

文章目录 万户协同办公平台 ezoffice存在未授权访问漏洞 附POC1. 万户协同办公平台 ezoffice简介2.漏洞描述3.影响版本4.fofa查询语句5.漏洞复现6.POC&EXP7.整改意见8.往期回顾 万户协同办公平台 ezoffice存在未授权访问漏洞 附POC 免责声明&#xff1a;请勿利用文章内的相…

数据仓库总结

1.为什么要做数仓建模 数据仓库建模的目标是通过建模的方法更好的组织、存储数据&#xff0c;以便在性能、成本、效率和数据质量之间找到最佳平衡点。 当有了适合业务和基础数据存储环境的模型&#xff08;良好的数据模型&#xff09;&#xff0c;那么大数据就能获得以下好处&…

C语言每日一练------Day(6)

本专栏为c语言练习专栏&#xff0c;适合刚刚学完c语言的初学者。本专栏每天会不定时更新&#xff0c;通过每天练习&#xff0c;进一步对c语言的重难点知识进行更深入的学习。 今日练习题关键字&#xff1a;整数转换 异或 &#x1f493;博主csdn个人主页&#xff1a;小小unicorn…

Ubuntu下的QT开发

ubuntu安装QT的组件如下&#xff1a; 若要在ubuntu下启动QT有两种方案&#xff0c;一种是在菜单栏搜索qt双QT Create&#xff1b;另一种则是使用命令&#xff1a;/opt/Qt5.12.9/Tools/QtCreator/bin/qtcreator.sh

小白视角:一文读懂3TS腾讯事务处理验证系统的基础知识

小白视角&#xff1a;一文读懂3TS腾讯事务处理验证系统的基础知识 一、解读结果图1.1 异常测试用例1.2 事务的隔离级别&#xff08;1&#xff09;SQL标准隔离级别&#xff08;2&#xff09;快照隔离&#xff08;Snapshot Isolation&#xff0c;简称 SI&#xff09;&#xff08;…

Linux环境离线安装MySQL8.0.33

目录 一、准备 1、检查libaio.so.1 2、卸载删除原有的mariadb 3、删除my.cnf 4、下载mysql安装包 二、安装 1、上传mysql 2、建立mysql所需目录 3、建立配置文件my.cnf 4、创建mysql用户并授权 5、初始化数据库 6、启动MySQL数据库 7、常见启动报错处理 8、配置M…

VS的调试技巧

Visual Studiohttps://visualstudio.microsoft.com/zh-hans/vs/ 目录 1、什么是调试&#xff1f; 2、debug和release 3、调试 3.1、环境 3.2、 快捷键 3.2.1、F10和F11 3.2.2、ctrlF5 3.2.3、F5与F9 3.2.3.1、条件断点 3.3、监视和内存观察 3.3.1、监视 3.3.2、内存 …

【爬虫GUI】YouTube评论采集软件,突破反爬,可无限爬取!

文章目录 一、背景介绍1.1 软件说明1.2 效果演示 二、科普知识2.1 关于视频id2.2 关于评论时间 三、爬虫代码3.1 界面模块3.2 爬虫模块3.3 日志模块 四、获取源码及软件 一、背景介绍 你好&#xff0c;我是马哥python说 &#xff0c;一名10年程序猿。 最近我用python开发了一…

基于蜜獾算法优化的BP神经网络(预测应用) - 附代码

基于蜜獾算法优化的BP神经网络&#xff08;预测应用&#xff09; - 附代码 文章目录 基于蜜獾算法优化的BP神经网络&#xff08;预测应用&#xff09; - 附代码1.数据介绍2.蜜獾优化BP神经网络2.1 BP神经网络参数设置2.2 蜜獾算法应用 4.测试结果&#xff1a;5.Matlab代码 摘要…

融合正余弦和柯西变异的麻雀搜索算法(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

【数据结构】排序(插入、选择、交换、归并) -- 详解

一、排序的概念及其运用 1、排序的概念 排序&#xff1a;所谓排序&#xff0c;就是使一串记录&#xff0c;按照其中的某个或某些关键字的大小&#xff0c;递增或递减的排列起来的操作。 稳定性&#xff1a;假定在待排序的记录序列中&#xff0c;存在多个具有相同的关键字的记…

APP弱网测试

测试用例 在弱网的条件下 页面的响应正常页面展示的数据无误页面的一致性无误&#xff08;图片展示、排版预期一致、数据展示无误&#xff09;是否会出现ANR、Crash 在网络切换的情况下 页面交互无误无奔溃、显示错乱客户端服务端数据一致性展示无误请求堆积的出路无误 在无网…

谈谈智能安防领域

目录 1.什么是智能安防 2.智能安防的发展过程 3.智能安防涉及到的知识 4.智能安防给人类带来的福利 1.什么是智能安防 智能安防是基于人工智能技术的安全防护系统&#xff0c;旨在通过智能化的方法保护人员和财产的安全。它利用传感器、摄像头、算法等技术&#xff0c;通过识…

前端面试必备 | uni-app 篇(P1-15)

文章目录 1. 请简述一下uni-app的定义和特点。2. uni-app兼容哪些前端框架&#xff1f;请列举几个。3. 请简述一下uni-app的跨平台工作原理。4. 什么是条件编译&#xff1f;在uni-app中如何实现条件编译&#xff1f;5. uni-app中的页面生命周期有哪些&#xff1f;请简要介绍。6…