抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

摘要:本文学习了网络编程的相关知识,包括TCP网络编程和UDP网络编程,以及如何使用RMI远程调用。

环境

Windows 10 企业版 LTSC 21H2
Java 1.8

1 基础

1.1 定义

网络编程就是在两个或两个以上的设备之间传输数据。程序员所作的事情就是把数据发送到指定的位置,或者接收到指定的数据,这个就是狭义的网络编程范畴。

网络编程的基本模型就是客户机到服务器模型,简单的说就是两个进程之间相互通讯,然后其中一个提供固定位置,而另一个则需要知道这个固定位置,就能建立两者之间的联系,然后完成数据的通讯就可以了。提供固定位置的通常称为服务器,建立联系的通常称为客户端。

Java从语言级上提供了对网络应用程序的支持,程序员能够很容易开发常见的网络应用程序,联网的底层细节被隐藏在安装系统里,由JVM进行控制。

1.2 网络通信

网络通信中的两个要素

  • IP地址和端口号:用于在网络上找到主机地址和主机上的特定应用。
  • 网络协议,用于可靠高效地进行数据传输,有两套参考模型:
    1. OSI参考模型:模型过于理想化,未能在因特网上进行广泛推广。
    2. TCP/IP参考模型(或TCP/IP协议):事实上的国际标准,还可以细分为四层和五层。

1.3.1 IP地址

为了解决如何在网络上找到主机地址的问题,引入了IP地址和域名(Domain Name)的概念。

使用IP地址能够在网络上唯一标识网络设备,但由于IP地址不容易记忆,又创造用于映射IP地址的域名,一个IP地址可以对应多个域名,一个域名只能对应一个IP地址。

在实际传输数据前需要将域名转换为IP地址,实现这种功能的服务器称为DNS服务器,也就是域名解析。

1.3.2 端口号

为了解决如何在主机上找到特定应用的问题,引入了端口(Port)的概念。

使用端口能够在主机中唯一标识应用,在主机上可以通过端口区分发送给每个应用的数据,实现了多个网络程序在共同的主机上运行,并且不会互相干扰。

1.3.3 网络协议

有了IP地址和端口号以后,在进行网络通讯交换时,就可以通过IP地址查找到主机,然后通过端口标识程序,这样就可以进行网络数据的交换了。

为了解决如何保证数据交换的安全可靠,引入了网络协议(Protocol)的概念。

网络协议用于在实际进行数据交换时规定数据的格式,避免格式不同导致的数据识别错误等问题。

常见的网络模型对比如下:

OSI七层网络模型 TCP/IP四层网络模型 TCP/IP五层网络模型 网络协议 工作设备
应用层 应用层 应用层 HTTP HTTPS FTP SMTP POP3 计算机及应用
表示层
会话层
传输层 传输层 传输层 TCP UDP 四层交换机 四层路由器
网络层 网络层 网络层 IP ICMP ARP RARP 三层交换机 路由器 网关
数据链路层 网络接口层 数据链路层 Ethernet PPP 交换机 网桥
物理层 物理层 USB 中继器 集线器

TCP和UDP比较:

特性 TCP UDP
是否连接 面向连接。 无连接。
是否可靠 可靠传输,使用流量控制和拥塞控制。 不可靠传输,不使用流量控制和拥塞控制。
连接对象个数 只能是一对一通信。 支持一对一,一对多,多对一和多对多交互通信。
传输方式 面向字节流。 面向报文。
首部开销 首部最小20字节,最大60字节。 首部开销小,仅8字节。
适用场景 适用于要求可靠传输的应用,例如文件传输。 适用于实时应用,例如IP电话、视频会议。

1.3 统一资源定位符

统一资源定位符(URL,Uniform Resource Locator),表示网络上某一资源的地址,通过URL可以访问网络上的各种资源。

URL主要由协议和资组成,语法:

java
1
传输协议://主机名:端口号/文件名#内部引用?参数列表。

1.3.1 URL

构造方法:

java
1
2
3
4
// 根据String表示形式创建URL对象
public URL(String spec);
// 根据指定protocol、host、port和file创建URL对象
public URL(String protocol, String host, int port, String file);

常用方法:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 获取此URL的协议名称
public String getProtocol();
// 获取此URL的主机名(如果适用)
public String getHost();
// 获取此URL的端口号
public int getPort();
// 获取此URL的文件名
public String getFile();
// 获取此URL的内部引用
public String getRef();
// 获取此URL的路径部分
public String getPath();
// 读取网络资源
public final InputStream openStream();
// 创建URLConnection实例对象
public URLConnection openConnection();

示例:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) {
try {
URL url = new URL("http://www.gamelan.com:80/Gamelan/network.html#BOTTOM");
System.out.println(url.getProtocol());
System.out.println(url.getHost());
System.out.println(url.getPort());
System.out.println(url.getFile());
System.out.println(url.getRef());
System.out.println(url.getPath());
} catch (MalformedURLException e) {
e.printStackTrace();
}
}

结果:

java
1
2
3
4
5
6
http
www.gamelan.com
80
/Gamelan/network.html
BOTTOM
/Gamelan/network.html

1.3.2 URLConnection

URLConnection表示到URL所引用的远程对象的连接。当与URL建立连接时,首先要生成对应的URLConnection对象。如果连接过程失败,将产生IOException。

获取方法:

java
1
2
// 通过URL类的方法获取URLConnection实例
public URLConnection openConnection();

常用方法:

java
1
2
3
4
// 获取输入流
public InputStream getInputStream();
// 获取输出流
public OutputStream getOutputStream();

2 TCP网络编程

2.1 常用类

2.1.1 InetAddress

此类表示互联网协议(IP)地址。

常用方法:

java
1
2
3
4
5
6
7
8
// 根据IP地址或者域名获取InetAddress实例
public static InetAddress getByName(String host);
// 获取IP地址为本地的InetAddress实例
public static InetAddress getLocalHost();
// 获取InetAddress实例的主机名
public String getHostName();
// 获取InetAddress实例的IP地址
public String getHostAddress();

示例:

java
1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
try {
InetAddress baidu = InetAddress.getByName("www.baidu.com");
InetAddress local = InetAddress.getLocalHost();
System.out.println(baidu.getHostName());
System.out.println(local.getHostAddress());
} catch (UnknownHostException e) {
e.printStackTrace();
}
}

结果:

java
1
2
www.baidu.com
192.168.1.109

2.1.2 InetSocketAddress

此类实现IP套接字地址(IP地址和端口号)。

构造方法:

java
1
2
3
4
5
6
// 根据端口创建本地套接字地址
public InetSocketAddress(int port);
// 根据IP地址和端口号创建套接字地址
public InetSocketAddress(InetAddress addr, int port);
// 根据主机名和端口号创建套接字地址
public InetSocketAddress(String hostname, int port);

常用方法:

java
1
2
3
4
5
6
7
8
// 获取InetAddress实例
public final InetAddress getAddress();
// 获取主机名
public final String getHostName();
// 获取端口号
public final int getPort();
// 构造此InetSocketAddress的字符串表示形式(主机名/IP:端口号)
public String toString();

示例:

java
1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) {
try {
InetSocketAddress local = new InetSocketAddress(InetAddress.getLocalHost(), 80);
InetSocketAddress baidu = new InetSocketAddress("www.baidu.com", 80);
System.out.println(baidu);
System.out.println(baidu.getHostName());
System.out.println(local.getAddress().getHostAddress());
System.out.println(local.getPort());
} catch (UnknownHostException e) {
e.printStackTrace();
}
}

结果:

java
1
2
3
4
www.baidu.com/182.61.200.7:80
www.baidu.com
192.168.1.109
80

2.1.3 Socket

此类实现客户端套接字(也可以就叫“套接字”)。套接字是两台机器间通信的端点。

构造方法:

java
1
2
3
4
// 创建一个流套接字并将其连接到指定IP地址的指定端口号
public Socket(InetAddress address, int port);
// 创建一个流套接字并将其连接到指定主机上的指定端口号
public Socket(String host, int port);

常用方法:

java
1
2
3
4
5
6
// 返回此套接字的输出流
public OutputStream getOutputStream();
// 返回此套接字的输入流
public InputStream getInputStream();
// 关闭此套接字
public void close();

2.1.4 ServerSocket

此类实现服务器套接字。服务器套接字等待请求通过网络传入。

构造方法:

java
1
2
// 创建绑定到特定端口的服务器套接字
public ServerSocket(int port);

常用方法:

java
1
2
3
4
// 侦听并接受到此套接字的连接
public Socket accept();
// 关闭此套接字
public void close();

2.2 客户端

2.2.1 建立连接

在客户端网络编程中,首先需要建立连接。

示例:

java
1
Socket s = new Socket("192.168.1.103", 8800);

使用服务端8800号端口建立连接,如果建立连接时本机网络不通,或服务器端程序未开启,会抛出异常。

2.2.2 数据交换

建立连接后,需要获得输入流和输出流与服务器端进行通信。使用输出流发送数据到服务器,使用输入流接收服务器发送的数据。

示例:

java
1
2
OutputStream os = s.getOutputStream();// 获得输出流
InputStream is = s.getInputStream();// 获得输入流

2.2.3 关闭连接

当数据交换完成以后,关闭网络连接,释放网络连接占用的系统端口和内存等资源。

示例:

java
1
s.close();

2.3 服务端

2.3.1 监听端口

在服务器端网络编程中,由于服务器端实现的是被动等待连接,所以首先要监听是否有客户端连接请求。

示例:

java
1
ServerSocket ss = new ServerSocket(8800);

如果服务端8800号端口已经被别的程序占用,那么将抛出异常。

2.3.2 获得连接

当有客户端发起连接请求时建立连接。

示例:

java
1
Socket s = ss.accept();

2.3.3 数据交换

获得连接后,需要获得输入流和输出流与客户端端进行通信。使用输入流读取客户端发送的数据,使用输出流发送数据到客户端,服务端交换数据的顺序和客户端刚好相反。

示例:

java
1
2
OutputStream os = s.getOutputStream();// 获得输出流
InputStream is = s.getInputStream();// 获得输入流

2.3.4 关闭连接

当数据交换完成以后,关闭网络连接,同时关闭监听。

示例:

java
1
2
s.close();
ss.close();

2.4 发送消息

客户端示例:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public static void main(String[] args) {
Socket s = null;
OutputStream os = null;
try {
System.out.println("客户端已启动");
s = new Socket(InetAddress.getLocalHost(), 8800);
os = s.getOutputStream();
os.write("你好,我是客户端".getBytes());
System.out.println("客户端成功发送数据");
} catch (IOException e) {
e.printStackTrace();
} finally {
if (os != null) {
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (s != null) {
try {
s.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

服务端示例:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public static void main(String[] args) {
ServerSocket ss = null;
Socket s = null;
InputStream is = null;
ByteArrayOutputStream baos = null;
try {
System.out.println("服务端已启动");
ss = new ServerSocket(8800);
s = ss.accept();
is = s.getInputStream();
byte[] temp = new byte[1024];
int len;
baos = new ByteArrayOutputStream();
while ((len = is.read(temp)) != -1) {
baos.write(temp, 0, len);
}
System.out.println("服务端成功接收数据:" + baos.toString());
} catch (IOException e) {
e.printStackTrace();
} finally {
if (baos != null) {
try {
baos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (is != null) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (s != null) {
try {
s.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (ss != null) {
try {
ss.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

客户端结果:

java
1
2
客户端已启动
客户端成功发送数据

服务端结果:

java
1
2
服务端已启动
服务端成功接收数据:你好,我是客户端

2.5 响应消息

客户端示例:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public static void main(String[] args) {
Socket s = null;
OutputStream os = null;
InputStream is = null;
ByteArrayOutputStream baos = null;
try {
System.out.println("客户端已启动");
s = new Socket(InetAddress.getLocalHost(), 8800);
os = s.getOutputStream();
os.write("你好,我是客户端".getBytes());
System.out.println("客户端成功发送数据");
s.shutdownOutput();

is = s.getInputStream();
byte[] temp = new byte[1024];
int len;
baos = new ByteArrayOutputStream();
while ((len = is.read(temp)) != -1) {
baos.write(temp, 0, len);
}
System.out.println("客户端成功接收服务端回传数据:" + baos.toString());
} catch (IOException e) {
e.printStackTrace();
} finally {
if (os != null) {
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (s != null) {
try {
s.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (baos != null) {
try {
baos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (is != null) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

服务端示例:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
public static void main(String[] args) {
ServerSocket ss = null;
Socket s = null;
InputStream is = null;
ByteArrayOutputStream baos = null;
OutputStream os = null;
try {
System.out.println("服务端已启动");
ss = new ServerSocket(8800);
s = ss.accept();
is = s.getInputStream();
byte[] temp = new byte[1024];
int len;
baos = new ByteArrayOutputStream();
while ((len = is.read(temp)) != -1) {
baos.write(temp, 0, len);
}
System.out.println("服务端成功接收数据:" + baos.toString());

os = s.getOutputStream();
os.write("你好,我是服务端".getBytes());
System.out.println("服务端成功发送回传数据");
} catch (IOException e) {
e.printStackTrace();
} finally {
if (baos != null) {
try {
baos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (is != null) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (s != null) {
try {
s.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (ss != null) {
try {
ss.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (os != null) {
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

客户端结果:

java
1
2
3
客户端已启动
客户端成功发送数据
客户端成功接收服务端回传数据:你好,我是服务端

服务端结果:

java
1
2
3
服务端已启动
服务端成功接收数据:你好,我是客户端
服务端成功发送回传数据

3 UDP网络编程

3.1 常用类

3.1.1 DatagramSocket

此类表示用来发送和接收数据报包的套接字。

构造方法:

java
1
2
3
4
5
6
// 创建数据报套接字,将其绑定到本地地址上的随机端口
public DatagramSocket();
// 创建数据报套接字,将其绑定到本地地址上的指定端口
public DatagramSocket(int port);
// 创建数据报套接字,将其绑定到指定地址上的指定端口
public DatagramSocket(int port, InetAddress laddr);

常用方法:

java
1
2
3
4
5
6
// 从此套接字发送数据报包
public void send(DatagramPacket p);
// 从此套接字接收数据报包
public synchronized void receive(DatagramPacket p);
// 关闭此数据报套接字
public void close();

3.1.2 DatagramPacket

此类表示数据报包。

构造方法:

java
1
2
3
4
5
6
7
8
// 用来接收数据包
public DatagramPacket(byte buf[], int length);
// 用来接收数据包
public DatagramPacket(byte buf[], int offset, int length);
// 用来将数据包发送到指定主机上的指定端口
public DatagramPacket(byte buf[], int length, InetAddress address, int port);
// 用来将数据包发送到指定主机上的指定端口
public DatagramPacket(byte buf[], int offset, int length, InetAddress address, int port);

常用方法:

java
1
2
3
4
// 从数据报包获得IP地址
public synchronized InetAddress getAddress();
// 从数据报包获得端口号
public synchronized int getPort();

3.2 客户端

3.2.1 建立连接

与TCP建立连接不同,使用UDP建立连接不需要指定服务器的IP和端口号。

示例:

java
1
DatagramSocket ds = new DatagramSocket();

可以指定客户端连接使用的端口号。

示例:

java
1
DatagramSocket ds = new DatagramSocket(8811);

使用客户端8811号端口建立连接,一般在建立客户端连接时没有必要指定端口号。

3.2.2 发送数据

在UDP网络编程中,不需要使用IO流,将数据内容以及服务器的IP地址和端口号封装发送即可。

示例:

java
1
2
3
byte[] buffer = "".getBytes();
DatagramPacket dp = new DatagramPacket(buffer, 0, buffer.length, InetAddress.getLocalHost(), 8800);
ds.send(dp);

按照UDP协议的约定,不保证数据一定被正确传输,如果数据在传输过程中丢失,那就丢失了。

3.2.3 接收数据

在UDP网络编程中,不需要使用IO流,将数据内容封装接收即可。

示例:

java
1
2
3
byte[] buffer = new byte[1024];
DatagramPacket dp = new DatagramPacket(buffer, 0, buffer.length);
ds.receive(dp);

3.2.4 关闭连接

当数据交换完成以后,关闭网络连接。

示例:

java
1
ds.close();

需要说明的是,和TCP建立连接的方式不同,在UDP建立连接时没有固定的IP地址和端口号,可以将数据包发送到不同的IP地址和端口号,这点是TCP无法做到的。

3.3 服务端

3.3.1 建立连接

与UDP建立连接类似,但是需要指定服务端连接使用的端口号。

示例:

java
1
DatagramSocket ds = new DatagramSocket(8800);

使用服务端8800号端口建立连接,由于服务器端的端口需要固定,所以一般在建立服务器端连接时都指定端口号。

3.3.2 接收数据

将数据内容封装接收即可。

示例:

java
1
2
3
byte[] buffer = new byte[1024];
DatagramPacket dp = new DatagramPacket(buffer, 0, buffer.length);
ds.receive(dp);

3.3.3 发送数据

将数据内容以及客户端的IP地址和端口号封装发送即可。

示例:

java
1
2
3
byte[] buffer = "".getBytes();
DatagramPacket dp = new DatagramPacket(buffer, 0, buffer.length, InetAddress.getLocalHost(), 8811);
ds.send(dp);

建议通过前面接收数据的数据报包获取客户端的IP地址和端口号发送数据。

示例:

java
1
2
InetAddress address = dp.getAddress();
int port = dp.getPort();

3.3.4 关闭连接

当数据交换完成以后,关闭网络连接。

示例:

java
1
ds.close();

3.4 发送消息

客户端示例:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) {
try {
System.out.println("客户端已启动");
DatagramSocket ds = new DatagramSocket();
byte[] buffer = "我是客户端".getBytes();
DatagramPacket dp = new DatagramPacket(buffer, 0, buffer.length, InetAddress.getLocalHost(), 8800);
ds.send(dp);
System.out.println("客户端成功发送数据");
ds.close();
} catch (IOException e) {
e.printStackTrace();
}
}

服务端示例:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) {
try {
System.out.println("服务端已启动");
DatagramSocket ds = new DatagramSocket(8800);
byte[] buffer = new byte[1024];
DatagramPacket dp = new DatagramPacket(buffer, 0, buffer.length);
ds.receive(dp);
System.out.println("服务端成功接收数据:" + new String(dp.getData(), 0, dp.getLength()));
ds.close();
} catch (IOException e) {
e.printStackTrace();
}
}

客户端结果:

java
1
2
客户端已启动
客户端成功发送数据

服务端结果:

java
1
2
服务端已启动
服务端成功接收数据:我是客户端

3.5 响应消息

客户端示例:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public static void main(String[] args) {
try {
System.out.println("顾客已上线");
Scanner scan = new Scanner(System.in);
DatagramSocket ds = new DatagramSocket(8811);
while (true) {
String message = scan.next();
byte[] bufMes = message.getBytes();
DatagramPacket dpMes = new DatagramPacket(bufMes, 0, bufMes.length, InetAddress.getLocalHost(), 8800);
ds.send(dpMes);
byte[] bufRec = new byte[1024];
DatagramPacket dpRec = new DatagramPacket(bufRec, 0, bufRec.length);
ds.receive(dpRec);
String receive = new String(dpRec.getData(), 0, dpRec.getLength());
System.out.println("客服说:" + receive);
if (message.equals("bye")) {
break;
}
}
scan.close();
ds.close();
} catch (IOException e) {
e.printStackTrace();
}
}

服务端示例:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public static void main(String[] args) {
try {
System.out.println("客服已上线");
Scanner scan = new Scanner(System.in);
DatagramSocket ds = new DatagramSocket(8800);
while (true) {
byte[] bufRec = new byte[1024];
DatagramPacket dpRec = new DatagramPacket(bufRec, 0, bufRec.length);
ds.receive(dpRec);
String receive = new String(dpRec.getData(), 0, dpRec.getLength());
System.out.println("顾客说:" + receive);
String message = scan.next();
byte[] bufMes = message.getBytes();
DatagramPacket dpMes = new DatagramPacket(bufMes, 0, bufMes.length, InetAddress.getLocalHost(), 8811);
ds.send(dpMes);
if (message.equals("bye")) {
break;
}
}
scan.close();
ds.close();
} catch (IOException e) {
e.printStackTrace();
}
}

客户端结果:

java
1
2
3
4
5
6
7
8
9
10
11
顾客已上线
客服您好!
客服说:您好,请问有什么事情吗?
我要退货
客服说:好的,请填写退货单号
退货单号已经填好了
客服说:退货成功!请问还有其他事情吗?
没有了,再见!
客服说:好的,再见!
bye
客服说:bye

服务端结果:

java
1
2
3
4
5
6
7
8
9
10
11
客服已上线
顾客说:客服您好!
您好,请问有什么事情吗?
顾客说:我要退货
好的,请填写退货单号
顾客说:退货单号已经填好了
退货成功!请问还有其他事情吗?
顾客说:没有了,再见!
好的,再见!
顾客说:bye
bye

4 RMI远程调用

1.1 通信方式

常见的通信方式分为两种:

  • 基于RPC远程调用的同步方式。
  • 基于中间件代理的异步方式。

1.1.1 RPC远程调用

基于RPC远程调用的同步方式:
20250508095219-RPC远程调用

RPC(Remote Procedure Call,远程过程调用)是一种服务通信的调用方式,并不是具体的协议。

在RPC远程调用方式下,不同系统之间直接调用通信,每个请求直接从调用方发送到被调用方,然后要求被调用方返回响应结果给调用方,以确定本次调用结果是否成功。

此处的同步并不代表调用方式,RPC远程调用也可以有异步非阻塞的调用方式,但本质上仍然是需要在指定时间内得到被调用方的直接响应。

RPC的实现有很多,比如最早的CORBA和RMI,以及最近的WebService和Dubbo,甚至也可以将RESTful API看做是RPC的实现。

1.1.2 中间件代理

基于中间件代理的异步方式:
20250508095916-中间件代理

在中间件代理方式下,各子系统之间无需强耦合直接连接,调用方只需要将请求转化成异步事件(通常为异步消息)发送给中间代理,发送成功即可认为该异步链路调用完成,剩下的工作会由中间代理负责将事件可靠通知到下游的调用系统,确保任务执行完成。

中间件代理一般使用消息中间件,比如老牌的ActiveMQ和RabbitMQ,炙手可热的Kafka,以及阿里巴巴自主研发的RocketMQ。

1.2 定义

RMI(Remote Method Invocation,远程方法调用)是Java中的一种远程通信协议,允许程序利用序列化机制远程调用方法,可以看做是RPC的一种实现。

RMI与Socket的区别:

  • Socket独立于开发语言,客户端和服务端可以使用不同的开发语言。RMI和Java语言绑定,客户端和服务端都必须使用Java语言开发。
  • Socket属于传输层协议,使用TCP协议和UDP协议进行通信。RMI属于应用层协议,传输层使用Java远程消息交换协议(JRMP,Java Remote Messaging Protocol)进行通信。
  • Socket更灵活,可以控制序列化机制。RMI更方便,在Socket的基础上增加了对象序列化机制。
  • Socket占用的带宽更少,适合需要传输大量数据的场景。RMI占用的宽带较多,适合处理需要逻辑计算的场景。

1.3 使用

1.3.1 公共接口

编写公共接口,暴露服务:

java
1
2
3
4
5
6
7
8
9
package base;

import java.rmi.Remote;
import java.rmi.RemoteException;

// 必须继承Remote接口,因为网络通信是不可靠的,所以要抛出RemoteException异常
public interface BaseService extends Remote {
String hello() throws RemoteException;
}

1.3.2 客户端

编写客户端启动类:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package client;

import base.BaseService;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class MainApp {
public static void main(String[] args) throws Exception {
// 连接注册中心
Registry registry = LocateRegistry.getRegistry("localhost", 8888);
// 查找BaseService服务
BaseService baseService = (BaseService) registry.lookup("BaseService");
// 调用方法
String hello = baseService.hello();
// 打印结果
System.out.println(hello);
}
}

1.3.3 服务端

编写服务端实现类:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package server;

import base.BaseService;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

// 必须实现公共接口,必须继承UnicastRemoteObject类用于暴露远程服务
public class ServerServiceImpl extends UnicastRemoteObject implements BaseService {
protected ServerServiceImpl() throws RemoteException {
super();
}
@Override
public String hello() throws RemoteException {
System.out.println("call hello()");
return "hello";
}
}

编写服务端启动类:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package server;

import base.BaseService;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class MainApp {
public static void main(String[] args) throws Exception {
// 创建注册中心
Registry registry = LocateRegistry.createRegistry(8888);
// 创建BaseService服务
BaseService baseService = new ServerServiceImpl();
// 注册服务
registry.rebind("BaseService", baseService);
}
}

1.3.4 运行

先启动服务端启动类,然后启动客户端启动类。

1.4 原理

流程如下:
20250508164639-RMI原理

概念说明:

  • Stub:客户端的代理,对开发人员屏蔽了远程方法调用的细节。
  • Skeleton:服务端的代理,对开发人员屏蔽了远程方法调用的细节。

评论