网络通信

网络编程是指编写运行在多个设备(计算机)的程序,这些设备都通过网络连接起来。

java.net 包中 J2SEAPI 包含有类和接口,它们提供低层次的通信细节。你可以直接使用这些类和接口,来专注于解决问题,而不用关注通信细节。

本文涵盖内容:

  1. 网络基础知识:OSI 七层模型与 TCP/IP 四层模型
  2. 常见协议:IP、TCP、UDP、HTTP、HTTPS、SSL、RPC
  3. Java 网络编程:URL、URLConnection、InetAddress、Socket
  4. IO 模型:BIO(同步阻塞)、NIO(同步非阻塞)、AIO(异步非阻塞)

网络基础知识

OSI 分层模型

image-20230315105729738

OSI(Open System Interconnect),即开放式系统互联。 一般都叫 OSI 参考模型,是 ISO 组织在 1985 年研究的网络互连模型。

ISO 组织为了更好的使网络应用更为普及,推出了 OSI 参考模型。

每一层实现各自的功能和协议,并完成与相邻层的接口通信。OSI 的服务定义详细说明了各层所提供的服务。某一层的服务就是该层及其下各层的一种能力,它通过接口提供给更高一层。各层所提供的服务与这些服务是怎么实现的无关。

应用层

OSI 分层模型中最靠近用户的一层,为计算机用户提供应用接口,也为用户直接提供各种网络服务。

该层协议定义了应用进程之间的交互规则,通过不同的应用层协议为不同的网络应用提供服务。例如域名系统 DNS,支持万维网应用的 HTTP 协议,电子邮件系统采用的 SMTP 协议等。

在应用层交互的数据单元我们称之为报文。我们常见的应用层网络服务协议有:HTTPHTTPSFTPPOP3SMTP 等。

表示层

表示层提供各种用于应用层数据的编码和转换功能,确保一个系统的应用层发送的数据能被另一个系统的应用层识别。如果必要,该层可提供一种标准表示形式,用于将计算机内部的多种数据格式转换成通信中采用的标准表示形式数据压缩和加密也是表示层可提供的转换功能之一。

会话层

会话层就是负责建立、管理和终止表示层实体之间的通信会话。该层的通信由不同设备中的应用程序之间的服务请求和响应组成。

传输层

传输层的作用是为上层协议提供端到端的可靠和透明的数据传输服务,包括处理数据包错误、数据包次序和流量控制等问题。该层向高层屏蔽了下层数据通信的细节,使高层用户看到的只是在两个传输实体间的一条主机到主机的、可由用户控制和设定的、可靠的数据通路。它是计算机通信体系结构中关键的一层。我们通常说的,TCPUDP 就是在这一层。

网络层

两台计算机之间传送数据时其通信链路往往不止一条,所传输的信息甚至可能经过很多通信子网。网络层的主要任务就是选择合适的网间路由和交换节点,确保数据按时成功传送。在发送数据时,网络层把传输层产生的报文或用户数据报封装成分组和包,向下传输到数据链路层。

在网络层使用的协议是无连接的网际协议(Internet Protocol)和许多路由协议,因此我们通常把该层简单地称为 IP 层。

数据链路层

数据链路层通常也叫做链路层,在物理层和网络层之间。两台主机之间的数据传输,总是在一段一段的链路上传送的,这就需要使用专门的链路层协议。

在两个相邻节点之间传送数据时,数据链路层将网络层交下来的 IP 数据报组装成帧,在两个相邻节点间的链路上传送帧,而每一帧的数据又可以分成:报头 head 和数据 data 两部分:

  • head:标明数据发送者、接受者、数据类型,如 MAC 地址。
  • data:存储了计算机之间交互的数据。

通过控制信息我们可以知道一个帧的起止比特位置,此外,也能使接收端检测出所收到的帧有无差错,如果发现差错,数据链路层能够简单的丢弃掉这个帧,以避免继续占用网络资源。

物理层

实际最终信号的传输是通过物理层实现的。通过物理介质传输比特流。规定了电平、速度和电缆针脚。常用设备有(各种物理设备):集线器、中继器、调制解调器、网线、双绞线、同轴电缆等。这些都是物理层的传输介质。

数据传输过程

在计算机网络中,时时刻刻存在着大量的数据交换。数据是如何从源节点传递到目标节点的,数据包又是如何变化的?

首先设定源节点和目标节点是主机,即计算机,都按照 OSI 模型分为7层。

数据在源节点计算机向下传递的时候,到达一层,加上这一层的控制信息,称之为首部,在数据链路层还有尾部。数据在目的节点计算机向上传递的时候,到达一层,去掉这一层的控制信息

数据包在不同层的称呼也有所不同,物理层称之为比特,数据链路层称之为帧,网络层称之为分组,第4层-第7层称之为报文。

TCP/IP 分层模型

TCP/IP 协议,中文名称:传输控制协议/互联网协议,不是简单 的一个协议,而是一组特别的协议,包括:TCPIPUDPARP 等, 这些被称为子协议。在这些协议中,最重要、最著名的就是 TCPIP。 因此,大部分网络管理员称整个协议族为 TCP/IP

TCP/IP 四层模型可以说是 OSI 七层模型的简化版,主要有以下四层模型:

  1. 应用层
  2. 传输层
  3. 网络层
  4. 网络接口层
img

链路层

链路层有时也称作数据链路层或网络接口层,通常包括操作系统中的设备驱动程序和计算机中对应的网络接口卡。是数据的数据链路层(线路)和物理层的合集。

它们一起处理与电缆(或其他任何传输媒介)的物理接口细节。把链路层地址和网络层地址联系起来的协议有 ARPAddress Resolution Protocol,地址解析协议)和 RARPReverse Address Resolution Protocol,逆地址解析协议)。

数据链路层通常处理两台主机之间的数据传输,总是在一段一段的链路上传送的。数据链路层的作用是将网络层交下来的 IP 数据报组装成帧,在两个相邻节点间的链路上传送帧。每一帧包括数据和必要的控制信息(如同步信息,地址信息,差错控制等)。

物理层部分的作用是实现相邻计算机节点之间比特流的透明传送,尽可能屏蔽掉具体传输介质和物理设备的差异。

网络层

网络层处理分组在网络中的活动,例如分组的选路。在 TCP/IP 协议族中,网络层协议包括 IP 协议(Internet Protocol,网际协议)、 ICMP 协议(Internet Control Message Protocol,网际控制报文 协议)和 IGMP 协议(Internet Group Management Protocol, 网际组管理协议)。

传输层

传输层主要为两台主机上的应用程序提供端到端的通信。在 TCP/IP 协议族中,有两个互不相同的传输协议:TCPTransmission Control Protocol,传输控制协议)和 UDPUser Datagram Protocol,用户数据报协议)。

应用层

应用层负责处理特定的应用程序细节。几乎各种不同的 TCP/IP 实现都会提供下面这些通用的应用程序:Telnet 远程登录、SMTPSimple Mail Transfer Protocol,简单邮件传输协议)、FTPFile Transfer Protocol,文件传输协议)、HTTPHyper Text Transfer Protocol,超文本传输协议)等。

常见的协议

IP 协议

IP 指网际互连协议,Internet Protocol 的缩写,是 TCP/IP 体系中的网络层协议。设计 IP 的目的是提高网络的可扩展性:

  1. 解决互联网问题,实现大规模、异构网络的互联互通。
  2. 分割顶层网络应用和底层网络技术之间的耦合关系,以利于两者的独立发展。

根据端到端的设计原则,IP 只为主机提供一种无连接、不可靠的、尽力而为的数据包传输服务。

IP 地址

IP 地址:互联网协议地址(Internet Protocol Address) ,是 IP Address 的缩写。主要为计算机网络相互连接进行通信而设计的协议。

IP 地址被用来给 Internet 上的电脑起一个编号。大家日常见到的情况是每台联网的 PC 上都需要有 IP 地址,才能正常通信。我们可以把“个人电脑”比作“一台电话”,那么“IP 地址”就相当于“电话号码”,而 Internet 中的路由器,就相当于电信局的“程控式交换机”。

IP 地址是一个 32 位的二进制数,通常被分割为 4 个“8 位二进制数”(也就是 4 个字节)。IP 地址通常用“点分十进制”表示成(a.b.c.d)的形式,其中,a,b,c,d 都是 0~255 之间的十进制整数。

端口号

端口包括物理端口逻辑端口。物理端口是用于连接物理设备之间的接口,逻辑端口是逻辑上用于区分服务的端口。

TCP/IP 协议中的端口就是逻辑端口,通过不同的逻辑端口来区分不同的服务。一个 IP 地址的端口通过 16bit 进行编号,最多可以有 65536 个端口。端口是通过端口号来标记的,端口号只有整数,范围是从 0 到 65535。

端口有什么用呢?

我们知道,一台拥有 IP 地址的主机可以提供许多服务,比如 Web 服务、FTP 服务、SMTP 服务等,这些服务完全可以通 过 1 个 IP 地址来实现。那么,主机是怎样区分不同的网络服务呢?显然不能只靠 IP 地址,因为 IP 地址与网络服务的关系是一对多的关系。实际上是通过“IP 地址+端口号”来区分不同的服务的。

端口号小于 256 的定义为常用端口,服务器一般都是通过常用端口号来识别的。任何 TCP/IP 实现所提供的服务都用 1—1023 之间的端口号,是由 ICANN 来管理的; 客户端只需保证该端口号在本机上是惟一的就可以了。客户端的端口号因存在时间很短暂又称临时端口号;大多数 TCP/IP 实现给临时端口号分配 1024—5000 之间的端口号。大于 5000 的端口号是为其他服务器预留的。

TCP 协议

TCP 协议是面向连接的、可靠的传输层协议,规定了数据在网络中是如何传输的。

TCP 协议旨在适应支持多网络应用的分层协议层次结构。 连接到不同但相互连接的计算机通信网络的主计算机中的成对进程之间依靠TCP 提供可靠的通信服务TCP 假设它可以从较低级别的协议获得简单的,可能不可靠的数据报服务。 原则上,TCP 应该能够在从硬线连接到分组交换或电路交换网络的各种通信系统之上操作。

主要特点

TCP 是一种面向网络的通信协议,目的是在跨越多个网络通信时,为两个通信端点之间提供一条具有下列特点的通信方式:

  1. 基于流的方式。
  2. 面向连接。
  3. 可靠通信方式。
  4. 在网络状况不佳的时候尽量降低系统由于重传带来的带宽开销。
  5. 通信连接维护是面向通信的两个端点的,而不考虑中间网段和节点。

工作方式

三次握手:为了对每次发送的数据量进行跟踪与协商,确保数据段的发送和接收同步,根据所接收到的数据量而确认数据发送、接收完毕后何时撤消联系,并建立虚连接。

四次挥手:即终止 TCP 连接,就是指断开一个 TCP 连接时,需要客户端和服务端总共发送4个包以确认连接的断开。

img
建立连接

TCP 是网络中的传输层协议,使用三次握手协议建立连接。当主动方发出 SYN 连接请求后,等待对方回答 SYN + ACK ,并最终对对方的 SYN 执行 ACK 确认。这种建立连接的方法可以防止产生错误的连接,TCP 使用的流量控制协议是可变大小的滑动窗口协议。

img

TCP 三次握手的过程如下:

  1. 客户端发送 SYN(SEQ=x)报文给服务器端,进入 SYN_SENT 状态。
  2. 服务器端收到 SYN 报文,回应一个 SYN (SEQ=y)ACK(ACK=x+1)报文,进入 SYN_RECV 状态。
  3. 客户端收到服务器端的 SYN 报文,回应一个 ACK(ACK=y+1)报文,进入 Established 状态。

三次握手完成,TCP客户端和服务器端成功地建立连接,可以开始传输数据了。

其中:SYN 标志位数置1,表示建立 TCP 连接;ACK 标志表示验证字段。

连接终止

建立一个连接需要三次握手,而终止一个连接要经过四次握手,这是由 TCP 的半关闭(half-close)造成的。

img

四次握手的过程如下:

  1. 某个应用进程首先调用 close,标注该端执行“主动关闭”(active close)。该端的 TCP 于是发送一个 FIN 分节,表示数据发送完毕。
  2. 接收到这个 FIN 的对端执行 “被动关闭”(passive close),这个 FINTCP 确认。
  3. 一段时间后,接收到这个文件结束符的应用进程将调用 close 关闭它的套接字。这导致它的 TCP 也发送一个 FIN
  4. 接收这个最终 FIN 的原发送端 TCP (即执行主动关闭的那一端)确认这个 FIN

既然每个方向都需要一个 FIN 和一个 ACK ,因此通常需要4个分节。

11种状态名词解析:

  1. LISTEN:等待从任何远端TCP 和端口的连接请求。

  2. SYN_SENT:发送完一个连接请求后等待一个匹配的连接请求。

  3. SYN_RECEIVED:发送连接请求并且接收到匹配的连接请求以后等待连接请求确认。

  4. ESTABLISHED:表示一个打开的连接,接收到的数据可以被投递给用户。连接的数据传输阶段的正常状态。

  5. FIN_WAIT_1:等待远端TCP 的连接终止请求,或者等待之前发送的连接终止请求的确认。

  6. FIN_WAIT_2:等待远端TCP 的连接终止请求。

  7. CLOSE_WAIT:等待本地用户的连接终止请求。

  8. CLOSING:等待远端TCP 的连接终止请求确认。

  9. LAST_ACK:等待先前发送给远端TCP 的连接终止请求的确认(包括它字节的连接终止请求的确认)。

  10. TIME_WAIT:等待足够的时间过去以确保远端TCP 接收到它的连接终止请求的确认。

  11. CLOSED:不在连接状态(这是为方便描述假想的状态,实际不存在)。

可靠性实现

可靠性

TCP 提供一种面向连接的、可靠的字节流服务。面向连接意味着两个使用 TCP 的应用(通常是一个客户和一个服务器)在彼此交换数据包之前必须先建立一个 TCP 连接。这一过程与打电话很相似,先拨号振铃,等待对方摘机说“喂”,然后才说明是谁。在一个 TCP 连接中,仅有两方进行彼此通信。广播和多播不能用于 TCP

TCP 通过下列方式来提供可靠性:

  1. 应用数据被分割成 TCP 认为最适合发送的数据块。这和 UDP 完全不同,应用程序产生的数据长度将保持不变。由 TCP 传递给 IP 的信息单位称为报文段或段(segment)。

  2. TCP 发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段。当 TCP 收到发自 TCP 连接另一端的数据,它将发送一个确认。TCP 有延迟确认的功能,在此功能没有打开,则是立即确认。功能打开,则由定时器触发确认时间点。

  3. TCP 将保持它首部和数据的检验和。这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错,TCP 将丢弃这个报文段和不确认收到此报文段(希望发端超时并重发)。

  4. 既然 TCP 报文段作为 IP 数据报来传输,而 IP 数据报的到达可能会失序,因此 TCP 报文段的到达也可能会失序。如果必要,TCP 将对收到的数据进行重新排序,将收到的数据以正确的顺序交给应用层。

  5. 既然 IP 数据报会发生重复,TCP 的接收端必须丢弃重复的数据。

  6. TCP 还能提供流量控制。TCP 连接的每一方都有固定大小的缓冲空间。TCP 的接收端只允许另一端发送接收端缓冲区所能接纳的数据。这将防止较快主机致使较慢主机的缓冲区溢出。

重传策略

TCP 协议用于控制数据段是否需要重传的依据是设立重发定时器。在发送一个数据段的同时启动一个重传,如果在重传超时前收到确认就关闭该重传,如果重传超时前没有收到确认,则重传该数据段。在选择重发时间的过程中,TCP 必须具有自适应性。它需要根据互联网当时的通信情况,给出合适的重发时间。

窗口确认

TCP 的一项功能就是确保每个数据段都能到达目的地。位于目的主机的 TCP 服务对接受到的数据进行确认,并向源应用程序发送确认信息。

使用数据报头序列号以及确认号来确认已收到包含在数据段的相关的数据字节。

TCP 在发回源设备的数据段中使用确认号,指示接收设备期待接收的下一字节。这个过程称为期待确认。

源主机在收到确认消息之前可以传输的数据的大小称为窗口大小。用于管理丢失数据和流量控制。

UDP 协议

Internet 协议集支持一个无连接的传输协议,该协议称为用户数据报协议。UDP 为应用程序提供了一种无需建立连接就可以发送封装的 IP 数据包的方法。

Internet 的传输层有两个主要协议,互为补充。无连接的是 UDP,它除了给应用程序发送数据包功能并允许它们在所需的层次上架构自己的协议之外,几乎没有做什么特别的事情。面向连接的是 TCP,该协议几乎做了所有的事情。

概述

UDPUser Datagram Protocol 的简称, 中文名是用户数据报协议,是 OSIOpen System Interconnection,开放式系统互联) 参考模型中一种无连接传输层协议,提供面向事务的简单不可靠信息传送服务。

UDP 协议与 TCP 协议一样用于处理数据包,在 OSI 模型中,两者都位于传输层,处于 IP 协议的上一层。UDP 有不提供数据包分组、组装和不能对数据包进行排序的缺点,也就是说,当报文发送之后,是无法得知其是否安全完整到达的。UDP 用来支持那些需要在计算机之间传输数据的网络应用。包括网络视频会议系统在内的众多的客户/服务器模式的网络应用都需要使用 UDP 协议。UDP 协议从问世至今已经被使用了很多年,虽然其最初的光彩已经被一些类似协议所掩盖,但即使在今天 UDP 仍然不失为一项非常实用和可行的网络传输层协议。 许多应用只支持 UDP,如:多媒体数据流,不产生任何额外的数据,即使知道有破坏的包也不进行重发。当强调传输性能而不是传输的完整性时,如:音频和多媒体应用,UDP 是最好的选择。在数据传输时间很短,以至于此前的连接过程成为整个流量主体的情况下,UDP 也是一个好的选择。

特点

  1. UDP无连接的。即发送数据之前不需要建立连接,因此减少了开销和发送数据之前的时延。

  2. UDP 使用尽最大努力交付。即不保证可靠交付,因此主机不需要维护复杂的连接状态表。

  3. UDP 是面向报文的。发送方的 UDP 对应用程序交下来的报文,在添加首部后就向下交付 IP 层。UDP 对应用层交下来的报文,既不合并,也不分拆,而是保留这些报文的边界。这就是说,应用层交给 UDP 多长的报文,UDP 就照样发送,即一次发送一个报文。在接收方的 UDP,对 IP 层交上来的 UDP 用户数据报,在去除首部后就原封不动地交付上层的应用进程。也就是说,UDP 一次交付一个完整的报文。因此,应用程序必须选择合适大小的报文。若报文太长,UDP 把它交给 IP 层后,IP 层在传送时可能要进行分片处理,这会降低 IP 层的效率。反之,若报文太短,UDP 把它交给 IP 层后,会使 IP 数据报的首部的相对长度太大,这也降低了 IP 层的效率。

  1. UDP 没有拥塞控制。因此网络出现的拥塞不会使源主机的发送速率降低。这对某些实时应用是很重要的。很多的实时应用(如:IP电话、实时视频会议等)要求源主机以恒定的速率发送数据,并且允许在网络出现拥塞时丢失一部分数据,但却不允许数据有太大的时延。UDP 协议正好适合这种要求。
  2. UDP 支持一对一、一对多、多对一和多对多的交互通信。
  3. UDP 的首部开销小,只有 8 个字节,比 TCP 的 20 个字节的首部要短。

UDP 的首部格式

用户数据报 UDP 有两个部分组成:首部 + 数据部分。首部部分很简单,只有 8 个字节,由四个字段组成,每个字段的长度都是两个字节。各字段含义如下:

  1. 源端口:源端口号。在需要对方回信时选用。不需要使用时可用 0 填充。

  2. 目的端口:目的端口号。这在终点交付报文时必须使用。

  3. 长度:UDP 用户数据报的长度,其最小值是 8(即仅有首部部分),单位:字节。

  4. 校验和:检测 UDP 用户数据报在传输过程中是否出错。有错就丢弃。

当传输层从 IP 层收到 UDP 数据报时,就根据首部中的目的端口,把 UDP 数据报通过相应的端口,上交最后的终点——应用进程。

UDP 校验和

UDP 用户数据报首部中的校验和的计算方法有些特殊。在计算校验和时,要在 UDP 用户数据报之前增加 12 个字节的伪首部。所谓“伪首部”是因为这种伪首部并不是 UDP 用户数据报真正的首部。只是在计算校验和时,临时添加在 UDP 用户数据报前面,得到一个临时的 UDP 用户数据报。校验和就是按照这个临时的 UDP 用户数据报来计算。伪首部既不向下传送也不向上递交,而仅仅是为了计算校验和。

在发送方,首先是先把全零放入首部中的校验字段和字段,再把伪首部以及 UDP 用户数据报看成是由许多 16 位的字串接起来的。若 UDP 用户数据报的数据部分不是偶数个字节,则要填入一个全零字节(但此字节不发送)。然后按照二进制反码计算出这些 16 位字的和。将此和的二进制反码写入检验和字段后,就发送这样的 UDP 用户数据报。在接收方,把收到的 UDP 用户数据报连同伪首部(以及可能的填充全零字节)一起,按二进制反码求这些 16 位字的和。当无差错时其结果应为全 1。否则就表明有差错出现,接收方就应丢弃这个 UDP 用户数据报(也可以上交给应用层,但附上出现了差错的警告)。

TCP 和 UDP 之间的区别

  1. 基于连接与无连接。
  2. TCP 要求系统资源较多,UDP 较少。
  3. UDP 程序结构较简单。
  4. 流模式(TCP)与数据报模式(UDP)。
  5. TCP 保证数据正确性,UDP 可能丢包。
  6. TCP 保证数据顺序,UDP 不保证。

TCP 与 UDP 对比总结:

特性 TCP UDP
连接方式 面向连接(三次握手) 无连接
可靠性 可靠传输 尽力交付,不保证可靠
传输方式 字节流 数据报
传输效率 较低 较高
资源消耗 较多 较少
适用场景 文件传输、邮件、网页 视频直播、语音、DNS

HTTP 协议

超文本传输协议(Hyper Text Transfer ProtocolHTTP)是一个简单的请求-响应协议,它通常运行在 TCP 之上。它指定了客户端可能发送给服务器什么样的消息以及得到什么样的响应。请求和响应消息的头以 ASCII 形式给出;而消息内容则具有一个类似 MIME 的格式。这个简单模型是早期 Web 成功的有功之臣,因为它使开发和部署非常地直截了当。

简介

万维网 WWWWorld Wide Web)发源于欧洲日内瓦量子物理实验室 CERN,正是 WWW 技术的出现使得因特网得以超乎想象的速度迅猛发展。这项基于 TCP/IP 的技术在短短的十年时间内迅速成为已经发展了几十年的 Internet 上的规模最大的信息系统,它的成功归结于它的简单、实用。在 WWW 的背后有一系列的协议和标准支持它完成如此宏大的工作,这就是 Web 协议族,其中就包括 HTTP 超文本传输协议。

在1990年,HTTP 就成为 WWW 的支撑协议。当时由其创始人 WWW 之父蒂姆·伯纳斯·李提出,随后 WWW 联盟成立,组织了 IETFInternet Engineering Task Force)小组进一步完善和发布 HTTP

HTTP 是应用层协议,同其他应用层协议一样,是为了实现某一类具体应用的协议,并由某一运行在用户空间的应用程序来实现其功能。

HTTP 是基于 B/S 架构进行通信的,而 HTTP 的服务器端实现程序有 httpdnginx 等,其客户端的实现程序主要是 Web 浏览器,例如 FirefoxInternet ExplorerGoogle ChromeSafariOpera 等,此外,客户端的命令行工具还有 elinkcurl 等。Web 服务是基于 TCP 的,因此为了能够随时响应客户端的请求,Web 服务器需要监听在80端口。这样客户端浏览器和 Web 服务器之间就可以通过 HTTP 进行通信了。

通常,由 HTTP 客户端发起一个请求,创建一个到服务器指定端口(默认是80端口)的 TCP 连接。HTTP 服务器则在那个端口监听客户端的请求。一旦收到请求,服务器会向客户端返回一个状态,比如 HTTP/1.1 200 OK ,以及返回的内容,如请求的文件、错误消息、或者其它信息。

HTTP 的工作原理

HTTP 协议定义 Web 浏览器如何从 Web 服务器请求 Web 页面,以及服务器如何把 Web 页面传送给浏览器。HTTP 协议采用了请求/响应模型。浏览器向服务器发送一个请求报文,请求报文包含请求的方法URL协议版本请求头部请求数据。服务器以一个状态行作为响应,响应的内容包括协议的版本成功或者错误代码服务器信息响应头部响应数据

以下是 HTTP 请求/响应的步骤:

  1. 浏览器连接到 Web 服务器 一个 HTTP 客户端,通常是浏览器,与 Web 服务器的 HTTP 端口(默认为80)建立一个 TCP 套接字连接。

    例如,http://www.baidu.com

  2. 发送 HTTP 请求 通过 TCP 套接字,浏览器向 Web 服务器发送一个文本的请求报文,一个请求报文由请求行请求头部空行请求数据4部分组成。

  3. 服务器接受请求并返回 HTTP 响应 Web 服务器解析请求,定位请求资源。服务器将资源复本写到 TCP 套接字,由浏览器读取。一个响应由状态行响应头部空行响应数据4部分组成。

  4. 释放连接 TCP 连接 若 connection 模式为 close,则服务器主动关闭 TCP 连接,浏览器被动关闭连接,释放 TCP 连接;若 connection 模式为keepalive,则该连接会保持一段时间,在该时间内可以继续接收请求。

  5. 浏览器解析 HTML 内容 浏览器首先解析状态行,查看表明请求是否成功的状态代码。然后解析每一个响应头,响应头告知以下为若干字节的 HTML 文档和文档的字符集。浏览器读取响应数据 HTML,根据 HTML 的语法对其进行格式化,并在浏览器窗口中显示。

例如:在浏览器地址栏键入URL,按下回车之后会经历以下流程:

  1. 浏览器向 DNS 服务器请求解析该 URL 中的域名所对应的 IP 地址;
  2. 解析出 IP 地址后,根据该 IP 地址和默认端口 80,和服务器建立 TCP 连接;
  3. 浏览器发出读取文件(URL 中域名后面部分对应的文件)的HTTP 请求,该请求报文作为 TCP 三次握手的第三个报文的数据发送给服务器;
  4. 服务器对浏览器请求作出响应,并把对应的 html 文本发送给浏览器;
  5. 释放 TCP连接;
  6. 浏览器将该 html 文本并显示内容;  
img
请求-响应 模式

HTTP 协议是基于 TCP/IP 协议之上的应用层协议。

HTTP 协议规定,请求从浏览器发出,最后服务器端响应该请求并返回。换句话说,肯定是先从浏览器开始建立通信的,服务器端在没有接收到请求之前不会发送响应。

img
无状态保存

HTTP 是一种无状态协议HTTP 协议自身不对请求和响应之间的通信状态进行保存。也就是说在 HTTP 这个级别,协议对于发送过的请求或响应都不做持久化处理。

img

使用 HTTP 协议,每当有新的请求发送时,就会有对应的新响应产生。协议本身并不保留之前一切的请求或响应报文的信息。这是为了更快地处理大量事务,确保协议的可伸缩性,而特意把 HTTP 协议设计成如此简单的。

无连接

无连接的含义是限制每次连接只处理一个请求。服务器处理完浏览器的请求,并收到浏览器的应答后,即断开连接。采用这种方式可以节省传输时间,并且可以提高并发性能,不用和每个浏览器建立长久的连接,请求一次响应一次,服务端和浏览器就中断了。

但是无连接有两种方式,早期的 HTTP协议是一个请求一个响应之后,直接就断开了。但是现在的 HTTP 协议1.1版本后不是直接就断开连接了,而是等几秒钟,这几秒钟是等什么呢,等着浏览器有后续的操作,如果浏览器在这几秒钟之内有新的请求,那么还是通过之前的连接通道来收发消息,如果过了这几秒钟浏览器没有发送新的请求,那么就会断开连接。

这样可以提高效率,减少短时间内建立连接的次数,因为建立连接也是需要耗费时间的。

HTTP 请求方法

我们需要知道的是,当我们访问各种网页的时候,之所以能够看到页面,根本原因是发送了 HTTP 请求然后得到了响应,从而页面才会弹出来。再或者我们上传一些照片和视频时,之所以可以上传成功也是因为我们通过 HTTP 请求来进行上传,从而将资源放在了网上。

当浏览器像服务端发送请求的时候,针对于不同意图的请求,我们分成了不同的 HTTP 请求方法。HTTP/1.1 协议中共定义了八种方法来以不同方式操作指定的资源:

  1. GET 请求:GET 方法是最常见也是最简单的 HTTP 请求方法,它主要用作于获取资源。也就是说我浏览器请求什么,你服务器就原样给我返回什么。我请求的是文本,你就保持原样返回;我请求的是像 CGI 那样的程序,你就给我返回运行的结果。

  2. HEAD 请求:与 GET 方法一样,都是向服务器发出指定资源的请求。只不过服务器将不传回资源的本文部分,只是用来确定请求的有效性及资源的更新日期时间等。它的好处在于,使用这个方法可以在不必传输全部内容的情况下,就可以获取其中“关于该资源的信息”(元信息或称元数据)。

  3. POST 请求:POST 方法主要用来传输实体的主体。请求服务器进行处理(例如提交表单或者上传文件),数据被包含在请求本文中。这个请求可能会创建新的资源或修改现有资源,或二者皆有。 也就是说,当客户端需要向服务器传输一些东西的时候呢,这个时候就可以用 POST 方法了。那 GET 方法可以不可以呢?当然也可以,但是我们不推荐使用 GET 方法来对实体的主体进行传输。

  4. PUT 请求:向指定资源位置上传其最新内容。PUT 方法主要用来传输文件,就像 FTP 协议的文件上传一样。但是由于 Http/1.1PUT 方法不带验证机制,存在安全性问题,所以一般的网站都不用这个方法来进行文件传输。

  5. DELETE 请求:DELETE 方法主要是用来删除某个资源,是和 PUT 完全相反的方法。同时该方法也不带认证机制,所以一般网站并不会对它进行开放使用。

  6. TRACE 请求:回显服务器收到的请求,主要用于测试或诊断。

  7. OPTIONS 请求:OPTIONS 方法用来查询,请求的指定资源都支持什么 HTTP 方法。这个方法可使服务器传回该资源所支持的所有HTTP请求方法。用’*’来代替资源名称,向 Web 服务器发送 OPTIONS 请求,可以测试服务器功能是否正常运作。

  8. CONNECT 请求:HTTP/1.1 协议中预留给能够将连接改为管道方式的代理服务器。通常用于 SSL 加密服务器的链接。

GET和POST的区别:

  1. GET在浏览器回退时是无害的,而POST会再次提交请求。

  2. GET产生的URL地址可以被Bookmark,而POST不可以。

  3. GET请求会被浏览器主动cache,而POST不会,除非手动设置。

  4. GET请求只能进行URL编码,而POST支持多种编码方式。

  5. GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留。

  6. GET请求在URL中传送的参数是有长度限制的,而POST么有。

  7. 对参数的数据类型,GET只接受ASCII字符,而POST没有限制。.

  8. GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息。

  9. GET参数通过URL传递,POST放在Request body中。

注意事项:

  1. 方法名称是区分大小写的。当某个请求所针对的资源不支持对应的请求方法的时候,服务器应当返回状态码405(Method Not Allowed),当服务器不认识或者不支持对应的请求方法的时候,应当返回状态码501(Not Implemented)。
  2. HTTP 服务器至少应该实现 GETHEAD 方法,其他方法都是可选的。当然,所有的方法支持的实现都应当匹配下述的方法各自的语义定义。此外,除了上述方法,特定的 HTTP 服务器还能够扩展自定义的方法。例如 PATCH 用于将局部修改应用到资源。

HTTP 状态码

所有 HTTP 响应的第一行都是状态行,依次是当前 HTTP 版本号,3位数字组成的状态代码,以及描述状态的短语,彼此间由空格分隔。

状态码的第一个数字代表当前响应的类型:

  • 1xx 消息——请求已被服务器接收,继续处理。
  • 2xx 成功——请求已成功被服务器接收、理解、并接受。
  • 3xx 重定向——需要后续操作才能完成这一请求。
  • 4xx 请求错误——请求含有词法错误或者无法被执行。
  • 5xx 服务器错误——服务器在处理某个正确请求时发生错误。

虽然 RFC 2616 中已经推荐了描述状态的短语,例如 200 OK404 Not Found,但是 WEB 开发者仍然能够自行决定采用何种短语,用以显示本地化的状态描述或者自定义信息。

img
消息 描述
100 Continue 服务器仅接收到部分请求,但是一旦服务器并没有拒绝该请求,客户端应该继续发送其余的请求。
101 Switching Protocols 服务器转换协议,服务器将遵从客户的请求转换到另外一种协议。
消息 描述
200 OK 请求成功(其后是对GET和POST请求的应答文档。)
201 Created 请求被创建完成,同时新的资源被创建。
202 Accepted 供处理的请求已被接受,但是处理未完成。
203 Non-authoritative Information 文档已经正常地返回,但一些应答头可能不正确,因为使用的是文档的拷贝。
204 No Content 没有新文档。浏览器应该继续显示原来的文档。如果用户定期地刷新页面,而Servlet可以确定用户文档足够新,这个状态代码是很有用的。
205 Reset Content 没有新文档。但浏览器应该重置它所显示的内容。用来强制浏览器清除表单输入内容。
206 Partial Content 客户发送了一个带有Range头的GET请求,服务器完成了它。
消息 描述
300 Multiple Choices 多重选择。链接列表。用户可以选择某链接到达目的地。最多允许五个地址。
301 Moved Permanently 所请求的页面已经转移至新的url。
302 Found 所请求的页面已经临时转移至新的url。
303 See Other 所请求的页面可在别的url下被找到。
304 Not Modified 未按预期修改文档。客户端有缓冲的文档并发出了一个条件性的请求(一般是提供If-Modified-Since头表示客户只想比指定日期更新的文档)。服务器告诉客户,原来缓冲的文档还可以继续使用。
305 Use Proxy 客户请求的文档应该通过Location头所指明的代理服务器提取。
306 Unused 此代码被用于前一版本。目前已不再使用,但是代码依然被保留。
307 Temporary Redirect 被请求的页面已经临时移至新的url。
消息 描述
400 Bad Request 服务器未能理解请求。
401 Unauthorized 被请求的页面需要用户名和密码。
401.1 登录失败。
401.2 服务器配置导致登录失败。
401.3 由于ACL对资源的限制而未获得授权。
401.4 筛选器授权失败。
401.5 ISAPI/CGI应用程序授权失败。
401.7 访问被Web服务器上的URL授权策略拒绝。这个错误代码为IIS 6.0所专用。
402 Payment Required 此代码尚无法使用。
403 Forbidden 对被请求页面的访问被禁止。
403.1 执行访问被禁止。
403.2 读访问被禁止。
403.3 写访问被禁止。
403.4 要求SSL。
403.5 要求SSL 128。
403.6 IP地址被拒绝。
403.7 要求客户端证书。
403.8 站点访问被拒绝。
403.9 用户数过多。
403.10 配置无效。
403.11 密码更改。
403.12 拒绝访问映射表。
403.13 客户端证书被吊销。
403.14 拒绝目录列表。
403.15 超出客户端访问许可。
403.16 客户端证书不受信任或无效。
403.17 客户端证书已过期或尚未生效。
403.18 在当前的应用程序池中不能执行所请求的URL。这个错误代码为IIS 6.0所专用。
403.19 不能为这个应用程序池中的客户端执行CGI。这个错误代码为IIS 6.0所专用。
403.20 Passport登录失败。这个错误代码为IIS 6.0所专用。
404 Not Found 服务器无法找到被请求的页面。
404.0 (无)–没有找到文件或目录。
404.1 无法在所请求的端口上访问Web站点。
404.2 Web服务扩展锁定策略阻止本请求。
404.3 MIME映射策略阻止本请求。
405 Method Not Allowed 请求中指定的方法不被允许。
406 Not Acceptable 服务器生成的响应无法被客户端所接受。
407 Proxy Authentication Required 用户必须首先使用代理服务器进行验证,这样请求才会被处理。
408 Request Timeout 请求超出了服务器的等待时间。
409 Conflict 由于冲突,请求无法被完成。
410 Gone 被请求的页面不可用。
411 Length Required “Content-Length”未被定义。如果无此内容,服务器不会接受请求。
412 Precondition Failed 请求中的前提条件被服务器评估为失败。
413 Request Entity Too Large 由于所请求的实体的太大,服务器不会接受请求。
414 Request-url Too Long 由于url太长,服务器不会接受请求。当post请求被转换为带有很长的查询信息的get请求时,就会发生这种情况。
415 Unsupported Media Type 由于媒介类型不被支持,服务器不会接受请求。
416 Requested Range Not Satisfiable 服务器不能满足客户在请求中指定的Range头。
417 Expectation Failed 执行失败。
423 锁定的错误。
消息 描述
500 Internal Server Error 请求未完成。服务器遇到不可预知的情况。
500.12 应用程序正忙于在Web服务器上重新启动。
500.13 Web服务器太忙。
500.15 不允许直接请求Global.asa。
500.16 UNC授权凭据不正确。这个错误代码为IIS 6.0所专用。
500.18 URL授权存储不能打开。这个错误代码为IIS 6.0所专用。
500.100 内部ASP错误。
501 Not Implemented 请求未完成。服务器不支持所请求的功能。
502 Bad Gateway 请求未完成。服务器从上游服务器收到一个无效的响应。
502.1 CGI应用程序超时。
502.2 CGI应用程序出错。
503 Service Unavailable 请求未完成。服务器临时过载或宕机。
504 Gateway Timeout 网关超时。
505 HTTP Version Not Supported 服务器不支持请求中指明的HTTP版本。

报文格式

HTTP 报文分为两种,请求报文和响应报文,大致格式如下:

请求报文格式:

  1. 请求行:请求方法、URL 和协议版本号。
  2. 头部:按照规范,根据自己需要来选择性添加。
  3. 包体:即数据内容,post 请求时,这部分才有数据;get 请求时,数据附在 URL 参数里,以 ? 形式体现。

响应报文格式:

  1. 状态行: 协议版本号 、状态码和状态码说明。

  2. 头部:按照规范,根据自己需要来选择性添加。

  3. 包体: 一般为服务端返回给客户端的数据。

请求报文
结构构成:请求行 + 请求头部 + 请求数据
img
img
请求行
  1. 请求方法:HTTP 协议的请求方法有 GETPOSTHEADPUTDELETE 等。GETPOST 是最常用的请求方法。

  2. 统一资源定位符:URL 是一种资源位置的抽象唯一识别方法。组成:://:/。端口和路径有事可以省略(HTTP 默认端口号是80,HTTPS 默认端口443)。

  1. HTTP 协议版本字段:指定使用 HTTP 的协议版本,例如:HTTP/1.1等。
请求头部

请求头部为请求报文添加了一些附加信息,由 名/值 对组成,每行一对,名和值之间使用冒号分隔。请求头部通知服务器有关于客户端请求的信息。常见头部字段:

  • Cache-Control:控制缓存的行为,一般包括一个“max-age=3400”的值,代表缓存的有效时间为资源返回后的3400秒之内。
  • Connection:逐跳首部、连接的管理。如果值为其他字段的名字,代表代理服务器转发时将。
  • Accept:用户代理可处理的媒体类型。
  • Accept-Charset:优先的字符集。
  • Accept-Encoding:优先的内容编码。
  • Authorization:Web认证信息。
  • Expect:期待服务器的特定行为。
  • From:用户的电子邮箱地址。
  • Host:请求资源所在的服务器主机名/域名。
  • If-Match:比较实体标记(Etag)。
  • If-Modified-Since:比较资源的更新时间,出现在条件get请求中。
  • If-None-Match:比较实体标记(与If-Match相反)。
  • If-Range:资源未更新时发送实体Byte的范围请求。
  • If-Unmodified-Since:比较资源的更新时间(与If-Modified-Since相反)。
  • Max-Forwards:最大传输逐跳数。
  • Proxy-Authorization:代理服务器要求客户端的认证信息。
  • Range:实体的字节范围请求。
  • Referer:请求中URI的原始获取方。
  • TE:传输编码的优先级。
  • User-Agent:HTTP客户端程序的信息。如果是浏览器则是对应浏览器的用户代理字符串。
  • Content-Encoding:实体主体适用的编码方式。
  • Content-length:实体主体的大小(单位:字节)。
  • Content-Type:实体主体的媒体类型。
  • 空行:请求头部的最后会有一个空行,表示请求头部结束,接下来为请求数据。
包体

也叫请求数据,请求数据不在 GET 方法中使用,而在 POST 方法中使用。POST 方法适用于需要客户填写表单的场合。与请求数据相关的最高频使用的请求头部是 Content-TypeContent-Length ,分别代表数据类型和数据长度。

部分常用的Content-Type:

  • text/plain :纯文本格式 .txt

  • text/xml : XML格式 .xml

  • image/gif :gif图片格式 .gif

  • image/jpeg :jpg图片格式 .jpg

  • image/png:png图片格式 .png

  • audio/mp3 : 音频mp3格式 .mp3

  • audio/rn-mpeg :音频mpga格式 .mpga

  • video/mpeg4 : 视频mp4格式 .mp4

  • video/x-mpg : 视频mpa格式 .mpg

  • video/x-mpeg :视频mpeg格式 .mpeg

  • video/mpg : 视频mpg格式 .mpg

  • 以application开头的媒体格式类型:

  • application/xhtml+xml :XHTML格式

  • application/xml : XML数据格式

  • application/atom+xml :Atom XML聚合格式

  • application/json : JSON数据格式

  • application/pdf :pdf格式

  • application/msword : Word文档格式

  • application/octet-stream : 二进制流数据(如文件下载)

响应报文
结构组成:状态行 + 响应头部 + 响应数据
img
img
状态行
  1. 协议版本:协议版本与请求报文一致。

  2. 状态码:状态码由三位数字组成,第一位数字表示响应的类型。

  3. 状态码描述:状态码描述是对状态码的简单描述。

响应头部

常见响应头部及描述如下:

  • Cache-Control:控制缓存的行为,一般包括一个“max-age=3400”的值,代表缓存的有效时间为资源返回后的3400秒之内。
  • Connection:逐跳首部、连接的管理。如果值为其他字段的名字,代表代理服务器转发时将不再携带该字段。如果值为close或Keep-Alive则分别表示连接为短/长连接。
  • Accept-Ranges:是否接受字节范围请求。
  • Age:推算资源创建经过时间。
  • ETag :资源的匹配信息。
  • Location:令客户端重定向至指定URI。
  • Proxy-Authenticate:代理服务器对客户端的认证信息。
  • Server :HTTP服务器的安装信息。
  • Vary:代理服务器缓存的管理信息。
  • WWW-Authenticate:服务器对客户端的认证信息。
  • Content-Encoding:实体主体适用的编码方式。
  • Content-length:实体主体的大小(单位:字节)。
  • Content-Type:实体主体的媒体类型。
  • 空行:响应头部的最后会有一个空行,表示响应头部结束,接下来为响应数据。与请求报文一致。
响应数据

也叫响应体或响应正文(response body),用于存放需要返回给客户端的数据信息。

HTTPS 协议

HTTPS (全称:Hypertext Transfer Protocol Secure ),是以安全为目标的 HTTP 通道,在 HTTP 的基础上通过传输加密和身份认证保证了传输过程的安全性。HTTPSHTTP 的基础下加入 SSLHTTPS 的安全基础是 SSL,因此加密的详细内容就需要 SSL

HTTPS 存在不同于 HTTP 的默认端口及一个加密/身份验证层。这个系统提供了身份验证与加密通讯方法。它被广泛用于万维网上安全敏感的通讯,例如交易支付等方面 。

设计目标

HTTPS 协议是由 HTTP 加上 SSL 协议构建的可进行加密传输、身份认证的网络协议,主要通过数字证书、加密算法、非对称密钥等技术完成互联网数据传输加密,实现互联网传输安全保护。设计目标主要有三个:

  1. 数据保密性:保证数据内容在传输过程中不会被第三方查看。就像快递员传递包裹一样,都进行了封装,快递员无法获知快递里面装了什么 。
  2. 数据完整性:及时发现被第三方篡改的传输内容。就像快递员虽然不知道包裹里装了什么东西,但他有可能中途掉包,数据完整性就是指如果被掉包,我们能轻松发现并拒收 。
  3. 身份校验安全性:保证数据到达用户期望的目的地。就像我们邮寄包裹时,虽然是一个封装好的未掉包的包裹,但必须确定这个包裹不会送错地方,通过身份校验来确保送对了地方。
HTTPS 原理
  1. 客户端将它所支持的算法列表和一个用作产生密钥的随机数发送给服务器。
  2. 服务器从算法列表中选择一种加密算法,并将它和一份包含服务器公用密钥的证书发送给客户端;该证书还包含了用于认证目的的服务器标识,服务器同时还提供了一个用作产生密钥的随机数。
  3. 客户端对服务器的证书进行验证,并抽取服务器的公用密钥;然后,再产生一个称作 pre_master_secret 的随机密码串,并使用服务器的公用密钥对其进行加密,并将加密后的信息发送给服务器。
  4. 客户端与服务器端根据 pre_master_secret 以及客户端与服务器的随机数值独立计算出加密和 MAC 密钥。
  5. 客户端将所有握手消息的 MAC 值发送给服务器。
  6. 服务器将所有握手消息的 MAC 值发送给客户端。
优缺点

优点

  1. 使用 HTTPS 协议可认证用户和服务器,确保数据发送到正确的客户机和服务器。
  2. HTTPS 协议是由 SSL + HTTP 构建的可进行加密传输、身份认证的网络协议,要比 HTTP 安全,可防止数据在传输过程中被窃取、改变,确保数据的完整性。
  3. HTTPS 是现行架构下最安全的解决方案,虽然不是绝对安全,但它大幅增加了中间人攻击的成本。

缺点

  1. 相同网络环境下,HTTPS 协议会使页面的加载时间延长近 50%,增加 10%到 20% 的耗电。此外,HTTPS 协议还会影响缓存,增加数据开销和功耗。
  2. HTTPS 协议的安全是有范围的,在黑客攻击、拒绝服务攻击和服务器劫持等方面几乎起不到什么作用。
  3. 最关键的是,SSL 证书的信用链体系并不安全。特别是在某些国家可以控制 CA 根证书的情况下,中间人攻击一样可行。
  4. 成本增加,部署 HTTPS 后,因为 HTTPS 协议的工作要增加额外的计算资源消耗,例如 SSL 协议加密算法和 SSL 交互次数将占用一定的计算资源和服务器成本。在大规模用户访问应用的场景下,服务器需要频繁地做加密和解密操作,几乎每一个字节都需要做加解密,这就产生了服务器成本。不过随着云计算技术的发展,数据中心部署的服务器使用成本在规模增加后逐步下降,相对于用户访问的安全提升,其投入成本已经下降到可接受程度。

SSL 协议

安全套接层协议(简称 SSLSecurity Socket Layer)是网景(Netscape)公司提出的基于 WEB 应用的安全协议,它包括:服务器认证、客户认证(可选)、SSL 链路上的数据完整性和 SSL 链路上的数据保密性。SSL 主要使用公开密钥体制和 X.509 数字证书技术保护信息传输的机密性和完整性,它不能保证信息的不可抵赖性,主要适用于点对点之间的信息传输,常用 Web Server 方式。

SSL 安全协议也是国际上最早应用于电子商务的一种网络安全协议, 现在被许多网上商店和网上银行所使用。

特性

  • 连接是保密的,加密发生在初始的握手之后,用来定义一个密钥,加密是对称的。
  • 对等体的身份可以用非对称算法或公共密要认证。
  • 连接是可靠的,信息传输包括一个采用密要消息认证码(MAC)的信息完整性检查;安全散列函数(如SHAMD5等)用于 MAC 计算。

服务

  • 认证用户和服务器,使得它们能够确信数据将被发送到正确的客户机和服务器上。
  • 加密数据以隐藏被传送的数据。
  • 维护数据的完整性,确保数据在传输过程中不被改变。

RPC 协议

RPCRemote Procedure Call)远程过程调用协议是一个用于建立适当框架的协议。从本质上讲,它使一台机器上的程序能够调用另一台机器上的子程序,而不会意识到它是远程的。

RPC 是一种软件通信协议,一个程序可以用来向位于网络上另一台计算机的程序请求服务,而不必了解网络的细节。RPC 被用来像本地系统一样调用远程系统上的其他进程。过程调用有时也被称为函数调用或子程序调用。

为什么有 RPC

SocketHTTP 编程使用消息传递范式。客户端向服务器发送一个消息,而服务器通常会发送一个消息回来。双方都负责以双方都能理解的格式创建消息,并从这些消息中读出数据。

然而,大多数独立的应用程序并没有那么多地使用消息传递技术。一般来说,首选的机制是方法(函数或过程)的调用。在这种方式中,程序将调用一个带有参数列表的方法,并在完成方法调用后有一组返回值。这些值可能是方法返回值,或者如果地址被作为参数传递,那么这些地址的内容可能已经被改变。地址的内容可能已经被改变。

RPC 就是将这种编程方式引入网络世界的一种尝试。因此,客户端将进行在它看来是正常的过程调用。客户端会将其打包成网络消息并传送给服务器。服务器会将其解包,并在服务器端将其转回为过程调用。服务器端的过程调用,这个调用的结果将被打包,以便返回给客户端。

本地过程和远程过程

过程是什么? 过程就是业务处理计算任务,更直白的说,就是程序

RPC 就是想调用本地方法一样调用远程的过程。与本地过程调用一样,RPC 也是一种同步操作,需要挂起请求程序,直到返回远程过程的结果。但是,使用共享相同地址空间的轻量级进程或线程可以同时执行多个 RPC

RPC 如何工作

RPC 的工作期间,将执行以下步骤:

img

RPC 的工作过程如下:

  1. 服务消费方(client)调用以本地调用方式调用服务。
  2. client stub 接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体;这就是所谓的 marshalling
  3. client stub 找到服务地址,并将消息发送到服务端;这个过程可能是面向连接或无连接的。
  4. server stub 收到消息后进行解码;也叫 unmarshalling
  5. server stub 根据解码结果调用本地的服务。
  6. 本地服务执行并将结果返回给 server stub
  7. server stub 将返回结果打包成消息并发送至消费方。
  8. client stub 接收到消息,并进行解码。
  9. 服务消费方得到最终结果。并且返回值被设置在本地进程的堆栈中。

RPC 的优缺点

以下是 RPC 为开发人员和应用程序管理员提供的一些优势:

  • 帮助客户端通过传统使用高级语言的过程调用与服务器进行通信。
  • 可以在分布式环境以及本地环境中使用。
  • 支持面向进程和面向线程的模型。
  • 对用户隐藏内部消息传递机制。
  • 只需极少的努力即可重写和重新开发代码。
  • 提供抽象,即对用户隐藏网络通信的消息传递性质。
  • 省略许多协议层以提高性能。

另一方面,RPC 的一些缺点包括:

  • 客户端和服务器对各自的例程使用不同的执行环境,并且资源(例如,文件)的使用也更加复杂。因此,RPC 系统并不总是适合传输大量数据。
  • RPC 非常容易发生故障,因为它涉及通信系统,另一台计算机和另一个进程。
  • RPC 没有统一的标准;它可以通过多种方式实现。
  • RPC 只是基于交互的,因此,在硬件架构方面,它不提供任何灵活性。

什么是 URL

统一资源定位符(Uniform Resource Locator):URL 是对可以从互联网上得到资源的位置和访问方法的一种简洁表示,互联网上的资源文件都有一个唯一的 URL。它由协议、服务器名称(IP 地址)、路径、文件名和请求参数组成。

分类

  • 绝对 URL:显示文件的完整路径,这意味着绝对 URL 本身所在的位置与被引用的实际文件的位置无关。
  • 相对 URL:以包含 URL 本身的文件夹的位置为参考点,描述目标文件夹的位置。

URL 类

完成了 URL 的定义,接下来就可以获得 URL 的通信连接。

构造器

构造方法 描述
public URL(String protocol, String host, int port, String file) 通过给定的参数(协议、主机名、端口号、文件名)创建 URL。
public URL(String protocol, String host, String file) 使用指定的协议、主机名、文件名创建 URL,端口使用协议的默认端口。
public URL(String url) 通过给定的 URL 字符串创建 URL。
public URL(URL context, String url) 使用基地址和相对 URL 创建。

常用方法

常用方法 描述
public String getPath() 返回 URL 路径部分。
public String getQuery() 返回 URL 查询部分。
public String getAuthority() 返回此 URL 的授权部分。
public int getPort() 返回 URL 端口号。
public int getDefaultPort() 返回协议的默认端口号。
public String getProtocol() 返回 URL 的协议。
public String getHost() 返回 URL 的主机。
public String getFile() 返回 URL 文件名部分。
public String getRef() 返回此 URL 的锚点(也称为”引用”)。
public URLConnection openConnection() 打开一个 URL 连接,并运行客户端访问资源。

实例演示

// 以下实例演示了使用 java.net 的 URL 类获取 URL 的各个部分参数:
import java.net.*;
import java.io.*;
public class URLDemo {
    public static void main(String[] args) {
        try {
            URL url = new URL("https://www.baidu.com/s?wd=java");
            System.out.println("URL 为:" + url.toString());
            System.out.println("协议为:" + url.getProtocol());
            System.out.println("验证信息:" + url.getAuthority());
            System.out.println("文件名及请求参数:" + url.getFile());
            System.out.println("主机名:" + url.getHost());
            System.out.println("路径:" + url.getPath());
            System.out.println("端口:" + url.getPort());
            System.out.println("默认端口:" + url.getDefaultPort());
            System.out.println("请求参数:" + url.getQuery());
            System.out.println("定位位置:" + url.getRef());
        }catch(IOException e) {
            e.printStackTrace();
        }
    }
}

URLConnection 类

java.net 包中,定义了专门的 URLConnection 类来表示与 URL 建立的通信连接,URLConnection 类的对象使用 URL 类的 openConnection() 方法获得。

获取实例

因为 URLConnection 类是一个抽象类,没有办法通过 new 产生对象,通过调用 URL 类的 openConnection() 方法获取实例。

public abstract class URLConnection {
    // ....
}
URL url = new URL("http://www.baidu.com");
URLConnection urlConnection = url.openConnection();
HttpURLConnection connection = (HttpURLConnection)urlConnection;

URLConnection 是个抽象类,它有两个直接子类分别是 HttpURLConnectionJarURLConnection。另外一个重要的类是 URL,通常URL可以通过传给构造器一个 String 类型的参数来生成一个指向特定地址的 URL 实例。

每个 HttpURLConnection 实例都可用于生成单个请求,但是其他实例可以透明地共享连接到 HTTP 服务器的基础网络。请求后在 HttpURLConnectionInputStreamOutputStream 上调用 close() 方法可以释放与此实例关联的网络资源,但对共享的持久连接没有任何影响。如果在调用 disconnect() 时持久连接空闲,则可能关闭基础套接字。

实例演示

public class URLConnectionDemo{
    public static void main(){
        URL url = new URL("http://www.baidu.com");
        URLConnection urlConnection = url.openConnection();
        HttpURLConnection connection = (HttpURLConnection)urlConnection;
        Reader reader = new InputStreamReader(connection.getInputStream())
        BufferedReader in = new BufferedReader(reader);
        String urlString = "";
        String current;
        while((current = in.readLine()) != null){
            urlString += current + "\n";
        }
        System.out.println(urlString);
    }
}

InetAddress 类

java.net.InetAddress 类是 JavaIP地址(包括 IPv4IPv6)的高层表示。大多数其他网络都要用到这个类,包括 SocketServerSocketURLDatagramSocketDatagramPacket 等。一般情况下,它包括一个主机名和一个IP地址。

获取实例

InetAddress 类没有公共构造函数,通过一系列静态方法获取到当前类的实例。

// 这个方法会建立与本地DNS服务器的一个连接,来查找地址(如果之前查找过这个主机,这个信息可能会在本地缓存,如果这样的话,就不需要再建立网络连接)。如果DNS服务器找不到这个地址,会抛出一个UnknowHostException异常。
InetAddress address = InetAddress.getByName("www.baidu.com");
// 获取代表本机地址的抽象
InetAddress address = InetAddress.getLocalHost();
// 获取代表指定IP地址的抽象
InetAddress ia = InetAddress.getByName("192.168.160.88");

常用方法

常用方法 描述
public String getHostName() 获得该 InetAddress 对象的主机名称。
public String getCanonicalHostName() 获取此 IP 地址的全限定域名。
public bytes[] getHostAddress() 获取该 InetAddress 对象对应的IP地址字符串。
public static InetAddress getLocalHost() 获取本机对应的 InetAddress 对象。
public static InetAddress getByName(String host) 根据主机获得对应的 InetAddress 对象,参数 host 可以是 IP 地址或域名。
public static InetAddress[] getAllByName(String host) 根据主机获得对应的 InetAddress 对象。
public static InetAddress getByAddress(byte[] addr) 获取 addr 所封装的IP地址对应的 InetAddress 对象。
public boolean isReachable(int timeout) 判断是否可以到达该地址。

实例演示

public class InetAddressDemo {
    public static void main(String[] args) {
        try{
            //获取自己本机地址信息
            InetAddress localIp=InetAddress.getLocalHost();
            //1获取此IP地址的全限定域名
            System.out.println("1.localIp.getCanonicalHostName()=" + localIp.getCanonicalHostName());
            //2获取该InetAddress对象对应的IP地址字符串
            System.out.println("2.localIp.getHostAddress()=" + localIp.getHostAddress());
            //3获得该InetAddress对象的主机名称
            System.out.println("3.localIp.getHostName()=" + localIp.getHostName());
            System.out.println("4.localIp.toString()=" + localIp.toString());
            //4.判断是否可以到达该地址
            System.out.println("5.localIp.isReachable(5000)=" + localIp.isReachable(5000));
            System.out.println("--------------------------------------------------------");
            //获取指定域名地址的信息(百度)
            InetAddress baiduip = InetAddress.getByName("www.baidu.com");
            //1.获取此IP地址的全限定域名
            System.out.println("1.baiduIp.getCanonicalHostName()=" + baiduip.getCanonicalHostName());
            //2.获取该InetAddress对象对应的IP地址字符串
            System.out.println("2.baiduIp.getHostAddress()=" + baiduip.getHostAddress());
            //3.获得该InetAddress对象的主机名称
            System.out.println("3.baiduIp.getHostName()=" + baiduip.getHostName());
            System.out.println("4.baiduIp.toString()=" + baiduip.toString());
            //4.判断是否可以到达该地址
            System.out.println("5.baiduIp.isReachable(5000)=" + baiduip.isReachable(5000));
            System.out.println("--------------------------------------------------------");
            //获取指定原始IP地址信息
            InetAddress ip = InetAddress.getByAddress(new byte[]{127,0,0,1});
            // InetAddress ip = InetAddress.getByName("127.0.0.1");
            System.out.println("1.ip.getCanonicalHostName()=" + ip.getCanonicalHostName());
            System.out.println("2.ip.getHostAddress()=" + ip.getHostAddress());
            System.out.println("3.ip.getHostName()=" + ip.getHostName());
            System.out.println("4.ip.toString()=" + ip.toString());
            System.out.println("5.ip.isReachable(5000)=" + ip.isReachable(5000));
        }catch(UnknownHostException e){
            e.printStackTrace();
        }catch(Exception e){
            e.printStackTrace();
        }
    }
}

输出结果

image-20230317111553356

Socket 编程

Java 最初是作为网络编程语言出现的,其对网络提供了高度的支持,使得客户端和服务器的沟通变成了现实,而在网络编程中,使用最多的就是 Socket,它又称为套接字,Socket 是计算机网络通信的基本的技术之一。如今大多数基于网络的软件,如浏览器,即时通讯工具甚至是 P2P 下载都是基于 Socket 实现的。

Socket 就在应用程序的传输层和应用层之间,设计了一个 Socket 抽象层,传输层的底一层的服务提供给 Socket 抽象层,Socket 抽象层再提供给应用层。

Socket 编程分类

关于 socket 编程,由上图可以看出,有两种通信协议可以进行选择。一种是数据报通信,另一种就是流通信。

  1. TCP 方式
  2. UDP 方式

UDP 方式

数据报通信协议,就是我们常说的 UDPUser Data Protocol 用户数据报协议)。UDP 是一种无连接的协议,这就意味着我们每次发送数据报时,需要同时发送本机的 Socket 描述符和接收端的 Socket 描述符。因此,我们在每次通信时都需要发送额外的数据。

代码实现

public class UDPDemo {
    public static void main(String[] args) {
        new UDPServer().start();
        new UDPClient().start();
    }
}
class UDPServer extends Thread {
    public void run() {
        try {
            // 1.创建服务器端 DatagramSocket,指定端口
            DatagramSocket socket = new DatagramSocket(8888);
            // 2.创建数据报,用于接收客户端发送的数据
            byte[] data = new byte[1024];//创建字节数组,指定接收的数据包的大小
            DatagramPacket packet = new DatagramPacket(data, data.length);
            // 3.接收客户端发送的数据
            System.out.println("****服务器端已经启动,等待客户端发送数据");
            socket.receive(packet);// 此方法在接收到数据报之前会一直阻塞
            // 4.读取数据
            String info = new String(data, 0, packet.getLength());//创建字符串对象
            System.out.println("我是服务器,客户端说:" + info);//输出提示信息
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }
}
class UDPClient extends Thread {
    public void run() {
        try {
            // 1.定义服务器的地址、端口号、数据
            InetAddress address = InetAddress.getByName("localhost");
            byte[] data = "Hello,I`m GuanWei".getBytes();
            //2.创建数据报,包含发送的数据信息
            DatagramPacket packet = new DatagramPacket(data, data.length, address, 8888);
            DatagramSocket socket = new DatagramSocket(); // 3.创建 DatagramSocket 对象
            socket.send(packet);// 4.向服务器端发送数据报
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }
}

运行流程

UDP 的通信建立的步骤,UDP 客户端首先向被动等待联系的服务器发送一个数据报文。

一个典型的 UDP 客户端要经过下面三步操作:

  1. 创建一个 DatagramSocket 实例,可以有选择地对本地地址和端口号进行设置,如果设置了端口号,则客户端会在该端口号上监听从服务器端发送来的数据。
  2. 使用 DatagramSocket 实例的 send()receive() 方法来发送和接收 DatagramPacket 实例,进行通信。
  3. 通信完成后,调用 DatagramSocket 实例的 close() 方法来关闭该套接字。 由于 UDP 是无连接的,因此 UDP 服务端不需要等待客户端的请求以建立连接。另外,UDP 服务器为所有通信使用同一套接字,这点与 TCP 服务器不同,TCP 服务器则为每个成功返回的 accept() 方法创建一个新的套接字。

一个典型的 UDP 服务端要经过下面三步操作:

  1. 创建一个 DatagramSocket 实例,指定本地端口号,并可以有选择地指定本地地址。此时,服务器已经准备好从任何客户端接收数据报文。
  2. 使用 DatagramSocket 实例的 receive() 方法接收一个 DatagramPacket 实例,当 receive() 方法返回时,数据报文就包含了客户端的地址,这样就知道了回复信息应该发送到什么地方。
  3. 使用 DatagramSocket 实例的 send() 方法向服务器端返回 DatagramPacket 实例。

TCP 方式

流通信协议,也叫做 TCP(Transfer Control Protocol,传输控制协议)。和 UDP 不同,TCP 是一种基于连接的协议。在使用流通信之前,我们必须在通信的一对 Socket 之间建立连接。其中一个 Socket 作为服务器进行监听连接请求。另一个则作为客户端进行连接请求。一旦两个 Socket 建立好了连接,他们可以单向或双向进行数据传输。

代码实现

public class TCPDemo {
    public static void main(String[] args) {
        new TCPServer().start();
        new TCPClient().start();
    }
}

class TCPServer extends Thread {
    public void run() {
        try {
            ServerSocket ss = new ServerSocket(8888);
            Socket s = ss.accept();
            System.out.println("服务器:准备发送内容到客户端");
            OutputStream out = s.getOutputStream();
            out.write("你好,我是GuanWei".getBytes());
            out.close();
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }
}

class TCPClient extends Thread {
    public void run() {
        try {
            InetAddress address = InetAddress.getLocalHost();
            Socket s = new Socket(address, 8888);
            System.out.println("客户端开始接受内容");
            InputStream in = s.getInputStream();
            byte[] bytes = new byte[1000];
            int len = in.read(bytes);
            String str = new String(bytes, 0, len);
            System.out.println(str);
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }
}

运行流程

Socket 套接字使用 TCP 提供了两台计算机之间的通信机制。服务器会创建一个服务端套接字,客户端程序创建一个套接字,并尝试连接服务器的套接字。

当连接建立时,服务器会创建一个 Socket 对象。客户端和服务器现在可以通过对 Socket 对象的写入和读取来进行通信。

java.net.Socket 类代表一个套接字,并且 java.net.ServerSocket 类为服务器程序提供了一种来监听客户端,并与他们建立连接的机制。

以下步骤在两台计算机之间使用套接字建立 TCP 连接时会出现:

  1. 服务器实例化一个 ServerSocket 对象,表示通过服务器上的端口通信。
  2. 服务器调用 ServerSocket 类的 accept() 方法,该方法将一直等待,直到客户端连接到服务器上给定的端口。
  3. 服务器正在等待时,一个客户端实例化一个 Socket 对象,指定服务器名称和端口号来请求连接。
  4. Socket 类的构造函数试图将客户端连接到指定的服务器和端口号。如果通信被建立,则在客户端创建一个 Socket 对象能够与服务器进行通信。
  5. 在服务器端,accept() 方法返回服务器上一个新的 Socket 引用,该 Socket 连接到客户端的 Socket

BIO、NIO 和 AIO

同步阻塞 IO

BIOJava BIO (blocking I/O), 同步阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。

image-20230317172331023

如图中,一个服务端可以连接多个客户端,但是接入一个客户端,服务端都需要用一个线程维持着客户端与服务端的连接。如果客户端数量很多,那么服务端也需要对应的线程数量与之连接。

一对一代码实现

服务器
public class Server {
    public static void main(String[] args) throws IOException {
        System.out.println("服务器启动.....");
        // 1.定义一个ServerSocket,并定义端口是 8888
        ServerSocket ss = new ServerSocket(8888);
        // 2.监听客户端的socket请求,当客户端请求进入时,创建一个服务端套接字
        Socket socket = ss.accept();
        // 3.从客户端传来的socket里面获取到字节输入流
        InputStream in = socket.getInputStream();
        // 4.获取到输入流中的内容并输出到控制台
        byte[] bytes = new byte[1000];
        int len = in.read(bytes);
        String str = new String(bytes, 0, len);
        System.out.println(str);
        // 5.关闭字节输入流
        in.close();
    }
}
客户端
public class Client {
    public static void main(String[] args) throws IOException {
        // 1.创建socket将对象请求服务端的链接
        Socket socket = new Socket("127.0.0.1",8888);
        // 2.从socket对象中获取一个字节输出流
        OutputStream out = socket.getOutputStream();
        // 3.输出内容到字节输出流
        out.write("你好,我是GuanWei".getBytes());
        // 4.关闭字节输出流
        out.close();
    }
}

一对多代码实现

服务器
public class Server {
    public static void main(String[] args) throws IOException {
        // 1.定义一个ServerSocket,并定义端口是 8888
        ServerSocket ss = new ServerSocket(8888);
        while(true){
            // 2.监听客户端的socket请求
            Socket socket = ss.accept();
            new SocketThread(socket).start();
        }
    }
}
Socket 线程类
public class SocketThread extends Thread{
    private Socket socket;
    public SocketThread(Socket socket) {
        this.socket = socket;
    }
    public void run() {
        try {
            // 1.从客户端传来的socket里面获取到流
            InputStream in = socket.getInputStream();
            // 2.获取到输入流中的内容并输出到控制台
            byte[] bytes = new byte[1000];
            int len = in.read(bytes);
            String str = new String(bytes, 0, len);
            System.out.println(str);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
客户端
public class Client {
    public static void main(String[] args) throws IOException {
        // 1.创建socket将对象请求服务端的链接
        Socket socket = new Socket("127.0.0.1",8888);
        // 2.从socket对象中获取一个字节输出流
        OutputStream out = socket.getOutputStream();
        // 3.输出内容到字节输出流
        out.write("你好,我是GuanWei".getBytes());
        // 4.关闭字节输出流
        out.close();
    }
}

注意:客户端代码和一对一没有区别,只是在运行时需要启动多个客户端来访问服务器。然后测试一下,分别从两个客户端发送信息服务端都会接收到,这时候启用的是两个线程,每个 socket 线程对象都对应一个服务端的链接。

同步非阻塞 IO

Java NIO (non-blocking I/O),同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上, 多路复用器轮询到连接有 I/O 请求时才启动一个线程进行处理。NIO 的本质就是避免原始的 TCP 建立连接使用3次握手的操作,减 少连接的开销。是解决高并发I/O 高性能的有效方式。

image-20230320091806123

一个线程中就可以调用多路复用接口(java 中是 select)阻塞同时监听来自多个客户端的 IO 请求,一旦有收到 IO 请求就调用对应函数处理,NIO 擅长1个线程管理多条连接,节约系统资源。

NIO 包含3个核心的组件:

  • Channel (通道)
  • Buffer (缓冲区)
  • Selector (选择器)

它们之间的关系如下:

关系图的说明:

  • 每个 Channel 对应一个 Buffer
  • Selector 对应一个线程,一个线程对应多个 Channel
  • 该图反应了有三个 Channel 注册到该 Selector
  • 程序切换到那个 Channel 是由事件决定的(Event)。
  • Selector 会根据不同的事件,在各个通道上切换。
  • Buffer 就是一个内存块,底层是有一个数组。
  • 数据的读取和写入是通过 Buffer,但是需要 flip() 切换读写模式,而 BIO 是单向的,要么是输入流要么是输出流。

Channel

Channel(通道) 是 NIO 的核心概念,它表示一个打开的连接,这个连接可以连接到 I/O 设备(例如:磁盘文件,Socket等)或者一个支持 I/O 访问的应用程序,Java NIO 使用缓冲区和通道来进行数据传输。

通道就像自来水管道一样,网络数据通过 Channel 读取和写入,通道与流不同之处在于通道是双向的,而流只是一个方向上移动,而通道可以用于读,写或者二者同时进行,最关键的是可以与多路复用器结合起来,有多种的状态位,方便多路复用器去识别。

通道的主要实现类:

  • FileChannel 类
  • SocketChannel 类
  • ServerSocketChannel 类
FileChannel 类

本地文件 IO 通道,用于读取、写入、映射和操作文件的通道,使用文件通道操作文件的一般流程为:

  1. 获取通道
  2. 创建字节缓冲区
  3. 读写操作
  4. 数据刷盘(写入时需要)
  5. 关闭通道
// FileChannel 读取操作
public class FileChannelReadDemo {
    public static void main(String[] args) throws IOException {
        // 文件通道通过 FileChannel 的静态方法 open() 来获取,获取时需要指定文件路径和文件打开方式。
        String filePath = "D://dailyblue//guanwei.txt";
        FileChannel channel = FileChannel.open(Paths.get(filePath), StandardOpenOption.READ);
        // 分配字节缓存
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        // 读取通道中的数据,并写入到 buffer 中,如果返回-1,表示到了文件末尾。
        int len = channel.read(buffer);
        // 缓存区切换到读 模式简言之,反转缓冲区写后读;用于buffer缓存对象内容填写后,转为读模式。(写进buffer后再flip(),可以读出buffer中的数据)
        buffer.flip();
        String s = new String(buffer.array(), 0, len);
        System.out.print(s);
        // 关闭通道
        channel.close();
    }
}
// FileChannel 写入操作
public class FileChannelWriteDemo {
    public static void main(String[] args) throws IOException {
        // 文件通道通过 FileChannel 的静态方法 open() 来获取,获取时需要指定文件路径和文件打开方式。
        String filePath = "D://dailyblue//guanwei.txt";
        FileChannel channel = FileChannel.open(Paths.get(filePath), StandardOpenOption.WRITE);
        // 分配字节缓存
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        String str = "GuanWei 带你学习 FileChannel循环写入的操作";
        for (int i = 0; i < str.length(); i++) {
            // 模拟每次写入一个字符
            String s = str.substring(i, i + 1);
            // 将内容存放到buffer缓冲区
            buffer.put(s.getBytes());
            // 缓存区已满或者已经遍历到最后一个字符
            if (buffer.position() == buffer.limit() || i == str.length() - 1) {
                // 将缓冲区由写模式置为读模式
                buffer.flip();
                // 写入
                channel.write(buffer);
                // 清空缓存区,将缓冲区置为写模式,下次才能使用
                buffer.clear();
            }
        }
        // 将数据刷出到物理磁盘
        channel.force(false);
        // 关闭通道
        channel.close();
    }
}
SocketChannel 类

网络套接字 IO 通道,TCP 协议,针对面向流的连接套接字的可选择通道(一般用在客户端)。SocketChannel 就是 NIO 对于非阻塞 socket 操作的支持的组件,其在 socket 上封装了一层,主要是支持了非阻塞的读写。同时改进了传统的单向流 APIChannel 同时支持读写。

TCP 客户端使用 SocketChannel 与服务端进行交互的流程为:

  1. 打开通道,连接到服务端

  2. 分配缓冲区

  3. 配置是否为阻塞方式。(默认为阻塞方式)

  4. 与服务端进行数据交互

  5. 关闭连接

public class SocketChannelClient {
    public static void main(String[] args) throws IOException {
        // 打开通道,此时还没有打开 TCP 连接
        SocketChannel channel = SocketChannel.open();
        // 连接到服务端
        InetSocketAddress address = new InetSocketAddress("localhost", 9999);
        channel.connect(address);
        // 分配一个 1024 字节的缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        // 与服务端进行数据交互,客户端模拟读取操作
        int len = channel.read(buffer);
        String str = new String(buffer.array(), 0, len);
        System.out.println("客户端发送过来的数据是:" + str);
        // 关闭通道
        channel.close();
    }
}
ServerSocketChannel 类

网络通信 IO 操作,TCP 协议,针对面向流的监听套接字的可选择通道(一般用于服务端),流程如下:

  1. 打开一个 ServerSocketChannel 通道

  2. 绑定端口

  3. 分配缓冲区

  4. 阻塞等待连接到来,有新连接时会创建一个 SocketChannel 通道,服务端可以通过这个通道与连接过来的客户端进行通信

  5. 通过 SocketChannel 与客户端进行数据交互

  6. 关闭 SocketChannel

public class SocketChannelServer {
    public static void main(String[] args) throws IOException {
        // 打开通道
        ServerSocketChannel channel = ServerSocketChannel.open();
        // 绑定端口
        InetSocketAddress address = new InetSocketAddress(9999);
        channel.bind(address);
        // 分配缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        // 阻塞,直到有连接过来
        SocketChannel client = channel.accept();
        // 通过 SocketChannel 与客户端进行数据交互 这里模拟写入操作
        buffer.put("hello,client".getBytes());
        buffer.flip();
        client.write(buffer);
        buffer.clear();
        // 关闭 SocketChannel
        channel.close();
    }
}
非阻塞模式

可以设置 SocketChannel 为非阻塞模式(non-blocking mode)。设置之后,就可以在异步模式下调用 connect()read()write()了。

Buffer

缓冲区 BufferJava NIO 中一个核心概念,在 NIO 库中,所有数据都是用缓冲区处理的。

在读取数据时,它是直接读到缓冲区中的,在写入数据时,它也是写入到缓冲区中的,任何时候访问 NIO 中的数据,都是将它放到缓冲区中。而在面向流 I/O 系统中,所有数据都是直接写入或者直接将数据读取到 Stream 对象中。

缓冲区类型

Buffer 类的 Java 文档中可以发现,Buffer 类是一个抽象类,它具有7个直接子类,分别是 ByteBufferCharBufferDoubleBufferFloatBufferIntBufferLongBufferShortBuffer,也就是缓冲区中存储的数据类型并不像普通 I/O 流只能存储 bytechar 数据类型,Buffer 类能存储的数据类型是多样的。

从类图中可以看到,7 种数据类型对应着 7 种子类,这些名字是 Heap 开头子类,数据是存放在 JVM 堆中的。

MappedByteBuffer 是存放在堆外的直接内存中,可以映射到文件。

通过 java.nio 包和 MappedByteBuffer 允许 Java 程序直接从内存中读取文件内容,通过将整个或部分文件映射到内存,由操作系统来处理加载请求和写入文件,应用只需要和内存打交道,这使得 IO 操作非常快。

Mmap 内存映射和普通标准 IO 操作的本质区别在于它并不需要将文件中的数据先拷贝至 OS 的内核 IO 缓冲区,而是可以直接将用户进程私有地址空间中的一块区域与文件对象建立映射关系,这样程序就好像可以直接从内存中完成对文件读/写操作一样。

只有当缺页中断发生时,直接将文件从磁盘拷贝至用户态的进程空间内,只进行了一次数据拷贝,对于容量较大的文件来说(文件大小一般需要限制在1.5~2G以下),采用 Mmap 的方式其读/写的效率和性能都非常高,大家熟知的 RocketMQ 就使用了该技术。

Buffer 数据流程

应用程序可以通过与 I/O 设备建立通道来实现对 I/O 设备的读写操作,操作的数据通过缓冲区 Buffer 来进行交互。

从 I/O 设备读取数据时:

  1. 应用程序调用通道 Channelread() 方法

  2. 通道往缓冲区 Buffer 中填入 I/O 设备中的数据,填充完成之后返回

  3. 应用程序从缓冲区 Buffer 中获取数据

往 I/O 设备写数据时:

  1. 应用程序往缓冲区 Buffer 中填入要写到 I/O 设备中的数据
  2. 调用通道 Channelwrite() 方法,通道将数据传输至 I/O 设备
Buffer的重要属性
  • capacity:容量缓冲区的容量,是它所包含的元素的数量。不能为负并且不能更改。

  • position:缓冲区的位置是下一个要读取或写入的元素的索引。不能为负,并且不能大于 limit

  • limit:缓冲区的限制,缓冲区的限制不能为负,并且不能大于 capacity

  • 标记 (mark)与重置 (reset):标记是一个索引,通过 Buffer 中的 mark() 方法指定 Buffer 中一个特定的 position,之后可以通过调用 reset() 方法恢复到这个 position

缓冲区核心方法
方法 描述
put(byte b) 将给定单个字节写入缓冲区的当前位置
put(byte[] bytes) 将 bytes 数组中的字节写入缓冲区的当前位置
put(int index, byte b) 将指定字节写入缓冲区的索引位置(不会移动 position)
get() 读取单个字节
get(byte[] bytes) 批量读取多个字节到 bytes 数组中
get(int index) 读取指定索引位置的字节(不会移动 position)
flip() Buffer 有两种模式,写模式和读模式。在写模式下调用 flip() 之后,Buffer 从写模式变成读模式
clear() 清除缓冲区内容

Selector(选择器)

Selector 类是 NIO 的核心类,Selector(选择器)选择器提供了选择已经就绪的任务的能力。

Selector 会不断的轮询注册在上面的所有 channel,如果某个 channel 为读写等事件做好准备,那么就处于就绪状态,通过 Selector 可以不断轮询发现出就绪的 channel,进行后续的 IO 操作。

一个 Selector 能够同时轮询多个 channel,这样,一个单独的线程就可以管理多个 channel,从而管理多个网络连接,这样就不用为每一个连接都创建一个线程,同时也避免了多线程之间上下文切换导致的开销。

选择器使用步骤
  1. 获取选择器
  2. 获取可选择通道
  3. 将通道注册到选择器
  4. 轮询 select 就绪事件
监听事件类型

当调用 register(Selector sel, int ops) 将通道注册选择器时,选择器对通道的监听事件,需要通过第二个参数 ops 指定。可以监听的事件类型(用 可使用 SelectionKey 的四个常量表示):

  1. 读:SelectionKey.OP_READ,代表读操作, 值为 1。
  2. 写:SelectionKey.OP_WRITE ,代表写操作, 值为 4。
  3. 连接:SelectionKey.OP_CONNECT,代表连接已经建立, 值为 8。
  4. 接收:SelectionKey.OP_ACCEPT,有新的网络连接可以 accept, 值为 16
代码实现
// 服务器代码实现
public class SelectorServerDemo {
    public static void main(String[] args) {
        try {
            //1.获取管道
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            //2.设置非阻塞模式
            serverSocketChannel.configureBlocking(false);
            //3.绑定端口
            serverSocketChannel.bind(new InetSocketAddress(9999));
            //4.获取选择器
            Selector selector = Selector.open();
            //5.将通道注册到选择器上,并且开始指定监听的接收事件
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            //6.轮询已经就绪的事件
            while (selector.select() > 0) {
                System.out.println("开始进行事件处理");
                //7.获取选择器中所有注册的通道中已准备好的事件
                Iterator
<SelectionKey> it = selector.selectedKeys().iterator();
                //8.开始遍历事件
                while (it.hasNext()) {
                    SelectionKey selectionKey = it.next();
                    //9.判断这个事件具体是啥
                    if (selectionKey.isAcceptable()) {
                        //10.获取当前接入事件的客户端通道
                        SocketChannel socketChannel = serverSocketChannel.accept();
                        //11.切换成非阻塞模式
                        socketChannel.configureBlocking(false);
                        //12.将本客户端注册到选择器
                        socketChannel.register(selector, SelectionKey.OP_READ);
                    } else if (selectionKey.isReadable()) {
                        //13.获取当前选择器上的读
                        SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
                        //14.读取
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        int len;
                        while ((len = socketChannel.read(buffer)) > 0) {
                            buffer.flip();
                            System.out.println(new String(buffer.array(), 0, len));
                            //清除之前的数据(覆盖写入)
                            buffer.clear();
                        }
                    }
                    //15.处理完毕后,移除当前事件
                    it.remove();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
// 客户端代码实现
public class SelectorClientDemo {
    public static void main(String[] args) {
        try {
            SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9999));
            socketChannel.configureBlocking(false);
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            Scanner scanner = new Scanner(System.in);
            while (true) {
                System.out.print("请输入:");
                String msg = scanner.nextLine();
                if("exit".equals(msg)){
                    break;
                }
                buffer.put(msg.getBytes());
                buffer.flip();
                socketChannel.write(buffer);
                buffer.clear();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

异步非阻塞 IO

Java AIO(NIO 2.0) :异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的 I/O 请求都是由 OS 先完成了再通知服务器应用去启动线程进行处理。

NIO 不同,当进行读写操作时,只须直接调用 APIreadwrite 方法即可,这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入read 方法的缓冲区,对于写操作而言,当操作系统将 write 方法传递的流写入完毕时,操作系统主动通知应用程序。

即可以理解为,read/write 方法都是异步的,完成后会主动调用回调函数。在 JDK1.7 中,这部分内容被称作 NIO 2.0 ,主要在Java.nio.channels 包下增加了下面四个异步通道:

  • AsynchronousSocketChannel
  • AsynchronousServerSocketChannel
  • AsynchronousFileChannel
  • AsynchronousDatagramChannel

特点分析

三种 IO 的区别

  • Java BIO:同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
  • Java NIO:同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有 I/O 请求时才启动一个线程进行处理。
  • Java AIO(NIO 2.0):异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的 I/O 请求都是由 OS 先完成了再通知服务器应用去启动线程进行处理。

BIO、NIO、AIO 适用场景分析:

  • BIO 方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4 以前的唯一选择,但程序直观简单易理解。
  • NIO 方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4 开始支持。
  • AIO 方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用 OS 参与并发操作,编程比较复杂,JDK7开始支持。

总结:

本文从网络基础模型出发,系统地介绍了 Java 网络编程的核心知识:

  • 网络模型:OSI 七层模型和 TCP/IP 四层模型是网络通信的理论基础。
  • 核心协议:TCP 提供可靠的面向连接服务,UDP 提供高效的无连接服务,HTTP/HTTPS 是应用层最常用的协议。
  • Java APIURL/URLConnection 用于高层网络访问,InetAddress 用于地址解析,Socket/ServerSocket 用于底层 TCP 通信,DatagramSocket 用于 UDP 通信。
  • IO 模型演进:BIO → NIO → AIO,是从”一连接一线程”到”多路复用”再到”操作系统回调”的不断优化过程。NIO 的三大核心组件 Channel、Buffer、Selector 是高并发网络编程的基础。

By admin

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注