UDP通信
特点:无连不是先接、不可靠通信
不事先建立连接;发送端每次把要发送的数据(限制在64KB内)、接收端IP、等信息封装成一个数据包,发出去就不管了
java提供了一个java.net.DatagramSocket类来实现UDP通信
DatagramSocket
DatagramSocket
用于创建客户端和服务器端的 UDP 通信。它提供了发送和接收 DatagramPacket
的方法。
构造器:
-
DatagramSocket(int port)
: 创建绑定到特定端口的DatagramSocket
。 -
DatagramSocket(int port, InetAddress laddr)
: 创建绑定到特定地址和端口的DatagramSocket
。
处理数据包方法:
-
send(DatagramPacket p)
: 发送DatagramPacket
。 -
receive(DatagramPacket p)
: 接收DatagramPacket
。
DatagramPacket
DatagramPacket
是用来封装 UDP 数据包的类。它包含数据、源端口、目标端口和 IP 地址。
构造器:
-
DatagramPacket(byte[] buf, int length)
: 创建一个空的数据包,数据存储在buf
数组中,长度为length
。 -
DatagramPacket(byte[] buf, int length, InetAddress address, int port)
: 创建一个数据包,指定数据、目标 IP 地址和端口。
创建数据包:
-
首先,你需要一个字节数组来存储你的数据。
-
然后,使用适当的构造器创建
DatagramPacket
对象。 -
对于发送,你需要设置目标 IP 地址和端口。
-
对于接收,
DatagramSocket.receive()
方法会填充DatagramPacket
的源 IP 地址和端口。
示例代码
//服务器端
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
public class UDPServer {
public static void main(String[] args) throws Exception {
DatagramSocket serverSocket = new DatagramSocket(12345);//创建服务端对象,指定服务端端口
byte[] receiveData = new byte[1024];//指定字节数组大小
DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);//创建数据包
serverSocket.receive(receivePacket);//数据包接收数据
//接收到指定长度的字节数组并转为字符串
String receivedMessage = new String(receivePacket.getData(), 0, receivePacket.getLength());
System.out.println("Server received: " + receivedMessage);
serverSocket.close();
}
}
//客户端
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
public class UDPClient {
public static void main(String[] args) throws Exception {
InetAddress IPAddress = InetAddress.getByName("localhost");//创建客户端对象,指定接收主机IP
int port = 12345;//指定要发送到主机的服务端端口
String sentence = "Hello World!";
byte[] buf = sentence.getBytes();//封装数据到字节数组中
DatagramPacket packetToSend = new DatagramPacket(buf, buf.length, IPAddress, port);
DatagramSocket clientSocket = new DatagramSocket();//创建数据包用于接收数据
clientSocket.send(packetToSend);//数据包发送数据
clientSocket.close();
}
}
demo1:实现一发一收
-
实现客户端、服务端
-
创建客户端对象
-
创建数据包对象,封装发出的数据
-
开始使用数据包发送数据
-
释放资源(数据包)
-
创建服务端对象(注册端口)
-
创建数据包对象,接收数据
-
开始使用数据包接收数据
-
从字节数组中,把本次接收的数据直接打印出来(注意:可能多次接收数据,字节数组被多次不完全覆盖,因此需要接收多少就倒多少)
-
(最后,可以通过socket获取客户端的IP地址)
demo2:实现多发多收
直接对demo1进行改造:
-
实现客户端时,直接在客户端发送信息的代码块处放入循环中
-
注意:此时,客户端只能指定的发一两段信息,因此,直接实现让客户从键盘输入
-
实现服务端时,直接在接收处代码块放入死循环中
-
注意:服务端的资源一定要在最后关闭,也就是说,资源关闭不包括在循环中,或者说,一般服务端的的资源不会关闭
-
(最后,可以通过socket获取客户端的IP地址)
TCP通信
特点:面向连接、可靠通信
通信双方事先会采用“三次握手”方式建立可靠连接,实现端到端的通信;底层能保证数据成功传给服务端。
java提供了一个java.net.Socket类和ServerSocket类来实现TCP通信
模型:
在 Java 中,Socket
类是用于建立 TCP(传输控制协议)网络连接的。TCP 是一种面向连接的、可靠的、基于字节流的协议,它确保数据以发送顺序到达目的地。
Socket 类
Socket
类位于 java.net
包中,它代表了一个客户端的 TCP 连接端点。
构造方法
Socket
类有几个构造方法,用于以不同的方式创建套接字:
-
无参构造方法:
Socket()
创建一个新的未连接的套接字。
-
指定服务器地址和端口的构造方法:
Socket(InetAddress address, int port) Socket(String host, int port)
这两个构造方法都会创建一个新的套接字并尝试连接到指定的 IP 地址和端口,或者主机名和端口。
-
指定服务器地址、端口和本地端口的构造方法:
Socket(InetAddress address, int port, InetAddress localAddress, int localPort) Socket(String host, int port, InetAddress localAddress, int localPort)
这些构造方法允许你指定本地绑定的地址和端口,以及远程服务器的地址和端口。
-
指定服务器地址、端口、超时时间和操作模式的构造方法:
Socket(InetAddress address, int port, boolean stream) Socket(String host, int port, boolean stream)
这些构造方法允许你指定是否使用流模式(对于 TCP,通常设置为
true
)。
数据处理方法
Socket
类提供了几个方法来处理数据:
-
获取输入流:
InputStream getInputStream()
返回一个输入流,用于从套接字读取数据。
-
获取输出流:
OutputStream getOutputStream()
返回一个输出流,用于向套接字写入数据。
-
关闭套接字:
void close()
关闭套接字,释放系统资源。
-
连接服务器:
void connect(SocketAddress endpoint) void connect(SocketAddress endpoint, int timeout)
这些方法用于建立到指定套接字地址的连接,可选地指定连接超时时间。
-
设置套接字选项:
-
setKeepAlive(boolean on)
: 设置 TCP 保活机制。 -
setReuseAddress(boolean on)
: 允许本地地址被重用。 -
setSoLinger(boolean on, int linger)
: 设置套接字的 SO_LINGER 选项,控制关闭套接字时的行为。
-
示例代码
客户端:
import java.io.*;
import java.net.*;
public class TCPClient {
public static void main(String[] args) {
try (Socket socket = new Socket("localhost", 1234)) {
OutputStream output = socket.getOutputStream();
output.write("Hello Server!".getBytes());
InputStream input = socket.getInputStream();
byte[] buffer = new byte[1024];
int bytesRead = input.read(buffer);
System.out.println(new String(buffer, 0, bytesRead));
} catch (IOException e) {
e.printStackTrace();
}
}
}
服务器端:
import java.io.*;
import java.net.*;
public class TCPServer {
public static void main(String[] args) {
try (ServerSocket serverSocket = new ServerSocket(1234)) {
Socket socket = serverSocket.accept();
InputStream input = socket.getInputStream();
byte[] buffer = new byte[1024];
int bytesRead = input.read(buffer);
System.out.println(new String(buffer, 0, bytesRead));
OutputStream output = socket.getOutputStream();
output.write("Hello Client!".getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}
}
在这些示例中,客户端创建了一个 Socket
对象来连接服务器,并使用输入输出流来发送和接收数据。服务器端使用 ServerSocket
来监听连接请求,并接受客户端的连接,然后也使用输入输出流来处理数据。
demo1:实现一发一收
-
创建客户端的Socket对象(套接字对象),请求与 服务端的连接
-
使用socket对象调用getOutputStream()方法得到字节输出流
-
使用数据输出流(其他高级流也行,这个最适合)完成数据的发送
-
释放资源:关闭socket管道
-
创建ServerSocket的对象,同时为服务端注册端口
-
使用serverSocket对象,调用accept方法,等待客户端的连接请求得到socket对象
-
从socket通信管道中得到一个字节输入流
-
把原始的字节输入流包装为数据输入流(其他高级流也行,这个最适合)
-
使用数据输入流读取客户端发送的消息
注意:接收数据时数据输入流对象调用的方法和数据输出流对象调用的方法相对应
(最后,可以通过socket获取客户端的IP地址)
demo2:实现多发多收
改造demo1:
-
将客户端写数据部分代码块放入死循环
-
将死循环内改造为用户键盘输入(发送后,调用flush方法刷新立即写出)
-
将服务端读取客户端消息放入死循环,并把关闭套接字去掉,实现多收
-
注意:如果客户端离线,服务端处会出现异常,因此在服务端捕获异常,就可以知道客户端的在线离线状态
支持服务端与多个客户端同时通信【客户端多开】
改造demo2:
-
由于需要每一个套接字都需要一个线程负责,所以需要专门写一个服务端的读取线程类,用于接收并处理套接字
-
主要核心的接收信息功能就在run方法中写了
-
主线程中拿到套接字时可以适当的输出客户端上线状态
-
run方法中也可以添加客户端的下线状态(客户端的下线会使得服务端的接收消息处一直等待发生异常,因此需要捕获异常----提示客户端下线)
问题:
为什么UDP通信可以单线程实现多客户端收发信息,但是TCP通信不可以单线程实现多客户端收发信息?
答:
UDP(用户数据报协议)和TCP(传输控制协议)在设计上有着本质的区别,这导致了它们在处理多客户端通信时的不同方式。
UDP通信单线程模型
UDP 是无连接的协议,它的主要特点是简单和高效。在 UDP 中,数据以数据报的形式发送,每个数据报都是独立的,不保证数据报的顺序、可靠性或完整性。因此,UDP 通信可以很容易地使用单线程模型来处理多个客户端:
-
无状态:UDP 服务器不需要维护任何关于客户端状态的信息,每个数据报都是独立的,服务器可以简单地接收和发送数据报,而不需要跟踪任何特定的客户端。
-
并发处理:由于 UDP 是无连接的,服务器可以同时接收来自多个客户端的数据报,并且不需要为每个客户端创建独立的连接或线程。
-
简单性:UDP 的简单性使得它很容易实现一个单线程的服务器,该服务器可以循环接收数据报,并根据需要广播或转发给其他客户端。
TCP通信多线程或非阻塞模型
TCP 是面向连接的协议,它提供了数据的顺序、可靠性和完整性保证。TCP 连接需要经过三次握手过程来建立,并且在连接期间,服务器需要维护每个客户端的状态信息:
-
连接状态:TCP 服务器需要为每个客户端维护一个连接状态,这包括序列号、确认号、窗口大小等信息。
-
顺序保证:TCP 保证数据的顺序,因此服务器需要确保数据按照正确的顺序发送和接收。
-
流量控制和拥塞控制:TCP 需要处理流量控制和拥塞控制,这需要服务器为每个连接维护额外的状态信息。
-
资源消耗:由于 TCP 连接需要更多的资源和处理,因此当客户端数量增加时,单线程模型可能会成为性能瓶颈。
-
并发限制:单线程处理多个 TCP 连接可能会导致性能问题,因为每个连接都需要进行数据的读取和写入,这可能会阻塞线程,影响其他连接的处理。
因此,为了有效地处理多个 TCP 客户端,通常需要使用多线程模型或非阻塞 I/O(如 Java 的 NIO):
-
多线程模型:为每个客户端连接创建一个新线程,这样可以并行处理多个连接,但可能会消耗大量资源。
-
非阻塞 I/O:使用非阻塞 I/O 和事件驱动的方法,如 Java 的 NIO(New I/O)库,可以更高效地处理多个连接,而不需要为每个连接创建一个线程。
总之,UDP 的无连接特性和简单性使其适合于单线程模型,而 TCP 的面向连接特性和对可靠性的保证使其更适合于多线程或非阻塞 I/O 模型。