Java网络编程(二)

文章目录

一、从两个人通信到多人通信

上一篇已经实现了两个程序之间通过 Socket 发送和接收消息,下来继续往多人通信扩展。

如果现在有 A、B、C 三个客户端都需要聊天,最直接的想法是让它们两两建立连接。但这样客户端一多,连接数量也会跟着增加,程序会越来越难处理。

所以这次改成了比较常见的结构:所有客户端都连接到同一个 Server,由 Server 统一负责接收和转发消息。大致结构如下:

text 复制代码
        Client A
           |
Client B--Server--Client C

A 发出的消息先到 Server,Server 收到以后再转发给 B、C。这样客户端只需要和服务器保持一条连接,不需要自己管理其他客户端。


二、服务端的基本结构

写一个 Server 类,把服务端需要的功能分别封装成方法。

首先是初始化:

java 复制代码
ServerSocket ss = null;

public void init() {
    try {
        ss = new ServerSocket(54321);
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

服务端监听 54321 端口,然后单独写了一个 listen() 方法:

java 复制代码
public Socket listen() {
    try {
        return ss.accept();
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

每有一个客户端连接,accept() 就会返回一个对应的 Socket

因为现在不再只有一个客户端,所以服务端还需要把这些 Socket 保存起来:

java 复制代码
ArrayList<Socket> SocketList = new ArrayList<>();

客户端连接成功之后:

java 复制代码
Socket s = server.listen();
server.SocketList.add(s);

这样服务端就能够知道当前有哪些客户端在线,后面转发消息时也可以直接遍历这个列表。


三、阻塞问题

一开始处理多客户端消息时,思路是把所有 Socket 放进列表,然后循环读取:

java 复制代码
for (...) {
    InputStream is = socket.getInputStream();
    receive(is);
}

看起来只要不断循环,就能够把每个客户端的消息都读取出来。

实际测试以后才发现不是这样,read() 本身是阻塞的。如果当前循环先读客户端 1,而客户端 1 没有发消息,程序就会一直停在这里。即使客户端 2、客户端 3 已经发了消息,也暂时读取不到。

text 复制代码
客户端1
↓
客户端2
↓
客户端3
↓
客户端4

基本要按照连接顺序一个个发送,前面的人不发,后面的人就会被卡住。

所以问题并不在有没有写 while(true),而在于所有客户端共用同一个读取流程。只要前面的 read() 阻塞,后面的 Socket 就没有机会继续执行。


四、一个客户端对应一个线程

解决方法就是把每个客户端的消息监听拆开。每有一个客户端连接进来,就给这个 Socket 创建一个独立线程:

java 复制代码
public void proClintMsg(Socket socket) {

    new Thread(() -> {

        while (true) {
            try {
                InputStream is =
                        socket.getInputStream();

                String msg =
                        receive(is);

                replyMsg(socket, msg);

            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }

    }).start();
}

连接成功以后直接调用:

java 复制代码
server.proClintMsg(s);

这样 A、B、C 三个客户端就分别有自己的监听线程。

text 复制代码
Socket A → Thread A
Socket B → Thread B
Socket C → Thread C

A 的 read() 即使正在等待,也不会影响 B 和 C。

重新测试以后,就不再要求客户端严格按照连接顺序发送消息了,任何一个客户端都可以随时向服务器发消息。


五、消息的发送和接收

服务端的发送方法:

java 复制代码
public void send(String message, OutputStream os) {
    try {
        byte[] msgs = message.getBytes();

        int len = msgs.length;

        os.write(len);
        os.write(msgs);
        os.flush();

    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

这里先将字符串转换成字节数组:

java 复制代码
byte[] msgs = message.getBytes();

然后先发送:

java 复制代码
os.write(len);

再发送真正的消息:

java 复制代码
os.write(msgs);

接收端按照相同的格式读取:

java 复制代码
public String receive(InputStream is) {
    try {
        int len = is.read();

        byte[] msgs = new byte[len];

        is.read(msgs);

        String msg = new String(msgs);

        System.out.println("收到:" + msg);

        return msg;

    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

也就是:

text 复制代码
先读取消息长度
        ↓
根据长度创建 byte[]
        ↓
读取消息正文
        ↓
byte[] 转成 String

发送和接收两边必须使用相同的数据格式,否则消息就无法正常解析。


六、服务端实现消息转发

服务端能够同时收到多个客户端的消息以后,下一步就是把收到的消息转发出去。

这里写了一个 replyMsg() 方法:

java 复制代码
public void replyMsg(Socket socket, String msg) {

    for (int i = 0;
         i < SocketList.size();
         i++) {

        Socket s =
                SocketList.get(i);

        if (s != socket) {

            try {
                send(
                    msg,
                    s.getOutputStream()
                );

            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

其中传进来的 socket 就是当前发送消息的客户端。

服务端遍历所有已经连接的 Socket:

java 复制代码
SocketList

如果发现:

java 复制代码
s != socket

说明这个 Socket 不是消息发送者,就把消息转发给它。比如 A 发送:

text 复制代码
hello

整个过程就是:

text 复制代码
A
↓
Server
↓
B
↓
C

A 自己不会再收到一遍自己发送的消息。


七、客户端同时负责发送和接收

服务端虽然已经能够转发消息,但一开始客户端只有发送功能,所以 Server 把消息发回来以后,客户端并没有代码去读取。

因此客户端也需要增加接收消息的方法:

java 复制代码
public String readMsg() {

    try {
        InputStream is =
                socket.getInputStream();

        int len = is.read();

        byte[] msgs =
                new byte[len];

        is.read(msgs);

        return new String(msgs);

    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

但是发送消息本身已经有一个:

java 复制代码
while (true)

如果接收消息再直接写一个死循环,两个循环不能顺序执行,所以这里也使用线程处理。

java 复制代码
new Thread(() -> {

    while (true) {

        String msg =
                client.readMsg();

        System.out.print("接收消息:");

        System.out.println(msg);

        System.out.println("请输入:");
    }

}).start();

主线程仍然负责键盘输入和发送:

java 复制代码
Scanner sc = new Scanner(System.in);

while (true) {

    System.out.println("请输入:");

    String message =
            sc.nextLine();

    client.send(message);
}

于是客户端现在实际有两部分同时运行:

text 复制代码
主线程
↓
输入消息
↓
发送


子线程
↓
等待服务端消息
↓
接收并显示

服务端主要负责三件事情:

text 复制代码
1. 监听客户端连接
2. 为每个客户端创建线程并接收消息
3. 将收到的消息转发给其他客户端

客户端则主要负责:

text 复制代码
1. 连接服务器
2. 输入并发送消息
3. 单独启动线程接收其他客户端消息

整体结构大概是:

text 复制代码
             Server
               |
      ---------------------
      |         |         |
   Client1   Client2   Client3
      |         |         |
   Thread1   Thread2   Thread3

服务端通过 SocketList 保存客户端连接,每个 Socket 有自己的消息处理线程,客户端自身又将发送和接收拆开。

相关推荐
Flittly1 小时前
【雕虫大技】Agent 动态 Skill 供应链安全加固(三):输出校验与治理闭环实战
java·spring boot·spring
Poo_Chai1 小时前
QT emit信号后完整处理流程,包括槽函数响应流程
java·开发语言·数据库
瑞码空间1 小时前
Java 图形界面(GUI)完整知识点手册
java·开发语言·图形界面·swing
树码小子1 小时前
开启 IDEA 中的断言功能
android·java·intellij-idea
茶本无香2 小时前
Java调用Shell脚本执行SQL数据库操作:从入门到实战
java·sql·shell
风流 少年2 小时前
Spring AI 2.0:MCP
java·后端·spring
小绫网络安全2 小时前
Kali Linux渗透测试入门教程:从零搭建到实战攻防
linux·运维·服务器
天疆说2 小时前
01 硬件选型与 llama.cpp 部署:4× RTX 5880 Ada 跑 DeepSeek-V4-Flash
java·redis·llama