这是一个 socket 类,本质上相当于一个文件,在系统中,还有一种特殊的 socket 文件,对应到网卡设备。
构造一个 DatagramSocket
对象,就相当于打开了一个内核中的 socket 文件
构造方法:
构造方法 | 说明 |
---|---|
DatagramSocket() |
构造一个数据报套接字,并将其绑定到本地主机上的任何可用端口 |
DatagramSocket(int port) |
构造一个数据报套接字,并将其绑定到本地主机上的指定端口 |
普通方法:
方法 | 说明 |
---|---|
void receive(DatagramPacket p) |
从该套接字接收数据报,此方法会一直阻塞,直到接收到数据报为止 |
void send(DatagramPacket p) |
从此套接字发送数据报 |
void close() |
关闭此数据报套接字 |
表示一个 UDP 数据报,UDP 是面向数据报的协议,传输数据就是以 DatagramPacket
为基本单位
构造方法 | 说明 |
---|---|
DatagramPacket(byte buf[], int length) |
构造一个 DatagramPacket ,用于接收长度为 length 的数据包,length 参数必须小于或等于 buf.length |
DatagramPacket(byte buf[], int length, SocketAddress address) |
构造一个数据报,用于将长度为 length 的数据报发送到指定主机上的指定端口号。length 参数必须小于或等于 buf.length |
DatagramPacket(byte buf[], int offset, int length, SocketAddress address) |
构造一个数据报,用于将偏移量为 ioffset 的 length 长度的数据报发送到指定主机上的指定端口号。length 参数必须小于或等于 buf.length |
DatagramPacket(byte buf[], int length, InetAddress address, int port) |
构造一个数据报,用于将长度为 length 的数据包发送到指定主机上的指定端口号。length 参数必须小于或等于 buf.length |
方法 | 说明 |
---|---|
InetAddress getAddress() |
返回发送此数据报或接收数据报的机器的 IP 地址 |
SocketAddress getSocketAddress() |
获取此数据包发送到或来自的远程主机的 SocketAddress (通常为IP地址+端口号) |
int getPort() |
返回发送的数据报中的接收端主机端口号,或者接收的数据报中的发送端主机端口号 |
byte[] getData() |
返回数据缓冲区 |
int getLength() |
返回要发送的数据长度或接收的数据长度 |
InetSocketAddress
创建 DatagramPacket
时,需要 SocketAddress
,该对象通过 InetSocketAddress
创建。
构造方法 | 说明 |
---|---|
InetSocketAddress(InetAddress addr, int port) |
根据 IP 地址和端口号创建套接字地址 |
服务器:
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class UDPEchoServer {
private final DatagramSocket socket;
public UDPEchoServer(int port) throws SocketException {
socket = new DatagramSocket(port);
}
public void start() throws IOException {
System.out.println("服务器 启动!");
while (true) {
// 读取客户端发来的请求
DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096); // 空的,用于接收响应
socket.receive(requestPacket);
// 对请求进行解析,把 DatagramPacket 转成 String
String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
// 处理响应
String response = process(request);
// 把响应构造成 DatagramPacket 对象
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),
response.getBytes().length, requestPacket.getSocketAddress());
// 把响应发送给客户端
socket.send(responsePacket);
System.out.printf("[%s:%d] req=%s;resp=%s\n", requestPacket.getAddress().toString(),
requestPacket.getPort(), request, response);
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
UDPEchoServer server = new UDPEchoServer(8000);
server.start();
}
}
客户端:
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class UDPEchoClient {
private final DatagramSocket socket;
public UDPEchoClient() throws SocketException {
// 客户端端口一般自动分配
socket = new DatagramSocket();
}
public void start() throws IOException {
Scanner scanner = new Scanner(System.in);
while (true) {
// 客户端从控制台读取数据
System.out.print("> ");
String request = scanner.next();
// 构造 DatagramPacket
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),
request.getBytes().length, InetAddress.getByName("127.0.0.1"), 8000);
// 发送给服务器
socket.send(requestPacket);
// 从服务器读取响应
DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(responsePacket);
// 把响应数据转成字符串
String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
System.out.printf("req: %s; resp: %s\n", request, response);
}
}
public static void main(String[] args) throws IOException {
UDPEchoClient client = new UDPEchoClient();
client.start();
}
}
因为这里的 socket 创建出来就会一直用,所以是伴随程序的整个生命周期的,所以不需要手动调用 close()
去关闭
技巧:Windows 使用
netstat -ano | findstr "端口号"
,可以查找占用该端口的进程pid
ServerSocket
是创建 TCP 服务端 Socket 的 API
构造方法 | 说明 |
---|---|
ServerSocket(int port) |
创建绑定到指定端口的服务器套接字 |
方法 | 说明 |
---|---|
Socket accept() |
侦听要与此套接字建立的连接并接受该连接。该方法将阻塞,直到建立连接为止。 |
void close() |
关闭此套接字,相当于发送 FIN |
构造方法 | 说明 |
---|---|
Socket(String host, int port) |
创建流套接字并将其连接到命名主机上的指定端口号 |
方法 | 说明 |
---|---|
InetAddress getInetAddress() |
返回套接字所连接的地址 |
int getPort() |
返回此套接字所连接的远程端口号 |
InputStream getInputStream() |
返回此套接字的输入流 |
OutputStream getOutputStream() |
返回此套接字的输出流 |
服务端:
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;
public class TCPEchoServer {
private final ServerSocket serverSocket;
public TCPEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器 启动!");
while (true) {
Socket clientSocket = serverSocket.accept();
// 创建新线程去完成工作,主线程继续accept
Thread t = new Thread(() -> {
try {
processConnect(clientSocket);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
t.start();
}
}
// 短连接:一个连接只进行一次数据交互(一个请求 + 一个响应)
// 长连接:一个连接进行多次数据交互(N 个请求 + N 个响应)
public void processConnect(Socket clientSocket) throws IOException {
System.out.printf("[%s:%d] 建立连接\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
try (InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()) {
Scanner scanner = new Scanner(inputStream);
PrintWriter printWriter = new PrintWriter(outputStream);
// 长连接
while (true) {
if (!scanner.hasNext()) {
System.out.printf("[%s:%d] 断开连接\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
break;
}
// 读取请求并解析
String request = scanner.next();
// 根据请求计算响应
String response = process(request);
// 把响应写回客户端
printWriter.println(response); // 注意补上空白符,如换行,对面next读的时候要读到空白符才往下走
printWriter.flush();
System.out.printf("[%s:%d] req: %s, resp: %s\n", clientSocket.getInetAddress().toString(),
clientSocket.getPort(), request, response);
}
} finally {
// 一定要记得关闭 clientSocket
// 因为它是 accept 创建出来的,每来一个连接就会创建一个,占用文件描述符资源
clientSocket.close();
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TCPEchoServer server = new TCPEchoServer(8000);
server.start();
}
}
客户端:
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;
public class TCPEchoClient {
private final Socket socket;
public TCPEchoClient() throws IOException {
socket = new Socket("127.0.0.1", 8000); // 此时触发三次握手
}
public void start() throws IOException {
Scanner scanner = new Scanner(System.in);
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
Scanner scannerNet = new Scanner(inputStream);
PrintWriter printWriter = new PrintWriter(outputStream);
while (true) {
// 从控制台读取用户输入
System.out.print("> ");
String request = scanner.next();
// 把请求发送给服务器
printWriter.println(request); // 注意补上空白符,如换行,对面next读的时候要读到空白符才往下走
printWriter.flush();
// 从服务器读取响应
String response = scannerNet.next();
System.out.printf("req: %s; resp: %s\n", request, response);
}
}
}
public static void main(String[] args) throws IOException {
TCPEchoClient client = new TCPEchoClient();
client.start();
}
}
上述服务端代码还可以使用线程池改进:
public void start() throws IOException {
System.out.println("服务器 启动!");
// 使用线程池,适合写自动扩容版本的
ExecutorService service = Executors.newCachedThreadPool();
while (true) {
Socket clientSocket = serverSocket.accept();
service.submit(() -> {
try {
processConnect(clientSocket);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
}
注意:
这里的 TCP 服务器之所以使用多线程,是因为处理的是长连接,与客户端建立好连接之后,什么时候断开连接不确定,这一个连接里要处理多少请求,也不确定,单线程处理连接里的循环的时候,就无法 accept 新的连接了。
如果是短连接,每次连接只处理一个请求,就可以不使用多线程了。