天天看点

Java Language——网络编程1.Java网络API

计算机网络主要功能包括资源共享、信息传输和集中处理、负载均衡和分布式处理、综合信息服务等。实际上 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

通信协议通常由语义部分、语法部分、变换规则三部分组成。其实所谓的协议就是在数据传输基础上封装自己的文本内容,先自上而下,后自下而上处理数据头部:

Java Language——网络编程1.Java网络API

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 三次握手的流程:

Java Language——网络编程1.Java网络API
  1. 第一次握手:建立连接时,Client 发送 SYN 报文(seq=x)到 Server,并进入 SYN_SEND 状态,等待 Server 确认;
  2. 第二次握手:Server 收到 SYN 报文,必须确认 Client 的 SYN(ack=x+1),同时自己也发送一个 SYN 报文(seq=y),即 SYN + ACK 报文,此时 Server 进入 SYN_RECV 状态;
  3. 第三次握手: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 四次挥手的流程:

Java Language——网络编程1.Java网络API

TCP 连接必须经过时间 2MSL 后才真正释放掉。

  1. 第一次挥手:Client 发送一个 FIN 报文,用来关闭 Client 到 Server 的数据传送,Client 进入 FIN_WAIT_1 状态;
  2. 第二次挥手:Server 收到 FIN 报文后,发送一个 ACK 报文给 Client,确认序号为收到序号 +1(与 SYN 相同,一个 FIN 占用一个序号),Server 进入 CLOSE_WAIT 状态;
  3. 第三次挥手:Server 发送一个 FIN 报文,用来关闭 Server 到 Client 的数据传送,Server 进入 LAST_ACK 状态;
  4. 第四次挥手: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 会话的发送方,任何时候其发送缓存内的数据,都可以分为四类:

  1. 已经发送并且得到 ACK 回应的;
  2. 已经发送但没有得到 ACK 回应的;
  3. 未发送,但对端允许发送的;
  4. 未发送且由于达到了滑动窗口的大小,对端不允许发送的;

2、3 这两部分数据所组成的连续空间就是滑动窗口。

Java Language——网络编程1.Java网络API

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。

多点广播示意图如下:

Java Language——网络编程1.Java网络API

创建 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 优化。