计算机网络主要功能包括资源共享、信息传输和集中处理、负载均衡和分布式处理、综合信息服务等。实际上 Java 的网络编程就是服务器通过 ServerSocket 建立监听,客户端通过 Socket 连接到指定服务器后,通信双方就可以通过 IO 流进行通信了。
OSI 七层模型:
OSI 七层模型 | TCP/IP概念层模型 | 功能 | 包含协议 |
---|---|---|---|
应用层 | 应用层 | 文件传输、电子邮件、文件服务、虚拟终端 | HTTP、SNMP、FTP、TFTP、SMTP、DNS、Telnet |
表示层 | 数据格式化、代码转换、数据加密 | 没有协议 | |
会话层 | 解除或建立与别的接点的联系 | 没有协议 | |
传输层 | 传输层 | 提供端对端的接口 | TCP(传输控制协议, 安全度高)、UDP(效率快, 安全度低, 可能会有数据丢失)、SSL、TLS |
网络层 | 网络层 | 为数据包选择路由 | IP、ICMP、RIP、OSPF、BGP、IGMP |
数据链路层 | 链路层 | 传输有地址的帧以及错误检测功能 | SLIP、CSLIP、PPP、ARP、RARP、MTU |
物理层 | 以二进制数据形式在物理媒体上传输数据 | ISO2110、IEEE802、IEEE802.2 |
通信协议通常由语义部分、语法部分、变换规则三部分组成。其实所谓的协议就是在数据传输基础上封装自己的文本内容,先自上而下,后自下而上处理数据头部:
IP地址:32位整数(4个8位二进制数),NIC 统一负责全球 IP 地址的规划、管理,而 Intel NIC、APNIC、RIPE 三大网络信息中心具体负责美国及其他地区的 IP 地址分配,APNIC(总部在日本东京大学)负责亚太地区的 IP 管理,我国申请 IP 地址也要通过 APNIC。IP 地址被分为 A、B、C、D、E 五类。
A类:10.0.0.0-10.255.255.255
B类:172.16.0.0-172.31.255.255
C类:192.168.0.0-192.168.255.255
端口号:16位整数,0-65535
公认端口:0-1023
注册端口:1024-49151
动态和私有端口:49152-65535
1.Java网络API
Java 提供了四大网络通信相关的类 :
- InetAddress:用于标识网络上的硬件资源,表示 IP 地址;
- URL:统一资源定位符,格式为 协议名称和资源名称,中间用冒号隔开);
- Sockets:使用 TCP 协议实现的网络通信的 Socket 相关的类);
- Datagram:使用 UDP 协议,将数据保存在数据报中,通过网络进行通信)。
java.net 包下 URL 和 URLConnection 等类提供了以编程方式访问 web 服务的功能,URLDecoder 和 URLEncoder 提供了普通字符串和 application/x-www-form-urlencoded MIME 字符串相互转换的静态方法。
1.InetAddress
Java 提供了 InetAddress 类表示 IP 地址:
InetAddress ip = InetAddress.getByName("www.baidu.com"); // 根据主机名来获取对应的InetAddress实例
boolean b = ip.isReachable(1000); // 判断是否可达
String address = ip.getHostAddress(); // 获取该InetAddress实例的IP字符串
InetAddress local = InetAddress.getByAddress(new byte[]{127, 0, 0, 1}); //根据原始IP地址来获取对应的InetAddress实例
boolean b1 = ip.isReachable(1000); // 判断是否可达
String hostName = local.getCanonicalHostName(); // 获取该InetAddress实例对应的全限定域名
2.URL编码
URLDecoder 和 URLEncoder 提供了 URL 编码解码的功能,用于普通字符串和 application/x-www-form-urlencoded MIME 字符串之间的相互转换:
// URL编码
String s = URLDecoder.decode("%E5%8C%97%E4%BA%AC", "UTF-8");
// URL解码
String s1 = URLEncoder.encode("北京" , "UTF-8");
3、URLConnection
URLConnection 指应用程序与 URL 之间的通信连接,HttpURLConnection 指 URL 与 URL 之间的 HTTP 连接,程序可以通过 URLConnection 实例向 URL 发送请求、读取 UR 引用的资源。
4.TCP协议
TCP 协议(传输控制协议)是面向连接的、可靠的、有序的、重量级的、基于字节流的传输层通信协议,TCP 将应用层的数据流分割成报文段并发送给目标节点的 TCP 层。
TCP 为了保证不丢失包,所有数据包都有序号,对方收到则发送 ACK 确认,未收到则重传。TCP 还会使用校验和来检验数据在传输过程中是否有误。
1.TCP的三次握手
“握手” 是为了建立连接,三次握手的过程由客户端进行触发,TCP 三次握手的流程:
- 第一次握手:建立连接时,Client 发送 SYN 报文(seq=x)到 Server,并进入 SYN_SEND 状态,等待 Server 确认;
- 第二次握手:Server 收到 SYN 报文,必须确认 Client 的 SYN(ack=x+1),同时自己也发送一个 SYN 报文(seq=y),即 SYN + ACK 报文,此时 Server 进入 SYN_RECV 状态;
- 第三次握手:Client 收到 Server 的 SYN + ACK 报文,向 Server 发送确认报文 ACK(ack=y+1),此包发送完毕,Client 和 Server 进入 ESTAB_LISHED 状态,完成三次握手。
常见的问题:
1、为什么需要三次握手才能建立起连接?
为了初始化 Sequence Number 的初始值,通信双方需要通知对方自己的 Sequence Number,也就是图中的 x 和 y,这个号会作为以后数据通信的序号,以保证应用层接收到的数据不会因为网络的问题而乱序,TCP 会用这个序号来拼接数据,因此在服务器回发它的 Sequence Number 及第二次握手之后,客户端还需要发送确认报文给服务端,告知服务端客户端已经收到服务端 Sequence Number 了。
2、首次握手 SYN 超时?
服务端收到客户端的 SYN,回复 SYN-ACK 的时候未收到 ACK 确认,服务端就会不断重试直至超时,Linux 默认等待 63 秒才断开连接。
3、建立连接后,客户端出现故障怎么办?
TCP 有保活机制,在一段时间,连接处于非活动状态,开启保活功能的一端将向对方发送保活探测报文,如果未收到响应则继续发送,尝试次数达到保活探测数仍未收到响应则中断连接。
2.TCP的四次挥手
“挥手” 是为了断开连接,四次挥手的过程由客户端或服务端执行 close 进行触发,这里我们假设由客户端主动触发 close,TCP 四次挥手的流程:
TCP 连接必须经过时间 2MSL 后才真正释放掉。
- 第一次挥手:Client 发送一个 FIN 报文,用来关闭 Client 到 Server 的数据传送,Client 进入 FIN_WAIT_1 状态;
- 第二次挥手:Server 收到 FIN 报文后,发送一个 ACK 报文给 Client,确认序号为收到序号 +1(与 SYN 相同,一个 FIN 占用一个序号),Server 进入 CLOSE_WAIT 状态;
- 第三次挥手:Server 发送一个 FIN 报文,用来关闭 Server 到 Client 的数据传送,Server 进入 LAST_ACK 状态;
- 第四次挥手:Client 收到 FIN 报文后,Client 进入 TIME_WAIT 状态,接着发送一个 ACK 报文给 Server,确认序号为收到序号 +1,Server 进入 CLOSED 状态,完成四次挥手。
常见的问题:
1、为什么需要四次挥手才能断开连接?
因为 TPC 是全双工通信,发送方和接收方都需要 FIN 报文和 ACK 报文,发送方和接收方各自需要两次挥手即可,只不过有一方是被动的。
2、为什么会有 TIME_WAIT 状态?
确保有足够的时间让对方收到 ACK 报文;避免新旧连接混淆。
3、服务器出现大量 CLOSE_WAIT 状态的原因?
对方关闭 socket 连接后,我方忙于读或写,没有及时关闭连接。多数情况是程序里有 bug,需要检查代码,特别是释放资源的代码;检查配置,特别是处理请求的线程配置
3.TCP的滑动窗口
RTT:发送一个数据包到收到对应的 ACK 所花费的时间。
RTO:重传时间间隔。
TCP 使用滑动窗口做流量控制和乱序重排。滑动窗口保证了 TCP 的可靠性和流控特性。
对于 TCP 会话的发送方,任何时候其发送缓存内的数据,都可以分为四类:
- 已经发送并且得到 ACK 回应的;
- 已经发送但没有得到 ACK 回应的;
- 未发送,但对端允许发送的;
- 未发送且由于达到了滑动窗口的大小,对端不允许发送的;
2、3 这两部分数据所组成的连续空间就是滑动窗口。
5.TCP通信
Socket 是 Java 里的 TCP/IP 实现。TCP/IP 协议是一种可靠协议,它在通信两端各建立一个 Socket,从而在通信两端之间形成网络虚拟链路进行通信,Java 使用 Socket 对象来代表两端的通信接口,并通过 Socket 产生 IO 流来进行网络通信。
基于 TCP 协议实现网络通信的类包含客户端的 Socket 类(实现了 TCP/IP 协议,可以连接到服务端收发数据),服务端的 ServerSocket 类。
IP 地址 + 端口就组成了所谓的 Socket,Socket 是网络上运行的程序之间双向通信链路的终结点,是 TCP 和 UDP 的基础。
1、单客户端与单服务端的通信
服务端开启线程,启动 Socket 服务监听,等待客户端连接,连接成功读取数据:
ServerSocket serverSocket = new ServerSocket(10000); // 创建ServerSocket
Socket socket = serverSocket.accept(); // 开始监听端口,等待客户端连接,若连接上则继续往下执行
DataInputStream reader = new DataInputStream(socket.getInputStream()); // 获取数据输入流
String msg = reader.readUTF(); // 读一个UTF-8的信息
socket.shutdownInput(); // 关闭输入流
System.out.println(msg); // 输出到控制台
DataOutputStream writer = new DataOutputStream(socket.getOutputStream()); // 获取数据输出流
writer.writeUTF("服务端响应消息: 呵呵.."); // 写一个UTF-8的信息
socket.shutdownOutput(); // 关闭输出流
// 关闭相关资源
// 对于同一个socket,如果关闭了输出/输入流,则与该输出流关联的socket也会被关闭,所以一般不用关闭流,直接关闭socket即可。
socket.close();
serverSocket.close(); // 关闭socket也会关闭流
客户端需要在子线程中建立服务端连接并发送消息:
Socket socket = new Socket("localhost", 10000); // 创建客户端Socket
DataOutputStream writer = new DataOutputStream(socket.getOutputStream()); // 获取数据输出流
writer.writeUTF("客户端发送消息: 嘿嘿.."); // 写一个UTF-8的信息
socket.shutdownOutput(); // 关闭输出流
DataInputStream reader = new DataInputStream(socket.getInputStream()); // 获取数据输入流
String msg = reader.readUTF(); // 读一个UTF-8的信息
socket.shutdownInput(); // 关闭输入流
System.out.println(msg); // 输出到控制台
socket.close(); // 关闭socket也会关闭流
最终就可以在服务端和客户端的控制台上分别看到 “客户端发送消息: 嘿嘿…” 与 “服务端响应消息: 呵呵…” 信息了。
2、多客户端与单服务端的通信
实现多客户端通信则需要在服务端创建 ServerSocket,循环调用 accept() 等待客户端连接。首先创建服务端线程处理类:
public class ServerThread extends Thread {
Socket socket = null; // 和线程相关的Socket
DataInputStream reader;
DataOutputStream writer;
public ServerThread(Socket socket) {
this.socket = socket;
}
// 线程执行的操作,响应客户端的请求
@Override
public void run() {
try {
DataInputStream reader = new DataInputStream(socket.getInputStream());// 获取数据输入流
String msg = reader.readUTF(); // 读一个UTF-8的信息
socket.shutdownInput(); // 关闭输入流
System.out.println(msg); // 输出到控制台
writer = new DataOutputStream(socket.getOutputStream());// 获取数据输出流
writer.writeUTF("服务端响应消息: 呵呵.."); // 写一个UTF-8的信息
socket.shutdownOutput(); // 关闭输出流
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (socket!=null)
socket.close(); // 关闭socket也会关闭流
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
服务端代码修改为:
ServerSocket serverSocket = new ServerSocket(10000); // 创建ServerSocket
Socket socket = null;
// 循环监听等待客户端连接
while (true) {
socket = serverSocket.accept(); // 开始监听端口,等待客户端连接,若连接上则继续往下执行
ServerThread serverThread = new ServerThread(socket);
// 未设置优先级可能会导致运行时速度非常慢,可降低优先级。
serverThread.setPriority(4); // 设置线程优先级,范围[1,10],默认5
serverThread.start(); // 启动线程
}
客户端代码不变。最终就可以运行多个客户端来进行通信了。
另外在实际应用中,更多的是传递对象,传递对象可以使用 ObjectOutputStream 对象序列化流,传递对象:
Socket socket = new Socket("localhost", 10000); // 创建客户端Socket
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
oos.writeObject(object); // 写一个对象,序列化流
socket.shutdownOutput(); // 关闭输出流
socket.close(); // 关闭socket也会关闭流
6.UDP协议
UDP 协议(用户数据报协议)是无连接的、不可靠的、无序的、面向报文的、轻量级的,UDP 协议以数据报作为数据传输的载体,UDP 数据包报头只有 8 个字节,相比于 TCP 数据包报头有 20 个字节,额外开销较小。
UDP 速度要比 TCP 快,多用于在线视频媒体、电视广播、多人在线游戏等。
7.UDP通信
进行数据传输时,首先需要将要传输的数据定义成数据报(Datagram),在数据报中指明数据所要达到的 Socket(主机地址和端口号),然后再将数据报发送出去。基于 UDP 协议实现网络通信的类包含 DatagramPacket 类(表示数据报包,UDP 通信中的数据单元)和 DatagramSocket 类(进行端到端通信的类)。
1、使用 DatagramSocket 发送、接受数据
服务端代码:
DatagramSocket socket = new DatagramSocket(10000); // 创建服务端DatagramSocket
byte[] data = new byte[1024]; // 创建字节数组
DatagramPacket packet = new DatagramPacket(data, data.length); // 创建数据报,用于接收客户端发送的数据
socket.receive(packet); // 接收客户端发送的数据,接收到数据报才继续执行(会阻塞)
String info = new String(data, 0, packet.getLength()); // 读取数据
System.out.println("服务端接收到信息: "+info);
// 向客户端响应数据
InetAddress address = packet.getAddress();
int port = packet.getPort();
byte[] data2 ="欢迎您!".getBytes();
DatagramPacket packet2 = new DatagramPacket(data2, data2.length, address, port); // 创建数据报,包含响应的数据
socket.send(packet2); // 响应客户端
socket.close(); // 关闭资源
客户端代码:
// 定义服务器地址、端口号、数据
InetAddress address = InetAddress.getByName("localhost");
int port = 10000;
byte[] data ="嘿嘿..".getBytes();
DatagramPacket packet = new DatagramPacket(data, data.length, address, port); // 创建数据报,包含发送的数据
DatagramSocket socket = new DatagramSocket(); // 创建客户端DatagramSocket
socket.send(packet); // 向服务端发送数据
// 接受服务端响应的数据
byte[] data2 = new byte[1024]; // 创建字节数组
DatagramPacket packet2 = new DatagramPacket(data2, data2.length); // 创建数据报,用于接收客户端发送的数据
socket.receive(packet2); // 接收服务端响应数据,接收到数据报才继续执行(会阻塞)
String info2 = new String(data, 0, packet.getLength()); // 读取数据
System.out.println("服务端响应信息: "+info2);
最终就可以在服务端和客户端的控制台上看到相应的信息了。实现 UDP 多客户端单服务端通信可以参考 TPC 开启多线程的方式,这里不再赘述。
2、使用 MulticastSocket 实现多点广播
DatagramSocket 只允许数据报发送到指定的目标地址,而 MulticastSocket 可以将数据报以广播方式发送到多个客户端。MulticastSocket 继承于 DatagramSocket。
多点广播示意图如下:
创建 MulticastSocket 对象后还需要加入到指定的多点广播地址,MulticastSocket 使用 joinGroup(InetAddress multicastAddr) 方法加入到指定组,使用 leaveGroup(InetAddress multicastAddr) 方法脱离一个组。
MulticastSocket 用于发送、接受数据报的方法和 DatagramSocket 完全一样。但 MulticastSocket 多了一个 setTimeToLive(int ttl) 方法,ttl 参数用于设置数据报最多可以跨过多少个网络(0:停留在本地主机;1:本地局域网;32:只能发送到本站点的网络上;64:保留在本地区;128:保留在本大洲;255:所有地方),默认1。
8.代理服务器
从 Java5 开始,java.net 包下提供了 Proxy(表示代理服务器)、ProxySelector(表示代理选择器)两个类。
常见问题
1、http 和 https 的区别?
https 即安全超文本传输协议,https 在传输层增加了 SSL 层,SSL 采用身份认证和数据加密保证网络通信的安全和数据的完整性。
- https 需要到 CA 申请证书,而 http 不需要;
- https 是密文传输,而 http 是明文传输;
- 连接方式不同,https 默认使用 443 端口,http 使用 80 端口;
- https = http + 加密 + 认证 + 完整性保护,较 http 安全。
2、https 真的安全吗?
不一定,浏览器默认填充 http://,请求需要进行转发到 https 的端口,有被劫持的风险。这一点可以使用 HSTS 优化。