文章目录
- UDP Socket API
- DatagramSocket
- DatagramPacket
- 例子:UDP版本的回显服务器-客户端
- TCP Socket API
- ServerSocket
- Socket
- 例子:TCP版本的回显服务器-客户端
UDP Socket API
DatagramSocket
这是一个 socket 类,本质上相当于一个文件,在系统中,还有一种特殊的 socket 文件,对应到网卡设备。
构造一个 DatagramSocket
对象,就相当于打开了一个内核中的 socket 文件
构造方法:
构造方法 | 说明 |
---|---|
DatagramSocket() | 构造一个数据报套接字,并将其绑定到本地主机上的任何可用端口 |
DatagramSocket(int port) | 构造一个数据报套接字,并将其绑定到本地主机上的指定端口 |
普通方法:
方法 | 说明 |
---|---|
void receive(DatagramPacket p) | 从该套接字接收数据报,此方法会一直阻塞,直到接收到数据报为止 |
void send(DatagramPacket p) | 从此套接字发送数据报 |
void close() | 关闭此数据报套接字 |
DatagramPacket
表示一个 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 地址和端口号创建套接字地址 |
例子:UDP版本的回显服务器-客户端
服务器:
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 转成 StringString 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();// 构造 DatagramPacketDatagramPacket 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
TCP Socket API
ServerSocket
ServerSocket
是创建 TCP 服务端 Socket 的 API
构造方法 | 说明 |
---|---|
ServerSocket(int port) | 创建绑定到指定端口的服务器套接字 |
方法 | 说明 |
---|---|
Socket accept() | 侦听要与此套接字建立的连接并接受该连接。该方法将阻塞,直到建立连接为止。 |
void close() | 关闭此套接字,相当于发送 FIN |
Socket
构造方法 | 说明 |
---|---|
Socket(String host, int port) | 创建流套接字并将其连接到命名主机上的指定端口号 |
方法 | 说明 |
---|---|
InetAddress getInetAddress() | 返回套接字所连接的地址 |
int getPort() | 返回此套接字所连接的远程端口号 |
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;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();// 创建新线程去完成工作,主线程继续acceptThread 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 新的连接了。
如果是短连接,每次连接只处理一个请求,就可以不使用多线程了。