目录

1基本的通信架构

1.1CS 架构(Client-Server)

1.2BS 架构(Browser-Server)

2网络通信三要素

2.1 IP

2.2 端口

2.3 协议

协议定义

UDP 协议

TCP 协议

3 实际开发

3.1 UDP 通信

3.1.1服务端收消息测试

3.1.2 多客户端多发多收

3.2 TCP通信

3.2.1 TCP 通信 - 客户端开发

3.3.2TCP 通信 - 服务端程序的开发

3.3.3 TCP1v1通信效果

3.3.4 TCP通信的多发多收

3.3.5 支持多个客户端同时与服务端通信

​编辑

4 TCP通信-综合案例

4.1 即时通信-群聊

4.2 BS架构

5 拓展——线程池优化BS架构


1基本的通信架构

基本的通信架构主要分为 2 种形式

  • CS 架构:Client(客户端)/ Server(服务端)
  • BS 架构:Browser(浏览器)/ Server(服务端)

1.1CS 架构(Client-Server)

1. 定义

客户端(Client)是独立安装的应用程序,直接与服务端(Server)进行通信,由服务端处理核心业务与数据。

2. 典型示例

  • 微信(移动端 / 桌面客户端)
  • IntelliJ IDEA(开发工具客户端)
  • 视频剪辑软件(客户端)

3. 特点

  • ✅ 性能更优:客户端可承担部分计算,减轻服务端压力
  • ✅ 功能丰富:支持复杂交互、离线使用
  • ❌ 维护成本高:更新需推送至所有客户端,不同系统适配复杂
  • 客户端(Client)
    • 需程序员开发实现
    • 用户需单独安装客户端程序
  • 服务端(Server)
    • 需程序员开发实现
  • 通信:客户端与服务端双向数据交互

1.2BS 架构(Browser-Server)

1. 定义

通过浏览器(Browser)作为客户端,无需额外安装,直接访问服务端部署的网页应用。

2. 典型示例

  • 各类网站(如淘宝、知乎)
  • 网页版办公系统

3. 特点

  • ✅ 跨平台:只需浏览器,无需考虑操作系统差异
  • ✅ 维护便捷:更新仅需在服务端完成
  • ❌ 性能受限:依赖浏览器与网络,复杂功能实现难度大
  • 浏览器端(Browser)
    • 无需程序员开发(使用现成浏览器,如 Chrome、Edge)
    • 用户需安装浏览器
  • 服务端(Server)
    • 需程序员开发实现
  • 通信:浏览器与服务端双向数据交互

2网络通信三要素

2.1 IP

IP地址

设备在网络中的地址,是唯一的标识。

IP域名

  • 用户输入域名(如 www.google.com)。
  • 操作系统向 DNS 服务器发送查询请求。
  • DNS 服务器返回域名对应的 IP 地址。
  • 设备使用该 IP 地址与目标服务器建立连接。

公网 IP, 内网 IP

  • 公网 IP:是可以连接互联网的 IP 地址;内网 IP:也叫局域网 IP,只能组织机构内部使用。
  • 192.168. 开头的就是常见的局域网地址,范围即为 192.168.0.0--192.168.255.255,专门为组织机构内部使用。

特殊 IP 地址

  • 127.0.0.1、localhost:代表本机 IP,只会寻找当前所在的主机。

IP 常用命令

  • ipconfig:查看本机 IP 地址。
  • ping IP 地址:检查网络是否连通。

InetAddress 相关知识(文字提取)


核心定义

  • InetAddress:代表 IP 地址

InetAddress 的常用方法

名称 说明
public static InetAddress getLocalHost() 获取本机 IP,会以一个 InetAddress 的对象返回
public static InetAddress getByName(String host) 根据 IP 地址或者域名,返回一个 InetAddress 对象
public String getHostName() 获取该 IP 地址对象对应的主机名
public String getHostAddress() 获取该 IP 地址对象中的 IP 地址信息
public boolean isReachable(int timeout)

在指定毫秒内,判断主机与该 IP 对应的主机是否能连

package com.yw;

import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;

public class InetAddressTest {
    public static void main(String[] args) throws IOException {

        //1.获取本机IP地址对象
        InetAddress ip1 = InetAddress.getLocalHost();
        System.out.println(ip1.getHostName());
        System.out.println(ip1.getHostAddress());

        //2.获取指定IP或者域名的IP地址对象
        InetAddress ip2 = InetAddress.getByName("www.baidu.com");
        System.out.println(ip2.getHostName());
        System.out.println(ip2.getHostAddress());

        //测试6s内是否能和百度联通
        System.out.println(ip2.isReachable(6000));




    }
}

如上是使用InetAddress类的示例。

1. IP 地址的代表类是谁?

在 Java 中,代表 IP 地址的类是 java.net.InetAddress,它是对 IP 地址的抽象封装,同时也提供了域名解析等相关功能。


2. 如何获取本机 IP 对象?

可以通过 InetAddress 类的静态方法来获取:

  • 获取本机 IP 对象

    InetAddress localHost = InetAddress.getLocalHost();
    
  • 根据域名 / IP 字符串获取对象

    InetAddress byName = InetAddress.getByName("www.baidu.com");
    InetAddress byIp = InetAddress.getByName("192.168.1.1");
    

3. 如何判断与该 IP 地址对象是否互通?

使用 InetAddress 类的 isReachable(int timeout) 方法,在指定毫秒超时时间内测试与目标主机的网络连通性:

InetAddress address = InetAddress.getByName("192.168.1.1");
boolean reachable = address.isReachable(5000); // 5秒超时
if (reachable) {
    System.out.println("与目标主机互通");
} else {
    System.out.println("无法连通目标主机");
}

该方法模拟了 ping 命令的功能,用于检测网络是否可达。

2.2 端口

应用程序在设备中唯一的标识。

  • 标记正在计算机设备上运行的应用程序,被规定为一个 16 位的二进制数,范围是 0~65535。

分类

  • 周知端口:0~1023,被预先定义的知名应用占用(如:HTTP 占用 80,FTP 占用 21)
  • 注册端口:1024~49151,分配给用户进程或某些应用程序
  • 动态端口:49152 到 65535,之所以称为动态端口,是因为它一般不固定分配某种进程,而是动态分配

注意

  • 我们自己开发的程序一般选择使用注册端口,且一个设备中不能出现两个程序的端口号一样,否则出错。

2.3 协议

连接和数据在网络中传输的规则。

网络上通信的设备,事先规定的连接规则,以及传输数据的规则被称为网络通信协议。

开放式网络互联标准:OSI 网络参考模型

  • OSI 网络参考模型:全球网络互联标准。
OSI 网络参考模型 TCP/IP 网络模型 各层对应 面向操作
应用层 应用层 HTTP、FTP、SMTP… 应用程序需要关注的:浏览器,邮箱。程序员一般在这一层开发
表示层 应用层 HTTP、FTP、SMTP… 应用程序需要关注的:浏览器,邮箱。程序员一般在这一层开发
会话层 应用层 HTTP、FTP、SMTP… 应用程序需要关注的:浏览器,邮箱。程序员一般在这一层开发
传输层 传输层 UDP、TCP… 选择使用的 TCP,UDP 协议
网络层 网络层 IP… 封装源和目标 IP
数据链路层 数据链路层 + 物理 比特流… 物理设备中传输
物理层 数据链路层 + 物理 比特流… 物理设备中传输

协议定义

  • UDP(User Datagram Protocol):用户数据报协议
  • TCP(Transmission Control Protocol):传输控制协议

UDP 协议

  • 特点:无连接、不可靠通信。
  • 不事先建立连接,数据按照包发,一包数据包含:自己的 IP、程序端口,目的地 IP、程序端口和数据(不带连接内容,限制在64KB内)等。
  • 发送方不管对方是否在线,数据在中间丢失也不管,如果接收方收到数据也不返回确认,故是不可靠的。

UDP通信效率高,例如应用在语音通话、视频直播等。

TCP 协议

  • 特点:面向连接、可靠通信。
  • TCP 的最终目的:要保证在不可靠的信道上实现可靠的传输。
  • TCP 主要有三个步骤实现可靠传输:三次握手建立连接,传输数据进行确认,四次挥手断开连接。

通信效率相对不高!例如应用在网页、文件下载、支付等。

📌 UDP 数据包限制在 64KB 内的核心原因

这个限制源于UDP 协议头的设计IP 协议的最大传输单元(MTU)约束


1. 协议层面的根本限制(UDP+IP)

  • UDP 头Length字段为16 位,最大值是 216−1=65535 字节(即 64KB-1),这直接规定了单个 UDP 数据报的最大长度。
  • IP 层叠加:UDP 数据报会被封装进 IP 数据包,而 IPv4/IPv6 的Total Length字段同样是16 位,上限也是 65535 字节,且需要扣除 IP 头(最小 20 字节)和 UDP 头(8 字节),因此有效载荷最大约为 65507 字节,仍在 64KB 量级。

2. 实际传输的工程限制

  • MTU 限制:以太网的 MTU 通常为 1500 字节,意味着超过这个大小的 UDP 包会在 IP 层被分片传输。
  • 分片风险:一旦分片后任意一个分片丢失,整个 UDP 包都会被丢弃,且 UDP 本身无重传机制,可靠性极差。
  • 实践建议:为避免分片和丢包,实际开发中通常将 UDP 包控制在512 字节~1472 字节(以太网 MTU - IP 头 - UDP 头)以内,远小于 64KB 的理论上限。

💡 补充说明

  • 64KB 是理论最大值,并非推荐值,实际使用中会远小于这个限制。
  • 若需要传输更大的数据,需在应用层手动拆分数据包,或改用 TCP 协议(TCP 天然支持大数据流的可靠传输)。                    

可靠连接:确定通信双方,收发消息都是正常无问题的!(全双工)

三次握手:确保双方收发消息都是正常无问题的

四次挥手:确保双方数据收发都已经完成!



3 实际开发

3.1 UDP 通信

  • 特点:无连接、不可靠通信。
  • 不事先建立连接;发送端每次把要发送的数据(限制在 64KB 内)、接收端 IP 等信息封装成一个数据包,发出去就不管了。
  • Java 提供了一个java.net.DatagramSocket类来实现 UDP 通信。

DatagramSocket: 用于创建客户端、服务端

构造器 说明
public DatagramSocket() 创建客户端的 Socket 对象,系统会随机分配一个端口号。
public DatagramSocket(int port) 创建服务端的 Socket 对象

DatagramPacket:创建数据包

构造器 说明
public DatagramPacket(byte[] buf, int length, InetAddress address, int port) 创建发出去的数据包对象
public DatagramPacket(byte[] buf, int length) 创建用来接

DatagramPacket 核心方法

方法 说明
public int getLength() 获取数据包实际接收到的字节个数

方法

说明

public void send(DatagramPacket dp)

发送数据包

public void receive(DatagramPacket p)

使用数据包接收数据

3.1.1服务端收消息测试

package com.yw.d2_udp1;

import java.io.IOException;
import java.net.*;

/**
 * 目标:完成UDP通信快速入门:实现1发1收。
 */
public class Client {
    public static void main(String[] args) throws IOException {
        //1.创建客户端对象
        DatagramSocket socket = new DatagramSocket();
        // 2、创建数据包对象封装要发出去的数据
        /*
        public DatagramPacket(byte buf[], int length,
                              InetAddress address, int port)
            参数一:封装要发出去的数据。
            参数二:发送出去的数据大小(字节个数)
            参数三:服务端的IP地址(找到服务端主机)
            参数四:服务端程序的端口。
         */
        byte[] bytes = "Hello!我是客户端".getBytes();
        DatagramPacket packet = new DatagramPacket(bytes, bytes.length,
                InetAddress.getLocalHost(), 6666);


        //3.发送数据包
        socket.send(packet);
        System.out.println("数据发送完毕!");
        socket.close();
    }
}
com.yw.d2_udp1;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;

public class Server {
    public static void main(String[] args) throws IOException {

        System.out.println("服务器启动!");
        //1.创建服务器对象
        DatagramSocket socket = new DatagramSocket(6666);

        //2.创建数据包对象,用于接收数据
        byte[] buffer = new byte[1024*64];//64KB
        DatagramPacket packet = new DatagramPacket(buffer,buffer.length);

        //3.正式使用数据包接收数据
        socket.receive(packet);

        //4.获取数据,从字节数组中把收到的数据直接打印出来
        //读取多少就输出多少
        //获取本次接受了多少数据
        int length = packet.getLength();
        String rs = new String(buffer,0, length);//offset 从0开始
        System.out.println(rs);


        System.out.println(packet.getAddress().getHostAddress());//可以查看客户端IP
        System.out.println(packet.getPort());//可以查看客户端端口 有这些信息方便以后回复消息
        socket.close();//释放资源
    }
}

3.1.2 多客户端多发多收

package com.yw.d3_udp2;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;

public class Server {
    public static void main(String[] args) throws IOException {

        System.out.println("服务器启动!");
        //1.创建服务器对象
        DatagramSocket socket = new DatagramSocket(6666);

        //2.创建数据包对象,用于接收数据
        byte[] buffer = new byte[1024*64];//64KB
        DatagramPacket packet = new DatagramPacket(buffer,buffer.length);

        while (true) {
            //3.正式使用数据包接收数据
            socket.receive(packet);

            //4.获取数据,从字节数组中把收到的数据直接打印出来
            //读取多少就输出多少
            //获取本次接受了多少数据
            int length = packet.getLength();
            String rs = new String(buffer,0, length);//offset 从0开始
            System.out.println("客户端:"+rs);


//            System.out.println(packet.getAddress().getHostAddress());//可以查看客户端IP
//            System.out.println(packet.getPort());//可以查看客户端端口 有这些信息方便以后回复消息

        }
    }
}
package com.yw.d3_udp2;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Scanner;

/**
 * 目标:完成UDP通信快速入门:实现多发。
 */
public class Client {
    public static void main(String[] args) throws IOException {
        //1.创建客户端对象
        DatagramSocket socket = new DatagramSocket();//动态端口,动态分配
        // 2、创建数据包对象封装要发出去的数据
        /*
        public DatagramPacket(byte buf[], int length,
                              InetAddress address, int port)
            参数一:封装要发出去的数据。
            参数二:发送出去的数据大小(字节个数)
            参数三:服务端的IP地址(找到服务端主机)
            参数四:服务端程序的端口。
         */

        Scanner sc = new Scanner(System.in);
        while (true) {
            System.out.println("请发送消息");
            //如果用户输入exit 就结束

            //用户输入数据
           String msg = sc.nextLine();
           if ("exit".equals(msg)){
               System.out.println("聊天结束!");
               socket.close();//释放资源
               break;
           }
           byte[] bytes = msg.getBytes();
            DatagramPacket packet = new DatagramPacket(bytes, bytes.length,
                    InetAddress.getLocalHost(), 6666);
            //3.发送数据包
            socket.send(packet);
        }




    }
}

提问:UDP的接收端为什么可以接收很多发送端的消息?

UDP 的接收端之所以可以接收来自许多不同发送端的消息,根本原因在于 UDP 是一个面向无连接的、基于数据报的协议

以下是具体的原理和原因:

  1. 无连接特性

    • TCP(面向连接): 在通信之前必须通过三次握手建立一条固定的连接(管道)。这条连接通常是点对点的(一个 socket 绑定到另一个确定的 socket),因此一个 TCP 服务器为了服务多个客户端,通常需要为每个客户端创建一个新的 socket 或使用多线程处理。

    • UDP(无连接): 发送方在发送数据前不需要与接收方建立连接。发送端只需要知道接收端的 IP 地址和端口号,就可以直接把数据包(数据报)扔到网络中。

  2. 唯一标识:IP地址和端口号

    • 虽然 UDP 接收端只有一个固定的端口(比如 12345)在监听,但操作系统和网络协议栈能够区分这些消息。

    • 每个到达接收端的数据包中,都附带了发送端的 IP 地址和端口号(即源地址和源端口)。

    • 这就好比一个公司的前台(接收端端口),任何人都可以往这个前台扔信件。前台虽然只有一个,但她打开信件后,知道哪封信是谁(发送端 IP 和端口)寄来的。

  3. 接收端的工作机制

    • 接收端通常调用 recvfrom 函数。当内核(操作系统)的协议栈收到一个 UDP 数据报时,它会检查目标端口。

    • 如果目标端口上有应用程序在监听,内核就会把数据报拷贝到该应用程序的接收缓冲区中。

    • 应用程序调用 recvfrom 时,不仅能拿到数据内容,还能拿到发送端的地址结构体(包含 IP 和端口)。这样,接收端就知道消息来自谁,并且如果需要回复,可以调用 sendto 函数,指定这个地址将数据发回去。

总结:
UDP 接收端就像一个公共的信箱,只有一个投递口。任何寄信人(发送端)只要在信封上写对这个信箱的地址(接收端 IP 和端口),都能把信放进去。收信人(接收端程序)每次取信时,都能从信封上的寄件人信息知道是谁寄来的,从而可以进行区分和选择性回复。这个过程不需要提前与任何寄信人约定或建立连接。

3.2 TCP通信

  • 特点:面向连接、可靠通信。
  • 通信双方事先会采用 “三次握手” 方式建立可靠连接,实现端到端的通信;底层能保证数据成功传给服务端。
  • Java 提供了一个java.net.Socket类来实现 TCP 通信。

3.2.1 TCP 通信 - 客户端开发

  • 客户端程序就是通过java.net包下的Socket类来实现的。
构造器 说明
public Socket(String host , int port)

根据指定的服务器 ip、端口号请求与服务端建立连接,连接通过,就获得了客户端 socket

方法 说明
public OutputStream getOutputStream() 获得字节输出流对象
public InputStream getInputStream() 获得字节输入流
  • 客户端实现步骤

  1. 创建客户端的 Socket 对象,请求与服务端的连接。
  2. 使用 socket 对象调用getOutputStream()方法得到字节输出流。
  3. 使用字节输出流完成数据的发送。
  4. 释放资源:关闭 socket 管道。
package com.yw.d4_tcp1;

import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;

/*
  目标:完成TCP通信-客户端开发,实现发1收1
* */
public class Client  {
    public static void main(String[] args) throws IOException {

        //1.创建Socket对象,并同时请求与服务端程序的连接。
        Socket socket = new Socket("127.0.0.1",8888);

        //2.从socket通信管道中得到一个字节输出流,用来发数据给服务端程序
        OutputStream os = socket.getOutputStream();

        //3.把低级的字节输出流包装成数据输出流
        DataOutputStream dos = new DataOutputStream(os);

        //4.开始写数据出去
        dos.writeUTF("我来了!!");
        dos.close();

        socket.close();//释放连接资源


    }
}


思考问题:

  • TCP 通信,客户端的代表类是谁?
  • TCP 通信,如何使用 Socket 管道发送、接收数据?

3.3.2TCP 通信 - 服务端程序的开发

  • 服务端是通过java.net包下的ServerSocket类来实现的。

ServerSocket

构造器 说明
public ServerSocket(int port) 为服务端程序注册端口
方法 说明
public Socket accept() 阻塞等待客户端的连接请求,一旦与某个客户端成功连接,则返回服务端这边的 Socket 对象。

服务端实现步骤

  1. 创建ServerSocket对象,注册服务端端口。
  2. 调用ServerSocket对象的accept()方法,等待客户端的连接,并得到Socket管道对象。
  3. 通过Socket对象调用getInputStream()方法得到字节输入流,完成数据的接收。
  4. 释放资源:关闭socket管道。
package com.yw.d4_tcp1;

import java.io.DataInput;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {
    public static void main(String[] args) throws IOException {
        System.out.println("*****服务端启动*****");
        //1.创建ServerSocket的对象,同时为服务端注册端口
        ServerSocket serverSocket = new ServerSocket(8888);

        //2.使用serverSocket对象,调用accept方法 ,等待客户端的连接
        Socket socket = serverSocket.accept();

        //3.从socket通信管道获取字节输入流,并读取数据
        InputStream is = socket.getInputStream();

        //4.把原始的字节输入流包装成数据输入流
        DataInputStream dis = new DataInputStream(is);

        //5.读取数据
        String rs = dis.readUTF();
        System.out.println("服务端收到数据:" + rs);

        //其实也可以获取客户端的IP地址
        System.out.println("服务端收到来自:" + socket.getRemoteSocketAddress());

        dis.close();
        socket.close();
    }

}

3.3.3 TCP1v1通信效果

3.3.4 TCP通信的多发多收

  • 使用 TCP 通信实现:多发多收消息。
  1. 客户端使用死循环,让用户不断输入消息。
  2. 服务端也使用死循环,控制服务端收完消息,继续等待接收下一个消息。

package com.yw.d5_tcp2;

import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;

/*
  目标:完成TCP通信-客户端开发,实现发1收1
* */
public class Client {
    public static void main(String[] args) throws IOException {

        //1.创建Socket对象,并同时请求与服务端程序的连接。
        Socket socket = new Socket("127.0.0.1",8888);

        //2.从socket通信管道中得到一个字节输出流,用来发数据给服务端程序
        OutputStream os = socket.getOutputStream();

        //3.把低级的字节输出流包装成数据输出流
        DataOutputStream dos = new DataOutputStream(os);

        Scanner sc = new Scanner(System.in);

        while (true) {
            System.out.println("请输入:");
            String msg = sc.nextLine();
            //若用户想退出
            if ("exit".equals(msg)) {
                System.out.println("**已退出,欢迎下次使用**");
                dos.close();
                socket.close();
                break;
            }
            //4.开始写数据出去
            dos.writeUTF(msg);
            //刷新缓冲区,即立即把消息发送出去
            dos.flush();
        }




    }
}

package com.yw.d5_tcp2;

import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {
    public static void main(String[] args) throws IOException {
        System.out.println("*****服务端启动*****");
        //1.创建ServerSocket的对象,同时为服务端注册端口
        ServerSocket serverSocket = new ServerSocket(8888);

        //2.使用serverSocket对象,调用accept方法 ,等待客户端的连接
        Socket socket = serverSocket.accept();

        //3.从socket通信管道获取字节输入流,并读取数据
        InputStream is = socket.getInputStream();

        //4.把原始的字节输入流包装成数据输入流
        DataInputStream dis = new DataInputStream(is);

        while (true) {
            try {
                //5.读取数据
                String rs = dis.readUTF();
                System.out.println("服务端收到数据:" + rs);

                //其实也可以获取客户端的IP地址
                System.out.println("**备注**服务端收到来自:" + socket.getRemoteSocketAddress()+"**");
            } catch (IOException e) {
                System.out.println(socket.getRemoteSocketAddress()+"客户端已退出");
                dis.close();
                socket.close();
                break;
            }
        }


    }

}

3.3.5 支持多个客户端同时与服务端通信

如图,现在服务端程序只能接收一个客户端发来的消息。分析服务端的代码,服务端等待接收一个客户端的socket通信,一旦接受到一个socket连接之后,就直接跑到了while死循环里去,一直等待接到的客户端的消息。

目前开发的服务端程序,是否可以支持与多个客户端同时通信?

  • 不可以的。
  • 因为服务端现在只有一个主线程,只能处理一个客户端的消息

那怎么办????——————多线程!!!!

package com.yw.d6_tcp3;

import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;

/*
  目标:完成TCP通信-客户端开发,实现发1收1
* */
public class Client {
    public static void main(String[] args) throws IOException {

        //1.创建Socket对象,并同时请求与服务端程序的连接。
        Socket socket = new Socket("127.0.0.1",8888);

        //2.从socket通信管道中得到一个字节输出流,用来发数据给服务端程序
        OutputStream os = socket.getOutputStream();

        //3.把低级的字节输出流包装成数据输出流
        DataOutputStream dos = new DataOutputStream(os);

        Scanner sc = new Scanner(System.in);

        while (true) {
            System.out.println("请输入:");
            String msg = sc.nextLine();
            //若用户想退出
            if ("exit".equals(msg)) {
                System.out.println("**已退出,欢迎下次使用**");
                dos.close();
                socket.close();
                break;
            }
            //4.开始写数据出去
            dos.writeUTF(msg);
            //刷新缓冲区,即立即把消息发送出去
            dos.flush();
        }




    }
}
package com.yw.d6_tcp3;

import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {
    public static void main(String[] args) throws IOException {
        System.out.println("*****服务端启动*****");
        //1.创建ServerSocket的对象,同时为服务端注册端口
        ServerSocket serverSocket = new ServerSocket(8888);

        while (true) {
            //2.使用serverSocket对象,调用accept方法 ,等待客户端的连接
            Socket socket = serverSocket.accept();
            new ServerReaderThread(socket).start();



//            //3.从socket通信管道获取字节输入流,并读取数据
//            InputStream is = socket.getInputStream();
//
//            //4.把原始的字节输入流包装成数据输入流
//            DataInputStream dis = new DataInputStream(is);

//            while (true) {
//                try {
//                    //5.读取数据
//                    String rs = dis.readUTF();
//                    System.out.println("服务端收到数据:" + rs);
//
//                    //其实也可以获取客户端的IP地址
//                    System.out.println("**备注**服务端收到来自:" + socket.getRemoteSocketAddress()+"**");
//                } catch (IOException e) {
//                    System.out.println(socket.getRemoteSocketAddress()+"客户端已退出");
//                    dis.close();
//                    socket.close();
//                    break;
//                }
//            }
        }


    }

}
package com.yw.d6_tcp3;

import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;

public class ServerReaderThread extends Thread{
    private Socket socket;
    public ServerReaderThread(Socket socket) {
        this.socket = socket;
    }
    public void run() {
        try {
            InputStream is = socket.getInputStream();
            DataInputStream dis = new DataInputStream(is);
            while (true) {
                String rs = dis.readUTF();
                //其实也可以获取客户端的IP地址
                System.out.println("**备注**服务端收到来自:" + socket.getRemoteSocketAddress()+":");
                System.out.println("服务端收到数据:" + rs);


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

    }
}

如图,服务端收到了来自多个客户端的消息。

拓展 :可追踪到客户端上线/离线——对服务端及线程类进行逻辑优化

package com.yw.d6_tcp3;

import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;

public class ServerReaderThread extends Thread{
    private Socket socket;
    public ServerReaderThread(Socket socket) {
        this.socket = socket;
    }
    public void run() {
        try {
            InputStream is = socket.getInputStream();
            DataInputStream dis = new DataInputStream(is);
            while (true) {
                try {
                    String rs = dis.readUTF();
                    //其实也可以获取客户端的IP地址
                    System.out.println("**备注**服务端收到来自:" + socket.getRemoteSocketAddress()+":");
                    System.out.println("服务端收到数据:" + rs);
                } catch (IOException e) {
                    System.out.println("有人下线了:"+socket.getRemoteSocketAddress());
                    dis.close();
                    socket.close();
                    break;
                }


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

    }
}
package com.yw.d6_tcp3;

import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {
    public static void main(String[] args) throws IOException {
        System.out.println("*****服务端启动*****");
        //1.创建ServerSocket的对象,同时为服务端注册端口
        ServerSocket serverSocket = new ServerSocket(8888);


        while (true) {
            //2.使用serverSocket对象,调用accept方法 ,等待客户端的连接
            Socket socket = serverSocket.accept();
            System.out.println("有人上线了"+socket.getRemoteSocketAddress());
            new ServerReaderThread(socket).start();



//            //3.从socket通信管道获取字节输入流,并读取数据
//            InputStream is = socket.getInputStream();
//
//            //4.把原始的字节输入流包装成数据输入流
//            DataInputStream dis = new DataInputStream(is);

//            while (true) {
//                try {
//                    //5.读取数据
//                    String rs = dis.readUTF();
//                    System.out.println("服务端收到数据:" + rs);
//
//                    //其实也可以获取客户端的IP地址
//                    System.out.println("**备注**服务端收到来自:" + socket.getRemoteSocketAddress()+"**");
//                } catch (IOException e) {
//                    System.out.println(socket.getRemoteSocketAddress()+"客户端已退出");
//                    dis.close();
//                    socket.close();
//                    break;
//                }
//            }
        }


    }

}


4 TCP通信-综合案例

4.1 即时通信-群聊

即客户端和客户端的通信。

package com.yw.d7_tcp4;

import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;

/*
  目标:完成TCP通信-客户端开发,实现发1收1
* */
public class Client {
    public static void main(String[] args) throws IOException {

        //1.创建Socket对象,并同时请求与服务端程序的连接。
        Socket socket = new Socket("127.0.0.1",8888);

        //创建一个线程,负责随时从socket中接收消息
        new ClientReaderThread(socket).start();

        //2.从socket通信管道中得到一个字节输出流,用来发数据给服务端程序
        OutputStream os = socket.getOutputStream();

        //3.把低级的字节输出流包装成数据输出流
        DataOutputStream dos = new DataOutputStream(os);

        Scanner sc = new Scanner(System.in);

        while (true) {
            System.out.println("请输入:");
            String msg = sc.nextLine();
            //若用户想退出
            if ("exit".equals(msg)) {
                System.out.println("**已退出,欢迎下次使用**");
                dos.close();
                socket.close();
                break;
            }
            //4.开始写数据出去
            dos.writeUTF(msg);
            //刷新缓冲区,即立即把消息发送出去
            dos.flush();
        }




    }
}

package com.yw.d7_tcp4;

import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;

public class ClientReaderThread extends  Thread{
    private Socket socket;
    public ClientReaderThread(Socket socket) {
        this.socket = socket;
    }
    public void run() {
        try {
            InputStream is = socket.getInputStream();
            DataInputStream dis = new DataInputStream(is);
            while (true) {
                try {
                    String rs = dis.readUTF();

                    System.out.println("**备注**收到来自:" + socket.getRemoteSocketAddress()+":");
                    System.out.println("收到数据:" + rs);


                } catch (IOException e) {
                    System.out.println("自己下线了:"+socket.getRemoteSocketAddress());
                    dis.close();
                    socket.close();
                    break;
                }


            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}
package com.yw.d7_tcp4;

import com.yw.d7_tcp4.ServerReaderThread;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;

public class Server {
    //创建一个集合,保存所有上线的Socket
    public static List<Socket> onLineSocket = new ArrayList<>();
    public static void main(String[] args) throws IOException {
        System.out.println("*****服务端启动*****");
        //1.创建ServerSocket的对象,同时为服务端注册端口
        ServerSocket serverSocket = new ServerSocket(8888);


        while (true) {
            //2.使用serverSocket对象,调用accept方法 ,等待客户端的连接
            Socket socket = serverSocket.accept();
            onLineSocket.add(socket);
            System.out.println("有人上线了"+socket.getRemoteSocketAddress());
            new ServerReaderThread(socket).start();


        }


    }

}
package com.yw.d7_tcp4;

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

public class ServerReaderThread extends Thread{
    private Socket socket;
    public ServerReaderThread(Socket socket) {
        this.socket = socket;
    }
    public void run() {
        try {
            InputStream is = socket.getInputStream();
            DataInputStream dis = new DataInputStream(is);
            while (true) {
                try {
                    String rs = dis.readUTF();
                    //其实也可以获取客户端的IP地址
                    System.out.println("**备注**服务端收到来自:" + socket.getRemoteSocketAddress()+":");
                    System.out.println("服务端收到数据:" + rs);
                    //把这个消息分发给全部客户端进行接收
                    sendMsgToAll(rs);
                } catch (IOException e) {
                    System.out.println("有人下线了:"+socket.getRemoteSocketAddress());
                    Server.onLineSocket.remove(socket);
                    dis.close();
                    socket.close();
                    break;
                }


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

    }

    private void sendMsgToAll(String rs) throws IOException {
        //发送给全部在线的socket管道接收
        for (Socket onLineSocket : Server.onLineSocket) {
          OutputStream os = onLineSocket.getOutputStream();
          DataOutputStream dos = new DataOutputStream(os);
          dos.writeUTF(rs);
          dos.flush();
            
        }
    }
}

总结

群聊是一种一对多 / 多对多的通信模式:

  • 一个用户发送的消息,会被群组内所有其他在线用户同时接收并展示。
  • 本质是服务端将单条客户端消息,广播转发给群组内的所有连接对象,实现多人实时交流。

群聊的核心实现思路 🛠️

以服务端 - 客户端架构为例,实现群聊主要分为以下步骤:

1. 维护客户端连接集合

  • 服务端需要用一个线程安全的容器(如 CopyOnWriteArrayListConcurrentHashMap),保存所有已连接的客户端 Socket 或输出流。
  • 每当有新客户端连接时,将其加入集合;客户端断开时,从集合中移除。

2. 消息广播逻辑

  • 当服务端收到某客户端的消息后,遍历所有已保存的客户端连接:
    • 排除消息发送者自身(可选,根据需求决定是否回显)。
    • 将消息内容写入其他所有客户端的输出流,完成广播。

3. 多线程并发处理

  • 服务端主线程负责监听新连接,每接受一个客户端连接,就创建一个新线程处理该客户端的消息读取。
  • 新线程在读取到消息后,调用广播方法,将消息分发给所有在线客户端。

4.2 BS架构

客户端不需要开发,客户端是由网页提供服务的;服务端必须需要开发。

package com.yw.d8_tcp5;

import com.yw.d7_tcp4.ServerReaderThread;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;

public class Server {
    //创建一个集合,保存所有上线的Socket
    public static List<Socket> onLineSocket = new ArrayList<>();
    public static void main(String[] args) throws IOException {
        System.out.println("*****服务端启动*****");
        //1.创建ServerSocket的对象,同时为服务端注册端口
        ServerSocket serverSocket = new ServerSocket(8888);


        while (true) {
            //2.使用serverSocket对象,调用accept方法 ,等待客户端的连接
            Socket socket = serverSocket.accept();
            onLineSocket.add(socket);
            System.out.println("有人上线了"+socket.getRemoteSocketAddress());
            new ServerReaderThread(socket).start();


        }


    }

}
package com.yw.d8_tcp5;

import com.yw.d7_tcp4.Server;

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

public class ServerReaderThread extends Thread{
    private Socket socket;
    public ServerReaderThread(Socket socket) {
        this.socket = socket;
    }
    public void run() {
        //立即响应一个网页内容,“网络通信BS测试”给浏览器展示。
        try {
            OutputStream os = socket.getOutputStream();
            DataOutputStream dos = new DataOutputStream(os);
            dos.writeUTF("网络通信BS测试");

            dos.close();
            socket.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }


    }

    private void sendMsgToAll(String rs) throws IOException {
        //发送给全部在线的socket管道接收
        for (Socket onLineSocket : Server.onLineSocket) {
          OutputStream os = onLineSocket.getOutputStream();
          DataOutputStream dos = new DataOutputStream(os);
          dos.writeUTF(rs);
          dos.flush();
            
        }
    }
}

启动服务端,观察是否响应。

可以看到服务端可以接收到请求,但只请求了一次为什么收到这么多呢?原因是浏览器在请求失败后,会再次尝试请求。但页面始终没有响应。

这是为什么???--------

这里涉及到计算机网络的HTTP协议,并不是什么数据都可以直接由服务器扔给浏览器,必须遵循浏览器响应HTTP协议规定的数据格式,否则浏览器不识别返回的数据。

package com.yw.d8_tcp5;

import com.yw.d8_tcp5.ServerReaderThread;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;

public class Server {
    //创建一个集合,保存所有上线的Socket
    public static List<Socket> onLineSocket = new ArrayList<>();
    public static void main(String[] args) throws IOException {
        System.out.println("*****服务端启动*****");
        //1.创建ServerSocket的对象,同时为服务端注册端口
        ServerSocket serverSocket = new ServerSocket(8888);


        while (true) {
            //2.使用serverSocket对象,调用accept方法 ,等待客户端的连接
            Socket socket = serverSocket.accept();
            onLineSocket.add(socket);
            System.out.println("有人上线了"+socket.getRemoteSocketAddress());
            new ServerReaderThread(socket).start();


        }


    }

}

package com.yw.d8_tcp5;

import com.yw.d7_tcp4.Server;

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

public class ServerReaderThread extends Thread{
    private Socket socket;
    public ServerReaderThread(Socket socket) {
        this.socket = socket;
    }
    public void run() {
        //立即响应一个网页内容,“网络通信BS测试”给浏览器展示。
        try {
            OutputStream os = socket.getOutputStream();
            //打印流天生支持换行
            PrintStream ps = new PrintStream(os);
            ps.println("HTTP/1.1 200 OK");
            ps.println("Content-Type:text/html;charset=UTF-8");
            ps.println();//必须要换行
            ps.println("<div style='color:red;font-size:120px;text-align:center'>网络通信BS测试</div>");
            ps.close();

            socket.close();

            socket.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }


    }

    private void sendMsgToAll(String rs) throws IOException {
        //发送给全部在线的socket管道接收
        for (Socket onLineSocket : Server.onLineSocket) {
          OutputStream os = onLineSocket.getOutputStream();
          DataOutputStream dos = new DataOutputStream(os);
          dos.writeUTF(rs);
          dos.flush();
            
        }
    }
}

5 拓展——线程池优化BS架构

每次请求都开一个线程,当高并发时,容易宕机。所以需要新的方案来优化————线程池

线程池可以控制线程的数量以及任务的数量,这样即使并发量大,也可以避免系统的瘫痪和死机。

package com.yw.d9_tcp6;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class Server {
    //创建一个集合,保存所有上线的Socket
    public static List<Socket> onLineSocket = new ArrayList<>();
    public static void main(String[] args) throws IOException {
        System.out.println("*****服务端启动*****");
        //1.创建ServerSocket的对象,同时为服务端注册端口
        ServerSocket serverSocket = new ServerSocket(8888);

        //2.创建一个线程池,负责处理通信管道的任务
        ThreadPoolExecutor pool = new ThreadPoolExecutor(16*2,16*2,0, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(8)
                ,Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());


        while (true) {
            //3.使用serverSocket对象,调用accept方法 ,等待客户端的连接
            Socket socket = serverSocket.accept();

            //4.把这个客户端对应的socket通信管道,交给一个独立的线程负责
            pool.execute(new ServerReaderRunnable(socket));


        }


    }

}
package com.yw.d9_tcp6;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class Server {
    //创建一个集合,保存所有上线的Socket
    public static List<Socket> onLineSocket = new ArrayList<>();
    public static void main(String[] args) throws IOException {
        System.out.println("*****服务端启动*****");
        //1.创建ServerSocket的对象,同时为服务端注册端口
        ServerSocket serverSocket = new ServerSocket(8888);

        //创建一个线程池,负责处理通信管道的任务
        ThreadPoolExecutor pool = new ThreadPoolExecutor(16*2,16*2,0, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(8)
                ,Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());


        while (true) {
            //2.使用serverSocket对象,调用accept方法 ,等待客户端的连接
            Socket socket = serverSocket.accept();

            //3.把这个客户端对应的socket通信管道,交给一个独立的线程负责
            pool.execute(new ServerReaderRunnable(socket));


        }


    }

}

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐