[Socket case] Native java realizes TCP-based multi-person chat room

Effect picture

 

Features

From the project code structure, the code is mainly divided into simple server and client. After running the server, you can run multiple clients to connect to the server. When a client sends a message, it will be forwarded via the server to other clients besides itself.

Although the code is not much, and it is directly handwritten in native Java, it is close to the function of a chat room to a large extent, which is more valuable than many introductory cases about Socket on the Internet. You can even package the corresponding code into a jar package. As shown in the figure below, put server.jar on the cloud server to run. Then other people can run client.jar through cmd to join the chat room and conduct group chat.

Experience

https://www.ysqorz.top/uploaded/file/client.jar

Those who want to try the effect can download client.jar through the above link. Then run cmd as an administrator in the directory where the jar package is located. Run the jar package through the command: java -jar client.jar. You can connect to the server. If someone else is also running the jar package and connecting to the server, then group chat is possible. Of course, you can run multiple cmd to simulate a multi-person situation.

Realization idea

1. Multiple clients establish a TCP connection with the server, and the process of waiting for the connection is in a blocking state. Therefore, a separate thread Connector is required to handle the client connection, and the thread is only responsible for this. That is to say, once the thread waits for a client connection, it will establish its connection, and then it will be ignored. The client's messaging is completed by other threads.

2. For each individual client, the sending and receiving of its messages needs to be completed by a special class ClientHandler. As the name suggests, ClientHandler means client-side handler. Each ClientHandler is responsible for sending and receiving messages from a client. Therefore, the server holds as many ClientHandlers as there are clients that establish connections. Therefore, the server needs to hold and maintain a ClientHandler list.

3. Separate read and write for each client . This is very necessary. When reading a client message, it will be in a blocking state. If it is not separated, the client cannot be written. In many cases on the Internet, it blocks when reading, and then automatically sends back the message after reading the message from the client. To perform read-write separation, the client's read-write operations must be handled by a separate thread. Therefore, each ClientHandler holds two threads for reading and writing.

The above analyzed the difficulties and main ideas of realizing the chat room case, more pits and details, refer to the source code! This case involves the use of Socket, multithreading, Java object-oriented thinking, and the division of functional responsibilities.

Please indicate the source  

Complete code

Package server

package server;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import utils.CloseUtil;

/**
 * 客户端处理者
 * @author	passerbyYSQ
 * @date	2020-9-23 9:18:31
 */
public class ClientHandler {
    private Socket socket;
    private ClientInfo clientInfo;
    
    // 对客户端进行读写分离
    // 负责从客户端读的单独线程
    private ReadHandler reader;
    // 负责向客户端写的单独线程
    private WriteHandler writer;
    
    // 事件回调。当某个客户端的事件触发时,回调给TcpServer
    private ClientEventCallback callback;

    public ClientHandler(Socket socket, ClientEventCallback callback) throws IOException {
        this.socket = socket;
        this.callback = callback;
        clientInfo = new ClientInfo(socket.getLocalAddress(), socket.getPort());
        
        System.out.println("新客户端链接:" + clientInfo);
        
        reader = new ReadHandler(socket.getInputStream());
        reader.start();
        writer = new WriteHandler(socket.getOutputStream());
    }

    public void sendMsg(String msg) {
        writer.send(msg);
    }

    public void exit() {
        reader.exit();
        writer.exit();
        CloseUtil.close(socket);
    }

    public ClientInfo getClientInfo() {
        return clientInfo;
    }

    interface ClientEventCallback {
        void onMsgReceived(ClientHandler handler, String msg);

        void onClientExit(ClientHandler handler);
    }

    class ClientInfo {
        InetAddress inetAddr;
        Integer port;

        ClientInfo(InetAddress inetAddr, Integer port) {
            this.inetAddr = inetAddr;
            this.port = port;
        }

        @Override
        public String toString() {
            return "client [ip=" + this.inetAddr.getHostAddress() + ", port=" + this.port + "]";
        }
    }

    class ReadHandler extends Thread {
        DataInputStream in;
        boolean running = true;

        ReadHandler(InputStream inputStream) {
            in = new DataInputStream(inputStream);
        }

        public void run() {
            while(running) {
                try {
                    String msg = in.readUTF();
                    if ("bye".equalsIgnoreCase(msg)) {
                    	exit();
                    	callback.onClientExit(ClientHandler.this);
                        break;
                    }
                    
                    callback.onMsgReceived(ClientHandler.this, msg);
                    
                } catch (IOException e) {
                    System.out.println(clientInfo.toString() + "关闭:" + 
                    		"异常-" + e.getCause() + ",信息-" + e.getMessage());
                    exit();
                }
           }
        }

        void exit() {
            running = false;
            CloseUtil.close(in);
        }
    }

    /**
     * 负责向客户端写的单独线程。
     * 不继承Thread类,而是巧妙地采用一个单例线程池来发送消息
     */
    class WriteHandler {
        DataOutputStream out;
        ExecutorService executor;

        WriteHandler(OutputStream outputStream) {
            out = new DataOutputStream(outputStream);
            executor = Executors.newSingleThreadExecutor();
        }

        void send(final String msg) {
            executor.execute(new Runnable() {
                public void run() {
                    try {
                        out.writeUTF(msg);
                        out.flush();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }

                }
            });
        }

        void exit() {
            CloseUtil.close(out);
            executor.shutdownNow();
        }
    }
}
package server;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;

import server.ClientHandler.ClientEventCallback;
import utils.CloseUtil;

public class TcpServer implements ClientEventCallback {
    private static ServerSocket serverSocket;
    // 客户端处理者的列表
    private List<ClientHandler> clientHandlerList = new ArrayList<>();
    private Connector connector;

    public void setup() throws IOException {
        serverSocket = new ServerSocket(8096);
        connector = new Connector();
        connector.start();
        
        System.out.println("服务器启动成功,服务器信息:ip-" + 
        		serverSocket.getInetAddress().getHostAddress() + 
        		", port-" + serverSocket.getLocalPort());
    }

    public void exit() {
        exitAllClients();
        connector.exit();
        CloseUtil.close(serverSocket);
    }

    public void exitAllClients() {
        System.out.println("客户端数量:" + clientHandlerList.size());
        for (ClientHandler handler : clientHandlerList) {
        	handler.exit();
        }
        clientHandlerList.clear();
    }

    /**
     * 消息到达时的转发
     */
    @Override
    public void onMsgReceived(ClientHandler handler, String msg) {
        msg = handler.getClientInfo().toString() + ":" + msg;
        System.out.println(msg);
        broadcast(handler, msg);
    }

    /**
     * 客户端退出时的回调
     */
    @Override
    public void onClientExit(ClientHandler handler) {
    	handler.exit();
        clientHandlerList.remove(handler);
        
    	String msg = handler.getClientInfo() + 
    			"已退出群聊。当前客户端数量:" + clientHandlerList.size();
    	broadcast(handler, msg);
    	
        System.out.println(msg);
        
        
    }

    /**
     * 转发消息
     * @param handler	消息所来自的客户端
     * @param msg
     */
    private void broadcast(ClientHandler handler, String msg) {
    	for (ClientHandler clientHandler : clientHandlerList) {
    		// 转发消息时跳过自己
    		if (clientHandler == handler) {
                continue;
            }
    		clientHandler.sendMsg(msg);
        }
    }

    /**
     * 负责处理客户端连接的单独线程
     */
    class Connector extends Thread {
        boolean running = true;

        public Connector() {
            super("负责处理客户端连接的单独线程");
            this.setPriority(MAX_PRIORITY);
        }

        public void run() {
            while(running) {
                try {
                    Socket socket = serverSocket.accept();
                    ClientHandler handler = new ClientHandler(socket, TcpServer.this);
                    clientHandlerList.add(handler);
                    
                    broadcast(handler, handler.getClientInfo() + 
                    		"加入群聊。当前客户端数量:" + clientHandlerList.size());
                } catch (IOException e) {
                    System.out.println("ServerSocket异常关闭:异常-" + 
                    		e.getCause() + ",信息-" + e.getMessage());
                    exit();
                }
            }

        }

        void exit() {
            running = false;
        }
    }
}
package server;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Server {
	
    public static void main(String[] args) throws IOException {
        TcpServer tcpServer = new TcpServer();
        tcpServer.setup();
        
        BufferedReader bufReader = new BufferedReader(new InputStreamReader(System.in));
        boolean flag = true;

        do {
            String command = bufReader.readLine();
            if (command == null) {
                break;
            }

            switch (command.toLowerCase()) {
	            case "exit clients": {
	            	tcpServer.exitAllClients();
	            	break;
	            }
	            case "exit": {
	            	tcpServer.exit();
	            	flag = false;
	            	break;
	            }
	            default: {
	            	System.out.println("Unsupport commond!");
	            }
            }
        } while(flag);

        tcpServer.exit();
    }
}

Package client

package client;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import utils.CloseUtil;

public class TcpClient {
    public static final String SERVER_IP = "119.45.164.115";
    public static final int SERVER_PORT = 8096;
    private Socket socket = new Socket(SERVER_IP, SERVER_PORT);
    private ReadHandler reader;
    private WriteHandler writer;

    public TcpClient() throws IOException {
        System.out.println("连接服务器成功,服务器信息:ip-" + 
        		socket.getInetAddress().getHostAddress() + 
        		", port-" + socket.getPort());
        System.out.println("客户端信息:ip-" + socket.getLocalAddress() + 
        		", port-" + socket.getLocalPort());
        
        reader = new TcpClient.ReadHandler(socket.getInputStream());
        reader.start();
        writer = new TcpClient.WriteHandler(socket.getOutputStream());
    }

    public void exit() {
        reader.exit();
        writer.exit();
        CloseUtil.close(socket);
    }

    public void send(String msg) {
        writer.send(msg);
    }

    class ReadHandler extends Thread {
        DataInputStream in;
        boolean running = true;

        ReadHandler(InputStream inputStream) {
            this.in = new DataInputStream(inputStream);
        }

        public void run() {
            while(running) {
                try {
                    String msg = this.in.readUTF();
                    System.out.println(msg);
                     
                } catch (IOException e) {
                    System.out.println("客户端关闭:异常-" + e.getCause() + 
                    		",信息-" + e.getMessage());
                    exit();
                }
            }
        }

        void exit() {
            running = false;
            CloseUtil.close(in);
        }
    }

    class WriteHandler {
        DataOutputStream out;
        ExecutorService executor;

        WriteHandler(OutputStream outputStream) {
            out = new DataOutputStream(outputStream);
            executor = Executors.newSingleThreadExecutor();
        }

        void send(final String msg) {
            executor.execute(new Runnable() {
                public void run() {
                    try {
                        out.writeUTF(msg);
                        out.flush();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }

                }
            });
        }

        void exit() {
            CloseUtil.close(out);
            executor.shutdownNow();
        }
    }
}
package client;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Client {
	
    public static void main(String[] args) throws IOException {
        TcpClient tcpClient = new TcpClient();
        
        BufferedReader bufReader = new BufferedReader(
        		new InputStreamReader(System.in));
        
        boolean isClosed = false;

        while(true) {
            String msg = bufReader.readLine();
            // socket异常断开,可能造成msg为null,引发下面的空指针异常
            if (msg == null) {
                break;
            }
            // 空字符串不发送
            if (msg.length() == 0) {
            	continue;
            }
            // 退出客户端
            if ("exit".equalsIgnoreCase(msg)) {
        		break;
        	}
            
            // 连接未断开时,才发送
            if (!isClosed) {
            	// 断开连接
                if ("bye".equalsIgnoreCase(msg)) {
                    System.out.println("已请求服务器断开连接,输入exit退出客户端!");
                    isClosed = true;
                } 
            	tcpClient.send(msg);
            }
        }

        tcpClient.exit();
    }
}

Package utils

package utils;

import java.io.Closeable;
import java.io.IOException;

/**
 * 关闭资源的工具类
 * @author	passerbyYSQ
 * @date	2020-9-23 8:55:33
 */
public class CloseUtil {
	
    public static void close(Closeable... closeableArr) {
    	if (closeableArr == null) {
    		return;
    	}
        for (Closeable closeable : closeableArr) {
        	try {
				closeable.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
        }
    }
}

 

Guess you like

Origin blog.csdn.net/qq_43290318/article/details/108760556