一文谈清网络编程 | 超详细教程(附源码)
目录
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 是一个面向无连接的、基于数据报的协议。
以下是具体的原理和原因:
无连接特性
TCP(面向连接): 在通信之前必须通过三次握手建立一条固定的连接(管道)。这条连接通常是点对点的(一个 socket 绑定到另一个确定的 socket),因此一个 TCP 服务器为了服务多个客户端,通常需要为每个客户端创建一个新的 socket 或使用多线程处理。
UDP(无连接): 发送方在发送数据前不需要与接收方建立连接。发送端只需要知道接收端的 IP 地址和端口号,就可以直接把数据包(数据报)扔到网络中。
唯一标识:IP地址和端口号
虽然 UDP 接收端只有一个固定的端口(比如
12345)在监听,但操作系统和网络协议栈能够区分这些消息。每个到达接收端的数据包中,都附带了发送端的 IP 地址和端口号(即源地址和源端口)。
这就好比一个公司的前台(接收端端口),任何人都可以往这个前台扔信件。前台虽然只有一个,但她打开信件后,知道哪封信是谁(发送端 IP 和端口)寄来的。
接收端的工作机制
接收端通常调用
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() |
获得字节输入流 |
-
客户端实现步骤
- 创建客户端的 Socket 对象,请求与服务端的连接。
- 使用 socket 对象调用
getOutputStream()方法得到字节输出流。 - 使用字节输出流完成数据的发送。
- 释放资源:关闭 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 对象。 |
服务端实现步骤
- 创建
ServerSocket对象,注册服务端端口。 - 调用
ServerSocket对象的accept()方法,等待客户端的连接,并得到Socket管道对象。 - 通过
Socket对象调用getInputStream()方法得到字节输入流,完成数据的接收。 - 释放资源:关闭
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 通信实现:多发多收消息。
- 客户端使用死循环,让用户不断输入消息。
- 服务端也使用死循环,控制服务端收完消息,继续等待接收下一个消息。
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. 维护客户端连接集合
- 服务端需要用一个线程安全的容器(如
CopyOnWriteArrayList、ConcurrentHashMap),保存所有已连接的客户端 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));
}
}
}
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐







所有评论(0)