核心特性 | 说明(结合之前的 UDP 字典服务器场景) |
|---|---|
无连接 | 客户端与服务器无需提前建立连接,客户端直接发送数据报(DatagramPacket),服务器通过端口监听接收,无需 “三次握手” |
面向数据报 | 数据以独立的 “数据包” 为单位传输,每个数据包包含完整的目标地址和端口,独立处理,不合并、不拆分 |
不可靠传输 | 不保证数据到达顺序、完整性,丢失不重传、重复不丢弃、乱序不排序,依赖上层业务处理可靠性(如字典查询可容忍偶尔丢失) |
低延迟、高效率 | 无连接建立 / 关闭、无确认重传等开销,传输速度快,适合对延迟敏感、对可靠性要求不高的场景 |
无流量 / 拥塞控制 | 发送方按自身速率发送数据,不考虑接收方处理能力和网络状态 |
DatagramSocket 是UDP 的Socket,用于发送和接收UDP数据报。
DatagramSocket 构造方法: 就是 打开 ‘文件’

DatagramSocket 方法:receive 就是读取 ,send 就是写


DatagramPacket是UDP Socket发送和接收的数据报。
DatagramPacket表示UDP完整的数据报
DatagramPacket 构造方法:

UDP数据包的载荷数据,就饿可以通过构造方法来指定
DatagramPacket 方法:

构造UDP发送的数据报时,需要传入 SocketAddress ,该对象可以使用 InetSocketAddress 来创建。
InetSocketAddress ( SocketAddress 的子类)构造方法:


注意:真实的服务器响应与请求是不一样的,这个是回显服务器

具体实现:

DatagramPacket是 Java 中用于表示 UDP 数据报的类。new byte[4096]:创建一个长度为 4096 字节的字节数组,用来存储接收到的 UDP 数据报的载荷部分(即实际传输的有效数据)。length: 4096:指定接收数据的最大长度为 4096 字节,确保接收到的数据不会超出字节数组的容量。
requestPacket.getData():获取存储 UDP 数据报载荷的字节数组(即接收到的二进制数据)。offset: 0:指定从字节数组的起始位置(第 0 个字节)开始转换。requestPacket.getLength():获取实际接收到的数据长度,确保只转换有效数据部分。



receive() 方法的 DatagramPacket 参数是一个典型的"输出型参数":输出型参数:调用方提供一个"空容器",方法执行后这个容器被填充了数据。
UDP 服务器中Socket对象的生命周期和资源释放:
Socket对象会伴随服务器的整个运行过程,从服务器启动到停止,始终用于处理客户端的 UDP 数据报通信。close方法来释放Socket资源。PCB 是 进程控制块(Process Control Block)的缩写,是操作系统用于管理进程的核心数据结构

服务器会在 socket.receive(requestPacket) 处阻塞,直到客户端发送 UDP 请求才会继续执行。

package network;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class UdpEchoServer {
private DatagramSocket socket = null;
public UdpEchoServer(int port) throws SocketException {
// 指定了一个固定端口号, 让服务器来使用.
socket = new DatagramSocket(port);
}
public void start() throws IOException {
// 启动服务器
System.out.println("服务器启动");
while (true) {
// 循环一次, 就相当于处理一次请求.
// 处理请求的过程, 典型的服务器都是分成三个步骤的.
// 1. 读取请求并解析.
// DatagramPacket 表示一个 UDP 数据报. 此处传入的字节数组, 就保存 UDP 的载荷部分.
DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(requestPacket);
// 把读取到的二进制数据, 转成字符串. 只是构造有效的部分.
String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
// 2. 根据请求, 计算响应. (服务器最关键的逻辑)
// 但是此处写的是回显服务器. 这个环节相当于省略了.
String response = process(request);
// 3. 把响应返回给客户端
// 根据 response 构造 DatagramPacket, 发送给客户端.
// 此处不能使用 response.length()
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,
requestPacket.getSocketAddress());
// 此处还不能直接发送. UDP 协议自身没有保存对方的信息(不知道发给谁)
// 需要指定 目的 ip 和 目的端口.
socket.send(responsePacket);
// 4. 打印一个日志
System.out.printf("[%s:%d] req: %s, resp: %s\n", requestPacket.getAddress().toString(), requestPacket.getPort(),
request, response);
}
}
// 后续如果要写别的服务器, 只修改这个地方就好了.
// 不要忘记, private 方法不能被重写. 需要改成 public
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoServer server = new UdpEchoServer(9090);
server.start();
}
}我们的数据包中,只存储了他自己的IP和端口号,所以客户端这里要记一下服务器的IP和端口号
-目的 IP:服务器的 IP(serverIP)。
-目的端口:服务器的端口(serverport)。
-源 IP:客户端所在主机的 IP。
-源端口:操作系统随机分配的空闲端口。



package network;
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class UdpEchoClient {
private DatagramSocket socket = null;
// UDP 本身不保存对端的信息, 就自己的代码中保存一下
private String serverIp;
private int serverPort;
// 和服务器不同, 此处的构造方法是要指定访问的服务器的地址.
public UdpEchoClient(String serverIp, int serverPort) throws SocketException {
this.serverIp = serverIp;
this.serverPort = serverPort;
socket = new DatagramSocket();
}
public void start() throws IOException {
Scanner scanner = new Scanner(System.in);
while (true) {
// 1. 从控制台读取用户输入的内容.
System.out.println("请输入要发送的内容:");
if (!scanner.hasNext()) {
break;
}
String request = scanner.next();
// 2. 把请求发送给服务器, 需要构造 DatagramPacket 对象.
// 构造过程中, 不光要构造载荷, 还要设置服务器的 IP 和端口号
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
InetAddress.getByName(serverIp), serverPort);
// 3. 发送数据报
socket.send(requestPacket);
// 4. 接收服务器的响应
DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(responsePacket);
// 5. 从服务器读取的数据进行解析, 打印出来.
String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
System.out.println(response);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient client = new UdpEchoClient("127.0.0.1", 9090);
client.start();
}
}字典服务器的实现是基于UDPServer实现:
其他的都一样,接收的数据需要处理一下,需要在我们构造方法设置的字典里,寻找有没有英文对应的
只需要重写一下处理请求返回相应的数据
import java.io.IOException;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;
public class UdpDictServer extends UdpEchoServer {
private Map<String, String> dict = new HashMap<String, String>();
public UdpDictServer(int port) throws SocketException {
// 调用父类构造方法这句代码, 必须放到子类构造方法的第一行
super(port);
dict.put("hello", "你好");
dict.put("world", "世界");
dict.put("cat", "小猫");
dict.put("dog", "小狗");
dict.put("pig", "小猪");
// 可以添加更多更多的数据.
}
@Override
public String process(String request) {
return dict.getOrDefault(request, "没有找到该单词");
}
public static void main(String[] args) throws IOException {
UdpDictServer server = new UdpDictServer(9091);
server.start();
}
}ServerSocket(int port) 绑定端口后,就像在这个端口上 “开了一扇门”,持续监听是否有客户端来敲门。accept() 方法时,服务端会 “阻塞等待”—— 直到有客户端发起连接请求,才会返回一个 Socket 对象(相当于给这个客户端 “开了一扇专属的小门”)。Socket 时,需指定服务端的 IP 和端口(相当于 “主动敲门”);accept() 得到的 Socket,是与该客户端的双向通信通道: Socket 的输入流(getInputStream())读数据,输出流(getOutputStream())写数据。

ServerSocket 是创建TCP服务端Socket的API。


Socket
Socket 是客户端Socket,或服务端中接收到客⼾端建立连接(accept方法)的请求后,返回的服 务端Socket。 不管是客⼾端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及⽤来与对方收发数据的。
Socket 构造方法:


核心是解释 writer.println(request) 并非 “立即把数据发送到服务器”,而是经历了 “内存缓冲区暂存” 的中间步骤。
String request = scanner.next();:从控制台读取用户输入的字符串。writer.println(request);:通过 PrintWriter(或类似输出流)尝试 “发送数据”,但这个操作并非直接把数据发送到网卡。在 Java 的 IO 流(包括网络编程的套接字流)中,为了提升 IO 效率,会引入 “输出缓冲区”(属于内存空间的一部分):
writer.println(...) 时,数据会先被写入到内存中的输出缓冲区(即注释里的 “发送缓冲区”),而不是直接通过网卡发送到服务器。writer.flush() 方法、或者流被关闭时,缓冲区里的数据才会被 “刷出”,真正通过网卡发送到服务器。缓冲区是为了减少底层 IO 操作的频率。如果每次调用 println 都直接触发网卡发送,会因为频繁的硬件交互导致性能低下。通过 “先存到内存缓冲区,批量发送” 的方式,能大幅提升 IO 效率。
如果忽视这个机制,可能会出现 “数据发送延迟” 或 “数据丢失” 的问题。比如:
writer.flush(),且缓冲区没满、流也没关闭,服务器就会收不到数据;writer.flush(),强制把缓冲区数据刷到网卡;PrintWriter 时开启 “自动刷新”,例如 new PrintWriter(outputStream, true)(第二个参数为 true 时,调用 println 后会自动触发 flush)。PrintWriter 缓冲机制与方法行为write() 与 println :PrintWriter 的缓冲机制PrintWriter 是 Java 用于字符输出的工具类,默认带有输出缓冲区(数据先存到内存缓冲区,而非直接发送到目标设备 / 网络)。只有当缓冲区满、调用 flush() 方法、或流关闭时,数据才会真正被发送。
println() 与 write() 的行为差异方法 | 缓冲行为 | 触发发送的条件 |
|---|---|---|
println() | 数据先存入缓冲区 | 需显式调用 flush()、缓冲区满、或流关闭;若创建 PrintWriter 时指定 autoFlush = true(如 new PrintWriter(out, true)),则 println() 后会自动触发 flush |
write() | 数据同样先存入缓冲区(本质与 println 无差异) | 需显式调用 flush()、缓冲区满、或流关闭;write() 本身不会自动触发 flush |
这里的 “套壳” 可理解为 PrintWriter 的缓冲策略配置(如是否启用 autoFlush)。若未对 PrintWriter 做特殊配置(即 “不套壳”),println() 和 write() 都会依赖缓冲区逻辑;只有显式配置 autoFlush 或主动调用 flush(),才能确保数据及时发送。
writer.flush(),或在创建 PrintWriter 时开启自动刷新(new PrintWriter(outputStream, true))。Scanner读取逻辑:当执行 writer.print(request) 时,客户端已将数据成功发送至服务器并被接收,但服务器尚未对该数据进行业务逻辑层面的处理(如回声响应、数据解析等)。
Scanner的hasNext()/next()行为解析
Scanner的阻塞逻辑导致处理延迟。Scanner的行为,通过合理的 IO 策略(如readLine()、自定义协议解析)确保数据被完整、及时地处理。所以这里就产生了一个问题:
wrinter.printLn();,Scanner 的 hasNext() 方法是 ** 基于 “分隔符” 来判定是否存在下一个 “标记(token)”** 的,默认规则如下:
Scanner 默认的分隔符是空白符,包括:
\n)、回车符(\r)\t)、翻页符等当 hasNext() 执行时,它会持续扫描输入,直到遇到 “空白符”,此时认为一个 “完整的标记” 存在,hasNext() 返回 true;如果输入流已经关闭且没有更多可扫描的内容,则返回 false。
println我们熟知这个是有换行符打印
hasNext是识别空白符结束读取的一个函数
所以你的客户端传入的数据,犹豫print 是没有换行这个操作的,就在这里堵塞住了(仍在这里等待接下里的输入知道识别到空白符)
服务器用 Scanner.next()/hasNext() 等依赖 “空白符(换行、空格等)” 的方式读取,而客户端 print 未发送换行符,会导致服务器读取阻塞,无法触发处理逻辑
package network;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoClient {
private Socket socket = null;
public TcpEchoClient(String serverIp, int serverPort) throws IOException {
// 客户端在 new Socket 的时候, 就会和服务器建立 TCP 连接.
// 此时少了服务器 IP 和 端口.
socket = new Socket(serverIp, serverPort);
// socket.connect(new InetSocketAddress(InetAddress.getByName(serverIp), serverPort));
}
public void start() {
System.out.println("client start!");
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
Scanner scanner = new Scanner(System.in);
Scanner scannerNetwork = new Scanner(inputStream);
PrintWriter writer = new PrintWriter(outputStream);
while (true) {
// 1. 从控制台读取用户输入.
System.out.print("-> ");
String request = scanner.next();
// 2. 把请求发送给服务器.
writer.println(request);
writer.flush();
// 3. 从服务器读取响应
if (!scannerNetwork.hasNext()) {
break;
}
String response = scannerNetwork.next();
// 4. 把响应显示到控制台上
System.out.println(response);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client = new TcpEchoClient("127.0.0.1", 9090);
client.start();
}
}package network;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TcpEchoServer {
private ServerSocket serverSocket = null;
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("server start!");
// 不能使用 FixedThreadPool, 线程数目固定.
ExecutorService executorService = Executors.newCachedThreadPool();
while (true) {
// 首先要先接受客户端的连接, 然后才能进行通信.
// 如果有客户端和服务器建立好了连接, accept 能够返回.
// 否咋 accept 会阻塞.
// [有连接]
Socket socket = serverSocket.accept();
// 通过这个方法处理这个客户端整个的连接过程.
// 直接调用 processConnection, 此时就会 "顾此失彼" 一旦进入到 processConnection 方法
// 就不能再次调用 accept
// processConnection(socket);
// 此处创建新线程. 在新线程里, 调用 processConnection
// Thread t = new Thread(() -> {
// processConnection(socket);
// });
// t.start();
// 如果当前线程数目进一步增多, 创建销毁进一步频繁, 此时线程创建销毁开销不可忽视了.
// 使用线程池, 是进一步的改进手段.
executorService.submit(() -> {
processConnection(socket);
});
}
}
private void processConnection(Socket socket) {
// 在一次连接中, 客户端和服务器之间可能会进行多组数据传输.
System.out.printf("[%s:%d] 客户端上线!\n", socket.getInetAddress(), socket.getPort());
// [面向字节流] & [全双工]
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
Scanner scanner = new Scanner(inputStream);
PrintWriter writer = new PrintWriter(outputStream);
while (true) {
// 处理多次请求/响应的读写操作.
// 一次循环就是读写一个请求/响应
// 1. 读取请求并解析 (可以直接使用 Scanner 完成)
if (!scanner.hasNext()) {
// 客户端关闭了连接
System.out.printf("[%s:%d] 客户端下线!\n", socket.getInetAddress(), socket.getPort());
// 假设这里有一系列的逻辑呢??
break;
}
String request = scanner.next();
// 2. 根据请求计算响应
String response = process(request);
// 3. 把响应写回客户端
writer.println(response);
writer.flush();
// 4. 打印日志
System.out.printf("[%s:%d] req: %s, resp: %s\n", socket.getInetAddress(), socket.getPort(),
request, response);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
// 假如在这之前也有一系列逻辑呢?
socket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(9090);
server.start();
}
}package network;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class TcpDictServer extends TcpEchoServer {
private Map<String, String> dict = new HashMap<>();
public TcpDictServer(int port) throws IOException {
super(port);
dict.put("hello", "你好");
dict.put("world", "世界");
dict.put("cat", "小猫");
dict.put("dog", "小狗");
}
public String process(String request) {
return dict.getOrDefault(request, "没有找到该单词");
}
public static void main(String[] args) throws IOException {
TcpDictServer server = new TcpDictServer(9090);
server.start();
}
}同时启动多个 IntelliJ IDEA 实例,方便并行处理不同项目或任务。
其核心含义是:允许同一应用程序同时启动并运行多个独立的实例。每个实例拥有各自独立的进程、内存空间和运行状态,彼此互不干扰。
作用场景:当启用该设置时,用户可多次打开同一程序(如多次启动文档编辑软件处理不同文件、同时运行多个工具类应用等);若禁用,则可能仅能运行一个实例,再次尝试启动时会激活已运行的实例而非新建。
第一步找到:

第二步点击:

第三步:

第四步:勾选上红色部分即可


Java 网络编程中出现的端口占用异常(BindException: Address already in use: bind
该异常表明你要启动的 TcpEchoServer 试图绑定的端口,已经被其他进程(或同一程序的另一个实例)占用了。
TCP和UDP 协议不同,可以是使用同一个端口号,是不会冲突的
协议相同,就需要干掉上一个进程
注意:端口只能被一个进程绑定
勾选上这个,这样我们就可以启动多个客户端连接服务器了
新问题又来了。我这个这里的有两个客户端连接服务器,由于while 和 这里的hasNext的共同作用下,我们就会堵塞在客户端1这里,客户端2 就不会有任何的反应和日志

这就是多线程的由来,所以我们就可以使用多线程去解决这个问题
服务器代码的核心逻辑是:
while(true) 循环通过 serverSocket.accept() 等待客户端连接(accept() 是阻塞方法,没有新连接时会一直卡住);processConnection(clientSocket) 处理该连接;processConnection 方法中又有一个 while(true) 循环,通过 scanner.hasNext() 等待客户端发送请求(hasNext() 也是阻塞方法,客户端不发数据时会一直卡住)。单线程模式下,服务器同一时间只能执行一个操作,导致两个关键阻塞点冲突:
accept() 与 processConnection 无法并行当服务器在 processConnection 中等待客户端 1 发送请求时(阻塞在 hasNext()),会一直停留在该流程,无法回到外层循环执行 accept(),此时新的客户端(如客户端 2)发起连接请求,服务器无法响应,导致新连接被阻塞。
scanner.hasNext(),既无法处理客户端 1 的后续请求,也无法接受其他新客户端的连接,导致整个服务器 “卡住”。
服务器端口固定,多个客户端通过 “不同的本地端口 + 五元组唯一性”,可以同时连接到服务器的同一个端口
在网络通信中,多个客户端可以同时连接到服务器的同一个端口,核心原因是通过 **“五元组”(源 IP、源端口、目的 IP、目的端口、传输层协议)** 来区分不同的连接,具体说明如下:
即使多个客户端连接到服务器的同一个端口,每个连接的 “五元组” 都是唯一的:
(192.168.1.2, 50000, 192.168.1.100, 8080, TCP);(192.168.1.3, 50001, 192.168.1.100, 8080, TCP);以常见的 Web 浏览为例:
注意:线程本身的资源开销、线程池管理结构的开销方面看的话,我们消耗的资源又太多了,所以使用线程池控制
1MB(可通过 JVM 参数 -Xss 调整,如 -Xss256k 可将栈空间缩小到 256KB)。若线程池配置 10 个核心线程,默认栈空间消耗约为 10MB(10 × 1MB)。线程销毁主要是回收栈空间和操作系统线程资源:
线程池内部包含工作队列、线程状态控制器、Worker 线程集合等对象,这些结构的资源消耗相对较小:
LinkedBlockingQueue 初始时仅占用少量内存(存储队列节点的引用),后续随任务数量动态变化;ctl 变量(用于管理线程池状态和线程数),仅占用几个字节的内存;HashSet),内存消耗可忽略。线程池的资源消耗是高度可配置的,可通过以下方式优化:

UDP就是以DatagramPacket 作为单位的
TCP则是 以字节为单位,实际上一个请求,往往是由多个字节构成的
DatagramPacket(数据报包)UDP 是基于 “数据报” 的无连接协议,每次发送 / 接收数据都以 DatagramPacket 为独立单元。一个 DatagramPacket 包含:
DatagramPacket,接收方就会以同样的单位接收,天然具备 “数据边界”。TCP 是基于 “字节流” 的面向连接协议,没有明确的 “数据包” 边界,数据以连续的字节流形式传输。这意味着:
无论 UDP 还是 TCP,一个业务请求(如一次消息、一次文件片段)往往由多个字节构成:
DatagramPacket 中发送;这种传输单位的差异,直接导致了 UDP “发送即完整”、TCP “流式无边界” 的特性,也决定了两者在可靠性、传输效率、使用场景上的不同(如 UDP 适合音视频直播,TCP 适合文件传输、网页访问等)。
服务端客户端Socket 释放的问题?
服务端的生命周期是贯穿整个进程,所以随着这个进程的结束而结束
客户端的生命周期是随着用户的登录而创建连接产生的,所以是随着连接的连接和断开为一个生命周期的
Socket(ServerSocket)的释放服务端的 ServerSocket 主要负责监听客户端连接请求,其生命周期与服务进程强关联:
ServerSocket 会随进程退出自动释放资源。serverSocket.close(),这会立即停止监听新连接,并释放占用的端口和文件描述符。Socket 的释放(服务端视角)服务端在接收到客户端连接后,会创建一个新的 Socket 实例用于与该客户端通信(如多线程服务端中,每个客户端对应一个线程和一个 Socket):
socket.close() 释放资源。若未主动关闭,会导致文件描述符泄漏,最终可能因资源耗尽导致服务不可用。read() 返回 -1 或抛出 IOException)来感知,并及时关闭对应的 Socket。Socket 的释放客户端 Socket 是主动发起连接的一方,生命周期与 “连接建立→通信→连接断开” 强关联:
socket.close() 关闭连接,释放本地端口和网络资源。Socket 会在垃圾回收时被回收,但该过程不可控,可能导致资源长期占用(如端口被占用、网络缓冲区未释放)。try-with-resources 自动管理Java 中推荐使用try-with-resources 语法(Socket 实现了 AutoCloseable 接口),确保无论是否发生异常,Socket 都会被自动关闭:
try (Socket socket = new Socket(serverIp, serverPort);
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {
// 通信逻辑
} catch (IOException e) {
// 异常处理
}
// 代码块结束后,socket、out、in 会自动调用 close() 释放资源或者
try() {
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
socket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
是一种高效的网络 I/O 模型,核心能力是让一个线程同时监听多个 I/O 通道(如 Socket)。当某个通道有数据可读 / 可写时,再触发处理逻辑。
select、poll、epoll),JVM 本身没有 “原生 API”,需要通过封装这些系统调用实现。是 Java 对IO 多路复用机制的封装,提供了Channel(通道)、Selector(选择器,实现多路复用)、Buffer(缓冲区)等组件,实现了非阻塞 IO和高效的多路复用能力。
是基于 Java NIO 的高性能网络框架,它对 NIO 进行了更友好的封装和增强,解决了 NIO 原生 API 的易用性问题,同时提供了丰富的功能(如编解码、心跳检测、断线重连、流量控制等)。
如果只是单个线程,无法同时响应多个客户端. 此处给每个客户端都分配⼀个线程
// 启动服务器
public void start() throws IOException {
System.out.println("服务器启动!");
while (true) {
Socket clientSocket = serverSocket.accept();
Thread t = new Thread(() -> {
processConnection(clientSocket);
});
t.start();
}
}为了避免频繁的创建销毁线程池,也可以引入线程池。
// 启动服务器
public void start() throws IOException {
System.out.println("
服务器启动
!");
ExecutorService service = Executors.newCachedThreadPool();
while (true) {
Socket clientSocket = serverSocket.accept();
// 使⽤线程池, 来解决上述问题
}
}
service.submit(new Runnable() {
@Override
public void run() {
processConnection(clientSocket);
}
});TCP发送数据时,需要先建立连接,什么时候关闭连接就决定是短连接还是长连接:
短连接:每次接收到数据并返回响应后,都关闭连接,即是短连接。也就是说,短连接只能⼀次收发 数据。
长连接:不关闭连接,⼀直保持连接状态,双方不停的收发数据,即是长连接。也就是说,⻓连接可以多次收发数据。
对比以上长短连接,两者区别如下:
• 建立连接、关闭连接的耗时:短连接每次请求、响应都需要建立连接,关闭连接;而长连接只需要 第⼀次建立连接,之后的请求、响应都可以直接传输。相对来说建立连接,关闭连接也是要耗时 的,长连接效率更⾼。
• 主动发送请求不同:短连接⼀般是客户端主动向服务端发送请求;而长连接可以是客⼾端主动发送 请求,也可以是服务端主动发。
• 两者的使用场景有不同:短连接适用于客⼾端请求频率不高的场景,如浏览网页等。长连接适⽤于 客户端与服务端通信频繁的场景,如聊天室,实时游戏等。
基于BIO(同步阻塞IO)的长连接会⼀直占用系统资源。对于并发要求很高的服务端系统来说,这样的 消耗是不能承受的。
由于每个连接都需要不停的阻塞等待接收数据,所以每个连接都会在⼀个线程中运行。
⼀次阻塞等待对应着⼀次请求、响应,不停处理也就是长连接的特性:⼀直不关闭连接,不停的处理 请求。
实际应用时,服务端⼀般是基于NIO(即同步非阻塞IO)来实现长连接,性能可以极大的提升。