Java實現多對多網絡通訊的流程

基本流程

客戶端發送信息(指定目標客戶端)至固定的一個服務端,服務端接收信息進行處理後發送至相應的客戶端

在這裡插入圖片描述

通訊核心類

Socket類與流相輔相成,完成通訊。在accept方法返回瞭一個Socket對象後,獲取socket的輸入輸出流,就可以接收信息或發送信息瞭,以一對一為例:

服務端 :

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * @ClassName Server
 * @Description 服務端
 * @Author issac
 * @Date 2021/4/13 17:26
 */
public class Server {
    public static void main(String[] args) throws IOException {
        // 創建服務端套接字並指定端口
        ServerSocket server = new ServerSocket(88);
        // 接收創建建立,返回連接創建好後服務器的socket對象
        Socket socket = server.accept();

        InputStreamReader reader = new InputStreamReader(socket.getInputStream());
        BufferedReader bufferedReader = new BufferedReader(reader);

        // 獲取請求
        String request = bufferedReader.readLine();
        System.out.println("client say:" + request);

        // 寫到輸出流傳遞給客戶端
        PrintWriter writer = new PrintWriter(socket.getOutputStream());
        String line = "hello too";
        writer.println(line);
        writer.flush();

        // 關閉處理流的工具、socket套接字、服務套接字
        writer.close();
        bufferedReader.close();
        socket.close();
        server.close();
    }
}

客戶端 :

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

/**
 * @ClassName Client
 * @Description 客戶端
 * @Author issac
 * @Date 2021/4/13 17:26
 */
public class Client {
   public static void main(String[] args) throws IOException {
        // 創建socket連接,指明其地址和端口
        Socket socket = new Socket("127.0.0.1", 88);

        // 獲取套接字的輸出流,輸出hello
        PrintWriter writer = new PrintWriter(socket.getOutputStream());
        String readLine = "Hello";
        writer.println(readLine);
        writer.flush();

        // 從套接字的輸入流中獲取信息
        InputStreamReader reader = new InputStreamReader(socket.getInputStream());
        BufferedReader bufferedReader = new BufferedReader(reader);
        String respond = bufferedReader.readLine();
        System.out.println("server say:" + respond);

        bufferedReader.close();
        writer.close();
        socket.close();
    }
}

運行結果:

在這裡插入圖片描述

需要註意的是accept方法在沒有連接的時候會阻塞,而導致後面的代碼無法執行,在接下來的多對多通訊中需要依靠多線程來解決這個問題。

多對多代碼實現

為瞭方便服務端和客戶端對信息的處理,解析。首先定義一個消息類,定義屬性分別為端口的本地地址,發送的消息內容,發送的目標地址。定義靜態方法:將字符串解析為該類實例,處理消息的收發:

import com.alibaba.fastjson.JSON;
import java.io.Serializable;
import com.alibaba.fastjson.JSON;
import java.io.*;
import java.net.Socket;

/**
 * 在網絡中,所有被進行通訊的對象,都需要實現 Serializable 這個接口
 * <p>
 * 該類,主要用於本項目例子中,socket傳輸的對象,請勿使用其他或字符串,
 * 為瞭後期更方便修改或者是其他操作
 *
 * @ClassName SocketMessage
 * @Description TODO
 * @Author issac
 * @Date 2021/4/18 22:02
 */
public class SocketMessage implements Serializable {

    /**
     * 我自己的名稱  ip:port
     **/
    private String key;

    /**
     * 我的目標 ip:port
     **/
    private String to;

    /**
     * 發送的內容
     **/
    private String content;

    public String getKey() {
        return key;
    }

    public void setKey(String key) {
        this.key = key;
    }

    public String getTo() {
        return to;
    }

    public void setTo(String to) {
        this.to = to;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    /**
     * 向目標客戶端寫出從發送者獲取到的消息
     */
    public static void writeTargetMessage(SocketMessage message, Socket socket) throws IOException {
        PrintWriter writer = new PrintWriter(socket.getOutputStream());
        // 統一字符串標準,以便於服務端解析
        writer.println(JSON.toJSONString(message));
        writer.flush();
    }

    /**
     * 將輸入流中接收的字符串解析為SocketMessage對象
     *
     * @param is
     * @return SocketMessage
     * @throws Exception
     */
    public static SocketMessage parseSocket(InputStream is) throws Exception {

        BufferedReader reader = new BufferedReader(new InputStreamReader(is));

        String info = reader.readLine();

        return parseSocketByStr(info);
    }


    /**
     * 將傳入字符串解析為SocketMessage對象並返回
     *
     * @param str
     * @return SocketMessage
     */
    public static SocketMessage parseSocketByStr(String str) {
        SocketMessage socketMessage = null;
        try {
            socketMessage = JSON.parseObject(str, SocketMessage.class);
        } catch (Exception ex) {
            throw new RuntimeException("socket之間通訊不能不使用SocketMessage");
        }
        return socketMessage;
    }

    @Override
    public String toString() {
        // 通過 阿裡巴巴 的FastJson 庫,將一個對象轉換為 字符串 ,統一標準,以便於將字符串解析為該類
        return JSON.toJSONString(this);
    }
}

再單獨定義一個服務端的消息處理類,該類用於發送消息至特定的客戶端,所以定義兩個屬性,1.發送的消息,2.目標客戶端的套接字:

import java.net.Socket;

/**
 * @ClassName SocketMessageHandler
 * @Description  服務端針對客戶端的消息處理器
 * @Author issac
 * @Date 2021/4/18 22:34
 */
public class SocketMessageHandler {

    SocketMessage sm;

    Socket targetSocket;

    public SocketMessageHandler(SocketMessage sm,Socket targetSocket) {
        this.sm = sm;
        this.targetSocket = targetSocket;
    }

    public void setSm(SocketMessage sm) {
        this.sm = sm;
    }

    /**
     * 發送消息
     */
    public void send() {

        if (this.sm == null) {
            return;
        }
        try {
            System.out.println(sm.getContent());

            // 發送
            SocketMessage.writeTargetMessage(sm, this.targetSocket);

        } catch ( Exception ex) {
            ex.printStackTrace();
        }
    }
}

接下來進行服務端的定義,我們的服務端需要處理多個客戶端的消息,所以要定義一個容器存放客戶端地址,在此之前我們已經定義瞭處理服務端消息的SocketMessageHandler類,因為我們的最終目的是為瞭處理信息,所以可以直接將SocketMessageHandler類存放至容器。我們用map來存儲,而key就是客戶端的地址:

import com.issac.task_05.task.msg.SocketMessage;
import com.issac.task_05.task.msg.SocketMessageHandler;

import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;

/**
 * n - m: 一個服務端,同時服務多個客戶端
 *
 * @ClassName SocketServer
 * @Description 服務端
 * @Author issac
 * @Date 2021/4/18 21:29
 */
public class SocketServer {

	// 存放消息處理器
    private static final Map<String, SocketMessageHandler> clientContainer = new HashMap<>();

    public static void main(String[] args) {

        try {
            ServerSocket ss = new ServerSocket(8888);
            Socket accept;
            while (true) {

                /* 隻有建立新連接時accept才會有響應而執行以下代碼,否則會阻塞:客戶端與服務器連接,並將已連接的客戶端放入容器 */
                accept = ss.accept();
                SocketMessage msg = SocketMessage.parseSocket(accept.getInputStream()); // 獲取信息
                System.out.println("客戶端建立連接:" + msg.getKey());

                // 建立連接後將客戶端地址存入容器
                clientContainer.put(msg.getKey(), new SocketMessageHandler(msg, accept));


                /* 在已經建立連接後,沒有新連接,accept會處於阻塞狀態,因此我們需要另外開辟一個線程來處理消息 */
                new ServerThread(accept, clientContainer).start();
            }

        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}

在這裡需要註意ServerSocket類的accept方法,在沒有新連接的時候,該方法會阻塞,而之後的代碼就無法執行瞭。我們在客戶端與服務端連接成功之後進行消息收發的時候是沒有新連接產生的,此時的阻塞導致無法進行通訊,於是乎我們需要再開辟一個線程,進行消息處理。那麼我們定義一個繼承Thread的消息處理類,將每次連接成功返回的套接字接收,進行信息處理。如此一來,隻要有消息的傳遞該線程就可以進行獲取:

import com.issac.task_05.task.msg.SocketMessage;
import com.issac.task_05.task.msg.SocketMessageHandler;

import java.io.InputStream;
import java.net.Socket;
import java.util.Map;

/**
 * @ClassName ServerThread
 * @Description 處理信息
 * @Author issac
 * @Date 2021/4/21 21:25
 */
public class ServerThread extends Thread{
    private Socket socket;
    InputStream inputStream;
    Map<String, SocketMessageHandler> clientContainer;

    public ServerThread(Socket socket,Map<String, SocketMessageHandler> clientContainer){
        this.socket = socket;
        this.clientContainer = clientContainer;
    }

    public void run(){
        try{
            while (true){
                // 將輸入流中的數據解析為SocketMessage對象
                inputStream = socket.getInputStream();
                SocketMessage msg = SocketMessage.parseSocket(inputStream);
                System.out.println(msg);

                // 在容器中獲取目標地址
                SocketMessageHandler socketMessageHandler = clientContainer.get(msg.getTo());

                // 設置需要傳輸的信息
                socketMessageHandler.setSm(msg);

                // 傳輸信息
                socketMessageHandler.send();

            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

最後就是客戶端瞭,每個客戶端所對應的服務端都相同,在客戶端寫一個簡易的菜單,選擇接收或發送消息即可:

import com.issac.task_05.task.msg.SocketMessage;

import java.net.Socket;
import java.util.Scanner;

/**
 * @ClassName Client
 * @Description 客戶端
 * @Author issac
 * @Date 2021/4/19 21:08
 */
public class Client {

    public static void main(String[] args) {

        Scanner scanner = new Scanner(System.in);

        Socket s = null;

        try {
            s = new Socket("localhost", 8888);
            // 第一次啟動,創建socket,向服務器發送我是誰
            SocketMessage initMsg = getSocketMsg(s.getLocalSocketAddress().toString(), null, null);
            System.out.println("開始與服務器建立連接: " + initMsg.toString());
            SocketMessage.writeTargetMessage(initMsg, s);


            // 開始 循環等待
            while (true) {

                System.out.println("===================menu=====================");
                System.out.println("1:發送消息");
                System.out.println("2:接收消息");

                int choice = scanner.nextInt();

                switch (choice){
                    case 1: // 發送消息
                        String target = input("請輸入您要發給誰:");
                        String content = input("請輸入您要發送的內容:");
                        System.out.println();

                        SocketMessage afterMsg = getSocketMsg(s.getLocalSocketAddress().toString(), target, content);
                        SocketMessage.writeTargetMessage(afterMsg, s);
                        break;
                    case 2: // 接收並打印消息
                        showRequiredMsg(s);
                        break;
                    default:

                }
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    /**
     * 根據提示輸入內容
     **/
    public static String input(String tip) {
        Scanner input = new Scanner(System.in);
        System.out.println(tip);
        return input.next();
    }

    /**
     * 將用戶輸入傳遞的本地地址,目標地址與傳遞內容轉化為SocketMessage對象
     * @param localSocketAddress
     * @param to
     * @param content
     * @return
     */
    public static SocketMessage getSocketMsg(String localSocketAddress, String to, String content) {
        SocketMessage socketMessage = new SocketMessage();

        // to 為null的時候,說明隻是對服務器的初始
        socketMessage.setKey(localSocketAddress.replaceAll("\\/", ""));
        socketMessage.setTo(to);
        socketMessage.setContent(content);

        return socketMessage;
    }

    /**
     * 接收消息並打印
     * @param socket
     * @throws Exception
     */
    public static void showRequiredMsg(Socket socket) throws Exception {
        SocketMessage socketMessage = SocketMessage.parseSocket(socket.getInputStream());
        String source = socketMessage.getKey();
        String content = socketMessage.getContent();
        System.out.println("接收到來自《"+source+"》的信息:"+content+"\n");
    }
}

運行結果:

在這裡插入圖片描述
在這裡插入圖片描述

到此這篇關於Java實現多對多網絡通訊的流程的文章就介紹到這瞭,更多相關Java多對多網絡通訊內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: