Java 网络编程

本文将介绍 Java 中的网络通信。

一、基础知识

1. 计算机网络

计算机网络,指将计算机等设备用通信线路互连成网络系统,从而实现众多设备之间信息的互通。

  • 根据计算机网络的规模大小和延伸范围,可以分为:
    • 局域网 LAN
    • 城域网 MAN
    • 广域网 WAN
  • 根据计算机网络的拓扑结构,可以分为:
    • 星型网络
    • 总线型网络
    • 环型网络
    • 树型网络
    • 星型环型网络
  • 根据计算机网络的传输介质,可以分为:
    • 双绞线网络
    • 同轴电缆网
    • 光纤网
    • 卫星网

2. 计算机网络分层

根据国际标准化组织提出的“开放系统互连参考模型”,计算机网络被分为:

  • 物理层
  • 数据链路层
  • 网络层
  • 传输层
  • 会话层
  • 表示层
  • 应用层

3. 网络编程三要素

  • IP 地址

  • 端口

  • 协议

4. IP 地址

IP 地址用于唯一地标识网络中的通信实体,两个通信实体不能共用 IP 地址。

在基于 IP 协议网络中进行网络传输,都应该使用 IP 地址进行设备的标识。

5. 端口

网络中的通信往往是应用程序之间的通信,并且一个通信实体往往有着多个应用程序。

例如:

一台电脑,安装了 QQ 和 微信,QQ 需要与 QQ 服务器进行通信,微信需要与微信服务器进行通信。

端口用于唯一地标识通信实体中的应用程序,两个应用程序不能共用端口。

端口号可以为 0~65535,通常分为以下三类:

  • 公认端口:0~1023
  • 注册端口:1024~49151
  • 动态端口/私有端口:49152~65535

6. 协议

在计算机网络中,连接和通信的规则被称为网络通信协议。它对数据的传输格式、传输速率、传输步骤等做了统一的规定,只有通信双方均遵守规定才能完成数据通信。

常见的协议有:

  • UDP 协议:不可靠,差错控制开销小,传输大小限制在 64kb 以下,不需要建立连接
  • TCP 协议:可靠,差错控制开销大,传输大小无限制,需要建立连接

二、InetAddress

1. 什么是 InetAddress?

Java 提供了 InetAddress 类用于标识 IP 地址。

2. 获取 InetAddress 对象

InetAddress 类没有构造方法,通过静态方法来获取 InetAddress 对象。

方法 说明
getByName(String str) 根据主机名 / IP 地址的字符串获取对象
getByAddress(byte[] bytes) 根据原始 IP 地址获取对象
getLocalHost() 获取本机 IP 地址对应的对象

3. 常用方法

方法 说明
getHostAddress() 获取字符串形式的 IP 地址
getHostName() 获取 IP 地址对应的主机名
isReachable(int time) 判断是否能够

三、UDP 通信

1. UDP 协议

UDP 协议,又称用户数据报协议,是无连接通信协议,是一种不可靠的网络协议。在传输时,数据的发送端和接收端不建立逻辑连接。

当发送端发送数据时,不会事先确认接收端是否存在,也不会管接收端是否受到数据;

当接收端接收数据时,并不会向发送端发出反馈。

使用 UDP 协议进行通信,消耗资源小,通信效率高,因此适用于实时性强、数据完整性要求不高的数据传输。

2. Java 中的 UDP

使用 DatagramSocket 代表 UDP 中的 Socket,用于发送和接收数据报;

使用 DatagramPacket 代表 UDP 中的 数据报,用于被发送和接收。

3. DatagramSocket

(1) 构造器

方法 说明
DatagramSocket() 创建对象,并将对象绑定至本机 IP,随机端口
DatagramSocket(int prot) 创建对象,并将对象绑定至本机 IP,指定端口
DatagramSocket(int prot, InetAddress inetAddress) 创建对象,并将对象绑定至指定 IP,指定端口

(2) 常用方法

方法 说明
send(DatagramPacket datagramPacket) 发送数据报
receive(DatagramPacket datagramPacket) 接收数据报

需要注意的是:

DatagramSocket 发送数据和接收数据时仅填入一个数据报参数,DatagramSocket 并不关心数据的来源和去向,只是进行了发送和接收的动作。

4. DatagramPacket

(1) 构造器

用于发送的 DatagramPacket 对象:

方法 说明
DatagramPacket(byte[] bytes, int length, InetAddress inetAddress, int prot) 用 bytes 数组中 0~length-1 的数据建立 DatagramPacket 对象,并指定去向
DatagramPacket(byte[] bytes, int offset, int length, InetAddress inetAddress, int prot) 用 bytes 数组中 offset~length-1 的数据建立 DatagramPacket 对象,并指定去向

用于接收的 DatagramPacket 对象:

方法 说明
DatagramPacket(byte[] bytes, int length) 用于接收数据的对象,数据会放入 bytes 数组的 0~length-1 处
DatagramPacket(byte[] bytes, int offset, int length) 用于接收数据的对象,数据会放入 bytes 数组的 offset~length-1 处

(2) 常用方法

虽然 UDP 协议并不会在发送和接收端之间建立通信,但可以通过 DatagramPacket 的方法来获取部分信息。

方法 说明
getAddress() 在发送端,获取目标 IP ;在接收端,获取发送端 IP
getProt() 在发送端,获取目标端口;在接收端,获取发送端端口

5. 发送数据

  • 用数据建立 DatagramPacket 对象,并指定去向
  • 创建 DatagramSocket 对象
  • 调用 DatagramSocket 对象的 send() 方法发送数据
  • 关闭 DatagramSocket 对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 用数据建立 DatagramPacket 对象,并指定去向
byte[] bytes = 数据.getBytes();
int length = bytes.length;
InetAddress inetAddress = InetAddress.getByName("目标IP");
int prot = 端口号
DatagramPacket datagramPacket = new DatagramPacket(bytes, length, inetAddress, prot);

// 创建 DatagramSocket 对象
DatagramSocket datagramSocket = new DatagramSocket();

// 调用 DatagramSocket 对象的 send() 方法发送数据
datagramSocket.send(datagramPacket);

// 关闭 DatagramSocket 对象
datagramSocket.close();

6. 接收数据

  • 创建用于接收数据的 DatagramSocket 对象,指定端口号

  • 创建 DatagramSocket 对象

  • 调用 DatagramSocket 对象的 receive() 方法接收数据

  • 关闭 DatagramSocket 对象

1
2
3
4
5
6
7
8
9
10
11
12
13
// 创建用于接收数据的 DatagramSocket 对象,指定端口号
DatagramSocket datagramSocket = new DatagramSocket(端口号);

// 创建 DatagramSocket 对象
byte[] bytes = new byte[1024];
int length = bytes.length;
DatagramPacket datagramPacket = new DatagramPacket(bytes, length);

// 调用 DatagramSocket 对象的 receive() 方法接收数据
datagramSocket.receive(datagramPacket);

// 关闭 DatagramSocket 对象
datagramSocket.close();

7. 示例

发送端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import Java.io.IOException;
import Java.net.*;

public class SendData {
public static void main(String[] args) throws IOException {
// 数据包
byte[] bytes = "测试数据测试数据".getBytes();
DatagramPacket datagramPacket = new DatagramPacket(bytes, bytes.length, InetAddress.getByName("192.168.56.1"), 16666);

DatagramSocket datagramSocket = new DatagramSocket();
datagramSocket.send(datagramPacket);
datagramSocket.close();
}
}

接收端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import Java.io.IOException;
import Java.net.DatagramPacket;
import Java.net.DatagramSocket;
import Java.net.SocketException;
import Java.util.Arrays;

public class GetData {
public static void main(String[] args) throws IOException {
DatagramSocket datagramSocket = new DatagramSocket(16666);

byte[] bytes = new byte[1024];
DatagramPacket datagramPacket = new DatagramPacket(bytes, bytes.length);

datagramSocket.receive(datagramPacket);

System.out.println(new String(datagramPacket.getData(), 0, datagramPacket.getLength()));

datagramSocket.close();
}
}

四、TCP 通信

先连接,再传输

1. TCP 协议

(1) 什么是 TCP 协议?

TCP 协议是一种可靠的网络协议,通过 TCP 协议可以实现可靠无差别的数据传输。

(2) 连接阶段

在通信前需要进行连接,此时需要明确客户端和服务器端,由客户端发起连接请求,经过服务器端和客户端的三次握手之后,建立网络虚拟链路。

(3) 通信阶段

在网络虚拟链路建立完成后,无需再区分服务器端和客户端,而是通过各自的 Socket 进行通信。

2. 三次握手

在 TCP 协议中,通信之前需要先建立网络虚拟链路,由客户端向服务器端发出连接请求,经过三次握手后完成连接的创建。

  • 第一次握手:客户端向服务器端发出连接请求
  • 第二次握手:服务器端收到请求后,向客户端发出通知,表明收到了连接请求
  • 第三次握手:客户端再次向服务器端发出确认信息,确认连接

3. Java 中的 TCP

在建立连接时,使用 ServerSocket 代表服务器,Socket 代表客户端;

在进行传输时,通过 Socket 产生的 IO 流来进行网络通信。

4. 服务器端

(1) ServerSocket

ServerSocket 代表服务器端。

ServerSocket 对象用于监听来自客户端的连接请求,建立连接并返回对应的 Socket 对象。

(2) 构造器

方法 说明
ServerSocket(int prot) 用指定端口构造 ServerSocket
ServerSocket(int prot, int backlog) 用指定端口构造 ServerSocket,设置连接请求队列的最大长度
ServerSocket(int prot, int backlog, InetAddress inetAddress) 用指定 IP,指定端口构造 ServerSocket,设置连接请求队列的最大长度

当机器存在多个 IP 地址时,允许显式指定 ServerSocket 要绑定的 IP 地址。

(3) 常用方法

方法 说明
accept() 用于从连接请求队列中取出一个连接请求,并创建与客户端相对应的 Socket 对象;通常情况下,服务器端应该通过循环不断接收请求;如果连接请求队列为空,则会一直等待,直至收到连接请求。
close() 用于关闭 ServerSocket 对象

5. Socket

(1) Socket

Socket 是对网络通信中两个端点的抽象,在服务器端与客户端建立网络虚拟链路后,通过 Socket 进行数据的传输。

(2) 服务器端的 Socket

服务器端的 accept() 方法会进行连接,并返回与连接的客户端 Socket 对象相对应的 Socket 对象。

(3) 客户端的 Socket

通过以下构造方法构造 Socket 对象。

方法 说明
Socket(InetAddress inetAddress, int prot) 创建连接到指定 IP,指定端口的 Socket(使用 inetAddress 对象描述指定 IP)
Socket(String str, int prot) 创建连接到指定 IP,指定端口的 Socket(使用主机名 / IP 地址的字符串描述指定 IP)
Socket(InetAddress inetAddress, int prot, InetAddress localInetAddress, int localProt) 创建连接到指定 IP,指定端口的 Socket(使用 inetAddress 对象描述指定 IP),并且指定本地 IP 和本地端口
Socket(String str, int prot, InetAddress localInetAddress, int localProt) 创建连接到指定 IP,指定端口的 Socket(使用主机名 / IP 地址的字符串描述指定 IP),并且指定本地 IP 和本地端口

(4) 常用方法

方法 说明
getInputStream() 获取 Socket 对象对应的输入流
getOutputStream() 获取 Socket 对象对应的输出流
shutdownInput() 关闭输入流
shutdownOutput() 关闭输出流

(5) 连接超时

如果希望为 Socket 对象设置连接服务器的超时时长,

经过指定时间后,若还没有连接上,则认为连接超时

应该先创建一个无连接的 Socket 对象,再调用 connect() 方法连接服务器端,调用方法时传入超时时长参数。

1
2
Socket socket = new Socket();
socket.connect(new InetSocketAddress(IP地址, 端口号), 超时时长);

(6) 读写超时

Socket 对象提供了 setSoTimeout() 用于设置超时时长。

设置之后如果读、写操作超过时间限制,将会抛出异常,程序可以捕获并进行处理。

1
2
Socket socket = new Socket(IP地址, 端口号);
socket.setSoTimeout(超时时长);

6. 连接

服务器端

1
2
3
4
5
6
7
public class Server {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(端口号);
Socket socket = serverSocket.accept();
···
}
}

客户端

1
2
3
4
5
6
public class user {
public static void main(String[] args) throws IOException {
Socket socket = new Socket(IP地址, 端口号);
···
}
}

7. 通信

输出

1
2
3
4
5
// 获得输出流
OutputStream outputStream = socket.getOutputStream();

// 通过输出流输出内容
outputStream.write("内容".getBytes());

输入

1
2
3
4
5
6
7
// 获得输入流
InputStream inputStream = socket.getInputStream();

// 通过输入流读入内容
byte[] bytes = new byte[1024];
int length = inputStream.read(bytes);
System.out.println(new String(bytes, 0, length));

8. 客户端的多线程

在实际的应用中,客户端和服务器端之间需要保持长时间通信,不断地进行信息的输入和输出。此时如果不启用多线程,将会发生堵塞的情况。

为了使一个服务器端可以同时服务于多个客户端,可以加入多线程:

多线程类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ServerThread extends Thread {
private Socket socket;

// 通过构造方法获得当前连接的socket
public ServerThread(Socket socket) {
this.socket = socket;
}

@Override
public void run() {
···做些什么···
}
}

服务器端

1
2
3
4
5
6
7
8
9
10
11
12
public class Server {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(端口号);
// 通过循环不断接收请求
while (true) {
// 建立连接,获得socket
Socket socket = serverSocket.accept();
// 新建线程,传入socket,启动
new ServerThread(socket).start();
}
}
}

参考