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 有自己的消息处理线程,客户端自身又将发送和接收拆开。

相关推荐
mldong5 小时前
引擎从不发一条消息:jeeflow 的两类扩展点与消息模块的分工
java·架构
君顾16 小时前
智慧零售实战指南:从技术架构到落地方案全解析
java·开发语言·零售
4A广告文案6 小时前
名创优品在成都开了家“乐园”,顺便造了个新IP
网络·网络协议·tcp/ip
zhanghe6876 小时前
es的密码带有特殊字符,导出
java
pnoker7 小时前
IoT DC3 概念解读:把设备抽象成一套语义模板——位号、指令、事件与面向智能体的设备建模
java·人工智能·物联网·iot·dc3
my_realmy7 小时前
Java SE 基础学习笔记(一):面向对象、继承多态、抽象接口与异常(JDK 8)
java·多线程·并发··生产者消费者模式
Fnetlink17 小时前
Fnet 云网安 260904
网络·人工智能·安全·web安全·网络安全
乌恩大侠7 小时前
GH200 新增 NVMe SSD 分区、格式化与挂载完整流程
运维·服务器·前端
天远数科7 小时前
风险治理实战:基于天远车信盟出险构建自动化理赔核保流水线
java·网络·人工智能·自动化
Mr.朱鹏8 小时前
Linux 服务器 LVM 根分区在线动态扩容
linux·服务器·数据库