一个小demo:Socket 长连接(双向心跳、超时重试)

服务端

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.*;

public class Server {

    /**
     * 客户端集合
     */
    private static List<Socket> clientList = new ArrayList<>();

    /**
     * 客户端心跳时间集合
     */
    private static Map<Socket, Date> heartbeatMap = new HashMap<>(16);

    /**
     * 心跳超时时间
     */
    private static final long TIMEOUT = 10 * 1000;

    /**
     * 服务端端口
     */
    private static final int PORT = 12345;

    /**
     * 以下为与客户端约定的指令,分别是:心跳、心跳回执、退出和退出回执
     */
    private static final String HEARTBEAT = "heartbeat";
    private static final String HEARTBEAT_RECEIPT = "heartbeat_receipt";
    private static final String EXIT = "exit";
    private static final String EXIT_RECEIPT = "exit_receipt";

    public static void main(String[] args) {
        new Server().start();
    }

    /**
     * 功能描述:
     * <服务端启动>
     *
     *
     * @return void
     * @author zhoulipu
     * @date   2019/8/8 15:52
     */
    private void start() {
        try {
            // 服务端开启
            ServerSocket server = new ServerSocket(PORT);
            System.out.println("服务端开启,等待客户端连接中...");
            // 循环监听客户端连接
            while (true) {
                // 等待客户端进行连接
                Socket client = server.accept();
                // 将客户端添加到集合
                clientList.add(client);
                System.out.println("有建立连接了,客户端地址:" + client.getRemoteSocketAddress().toString().replace("/", "") + ",当前连接数量:" + clientList.size());
                // 添加首次连接时间作为心跳
                heartbeatMap.put(client, new Date());
                // 开启新线程处理消息
                new MessageListener(client).start();
                // 开启新线程监测心跳
                new HeartbeatListener(client).start();
            }
        } catch (IOException e) {
            e.getStackTrace();
        }
    }

    /**
     * 消息处理
     */
    class MessageListener extends Thread {

        private Socket client;

        private MessageListener(Socket socket) {
            this.client = socket;
        }

        @Override
        public void run() {
            try {
                // 客户端连接后立即下发消息
                sendMsg(client, "连接已经建立,请开始消息传输");
                String msg;
                // 当心跳存在时,循环处理消息
                while (heartbeatMap.get(client) != null) {
                    // 读取客户端消息
                    msg = receiveMsg(client);
                    if (msg == null) {
                        // 表示连接已断开,等待心跳监听线程来处理
                        continue;
                    }
                    if (HEARTBEAT.equals(msg)) {
                        // 记录客户端的心跳时间
                        heartbeatMap.put(client, new Date());
                        // 发送回执消息
                        sendMsg(client, HEARTBEAT_RECEIPT);
                    } else if (EXIT.equals(msg)) {
                        // 客户端主动下线,删除连接和心跳
                        clientList.remove(client);
                        heartbeatMap.remove(client);
                        // 发送回执消息
                        sendMsg(client, EXIT_RECEIPT);
                        // 关闭连接
                        try {
                            client.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                        System.out.println("有断开连接了,客户端地址:" + client.getRemoteSocketAddress().toString().replace("/", "") + ",当前连接数量:" + clientList.size());
                    } else {
                        System.out.println("[" + client.getPort() + "]:" + msg);
                        sendMsg(client, "我已接收信息\"" + msg + "\"");
                    }
                }
            } catch (IOException e) {
                e.getStackTrace();
            }
        }
    }

    /**
     * 心跳监测
     */
    class HeartbeatListener extends Thread {

        private Socket client;

        private HeartbeatListener(Socket socket) {
            this.client = socket;
        }

        @Override
        public void run() {
            Date time, now;
            // 当心跳存在时,循环处理消息
            while ((time = heartbeatMap.get(client)) != null) {
                now = new Date();
                // 比对当前时间和最新心跳时间
                if (now.getTime() - time.getTime() > TIMEOUT) {
                    // 客户端心跳超时(这里当作断开连接处理),删除连接和心跳
                    heartbeatMap.remove(client);
                    clientList.remove(client);
                    // 关闭连接
                    try {
                        client.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    System.out.println("有心跳超时了,客户端地址:" + client.getRemoteSocketAddress().toString().replace("/", "") + ",当前连接数量:" + clientList.size());
                }
            }
        }
    }

    /**
     * 功能描述:
     * <发送消息>
     *
     * @param socket 1
     * @param msg    2
     * @return void
     * @author zhoulipu
     * @date 2019/8/8 15:03
     */
    private void sendMsg(Socket socket, String msg) throws IOException {
        OutputStream out = socket.getOutputStream();
        PrintWriter writer = new PrintWriter(out);
        // 使用pw.write(msg); msg末尾必须加"\n"转义, println自动添加转义
        writer.println(msg);
        writer.flush();
    }

    /**
     * 功能描述:
     * <接受消息>
     *
     * @param socket 1
     * @return java.lang.String
     * @author zhoulipu
     * @date 2019/8/8 15:45
     */
    private String receiveMsg(Socket socket) throws IOException {
        InputStream in = socket.getInputStream();
        BufferedReader reader = new BufferedReader(new InputStreamReader(in));
        return reader.readLine();
    }

}

客户端

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.*;

public class Client {

    /**
     * 服务端地址
     */
    private static final String HOST = "127.0.0.1";

    /**
     * 服务端端口
     */
    private static final int PORT = 12345;

    /**
     * 心跳间隔时间(毫秒)
     */
    private static final long HEARTBEAT_INTERVAL = 2 * 1000;

    /**
     * 心跳超时时间(毫秒)
     */
    private static final long HEARTBEAT_TIMEOUT = 10 * 1000;

    /**
     * 以下为与客户端约定的指令,分别是:心跳、心跳回执和退出回执,退出由客户端触发
     */
    private static final String HEARTBEAT = "heartbeat";
    private static final String HEARTBEAT_RECEIPT = "heartbeat_receipt";
    private static final String EXIT_RECEIPT = "exit_receipt";

    /**
     * 连接通道,因为重连需要新建连接,所以存一个Map,保证新连接可以被共用
     */
    private static Map<String, Socket> socketMap = new HashMap<>(16);

    /**
     * 连接状态,因为重连需要新建连接,所以存一个Map,缓存共用
     */
    private static Map<String, Boolean> connectionStatusMap = new HashMap<>(16);

    /**
     * 心跳回执,因为重连需要新建连接,所以存一个Map,缓存共用
     */
    private static Map<String, Date> heartbeatReceiptTimeMap = new HashMap<>(16);

    /**
     * 连接重试,因为重连需要新建连接,所以存一个Map,缓存共用,null:正常连接,true:需要重新连接,false:不用重新连接
     */
    private static Map<String, Boolean> connectionRetryStatusMap = new HashMap<>(16);

    /**
     * 重试次数,因为重连需要新建连接,所以存一个Map,缓存共用
     */
    private static Map<String, Integer> connectionRetryCountMap = new HashMap<>();

    /**
     * 重试间隔
     */
    private static final long CONNECTION_RETRY_INTERVAL = 5 * 1000;


    public static void main(String[] args) {
        // 生成唯一key,保证连接重试后缓存共用
        String threadKey = UUID.randomUUID().toString();
        new Client().start(threadKey);
    }

    /**
     * 功能描述:
     * <客户端启动>
     *
     * @param  threadKey 1
     * @return void
     * @author zhoulipu
     * @date   2019/8/8 15:51
     */
    private void start(String threadKey) {
        try {
            // 客户端开启,建立连接
            Socket socket = new Socket(HOST, PORT);
            // 当连接建立成功,立即缓存(连接失败会抛异常,不会缓存)
            socketMap.put(threadKey, socket);
            // 添加首次连接时间作为心跳回执
            heartbeatReceiptTimeMap.put(threadKey, new Date());
            System.out.println("连接服务器成功,本机地址:" + socket.getLocalSocketAddress().toString().replace("/", ""));
            // 更新连接状态
            connectionStatusMap.put(threadKey, true);
            // 重置重试状态
            connectionRetryStatusMap.put(threadKey, null);
            // 重置重试次数
            connectionRetryCountMap.put(threadKey, 0);
            // 开启新线程处理消息
            new MessageListener(threadKey).start();
        } catch (IOException e) {
            e.getStackTrace();
        }
        // 开启新线程监听心跳
        new HeartbeatListener(threadKey).start();
        // 开启新线程连接重试
        new ConnectionRetryListener(threadKey).start();
    }

    /**
     * 消息处理
     */
    class MessageListener extends Thread {

        private String threadKey;

        private MessageListener(String threadKey) {
            this.threadKey = threadKey;
        }

        @Override
        public void run() {
            try {
                String msg;
                // 当连接正常时,循环处理消息
                while (connectionStatusMap.get(threadKey)) {
                    msg = receiveMsg(threadKey);
                    if (msg == null) {
                        // 表示连接已断开,等待心跳监听线程来处理
                        continue;
                    }
                    if (HEARTBEAT_RECEIPT.equals(msg)) {
                        // 收到心跳回执,记录时间
                        heartbeatReceiptTimeMap.put(threadKey, new Date());
                    } else if (EXIT_RECEIPT.equals(msg)) {
                        // 收到退出回执
                        // 设置重试状态,防止重新连接
                        connectionRetryStatusMap.put(threadKey, false);
                        System.out.println("[服务端]:我已收到关闭指令,连接已关闭");
                        // 关闭连接
                        try {
                            socketMap.get(threadKey).close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                        // 关闭程序 System.exit(0)可以关闭调用此线程的其他方法,相当于连根拔除
                        System.exit(0);
                    } else {
                        System.out.println("[服务端]:" + msg);
                        // 开启消息处理线程,防止控制台无输入时导致心跳回执时间无法更新
                        new TaskHandler (threadKey).start();
                    }
                }
            } catch (IOException e) {
                e.getStackTrace();
            }
        }
    }

    /**
     * 消息处理
     */
    class TaskHandler extends Thread {

        private String threadKey;

        private TaskHandler (String threadKey) {
            this.threadKey = threadKey;
        }

        // 消息正常传输
        @Override
        public void run() {
            try {
                System.out.print("[客户端]:");
                // 这里有个问题,因为客户端从控制台获取信息,当一次连接失败时,如果没控制台信息输入,这个线程卡住。等重新连接后,再在控制台输入,会丢失一次消息。正常业务处理不会出现这种情况
                sendMsg(threadKey, new Scanner(System.in).nextLine());
            } catch (IOException e) {
                e.getStackTrace();
            }
        }

    }

    /**
     * 心跳监测
     */
    class HeartbeatListener extends Thread {

        private String threadKey;

        private HeartbeatListener(String threadKey) { this.threadKey = threadKey; }

        @Override
        public void run() {
            try {
                Date time, now;
                // 当心跳存在时,循环处理消息
                while ((time = heartbeatReceiptTimeMap.get(threadKey)) != null) {
                    now = new Date();
                    // 比对当前时间和最新心跳回执
                    if (now.getTime() - time.getTime() > HEARTBEAT_TIMEOUT) {
                        // 服务端心跳超时,更改连接状态和重试状态,关闭线程,等待重试
                        connectionStatusMap.put(threadKey, false);
                        connectionRetryStatusMap.put(threadKey, true);
                        // 关闭连接
                        try {
                            socketMap.get(threadKey).close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                        return;
                    }
                    // 发送心跳
                    sendMsg(threadKey, HEARTBEAT);
                    // 心跳间隔
                    try {
                        Thread.sleep(HEARTBEAT_INTERVAL);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            } catch (IOException e) {
                e.getStackTrace();
            }
        }
    }

    /**
     * 连接重试
     */
    class ConnectionRetryListener extends Thread {

        private String threadKey;

        private ConnectionRetryListener(String threadKey) { this.threadKey = threadKey; }

        @Override
        public void run() {
            // 连接正常或需要重连时循环监听
            while (connectionRetryStatusMap.get(threadKey) == null || connectionRetryStatusMap.get(threadKey)) {
                // 需要重连
                if (connectionRetryStatusMap.get(threadKey) != null) {
                    // 更新重连次数
                    connectionRetryCountMap.put(threadKey, connectionRetryCountMap.getOrDefault(threadKey, 0) + 1);
                    // 这个没什么用,我只是为了控制台打印好看
                    if (connectionRetryCountMap.get(threadKey) == 1) {
                        System.out.println();
                    }
                    System.out.println("连接已断开,正在尝试第" + connectionRetryCountMap.get(threadKey) + "次连接...");
                    // 重试间隔
                    try {
                        Thread.sleep(CONNECTION_RETRY_INTERVAL);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // 连接重试
                    new Client().start(threadKey);
                    return;
                }
            }
        }
    }

    /**
     * 功能描述:
     * <发送消息>
     *
     * @param threadKey 1
     * @param msg       2
     * @return void
     * @author zhoulipu
     * @date 2019/8/8 15:45
     */
    private void sendMsg(String threadKey, String msg) throws IOException {
        OutputStream out = socketMap.get(threadKey).getOutputStream();
        PrintWriter writer = new PrintWriter(out);
        // 使用pw.write(msg); msg末尾必须加"\n"转义, println自动添加转义
        writer.println(msg);
        writer.flush();
    }

    /**
     * 功能描述:
     * <接受消息>
     *
     * @param threadKey 1
     * @return java.lang.String
     * @author zhoulipu
     * @date 2019/8/8 15:45
     */
    private String receiveMsg(String threadKey) throws IOException {
        InputStream in = socketMap.get(threadKey).getInputStream();
        BufferedReader reader = new BufferedReader(new InputStreamReader(in));
        return reader.readLine();
    }

}
  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,下面是一个简单的 Python 长连接的示例代码: ```python import socket HOST = '127.0.0.1' # 服务器 IP 地址 PORT = 8888 # 服务器端口号 # 创建 socket 对象 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 设置 socket 地址,绑定 IP 和端口号 s.bind((HOST, PORT)) # 监听客户端连接请求 s.listen(1) print('等待客户端连接...') while True: # 接受客户端连接请求,返回一个客户端 socket 对象和客户端地址 conn, addr = s.accept() print('已连接客户端:', addr) while True: # 从客户端接收数据 data = conn.recv(1024) if not data: break print('收到客户端消息:', data.decode()) # 向客户端发送数据 conn.sendall('已收到消息'.encode()) # 关闭客户端连接 conn.close() print('客户端已断开连接') ``` 这个示例代码中,我们创建了一个 socket 对象,并将其绑定到了指定的 IP 和端口号上。然后通过 `listen()` 方法开始监听客户端连接请求。当有客户端连接时,我们通过 `accept()` 方法接收客户端连接请求,并返回客户端 socket 对象和客户端地址。在与客户端建立的连接中,我们可以通过客户端 socket 对象的 `recv()` 方法接收来自客户端的数据,也可以通过 `sendall()` 方法向客户端发送数据。当客户端关闭连接时,我们通过 `close()` 方法关闭与客户端的连接。 以上是一个简单的 Python 长连接的示例代码,你可以根据自己的实际需求进行修改和扩展。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值