基于TCP协议的Socket通信(2)

在《基于 TCP 协议的 Socket 通信(1)》中,我们介绍了 TCP Socket 通信的基础,包括单客户端与服务器的简单通信实现。本篇作为系列的第二部分,将聚焦于 基于 TCP 协议的 Socket 通信的多客户端支持,即如何让服务器处理多个客户端的并发连接,同时在 Android 客户端实现持续通信(如实时聊天)。本文将详细讲解多客户端通信的原理、实现步骤、Android 环境配置、代码示例、常见问题及注意事项,结合中文讲解,适合希望深入理解 Socket 并发处理的开发者。


一、多客户端 Socket 通信原理

1. 多客户端通信需求

  • 目标:服务器能够同时处理多个客户端的连接,支持实时通信(如聊天室)。
  • 场景
  • 多个 Android 设备连接到服务器,发送和接收消息。
  • 服务器广播消息或处理特定客户端请求。
  • 挑战
  • 服务器需要并发处理多个 Socket 连接。
  • 客户端需要保持长连接,实时接收消息。
  • 确保消息正确分发和资源管理。

2. 技术方案

  • 服务器端
  • 使用 ServerSocket 监听端口,接受多个客户端连接。
  • 为每个客户端创建一个线程(或使用线程池)处理通信。
  • 维护客户端列表,用于广播或定向发送消息。
  • 客户端端(Android):
  • 使用 Socket 建立长连接。
  • 异步处理消息发送和接收(使用线程或 Kotlin 协程)。
  • 处理断线重连和错误。
  • 通信协议
  • 定义消息格式(如 JSON)区分客户端和消息类型。
  • 示例:{"clientId":"user1","type":"message","content":"Hello"}

3. 并发处理方式

  • 多线程:为每个客户端分配一个线程,简单但资源消耗大。
  • 线程池:使用 ExecutorService 管理线程,适合中等规模连接。
  • NIO/异步 IO(高级):使用 Java NIO 或 Netty,适合高并发场景(本文暂不深入,留给后续系列)。
  • Android 推荐:客户端使用 Kotlin 协程简化异步处理,服务器使用线程池。

二、Android 环境配置

1. 权限声明

AndroidManifest.xml 中添加权限:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

2. 检查网络状态

确保网络可用:

import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;

public boolean isNetworkAvailable(Context context) {
    ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
    NetworkInfo networkInfo = cm.getActiveNetworkInfo();
    return networkInfo != null && networkInfo.isConnected();
}

3. 异步处理

  • 原因:Android 主线程禁止网络操作,需异步处理。
  • 推荐方式
  • Kotlin 协程:现代化异步处理,简洁高效。
  • Java 线程/ExecutorService:传统方式,适合简单场景。
  • 依赖(Kotlin 协程):
  implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'

三、多客户端 Socket 通信实现

以下是一个完整的多客户端 TCP Socket 通信示例:

  • 服务器:支持多个客户端连接,广播消息。
  • Android 客户端:保持长连接,发送和接收消息,显示实时聊天内容。

1. 服务器代码(Java 示例)

服务器监听端口,接受多个客户端连接,使用线程池处理并发,并广播消息。

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ChatServer {
    private static final int PORT = 12345;
    private static final List<ClientHandler> clients = new ArrayList<>();
    private static final ExecutorService executor = Executors.newFixedThreadPool(10); // 线程池

    public static void main(String[] args) {
        try (ServerSocket serverSocket = new ServerSocket(PORT)) {
            System.out.println("服务器启动,监听端口: " + PORT);
            while (true) {
                Socket clientSocket = serverSocket.accept();
                System.out.println("新客户端连接: " + clientSocket.getInetAddress());
                ClientHandler clientHandler = new ClientHandler(clientSocket);
                clients.add(clientHandler);
                executor.execute(clientHandler); // 分配线程处理客户端
            }
        } catch (IOException e) {
            System.err.println("服务器错误: " + e.getMessage());
        } finally {
            executor.shutdown();
        }
    }

    // 广播消息给所有客户端
    private static void broadcast(String message, ClientHandler sender) {
        synchronized (clients) {
            for (ClientHandler client : clients) {
                if (client != sender) {
                    client.sendMessage(message);
                }
            }
        }
    }

    // 客户端处理类
    private static class ClientHandler implements Runnable {
        private final Socket socket;
        private PrintWriter out;
        private BufferedReader in;
        private String clientId;

        public ClientHandler(Socket socket) {
            this.socket = socket;
        }

        @Override
        public void run() {
            try {
                // 初始化输入输出流
                out = new PrintWriter(socket.getOutputStream(), true);
                in = new BufferedReader(new InputStreamReader(socket.getInputStream()));

                // 获取客户端 ID
                clientId = in.readLine();
                System.out.println(clientId + " 已连接");
                broadcast(clientId + " 加入聊天室", this);

                // 循环读取客户端消息
                String message;
                while ((message = in.readLine()) != null) {
                    System.out.println(clientId + ": " + message);
                    broadcast(clientId + ": " + message, this);
                }
            } catch (IOException e) {
                System.err.println(clientId + " 错误: " + e.getMessage());
            } finally {
                disconnect();
            }
        }

        public void sendMessage(String message) {
            out.println(message);
        }

        private void disconnect() {
            try {
                broadcast(clientId + " 离开聊天室", this);
                synchronized (clients) {
                    clients.remove(this);
                }
                if (in != null) in.close();
                if (out != null) out.close();
                if (socket != null) socket.close();
            } catch (IOException e) {
                System.err.println("关闭连接错误: " + e.getMessage());
            }
        }
    }
}
  • 说明
  • 使用 ExecutorService 管理线程池,处理多客户端并发。
  • 维护 clients 列表,记录所有连接的客户端。
  • ClientHandler 处理单个客户端的通信,广播消息给其他客户端。
  • 客户端首次连接发送 ID(如用户名),用于标识消息来源。

2. Android 客户端代码(Kotlin 协程)

Android 客户端实现长连接,发送和接收消息,显示实时聊天内容。

import android.os.Bundle
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.io.BufferedReader
import java.io.InputStreamReader
import java.io.PrintWriter
import java.net.Socket

class MainActivity : AppCompatActivity() {
    private val SERVER_IP = "192.168.1.100" // 替换为实际服务器 IP
    private val SERVER_PORT = 12345
    private lateinit var socket: Socket
    private lateinit var out: PrintWriter
    private lateinit var inReader: BufferedReader
    private lateinit var messageText: TextView
    private lateinit var inputEditText: EditText
    private var isConnected = false

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        messageText = findViewById(R.id.messageText)
        inputEditText = findViewById(R.id.inputEditText)
        val sendButton = findViewById<Button>(R.id.sendButton)

        // 连接服务器
        GlobalScope.launch(Dispatchers.IO) {
            connectToServer("用户" + (1000..9999).random()) // 随机生成用户 ID
        }

        // 发送消息
        sendButton.setOnClickListener {
            val message = inputEditText.text.toString().trim()
            if (message.isNotEmpty() && isConnected) {
                GlobalScope.launch(Dispatchers.IO) {
                    sendMessage(message)
                    runOnUiThread { inputEditText.text.clear() }
                }
            } else {
                Toast.makeText(this, "未连接或消息为空", Toast.LENGTH_SHORT).show()
            }
        }
    }

    private suspend fun connectToServer(clientId: String) {
        try {
            socket = Socket(SERVER_IP, SERVER_PORT)
            out = PrintWriter(socket.getOutputStream(), true)
            inReader = BufferedReader(InputStreamReader(socket.getInputStream()))
            isConnected = true

            // 发送客户端 ID
            out.println(clientId)

            // 循环接收消息
            while (isConnected) {
                val message = inReader.readLine() ?: break
                runOnUiThread {
                    messageText.append("$message\n")
                }
            }
        } catch (e: Exception) {
            runOnUiThread {
                Toast.makeText(this@MainActivity, "连接错误: ${e.message}", Toast.LENGTH_SHORT).show()
            }
            isConnected = false
        }
    }

    private suspend fun sendMessage(message: String) {
        try {
            out.println(message)
        } catch (e: Exception) {
            runOnUiThread {
                Toast.makeText(this@MainActivity, "发送失败: ${e.message}", Toast.LENGTH_SHORT).show()
            }
            isConnected = false
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        if (::socket.isInitialized && !socket.isClosed) {
            try {
                isConnected = false
                inReader.close()
                out.close()
                socket.close()
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }
    }
}
  • 布局文件res/layout/activity_main.xml):
  <LinearLayout
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      android:orientation="vertical"
      android:padding="16dp">
      <TextView
          android:id="@+id/messageText"
          android:layout_width="match_parent"
          android:layout_height="0dp"
          android:layout_weight="1"
          android:background="#F5F5F5"
          android:padding="8dp"
          android:scrollbars="vertical" />
      <EditText
          android:id="@+id/inputEditText"
          android:layout_width="match_parent"
          android:layout_height="wrap_content"
          android:hint="输入消息" />
      <Button
          android:id="@+id/sendButton"
          android:layout_width="match_parent"
          android:layout_height="wrap_content"
          android:text="发送" />
  </LinearLayout>
  • 说明
  • 客户端使用 Kotlin 协程管理异步连接和消息接收。
  • 保持长连接,实时接收服务器消息并显示在 TextView
  • 用户输入消息,发送到服务器,服务器广播给其他客户端。
  • 随机生成用户 ID,避免手动输入。

3. 运行效果

  • 启动服务器(运行 ChatServer)。
  • 多个 Android 设备运行客户端,连接服务器。
  • 客户端发送消息(如 “Hello”),服务器广播到其他客户端,显示在 TextView 中。
  • 客户端连接/断开时,服务器广播通知。

四、常见问题及注意事项

  1. 网络权限
  • 确保 INTERNETACCESS_NETWORK_STATE 权限已声明。
  • Android 9+ 默认禁用 HTTP,推荐使用 TLS 加密 Socket(后续系列讲解)。
  1. 异步处理
  • 使用 Kotlin 协程(Dispatchers.IO)或线程池处理网络操作。
  • 避免主线程操作 Socket:
    java // Java 线程池 ExecutorService executor = Executors.newFixedThreadPool(4); executor.execute(() -> connectToServer("Message"));
  1. 连接超时
  • 设置 Socket 超时:
    java socket.setSoTimeout(10000); // 10秒超时
  1. 错误处理
  • 处理 IOExceptionUnknownHostException 等:
    java try { Socket socket = new Socket("example.com", 12345); } catch (UnknownHostException e) { Log.e("Socket", "未知主机: " + e.getMessage()); } catch (IOException e) { Log.e("Socket", "IO 错误: " + e.getMessage()); }
  • 实现断线重连:
    kotlin private suspend fun reconnect() { while (!isConnected) { try { connectToServer("用户" + (1000..9999).random()) break } catch (e: Exception) { delay(5000) // 5秒后重试 } } }
  1. 资源管理
  • 确保 Socket 和流正确关闭:
    java socket.close(); out.close(); in.close();
  • 在 Activity 销毁时清理资源(见 onDestroy)。
  1. Android 4.4+ 注意
  • Chromium 引擎:支持 WebSocket,可作为 TCP Socket 的替代(后续系列讲解)。
  • 网络安全:推荐使用 SSLSocket 加密通信:
    java import javax.net.ssl.SSLSocketFactory; SSLSocketFactory factory = (SSLSocketFactory) SSLSocketFactory.getDefault(); Socket socket = factory.createSocket("example.com", 443);
  • WebView 集成:可通过 JavaScript 调用 Socket(参考前文)。
  1. 性能优化
  • 使用线程池或 NIO 减少线程开销。
  • 使用缓冲流(如 BufferedReader):
    java BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
  • 避免频繁创建 Socket,保持长连接。
  1. 通信协议
  • 定义明确的 JSON 格式:
    json {"clientId":"user1","type":"message","content":"Hello"}
  • 使用分隔符(如换行符 \n)分割消息。

五、学习建议与实践

  1. 学习路径
  • 掌握多客户端并发处理(线程池、NIO)。
  • 学习 JSON 或自定义协议解析消息。
  • 实现断线重连和错误处理。
  • 探索 WebSocket 作为 TCP Socket 的替代。
  • 学习 TLS/SSL 加密(后续系列)。
  1. 实践项目
  • 简单项目:实现多客户端聊天室,广播消息。
  • 进阶项目:支持私聊功能(定向发送消息)。
  • 高级项目:结合 WebView,实现 H5 聊天界面(后续系列)。
  1. 调试工具
  • Wireshark:抓包分析 TCP 数据。
  • Netcat (nc):测试服务器(如 nc -l 12345)。
  • Logcat:记录 Socket 日志。
  • Telnet:手动测试服务器响应。
  1. 推荐资源
  • Java Socket 文档:https://docs.oracle.com/javase/8/docs/api/java/net/ServerSocket.html
  • Android 网络编程:https://developer.android.com/training/basics/network-ops/connecting
  • TCP 协议:https://tools.ietf.org/html/rfc793
  • 测试服务器:本地 nc 或在线服务。

六、总结

  • 多客户端通信
  • 服务器使用线程池处理并发连接,维护客户端列表,广播消息。
  • 客户端保持长连接,异步发送/接收消息。
  • Android 配置
  • 声明网络权限,检查网络状态。
  • 使用 Kotlin 协程简化异步处理。
  • 代码示例
  • 服务器支持多客户端,广播消息。
  • Android 客户端实现实时聊天界面。
  • 注意事项
  • 异步处理、错误处理、资源管理。
  • Android 4.4+ 支持 WebSocket 和 TLS。
  • 定义明确通信协议(如 JSON)。
  • 推荐
  • 使用线程池或协程优化并发。
  • 生产环境考虑 WebSocket 或 Netty。

七、后续系列预告

本篇为基于 TCP 协议的 Socket 通信系列(2),聚焦多客户端支持。后续系列将深入探讨:

  • 系列(3):结合 WebView 实现 H5 与 Socket 的混合通信。
  • 系列(4):使用 TLS/SSL 加密 Socket 通信。
  • 系列(5):高级应用(如聊天室、文件传输)。

如果需要更详细的代码示例(如 JSON 协议解析、私聊功能、断线重连)或特定场景的讲解,请告诉我!

类似文章

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注