目录
TCP与UDP协议的区别
基于 UDP 协议实现回显服务器
UDP Socket 编程常用 Api
UDP 服务器
UDP 客户端
基于 TCP 协议实现回显服务器
TCP Socket 编程常用 Api
TCP 服务器
TCP 客户端
TCP 服务端常见的 bug
客户端发送数据后,没有响应
服务器仅支持与一个客户端建立连接
TCP与UDP协议的区别
传输控制协议(TCP)与 用户数据报协议(UDP)是传输层两个重要的协议,二者互补共存,共同支撑互联网的多层次传输需求。
二者的区别如下:
· TCP 协议是有连接的、可靠传输、面向字节流、全双工
· UDP 协议是无连接的、不可靠传输、面向数据报、全双工
其中有/无连接是指:如果通信双方保存了通信对端的信息,就相当于是有连接;如果不保存对端的信息,就是无连接。
可靠传输/不可靠传输:此处的可靠不是指 100% 能到达对方,而是指“尽可能”确保数据的传输,而“不可靠”则意味着完全不保证数据是否能成功到达对方。TCP 通过一些内置机制(确认应答机制、重传机制等)保证了可靠传输,UDP则没有可靠性机制。
面向字节流/面向数据报:TCP 是面向字节流的,TCP 的传输过程就和文件流/水流是一样的;而 UDP 是面向数据报的,其传输数据的基本单位是数据报,一次发送/接收必须发送/接收完整的数据报。
全双工/半双工:全双工是指一个通信链路既可以发送数据也可以接收数据(双向通信),半双工是指一个通信链路只能发送/接收数据(单向通信)。
TCP 和 UDP 的选择以及适用场景:
· 当数据准确性 > 传输延迟时(例如软件更新下载、文件传输、网页浏览等场景),选择 TCP。
因为 TCP 能够保证数据的可靠传输,通过确认应答、重传机制等手段确保数据的完整性和准确性,适用于对数据传输准确性要求较高的应用。
· 当实时性 > 数据完整性时(例如多人游戏同步、在线视频会议、实时语音通话等场景),选择 UDP。
UDP适用于对实时性要求较高的场景,虽然它不保证数据的完整性,但能减少延迟,确保数据传输的实时性。特别是在实时通信中,丢失少量数据包对用户体验的影响相对较小,而过高的延迟可能影响整体体验。
基于 UDP 协议实现回显服务器
UDP Socket 编程常用 Api
· DatagramSocket:是一种用于网络通信的套接字对象,代表了操作系统中一个特定类型的“文件”资源。可以将其理解为操作系统对网络设备(如网卡)的一种抽象表示,就像操作系统将硬盘、键盘等设备抽象为文件一样,DatagramSocket 抽象了网络数据的发送与接收通道。它专门用于通过 UDP(用户数据报协议) 发送和接收数据报(Datagram),支持无连接、不可靠但高效的通信方式。通过 DatagramSocket,应用程序能够将数据以数据报的形式发送到目标地址,同时也能接收来自网络上其他节点的数据报,实现轻量级的网络通信。
· DatagramSocket的构造方法包括以下两种形式:
DatagramSocket():创建一个 UDP 数据报套接字的 Socket,绑定到本机任意一个随机端口(通常用于客户端)
DatagramSocket(int port):创建一个 UDP 数据报套接字的 Socket,绑定到本机指定的端口(通常用于服务端)
· DatagramSocket类下的方法:
void receive(DatagramPacket p):从套接字接收数据报,如果没有接收到数据报就会阻塞等待
void send(DatagramPacket p):从套接字发送数据报,不会阻塞等待,直接发送
void close():关闭此数据报套接字
· DatagramPacket:是 UDP Socket 发送/接收的数据报,其构造方法包括:
DatagramPacket(byte[] buf, int length):构造⼀个DatagramPacket以⽤来接收数据报,接收的数据保存在字节数组(第⼀个参数buf)中,接收指定长度(第⼆个参数length)
DatagramPacket(byte[] buf, int offset, int length, SocketAddress address):构造⼀个DatagramPacket以⽤来发送数据报,发送的数据为字节数组(第⼀个参数buf)中,从0到指定长度(第⼆个参数length)。address指定⽬的主机的 IP 和端口号。
· DatagramPacket类下的方法:
InetAddress getAddress():从接收的数据报中,获取发送端主机 IP 地址;或从发送的数据报中,获取接收端主机 IP 地址
int getPort():从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获取接收端主机端口号
byte[] getData():获取数据报中的数据
回显服务器需要实现两个程序:UDP 服务器和 UDP 客户端,主动发起通信的一方称为客户端,被动接收的一方的是服务器。
UDP 服务器
package network;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
/**
* Created with IntelliJ IDEA.
* Description: 回显服务器
* 客户端发啥样的请求,服务器就返回啥样的响应
* User: Li_yizYa
* Date: 2025/5/13
* Time: 15:29
*/
public class UdpEchoServer {
private DatagramSocket socket = null;
public UdpEchoServer(int port) throws SocketException {
socket = new DatagramSocket(port);
}
/**
* 通过 start 启动服务器的核心流程
*/
public void start() throws IOException {
System.out.println("服务器启动!");
while (true) {
// 此处通过 “死循环” 不停的处理客户端的请求.
// 1. 读取客户端的请求并解析
DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(requestPacket);
// 上述收到的数据,是二进制 byte[] 的形式体现的,后续代码如果要进行打印之类的处理操作
// 需要转成字符串才好处理
String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
// 2. 根据请求计算响应,由于此处是回显服务器,响应就是请求
String response = process(request);
// 3. 把响应写回到客户端
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,
requestPacket.getSocketAddress());
socket.send(responsePacket);
// 4. 打印日志
System.out.printf("[%s:%d] req=%s, resp=%s\\n", requestPacket.getAddress(), requestPacket.getPort(),
request, response);
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoServer server = new UdpEchoServer(9090);
server.start();
}
}
UDP 客户端
package network;
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
/**
* Created with IntelliJ IDEA.
* Description:
* User: Li_yizYa
* Date: 2025/5/13
* Time: 15:29
*/
public class UdpEchoClient {
DatagramSocket socket = null;
private String serverIP;
private int serverPort;
public UdpEchoClient(String serverIP, int serverPort) throws IOException {
socket = new DatagramSocket();
this.serverIP = serverIP;
this.serverPort = serverPort;
}
public void start() throws IOException {
System.out.println("启动客户端");
Scanner scanner = new Scanner(System.in);
while (true) {
// 1. 从控制台读取到用户的输入
System.out.print("-> ");
String request = scanner.next();
// 2. 构造一个 UDP 请求,发送给服务器
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
InetAddress.getByName(this.serverIP), this.serverPort);
socket.send(requestPacket);
// 3. 从服务器读取到响应
DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(responsePacket);
String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
// 4. 把响应打印到控制台上
System.out.println(response);
}
}
public static void main(String[] args) throws IOException {
// 127.0.0.1 特殊 IP,环回 IP
// 如果客户端和服务器在同一个主机上,就使用这个 IP
UdpEchoClient client = new UdpEchoClient("127.0.0.1", 9090);
client.start();
}
}
基于 TCP 协议实现回显服务器
TCP Socket 编程常用 Api
在TCP Socket编程中,核心类是 ServerSocket(专门给服务器使用的 Socket 对象)和 Socket(既会给客户端使用,又会给服务器使用)。由于 TCP 协议是面向字节流的,其数据的基本传输单位就是 byte,因此不需要像 UDP 协议那样定义一个类来表示 “数据报” 对象。
· ServerSocket:是创建 TCP 服务器 Socket 的 Api,其构造方法为:
ServerSocket(int port):创建⼀个服务端流套接字Socket,并绑定到指定端口
· ServerSocket 类下常用的方法:
Socket accept():开始监听指定端口(创建时绑定的端口),有客户端连接后,返回⼀个服务端 Socket 对象,并基于该 Socket 建立与客户端的连接,否则阻塞等待
void close():关闭此套接字
· Socket:其是客户端 Socket,或服务端接收到客户端建立连接(accept方法)的请求后,返回的服务端 Socket。不管是客户端还是服务端 Socket,都是双方建立连接以后,保存的对端信息,及时用来与对方收发数据的。其构造方法为:
Socket(String host, int port):创建⼀个客户端流套接字 Socket,并与对应 IP 的主机 上,对应端口的进程建立连接。
· Socket 类下的方法:
InetAddress getInetAddress():返回套接字所在的地址
InputStream getInputStream():返回此套接字的输入流
OutputStream getOutputStream():返回此套接字的输出流
TCP 服务器
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;
/**
* Created with IntelliJ IDEA.
* Description: TCP回显服务器
* 客户端发啥样的请求,服务器就返回啥样的响应
* User: Li_yizYa
* Date: 2025/5/21
* Time: 20:19
*/
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("启动服务器");
while (true) {
Socket clientSocket = serverSocket.accept();
Thread t = new Thread(() -> {
try {
processConnection(clientSocket);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
t.start();
}
}
// 针对一个连接,提供处理逻辑
private void processConnection(Socket clientSocket) throws IOException {
// 先打印一下客户端信息
System.out.printf("[%s:%d] 客户端上线!\\n", clientSocket.getInetAddress(), clientSocket.getPort());
// TCP 是全双工的通信,一个 socket 对象,既可以读,也可以写
// 获取到 socket 中持有的流对象
try (InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()) {
// 使用 Scanner 包装一下 inputStream,就可以更方便的读取这里的请求数据了
while (true) {
// 1. 读取请求并解析
Scanner scanner = new Scanner(inputStream);
PrintWriter printWriter = new PrintWriter(outputStream);
if (!scanner.hasNext()) {
// 如果 scanner 无法读取出数据,说明客户端关闭了连接,导致服务器这边读取到 “末尾”
break;
}
String request = scanner.next();
// 2. 根据请求计算响应
String response = process(request);
// 3. 把响应写回给客户端
// 此处可以按照字节数组来写,也可以右另外一种写法
// outputStream.write(response.getBytes());
printWriter.println(response);
printWriter.flush();
// 打印日志
System.out.printf("[%s:%d] req=%s; resp=%s\\n", clientSocket.getInetAddress(),
clientSocket.getPort(), request, response);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
System.out.printf("[%s:%d] 客户端下线!\\n", clientSocket.getInetAddress(), clientSocket.getPort());
clientSocket.close();
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(9090);
server.start();
}
}
TCP 客户端
package network;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
/**
* Created with IntelliJ IDEA.
* Description:
* User: Li_yizYa
* Date: 2025/5/21
* Time: 20:20
*/
public class TcpEchoClient {
private Socket socket = null;
/**
* 构造方法
* @param serverIp 服务器 Ip
* @param serverPort 服务器端口号
*/
public TcpEchoClient(String serverIp, int serverPort) throws IOException {
socket = new Socket(serverIp, serverPort);
}
public void start() {
System.out.println("客户端启动");
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
Scanner scanner = new Scanner(inputStream);
Scanner scannerIn = new Scanner(System.in);
PrintWriter printWriter = new PrintWriter(outputStream);
while (true) {
// 1. 从控制台读取数据
System.out.print("-> ");
String request = scannerIn.next();
// 2. 把请求发给服务器
printWriter.println(request);
// 引入 flush(冲刷) 操作,主动刷新缓冲区
printWriter.flush();
// 3. 从服务器读取响应
if (!scanner.hasNext()) {
break;
}
String response = scanner.next();
// 4. 打印响应结果
System.out.println(response);
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client = new TcpEchoClient("127.0.0.1", 9090);
client.start();
}
}
TCP 服务端常见的 bug
客户端发送数据后,没有响应
在客户端中,我们通过下面的方式给服务端发送请求:
// 2. 把请求发给服务器
printWriter.println(request);
之所以这里没有响应,是因为其实客户端的数据并没有发送出去,因为 PrintWriter 这个类以及 IO 流中的很多类,都是自带缓冲区的,引入缓冲区之后,进行 写入数据操作 时,不会立即触发 IO,而是先将其放在内存缓冲区中,等缓冲区数据到达一定数量后,才会统一发送。而上述的问题,其实就是因为数据比较少,并未触发发送操作。因此,我们通过引入 flush 操作就可以解决该问题:
// 引入 flush(冲刷) 操作,主动刷新缓冲区
printWriter.flush();
服务器仅支持与一个客户端建立连接
对于该问题,引入多线程操作修改服务器代码中的 start 方法即可,每有一个客户端请求建立与服务器的连接,就创建一个线程来专门处理与该客户端之间的数据发送/接收,具体代码如下:
public void start() throws IOException {
System.out.println("启动服务器");
while (true) {
Socket clientSocket = serverSocket.accept();
Thread t = new Thread(() -> {
try {
processConnection(clientSocket);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
t.start();
}
}
评论前必须登录!
注册