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

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

TCP网络通信

基本介绍

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

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

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

  4. 基于Socket的TCP编程
    多线程网络实现在线聊天系统(详细源码)_第1张图片
    多线程网络实现在线聊天系统(详细源码)_第2张图片

客户端通过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();
	}
}

聊天室

通用类型

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

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

用户

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

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配套起来,形成逻辑上的客户端和服务端的数据传输通道。
    多线程网络实现在线聊天系统(详细源码)_第4张图片
    注意: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);退出进程,因为一个用户会进行登录成功后,有主线程运行,与服务器进行通信的线程运行,如果仅仅退出子线程服务器端会报错,所以应该整个进程退出。

多线程网络实现在线聊天系统(详细源码)_第5张图片

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客户端可以进行跨主机通信。

你可能感兴趣的:(Java基础,实战项目,网络,java)